显式锁 StampedLock
悲观锁:
假定会发生并发冲突,屏蔽一切可能违反数据完整性的错误。
读取被关锁:
在读取之前一定要判断一下,数据又没有正在被修改。
乐观锁:
假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
读取乐观锁:
在读取之前就不需要来判断数据的一致性,我只管读我自己的就可以了。
StampedLock 是基于能力的锁,可以很好的实现悲观锁和乐观锁的逻辑。
使用三种模式来控制读/写访问:
(1)写:
writeLock 可能阻塞等待独占访问,返回一个标志,可用在方法unlockWrite以释放锁。也提供了无时间和带时间版本的tryWriteLock方法。
当锁以写模式持有时,没有读锁可以获取,所有乐观性读确认将失败。
(2)读:
readLock 可能为非独占访问而阻塞等待,返回一个标志用于unlockRead以释放锁。也提供了无时间和带时间版本的tryReadLock方法。
(3)乐观读:
只有在锁当前没有以写模式持有时,tryOptimisticRead返回一个非0标志。
如果锁自给定标志以来没有以写模式持有,方法validate 返回true;
这种模式可以认为是一种极弱版本的读锁。可以在任意时间被写者打破。
在短的只读代码段使用乐观模式常常可以减少竞争并提供吞吐量。
乐观读片段section应该只读字段并持有本地变量,用于以后使用,在确认以后,乐观读模式里的字段读取可能很不一致,所以惯例只用于当你对数据表示足够熟悉,可以检查一致性,重复调用validate()方法。
e.g. 这些步骤典型的在第一次读取对象或数组引用,然后访问其中字段、元素或方法时要求。
StampedLock利用了Lock的机制。再加上stamp作为锁的标志状态,实现了锁与锁之间的悲观和乐观。
StampedLock设计用作开发线程安全组建的内部工具。
StampedLock是可序列化的,但总是反序列化为初始未锁定状态,所以对于远程锁定没有帮助。
StampedLock的调度策略不是一惯地倾向于选择读而不是选择写,或相反。所有“try”方法都是尽最大努力的,不必向任何调度或公平策略确认。
任何用于获取的“try”方法或不带任何状态的信息的转换锁模式的方法返回0,后续的调用可能成功。
Java doc:
1 class Point { 2 3 private double x, y; 4 private final StampedLock sl = new StampedLock(); 5 6 // 独占锁的方法 7 void move(double deltaX, double deltaY) { 8 9 long stamp = sl.writeLock(); 10 11 try { 12 13 x += deltaX; 14 y += deltaY; 15 16 } finally { 17 sl.unlockWrite(stamp); 18 } 19 } 20 21 // 乐观读锁 22 double distanceFromOrigin() { 23 24 long stamp = sl.tryOptimisticRead(); // 获得一个乐观读锁 25 double currentX = x, currentY = y; // 读入本地变量。 26 27 if (!sl.validate(stamp)) { // 检查发生乐观读锁后同时是否又其他写锁发生? 28 29 stamp = sl.readLock(); // 如果没有, 我们再次获得一个读悲观锁 30 31 try { 32 // 读入本地变量 33 currentX = x; 34 currentY = y; 35 36 } finally { 37 sl.unlockRead(stamp); 38 } 39 } 40 41 return Math.sqrt(currentX * currentX + currentY * currentY); 42 } 43 44 // 悲观读锁 45 void moveIfAtOrigin(double newX, double newY) { 46 47 long stamp = sl.readLock(); 48 49 try { 50 51 while (x == 0.0 && y == 0.0) { 52 53 long ws = sl.tryConvertToWriteLock(stamp); // 将读锁转换 54 55 if (ws != 0L) { // 确认转为写锁是否成功 56 57 stamp = ws; // 成功, 替换票据 58 x = newX; 59 y = newY; 60 break; 61 } else { 62 // 如果不能成功转换为写锁 63 sl.unlockRead(stamp); // 释放读锁 64 stamp = sl.writeLock(); // 显式直接进行写锁, 再通过while循环再试。 65 } 66 } 67 68 } finally { 69 sl.unlock(stamp); // 释放读锁或者写锁 70 } 71 } 72 }
StampedLock 是JDK1.8 之后新推出的一个API,可大幅度提高程序的读取锁的吞吐量。在大多数都是读取,很少写入的情况下,乐观读锁模式可以极大提供吞吐量,也可以减少这种情况下写饥饿的现象。
StampedLock 在Lock的基础上,实现了可以满足乐观锁和悲观锁等一些在读线程越来越多的业务场景,对吞吐量又巨大的改进。但对于其加锁操作,很容易误用其他方法,如果理解不深入,也更容易出现死锁,和不必要的麻烦。
死锁:
在两段不同的逻辑都在等待对方的锁释放才能继续往下工作时,这个时候就会产生死锁,表面现象就是程序再也执行不下去。
1 public class Count { 2 3 private byte[] lock1 = new byte[1]; 4 private byte[] lock2 = new byte[1]; 5 6 public int number = 0; 7 8 public void add() { 9 10 synchronized (lock1) { 11 12 try { 13 14 Thread.sleep(1001); // mock user do something 15 16 } catch (InterruptedException e) { 17 e.printStackTrace(); 18 } 19 20 synchronized (lock2) { 21 22 number += 1; 23 } 24 25 System.out.printf(Thread.currentThread().getName() + "-" + number); 26 } 27 } 28 29 public void remove() { 30 31 synchronized (lock2) { 32 33 try { 34 35 Thread.sleep(1001); // mock user do something 36 37 } catch (InterruptedException e) { 38 e.printStackTrace(); 39 } 40 41 synchronized (lock1) { 42 43 number -= 1; 44 } 45 46 System.out.printf(Thread.currentThread().getName() + "-" + number); 47 } 48 } 49 }
然后做两个线程:线程A, 线程B,一个调用add方法, 一个调用remove方法,通过工具来监控,线程A, 线程B永远停在这里,永远不执行了。
更多线程安全链接: