Java:并发笔记-09

说明:这是看了 bilibili 上 黑马程序员 的课程 java并发编程 后做的笔记

7. 共享模型之工具-2

原理:AQS 原理

对于 AQS 的原理这部分内容,没很好的理解,等功力深厚了再回来好好理解一下,笔记也就不贴出来了

原理:ReentrantLock 原理

同样对于 ReentrantLock 的原理这部分内容,没很好的理解,等功力深厚了再回来好好理解一下

7.2 J.U.C

3. 读写锁

3.1 ReentrantReadWriteLock

当读操作远远高于写操作时,这时候使用 读写锁读-读 可以并发,提高性能。 类似于数据库中的 select ... from ... lock in share mode;

提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

  1. class DataContainer{
  2. private Object data;
  3. // 读写锁
  4. private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
  5. // 读锁
  6. private ReentrantReadWriteLock.ReadLock r = rw.readLock();
  7. // 写锁
  8. private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
  9. public Object read(){
  10. LoggerUtils.LOGGER.debug("获取读锁...");
  11. r.lock();
  12. try {
  13. LoggerUtils.LOGGER.debug("读取数据");
  14. Sleeper.sleep(1);
  15. return data;
  16. }finally {
  17. LoggerUtils.LOGGER.debug("释放读锁...");
  18. r.unlock();
  19. }
  20. }
  21. public void write(){
  22. LoggerUtils.LOGGER.debug("获取写锁...");
  23. w.lock();
  24. try {
  25. LoggerUtils.LOGGER.debug("写入数据");
  26. Sleeper.sleep(1);
  27. }finally {
  28. LoggerUtils.LOGGER.debug("释放写锁...");
  29. w.unlock();
  30. }
  31. }
  32. }

测试 读锁-读锁 可以并发

  1. DataContainer dataContainer = new DataContainer();
  2. new Thread(()->{
  3. dataContainer.read();
  4. }, "t1").start();
  5. new Thread(()->{
  6. dataContainer.read();
  7. }, "t2").start();

输出结果,从这里可以看到 Thread-0 锁定期间,Thread-1 的读操作不受影响

  1. 14:40:04.638 cn.util.LoggerUtils [t2] - 获取读锁...
  2. 14:40:04.638 cn.util.LoggerUtils [t1] - 获取读锁...
  3. 14:40:04.640 cn.util.LoggerUtils [t2] - 读取数据
  4. 14:40:04.640 cn.util.LoggerUtils [t1] - 读取数据
  5. 14:40:05.642 cn.util.LoggerUtils [t2] - 释放读锁...
  6. 14:40:05.642 cn.util.LoggerUtils [t1] - 释放读锁...

测试 读锁-写锁 相互阻塞

  1. DataContainer dataContainer = new DataContainer();
  2. new Thread(()->{
  3. dataContainer.read();
  4. }, "t1").start();
  5. new Thread(()->{
  6. dataContainer.write();
  7. }, "t2").start();

输出结果

  1. 14:41:08.246 cn.util.LoggerUtils [t1] - 获取读锁...
  2. 14:41:08.246 cn.util.LoggerUtils [t2] - 获取写锁...
  3. 14:41:08.249 cn.util.LoggerUtils [t1] - 读取数据
  4. 14:41:09.251 cn.util.LoggerUtils [t1] - 释放读锁...
  5. 14:41:09.251 cn.util.LoggerUtils [t2] - 写入数据
  6. 14:41:10.252 cn.util.LoggerUtils [t2] - 释放写锁...

写锁-写锁 也是相互阻塞的,这里就不测试了

注意事项

  • 读锁不支持条件变量,写锁支持条件变量

  • 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待

    1. r.lock(); // 获取读锁
    2. try {
    3. // ...
    4. w.lock(); // 有读锁的情况下去获取写作,会导致写锁永久等待
    5. try {
    6. // ...
    7. } finally{
    8. w.unlock();
    9. }
    10. } finally{
    11. r.unlock();
    12. }
  • 重入时降级支持:即持有写锁的情况下去获取读锁

    1. class CachedData {
    2. Object data; // 需要缓存的数据
    3. // 是否有效,如果失效,需要重新计算 data
    4. volatile boolean cacheValid; // 数据是否有效
    5. final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    6. void processCachedData() {
    7. rwl.readLock().lock(); // 加一个读锁
    8. if (!cacheValid) { // 数据已经失效
    9. // 获取写锁前必须释放读锁,因为不支持升级
    10. rwl.readLock().unlock();
    11. rwl.writeLock().lock();
    12. try {
    13. // 判断是否有其它线程已经获取了写锁、更新了缓存, 避免重复更新
    14. if (!cacheValid) {
    15. data = ...
    16. cacheValid = true;
    17. }
    18. // 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存
    19. rwl.readLock().lock();
    20. } finally {
    21. rwl.writeLock().unlock();
    22. }
    23. }
    24. // 自己用完数据, 释放读锁
    25. try {
    26. use(data);
    27. } finally {
    28. rwl.readLock().unlock();
    29. }
    30. }
    31. }
应用:缓存

1. 缓存更新策略

