【JUC高并发编程】—— 了解JUC

一、集合的线程安全

ArrayList 线程不安全

通过代码演示 ArrayList 集合的线程不安全问题

在这里插入图片描述

package com.atguigu.lock;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * @author Bonbons
 * @version 1.0
 * 演示List集合线程不安全问题
 */
public class ThreadNoSecurity {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                //向集合中添加元素
                list.add(UUID.randomUUID().toString().substring(0, 8));
                //获取集合内容
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

ConcurrentModificationException(并发修改异常)是Java中常见的一种运行时异常,在多线程或并发编程中经常会遇到。它表示在迭代一个集合(如List、Set、Map等)过程中,如果同时进行了集合的修改操作,就会抛出该异常

此处是list调用toString方法取数据打印的时候,存在其他线程在修改数据,导致了异常
在这里插入图片描述

1️⃣ 解决方案一:指定运行类型为 Vector List<String> list = new Vector<>();

  • VectorJava中线程安全的集合类,使用方式与ArrayList相似,因此可以通过使用Vector代替ArrayList,解决线程不安全的问题
  • 它内部的方法上使用了同步锁(synchronized)来保证多线程环境下的安全性
  • Vector内部维护了一个数组,用于存储集合中的元素。当多个线程同时对Vector进行操作时,会通过同步锁来保证同一时间只有一个线程可以访问和修改集合中的元素,从而避免多个线程同时访问时的竞态条件(race condition)问题
  • 虽然Vector是线程安全的集合类,但由于其实现方式比较老旧,性能略低于ArrayList等现代集合类,因此在单线程环境下,推荐使用ArrayList等现代集合类
    在这里插入图片描述
    2️⃣ 解决方案二:使用Collections工具类的 synchronizedArrayList 方法

List<String> list = Collections.synchronizedArrayList(new ArrayList<String>());

在这里插入图片描述
3️⃣ 解决方案三:使用 Java 并发包为我们提供的 CopyOnWriteArrayList 写时复制技术

List<String> list = new CopyOnWriteArrayList<>();

(1)它相当于线程安全的 ArrayLis,和 ArrayList 一样,它是个可变数组;但是和ArrayList 不同的时,它具有以下特性

  • 它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多
    于可变操作,需要在遍历期间防止线程间的冲突。
  • 它是线程安全的。
  • 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove()
    等等)的开销很大。
  • 迭代器支持 hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
  • 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代
    器时,迭代器依赖于不变的数组快照。
    在这里插入图片描述

(2)下面从“动态数组”和“线程安全”两个方面进一步对CopyOnWriteArrayList 的原理进行说明

  • “动态数组”机制
    • 它内部有个“volatile 数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile 数组”, 这就是它叫做 CopyOnWriteArrayList 的原因
    • 由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList 效率很低;但是单单只是进行遍历查找的话,效率比较高。
  • “线程安全”机制
    • 通过 volatile 和互斥锁来实现的。
    • 通过“volatile 数组”来保存数据的。一个线程读取 volatile 数组时,总能看到其它线程对该 volatile 变量最后的写入;就这样,通过 volatile 提供了“读取到的数据总是最新的”这个机制的保证。
    • 通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将数据更新到“volatile 数组”中,然后再“释放互斥锁”,就达到了保护数据的目的。

(3)下面是它 add 方法的源码:【这个 setArraygetArray 是这个类定义的方法,它有个私有属性Object数组】

在这里插入图片描述

HashSet 线程不安全

HashSetJava中的一种集合类,它基于HashMap实现,使用了哈希表的数据结构来存储集合中的元素,因此具有快速查找元素的特性。

但是,HashSet是非线程安全的,即在多个线程同时对HashSet进行操作时可能会出现线程不安全问题,如ConcurrentModificationException异常等

1️⃣ 演示

package com.atguigu.lock;

import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

/**
 * @author Bonbons
 * @version 1.0
 * 演示 Set 集合线程不安全问题
 */
public class SetNoSecurity {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                //向集合中添加元素
                set.add(UUID.randomUUID().toString().substring(0, 8));
                //获取集合内容
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
    }
}

在这里插入图片描述
2️⃣ 解决方案:

(1)使用Collections.synchronizedSet()方法将HashSet转化为线程安全的Set
Set<String> set = Collections.synchronizedSet(new HashSet<String>());

(2)JUC(Java Util Concurrent)包中提供了一种名为ConcurrentHashSet的线程安全集合类,其使用方式与HashSet基本一致,但能够保证在并发环境下的线程安全性

Set<String> set = new ConcurrentHashSet<>();

(3)使用 JUC 为我们提供的 CopyOnWriteArraySetSet<String> set = new CopyOnWriteArraySet<>();

源码:

public boolean add(E e) {
	return al.addIfAbsent(e);
}

