九、ReentrantReadWriteLock
1)使用
可以有多个读同时发生,读写不能同时发生,写写不能同时发生
@Slf4j(topic = "c.pool")
public class Test7 {public static void main(String[] args) {DataContainer dataContainer = new DataContainer();//同时读读
// new Thread(() -> {
// dataContainer.read();
// }, "t1").start();
// new Thread(() -> {
// dataContainer.read();
// }, "t2").start();//同时读写
// new Thread(() -> {
// dataContainer.read();
// }, "t1").start();
// new Thread(() -> {
// dataContainer.write();
// }, "t2").start();// 同时写写new Thread(() -> {dataContainer.write();}, "t1").start();new Thread(() -> {dataContainer.write();}, "t2").start();}
}
@Slf4j(topic = "c.DataContainer")
class DataContainer {private Object data;private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();public Object read() {readLock.lock();log.debug("获取读锁");try {log.debug("读取");Thread.sleep(1000);return data;} catch (InterruptedException e) {throw new RuntimeException(e);} finally {log.debug("释放读锁");readLock.unlock();}}public void write() {writeLock.lock();log.debug("获取写锁");try {log.debug("写入");} finally {log.debug("释放写锁");writeLock.unlock();}}
}
2)注意事项
- 读锁不支持条件变量,写锁支持
- 重入不支持升级:支持重入,但是不支持重入锁升级,获取了读锁只能重入读锁,获取写锁就阻塞
- 重入支持降级:获取了写锁,可以获取读锁
3)应用
- 保证缓存与数据库一致性
- 可以用在数据库读取 获取缓存 跟 更新数据库清除缓存的时候
- 获取缓存前用读锁
- 清空缓存 跟 更新数据库之间前用写锁保证原子性,保证一致性 ,使用写锁之前需要解开写锁
代码 略------
4)读写锁原理
读写锁用的是同一个Sync 同步器,因此等待队列、state等也是同一个
a. t1线程 w.lock, t2 线程 r.lock
1)t1 成功获得锁, 因为一开始,所以流程跟 ReentrantLock枷锁没有什么特殊之处
不同的是:ReentrantLock 的 state 0 表示没上锁, 1 表示上锁
ReentrantReadWriteLock 的 state 的低16位是表示写锁状态, 高 16 位是 读锁状态 ,然后0 表示没上锁,1表示上锁
2)t2 执行 r.lock 获取读锁,这时候进入读锁 的 sync.acquireShared(1)流程,首先会进入 tryAcquireShared 流程。
如果有写锁占据,那么tryAcquireShared 返回 -1 表示失败
tryAcquireShared 返回值表示
- -1 表示失败
- 0 表示成功, 但后继结点 不会继续唤醒
- 正数表示成功,而且数值是还有几个后继结点需要唤醒,读写锁返回1
3)这时进入 sync.doAcquireShared(1) 流程,首先调用addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式 而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态。
b. t3 线程 r.lock, t4 线程 w.lock
c. t1 w.unlock
这时候会走到写锁的 sync. release(1) 流程, 调用 sync. tryRelease(1) 成功之后如图
这样能够体现 读读能够并发的效果,因为如果下一个节点是 SHARE 读锁,所以会继续唤醒,直到队尾或者碰到写锁。
d. t2 线程 r.unlock t3 线程 r.unlock
此时 t2 跟 t3 同时在运行,都释放 读锁
t2 会进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数 - 1, 但由于计数还不为0
十、StampedLock
该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用 。
加解读锁
long stamp = lock.readLock();
lock.unlockRead(stamp);
加解写锁
long stamp = lock.writeLock();
lock.unlockWrite(stamp);
乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通 过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。
long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){// 锁升级 这里是乐观读 升级 读锁
}
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法
class DataContainerStamped {private int data;private final StampedLock lock = new StampedLock();public DataContainerStamped(int data) {this.data = data;}public int read(int readTime) {long stamp = lock.tryOptimisticRead();log.debug("optimistic read locking...{}", stamp);sleep(readTime);if (lock.validate(stamp)) {log.debug("read finish...{}, data:{}", stamp, data);return data;}// 锁升级 - 读锁log.debug("updating to read lock... {}", stamp);try {stamp = lock.readLock();log.debug("read lock {}", stamp);sleep(readTime);log.debug("read finish...{}, data:{}", stamp, data);return data;} finally {log.debug("read unlock {}", stamp);lock.unlockRead(stamp);}}public void write(int newData) {long stamp = lock.writeLock();log.debug("write lock {}", stamp);try {sleep(2);this.data = newData;} finally {log.debug("write unlock {}", stamp);lock.unlockWrite(stamp);}}
}
测试 读-读 可以优化
public static void main(String[] args) {DataContainerStamped dataContainer = new DataContainerStamped(1);new Thread(() -> {dataContainer.read(1);}, "t1").start();sleep(0.5);new Thread(() -> {dataContainer.read(0);}, "t2").start();
}
//输出结果,可以看到实际没有加读锁
// 15:58:50.217 c.DataContainerStamped [t1] - optimistic read locking...256
// 15:58:50.717 c.DataContainerStamped [t2] - optimistic read locking...256
// 15:58:50.717 c.DataContainerStamped [t2] - read finish...256, data:1
// 15:58:51.220 c.DataContainerStamped [t1] - read finish...256, data:1
测试 读-写 时优化读补加读锁
public static void main(String[] args) {DataContainerStamped dataContainer = new DataContainerStamped(1);new Thread(() -> {dataContainer.read(1);}, "t1").start();sleep(0.5);new Thread(() -> {dataContainer.write(100);}, "t2").start();
}
// 15:57:00.219 c.DataContainerStamped [t1] - optimistic read locking...256
// 15:57:00.717 c.DataContainerStamped [t2] - write lock 384
// 15:57:01.225 c.DataContainerStamped [t1] - updating to read lock... 256
// 15:57:02.719 c.DataContainerStamped [t2] - write unlock 384
// 15:57:02.719 c.DataContainerStamped [t1] - read lock 513
// 15:57:03.719 c.DataContainerStamped [t1] - read finish...513, data:1000
// 15:57:03.719 c.DataContainerStamped [t1] - read unlock 513
注意
- StampedLock 不支持条件变量
- StampedLock 不支持可重入
十一、Semaphore
信号量,用来 限制能同时访问共享资源的线程上限。
理解:
ReentrantLock 同一时刻还是只能有一个线程访问共享资源
Semaphore 是能够让同一个时刻,多个线程访问多个共享变量,但是访问线程个数加以限制
作用:
有一个停车场,里面有很多停车位,但是是有限制的,然后有汽车想要进去停车,停车场外面有一个公示牌,显示停车位还剩多少,如果不是0,汽车就能够进入停车,如果为0,就需要等里面的开走了,公示牌计数不为0了,那么就可以进入停车,semaphore相当于公示牌的作用,计数不为0的时候,汽车线程就可以进来。
基本使用:
public static void main(String[] args) {// 1. 创建 semaphore 对象Semaphore semaphore = new Semaphore(3);// 2. 10个线程同时运行for (int i = 0; i < 10; i++) {new Thread(() -> {// 3. 获取许可try {semaphore.acquire();} catch (InterruptedException e) {e.printStackTrace();}try {log.debug("running...");sleep(1);log.debug("end...");} finally {// 4. 释放许可semaphore.release();}}).start();}
}
// 07:35:15.485 c.TestSemaphore [Thread-2] - running...
// 07:35:15.485 c.TestSemaphore [Thread-1] - running...
// 07:35:15.485 c.TestSemaphore [Thread-0] - running...
// 07:35:16.490 c.TestSemaphore [Thread-2] - end...
// 07:35:16.490 c.TestSemaphore [Thread-0] - end...
// 07:35:16.490 c.TestSemaphore [Thread-1] - end...
// 07:35:16.490 c.TestSemaphore [Thread-3] - running...
// 07:35:16.490 c.TestSemaphore [Thread-5] - running...
// 07:35:16.490 c.TestSemaphore [Thread-4] - running...
// 07:35:17.490 c.TestSemaphore [Thread-5] - end...
// 07:35:17.490 c.TestSemaphore [Thread-4] - end...
// 07:35:17.490 c.TestSemaphore [Thread-3] - end...
// 07:35:17.490 c.TestSemaphore [Thread-6] - running...
// 07:35:17.490 c.TestSemaphore [Thread-7] - running...
// 07:35:17.490 c.TestSemaphore [Thread-9] - running...
// 07:35:18.491 c.TestSemaphore [Thread-6] - end...
// 07:35:18.491 c.TestSemaphore [Thread-7] - end...
// 07:35:18.491 c.TestSemaphore [Thread-9] - end...
// 07:35:18.491 c.TestSemaphore [Thread-8] - running...
// 07:35:19.492 c.TestSemaphore [Thread-8] - end...
应用:
- 使用 Semaphore 限流,在访问高峰期时,让请求线程阻塞,高峰期过去之后释放许可,当然它只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比 Tomcat LimitLatch的实现)
- 用 Semaphore 实现简单连接池,对比 【享元模式】下的实现(用 wait notify),性能和可读性会更好,注意下面的实现中 线程数 和 数据库连接数是相等的。
package com.itheima.test6;import lombok.extern.slf4j.Slf4j;import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicIntegerArray;public class Test2Pool {public static void main(String[] args) {Pool pool = new Pool(2);for (int i = 0; i < 5; i ++ ) {new Thread(() -> {Connection borrow = pool.borrow();try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}pool.free(borrow);}).start();}}
}
@Slf4j(topic = "c.Pool")
class Pool {// 连接池的大小private final int poolSize;// 连接池数组private Connection[] connection;// 连接池判断是否可用 0表示可用 1表示不可用private AtomicIntegerArray status;// 数量跟线程数一样private Semaphore semaphore;public Pool(int poolSize) {this.poolSize = poolSize;this.semaphore = new Semaphore(poolSize);connection = new Connection[poolSize];status = new AtomicIntegerArray(poolSize);for (int i = 0; i < poolSize; i ++ ) {connection[i] = new MyMarkConnection("连接" + i);}}// 取连接public Connection borrow() {// 获取许可semaphore.acquire(); // 没有许可,在这里等待for (int i = 0; i < poolSize; i ++ ) {if (status.get(i) == 0) {if (status.compareAndSet(i, 0, 1)) {log.debug("borrow");return connection[i];}}}return null; // 不会执行}// 归换连接public void free(Connection connection) {for (int i = 0; i < poolSize; i ++ ) {if (this.connection[i] == connection) {status.set(i, 0);semaphore.release();break;}}}
}class MyMarkConnection implements Connection{//记得补全实现一下所有方法
}
原理:
Semaphore 有点像一个停车场,permits 就好像停车位数量,当线程获得了 permits 就像是获得了停车位,然后 停车场显示空余车位减一
刚开始,permits(state)为 3,这时 5 个线程来获取资源
假设其中 Thread-1,Thread-2,Thread-4 cas 竞争成功,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列 park 阻塞
这时 Thread-4 释放了 permits,状态如下
接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,断开原来的 head 节点,因为是共享节点,所以继续唤醒下一个节点
unpark 接下来的 Thread-3 节点,但是由于没有许可,permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态
十二、CountdownLatch (倒计时锁)
用来进行线程同步协作,等待所有线程完成倒计时。
其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一
基本用法:
public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(3);new Thread(() -> {log.debug("begin...");sleep(1);latch.countDown();log.debug("end...{}", latch.getCount());}).start();new Thread(() -> {log.debug("begin...");sleep(2);latch.countDown();log.debug("end...{}", latch.getCount());}).start();new Thread(() -> {log.debug("begin...");sleep(1.5);latch.countDown();log.debug("end...{}", latch.getCount());}).start();log.debug("waiting...");latch.await();log.debug("wait end...");
}
// 18:44:00.778 c.TestCountDownLatch [main] - waiting...
// 18:44:00.778 c.TestCountDownLatch [Thread-2] - begin...
// 18:44:00.778 c.TestCountDownLatch [Thread-0] - begin...
// 18:44:00.778 c.TestCountDownLatch [Thread-1] - begin...
// 18:44:01.782 c.TestCountDownLatch [Thread-0] - end...2
// 18:44:02.283 c.TestCountDownLatch [Thread-2] - end...1
// 18:44:02.782 c.TestCountDownLatch [Thread-1] - end...0
// 18:44:02.782 c.TestCountDownLatch [main] - wait end...
这里我们想到前面学过的 join 用 join 来解决,也可以等其他线程执行完,
但是 join 是比较底层的api,用起来比较繁琐
因为这里是我们自己创建的线程,但是一般情况下我们是线程池中的线程,然后复用,不会随便停止,所以需要用 CountdownLatch 。
实现:
public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(3);ExecutorService service = Executors.newFixedThreadPool(4);service.submit(() -> {log.debug("begin...");sleep(1);latch.countDown();log.debug("end...{}", latch.getCount());});service.submit(() -> {log.debug("begin...");sleep(1.5);latch.countDown();log.debug("end...{}", latch.getCount());});service.submit(() -> {log.debug("begin...");sleep(2);latch.countDown();log.debug("end...{}", latch.getCount());});service.submit(()->{try {log.debug("waiting...");latch.await();log.debug("wait end...");} catch (InterruptedException e) {e.printStackTrace();}});
}
// 18:52:25.831 c.TestCountDownLatch [pool-1-thread-3] - begin...
// 18:52:25.831 c.TestCountDownLatch [pool-1-thread-1] - begin...
// 18:52:25.831 c.TestCountDownLatch [pool-1-thread-2] - begin...
// 18:52:25.831 c.TestCountDownLatch [pool-1-thread-4] - waiting...
// 18:52:26.835 c.TestCountDownLatch [pool-1-thread-1] - end...2
// 18:52:27.335 c.TestCountDownLatch [pool-1-thread-2] - end...1
// 18:52:27.835 c.TestCountDownLatch [pool-1-thread-3] - end...0
// 18:52:27.835 c.TestCountDownLatch [pool-1-thread-4] - wait end...
应用:等待多线程准备
package com.itheima.test8;import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Test9 {public static void main(String[] args) throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(10);ExecutorService pool = Executors.newFixedThreadPool(10);Random random = new Random();String[] all = new String[10];for (int i = 0; i < 10; i ++) {int k = i;pool.submit(() -> {for (int j = 0; j <= 100; j ++ ) {try {Thread.sleep(random.nextInt(100));} catch (InterruptedException e) {throw new RuntimeException(e);}all[k] = j + "%";System.out.print("\r" + Arrays.toString(all));}countDownLatch.countDown();});}countDownLatch.await();System.out.println("\n游戏开始");pool.shutdown();;}
}
十三、CycliBarrier (循环栅栏)
- CountdownLatch 每次使用都需要创建,只有在构造方法的时候能够传入值,如果你想循环,就需要每次都创建一个 CountdownLatch对象,
- 这时候就可以用 CycliBarrier 就比较适合这种复用场景。也是用来多线程协作,等待线程满足某个计数。用法类似,但是不同点是可以复用。
但是是使用await 是计数 - 1的作用。
计数 最好跟 线程池线程数数一样,才能够达到最佳效果。
解释
如果线程池中线程数为3,然后CyclicBarrier(2) 设置了2,那么如果下面的例子中task1跟task2循环执行3次,然后task1执行1秒,task2执行2秒,但是我们可以允许三个线程同时执行,然后就是task1 1秒 task2 2秒 task1 1秒,然后CyclicBarrier只能统计两个,所以我们统计的可能不是task1 跟 task2,可能是 task1 task1,因为执行时间不一样,所以可能导致结果有误差,所以建议设置成相同的,才能达到最佳效果。
每次计数从0开始,然后每次await + 1, 等到达到构造方法的时候设置的大小的时候,就停止,然后重置为0,可以复用。
@Slf4j(topic = "c.Test9")
public class Test9 {public static void main(String[] args) throws InterruptedException {ExecutorService pool = Executors.newFixedThreadPool(2);// 第二个参数可以统计返回结果CyclicBarrier barrier = new CyclicBarrier(2, () -> {log.debug("task1 , task2 final");});pool.submit(() -> {log.debug("task1 begin --- ");try {Thread.sleep(1000);log.debug(barrier.getNumberWaiting() + " ");barrier.await();
// log.debug("task1 end ---");} catch (InterruptedException | BrokenBarrierException e) {throw new RuntimeException(e);}});pool.submit(() -> {log.debug("task2 begin --- ");try {Thread.sleep(2000);log.debug(barrier.getNumberWaiting() + " ");barrier.await();
// log.debug("task2 end ---");} catch (InterruptedException | BrokenBarrierException e) {throw new RuntimeException(e);}});}
}
十四、线程安全集合类
重点介绍 java.util.concurrent.* 下的线程安全集合类,可以发现它们有规律,里面包含三类关键词: Blocking、CopyOnWrite、Concurrent
- Blocking 大部分实现基于锁,并提供用来阻塞的方法 (例如,队列为空的时候,我们取元素就需要阻塞等待,然后队列满了的时候,我们放元素也需要等待)
- CopyOnWrite 之类容器修改开销相对较重 (修改时拷贝的方式,避免多线程访问时读写的线程安全,适合多读少写场景)
- Concurrent 类型的容器
-
- 内部很多操作使用 cas 优化,一般可以提供较高吞吐量
- 弱一致性
-
-
- 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍 历,这时内容是旧的
- 求大小弱一致性,size 操作未必是 100% 准确
- 读取弱一致性 (可能被其他线程修改了)
-
遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,不能并发修改
上面这些安全容器,并发可以修改的使用fail-safe的机制
抛出 ConcurrentModificationException,不再继续遍历
十五、ConcurrentHashMap
1. 练习:单词计数
生成测试数据
static final String ALPHA = "abcedfghijklmnopqrstuvwxyz";
public static void main(String[] args) {int length = ALPHA.length();int count = 200;List<String> list = new ArrayList<>(length * count);for (int i = 0; i < length; i++) {char ch = ALPHA.charAt(i);for (int j = 0; j < count; j++) {list.add(String.valueOf(ch));}}Collections.shuffle(list);for (int i = 0; i < 26; i++) {try (PrintWriter out = new PrintWriter(new OutputStreamWriter(new FileOutputStream("tmp/" + (i+1) + ".txt")))) {String collect = list.subList(i * count, (i + 1) * count).stream().collect(Collectors.joining("\n"));out.print(collect);} catch (IOException e) {}}
}
模版代码,模版代码中封装了多线程读取文件的代码
private static <V> void demo(Supplier<Map<String,V>> supplier,BiConsumer<Map<String,V>,List<String>> consumer) {Map<String, V> counterMap = supplier.get();List<Thread> ts = new ArrayList<>();for (int i = 1; i <= 26; i++) {int idx = i;Thread thread = new Thread(() -> {List<String> words = readFromFile(idx);consumer.accept(counterMap, words);});ts.add(thread);}ts.forEach(t->t.start());ts.forEach(t-> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});System.out.println(counterMap);}public static List<String> readFromFile(int i) {ArrayList<String> words = new ArrayList<>();try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("tmp/"+ i +".txt")))) {while(true) {String word = in.readLine();if(word == null) {break;}words.add(word);}return words;} catch (IOException e) {throw new RuntimeException(e);}}
你要做的是实现两个参数
- 一是提供一个 map 集合,用来存放每个单词的计数结果,key 为单词,value 为计数
- 二是提供一组操作,保证计数的安全性,会传递 map 集合以及 单词 List
- 正确结果输出应该是每个单词出现 200 次
{a=200, b=200, c=200, d=200, e=200, f=200, g=200, h=200, i=200, j=200, k=200, l=200, m=200, n=200, o=200, p=200, q=200, r=200, s=200, t=200, u=200, v=200, w=200, x=200, y=200, z=200}
下面的实现为:
demo(// 创建 map 集合// 创建 ConcurrentHashMap 对不对?() -> new HashMap<String, Integer>(),// 进行计数(map, words) -> {for (String word : words) {Integer counter = map.get(word);int newValue = counter == null ? 1 : counter + 1;map.put(word, newValue);}}
);
有没有问题?请改进
参考解答1
demo(() -> new ConcurrentHashMap<String, LongAdder>(),(map, words) -> {for (String word : words) {// 注意不能使用 putIfAbsent,此方法返回的是上一次的 value,首次调用返回 nullmap.computeIfAbsent(word, (key) -> new LongAdder()).increment();}}
);
参考解答2
demo(() -> new ConcurrentHashMap<String, Integer>(),(map, words) -> {for (String word : words) {// 函数式编程,无需原子变量map.merge(word, 1, Integer::sum);}}
);
2. HashMap并发死链
hashmap是采用取模然后模相同就放到同一桶下标的链表下
JDK7使用是头插法,每次新的元素都会放到最顶端,然后指向上一个头结点
当map中元素超过map桶下标大小的4分之3时,就会进行一倍扩容,然后将原来的map中的元素重新插入新的map,并且重新取模
如果两个线程同时刚好同时进行扩容
线程1 已经将 其中一个桶下标的链表的头结点跟next节点拿到, 然后准备去放到新的扩容对应的hash模位置
但是线程2 这时候也进行了扩容,然后已经扩容完毕,然后刚好next的下一个节点被修改了,指向了线程1中的头节点
这时候线程1 去修改新的hash模位置,头结点模完之后,next 模完之后,刚好next头插法在头结点前,
因为引用已经变了,头结点又插在next前面,这就形成了死链,互相指向对方。
小结:
- 究其原因,是因为在多线程环境下使用了非线程安全的 map 集合
- JDK 8 虽然将扩容算法做了调整,不在将元素加入链表头(而是保持与扩容前一样的顺序),但仍不意味着能够在多线程环境下能够安全扩容,还会出现其他问题(如扩容丢数据)
3. JDK 8 ConcurrentHashMap
a. 重要属性和内部类
// 默认为 0
// 当初始化时, 为 -1
// 当扩容时, 为 -(1 + 扩容线程数)
// 当初始化或扩容完成后,为 下一次的扩容的阈值大小
private transient volatile int sizeCtl;
// 整个 ConcurrentHashMap 就是一个 Node[]
static class Node<K,V> implements Map.Entry<K,V> {}
// hash 表
transient volatile Node<K,V>[] table;
// 扩容时的 新 hash 表
private transient volatile Node<K,V>[] nextTable;
// 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点
static final class ForwardingNode<K,V> extends Node<K,V> {}
// 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node
static final class ReservationNode<K,V> extends Node<K,V> {}
// 作为 treebin 的头节点, 存储 root 和 first
static final class TreeBin<K,V> extends Node<K,V> {}
// 作为 treebin 的节点, 存储 parent, left, right
static final class TreeNode<K,V> extends Node<K,V> {}
b. 重要方法
// 获取 Node[] 中第 i 个 Node
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i)// cas 修改 Node[] 中第 i 个 Node 的值, c 为旧值, v 为新值
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v)// 直接修改 Node[] 中第 i 个 Node 的值, v 为新值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)
c. 构造器分析
可以看到实现了懒惰初始化,在构造方法中仅仅计算了 table 的大小,以后在第一次使用时才会真正创建
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)throw new IllegalArgumentException();// 初始容量要保证最少等于并发度if (initialCapacity < concurrencyLevel) // Use at least as many binsinitialCapacity = concurrencyLevel; // as estimated threadslong size = (long)(1.0 + (long)initialCapacity / loadFactor); //计算的值会转成是2的幂次方// tableSizeFor 仍然是保证计算的大小是 2^n, 即 16,32,64 ... int cap = (size >= (long)MAXIMUM_CAPACITY) ?MAXIMUM_CAPACITY : tableSizeFor((int)size);this.sizeCtl = cap;
}
d. get 流程
public V get(Object key) {Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;// spread 方法能确保返回结果是正数int h = spread(key.hashCode());//判断哈希表是不是空的 && 找到h的模的位置判断头结点是不是我们要找的节点, hash一不一样,并且 key是不是相同,或者是不是相同if ((tab = table) != null && (n = tab.length) > 0 &&(e = tabAt(tab, (n - 1) & h)) != null) {// 如果头结点已经是要查找的 keyif ((eh = e.hash) == h) {if ((ek = e.key) == key || (ek != null && key.equals(ek)))return e.val;}// hash 为负数表示该 bin 在扩容中或是 treebin, 这时调用 find 方法来查找else if (eh < 0)return (p = e.find(h, key)) != null ? p.val : null;// 正常遍历链表, 用 equals 比较while ((e = e.next) != null) {if (e.hash == h &&((ek = e.key) == key || (ek != null && key.equals(ek))))return e.val;}}return null;
}
e. put 流程
以下数组简称(table),链表简称(bin)
public V put(K key, V value) {return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException();// 其中 spread 方法会综合高位低位, 具有更好的 hash 性int hash = spread(key.hashCode());int binCount = 0;for (Node<K,V>[] tab = table;;) {// f 是链表头节点// fh 是链表头结点的 hash// i 是链表在 table 中的下标Node<K,V> f; int n, i, fh;// 要创建 tableif (tab == null || (n = tab.length) == 0)// 初始化 table 使用了 cas, 无需 synchronized 创建成功, 进入下一轮循环tab = initTable();// 要创建链表头节点else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {// 添加链表头使用了 cas, 无需 synchronizedif (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break;}// 帮忙扩容else if ((fh = f.hash) == MOVED)// 帮忙之后, 进入下一轮循环tab = helpTransfer(tab, f);else {V oldVal = null;// 锁住链表头节点synchronized (f) {// 再次确认链表头节点没有被移动if (tabAt(tab, i) == f) {// 链表if (fh >= 0) {binCount = 1;// 遍历链表for (Node<K,V> e = f;; ++binCount) {K ek;// 找到相同的 keyif (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {oldVal = e.val;// 更新if (!onlyIfAbsent)e.val = value;break;}Node<K,V> pred = e;// 已经是最后的节点了, 新增 Node, 追加至链表尾if ((e = e.next) == null) {pred.next = new Node<K,V>(hash, key,value, null);break;}}}// 红黑树else if (f instanceof TreeBin) {Node<K,V> p;binCount = 2;// putTreeVal 会看 key 是否已经在树中, 是, 则返回对应的 TreeNodeif ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {oldVal = p.val;if (!onlyIfAbsent)p.val = value;}}}// 释放链表头节点的锁}if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD)// 如果链表长度 >= 树化阈值(8), 进行链表转为红黑树treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}// 增加 size 计数addCount(1L, binCount);return null;
}
private final Node<K,V>[] initTable() {Node<K,V>[] tab; int sc;while ((tab = table) == null || tab.length == 0) {if ((sc = sizeCtl) < 0)Thread.yield();// 尝试将 sizeCtl 设置为 -1(表示初始化 table)else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {// 获得锁, 创建 table, 这时其它线程会在 while() 循环中 yield 直至 table 创建try {if ((tab = table) == null || tab.length == 0) {int n = (sc > 0) ? sc : DEFAULT_CAPACITY;Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];table = tab = nt;sc = n - (n >>> 2);}} finally {sizeCtl = sc;}break;}}return tab;
}
// check 是之前 binCount 的个数
private final void addCount(long x, int check) {CounterCell[] as; long b, s;if (// 已经有了 counterCells, 向 cell 累加(as = counterCells) != null ||// 还没有, 向 baseCount 累加!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {CounterCell a; long v; int m;boolean uncontended = true;if (// 还没有 counterCellsas == null || (m = as.length - 1) < 0 ||// 还没有 cell(a = as[ThreadLocalRandom.getProbe() & m]) == null ||// cell cas 增加计数失败!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {// 创建累加单元数组和cell, 累加重试fullAddCount(x, uncontended);return;}if (check <= 1)return;// 获取元素个数s = sumCount();}if (check >= 0) {Node<K,V>[] tab, nt; int n, sc;while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&(n = tab.length) < MAXIMUM_CAPACITY) {int rs = resizeStamp(n);if (sc < 0) {if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||transferIndex <= 0)break;// newtable 已经创建了,帮忙扩容if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))transfer(tab, nt);}// 需要扩容,这时 newtable 未创建else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2))transfer(tab, null);s = sumCount();}}
}
f. size 计算流程
size 计算实际发生在 put,remove 改变集合元素的操作之中
- 没有竞争发生,向 baseCount 累加计数
- 有竞争发生,新建 counterCells,向其中的一个 cell 累加计数
-
- counterCells 初始有两个 cell
- 如果计数竞争比较激烈,会创建新的 cell 来累加计数
public int size() {long n = sumCount();return ((n < 0L) ? 0 :(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :(int)n);
}
final long sumCount() {CounterCell[] as = counterCells; CounterCell a;// 将 baseCount 计数与所有 cell 计数累加long sum = baseCount;if (as != null) {for (int i = 0; i < as.length; ++i) {if ((a = as[i]) != null)sum += a.value;}}return sum;
}
Java 8 数组(Node) +( 链表 Node | 红黑树 TreeNode ) 以下数组简称(table),链表简称(bin)
- 初始化,使用 cas 来保证并发安全,懒惰初始化 table
- 树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将链表树化,树化过程 会用 synchronized 锁住链表头
- put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素 添加至 bin 的尾部
- get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 它会让 get 操作在新 table 进行搜索
- 扩容,扩容时以 bin 为单位进行,需要对 bin 进行synchronized,但这时妙的是其它竞争线程也不是无事可做,它们会帮助把其它 bin进行扩容,扩容时平均只有1/6 的节点会被复制到新table 中
- size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中。最后统计数量时累加 即可
4. JDK 7 ConcurrentHashMap
它维护了一个 segment 数组,每个 segment 对应一把锁 (底层实现的是 ReentrantLock)
- 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 jdk8 中是类似的
- 缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了, 并且不是懒惰初始化
a. 构造器分析
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)throw new IllegalArgumentException();if (concurrencyLevel > MAX_SEGMENTS)concurrencyLevel = MAX_SEGMENTS;// ssize 必须是 2^n, 即 2, 4, 8, 16 ... 表示了 segments 数组的大小int sshift = 0;int ssize = 1;while (ssize < concurrencyLevel) {++sshift;ssize <<= 1;}// segmentShift 默认是 32 - 4 = 28this.segmentShift = 32 - sshift;// segmentMask 默认是 15 即 0000 0000 0000 1111this.segmentMask = ssize - 1;if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;int c = initialCapacity / ssize;if (c * ssize < initialCapacity)++c;int cap = MIN_SEGMENT_TABLE_CAPACITY;while (cap < c)cap <<= 1;// 创建 segments and segments[0]Segment<K,V> s0 =new Segment<K,V>(loadFactor, (int)(cap * loadFactor),(HashEntry<K,V>[])new HashEntry[cap]);Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]this.segments = ss;
}
构造完成,如下图所示
b. put 流程
public V put(K key, V value) {Segment<K,V> s;if (value == null)throw new NullPointerException();int hash = hash(key);// 计算出 segment 下标int j = (hash >>> segmentShift) & segmentMask;// 获得 segment 对象, 判断是否为 null, 是则创建该 segmentif ((s = (Segment<K,V>)UNSAFE.getObject (segments, (j << SSHIFT) + SBASE)) == null) {// 这时不能确定是否真的为 null, 因为其它线程也发现该 segment 为 null,// 因此在 ensureSegment 里用 cas 方式保证该 segment 安全性s = ensureSegment(j);}// 进入 segment 的put 流程return s.put(key, hash, value, false);
}
segment 继承了可重入锁(ReentrantLock),它的 put 方法为
final V put(K key, int hash, V value, boolean onlyIfAbsent) {// 尝试加锁HashEntry<K,V> node = tryLock() ? null :// 如果不成功, 进入 scanAndLockForPut 流程// 如果是多核 cpu 最多 tryLock 64 次, 进入 lock 流程// 在尝试期间, 还可以顺便看该节点在链表中有没有, 如果没有顺便创建出来scanAndLockForPut(key, hash, value);// 执行到这里 segment 已经被成功加锁, 可以安全执行V oldValue;try {HashEntry<K,V>[] tab = table;int index = (tab.length - 1) & hash;HashEntry<K,V> first = entryAt(tab, index);for (HashEntry<K,V> e = first;;) {if (e != null) {// 更新K k;if ((k = e.key) == key ||(e.hash == hash && key.equals(k))) { oldValue = e.value;if (!onlyIfAbsent) {e.value = value;++modCount;}break;}e = e.next;}else {// 新增// 1) 之前等待锁时, node 已经被创建, next 指向链表头if (node != null)node.setNext(first);else// 2) 创建新 nodenode = new HashEntry<K,V>(hash, key, value, first);int c = count + 1; // 3) 扩容if (c > threshold && tab.length < MAXIMUM_CAPACITY)rehash(node);else// 将 node 作为链表头setEntryAt(tab, index, node);++modCount;count = c;oldValue = null;break;}}} finally {unlock();}return oldValue;
}
c. rehash 流程 (扩容)
发生在 put 中,因为此时已经获得了锁,因此 rehash 时不需要考虑线程安全
private void rehash(HashEntry<K,V> node) {HashEntry<K,V>[] oldTable = table;int oldCapacity = oldTable.length;int newCapacity = oldCapacity << 1;threshold = (int)(newCapacity * loadFactor);HashEntry<K,V>[] newTable =(HashEntry<K,V>[]) new HashEntry[newCapacity];int sizeMask = newCapacity - 1;for (int i = 0; i < oldCapacity ; i++) {HashEntry<K,V> e = oldTable[i];if (e != null) {HashEntry<K,V> next = e.next;int idx = e.hash & sizeMask;if (next == null) // Single node on listnewTable[idx] = e;else { // Reuse consecutive sequence at same slotHashEntry<K,V> lastRun = e;int lastIdx = idx;// 过一遍链表, 尽可能把 rehash 后 idx 不变的节点重用for (HashEntry<K,V> last = next;last != null;last = last.next) {int k = last.hash & sizeMask;if (k != lastIdx) {lastIdx = k;lastRun = last;}}newTable[lastIdx] = lastRun;// 剩余节点需要新建for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {V v = p.value;int h = p.hash;int k = h & sizeMask;HashEntry<K,V> n = newTable[k];newTable[k] = new HashEntry<K,V>(h, p.key, v, n);}}}}// 扩容完成, 才加入新的节点int nodeIndex = node.hash & sizeMask; // add the new nodenode.setNext(newTable[nodeIndex]);newTable[nodeIndex] = node;// 替换为新的 HashEntry tabletable = newTable;
}
d. get 流程
get 时并未加锁,用了 UNSAFE 方法保证了可见性,扩容过程中,get 先发生就从旧表取内容,get 后发生就从新 表取内容
public V get(Object key) {Segment<K,V> s; // manually integrate access methods to reduce overheadHashEntry<K,V>[] tab;int h = hash(key);// u 为 segment 对象在数组中的偏移量long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;// s 即为 segmentif ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&(tab = s.table) != null) {for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);e != null; e = e.next) {K k;if ((k = e.key) == key || (e.hash == h && key.equals(k)))return e.value;}}return null;
}
e. size 计算流程
- 计算元素个数前,先不加锁计算两次,如果前后两次结果如一样,认为个数正确返回
- 如果不一样,进行重试,重试次数超过 3,将所有 segment 锁住,重新计算个数返回
public int size() {// Try a few times to get accurate count. On failure due to// continuous async changes in table, resort to locking.final Segment<K,V>[] segments = this.segments;int size;boolean overflow; // true if size overflows 32 bitslong sum; // sum of modCountslong last = 0L; // previous sumint retries = -1; // first iteration isn't retrytry {for (;;) {if (retries++ == RETRIES_BEFORE_LOCK) {// 超过重试次数, 需要创建所有 segment 并加锁for (int j = 0; j < segments.length; ++j)ensureSegment(j).lock(); // force creation}sum = 0L;size = 0;overflow = false;for (int j = 0; j < segments.length; ++j) {Segment<K,V> seg = segmentAt(segments, j);if (seg != null) {sum += seg.modCount;int c = seg.count;if (c < 0 || (size += c) < 0)overflow = true;}}if (sum == last)break;last = sum;}} finally {if (retries > RETRIES_BEFORE_LOCK) {for (int j = 0; j < segments.length; ++j)segmentAt(segments, j).unlock();}}return overflow ? Integer.MAX_VALUE : size;
}
十六、LinkedBlockingQueue原理
1. 基本的入队出队
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {static class Node<E> {E item;/*** 下列三种情况之一* - 真正的后继节点* - 自己, 发生在出队时* - null, 表示是没有后继节点, 是最后了*/Node<E> next;Node(E x) { item = x; }}
}
出队
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
E x = first.item;
first.item = null;
return x;
2. 加锁分析
==高明之处==在于用了两把锁和 dummy 节点
- 用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行
- 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
-
- 消费者与消费者线程仍然串行
- 生产者与生产者线程仍然串行
线程安全分析
- 当节点总数大于 2 时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock 保证的是 head 节点的线程安全。两把锁保证了入队和出队没有竞争
- 当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争
- 当节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notEmpty 条件阻塞,有竞争,会阻塞
PUT操作
public void put(E e) throws InterruptedException {if (e == null) throw new NullPointerException();int c = -1;Node<E> node = new Node<E>(e);final ReentrantLock putLock = this.putLock;// count 用来维护元素计数final AtomicInteger count = this.count;putLock.lockInterruptibly();try {// 满了等待while (count.get() == capacity) {// 倒过来读就好: 等待 notFullnotFull.await();}// 有空位, 入队且计数加一enqueue(node);c = count.getAndIncrement(); // 除了自己 put 以外, 队列还有空位, 由自己叫醒其他 put 线程if (c + 1 < capacity)notFull.signal();} finally {putLock.unlock();}// 如果队列中有一个元素, 叫醒 take 线程if (c == 0)// 这里调用的是 notEmpty.signal() 而不是 notEmpty.signalAll() 是为了减少竞争signalNotEmpty();
}
take 操作
public E take() throws InterruptedException {E x;int c = -1;final AtomicInteger count = this.count;final ReentrantLock takeLock = this.takeLock;takeLock.lockInterruptibly();try {while (count.get() == 0) {notEmpty.await();}x = dequeue();c = count.getAndDecrement();if (c > 1)notEmpty.signal();} finally {takeLock.unlock();}// 如果队列中只有一个空位时, 叫醒 put 线程// 如果有多个线程进行出队, 第一个线程满足 c == capacity, 但后续线程 c < capacityif (c == capacity)// 这里调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争signalNotFull()return x;
}
主要就是有一个优化,put线程可以唤醒put线程,而不是只是可以put线程唤醒take线程,使用signal而不是signalAll ,减少竞争。
3. 性能比较
主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较
- Linked 支持有界,Array 强制有界
- Linked 实现是链表,Array 实现是数组
- Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
- Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
- Linked 两把锁,Array 一把锁
十七、ConcurrentLinkedQueue 原理
ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是
- 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
- dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
- 只是这【锁】使用了 cas 来实现
事实上,ConcurrentLinkedQueue 应用还是非常广泛的 例如之前讲的 Tomcat 的 Connector 结构时,Acceptor 作为生产者向 Poller 消费者传递事件信息时,正是采用了 ConcurrentLinkedQueue 将 SocketChannel 给 Poller 使用
十八、CopyOnWriteArrayList
CopyOnWriteArraySet 是它的马甲 也就是Set底层也是使用的 CopyOnWriteArrayList,只是做了判重的添加。
底层实现使用 写入时拷贝 的思想,增删改的时候 会将底层的数据拷贝一份,然后在新数组上进行修改,这时 不影响 其他线程的 并发读、读写分离。
以新增为例:
public boolean add(E e) {synchronized (lock) {// 获取旧的数组Object[] es = getArray();int len = es.length;// 拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程)es = Arrays.copyOf(es, len + 1);// 添加新元素es[len] = e;// 替换旧的数组setArray(es);return true;}
}
这里的源码版本是 Java 11,在 Java 1.8 中使用的是可重入锁而不是 synchronized
其它读操作并未加锁,例如:
public void forEach(Consumer<? super E> action) {Objects.requireNonNull(action);for (Object x : getArray()) {@SuppressWarnings("unchecked") E e = (E) x;action.accept(e);}
}
适合读多写少的场景,写多会加synchronized,影响性能。
get 弱一致性 :
不容易测试,但问题确实存在
迭代器弱一致性
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iter = list.iterator();
new Thread(() -> {list.remove(0);System.out.println(list);
}).start();
sleep1s();
while (iter.hasNext()) {System.out.println(iter.next());
}
不要觉得弱一致性就不好
- 数据库的 MVCC 都是弱一致性的表现
- 并发高和一致性是矛盾的,需要权衡