JAVA多线程浅谈

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_37465188/article/details/85341587

多线程简介以及一些问题

线程的几种状态

1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权。即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
阻塞的情况分三种:
(1)、等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,
(2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
(3)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程安全指的是什么

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

死锁

当然死锁的产生是必须要满足一些特定条件的:
1.互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放
2.请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
3.不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用
4.循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞

怎样避免死锁

  • 加锁顺序
    当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。
  • 加锁时限
    另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行。
  • 死锁检测
    死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。
    每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。
    当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。
    当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。

一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)
一个更好的方案是给这些线程设置优先级让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

JAVA中的多线程

多线程的相关方法

  • start():
    start()方法的作用讲得直白点就是通知"线程规划器",此线程可以运行了,正在等待CPU调用线程对象得run()方法。但是CPU执行哪个线程的代码具有不确定性。
  • run():
    线程开始执行,虚拟机调用的是线程run()方法中的内容。

注意:调用线程的start()方法才是真正的多线程,调用线程的run()方法不是真正的多线程。

  • isAlive():
    测试线程是否处于活动状态,只要线程启动且没有终止,方法返回的就是true
  • getId():
    这个方法比较简单,就不写例子了。在一个Java应用中,有一个long型的全局唯一的线程ID生成器threadSeqNumber,每new出来一个线程都会把这个自增一次,并赋予线程的tid属性,这个是Thread自己做的,用户无法执行一个线程的Id。
  • getName():
    这个方法也比较简单,也不写例子了。我们new一个线程的时候,可以指定该线程的名字,也可以不指定。如果指定,那么线程的名字就是我们自己指定的,getName()返回的也是开发者指定的线程的名字;如果不指定,那么Thread中有一个int型全局唯一的线程初始号生成器threadInitNum,Java先把threadInitNum自增,然后以"Thread-threadInitNum"的方式来命名新生成的线程。
  • getPriority()和setPriority(int newPriority):
    两个在等待CPU的线程,优先级高的线程越容易被CU选择执行
  • isDaeMon、setDaemon(boolean on)
    讲解两个方法前,首先要知道理解一个概念。Java中有两种线程,一种是用户线程,一种是守护线程。守护线程是一种特殊的线程,它的作用是为其他线程的运行提供便利的服务,最典型的应用便是GC线程。如果进程中不存在非守护线程了,那么守护线程自动销毁,因为没有存在的必要,为别人服务,结果服务的对象都没了,当然就销毁了。
    MyThread1作为守护线程,main函数都运行完了,自然也没有存在的必要了,就自动销毁了,因此也就没有再往下打印数字。
    关于守护线程,有一个细节注意下,setDaemon(true)必须在线程start()之前
  • interrupt(),interrupted(),isInterrupted():
    interrupt()是用来设置中断状态的。返回true说明中断状态被设置了而不是被清除了。我们调用sleep、wait等此类可中断(throw InterruptedException)方法时,一旦方法抛出InterruptedException,当前调用该方法的线程的中断状态就会被jvm自动清除了,就是说我们调用该线程的isInterrupted 方法时是返回false。如果你想保持中断状态,可以再次调用interrupt方法设置中断状态。这样做的原因是,java的中断并不是真正的中断线程,而只设置标志位(中断位)来通知用户。如果你捕获到中断异常,说明当前线程已经被中断,不需要继续保持中断位。
    interrupted是静态方法,返回的是当前线程的中断状态。例如,如果当前线程被中断(没有抛出中断异常,否则中断状态就会被清除),你调用interrupted方法,第一次会返回true。然后,当前线程的中断状态被方法内部清除了。第二次调用时就会返回false如果你刚开始一直调用isInterrupted,则会一直返回true,除非中间线程的中断状态被其他操作清除了。
  • join():
    意思是,join()方法会使调用join()方法的线程所在的线程(也就是main线程)无限阻塞,直到调用join()方法的线程销毁为止,此例中main线程就会无限期阻塞直到mt的run()方法执行完毕。
    join()方法的一个重点是要区分出和sleep()方法的区别。join(2000)也是可以的,表示调用join()方法所在的线程最多等待2000ms,两者的区别在于:
    sleep(2000)不释放锁,join(2000)释放锁,因为join()方法内部使用的是wait(),因此会释放锁。
  • wait()方法:
    再Object中,该对象锁控制的同步块或同步方法中去调用该对象的wait方法,将该线程挂起放入该对象等待队列。

Thread中的静态方法

  • currentThread():
    currentThread()方法返回的是对当前正在执行线程对象的引用。
  • sleep(long millis)(注意啦,这是一个静态方法,所以与wait()的区别不只是一个释放锁,另一个不释放锁):
    sleep(long millis)方法的作用是在指定的毫秒内让当前"正在执行的线程"(看来之前理解的有点问题)休眠(暂停执行)。这个"正在执行的线程"是关键,指的是Thread.currentThread()返回的线程。根据JDK API的说法,“该线程不丢失任何监视器的所属权”,简单说就是sleep代码上下文如果被加锁了,锁依然在,但是CPU资源会让出给其他线程。
  • yield():
    暂停当前执行的线程对象,并执行其他线程。这个暂停是会放弃CPU资源的,并且放弃CPU的时间不确定,有可能刚放弃,就获得CPU资源了,也有可能放弃好一会儿,才会被CPU执行
  • interrupted():
    上面有讲到啦。

Java内存区域

就是程序计数器,栈,堆,方法区,本地栈这些。

JAVA内存模型

java内存模型

volatile关键字

  • 保持一定的可见性,结合上面的内存模型进行理解。
  • 保持一定的有序性。
    有序性:

重排序:java编译器和处理器重排序的原因是为了提高了代码的执行效率 ,但是它们不会影响单线程的运行,即(单线程中如果值的变化有依赖关系,则它们之间可能会进行重排序,但是如果这些值之间没有依赖关系,它们可能会进行重排序)
具体可以参看:内存模型、可见性、指令重排序

synchronized 语句

synchronized 修饰方法时锁定的是调用该方法的对象。它并不能使调用该方法的多个对象在执行顺序上互斥。
重入锁:重进入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞。
重入锁实现重入性:每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁

阻塞队列

阻塞队列浅谈

ReentrantLock

ReentrantLock浅谈

ThreadLocal

深入分析ThreadLocal
这里呢,有几个问题,就是给线程的map里面复制的引用还是对象?Java里面对于多线程的工作内存理解有问题?

CountDownLatch

CountDownLatch 顾名思义,就是一个能实现减法的东西,用于多线程,当一个线程执行需要等待其他线程执行完时使用。
重点就是这个线程执行await()方法,其他线程执行countDown()方法。当countDown()方法把CountDownLatch里面的值减小到0后,执行await()方法的这个就可运行啦。
CountDownLatch略谈

线程池

使用目的

减少创建和销毁线程的开销

线程池中的一些参数
  • corePoolSize:核心池的大小(核心工人,来了除非公司倒闭,不然不会被辞退),这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
  • maximumPoolSize:线程池最大线程数(公司最多工人人数,视情况而定,项目少的时候回会裁员,项目多的时候,会招人,招的人不大于这个数字),这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
  • keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
  • unit:参数keepAliveTime的时间单位。
  • workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:

ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;

  • threadFactory:线程工厂,主要用来创建线程;
  • handler:表示当拒绝处理任务时的策略,有以下四种取值:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

几个重要的方法

execute()
submit()
shutdown()
shutdownNow()

线程池的几个状态

volatile int runState;
static final int RUNNING = 0;
static final int SHUTDOWN = 1;
static final int STOP = 2;
static final int TERMINATED = 3;

  • execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。

  • submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果(Future相关内容将在下一篇讲述)。

  • 当创建线程池后,初始时,线程池处于RUNNING状态;

  • 如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;

  • 如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;

  • 当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。