public boolean addIfAbsent(E e) {
	Object[] snapshot = getArray();
	return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false : addIfAbsent(e, snapshot);
}

HashMap 线程不安全

1️⃣ 演示:

package com.atguigu.lock;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * @author Bonbons
 * @version 1.0
 * 演示HashMap的线程不安全问题
 */
public class HashMapNoSecurity {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();

        for (int i = 0; i < 30; i++) {
            String key = String.valueOf(i);
            new Thread(() -> {
                //向集合中添加元素
                map.put(key, UUID.randomUUID().toString().substring(0, 8));
                //获取集合内容
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }
}

在这里插入图片描述
2️⃣ 解决方案:

使用 JUC 提供的 ConcurrentHashMap 来解决并发异常

ConcurrentHashMapJava中的一种高效的线程安全集合类,它通过分段锁(Segment)的方式实现了快速并发访问的功能。

使用方式类似于HashMap,但它能够保证在并发环境下的线程安全性,同时具有良好的并发性能和可扩展性

Map<String, String> map = new ConcurrentHashMap<>();

二、多线程锁

  • 首先,我们要知道 synchronized 在不同的作用范围锁的是什么?
    • 作用在普通方法上,锁的是这个类的对象
    • 作用在静态方法上,锁的是这个类的Class字节码对象
    • 作用在代码块上, 锁的是同步代码块括号里的对象

线程八锁问题

此部分主要以线程八锁展开论述,接下来我们通过代码来演示

1️⃣ 标准访问,先打印短信还是邮件?

package com.atguigu.sync;

import java.util.concurrent.TimeUnit;

/**
 * @author Bonbons
 * @version 1.0
 * 演示synchronized线程八锁问题
 */
public class EightLock {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(phone::sendMessage, "t1").start();
        Thread.sleep(2000);
        new Thread(phone::sendEmail, "t2").start();
    }
}

class Phone{
    public synchronized void sendMessage() {
            System.out.println("sendMessage...");
    }

    public synchronized void sendEmail(){
        System.out.println("sendEmail...");
    }
}
  • 结果是先打印短信、再打印邮件
    • 因为两个方法锁的都是 Phone 的实例对象,这两个线程调用的就是同一个实例对象
    • 我们线程的启动顺序是固定的,并且在两个线程启动之间还添加了 2s 的睡眠
      • 确保 t1 线程先启动,t1启动后先获得 phone 对象的锁,t2 启动后只能等 t1 线程释放锁才能继续执行
  • 所以,打印的结果是固定的

在这里插入图片描述

  • 在此处使用了方法引用去简化 Lambda 表达式:
    • 最初是使用Lambda表达式简化了我们实现Runnable接口的匿名内部类
    • 在使用方法引用时,对象::方法名表示将对象的某个方法作为lambda表达式的实现

2️⃣ 停 4 秒在短信方法内,先打印短信还是邮件?

package com.atguigu.sync;
import java.util.concurrent.TimeUnit;
public class EightLock {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendMessage();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t1").start();
        Thread.sleep(2000);
        new Thread(phone::sendEmail, "t2").start();
    }
}

class Phone{
    public synchronized void sendMessage() throws InterruptedException {
            //睡眠4s
            TimeUnit.SECONDS.sleep(4);
            System.out.println("sendMessage...");
    }

    public synchronized void sendEmail(){
        System.out.println("sendEmail...");
    }
}
  • 结果还是先打印Message,再打印邮件

    • 因为锁的是同一个Phone的实例对象,而且我们先启动的是 t1 线程,无论 t1 线程睡眠多久
    • t2 线程都要等待 t1 线程执行完并释放锁才能执行
  • 说一说这个案例与前一个案例体验上的差异:

    • 前一个案例代码运行后,立刻打印Message,然后等2s后打印Email
    • 当前这个案例因为 t1 线程睡眠了4s,所以启动4s后控制台立刻打印出 Message 和 Email
      • 感觉 Message 和 Email 几乎是同时打印出来的,为什么他们之间没有等2s呢?
        • 因为在两个线程启动之间,我们让线程睡2s是让主线程睡2s,确保先启动t1线程
        • 因为开局 t1 获得锁后睡眠了4s,在t1睡2s后,t2就已经启动了,一旦t1打印Message释放锁,t2就会立刻打印

在这里插入图片描述

3️⃣ 新增普通的 hello 方法,是先打短信还是 hello?

package com.atguigu.sync;

import java.util.concurrent.TimeUnit;

/**
 * @author Bonbons
 * @version 1.0
 * 演示synchronized线程八锁问题
 */
public class EightLock {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendMessage();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t1").start();
        Thread.sleep(2000);
        new Thread(phone::getHello, "t2").start();
    }
}

