- 什么是JUC
- 进程和线程回顾
- Lock锁
- 生产者和消费者
- 8锁的线程
- 集合类不安全
- Callable
- CountDownLatch、CyclicBarrier、Semaphore
- 读写锁
- 阻塞队列
- 线程池
- 四大函数式接口
- Stream流式计算
- 分支合并
- 异步回调
- JMM
- volatile
- 深入单例模式
- 深入理解CAS
- 原子引用
- 可重入锁、公平锁非公平锁、自旋锁、死锁...
学习方法:官方文档+源码
一. 什么是JUC
- JUC就是 java.util .concurrent 工具包的简称。这是一个处理线程的工具包,JDK1.5 开始出现的。
java.util.concurrent 包是在并发编程中使用的工具类,有以下三个包:
- java.util.concurrent
- java.util.concurrent.atomic
- java.util.concurrent.locks
二. 进程和线程回顾
- 进程(Process):是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令以及数据的集合,进程是程序的实体。
- 线程(Thread):线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。通常在一个进程中可以包含若干个线程,每条线程执行不同的任务,当然一个进程中至少有一个线程,不然没有存在的意义,线程可以利用进程所有拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
- 进程:就是操作系统中正在运行的一个应用程序,程序一旦运行就是进程;进程是资源分配的最小单位。 QQ.exe, music.exe, word.exe ,这就是多个进程
- 线程:是系统分配处理器时间资源的基本单元,或者说是进程之内独立执行的一个单元执行流。线程是程序执行的最小单位。每个进程中都存在一个或者多个线程,比如用word写文章时,就会有一个线程默默帮你定时自动保存。
Java默认有几个线程?
- 2个:main线程、GC线程(垃圾回收线程)
- main线程是用户线程,GC垃圾回收线程是守护线程。
垃圾回收:
- 当堆内存中的类对象或者数组对象没有被任何变量引用时,就会被判定为内存中的垃圾,Java存在自动垃圾回收器,会定期进行清理。
线程对于Java而言:Thread、Runnable、Callable
Java真的可以开启线程吗?
- 开不了,start()方法底层调用的start0()是native本地方法,底层的C++
package com.gch.juc;
public class StartDemo1 {
public static void main(String[] args) {
new Thread().start(); // 实际上调用的是start0方法
}
}
并发、并行
- 并发编程:并发、并行
- 并发(多线程操作同一个资源) CPU一核,模拟出来多条线程,快速交替执行。
- 一个CPU的核心一次只能执行一条指令
- 并行(多线程操作多个资源) CPU多核,多个线程可以同时执行;线程池
package com.gch.juc;
public class cpuCores {
public static void main(String[] args) {
// 获取CPU的核数
// CPU密集型、IO密集型
System.out.println(Runtime.getRuntime().availableProcessors()); // 12
}
}
并发编程的本质 / 为什么要使用并发编程:
- 充分利用CPU的资源,充分的利用处理器的每一个核{充分利用多核CPU的计算能力},以达到最高的处理性能。
- 方便进行业务拆分,提高应用性能,提高系统的并发能力。
并发编程有什么缺点?
- 比如:内存泄漏、上下文切换、线程安全问题、死锁
什么是多线程?
- 多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程 来执行不同的任务。
多线程的好处:
- 可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
- 可以轻松实现一些需要异步操作的功能:比如网络请求、IO操作等可以新开一个线程来进行这些操作,让主线程不被阻塞。
多线程的劣势:
- 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
- 多线程需要协调和管理,所以也需要CPU时间跟踪线程;
- 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题,也就是多线程访问共享资源时需要考虑线程同步问题,否则逻辑出错就容易导致死锁。
- 线程的创建和销毁都需要消耗系统资源,同时线程的使用也会给系统带来上下文切换的额外负担。
什么是上下文切换?
-
多线程编程中一般线程的个数都大于 CPU 核心的个数,而 一个 CPU 核心在任意时刻只能被一个线程使用 ,为了让这些线程都能得到有效执行, CPU 采取的策略是为每个线程分配时间片并轮转的形式 。 当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
-
概括来说就是: 当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态 。 任务从保存到再加载的过程就是一次上下文切换。
- 时间开销、增加系统的资源消耗,增加资源管理的负担、影响系统的响应时间、缓存失效、降低系统的整体性能和响应能力。
-
缓存失效:现代计算机系统通常具有多级缓存,缓存的作用是提高程序的执行效率。但是,当发生上下文切换时,当前任务的缓存内容可能会被切换出去,下次再切换回来时会引发缓存失效,导致额外的访存开销。
访存开销
-
访存开销(Memory Access Overhead)指的是在计算机系统中访问主存(内存)所需要的额外开销和延迟。主存是计算机中用于存储数据和指令的物理存储器,CPU需要从主存中读取数据和指令来执行计算任务。
访存开销涉及到多个方面的开销:
-
内存延迟:访问主存的速度变慢。 CPU内部的高速缓存(Cache)
-
内存带宽:CPU和主存之间有一条数据传输通道,称为内存总线(Memory Bus),它决定了在特定时间内可以传输的数据量。如果数据量过大或传输速率较低,会导致内存带宽成为瓶颈,影响数据的读取和写入速度。
-
数据传输开销:CPU需要将数据从主存中读取到寄存器或缓存中进行处理,或者将计算结果写回到主存。这涉及到数据的传输和复制操作,会消耗额外的时间和资源。
线程有几个状态?
- 新建(new):新创建了一个线程对象。
- 就绪(可运行状态)(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
- 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。
- 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被 CPU 调用以进入到运行状态。
- (一). 无限等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waittingqueue)中,使本线程进入到等待阻塞状态;
- (二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
- (三). 计时等待阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
- 死亡(dead)(结束):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
wait/sleep区别:
- 来自不同的类:wait===>Object sleep===>Thread
-
sleep 是 Thread 的静态方法 ; wait 是 Object 的方法,任何对象实例都能调用 。
企业当中,让线程休眠会用Thread.sleep吗?
- 不会,会用TimeUnit这个工具类来操作:
2. 关于锁的释放:有没有释放锁(释放资源)
- wait会释放锁
- sleep睡觉了,抱着锁睡觉,不会释放锁
- 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
- sleep是线程被调用时,占着cpu去睡觉,其他线程不能占用cpu,os认为该线程正在工作,不会让出系统资源
- wait是进入等待池等待,让出系统资源,其他线程可以占用cpu。
- sleep(100L)是占用cpu,线程休眠100毫秒,其他进程不能再占用cpu资源
- wait(100L)是进入等待池中等待,交出cpu等系统资源供其他进程使用,在这100毫秒中,该线程可以被其他线程notify,但不同的是其他在等待池中的线程不被notify不会出来,但这个线程在等待100毫秒后会自动进入就绪队列等待系统分配资源,换句话说,sleep(100)在100毫秒后肯定会运行,但wait在100毫秒后还有等待os调用分配资源,所以wait100的停止运行时间是不确定的,但至少是100毫秒。
- 就是说sleep有时间限制的就像闹钟一样到时候就叫了,而wait()是无限期的除非用户主动notify。
3、使用范围不同
- 线程wait,notify和notifyAll方法只能在同步控制方法或者同步控制块里面使用
- 而sleep可以在任何地方使用。
synchronized(x){
//或者wait()
x.notify()
}
4. 是否需要捕获异常
- sleep必须捕获异常 --- throws InterruptedException
- 而wait,notify和notifyAll不需要捕获异常。
并发、并行、串行:
- 串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线 程不安全情况,也就不存在临界区的问题。串行是一次只能取得一个任务,并执行这 个任务。
- 并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任 务是同时执行。
- 并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”
- 并发 = 俩个人用一台电脑。
- 并行 = 俩个人分配了俩台电脑。
- 串行 = 俩个人排队使用一台电脑。
- 例子:春运抢票 电商秒杀...
- 例子:泡方便面,电水壶烧水,一边撕调料倒入桶中
协程:
- 协程是一种用户态的轻量级线程,一个线程可以拥有多个协程,协程调度完全由用户控制。
管程 :
-
管程它的描述叫Monitor,Monitor翻译过来叫做监视器,平时所说的锁就叫监视器;
-
在Java中叫锁,在操作系统中叫监视器;
-
监视器本身是一种同步机制,保证在同一时间只能有一个线程来对共享资源进行访问,而别的线程不能进行访问 ;
-
JVM 中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程 (monitor)对象,管程(monitor)会随着 java 对象一同创建和销毁。
用户线程和守护线程:
-
用户(User)线程:运行在前台,执行具体的任务,如程序的主线程、自定义线程等都是用户线程。
-
守护(Daemon)线程:运行在后台,为其它前台线程服务的的一种特殊的线程,比如GC垃圾回收线程。与守护线程相对应的就是用户线程,守护线程就是守护用户线程,当用户线程全部执行完结束之后,守护线程也会跟着陆续结束。也就是守护线程必须伴随着用户线程,如果一个应用内只存在一个守护线程,没有用户线程,守护线程自然会被退出。
-
main函数所在的线程{main线程}就是一个用户线程,main函数在启动的同时在JVM内部同时还启动了好多守护线程,比如GC垃圾回收线程。
三. Lock锁(重点)
多线程编程步骤:
- 第一:创建资源类,创建属性和操作方法
- 第二:创建多线程调用资源类的方法
传统synchronized
- 虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定义的一部分,因此,synchronized 关键字不能被继承。
package com.gch.juc;
/**
* 真正的多线程开发,公司中的开发,降低耦合度
* 线程就是一个单独的资源类,没有任何附属的操作!
* 1.属性 2.方法
* 多线程编程的企业级套路:
* 1. 在高内聚低耦合的前提下,多线程操作资源类(对外暴露的调用方法)
* @author A.G.H
*/
public class SaleTicketDemo {
public static void main(String[] args) {
// 并发:多线程操作同一个资源类,把资源类丢入线程
Ticket t = new Ticket();
// @FunctionalInterface 函数式接口,JDK1.8===>Lambda表达式(参数)->{代码}
new Thread(()->{
t.saleTicket();
},"窗口1").start();
new Thread(()->{
t.saleTicket();
},"窗口2").start();
new Thread(()->{
t.saleTicket();
},"窗口3").start();
}
}
/**
* 资源类 OOP:属性+方法
*/
class Ticket{
/**
* 属性
*/
private static int number = 1; // 1 - 30
/**
* 方法
*/
// synchronized本质:队列(排队),锁
public synchronized void saleTicket() {
while(true){
if(number > 30){
System.out.println("卖完了");
break;
}else{
System.out.println(Thread.currentThread().getName() + "正在卖第" + number + "张票!");
number++;
}
}
}
}
---------------------------------------------------------------------------------------------------------------------
Lock接口
公平锁:十分公平,可以先来后到 举例:3h 3s
非公平锁:十分不公平,可以插队(默认)
Java为什么默认使用非公平锁?
答:为了公平,公平锁不公平,非公平锁才公平
使用juc.locks包下的类操作Lock锁+lambda表达式:
package com.gch.juc;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SaleTicketDemo2 {
public static void main(String[] args) {
// 并发:多线程操作同一资源类,把资源类丢入线程
Ticket2 t = new Ticket2();
// @FunctionalInterface:函数式接口 ---> jdk1.8 lambda表达式简化函数式接口:(参数)->{代码}
// 创建并开启线程
new Thread(()->{t.saleTicket();},"窗口1").start();
new Thread(()->{t.saleTicket();},"窗口2").start();
new Thread(()->{t.saleTicket();},"窗口3").start();
}
}
/**
* 资源类 OOP:属性+方法
*/
class Ticket2{
/**
* 属性
*/
private static int number = 1; // 1 - 30
// 1.定义Lock锁,锁对象必须是唯一贺不可替换的
private static final Lock lock = new ReentrantLock();
/**
* 方法
* Lock锁实现线程同步
* Lock锁三部曲:
* 1.new ReentrantLock();
* 2.lock.lock(); // 加锁
* 3.lock.unlock(); // 解锁
*/
public void saleTicket() {
while(true){
// 2.上锁
lock.lock();
try {
if(number > 30){
System.out.println("卖完了");
break;
}else{
System.out.println(Thread.currentThread().getName() + "正在卖第" + number + "张票!");
number++;
}
} finally {
// 3.解锁
lock.unlock();
}
}
}
}
谈谈synchronized与Lock锁的区别:
- 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(线程执行完同步代码块或同步方法后会自动释放锁,即使线程执行过程中发生异常也会自动释放锁),Lock需要线程在finally代码块中显示调用unlock()方法手动释放锁,以确保锁被正确释放,否则容易造成线程死锁;
- synchronized只能实现独占锁,即同一时间只能有一个线程获取到锁并访问共享资源,其它线程需要等待。而lock可以实现独占锁和共享锁,独占锁与synchronized功能一样,共享锁(读锁)可以让多个线程同时访问共享资源,从而提高程序的并发性能。
- 用synchronized关键字的两个线程:线程1和线程2,如果当前线程1获得锁,则线程2等待;如果线程1获得锁后阻塞,线程2也会一直等待下去。synchronized阻塞式:如果其它线程无法获取synchronized锁,它们会进入阻塞状态,等待锁的释放。而Lock锁就不一定会等待下去,lock.tryLock()方法用于尝试获取锁。tryLock()是一种非阻塞式的锁定方法,它尝试获取锁,如果获取成功,就立即返回true,这样线程可以继续执行后续操作。如果获取失败,就立即返回false,而不会让当前线程进入休眠状态,也就是说它不会阻塞线程。与lock()方法不同的是,tryLock()方法可以避免线程因等待无法获取锁而被阻塞的情况,因此,对于实时系统和高性能的应用程序来说,tryLock()方法更具有实用性和可应用性。
6. synchronized的锁可重入、不可中断(当一个线程获取对象的锁后,其他线程如果想要获取该锁,只能等待该线程释放锁之后才能继续执行)、非公平;而Lock锁可重入,可以通过构造函数来选择是公平锁还是非公平锁。
7. Lock锁适合大量同步代码的同步问题,synchronized锁适合代码少量的同步问题。
锁是什么?如何判断锁的是谁?
- 锁(lock)是一种同步机制,用于控制对共享资源的访问,确保在任何时候只有一个线程可以访问该资源。锁能够保证线程安全,防止多个线程同时访问共享资源时产生的数据不一致问题。
- synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
synchronized锁 => 锁的是方法的调用,具体表现为以下3种形式:
- 对于普通实例同步方法,锁的是当前类的实例对象
- 对于静态同步方法,锁的是当前Class类对象
- 对于同步代码块,锁的是synchronized括号里配置的对象
四. 生产者和消费者问题
常见面试题:手写单例模式、手写排序算法(冒泡,选择,快速)、手写生产者消费者,手写死锁。
4.1 生产者和消费者synchronized版:
package com.gch.producer_consumer;
/**
数字:资源类
*/
public class Data {
// 属性
private int number = 0;
// 方法
/**
生产者:+1
*/
public synchronized void increment() throws InterruptedException {
if(number == 0){
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 通知其它线程我+1完毕,该其它线程-1了
this.notifyAll();
}
// 等待
this.wait();
}
/**
消费者:-1
*/
public synchronized void decrement() throws InterruptedException {
if(number == 1){
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 通知其它线程我-1完毕了,该其它线程+1了
this.notifyAll();
}
// 等待
this.wait();
}
}
package com.gch.producer_consumer;
/**
* 线程通信:生产者和消费者问题(等待唤醒机制),可以让线程交替执行
* A B 操作同一个变量 num = 0
* A num + 1
* B num - 1
* 口诀:等待,业务,唤醒(通知)
* 真实的业务耦合性必须降低
*/
public class WaitAndNotify {
public static void main(String[] args) {
Data data = new Data();
/**
生产者:+1
*/
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
data.increment();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"生产者").start();
/**
消费者:-1
*/
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"消费者").start();
}
}
4.2 问题升级:防止虚假唤醒问题,4个线程,2个加,2个减:
package com.gch.spurious_wakeup;
/**
数字:资源类
*/
public class Data {
// 属性
private int number = 0;
// 方法
/**
生产者:+1
*/
public synchronized void increment() throws InterruptedException {
if(number == 1){
// 等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 通知其它线程我+1完毕,该其它线程-1了
this.notifyAll();
}
/**
消费者:-1
*/
public synchronized void decrement() throws InterruptedException {
if(number == 0){
// 等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 通知其它线程我-1完毕了,该其它线程+1了
this.notifyAll();
}
}
package com.gch.spurious_wakeup;
/**
* 线程通信:生产者和消费者问题(等待唤醒机制),可以让线程交替执行
* A B 操作同一个变量 num = 0
* A num + 1
* B num - 1
* 口诀:等待,业务,唤醒(通知)
* 真实的业务耦合性必须降低
*/
public class WaitAndNotify {
public static void main(String[] args) {
Data data = new Data();
/**
生产者:+1
*/
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
data.increment();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"生产者1").start();
/**
消费者:-1
*/
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"消费者1").start();
/**
生产者:+1
*/
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
data.increment();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"生产者2").start();
/**
消费者:-1
*/
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"消费者2").start();
}
}
if语句可能会导致线程虚假唤醒问题!!!
4.3 解决虚假唤醒问题:把if语句改为while循环:
4.4 JUC版的生产者消费者问题 --- Condition --- 实现精准唤醒
package com.gch.condition;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
数字:资源类
*/
public class Data {
// 要操作的数据
private int number = 0;
// 锁对象:唯一的,不可替换的
private static final Lock lock = new ReentrantLock();
// 获取四个Condition实例分配给四个线程
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
private Condition conditionC = lock.newCondition();
private Condition conditionD = lock.newCondition();
/**
生产者A:+1
*/
public void incrementA() {
lock.lock();
try {
// 业务代码
while(number == 1){
conditionA.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 通知其它线程-1
conditionB.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
/**
消费者B:-1
*/
public void decrementB() {
lock.lock();
try {
// 业务代码
while(number == 0){
conditionB.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 通知其它线程+1
conditionC.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
/**
生产者C:+1
*/
public void incrementC() {
lock.lock();
try {
// 业务代码
while(number == 1){
conditionC.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 通知其它线程-1
conditionD.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
/**
消费者D:-1
*/
public void decrementD() {
lock.lock();
try {
// 业务代码
while(number == 0){
conditionD.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 通知其它线程+1
conditionA.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
package com.gch.condition;
/**
Condition接口实现线程通信,实现精准唤醒(有序执行)
*/
public class LockAndCondition {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{data.incrementA();},"生产者A+1").start();
new Thread(()->{data.decrementB();},"消费者B-1").start();
new Thread(()->{data.incrementC();},"生产者C+1").start();
new Thread(()->{data.decrementD();},"消费者D-1").start();
}
}
五. 6锁现象
锁是什么,如何判断锁的是谁?
永远的知道什么是锁,锁到底锁的是谁? 深刻理解我们的锁
锁肯定锁的是对象===>Class(对象怎么来的?通过类new出来的)
1. 标准访问,请问先打印sendMsg还是先打印Call?
package com.gch.lock8;
public class Phone {
/**
发短信
*/
public synchronized void sendMsg() {
System.out.println("Send Msg...");
}
/**
打电话
*/
public synchronized void call() {
System.out.println("Call...");
}
}
package com.gch.lock8;
import java.util.concurrent.TimeUnit;
/**
8锁:就是关于锁的8个问题
* 1.标准情况下是A线程先执行sendMsg方法呢还是B线程先调用call方法呢?
* 锁的特性:synchronized锁 => 锁的对象是方法的调用者
* sendMsg()方法和call()方法用的是同一把锁 => phone对象
* 谁先获取到锁对象,谁就先执行
* 谁先获取Phone对象的锁,谁就先执行
*/
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
for (int i = 0; i < 10; i++) {
new Thread(()->{phone.sendMsg();},"A").start();
/**
让主线程休眠1秒钟
*/
TimeUnit.SECONDS.sleep(1);
new Thread(()->{phone.call();},"B").start();
}
}
}
结论:被synchronized修饰的方法,锁的对象是方法的调用者。因为两个方法的调用者是同一 个,所以两个方法用的是同一个锁,先调用方法的先执行!
2. sendMsg()方法延迟/休眠x秒,请问是先打印sendMsg还是先打印hello?
package com.gch.lock8;
import java.util.concurrent.TimeUnit;
public class Phone {
/**
发短信
*/
public synchronized void sendMsg() {
/**
sendMsg()方法休眠4s
*/
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Send Msg...");
}
/**
打电话
*/
public synchronized void call() {
System.out.println("Call...");
}
}
package com.gch.lock8;
import java.util.concurrent.TimeUnit;
/**
8锁:就是关于锁的8个问题
* 1.标准情况下是A线程先执行sendMsg方法呢还是B线程先调用call方法呢?
* 锁的特性:synchronized锁 => 锁的对象是方法的调用者
* sendMsg()方法和call()方法用的是同一把锁 => phone对象
* 谁先获取到锁对象,谁就先执行
* 谁先获取Phone对象的锁,谁就先执行
* synchronized锁的对象是方法的调用者,两个方法用的是同一把锁
* 谁先拿到谁就先执行!
*/
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{phone.sendMsg();},"A").start();
/**
让主线程休眠1秒钟
*/
TimeUnit.SECONDS.sleep(1);
new Thread(()->{phone.call();},"B").start();
}
}
结论:被synchronized修饰的方法,锁的对象是方法的调用者。因为两个方法的调用者是同一 个,所以两个方法用的是同一把锁,先调用方法的先执行,第二个方法只有在第一个方法 执行完释放锁之后才能执行。
3、新增一个普通方法hello()没有同步,请问先打印sendMsg还是Hello?
package com.gch.lock8;
import java.util.concurrent.TimeUnit;
public class Phone {
/**
发短信
*/
public synchronized void sendMsg() {
/**
sendMsg()方法休眠4s
*/
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Send Msg...");
}
/**
hello
*/
public void hello(){
System.out.println("Hello...");
}
}
package com.gch.lock8;
import java.util.concurrent.TimeUnit;
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{phone.sendMsg();},"A").start();
/**
让主线程休眠1秒钟
*/
TimeUnit.SECONDS.sleep(1);
new Thread(()->{phone.hello();},"B").start();
}
}
结论:新增的方法没有被synchronized修饰,不是同步方法,不受锁的影响,所以不需要等待。
4. 两个Phone对象,分别调用sendMsg和Call,谁先执行?
package com.gch.lock8;
import java.util.concurrent.TimeUnit;
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
// 两个对象=>两个不同的调用者=>两把锁
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(()->{phone1.sendMsg();},"A").start();
/**
让主线程休眠1秒钟
*/
TimeUnit.SECONDS.sleep(1);
new Thread(()->{phone2.call();},"B").start();
}
}
package com.gch.lock8;
import java.util.concurrent.TimeUnit;
public class Phone {
/**
发短信
*/
public synchronized void sendMsg() {
/**
sendMsg()方法休眠4s
*/
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Send Msg...");
}
/**
打电话
*/
public synchronized void call() {
System.out.println("Call...");
}
}
5. 两个静态的同步方法,同一个Phone类对象,是先执行sendMsg还是call?
package com.gch.lock8;
import java.util.concurrent.TimeUnit;
public class Phone {
/**
发短信
*/
public static synchronized void sendMsg() {
/**
sendMsg()方法休眠4s
*/
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Send Msg...");
}
/**
打电话
*/
public static synchronized void call() {
System.out.println("Call...");
}
}
package com.gch.lock8;
import java.util.concurrent.TimeUnit;
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{Phone.sendMsg();},"A").start();
/**
让主线程休眠1秒钟
*/
TimeUnit.SECONDS.sleep(1);
new Thread(()->{Phone.call();},"B").start();
}
}
6. 一个普通实例同步方法,一个静态同步方法,同一个Phone,请问先打印sendMsg还是call?
package com.gch.lock8;
import java.util.concurrent.TimeUnit;
public class Phone {
/**
发短信
*/
public static synchronized void sendMsg() {
/**
sendMsg()方法休眠4s
*/
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Send Msg...");
}
/**
打电话
*/
public synchronized void call() {
System.out.println("Call...");
}
}
package com.gch.lock8;
import java.util.concurrent.TimeUnit;
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{Phone.sendMsg();},"A").start();
/**
让主线程休眠1秒钟
*/
TimeUnit.SECONDS.sleep(1);
new Thread(()->{phone.call();},"B").start();
}
}
结论:被synchronize修饰的static静态方法,锁的对象是类的class对象。仅仅被synchronized 修饰普通实例的方法,锁的对象是方法的调用者。因为两个方法锁的对象不是同一个,所 以两个方法用的不是同一个锁,并且synchronized是非公平锁,因此后调用的方法不需要 等待先调用的方法。
总结:synchronized是对象锁,synchronized实现同步的基础:Java中的每一个对象都 可以作为锁
具体的表现为以下三种形式:
- 对于实例同步方法,锁的是方法的调用者(this)
- 对于静态同步方法,锁的是当前的Class类对象
- 对于同步代码块,锁的是synchronized括号里面的锁对象
六. 集合类不安全
6.1 ArrayList在开启多线程下,线程不安全:
- 在JDK1.5之前,如果想要使用并发安全的List只能选择Vector,而Vector是一种老旧的集合,已经被淘汰。Vector对于增删改查等方法基本都加了synchronized关键字{同步方法},这种方式虽然能够保证线程同步,但这相当于对整个Vector加上了一把大锁,使得每个方法执行的时候都要去获得锁,导致性能非常低下。
- JDK1.5引入了java.util.concurrent(JUC)包,其中提供了很多线程安全并且并发性能良好的容器,其中唯一线程安全的List实现就是CopyOnWriteArrayList。
package com.gch.unsafe;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public class ListTest {
public static void main(String[] args) {
// public static <T> List<T> asList(T... a) {
// return new Arrays.ArrayList<>(a);
// }
List<String> list1 = Arrays.asList("1","2","3");
list1.forEach(System.out::println);
System.out.println("list1..............list2...................");
// 并发下ArrayList是不安全的 => 增删改查方法都没有加锁
// java.util.ConcurrentModificationException => 并发修改异常
/**
解决方案:换集合类
1.用线程安全的Vector => List<String> list2 = new Vector<>();
2.用Collections工具类 => List<String> list2 = Collections.synchronizedList(new ArrayList<>());
3.CopyOnWriteArrayList类 => List<String> list2 = new CopyOnWriteArrayList<>();
*/
List<String> list2 = new ArrayList<>();
// 1. List<String> list2 = new Vector<>(); jdk1.0J就存在了
// 2. List<String> list2 = Collections.synchronizedList(new ArrayList<>());
// 3. List<String> list2 = new CopyOnWriteArrayList<>();
// CopyOnWrite => 写入时复制 简称COW 是一种计算机程序设计领域的优化策略
// 只会复制数据的引用而不会复制数据本身 写操作会创建原数组的副本,在新数组上进行操作
// 因此读操作仍然可以并发的访问原数组,从而实现读写分离{MyCat}
// CopyOnWriteArrayList在并发场景下比Vector性能好在哪里?
// COWArrayList只是在写操作{增删改=>add,remove,set}上加了锁,加了synchronized同步代码块
// 而读操作{get(index)}没有加锁
// Vector的增删改查等方法基本上都加了锁,加了synchronized同步方法,保证线程同步
// 所以Vector每次进行读操作时都要进行加锁,性能较低
// 因此CopyOnWriteArrayList的读操作比Vector性能更高,CopyOnWriteArrayList适用于读多写少的场景
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
new Thread(()->{
list2.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list2);
},String.valueOf(i)).start();
}
}
// UUID - 通用唯一识别码
// UUID使用16进制表示,共有36个字符{32个字母数字+4个连接符"-"}组成
System.out.println(UUID.randomUUID());
}
}
写入时复制(CopyOnWrite)思想
- 写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
- 读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array
6.2 HashSet线程不安全:
package com.gch.unsafe;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* 并发环境下HashSet是线程不安全的 =>
* java.util.ConcurrentModificationException{并发修改异常}
* 1. Set<String> set = new HashSet<>(); => 线程不安全的
* 2. Set<String> set = Collections.synchronizedSet(new HashSet<>()); 线程安全的
* 3. Set<String> set = new CopyOnWriteArraySet<>(); => 线程安全的
*/
public class SetTest {
public static void main(String[] args) {
// 多线程下HashSet是线程不安全的,会引起并发修改异常
Set<String> set = new HashSet<>();
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
}
HashSet底层是什么?
- HashSet底层就是HashMap。
- HashSet的add方法底层是HashMap的put方法。
- Set集合的本质就是Map集合的Key{因为Map集合的Key是无法重复的!}
6.3 HashMap线程不安全:
package com.gch.unsafe;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Map<String,String> map = new HashMap<>();
* HashMap在并发环境下是线程不安全的 => java.util.ConcurrentModificationException
* 解决方案:
* 1. Map<String,String> map = Collections.synchronizedMap(new HashMap<>());
* 2. Map<String,String> map = new ConcurrentHashMap<>();
* HashMap有四个构造方法
* 两个重要参数:
* 1. float loadFactor => 加载/负载因子 HashMap默认加载因子为0.75
* 2. int initialCapacity => 初始容量 默认初始容量为16
* HashMap初始化时,尽量指定初始值大小
*/
public class MapTest {
public static void main(String[] args) {
// Map是这样用的吗? 不是,工作中不用HashMap
// HashMap初始化时,尽量指定初始值大小
Map<String,String> map = new HashMap<>();
// 默认等价于什么? Map<String,String> map = new HashMap<>(16, 0.75F);
// 工作中,常常会自己根据业务来写参数,提高效率
// Map<String,String> map = new HashMap<>(16, 0.75F);
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
new Thread(()->{
map.put(UUID.randomUUID().toString().substring(0,3), UUID.randomUUID().toString().substring(0,1));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
}
7. Callable
使用Runnable和Callable接口都可以创建多线程,它们有什么区别呢?
- Runnable接口的run()方法没有返回值,而Callable接口的call()方法有返回值,是个泛型接口
- Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部try...catch...消化,不能继续往上抛出异常
- Thread只能接收Runnable线程任务对象,而FutureTask实现了Runnable接口,是Runnable对象,并且FutureTask可以包装Callable对象成为线程任务对象。
代码演示:
package com.gch.callable;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// new Thread(new FutureTask<String>(new MyCallable())).start();
Callable call = new MyCallable();
// FutureTask => 未来任务对象
// 作用1:FutureTask实现了Runnable接口,是Runnable对象=>{是线程任务对象},
// 并且FutureTask可以封装Callable对象成为线程任务对象交给Thread处理
// 作用2:可以在线程执行完毕之后通过调用其get方法得到线程执行完成的结果
FutureTask<String> ft = new FutureTask<String>(call);
// 提问:启动A,B两个线程包装同一个FutureTask对象执行,会打印几个call?
// 回答:会打印一次call
// 原因:如下
new Thread(ft,"A").start();
new Thread(ft,"B").start();
// FutureTask的get()方法会阻塞当前线程直到任务完成并返回结果
// 如果call方法没有执行完毕,这里的代码会等待,直到线程跑完才提取结果。
String rs = ft.get();
System.out.println(rs);
}
}
class MyCallable implements Callable<String> {
@Override
public String call(){
System.out.println("Call...");
//System.out.println(UUID.randomUUID().toString());
return "洋宝";
}
}
// 提问:启动A,B两个线程包装同一个FutureTask对象执行,会打印几个call?
// 回答:会打印一次call
原因:
- FutureTask是一个表示可能还没有完成的异步任务的类,它的构造方法接收一个Callable对象作为参数,该Callable对象实现了call()方法,用于执行异步任务并返回结果。
- FutureTask的特性是:在多个线程中对同一个FutureTask对象调用run()方法或通过ExecutorService执行时,FutureTask会确保call()方法只会被调用一次。
- 当启动两个线程包装同一个FutureTask对象,只会执行一次Callable任务,这是因为FutureTask内部的状态控制机制以及缓存机制{FutureTask类内部对任务执行结果进行了缓存}。
状态控制机制主要设计两个状态变量,分别是"state"和"result":
- "state"表示任务的执行状态(比如,正在运行、已完成、已取消等)
- "result"表示任务的返回结果,类型为泛型。
- "result"变量是在任务执行过程中暂存结果的临时变量
- 如果任务已完成且返回正常返回结果,则存储实际的返回值
- 如果任务已完成但是抛出了异常,则存储相应的异常对象
FutureTask类内部对任务执行结果进行了缓存
- 在FutureTask类中,缓存是通过一个内部的outcome属性来实现的,outcome属性可以保存/缓存任务的执行结果,它的类型的Object,可以存储Callable任务执行的返回值或者Exception异常。
- 这种缓存的机制 / 这种设计是为了 => 可以避免重复执行任务,提高性能。
执行流程:
- 当第一个线程调用FutureTask的run()方法执行任务后,任务的状态变量state会设为为已完成状态,那么FutureTask类内部会对任务执行结果进行缓存,当其它线程再次尝试执行同一个FutureTask,调用FutureTask的run()方法时,发现state不是NEW,会直接返回缓存中的结果,而不会再次执行call方法。
- 所以,当启动多个线程包装同一个FutureTask对象时,只有第一个调用run()方法的线程会执行任务,其它线程会直接返回,这样设计的目的是避免重复执行任务,提高性能。
result变量与oucome属性的区分:
- 在FutureTask类中,result变量用于暂存任务执行的结果,而真正用于缓存结果的是outcome属性。
- outcome属性是用于缓存任务的执行结果的,而result变量是在任务执行过程中暂存结果的临时变量,当任务最终完成时,会将result的值缓存到outcome属性中,以便后续可以获取到任务的执行结果。
总结:
- 在多线程环境下,只有第一个调用FutureTask的run()方法的线程会执行任务,其它线程会直接返回缓存中的结果,所以,启动两个线程包装同一个FutureTask对象时,只会执行一次任务,输出一次结果,这是由于FutureTask内部的状态控制机制以及缓存机制。
- 如果希望多次执行call()方法并输出多次结果,也就是每个线程都执行一个Callable任务,而不是用缓存结果,需要每次执行任务时创建一个新的FutureTask对象,并将其包装在不同的线程中执行,这样每个线程执行的任务就是独立的,不会受到结果缓存的影响。
缓存机制的前提:
- FutureTask的缓存机制的前提是:任务只需执行一次或每次执行的结果相同,那么就可以利用缓存机制避免重复执行。
FutureTask的源码解析:
package com.gch.callable;
import java.util.concurrent.*;
/**
* FutureTask的实现主要依靠自旋锁和CAS操作来实现线程安全的状态转换和结果获取
* 通过状态变量state的不同取值和结果变量outcome的复制,来表示任务的不同状态和结果。
* 同时,通过waiters来维护等待任务完成的线程。
* 这些机制保证了FutureTask的正确性和并发性。
* @param <V> == callable.call()方法的返回值的数据类型
*/
public class FutureTask<V> implements RunnableFuture<V> {
/** 表示任务的状态,使用volatile修饰,确保可见性 */
private volatile int state;
/** 任务尚未执行 */
private static final int NEW = 0;
/** 任务正在执行中 */
private static final int COMPLETING = 1;
/** 任务已完成且正常返回结果 */
private static final int NORMAL = 2;
/** 任务已完成但是抛出了异常 */
private static final int EXCEPTIONAL = 3;
/** 任务被取消 */
private static final int CANCELLED = 4;
/** 任务正在被中断 */
private static final int INTERRUPTING = 5;
/** 任务已被中断 */
private static final int INTERRUPTED = 6;
/** 保存需要执行的Callable任务 */
private Callable<V> callable;
/** 缓存任务执行的结果 */
private Object outcome;
/** 表示执行任务的线程 */
private volatile Thread runner;
/** 表示等待任务完成的线程 */
private volatile WaitNode waiters;
// ...
/**
* 构造函数,接收一个Callable任务作为参数
* 使用给定的Callable对象创建一个FutureTask实例
*/
public FutureTask(Callable<V> callable) {
if (callable == null) {
throw new NullPointerException();
}
this.callable = callable;
// 初始化为NEW状态
this.state = NEW;
}
// ...
/**
* 核心的任务执行方法
* result变量是在run()方法中通过调用callable.call()方法得到的任务执行结果
* 而在最终设置任务的执行结果时,会使用set()方法将result变量的值缓存到outcome属性中
* 因此,outcome属性是用于缓存任务的执行结果的,
* 而result变量是在任务执行过程中暂存结果的临时变量。
* 当任务最终完成时,会将result的值缓存到outcome属性中,以便后续可以获取到任务的执行结果。
*/
public void run() {
// 检查任务的状态,如果不是NEW状态,如果任务已经完成或被取消,则直接返回
if (state != NEW || !UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
// 任务会调用callable.call()执行Callable任务,将执行结果保存在result变量中
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
// 如果任务执行过程中发生异常,设置异常结果,并将异常对象存储在result中
setException(ex);
}
if (ran)
// 任务成功执行,调用set(result)设置任务执行结果到outcome属性中
set(result);
}
} finally {
state = ranOrCancelled(state); // 设置任务状态
}
}
/**
* 设置任务的执行结果=>使用set()方法将result变量的值缓存到outcome属性中
* set()方法使用CAS原子操作来设置outcome属性的值,确保只有一个线程能够成功设置结果
* 这就是缓存的实现机制,通过将结果保存在outcome属性中,可以避免多个线程重复执行同一个Callable任务
* 当第二个线程执行任务时,会发现outcome属性已经被第一个线程设置好了,所以不再执行Callable任务,直接返回缓存的结果
*/
private void set(V v) {
// 如果结果为空,则抛出异常
if (STATE.compareAndSet(this, NEW, COMPLETING)) {
outcome = v;
STATE.setRelease(this, NORMAL); // final state
finishCompletion();
}
}
/**
* 设置任务执行异常结果
*/
private void setException(Throwable t) {
// 如果结果为空,则抛出异常
if (UNSAFE.compareAndSwapObject(this, outcomeOffset, null,
new ExecutionException(t))) {
//...
}
}
/**
* 取消任务的方法
*/
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
/**
* 判断任务是否被取消
* @return
*/
@Override
public boolean isCancelled() {
return false;
}
/**
* 判断任务是否已完成
* @return
*/
@Override
public boolean isDone() {
return false;
}
/**
* 获取任务的结果
* 如果任务已完成,则直接返回结果;如果任务还未完成,则阻塞等待任务完成,并返回结果
* @return
* @throws InterruptedException
* @throws ExecutionException
*/
@Override
public V get() throws InterruptedException, ExecutionException {
return null;
}
/**
* 在指定的时间内获取任务的结果
* 如果任务已完成,直接返回结果;
* 如果任务还未完成,则阻塞等待任务完成,并在指定的时间内返回结果,超时则抛出TimeoutException异常
* @param timeout the maximum time to wait
* @param unit the time unit of the timeout argument
* @return
* @throws InterruptedException
* @throws ExecutionException
* @throws TimeoutException
*/
@Override
public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
return null;
}
// ...
}
8. JUC的三大常用辅助类{并发工具类}
8.1 CountDownLatch
- CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能,比如有一个任务A,它要等待其它三个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
- CountDownLatch的作用就是允许count个线程阻塞在一个地方,直到所有线程的任务都执行完毕。
- CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。
- 当调用await()方法的时候,如果state{count}不为0,那就证明任务还没有执行完毕,await()方法就会一直阻塞,也就是说await()方法之后的语句不会被执行,直到count个线程调用countDown()使state值被减为0或者调用await()的线程被中断,该线程才会从阻塞中被唤醒,await()方法之后的语句得到执行。
- 调用await()方法的线程会利用AQS排队,一旦count被减为0,则会将AQS中排队的线程依次唤醒。
举例:就好比关门,客人都出去了,老板才关门。
package com.gch.helperclass;
import java.util.concurrent.CountDownLatch;
/**
CountDownLatch => 减法计数器
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 创建了一个count为6的CountDownLatch对象 总数是6
// public CountDownLatch(int count):count为计数器的初始值(一般需要多少个线程执行,count就设为几)
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + " => Get Out...");
// 每调用一次,计数器值-1,直到count被减为0,代表所有线程全部执行完毕
countDownLatch.countDown(); // -1
}).start();
}
countDownLatch.await(); // 等待计数器归零,然后向下执行
System.out.println("Close...");
}
}
8.2 CyclicBarrier
- CyclicBarrier它的作用就是会让所有线程等待完成后才会继续下一步行动。
- CyclicBarrier是Java标准库中的一个同步辅助类,用于控制多个线程之间的同步。当一组线程都达到了某个同步点时,它们可以阻塞等待彼此,并在达到同步点后继续执行。
- CyclicBarrier内部使用了count计数器来追踪到达同步点的线程数,并通过await()方法来实现线程的等待和唤醒。
- CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
- CyclicBarrier它就相当于是一个栅栏,线程在到达栅栏后需要等待其它线程,等所有线程都到达后,再一起通过。
- 它的栅栏(Barrier)可以重复使用(Cyclic) => 可以在等待的线程被释放之后重新使用。
- CyclicBarrier的原理:
- CyclicBarrier内部通过一个count变量作为计数器,count初始值为parties属性的初始化值,每一个线程到了栅栏这里了,那么就将计数器count减1.如果count值为0了,表示这是这一代最后一个线程到达栅栏,此时就可以去执行我们构造方法中输入的任务了。
- 每个线程调用await()方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
- 当到达屏障的线程数量达到count时,await()之后的方法才会被执行。
作用:好比集齐7个龙珠召唤神龙,或者人到齐了再开会!
Demo:
package com.gch.helperclass;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
CyclicBarrier => 加法计数器
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙成功!");
});
for (int i = 0; i < 7; i++) {
final int temp = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "收集" + temp + "个神龙...");
try {
// await()方法实际上底层调用的是dowait()方法
// 当调用await()方法时,当前线程会阻塞等待,直到所有参与线程都到达同步点
// await()方法的签名中抛出了两个异常,用于处理中断和破坏屏障的情况
cyclicBarrier.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
}).start();
}
}
}
CyclicBarrier的源码:
package com.gch.helperclass;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/** 同步辅助类 */
public class CyclicBarrier {
/**静态内部类Generation */
private static class Generation {
Generation() {} // 防止创建访问构造函数
boolean broken; // 初始化为false
}
/** 同步操作锁:用于保护屏障入口的锁 */
private final ReentrantLock lock = new ReentrantLock();
/** 线程拦截器 => 阻塞与唤醒线程使用的 */
private final Condition trip = lock.newCondition();
/** 每次屏障拦截的线程数 */
private final int parties;
/** 换代前执行的任务 */
private final Runnable barrierCommand;
/** 表示栅栏的当前代 */
private Generation generation = new Generation();
/** 计数器 */
private int count;
/**
* @param parties => 每次屏障拦截的线程数,当拦截的线程数量达到这个值的时候就打开栅栏,让所有线程通过
* @param barrierAction => 统一到达同步点后执行的任务
*/
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
/**
* CyclicBarrier的默认有参构造
* @param parties
*/
public CyclicBarrier(int parties) {
this(parties, null);
}
}
cyclicBarrier.await()方法底层调用的是dowait(false,0L)方法。
daowait()方法源码解读:
- dowait()方法主要负责控制线程的等待和唤醒逻辑,并处理与计数器、屏障破坏、超时等相关的操作。
- 该方法首先会获取内部的lock锁,确保线程之间的同步。
- 然后它会检查屏障是否已经被破坏(也就是计数器count是否为0),如果为0,说明屏障已经被破坏,此时会抛出BrokenBarrierException异常。
- 接着检查当前线程是否被中断,如果被中断则抛出InterruptedException异常。
- 当计数器count不为0时,当前线程会将计数器的值减1,并将减1的值赋给index,然后它会进入一个死循环,不断调用await()方法等待其它线程到达,在等待过程中,会检查当前线程是否被中断,如果被中断则抛出InterruptedException异常。直到index为0,说明当前线程是最后一个到达的线程,意味着所有线程都到达了同步点,此时会执行构造函数中定义的Runnable任务,然后唤醒所有在屏障上等待的线程,并重置计数器的值(将count重置为parties属性的初始化值),然后进入下一轮执行。
- 最后,dowait()方法在释放lock锁之前,会返回当前线程在数组中的序号index。
- 综上所述:await()方法确实会阻塞当前线程,并让计数器count - 1,但并不是通过让计数器+1来实现的,而是根据计数器的值来判断当前线程是否为最后一个到达的线程,并根据情况进行不同的处理。
说说CyclicBarrier和CountDownLatch的区别?
- 两个类都位于java.util.concurrent包下,都是Java并发编程中的两个同步辅助类,它们都用于控制多个线程的同步,二者的区别在于:
- CyclicBarrier适用于多个线程相互等待,直到达到一个同步点后再继续执行的场景;而CountDownLatch适用于一个或多个线程等待其它线程完成操作后再执行某个特定任务的场景。
- CyclicBarrier可以被重复使用,当所有线程都到达同步点并执行完成后,CyclicBarrier将被重置,可以再次被使用;而CountDownLatch在计数器减到0后,不能再次被使用。
8.3 Semaphore
semaphore:信号量 举例:停车位
- Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使用该信号量,通过acquire()来获取许可,如果没有许可可用则线程阻塞,并通过AQS排队,可以通过release()方法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第一个线程开始一次唤醒,直到没有空闲许可。
- Semaphore作用的本质是限制某段代码块中线程的并发数(量),也就是允许自定义多少线程同时访问,而synchronized和ReentrantLock都是一次只允许一个线程访问某个资源,就这一点而言,单纯的synchronized关键字是实现不了的,可以说Semaphore是synchronized的加强版。
- Semaphore有一个构造函数,可以传入一个int型整数permits,表示某段代码最多有n个线程可以访问,如果将Semaphore构造函数中传入的int型整数permits设为1,相当于变成一个synchronized了。
Semaphore
通常用于那些资源有明确访问数量限制的场景比如限流(仅限于单机模式,实际项目中推荐使用 Redis +Lua 来做限流)。 信号量主要用于两个目的:一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
package com.gch.helperclass;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* Semaphore(信号量)可以用来控制同时访问特定资源的线程数量
*/
public class SemaphoreDemo {
public static void main(String[] args) {
// 初始化共享资源数量{许可证的数量 => 只有拿到许可证的线程才能执行}
// 表示同时允许最多多少个线程使用该信号量
Semaphore available = new Semaphore(5);
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
// 获取1个许可
available.acquire();
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + " => Execute...");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally{
// 释放1个许可
available.release();
}
}).start();
}
}
}
怎么控制同一时间只有 3 个线程运行?
- 用 Semaphore。
9. ReentrantReadWriteLock读写锁 {ReadWriteLock}
补充:行锁、表锁(MySQL)
比如,我现在要对数据库中的某一张表进行操作,而在表里面会有很有条记录。
- 表锁:比如我现在对该表的第一条记录进行操作,我把整张表都上锁,这就叫表锁。
- 行锁:比如我现在只想操作该表的第一条记录,我就对第一条记录上锁,那别人是不能操作第二条记录、第三条记录包括其它记录,这就叫行锁。
提问:表锁和行锁谁会发生死锁?
- 因为表锁锁的是整张表,我操作的时候,整张表别人都不能操作,所以它不会有死锁。
- 但是行锁,因为我只操作某一行,所以它会发生死锁。
- 死锁的特点:多个线程互相等待,最终我等你,你等我,最终两人都不能往下进行,这个叫死锁,所以行锁会发生死锁。
提问:读锁和写锁谁会发生死锁?
- 答:读锁和写锁都会发生死锁。
读锁操作发生死锁:
- 比如我现在有张表,表里面有条记录,现在有两条线程同时对该记录进行读取操作,线程1在读取的时候还进行了一个修改{写}操作,但是线程2此时在对该记录在进行读取操作,只有当线程2读取操作完成后,线程1才能进行修改{写}操作,但是这个时候恰巧线程2也要对该记录进行修改操作,此时就会产生死锁:线程2的修改/写操作要等待线程1读取操作完成,线程1的写操作要等待线程2的读操作完成,所以它们之间就互相等待了:
- 1的改在等2的读完成,2的改在等1的读完成,因此读锁是会发生死锁的。
- 线程1在读的时候可以改,线程2在读的时候也可以改,1的改要等2的读完成,2的改要等1的读完成。
写锁操作发生死锁:
- 比如我现在有两个线程,线程1在对我的第一条记录进行写操作,那么此时别的线程来的时候就不能操作我的第一条记录了;线程2在对我的第二条记录也进行写操作,那这个时候再来别的线程进行操作就只能阻塞等待了。
- 注意:线程1在操作第一条记录的时候,也可以去操作我们的第二条记录,它可以一个线程中操作多条记录。
- 假如线程1此时又去操作我们的第二条记录,因为第二条记录线程2正在进行写操作,所以线程1的操作只能等待;而碰巧线程2此时又去操作我们的第一条记录,由于第一条记录线程1正在进行写操作,所以线程2对第一条记录的操作也只能是等待,此时就会由于互相等待而发生死锁:线程1操作第二条记录要等线程2释放之后才能进行,而线程2操作第一条记录要等线程1释放之后才能进行。
代码演示:
package com.gch.readwritelock;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class ReadWriteLockDemo {
public static void main(String[] args) {
// 创建资源类对象
MyCache cache = new MyCache();
// 创建线程存数据
for (int i = 0; i < 5; i++) {
final int num = i;
new Thread(()->{
cache.put(num+"",num);
},String.valueOf(i)).start();
}
// 创建线程取数据
for (int i = 0; i < 5; i++) {
final int num = i;
new Thread(()->{
Object value = cache.get(num+"");
},String.valueOf(i)).start();
}
}
}
/**
资源类:模拟缓存
*/
class MyCache{
/** 创建Map集合 加上volatile关键字修饰保证可见性 */
private volatile Map<String,Object> map = new HashMap<>();
/**
存数据
*/
public void put(String key,Object value) {
System.out.println(Thread.currentThread().getName() + "正在进行写操作=>" + key);
try {
// 暂停一会儿 毫秒
TimeUnit.MICROSECONDS.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 放数据
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "写操作完成=>" + key);
}
/**
取数据
*/
public Object get(String key) {
Object result = null;
System.out.println(Thread.currentThread().getName() + "正在进行读操作=>" + key);
try { // 暂停一会儿 毫秒
TimeUnit.MICROSECONDS.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
result = map.get(key);
System.out.println(Thread.currentThread().getName() + "读操作完成=>" + key);
return result;
}
}
读写锁案例实现:
package com.gch.readwritelock;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
读写锁是一种优化{读多写少}
读和读不互斥,读和读不竞争锁,而读和写,写和写实现互斥
*/
public class ReadWriteLockDemo {
public static void main(String[] args) throws InterruptedException {
// 创建资源类对象
MyCache cache = new MyCache();
// 创建线程存/写数据
for (int i = 0; i < 5; i++) {
final int num = i;
new Thread(()->{
cache.put(num+"",num);
},String.valueOf(i)).start();
}
TimeUnit.SECONDS.sleep(3);
// 创建线程取/读数据
for (int i = 0; i < 5; i++) {
final int num = i;
new Thread(()->{
Object value = cache.get(num+"");
},String.valueOf(i)).start();
}
}
}
/**
资源类:模拟缓存
*/
class MyCache{
/** 创建Map集合 加上volatile关键字修饰保证可见性 */
private volatile Map<String,Object> map = new HashMap<>();
/** 创建读写锁对象 */
private ReadWriteLock rwl = new ReentrantReadWriteLock();
/**
存数据
*/
public void put(String key,Object value) {
// 添加写锁
rwl.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在进行写操作=>" + key);
// 暂停一会儿 毫秒
TimeUnit.MICROSECONDS.sleep(300);
// 放数据
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "写操作完成了=>" + key);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally{
// 释放写锁
rwl.writeLock().unlock();
}
}
/**
取数据
*/
public Object get(String key) {
// 添加读锁
rwl.readLock().lock();
try {
Object result = null;
System.out.println(Thread.currentThread().getName() + "正在进行读操作=>" + key);
// 暂停一会儿 毫秒
TimeUnit.MICROSECONDS.sleep(300);
result = map.get(key);
System.out.println(Thread.currentThread().getName() + "读操作完成=>" + key);
return result;
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally{
// 释放读锁
rwl.readLock().unlock();
}
}
}
读写锁的演变:
读写锁:一个资源可以被多个读线程访问,或者可以被一个写线程访问,但是不能同时存在 读写线程,读写互斥,写写互斥,读读共享的。
读写锁的锁降级:
锁降级:将写入锁降级为读锁。
- 一般认为,写的权限是要高于读的权限的,而锁降级就是将写锁的操作降级为读锁的操作。
- 在读的时候不能进行写操作,只有读完之后才可以写;但是我在写的时候可以进行读操作。
在JDK8中说明了锁降级的过程:
- 第一步先获取写锁,获取写锁之后并不是马上释放,再获取读锁,然后释放写锁,在释放写锁的过程中就可以把写锁最终降级为读锁。
锁升级:锁的要求越来越严格。
锁降级:降低锁的要求。锁降级举例:
- 比如我现在有两个同学,一个是小张,一个是小王,小张学习特别好,他是一个学霸,然后小王学习特别差,是一个学渣,这个时候学渣要去抄学霸的作业,怎么抄?
- 学霸是不是要开始写,学渣才能开始抄,若学霸不写的话,学渣没得可抄,学霸就是写锁,他要开始写,学渣就是读锁,他要开始抄,这个时候学霸{写锁}写完了要开始释放了,学渣{读锁}也抄完了,当然也要开始释放,这就是锁降级的基本过程。
- 注意:锁降级这个过程只能从写锁降级到读锁,读锁是不能升级为写锁的,就好比学霸还没开始写作业,学渣是没得可抄的,所以它不能从读锁升级为写锁,这就是锁降级的一个特点。
锁降级的目的:是为了提高数据的可见性,因为如果学吧没有写,我学渣就读不到,我读不 到的话,就不能进行抄的过程。
代码演示读写锁降级:
package com.gch.readwritelock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
代码演示读写锁降级
*/
public class Demo {
public static void main(String[] args) {
// 可重入读写锁对象
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
// public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
// public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
// 读锁
ReentrantReadWriteLock.ReadLock rl = rwl.readLock();
// 写锁
ReentrantReadWriteLock.WriteLock wl = rwl.writeLock();
// 锁降级
// 1.获取写锁
wl.lock();
System.out.println("学霸开始写...");
// 2.获取读锁
rl.lock();
System.out.println("学渣开始抄...");
// 3.释放写锁
wl.unlock();
System.out.println("学霸写完了...");
// 4.释放读锁
rl.unlock();
System.out.println("学渣也抄完了...");
}
}
10. BlockingQueue阻塞队列
FIFO:先进先出{FirstInput / FirstOuput}
10.1 阻塞队列概述
- java.util.concurrent 包中,BlockingQueue 很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。
- 阻塞队列,顾名思义,首先它是一个队列, 通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;
- 共享:多个线程能同时进行操作
- 当队列是空的,从队列中获取元素的操作将会被阻塞
- 当队列是满的,从队列中添加元素的操作将会被阻塞
- 试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素;
- 试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增。
- 好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切 BlockingQueue 都给你一手包办了
多线程环境中,通过队列可以很容易实现数据共享 ,比如经典的 “生产者”和“消费者”模型 中,生产者线程会向队列中添加数据,而消费者线程会从队列中取出数据进行处理。
通过队列可以很便利地实现两者之间的数据共享。 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
- 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒
- 阻塞队列在多线程开发中有着广泛的运用,最常见的例子无非是我们的线程池,从源码中我们就能看出当核心线程无法及时处理任务时,这些任务都会扔到
workQueue
{Blocking Queue}中。
10.2 阻塞队列的架构
Deque和Queue的区别:
- Deque:双向队列 / 双端队列,在队列的两端均可以插入或删除元素。
- Queue:Queue是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进 先出(FIFO) 规则。
10.3 阻塞队列分类
Java 中常用的阻塞队列实现类有以下几种:
ArrayBlockingQueue{常用}:基于数组实现的有界阻塞队列。在创建时需要指定容量大小,并支持公平和非公平两种方式的锁访问机制。
LinkedBlockingQueue{常用}:基于单向链表实现的可选有界阻塞队列。在创建时可以指定容量大小,如果不指定则默认为
Integer.MAX_VALUE
。和ArrayBlockingQueue
类似, 它也支持公平和非公平的锁访问机制。PriorityBlockingQueue:基于优先级排序的无界阻塞队列。元素必须实现
Comparable
接口或者在构造函数中传入Comparator
对象,并且不能插入 null 元素。(优先级的判断通过构造函数传入的 Compator 对象来决定),但需要注意的是 PriorityBlockingQueue 并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。在实现 PriorityBlockingQueue 时,内部控制线程同步的锁采用的是 公平锁 。 注意, 生产者生产数据的速度绝对不能快于消费者消费数据的速度 ,否则时间一长,会最终 耗尽所有的可用堆内存空间 。priority:优先级
SynchronousQueue:同步队列,没有容量,是一种不存储元素的阻塞队列。每个插入操作都必须等待对应的删除操作,反之删除操作也必须等待插入操作。一句话总结: 不存储元素的阻塞队列,也即单个元素的队列,相当于它最多只能放一个元素。当一个线程调用SynchronousQueue的put方法插入一个元素时,它会被阻塞直到有另一个线程调用take方法来接收这个元素。反过来,当一个线程调用take方法来接收一个元素时,它会被阻塞直到有另一个线程调用put方法来插入一个元素。此队列不允许
null
元素。需要注意的是,SynchronousQueue并不存储元素,它仅仅是起到一个线程间数据交换的作用。另外,由于SynchronousQueue的特殊性,它不支持peek操作,即无法查看队首元素。因此,SynchronousQueue
通常用于线程之间的直接传递数据。synchronous:同步DelayQueue :延迟队列,其中的元素只有到了其指定的延迟时间,才能够从队列中出队。DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
一句话总结: 使用优先级队列实现的延迟无界阻塞队列。 delay:延迟,延时
LinkedTransferQueue:由链表组成的无界阻塞队列。 LinkedBlockingDeque:由链表组成的双向阻塞队列。
package com.gch.synchronousqueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
/**
SynchronousQueue:同步队列
*/
public class SynchronousQueueDemo {
public static void main(String[] args) throws InterruptedException {
// 创建同步队列SynchronousQueue的对象
SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();
/** synchronousQueue不允许存放null元素,否则运行会抛异常-java.lang.NullPointerException */
// synchronousQueue.put(null);
// synchronousQueue.take();
new Thread(()->{
try {
synchronousQueue.put("Semaphore...");
System.out.println(Thread.currentThread().getName() + " : 传递了Semaphore...");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"A").start();
TimeUnit.SECONDS.sleep(2);
new Thread(()->{
try {
String s = synchronousQueue.take();
System.out.println(Thread.currentThread().getName() + " : 接收了" + s);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"B").start();
}
}
10.4 阻塞队列核心方法
尽量按组匹配使用!!!
解释:
11. ThreadPool线程池
11.1 线程池概述
- 程序的运行,其本质上,是对系统资源(CPU、内存、磁盘、网络等等)的使用。如何高效的使用这些资源是我们编程优化演进的一个方向。
- 线程池就借助了池化技术来减少线程的创建以及销毁所带来的性能以及资源开销,提高线程的复用性和执行效率。
池化技术
- 池化技术是一种资源管理技术,它的目的是在需要频繁获取和释放某种资源的情况下,通过对这些资源进行预先创建和复用,以提高系统性能和资源利用率。
- 具体的说,池化技术就是将可重复使用的资源组织在一个池中,这个池可以是对象池、连接池、线程池、内存池等,取决于所管理的资源类型。
- 池化技术在很多场景下都有广泛应用,比如数据库连接池可以管理和复用数据库连接;线程池可以管理线程对象的创建以及销毁;对象池可以管理对象实例。
线程池的优势:
- 通过复用线程降低资源消耗
- 提高响应速度
- 提高线程的可管理性
线程池的主要特点:线程复用,控制最大并发数,管理线程{管理线程的创建以及销毁}。
补充:什么是线程组?为什么Java中不推荐使用?
- ThreadGroup类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组对象,组中还可以有线程。
- 线程组和线程池是两个不同的概念,它们的作用完全不同,线程组是为了方便线程的管理,而线程池是为了管理线程的生命周期{创建以及销毁}。
- 为什么不推荐使用线程组?因为使用线程组有很多安全隐患把,没有具体追究,如果需要使用,推荐使用线程池。
11.2 线程池架构
- Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors, ExecutorService,ThreadPoolExecutor 这几个类
- ThreadPoolExecutor就是线程池。
什么是Executors?
- Executors:线程池的工具类,通过调用方法可以返回不同类型的线程池对象。
-
Executors框架实现的就是线程池的功能。
-
Executors工厂类中提供的newCachedThreadPool、newFixedThreadPool 、newScheduledThreadPool 、newSingleThreadExecutor 等方法其实也只是ThreadPoolExecutor的构造函数参数不同而已,通过传入不同的参数,就可以构造出适用于不同应用场景下的线程池。
-
总结:Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的。
11.3 线程池四种创建方式?
- newCachedThreadPool()创建一个可缓存线程池,如果当前线程池长度超过处理需要,可灵活回收空闲线程;当需要增加时,它可以灵活的添加新的线程。
- newFixedThreadPool(int nThreads) 创建一个定长线程池,创建固定线程数量的线程池,可控制线程最大并发数。
- newScheduledThreadPool(int corePoolSize) 创建一个定长线程池,支持定时及周期性执行任务。
- newSingleThreadExecutor ()创建只有一个线程的线程的线程池对象{单线程化的线程池},它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。缺点:它是单线程的,高并发业务下就很无力。如果这个唯一的线程因为出现异常而结束,那么线程池会补一个新线程。
- newFixedThreadPool(int nThreads)和newSingleThreadExecutor()允许请求的任务的阻塞队列长度是Integer.MAX_VALUE,可能出现OOM(java.lang.OutOfMemeoryError)
- newCachedThreadPool()和newScheduledThreadPool(int corePoolSize)创建的线程数量最大上限是Integer.MAX_VALUE,线程数可能会随着任务1:1增长,可能出现OOM(java.lang.OutOfMemeoryError)
在 Java 中 Executor 和 Executors 的区别?
- Executor是一个接口,Executors是一个Class类。
- Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
- Executor 接口对象能执行我们的线程任务。
- ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法。比如可以获取任务的返回值。
- ThreadPoolExecutor是Executor接口的实现类,使用 ThreadPoolExecutor 可以创建自定义线程池。
线程池中 submit() 和 execute() 方法有什么区别?
相同点:
- 相同点就是都可以开启线程执行池中的任务{都是用于提交任务的方法}。
不同点:
- 接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。submit()方法有多个重载形式,可以接收Runnable和Callable类型的任务对象,并可以返回具体的执行结果。
- 返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()方法没有返回值,无法获取任务的执行结果
- 异常处理:submit()方法通过Future对象可以捕获和处理任务过程中抛出的异常;而executre()方法在任务执行过程中如果抛出异常,无法直接获取和处理。
在使用submit()
方法提交任务并得到Future
对象后,可以通过以下方式捕获和处理异常:
Future<?> future = executorService.submit(task);
// 判断任务是否已经完成
if (future.isDone()) {
try {
future.get(); // 获取任务执行结果
} catch (InterruptedException e) {
// 处理中断异常
e.printStackTrace();
} catch (ExecutionException e) {
// 处理任务执行异常
// 如果任务执行过程中发生异常,ExecutionException会被捕获
// 并可以通过调用getCause()方法获取实际的异常原因进行处理
Throwable cause = e.getCause();
if (cause != null) {
cause.printStackTrace();
}
}
}
11.4 ThreadPoolExecutor 七大参数
- corePoolSize:核心线程数。在创建了线程池后,线程中没有任何线程,等到有任务到来时才创建线程去执行任务。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建 一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到阻塞队列当中。
- maximumPoolSize :最大线程数。表明线程中最多能够创建的线程数量,此值必须大于等于1。
- keepAliveTime :空闲的线程存活时间。
- TimeUnit :存活的时间单位。
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
7. RejectedExecutionHandler:任务的拒绝策略
线程池的执行原理?
- 当执行了executre()方法{提交任务}的时候,线程池中的线程才会创建。
线程池参数的设置流程:
根据业务场景设置理论值;
接口功能实现后进行压测测试,确认接口响应;
上线后监控系统2-3天。
参数参考:
- corePoolSize:20左右
- maxinumPoolSize:30-50
- wrokQueue:几百到一两千
- handler:一般来说是是CallerRunPolicy,当线程池已满无法接受新提交的任务时,会让当前线程也就是提交任务的线程来去执行该任务,避免了丢失任务。
场景:晚上流量与白天流量不一致,如果在流量低的时候想要降低内存的使用,该怎么做?
- 可以考虑降低核心线程数,调高任务队列,把超时时间设置略高一些
- 引入第三方依赖,实现动态线程池参数配置
什么是CPU密集和IO密集?
- CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
- IO密集型,即该任务需要大量的IO,即大量的阻塞。
一个线程运行时发生异常会怎样?
- 如果异常没有捕获该线程将会停止执行。
线程异常的处理方案:
- 手动try..catch...,将任务放到补偿表,或者仅仅输出日志,后面通过日志监控进行补偿
- submit()执行,Future.get()接收异常
- 重写ThreadPoolExecutor的afterExecute()方法,处理传递的异常引用
- 为工作者线程设置UncaughtExceptionHandler{thread.setUncaughtExceptionHandler(new 自定义类实现UncaughtExceptionHandler接口,并重写了在uncaughtException()方法)},在uncaughtException()方法中处理未捕获到的异常。
UncaughtExceptionHandler 是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。
补偿表(Compensation Table):
- 补偿表是在分布式系统中用于实现事务的一种机制,是一种通用的分布式事务管理机制,它提供了一种可靠的方式来处理事务的各种异常情况,确保系统数据的一致性和可靠性。
- 补偿表通常由两个部分组成:状态字段和操作记录字段。
- 在分布式事务中,多个操作可能涉及不同的资源或节点,在执行过程中可能会出现故障或异常情况,导致事务无法正常完成。
- 在分布式环境中,当出现故障或者异常时,系统通过补偿表来判断事务的当前状态,并根据事务状态采取相应的补偿操作以实现事务的最终一致性。
- 比如:对于执行成功的事务,可以直接提交;对于执行失败的事务,可以进行回滚;对于正在执行的事务,可以根据操作记录进行重新执行...
线程池都有哪些状态?
- RUNNING:线程池正常运行,既接受新任务,也会处理队列中的任务
- SHUTDOWN:当调用线程池的shutdown()方法时,线程池就进入SHUTDOWN状态,线程池不能接受新任务,但是线程池会把阻塞队列中的剩余任务执行完,剩余任务都处理完之后,会中断所有工作线程。
- STOP:当调用线程池的shutdownNow()方法时,线程池就进入了STOP状态,线程池既不能接受新任务,也不会处理阻塞队列中的任务,并且会中断所有的工作线程,包括正在运行的线程也会被中断,并返回队列中未执行的任务{List<Runnable>}
- TIDYING:当线程池中没有线程在运行后,线程池的状态就会自动变为TIDYING,并且会调用terminated()方法,该方法是空方法,留给程序员进行扩展。TIDYING
状态是为了在线程池状态转换时提供一个过渡的状态。当线程池从SHUTDOWN状态向TERMINATED状态转变时,会进行一些清理工作,比如中止未执行的任务、终止工作线程等,在这些清理工作完成之前,线程池就处于TIDYING状态,当所有的清理工作完成后,线程池进入TERMINATED状态,表示线程池已经完全关闭,没有任何活动的任务和线程。
通过引入TIDYING状态和TERMINATED状态,可以更准确的表示线程池的状态转换过程,以便程序能够根据不同的状态做出对应的处理。TIDYING是为了保留扩展性,这样在关闭前还有操作的机会,比如完成任务后发一个通知。
- TERMINATED:线程池已经关闭,terminated()方法执行完之后,线程池就会进入TERMINATED状态。在ThreadPoolExecutor中terminated()是一个空方法,可以自定义线程池重写这个方法。
12. Fork / Join分支合并框架
Fork:分支 Join:合并
12.1 Fork/Join 框架简介 - JDK1.7
- 意义:并行执行任务,提高效率,尤其是在大数据量的情况下!
- Fork:把一个复杂任务进行分拆,大事化小
- Join:把分拆任务的结果进行合并
- 任务分割:首先 Fork/Join 框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割
- 执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据。
12.2 ForkJoin特点:工作窃取
- 简单理解,就是一个工作线程下会维护一个包含多个子任务的双端队列。而对于每个工作线程来说,会从头部到尾部依次执行任务。这时,总会有一些线程执行的速度较快,很快就把所有任务消耗完了。那这个时候怎么办呢,总不能空等着吧,多浪费资源啊。
- 工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。工作窃取的运行流程图如下:
为什么需要使用工作窃取算法呢?
- 假如我们现在需要完成一个比较大的任务,我们可以把这个大任务分割为若干个互不依赖的子任务,为了减少线程间的竞争,我们把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应。
- 比如A线程负责处理A队列里的任务,B线程负责处理B队列里的任务,但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行,也就是先做完任务的工作线程会从其他未完成任务的线程尾部依次获取任务去执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
- 工作窃取算法的优点是充分利用线程进行并行计算,充分利用CPU的资源,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。
在 Java 的 Fork/Join 框架中,使用两个类完成上述操作:
ForkJoinTask:我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务。 该类提供了在任务中执行 fork 和 join 的机制。通常情况下我们不需要直接继承ForkJoinTask 类,只需要继承它的子类,Fork/Join 框架提供了两个子类:
- RecursiveAction:用于递归没有返回结果的任务
- RecursiveTask:用于递归有返回结果的任务
• ForkJoinPool:ForkJoinTask 需要通过 ForkJoinPool 来执行
12.3 Fork 方法
12.4 join 方法
- Join 方法的主要作用是阻塞当前线程并等待获取结果。
12.4 入门案例
- 低:for循环 中:ForkJoin 高:Stream并行流
package com.gch.forkjoin;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
/**
Fork/Join分支合并框架
*/
public class ForkJoinDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建ForkJoin任务类-MyTask对象
MyTask myTask = new MyTask(0,100);
// 创建分支合并池ForkJoinPool对象
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 往分支合并池里提交任务
ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask);
// 获取最终合并之后的结果
Integer result = forkJoinTask.get();
// 结果输出
System.out.println("0-100的和:" + result);
// 关闭池对象
forkJoinPool.shutdown();
}
}
/**
ForkJoin任务类
*/
class MyTask extends RecursiveTask<Integer> {
/** 拆分的时候差值不能超过10,计算10以内的运算 */
private static final Integer VALUE = 10;
/** 拆分开始值 */
private int begin;
/** 拆分结束值 */
private int end;
/** 返回结果 */
private int result;
/**
创建有参构造
*/
public MyTask(int begin,int end){
this.begin = begin;
this.end = end;
}
/**
* 拆分和合并的过程
* @return
*/
@Override
protected Integer compute() {
// 判断相加的两个数值的差值是否大于10
if ((end - begin) <= 10) {
// 相加操作
for(int i = begin;i <= end;i++){
result += i;
}
} else {
// 进一步做拆分
// 1.获取中间值
int middle = (begin + end) / 2;
// 2.拆分左边
MyTask task01 = new MyTask(begin,middle);
// 3.拆分右边
MyTask task02 = new MyTask(middle + 1,end);
// 调用方法拆分
task01.fork();
task02.fork();
// 合并结果
result = task01.join() + task02.join();
}
return result;
}
}
Fork/Join框架使用有哪些要注意的地方?
-
如果任务拆解的很深,系统内的线程数量堆积,导致系统性能严重下降;
如果函数的调用栈很深,会导致栈内存溢出(StackOverflowMemoryError
12.5 Stream并行流解决:
package com.gch.stream;
import java.util.stream.IntStream;
/**
Stream并行流:充分利用多核cpu
*/
public class StreamParallel {
public static void main(String[] args) {
// .rangeClosed(int a,int b):闭区间,包前也包后 range:范围,区间
// .range(int a,int b):开区间,包前不包后
// .parallel():将流变为并行流
int sum = IntStream.rangeClosed(1,100).parallel().sum();
System.out.println("Sum = " + sum);
}
}
13. CompletableFuture异步回调
13.1 CompletableFuture 简介
- CompletableFuture 在 Java 里面被用于异步编程,异步通常意味着非阻塞,可以使得我们的任务单独运行在与主线程分离的其他线程中,并且通过回调可以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息。
- CompletableFuture 实现了 Future, CompletionStage 接口,实现了 Future接口就可以兼容现在有线程池框架,而 CompletionStage 接口才是异步编程的接口抽象,里面定义多种异步方法,通过这两者集合,从而打造出了强大的CompletableFuture 类。
13.2 Future 与 CompletableFuture
- Futrue 在 Java 里面,通常用来表示一个异步任务的引用,比如我们将任务提交到线程池里面,然后我们会得到一个 Futrue,在 Future 里面有 isDone 方法来判断任务是否处理结束,还有 get 方法可以一直阻塞直到任务结束然后获取结果,但整体来说这种方式,还是同步的,因为需要客户端不断阻塞等待或者不断轮询才能知道任务是否完成。
-
当我们需要调用一个函数方法时。如果这个函数执行很慢 , 那么我们就要进行等待。但有时候 , 我们可能并不急着要结果。 因此, 我们可以 让被调用者立即返回,让他在后台慢慢处理这个请求 。对于调用者来说 , 则可以先处理一些其他任务, 在真正需要数据的场合再去尝试获取需要的数据。
- 阻塞:需要一些时间才能拿到结果。
- 异步调用:Ajax
创建 CompletableFuture
常见的创建 CompletableFuture 对象的方法如下:
- 通过 new 关键字。
- 基于 CompletableFuture 自带的静态工厂方法:runAsync()、supplyAsync() 。
静态工厂方法
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier);
// 使用自定义线程池(推荐)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor);
static CompletableFuture<Void> runAsync(Runnable runnable);
// 使用自定义线程池(推荐)
static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);
- runAsync() 方法接受的参数是 Runnable ,这是一个函数式接口,不允许返回值。当你需要异步操作且不关心返回结果的时候可以使用 runAsync() 方法。
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
- supplyAsync() 方法接受的参数是 Supplier<U> ,这也是一个函数式接口,U 是返回结果值的类型。当你需要异步操作且关心返回结果的时候,可以使用 supplyAsync() 方法。
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
代码演示:没有返回值的异步调用 + 有返回值的异步调用
package com.gch.completablefuture;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
CompletableFuture异步回调
*/
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 异步调用 -> 没有返回值
CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(()->{
System.out.println(Thread.currentThread().getName() + " completableFuture1...");
});
completableFuture1.get();
// 异步调用 -> 有返回值
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName() + " completableFuture2...");
// 模拟异常
int i = 1 / 0;
return 1024;
});
completableFuture2.whenComplete((t,u)->{
// 第一个参数t代表方法的返回值
System.out.println("t : " + t);
// 第二个参数u代表方法运行过程中产生的异常信息,如果方法运行没有产生异常,则输出null
System.out.println("u : " + u);
}).get();
}
}
14. 四大函数式接口(必须掌握)
新时代的程序员:lambda表达式、链式编程、函数式接口、Stream流式计算
- 函数式接口:只有一个抽象方法的接口。 @FunctionalInterface
- 简化编程模型,在型版本的框架底层大量应用!
java.util.function,Java 内置核心四大函数式接口,可以使用Lambda表达式
14.1 函数型接口 - Function<T,R>
package com.gch.function;
import java.util.function.Function;
/**
函数型接口-Function<T,R> => 有一个输入,有一个输出
*/
public class FunctionDemo {
public static void main(String[] args) {
/** 匿名内部类 */
Function<String,String> function1 = new Function<>(){
/**
* 方法形参的输入 = 方法返回值的输出
* @param str the function argument
* @return
*/
@Override
public String apply(String str) {
return str;
}
};
// Lambda表达式简化
Function<String,String> function2 = (str) ->{
return str;
};
}
}
14.2 断定型接口-Predicate<T>
package com.gch.function;
import java.util.function.Predicate;
/**
断定型接口 - Predicate<T>:有一个输入参数,方法返回值类型为boolean
*/
public class PredicateDemo {
public static void main(String[] args) {
Predicate<String> predicate1 = new Predicate<>(){
/**
* 判断字符串是否为空
* @param str the input argument
* @return
*/
@Override
public boolean test(String str) {
return str.isEmpty();
}
};
// Lambda表达式简化
Predicate<String> predicate2 = (str)->{
return str.isEmpty();
};
}
}
14.3 消费型接口-Consumer<T>
package com.gch.function;
import java.util.function.Consumer;
/**
Consumer<T>-消费型接口
*/
public class ConsumerDemo {
public static void main(String[] args) {
Consumer<String> consumer1 = new Consumer<>(){
/**
* 只有一个输入参数,没有方法返回值,没有输出
* @param str the input argument
*/
@Override
public void accept(String str) {
System.out.println(str);
}
};
// Lambda表达式简化
Consumer<String> consumer2 = (str)->{
System.out.println(str);
};
}
}
14.4 供给型接口-Supplier<T>
package com.gch.function;
import java.util.function.Supplier;
/**
Supplier<T>-供给型接口
*/
public class SupplierDemo {
public static void main(String[] args) {
Supplier<String> supplier1 = new Supplier<>(){
/**
* 没有输入参数,只有方法返回值
* @return
*/
@Override
public String get() {
return "Str...";
}
};
// Lambda表达式简化
Supplier<String> supplier2 = ()->{
return "Str..";
};
}
}
15. Stream流式计算
15.1 什么是Stream流式计算?
- 大数据时代:存储 + 计算
- 集合、MySQL本质就是存储东西的。
- 计算都应该交给流来操作。
15.2 Stream流的特点:
- Stream 自己不会存储元素。
- Stream 不会改变源对象,相反,他们会返回一个持有结果的新Stream。
- Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
15.3 代码演示
package com.gch.stream;
/**
定义实体类User
*/
public class User {
private int id;
private String userName;
private int age;
// 省略Setter,Getter,toString...
}
package com.gch.stream;
import java.util.Arrays;
import java.util.List;
/**
* Stream算法题:
* 题目要求:一分钟之内完成此题,只能用一行代码实现!
* 现在有五个用户,筛选:
* 1.ID必须是偶数
* 2.年龄必须大于23岁
* 3.用户名转为大写字母
* 4.用户名字母倒着排序
* 5.只输出一个用户!
*/
public class StreamDemo {
public static void main(String[] args) {
User u1 = new User(1,"a",21);
User u2 = new User(2,"b",22);
User u3 = new User(3,"c",23);
User u4 = new User(4,"d",24);
User u5 = new User(5,"e",25);
// 将数组转化为集合{用来存储的}
List<User> list = Arrays.asList(u1,u2,u3,u4,u5);
// 计算交给Stream流
// Lambda表达式、链式编程、函数式接口、Stream流式计算
list.stream().filter(s -> s.getId() % 2 == 0).filter(s -> s.getAge() > 23).map(s -> s.getUserName().toUpperCase())
.sorted((o1, o2) -> {
return o2.compareTo(o1);
}).limit(1).forEach(System.out::println);
}
}