JUC(java.util.concurrent)下的常用类
1、ReentrantLock
- (1) lock 一定要写在 try 之前
a) 如果没有加锁成功,会在 finally 里面释放锁,如果没有得到锁,就释放锁,会报错
b) 如果 lock 写在了 try 的里面,最终 unlock 的异常会覆盖掉 业务的正常异常,增加了调试的难度 - (2)一定要记得在 finally 里面unlock()
如果不进行 unlock() ,可能会一直出现锁占用 的问题,可能出现死锁
2、Semaphore(信号量)
- acquire():尝试获取锁,如果可以正常获取到,则执行后面的业务逻辑,如果获取失败,则阻塞等待
- release():释放锁
- 使用信号量实现一个实例
一个停车场,有四辆车,有两个车位,四辆车依次进入
package thread.thread0621;
import java.util.Random;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 信号量演示
* **/
public class ThreadDemo98 {
public static void main(String[] args) {
// 创建信号量
Semaphore semaphore = new Semaphore(2); // 两个停车位
// 四辆车,四个线程
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10,10,0, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100)
);
for (int i = 0; i < 4; i++) {
// 创建任务
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "到达停车场------------");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 试图进入停车场
try {
// 尝试获取锁
semaphore.acquire();
// 当代码执行到此处,说明获取到锁
System.out.println(Thread.currentThread().getName() + "进入停车场");
int num = 1 + new Random().nextInt(5);
try {
Thread.sleep(num * 1000); // 休眠 1 到 5 秒
} catch (InterruptedException e) {
e.printStackTrace();
}
// 离开停车场
System.out.println(Thread.currentThread().getName() + "离开停车场..........");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 释放锁
semaphore.release();
}
}
});
}
}
}
3、计数器 CountDownLauth
- 用来保证一组线程同时完成某个操作之后才能继续进行后面的任务
- await():等待,当线程数量不满足countDownLauth的数量的时候,执行此代码会阻塞等待,直到数量满足之后(所有线程都到达后)执行await()后的代码
- countDown():计数器
- 问:CountDownLauth 是如何实现的?
答:在CountDownLauth 里面有一个计数器, 每次调用countDown()方法的时候,计数器的数量-1, 直到减到0之后,就可以执行await()之后的代码了 - 实例:每个选手都到终点之后,在宣布成绩
package thread.thread0621;
import java.util.concurrent.CountDownLatch;
/**
* 计数器实例
* */
public class ThreadDemo99 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(5);
for (int i = 1; i < 6; i++) {
final int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始起跑");
try {
Thread.sleep(finalI * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "到达终点");
// 计数器-1
latch.countDown();
}
}).start();
// 阻塞等待
latch.await();
System.out.println("所有人都到达了重点,公布排名");
}
}
}
- CountDownLatch 的缺点:
CountDownLatch 计数器的使用时一次性的,当用完一次之后,就不能再使用了
4、循环屏障(CyclicBarrier)
- cyclicBarrier.await();,在执行过程中有以下操作:
1、计数器 -1
2、判断计数器是否为0,如果为0执行之后的代码,如果不为0阻塞等待
当计数器为0是,首先会执行await(0之后的代码,将计数器重置 - 问:CyclicBarrier 和 CountDownLatch 区别?
答:CountDownLatch 计数器只能使用一次, CyclicBarrier 的计数器可以重复使用
package thread.thread0621;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* 循环屏障势力
* */
public class ThreadDemo100 {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("执行了cyclicBarrier 里面的 Runnable");
}
});
for (int i = 1; i < 11; i++) {
int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()
+ "开始起跑");
try {
Thread.sleep(finalI * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println(Thread.currentThread().getName()
+ "等待其他人");
// 计数器 -1 ,判断计数器是否为0
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
// 执行到慈航,表示已经有一组线程满足条件
System.out.println(Thread.currentThread().getName()
+ "执行结束");
}
}).start();
}
}
}
线程安全容器(ConcurrentHashMap)
HashMap 相关的高频问题:底层实现结构、负载因子、哈西冲突的解决方案…、线程安全问题
HashMap 是非线程安全的容器
- 在JDK1.7 下会造成死循环
- 在JDK1.8 下会造成数据覆盖
HahsMap JDK1.7 死循环分析
- HashMap 的存储结构为:
数组 + 链表/红黑树
当链表长度大于8,数组长度大于64时,会升级为红黑树
当链表长度小于6,会降级为链表 - 双线程在进行扩容时可能会出现循环引用,循环引用就会导致死循环
- HashMap扩容代码如下(JDK1.7为头插)
使用ConcurrentHashMap来解决线程不安全问题
- JDK 1.7 是将 ConcurrentHashMap 分成几个 segment 进行加锁(悲观锁)
- JDK 1.8 锁优化,读的时候不加锁,写的时候加锁,使用了大量的 CAS、Voiltail 进行加锁(乐观锁)
Hahstable 也是线程安全的
- 是直接对方法整体进行 synchronized 加锁,一次一般情况下不适用Hahstable
问:HahsMap 、ConcurrentHashMap、Hahstable 区别:
1、HahsMap 非线程安全的容器,它在JDK1.7会造成死循环,JDK1.8会造成数据覆盖
2、Hashtable 实现线程安全的手段比较简单,他是在put 方法整体加了一把锁, 使用 synchronized 修饰,因此性价比不高,所以使用频率比较低;而 ConcurrentHashMap 是 HahsMap 在多线程下的替代方案,他在JDK1.7的时候使用 Lock 加分段锁的方案来实现线程安全,而在JDK1.8 的时候使用大量的 CAS、 Volatile 来实现线程安全,并且在JDK1.8 的时候读取的时候不加锁(读取的数据可能不是最新的,因为读取和写入可以同时进行),只有在写的时候才加锁