CAS

简单的介绍:

是一种乐观锁,比较然后修改,如果比较的时候和预期值一样的话,那么就修改它(也就是时说它获得了锁子,修改的意义是告诉其他线程他获得了锁子,其它线程得歇一下啦)。

底层支持

CAS这个操作是CPU支持的啦,是CPU的一个原子操作。JAVA里面有一个Unsafe的类来执行这个操作。

如何使用

谈到如何使用这个话题,那就要看一看JAVA中的并发包中的原子操作类(Atomic系列):
原子更新基本类型主要包括3个类:

AtomicBoolean:原子更新布尔类型
AtomicInteger:原子更新整型
AtomicLong:原子更新长整型

这3个类的实现原理和使用方式几乎是一样的,这里我们以AtomicInteger为例进行分析,AtomicInteger主要是针对int类型的数据执行原子操作,它提供了原子自增方法、原子自减方法以及原子赋值方法等,鉴于AtomicInteger的源码不多,自己去看源码吧,思路就是这么回事。

产生的问题及解决方法:
  • ABA问题:
    顾名思义,就是一个线程把A改成了B,另一个线程又把B改成了A,然后现在的这个线程就很懵逼了,因为在他看来A还是A,但是它应该获取锁失败的,它却成功了。
    这怎么避免呢,那么要么你就不要让他产生ABA的情况,也就是你变成的时候注意一下。或者你可以使用AtomicStampedReference类(即对比内容,又对比时间,完美)。
  • 循环时间开销大: 自旋CAS(一直旋转(运行)直到获取到锁)如果长时间获取不了锁的话,对CPU带来了极大的开销,这个问题很让人难受,所以聪明的设计者使用了让自旋字数有限的方法和运用CPU指令(pause)的方法来让它不要那么一直利用CPU。
  • 只能保证一个共享变量的原子操作:
    解决方法:
  1. i = 1; j = a; k = i * j = 1 * a;
    那么这里的k就是代表两个变量啦,对K进行CAS,你懂了没,反正我表达的已经很清楚了。
  2. jdk 1.5之后提供了AtomicReference 来让你直接上一个对象,既然是对象的话,你就可以在对象里面多放几个变量啦。
最后呢,来个彩蛋,就是使用AtomicReference来进行自旋CAS的一种方法,如下:
public class SpinLock {
  private AtomicReference<Thread> sign =new AtomicReference<>();
 
  public void lock(){
    Thread current = Thread.currentThread();
    while(!sign .compareAndSet(null, current)){
    }
  }
 
  public void unlock (){
    Thread current = Thread.currentThread();
    sign .compareAndSet(current, null);
  }
}

AQS

定义:

抽象队列同步器(AbstractQueuedSynchronizer,简称AQS)是用来构建锁或者其他同步组件的基础框架,它使用一个整型的volatile变量(命名为state)和CAS来维护同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

解析:

解析第一步,直接上代码,代码很多时候比语言更能说明问题。但是在看这段代码前,一定要了解volatile变量和CAS是什么,不然看起来肯定是比较困难的。代码并不难,但是都是需要一点一滴的积累着来学习的。
AQS的域:

private transient volatile Node head; //同步队列的head节点,持有锁的就是他了
private transient volatile Node tail; //同步队列的tail节点,直接持有,方便后面插入
private volatile int state; //同步状态

AQS的方法:
这里把方法分为如下三类:
1.修改状态的方法:(这个很简单了,方法内容如方法名)

protected final int getState();  //获取同步状态
protected final void setState(int newState);  //设置同步状态
protected final boolean compareAndSetState(int expect, int update);  //CAS设置同步状态

2.AQS提供的模板方法,主要有以下三类:
1)独占式获取和释放同步状态