class Phone{
    public synchronized void sendMessage() throws InterruptedException {
            TimeUnit.SECONDS.sleep(4);
            System.out.println("sendMessage...");
    }
    public void getHello(){
        System.out.println("Hello...");
    }
}
  • 结果是先打印 Hello, 再打印 Message
    • 我们 getHello 方法没有使用 synchronized,所以锁对这个方法不起作用,调用这个方法的线程启动后就会立刻打印 Hello
    • 我们 t1 线程睡4s,我们t2线程在2s后启动,所以先打印完 Hello 2s 后打印的 Message

在这里插入图片描述

4️⃣ 现在有两部手机,先打印短信还是邮件?

package com.atguigu.sync;

import java.util.concurrent.TimeUnit;

/**
 * @author Bonbons
 * @version 1.0
 * 演示synchronized线程八锁问题
 */
public class EightLock {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        Phone phone1 = new Phone();
        new Thread(() -> {
            try {
                phone.sendMessage();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t1").start();
        Thread.sleep(2000);
        new Thread(phone1::sendEmail, "t2").start();
    }
}

class Phone{
    public synchronized void sendMessage() throws InterruptedException {
            TimeUnit.SECONDS.sleep(4);
            System.out.println("sendMessage...");
    }

    public synchronized void sendEmail(){
        System.out.println("sendEmail...");
    }
}
  • 结果是先打印 Email,再打印 Message
    • 因为我们锁的是两个不同的 Phone 实例对象,两个线程之间不受影响
    • 开始运行后2s t2线程打印 Email,运行4s 后t1线程苏醒打印 Message

在这里插入图片描述

5️⃣ 两个静态同步方法,1 部手机,先打印短信还是邮件?

package com.atguigu.sync;

import java.util.concurrent.TimeUnit;

public class EightLock {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendMessage();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t1").start();
        new Thread(() -> {
            phone.sendEmail();
        }, "t1").start();
    }
}

class Phone{
    public static synchronized void sendMessage() throws InterruptedException {
            TimeUnit.SECONDS.sleep(4);
            System.out.println("sendMessage...");
    }

    public static synchronized void sendEmail(){
        System.out.println("sendEmail...");
    }
}
  • 先打印 Message,在打印Email
    • 因为两个方法锁的是同一个对象,都是当前类的 Class 对象
    • 所以4s后打印 Message,然后立即打印 Email

在这里插入图片描述

6️⃣ 两个静态同步方法,2 部手机,先打印短信还是邮件?

package com.atguigu.sync;

import java.util.concurrent.TimeUnit;

public class EightLock {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        Phone phone1 = new Phone();
        new Thread(() -> {
            try {
                phone.sendMessage();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t1").start();
        new Thread(() -> {
            phone1.sendEmail();
        }, "t1").start();
    }
}

class Phone{
    public static synchronized void sendMessage() throws InterruptedException {
            TimeUnit.SECONDS.sleep(4);
            System.out.println("sendMessage...");
    }
	Thread.sleep(2000);
    public static synchronized void sendEmail(){
        System.out.println("sendEmail...");
    }
}
  • 结果和上面的相同,先打印 Message,再打印 Email
    • 因为当 synchronized 作用在静态方法上时,锁住的是类的Class对象,和是否为类的多个实例对象无关
    • 所以等4s 打印 Message,然后立刻打印 Email

在这里插入图片描述

7️⃣ 1 个静态同步方法,1 个普通同步方法,1 部手机,先打印短信还是邮件?

package com.atguigu.sync;

import java.util.concurrent.TimeUnit;

public class EightLock {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendMessage();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t1").start();
        Thread.sleep(2000);
        new Thread(() -> {
            phone.sendEmail();
        }, "t1").start();
    }
}

class Phone{
    public static synchronized void sendMessage() throws InterruptedException {
            TimeUnit.SECONDS.sleep(4);
            System.out.println("sendMessage...");
    }

