目录
1. List集合线程安全问题
2.HashSet集合线程安全
3.HashMap集合线程安全
4.八种问题
5.公平锁和非公平锁
6.可重入锁
7.死锁
8.Callable接口
FutureTask
9.JUC辅助类
9.1 CountDownLatch 减少计数
9.2 CyclicBarrier 循环栅栏
9.3 Semaphore 信号量
1. List集合线程安全问题
List集合是线程不安全的,当有多个线程对list进行修改时会出现异常,如下代码
/**
* List集合线程不安全
*/
public class Test08 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
//多线程,加入元素
for (int i = 0; i < 30; i++) {
new Thread(()->{
//添加
list.add(UUID.randomUUID().toString().substring(0,8));
//获取内容 ConcurrentModificationException
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
1.1 Vector : 使用较少
List<String> list = new Vector<>();
Vector采用Synchronized 关键字解决并发问题,效率不高
1.2 Collections工具类,使用较少
List<String> list = Collections.synchronizedList(new ArrayList<>());
1.3 JUC解决方案 , 常用
List<String> list = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList 底层原理:
写时复制技术:
对于一个数据,读时支持并发读其内容。写时是独立写(先复制一个与原集合相同的集合,写入新的内容,新内容与之前的集合合并),后面再读就是读新集合中内容。
JDK帮助文档中对其解释:
它是一个ArrayList线程安全版的变种,对其的一系列操作都是通过对底层数组做一个新拷贝完成的。这通常代价很高,当遍历操作的数量远远超过修改时,可能比替代方法更有效,当你不能或不想同步遍历时,但需要排除并发线程之间的干扰时,这很有用。
其add方法源码:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//1.数组复制
Object[] newElements = Arrays.copyOf(elements, len + 1);
//2. 新元素加入
newElements[len] = e;
//3. 合并
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
2.HashSet集合线程安全
Set<String> set = new HashSet<>();
解决方案:
Set<String> set = new CopyOnWriteArraySet<>();
3.HashMap集合线程安全
Map<String, String> map = new HashMap<>();
解决方案:
Map<String, String> map = new ConcurrentHashMap<>();
4.八种问题
1.先输出短信、再输出邮件
class Phone{
public synchronized void sendSMS() throws Exception{
System.out.println("-------sendSMS");
}
public synchronized void sendEmail() throws Exception{
System.out.println("------sendEmail");
}
public void getHello(){
System.out.println("------getHello");
}
}
public class Test09 {
public static void main(String[] args) throws InterruptedException {
Phone p1 = new Phone();
new Thread(()->{
try {
p1.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"AA").start();
//这里sleep,将导致最终结果一定是先SMS再Email
Thread.sleep(100);
new Thread(()->{
try {
p1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"BB").start();
}
}
2.SMS中停留4秒,仍然是先SMS再Email
class Phone{
public synchronized void sendSMS() throws Exception{
//停留4秒
TimeUnit.SECONDS.sleep(4);
System.out.println("-------sendSMS");
}
public synchronized void sendEmail() throws Exception{
System.out.println("------sendEmail");
}
public void getHello(){
System.out.println("------getHello");
}
}
public class Test09 {
public static void main(String[] args) throws InterruptedException {
Phone p1 = new Phone();
//Phone p2 = new Phone();
new Thread(()->{
try {
p1.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"AA").start();
Thread.sleep(100);
new Thread(()->{
try {
p1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"BB").start();
}
}
3.先Hello再SMS
当把线程BB中方法改为getHello,会使得hello先输出,等待4秒后SMS输出。
p1.getHello();
4.现在有2部手机,先Email后EMS
public static void main(String[] args) throws InterruptedException {
Phone p1 = new Phone();
Phone p2 = new Phone();
new Thread(()->{
try {
p1.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"AA").start();
Thread.sleep(100);
new Thread(()->{
try {
p2.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"BB").start();
}
5.一部手机,两个静态方法,输出SMS,Email
class Phone{
public static synchronized void sendSMS() throws Exception{
//停留4秒
TimeUnit.SECONDS.sleep(4);
System.out.println("-------sendSMS");
}
public static synchronized void sendEmail() throws Exception{
System.out.println("------sendEmail");
}
public void getHello(){
System.out.println("------getHello");
}
}
public class Test09 {
public static void main(String[] args) throws InterruptedException {
Phone p1 = new Phone();
Phone p2 = new Phone();
new Thread(()->{
try {
p1.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"AA").start();
Thread.sleep(100);
new Thread(()->{
try {
p1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"BB").start();
}
}
5.公平锁和非公平锁
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
- 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
- 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
//true表示公平锁
private final ReentrantLock reentrantLock = new ReentrantLock(true);
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && //这里有人吗
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //有人就排队
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
6.可重入锁
synchronized(隐式)和Lock(显式)都是可重入锁
隐式:上锁和解锁过程自动完成
synchronized 可重入锁也叫递归锁,可递归调用
7.死锁
死锁:两个或以上进程在执行过程中,因为争夺资源而造成的一种互相等待的现象,如果没有外力干涉,无法再执行下去。
产生死锁的原因:1.系统资源不足,2.进程运行推进顺序不合适,3.资源分配不当
手写简单死锁代码:
/**
* DeadLock演示
*/
public class Test10 {
static Object a = new Object();
static Object b = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized(a){
System.out.println("持有锁A:"+a);
synchronized (b){
System.out.println("获取锁b");
}
}
},"AA").start();
new Thread(()->{
synchronized(b){
System.out.println("持有锁B:"+b);
synchronized (a){
System.out.println("获取锁a");
}
}
},"BB").start();
}
}
死锁验证方法:
打开控制台
输入jps,查到当前类的进程号,输入jstack 进程 查询堆栈信息
检查堆栈信息
8.Callable接口
Runnable和Callable的区别:
1、Callable规定的方法是call(),Runnable规定的方法是run().
2、Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
3、call方法可以抛出异常,run方法不可以
4、运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
FutureTask
简单使用:
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
return 1024;
});
new Thread(futureTask,"aa线程").start();
System.out.println("取值:"+futureTask.get());
System.out.println("main结束");
}
9.JUC辅助类
9.1 CountDownLatch 减少计数
实现,6个同学陆续离开教室后,班长锁门。
/**
* 演示CountDownLatchDemo
*/
public class Test12 {
//6个同学陆续离开教室后,班长才锁门
public static void main(String[] args) {
//创建6个线程
for (int i = 1; i <=6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "离开了教室");
},String.valueOf(i)).start();
}
//输出班长锁门
System.out.println(Thread.currentThread().getName() + ":锁门了");
}
}
但出现问题:
班长并没有等6个同学都离开,而是至少有一个同学没出去就锁了。
解决:
//6个同学陆续离开教师后,班长才锁门
public static void main(String[] args) throws InterruptedException {
//CountDownLatch计数器
CountDownLatch countDownLatch = new CountDownLatch(6);
//创建6个线程
for (int i = 1; i <=6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "离开了教室");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + ":锁门了");
}
9.2 CyclicBarrier 循环栅栏
简介:它能阻塞一组线程直到某个事件的发生,它能阻塞一组线程直到某个事件的发生,CyclicBarrier :可以使一定数量的线程反复地在栅栏位置处汇集。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。
构造方法:
CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程使用await()方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
CyclicBarrier的另一个构造函数CyclicBarrier(int parties, Runnable barrierAction),用于线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。
await方法:
调用await方法的线程告诉CyclicBarrier自己已经到达同步点,然后当前线程被阻塞
代码演示:
/**
* 演示CyclicBarrier
*/
public class Test13 {
//创建固定值
private static final int NUMBER = 7;
//集齐七颗龙珠可以召唤神龙
public static void main(String[] args) {
//创建CyclicBarrier
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER,()->{
System.out.println("集齐7颗龙珠就可以召唤神龙");
});
//集齐7颗龙珠过程
for (int i = 1; i <= 7; i++) {
new Thread(()->{
try{
System.out.println(Thread.currentThread().getName() + "星龙珠已经集齐");
//等待
cyclicBarrier.await();
}catch (Exception e){
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
CyclicBarrier和CountDownLatch的区别:
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能够处理更为复杂的场景;
CyclicBarrier还提供了一些其他有用的方法,比如getNumberWaiting()方法可以获得CyclicBarrier阻塞的线程数量,isBroken()方法用来了解阻塞的线程是否被中断;
CountDownLatch允许一个或多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置
9.3 Semaphore 信号量
简介:
信号量。从概念上讲,信号量维护一组许可,许可也可以理解为一组资源的使用权。其中获取许可我们可以使用acquire方法,归还许可,我们可以使用release方法。信号量通常用于限制某些资源的线程数。例如限制一个 web接口的并发。
semaphore 释放资源的过程:
应用层调用release方法释放资源
tryReleaseShared尝试释放资源
释放资源成功,则唤醒下一个节点
代码演示:
/**
* 演示信号量 Semaphore
*/
public class Test14 {
//6辆车,停3个停车位
public static void main(String[] args) {
//创建信号量
Semaphore semaphore = new Semaphore(3);
//6辆车
for (int i = 0; i < 6; i++) {
new Thread(()->{
try{
//获取
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到了车位");
//模拟停车时间
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName()+"--离开了车位");
}catch (Exception e){
e.printStackTrace();
}finally {
//释放
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}