更新时,是先清缓存还是先更新数据库

  • 先清缓存

  • 先更新数据库

  • 补充一种情况,假设查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询

这种情况的出现几率非常小,见 facebook 论文

2. 读写锁实现一致性缓存

使用读写锁实现一个简单的按需加载缓存

  1. class GenericCachedDao<T> {
  2. // HashMap 作为缓存非线程安全, 需要保护
  3. HashMap<SqlPair, T> map = new HashMap<>();
  4. ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
  5. GenericDao genericDao = new GenericDao();
  6. public int update(String sql, Object... params) {
  7. SqlPair key = new SqlPair(sql, params);
  8. // 加写锁, 防止其它线程对缓存读取和更改
  9. lock.writeLock().lock();
  10. try {
  11. int rows = genericDao.update(sql, params);
  12. map.clear(); // 清空缓存
  13. return rows;
  14. } finally {
  15. lock.writeLock().unlock();
  16. }
  17. }
  18. public T queryOne(Class<T> beanClass, String sql, Object... params) {
  19. SqlPair key = new SqlPair(sql, params);
  20. // 加读锁, 防止其它线程对缓存更改
  21. lock.readLock().lock();
  22. try {
  23. T value = map.get(key); // 从缓存里获取数据
  24. if (value != null) {
  25. return value;
  26. }
  27. } finally {
  28. lock.readLock().unlock();
  29. }
  30. // 到这里说明缓存失效了
  31. // 加写锁, 防止其它线程对缓存读取和更改
  32. lock.writeLock().lock();
  33. try {
  34. // get 方法上面部分是可能多个线程进来的, 可能已经向缓存填充了数据
  35. // 为防止重复查询数据库, 再次验证
  36. T value = map.get(key);
  37. if (value == null) {
  38. // 如果没有, 查询数据库
  39. value = genericDao.queryOne(beanClass, sql, params);
  40. map.put(key, value); // 将查询的数据放入缓存
  41. }
  42. return value;
  43. } finally {
  44. lock.writeLock().unlock();
  45. }
  46. }
  47. // 作为 key 保证其是不可变的
  48. class SqlPair {
  49. private String sql;
  50. private Object[] params;
  51. public SqlPair(String sql, Object[] params) {
  52. this.sql = sql;
  53. this.params = params;
  54. }
  55. @Override
  56. public boolean equals(Object o) {
  57. if (this == o) {
  58. return true;
  59. }
  60. if (o == null || getClass() != o.getClass()) {
  61. return false;
  62. }
  63. SqlPair sqlPair = (SqlPair) o;
  64. return sql.equals(sqlPair.sql) &&
  65. Arrays.equals(params, sqlPair.params);
  66. }
  67. @Override
  68. public int hashCode() {
  69. int result = Objects.hash(sql);
  70. result = 31 * result + Arrays.hashCode(params);
  71. return result;
  72. }
  73. }
  74. }

注意

  • 以上实现体现的是读写锁的应用,保证缓存和数据库的一致性,但有下面的问题没有考虑

    • 适合读多写少,如果写操作比较频繁,以上实现性能低
    • 没有考虑缓存容量
    • 没有考虑缓存过期
    • 只适合单机
    • 并发性还是低,目前只会用一把锁
    • 更新方法太过简单粗暴,清空了所有 key(考虑按类型分区或重新设计 key)
  • 乐观锁实现:用 CAS 去更新
原理:读写锁原理

这部分的内容也暂时放放了,有待理解

3.2 StampedLock

该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合 使用

加解读锁

  1. long stamp = lock.readLock();
  2. lock.unlockRead(stamp);

加解写锁

  1. long stamp = lock.writeLock();
  2. lock.unlockWrite(stamp);

乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。

  1. long stamp = lock.tryOptimisticRead();
  2. // 验戳
  3. if(!lock.validate(stamp)){
  4. // 锁升级
  5. }

提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

  1. class DataContainerStamped{
  2. private int data;
  3. private final StampedLock lock = new StampedLock();
  4. public DataContainerStamped(int data) {
  5. this.data = data;
  6. }
  7. public int read(int readTime){
  8. // 获取乐观读锁
  9. long stamp = lock.tryOptimisticRead();
  10. LoggerUtils.LOGGER.debug("optimistic read locking...{}", stamp);
  11. Sleeper.sleep(readTime);
  12. if(lock.validate(stamp)){ // 对读锁进行校验
  13. LoggerUtils.LOGGER.debug("read finish...{}, data:{}", stamp, data);
  14. return data;
  15. }
  16. // 锁升级 - 读锁
  17. LoggerUtils.LOGGER.debug("updating to read lock... {}", stamp);
  18. try {
  19. stamp = lock.readLock();
  20. LoggerUtils.LOGGER.debug("read lock {}", stamp);
  21. Sleeper.sleep(readTime);
  22. LoggerUtils.LOGGER.debug("read finish...{}, data:{}", stamp, data);
  23. return data;
  24. }finally {
  25. LoggerUtils.LOGGER.debug("read unlock {}", stamp);
  26. lock.unlockRead(stamp);
  27. }
  28. }
  29. public void write(int newData){
  30. // 获取写锁去更新数据
  31. long stamp = lock.writeLock();
  32. LoggerUtils.LOGGER.debug("write lock {}", stamp);
  33. try {
  34. Sleeper.sleep(2);
  35. this.data = data;
  36. }finally {
  37. LoggerUtils.LOGGER.debug("write unlock {}", stamp);
  38. lock.unlockWrite(stamp);
  39. }
  40. }
  41. }

测试 读-读 可以优化

  1. DataContainerStamped dataContainer = new DataContainerStamped(1);
  2. new Thread(()->{
  3. dataContainer.read(1);
  4. }, "t1").start();
  5. Sleeper.sleep(0.5);
  6. new Thread(()->{
  7. dataContainer.read(0);
  8. }, "t2").start();

输出结果,可以看到实际没有加读锁

  1. 16:25:04.007 cn.util.LoggerUtils [t1] - optimistic read locking...256
  2. 16:25:04.272 cn.util.LoggerUtils [t2] - optimistic read locking...256
  3. 16:25:04.272 cn.util.LoggerUtils [t2] - read finish...256, data:1
  4. 16:25:05.010 cn.util.LoggerUtils [t1] - read finish...256, data:1

测试 读-写 时优化读补加读锁

  1. DataContainerStamped dataContainer = new DataContainerStamped(1);
  2. new Thread(()->{
  3. dataContainer.read(1);
  4. }, "t1").start();
  5. Sleeper.sleep(0.5);
  6. new Thread(()->{
  7. dataContainer.write(2);
  8. }, "t2").start();

输出结果

  1. 16:25:26.027 cn.util.LoggerUtils [t1] - optimistic read locking...256
  2. 16:25:26.295 cn.util.LoggerUtils [t2] - write lock 384
  3. 16:25:27.031 cn.util.LoggerUtils [t1] - updating to read lock... 256
  4. 16:25:28.296 cn.util.LoggerUtils [t2] - write unlock 384
  5. 16:25:28.298 cn.util.LoggerUtils [t1] - read lock 513
  6. 16:25:29.299 cn.util.LoggerUtils [t1] - read finish...513, data:1
  7. 16:25:29.299 cn.util.LoggerUtils [t1] - read unlock 513

注意

  • StampedLock 不支持条件变量
  • StampedLock 不支持可重入

4. Semaphore

基本使用

