Java之多线程讲解

一、线程解析

(1)线程的创建

  1、继承Thread类创建线程类,代码如下:

//1、定义一个继承Thread类的子类,并重写该类的run()方法;
//2、创建Thread子类的实例,即创建了线程对象;
//3、调用该线程对象的start()方法启动线程。

class SomeThead extends Thraad   { 
    public void run()   { 
     System.out.println("必须要重写Thread方法");
    }  
 } 
public static void main(String[] args){
 SomeThread oneThread = new SomeThread();   
  步骤3:启动线程:   
 oneThread.start(); 
}

  2、实现Runnable接口创建线程类

//1、定义Runnable接口的实现类,并重写该接口的run()方法;
//2、创建Runnable实现类的实例,并以此实例作为Thread的target对象,即该Thread对象才是真正的线程对象。

class SomeRunnable implements Runnable   { 
  public void run()   { 
  System.out.println("必须要重写run方法");
  }  
} 
Runnable oneRunnable = new SomeRunnable();   
Thread oneThread = new Thread(oneRunnable);   
oneThread.start();

(2)线程的生命周期

  

  1、新建状态:用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新建状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态

  2、就绪状态:处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于可运行池之中。

  3、运行状态:处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

   1)运行状态变就绪状态:如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。

   2)运行状态变阻塞状态:线程调用sleep方法主动放弃所占用的系统资源;线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁;线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有

   3)运行状态变死亡状态:当线程的run()方法执行完,或者被强制性地终止,例如出现异常,调用了stop()方法等等,就会从运行状态转变为死亡状态。

  4、阻塞状态:处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。 在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待。

  5、死亡状态:当线程的run()方法执行完,或者被强制性地终止,例如出现异常,调用了stop()方法等等,就会从运行状态转变为死亡状态。

(3)线程管理

  1、线程睡眠--sleep()方法:

   1)使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。

   2)而sleep(),join()方法是在线程类中实现的,sleep()的作用是将当前线程暂停一定的时间,但在这期间并不释放锁。

   3)sleep是静态方法最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程而不是调用它的线程对象,它只对正在运行状态的线程对象有效。如下:

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        System.out.println(Thread.currentThread().getName());  
        MyThread myThread=new MyThread();  
        myThread.start();  
        myThread.sleep(1000);//这里sleep的就是main线程,而非myThread线程  
        Thread.sleep(10);  
        for(int i=0;i<100;i++){  
            System.out.println("main"+i);  
        }  
    }  
}

  2、线程睡眠--yield()方法

  yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。

  3、sleep()和yield()方法的区别:

   1)sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。

   2)sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。

   3)sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。

  4、线程合并--join方法

   线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,注意,它不是静态方法。

  5、Object中与线程相关的方法

   锁池:假设某对象锁当前被线程A所持有,而其他想持有该锁的线程就会先放进锁池中,待线程A释放所持有的锁时,锁池中线程可以竞争持有该锁。也就是说,锁池中的线程对象具备竞争锁的资格。

   等待池:当锁对象调用wait方法时,则持有该对象锁的线程进入等待池中,进入等待池中的线程对象不具备持有锁的资格。

   线程通信借助于wait(),notify(),notifyAll()等object类的方法,既然wait()等方法是和线程相关的,为何不在线程类中实现呢?wait(),notify(),notifyAll()方法其实属于锁对象,而所有对象都可以当做锁来使用,wait()方法的意思是持有该锁对象的线程对象进入wait状态:释放锁,然后将当前线程放入等待池中。

   而sleep(),join()方法是在线程类中实现的,其调用者是线程对象。sleep()的作用是将当前线程暂停一定的时间,但在这期间并不释放锁。

   notify和notifyAll的区别notify()用于随机唤醒一个等待该锁对象的线程,notifyAll()用于唤醒所有等待该锁对象的线程。(被唤醒表示该线程具备了竞争锁的资格)也即是:notify()会在等待池中随机选择一个线程对象放入锁池中。而notifyAll会将所有等待该锁的线程放入锁池中。

  6、如何中止一个正在运行的线程:

   1)利用stop(),resume()、suspend()、destory()方法来结束线程.但是调用这些方法会立刻停止run()方法中剩余的全部工作.会导致某些工作还未完成就已经结束如文件数据库的关闭.另外,调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。

   2)使用退出标志位来终止线程,当run方法执行完后,线程就会推出,但是有时run方法是不会永远结束的如循环处理请求,此时需要设置退出标志位来跳出循环来.完成run()的执行.

   3)使用interrupt()来中断线程.调用 interrupt() 方法仅仅是在当前线程中打一个停止的标记,并不是真的停止线程。想要是实现调用interrupt()方法真正的终止线程,则可以在线程的run方法中做处理即可,比如直接跳出run()方法使线程结束,视具体情况而定。

  7、设置线程的优先级:每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~·0之间

