JUC基础

news2025/7/24 5:08:39

  • synchronized 复习
    • 虚假唤醒
      • 什么是虚假唤醒
      • 虚假唤醒产生的原因?
      • 解决虚假唤醒?
  • Lock接口
    • ReentrantLock 和 synchronized 的区别
    • Lock 实现线程通信
    • Lock 实现线程定制化通信
  • 集合线程安全
    • ArrayList
    • HashSet
    • HashMap
  • synchronized 锁的范围
  • 多线程锁
    • 公平锁和非公平锁
    • 可重入锁
    • 死锁
  • Callable 接口
  • 辅助类
    • CountDownLatch
    • CyclicBarrier
    • Semaphore
  • ReentrantReadWriteLock 读写锁
  • 阻塞队列
  • 线程池
    • 参数说明
    • 线程池工作流程图
    • 拒绝策略
    • 自定义线程池
  • 异步回调

synchronized 复习

关于 synchronized 的复习在多线程中进行了详细讲解: https://blog.csdn.net/aetawt/article/details/127762885

虚假唤醒

什么是虚假唤醒

当一定的条件触发时会唤醒很多在阻塞态的线程,但只有部分的线程唤醒是有用的,其余线程的唤醒是多余的。
比如说卖货,如果本来没有货物,突然进了一件货物,这时所有的顾客都被通知了,但是只能一个人买,所以其他人都是无用的通知。

举例说明

/**
 *
 * Author: YZG
 * Date: 2022/11/12 19:46
 * Description:  演示虚假唤醒问题
 *  例题: 要求创建四个线程,其中俩个为消费者,俩个为生产者
 *  生产者每生产一件产品,都要等待消费者完成消费后,再去生产
 */
public class Demo01 {
    public static void main(String[] args) {
        Product product = new Product();

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
              product.produce();
            }
        },"生产者A").start();
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                product.consume();
            }
        },"消费者A").start();

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                product.produce();
            }
        },"生产者B").start();

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                product.consume();
            }
        },"消费者B").start();
    }
}

// 产品类
class Product {
    // 产品数量
    private int productCount = 0 ;

