线程安全
1.0 Synchronized 和 ReentrantLock的区别
- Synchronized是Java内建的同步机制,所以也有人称其为Intrinsic Lock,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。
在Java5之前,Synchronized是仅有的同步手段,在代码中Synchronized可以用来修饰方法,代码块等,本质上,Synchronized方法等同于吧方法全部语句用Synchronized块包起来。 - ReentrantLock,通常翻译为再入锁,是Java5提供的锁实现,它的语义和Synchronized基本相同。再入锁通过代码直接调用lock()方法进行获取,代码书写更加灵活。与此同时ReentrantLock提供了很多实用的方法,能够实现很多Synchronized无法做到的细节控制,比如可以控制fairness,也就是公平性,或利用定义条件等。但是编码中也需要注意,必须要明确调用unlock方法释放,不然就会一直持有该锁。
- Synchronized和ReentrantLock的性能不能一概而论,早期版本的Synchronized在很多场景下性能差异较大,在后续版本进行了较多改进,在底竞争的场景下性能表现可能优于ReentrantLock
2.0 需要掌握的知识点
- 理解什么是线程安全
- synchronized 和 ReentrantLock 的基本使用与案例
- 掌握 Synchronized 和 ReentrantLock的底层实现;理解锁膨胀,降级;理解偏向锁、自旋锁、轻量级锁、重量级锁等概念
- 掌握并发包中java.util.concurrent.lock 的各种不同实现和案例分析。
3.0 什么是线程安全
线程安全是一个多线程环境下正确性的概念,也就是保证多线程环境下共享的、可修改的状态的正确性,这里的状态反应在程序中可以看做是数据。
换个角度,如果线程不是共享的,或者是不可修改的,也就不存在线程安全问题,进而可以推断出保证线程安全的两个方法:
- 封装:通过封装我们可以将对象内部状态隐藏、保护起来。
- 不可变:final 和 Immutable就是这个道理,但是JAVA语言目前还没有真正意义上的原生不可变,但未来可能会引用。
线程安全需要保证几个基本特性:
- 原子性:相关操作不会中途被其他线程干扰,一般通过同步机制实现
- 可见性:是一个线程修改了某个共享变量,其状态能够立即被其他线程知道,通常被解释为将线程本地状态反应到主内存上,volatile就是负责保证可见性。
- 有序性:保证线程内,串行语义,避免指令重排序。
4.0 锁类型
Synchronized 分析及应用
ReentrantLock 分析及应用
其他锁(ReadWriteLock、StampedLock)
我们发现锁并不都是实现了Lock接口,ReadWriteLock是一个单独的接口,它通常是代表了一对锁,分别对应了只读和写操作,标准类库中提供了再入版本的读写锁实现(ReentrantReadWriteLock),对应语义和ReentrantLock比较相似。
StampedeLock也是一个单独的类型,从类图中可以看出它是不支持再入性的语义的,也就是说它不支持以持有锁的线程为单位。
为什么我们需要ReadWriteLock(读写锁)等其他锁?
这是因为虽然ReentrantLock和Synchronized简单实用,但是行为上有一定局限性,通俗点说就是太霸道,要么不占,要么独占。实际场景中,有的时候不需要大量竞争的写操作,而是以并发读取为主,如何进一步优化并发操作的粒度呢?
Java并发提供的读写锁等扩展了锁的能力,它所基于的原理是多个读操作是不需要互斥的,因为读操作并不需要更改数据,所以不存在互相干扰,而写操作则会导致并发一致性的问题,所以写线程之间、读写线程之间需要精心设计各种互斥逻辑。
下面是一个基于读写锁实现的数据结构,当数据量较大,并发读多,并发写少的情况下,能够比纯同步版凸显出优势。
public class RWSample {
private fnal Map<String, String> m = new TreeMap<>();
private fnal ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private fnal Lock r = rwl.readLock();
private fnal Lock w = rwl.writeLock();
public String get(String key) {
r.lock();
Sysem.out.println("读锁锁定!");
try {
return m.get(key);
} fnally {
r.unlock();
}
}
public String put(String key, String entry) {
w.lock();
Sysem.out.println("写锁锁定!");
try {
return m.put(key, entry);
} fnally {
w.unlock();
}
}
// …
}
在运行中,如果读锁试图锁定时,写锁是被某个线程持有,读锁将无法获得,而只好等待对方操作结束,这样就可以自动保证不会读取到有争议的数据。
读写锁看起来比Synchronized的粒度更细一些,但是在实际应用中表现也不进人如意,主要因为相对比较大的开销。
StampedLock
所以JDK后期引入了StampedLock,在提供类似读写锁的同时,还支持优化读模式。优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先试着修改,然后通过validate方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁。
参考下面样例代码:
public class StampedSample {
private fnal StampedLock sl = new StampedLock();
void mutate() {
long samp = sl.writeLock();
try {
write();
} fnally {
sl.unlockWrite(samp);
}
}
Data access() {
long samp = sl.tryOptimisicRead();
Data data = read();
if (!sl.validate(samp)) {
samp = sl.readLock();
try {
data = read();
} fnally {
sl.unlockRead(samp);
}
}
return data;
}
// …
}
注意:这里的WriteLock 和 UnLockWrite一定要保证成对调用
并发包内的各种同步工具,不仅仅是各种Lock,其他如Semaphore、CountDownLatch,甚至早期的FutureTask等,都是基于AQS框架的。
AQS框架详述