public final void acquire(int arg) //独占式获取同步状态,如果不成功会进入同步队列等待。
public final void acquireInterruptibly(int arg) //与acquire不同的是,能响应中断
public final boolean tryAcquireNanos(int arg, long nanosTimeout) //增加超时机制
public final boolean release(int arg) //独占式释放同步状态,该方法会调用重写的tryRelease(int arg)。

2)共享式获取和释放同步状态

public final void acquireShared(int arg) //共享式获取同步状态,如果不成功会进入同步队列等待。与独占式不同的是,同一时刻可以有多个线程获取到同步状态。
public final void acquireSharedInterruptibly(int arg) //可响应中断
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) //超时机制
public final boolean releaseShared(int arg) //共享式释放同步状态,该方法会调用重写的tryReleaseShared(int arg)。

同样以上三种获取同步状态的方法会调用自定义的tryAcquireShared方法。

3)查询同步队列中的等待线程情况:

publicfinalCollection<Thread>getQueuedThreads()
publicfinalbooleanhasQueuedThreads()//返回包含可能正在等待获取的线程列表,需要遍历链表。返回的只是个估计值,且是无序的。这个方法的主要是为子类提供的监视同步队列措施而设计。

3.以上AQS的方法都为final方法,不能被子类重写,因为它们对于任何自定义同步器应该是不需要更改的,下面为AQS提供的可以重写的方法。开发者需要根据自定义同步组件的特点,重写以下方法。这些方法的实现在内部必须是线程安全的,通常应该很短并且不被阻塞。

protected boolean tryAcquire(int arg) //独占式获取同步状态,此方法应该查询是否允许它在独占模式下获取对象状态,如果允许,则获取它。返回值语义:true代表获取成功,false代表获取失败。
protected boolean tryRelease(int arg) //独占式释放同步状态 
protected int tryAcquireShared(int arg) //共享式获取同步状态,返回值语义:负数代表获取失败、0代表获取成功但没有剩余资源、正数代表获取成功,还有剩余资源。
protected boolean tryReleaseShared(int arg) //共享式释放同步状态 
protected boolean isHeldExclusively() //AQS是否被当前线程所独占

独占式获取同步状态
1)调用自定义的tryAcquire方法,该方法要保证线程线程安全的获取同步状态(如Mutex中的tryAcquire使用CAS更新保证原子性),如果获取成功则return。

2)如果获取失败,构造Node,并通过addWaiter方法将节点插入队列尾部。Node.EXCLUSIVE表示节点以独占方式等待。

3)acquireQueued方法中该节点自旋方式尝试获取同步状态。如果获取不到同步状态,则阻塞节点中的线程,被阻塞线程唤醒依靠前驱节点的出队或阻塞线程被终端来实现。

刚看完上面这三个,你可能会有点懵,但是不要着急,当你慢慢的一点一点看完后面这些,然后再回头看这些就会豁然开朗,有种拨云见日,妙不可言的感觉。

//该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出.
public final void acquire(int arg) {
     if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
//上面这按个方法简直妙不可言,接下来慢慢看其内部的代码

//将节点添加到队尾
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);    // Try the fast path of enq; backup to full enq on failure
   //快速尝试入队,如果失败则需要调用enq(node)方法入队,这样做有什么好处?有一定概率减少一次方法调用
    //compareAndSetTail保证Node入队是线程安全的
         Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