    // 生产产品
    public  synchronized void produce()  {
        System.out.println(Thread.currentThread().getName() + " 进入到 produce 方法");
        if (productCount > 0) {
            // 等待消费
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        productCount++;
        System.out.println(Thread.currentThread().getName() + " 生产产品, 剩余 :" + productCount + " 件产品");
        // 唤醒其他线程消费
        notifyAll();
    }

    // 消费者
    public synchronized void consume()  {
        System.out.println(Thread.currentThread().getName() + " 进入到 consume 方法");

        if (productCount == 0) {
            // 等待生产
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        productCount--;
        System.out.println(Thread.currentThread().getName() + " 消费产品, 剩余 :" + productCount + " 件产品");
        notifyAll();
    }

}

我们的要求是,每生产一件产品就要有消费者去消费掉,没有产品时,等待生产者生产,按道理来说我们预计的结果:

image-20221112214249208

但实际上 却没有预想到的结果,并没有满足我们的需求,其实这就属于 虚假唤醒 ,下面探究为什么出现这样的问题。

image-20221112214430734

为了更好的理解 虚假唤醒 , 在俩个方法中增加一个输出语句:

System.out.println(Thread.currentThread().getName() + " 进入到 produce 方法");
System.out.println(Thread.currentThread().getName() + " 进入到 consume 方法");
生产者A 进入到 produce 方法
生产者A 生产产品, 剩余 :1 件产品
消费者A 进入到 consume 方法
消费者A 消费产品, 剩余 :0 件产品 
消费者A 进入到 consume 方法
生产者A 进入到 produce 方法
生产者A 生产产品, 剩余 :1 件产品  
生产者A 进入到 produce 方法
消费者A 消费产品, 剩余 :0 件产品
生产者B 进入到 produce 方法 
生产者B 生产产品, 剩余 :1 件产品
生产者B 进入到 produce 方法
生产者A 生产产品, 剩余 :2 件产品
---------------------------------------------------- 出现问题
生产者A 进入到 produce 方法
生产者B 生产产品, 剩余 :3 件产品
生产者B 进入到 produce 方法
消费者A 进入到 consume 方法
消费者A 消费产品, 剩余 :2 件产品
消费者A 进入到 consume 方法
消费者A 消费产品, 剩余 :1 件产品
消费者A 进入到 consume 方法
消费者A 消费产品, 剩余 :0 件产品
生产者B 生产产品, 剩余 :1 件产品
生产者B 进入到 produce 方法
生产者A 生产产品, 剩余 :2 件产品
生产者A 进入到 produce 方法
生产者B 生产产品, 剩余 :3 件产品
生产者B 进入到 produce 方法
消费者B 进入到 consume 方法
消费者B 消费产品, 剩余 :2 件产品
消费者B 进入到 consume 方法
消费者B 消费产品, 剩余 :1 件产品
消费者B 进入到 consume 方法
消费者B 消费产品, 剩余 :0 件产品
消费者B 进入到 consume 方法
生产者B 生产产品, 剩余 :1 件产品
生产者A 生产产品, 剩余 :2 件产品
消费者B 消费产品, 剩余 :1 件产品
消费者B 进入到 consume 方法
消费者B 消费产品, 剩余 :0 件产品

Process finished with exit code 0

根据以上输出结果,逐步分析:

1、生产者 A 进入到 produce 方法中,此时 : productCount > 0 并不成立,因此 生产产品 ,ProductCount = 1 ,.

生产者A 进入到 produce 方法
生产者A 生产产品, 剩余 :1 件产品

2、在 生产者A 生产的过程中,执行 notifyAll() 之前,消费者A其实 已经进入到了 consume 方法中,由于 productCount == 0 成立,执行 wait() 进行等待,并释放锁。

消费者A 进入到 consume 方法

3、 生产者A 生产完成后,执行 notifyAll() , 唤醒 消费者A,进行消费。ProductCount = 0

消费者A 消费产品, 剩余 :0 件产品

4、消费者A 消费完成后,执行 notifyAll() 方法,唤醒 生产者 A,但是 唤醒生产者之后,消费者A 又抢到了CPU 时间片,进入到了 consume 方法。此时:productCount == 0 成立,执行 wait() 进行等待,并释放锁

消费者A 进入到 consume 方法

5、在 消费者A 等待过程中,生产者A 抢到了CPU 时间片,进入到 produce 方法

生产者A 进入到 produce 方法

6、此时:productCount > 0 并不成立,因此 生产产品 ,ProductCount = 1 , 然后执行 notify() 方法,唤醒消费者 A

生产者A 生产产品, 剩余 :1 件产品

7、在 消费者A 消费之前, 生产者A 抢到 CPU 时间片,进入到 produce 方法,此时 ProductCount = 1productCount > 0 成立,生产者A执行 wait() 进行等待,并释放锁

生产者A 进入到 produce 方法

8、生产者A 等待之后,消费者A 抢到CPU时间片进行消费,ProductCount = 0

消费者A 消费产品, 剩余 :0 件产品

9、在消费者A 消费完成之后, 执行 notifyAll() 之前, 由于生产者A 在第七步释放了锁,因此生产者B 也进入到 produce 方法,此时productCount > 0 不成立,生产者B执行 wait() 进行等待,并释放锁注意:此时 生产者A和生产者B 都停留在了 if 代码块中

生产者B 进入到 produce 方法

10、这时,消费者A终于执行了 notifyAll() ,此时 消费者A 唤醒的是 生产者A 和 生产者 B , 但是我们知道 同步方法 中只允许有一个线程执行,因此 生产者B 抢到 CPU 时间片,进行生产产品,ProductCount = 1,此时的生产者A 仍然停留在 if 代码块中。

生产者B 生产产品, 剩余 :1 件产品

11、生产者B 生产完,又抢到了CPU 时间片,此时:productCount > 0 成立,生产者B执行 wait() 进行等待,并释放锁

生产者B 进入到 produce 方法

12、由于 生产者 B 释放了锁,因此生产者A拿到锁,此时的 生产者A 继续往下执行,生产者A 生产产品,ProductCount = 2

生产者A 生产产品, 剩余 :2 件产品

虚假唤醒产生的原因?

从上面的过程中可以看出,虚假唤醒的原因是:消费者A 同时唤醒了停留在if代码块中的 生产者A 和 生产者B,虽然生产者 A 卡在了 if 代码块中,但是等生产者 A 再次拿到锁后,他是已经进行完 if 判断的,因此又会增加一个产品。

解决虚假唤醒?

在上面中说道,由于生产者A 跳过了 if 判断,那么我们只需要使用 while 循环,重新判断一下即可。在 jdk8 中的文档中,也建议我们将 wait() 方法写在循环中

image-20221112224931553

修改完后

生产者A 进入到 produce 方法
生产者A 生产产品, 剩余 :1 件产品
生产者A 进入到 produce 方法
消费者A 进入到 consume 方法
消费者A 消费产品, 剩余 :0 件产品
消费者A 进入到 consume 方法
生产者A 生产产品, 剩余 :1 件产品
生产者A 进入到 produce 方法
消费者A 消费产品, 剩余 :0 件产品
消费者A 进入到 consume 方法
生产者A 生产产品, 剩余 :1 件产品
生产者A 进入到 produce 方法
消费者A 消费产品, 剩余 :0 件产品
消费者A 进入到 consume 方法
生产者A 生产产品, 剩余 :1 件产品
生产者A 进入到 produce 方法
消费者A 消费产品, 剩余 :0 件产品
消费者A 进入到 consume 方法
生产者B 进入到 produce 方法
生产者B 生产产品, 剩余 :1 件产品
生产者B 进入到 produce 方法
消费者A 消费产品, 剩余 :0 件产品
生产者A 生产产品, 剩余 :1 件产品
消费者B 进入到 consume 方法
消费者B 消费产品, 剩余 :0 件产品
消费者B 进入到 consume 方法
生产者B 生产产品, 剩余 :1 件产品
生产者B 进入到 produce 方法
消费者B 消费产品, 剩余 :0 件产品
消费者B 进入到 consume 方法
生产者B 生产产品, 剩余 :1 件产品
生产者B 进入到 produce 方法
消费者B 消费产品, 剩余 :0 件产品
消费者B 进入到 consume 方法
生产者B 生产产品, 剩余 :1 件产品
生产者B 进入到 produce 方法
消费者B 消费产品, 剩余 :0 件产品
消费者B 进入到 consume 方法
生产者B 生产产品, 剩余 :1 件产品
消费者B 消费产品, 剩余 :0 件产品

Process finished with exit code 0

Lock接口

ReentrantLock 为 Lock 接口的主要实现类

ReentrantLock 和 synchronized 的区别

相似点

这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待

区别

这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。

Synchronize经过过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。

由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:

1.等待可中断:持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。

2.公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。

3.锁绑定多个条件:一个ReentrantLock对象可以同时绑定对个对象。

Lock 实现线程通信

Lock替换synchronized方法和语句的使用, Condition取代了对象监视器方法的使用。

Condition是Java提供了来实现等待/通知的类,Condition类还提供比wait/notify更丰富的功能,Condition对象是由lock对象所创建的。但是同一个锁可以创建多个Condition的对象,即创建多个对象监视器。这样的好处就是可以指定唤醒线程。notify唤醒的线程是随机唤醒一个。

通过 Lock 的 newCondition 方法获取 Condition 对象,

image-20221113154735788

Condition中实现线程通信的方法

void await()相当于 Object 中的 wait()
void``signal()相当于 object 中的 notify()
void``signalAll()相当于 object 中的 notifyAll()

使用 Lock 实现上面消费者、生产者的例子

public class Demo02 {
    public static void main(String[] args) {
        Product2 product = new Product2();

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                product.produce();
            }
        }, "生产者A").start();
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                product.consume();
            }
        }, "消费者A").start();

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                product.produce();
            }
        }, "生产者B").start();

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                product.consume();
            }
        }, "消费者B").start();
    }
}

// 产品类
class Product2 {
    // 产品数量
    private int productCount = 0;
    // 获取Lock对象
    private final ReentrantLock lock = new ReentrantLock();
    // 获取 condition 对象
    private final Condition condition = lock.newCondition();


