(10)关于锁优化的几点建议
-
减少锁持有的时间
-
减小锁粒度
HashMap是线程不安全的
HashTable无论是进行读还是写操作都需要获取锁,因此有了ConcurrentHashMap的出现
如ConcurrentHashMap有16个Segment,也就是有16把锁,这样的话不同的线程获取不同锁锁住某一个Segment,就可以实现高并发的操作,这也是减小锁粒度的一个典型应用
-
使用读写锁替换独占锁
ReentrantLock的升级版ReentrantReadWriteLock就可以实现效率的提升。
ReentrantReadWriteLock有两个锁:一个是与读相关的锁,称为“共享锁”;另一个是与写相关的锁,称为“排它锁”。
-
锁分离
只要操作互不影响,锁就可以分离。最典型的一个例子就是LinkedBlockingQueue ,示意图如下:
LinkedBlockingQueue是基于链表的,take和put方法分别是向链表中取数据和写数据,他们的操作一个是从队列的头部一个是队列的尾部,从理论上说他们是不冲突的,也就是说可以锁分离的。
如果使用独占锁的话,则要求两个操作在进行时首先要获取当前队列的锁,那么take和put就不是先真正的并发了,因此,在JDK中正是实现了两种不同的锁,一个是takeLock一个是putLock。
- 锁粗化
首先简单例子
public void syncMethod() {
synchronized (lock) { //第一次加锁
method1();
}
method3();
synchronized (lock) { //第二次加锁
mutextMethod();
}
method4();
synchronized (lock) { //第三次加锁
method2();
}
}
如果加锁和线程上下文切换的时间超过方法的执行时间,倒不如使用下面的方式
public void syncMethod() {
synchronized (lock) {
method1();
method3();
mutextMethod();
method4();
method2();
}
}
- 锁消除
锁消除是在编译器级别的事情。
在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。
也许你会觉得奇怪,既然有些对象不可能被多线程访问,那为什么要加锁呢?写代码时直接不加锁不就好了。
但是有时,这些锁并不是程序员所写的,有的是JDK实现中就有锁的,比如Vector和StringBuffer这样的类,它们中的很多方法都是有锁的。当我们在一些不会有线程安全的情况下使用这些类的方法时,达到某些条件时,编译器会将锁消除来提高性能。
- Java虚拟机对锁的优化
(11)原子操作类
(12)读写锁ReentrantReadWriteLock深入分析
锁降级实例演示
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private Lock readLock = lock.readLock();
private Lock writeLock = lock.writeLock();
private boolean update;
public void processData() {
readLock.lock(); //读锁获取
if (!update) {
readLock.unlock(); //必须先释放读锁
writeLock.lock(); //锁降级从获取写锁开始
try {
if (!update) {
//准备数据流程(略)
update = true;
}
//获取读锁。在写锁持有期间获取读锁
//此处获取读锁,是为了防止,当释放写锁后,又有一个线程T获取锁,对数据进行改变,
//而当前线程下面对改变的数据无法感知。
//如果获取了读锁,则线程T则被阻塞,直到当前线程释放了读锁,那个T线程才有可能获取写锁。
readLock.lock();
} finally {
writeLock.unlock();//释放写锁
}
//锁降级完成
}
try {
//使用数据的流程
} finally {
readLock.unlock(); //释放读锁
}
}
LockSupport工具
不管是ReentrantReadWriteLock还是ReentrantLock,当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类完成相应的工作。LockSupport方法如下
总结
-
1、读锁的重入是允许多个申请读操作的线程的,而写锁同时只允许单个线程占有,该线程的写操作可以重入。
-
2、如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
-
3、对于同时占有读锁和写锁的线程,如果完全释放了写锁,那么它就完全转换成了读锁,以后的写操作无法重入,在写锁未完全释放时写操作是可以重入的。
-
4、公平模式下无论读锁还是写锁的申请都必须按照AQS锁等待队列先进先出的顺序。非公平模式下读操作插队的条件是锁等待队列head节点后的下一个节点是SHARED型节点,写锁则无条件插队。
-
5、读锁不允许newConditon获取Condition接口,而写锁的newCondition接口实现方法同ReentrantLock。
(13)等待/通知模式接口Condition接口深入分析
Condition接口实现原理
ConditionObject实现了Condition接口,是AQS的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类是一个比较合理的方式。每一个Condition对象都包含一个等待队列,该队列是Condition实现等待通知机制的关键。
和synchronized一样,在调用wait和notify等方法之前都必须要先获取锁,同样使用Condition对象的await和signal方法的时候也是要先获取到锁!
1.等待队列
- FIFO队列
- 每个节点包含一个线程的引用
- Condition拥有首节点和尾节点
- 节点更新过程没有使用CAS,通过锁来保证线程安全
Lock模式下同步队列和等待队列的关系
2.等待的实现
3.通知的实现
(14)多线程异步调用之Future模式
Future的类图关系