public class Test1 {  
        public static void main(String[] args) throws InterruptedException {  
            new MyThread("高级", 10).start();  
            new MyThread("低级", 1).start();  
        }  
    }  
      
    class MyThread extends Thread {  
        public MyThread(String name,int pro) {  
            super(name);//设置线程的名称  
            setPriority(pro);//设置线程的优先级  
        }  
        @Override  
        public void run() {  
            for (int i = 0; i < 100; i++) {  
                System.out.println(this.getName() + "线程第" + i + "次执行!");  
            }  
        }  
    }

  8、守护/后台线程

   守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。setDaemon(true)设置线程为守护线程;JRE判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状态

(4)线程同步

  可以利用内置锁(synchronized)和显示锁(java.util.concurrent.locks.ReentrantLock)

  1、synchronized关键字

   1)实现原理:

    a、synchronized是用java的monitor机制来实现的,就是synchronized关键字在经过javac编译后,会在同步块的前后分别形成monitorEnter和monitorExit这两个字节码指令。

    b、在执行monitorEnter指令时,首先要去尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值加1,而在执行monitorExit指令时会将锁计数器的值减1,一旦计数器的值为0,锁随机就会被释放。

    c、如果持有对象锁失败,那么当前线程就应当被阻塞等待,直到此线程获取到锁为止。

  注意:monitor机制是跟java对象结构相关的。HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(存储对象的hashCode、锁信息或分代年龄或GC标志等信息),实例数据跟对齐填充。

   2)synchronized修饰实例方法:由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

   3)修饰代码块:有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。

  synchronized充当哪几种锁类型:内置锁,同步锁,悲观锁。

  被synchronized修饰的代码段可以防止被多个线程同时执行,必须一个线程把synchronized修饰的代码段都执行完毕了,其他的线程才能开始执行这段代码。因为synchronized保证了在同一时刻,只能有一个线程执行同步代码块,所以执行同步代码块的时候相当于是单线程操作了,那么线程的可见性、原子性、有序性(线程之间的执行顺序)它都能保证了。

  2、使用重入锁实现线程同步

   1)java.util.concurrent.locks.ReentrantLock来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。ReenreantLock类的常用方法有:    

 ReentrantLock() : 创建一个ReentrantLock实例         
 lock() : 获得锁        
 unlock() : 释放锁

   2)synchronized与和reentrantlock的区别:

    a、ReentrantLock在等待锁时可以使用lockInterruptibly()方法选择中断, 改为处理其他事情,而synchronized关键字,线程需要一直等待下去。同样的,tryLock()方法可以设置超时时间,用于在超时时间内一直获取不到锁时进行中断。

    b、ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁,而synchronized的锁是非公平的。

reentrantlock缺点:ReentrantLock的主要缺点是方法需要置于try-finally块中,开发人员需要负责获取和释放锁,而开发人员常常忘记在finally中释放锁。

  3、synchronized与CAS相比较又叫做悲观锁,而CAS叫乐观锁

   1)CSA实现原理:

    a、定义3个操作数,变量的内存地址address,旧的预期值pre_value和准备的设置的新值new_value。

    b、CAS指令执行时,当且仅当adderss符合pre_value时,处理器才会用new_value更新address的值,如果adderss不符合pre_value时,则说明已经有其他的线程做了这两个操作,当前线程则什么都不会去做。

    c、但是不管是否更新了address值,都会返回address的旧值即pre_value。

  生动形象的例子:比如说给你儿子订婚。你儿子就是内存位置,你原本以为你儿子是和杨贵妃在一起了,结果在订婚的时候发现儿子身边是西施。这时候该怎么办呢?你一气之下不做任何操作。如果儿子身边是你预想的杨贵妃,你一看很开心就给他们订婚了,也叫作执行操作。

  4、volatile与synchronized关键字的区别:

   1)volatile只能作用于变量,使用范围较小。synchronized可以用在变量、方法、类、同步代码块等,使用范围比较广。

   2)volatile只能保证可见性和有序性,不能保证原子性。而可见性、有序性、原子性synchronized都能保证.

   3)volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。

