java锁原理分析

前言

在编程中,很多人都会尝试使用多线程的方式去编程,但是却很难保证自己写出来的多线程程序的正确性,在多线程中如果涉及到对共享资源的并发读写,这时就会产生资源的争夺。而在资源争夺中,第一想到的就是使用锁 ,对共享资源进行数据保护。java中提供了2种基本也是最常用的锁,synchronized、Lock!但是这2种锁有什么特点?分别使用的场景?在使用过程中应该注意哪些?各自有哪些不足呢?

本文将带着上面的问题并结合源码进行仔细分析,让你对锁有一个深入的了解。

synchronized 详解

一、概念

synchronized 是java中的关键字,利用锁的机制来实现同步,来达到对共享资源保护。

锁的机制有2种特质:
1、互斥性:在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制。互斥性我们往往称作原子性
2、可见性:确保在锁被释放之前,对共享变量所做的修改,对其余的线程是可见的(也就是说在获得锁的时候应获得最新的共享变量值),否则对于另外一个线程在操作共享变量是自己在本地缓存的副本上,这样就会引发数据的不一致。

二、对象锁和类锁

在使用Synchronized之前,先了解什么是对象锁和类锁
1、对象锁
在java中,每个对象都会有一个monitor对象,这个对象其实就是java对象锁,通常也被称作为"内置锁"或"对象锁",类的对象有多个,所有对象锁也有多个,相互之间互不干涉。
2、类锁
在java中,针对每一个类都有一个帧,可以称作为"类锁",类锁实际也是通过对象锁实现的,即类的Class对象锁,每一个类有且仅有一个Class对象(JVM加载类的时候产生),所有每一个类只有一个类锁。

三、修饰方法

1、修饰实例方法:作用于当前实例加锁,在进入同步代码方法时先获取当前实例锁
2、修饰静态方法:作用于当前类对象锁,在进入同步代码方法时先获取当前对象锁(类.class)

修饰实例方法:
顾名思义就是修饰类中的实例方法,并且默认是当前对象作为锁的对象,而一个对象只有一把锁,所以同一时刻只能有一个线程执行被同步的方法,等到线程执行完方法后,其他线程才能继续执行被同步的方法。正确实例代码如下:

public class SynchronizedTest implements Runnable {
      public static int count = 0;
    @Override
      public void run() {
          for (int i = 1; i <= 1000000; i++) {
            add();
          }
      }
  
      public synchronized void add() {
          ++count;
      }

      public static void main(String[] args) throws Exception {
          SynchronizedTest test = new SynchronizedTest();
          Thread t1 = new Thread(test);
          Thread t2 = new Thread(test);
          t1.start();
          t2.start();
          t1.join();
          t2.join();
         System.out.println(count); //2000000
}

}

错误代码:对同一共享资源使用不同实例锁

public class SynchronizedTest implements Runnable {

private Object object;

public SynchronizedTest(Object object) {
    this.object = object;
}

public static int count = 0;

@Override
public void run() {

    synchronized (object) {
        for (int i = 1; i <= 1000000; i++) {
            add();
        }
    }
}

public static  void add() {
    ++count;
}

public static void main(String[] args) throws Exception {
    Object object1 = new Object();
    Object object2 = new Object();
    SynchronizedTest firstLock = new SynchronizedTest(object1);
    SynchronizedTest secondLock = new SynchronizedTest(object2);
    Thread t1 = new Thread(firstLock);
    Thread t2 = new Thread(secondLock);
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);//小于 20000000
}

}

运行程序后,会发现结果永远小于2000000,说明synchronized没有起到同步的作用了,说明修饰实例方法只能作用实例对象,不能作用到类对象

修饰静态方法:

public class SynchronizedTest implements Runnable {

public static int count = 0;

@Override
public void run() {
    for (int i = 1; i <= 100000; i++) {
        add();
    }
}

public static synchronized void add() {
    ++count;
}

public static void main(String[] args) throws Exception {
    SynchronizedTest firstLock = new SynchronizedTest();
    SynchronizedTest secondLock = new SynchronizedTest();
    Thread t1 = new Thread(firstLock);
    Thread t2 = new Thread(secondLock);
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);=2000000
}

}

作用与静态方法的时候,不管实例化多少个实例对象,结果用于等2000000,说明锁对象是当前类.class,有且仅有一把锁,最终结果和实际结果一致!

修饰代码块:正确案例

ppublic class SynchronizedTest implements Runnable {

private Object object;

public SynchronizedTest(Object object) {
    this.object = object;
}

public static int count = 0;

@Override
public void run() {

    synchronized (object) {
        for (int i = 1; i <= 1000000; i++) {
            add();
        }
    }
}
public static void add() {
    ++count;
}
public static void main(String[] args) throws Exception {
    Object object = new Object();
    SynchronizedTest firstLock = new SynchronizedTest(object);
    SynchronizedTest secondLock = new SynchronizedTest(object);
    Thread t1 = new Thread(firstLock);
    Thread t2 = new Thread(secondLock);
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);//= 20000000
}

}

错误案例:

public class SynchronizedTest implements Runnable {
private Object object;

public SynchronizedTest(Object object) {
    this.object = object;
}

public static int count = 0;

@Override
public void run() {
    synchronized (object) {
        for (int i = 1; i <= 1000000; i++) {
            add();
        }
    }
}
public static void add() {
    ++count;
}
public static void main(String[] args) throws Exception {
    Object object1 = new Object();
    Object object2 = new Object();
    SynchronizedTest firstLock = new SynchronizedTest(object1);
    SynchronizedTest secondLock = new SynchronizedTest(object2);
    Thread t1 = new Thread(firstLock);
    Thread t2 = new Thread(secondLock);
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);//小于 20000000
}

}
代码块使用加Synchronized的时候,使用同一把锁,其他的线程就必须等待,这样也就保证了每次只有一个线程执行被同步的代码块。不是同一把同锁,无法达到共享资源同步结果

四、synchronized底层原理

在java虚拟机中的同步是基于进入和退出管程(Monitor)对象实现的,同步有显示同步(有明确的monitorenter和moniterexit)和隐士同步(ACC_SYNCHRONIZED)

1、代码块底层原理

下面是一段被synchronized修饰 的同步代码块,在代码块中操作共享变量:

public class SynchronizedBlockTest {

public int j = 0;

public void excuteSynTask() {
    synchronized (this) {
        j++;
    }
}

}
通过javap -c反编译出来如下:

    Compiled from"SynchronizedBlockTest.java"
    public class com.example.thread.SynchroniedTest.SynchronizedBlockTest{
    public int j;
    public com.example.thread.SynchroniedTest.SynchronizedBlockTest();
    Code:
    0:aload_0
    1:invokespecial #1                  // Method java/lang/Object."":()V
    4:aload_0
    5:iconst_0
    6:putfield      #2                  // Field j:I
    9:return

    public void excuteSynTask();
            Code:
            0:aload_0
            1:dup
            2:astore_1
            3:monitorenter //进入监视器(进入同步方法)
            4:aload_0
            5:dup
            6:getfield      #2                  // Field j:I
            9:iconst_1
            10:iadd
            11:putfield      #2                  // Field j:I
            14:aload_1
            15:monitorexit //退出监视器(退出同步方法)
            16:goto 24
            19:astore_2
            20:aload_1
            21:monitorexit //退出同步方法
            22:aload_2
            23:athrow
            24:return
            Exception table:
            from to target type
            4 16 19any
            19 22 19any
            }

从上面字节码可以看出同步代码块实现使用的monitorenter 和monnitorexit指令,其中monitorenter指令指向同步代码块起始的位置,monitorexit指令则指明同步代码块结束的位置,当执行monitorenter指令的时候,当前线程将试图获取objectref(即对象锁)所对应的moniter持有权,当objectref的monitor进入计数器为 0,那么此线程就可以成功取得monitor,并且将计数器的值设置为1,获取锁成功。如果当前线程已经拥有objectref的monitor的特有权,那么它可以重入这个monitor,重入时的计算器值也会被执行,执行线程将释放monitor(锁)并设置计数器值为0,其他线程将有机会持有monitor,值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

2、synchronized方法底层原理

