解决多线程安全问题-无非两个方法synchronized和lock 具体原理以及如何 获取锁AQS算法 (百度-美团)

 本篇文章主要讲了lock的原理 就是AQS算法,还有个姊妹篇 讲解synchronized的实现原理 也是阿里常常问的,html

参考:深刻分析Synchronized原理(阿里面试题)java

必定要看后面的文章,先说结论: node

非公平锁tryAcquire的流程是:检查state字段,若为0,表示锁未被占用,那么尝试占用,若不为0,检查当前锁是否被本身占用,若被本身占用,则更新state字段,表示重入锁的次数。若是以上两点都没有成功,则获取锁失败,返回false。面试

 

还有其余的锁,若是想要了解,参考:JAVA锁机制-可重入锁,可中断锁,公平锁,读写锁,自旋锁,算法

用synchronized实现ReentrantLock 美团面试题参考:使用synchronized 实现ReentrantLock(美团面试题目)编程

前几天去百度面试,面试官问多线程如何解决并发问题,感受本身对lock的原理了解不够,这里对两种方式synchronized和lock作个系统的总结:api

解决多线程的并发安全问题,java无非就是加锁,具体就是两个方法缓存

(1) Synchronized(java自带的关键字)安全

(2) lock 可重入锁 (可重入锁这个包java.util.concurrent.locks 底下有两个接口,分别对应两个类实现了这个两个接口: 多线程

       (a)lock接口, 实现的类为:ReentrantLock类 可重入锁;

       (b)readwritelock接口,实现类为:ReentrantReadWriteLock 读写锁)

也就是说有三种:

(1)synchronized 是互斥锁;

(2)ReentrantLock 顾名思义 :可重入锁

(3)ReentrantReadWriteLock :读写锁

读写锁特色:

a)多个读者能够同时进行读
b)写者必须互斥(只容许一个写者写,也不能读者写者同时进行)
c)写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)

总结来讲,Lock和synchronized有如下几点不一样:

1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)当synchronized块结束时,会自动释放锁,lock通常须要在finally中本身释放。synchronized在发生异常时,会自动释放线程占有的锁,所以不会致使死锁现象发生;而Lock在发生异常时,若是没有主动经过unLock()去释放锁,则极可能形成死锁现象,所以使用Lock时须要在finally块中释放锁;
3)lock等待锁过程当中能够用interrupt来终端等待,而synchronized只能等待锁的释放,不能响应中断。
4)lock能够经过trylock来知道有没有获取锁,而synchronized不能; 

5. 当synchronized块执行时,只能使用非公平锁,没法实现公平锁,而lock能够经过new ReentrantLock(true)设置为公平锁,从而在某些场景下提升效率。

六、LLock能够提升多个线程进行读操做的效率。(能够经过readwritelock实现读写分离)
七、synchronized 锁类型 可重入 不可中断 非公平 而 lock 是: 可重入 可判断 可公平(二者皆可) 
在性能上来讲,若是竞争资源不激烈,二者的性能是差很少的,而当竞争资源很是激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。因此说,在具体使用时要根据适当状况选择。 

首先看一下Synchronized的原理:

一、synchronized

把代码块声明为 synchronized,有两个重要后果,一般是指该代码具备 原子性(atomicity)和 可见性(visibility)

实现原子性的算范为CAS(Compare and Swap) 参考:Java多线程系列——原子类的实现(CAS算法)

(1) 原子性

原子性意味着个时刻,只有一个线程可以执行一段代码,这段代码经过一个monitor object保护。从而防止多个线程在更新共享状态时相互冲突。

 (2)  可见性

可见性则更为微妙,它要对付内存缓存和编译器优化的各类反常行为。啥是可见性呢?

答:它必须确保释放锁以前对共享数据作出的更改对于随后得到该锁的另外一个线程是可见的 。

做用:若是没有同步机制提供的这种可见性保证,线程看到的共享变量多是修改前的值或不一致的值,这将引起许多严重问题。 

