日常说到高并发往往针对共享资源进行读写操作很容易得到错误的结果,这个时候就需要应用到各种个样的锁,本文通过4种锁进行分享:
- synchronized
- ReentrantLock(可重入锁)
- ReentrantReadWriteLock(读写锁)
- StampedLock(戳锁)
Part-1:synchronized
- 同步代码块
public void testSynchronizedCode() {
synchronized (lockObject) {
System.out.println("同步代码块");
}
}
运行:javap -verbose testSynchronized.class
public void testSynchronizedCode();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #3 // Field lockObject:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
10: ldc #9 // String 同步代码块
12: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: aload_1
16: monitorexit
17: goto 25
20: astore_2
21: aload_1
22: monitorexit
23: aload_2
24: athrow
25: return
Exception table:
from to target type
7 17 20 any
20 23 20 any
进入代码块之前先通过monitorenter获取monitor锁,如果代码块执行过程中没有异常通过monitorexit释放monitor锁,
异常则通过monitorexit锁释放monitor锁
- 同步方法块
public synchronized void testSynchronizedMethod() {
System.out.println("同步方法");
}
运行:javap -verbose testSynchronized.class
public synchronized void testSynchronizedMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #7 // String 同步方法
5: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 18: 0
line 19: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Llsf/study/testSynchronized;
Part-2:ReentrantLock可重入锁
// 参数fair 代表是否公平锁,默认为false
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
//重入锁配合Condition进行使用实现等待功能
private final ReentrantLock reentrantLock = new ReentrantLock(true);
private final Condition addCondition = reentrantLock.newCondition();
private final Condition subCondition = reentrantLock.newCondition();
private int count = 0;
public void add() {
reentrantLock.lock();
try {
while (count >= 10) {
System.out.println("加法-等待区");
addCondition.await();
}
count++;
System.out.println("加---结果:" + count);
subCondition.signal();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
public void sub() {
reentrantLock.lock();
try {
while (count <= 0) {
System.out.println("减法-等待区");
subCondition.await();
}
count--;
System.out.println("减---结果:" + count);
addCondition.signal();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
Part-3:ReentrantReadWriteLock可重入读写锁
- 锁升级:ReentrantReadWriteLock中不支持
ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock();
reentrantReadWriteLock.readLock().lock();
System.out.println("读锁获取完毕");
reentrantReadWriteLock.writeLock().lock();
System.out.println("写锁获取完毕");
运行结果:
读锁获取完毕
- 锁降级:释放锁的顺序可以调整
ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock();
reentrantReadWriteLock.writeLock().lock();
System.out.println("写锁获取完毕");
reentrantReadWriteLock.readLock().lock();
System.out.println("读锁获取完毕");
reentrantReadWriteLock.writeLock().unlock();
System.out.println("写锁释放完毕");
reentrantReadWriteLock.readLock().unlock();
System.out.println("读锁释放完毕");
reentrantReadWriteLock.writeLock().lock();
System.out.println("写锁获取完毕");
运行结果:
写锁获取完毕
读锁获取完毕
写锁释放完毕
读锁释放完毕
写锁获取完毕
总结如下:读读共享、写读互斥,读写互斥、写写互斥
Part-4:StampedLock戳锁
StampedLock和ReadWriteLock相比,改进读的过程中也允许获取写锁后写入!我们读的数据就可能不一致,
需要额外代码判断读的过程中是否有写入,这种读锁是一种乐观锁。
通过StampedLocked类提供样例代码进行分析
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) { // an exclusively locked method
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
double distanceFromOrigin() { // A read-only method
long stamp = sl.tryOptimisticRead();// 尝试获取乐观锁
double currentX = x, currentY = y;
if (!sl.validate(stamp)) { //判断读的过程中是否有写入,如果没有则没有锁
stamp = sl.readLock(); // 获取读锁
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) {
long ws = sl.tryConvertToWriteLock(stamp);//尝试转化为写锁
if (ws != 0L) {
// 升级成功,更新锁戳标,退出循环
stamp = ws;
x = newX;
y = newY;
break;
} else {
// 读锁升级写锁失败,显示获取独占锁,循环重试
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}
}
ReentrantReadWriteLock其他线程尝试获取写锁的时候,会被阻塞,
StampedLock在乐观获取锁后,其他线程尝试获取写锁,也不会被阻塞,这其实是对读锁的优化,
在获取乐观读锁后,还需要对结果进行校验。
Part-5:总结
- Synchronized:在日常使用种无需关注锁的释放,并且是原生内容后期优化空间很大,一般开发种也最常用
- ReentrantLock:锁的细粒度和灵活度,可以指定锁分配策略(公平/非公平)
- ReentrantReadWriteLock:在读多写少的场景比较合适
- StampedLock:Jdk1.8版本新增功能,性能优于ReentrantReadWriteLock
参考资料
- https://blog.takipi.com/java-8-stampedlocks-vs-readwritelocks-and-synchronized/
- https://www.liaoxuefeng.com/wiki/1252599548343744/1309138673991714
- JDK源码