方法级别的同步是隐士的,即无需通过字节码指令来控制。JVM可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志来区分一个方法是否是同步方法.当方法调用的时候,调用指令将会检查方法中的ACC_SYNCHRONIZED 访问是否被标志了,如果设置了,执行线程将会先持有monitor,然后在执行方法,最后在方法完成的时候释放monitor。在方法执行的期间,执行线程持有monitor,其他任何线程无法在获得同一个monitor。如果线程在执行同步方法的时候抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法锁持有的monitor将在异常抛到同步方法之外时将会自动释放。

    public com.example.thread.SynchroniedTest.SynchronizedBlockTest();
            Code:
            0:aload_0
            1:invokespecial #1                  // Method java/lang/Object."":()V
            4:aload_0
            5:iconst_0
            6:putfield      #2                  // Field j:I
            9:return

    public synchronized void excuteSynTask();
            flags:ACC_PUBLIC,ACC_SYNCHRONIZED
          Code:
          0:aload_0
          1:dup
          2:getfield      #2                  // Field j:I
          5:iconst_1
          6:iadd
          7:putfield      #2                  // Field j:I
        10:return

从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的 确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该 ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。同时我们还必须注意到的是在java早期版本中,synchronized是属于重量级锁,效率低下,因为监视器是依赖于底层的操作系统来实现的,但是在操作系统实现线程之间的切换时需要从用户态转换到核心态,这个转态之间的转换是需要相对比较长的时间,时间成本相对比较高,这个也是早期版本中synchronized效率低下的原因。然而在java6之后官方从JVM层面对synchronized有了较大的优化,所以现在的synchonize锁效率也优化了很多。在java6之后,为了减少获得锁和释放锁带来的性能消耗,引入了轻量级锁和偏向锁,接下来Java官方在JVM层面synchronized锁的优化。

3、JAVA虚拟机对synchronized的优化

锁的状态总共优化总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,在升级到重量级锁,但是锁的升级只能是单向的,也就是说只能是低到高升级,不会出现锁的降级,关于重量级锁,前面已经讲述过了,下面介绍的剩下几种锁,

偏向锁

偏向锁是在java6之后才加入的新锁,他是针对加锁操作的优化手段,在大多数情况下,锁不仅存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁的代价而引入的偏向锁,偏向锁的核心思想是,如果一个线程获得了锁,那么就进入了偏向模式,此时Mark word 的结构也就变为了偏向锁结构,当这个线程再次请求锁的时候,就不需要再做任何同步操作了,即获取锁的过程,就省去了大量的有关锁的申请操作了,从而就提高了程序的性能。所以,对于没有锁的竞争的场合,偏向锁有很好的优化效果,毕竟这样既有可能连续多次是同一个线程重复的去申请相同的锁,但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样的场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

轻量级锁

假如偏向锁失败,虚拟机并不会立即升级为重量级锁,他还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word的结构也会变为轻量级锁结构,轻量级锁能够提升程序的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”。

自旋锁

当上面的轻量级锁失败之后,虚拟机为了避免线程真实的在操作系统层面上被挂起,这个时候可以利用自旋锁的优化手段。在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间转换需要从用户态转化为核心态,这个状态之间的转化需要较长的时间,时间的成本是想多比较高的,因此自旋锁会假设在不久的将来当前的线程就可以获得锁,因此虚拟机会让当前想要获取的线程做几个空的循环,一般不会太久,在经过一段时间的循环之后,如果得到了锁,就可以进入到了临界区。如果还不能获取到锁,那么就会将线程在操作系统层面上挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的,最后没有办法就只能升级为重量级锁。

锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更加的彻底,在java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次执行时,也叫及时编译 )

通过对运行上下的扫描,去除不可能存在资源竞争的锁,同过这种方式消除没有必要的锁,可以节省毫无意义的请求锁的时间,对于StringBuffer的append方法是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其余的线程锁使用,因此StringBuffer不可能存在资源竞争的背景,JVM会自动将其锁消除。

    public class StringBufferRemoveLock {
         public void add(String str, String str2) {
         StringBuffer sb = new StringBuffer();
        sb.append(str).append(str2);
    }

public static void main(String[] args) {
    StringBufferRemoveLock stringBufferRemoveLock = new StringBufferRemoveLock();
    for (int i = 0; i < 10000; i++) {
        stringBufferRemoveLock.add("abc", "" + i);
    }
}

}
}