[ˈsɛməˌfɔr] 信号量,用来限制能同时访问共享资源的线程上限。

  1. // 1. 创建 semaphore 对象
  2. Semaphore s = new Semaphore(3); // 限制上限为3
  3. // 2. 10个线程同时运行
  4. for (int i = 0; i < 10; i++) {
  5. new Thread(()->{
  6. // 3. 获取许可
  7. try {
  8. s.acquire(); // 获得此信号量
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. try {
  13. LoggerUtils.LOGGER.debug("running...");
  14. Sleeper.sleep(1);
  15. LoggerUtils.LOGGER.debug("end...");
  16. }finally {
  17. // 4. 释放许可
  18. s.release();
  19. }
  20. }).start();
  21. }

输出:

  1. 16:43:46.717 cn.util.LoggerUtils [Thread-0] - running...
  2. 16:43:46.718 cn.util.LoggerUtils [Thread-1] - running...
  3. 16:43:46.718 cn.util.LoggerUtils [Thread-2] - running...
  4. 16:43:47.723 cn.util.LoggerUtils [Thread-0] - end...
  5. 16:43:47.723 cn.util.LoggerUtils [Thread-2] - end...
  6. 16:43:47.723 cn.util.LoggerUtils [Thread-1] - end...
  7. 16:43:47.723 cn.util.LoggerUtils [Thread-3] - running...
  8. 16:43:47.723 cn.util.LoggerUtils [Thread-4] - running...
  9. 16:43:47.723 cn.util.LoggerUtils [Thread-5] - running...
  10. 16:43:48.723 cn.util.LoggerUtils [Thread-3] - end...
  11. 16:43:48.723 cn.util.LoggerUtils [Thread-4] - end...
  12. 16:43:48.723 cn.util.LoggerUtils [Thread-5] - end...
  13. 16:43:48.723 cn.util.LoggerUtils [Thread-6] - running...
  14. 16:43:48.723 cn.util.LoggerUtils [Thread-7] - running...
  15. 16:43:48.723 cn.util.LoggerUtils [Thread-8] - running...
  16. 16:43:49.723 cn.util.LoggerUtils [Thread-6] - end...
  17. 16:43:49.723 cn.util.LoggerUtils [Thread-7] - end...
  18. 16:43:49.723 cn.util.LoggerUtils [Thread-9] - running...
  19. 16:43:49.724 cn.util.LoggerUtils [Thread-8] - end...
  20. 16:43:50.724 cn.util.LoggerUtils [Thread-9] - end...
应用:Semaphore 应用

限制对共享资源的使用:semaphore 实现

  • 使用 Semaphore 限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可,当然它只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比 Tomcat LimitLatch 的实现)
  • 用 Semaphore 实现简单连接池,对比『享元模式』下的实现(用wait notify),性能和可读性显然更好,注意下面的实现中线程数和数据库连接数是相等的
  1. class Pool{
  2. // 1. 连接池大小
  3. private final int poolSize;
  4. // 2. 连接对象数组
  5. private Connection[] connections;
  6. // 3. 连接状态数组 0 表示空闲, 1 表示繁忙
  7. private AtomicIntegerArray states;
  8. // new:创建 semaphore 对象
  9. private Semaphore semaphore;
  10. // 4. 构造方法初始化
  11. public Pool(int poolSize) {
  12. this.poolSize = poolSize;
  13. // 让许可数与资源数一致
  14. this.semaphore = new Semaphore(poolSize);
  15. this.connections = new Connection[poolSize];
  16. this.states = new AtomicIntegerArray(new int[poolSize]);
  17. for (int i = 0; i < poolSize; i++) {
  18. connections[i] = new MockConnection("连接" + (i+1));
  19. }
  20. }
  21. // 5. 借连接
  22. public Connection borrow(){
  23. // 获取许可
  24. try {
  25. semaphore.acquire();
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. for (int i = 0; i < poolSize; i++) {
  30. // 获取空闲连接
  31. if(states.get(i) == 0){
  32. if(states.compareAndSet(i, 0, 1)){
  33. log.debug("borrow {}", connections[i]);
  34. return connections[i];
  35. }
  36. }
  37. }
  38. // 不会执行到这里
  39. return null;
  40. }
  41. // 6. 归还连接
  42. public void free(Connection connection){
  43. for (int i = 0; i < poolSize; i++) {
  44. if(connections[i] == connection){
  45. states.set(i, 0);
  46. log.debug("free {}", conn);
  47. // 有空闲了,则释放许可
  48. semaphore.release();
  49. break;
  50. }
  51. }
  52. }
  53. }
原理:Semaphore 原理

挖坑...

5. CountdownLatch

用来进行线程同步协作,等待所有线程完成倒计时。

其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一

  1. public static void main(String[] args) throws InterruptedException {
  2. CountDownLatch latch = new CountDownLatch(3);
  3. new Thread(() -> {
  4. LoggerUtils.LOGGER.debug("begin...");
  5. Sleeper.sleep(1);
  6. latch.countDown();
  7. LoggerUtils.LOGGER.debug("end...{}", latch.getCount());
  8. }).start();
  9. new Thread(() -> {
  10. LoggerUtils.LOGGER.debug("begin...");
  11. Sleeper.sleep(2);
  12. latch.countDown();
  13. LoggerUtils.LOGGER.debug("end...{}", latch.getCount());
  14. }).start();
  15. new Thread(() -> {
  16. LoggerUtils.LOGGER.debug("begin...");
  17. Sleeper.sleep(2.5);
  18. latch.countDown();
  19. LoggerUtils.LOGGER.debug("end...{}", latch.getCount());
  20. }).start();
  21. LoggerUtils.LOGGER.debug("main waiting...");
  22. latch.await();
  23. LoggerUtils.LOGGER.debug("wait end...");
  24. }

输出:

  1. 23:03:23.041 cn.util.LoggerUtils [main] - main waiting...
  2. 23:03:23.041 cn.util.LoggerUtils [Thread-2] - begin...
  3. 23:03:23.041 cn.util.LoggerUtils [Thread-0] - begin...
  4. 23:03:23.041 cn.util.LoggerUtils [Thread-1] - begin...
  5. 23:03:24.045 cn.util.LoggerUtils [Thread-0] - end...2
  6. 23:03:25.045 cn.util.LoggerUtils [Thread-1] - end...1
  7. 23:03:25.545 cn.util.LoggerUtils [Thread-2] - end...0
  8. 23:03:25.545 cn.util.LoggerUtils [main] - wait end...

可以配合线程池使用,改进如下:

  1. public static void main(String[] args) throws InterruptedException {
  2. CountDownLatch latch = new CountDownLatch(3);
  3. ExecutorService service = Executors.newFixedThreadPool(4);
  4. service.submit(() -> {
  5. LoggerUtils.LOGGER.debug("begin...");
  6. Sleeper.sleep(1);
  7. latch.countDown();
  8. LoggerUtils.LOGGER.debug("end...{}", latch.getCount());
  9. });
  10. service.submit(() -> {
  11. LoggerUtils.LOGGER.debug("begin...");
  12. Sleeper.sleep(2);
  13. latch.countDown();
  14. LoggerUtils.LOGGER.debug("end...{}", latch.getCount());
  15. });
  16. service.submit(() -> {
  17. LoggerUtils.LOGGER.debug("begin...");
  18. Sleeper.sleep(2.5);
  19. latch.countDown();
  20. LoggerUtils.LOGGER.debug("end...{}", latch.getCount());
  21. });
  22. service.submit(()->{
  23. try {
  24. LoggerUtils.LOGGER.debug("waiting...");
  25. latch.await();
  26. LoggerUtils.LOGGER.debug("wait end...");
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. });
  31. service.shutdown();
  32. }

输出:

  1. 23:08:32.160 cn.util.LoggerUtils [pool-1-thread-4] - waiting...
  2. 23:08:32.160 cn.util.LoggerUtils [pool-1-thread-2] - begin...
  3. 23:08:32.160 cn.util.LoggerUtils [pool-1-thread-1] - begin...
  4. 23:08:32.160 cn.util.LoggerUtils [pool-1-thread-3] - begin...
  5. 23:08:33.163 cn.util.LoggerUtils [pool-1-thread-1] - end...2
  6. 23:08:34.163 cn.util.LoggerUtils [pool-1-thread-2] - end...1
  7. 23:08:34.663 cn.util.LoggerUtils [pool-1-thread-3] - end...0
  8. 23:08:34.663 cn.util.LoggerUtils [pool-1-thread-4] - wait end...
应用:同步等待多线程准备完毕
  1. public static void main(String[] args) throws InterruptedException {
  2. AtomicInteger num = new AtomicInteger(0);
  3. ExecutorService service = Executors.newFixedThreadPool(10, (r)->{
  4. return new Thread(r, "t"+num.getAndIncrement());
  5. });
  6. CountDownLatch latch = new CountDownLatch(10);
  7. String[] all = new String[10];
  8. Random r = new Random();
  9. for (int i = 0; i < 10; i++) {
  10. int j = i;
  11. service.submit(()->{
  12. for (int k = 0; k <= 100; k++) {
  13. try {
  14. Thread.sleep(r.nextInt(100));
  15. }catch (InterruptedException e){
  16. }
  17. all[j] = Thread.currentThread().getName() + "(" + (k + "%") + ")";
  18. System.out.print("\r" + Arrays.toString(all)); // "\r":在同一行刷新输出
  19. }
  20. latch.countDown();
  21. });
  22. }
  23. latch.await();
  24. System.out.println("\n游戏开始...");
  25. service.shutdown();
  26. }

中间输出:

  1. [t0(38%), t1(44%), t2(41%), t3(39%), t4(41%), t5(39%), t6(32%), t7(43%), t8(41%), t9(36%)]

最后输出:

  1. [t0(100%), t1(100%), t2(100%), t3(100%), t4(100%), t5(100%), t6(100%), t7(100%), t8(100%), t9(100%)]
  2. 游戏开始...
应用:同步等待多个远程调用结束
  1. @RestController
  2. public class TestCountDownlatchController {
  3. @GetMapping("/order/{id}")
  4. public Map<String, Object> order(@PathVariable int id) {
  5. HashMap<String, Object> map = new HashMap<>();
  6. map.put("id", id);
  7. map.put("total", "2300.00");
  8. sleep(2000);
  9. return map;
  10. }
  11. @GetMapping("/product/{id}")
  12. public Map<String, Object> product(@PathVariable int id) {
  13. HashMap<String, Object> map = new HashMap<>();
  14. if (id == 1) {
  15. map.put("name", "小爱音箱");
  16. map.put("price", 300);
  17. } else if (id == 2) {
  18. map.put("name", "小米手机");
  19. map.put("price", 2000);
  20. }
  21. map.put("id", id);
  22. sleep(1000);
  23. return map;
  24. }
  25. @GetMapping("/logistics/{id}")
  26. public Map<String, Object> logistics(@PathVariable int id) {
  27. HashMap<String, Object> map = new HashMap<>();
  28. map.put("id", id);
  29. map.put("name", "中通快递");
  30. sleep(2500);
  31. return map;
  32. }
  33. private void sleep(int millis) {
  34. try {
  35. Thread.sleep(millis);
  36. } catch (InterruptedException e) {
  37. e.printStackTrace();
  38. }
  39. }
  40. }

rest 远程调用

  1. 通过 CountDownLatch 方式同步:

    1. RestTemplate restTemplate = new RestTemplate();
    2. log.debug("begin");
    3. ExecutorService service = Executors.newCachedThreadPool();
    4. CountDownLatch latch = new CountDownLatch(4);
    5. service.submit(() -> {
    6. Map<String, Object> r =
    7. restTemplate.getForObject("http://localhost:8080/order/{1}", Map.class, 1);
    8. Log.debug("end order: {}", r);
    9. latch.countDown();
    10. });
    11. service.submit(() -> {
    12. Map<String, Object> r =
    13. restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 1);
    14. Log.debug("end product: {}", r);
    15. latch.countDown();
    16. });
    17. service.submit(() -> {
    18. Map<String, Object> r =
    19. restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 2);
    20. Log.debug("end prodect: {}", r);
    21. latch.countDown();
    22. });
    23. service.submit(() -> {
    24. Map<String, Object> r =
    25. restTemplate.getForObject("http://localhost:8080/logistics/{1}", Map.class, 1);
    26. Log.debug("end logistics: {}", r);
    27. latch.countDown();
    28. });
    29. latch.wait();
    30. log.debug("执行完毕");
    31. service.shutdown();

    问题:输出的内容都会在线程池的中,并没有传递给主线程,主线程如何获取线程执行结果?如下:

  2. 通过 Future

    1. RestTemplate restTemplate = new RestTemplate();
    2. log.debug("begin");
    3. ExecutorService service = Executors.newCachedThreadPool();
    4. Future<Map<String,Object>> f1 = service.submit(() -> {
    5. Map<String, Object> r =
    6. restTemplate.getForObject("http://localhost:8080/order/{1}", Map.class, 1);
    7. return r;
    8. });
    9. Future<Map<String, Object>> f2 = service.submit(() -> {
    10. Map<String, Object> r =
    11. restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 1);
    12. return r;
    13. });
    14. Future<Map<String, Object>> f3 = service.submit(() -> {
    15. Map<String, Object> r =
    16. restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 2);
    17. return r;
    18. });
    19. Future<Map<String, Object>> f4 = service.submit(() -> {
    20. Map<String, Object> r =
    21. restTemplate.getForObject("http://localhost:8080/logistics/{1}", Map.class, 1);
    22. return r;
    23. });
    24. System.out.println(f1.get());
    25. System.out.println(f2.get());
    26. System.out.println(f3.get());
    27. System.out.println(f4.get());
    28. log.debug("执行完毕");
    29. service.shutdown();

    执行结果

    1. 19:51:39.711 c.TestCountDownLatch [main] - begin
    2. {total=2300.00, id=1}
    3. {price=300, name=小爱音箱, id=1}
    4. {price=2000, name=小米手机, id=2}
    5. {name=中通快递, id=1}
    6. 19:51:42.407 c.TestCountDownLatch [main] - 执行完毕