    public synchronized void sendEmail(){
        System.out.println("sendEmail...");
    }
}
  • 先打印 Email,再打印 Message
    • 因为锁的不是同一个对象,一个锁的是Phone的实例对象,一个锁定是Phone的字节码对象
    • 所以在启动后 2s 打印 Email,启动后 4s 打印 Message

在这里插入图片描述

8️⃣ 1 个静态同步方法,1 个普通同步方法,2 部手机,先打印短信还是邮件?

package com.atguigu.sync;

import java.util.concurrent.TimeUnit;

public class EightLock {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        Phone phone1 = new Phone();
        new Thread(() -> {
            try {
                phone.sendMessage();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t1").start();
        Thread.sleep(2000);
        new Thread(() -> {
            phone1.sendEmail();
        }, "t1").start();
    }
}

class Phone{
    public static synchronized void sendMessage() throws InterruptedException {
            TimeUnit.SECONDS.sleep(4);
            System.out.println("sendMessage...");
    }

    public synchronized void sendEmail(){
        System.out.println("sendEmail...");
    }
}
  • 效果和上面的那个相同,先打印 Email,再打印Message
    • 因为锁的不是同一个对象

在这里插入图片描述

公平锁和非公平锁

1️⃣ 先了解什么是公平锁,什么是非公平锁?

  • 公平锁是指多个线程按照申请锁的顺序来获取锁,即先来先得的原则,遵循“先来后到”的处理方式,保证不会出现饥饿现象

    • 当线程尝试获取锁时,如果锁已被占用,则会加入到等待队列中,等待队列中的线程将按照先来先得的顺序被唤醒来竞争锁
    • ReentrantLock就是Java中的一种公平锁实现
  • 非公平锁则是多个线程获取锁的顺序是不确定的

    • CPU调度器随机分配,可能会导致某些线程一直获取不到锁,出现一些线程饥饿情况。
    • ReentrantLock中,默认使用的就是非公平锁,因为这种形式更高效,减少了线程上下文切换的次数,从而减少了CPU时间的浪费

总体来说:

  • 公平锁能保证多线程获得锁的顺序是按照申请锁的顺序来的
  • 非公平锁则是多线程获得锁的顺序是不确定的,具有随机性
  • 在处理高并发并且锁竞争不激烈的情况下,非公平锁性能更好一些
  • 但是当锁竞争激烈且并发量高时,使用公平锁可以避免“饥饿”现象的出现,体现了良好的公平性和可预测性

2️⃣ 案例演示:

我们还是以三个售票员卖三十张票为例,来说明采用非公平锁和公平锁的区别

package com.atguigu.lock;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Bonbons
 * @version 1.0
 * 使用Lock锁实现售票
 */
public class SaleTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(() -> {
            for(int i = 1; i <= 20; i++){
                ticket.sale();
            }
        }, "AA").start();

        new Thread(() -> {
            for(int i = 1; i <= 20; i++){
                ticket.sale();
            }
        }, "BB").start();

        new Thread(() -> {
            for(int i = 1; i <= 20; i++){
                ticket.sale();
            }
        }, "CC").start();
    }
}

//资源类
class Ticket{
    private int num = 30;
    //创建Lock锁的对象 [如果构造方法传入参数 true,那么代表启用公平锁,默认使用不公平锁]
    private final ReentrantLock lock = new ReentrantLock();

    public void sale(){
        //上锁
        lock.lock();
        try{
            //判断是否有余票
            if(num > 0){
                //打印
                System.out.println(Thread.currentThread().getName() + "售票: " + (num--) +
                        " ;剩余: " + (num));
            }
        }finally {
            //释放
            lock.unlock();
        }
    }
}

使用公平锁,不会出现线程饥饿的现象
在这里插入图片描述
不使用公平锁,可以提高效率,但是可能会出现线程饥饿的现象,导致资源不能充分被利用
在这里插入图片描述

3️⃣ 我们来看一下 ReentrantLock 的构造器源码是如何实现的

在这里插入图片描述

4️⃣ 总结

  • 非公平锁:效率高、可能导致线程饥饿
  • 公平锁:阳光普照、效率偏低

可重入锁

1️⃣ 基本概念

可重入锁,也称为递归锁,是一种特殊类型的锁,它允许同一线程多次获取同一个锁,而不会出现死锁或阻塞现象。当线程持有一个可重入锁时,它可以继续获取该锁而不会被阻塞。

通俗地讲,可重入锁就像是一把钥匙,同一个人可以用这把钥匙打开同一扇门多次,而不会出现卡住或无法打开的情况。

可重入锁通常是通过给锁分配一个线程 ID线程计数器来实现的。当该线程再次请求锁时,会检查线程 ID 或计数器以确认该线程已经持有锁。如果是同一个线程,则允许该线程再次获取锁,否则该线程将被阻塞直到锁被释放。
在这里插入图片描述

可重入锁的一个优点是它可以减少锁竞争的次数,提高系统并发性能。另一个优点是它可以避免死锁,因为线程不会被自己阻塞

常见的可重入锁包括 内置锁(如Java中的synchronized关键字)和== 显式锁==(如Java中的ReentrantLock类)

2️⃣ 案例演示:

(1)基于同步代码块演示

package com.atguigu.lock;

/**
 * @author Bonbons
 * @version 1.0
 * 演示一个最简单的可重入锁
 */
public class SimpleReentrantLock {
    public static void main(String[] args) {
        Object o = new Object();
        new Thread(() -> {
            synchronized (o){
                System.out.println("这里是外层");
                synchronized (o){
                    System.out.println("这里是中层");
                    synchronized (o){
                        System.out.println("这里是内层");
                    }
                }
            }
        }, "A").start();
    }
}

对于我们的线程 A,多次获取Object对象o的锁,都没有发生死锁

在这里插入图片描述
(2)基于同步方法演示

package com.atguigu.lock;

/**
 * @author Bonbons
 * @version 1.0
 * 演示一个最简单的可重入锁
 */
public class SimpleReentrantLock {
    public static void main(String[] args) {
        run();
    }
    //可重入锁也叫递归锁
    public synchronized static void run(){
        run();
    }
}

出现了栈溢出的现象,如果不是可重入锁,那么在第一次调用run方法之后,第二次调用就会失败,陷入死锁的情况
在这里插入图片描述

(3)使用Lock接口的实现类 ReemtrantLock 演示

package com.atguigu.lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Bonbons
 * @version 1.0
 * 演示ReentrantLock实现可重入锁
 */
public class SimpleReentrantLock2 {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();

