多线程内部是如何实现同步

  • wait/notify
  • synchronized
  • ReentrantLock

自旋

  1. volatile int status =0 // 标识--是否有线程在同步代码块中
  2. void lock(){
  3. // CAS自旋,判断内存status的值为0,如果为0则修改status的值为1,返回true,进入到方法中
  4. // 如果此时status的值不为0,则compareAndSet的值返回为false,一直的进行等待
  5. while(!compareAndSet(0,1)){
  6. // 获取不到🔐的线程执行内部的逻辑,一直的进行CAS循环操作
  7. }
  8. }
  9. //
  10. void unlock(){
  11. status = 0;
  12. }
  13. boolean compareAndSet(int except,int newValue){
  14. // cas操作,修改status成功则返回true
  15. }

缺点:耗费CPU资源,没有竞争到锁的线程会一直占用CPU进场CAS的操作,假设某个线程获取到锁之后要花费NS处理业务逻辑,那另外的一个线程就会白白的花费Ns的CPU等待

思路:让线程让出CPU

yeild+自旋

  1. volatile int status =0 // 标识--是否有线程在同步代码块中
  2. void lock(){
  3. // CAS自旋,判断内存status的值为0,如果为0则修改status的值为1,返回true,进入到方法中
  4. // 如果此时status的值不为0,则compareAndSet的值返回为false,一直的进行等待
  5. while(!compareAndSet(0,1)){
  6. // 获取不到🔐的线程执行内部的逻辑,的线程进入都会在while阻塞住
  7. yeild(); // 自己实现让出CPU,暂停当前正在执行的线程对象,并执行其他线程
  8. }
  9. }
  10. //
  11. void unlock(){
  12. status = 0;
  13. }
  14. boolean compareAndSet(int except,int newValue){
  15. // cas操作,修改status成功则返回true
  16. }

要解决自旋锁性能问题必须让竞争锁失败的线程不空转,而是在获取不到锁的时候能把CPU给让出来,yeild()方法就能让出CPU资源,当线程竞争失败的时候,会调用yeild方法让出CPU。自旋+yeild的方法并没有完全的解决问题。当系统只有两个锁进行竞争的时候,yeild是生效的。需要注意的是方法让出当前CPU。有可能操作系统下次还是选择运行该线程,比如里面有2000个线程,这个时候就会有问题,比如当t2线程进入到while循环中,此时t2线程进行到可运行状态,但是由于线程的调度是由CPU进行决定的,所以下一次还是有可能会CPU选择t2执行CAS操作,又进入到while的循环体内,其他的线程此时依然会出现空转的情况。

park+自旋

park方法:该如何让线程真正停止不往前执行呢: 真正让线程停止下来(阻塞),Java提供了一个较为底层的并发工具类:LockSupport,该类常用的方法有两个,1 park(Object blocker) 表示阻塞指定线程,参数blocker当前线程对象 2 unpark(Thread thread) 唤醒指定线程,数thread指定线程对象

  1. volatile int status =0 // 标识--是否有线程在同步代码块中
  2. void lock(){
  3. // CAS自旋,判断内存status的值为0,如果为0则修改status的值为1,返回true,进入到方法中
  4. // 如果此时status的值不为0,则compareAndSet的值返回为false,一直的进行等待
  5. while(!compareAndSet(0,1)){
  6. // 获取不到🔐的线程执行内部的逻辑,的线程进入都会在while阻塞住
  7. park(); // 让线程停止下来处于阻塞的状态
  8. }
  9. }
  10. // 获取到锁的线程进行执行业务逻辑,过了20秒之后
  11. void unlock(){
  12. status =1;
  13. // 释放线程
  14. lock_notify();
  15. }
  16. void park(){
  17. // 将当前线程加入到等待队列中
  18. parkQueue.add(currentThread);
  19. // 将当前线程释放CPU 阻塞
  20. releaseCpu();
  21. }
  22. void lock_notify(){
  23. // 得到要唤醒的线程头部线程
  24. Thread t = parkQueue.header();
  25. // 唤醒等待线程
  26. unpark(t);
  27. }
  28. boolean compareAndSet(int except,int newValue){
  29. // cas操作,修改status成功则返回true
  30. }

ReentrantLock类关系图

image.png
image.png
从图中我们可以看出当我们创建一个ReentrantLock实例的时候,默认是使用其内部类NonfairSyncFairSync创建的。其中当我们使用的是默认的构造函数的时候其使用的是非公平锁的方式实现的,如果我们想要实现公平锁,那么需要在构造函数中传入一个boolean值True这个时候就可以创建出一个公平锁出来。下面来看一下ReentrantLock创建一个公平锁的时候是如何的进行加锁的。

FairSync的Lock方法实现原理

image.png
从图中可以看到FairSync继承的是Sync类,Sync是一个ReentrantLock的内部类,同时也是抽象类,里面有两个方法lock()方法,和tryAcquire()方法,这两个方法分别由子类进行实现,FairSyncNonfairSync,公有的一些方法是放在Sync类中进行实现的,所以抽象类一般比较适用于模板设计模式。这里当然也是其中的一个体现。

下面就正式的看一下FairSync的lock方法,这个方法其实就只有一行代码acquire(1)。但是这一行代码其调用的是父类Sync的父类的AbstrtactQueuedSynchronizer里面的acquire()方法
image.png
image.png
AbstrtactQueuedSynchronizer里面的acquire()方法。会执行tryAcquire()方法的调用。具体的实现又根据公平锁和非公平锁,又有差异,所以该方法的实现放在子类中进行实现的,现在看在公平锁中tryAcquire()方法的实现逻辑。
image.png

  • Thread.currentThread()会拿到当前的线程
  • getState()方法会获取到,state的值,该值是为了进行CAS的操作。当state的值为0的时候,说明该锁是自由状态,可以被获取到。但是会不会被当前的线程占有该锁呢?这个其实不一定,因为如果是只要此线程以来就会占锁,那前面等待的线程就是白等啦,这还算是公平的吗,所以可想而知,因此会进行一系列的判断,是否确实应该是自己占,这个时候才会去占有该锁。下图是对该逻辑的解释:

image.png

  • hashQueuedPredecessors(),该函数就是判断此线程是否需要进行排队操作,查看其源码我们来理解一下该函数的意思。

image.png
首先我们看到有个头尾指针,作为一个程序员的直觉,很明显这个是一个双向链表,如果细看AbstrtactQueuedSynchronizer里面的方法,就会发现,里面有static final修饰的Node节点,该节点的重要的组成部分为:

  1. // 头指针
  2. volatile Node head;
  3. // 尾指针
  4. volatile Node tail;
  5. // 当前线程
  6. volatile Thread thread;