6. CyclicBarrier

[ˈsaɪklɪk ˈbæriɚ] 循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行

  1. CyclicBarrier cb = new CyclicBarrier(2); // 个数为2时才会继续执行
  2. new Thread(()->{
  3. System.out.println("线程1开始.."+new Date());
  4. try {
  5. cb.await(); // 当个数不足时,等待
  6. } catch (InterruptedException | BrokenBarrierException e) {
  7. e.printStackTrace();
  8. }
  9. System.out.println("线程1继续向下运行..."+new Date());
  10. }).start();
  11. new Thread(()->{
  12. System.out.println("线程2开始.."+new Date());
  13. try {
  14. Thread.sleep(2000);
  15. } catch (InterruptedException e) { }
  16. try {
  17. cb.await(); // 2 秒后,线程个数够2,继续运行
  18. } catch (InterruptedException | BrokenBarrierException e) {
  19. e.printStackTrace();
  20. }
  21. System.out.println("线程2继续向下运行..."+new Date());
  22. }).start();

注意 CyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的

CyclicBarrier 可以被比喻为『人满发车』

7. 线程安全集合类概述

线程安全集合类可以分为三大类:

  • 遗留的线程安全集合如 HashtableVector
  • 使用 Collections 装饰的线程安全集合,如:
    • Collections.synchronizedCollection
    • Collections.synchronizedList
    • Collections.synchronizedMap
    • Collections.synchronizedSet
    • Collections.synchronizedNavigableMap
    • Collections.synchronizedNavigableSet
    • Collections.synchronizedSortedMap
    • Collections.synchronizedSortedSet
  • java.util.concurrent.*