        new Thread(() -> {
            lock.lock();
            try{
                System.out.println("外层");
                lock.lock();
                try{
                    System.out.println("中层");
                    lock.lock();
                    try{
                        System.out.println("内层");
                    }finally {
                        lock.unlock();
                    }
                }finally {
                    lock.unlock();
                }
            }finally {
                lock.unlock();
            }
        }).start();
    }
}

我们要确保每次加锁都应对应的显式释放锁,否则在后续其他线程获取锁的过程中可能会出现问题

在这里插入图片描述

死锁

在这里插入图片描述
1️⃣ 什么是死锁?

死锁是指两个或多个线程在互相等待对方持有的资源时,导致它们都被阻塞,无法继续执行
简单来说,就是因为两个线程相互等待对方放弃其占用的锁而导致的一种阻塞状态

死锁通常发生在多线程并发的环境下,其中每个线程都在等待其他线程完成其任务并释放它所需要的资源。如果两个或多个线程都无法继续执行,这就形成了死锁状态。

例如,线程 A 持有资源 R1,在执行过程中需要访问资源 R2 才能继续执行,但此时资源 R2 被线程 B 占有,因此线程 A 等待线程 B 释放资源 R2。同时,线程 B 同样需要访问资源 R1 才能继续执行,但此时资源 R1 被线程 A 占有,因此线程 B 等待线程 A 释放资源 R1。这样,线程 A 和线程 B 就进入了死锁状态。

  • 解决死锁的方法包括预防死锁、避免死锁和检测死锁
    • 预防死锁 通常使用资源分配图和银行家算法来防止多个线程占用资源,从而避免死锁的发生
    • 避免死锁 则是在设计时考虑到可能出现的死锁情况,并采取一些有效的措施来避免死锁的发生
    • 检测死锁 则是在死锁发生后,及时检测死锁并采取一些措施来解除死锁状态

2️⃣ 手写一个死锁:

package com.atguigu.lock;

/**
 * @author Bonbons
 * @version 1.0
 * 演示 synchronized
 */
public class DeadLock {
    public static void main(String[] args) {
        Object a = new Object();
        Object b = new Object();
        new Thread(() -> {
            synchronized (a){
                System.out.println("当前线程" + Thread.currentThread().getName() + "持有锁a,尝试获取锁b");
                synchronized (b){
                    System.out.println("当前线程" + Thread.currentThread().getName() +"持有锁b");
                }
            }
        }, "AA").start();
        new Thread(() -> {
            synchronized (b){
                System.out.println("当前线程" + Thread.currentThread().getName() + "持有锁b,尝试获取锁a");
                synchronized (a){
                    System.out.println("当前线程" + Thread.currentThread().getName() +"持有锁a");
                }
            }
        }, "BB").start();
    }
}

在这里插入图片描述

3️⃣ 使用终端查看情况

(1)jps 指令是 Java Virtual Machine Process Status Tool 的缩写,用于获取正在运行的Java虚拟机进程的信息

  • jps指令是JDK提供的工具之一,可以通过命令行进行使用。

  • jps指令的具体用法为:在命令行中输入jps,并按回车,就会列出当前所有正在运行的Java虚拟机进程的进程 ID 和进程名称

  • 如果想要获取某个进程的更详细信息,可以结合jstack、jstat、jmap等命令来进行使用。

常用的jps指令参数包括:

-l:列出完整的包名,应用程序主类名,以及Java虚拟机参数
-m:列出虚拟机当前执行的主类的名称
-v:列出虚拟机启动时的参数

例如,执行jps -l命令可以列出所有正在运行的Java虚拟机进程的详细进程信息,如下所示:

C:\&gt;jps -l
39568 jdk.jcmd/sun.tools.jps.Jps
38368 sun.rmi.server.UnicastRef2
34476 org.apache.catalina.startup.Bootstrap
39488 sun.tools.jconsole.JConsole

其中,39568 表示进程的IDjdk.jcmd/sun.tools.jps.Jps 表示进程的完整类名和参数信息,表示正在运行的JPS命令本身
在这里插入图片描述
(2)jstack 指令用于打印出Java虚拟机进程中各个线程的栈帧和调用信息,以及锁信息和死锁信息等,可以帮助我们快速定位线程出现的问题

