Java 多线程 重入锁

  作为关键字synchronized的替代品(或者说是增强版),重入锁是synchronized的功能扩展。在JDK 1.5的早期版本中,重入锁的性能远远好于synchronized,但从JDK 1.6开始,JDK优化了synchronized,使两者性能差距不大。重入锁使用java.util.concurrent.locks.ReentrantLock类来实现。

  使用重入锁可以指定何时加锁和何时释放锁,对逻辑控制的灵活性远远好于synchronized,退出临界区时必须释放锁。之所以称为重入锁,是因为一个线程可以连续获得这种锁。而synchronized隐式地支持重进入。具体而言,重进入指线程在获取锁之后能再次获取该锁而不会被锁阻塞,该特性的实现需要解决两个问题:

  1 线程再次获取锁

  锁需要识别获取锁的线程是否为当前占有锁的线程,如果是,则再次成功获取。

  2 锁的最终释放

  线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求获取锁需要计数自增,计数表示锁被重复获取的次数,而锁被释放时计数自减,当计数为0时表示锁已经最终成功释放。

  ReentrantLock通过组合自定义同步器实现锁的获取与释放,以非公平性(默认)实现为例,获取同步状态代码如下:

 1 /**
 2  * Performs non-fair tryLock.  tryAcquire is implemented in
 3  * subclasses, but both need nonfair try for trylock method.
 4  */
 5 final boolean nonfairTryAcquire(int acquires) {
 6     final Thread current = Thread.currentThread();
 7     int c = getState();
 8     if (c == 0) {
 9         if (compareAndSetState(0, acquires)) {
10             setExclusiveOwnerThread(current);
11             return true;
12         }
13     }
14     else if (current == getExclusiveOwnerThread()) {
15         int nextc = c + acquires;
16         if (nextc < 0) // overflow
17             throw new Error("Maximum lock count exceeded");
18         setState(nextc);
19         return true;
20     }
21     return false;
22 }

  再次获取同步方法的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则增加同步状态值并返回true,表示获取同步状态成功。也就是说,成功获取锁的线程再次获取锁,只是增加了同步状态值,这也要求ReentrantLock在释放同步状态时减少同步状态值,释放同步状态代码如下:

 1 protected final boolean tryRelease(int releases) {
 2     int c = getState() - releases;
 3     if (Thread.currentThread() != getExclusiveOwnerThread())
 4         throw new IllegalMonitorStateException();
 5     boolean free = false;
 6     if (c == 0) {
 7         free = true;
 8         setExclusiveOwnerThread(null);
 9     }
10     setState(c);
11     return free;
12 }

  如果锁被获取了n次,那么前n-1次tryRelease(int releases)方法必须返回false。也就是说,只有同步状态完全释放了,该方法才能返回true。该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。

  简单的重入锁使用例子如下:

  

  修改上述代码第7-12行:

  

  在这种情况下,一个线程连续两次获得同一把锁。如果不允许该操作,那么同一个线程在第2次请求获得锁时,会和自己产生死锁,程序卡死在第2次申请锁的过程中。如果同一个线程多次获得锁,那么释放锁的次数与其相同。如果释放锁的次数多,那么会有java.lang.IllegalMonitorStateException异常;反之,如果释放锁的次数少了,那么该线程依然持有这个锁。

  除了使用上的灵活性之外,重入锁还提供了一些高级功能:

  1 中断响应

  对于synchronized,如果一个线程在等待锁,那么结果只有两种可能:要么获得这把锁继续执行,要么保持等待。而使用重入锁可以提供另外一种可能:线程可以被中断。也就是说,线程在等待锁的过程中,程序可以根据需要取消对锁的请求。例如,小王与小明一起去打球,如果小王等了10分钟,小明还没到,接到小明的电话了解到由于突发情况不能如约了,那么小王就扫兴地打道回府了。对于线程而言,如果一个线程正在等待锁,那么它可以收到一个通知,被告知无需再等待,可以停止工作了。这种情况对于处理死锁是有一定帮助的。

  2 锁申请等待限时

  除了等待外部通知之外,避免死锁还有另外一种方法:限时等待。继续上面的例子,如果小王等了半个小时,小明还不来,那么小王就扫兴离去了。对于线程而言,如果给定一个等待时间,让线程自动放弃,那么可以避免线程持续等待。

  3 公平锁

  在大多数情况下,锁的申请都是非公平的。只要CAS设置同步状态成功,则表示当前线程获得了锁。也就是说,线程1首先申请了锁A,接着线程2也申请了锁A,那么当锁A可用时,哪个线程获得锁是不一定的,系统会从锁A的等待队列中随机选择一个,不能保证公平性。如同买票不排队,大家乱哄哄地围在售票窗口前,售票员忙得焦头烂额,顾不得谁先谁后,随便找个人出票就完事了。而公平锁保证锁的获取遵守FIFO原则即先到先得,不会产生饥饿现象,代价是大量的线程切换。synchronized产生的锁是非公平的,而重入锁可以设置是否公平。公平的重入锁要求系统维护一个有序队列,实现成本比较高,性能较低。所以,默认情况下的锁是非公平的。

 

  ReentrantLock类常见方法如下:

  1 public ReentrantLock(boolean fair)

  当参数fair是true时,表示该重入锁是公平的。

  2 lock()

  获取锁,如果锁已被占用,则等待。

  3 lockInterruptibly()

  获取锁,但优先响应中断。

  4 tryLock()

  尝试获取锁,如果成功,返回true;否则,返回false。该方法不等待,立即返回。

  5 tryLock(long time, TimeUnit unit)

  在给定时间内尝试获取锁。

  6 unlock()

  释放锁。

 

  重入锁的实现包含3个要素:

  1 原子状态

  原子状态使用CAS操作来存储当前锁的状态,判断锁是否已被其他线程持有。

  2 等待队列

  所有没有获取到锁的线程会进入等待队列进行等待。当有线程释放锁后,系统就从等待队列中唤醒一个线程,继续工作。

  3 阻塞原语park()和unpark()

  挂起和恢复线程,没有获取到锁的线程会被挂起。

  

  参考资料

  《实战Java高并发程序设计》 P71-80

  《Java并发编程的艺术》 5.3 重入锁

原文地址:https://www.cnblogs.com/WJQ2017/p/8276326.html