重点介绍 java.util.concurrent.* 下的线程安全集合类,可以发现它们有规律,里面包含三类关键词:Blocking、CopyOnWrite、Concurrent

  • Blocking 大部分实现基于锁,并提供用来阻塞的方法
  • CopyOnWrite 之类容器修改开销相对较重
  • Concurrent 类型的容器
    • 内部很多操作使用 cas 优化,一般可以提供较高吞吐量
    • 弱一致性
      • 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的
      • 求大小弱一致性,size 操作未必是 100% 准确
      • 读取弱一致性

遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出ConcurrentModificationException,不再继续遍历

8. ConcurrentHashMap

可见之前的博文:Java:ConcurrentHashMap类小记

练习:单词计数

生成测试数据

  1. static final String ALPHA = "abcedfghijklmnopqrstuvwxyz";
  2. public static void main(String[] args) {
  3. int length = ALPHA.length();
  4. int count = 200;
  5. List<String> list = new ArrayList<>(length * count);
  6. for (int i = 0; i < length; i++) {
  7. char ch = ALPHA.charAt(i);
  8. for (int j = 0; j < count; j++) {
  9. list.add(String.valueOf(ch));
  10. }
  11. }
  12. Collections.shuffle(list);
  13. for (int i = 0; i < 26; i++) {
  14. try (PrintWriter out = new PrintWriter(
  15. new OutputStreamWriter(
  16. new FileOutputStream("./tmp/"+(i+1)+".txt")))){
  17. String collect = list.subList(i*count, (i+1)*count).stream().collect(Collectors.joining("\n"));
  18. out.print(collect);
  19. }catch (IOException e){
  20. e.printStackTrace();
  21. }
  22. finally { }
  23. }
  24. }