通常来讲,线程以某种没必要让其余线程当即能够看到的方式(无论这些线程在寄存器中、在处理器特定的缓存中,仍是经过指令重排或者其余编译器优化),不受缓存变量值的约束,可是若是开发人员使用了同步,那么运行库将确保某一线程对变量所作的更新先于对现有synchronized 块所进行的更新,当进入由同一监控器(lock)保护的另外一个synchronized 块时,将马上能够看到这些对变量所作的更新。相似的规则也存在于volatile变量上。

——volatile只保证可见性,不保证原子性 

(3)synchronize的限制:

  1. 当线程尝试获取锁的时候,若是获取不到锁会一直阻塞, 它没法中断一个正在等候得到锁的线程;
  2. 若是获取锁的线程进入休眠或者阻塞,除非当前线程异常,不然其余线程尝试获取锁必须一直等待,也没法经过投票获得锁,若是不想等下去,也就无法获得锁。

二、ReentrantLock (可重入锁) 

何为可重入(美团面试提问过此处):参考:如何理解ReentrantLock的可重入和互斥?

可重入的意思是某一个线程是否可屡次得到一个锁,在继承的状况下,若是不是可重入的,那就造成死锁了,好比递归调用本身的时候;,若是不能可重入,每次都获取锁不合适,好比synchronized就是可重入的,ReentrantLock也是可重入的

锁的概念就不用多解释了,当某个线程A已经持有了一个锁,当线程B尝试进入被这个锁保护的代码段的时候.就会被阻塞.而锁的操做粒度是”线程”,而不是调用(至于为何要这样,下面解释).同一个线程再次进入同步代码的时候.可使用本身已经获取到的锁,这就是可重入锁java里面内置锁(synchronize)和Lock(ReentrantLock)都是可重入的 

我本身写了个例子:   

package entrantlock_test;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class parent {
    
     protected Lock lock=new ReentrantLock();
     
     public void test(){
         lock.lock();
         try{
             System.out.println("Parent");
         }finally{
             lock.unlock();
         }
     }
     
     
}

class Sub extends parent{

    @Override
    public void test() {
        // TODO Auto-generated method stub
        lock.lock();
        try{
        super.test();
        System.out.println("Sub");
        
        }finally{
            lock.unlock();
        }
    }
    
    
}

public class LockTest{
    
    public static void main(String[] args){
        
        Sub s=new Sub();
        s.test();
        
    }
}
View Code

 

 2.1 . 为何要可重入 

若是线程A继续再次得到这个锁呢?好比一个方法是synchronized,递归调用本身,那么第一次已经得到了锁,第二次调用的时候还能进入吗? 直观上固然须要能进入.这就要求必须是可重入的.可重入锁又叫作递归锁,否则就死锁了。 

 它实现方式是:

为每一个锁关联一个获取计数器和一个全部者线程,当计数值为0的时候,这个所就没有被任何线程只有.当线程请求一个未被持有的锁时,JVM将记下锁的持有者,而且将获取计数值置为1,若是同一个线程再次获取这个锁,技术值将递增,退出一次同步代码块,计算值递减,当计数值为0时,这个锁就被释放.ReentrantLock里面有实现

其实也有不可重入锁:这个还真有.Linux下的pthread_mutex_t锁是默认是非递归的。能够经过设置PTHREAD_MUTEX_RECURSIVE属性,将pthread_mutex_t锁设置为递归锁。若是要本身实现不可重入锁,同可重入锁,这个计数器只能为1.或者0,再次进入的时候,发现已是1了,就进行阻塞.jdk里面没有默认的实现类.

Java.util.concurrent.lock 中的Lock 框架是锁定的一个抽象,Lock弥补了synchronized的局限,提供了更加细粒度的加锁功能。  

ReentrantLock 类是惟一实现了Lock的类 ,它拥有与synchronized 相同的并发性和内存语义,可是添加了相似锁投票定时锁等候可中断锁等候的一些特性。此外,它还提供了在激烈争用状况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 能够花更少的时候来调度线程,把更多时间用在执行线程上。)  

