/Java_ConcurrentPractice

Some code about concurrency

Primary LanguageJava

Java_ConcurrentPractice

并发练习系列

  • 生产者消费者
  • 线程池
  • LRU 博文地址
  • 使用await, signal,signalAll实现生产者消费者
  • 多线程打印字符数组
  • 两个线程交替打印奇偶数
  • 设计一段死锁代码

myFuture:自己实现的Future模式,核心是加一个FutureData类,把设置数据和获取数据方法用synchronize修饰,目的是可以直接返回FutureData,但是RealData的结果阻塞获取。

JDKFuture:JDK实现的Future模式,使用juc包内的FutureTask代替我们自己写的FutureData。给FutureTask传入一个继承callable接口的线程类实例(这句话怎么这么拗口呢...)换句话说,就是传入所要异步调用的任务新建线程池用于提交执行FutureTask,就可以在未来某个时间段获取的到返回值。

核心**是异步调用。去除了主函数的等待时间,并使得原本需要等待的时间段可以用于处理其他业务逻辑。用我的话解释就是,一个函数的调用必须花那么多时候,但你可以选择傻等或者机智的先去做其他事情。Future模式即相当于你拿到一个调用函数的凭证,在未来的某个时候,你可以去取得函数的结果

juc包脉络梳理

J.U.C 即java.util.concurrent,由Doug Lea大神编写,在jdk1.5之后引入,由提供java并发解决方案的包。

juc有两大核心原理:CAS是原子类的基础,AQS是锁、信号量等的基础。内容我将分为五部分介绍。

《java并发的艺术》指出了J.U.C包的基本操作

  • 首先,声明共享变量为 volatile
  • 然后,使用CAS的原子条件更新来实现线程之间的同步。
  • 同时,配合以 volatile的读/写和CAS所具有的 volatile读和写的内存语义来实现线程之间的通信。

两大核心

CAS是原子类的基础

AQS是锁、信号量等的基础

1、锁(AQS):可重入锁、读写锁、condition、lockSupport

读写锁

  • 读写锁维护了一对锁,一个读锁和一个写锁。读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞,目的是读取到正确的数据,不会出现脏读。
  • 它通过分离读和写,提示了性能。
  • 使用场景:读多写少,比如一个共享的用作缓存数据结构,大部分时间提供读服务(例如查询和搜索)。

2、线程执行器:线程池、FutureTask

线程池最重要的知识点是线程的创建时机。

  • 线程池是懒加载的,声明的时候并不会创建好线程等待任务,而是当提交第一个任务时才去新建线程;
  • 当提交一个任务时,如果当前线程数小于corePoolSize,就直接创建一个新线程执行任务;
  • 如果当前线程数大于corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;
  • 如果阻塞队列满了,并且当前线程数小于maxPoolSize,那就创建新的线程执行当前任务;
  • 如果池里的线程数大于maxPoolSize,这时再有任务来,只能调用拒绝策略

3、并发集合类:Chm、阻塞队列

chm见HashMap一文。

CopyOnWriteArrayList

读写分离

  • 写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
  • 写操作需要加锁,防止并发写入时导致写入数据丢失。
  • 写操作结束之后需要把原始数组指向新的复制数组。

适用场景

CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。

但是 CopyOnWriteArrayList 有其缺陷:

  • 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
  • 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。

所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。

阻塞队列

多用于生产者消费者模式。JDK7 提供了 7 个阻塞队列。分别是

  • ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
  • PriorityBlockingQueue :一个支持优先级排序无界阻塞队列。
  • DelayQueue:一个使用优先级队列实现的无界阻塞队列。
  • SynchronousQueue:一个不存储元素的阻塞队列。
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

以LinkedBlockingQueue为例子,最重要的两个方法是put和take。如果put或者take操作无法立即执行,这两个方法调用将会发生阻塞,直到能够执行。

抛出异常 特殊值 阻塞 超时
插入 add(e) offer(e) put(e) offer(e, time, unit)
移除 remove() poll() take() poll(time, unit)
检查 element() peek() 不可用 不可用

4、原子类(CAS)

原子类就具有原子操作特征的类,方便无锁的进行原子操作。

它基于CAS为原理,CAS全称 Compare And Swap(比较与交换),在更新一个变量的时候,拿预期值与当前内存里的值做比较,一致才做更新操作,否则抛弃当前更新值,进行重试或抛出异常。

原子类通过调用Unsafe类的本地方法compareAndSwapInt()来对变量最更新操作。

CAS的关键在于比较和更新这两个操作是一个整体,操作系统的CMPXCHG指令通过lock前缀保证比较和更新两个操作是原子的

CAS的是一种无锁算法(是没有线程被阻塞,而不是不加锁),合适读多写少的情况,原子类尤其适合更新变量的情况。

5、并发工具类信号量、倒计时器、循环栅栏、交换器、Executors

并发工具类包装了一些并发控制工具

  • 信号量Semaphore,限制访问资源的线程数目。
  • 倒计时器CountDownLatch,让主线程等待一组事件发生后继续执行,这里的事件就是指countDown()。类似“倒数十个数火箭就发射”。
CountDownLatch countDownLatch = new CountDownLatch(3);

//下面的代码是在每个线程里。。。
countDownLatch.countDown();
System.out.println("countDown()后的这句话不会被阻塞,每个线程还可以做其他工作");

//countDownLatch.await();
System.out.println("await()后的这句话会被阻塞,直到倒计数3次全完成了,才会输出");
  • 循环栅栏CyclicBarrier,**阻塞当前线程,等待其他线程都完成了,所有线程同时触发执行某事件。**类似“公寓的班车总是在公寓楼下装满一车人之后,出发并开到地铁站,接着再回来接下一班人。”构造函数需要传入两个参数,第一个参数指定我们的屏障最多拦截多少个线程后就打开屏障,第二个参数指明最后一个到达屏障的线程需要额外做的操作。注意屏障是循环发生的
 CyclicBarrier barrier = new CyclicBarrier(3,new BarrierTask)
//下面的代码是在每个线程里。。。
System.out.println(thread.getName()+"调用await方法");
 barrier.await();
 System.out.println("任务完成");
 barrier.await();
 System.out.println("任务完成");
 
 // BarrierTask任务是输出System.out.println("BarrierTask");
 
/*
         输出结果:
         thread1.调用await方法
         thread2.调用await方法
         BarrierTask
         任务完成
         thread1.调用await方法
         thread2.调用await方法
         BarrierTask
*/

与CountDownLatch与CyclicBarrier的区别:

  1. CountDownLatch一旦倒计数完成,await 方法就会失效,它是一次性的。CyclicBarrier可以重置倒计数器,它是循环发生的。
  2. CountDownLatch强调一个线程等待其他线程执行完成后才能继续执行,countDown()后的代码是不阻塞的,await()才是阻塞的。CountDownLatch强调线程之间的相互等待。它没有countDown()方法,用await()做计数。必须所有线程等准备完毕,一次性全部激活执行第二个参数的任务。像一个百米冲刺一样。

网上并发学习资源

不可不说的Java“锁”事 美团技术团队的文章