    // 生产产品
    public void produce() {
        try {
            // 上锁
            lock.lock();
            while (productCount > 0) {
                // 等待消费
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            productCount++;
            System.out.println(Thread.currentThread().getName() + " 生产产品, 剩余 :" + productCount + " 件产品");
            // 唤醒其他线程消费
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    // 消费者
    public void consume() {
        try {
            lock.lock();
            while (productCount == 0) {
                try {
                    // 等待生产
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            productCount--;
            System.out.println(Thread.currentThread().getName() + " 消费产品, 剩余 :" + productCount + " 件产品");
            // 唤醒其他线程
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

}

Lock 实现线程定制化通信

通过一个例子演示 线程间定制化的通信

有三个线程AA、BB、CC ,要求 三个线程按顺序打印,AA 打印 5次,BB打印10次,CC 打印 15 次,一共打印十轮

如何判断什么时候 AA 执行?什么时候 BB 执行? 什么时候CC执行?

可以利用一个 标志位 flag, 当 flag = 1 时 AA 打印,当 flag =2 时 BB 打印,当 flag = 3时 CC 打印

image-20221113160911435

代码演示

public class Demo03 {
    public static void main(String[] args) {
        ShareResource share = new ShareResource();
        new Thread(() -> {
            // 打印十轮
            for (int i = 1; i <= 10; i++) {
                share.print5(i);
            }
        },"AA").start();

        new Thread(() -> {
            // 打印十轮
            for (int i = 1; i <= 10; i++) {
                share.print10(i);
            }
        },"BB").start();

        new Thread(() -> {
            // 打印十轮
            for (int i = 1; i <= 10; i++) {
                share.print15(i);
            }
        },"CC").start();
    }
}

class ShareResource{
    // 标志位
    private int flag = 1;
    // 创建锁
    private Lock lock = new ReentrantLock();

    Condition c1 = lock.newCondition();
    Condition c2 = lock.newCondition();
    Condition c3 = lock.newCondition();

    // 打印5次
    public void print5(int loop) {
        // System.out.println(Thread.currentThread().getName() + "进入到了 print5");
        try {
            // 上锁
            lock.lock();
            while ( flag != 1 ) {
                    // 等待
                    c1.await();
            }
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + " 打印: " + i + ", 轮数: " + loop);
            }
            // 修改标志位
            flag = 2;
            // 通知 BB
            c2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 解锁
            lock.unlock();
        }
    }

    // 打印10次
    public void print10(int loop) {
        // System.out.println(Thread.currentThread().getName() + "进入到了 print10");
        try {
            // 上锁
            lock.lock();
            while ( flag != 2 ) {
                // 等待
                c2.await();
            }
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " 打印: " + i + ", 轮数: " + loop);
            }
            // 修改标志位
            flag = 3;
            // 通知CC
            c3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 解锁
            lock.unlock();
        }
    }

    // 打印15次
    public void print15(int loop) {
        // System.out.println(Thread.currentThread().getName() + "进入到了 print15");
        try {
            // 上锁
            lock.lock();
            while ( flag != 3 ) {
                // 等待
                c3.await();
            }
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + " 打印: " + i + ", 轮数: " + loop);
            }
            // 修改标志位
            flag = 1;
            // 通知 AA
            c1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 解锁
            lock.unlock();
        }
    }
}

在刚开始写这个案例时,一直有一个疑问? 为什么要创建三个 Condition 对象?

我表达一下我自己的理解:
首先使用一个 Condition 对象 是可以完成这个案例的,前提是:使用 while 循环,并且在唤醒时 使用 signalAll() 唤醒所有线程,但是这并没有体现出 指定唤醒 ,比如: 在 CC 线程执行时,唤醒 AA、BB 线程,此时 BB 线程不符合 while 循环条件,它就会 wait 等待。

​ 使用三个 Condition 对象,与三个线程相关联,在唤醒时就可以进行指定唤醒。

集合线程安全

ArrayList

在多线程下 同时操作 ArrayList 集合可能会出现:ConcurrentModificationException 异常

以ArrayList 为例演示集合的不安全性

public class Demo04 {
    public static void main(String[] args) {
        ArrayList<Object> list = new ArrayList<>();

        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0,4));
                
                System.out.println(list);
            }).start();
        }
    }
}

出现异常

出现异常的原因就是 ArrayList 是非线性安全的,在输出 list 集合的同时,可能其他线程正在操作这个集合。

image-20221113171804152

解决方案一

使用 Vector集合替换,Vector在底层源码中对方法加上了 synchronized 关键字,因此保证了线程相对安全。

public class Demo04 {
    public static void main(String[] args) {
        // ArrayList<Object> list1 = new ArrayList<>();
        //第一种方式:
        Vector coll = new Vector();


        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                coll.add(UUID.randomUUID().toString().substring(0,4));
                System.out.println(coll);
            }).start();
        }
    }
}

解决方案二

Collections 中提供了一个 synchronizedXXX 方法,用于将集合转换为线程安全的

image-20221113181134598

public class Demo04 {
    public static void main(String[] args) {
        // ArrayList<Object> list1 = new ArrayList<>();
        //第一种方式:
        Vector coll = new Vector();
        // 第二种方式:
        // List<Object> coll = Collections.synchronizedList(list1);


        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                coll.add(UUID.randomUUID().toString().substring(0,4));
                System.out.println(coll);
            }).start();
        }
    }
}

解决方案三

使用 CopyOnWriteArrayList 替换 ArrayList

CopyOnWriteArrayList 底层采用的数组结构,使用 写时复制技术,来保证线程的安全性。

CopyOnWriteArrayList 实现过程

  • 在增加元素时,CopyOnWriteArrayList 会复制一个 Object 类型的数组,在新数组中实现 增加操作。增加完将新数组覆盖原数组
  • 在读取元素时,在原有的数组中读取,实现 读写分离

image-20221113182903131

源码分析

1、在 CopyOnWriteArrayList 定义了这俩个属性,一个是Lock锁,一个是 Object类型的数组

    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

2、 CopyOnWriteArrayList 提供了三种构造器。

  • 第一种空参构造器,默认初始化数组长度为 0
  • 第二个指定 CopyOnWriteArrayList 数组的初始元素
  • 第三个指定写数据时的 副本 数组
    /**
     * Creates an empty list.
     */
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

    /**
     * Creates a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param c the collection of initially held elements
     * @throws NullPointerException if the specified collection is null
     */
    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            elements = c.toArray();
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elements.getClass() != Object[].class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }

    /**
     * Creates a list holding a copy of the given array.
     *
     * @param toCopyIn the array (a copy of this array is used as the
     *        internal array)
     * @throws NullPointerException if the specified array is null
     */
    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

3、增加元素

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        // 上锁
        lock.lock();
        try {
        	// 获取原始数组
            Object[] elements = getArray();
            int len = elements.length;
            // 拷贝 原始数组
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            // 将元素增加到新数组中
            newElements[len] = e;
            // 覆盖原始数组
            setArray(newElements);
            return true;
        } finally {
            // 解锁
            lock.unlock();
        }
    }

通过源码也就说明了,在增加元素时,复制了新的数组集合,并覆盖了原始数组。同时使用Lock锁保证了线程安全性

4、读取元素

