上文说到一些孪生的类,而且通过对比可以看到,线程安全的类的解决方案是对类中每个方法都加上synchronized关键字。而且不管是读取还是写入,都加了锁。 在上文结尾我们简单提到了一些别的加锁方法。今天我们就来看看不在方法上加锁的其他加锁方式(下面源代码如无特殊说明都是基于jdk11)
1: concurrentMap
每说到并发的集合类时,都会说到concurrentHashMap ,然后网上一搜可以看到好多相关的概念,segment,链表,红黑树,等等。现在我们一起来看看concurrentMap的加锁方式,同样选取的 put 方法,因为一般加锁都是对修改做的。
jdk1.7中的put:
中间我们可以看到先是定位segment的位置,然后调用segment的put方法:
static final class Segment<K,V> extends ReentrantLock implements Serializable
Segment 是继承ReentrantLock , 在代码432行,有 tryLock() 这就是对这一块segment 加锁。 比直接对方法加锁细化了一步。
jdk 11中的put
在代码中我们可以看到在调用put时,重载了putVal , 然后注意划线的重点位置:先通过hash值取到对应的Node节点f 。
然后对 f 进行加锁,这样把锁的粒度就降到了节点上。
本次对concurrentMap只探讨加锁方式,其他类如节点转换等, 我们以后再做讨论。
2.CopyOnWriteList
我们用的最多应该是ArrayList , LinkedList 。但我们在使用他们的时候,很多时候都是在方法里面新建,在方法里面进行使用,所以基本上不会出现线程不安全的问题。但如果同一个List被多个线程所使用,则有可能抛出并发修改的异常。 可能抛异常的代码如下:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Runnable read =()->{
while (true){
for (String s : list) {
System.out.println(s);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Runnable write = ()->{
while (true){
list.add("aaa");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread rThread = new Thread(read);
Thread wThread = new Thread(write);
wThread.start();
rThread.start();
}
通过跟踪源代码可以看到,是循环调用next()时,我们同时想向里面add数据,就抛出了并发修改的异常。
我们把代码中的 List<String> list = new ArrayList<>();换成 CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); 就不会上述异常了,小伙伴们可以自己试下。
在CopyOnWriteArrayList 中,我们每一次的新增操作,都会复制一遍原有的数据,对复制的数据进行操作,操作完之后再设置回去
430,431行是获取原有数组, 432行是新建一个数组并扩容加1 ,433行是赋值。
434行是替换掉老的数组。这样读还是读的老数据,修改是对复制的副本进行的操作。
同时在代码中我们可以看到,加锁方式,由在方法体上加锁,变成了对Object 加锁,细化了锁的粒度。
总结:加锁是门艺术,是门学问。在concurrentHashMap部分,我特意把jdk1.7的源码找了出来,因为现在网上很多文章和视频一说起concurrentHashMap 就是segment 分段加锁,但实际上到了jdk11已经有很大变化了。
—END—
前期回顾:
Java学习|图说String(一):String的存储方式
QQ群:661749608
微信群请点击公众号菜单进微信群
们哦~
文字: 微笑的小小刀
排版:花音