  • jstack 指令常用于诊断Java虚拟机出现线程阻塞、死锁、性能问题等情况,通过查看线程调用栈和锁信息,可以帮助我们找到问题所在
  • jstack 指令的使用方法为,在命令行输入jstack并加上进程ID(也可以使用进程名称),然后按回车,就会打印出对应进程中各个线程的调用栈信息。例如:
C:\&gt;jstack 38368
2020-08-11 20:32:05
Full thread dump OpenJDK 64-Bit Server VM (11.0.8+10-LTS mixed mode):

jstack 指令还支持一些选项参数,常用的参数包括:

-F:强制输出栈信息,即使进程未响应也会输出
-l:输出更加详细的锁信息和死锁信息

例如,执行 jstack -F -l 38368 命令可以强制输出进程38368中各个线程的详细栈信息和死锁信息,如下所示:

C:\&gt;jstack -F -l 38368
2020-08-11 23:52:40
Full thread dump OpenJDK 64-Bit Server VM (11.0.8+10-LTS mixed mode):

Found one Java-level deadlock:
=============================
"Thread-3":
  waiting for ownable synchronizer 0x000000076badf5c8, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "Thread-2"
"Thread-2":
  waiting for ownable synchronizer 0x000000076badf598, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "Thread-3"

在这里插入图片描述


三、Callable 接口

Callable 概述

1️⃣ 背景介绍:

  • 目前我们学习了有两种创建线程的方法:

    • 一种是通过创建 Thread 类,另一种是通过使用 Runnable 创建线程
    • 但是,Runnable 缺少的一项功能是,当线程终止时(即 run()完成时),我们无法使线程返回结果
    • 为了支持此功能,Java 中提供了 Callable 接口。
  • Callable 接口的特点如下(重点)

    • 为了实现 Runnable,需要实现不返回任何内容的 run()方法,而对于Callable,需要实现在完成时返回结果的 call()方法。
    • call()方法可以引发异常,而 run()则不能。
    • 为实现 Callable 而必须重写 call 方法
    • 不能直接替换 runnable,因为 Thread 类的构造方法根本没有 Callable
      在这里插入图片描述

2️⃣ Future 接口介绍:

Future接口是Java中用于异步编程的一个重要接口,在Java 5中引入,是一种计算结果的占位符,代表着一个异步计算的结果

当进行一项异步操作时,有时需要在操作执行完毕之前执行其他任务,这时可以使用Future接口来表示操作的返回结果,避免进行阻塞等待并发挤压操作

Future接口可以理解为一种异步任务的结果占位符,根据不同的实现方式,可以获取到异步任务的执行结果或者状态,提供了一种非常灵活的异步编程方式。

Future 接口包含如下方法:

//cancel方法用于取消任务的执行
boolean cancel(boolean mayInterruptIfRunning);
//isCancelled方法用于判断任务是否被取消
boolean isCancelled();
//isDone方法用于判断任务是否完成
boolean isDone();
//get方法用于获取任务执行的结果,如果任务未完成则会阻塞,直到任务完成
V get() throws InterruptedException, ExecutionException;
//与get方法类似,但是可以指定阻塞的时间,如果超时则抛出TimeoutException异常
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;

需要注意的是,Future接口对于异步操作的执行是阻塞的,即在调用get方法时会一直等待异步操作的结果,如果异步操作很耗时,则会影响程序的执行效率

因此,在Java 8中新增加了CompletableFuture,它是Future接口的扩展实现,提供了更灵活的异步编程方式,支持链式操作和组合多个异步操作,更加适合在异步编程场景中使用。

Callable 使用方式

1️⃣ 继续完善上面的案例:

package com.atguigu.lock;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author Bonbons
 * @version 1.0
 * 分别演示采用Runnable、Callable接口创建Thread
 */
public class CreateThread {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        new Thread(new MyThread1(), "AA").start();
        FutureTask<Integer> futureTask = new FutureTask<>(() ->{
            System.out.println(Thread.currentThread().getName() + " come in Callable");
            return 1024;
        });
        new Thread(futureTask, "BB").start();
        //如果当前还没有get到结果,那么就打印 wait
        while(!futureTask.isDone()){
            System.out.println("wait...");
        }
        System.out.println(futureTask.get());
		System.out.println("再次获取结果: " + futureTask.get());
        System.out.println(Thread.currentThread().getName() + " over");

    }
}
class MyThread1 implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " come in Callable");

    }
}

class MyThread2 implements Callable {