通过源码看到,在读取元素时,并没有使用新的数组,而是在原始数组中读取,并且读取元素并没有加锁,因此在读取元素上也保证了效率。

   @SuppressWarnings("unchecked")
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

    /**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        return get(getArray(), index);
    }

解决ArrayList线程安全问题

public class Demo04 {
    public static void main(String[] args) {
        // ArrayList<Object> list1 = new ArrayList<>();
        //第一种方式:
        // Vector coll = new Vector();
        // 第二种方式:
        // List<Object> coll = Collections.synchronizedList(list1);
        // 第三种方式:
        CopyOnWriteArrayList<Object> coll = new CopyOnWriteArrayList<>();


        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                coll.add(UUID.randomUUID().toString().substring(0,4));
                // coll.get()
                System.out.println(coll);
            }).start();
        }
    }
}

HashSet

HashSet 同样是线程非安全的,在多线程下操作同样可能会造成 ConcurrentModificationException 异常

public class Demo05 {
    public static void main(String[] args) {
        HashSet<Object> set = new HashSet<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0,4));
                // coll.get()
                System.out.println(set);
            }).start();
        }
    }
}

出现异常:

image-20221113205950015

解决方法

使用 CopyOnWriteArraySet 替换

public class Demo05 {
    public static void main(String[] args) {
        // HashSet<Object> set = new HashSet<>();
        // 线程安全
        CopyOnWriteArraySet<Object> set = new CopyOnWriteArraySet<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0,4));
                // coll.get()
                System.out.println(set);
            }).start();
        }
    }
}

源码分析

在 CopyOnWriteArraySet 里定义了一个 CopyOnWriteArrayList 属性,说明 CopyOnWriteArraySet 底层使用的仍然是数组,并且调用的也都是 CopyOnWriteArrayList 中的方法。

private final CopyOnWriteArrayList<E> al;

image-20221113210632613

HashMap

public class Demo05 {
    public static void main(String[] args) {
        HashMap<Object, Object> map = new HashMap<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                map.put(new Random().nextInt(),UUID.randomUUID().toString().substring(0,4));
                // 遍历
                map.forEach((o1,o2) -> {
                    System.out.println(o1 + " :" + o2);
                });
            }).start();
        }
    }
}

出现异常

image-20221113211700939

解决方法

使用 ConcurrentHashMap 代替

public class Demo05 {
    public static void main(String[] args) {
        
        // HashMap 线程安全问题
        // HashMap<Object, Object> map = new HashMap<>();
        ConcurrentHashMap<Object, Object> map = new ConcurrentHashMap<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                map.put(new Random().nextInt(),UUID.randomUUID().toString().substring(0,4));
                // 遍历
                map.forEach((o1,o2) -> {
                    System.out.println(o1 + " :" + o2);
                });
            }).start();
        }
    }
}

synchronized 锁的范围

针对以下八种情况,演示锁的同步范围:

/**
 *
 * Author: YZG
 * Date: 2022/11/13 21:22
 * Description:
 * 1 标准访问,先打印短信还是邮件
 * --------- sendSMS
 * --------- sendEmail
 * synchronized 作用在方法中,锁的是 this 对象,当前t2,t1 使用一个 phone 对象。因此 t2 会等待 t1 执行完,释放锁才会执行
 *
 * 2停4秒在短信方法内,先打印短信还是邮件
 * --------- sendSMS
 * --------- sendEmail
 * 3新增普通的hello方法,是先打短信还是hello
 * --------- sendHello
 * --------- sendSMS
 * 4现在有两部手机,先打印短信还是邮件
 *--------- sendEmail
 * --------- sendSMS
 * t2、t1 都各自掌握了一个phone锁,因此各执行各的,没有影响
 *
 * 5两个静态同步方法, 1部手机,先打印短信还是邮件
 *--------- sendSMS
 * --------- sendEmail
 * synchronized 作用在方法中,并加上static,锁的是 Class 实例对象,无论有多少phone 对象都没有,类的Class实例对象只有一个
 *
 * 6两个静态同步方法, 2部手机,先打印短信还是邮件
 *--------- sendSMS
 * --------- sendEmail
 * 7 1个静态同步方法1个普通同步方法,1部手机,先打印短信还是邮件
 *--------- sendEmail
 * --------- sendSMS
 * t1 的锁是 phone 对象,t2的锁是 Class实例,因此互不影响,各自执行
 *
 * 8 1个静态同步方法, 1个普通同步方法, 2部手机,先打印短信还是邮件
 *--------- sendEmail
 * --------- sendSMS
 */
public class Demo06 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        // Phone phone2 = new Phone();

        new Thread(Phone::sendEMS,"t1").start();

        TimeUnit.MILLISECONDS.sleep(20);

        new Thread(phone::sendEmail,"t2").start();
    }
}

class Phone {

    public synchronized void sendEMS(){
        // try {
        //     TimeUnit.SECONDS.sleep(4);
        // } catch (InterruptedException e) {
        //     e.printStackTrace();
        // }
        System.out.println("--------- sendSMS");
    }

    public  synchronized  void sendEmail() {
        System.out.println("--------- sendEmail");
    }

    public  void sendHello() {
        System.out.println("--------- sendHello");
    }
}

总结

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是Synchoni zed括号里配置的对象

多线程锁

公平锁和非公平锁

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

  • 优点:所有的线程都能得到资源,不会饿死在队列中。
  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

synchronized 是非公平锁,而Lock中可以设置公平锁或者非公平锁

创建 ReentrantLock 实例时,通过 true 或者 false 设置公平锁或者非公平锁


    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

非公平锁演示

通过设置参数为 false 或者不写,都是非公平锁

//第一步  创建资源类,定义属性和和操作方法
class LTicket {
    //票数量
    private int number = 30;

    //创建可重入锁
    private final ReentrantLock lock = new ReentrantLock();
    //卖票方法
    public void sale() {
        //上锁
        lock.lock();
        try {
            //判断是否有票
            if(number > 0) {
                System.out.println(Thread.currentThread().getName()+" :卖出"+(number--)+" 剩余:"+number);
            }
        } finally {
            //解锁
            lock.unlock();
        }
    }
}