二、线程池

  Java中已经提供了创建线程池的一个类:Executor,一般使用它的子类:ThreadPoolExecutor.

public ThreadPoolExecutor(int corePoolSize,  
                              int maximumPoolSize,  
                              long keepAliveTime,  
                              TimeUnit unit,  
                              BlockingQueue<Runnable> workQueue,  
                              ThreadFactory threadFactory,  
                              RejectedExecutionHandler handler)

(1)参数解析:

  1)corePoolSize :核心池的大小,如果调用了prestartAllCoreThreads()或者prestartCoreThread()方法,会直接预先创建corePoolSize的线程,否则当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;这样做的好处是,如果任务量很小,那么甚至就不需要缓存任务,corePoolSize的线程就可以应对;

  2)maximumPoolSize:线程池最大线程数,表示在线程池中最多能创建多少个线程,如果运行中的线程超过了这个数字,那么相当于线程池已满,新来的任务会使用RejectedExecutionHandler 进行处理;

  3)keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止,然后线程池的数目维持在corePoolSize 大小;

  4)unit:参数keepAliveTime的时间单位;

  5)workQueue:一个阻塞队列,用来存储等待执行的任务,如果当前对线程的需求超过了corePoolSize大小,才会放在这里;

  6)threadFactory:线程工厂,主要用来创建线程,比如可以指定线程的名字;

  7)handler:如果线程池已满即线程数量大于maximumPoolSize,新的任务的处理方式

(2)线程池执行流程图

由图可知:

  1)线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。

  2)当调用 execute() 方法添加一个任务时,线程池会做如下判断:

   1、如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;

   2、如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。

   3、如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程运行这个任务;

   4、如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。

  3)当一个线程完成任务时,它会从队列中取下一个任务来执行。

  4)当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

  这样的过程说明,并不是先加入任务就一定会先执行。假设队列大小为 10,corePoolSize 为 3,maximumPoolSize 为 6,那么当加入 20 个任务时,执行的顺序就是这样的:首先执行任务 1、2、3,然后任务 4~13 被放入队列。这时候队列满了,任务 14、15、16 会被马上执行,而任务 17~20 则会抛出异常。最终顺序是:1、2、3、14、15、16、4、5、6、7、8、9、10、11、12、13。

public class ThreadPool {
    private static ExecutorService pool;
    public static void main( String[] args )
    {
        //自定义拒绝策略
        pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5),
                Executors.defaultThreadFactory(), new RejectedExecutionHandler() {
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.out.println(r.toString()+"执行了拒绝策略");
                
            }
        });
          
        for(int i=0;i<10;i++) {
            pool.execute(new ThreadTask());
        }    
    }
}

public class ThreadTask implements Runnable{    
    public void run() {
        try {
            //让线程阻塞,使后续任务进入缓存队列
            Thread.sleep(1000);
            System.out.println("ThreadName:"+Thread.currentThread().getName());
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    
    }
}
//结果为:
com.hhxx.test.ThreadTask@33909752执行了拒绝策略
com.hhxx.test.ThreadTask@55f96302执行了拒绝策略
com.hhxx.test.ThreadTask@3d4eac69执行了拒绝策略
ThreadName:pool-1-thread-2
ThreadName:pool-1-thread-1
ThreadName:pool-1-thread-1
ThreadName:pool-1-thread-2
ThreadName:pool-1-thread-1
ThreadName:pool-1-thread-2
ThreadName:pool-1-thread-1

(3)常见的线程池

  1)CachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。

  2)SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。

  3)FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程

  4)SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。

(4)线程池中常见的阻塞队列

  1)ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。

  2)LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。

  3)PriorityBlockingQueue是一个没有边界的队列,它的排序规则和 java.util.PriorityQueue一样。需要注意,PriorityBlockingQueue中允许插入null对象。所有插入PriorityBlockingQueue的对象必须实现 java.lang.Comparable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的。

线程池参考链接:https://zhuanlan.zhihu.com/p/70073802

        https://blog.csdn.net/weixin_40271838/article/details/79998327

猜你喜欢

转载自www.cnblogs.com/hdc520/p/12593505.html