用sychronized修饰的方法或者语句块在代码执行完以后锁自动释放,而是用Lock须要咱们手动释放锁,因此为了保证锁最终被释放(发生异常状况),要把互斥区放在try内,释放锁放在finally内!!  

Lock 接口api以下  

public interface Lock {
void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }

 

 lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。

unLock()方法是用来释放锁的。
在Lock中声明了四个方法来获取锁,那么这四个方法有何区别呢?

  首先lock()方法是日常使用得最多的一个方法,就是用来获取锁。若是锁已被其余线程获取,则进行等待。

  因为在前面讲到若是采用Lock,必须主动去释放锁,而且在发生异常时,不会自动释放锁。所以通常来讲,使用Lock必须在try{}catch{}块中进行,而且将释放锁的操做放在finally块中进行,以保证锁必定被被释放,防止死锁的发生。一般使用Lock来进行同步的话,是如下面这种形式去使用的: 

Lock lock = ...;
lock.lock(); try{ //处理任务 }catch(Exception ex){ }finally{ lock.unlock(); //释放锁 }

     tryLock()方法是有返回值的,它表示用来尝试获取锁,若是获取成功,则返回true,若是获取失败(即锁已被其余线程获取),则返回false,也就说这个方法不管如何都会当即返回。在拿不到锁时不会一直在那等待。

  tryLock(long time, TimeUnit unit)方法和tryLock()方法是相似的,只不过区别在于这个方法在拿不到锁时会等待必定的时间,在时间期限以内若是还拿不到锁,就返回false。若是若是一开始拿到锁或者在等待期间内拿到了锁,则返回true。

  因此,通常状况下经过tryLock来获取锁时是这样使用的: 

Lock lock = ...;
if(lock.tryLock()) { try{ //处理任务 }catch(Exception ex){ }finally{ lock.unlock(); //释放锁 } }else { //若是不能获取锁,则直接作其余事情 }

  

   lockInterruptibly()方法比较特殊,当经过这个方法去获取锁时,若是线程正在等待获取锁,则这个线程可以响应中断,即中断线程的等待状态。也就使说,当两个线程同时经过lock.lockInterruptibly()想获取某个锁时,倘若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法可以中断线程B的等待过程。

  因为lockInterruptibly()的声明中抛出了异常,因此lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。

  所以lockInterruptibly()通常的使用形式以下: 

public void method() throws InterruptedException {
lock.lockInterruptibly();
try { 
//.....
}
finally {
lock.unlock();
} 
}

 注意,当一个线程获取了锁以后,是不会被interrupt()方法中断的。单独调用interrupt()方法不能中断正在运行过程当中的线程,只能中断阻塞过程当中的线程。

  所以当经过lockInterruptibly()方法获取某个锁时,若是不能获取到,只有进行等待的状况下,是能够响应中断的。

  而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是没法被中断的,只有一直等待下去。   

2 AQS

    AbstractQueuedSynchronizer简称AQS,是一个用于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解决了在实现同步容器时设计的大量细节问题。

    AQS使用一个FIFO的队列表示排队等待锁的线程,它维护一个status的变量,每一个节点维护一个waitstatus的变量,当线程获取到锁的时候,队列的status置为1,此线程执行完了,那么它的waitstatus为-1;队列头部的线程执行完毕以后,它会调用它的后继的线程(百度面试)。

队列头节点称做“哨兵节点”或者“哑节点”,它不与任何线程关联。其余的节点与等待线程关联,每一个节点维护一个等待状态waitStatus。如图

     AQS中还有一个表示状态的字段state,例如ReentrantLocky用它表示线程重入锁的次数,Semaphore用它表示剩余的许可数量,FutureTask用它表示任务的状态。对state变量值的更新都采用CAS操做保证更新操做的原子性。

    AbstractQueuedSynchronizer继承了AbstractOwnableSynchronizer,这个类只有一个变量:exclusiveOwnerThread,表示当前占用该锁的线程,而且提供了相应的get,set方法。

    理解AQS能够帮助咱们更好的理解JCU包中的同步容器。

3 lock()与unlock()实现原理

        ReentrantLock是Lock的默认实现之一。那么lock()和unlock()是怎么实现的呢?首先咱们要弄清楚几个概念

  • 可重入锁。可重入锁是指同一个线程能够屡次获取同一把锁。ReentrantLock和synchronized都是可重入锁。
  • 可中断锁。可中断锁是指线程尝试获取锁的过程当中,是否能够响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。
  • 公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则容许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,可是也能够设置为公平锁。
  • CAS操做(CompareAndSwap)。CAS操做简单的说就是比较并交换。CAS 操做包含三个操做数 —— 内存位置(V)、预期原值(A)和新值(B)。若是内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。不然,处理器不作任何操做。不管哪一种状况,它都会在 CAS 指令以前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;若是包含该值,则将 B 放到这个位置;不然,不要更改该位置,只告诉我这个位置如今的值便可。” Java并发包(java.util.concurrent)中大量使用了CAS操做,涉及到并发的地方都调用了sun.misc.Unsafe类方法进行CAS操做。

    ReentrantLock提供了两个构造器,分别是 

public ReentrantLock() {
    sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }

 

    默认构造器初始化为NonfairSync对象,即非公平锁,而带参数的构造器能够指定使用公平锁和非公平锁。由lock()和unlock的源码能够看到,它们只是分别调用了sync对象的lock()和release(1)方法。

    Sync是ReentrantLock的内部类,它的结构以下

 能够看到Sync扩展了AbstractQueuedSynchronizer。

3.3 NonfairSync

    咱们从源代码出发,分析非公平锁获取锁和释放锁的过程。 

3.3.1 lock() 

    lock()源码以下 

final void lock() {
    if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }

      首先用一个CAS操做,判断state是不是0(表示当前锁未被占用),若是是0则把它置为1,而且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操做只能保证一个线程操做成功,剩下的只能乖乖的去排队啦。

    “非公平”即体如今这里,若是占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了(请注意此处的非公平锁是指新来的线程跟队列头部的线程竞争锁,队列其余的线程仍是正常排队,百度面试题)。

    若当前有三个线程去竞争锁,假设线程A的CAS操做成功了,拿到了锁开开心心的返回了,那么线程B和C则设置state失败,走到了else里面。咱们往下看acquire。

acquire(arg)

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

代码很是简洁,可是背后的逻辑却很是复杂,可见Doug Lea大神的编程功力。

 1. 第一步。尝试去获取锁。若是尝试获取锁成功,方法直接返回。

tryAcquire(arg) 

final boolean nonfairTryAcquire(int acquires) {
    //获取当前线程
    final Thread current = Thread.currentThread(); //获取state变量值 int c = getState(); if (c == 0) { //没有线程占用锁 if (compareAndSetState(0, acquires)) { //占用锁成功,设置独占线程为当前线程  setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { //当前线程已经占用该锁 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); // 更新state值为新的重入次数  setState(nextc); return true; } //获取锁失败 return false; }

 

    非公平锁tryAcquire的流程是:检查state字段,若为0,表示锁未被占用,那么尝试占用,若不为0,检查当前锁是否被本身占用,若被本身占用,则更新state字段,表示重入锁的次数。若是以上两点都没有成功,则获取锁失败,返回false。

2. 第二步,入队。因为上文中提到线程A已经占用了锁,因此B和C执行tryAcquire失败,而且入等待队列。若是线程A拿着锁死死不放,那么B和C就会被挂起。

先看下入队的过程。

先看addWaiter(Node.EXCLUSIVE) 

/**
 * 将新节点和当前线程关联而且入队列
 * @param mode 独占/共享
 * @return 新节点
 */
private Node addWaiter(Node mode) {
    //初始化节点,设置关联线程和模式(独占 or 共享)
    Node node = new Node(Thread.currentThread(), mode); // 获取尾节点引用 Node pred = tail; // 尾节点不为空,说明队列已经初始化过 if (pred != null) { node.prev = pred; // 设置新节点为尾节点 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 尾节点为空,说明队列还未初始化,须要初始化head节点并入队新节点  enq(node); return node; }

 B、C线程同时尝试入队列,因为队列还没有初始化,tail==null,故至少会有一个线程会走到enq(node)。咱们假设同时走到了enq(node)里。 

/**
 * 初始化队列而且入队新节点
 */
private Node enq(final Node node) {
    //开始自旋
    for (;;) { Node t = tail; if (t == null) { // Must initialize // 若是tail为空,则新建一个head节点,而且tail指向head if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; // tail不为空,将新节点入队 if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }

 

这里体现了经典的自旋+CAS组合来实现非阻塞的原子操做。因为compareAndSetHead的实现使用了unsafe类提供的CAS操做,因此只有一个线程会建立head节点成功。假设线程B成功,以后B、C开始第二轮循环,此时tail已经不为空,两个线程都走到else里面。假设B线程compareAndSetTail成功,那么B就能够返回了,C因为入队失败还须要第三轮循环。最终全部线程均可以成功入队。

     当B、C入等待队列后,此时AQS队列以下:

3. 第三步,挂起。B和C相继执行acquireQueued(final Node node, int arg)。这个方法让已经入队的线程尝试获取锁,若失败则会被挂起。 

/**
 * 已经入队的线程尝试获取锁
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true; //标记是否成功获取锁
    try { boolean interrupted = false; //标记线程是否被中断过 for (;;) { final Node p = node.predecessor(); //获取前驱节点 //若是前驱是head,即该结点已成老二,那么便有资格去尝试获取锁 if (p == head && tryAcquire(arg)) { setHead(node); // 获取成功,将当前节点设置为head节点 p.next = null; // 原head节点出队,在某个时间点被GC回收 failed = false; //获取成功 return interrupted; //返回是否被中断过  } // 判断获取失败后是否能够挂起,若能够则挂起 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 线程若被中断,设置interrupted为true interrupted = true; } } finally { if (failed) cancelAcquire(node); } }

 

code里的注释已经很清晰的说明了acquireQueued的执行流程。假设B和C在竞争锁的过程当中A一直持有锁,那么它们的tryAcquire操做都会失败,所以会走到第2个if语句中。咱们再看下shouldParkAfterFailedAcquire和parkAndCheckInterrupt都作了哪些事吧。 

/**
 * 判断当前线程获取锁失败以后是否须要挂起.
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //前驱节点的状态
    int ws = pred.waitStatus; if (ws == Node.SIGNAL) // 前驱节点状态为signal,返回true return true; // 前驱节点状态为CANCELLED if (ws > 0) { // 从队尾向前寻找第一个状态不为CANCELLED的节点 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { // 将前驱节点的状态设置为SIGNAL  compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } /** * 挂起当前线程,返回线程中断状态并重置 */ private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }

 

    线程入队后可以挂起的前提是,它的前驱节点的状态为SIGNAL,它的含义是“Hi,前面的兄弟,若是你获取锁而且出队后,记得把我唤醒!”。因此shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,若符合则返回true,而后调用parkAndCheckInterrupt,将本身挂起。若是不符合,再看前驱节点是否>0(CANCELLED),如果那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱节点的状态设置为SIGNAL。

也就是说当队列头部的线程执行完了以后,这个线程会调用后面的队列的第一个线程(百度面试)。

     整个流程中,若是前驱结点的状态不是SIGNAL,那么本身就不能安心挂起,须要去找个安心的挂起点,同时能够再尝试下看有没有机会去尝试竞争锁。

    最终队列可能会以下图所示

  线程B和C都已经入队,而且都被挂起。当线程A释放锁的时候,就会去唤醒线程B去获取锁啦。

3.3.2 unlock()

unlock相对于lock就简单不少。源码以下 

public void unlock() {
    sync.release(1); } public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }

 

若是理解了加锁的过程,那么解锁看起来就容易多了。流程大体为先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,若是是则唤醒头结点的下个节点关联的线程,若是释放失败那么返回false表示解锁失败。这里咱们也发现了,每次都只唤起头结点的下一个节点关联的线程

   最后咱们再看下tryRelease的执行过程 

/**
 * 释放当前线程占用的锁
 * @param releases
 * @return 是否释放成功
 */
protected final boolean tryRelease(int releases) {
    // 计算释放后state值
    int c = getState() - releases; // 若是不是当前线程占用锁,那么抛出异常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { // 锁被重入次数为0,表示释放成功 free = true; // 清空独占线程 setExclusiveOwnerThread(null); } // 更新state值  setState(c); return free; }

 

这里入参为1。tryRelease的过程为:当前释放锁的线程若不持有锁,则抛出异常。若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,而且则清空独占线程,最后更新state值,返回free。 

3.3.3 小结

    用一张流程图总结一下非公平锁的获取锁的过程。    

 
 

3.4 FairSync

    公平锁和非公平锁不一样之处在于,公平锁在获取锁的时候,不会先去检查state状态,而是直接执行aqcuire(1),这里再也不赘述。    

4 超时机制

    在ReetrantLock的tryLock(long timeout, TimeUnit unit) 提供了超时获取锁的功能。它的语义是在指定的时间内若是获取到锁就返回true,获取不到则返回false。这种机制避免了线程无限期的等待锁释放。那么超时的功能是怎么实现的呢?咱们仍是用非公平锁为例来一探究竟。

public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

 仍是调用了内部类里面的方法。咱们继续向前探究  

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); }

 

这里的语义是:若是线程被中断了,那么直接抛出InterruptedException。若是未中断,先尝试获取锁,获取成功就直接返回,获取失败则进入doAcquireNanos。tryAcquire咱们已经看过,这里重点看一下doAcquireNanos作了什么。 

/**
 * 在有限的时间内去竞争锁
 * @return 是否获取成功
 */
private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException { // 起始时间 long lastTime = System.nanoTime(); // 线程入队 final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { // 又是自旋! for (;;) { // 获取前驱节点 final Node p = node.predecessor(); // 若是前驱是头节点而且占用锁成功,则将当前节点变成头结点 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return true; } // 若是已经超时,返回false if (nanosTimeout <= 0) return false; // 超时时间未到,且须要挂起 if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) // 阻塞当前线程直到超时时间到期 LockSupport.parkNanos(this, nanosTimeout); long now = System.nanoTime(); // 更新nanosTimeout nanosTimeout -= now - lastTime; lastTime = now; if (Thread.interrupted()) //相应中断 throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }

 

doAcquireNanos的流程简述为:线程先入等待队列,而后开始自旋,尝试获取锁,获取成功就返回,失败则在队列里找一个安全点把本身挂起直到超时时间过时。这里为何还须要循环呢?由于当前线程节点的前驱状态可能不是SIGNAL,那么在当前这一轮循环中线程不会被挂起,而后更新超时时间,开始新一轮的尝试 

三、读写锁ReentrantReadWriteLock

接口 ReadWriteLock,有个实现类是ReentrantReadWriteLock

读读互不干扰,写写互斥,若是有读也有写,那么写线程要优先读线程

对!读取线程不该该互斥!

咱们能够用读写锁ReadWriteLock实现:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; 
class Data {        
    private int data;// 共享数据    
    private ReadWriteLock rwl = new ReentrantReadWriteLock();       
    public void set(int data) {    
        rwl.writeLock().lock();// 取到写锁    
        try {    
            System.out.println(Thread.currentThread().getName() + "准备写入数据");    
            try {    
                Thread.sleep(20);    
            } catch (InterruptedException e) {    
                e.printStackTrace();    
            }    
            this.data = data;    
            System.out.println(Thread.currentThread().getName() + "写入" + this.data);    
        } finally {    
            rwl.writeLock().unlock();// 释放写锁    
        }    
    }       
  
    public void get() {    
        rwl.readLock().lock();// 取到读锁    
        try {    
            System.out.println(Thread.currentThread().getName() + "准备读取数据");    
            try {    
                Thread.sleep(20);    
            } catch (InterruptedException e) {    
                e.printStackTrace();    
            }    
            System.out.println(Thread.currentThread().getName() + "读取" + this.data);    
        } finally {    
            rwl.readLock().unlock();// 释放读锁    
        }    
    }    
}  

 

与互斥锁定相比,读-写锁定容许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)能够修改共享数据,但在许多状况下,任何数量的线程能够同时读取共享数据(reader 线程) 

从理论上讲,与互斥锁定相比,使用读-写锁定所容许的并发性加强将带来更大的性能提升。

在实践中,只有在多处理器上而且只在访问模式适用于共享数据时,才能彻底实现并发性加强。——例如,某个最初用数据填充而且以后不常常对其进行修改的 collection,由于常常对其进行搜索(好比搜索某种目录),因此这样的 collection 是使用读-写锁定的理想候选者。 

四、线程间通讯Condition

Condition能够替代传统的线程间通讯,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll()。

——为何方法名不直接叫wait()/notify()/nofityAll()?由于Object的这几个方法是final的,不可重写!

 

传统线程的通讯方式,Condition均可以实现。

注意,Condition是被绑定到Lock上的,要建立一个Lock的Condition必须用newCondition()方法。 

Condition的强大之处在于它能够为多个线程间创建不一样的Condition

看JDK文档中的一个例子:假定有一个绑定的缓冲区,它支持 put 和 take 方法。若是试图在空的缓冲区上执行take 操做,则在某一个项变得可用以前,线程将一直阻塞;若是试图在满的缓冲区上执行 put 操做,则在有空间变得可用以前,线程将一直阻塞。咱们喜欢在单独的等待 set 中保存put 线程和take 线程,这样就能够在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可使用两个Condition 实例来作到这一点。

——其实就是java.util.concurrent.ArrayBlockingQueue的功能

优势:
假设缓存队列中已经存满,那么阻塞的确定是写线程,唤醒的确定是读线程,相反,阻塞的确定是读线程,唤醒的确定是写线程。 

 

若是想查看 线程5个状态 请参考:Java线程的5种状态及切换(透彻讲解)-京东面试

如下是补充的知识点:

一、线程与进程:

在开始以前先把进程与线程进行区分一下,一个程序最少须要一个进程,而一个进程最少须要一个线程。

线程是程序执行流的最小单位,而进程是系统进行资源分配和调度的一个独立单位。  

 

2.java.util.concurrent.locks包经常使用类  

2.2 ReentrantLock
  ReentrantLock,意思是“可重入锁”,ReentrantLock是惟一实现了Lock接口的类,而且ReentrantLock提供了更多的方法。
详见:java.util.concurrent.locks.ReentrantLock ,再也不列举了。

2.3 ReadWriteLock
接口,只定义了两个方法:

Lock readLock();
Lock writeLock();
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操做分开,分红2个锁来分配给线程,从而使得多个线程能够同时进行读操做。

2.4 ReentrantReadWriteLock
实现了ReadWriteLock接口。

下面尝试写个例子,表示ReadWriteLock和使用synchronized的区别。 

 
1.若是有一个线程已经占用了读锁,则此时其余线程若是要申请写锁,则申请写锁的线程会一直等待释放读锁。

2.若是有一个线程已经占用了写锁,则此时其余线程若是申请写锁或者读锁,则申请的线程会一直等待释放写锁。 

 


参考:  lock与synchronized区别详解

参考:  Synchronized与Lock的区别与应用场景

参考:lock和synchronized的同步区别与选择 

参考:ReentrantLock实现原理