public class Demo07 {
    //第二步 创建多个线程,调用资源类的操作方法
    //创建三个线程
    public static void main(String[] args) {

        LTicket ticket = new LTicket();

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

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

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

输出结果:

从输出结果来看,线程B并没有抢到执行权,因此就可以看出这并不 “公平”

AA :卖出30 剩余:29
AA :卖出29 剩余:28
AA :卖出28 剩余:27
CC :卖出27 剩余:26
CC :卖出26 剩余:25
CC :卖出25 剩余:24
CC :卖出24 剩余:23
CC :卖出23 剩余:22
CC :卖出22 剩余:21
CC :卖出21 剩余:20
CC :卖出20 剩余:19
CC :卖出19 剩余:18
CC :卖出18 剩余:17
CC :卖出17 剩余:16
CC :卖出16 剩余:15
CC :卖出15 剩余:14
CC :卖出14 剩余:13
CC :卖出13 剩余:12
CC :卖出12 剩余:11
CC :卖出11 剩余:10
CC :卖出10 剩余:9
CC :卖出9 剩余:8
CC :卖出8 剩余:7
CC :卖出7 剩余:6
CC :卖出6 剩余:5
CC :卖出5 剩余:4
CC :卖出4 剩余:3
CC :卖出3 剩余:2
CC :卖出2 剩余:1
CC :卖出1 剩余:0

公平锁演示

//第一步  创建资源类,定义属性和和操作方法
class LTicket {
    //票数量
    private int number = 30;

    // 设置公平锁
    private final ReentrantLock lock = new ReentrantLock(true);
    //卖票方法
    public void sale() {
        //上锁
        lock.lock();
        try {
            //判断是否有票
            if(number > 0) {
                System.out.println(Thread.currentThread().getName()+" :卖出"+(number--)+" 剩余:"+number);
            }
        } finally {
            //解锁
            lock.unlock();
        }
    }
}

public class Demo07 {
    //第二步 创建多个线程,调用资源类的操作方法
    //创建三个线程
    public static void main(String[] args) {

        LTicket ticket = new LTicket();

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

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

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

输出结果:

AA :卖出30 剩余:29
BB :卖出29 剩余:28
CC :卖出28 剩余:27
AA :卖出27 剩余:26
BB :卖出26 剩余:25
CC :卖出25 剩余:24
AA :卖出24 剩余:23
BB :卖出23 剩余:22
CC :卖出22 剩余:21
AA :卖出21 剩余:20
BB :卖出20 剩余:19
CC :卖出19 剩余:18
AA :卖出18 剩余:17
BB :卖出17 剩余:16
CC :卖出16 剩余:15
AA :卖出15 剩余:14
BB :卖出14 剩余:13
CC :卖出13 剩余:12
AA :卖出12 剩余:11
BB :卖出11 剩余:10
CC :卖出10 剩余:9
AA :卖出9 剩余:8
BB :卖出8 剩余:7
CC :卖出7 剩余:6
AA :卖出6 剩余:5
BB :卖出5 剩余:4
CC :卖出4 剩余:3
AA :卖出3 剩余:2
BB :卖出2 剩余:1
CC :卖出1 剩余:0

Process finished with exit code 0

可重入锁

什么是 “可重入”,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现 死锁。

synchronized 和 ReentrantLock 都是可重入锁

synchronized 可重入锁演示

public class Demo08 {
    public static void main(String[] args) {
        Object o = new Object();
        synchronized (o) {
            System.out.println(Thread.currentThread().getName() + "第一次获得锁");
            synchronized (o) {
                System.out.println(Thread.currentThread().getName() + "第二次获得锁");
            }
            synchronized (o) {
                System.out.println(Thread.currentThread().getName() + "第三次获得锁");
            }
        }
    }
}

输出结果:从结果来看,main线程获取了三次锁并没有出现死锁的情况

main第一次获得锁
main第二次获得锁
main第三次获得锁

Lock可重入锁演示

Lock可重入锁的注意事项:

  • 由于Lock’是手动加锁释放锁,因此加锁次数和释放锁的次数一定要一样。否则在多线程场景下会造成阻塞,甚至死锁
public class Demo09 {
    public static void main(String[] args) {

        ReentrantLock lock = new ReentrantLock();
        try {
            lock.lock(); // 上锁
            System.out.println(Thread.currentThread().getName() + " 第一次获取锁");
            try {
                lock.lock(); // 上锁
                System.out.println(Thread.currentThread().getName() + " 第二次获取锁");
            } finally {
               lock.unlock(); // 解锁
            }
        } finally {
            lock.unlock(); // 解锁
        }
    }
}

演示可重入锁导致的死锁

由于主线程在 第二次获取锁时并没有释放锁,因此 t1 线程获取不到锁,就会一直等待。出现了死锁

public class Demo09 {
    public static void main(String[] args) {

        ReentrantLock lock = new ReentrantLock();
        try {
            lock.lock(); // 上锁
            System.out.println(Thread.currentThread().getName() + " 第一次获取锁");
            try {
                lock.lock(); // 上锁
                System.out.println(Thread.currentThread().getName() + " 第二次获取锁");
            } finally {
                // lock.unlock(); // 解锁
            }
        } finally {
            lock.unlock(); // 解锁
        }
        
        new Thread(() -> {
            lock.lock();
            System.out.println("t1");
            lock.unlock();
        },"t1").start();
    }
}

image-20221118121056404

死锁

死锁的理解

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续

image-20221118122019426

演示死锁

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");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b) {
                    System.out.println(Thread.currentThread().getName() + " 获取锁b");

                }
            }
        }, "A").start();

        new Thread(() -> {
            synchronized (b) {
                System.out.println(Thread.currentThread().getName() + " 获取锁b,试图获取锁a");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (a) {
                    System.out.println(Thread.currentThread().getName() + " 获取锁a");

                }
            }
        }, "B").start();
    }
}

监测死锁

通过以下俩个 jvm 命令来监测是否出现死锁

  • jps
  • jstack pid

image-20221118124603046

监测结果

image-20221118124634320

Callable 接口

创建线程的方法

  • 继承 Thread 类
  • 实现 Runnable 接口
  • 实现 Callable 接口(jdk1.5之后)
  • 线程池创建

Callable 接口创建线程的方式

  • 需要借助 Future 接口: 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等
  • FutrueTask是 Future是Future的唯一实现 类,它也实现了 Runnable 接口,并且提供了参数为 Callable 的构造器

image-20221118134144994

public class Demo10 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 使用 Callable 创建线程
        // Lambda 方式
       FutureTask<Integer> task =  new FutureTask<Integer>(() -> {
            return 200;
        });
       new Thread(task,"t1").start();
        // 通过 get() 获取计算结果
        Integer result = task.get();
        System.out.println(result);

        // 普通方式
        new Thread(new FutureTask<Integer>(new MyThread()),"t2").start();
    }
}

class MyThread implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        return 300;
    }
}

辅助类

CountDownLatch

  • CountDownLatch是 java.util.concurrent 包下提供的一个辅助类,
  • CountDownLatch类可以设置一个计数器 ,然后通过countDown方法来进行减1的操作,使用await方法等待计数器不大于0 ,然后继续执行await方法之后的语句。

CountDownLatch中的方法

voidawait() 导致当前线程等到锁存器计数到零,除非线程是 interrupted 。
booleanawait(long timeout, TimeUnit unit) 使当前线程等待直到锁存器计数到零为止,除非线程为 interrupted或指定的等待时间过去。
voidcountDown() 减少锁存器的计数,如果计数达到零,释放所有等待的线程。 调用countDown方法的线程不会阻塞
longgetCount() 返回当前计数。
StringtoString() 返回一个标识此锁存器的字符串及其状态。

演示

教室有6名同学 和 1 个班长,当所有同学走出教室之后,班长才能锁门

public class Demo11 {
    public static void main(String[] args) throws InterruptedException {
        // 创建 CountDownLatch 对象,设置计数器初始值
        CountDownLatch latch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " 号同学走出教室");
                // 每走出一个同学,计数器减一
                latch.countDown();
            }, String.valueOf(i)).start();
        }
        // 如果计数器不为0,就等待
        if (latch.getCount() != 0) {
            latch.await();
        }
        System.out.println(Thread.currentThread().getName() + " 班长锁门");
    }
}

输出结果:

1 号同学走出教室
3 号同学走出教室
2 号同学走出教室
4 号同学走出教室
5 号同学走出教室
6 号同学走出教室
main 班长锁门

CyclicBarrier

CyclicBarrier是一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。

CyclicBarrier类似于CountDownLatch也是个计数器, 不同的是CyclicBarrier数的是调用了CyclicBarrier.await()进入等待的线程数, 当线程数达到了CyclicBarrier初始时规定的数目时,所有进入等待状态的线程被唤醒并继续。 CyclicBarrier就象它名字的意思一样,可看成是个障碍, 所有的线程必须到齐后才能一起通过这个障碍。 CyclicBarrier初始时还可带一个Runnable的参数,此Runnable任务在CyclicBarrier的数目达到后,所有其它线程被唤醒前被执行。

构造器方法

Constructor and Description
CyclicBarrier(int parties) 创建一个新的 CyclicBarrier ,当给定数量的线程(线程)等待它时,它将跳闸,并且当屏障跳闸时不执行预定义的动作。
CyclicBarrier(int parties, Runnable barrierAction) 创建一个新的 CyclicBarrier ,当给定数量的线程(线程)等待时,它将跳闸,当屏障跳闸时执行给定的屏障动作,由最后一个进入屏障的线程执行。

方法

Modifier and TypeMethod and Description
intawait() 等待所有 parties已经在这个障碍上调用了 await
intawait(long timeout, TimeUnit unit) 等待所有 parties已经在此屏障上调用 await ,或指定的等待时间过去。
intgetNumberWaiting() 返回目前正在等待障碍的各方的数量。
intgetParties() 返回旅行这个障碍所需的聚会数量。
booleanisBroken() 查询这个障碍是否处于破碎状态。
voidreset() 将屏障重置为初始状态。

代码演示

public class Demo12 {
    public static void main(String[] args) throws InterruptedException {

        // 设置循环屏障,并设置屏障破碎后执行的语句
        CyclicBarrier barrier = new CyclicBarrier(7,() -> {
            System.out.println("****集齐了七颗龙珠");
        });
        for (int i = 1; i <= 7; i++) {
            new Thread(() -> {
                try {
                    System.out.println("第" + Thread.currentThread().getName() + " 颗龙珠被集齐");
                    barrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

Semaphore

一个计数信号量。 在概念上,信号量维持一组许可证。 如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它。 每个release()添加许可证,潜在地释放阻塞获取方。 但是,没有使用实际的许可证对象; Semaphore只保留可用数量的计数,并相应地执行。

构造方法

Constructor and Description
Semaphore(int permits) 创建一个 Semaphore与给定数量的许可证和非公平公平设置。
Semaphore(int permits, boolean fair) 创建一个 Semaphore与给定数量的许可证和给定的公平设置。

重要的俩个方法

Modifier and TypeMethod and Description
voidacquire() 从该信号量获取许可证,阻止直到可用,或线程为 interrupted 。
voidacquire(int permits) 从该信号量获取给定数量的许可证,阻止直到所有可用,否则线程为 interrupted 。
voidrelease() 释放许可证,将其返回到信号量。
voidrelease(int permits) 释放给定数量的许可证,将其返回到信号量。

案例

假设有六辆车,三个停车位,停车位沾满之后,剩余三辆车需要等待。

public class Demo13 {
    public static void main(String[] args) {
        // 创建 Semaphore 实例,并指定信号量
        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(5).nextInt());

                    System.out.println("第 "+ Thread.currentThread().getName() +  " 辆离开车位");
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    // 释放许可证
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }

    }
}

输出结果

1 辆停入车位
第 3 辆停入车位
第 2 辆停入车位
第 3 辆离开车位
第 1 辆离开车位
第 4 辆停入车位
第 4 辆离开车位
第 2 辆离开车位
第 5 辆停入车位
第 6 辆停入车位
第 5 辆离开车位
第 6 辆离开车位

ReentrantReadWriteLock 读写锁

解决线程安全问题使用ReentrantLock就可以了,但是ReentrantLock是独占锁,某一时刻只有一个线程可以获取该锁,而实际中会有写少读多的场景,显然ReentrantLock满足不了这个需求,所以ReentrantReadWriteLock应运而生。ReentrantReadWriteLock采用读写分离的策略,允许多个线程可以同时获取读锁。

其中读锁是可以多线程共享的,即共享锁,而写锁是排他锁,在更改时候不允许其他线程操作。

ReentrantReadWriteLock 内部类

Modifier and TypeClass and Description
static class ReentrantReadWriteLock.ReadLock 该锁由方法 readLock()返回。
static class ReentrantReadWriteLock.WriteLock 该锁由方法 writeLock()返回。

案例演示

多个线程线程分别对 map 集合进行读操作和写操作

未使用读写锁的情况

public class Demo14 {
    public static void main(String[] args) {
        for (int i = 1; i <= 6; i++) {
           //  lambda 表达式使用变量应为 final 类型
           final int finalI = i;
            new Thread(() -> {
               MapResource.put(finalI,finalI);
            },String.valueOf(i)).start();

            new Thread(() -> {
                MapResource.get(finalI);
            },String.valueOf(i)).start();
        }
    }
}

class MapResource {
    private static volatile Map<Integer,Integer> map = new HashMap<>();

    // 写操作
    public static void put(Integer key,Integer value) {
        try {
            System.out.println(Thread.currentThread().getName() + " 正在进行写操作....");
            TimeUnit.MILLISECONDS.sleep(10);
            map.put(key,value);
            System.out.println(Thread.currentThread().getName() + " 写操作完成....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 读操作
    public static Integer get(Integer key) {
        Integer result = null ;
        try {
            System.out.println(Thread.currentThread().getName() + " 正在进行读操作....");
            TimeUnit.MILLISECONDS.sleep(10);
            result = map.get(key);
            System.out.println(Thread.currentThread().getName() + " 读操作完成, 读取的值为: " + result);
            return  result ;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return  null;
        }
    }
}

输出结果

通过输出结果可以看出,在没有写操作之前,有的线程就已经读了,这是不行的。因此我们需要在读写的时候增加锁

1 正在进行写操作....
1 正在进行读操作....
2 正在进行写操作....
2 正在进行读操作....
4 正在进行读操作....
5 正在进行写操作....
3 正在进行写操作....
3 正在进行读操作....
5 正在进行读操作....
4 正在进行写操作....
6 正在进行写操作....
6 正在进行读操作....
5 写操作完成....
4 读操作完成, 读取的值为: null
6 写操作完成....
4 写操作完成....
5 读操作完成, 读取的值为: 5
6 读操作完成, 读取的值为: null
3 读操作完成, 读取的值为: 3
1 写操作完成....
2 读操作完成, 读取的值为: null
2 写操作完成....
3 写操作完成....
1 读操作完成, 读取的值为: null

增加读写锁

public class Demo14 {
    public static void main(String[] args) {
        MapResource resource = new MapResource();
        for (int i = 1; i <= 6; i++) {
           //  lambda 表达式使用变量应为 final 类型
           final int finalI = i;
            new Thread(() -> {
                resource.put(finalI,finalI);
            },String.valueOf(i)).start();

            new Thread(() -> {
                resource.get(finalI);
            },String.valueOf(i)).start();
        }
    }
}

class MapResource {
    private  volatile Map<Integer,Integer> map = new HashMap<>();
    private  ReentrantReadWriteLock lock  = new ReentrantReadWriteLock();

    // 写操作
    public  void put(Integer key,Integer value) {
        try {
            // 写锁
            lock.writeLock().lock();
            System.out.println(Thread.currentThread().getName() + " 正在进行写操作....");
            TimeUnit.MILLISECONDS.sleep(300);
            map.put(key,value);
            System.out.println(Thread.currentThread().getName() + " 写操作完成....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            // 解锁
            lock.writeLock().unlock();
        }
    }

    // 读操作
    public  Integer get(Integer key) {
        Integer result = null ;
        try {
            // 读锁
            lock.readLock().lock();
            System.out.println(Thread.currentThread().getName() + " 正在进行读操作....");
            TimeUnit.MILLISECONDS.sleep(300);
            result = map.get(key);
            System.out.println(Thread.currentThread().getName() + " 读操作完成, 读取的值为: " + result);
            return  result ;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return  null;
        }finally {
            // 解锁
            lock.readLock().unlock();
        }
    }
}

输出结果

1 正在进行写操作....
1 写操作完成....
1 正在进行读操作....
1 读操作完成, 读取的值为: 1
2 正在进行写操作....
2 写操作完成....
2 正在进行读操作....
2 读操作完成, 读取的值为: 2
3 正在进行写操作....
3 写操作完成....
3 正在进行读操作....
3 读操作完成, 读取的值为: 3
5 正在进行写操作....
5 写操作完成....
5 正在进行读操作....
5 读操作完成, 读取的值为: 5
4 正在进行写操作....
4 写操作完成....
4 正在进行读操作....
4 读操作完成, 读取的值为: 4
6 正在进行写操作....
6 写操作完成....
6 正在进行读操作....
6 读操作完成, 读取的值为: 6

Process finished with exit code 0

读写锁的注意点

  • 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
  • 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。

仔细想想,这个设计是合理的:因为当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁.

一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。

阻塞队列

阻塞队列,顾名思义,首先它是一个队列, 通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;。

image-20221118220026820

  • 当队列是空的,从队列中获取元素的操作将会被阻塞
  • 当队列是满的,从队列中添加元素的操作将会被阻塞.
  • 试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元愫
  • 试图向已满的队列中添新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增

常见的阻塞队列

  • ArrayBlockingQueue 是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
  • LinkedBlockingQueue 一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于 ArrayBlockingQueue 。
  • SynchronousQueue 一个不存储元素的阻塞队列
  • PriorityBlockingQueue 一个具有优先级的无限阻塞队列。 PriorityBlockingQueue 也是基于最小二叉堆实现
  • DelayQueue
    • 只有当其指定的延迟时间到了,才能够从队列中获取到该元素。
    • DelayQueue 是一个没有大小限制的队列,
    • 因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。

阻塞队列中的方法

img

线程池

线程池的好处

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池的种类

  • Executors.newCachedThreadPool():线程池根据需求创建线程,可扩容
  • Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
  • Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池

案例演示

public class Demo15 {
    public static void main(String[] args) {
        // 创建指定大小的线程池
        // ExecutorService threadPool1 = Executors.newFixedThreadPool(5);
        
        // 单个线程的线程池
        // ExecutorService threadPool2 = Executors.newSingleThreadExecutor();

        // 创建可扩容的线程池
        ExecutorService threadPool3 = Executors.newCachedThreadPool();
        try {
            for (int i = 1; i <= 10; i++) {
                threadPool3.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " 办理业务");
                });
            }
        } finally {
            threadPool3.shutdown();
        }
    }
}

参数说明

  1. 最大线程数(池中最大线程数的数量) maximumPoolSize
  2. 核心线程数(池中必须保持线程数的数量) corePoolSize
  3. 非核心线程数的活跃时间 keepAliveTime、TimeUnit
  4. 阻塞队列 workQueue
  5. 拒绝策略 RejectedExecutionHandler
  6. 线程工厂 ThreadFactory

线程池工作流程图

img

拒绝策略

  1. AbortPolicy:直接丢弃任务,抛出异常,这是默认策略
  2. CallerRunsPolicy:只用调用者所在的线程来处理任务
  3. DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务
  4. DiscardPolicy:直接丢弃任务,也不抛出异常

自定义线程池

        // 自定义线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                3,
                10,
                20,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(4),
                new ThreadPoolExecutor.AbortPolicy()
        );

image-20221119114441692

异步回调

public class Demo16 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //    异步回调,没有返回值
        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getName() + " 异步回调,没有返回值");
        });
        completableFuture.get();

