狂神说_JUC并发编程_1
0.学习方法
源码+官方文档:
其实官方文档就是源码编译出来的,其本质还是看源码,不过文档会比较方便学习
- 只有多看源码,多研究文档才会进步
- Java英文文档可以通过点击查看源码获取
- Java1.8中文文档(中文 – 谷歌版)
- 在线版: https://blog.fondme.cn/apidoc/jdk-1.8-google/
- 下载链接: https://pan.baidu.com/s/10wTC1F_4EUPsHzrn-_sPTw 密码:k7rm
1.什么是JUC
JUC其实就是Java.Util.concurrent包的缩写
java.util.concurrent
java.util.concurrent.atomi
java.util.concurrentlocks
是 java.util 工具包、包、分类
- 回顾开启线程的三种方式:
Thread
Runnable
Callable
2.线程与进程
线程、进程,如果不能使用一句话说出来的技术,不扎实!
打开(Win10)任务管理器可以清楚看到执行的线程与进程:
参考博客:什么是线程?什么是进程
进程:
- 官方定义:
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,是操作系统结构的基础
- 简单理解:
进行(运行)中的程序,如打开任务管理器后中各种.exe程序
线程:
- 官方定义:
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
- 简单理解:
线程是真正执行资源调度(使程序跑起来)的主体,一个进程往往可以包含多个线程,但至少包含一个线程。
如:开一个idea进程,其中至少有—> 线程1:输入代码,线程2:自动保存
😶老是强调多线程,那么 Java真的可以开启线程吗?
答案是 : 不能。查看Thread.start()
源码可得知:
public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } } // 本地方法,底层的C++ ,Java 无法直接操作硬件 private native void start0();
并发编程
并发编程:并发、并行
- 并发(多线程操作同一个资源)
- CPU 一核 ,模拟出来多条线程,天下武功,唯快不破,快速交替
- 并行(多个人一起行走)
- CPU 多核 ,多个线程可以同时执行; 线程池
public class Test1 { public static void main(String[] args) { // 获取cpu的核数 // CPU 密集型,IO密集型 System.out.println(Runtime.getRuntime().availableProcessors()); //输出为8 //说明笔者电脑为八核处理器 } }
线程的几个状态:
从源码回答,有理有据
public enum State { // 新生 NEW, // 运行 RUNNABLE, // 阻塞 BLOCKED, // 等待,死死地等 WAITING, // 超时等待 TIMED_WAITING, // 终止 TERMINATED; }
wait与sleep的区别
看源码说话嗷🎈
//Object.wait() public final void wait() throws InterruptedException { wait(0); } //Thread.sleep() public static native void sleep(long millis) throws InterruptedException;
来自不同的类
- wait() 来自 Object类
- sleep() 来自 Thread类
关于锁的释放
- wait() 会释放锁:wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。
- sleep() 不出让系统资源;(简单来说,就是抱着🔒睡觉)
是否需要捕获异常?
需要。源码都在上面写的死死的,throws InterruptedException 😓都不知网上随手一搜的博客说wait() 不用捕获异常怎么搞得。
使用范围:
wait() 需要在同步代码块中使用
// wait、notify/notifyAll必须在同步控制块、同步方法里面使用。而sleep的使用在任意地方。 synchronized(x){ x.notify() //或者wait() }
-
sleep()可以在任何地方睡
-
-
作用对象:
- wait()定义在Object类中,作用于对象本身
- sleep()定义在Thread 类中,作用当前线程。
-
方法属性:
- wait()是实例方法
- sleep()是静态方法 有
static
并发类中进程睡眠使用的函数TimeUnit.Days.sleep(1)
同时只要线程都有中断异常,所以wait也有中断异常。
3.Lock锁(重点)
- 回顾用传统的 synchronized 实现 线程安全的卖票例子
真正的多线程开发,公司中的开发,降低耦合性 线程就是一个单独的资源类,没有任何附属的操作! 对于资源类只有: 属性、方法
开启线程错误方式:
class Ticket implements Runnable{}
耦合度高,违背了oop(面向对象)思想
public class SaleTicket_WithSynchronized { public static void main(String[] args) { // 并发:多线程操作同一个资源类, 把资源类丢入线程 Ticket ticket = new Ticket(); // @FunctionalInterface 函数式接口,jdk1.8 lambda表达式 (参数)->{ 代码 } new Thread(() -> { for (int i = 1; i < 40; i++) { ticket.saleTicket(); } }, "A").start(); new Thread(() -> { for (int i = 1; i < 40; i++) { ticket.saleTicket(); } }, "B").start(); new Thread(() -> { for (int i = 1; i < 40; i++) { ticket.saleTicket(); } }, "C").start(); } } //资源类 OOP: class Ticket { //属性、方法 private int number = 30; //卖票方法 //用synchronized 上锁 public synchronized void saleTicket() { if (number > 0) { System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票,剩余" + number + "张"); } } }
lambda表达式简化了runnable函数式接口的使用,使得程序更加简洁,保证了资源类的独立性和程序的解耦合
package com.bupt; public class SaleTicket { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(()->{ for (int i = 0; i < 4; i++) { ticket.saleTicket(); } },"A").start(); //上下两个线程写法是等价的,Runable匿名内部类使用lambda表达式进行了简化 new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 4; i++) { ticket.saleTicket(); } } }, "B").start(); } } class Ticket{ //只是一个资源类,保证只遵循oop原则 private int number = 30; public synchronized void saleTicket(){ if(number > 0){ System.out.println(Thread.currentThread().getName() + "卖出了第" + number-- + "张票,剩余"+ number); } } }
用Lock接口
Lock源码定义:
常用上锁语句:
Lock l = ...; l.lock(); //加锁 try { // access the resource protected by this lock } finally { l.unlock(); //解锁}
ReentrantLock 可重入锁
-
公平锁与非公平锁(简单认识,后面详解)
- 公平锁: 非常公平, 不能够插队,必须先来后到!
- 非公平锁:非常不公平,可以插队 (默认都是非公平)
-
上案例
// Lock 三部曲 // 1、 new ReentrantLock(); // 2、 lock.lock(); // 加锁 // 3、 finally=> lock.unlock(); // 解锁 public class SaleTicket_WithLock { public static void main(String[] args) { // 并发:多线程操作同一个资源类, 把资源类丢入线程 Ticket ticket = new Ticket(); // @FunctionalInterface 函数式接口,jdk1.8 lambda表达式 (参数)->{ 代码 } new Thread(() -> { for (int i = 1; i < 40; i++) { ticket.saleTicket(); } }, "A").start(); new Thread(() -> { for (int i = 1; i < 40; i++) { ticket.saleTicket(); } }, "B").start(); new Thread(() -> { for (int i = 1; i < 40; i++) { ticket.saleTicket(); } }, "C").start(); } } class Ticket2 { // 属性、方法 private int number = 30; Lock lock = new ReentrantLock(); public void sale() { lock.lock(); // 加锁 try { // 业务代码 if (number > 0) { System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票,剩余:" + number + "张"); } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); // 解锁 } } }
Synchronized 和 Lock 区别
Lock为什么比synchronized 能更好的实现同步访问
Java中的锁——Lock和synchronized
4、相比于synchronized,Lock接口所具备的其他特性
①尝试非阻塞的获取锁tryLock():当前线程尝试获取锁,如果该时刻锁没有被其他线程获取到,就能成功获取并持有锁
②能被中断的获取锁lockInterruptibly():获取到锁的线程能够响应中断,当获取到锁的线程被中断的时候,会抛出中断异常同时释放持有的锁
③超时的获取锁tryLock(long time, TimeUnit unit):在指定的截止时间获取锁,如果没有获取到锁返回false
ReentrantLock中lock(),tryLock(),lockInterruptibly()的区别
join方法:原来主线程和子线程是并行的关系,但是一旦使用了join()方法,就会变成串行的关系;当主线程调用子线程的join()方法时,意味着必须等子线程执行完毕之后,主线程才会开始执行。
yield是线程失去CPU资源,从运行状态变成就绪状态
wait是释放当前锁,让其他线程获取锁,当被唤醒后就参与获取锁的竞争
非公平锁按照一定的策略比如耗时等选择线程执行,但是公平锁是按照先来后到,一般选择非公平锁
4.生产者消费者问题
从pc(producer<–>consumer)问题看锁的本质
1.synchronized版本
-
生产者消费者synchronized版本
案例:对一个数字不停进行 +1 -1操作,加完了减,减完了加
public class PC_WithSynchronized { public static void main(String[] args) { Data data = new Data(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"ADD").start(); new Thread(()->{ for (int i = 0; i < 10; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"MINUS").start(); } } //判断等待-->业务代码-->通知 //数字:资源类 class Data{ //属性 private int number = 0; //+1方法 public synchronized void increment() throws InterruptedException { if (number != 0){ //等待 this.wait(); } number++; System.out.println(Thread.currentThread().getName()+"-->"+number); //加完了通知其他线程 this.notifyAll(); } //-1 public synchronized void decrement() throws InterruptedException { if (number == 0){ //等待 this.wait(); } number--; System.out.println(Thread.currentThread().getName()+"-->"+number); //减完了通知其他线程 this.notifyAll(); } }
此时输出:
加减交替执行,一片祥和~
.. ADD-->1 MINUS-->0 ADD-->1 MINUS-->0 ADD-->1 MINUS-->0 ADD-->1 MINUS-->0 ADD-->1 MINUS-->0 ...
问题来了,现在只有"ADD"和"MINUS"两个线程执行操作,如果增加多两个线程会怎样呢?
于是在main方法中增加了"ADD2" 和 "MINUS2"两条线程:
new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"ADD2").start(); new Thread(()->{ for (int i = 0; i < 10; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"MINUS2").start();
出大问题了:出现了数据错误甚至死锁问题
原因如下:
- 用if判断只执行了一次判断,而wait()方法会导致🔒的释放
- 具体说明:以两个加法线程ADD、ADD2举例:
- 比如ADD先执行,执行时调用了wait方法,那它会等待,此时会释放锁。
- 那么线程ADD2 获得锁并且也会执行wait()方法,且释放锁,同时两个加线程一起进入等待状态,等待被唤醒。
- 此时减线程中的某一个线程执行完毕并且唤醒了这俩加线程(notifyAll),那么这俩加线程不会一起执行,其中ADD获取了锁并且加1,执行完毕之后ADD2再执行。
- 如果是if的话,那么ADD修改完num后,ADD2不会再去判断num的值,直接会给num+1,如果是while的话,ADD执行完之后,ADD2还会去判断num的值,因此就不会执行。
- 上述情况称为:虚假唤醒
-
此时解决方法:将 if 改为 while
package com.kuangstudy.JUC; /** * @BelongsProject: itstack-demo-design-1-00 * @BelongsPackage: com.kuangstudy.JUC * @Author: lgwayne * @CreateTime: 2021-02-20 17:32 * @Description: */ public class PC_WithSynchronized { public static void main(String[] args) { Data data = new Data(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"ADD").start(); new Thread(()->{ for (int i = 0; i < 10; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"MINUS").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"ADD2").start(); new Thread(()->{ for (int i = 0; i < 10; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"MINUS2").start(); } } //判断等待-->业务代码-->通知 //数字:资源类 class Data{ //data类相同,不再赘述 }
虚假唤醒的一个形象的例子
当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功
1.比如说买货,如果商品本来没有货物,突然进了一件商品,这是所有的线程都被唤醒了,但是只能一个人买,所以其他人都是假唤醒,获取不到对象的锁
相关博文:https://blog.csdn.net/qq_39455116/article/details/87101633
结果一片祥和~
... ADD2-->1 MINUS2-->0 ADD2-->1 MINUS2-->0 ADD2-->1 MINUS2-->0 ADD2-->1 MINUS2-->0 ADD2-->1 MINUS2-->0 ...
通过Lock找到condition来配合控制对线程的唤醒
public class PC_WithLock { public static void main(String[] args) { //主线程测试方法与上述一致,不再赘述 } } //判断等待-->业务代码-->通知 //数字:资源类 class Data_withLock { //属性 private int number = 0; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); //+1方法 public void increment() throws InterruptedException { lock.lock(); try { while (number != 0) { //等待 condition.await(); } number++; System.out.println(Thread.currentThread().getName() + "-->" + number); //加完了通知其他线程 condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } //-1 public void decrement() throws InterruptedException { lock.lock(); try { while (number == 0) { //等待 condition.await(); } number--; System.out.println(Thread.currentThread().getName() + "-->" + number); //减完了通知其他线程 condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } }
4.使用condition实现精准唤醒线程
用condition搭配状态位置试试
public class PC_WithAwakeByCondition { public static void main(String[] args) { Data_AwakeInOrder data = new Data_AwakeInOrder(); new Thread(()->{ for (int i = 0; i < 10; i++) { data.printA(); } },"线程A").start(); new Thread(()->{ for (int i = 0; i < 10; i++) { data.printB(); } },"线程B").start(); new Thread(()->{ for (int i = 0; i < 10; i++) { data.printC(); } },"线程C").start(); } } //资源类 class Data_AwakeInOrder{ private Lock lock = new ReentrantLock(); private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition(); private Condition condition3 = lock.newCondition(); private int number = 1; public void printA(){ lock.lock(); try { // 业务,判断-> 执行-> 通知 while (number != 1) { // 等待 condition1.await(); } System.out.println(Thread.currentThread().getName() + "=>AAAAAA"+"-----number为->"+number); // 唤醒,唤醒指定的人,B number = 2; condition2.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void printB(){ lock.lock(); try { // 业务,判断-> 执行-> 通知 while (number != 2) { // 等待 condition2.await(); } System.out.println(Thread.currentThread().getName() + "=>BBBBBB"+"-----number为->"+number); // 唤醒,唤醒指定的人C number = 3; condition3.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void printC(){ lock.lock(); try { // 业务,判断-> 执行-> 通知 while (number != 3) { // 等待 condition3.await(); } System.out.println(Thread.currentThread().getName() + "=>CCCCCC"+"-----number为->"+number); // 唤醒,唤醒指定的人A number = 1; condition1.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
运行结果:😁一片祥和~依序执行
... 线程A=>AAAAAA-----number为->1 线程B=>BBBBBB-----number为->2 线程C=>CCCCCC-----number为->3 线程A=>AAAAAA-----number为->1 线程B=>BBBBBB-----number为->2 线程C=>CCCCCC-----number为->3 ...
看视频的时候看到有人用wait和notify 也实现了精准唤醒,接下来稍作尝试
5.使用wait()与notify()实现精准唤醒
public class PC_AwakeWithWait { public static void main(String[] args) { Data_AwakeInOrderByWait data = new Data_AwakeInOrderByWait(); new Thread(()->{ for (int i = 0; i < 100; i++) { data.printA(); } },"T_A").start(); new Thread(()->{ for (int i = 0; i < 100; i++) { data.printB(); } },"T_B").start(); new Thread(()->{ for (int i = 0; i < 100; i++) { data.printC(); } },"T_C").start(); } } //资源类 class Data_AwakeInOrderByWait{ private int number = 1; public synchronized void printA() { // 业务,判断-> 执行-> 通知 while (number != 1) { // 等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + "=>AAAAAA->"+number); // 唤醒,唤醒指定的人,B number = 2; this.notifyAll(); } public synchronized void printB(){ // 业务,判断-> 执行-> 通知 while (number != 2) { // 等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + "=>BBBBBB->"+number); // 唤醒,唤醒指定的人,B number = 3; this.notifyAll(); } public synchronized void printC(){ // 业务,判断-> 执行-> 通知 while (number != 3) { // 等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + "=>CCCCCC->"+number); // 唤醒,唤醒指定的人,B number = 1; this.notifyAll(); } }
结果也确实可以实现精准唤醒:
... T_A=>AAAAAA->1 T_B=>BBBBBB->2 T_C=>CCCCCC->3 T_A=>AAAAAA->1 T_B=>BBBBBB->2 T_C=>CCCCCC->3 ...
后来的技术是迭代出的更优秀的版本。等学成归来对比下这两种唤醒的效率
5.8个代码加锁的例子
用4套八种加锁的例子对比理解锁的本质
public class Test1 { public static void main(String[] args) { Phone phone = new Phone(); for (int i = 0; i < 10; i++) { //锁的存在 new Thread(() -> { phone.sendSms(); }, "A").start(); new Thread(() -> { phone.call(); }, "B").start(); } } } class Phone { public synchronized void sendSms() { System.out.println("发短信###"); } public synchronized void call() { System.out.println("打电话******"); } }
... 发短信### 打电话****** 打电话****** 打电话****** 发短信### ...
在线程执行的过程中增加sleep()休眠(Phone类不变)
public static void main(String[] args) { Phone phone = new Phone(); for (int i = 0; i < 10; i++) { //锁的存在 new Thread(() -> { phone.sendSms(); }, "A").start(); //增加0.5s的休眠时间 try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { phone.call(); }, "B").start(); } }
发短信### 打电话****** 发短信### 打电话******
-
结果:
发短信—>打电话
依次执行
在Phone的 发短信的方法中增加2s的休眠(main方法不变)
class Phone { public synchronized void sendSms() { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信###"); } public synchronized void call() { System.out.println("打电话******"); } }
结果:
随机执行
总结
synchronized 锁的对象是方法的调用者,也就是实例化后的对象。
两个方法用的是同一个锁,谁先拿到谁执行!
在例子中,线程的调度是随机的,谁先拿到谁先执行
情况二:实例化一个对象
-
两个对象,两个调用者,两把🔒
-
两个问题:
3. 增加了一个普通方法后!先执行发短信还是Hello? 普通方法 4. 两个对象,两个同步方法, 发短信还是 打电话? // 打电话
public class Test2 { public static void main(String[] args) { // 两个对象,两个调用者,两把锁! Phone2 phone1 = new Phone2(); Phone2 phone2 = new Phone2(); //锁的存在 new Thread(phone1::sendSms,"A").start(); new Thread(phone1::hello,"C1").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(phone2::call,"B").start(); new Thread(phone2::hello,"C2").start(); } } class Phone2{ public synchronized void sendSms(){ try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信sssss <--"+Thread.currentThread().getName()); } public synchronized void call(){ System.out.println("打电话cccc <--"+Thread.currentThread().getName()); } public void hello(){ System.out.println("hello <--"+Thread.currentThread().getName()); } }
运行结果(多次)
hello <--C1 打电话cccc <--B hello <--C2 发短信sssss <--A
-
-
总结:
- synchronized 锁的对象是方法的调用者,也就是实例化后的对象。
- 普通方法不会上🔒,因此会首先打印hello,速度快
- A 线程调用 sendSms 时延时了 2s,因此会先调用B线程的发短信方法
-
-
情况三:静态方法
-
问题:
-
5.增加两个静态的同步方法,只有一个对象,先打印 发短信?打电话? 6.两个对象!增加两个静态的同步方法, 先打印 发短信?打电话?
例子1(实例化一个对象)
public class Test3 { public static void main(String[] args) { Phone3 phone1 = new Phone3(); new Thread(()->{ phone1.sendSms(); },"A").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(()->{ phone1.call(); },"B").start(); } } class Phone3{ public static synchronized void sendSms(){ try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信"); } public static synchronized void call(){ System.out.println("打电话"); } }
-
-
运行结果:
发短信 -->打电话
-
-
例子2:(实例化两个对象)
Phone3 phone1 = new Phone3(); Phone3 phone2 = new Phone3(); new Thread(()->{ phone1.sendSms(); },"A").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } //另一个实例化对象 new Thread(()->{ phone2.call(); },"B").start();
-
-
运行结果:
发短信 -->打电话
-
-
总结:
- 两个对象的Class类模板只有一个,static方法时在类加载的时候初始化,因此锁的是Class对象
- 在休眠后(极大可能)调用发短信对象,同时对模板上🔒,因此依次打印
情况四:静态与普通方法
问题:
7.1个静态的同步方法,1个普通的同步方法 ,一个对象,先打印 发短信?打电话?
8.1个静态的同步方法,1个普通的同步方法 ,两个对象,先打印 发短信?打电话?
例子7:
public class Test4 { public static void main(String[] args) { Phone4 phone1 = new Phone4(); new Thread(()->{ phone1.sendSms(); },"A").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(()->{ phone1.call(); },"B").start(); } } class Phone4{ public static synchronized void sendSms(){ try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信"); } public synchronized void call(){ System.out.println("打电话"); } }
-
- 运行结果:
- 打电话 --> 发短信
- 运行结果:
-
例子8:
Phone4 phone1 = new Phone4(); Phone4 phone2 = new Phone4(); new Thread(()->{ phone1.sendSms(); },"A").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(()->{ phone2.call(); },"B").start();
-
总结:
- 普通方法 🔒的是对象
- static 方法 🔒的是模板
双冒号表达式:
插播一个知识点,双冒号(::)
表达式
6.集合不安全类
-
List不安全
- 在狂神多线程 的 三个线程不安全的案例中有提及,今天在并发角度测试下
public class TestList { 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,5)); System.out.println(list); },String.valueOf(i)).start(); } } }
执行程序后会抛出:
java.util.ConcurrentModificationException
异常(后续抛出同样异常不做细说)
也就是不同线程同时操作了同一list索引元素抛出的异常。
解决方法:
public static void main(String[] args) { // List<String> list = new ArrayList<>(); //1.集合自带的线程安全的list // List<String> list = new Vector<>(); //2.Collections工具类强行上锁 // List<String> list =Collections.synchronizedList(new ArrayList<>()); //3.用JUC包下的读写数组CopyOnWriteArrayList,读写分离 List<String> list = new CopyOnWriteArrayList<>(); for (int i = 0; i < 10; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString().substring(0, 5)); System.out.println(list); }, String.valueOf(i)).start(); } }
一、CopyOnWriteArrayList介绍
①、CopyOnWriteArrayList,写数组的拷贝,支持高效率并发且是线程安全的,读操作无锁的ArrayList。所有可变操作都是通过对底层数组进行一次新的复制来实现。
②、CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。它不存在扩容的概念,每次写操作都要复制一个副本,在副本的基础上修改后改变Array引用。CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差。
③、CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用 ,因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。
二、CopyOnWriteArrayList 有几个缺点:
1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc。
(1、young gc :年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉(IBM的研究表明,98%的对象都是很快消亡的),这个GC机制被称为Minor GC或叫Young GC。
2、年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC
)
2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;
三、CopyOnWriteArrayList的一些方法
1、add(E e) :将指定元素添加到此列表的尾部,返回值为boolean。
2、add(int index, E element) : 在此列表的指定位置上插入指定元素。
3、clear():从此列表移除所有元素。
4、contains(Object o) :如果此列表包含指定的元素,则返回 true。
5、equals(Object o) :比较指定对象与此列表的相等性。
6、get(int index) : 返回列表中指定位置的元素。
7、hashCode() : 返回此列表的哈希码值。
8、indexOf(E e, int index) : 返回第一次出现的指定元素在此列表中的索引,从 index 开始向前搜索,如果没有找到该元素,则返回 -1。
9、indexOf(Object o) :返回此列表中第一次出现的指定元素的索引;如果此列表不包含该元素,则返回 -1。
10、isEmpty() :如果此列表不包含任何元素,则返回 true。
11、iterator() :返回以恰当顺序在此列表元素上进行迭代的迭代器,返回值为 Iterator。
12、lastIndexOf(E e, int index) :返回最后一次出现的指定元素在此列表中的索引,从 index 开始向后搜索,如果没有找到该元素,则返回 -1。
13、lastIndexOf(Object o) : 返回此列表中最后出现的指定元素的索引;如果列表不包含此元素,则返回 -1。
14、remove(int index) :移除此列表指定位置上的元素。
15、remove(Object o) :从此列表移除第一次出现的指定元素(如果存在),返回值为 boolean。
16、set(int index, E element) :用指定的元素替代此列表指定位置上的元素。
17、size() :返回此列表中的元素数。
18、subList(int fromIndex, int toIndex) :返回此列表中 fromIndex(包括)和 toIndex(不包括)之间部分的视图,返回值为 List 。
五、总结
CopyOnWriteArrayList这是一个ArrayList的线程安全的变体,其原理大概可以通俗的理解为:初始化的时候只有一个容器,很长一段时间,这个容器数据、数量等没有发生变化的时候,大家(多个线程),都是读取(假设这段时间里只发生读取的操作)同一个容器中的数据,所以这样大家读到的数据都是唯一、一致、安全的,但是后来有人往里面增加了一个数据,这个时候CopyOnWriteArrayList 底层实现添加的原理是先copy出一个容器(可以简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。
四、Java代码示例
package chapter3.copyonwrite; import java.util.Iterator; import java.util.concurrent.CopyOnWriteArrayList; /** * @author czd */ public class CopyOnWriteArrayListTest { public static void main(String[] args) { /** * 构造方法摘要 * CopyOnWriteArrayList(): * 创建一个空列表。 * CopyOnWriteArrayList(Collection<? extends E> c): * 创建一个按 collection 的迭代器返回元素的顺序包含指定 collection 元素的列表。 * CopyOnWriteArrayList(E[] toCopyIn): * 创建一个保存给定数组的副本的列表。 */ /** * 1、add(E e) :将指定元素添加到此列表的尾部,返回值为boolean。 * * 2、iterator() :返回以恰当顺序在此列表元素上进行迭代的迭代器,返回值为Iterator<E>。 */ CopyOnWriteArrayList<Integer> copyOnWriteArrayList = new CopyOnWriteArrayList<>(); Boolean addBoolean = copyOnWriteArrayList.add(1); System.out.println("是否添加到此列表的尾部?" + addBoolean); Iterator<Integer> iterator = copyOnWriteArrayList.iterator(); while (iterator.hasNext()){ System.out.println("iterator的结果: " + iterator.next()); } /** * 3、add(int index, E element) :在此列表的指定位置上插入指定元素。 */ CopyOnWriteArrayList<Integer> copyOnWriteArrayList1 = new CopyOnWriteArrayList<>(); copyOnWriteArrayList1.add(1); copyOnWriteArrayList1.add(2); copyOnWriteArrayList1.add(2,5); // copyOnWriteArrayList1.add(4,6); System.out.println("index = 0 的值为:" + copyOnWriteArrayList1.get(0)); System.out.println("index = 1 的值为:" + copyOnWriteArrayList1.get(1)); System.out.println("index = 2 的值为:" + copyOnWriteArrayList1.get(2)); // System.out.println("index = 4 的值为:" + copyOnWriteArrayList1.get(4)); /** * 注意:如果使用add(int index, E element),必须保证index的前面的index存在值,不然会报错。 * (可以把上两行注释去掉,运行试试结果) */ /** * 4.1、indexOf(E e, int index) * 返回第一次出现的指定元素在此列表中的索引,从 index 开始向前搜索,如果没有找到该元素,则返回 -1。 * 4.2、indexOf(Object o) * 返回此列表中第一次出现的指定元素的索引;如果此列表不包含该元素,则返回 -1。 */ CopyOnWriteArrayList<Integer> copyOnWriteArrayList2 = new CopyOnWriteArrayList<>(); copyOnWriteArrayList2.add(1); copyOnWriteArrayList2.add(2); copyOnWriteArrayList2.add(3); Integer indexFirst = copyOnWriteArrayList2.indexOf(2); System.out.println("2的index为:" + indexFirst); /** * 5.1、lastIndexOf(E e, int index) * 返回最后一次出现的指定元素在此列表中的索引,从 index 开始向后搜索,如果没有找到该元素,则返回 -1。 * 5.2、lastIndexOf(Object o) * 返回此列表中最后出现的指定元素的索引;如果列表不包含此元素,则返回 -1。 */ CopyOnWriteArrayList<Integer> copyOnWriteArrayList3 = new CopyOnWriteArrayList<>(); copyOnWriteArrayList3.add(1); copyOnWriteArrayList3.add(2); copyOnWriteArrayList3.add(3); copyOnWriteArrayList3.add(3); copyOnWriteArrayList3.add(4); Integer lastIndexOf = copyOnWriteArrayList3.lastIndexOf(3); System.out.println("列表中最后出现的指定元素的索引: " + lastIndexOf); /** * 6、remove(int index) :移除此列表指定位置上的元素,并且此下标后面的值会进一位, * 即index为1的值被remove掉,index为2的值就变为index为1的值。 */ CopyOnWriteArrayList<Integer> copyOnWriteArrayList4 = new CopyOnWriteArrayList<>(); copyOnWriteArrayList4.add(5); copyOnWriteArrayList4.add(6); copyOnWriteArrayList4.add(7); copyOnWriteArrayList4.add(8); Integer removeResult = copyOnWriteArrayList4.remove(1); System.out.println("copyOnWriteArrayList4索引为1的值:" + removeResult); System.out.println("copyOnWriteArrayList4中是否还存在索引为1的值:" + copyOnWriteArrayList4.get(1)); /** * 7、remove(Object o) :从此列表移除第一次出现的指定元素(如果存在)。 */ CopyOnWriteArrayList<String> copyOnWriteArrayList5 = new CopyOnWriteArrayList<>(); copyOnWriteArrayList5.add("5"); copyOnWriteArrayList5.add("6"); copyOnWriteArrayList5.add("6"); copyOnWriteArrayList5.add("7"); copyOnWriteArrayList5.add("7"); System.out.println("没被remove前的size: " + copyOnWriteArrayList5.size()); Boolean removeBoolean = copyOnWriteArrayList5.remove("7"); System.out.println("copyOnWriteArrayList5是否移除7成功?" + removeBoolean); System.out.println("index为3的值为:" + copyOnWriteArrayList5.get(3)); System.out.println("被remove后的size: " + copyOnWriteArrayList5.size()); /** * 8、set(int index, E element) :用指定的元素替代此列表指定位置上的元素。 */ CopyOnWriteArrayList<String> copyOnWriteArrayList6 = new CopyOnWriteArrayList<>(); copyOnWriteArrayList6.add("1"); copyOnWriteArrayList6.add("2"); copyOnWriteArrayList6.add("3"); copyOnWriteArrayList6.add("4"); copyOnWriteArrayList6.add("5"); System.out.println("没使用set()方法前,index = 1 的值为:" + copyOnWriteArrayList6.get(1)); copyOnWriteArrayList6.set(1,"czd"); System.out.println("使用set()方法后,index = 1 的值为:" + copyOnWriteArrayList6.get(1)); } }
Set不安全
例子
public class TestSet { public static void main(String[] args) { Set<Object> set = new HashSet<>(); for (int i = 0; i < 10; i++) { new Thread(() -> { set.add(UUID.randomUUID().toString().substring(0, 5)); System.out.println(set); }, String.valueOf(i)).start(); } } }
解决方法:
public static void main(String[] args) { // Set<Object> set = new HashSet<>(); //1.用collections工具类强行上锁: // Set<Object> set = Collections.synchronizedSet(new HashSet<>()); //2.用CopyOnWriteArrayList 实现的CopyOnWriteArraySet Set<String> set = new CopyOnWriteArraySet<>(); for (int i = 0; i < 10; i++) { new Thread(() -> { set.add(UUID.randomUUID().toString().substring(0, 5)); System.out.println(set); }, String.valueOf(i)).start(); } }
hashSet本质
public HashSet() { map = new HashMap<>(); } // add set 本质就是 map key是无法重复的! public boolean add(E e) { return map.put(e, PRESENT)==null; } private static final Object PRESENT = new Object(); // 不变的值,做value
Map不安全
例子:
public class TestMap { public static void main(String[] args) { // 默认等价于什么? new HashMap<>(16,0.75); // Map<String, Object> map = new HashMap<>(); //1.用juc下线程安全的map Map<String, Object> map = new ConcurrentHashMap<>(); for (int i = 0; i < 10; i++) { new Thread(() -> { map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0, 5)); System.out.println(map); }, String.valueOf(i)).start(); } } }
7.Callable使用
下列博客对callable有详细的介绍,值得一看✌
Callable接口及Futrue接口详解
目录
Callable接口
Futrue接口
1.使用Callable和Future的完整示例
2.使用Callable和FutureTask的完整示例
3.使用Runnable来获取返回结果的实现
有两种创建线程的方法-一种是通过创建Thread类,另一种是通过使用Runnable创建线程。但是,Runnable缺少的一项功能是,当线程终止时(即run()完成时),我们无法使线程返回结果。为了支持此功能,Java中提供了Callable接口。
- 为了实现Runnable,需要实现不返回任何内容的run()方法,
- 对于Callable,需要实现在完成时返回结果的call()方法。
- 请注意,不能使用Callable创建线程,只能使用Runnable创建线程。
- 另一个区别是call()方法可以引发异常,而run()则不能。
- 为实现Callable而必须重写call()方法。
为把Callable对象扔给Thread需要借助Runnable的实现类futureTask作为中间人
8. JUC常用辅助类
8.1 CountDownLatch
顾名思义:倒计时锁存器
不管你线程中间执行的情况,结果若是线程执行完了,那就再执行最后语句,如果没达到条件就一直等
(领导:不看过程只看结果)
public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { // 总数是6,必须要执行任务的时候,再使用! CountDownLatch countDownLatch = new CountDownLatch(6); for (int i = 1; i <=6 ; i++) { new Thread(()->{ System.out.println(Thread.currentThread().getName()+" Go out"); countDownLatch.countDown(); // 数量-1 },String.valueOf(i)).start(); } countDownLatch.await(); // 等待计数器归零,然后再向下执行 System.out.println("Close Door"); } }
1 Go out 5 Go out 4 Go out 3 Go out 2 Go out 6 Go out Close Door
如果创建了7条任务线程,但只countDown了6次,那么将会一直阻塞线程
总结:
CountDownLatch countDownLatch = new CountDownLatch(6); 创建线程总数
countDownLatch.countDown(); 实行完线程数-1
countDownLatch.await(); 等待计数器归零,然后再向下执行
每次有线程调用 countDown() 数量-1,假设计数器变为0,countDownLatch.await() 就会被唤醒,继续执行
8.2 CyclicBarrier
见名之意:循环障碍,与8.1 方法相反的,加法计时器
public class CyclicBarrierDemo { public static void main(String[] args) { /** * 集齐7颗龙珠召唤神龙 */ // 召唤龙珠的线程:7条线程 // 创建成功后执行runnable接口打印“召唤七颗龙珠成功” CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> { System.out.println("召唤七颗龙珠成功"); }); for (int i = 1; i <= 7; i++) { final int temp = i; new Thread(() -> { System.out.println(Thread.currentThread().getName() + "收集到第" + temp + "个龙珠"); try { cyclicBarrier.await(); } catch (InterruptedException | BrokenBarrierException e){ e.printStackTrace(); } }).start(); } } }
创建线程Barrier后开启线程执行:
CyclicBarrier(int parties, Runnable barrierAction) 创建一个新的 CyclicBarrier ,当给定数量的线程(线程)等待时,它将跳闸,当屏障跳闸时执行给定的屏障动作,由最后一个进入屏障的线程执行。
并在调用await()方法时自动+1:
如果执行的线程数到达设定值,则会执行 创建时设定的屏障动作,
如果无法到达则线程会处在阻塞状态
8.2.1 lambda为什么要用final?
原创来源: lambda里面赋值局部变量必须是final原因
简单复习下lambda函数:匿名实现函数式接口的方法
Runnable r= new Runnable() { @Override public void run() { System.out.println("这是常用实例化函数式接口"); } }; Runnable rl = ()->System.out.println("这是lambda实例化接口");
8.3 Semaphore
见名之意:信号量,可近似看作资源池。
举例子(抢车位):
public class SemaphoreDemo { public static void main(String[] args) { // 线程数量:停车位! 限流! //创建只有3个停车位的停车场 Semaphore semaphore = new Semaphore(3); for (int i = 1; i <=6 ; i++) { new Thread(()->{ // acquire() 得到 try { //抢车位。。。 semaphore.acquire(); System.out.println(Thread.currentThread().getName()+"抢到车位👍"); TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName()+"离开车位😀"); } catch (InterruptedException e) { e.printStackTrace(); } finally {、 //停车结束,离开车位 semaphore.release(); // release() 释放 } },String.valueOf(i)+"号车-->").start(); } } }
1号车-->抢到车位👍 3号车-->抢到车位👍 2号车-->抢到车位👍 2号车-->离开车位😀 1号车-->离开车位😀 4号车-->抢到车位👍 3号车-->离开车位😀 6号车-->抢到车位👍 5号车-->抢到车位👍 5号车-->离开车位😀 6号车-->离开车位😀 4号车-->离开车位😀
每个人都能停到车🚘
9.读写锁
ReadWriteLock 接口
所有已知实现类: ReentrantReadWriteLock
- 读:可多条线程同时获取数据
- 写:只能单条线程写入
独占锁/排它锁/写锁 共享锁/读锁
public class TestReadWriteLock { public static void main(String[] args) { //未上锁: // MyCache myCache = new MyCache(); //上了读写锁: MyCacheWithLock myCache = new MyCacheWithLock(); //写入: for (int i = 1; i <= 5; i++) { final int temp = i; new Thread(() -> { myCache.write(temp + "", temp + ""); }, String.valueOf(i)).start(); } for (int i = 1; i <= 5; i++) { final int temp = i; new Thread(() -> { myCache.read(temp + ""); }, String.valueOf(i)).start(); } } } class MyCacheWithLock { private volatile Map<String, Object> map = new HashMap<>(); //读写锁:对数据更精准控制 private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private Lock lock = new ReentrantLock(); //写数据:只希望有一个线程在执行 public void write(String key, Object value) { readWriteLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() + "写入" + key); map.put(key, value); System.out.println(Thread.currentThread().getName() + "写入完成!"); } catch (Exception e) { e.printStackTrace(); } finally { readWriteLock.writeLock().unlock(); } } //读数据:可一条或者多条同时执行 public void read(String key) { readWriteLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + "读取数据:" + key); Object o = map.get(key); System.out.println(Thread.currentThread().getName() + "读取数据完成-->" + o); } catch (Exception e) { e.printStackTrace(); } finally { readWriteLock.readLock().unlock(); } } /** * 存入数据过程上锁,安全 */ } /** * 未上锁: */ class MyCache { private volatile Map<String, Object> map = new HashMap<>(); //写数据: public void write(String key, Object value) { System.out.println(Thread.currentThread().getName() + "写入" + key); map.put(key, value); System.out.println(Thread.currentThread().getName() + "写入完成!"); } //读数据: public void read(String key) { System.out.println(Thread.currentThread().getName() + "读取数据:" + key); Object o = map.get(key); System.out.println(Thread.currentThread().getName() + "读取数据完成-->" + o); } /** * 运行结果: * 写入线程会被 读取线程中断,造成脏读,对数据不安全 */ }
10.阻塞队列
Interface BlockingQueue
是数据结构中队列Queue的延展使用。
什么情况下我们会使用阻塞队列?
对线程并发处理,线程池!
学会使用队列的操作: 添加 和 移除
第一组:
@Test public void test1(){ ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3); System.out.println(blockingQueue.add("a")); //true System.out.println(blockingQueue.add("b")); // true System.out.println(blockingQueue.add("c")); //true // System.out.println(blockingQueue.add("d")); // 会抛异常:java.lang.IllegalStateException: Queue full System.out.println("============================="); System.out.println(blockingQueue.remove()); //a System.out.println(blockingQueue.remove()); //b System.out.println(blockingQueue.remove()); //c // System.out.println(blockingQueue.remove()); //会抛出 NoSuchElementException异常 System.out.println(blockingQueue.element()); //如无元素则会报NoSuchElementException异常 }
第二组:
@Test public void test2(){ ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3); System.out.println(blockingQueue.offer("a"));//true System.out.println(blockingQueue.offer("b"));//true System.out.println(blockingQueue.offer("c"));//true System.out.println(blockingQueue.offer("d"));//false System.out.println("============================="); System.out.println(blockingQueue.poll()); //a System.out.println(blockingQueue.poll()); //b System.out.println(blockingQueue.poll()); //c System.out.println(blockingQueue.poll()); //null System.out.println("============================="); System.out.println(blockingQueue.offer("e"));//true System.out.println(blockingQueue.offer("f"));//true System.out.println(blockingQueue.peek()); //e : 队首元素 }
第三组:
@Test public void test3() throws InterruptedException { ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3); blockingQueue.put("a"); blockingQueue.put("b"); blockingQueue.put("c"); // blockingQueue.put("d"); // 队列没有位置了,一直阻塞 System.out.println("============================="); System.out.println(blockingQueue.take()); //a System.out.println(blockingQueue.take()); //b System.out.println(blockingQueue.take()); //c // System.out.println(blockingQueue.take()); // 队列没有值可以取,一直阻塞 }
第四组:
@Test public void test4() throws InterruptedException { ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3); System.out.println(blockingQueue.offer("a"));//true System.out.println(blockingQueue.offer("b"));//true System.out.println(blockingQueue.offer("c"));//true System.out.println(blockingQueue.offer("d", 2, TimeUnit.SECONDS));//false System.out.println("============================="); System.out.println(blockingQueue.poll()); //a System.out.println(blockingQueue.poll()); //b System.out.println(blockingQueue.poll()); //c System.out.println(blockingQueue.poll(2,TimeUnit.SECONDS)); //null }
SynchronousQueue 同步队列
没有容量==> 进去一个元素,必须等待取出来之后,才能再往里面放一个元素!
和其他的BlockingQueue 不一样, SynchronousQueue 不存储元素,put了一个元素,必须从里面先take取出来,否则不能再put进去值!
public class TestSynchronousQueue { public static void main(String[] args) { SynchronousQueue<String> bq = new SynchronousQueue<>(); new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + " put 1"); bq.put("1"); System.out.println(Thread.currentThread().getName() + " put 2"); bq.put("2"); System.out.println(Thread.currentThread().getName() + " put 3"); bq.put("3"); } catch (InterruptedException e) { e.printStackTrace(); } }, "T1").start(); new Thread(() -> { try { TimeUnit.SECONDS.sleep(1); System.out.println(Thread.currentThread().getName() + " get =>" + bq.take()); TimeUnit.SECONDS.sleep(1); System.out.println(Thread.currentThread().getName() + " get =>" + bq.take()); TimeUnit.SECONDS.sleep(1); System.out.println(Thread.currentThread().getName() + " get =>" + bq.take()); } catch (InterruptedException e) { e.printStackTrace(); } }, "T2").start(); } } /** T1 put 1 T2 get =>1 T1 put 2 T2 get =>2 T1 put 3 T2 get =>3 */
11.线程池(※)
学习目标:
三大方法
七大参数
四种拒绝策略
11.0 池化技术
在系统开发过程中,我们经常会用到池化技术来减少系统消耗,提升系统性能。
说人数: 简单点来说,就是提前保存大量的资源,以备不时之需,池化技术就是通过复用来提升性能。
常见池:
对象池
通过复用对象来减少创建对象、垃圾回收的开销;
连接池
(数据库连接池、Redis连接池和HTTP连接池等)通过复用TCP连接来减少创建和释放连接的时间
线程池通过复用线程提升性能
使用内存池的优点
- 降低资源消耗。这个优点可以从创建内存池的过程中看出,当我们在创建内存池的时候,分配的都是一块块比较规整的内存块,减少内存碎片的产生。
- 提高相应速度。这个可以从分配内存和释放内存的过程中看出。每次的分配和释放并不是去调用系统提供的函数或操作符去操作实际的内存,而是在复用内存池中的内存。
- 方便管理。
使用内存池的缺点
- 缺点就是很可能会造成内存的浪费,因为要使用内存池需要在一开始分配一大块闲置的内存,而这些内存不一定全部被用到。
11.1 三大方法
阿里巴巴 Java 开发手册中 对线程池的规范:
Executors 工具类中3大方法(详见API)
public static ExecutorService newSingleThreadExecutor() //创建一个线程池,根据需要创建新的线程,但在可用时将重用先前构建的线程。
public static ExecutorService newFixedThreadPool(int nThreads) //创建一个线程池,使用固定数量的线程操作了共享无界队列
public static ExecutorService newCachedThreadPool() //创建一个线程池,根据需要创建新的线程,但在可用时将重用先前构建的线程。
定时线程池(newScheduledThreadPool)
public class TestMethods { public static void main(String[] args) { // ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 单个线程:此时只有pool-1-thread-1 // ExecutorService threadPool = Executors.newFixedThreadPool(5); // 创建一个固定的线程池的大小: 此时最多有pool-1-thread-5 ok ExecutorService threadPool = Executors.newCachedThreadPool(); // 可伸缩的,遇强则强,遇弱则弱 : 此时最多开启到pool-1-thread-31 ok 去执行 try { for (int i = 0; i < 100; i++) { // 使用了线程池之后,使用线程池来创建线程 threadPool.execute(() -> { System.out.println(Thread.currentThread().getName() + " ok"); }); } } catch (Exception e) { e.printStackTrace(); } finally { // 线程池用完,程序结束,关闭线程池 threadPool.shutdown(); } } }
package com.zhw.learning.thread; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * @author zhw * 创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行 * * Executors.newScheduledThreadPool(3): * public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { * return new ScheduledThreadPoolExecutor(corePoolSize); * } * public ScheduledThreadPoolExecutor(int corePoolSize) { * super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, * new DelayedWorkQueue()); * } */ public class ScheduledThreadPoolTest { public static void main(String[] args) throws InterruptedException { final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3); System.out.println("提交时间: " + sdf.format(new Date())); //延迟3秒钟后执行任务 // scheduledThreadPool.schedule(new Runnable() { // @Override // public void run() { // System.out.println("运行时间: " + sdf.format(new Date())); // } // }, 3, TimeUnit.SECONDS); //延迟1秒钟后每隔3秒执行一次任务 scheduledThreadPool.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("运行时间: " + sdf.format(new Date())); } }, 1, 3, TimeUnit.SECONDS); Thread.sleep(10000); scheduledThreadPool.shutdown(); } }
11.2 七大参数
源码分析:
//创建单一线程的线程池 public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); } //创建固定线程数的线程池 public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory); } //创建代缓存的线程池: public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
其本质都是调用本质ThreadPoolExecutor
创建线程池,也是 阿里巴巴规范中提及的方法:
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小 int maximumPoolSize, //最大核心线程池大小 long keepAliveTime, //超时了没有人调用就会释放 TimeUnit unit, //超时单位 BlockingQueue<Runnable> workQueue,//阻塞队列 ThreadFactory threadFactory,// 线程工厂:创建线程的,一般不用动 RejectedExecutionHandler handler/*拒绝策略*/) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
11. 2.1 手动创建线程池:
银行办理业务举例:
- 有5个柜台,每次办理一个人。
- 当天值班柜员只有2人。
- 剩余3个临时工等待超过一定时间会离开柜台。
- 候客区有3个座位,可容纳3人等待。
public class TestDefPool { public static void main(String[] args) { // 自定义线程池!工作 ThreadPoolExecutor ExecutorService threadPool = new ThreadPoolExecutor( 2, //当天值班员工(核心线程池大小) 5, //柜台总数:最大核心线程池大小 3, //临时工的超时等待:超时了没有人调用就会释放 TimeUnit.SECONDS, //超时等待单位 new LinkedBlockingDeque<>(3),//候客区:阻塞队列 Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy()); //满了后告诉客人办不了业务了:抛异常RejectedExecutionException try { // 最大承载:Deque + max 此处为:5+3=8 // 超过 RejectedExecutionException for (int i = 1; i <= 9; i++) { // 使用了线程池之后,使用线程池来创建线程 threadPool.execute(() -> { System.out.println(Thread.currentThread().getName() + " ok"); }); } } catch ( Exception e) { e.printStackTrace(); } finally { // 线程池用完,程序结束,关闭线程池 threadPool.shutdown(); } } }
11.4 四种拒绝策略
/** 1. new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常:RejectedExecutionException 2.new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里! //公司叫你来银行办业务,银行满人办不了,回公司找人办 //新开辟的线程搞不定调用主线程 3.new ThreadPoolExecutor.DiscardPolicy() //银行办不了了,不办你业务 //队列满了,丢掉任务,不会抛出异常! 4.new ThreadPoolExecutor.DiscardOldestPolicy() //排队人满了,你看看最早开始的客户搞定没,没搞定就被拒绝了。 //队列满了,尝试去和最早的竞争,也不会抛出异常! */
11.5小结:
根据上述参数对线程池调优:主要针对线程池大小调优
IO密集型
io密集型则是根据当前应用的数量来设置最大线程数,一般是应用数*2
一般来说:文件读写、DB读写、网络请求等。
这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
CPU密集型
一般来说:计算型代码、Bitmap转换、Gson转换等。
这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数
// 获取CPU的核数
System.out.println(Runtime.getRuntime().availableProcessors());
java中的方法引用
Lock为什么比synchronized 能更好的实现同步访问
BlockingQueue(阻塞队列)详解