4、线程中断与synchronized

中断锁表达的意思是,在线程运行期间大断他,在java中,提供了下面三个有关线程中断的方法:

在讲解中断与Synchronized时先了解下面interrupt isInterrupted,interrupted方法的基本用法:

/ 中断线程(此线程并不一定是当前线程,而是指调用该方法的Thread实例所代表的线程)这个其实只是给线程打了一个中断标志,线程任然会继续运行
Thread.interrupt();

// 判断线程是否被中断,并不会清除中断状态
Thread.isInterrupt();

//表示的线程是否被中断并清除当前线程的中断状态
Thread.interrupted();
案例分析

java锁原理分析_第1张图片
图片1.png

从结果可以看出在调用了interrupt()方法的后,线程仍然在继续运行,并未停止,但是这个时候已经给线程设置了中断标志,两个isInterrupt方法都会的输出true;

对上面的案例在做一下变动:

java锁原理分析_第2张图片
图片2.png

上面可以看出第一次调用isInterrupted方法的时候返回结果是true

第一次和第二次调用interrupted方法返回结果是false 这个和之前解释interrupted不一致,应该一个是true一个是false才正确。 这其实有一个坑,interrputed方方法测试的是当前线程是否被中断,注意是当前线程!!

上面的当前线程其实就是main线程,而mythread.interrupt()调用的是mythread线程。所以这里调用mythread.interrupted()其调用的是main.interrupted

interrupted源码分析:

java锁原理分析_第3张图片
图片3.png

从源码中可以看出调用的是currentThread线程,true表示的是是否清除中断标志

请看下面的案例:

java锁原理分析_第4张图片
图片4.png

实际结果和预料之中是一致的。

注意:

若线程在阻塞状态时,调用了它的interrupt()方法,那么它的“中断状态”会被清除并且会收到一个InterruptedException异常。

例如,线程通过wait(),sleep()进入阻塞状态,此时通过interrupt()中断该线程;调用interrupt()会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,所以该“中断标记”会立即被清除为“false”,同时,会产生一个InterruptedException的异常。

中断与synchronized

public class SynchronizedBlock implements Runnable {

public synchronized void test() {
    System.out.println("开始调用test()方法");
    while (true) // Never releases lock
        Thread.yield(); //将执行权交给其他线程执行
}

public SynchronizedBlock() {

    new Thread(new Runnable() {
        @Override
        public void run() {
            test();
        }
    }).start();
}

@Override
public void run() {
    while (true) {
        System.out.println("开始执行----");
        if (Thread.interrupted()) {
            System.out.println("中断线程");
        } else {
            test();
        }
    }
}


public static void main(String[] args) throws InterruptedException {
    SynchronizedBlock synchronizedBlock = new SynchronizedBlock();
    Thread thread = new Thread(synchronizedBlock);
    thread.start();
    TimeUnit.SECONDS.sleep(1);
    thread.interrupt();
}

}
运行结果:
开始调用test()方法
开始执行----

通过上面的结果发现 控制台输出结果并没有:中断线程,因为interrupt对Synchronized修饰的方法,代码块不会响应中断。但是后文lock锁 对interrupt可以响应中断。

Lock

在jdk1.5 以后,增加了Juc并发包且提供了Lock接口用来实现锁的功能,他除了与synchronized关键字类似的同步功能,还提供了比synchronized更加灵活的实现。多线程下可以精确控制线程,且在jdk1.5 并发效率比synchornized更高的并发率,但是这也会带来一些缺点,下文将一步步去分析。

Lock本质上是一个接口(位于源码包中的java.util.concurrentLocks中)它包含以下几个方法

//尝试获取锁,获取锁成功则立马返回,否则阻塞当前线程
void lock();

//尝试获取锁,线程在成功获取锁之后被中断。则放弃获取锁并且抛出异常
Void lockInterruptibly() throws InterruptException;

//尝试获取锁,获取成成功之后,就返回true,否则就返回false
Boolean tryLock();