//初始化或自旋CAS直到入队成功
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
//自旋方式尝试获取同步状态
  final boolean acquireQueued(final Node node, int arg) {
      boolean failed = true;
      try {
          boolean interrupted = false;
          for (;;) {
              final Node p = node.predecessor();    //获取当前节点的前驱节点
              if (p == head && tryAcquire(arg)) {    //如果前驱节点是head节点则尝试获取同步状态
                  setHead(node);
                 p.next = null; // help GC
                 failed = false;
                 return interrupted;
             }
             if (shouldParkAfterFailedAcquire(p, node) &&
                 parkAndCheckInterrupt())    //判断当前线程是否应该被阻塞,如果是则阻塞直到被唤醒继续循环,如果不是则再次尝试获取同步状态。
                 interrupted = true;
         }
     } finally {
         if (failed)
             cancelAcquire(node);
     }
 }
 
 //判断当前线程是否应该被阻塞,如果线程应该被阻塞则返回true。检查和更新获取同步状态失败Node的前驱节点的waitStatus。
 //其中pred为node的前驱节点
 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
     int ws = pred.waitStatus;
     if (ws == Node.SIGNAL)
         /*
          * This node has already set status asking a release
          * to signal it, so it can safely park.
          */
         //前驱节点已经设置为SIGNAL状态,在前驱节点的线程释放同步状态会唤醒当前Node的线程。
         return true;
     if (ws > 0) {
         /*
          * Predecessor was cancelled. Skip over predecessors and
          * indicate retry.
          */
          //前驱节点是cancelled状态,跳过被取消的Node,直到向前找到waitStatus > 0的Node作为当前节点的前驱,然后重试获取同步状态。
         do {
             node.prev = pred = pred.prev;
         } while (pred.waitStatus > 0);
         pred.next = node;
     } else {
         /* waitStatus = 0是初始状态。
          * waitStatus must be 0 or PROPAGATE.  Indicate that we
          * need a signal, but don't park yet.  Caller will need to
          * retry to make sure it cannot acquire before parking.
          */
         //将前驱节点的等待状态改为SIGNAL。
         compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
     }
     return false;
 }
 
 //阻塞线程
 private final boolean parkAndCheckInterrupt() {
     LockSupport.park(this);    //使用LockSupport阻塞当前线程
     return Thread.interrupted();
 }

独占式释放同步状态:

public final boolean release(int arg) {
     if (tryRelease(arg)) {
         Node h = head;
         if (h != null && h.waitStatus != 0)
             unparkSuccessor(h);    //唤醒后继节点线程
         return true;
     }
     return false;
 } 
 //唤醒后继节点
 private void unparkSuccessor(Node node) {
     /*
      * If status is negative (i.e., possibly needing signal) try
      * to clear in anticipation of signalling.  It is OK if this
      * fails or if status is changed by waiting thread.
      *
      //将当前节点的waitStatus改为0-原始状态,目的是什么?
     int ws = node.waitStatus;
     if (ws < 0)
         compareAndSetWaitStatus(node, ws, 0);
 
     /*
      * Thread to unpark is held in successor, which is normally
      * just the next node.  But if cancelled or apparently null,
      * traverse backwards from tail to find the actual
      * non-cancelled successor.
      */
      //如果后继节点为null或被取消,则从tail向前找到正常的后继节点
     Node s = node.next;
     if (s == null || s.waitStatus > 0) {
         s = null;
         for (Node t = tail; t != null && t != node; t = t.prev)
             if (t.waitStatus <= 0)
                 s = t;
     }
     if (s != null)
         LockSupport.unpark(s.thread);    //唤醒后继节点
 }

共享式的和这个独占式的差不多。
感兴趣可自行查看后面的链接。

LockSupport

在阻塞和唤醒线程时,使用了LockSupport类。LockSupport提供了一系列阻塞和唤醒线程的公共方法。底层使用unsafe提供的方法实现。

void park()    //阻塞当前线程,只有调用unpark或中断才能从park方法中返回
  void parkNanos(long nanos)    //超时阻塞当前线程,超时则返回
  void parkUntil(long deadline)    //截止时间阻塞当前线程,直到deadline
  
  void unpark(Thread thread)    //唤醒处于阻塞状态的线程
  
  //jdk1.6中增加了以下方法,blocker表示当前线程要等待的对象。线程dump信息比使用park方法要多,方便问题排查和监控。
  void park(Object blocker)
  void parkNanos(Object blocker, long nanos)
  void parkUntil(Object blocker, long deadline)

参考:
Thread中的各种方法
JAVA中的CAS
AQS

猜你喜欢

转载自blog.csdn.net/qq_37465188/article/details/85341587
今日推荐