JAVA ReentrantLock 分析

简介:

并发编程中经常用到的莫非是这个ReentrantLock这个类,线程获取锁和释放锁。还有一个则是synchronized,常用来多线程控制获取锁机制。


先写一个简单的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package  com.multi.thread;
 
import  java.util.concurrent.locks.Lock;
import  java.util.concurrent.locks.ReentrantLock;
 
public  class  AQSDemo {
 
     public  static  void  main(String[] args) {
         Lock lock =  new  ReentrantLock( true );
 
         MyThread t1 =  new  MyThread( "t1" , lock);
         MyThread t2 =  new  MyThread( "t2" , lock);
         MyThread t3 =  new  MyThread( "t3" , lock);
         t1.start();
         t2.start();
         t3.start();
     }
     
 
}
 
class  MyThread  extends  Thread {
 
     private  Lock lock;
 
     public  MyThread(String name, Lock lock) {
         super (name);
         this .lock = lock;
     }
 
     @Override
     public  void  run() {
         lock.lock();
         try  {
             System.out.println(Thread.currentThread() +  "  is running " );
             try  {
                 Thread.sleep( 500 );
             catch  (InterruptedException e) {
                 e.printStackTrace();
             }
         finally  {
             lock.unlock();
         }
     }
 
}


这是个简单的用ReentrantLock的代码。


知识点理解:

ReentranctLock:

1) 可重入性:大致意思就是如果一个函数能被安全的重复执行,那么这个函数是可重复的。听起来很绕口。

2)可重入锁:一个线程可以重复的获取它已经拥有的锁。


特性:

1)ReentrantLock可以在不同的方法中使用。

2)支持公平锁和非公平锁概念

    static final class NonfairSync extends Sync;(非公平锁)

    static final class FairSync extends Sync;(公平锁)

3)支持中断锁,收到中断信号可以释放其拥有的锁。

4)支持超时获取锁:tryLock方法是尝试获取锁,支持获取锁的是带上时间限制,等待一定时间就会返回。


ReentrantLock就先简单说一下AQS(AbstractQueuedSynchronizer)。java.util.concurrent包下很多类都是基于AQS作为基础开发的,Condition,BlockingQueue以及线程池使用的worker都是基于起实现的,其实就是将负杂的繁琐的并发过程封装起来,以便其他的开发工具更容易的开发。其主要通过volatile和Unsafe类的原子操作,来实现阻塞和同步。


AQS是一个抽象类,其他类主要通过重载其tryAcquire(int arg)来获取锁,和tryRelease来释放锁。

AQS不在这里做分析,会有单独的一篇文章来学习AQS。


ReentrantLock类里面主要有三个类,Sync,NonfairSync,FairSync这三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。


Sync是ReentrantLock实现公平锁和非公平锁的主要实现,默认情况下ReentrantLock是非公平锁。

    Lock lock = new ReentrantLock(true);   :true则是公平锁,false就是非公平锁,什么都不传也是非公平锁默认的。


非公平锁:

    lock.lock();点进去代码会进入到,ReentranctLock内部类Sync。

    

1
2
3
4
5
6
7
8
9
10
11
     abstract  static  class  Sync  extends  AbstractQueuedSynchronizer {
         private  static  final  long  serialVersionUID = -5179523762034025860L;
 
         /**
          * Performs {@link Lock#lock}. The main reason for subclassing
          * is to allow fast path for nonfair version.
          */
         abstract  void  lock();
         
         ......省略。
       }

这个抽象类Sync的里有一个抽象方法,lock(),供给NonfairSync,FairSync这两个实现类来实现的。这个是一个模板方法设计模式,具体的逻辑供给子类来实现。