    @Override
    public Object call() throws Exception {
        return 20;
    }
}
  • 总结:
    • 在创建线程时,我们直接将 Runnable 接口的实现类替换为 Callable 接口的实现类是错误的

      • 因为 Thread 的构造方法不能直接使用 Callable 接口,所以我们就需要一个既能识别 Runnable接口又能识别Callable接口的中间人
      • 所以就有了我们的 FutureTask
        在这里插入图片描述
    • 对于 Future 接口的 get 获取结果的方法是阻塞式的,所以再没有获得结果之前一直处于阻塞状态

    • 为什么我们第二次获取返回值的时候没有阻塞呢?

      • 当调用FutureTaskget() 方法获取返回值时,如果任务还未完成,则调用线程将进入阻塞状态直到任务完成并返回结果
      • 在任务完成后,返回值将被缓存,因此对于相同的FutureTask实例,第二次调用get()方法时不需要重新计算任务,而是直接返回缓存的结果。这就是为什么第二次获取值要比第一次快的原因。
      • 在多线程环境中,如果有多个线程同时调用FutureTask的**get()**方法,即使它们都是在任务完成后调用的,也只有一个线程会计算任务并缓存结果,其他线程将等待直到计算完成并返回结果
      • 需要注意的是,如果有多个线程同时调用get()方法并且任务尚未完成,则计算任务的线程可能会被多个线程唤醒并且会重复计算任务。因此,对于多线程环境中的FutureTask,需要采取适当的同步措施来避免重复计算和缓存竞争。
        在这里插入图片描述

FutureTask

1️⃣ 什么是 FutureTask?

FutureTaskJava中用于异步执行任务的一个类,它实现了FutureRunnable接口,可以通过线程池或者直接调用Threadstart方法来执行任务

FutureTask可以用于执行一段耗时的计算任务,在任务执行完成后返回计算结果,可以通过get方法来获取计算结果

在任务还没有完成的时候,可以通过isDone方法来判断任务是否已经完成

如果任务还没有完成,还可以通过cancel方法来取消任务的执行

2️⃣ FutureTask的特点有:

  • 支持异步计算:FutureTask可以在新线程或者线程池中异步执行任务,并在任务执行完成后返回计算结果。
  • 支持任务取消:FutureTask可以通过cancel方法来取消任务的执行,避免进行不必要的计算和消耗。
  • 支持任务等待:FutureTask可以在任务执行完成之前阻塞调用线程,避免在等待任务完成时进行无效循环等浪费CPU资源的操作
  • 支持任务组合:由于FutureTask实现了Runnable接口,因此可以在任务中再次提交FutureTask任务,实现任务的组合和复杂计算

常见的使用方式是通过提交FutureTask任务到线程池中异步执行任务,然后在主线程中调用get方法来等待任务执行完成并获取执行结果
FutureTask也可以通过Thread对象直接启动线程来执行任务。
在这里插入图片描述


四、JUC强大的辅助类

JUC 中提供了三种常用的辅助类,通过这些辅助类可以很好的解决线程数量过多时 Lock 锁的频繁操作。这三种辅助类为:

  • CountDownLatch: 减少计数
  • CyclicBarrier: 循环栅栏
  • Semaphore: 信号灯

减少计数CountDownLatch

在这里插入图片描述

1️⃣ 什么是CountDownLatch?

CountDownLatchJava中一个非常常用的多线程工具类,它用于控制多个线程的执行顺序,让某个线程在等待其他线程执行完后再执行

CountDownLatch 构造函数中传入一个计数器,通过调用**await()**方法,线程可以阻塞等待计数器达到指定的值

CountDownLatch提供了countDown()方法,可以在某个线程中进行调用,计数器减1,当计数器减到0时,等待中的线程会被唤醒,继续执行

2️⃣ CountDownLatch的特点有:

  • 支持多线程等待:CountDownLatch可以让多个线程在一个等待点等待,直到计数器减为0时,所有等待中的线程被唤醒
  • 支持计数器减少:可以通过countDown()方法来动态减少计数器的值,从而控制等待线程的数量
  • 可以复用:CountDownLatch可以多次利用,一旦计数器减为0,可以通过reset()方法重置计数器值,重新利用。

3️⃣ CountDownLatch的应用场景包括但不限于以下几种:

  • 等待多个线程执行完毕:
    • 在某些场景下需要等待多个线程完成某项任务之后再进行下一步操作,可以利用CountDownLatch来实现线程的同步
  • 等待某些资源准备就绪:
    • 当多个线程需要共享某些资源时,可以利用CountDownLatch来管理资源的初始化和就绪状态
  • 控制程序的流程:
    • 通过CountDownLatch来控制主线程在等待其他线程完成某项工作之后再继续执行,从而实现复杂程序流程的控制

需要注意的是,在使用CountDownLatch时,如果计数器的值未能达到预期的值,等待线程将永远处于阻塞状态,因此需要仔细考虑计数器的值的设置