        //    异步回调,有返回值
        CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(() -> {

            System.out.println(Thread.currentThread().getName() + " 异步回调,有返回值");
            // 异常
            int i = 1 / 0;

            return 11;
        });
        completableFuture1.whenComplete((t, u) -> {
            System.out.println(t); // 返回值
            System.out.println(u); // 异常信息
        });
    }
}


各位彭于晏,如有收获点个赞不过分吧…✌✌✌

Alt


扫码关注公众号 【我不是秃神】 回复 JUC 可下载 MarkDown 笔记

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/35458.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CameraMetadata 知识学习整理

一、涉及的相关代码路径 system/media/camera/src/camera_metadata.c // metadata的核心内容&#xff0c;包含metadata内存分配&#xff0c;扩容规则&#xff0c;update, find等 system/media/camera/src/camera_metadata_tag_info.c // 所有android原生tag的在内存里面sect…

22/11/24

1&#xff0c;单调队列&#xff1b; (76条消息) 单调队列专题_Dull丶的博客-CSDN博客 2&#xff0c;kmp算法&#xff1b; 先是自己和自己匹配&#xff0c;求出ne数组&#xff0c;然后和另一串匹配&#xff0c;进行求解&#xff1b; 循环里三步&#xff1a;while&#xff0c…

【Lilishop商城】No2-3.确定软件架构搭建二(本篇包括接口规范、日志处理)

仅涉及后端&#xff0c;全部目录看顶部专栏&#xff0c;代码、文档、接口路径在&#xff1a; 【Lilishop商城】记录一下B2B2C商城系统学习笔记~_清晨敲代码的博客-CSDN博客 全篇只介绍重点架构逻辑&#xff0c;具体编写看源代码就行&#xff0c;读起来也不复杂~ 谨慎&#xf…

【数据聚类】基于粒子群、遗传和差分算法实现数据聚类附matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

