ReentrantLock和synchronized区别
(1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
(2)synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。
(3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以响应中断。
(4)synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢
/** * 多线程编程模板上: * 多线程操作资源类(封装共享资源和操作共享资源的方法) * 高内聚低耦合(将多线程需要操作的数据方法耦合在一个资源类中,将来多线程操作它的同一个对象就存在竞争关系) * * 多线程并发卖票,可能会出现超卖 * * synchronized: * 总结:独占的悲观的排他的可重入不可响应中断非公平的锁 * 用法: 成员方法(锁的是调用方法的对象)、静态方法(类)、代码块(指定对象) * 升级过程: * 不加锁-> 偏向锁 -> 自旋锁 -> 悲观锁 * 不加锁:加锁的方法只有一个线程访问时,不需要加锁 * 偏向锁:轻量级锁,获取锁的线程较少时而且锁的时间较短,锁偏向第一个线程:A线程获取到锁释放然后又尝试获取锁,B线程也在获取锁,锁优先给A * 自旋锁:获取锁失败的线程,尝试循环再次获取锁直到成功 * while(true){ 尝试获取锁 } * 自旋锁多线程并发时,等待锁的线程继续执行,CPU仍然给他分配时间片,时间片浪费了 * 自旋次数超过10次,自旋锁会升级为悲观锁 * 悲观锁:获取锁失败的线程会挂起到锁对象头中保存(不会被分配时间片) * 当使用锁的线程释放锁以后,会通知对象头中的挂起的线程们去争抢锁,争抢成功继续执行,失败继续挂起 * * 挂起线程和唤醒线程们争抢锁 会有上下文切换的时间损耗 * * * 特点: * jvm级别的锁,使用简单,自动加锁释放锁 * 可以重入: 持有锁的线程调用另一个需要此锁的方法时可以直接进入执行 * 非公平: 线程按照先来后到的顺序获取锁使用就是公平 * 不可响应中断:线程获取锁时 如果失败会一直阻塞获取锁 * 死锁: * 线程1:获取A锁的业务代码中再尝试获取另外一个B锁 * 线程2:获取B锁的业务代码中再尝试获取另外一个A锁 * 出现死锁的原因: * 持有锁未释放时尝试获取另一个锁 可能会导致死锁 * 解决: * 如何查询运行的java程序中是否出现死锁? * jps: 可以查看所有运行中的java进程 * jstack pid:可以查看pid进程的所有线程执行情况,以及是否有死锁 * 独占锁悲观锁 */
1. Lock锁简介
Lock锁是一个接口,众多实现类里我们常用的是ReentrantLock。
有三个实现:
ReentrantLock
ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock.WriteLock
相比同步锁,JUC包中的Lock锁的功能更加强大,它提供了各种各样的锁(公平锁,非公平锁,共享锁,独占锁……),所以使用起来很灵活
特点:需要手动编程实现加锁释放锁 可重入锁、独占的悲观的、可响应中断的、可公平可不公平的锁
2. 测试ReentrantLock的可重入性
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。下面以多线程抢票案例来测试ReentrantLock锁的可重入性
重入性详解(以ysnchronized为例)
首先,我们需要知道两个基本的概念:
1. 锁:是一个标识,可以用来控制对共享资源的访问。当一个线程获取了锁之后,其他试图获取该锁的线程将会阻塞,直到锁被释放。
2. 锁计数器:每一个线程都有一个锁计数器,它记录了线程获取到的锁的次数。那么,什么是重入性呢?简单的说,重入性指的就是:同一个线程在获取它已经持有的锁时,锁计数器会增加,而不是发生锁竞争。这就是重入。也就是说,如果一个线程调用一个synchronized方法,后续再调用同一个对象上的其他synchronized方法,是允许的。这是因为线程在第一次调用synchronized方法时已经获取了对象的锁,在再次调用时,发现自己已经持有该锁,这时锁计数器就加1,表示重入,然后继续执行。而如果同一个线程试图获得另一个对象的锁,或者其他线程试图获取该对象的锁,就会发生锁竞争,阻塞直到锁被释放。所以,总结一下:- 同一线程可以多次获取同一把锁,这是由于锁计数器的存在,每个线程都可以重入已经获取的锁。
- 不同的线程获取同一把锁,或者同一线程获取不同的锁,都会发生锁竞争,阻塞直到获得锁为止。举个简单的例子来说明:java public class ReentrantDemo { public synchronized void method1() { // 方法1获取this锁,锁计数器置1 System.out.println("method1"); method2(); // 重入方法2,锁计数器置2 } public synchronized void method2() { // 方法2也获取this锁,但由于是同一线程,只是重入,锁计数器置2 System.out.println("method2"); } }这里method1和method2都获取this锁,但在method1中调用method2时,由于是同一线程,锁计数器只是从1变为2,表示重入,所以不会发生锁竞争。
import java.util.concurrent.locks.ReentrantLock;
/**
* ClassName: Demo03
* Package: com.wangbaomin.juc.day01
* Description:
*
* @Author 王宝民
* @Create 2023/4/25 16:08
* @Version 1.0
*/
public class Demo03 {
public static void main(String[] args) {
SaleTicket01 saleTicket = new SaleTicket01();
new Thread(()->{
for (int i = 0; i < 40; i++) {
saleTicket.sale();
}
},"AA").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
saleTicket.sale();
}
},"BB").start();
}
}
class SaleTicket01{
//卖票
private int ticket = 20 ;
private ReentrantLock reentrantLock = new ReentrantLock();
public void sale() {
//开启ReentrantLock锁
reentrantLock.lock();
try {
//判断是否有票
if(ticket<=0){
System.out.println(Thread.currentThread().getName()+"票已售罄,余票:"+ ticket);
return;
}
this.check();//测试ReentrantLock锁的可重入性
try {
//卖票
System.out.println(Thread.currentThread().getName()+"正在出票,余票:"+ticket);
Thread.sleep(200);
ticket--;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"已出票,余票:"+ ticket);
} finally {
reentrantLock.unlock();
}
}
public synchronized void check(){
System.out.println("检票");
}
}
可以发现程序可以正常执行。。。说明该锁确实可重入。但是我们发现此时抢票的顺序不是轮询交替的,这里暴露出ReentrantLock锁的不公平性,当然可以开启公平性。不公平性主要原因在于CPU在分配时间片的时候是随机的
3. ReentrantLock锁的公平性
可以看到,当我们创建ReentrantLock锁实例时,默认创建不公平锁,开启公平锁很简单,只需要加入“true”参数
4. 死锁
1.下面先用简单代码理解一下死锁是什么?
2.万一出现了死锁,我们该怎么样排查?
/**
* ClassName: Demo04
* Package: com.wangbaomin.juc.day01
* Description:
*
* @Author 王宝民
* @Create 2023/4/25 18:18
* @Version 1.0
*/
//不可中断测试:线程在获取锁时,获取失败后如果一直堵塞,那么就是不可中断的,比如死锁
//案例分析,两个线程,一个上厕所,一个照镜子,上厕所的等着照镜子,照镜子的想着上厕所
public class Demo04 {
static Object aa = "aa";
static Object bb = "bb";
//首先需要两个对象分别持有一把锁
public static void main(String[] args) {
new Thread(()->{
Demo04.test1();
},"AA").start();
new Thread(()->{
Demo04.test2();
},"BB").start();
}
public static void test1() {
//上厕所时抢镜子
synchronized (aa){
System.out.println(Thread.currentThread().getName()+"上厕所----");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"上厕所结束,去照镜子----");
synchronized (bb){
System.out.println(Thread.currentThread().getName()+"照镜子----");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"照镜子结束");
}
}
}
public static void test2(){
//照镜子时抢厕所
synchronized (bb){
System.out.println(Thread.currentThread().getName()+"照镜子----");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"照镜子,去上厕所----");
synchronized (aa){
System.out.println(Thread.currentThread().getName()+"上厕所----");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"上厕所结束");
}
}
}
}
问题1解答:死锁就是当一个持有锁的对象去争抢另外一个对象持有的锁,双方都在等待彼此释放锁的过程,就是死锁
问题2解答:当生产环境中出现了死锁,调出CMD输入“jps”可以查看死锁发生的位置信息
输入“jstack 5556” 可以查看死锁详细信息
5. 中断测试
- lock锁是可中断的,通过Thread.interrupt()可以中断一个等待锁的线程。
- synchronzed锁是不可中断的,线程一旦试图获取就会一直等待,无法中断。import com.sun.org.apache.bcel.internal.generic.NEW; import java.util.concurrent.locks.ReentrantLock; /** * ClassName: Demo05 * Package: com.wangbaomin.juc.day01 * Description: * * @Author 王宝民 * @Create 2023/4/25 19:07 * @Version 1.0 */ public class Demo05 { //测试限时等待和中断,基于lock.tryLock public static void main(String[] args) { Demo05 demo05 = new Demo05(); //创建多个线程 for (int i = 0; i < 10; i++) { new Thread(()->{ demo05.test(); },"AA:"+i).start(); } } ReentrantLock lock = new ReentrantLock(); public void test(){ //测试中断 if(lock.tryLock()){//获取锁 System.out.println(Thread.currentThread().getName()+"获取到锁"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } lock.unlock();//释放锁 }else { System.out.println("获取锁失败"); } } }
6. 限时等待
限时等待:通过我们的tryLock方法来实现,可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以将这种方法用来解决死锁问题。
import com.sun.org.apache.bcel.internal.generic.NEW; import java.sql.Time; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** * ClassName: Demo05 * Package: com.wangbaomin.juc.day01 * Description: * * @Author 王宝民 * @Create 2023/4/25 19:07 * @Version 1.0 */ public class Demo05 { //测试限时等待基于lock.tryLock public static void main(String[] args) { Demo05 demo05 = new Demo05(); //创建多个线程 for (int i = 0; i < 10; i++) { new Thread(()->{ try { demo05.test(); } catch (InterruptedException e) { throw new RuntimeException(e); } },"AA:"+i).start(); } } ReentrantLock lock = new ReentrantLock(); public void test() throws InterruptedException { /* 测试限时等待,代码中通过lock.tryLock()指定参数来设置等待时间 测试中可以看到,我们设置了6秒等待时间,代码执行过程刚开始有个线程拿到锁后5秒钟将锁进行释放, 这时候会有第二个线程获取到锁,而其他线程依然处以等待阶段, 当第二个进程释放锁之后,等待时间已经超过6秒,那么剩余线程因为超时,所以全都获取锁失败 */ if(lock.tryLock(6, TimeUnit.SECONDS)){//获取锁 System.out.println(Thread.currentThread().getName()+"获取到锁"); try { Thread.sleep(5000); } catch (InterruptedException e) { throw new RuntimeException(e); } lock.unlock();//释放锁 }else { System.out.println("获取锁失败"); } } }
7. ReentrantReadWriteLock读写锁
在并发场景中用于解决线程安全的问题,我们几乎会高频率的使用到独占式锁,通常使用java提供的关键字synchronized或者concurrents包中实现了Lock接口的ReentrantLock。它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。
现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。大部分只是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性(出现脏读),而如果在这种业务场景下,依然使用独占锁的话,很显然这将是出现性能瓶颈的地方。针对这种读多写少的情况,java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。
接下来以缓存为例用代码演示读写锁,重现问题
① 首先不加读写锁我们看看会出现什么现象
import java.util.HashMap; import java.util.Map; /** * ClassName: Demo06 * Package: com.atguigu.juc.day01 * Description: * * @Author 王宝民 * @Create 2023/4/25 19:29 * @Version 1.0 */ public class Demo06 { //读和写 public static void main(String[] args) { //多个线程 Demo06 demo06 = new Demo06(); for (int i = 0; i < 20; i++) { int a = i ; new Thread(()->{ demo06.write("k"+a,"v"+a); },"aa"+i).start(); } for (int i = 0; i < 20; i++) { int a = i ; new Thread(()->{ demo06.read("k"+a); },"bb"+i).start(); } } private Map<String,String> map = new HashMap<>(); public void read(String key){ String s = map.get(key); System.out.println(s); } public void write(String key,String value){ map.put(key,value); } }
:可以看出在不加读写锁的时候,当我们读取数据时因为还没有来得及写入,所以会出现读取空数据的可能性
② 加入读写锁
import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * ClassName: Demo06 * Package: com.wangbaomin.juc.day01 * Description: * * @Author 王宝民 * @Create 2023/4/25 19:29 * @Version 1.0 */ public class Demo06 { //读和写 public static void main(String[] args) { //多个线程 Demo06 demo06 = new Demo06(); for (int i = 0; i < 5; i++) { int a = i ; new Thread(()->{ demo06.write("k"+a,"v"+a); },"aa"+i).start(); } for (int i = 0; i < 5; i++) { int a = i ; new Thread(()->{ demo06.read("k"+a); },"bb"+i).start(); } } private Map<String,String> map = new HashMap<>(); private ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock(); public void read(String key){ //加入读锁 rrwl.readLock().lock(); //准备读取 try { try { Thread.sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } String s = map.get(key); System.out.println(Thread.currentThread().getName()+"正在读取:"+s); } finally { rrwl.readLock().unlock(); } } public void write(String key,String value){ //加入写锁 rrwl.writeLock().lock(); //准备写入 try { try { System.out.println(Thread.currentThread().getName()+"正在写入"); Thread.sleep(500); map.put(key,value); } catch (InterruptedException e) { throw new RuntimeException(e); } } finally { rrwl.writeLock().unlock(); } } }
8.1 线程间通信(Synchronized)
需求:
两个线程操作一个初始值为0的变量,实现一个线程对变量增加1,一个线程对变量减少1,交替10轮。线程间通信模型:
生产者+消费者
通知等待唤醒机制
多线程编程模板中:
判断
干活
通知
/** * ClassName: Demo07 * Package: com.wangbaomin.juc.day01 * Description: * * @Author 王宝民 * @Create 2023/4/25 19:58 * @Version 1.0 */ public class Demo07 { /** * 线程间通信: * synchronized+ wait()+ notifyAll() * 需求:两个线程操作一个初始值为0的变量,实现一个线程对变量增加1,一个线程对变量减少1,交替10轮。 */ public static void main(String[] args) { Demo07 demo07 = new Demo07(); //创建两个线程 new Thread(()->{ for (int i = 0; i < 10; i++) { demo07.incr(); } },"aa").start(); new Thread(()->{ for (int i = 0; i < 10; i++) { demo07.decr(); } },"bb").start(); } //incr:判断a的值,如果a=0,调用incr+1 private int a = 0 ; public synchronized void incr(){ //判断 if(a!=0){ try { wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } //干活 a++; System.out.println(a); //通知 notifyAll(); } //decr:判断a的值,如果a=1,调用decr-1 public synchronized void decr(){ //判断 if(a!=1){ try { wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } //干活 a--; System.out.println(a); //通知 notifyAll(); } }
如果换成4个线程会怎样?
改造mian方法,加入CCC和DDD两个线程:
换成4个线程会导致错误,虚假唤醒
原因:wait()会释放锁, 在java多线程判断时,不能用if,程序出事出在了判断上面。
注意,消费者被唤醒后是从wait()方法(被阻塞的地方)后面执行,而不是重新从同步块开头。
解决办法:
中断和虚假唤醒是可能产生的,所以要用loop循环,if只判断一次,while是只要唤醒就要拉回来再判断一次。
if换成while
8.2 线程通信(Condition)对标synchronized
使用Condition实现线程通信,改造之前的代码(只需要改造方法):删掉incr和decr方法的synchronized
import com.sun.org.apache.bcel.internal.generic.NEW; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /** * ClassName: Demo08 * Package: com.wangbaomin.juc.day01 * Description: * * @Author 王宝民 * @Create 2023/4/25 20:32 * @Version 1.0 */ public class Demo08 { public static void main(String[] args) { //使用Condition实现线程之间的通信问题 Demo08 demo08 = new Demo08(); //准备四个线程 new Thread(()->{ for (int i = 0; i < 10; i++) { demo08.incr(); } },"AA").start(); new Thread(()->{ for (int i = 0; i < 10; i++) { demo08.decr(); } },"AA").start(); new Thread(()->{ for (int i = 0; i < 10; i++) { demo08.incr(); } },"AA").start(); new Thread(()->{ for (int i = 0; i < 10; i++) { demo08.decr(); } },"AA").start(); } private int a = 0 ; ReentrantLock lock = new ReentrantLock(); private Condition incr = lock.newCondition(); private Condition decr = lock.newCondition(); //incr增加 public void incr(){ //加锁 lock.lock(); //判断 while (a!=0){ try { incr.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } //工作 a++ ; System.out.println(a); //通知 incr.signal(); //释放锁 lock.unlock(); } //decr减少 public void decr(){ //加锁 lock.lock(); //判断 while (a!=1){ try { decr.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } //工作 a-- ; System.out.println(a); //通知 decr.signal(); //释放锁 lock.unlock(); } }
案例: 多线程之间按顺序调用,实现A->B->C。三个线程启动,要求如下: AA打印5次,BB打印10次,CC打印15次 接着 AA打印5次,BB打印10次,CC打印15次 。。。打印10轮import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /** * ClassName: Demo09 * Package: com.wangbaomin.juc.day01 * Description: * * @Author 王宝民 * @Create 2023/4/25 20:44 * @Version 1.0 */ public class Demo09 { public static void main(String[] args) { /* 案例: 多线程之间按顺序调用,实现A->B->C。三个线程启动,要求如下: AA打印5次,BB打印10次,CC打印15次 接着 AA打印5次,BB打印10次,CC打印15次 。。。打印10轮 */ Demo09 demo09 = new Demo09(); //创建三个线程 new Thread(()->{ for (int i = 0; i < 10; i++) { demo09.test1(); } }).start(); new Thread(()->{ for (int i = 0; i < 10; i++) { demo09.test2(); } }).start(); new Thread(()->{ for (int i = 0; i < 10; i++) { demo09.test3(); } }).start(); } //设置变量a ,如果a=0 ,那么执行方法打印AA, //如果a=1 ,那么执行方法打印BB, //如果a=2 ,那么执行方法打印CC, private int a = 0 ; private ReentrantLock lock = new ReentrantLock(); private Condition c1 = lock.newCondition(); private Condition c2 = lock.newCondition(); private Condition c3 = lock.newCondition(); //方法一 public void test1(){ //加锁 lock.lock(); try { //判断 while (a!=0){ try { c1.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } //工作 for (int i = 0; i < 5; i++) { System.out.print("AA\t"); } //通知 c2.signal(); a=1; } finally { lock.unlock(); } //释放锁 } //方法二 public void test2(){ //加锁 lock.lock(); //判断 try { while (a!=1){ try { c2.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } //工作 for (int i = 0; i < 10; i++) { System.out.print("BB\t"); } //通知 c3.signal(); a=2; //释放锁 } finally { lock.unlock(); } } //方法三 public void test3(){ //加锁 lock.lock(); //判断 try { while (a!=2){ try { c3.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } //工作 for (int i = 0; i < 15; i++) { System.out.print("CC\t"); } //通知 c1.signal(); a=0; System.out.println(); //释放锁 } finally { lock.unlock(); } } }
面试题:两个线程打印 两个线程,一个线程打印1-52,另一个打印字母A-Z打印顺序为12A34B...5152Z,要求用线程间通信import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /** * ClassName: Demo11 * Package: com.wangbaomin.juc.day01 * Description: * * @Author 王宝民 * @Create 2023/4/25 22:21 * @Version 1.0 */ public class Demo11 { private static int a = 1 ; private static String s = ""; private static int b = 65 ; private boolean flag = false ; /* 面试题:两个线程打印 两个线程,一个线程打印1-52,另一个打印字母A-Z打印顺序为12A34B...5152Z,要求用线程间通信 */ public static void main(String[] args) { Demo11 demo11 = new Demo11(); new Thread(()->{ while (a<=52){ demo11.test1(); } },"AA").start(); new Thread(()->{ while (b<=90){ demo11.test2(); } },"AA").start(); } // private int a = 1 ; // private String s = ""; // private int b = 65 ; // private char c = (char) b; // private int count = 0 ; private ReentrantLock lock = new ReentrantLock(); private Condition c1 = lock.newCondition(); private Condition c2 = lock.newCondition(); //打印1--52 public void test1(){ //加锁 lock.lock(); //判断 try { try { while (flag){ c1.await(); } } catch (InterruptedException e) { throw new RuntimeException(e); } //工作 for (int i = 0; i < 2; i++) { s = s + a; a+=1; System.out.println(s); } //通知 c2.signal(); flag=true; //释放锁 } finally { lock.unlock(); } } //打印A--Z(65-90) public void test2(){ //加锁 lock.lock(); //判断 try { try { while (!flag){ c2.await(); } } catch (InterruptedException e) { throw new RuntimeException(e); } //工作 // System.out.println(c); // System.out.println(b); s = s + (char) b; b+=1; System.out.println(s); //通知 c1.signal(); flag=false; //释放锁 } finally { lock.unlock(); } } }
8.3 并发容器类
8.3.1 重现线程不安全:List
首先以List作为演示对象,创建多个线程对List接口的常用实现类ArrayList进行add操作。
public class Demo01 { public static void main(String[] args) { //1.测试List集合的不安全性 List<String> list = new ArrayList<>(); for (int i = 0; i < 20; i++) { new Thread(()->{ list.add(UUID.randomUUID().toString().replace("-","").substring(1,12)); System.out.println(list); }, String.valueOf(i)).start(); } } }
出现了线程不安全错误
ArrayList在多个线程同时对其进行修改的时候,就会抛出java.util.ConcurrentModificationException异常(并发修改异常),因为ArrayList的add及其他方法都是线程不安全的,有源码佐证:
可以看出add方法上并没有synchronized或者lock锁,说明线程是不安全的
解决方案:
List接口有很多实现类,除了常用的ArrayList之外,还有Vector和SynchronizedList。
① 测试vector
public class Demo02 { public static void main(String[] args) { List<String> list = new Vector<>(); for (int i = 0; i < 10; i++) { new Thread(()->{ list.add(UUID.randomUUID().toString().replace("-","").substring(1,12)); System.out.println(list); },String.valueOf(i)).start(); } } }
② 测试SynchronizedList
public class Demo03 { public static void main(String[] args) { List<String> list = new ArrayList<>(); List<String> synList = Collections.synchronizedList(list); for (int i = 0; i < 10; i++) { new Thread(()->{ synList.add(UUID.randomUUID().toString().replace("-","").substring(1,12)); System.out.println(synList); },String.valueOf(i)).start(); } } }
分析vector和synchronizedList的add方法和迭代器源码,这两个集合的add方法都添加synchronized关键字,但是遍历集合时,迭代器涉及的代码没有加上线程同步代码 ,所以synchronizedList读数据性能相对较高,vector内存消耗比较大,适合一次增量比较大的情况
8.3.2 CopyOnWrite容器
什么是CopyOnWrite容器
CopyOnWrite容器(简称COW容器)即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。
先看看CopyOnWriteArrayList类:发现它的本质就是数组
再来看看CopyOnWriteArrayList的add方法:发现该方法加了lock锁,是线程安全的
public class Demo04 { public static void main(String[] args) { List<String> list = new CopyOnWriteArrayList(); for (int i = 0; i < 10; i++) { new Thread(()->{ list.add(UUID.randomUUID().toString().replace("-","").substring(0,12)); System.out.println(list); },String.valueOf(i)).start(); } } }
CopyOnWrite并发容器用于读多写少的并发场景。比如:白名单,黑名单。假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单一定周期才会更新一次。
缺点:
内存占用问题。写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存。通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。
数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
8.3.3 扩展类比:Set和Map
HashSet和HashMap也都是线程不安全的,类似于ArrayList,也可以通过代码证明。 报的错误 与ArrayList是相同的
public class Demo05 {
public static void main(String[] args) {
//HashMap
// Map<String,String> map = new HashMap<>();
// for (int i = 0; i < 10; i++) {
// String a = i + "";
// new Thread(()->{
// map.put(a, UUID.randomUUID().toString().replace("-","").substring(0,12));
// System.out.println(map);
// },String.valueOf(i)).start();
// }
//HashSet
Set<String> set = new HashSet<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().replace("-","").substring(0,12));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
那么集合类工具提供的Collections.synchornizedMap及Collections.synchronizedSet是否是安全的?测一下
public class Demo06 { public static void main(String[] args) { //SynchronizedMap() // Map<String,String> map = new HashMap<>(); // Map<String, String> synMap = Collections.synchronizedMap(map); // for (int i = 0; i < 20; i++) { // String a = i + ""; // new Thread(()->{ // synMap.put(a, UUID.randomUUID().toString().replace("-","").substring(0,12)); // System.out.println(synMap); // },String.valueOf(i)).start(); // } //SynchronizedSet() Set<String> set = Collections.synchronizedSet(new HashSet<>()); for (int i = 0; i < 30; i++) { new Thread(()->{ set.add(UUID.randomUUID().toString().replace("-","").substring(0,12)); System.out.println(set); },String.valueOf(i)).start(); } } }
结论是两种都是线程安全的,还有一个古老的线程安全的Map:HashTable(同样是在方法上加上了synchornized锁,读写效率都不高)
8.3.4 ConcurrentHashMap并发容器实现线程安全的原因
public class Demo07 { public static void main(String[] args) { Map<String, String> map = new ConcurrentHashMap<>(); for (int i = 0; i < 20; i++) { String a = i + ""; new Thread(()->{ map.put(a, UUID.randomUUID().toString().replace("-","").substring(0,12)); System.out.println(map); },String.valueOf(i)).start(); } } } //源码 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 (Node<K,V>[] tab = table;;) { 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 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 (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; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ((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; }
①第一次添加元素
②第二次添加元素
8.3.5 总结常见并发容器和同步容器
同步容器:
Vector、HashTable、Collections工具类中synchronized下的集合
并发容器:
CopyOnWriteArrayList、CopyOnWriteSet、ConcurrentHashMap
8.3.6 性能测试
①引入依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.3.18</version> </dependency>
import org.springframework.util.StopWatch; import java.util.List; import java.util.Random; import java.util.UUID; import java.util.Vector; import java.util.stream.IntStream; /** * ClassName: Demo08 * Package: com.wangbaomin.juc.day02 * Description: 性能测试 * * @Author 王宝民 * @Create 2023/4/26 19:27 * @Version 1.0 */ public class Demo08 { public static void main(String[] args) { StopWatch stopWatch = new StopWatch(); //1.创建List容器写入数据 List<Integer> list = new Vector<>(); //2.开始计时 stopWatch.start("vector->写入数据"); IntStream.rangeClosed(0,100000).parallel().forEach(a->{ list.add(new Random().nextInt(10000)); }); //3.计时结束 stopWatch.stop(); //4.开始读取数据 stopWatch.start("vector->读取数据"); IntStream.rangeClosed(0,100000).parallel().forEach(a->{ list.get(new Random().nextInt(10000)); }); //5.计时结束 stopWatch.stop(); //获取时间 System.out.println(stopWatch.prettyPrint()); } }
9. JUC强大的辅助类
JUC的多线程辅助类非常多,这里介绍三个:
CountDownLatch(倒计数器)
CyclicBarrier(循环栅栏)
Semaphore(信号量)
9.1 CountDownLatch (倒计数器)
需求:6个同学陆续离开教室后值班同学才可以关门。
import java.util.Random;
import java.util.concurrent.CountDownLatch;
/**
* ClassName: Demo09
* Package: com.wangbaomin.juc.day02
* Description: CountDownLatch
*
* @Author 王宝民
* @Create 2023/4/26 19:40
* @Version 1.0
*/
public class Demo09 {
public static void main(String[] args) {
//案例:6个同学陆续离开教室后值班同学才可以关门。
//创建6个线程代表6个同学
CountDownLatch downLatch = new CountDownLatch(6);
for (int i = 1; i <=6; i++) {
new Thread(()->{
try {
System.out.println("第"+Thread.currentThread().getName()+"位童鞋准备出去");
Thread.sleep(new Random().nextInt(5000));
System.out.println("第"+Thread.currentThread().getName()+"位童鞋出去了");
downLatch.countDown();//每当一位同学出门,计时器就会自动减1
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},String.valueOf(i)).start();
}
try {
downLatch.await();//当countDown计时器为0时释放等待执行下面代码
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("------关门------");
}
}
CountDownLatch 与 join 方法的区别
调用一个子线程的 join()方法后,该线程会一直被阻塞直到该线程运行完毕。而 CountDownLatch 则使用计数器允许子线程运行完毕或者运行中时候递减计数,也就是 CountDownLatch 可以在子线程运行任何时候让 await 方法返回而不一定必须等到线程结束;另外使用线程池来管理线程时候一般都是直接添加 Runnable 到线程池这时候就没有办法在调用线程的 join 方法了,countDownLatch 相比 Join 方法让我们对线程同步有更灵活的控制。
9.2 CyclicBarrier(循环栅栏)
从字面上的意思可以知道,这个类的中文意思是“循环栅栏”。大概的意思就是一个可循环利用的屏障。该命令只在每个屏障点运行一次。若在所有参与线程之前更新共享状态,此屏障操作很有用
常用方法:
CyclicBarrier(int parties, Runnable barrierAction) 创建一个CyclicBarrier实例,parties指定参与相互等待的线程数,barrierAction一个可选的Runnable命令,该命令只在每个屏障点运行一次,可以在执行后续业务之前共享状态。该操作由最后一个进入屏障点的线程执行。
CyclicBarrier(int parties) 创建一个CyclicBarrier实例,parties指定参与相互等待的线程数。
await() 该方法被调用时表示当前线程已经到达屏障点,当前线程阻塞进入休眠状态,直到所有线程都到达屏障点,当前线程才会被唤醒。
需求:通关游戏,3人一起通关,3道关卡,3人同时完成通关方可进入下一关卡
import java.util.Random; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; /** * ClassName: Demo10 * Package: com.wangbaomin.juc.day02 * Description: 测试 CyclicBarrier * * @Author 王宝民 * @Create 2023/4/26 19:55 * @Version 1.0 */ public class Demo10 { public static void main(String[] args) { //通关游戏,3人一起通关,3道关卡,3人同时完成通关方可进入下一关卡 CyclicBarrier barrier = new CyclicBarrier(3,()->{ System.out.println("恭喜通关");//每个线程最后一个通关者会调用 }); for (int i = 1; i <= 3; i++) { new Thread(()->{ try { System.out.println("第"+Thread.currentThread().getName()+"人准备通第一关"); Thread.sleep(new Random().nextInt(5000)); System.out.println("第"+Thread.currentThread().getName()+"人通过第一关"); } catch (InterruptedException e) { throw new RuntimeException(e); } try { barrier.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (BrokenBarrierException e) { throw new RuntimeException(e); } try { System.out.println("第"+Thread.currentThread().getName()+"人准备通第二关"); Thread.sleep(new Random().nextInt(5000)); System.out.println("第"+Thread.currentThread().getName()+"人通过第二关"); } catch (InterruptedException e) { throw new RuntimeException(e); } try { barrier.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (BrokenBarrierException e) { throw new RuntimeException(e); } try { System.out.println("第"+Thread.currentThread().getName()+"人准备通第三关"); Thread.sleep(new Random().nextInt(5000)); System.out.println("第"+Thread.currentThread().getName()+"人通过第三关"); } catch (InterruptedException e) { throw new RuntimeException(e); } try { barrier.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (BrokenBarrierException e) { throw new RuntimeException(e); } },String.valueOf(i)).start(); } } }
CyclicBarrier和CountDownLatch的区别?
CountDownLatch和CyclicBarrier都是Java并发包下的同步工具类,用于协调多个线程之间的同步。但是,它们的作用和原理是不同的:CountDownLatch:- 作用:允许一个或多个线程等待其他线程完成操作后再执行。
- 原理:通过一个计数器进行衰减实现的。计数器的值表示有多少线程没有完成任务。await()方法会阻塞当前线程,直到计数器为0。countDown()方法会对计数器减1。
- 特点:计数器的值只能衰减到0,不可重置。CyclicBarrier:- 作用:允许一组线程等待彼此到达一个公共屏障点后再继续执行。
- 原理:通过定义屏障点的个数实现的。每个线程调用await()方法到达屏障点后被阻塞,直到所有线程都到达屏障点后再唤醒继续执行。
- 特点:当所有线程都到达屏障点后,屏障点的个数会恢复初始值,可以重用。所以,总结来说,CountDownLatch和CyclicBarrier的主要区别在于:- CountDownLatch用于等待其他线程完成任务,而CyclicBarrier用于等待其他线程达到同一个屏障点。
- CountDownLatch通过计数器进行衰减,且只能从一个值衰减到0。CyclicBarrier通过设置屏障点的个数,并且可以重用。
- CountDownLatch的计数器无法重置,而CyclicBarrier的屏障点个数可以重置为初始值。
- CountDownLatch主要用于一次性同步,而CyclicBarrier可以用于多次同步。
9.3 Semaphore 信号量
Semaphore翻译成字面意思为 “信号量”
Semaphore可以控制同时访问的线程个数。非常适合需求量大,而资源又很紧张的情况。
比如给定一个资源数目有限的资源池,假设资源数目为N,每一个线程均可获取一个资源,但是当资源分配完毕时,后来线程需要阻塞等待,直到前面已持有资源的线程释放资源之后才能继续。
常用方法:
public Semaphore(int permits) // 构造方法,permits指资源数目(信号量) public void acquire() throws InterruptedException // 占用资源,当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。 public void release() // (释放)实际上会将信号量的值加1,然后唤醒等待的线程。信号量主要用于两个目的:
多个共享资源的互斥使用。
用于并发线程数的控制。保护一个关键部分不要一次输入超过N个线程。
import java.util.Random; import java.util.concurrent.Semaphore; /** * ClassName: Demo11 * Package: com.wangbaomin.juc.day02 * Description: 测试 Semaphore * * @Author 王宝民 * @Create 2023/4/26 20:17 * @Version 1.0 */ public class Demo11 { public static void main(String[] args) { //案例:6辆车抢占3个车位 //6个线程 3个公共资源 Semaphore semaphore = new Semaphore(3); for (int i = 1; i <= 6; i++) { new Thread(()->{ try { System.out.println("车辆"+Thread.currentThread().getName()+"准备停入"); semaphore.acquire(); System.out.println("车辆"+Thread.currentThread().getName()+"已停入"); Thread.sleep(new Random().nextInt(5000)); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("车辆"+Thread.currentThread().getName()+"已驶离"); semaphore.release(); },String.valueOf(i)).start(); } } }
10. callable接口
Thread类、Runnable接口使得多线程编程简单直接。
但Thread类和Runnable接口都不允许声明检查型异常,也不能定义返回值。没有返回值这点稍微有点麻烦。不能声明抛出检查型异常则更麻烦一些。
public void run()方法规范意味着你必须捕获并处理检查型异常。即使你小心捕获异常,也不能保证这个类(Runnable对象)的所有使用者都读取异常信息。
以上两个问题现在都得到了解决。从java5开始,提供了Callable接口,是Runable接口的增强版。用Call()方法作为线程的执行体,增强了之前的run()方法。因为call方法可以有返回值,也可以声明抛出异常。
这里出现了一个FutureTask,先认识该类
发现:FutureTask其实充当了一个中间人的角色,通过代码来测试一下
/** * 1. 创建Callable的实现类,并重写call()方法,该方法为线程执行体,并且该方法有返回值 */ class MyCallableThread implements Callable<Integer>{ @Override public Integer call() throws Exception { System.out.println(Thread.currentThread().getName() + "执行了!"); return 200; } } public class CallableDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { // 2. 创建Callable的实例,并用FutureTask类来包装Callable对象 // 3. 创建FutureTask对象,需要一个Callable类型的参数 FutureTask task = new FutureTask<Integer>(new MyCallableThread()); // 4. 创建多线程,由于FutureTask的本质是Runnable的实现类,所以第一个参数可以直接使用task new Thread(task, "threadName").start(); //new Thread(task, "threadName2").start(); /*while (!task.isDone()) { System.out.println("wait..."); }*/ System.out.println(task.get()); System.out.println(Thread.currentThread().getName() + " over!"); } }
FutureTask:未来的任务,用它就干一件事,异步调用。通常用它解决耗时任务,挂起堵塞问题。
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。
一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
FutureTask仅在call方法完成时才能get结果;如果计算尚未完成,则阻塞 get 方法。
一旦计算完成,就不能再重新开始或取消计算。get方法获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。
注意:
为了防止主线程阻塞,建议get方法放到最后
只计算一次,FutureTask会复用之前计算过得结果
创建多个线程,会怎样?(重用问题)
运行结果:依然只有一个就是threadName。
如果想打印threadName2的结果,即不想复用之前的计算结果。怎么办?再创建一个FutureTask对象即可。
① callable接口与runnable接口的区别?
相同点:都是接口,都可以编写多线程程序,都采用Thread.start()启动线程
不同点:
具体方法不同:一个是run,一个是call
Runnable没有返回值;Callable可以返回执行结果,是个泛型
Callable接口的call()方法允许抛出异常;Runnable的run()方法异常只能在内部消化,不能往上继续抛
它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。
② 获得多线程的方法几种?
传统的是继承thread类和实现runnable接口
java5以后又有实现callable接口和java的线程池获得
11. 阻塞队列(BlockingQueue)
栈与队列简单回顾:
栈(Stack):先进后出,后进先出
队列:先进先出
11.1 什么是BlockingQueue
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起
BlockingQueue即阻塞队列,是java.util.concurrent下的一个接口,因此不难理解,BlockingQueue是为了解决多线程中数据高效安全传输而提出的。从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种:
当队列满了的时候进行入队列操作
当队列空了的时候进行出队列操作
因此,当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空队列进行出队列操作时,它将会被阻塞,除非有另一个线程进行了入队列操作。
阻塞队列主要用在生产者/消费者的场景,下面这幅图展示了一个线程生产、一个线程消费的场景:
为什么需要BlockingQueue 好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
10.2 认识BlockingQueue
java.util.concurrent 包里的 BlockingQueue是一个接口,继承Queue接口,Queue接口继承 Collection。
BlockingQueue接口主要有以下7个实现类:
ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
LinkedTransferQueue:由链表组成的无界阻塞队列。
LinkedBlockingDeque:由链表组成的双向阻塞队列。
BlockingQueue接口有以下几个方法:
它的方法可以分成以下4类:
抛出异常
add正常执行返回true,element(不删除)和remove返回阻塞队列中的第一个元素
当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full
当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException
当阻塞队列空时,再调用element检查元素会抛出NoSuchElementException
特定值 插入方法,成功ture失败false 移除方法,成功返回出队列的元素,队列里没有就返回null 检查方法,成功返回队列中的元素,没有返回null
一直阻塞
如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
当阻塞队列满时,再往队列里put元素,队列会一直阻塞生产者线程直到put数据or响应中断退出
当阻塞队列空时,再从队列里take元素,队列会一直阻塞消费者线程直到队列可用
超时退出
如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。
返回一个特定值以告知该操作是否成功(典型的是 true / false)。
10.3 代码演示
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* ClassName: Demo01
* Package: com.wangbaomin.juc.day03
* Description: //BlockingQueue
*
* @Author 王宝民
* @Create 2023/4/27 13:45
* @Version 1.0
*/
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
//测试阻塞队列BlockingQueue
/*
1. add(e) remove() element() 抛出异常
2. offer(e) poll() peek() 特殊值
3. put(e) take() 阻塞
4. offer(e,time,unit) poll(time,unit) 超时
*/
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
//1.
/*
queue.add(1);
queue.add(2);
queue.add(3);
queue.add(4);//超出队列长度时会报异常
System.out.println(queue.remove());//会移除当前队列中第一个元素(先进先出)
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.element());//会获取当前队列中第一个元素,队列为空时会报异常
*/
//2.
/*
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);//超过队列长度的元素不会被加入队列中,也不会抛出异常
System.out.println(queue.poll());//先进先出,队列中没有元素时不会抛出异常,返回null值
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.peek());//获取元素,队列中没有元素返回null值
*/
//3.
/*
queue.put(1);
queue.put(2);
queue.put(3);
new Thread(()->{
queue.poll();
}).start();
queue.put(4);
System.out.println(queue.take());
*/
//4.
System.out.println(queue.offer(1, 200, TimeUnit.SECONDS));//加入堵塞队列成功返回true
queue.offer(2,2, TimeUnit.SECONDS);
queue.offer(3,2, TimeUnit.SECONDS);
System.out.println(queue.offer(4, 2, TimeUnit.SECONDS));//超出队列长度无法加入,超时时间到达后会被拒绝加入返回false
System.out.println(queue.poll(2, TimeUnit.SECONDS));
}
}
11. ThreadPool线程池
例子: 10年前单核CPU电脑,假的多线程,像马戏团小丑玩多个球,CPU需要来回切换。 现在是多核电脑,多个线程各自跑在独立的CPU上,不用切换效率高。
线程池的优势: 线程池做的工作主要是控制运行的线程数量,如果线程数量超过了最大数量,超出数量的线程排队等候,等线程任务执行完毕,再从队列中取出任务来执行。
它的主要特点为:线程复用;控制最大并发数;管理线程。
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,ExecutorService,ThreadPoolExecutor这几个类。
Executor接口是顶层接口,只有一个execute方法,过于简单。通常不使用它,而是使用ExecutorService接口:
那么问题来了,怎么创建一个连接池对象呢?通常使用Executors工具类
11.1 Executors工具类
架构图可以看到Executors工具类,有没有联想到Collections,Arrays等。没错,可以用它快速创建线程池。
List list = Arrays.asList(""); ExecutorService threadPool = Executors.newCachedThreadPool();
直接编码演示:每种连接池的效果
import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; /** * ClassName: Demo02 * Package: com.wangbaomin.juc.day03 * Description: ThreadPool 线程池的使用 * * @Author 王宝民 * @Create 2023/4/27 18:14 * @Version 1.0 */ public class Demo02 { //测试Executors.newSingleThreadExecutor() 一个线程进行测试 public static void main1(String[] args) { //创建线程池 ExecutorService pool = Executors.newSingleThreadExecutor(); pool.submit(()->{ System.out.println(Thread.currentThread().getName()+"执行了"); }); //单一的线程池只有一个线程,最多也只有一个,队列长度index.max,存在oom的可能性 /* return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); } */ pool.shutdown(); } //测试Executors.newSingleThreadExecutor() 多个线程进行测试 public static void main2(String[] args) { ExecutorService pool = Executors.newSingleThreadExecutor(); //测试多个线程是怎样的 for (int i = 1; i <= 10; i++) { pool.submit(()->{ System.out.println(Thread.currentThread().getName()+"执行了"); }); } //测试结果是有任务执行时,初始化一个核心线程, // 后面的线程任务会被放入阻塞队列中等前面线程执行完任务后回来队列中一个个取任务来执行 pool.shutdown(); } //测试Executors.newCacheThreadPool() 一个线程进行测试 public static void main3(String[] args) { ExecutorService pool = Executors.newCachedThreadPool(); pool.submit(()->{ System.out.println(Thread.currentThread().getName()+"被执行了"); }); /* public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } */ //分析参数,核心线程数没有被设置,最大线程数为index.max,设置了60s超时过期和非公平消息队列 pool.shutdown(); } //测试Executors.newCacheThreadPool() 多个线程进行测试 public static void main4(String[] args) { ExecutorService pool = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { pool.submit(()->{ System.out.println(Thread.currentThread().getName()+"被执行了"); }); } /* public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } */ //分析,会根据线程数量来初始化线程数量,存在oom的可能性大 pool.shutdown(); } //测试Executors.newFixedThreadPool() 固定线程数 public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(5); for (int i = 0; i < 10; i++) { Future<String> future = pool.submit(() -> { System.out.println(Thread.currentThread().getName() + "被执行了"); return "success"; }); } /* public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } */ //分析参数,1.固定的核心线程数 2.最大的线程数就是设置的线程数 // 3. 不维护空闲时间对空闲线程的管理 4.队列最大值为index.max 存在oom的可能 pool.shutdown(); } }
11.2. 线程池7大参数及自定义线程池
import java.util.concurrent.*;
/**
* ClassName: Demo03
* Package: com.wangbaomin.juc.day03
* Description: 线程池的7大参数
*
* @Author 王宝民
* @Create 2023/4/27 18:26
* @Version 1.0
*/
public class Demo03 {
public static void main(String[] args) {
//测试线程池的参数含义
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,
5,
3,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(8),
Executors.defaultThreadFactory(),
(Runnable r ,ThreadPoolExecutor e)->{
new Thread(()->{
r.run();
System.out.println(Thread.currentThread().getName()+"被其他线程执行");
},"AA").start();
System.out.println("自定义拒绝策略"+e);
}
);
System.out.println(pool);
for (int i = 1; i <= 14; i++) {
String a = i + "";
pool.execute(()->{
try {
Thread.sleep(200);//为了避免出现线程复用 这里延迟一会
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(a + Thread.currentThread().getName()+"被执行了");
});
}
pool.shutdown();
}
/*
int corePoolSize, 核心线程数
int maximumPoolSize, 最大的线程数
long keepAliveTime, 空闲时间
TimeUnit unit, 空闲时间单位
BlockingQueue<Runnable> workQueue, 阻塞队列
ThreadFactory threadFactory, 自定义线程池
RejectedExecutionHandler handler{ 拒绝访问处理器(有四种实现类)
1. new ThreadPoolExecutor.AbortPolicy() 会抛出异常且溢出的任务不会被执行
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.atguigu.juc.day03.Demo03$$Lambda$1/883049899@312b1dae rejected from java.util.concurrent.ThreadPoolExecutor@7530d0a[Running, pool size = 5, active threads = 4, queued tasks = 7, completed tasks = 1]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at com.atguigu.juc.day03.Demo03.main(Demo03.java:27)
2. new ThreadPoolExecutor.CallerRunsPolicy() 溢出的线程会被主线程 main执行
3. new ThreadPoolExecutor.DiscardOldestPolicy() 会将队列中等待时间最长的线程丢弃
分析:
比如上面代码有14个线程
1,2,3 前三个线程任务被核心线程执行
4,5,6,7,8,9,10,11, 会被放入队列中等待执行
12,13 最大线程数为5,那么会开启剩余2个线程来处理这两个线程任务
14 当第14个线程溢出时,会将队列中等待时间最久的4抛弃 代码执行结束发现没有4线程任务被执行的痕迹
4. new ThreadPoolExecutor.DiscardPolicy() 溢出的线程任务会被丢弃,不会抛异常
5. 如果线程任务都很重要,既不想被抛出异常,又不想被丢弃,还有一种方式自定义处理拒绝策略
(Runnable r ,ThreadPoolExecutor e)->{
r.run(); //r代表的是被拒绝执行的任务,这里会被主线程执行,
当然也可以将被拒绝的任务放入单独的线程去执行,也可以放入缓存中
System.out.println("自定义拒绝策略"+e); //e 代表不能被执行线程任务r的线程池
}
}
测试结果:
任务执行前线程数为0
任务开始时,如果任务量小于核心线程数,那么线程池就加载真实任务量的线程数来执行任务
如果任务量大于核心线程数但小于核心线程数+阻塞队列总数,那么依然会按照最大核心线程数来执行任务,剩下的会进入阻塞队列中等待前面执行结束的线程回来取任务
如果任务量大于核心线程数且大于核心线程数+阻塞队列总数但小于核心线程数+队列长度+(最大线程数-核心线程数),那么线程池会普通线程启动执行,最多有最大线程数个线程一起执行任务
如果任务量高于最大线程数+任务队列,那么多出来的任务将不会被执行
*/
}
11.3 自定义线程池线程数说明
CPU密集型 vs IO密集型
开发中我们可以把任务分为计算(CPU)密集型和IO密集型。
计算(CPU)密集型任务大部份时间用来做计算、逻辑判断,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,任务同时进行的数量应当等于CPU的核心数。一般公式:线程数量=CPU核数+1个
IO密集型CPU消耗很少,任务的大部分时间都在等待IO操作完成(99%的时间都花在IO上,花在CPU上的时间很少)。此类任务,任务越多,CPU效率越高,但也有一个限度。大部分任务都是IO密集型任务,比如Web应用。一般公式:线程数量=CPU核数/(1-阻塞系数) 阻塞系数为0.8~0.9之间
线程池底层工作原理
执行流程:
在创建了线程池后,线程池中的线程数为零。
当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
当一个线程完成任务时,它会从队列中取下一个任务来执行。
当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
11.4 拒绝策略
一般我们创建线程池时,为防止资源被耗尽,任务队列都会选择创建有界任务队列,但种模式下如果出现任务队列已满且线程池创建的线程数达到你设置的最大线程数时,这时就需要你指定ThreadPoolExecutor的RejectedExecutionHandler参数即合理的拒绝策略,来处理线程池"超载"的情况。
ThreadPoolExecutor自带的拒绝策略如下:
AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中 尝试再次提交当前任务。
DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。 如果允许任务丢失,这是最好的一种策略。
以上内置的策略均实现了RejectedExecutionHandler接口,也可以自己扩展RejectedExecutionHandler接口,定义自己的拒绝策略