非公平锁的lock的方法,虽然都可以自己看,但是还是粘贴出来,说一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  static  final  class  NonfairSync  extends  Sync {
         private  static  final  long  serialVersionUID = 7316153563782823691L;
 
         /**
          * Performs lock.  Try immediate barge, backing up to normal
          * acquire on failure.
          */
         final  void  lock() {
             if  (compareAndSetState( 0 1 ))
                 setExclusiveOwnerThread(Thread.currentThread());
             else
                 acquire( 1 );
         }
         ......省略
     }

其实重点看这个compareAndSetState(0,1),这个其实一个原子操作,是cas操作来获取线程的资源的。其代表的是如果原来的值是0就将其设为1,并且返回true。其实这段代码就是设置private volatile int state;,这个状态的。

其实现原理就是通过Unsafe直接得到state的内存地址然后直接操作内存的。设置成功,就说明已经获取到了锁,如果失败的,则会进入:

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

这个方法里,这个过程是先去判断锁的状态是否为可用,如果锁已被持有,则再判断持有锁的线程是否未当前线程,如果是则将锁的持有递增,这也是java层实现可重入性的原理。如果再次失败,则进入等待队列。就是要进去等待队列了AQS有一个内部类,是Node就是用来存放获取锁的线程信息。


AQS的线程阻塞队列是一个双向队列,提供了FiFO的特性,Head节点表示头部,tail表示尾部。

1)节点node,维护一个volatile状态,维护一个prev指向向前一个队列节点,根据前一个节点的状态来判断是否获取锁。

2)当前线程释放的时候,只需要修改自身的状态即可,后续节点会观察到这个volatile状态而改变获取锁。volatile是放在内存中的,共享的,所以前一个节点改变状态后,后续节点会看到这个状态信息。


获取锁失败后就会加入到队列里,但是有一点,不公平锁就是,每个新来的线程来获取所得时候,不是直接放入到队列尾部,而是也去cas修改state状态,看看是否获取锁成功。


总结非公平锁:

首先会尝试改变AQS的状态,改变成功了就获取锁,否则失败后再次通过判断当前的state的状态是否为0,如果为0,就再次尝试获取锁。如果state不为0,该锁已经被其他线程持有了,但是其它线程也可能也是自己啊,所以也要判断一下是否是自己获取线程,如果是则是获取成功,且锁的次数要加1,这是可重入锁,不是则加入到node阻塞队列里。加入到队列后则在for循环中通过判断当前线程状态来决定是否哟啊阻塞。可以看出在加入队列前及阻塞前多次尝试去获取锁,而避免进入线程阻塞,这是因为阻塞、唤醒都需要cpu的调度,以及上下文切换,这是个重量级的操作,应尽量避免


公平锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
FairSync类:
final  void  lock() {
    //先去判断锁的状态,而不是直接去获取
     acquire( 1 );
}
AQS类:
public  final  void  acquire( int  arg) {
     if  (!tryAcquire(arg) &&
         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
         selfInterrupt();
}
FairSync类:
protected  final  boolean  tryAcquire( int  acquires) {
     final  Thread current = Thread.currentThread();
     int  c = getState();
     if  (c ==  0 ) {
     
     //hasQueuedPredecessors判断是否有前节点,如果有就不会尝试去获取锁
         if  (!hasQueuedPredecessors() &&
             compareAndSetState( 0 , acquires)) {
             setExclusiveOwnerThread(current);
             return  true ;
         }
     }
     else  if  (current == getExclusiveOwnerThread()) {
         int  nextc = c + acquires;
         if  (nextc <  0 )
             throw  new  Error( "Maximum lock count exceeded" );
         setState(nextc);
         return  true ;
     }
     return  false ;
}


公平锁,主要区别是:什么事都要有个先来后到,先来的有先。获取锁的时候是先看锁是否可用并且是否有节点,就是是否有阻塞队列。有的话,就是直接放入到队列尾部,而不是获取锁。


释放锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 ;
}
protected  final  boolean  tryRelease( int  releases) {
     int  c = getState() - releases;
     if  (Thread.currentThread() != getExclusiveOwnerThread())
         throw  new  IllegalMonitorStateException();
     boolean  free =  false ;
     if  (c ==  0 ) {
         free =  true ;
         setExclusiveOwnerThread( null );
     }
     setState(c);
     return  free;
}