//尝试获取锁,若在规定的时间内获取到锁,立马就返回true,否则就返回false,未获取到锁之前被中断就抛出异常
Boolean tryLock(long time,TimeUnit unit) throws InterruptException;

//释放锁
Void unlock();

//返回当前锁的条件变量,通过条件变量可以实现类似notify和wait()a的功能,一个锁可以有多个条件变量
Condition newCondition();

Lock里面有三个实现类,第一个是ReentrantLock ,另外2个是ReentrantReadWriteLock类中ReadLock和WriteLock。

使用方法:多线程下访问共享之源时,访问前需要加上锁,访问结束的时候在解开锁,解锁条件代码必须放在finally中,不然会出现死锁

    ReentLock lock = new ReentLock();
    lock.lock();
    try {
    } finally {
        lock.lock();
    }

注意:加锁必须位于对资源访问的try外部,特别是使用lockInterruptibly方法锁的时候就必须这样子去做,这是为了防止线程在获取锁的时候被中断了,不需要也没有必要去释放锁。

ReentrantLock reentrantLock = new ReentrantLock();
    try {
        reentrantLock.lockInterruptibly();
        try {
            //access the resource protected by this lock
        } finally {
            reentrantLock.unlock();
          }
    } catch (InterruptedException exception) {
    }

ReentLock内部源码分析

概念

ReentLock 是基于AbstractQueendSynchronized来实现的,所以在了解ReentLock之前先简单的说一下AQS
我们最熟悉的同步锁应该就是synchronized(上文已经对其做了详细的介绍)
他是通过底层的monitorEnter 和monitorExit和ACC_SYNCHRONIZED来实现锁的获取和释放的
这里介绍的AbstractQueenSynchronized 同步器(AQS),是基与FIFO队列来实现的,通过state的状态,来实现acquire和release;state为0的表示该锁还没有被任何线程获取可以获取锁;state为1表示已经有线程已经获取了锁。

源码分析AQS

AQS是基于FIFO队列实现的,所以队列必然是有一个个节点组成的,下面从节点开始讲解:
//waitstatus 有下面几个状态
/**

  • CANCELLED =1 表示当前的线程被取消了
  • SIGNAL =-1 表示当前节点的后继节点阻塞了,需要被唤醒
  • CONDITION =-2 表示当前节点在等待Condition ,因为某个节点条件被阻塞
  • PROPAGATE=-3 表示锁的下一次获取可以无条件的传播
    */
    volatile int waitStatus;
    static final int CANCELLED = 1;
    static final int SIGNAL = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
介绍Node

//头节点
Private tansient volatile Node head;
//尾结点
Private transient volatile Node tail;
//同步状态
Private volatile int state;

*上面已经介绍完AQS几个重要的成员,下面开始通过一个demo,来分析ReentLock底层实习原理

public class ReentLockTest {

public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    final ReentrantLock lock = new ReentrantLock();
    for (int i = 0; i < 2; i++) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                lock.lock(); // 第一步
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.getStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        };
        executorService.submit(runnable);
    }
    executorService.shutdown();
  }}

从第一步开始查看lock方法

public void lock() {
    sync.lock();
}

ReentLock中有一个抽象类Sync ,它继承了AQS,所以RenntLock的实现也是基于Sync来完成实现的,NonfairSync(非公平锁)和FairSync公平锁继承了Sync。

公平锁:获取锁是有顺序的先来先到

*非公平锁:每个线程抢占锁的顺序是不固定的(不能说是随机的,在读写锁源码中可以看到)


java锁原理分析_第5张图片
图片5.png

RenntLock 我们平时用的最多的是非公平锁(并发率高),公平锁效率相比较低,下面介绍Nofair的lock方法

 final void lock () {
    //通过cas将AQS中state变量设置1
        if (compareAndSetState(0, 1))
            //等cas操作成功之后,将当前线程设置成排它线程,后面的线程无法在获取锁
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

前面的demo中启动了2个线程,假设A线程进来之后,调用compareAndSetState(0,1)方法成功,此时由于没有任何线程占有锁所以通过原子操作CAS,将state的状态由0修改为1,之后将该线程设置到AQS变量exclusiveOwnerThread中。此时线程B进入了lock方法,通过CAS操作,发现AQS中的state变量已经变成了1,设置失败于是就进入了else中的acquire方法。
也就是说非公平锁在线程第一次失败之后,会调用acquire方法进入队列中,然而公平锁是直接调用acquire方法
acquire方法调用父类AQS中的acuqire方法,源码如下:

 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
    //static final Node EXCLUSIVE = null;
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
    }