【App自动化测试】(十)特殊控件Toast识别

目录1. toast介绍2. toast定位3. 实例演示前言&#xff1a; 本文为在霍格沃兹测试开发学社中学习到的一些技术写出来分享给大家&#xff0c;希望有志同道合的小伙伴可以一起交流技术&#xff0c;一起进步~ &#x1f618; 1. toast介绍 Toast&#xff0c;简易的消息提示框。为了…

CANdelaStudio-从入门到深入目录

前文介绍诊断协议那些事儿专栏,为大家深入介绍了ISO 14229各个服务的基础知识、请求与响应的报文格式,详情可查看:诊断协议那些事儿,从本专题开始,将由浅入深的展开诊断实际开发与测试的数据库编辑,包含大量实际开发过程中的步骤、使用技巧与少量对Autosar标准的解读。希…

HTML5学习笔记(四)

CSS3 颜色样式 在CSS3中&#xff0c;增加了大量定义颜色方面样式的属性&#xff0c;主要包括以下3种。 ▶ opacity透明度 ▶ RGBA颜色 ▶ CSS3渐变 opacity透明度 opacity属性取值是一个数值&#xff0c;取值范围为0.0~1.0。其中0.0表示完全透明&#xff0c;1.0表示完全不透…

I/O模型

网络IO的本质 网络IO的本质就是socket流的读取&#xff0c;通常一次IO读取会涉及两个阶段与两个对象&#xff0c;其中两个对象为&#xff1a;用户进程&#xff08;线程&#xff09;Process&#xff08;Thread&#xff09;、内核对象&#xff08;kernel&#xff09;,两个阶段为…

北方地区长乐市污水厂(150000m3d)工艺设计

目 录 1设计说明书 3 1.1概述 3 1.1.1设计题目 3 1.1.2设计任务 3 1.1.3设计阶段&#xff08;设计程度&#xff09; 3 1.1.4设计依据 3 1.1.5设计原始资料 3 1.1.6设计工作量 5 1.1.7设计要求 5 1.1.8 毕业设计日期 5 1.2 设计要求 6 1.2.1 设计原则 6 1.2.3 设计内容 6 1.3 水…

PLC中ST编程的比较运算

比较运算符&#xff1a; >大于、 <小于、 >大于等于、 <小于等于、等于、 <>不等于。 BOOL类型的比较是通过1&#xff08;TRUE&#xff09;和0&#xff08;FALSE&#xff09;来比较的&#xff1b; 只有xIn_1为真&#xff0c;xIn_2为假的时候&#xff0c;xRe…

《小猫猫大课堂》1——小喵是如何开启敲代码之路的?

更新不易&#xff0c;麻烦多多点赞&#xff0c;欢迎你的提问&#xff0c;感谢你的转发&#xff0c; 最后的最后&#xff0c;关注我&#xff0c;关注我&#xff0c;关注我&#xff0c;你会看到更多有趣的博客哦&#xff01;&#xff01;&#xff01; 喵喵喵&#xff0c;你对我真…

【Vue基础系列】vue-router 万字详解,一篇彻底搞懂

目录 一、路由的简介 二、路由基本使用 三、嵌套路由 四、路由的query参数 五、路由的params参数 六、路由的props配置 七、编程式路由导航 八、缓存路由组件 九、两个新的生命周期钩子 十、路由守卫 一、路由的简介 我们在生活中经常听到路由器&#xff0c;但关于路由…

将项目部署至云服务器的详细过程 以community项目为例

文章目录1.申请一个2核4G的云服务器&#xff0c;系统选择CentOS 7.62.使用终端连接云服务器3.使用 wget 命令下载以下安装文件4.安装jdk125.安装maven6.安装MySQL7.初始化mysql数据库8.安装Redis9.安装kafka10.安装elasticsearch及其分词工具11.安装Wkhtmltopdf12.安装tomcat13…

【Spring框架】一文带你吃透基于注解的DI技术详细教程

本文目录 文章目录本文目录&#x1f496;基于注解的DI✨概念✨[Component](https://so.csdn.net/so/search?qComponent&spm1001.2101.3001.7020)注解创建对象✨声明组件扫描器✨创建对象的四个注解✨扫描多个包的三种方式✨Value简单类型属性赋值✨Value使用外部属性配置文…

外卖项目07---git

git&#xff1a;企业、公司等 目录 一、Git概述 105 1.1Git简介 105 1.2Git下载与安装 105 二、Git代码托管服务 106 2.1常用的Git代码托管服务 106 三、Git常用命令 107 3.1Git全局配置 3.2获取Git仓库 ​编辑 ​编辑 3.3工作区、暂存区、版本库概念 3.4Git工作…

ASPICE系列:顺利通过ASPICE流程软件单元验证(SWE.4)

上次的ASPICE评估是否出了问题而您不知道原因? 或者您马上要进行第一次评估&#xff1f; 本系列文章是关于如何准备ASPICE流程软件单元验证(SWE.4)评估的。我们探究这个过程&#xff0c;预期交付以及评估人员的观点。永远记住一个想法:怎样做才能成功地通过评估? 想要成功通…

【PdgCntEditor】利用PDF目录书签编辑软件PdgCntEditor为PDF型图书快速添加书签的方法

一、给PDF加书签的两种情况 1.1 文字版PDF添加书签的理想情形 假设我们弄到了一本PDF&#xff0c;这个PDF如果是由Word或WPS转化而来&#xff0c;其中的标题也就代表了目录&#xff0c;我们可以用acrobat PDF中的AutuBookmark插件实现自动识别标题为目录的方法来添加书签。 …

『Java安全』利用反射调用MimeLauncher.run()触发RCE

文章目录前言MimeLauncherrun()MimeLauncher()反射调用MimeLauncher.run()触发RCE条件PoC完前言 rt.jar内的sun.net.www.MimeLauncher类的run方法调用了exec 据说可以有效绕过某些免杀&#xff0c;下面分析一下调用过程 MimeLauncher run() 首先&#xff1a;调用了this.m.ge…

古人的名与字、号、讳、谥有什么区别

古人复杂的名字 这个世界上想来是不存在没有名字的人&#xff0c;即便真的有人没名字&#xff0c;也会被外人赠予姓名&#xff0c;比如说一些古人典籍里的“无名氏”&#xff0c;就是专门用来形容那些没有名字也不清楚根脚的人&#xff0c;即便是现如今一些作品不知道作者是谁…

信号与线性时不变系统的傅里叶描述

1、复正弦信号和线性时不变系统的频率相应 卷积积分和卷积和傅里叶变换冲激表示信号正弦表示信号输入信号表示为延迟冲激的加权叠加输入信号为复正弦信号的加权叠加输出可以用卷积的形式来表示输出可以用傅里叶的形式来表示 (1)频率响应Frequency response 线性时不变系统对正…