释放锁是很简单的,就是先去改变state这个状态的值,改变后如果状态为0,则说明释放成功了,如果直接可重入了多次,也要释放很多次的锁。


释放过程:

Head节点就是当前持有锁的线程节点,当释放锁时,从头结点的next来看,头结点的下一个节点如果不为null,且waitStatus不大于0,则跳过判断,否则从队尾向前找到最前的一个waitStatus的节点,然后通过LockSupport.unpark(s.thread)唤醒该节点线程。可以看出ReentrantLock的非公平锁只是在获取锁的时候是非公平的,如果进入到等待队列后,在head节点的线程unlock()时,会按照进入的顺序来得到唤醒,保证了队列的FIFO的特性。


参考文章:

http://silencedut.com/2017/01/09/%E7%94%B1ReentrantLock%E5%88%86%E6%9E%90JUC%E7%9A%84%E6%A0%B8%E5%BF%83AQS/


http://www.cnblogs.com/leesf456/p/5383609.html


https://github.com/pzxwhc/MineKnowContainer/issues/16


http://www.importnew.com/24006.html



本文转自 豆芽菜橙 51CTO博客,原文链接:http://blog.51cto.com/shangdc/1930644


相关文章
|
1月前
|
算法 Java
java面向对象和面向过程分析
java面向对象和面向过程分析
36 0
|
1月前
|
存储 Java 编译器
java和c++的主要区别、各自的优缺点分析、java跨平台的原理的深度解析
java和c++的主要区别、各自的优缺点分析、java跨平台的原理的深度解析
69 0
|
7天前
|
Java 调度
Java中常见锁的分类及概念分析
Java中常见锁的分类及概念分析
13 0
|
7天前
|
Java
Java中ReentrantLock中tryLock()方法加锁分析
Java中ReentrantLock中tryLock()方法加锁分析
8 0
|
24天前
|
人工智能 监控 算法
java智慧城管源码 AI视频智能分析 可直接上项目
Java智慧城管源码实现AI视频智能分析,适用于直接部署项目。系统运用互联网、大数据、云计算和AI提升城市管理水平,采用“一级监督、二级指挥、四级联动”模式。功能涵盖AI智能检测(如占道广告、垃圾处理等)、执法办案、视频分析、统计分析及队伍管理等多个模块,利用深度学习优化城市管理自动化和智能化,提供决策支持。
142 4
java智慧城管源码 AI视频智能分析 可直接上项目
|
2月前
|
算法 前端开发 JavaScript
【JAVA】JDK11新特性个人分析
【JAVA】JDK11新特性个人分析
53 0
|
3月前
|
存储 缓存 Java
深入理解Java中的逃逸分析
深入理解Java中的逃逸分析
45 0
|
7天前
|
Java
Java中关于ConditionObject的分析
Java中关于ConditionObject的分析
16 3
|
10天前
|
设计模式 缓存 安全
分析设计模式对Java应用性能的影响,并提供优化策略
【4月更文挑战第7天】本文分析了7种常见设计模式对Java应用性能的影响及优化策略:单例模式可采用双重检查锁定、枚举实现或对象池优化;工厂方法和抽象工厂模式可通过对象池和缓存减少对象创建开销;建造者模式应减少构建步骤,简化复杂对象;原型模式优化克隆方法或使用序列化提高复制效率;适配器模式尽量减少使用,或合并多个适配器;观察者模式限制观察者数量并使用异步通知。设计模式需根据应用场景谨慎选用,兼顾代码质量和性能。
|
1月前
|
缓存 Java
java使用MAT进行内存分析
java使用MAT进行内存分析