下面看tryAcquire(arg)方法:

    final boolean nonfairTryAcquire(int acquires) {
    //获取当前线程(B线程)
   final Thread current = Thread.currentThread();
   //获取当前AQS中标志位state变量值, 0 表示没有被任何线程占有;1: 表示当前锁已经被某个线程拿走了 
       int c = getState();
       if (c == 0) {
   //表示锁还没有任何线程拿走,当前线程可以去获取
    if (compareAndSetState(0, acquires)) {
   //当前线程获取锁成功,将AQS中exclusiveOwnerThread设置为当前线程
   setExclusiveOwnerThread(current);
      return true;
    }
} //如果是当前线程等于AQS中exclusiveOwnerThread线程表示的是当前线程已经获取锁,本次获取锁是重入
else if (current == getExclusiveOwnerThread()) {
    //获取AQS中的变量,并加acquires(可以自由指定,默认是1)
    int nextc = c + acquires;
    if (nextc < 0) // overflow
        throw new Error("Maximum lock count exceeded");
    setState(nextc);
    return true;
}
return false;

}

从上面可以看出如果线程B尝试获取锁失败之后,会调用acquireQueued方法,调用acquireQueued之前会先调用addWaiter方法

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode); //1
        Node pred = tail;
      if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
            pred.next = node;
           return node;
    }
}
enq(node);
return node;

}

方法参数中的mode是Node节点类型,在这里表示独占的锁,用当前线程在构造一个Node对象,线程B进来之后,此时队列里面是空的,所以尾节点tail为空,走enq方法

    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;
           }
       }
    }
 }

上面enq方法是一个循环,当前线程B进来之后将tail(此时可能为空)赋值给t变量,在判断t是否等于null(这里判断null是因为多线程,这时t也有可能不为空,目前这里只有A、B线程,不存在这种情况暂时不考虑)接下来调用compareAndSetHead,成功,head指向tail,如果失败 就重复尝试,直到成功为止,当head成功之后,进入else逻辑中,将后续的Node加入到队尾,如果compareAndSetTail 失败的话,通过for循环继续插入到队尾,最终所有没有成功获取到锁的线程全部加入到队列中。

 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)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

首先会判断node前驱节点是否为head,如果是证明当前线程就是下一个即将获取锁的线程,所以此时先尝试在在调用了tryAuquire方法,如果获取到了锁,那么就将之前的node(之前为head的node)设置为空,gc回收。

如果不是head或者获取失败之后,那么久调用shouldParkAfterFailedAcquire方法

   private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
      int ws = pred.waitStatus;
      if (ws == Node.SIGNAL)
          return true;
      if (ws > 0) {
          do {
              node.prev = pred = pred.prev;
          } while (pred.waitStatus > 0);
          pred.next = node;
      } else {
          compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
      }
      return false;
}

该方法是判断这个节点是否需要挂起,在介绍这个方法之前先看一下前面讲的Node节点中除了当前线程,前驱节点,后去节点,还有一个重要的变量waitstatus 这个变量是来表示当前node是否需要竞争锁,某些情况下,有些线程是会放弃锁的竞争的,比如condition

也就是说只有当node状态为SIGNAL情况下,当前节点才会被挂起,,假设当前的节点的WaitStatus就是SIGNAL,则调用parkAndCheckInterrupt方法,此时线程才被真正的挂起。

在上面调用acquireQueued方法,线程被挂起之后,还是处在for循环中,所以当线程被唤醒的时候,会继续执行,此时tryAcquire成功后,获取锁,之后将节点删除,这样获取锁的Node队列就没有了。
后面继续读写锁分析以及总结。。。。

清风不问烟雨

你可能感兴趣的:(java锁原理分析)