模版代码,模版代码中封装了多线程读取文件的代码

  1. private static <V> void demo(Supplier<Map<String, V>> supplier,
  2. BiConsumer<Map<String, V>, List<String>> consumer){
  3. Map<String, V> counterMap = supplier.get();
  4. List<Thread> ts = new ArrayList<>();
  5. for (int i = 1; i <= 26; i++) {
  6. int idx = i;
  7. Thread thread = new Thread(()->{
  8. List<String> words = readFromFile(idx);
  9. consumer.accept(counterMap, words);
  10. });
  11. ts.add(thread);
  12. }
  13. ts.forEach(t->t.start());
  14. ts.forEach(t->{
  15. try {
  16. t.join();
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. });
  21. System.out.println(counterMap);
  22. }
  23. public static List<String> readFromFile(int i){
  24. ArrayList<String> words = new ArrayList<>();
  25. try (
  26. BufferedReader in = new BufferedReader(
  27. new InputStreamReader(
  28. new FileInputStream("./tmp/"+i+".txt")))) {
  29. while (true){
  30. String word = in.readLine();
  31. if(word == null){
  32. break;
  33. }
  34. words.add(word);
  35. }
  36. }catch (IOException e){
  37. e.printStackTrace();
  38. }
  39. return words;
  40. }

你要做的是实现两个参数

  • 一是提供一个 map 集合,用来存放每个单词的计数结果,key 为单词,value 为计数
  • 二是提供一组操作,保证计数的安全性,会传递 map 集合以及 单词 List

正确结果输出应该是每个单词出现 200 次

  1. {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}

下面的实现为:

  1. demo(
  2. // 创建 map 集合
  3. ()->new HashMap<String, Integer>(),
  4. // 进行计数
  5. (map, words)->{
  6. for(String word: words){
  7. Integer count = map.get(word);
  8. int newValue = count==null ? 1 : count+1;
  9. map.put(word, newValue);
  10. }
  11. }
  12. );

有没有问题?请改进

参考解答1:

  1. demo(
  2. ()->new ConcurrentHashMap<String, LongAdder>(),
  3. (map, words)->{
  4. for(String word: words){
  5. // 如果缺少一个key,则计算生成一个值,然后将key value放入map中
  6. LongAdder value = map.computeIfAbsent(word, (key)->new LongAdder());
  7. // 执行累加
  8. value.increment();
  9. }
  10. }
  11. );

参考解答2:

  1. demo(
  2. ()->new ConcurrentHashMap<String, Integer>(),
  3. (map, words)->{
  4. for(String word: words){
  5. // 函数式编程,无需原子变量
  6. map.merge(word, 1, Integer::sum);
  7. }
  8. }
  9. );
原理:ConcurrentHashMap 原理

挖坑

后续还有些关于 BlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue等内容,同样对其也缺乏一定的理解,等有一定功力后再回来填坑了

9. CopyOnWriteArrayList

CopyOnWriteArraySet 是它的马甲底层实现采用了 写入时拷贝的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其它线程的并发读读写分离

以新增为例:

  1. public boolean add(E e) {
  2. synchronized (lock) {
  3. // 获取旧的数组
  4. Object[] es = getArray();
  5. int len = es.length;
  6. // 拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程)
  7. es = Arrays.copyOf(es, len + 1);
  8. // 添加新元素
  9. es[len] = e;
  10. // 替换旧的数组
  11. setArray(es);
  12. return true;
  13. }
  14. }

这里的源码版本是 Java 11,在 Java 1.8 中使用的是可重入锁而不是 synchronized,如下

  1. public boolean add(E e) {
  2. // 用了可重入锁
  3. final ReentrantLock lock = this.lock;
  4. lock.lock();
  5. try {
  6. // 获取旧的数组
  7. Object[] elements = getArray();
  8. int len = elements.length;
  9. // 拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程)
  10. Object[] newElements = Arrays.copyOf(elements, len + 1);
  11. // 添加新元素
  12. newElements[len] = e;
  13. // 替换旧的数组
  14. setArray(newElements);
  15. return true;
  16. } finally {
  17. lock.unlock();
  18. }
  19. }

其它读操作并未加锁,例如:

  1. public void forEach(Consumer<? super E> action) {
  2. Objects.requireNonNull(action);
  3. for (Object x : getArray()) {
  4. @SuppressWarnings("unchecked") E e = (E) x;
  5. action.accept(e);
  6. }
  7. }

适合『读多写少』的应用场景

get 弱一致性体现

时间点 操作
1 Thread-0 getArray()
2 Thread-1 getArray()
3 Thread-1 setArray(arrayCopy)
4 Thread-0 array[index]

代码体现:迭代器弱一致性

  1. CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
  2. list.add(1);
  3. list.add(2);
  4. list.add(3);
  5. Iterator<Integer> iter = list.iterator();
  6. new Thread(() -> {
  7. list.remove(0);
  8. System.out.println(list); // 打印 2 3
  9. }).start();
  10. TimeUnit.SECONDS.sleep(1l);
  11. while (iter.hasNext()) {
  12. System.out.println(iter.next()); // 打印 1 2 3
  13. }

不要觉得弱一致性就不好

  • 数据库的 MVCC(Multi-Version Concurrency Control,多版本并发控制) 都是弱一致性的表现
  • 并发高和一致性是矛盾的,需要权衡

Java:并发笔记-09的更多相关文章

  1. java并发笔记之四synchronized 锁的膨胀过程(锁的升级过程)深入剖析

    警告⚠️:本文耗时很长,先做好心理准备,建议PC端浏览器浏览效果更佳. 本篇我们讲通过大量实例代码及hotspot源码分析偏向锁(批量重偏向.批量撤销).轻量级锁.重量级锁及锁的膨胀过程(也就是锁的升 ...

  2. JAVA自学笔记09

    JAVA自学笔记09 1.子类的方法会把父类的同名方法覆盖(重写) 2.final: 1)可修饰类.方法.变量 2)修饰类时:此时该类变为最终类,它将无法成为父类而被继承 3)修饰方法时:该方法将无法 ...

  3. java并发笔记之证明 synchronized锁 是否真实存在

    警告⚠️:本文耗时很长,先做好心理准备 证明:偏向锁.轻量级锁.重量级锁真实存在 由[java并发笔记之java线程模型]链接: https://www.cnblogs.com/yuhangwang/ ...

  4. Java并发笔记——单例与双重检测

    单例模式可以使得一个类只有一个对象实例,能够减少频繁创建对象的时间和空间开销.单线程模式下一个典型的单例模式代码如下: ① class Singleton{ private static Single ...

  5. java并发笔记之synchronized 偏向锁 轻量级锁 重量级锁证明

    警告⚠️:本文耗时很长,先做好心理准备 本篇将从hotspot源码(64 bits)入手,通过分析java对象头引申出锁的状态:本文采用大量实例及分析,请耐心看完,谢谢   先来看一下hotspot的 ...

  6. Java并发笔记-未完待续待详解

    为什么需要并行? – 业务要求 – 性能 并行计算还出于业务模型的需要 – 并不是为了提高系统性能,而是确实在业务上需要多个执行单元. – 比如HTTP服务器,为每一个Socket连接新建一个处理线程 ...

  7. Java并发笔记(二)

    1. 活跃性危险 死锁(最常见) 饥饿 当线程由于无法访问它所需的资源而不能继续执行时,就发生了饥饿.引发饥饿最常见资源就是CPU时钟周期. 活锁 活锁指的是任务或者执行者没有被阻塞,由于某些条件没有 ...

  8. Java并发笔记(一)

    1. lock (todo) 2. 写时复制容器 CopyOnWrite容器即写时复制的容器.通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个 ...

  9. java并发笔记之java线程模型

    警告⚠️:本文耗时很长,先做好心理准备 java当中的线程和操作系统的线程是什么关系? 猜想: java thread —-对应-—> OS thread Linux关于操作系统的线程控制源码: ...

随机推荐

  1. shell运算方式

    1.(())--整数运算 [root@m01 /server/scripts]# a=1 [root@m01 /server/scripts]# b=2 [root@m01 /server/scrip ...

  2. AWVS13批量添加目标脚本

    # -*-coding:utf-8-*- # @Author:malphite.tang import json import requests from queue import Queue req ...

  3. 深入xLua实现原理之Lua如何调用C#

    xLua是腾讯的一个开源项目,为Unity. .Net. Mono等C#环境增加Lua脚本编程的能力.本文主要是探讨xLua下Lua调用C#的实现原理. Lua与C#数据通信机制 无论是Lua调用C# ...

  4. 【简单数据结构】二叉树的建立和递归遍历--洛谷 P1305

    题目描述 输入一串二叉树,用遍历前序打出. 输入格式 第一行为二叉树的节点数n.(n \leq 26n≤26) 后面n行,每一个字母为节点,后两个字母分别为其左右儿子. 空节点用*表示 输出格式 前序 ...

  5. Stage 1 项目需求分析报告

    迷你商城后台管理系统-- 需求分析 1. 引言 作为互联网热潮的崛起,消费者们的普遍差异化,实体商城要想在互联网的浪潮中继续发展,就需要制定出针对用户以及消费者的消费习惯以及喜爱品种的消费方案.从而企 ...

  6. 了解PHP-FPM

    在服务器上,当我们查看php进程时,全都是php-fpm进程,大家都知道这个就是php的运行环境,那么,它到底是个什么东西呢? PHP-FPM简介 PHP-FPM,就是PHP的FastCGI管理器,用 ...

  7. ubuntu提示:无法获得锁 /var/lib/dpkg/lock-frontend - open (11: 资源暂时不可用)

    root@uni-virtual-machine:/home/uni# apt install apt-transport-https ca-certificates curl software-pr ...

  8. django错误处理

    1.django.db.utils.OperationalError: no such table 意思:没有这个app应用对应的数据表的,可以用 python manage.py makemigra ...

  9. web自动化:IE11运行Python+selenium程序

    from selenium import webdriver # 运行此脚本前必须按要求修改注册表'''[HKEY_CURRENT_USER\Software\Microsoft\Internet E ...

  10. english note(6.17to6.23)

    6.17 http://www.51voa.com/VOA_Special_English/are-these-us-treasures-about-to-be-destroyed-82260_1.h ...