另外,在多线程环境中,需要注意对共享资源的同步控制。

4️⃣ 案例演示: 6 个同学陆续离开教室后值班同学才可以关门

package com.atguigu.juc;

import java.util.concurrent.CountDownLatch;

/**
 * @author Bonbons
 * @version 1.0
 *  6 个同学陆续离开教室后值班同学才可以关门
 */
public class CountDownLatchDemo {
    private static final int N = 6;
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(N);
        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() + " 锁上了教室");

    }
}

在这里插入图片描述

循环栅栏CyclicBarrier

在这里插入图片描述

1️⃣ 什么是 CyclicBarrier ?

CyclicBarrier 是一种多线程辅助工具类,它能够在所有参与线程到达某个屏障点时,暂停当前执行的线程,直到所有线程都到达屏障点后再继续执行

CyclicBarrier 类初始化时需要指定屏障点的数量n,每当一个线程调用 await() 方法后,计数器n的值就会减一,当n减为0时,所有等待的线程才被释放,CyclicBarrier 将自动重置计数器n回初始值

2️⃣ CyclicBarrier的主要特点有:

  • 支持多线程等待:CyclicBarrier类可以让多个线程在一个等待点处等待,直到所有的线程到达该点才继续执行
  • 支持计数器重置:计数器n在降为0后,CyclicBarrier将自动重置,使得该类可以重复利用。
  • 提供回调函数:CyclicBarrier提供了一个可选的Runnable类型参数,该参数在线程到达屏障点之后自动执行,可以用于线程到达屏障点后的后续处理

3️⃣ 总结:

CyclicBarrier 常用于多线程协作中,例如分治多线程程序中,在等待所有线程完成子任务之后再合并结果,或者在某项任务需要所有线程协同完成的场景下,使用CyclicBarrier可以实现所有线程开始执行时,共同等待其他线程完成的功能

需要注意的是,在使用CyclicBarrier时,必须确保计数器n的值大于线程数,否则部分线程将会永远等待

此外,所有要等待的线程调用 await() 方法后,都将被阻塞,因此需要确保在等待线程被唤醒之前,不存在由于某些原因导致锁死的情况

4️⃣ 案例演示:集齐 7 颗龙珠就可以召唤神龙

package com.atguigu.juc;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
 * @author Bonbons
 * @version 1.0
 * 集齐 7 颗龙珠就可以召唤神龙
 */
public class CyclicBarrierDemo {
    public static final int N = 7;
    public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(N, () -> {
            System.out.println("龙珠集齐召唤神龙");
        });
        for (int i = 1; i <= N; i++) {
            new Thread(() -> {
                System.out.println("已经集齐" + Thread.currentThread().getName() + "星龙珠");
                try {
                    //所有线程在屏障点都被阻塞,并在所有线程都通过该点后才被释放
                    cyclicBarrier.await();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }, String.valueOf(i)).start();
        }


    }
}

在这里插入图片描述

信号灯Semaphore

Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire 方法获得许可证,release 方法释放许可

1️⃣ 什么是 Semaphore?

Semaphore 是一种在并发编程中用于控制访问共享资源的工具类,它是一个计数器,用于跟踪同时访问该共享资源的线程数量

它允许多个线程在同一时刻访问该资源,但限制了同时访问该资源的线程数量,避免了资源的过度使用或竞争的出现,从而保证了线程安全

Semaphore 通常被用来限制同一时刻对共享资源的访问线程数量,也可以用来实现资源池或者某些需要限流的场景

Semaphore 的本质是一个计数器,它维护着一个许可的数量,当线程需要访问共享资源时,首先需要通过 acquire() 方法获取许可,如果当前许可数量为 0,则线程会被阻塞,直到其他线程 release() 释放许可

2️⃣ Semaphore 的常用方法包括:

  • acquire():获取一个许可,如果当前没有许可可用,则阻塞当前线程;
  • acquireUninterruptibly():获取一个许可,如果当前没有许可可用,则一直阻塞当前线程;
  • tryAcquire():尝试获取一个许可,如果当前没有许可可用,则立即返回 false;
  • tryAcquire(long timeout, TimeUnit unit):尝试在指定的等待时间内获取一个许可,如果当前没有许可可用,则阻塞当前线程直到超时,返回 false。

3️⃣ 案例演示: 抢车位, 6 部汽车 3 个停车位

package com.atguigu.juc;

import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * @author Bonbons
 * @version 1.0
 * 抢车位, 6 部汽车 3 个停车位
 */
public class SemaphoreDemo {
    public static void main(String[] args) {
        //默认是非公平设置,传入第二个参数为 true 设置为公平模式
        Semaphore semaphore = new Semaphore(3);

        for (int i = 1; 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 (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放
                    semaphore.release();
                }
            }, String.valueOf(i)).start();
        }
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bow.贾斯汀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值