2.4 J.U.C包中的工具
2.4.1 重入锁ReentrantLock —— 独占锁/互斥锁
互斥性:同一时刻,只允许一个线程进入到加锁的代码中。保证在多线程环境下,线程的顺序访问
重入性:一个持有锁的线程,在释放锁之前,如果再次访问了该同步锁的其他方法,不需要再次争抢锁,只需要记录重入次数。
static Lock lock = new ReentrantLock();
lock.lock();//抢占锁,如果没有抢占到锁,会阻塞
if (lock.tryLock()) { //试图抢占锁,如果没有抢占到锁,不会阻塞
// 锁抢占成功才会执行这段代码
}
lock.unlock(); //释放锁
如下一段简单演示求和代码,正常情况下结果是1000,可是当多个线程同时执行自增操作,会发现结果可能小于1000。
public class ReentrantLockDemo {
static Lock lock = new ReentrantLock();
public static int count = 0;
public static void incr() {
try {
Thread.sleep(1);
count ++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(()->ReentrantLockDemo.incr()).start();
}
Thread.sleep(6000);
System.out.println("result=" + count);
}
}
针对上面问题,需要加锁,加锁后会发现结果永远都是1000
public class ReentrantLockDemo {
static Lock lock = new ReentrantLock();
public static int count = 0;
public static void incr() {
lock.lock();//抢占锁
try {
Thread.sleep(1);
count ++;
} catch (InterruptedException e) {
e.printStackTrace();
} finally { //一定要在finally块释放锁,否则万一上面出错,那么就会死锁
lock.unlock(); //释放锁
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(()->ReentrantLockDemo.incr()).start();
}
Thread.sleep(6000);
System.out.println("result=" + count);
}
}
下面简单代码演示锁的重入特性,两个加锁的方法,一个方法调用另外一个,如果没有重入特性,理论上就会出现死锁,但是使用ReentrantLock,同一个线程再次加锁,只会增加重入次数,不会再次抢占锁,避免死锁的情况出现。
static Lock lock = new ReentrantLock();
public static int count = 0;
public static void incre() {
lock.lock(); //获得锁
count ++; //被保护对象
decr(); //这个时候不需要再次争抢锁
lock.unlock(); //释放锁
}
public static void decr() {
lock.lock(); //获得锁
count --; //被保护对象
lock.unlock(); //释放锁
}
2.4.2 AQS(AbstractQueuedSynchronizer)
队列同步工具
state被 volatile 关键字修饰,从而保证可见性;
cas方法保证原子性操作;
CAS原子性操作
// Atomically sets synchronization state to the given updated value
// if the current state value equals the expected value.
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
public final class Unsafe {
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
}
锁的设计思想
- 一个全局变量来标识锁是否被抢占;
- 如何抢占锁;
- 如果抢占到锁:抢占到锁执行业务逻辑;
- 如果没有抢占到锁:定义一个阻塞队列(双向链表),将抢占锁失败的线程加到队列中排队等候;
- 如何释放锁:修改全局变量标识,唤醒处于队列中的指定线程;
2.4.3 CountDownLatch —— 共享锁
是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作完成之后再执行,通过倒数计数的方式来判断count=0则释放锁。
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(3); //初始化计数count=3
for (int i = 0; i < 3; i ++) { //那么必须要有3个线程被调用,否则将会一直等待
new CountDownThread((i+1), countDownLatch).start();
}
countDownLatch.await(); //阻塞主线程,直到count计数为0
System.out.println("主线程继续执行...");
}
}
public class CountDownThread extends Thread {
private int num;
private CountDownLatch countDownLatch;
public CountDownThread(int num, CountDownLatch countDownLatch) {
this.num = num;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
System.out.println("thread" + num + "执行...");
countDownLatch.countDown(); //倒计时
}
}
从结果可以看出来,无论三个线程的顺序怎么变,但是主线程总是在三个线程全部执行完毕才会被解锁。
// 模拟高并发
public class CountDownSimulateConcurrent implements Runnable {
private static CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args) {
for (int i = 0; i < 1000; i ++) {
new Thread(new CountDownSimulateConcurrent()).start();
}
countDownLatch.countDown(); //1000个线程全部启动后取消阻塞开始并发运行
}
@Override
public void run() {
try {
countDownLatch.await(); //阻塞主线程
// TODO 要测试高并发的代码
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
CountDown源码分析:CountDown有个内部类Sync继承自AQS
public CountDownLatch(int count) {//将count存入AQS的state
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count); //共享锁
}
Sync(int count) {
setState(count); //state=count, 如果AQS中state=0则await被唤醒,每次调用countDown,state就会被减一
}
避免死锁
因为必须保证执行countDown()方法将count变为0才会唤醒阻塞的线程,所以一旦线程数和计数器数量不相等,或者一个线程异常阻塞,没有执行countDown()方法,那么所有线程就会永久阻塞,导致死锁。
为了避免死锁,我们一般会将countDown()方法放在finally块中,保证程序在异常情况下也一定会执行countDown()方法;此外,设置一个带超时时间的await,如果到了超时时间会直接抛异常,从而避免死锁。
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(3); //初始化计数count=3
for (int i = 0; i < 3; i ++) { //那么必须要有3个线程被调用,否则将会一直等待
new CountDownThread((i+1), countDownLatch).start();
}
try {
// 带超时时间,避免长时间阻塞
countDownLatch.await(20,TimeUnit.SECONDS); //阻塞主线程,直到count计数为0
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("执行线程异常");
} finally {
System.out.println("执行线程关闭");
}
System.out.println("主线程继续执行...");
}
}
public class CountDownThread extends Thread {
private int num;
private CountDownLatch countDownLatch;
public CountDownThread(int num, CountDownLatch countDownLatch) {
this.num = num;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
System.out.println("thread" + num + "执行...");
int a = b / 0;
} catch(Exception e) {
e.printStackTrace();
} finally {
countDownLatch.countDown(); //倒计时
}
}
}
2.4.4 Semaphore —— 共享锁
信号灯,可以控制同时访问的线程的个数,通过acquire获得一个许可,如果没有许可就等待,通过release释放一个许可,类似于限流的作用。
public class SemaphoreDemo {
public static void main(String[] args) {
// 限制当前可以获取的最大许可数量是5
Semaphore semaphore = new Semaphore(5);
for (int i = 0; i < 10; i ++) {
new Car((i + 1), semaphore).start();
}
}
static class Car extends Thread {
private int num;
private Semaphore semaphore;
public Car(int num, Semaphore semaphore) {
this.num = num;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire(); //获得许可
System.out.println("第" + num + "辆车获得一个许可");
Thread.sleep(2000);
System.out.println("第" + num + "辆车释放许可");
semaphore.release(); //释放许可
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Semaphore源码分析:Semaphore有个内部类Sync继承自AQS
public Semaphore(int permits) { //将许可存入AQS的state
sync = new Semaphore.NonfairSync(permits);
}
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1); //获得共享锁
}
2.4.5 CyclicBarrier —— 独占锁
可循用使用屏障,当一组线程到达屏障点时会被阻塞,直到最后一个线程到达屏障点,所有线程的阻塞才会被打开,所有被屏障拦截的线程才会继续工作。
应用场景:当需要所有子任务都完成之后才执行主任务时,这时候可以使用CyclicBarrier。
注意:如果执行的线程数量没有达到定义的parties数量,会导致死锁,所以一般会通过两种方式解决这种问题
a. cyclicBarrier.await(10, TimeUnit.SECONDS);设置过期时间,如果到了过期时间阻塞会被打开。
b. cyclicBarrier.reset();通过reset方法重置计数,会抛出BrokenBarrierException,根据异常进行处理。
public class CyclicBarrierDemo extends Thread {
@Override
public void run() {
System.out.println("主线程开始执行....");
}
// 定义了parties=3,那么必须是3个子线程执行完毕也会打开阻塞屏障
// 如果少于三个子线程,那么会造成死锁
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new CyclicBarrierDemo());
new CyclicDataImportThread("path1", cyclicBarrier).start();
new CyclicDataImportThread("path2", cyclicBarrier).start();
new CyclicDataImportThread("path3", cyclicBarrier).start();
System.out.println("希望三个子线程全部执行完毕之后再执行主线程后续方法....");
}
}
public class CyclicDataImportThread extends Thread {
private String path;
private CyclicBarrier cyclicBarrier;
@Override
public void run() {
System.out.println(path + "位置数据开始导入...");
try {
cyclicBarrier.await(); //阻塞
System.out.println(path + "数据导入完成...");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
public CyclicDataImportThread(String path, CyclicBarrier cyclicBarrier) {
this.path = path;
this.cyclicBarrier = cyclicBarrier;
}
}
执行结果:
实现原理
用到了ReentrantLock去获得锁并且释放锁,通过自循环去判断count的值是否为0,不为0则await,直到所有子线程执行完毕,count为0则breakBarrier,重置count=parties,通过signalAll()唤醒所有被阻塞的线程,并且开启新一轮的generation。
2.4.6 Condition
多线程协调通信的工具类,可以让某些线程一起等待某个条件condition,只有满足条件时,线程才会被唤醒。
condition类似于wait / notify, condition是JUC包中对应Lock的实现,而wait / notify是对应的synchronized。
public class ConditionDemo {
public static void main(String[] args) {
Lock lock = new ReentrantLock(); //使用同一个锁
Condition condition = lock.newCondition(); //使用同一个condition
ConditionWaitThread conditionWaitThread = new ConditionWaitThread(lock, condition);
ConditionNotifyThread conditionNotifyThread = new ConditionNotifyThread(lock, condition);
conditionWaitThread.start();
conditionNotifyThread.start();
}
}
public class ConditionWaitThread extends Thread {
private Lock lock;
private Condition condition;
public ConditionWaitThread(Lock lock, Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
System.out.println("begin - condition wait");
try {
lock.lock(); //获得锁
condition.await(); //阻塞
System.out.println("end - condition wait");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); //释放锁
}
}
}
public class ConditionNotifyThread extends Thread {
private Lock lock;
private Condition condition;
public ConditionNotifyThread(Lock lock, Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
System.out.println("begin condition notify");
try {
lock.lock(); //获得锁
condition.signalAll(); //唤醒
System.out.println("end condition notify");
} finally {
lock.unlock(); //释放锁
}
}
}
通过condition实现一个简单的队列
public class ConditionBlockingQueueDemo {
// 已经添加的元素数量
private int size;
// 容器
private List<String> items;
// 队列的大小
private int count;
private Lock lock = new ReentrantLock();
// 让put方法阻塞
private Condition putCondition = lock.newCondition();
// 让take方法阻塞
private Condition takeCondition = lock.newCondition();
public ConditionBlockingQueueDemo(int count) {
this.count = count;
this.items = new ArrayList<>(count);
}
private void put(String item) throws InterruptedException {
lock.lock();
try {
if (size >= count) {
System.out.println("队列已满,阻塞等待");
putCondition.await();
}
++size; //增加元素个数
this.items.add(item);
takeCondition.signal();
} finally {
lock.unlock();
}
}
private String take() throws InterruptedException {
lock.lock();
try {
if (size <= 0) {
System.out.println("队列已经空了,不能取了,阻塞等待");
takeCondition.await();
}
--size;
String item = this.items.remove(0);
putCondition.signal();
return item;
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ConditionBlockingQueueDemo demo = new ConditionBlockingQueueDemo(10);
Thread t1 = new Thread(()->{
try {
for (int i = 0; i < 1000; i++) {
String item = "item-" + (i + 1);
demo.put(item);
System.out.println("生产了一个元素: " + item);
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(100);
Thread t2 = new Thread(()->{
try {
for (;;) {
String item = demo.take();
System.out.println("消费了一个元素: " + item);
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t2.start();
}
}
打印结果如下
生产了一个元素: item-1
生产了一个元素: item-2
消费了一个元素: item-1
生产了一个元素: item-3
消费了一个元素: item-2
生产了一个元素: item-4
消费了一个元素: item-3
生产了一个元素: item-5
消费了一个元素: item-4
....
阻塞队列中的方法
添加元素
- add --> 如果队列满了,抛出异常
- offer --> true/false,添加成功返回true,否则返回false
- put --> 如果队列满了,则一直阻塞
- offer(timeout) --> 带有超时时间,如果队列满了,会阻塞timeout时长,超过阻塞时长,则返回false
移除元素
- element --> 如果队列为空,抛出异常
- peek --> true/false,移除成功返回true,否则返回false
- take --> 如果队列为空,则一直阻塞
- poll(timeout) --> 如果超时了还没返回元素,则返回null
JUC包中的阻塞队列
- ArrayBlockingQueue:基于数组结构
- LinkedBlockingQueue:基于链表结构
- PriorityBlockingQueue:基于优先级队列
- DelayQueue:允许延时执行的队列
- SynchronousQueue:没有任何存储结构的队列(在线程池Executors.newCachedThreadPool方法()里有用到)
2.4.7 ForkJoin
Fork/Join框架是一个Java7提供的一个用于并行执行任务的框架,把一个大任务拆分成若干个小任务,最终汇总每个小任务执行结果后得到大任务的结果,适用于数字求和. 通过双端队列和工作窃取算法实现。
Fork方法:让task任务拆分异步执行。
Join方法:让task任务同步执行,等待返回结果然后合并。
代码演示
public class ForkJoinDemo extends RecursiveTask<Integer> {
private final int THREHOLD = 200; //分割的阈值,每个任务的大小
private int start;
private int end;
public ForkJoinDemo(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
boolean canSplit = (end - start) <= THREHOLD;
if (canSplit) { // 可以分割
int middle = (start + end) / 2;
ForkJoinDemo leftTask = new ForkJoinDemo(start, middle);
ForkJoinDemo rightTask = new ForkJoinDemo(middle + 1, end);
leftTask.fork(); //此处会递归进入compute方法
rightTask.fork(); //此处会递归进入compute方法
Integer leftResult = leftTask.join();
Integer rightResult = rightTask.join();
sum = leftResult + rightResult;
} else {
for (int i = start; i <= end; i++) {
sum += i;
}
}
return sum;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinDemo forkJoinDemo = new ForkJoinDemo(1, 1000);
ForkJoinTask<Integer> result = forkJoinPool.submit(forkJoinDemo);
System.out.println(result.get());
}
}
ForkJoin的实际应用
public class ItemA {
private String name;
private Integer num;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getNum() {
return num;
}
public void setNum(Integer num) {
this.num = num;
}
}
public class ItemB {
private String name;
private Integer num;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getNum() {
return num;
}
public void setNum(Integer num) {
this.num = num;
}
}
public class Context {
private ItemA itemA;
private ItemB itemB;
public ItemA getItemA() {
return itemA;
}
public void setItemA(ItemA itemA) {
this.itemA = itemA;
}
public ItemB getItemB() {
return itemB;
}
public void setItemB(ItemB itemB) {
this.itemB = itemB;
}
}
@Service
public class ItemAService extends AbstractLoadDataProcessor {
@Override
public void load(Context context) {
ItemA itemA = new ItemA();
itemA.setName("item A");
itemA.setNum(100);
context.setItemA(itemA);
}
}
@Service
public class ItemBService extends AbstractLoadDataProcessor {
@Override
public void load(Context context) {
ItemB itemB = new ItemB();
itemB.setName("item B");
itemB.setNum(200);
context.setItemB(itemB);
}
}
public interface ILoadDataProcessor {
void load(Context context);
}
public abstract class AbstractLoadDataProcessor extends RecursiveAction implements ILoadDataProcessor {
protected Context context;
@Override
protected void compute() {
load(context); //调用子类实现
}
public Context getContext() {
this.join();
return context;
}
public void setContext(Context context) {
this.context = context;
}
}
任务拆分和合并
@Service
public class ItemForkJoinDataProcessor extends AbstractLoadDataProcessor implements ApplicationContextAware {
ApplicationContext applicationContext;
private List<AbstractLoadDataProcessor> loadDataProcessors = new ArrayList<>();
@Override
public void load(Context context) {
this.loadDataProcessors.forEach(abstractLoadDataProcessor -> {
abstractLoadDataProcessor.setContext(this.context);
abstractLoadDataProcessor.fork(); // 创建一个fork去拆分任务异步执行
});
}
@Override
public Context getContext() {
this.loadDataProcessors.forEach(ForkJoinTask::join);
return super.getContext();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
loadDataProcessors.add(applicationContext.getBean(ItemAService.class));
loadDataProcessors.add(applicationContext.getBean(ItemBService.class));
}
}
入口调用
@RestController
public class IndexController {
@Autowired
ItemForkJoinDataProcessor itemForkJoinDataProcessor;
@GetMapping("/index")
public Context index() {
Context context = new Context();
itemForkJoinDataProcessor.setContext(context);
ForkJoinPool forkJoinPool = new ForkJoinPool();
forkJoinPool.submit(itemForkJoinDataProcessor);
return itemForkJoinDataProcessor.getContext();
}
}
2.4.8 ConcurrentHashMap
HashMap是线程非安全的,多线程下会存在线程安全问题
HashTable是线程安全的,因为在方法上有加synchronized同步锁,但是锁的粒度太大,会影响性能,所以引入了CHM.
ConcurrentHashMap在性能和安全方面做了平衡,在方法内部代码块上加了synchronized锁。
使用方法
computeIfAbsent:如果不存在则修改值
computeIfPresent:如果存在则修改值
compute:computeIfAbsent和computeIfPresent的结合
merge:数据合并
存储结构和实现
jdk 1.7:使用segment分段锁,锁的粒度大
jdk 1.8:使用链表 + 红黑树
ConcurrentHashMap和HashMap原理基本类似,只是在HashMap的基础上需要支持并发操作,保证多线程情况下对HashMap操作的安全性。当某个线程对集合内的元素进行数据操作时,会锁定这个元素,如果其他线程操作的数据hash得到相同的位置,就必须等到这个线程释放锁之后才能进行操作。
数据结构
- 最外层是初始16位长度的数组,数据达到阈值(16 * 0.75)时会自动扩容(16 >> 1 = 32)
- 插入数据时,先对key进行hash计算得到数据将要插入到数组的位置下标,如果此位置为空,则插入;
- 如果此位置有数据,并且key相同,则替换做修改操作;
- 如果此位置有数据,但key不同,则追加到此下标位置;
- 初始情况下标位置是以单向链表结构存储数据,后续数据追加到链表尾部;
- 当数组长度扩容到64,且某个位置链表长度达到8时,会将单向链表转换为红黑树结构
- 做删除操作时,如果某个位置元素小于8时,会将红黑树转换为单向链表
扩容过程(满足两种情况会扩容):
- 当新增节点后,所在位置链表元素个数达到阈值8,并且数组长度小于64;
- 当增加集合元素后,当前数组内元素个数达到扩容阈值(16 * 0.75)时就会触发扩容;
- 当线程处于扩容状态下,其他线程对集合进行操作时会参与帮助扩容;
默认是16位长度的数组,如果扩容就会新创建一个32位长度的数组,并对数据进行迁移,采用高低位迁移;
高低位迁移原理
扩容之后,数据迁移,有些数据需要迁移,有些数据不需要,低位不变,高位迁移;
数据扩容,但是计算存储位置下标的公式不变:i = (n - 1) & hash,所以有些key在扩容前后得到的下标位置相同,而有些key在扩容后hash得到的下标位置发生了改变;
假设:某个key的hash为9,数组长度为16,扩容到32,hash后得到的位置依然是9
假设:某个key,数组长度为16时hash值为4,而扩容为32长度时hash值变成了20
所以,table长度发生变化之后,获取同一个key在集合数组中的位置发生了变化,那么就需要迁移
链表转红黑树
当数组长度大于等于64,且某个数组位置的链表长度大于等于8,会把该位置链表转化为红黑树
原理
put插入元素
public V put(K key, V value) {
return putVal(key, value, false); // 是否只有当不存在时才put
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
ConcurrentHashMap.Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new ConcurrentHashMap.Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
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 (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
ConcurrentHashMap.Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new ConcurrentHashMap.Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof ConcurrentHashMap.TreeBin) {
ConcurrentHashMap.Node<K,V> p;
binCount = 2;
if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}