石一歌的多线程进阶笔记
Java多线程进阶JUC
JUC
其实就是Java.Util.concurrent
包的缩写
回顾
线程的开启三种方式
- Thread 单继承会有
oop
问题 - Runnable 没有返回值、效率相比入 Callable 相对较低
- Callable 推荐
线程与进程
-
进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
-
线程
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
- Java 本身无法开启线程,需要调动本地方法
并发编程
-
并发
- 单核 ,模拟出来多条线程
-
并行
- 多核 ,多个线程可以同时执行; 线程池
-
查看电脑线程数
public class Test {
public static void main(String[] args) {
// 获取电脑的线程数,不是cpu的核心数
// CPU 密集型,IO密集型
System.out.println(Runtime.getRuntime().availableProcessors());
}
}
- 线程状态
public enum State {
// 新生
NEW,
// 运行
RUNNABLE,
// 阻塞
BLOCKED,
// 等待,死死地等
WAITING,
// 超时等待
TIMED_WAITING,
// 终止
TERMINATED;
}
-
wait 与 sleep 的区别
-
来源
//Object.wait() public final void wait() throws InterruptedException { wait(0); } //Thread.sleep() //注:实际开发,会使用TimeUnit.DAYS.sleep(1L),底层仍为Thread.sleep() public static native void sleep(long millis) throws InterruptedException;
-
锁的释放
-
wait() 会释放锁:wait 是进入线程等待池等待,出让系统资源,其他线程可以占用 CPU。
-
sleep() 不出让系统资源;
-
-
捕获异常
- 都需要捕获异常
-
使用范围
- wait() 需要在同步代码块、同步方法中使用
- sleep() 可以在任何地方使用
-
作用对象:
- wait() 定义在 Object 类中,作用于对象本身
- sleep() 定义在 Thread 类中,作用当前线程。
-
方法属性:
- wait() 是实例方法
- sleep() 是静态方法 有
static
-
锁
-
synchronized
- 同步方法
- 同步代码块
-
Lock
-
常用语句
Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }
-
ReentrantLock
可重入锁实现- 默认为非公平锁,可在构造函数选择
-
-
Synchronized 和 Lock 区别
详细区别,参照文末链接 **Java并发编程:Lock**
- Lock获取锁的其他方式
-
尝试非阻塞的获取锁
tryLock()
:当前线程尝试获取锁,如果该时刻锁没有被其他线程获取到,就能成功获取并持有锁 -
能被中断的获取锁
lockInterruptibly()
:获取到锁的线程能够响应中断,当获取到锁的线程被中断的时候,会抛出中断异常同时释放持有的锁 -
超时的获取锁
tryLock(long time, TimeUnit unit)
:在指定的截止时间获取锁,如果没有获取到锁返回 false
-
生产者消费者问题
synchronized 版本
- 单生产者单消费者
public class ProducerWithSynchronized {
public static void main(String[] args) {
SynchronizedData data = new SynchronizedData();
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();
}
}
//判断等待
//业务代码
//通知其他线程
//数字:资源类
public class SynchronizedData {
//属性
private int number = 0;
public synchronized void increment() throws InterruptedException {
while (number != 0) {
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "--" + number);
//加完了通知其他线程
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
while (number == 0) {
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "--" + number);
//减完了通知其他线程
this.notifyAll();
}
}
- 多生产者多消费者
-
出现虚假唤醒问题
- 一旦线程被唤醒,并得到锁,就不会再判断if条件,而执行if语句块外的代码,所以建议,凡是先要做条件判断,再wait的地方,都使用while循环来做
-
解决方法
- 将
if
判断换为while
,线程被再次唤醒后会继续判断条件
- 将
-
注意
- 单纯将
notifyAll()
换为notify()
,不再唤醒等待的多个线程,而是随机唤醒单个线程,不会解决虚假唤醒问题。
- 单纯将
-
Lock版本
通过 Lock 找到 condition 来配合控制对线程的唤醒
public class ProducerWithLock {
public static void main(String[] args) {
LockData data = new LockData();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
public class LockData {
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
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();
}
}
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();
}
}
}
Lock精准唤醒版本
这里我用的例子还是刚才的生产者消费者,方便展示问题。
刚才解决虚假唤醒的时候说过不能直接将notifyAll()
换为notify()
,也就是说我们必须使用全部唤醒。但是转到Lock时分离出来的Condition可以使用signal()
来实现精准唤醒,不会有资源的浪费。
弹幕说notifyAll()
和单个Condition的signalAll()
也可以的。我只能说精准唤醒是为了提升性能,不是为了实现功能。
public class AwakeByCondition {
public static void main(String[] args) {
AwakeInOrderByCondition data = new AwakeInOrderByCondition();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
public class AwakeInOrderByCondition {
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
//等待
condition1.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "-->" + number);
//加完了通知其他线程
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number == 0) {
//等待
condition2.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "-->" + number);
//减完了通知其他线程
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
8 个代码加锁问题
通常在两个线程启动中加入主线程休眠语句,以保证先发线程抢到锁。
TimeUnit.SECONDS.sleep(1);
单实例双同步方法
public class Lock1 {
/**
* 标准情况下 是先sendEmail() 还是先callPhone()?
* 答案:sendEmail
* 解释:被 synchronized 修饰的方式,锁的对象是方法的调用者
* 所以说这里两个方法调用的对象是同一个,先调用的先执行!
*/
public static void main(String[] args) {
Phone1 phone = new Phone1();
//锁的存在
new Thread(() -> {
phone.sendSms();
}, "A").start();
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone1 {
public synchronized void sendSms() {
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
资源类休眠
public class Lock2 {
/**
* sendEmail()休眠三秒后 是先执行sendEmail() 还是 callPhone()
* 答案: sendEmail
* 解释:被 synchronized 修饰的方式,锁的对象是方法的调用者
* 所以说这里两个方法调用的对象是同一个,先调用的先执行!
*/
public static void main(String[] args) {
Phone2 phone = new Phone2();
//锁的存在
new Thread(() -> {
phone.sendSms();
}, "A").start();
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone2 {
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
单实例普通方法混同步方法
public class Lock3 {
/**
* 被synchronized 修饰的方式和普通方法 先执行sendEmail() 还是 callPhone()
* 答案: callPhone
* 解释:新增加的这个方法没有 synchronized 修饰,不是同步方法,不受锁的影响!
*/
public static void main(String[] args) {
Phone3 phone = new Phone3();
//锁的存在
new Thread(() -> {
phone.sendSms();
}, "A").start();
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone3 {
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public void call() {
System.out.println("打电话");
}
}
双实例双同步方法
public class Lock4 {
/**
* 被synchronized 修饰的不同方法 先执行sendEmail() 还是callPhone()?
* 答案:callPhone
* 解释:被synchronized 修饰的不同方法 锁的对象是调用者
* 这里锁的是两个不同的调用者,所有互不影响
*/
public static void main(String[] args) {
Phone4 phoneA = new Phone4();
Phone4 phoneB = new Phone4();
//锁的存在
new Thread(() -> {
phoneA.sendSms();
}, "A").start();
new Thread(() -> {
phoneB.call();
}, "B").start();
}
}
class Phone4 {
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
单实例双静态方法
public class Lock5 {
/**
* 两个静态同步方法 都被synchronized 修饰 是先sendEmail() 还是callPhone()?
* 答案:sendEmail
* 解释:只要方法被 static 修饰,锁的对象就是 Class模板对象,这个则全局唯一!
*/
public static void main(String[] args) {
Phone5 phone = new Phone5();
//锁的存在
new Thread(() -> {
phone.sendSms();
}, "A").start();
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone5 {
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call() {
System.out.println("打电话");
}
}
单实例静态方法混同步方法
public class Lock6 {
/**
* 被synchronized 修饰的普通方法和静态方法 是先sendEmail() 还是 callPhone()?
* 答案:callPhone
* 解释:只要被static修饰锁的是class模板, 而synchronized 锁的是调用的对象
* 这里是两个锁互不影响,按时间先后执行
*/
public static void main(String[] args) {
Phone6 phone = new Phone6();
//锁的存在
new Thread(() -> {
Phone6.sendSms();
}, "A").start();
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone6 {
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
双实例双静态方法
public class Lock7 {
/**
* 两个静态同步方法 都被synchronized 修饰 是先sendEmail() 还是callPhone()?
* 答案:sendEmail
* 解释:只要方法被 static 修饰,锁的对象就是 Class模板对象,这个则全局唯一!
*/
public static void main(String[] args) {
Phone7 phoneA = new Phone7();
Phone7 phoneB = new Phone7();
//锁的存在
new Thread(() -> {
phoneA.sendSms();
}, "A").start();
new Thread(() -> {
phoneB.call();
}, "B").start();
}
}
class Phone7 {
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(7);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call() {
System.out.println("打电话");
}
}
双实例静态方法混同步方法
public class Lock8 {
/**
* 一个被static+synchronized 修饰的方法和普通的synchronized方法,先执行sendEmail()还是callPhone()?
* 答案:callPhone()
* 解释: 只要被static 修饰的锁的就是整个class模板
* 这里一个锁的是class模板 一个锁的是调用者
* 所以锁的是两个对象 互不影响
*/
public static void main(String[] args) {
Phone8 phoneA = new Phone8();
Phone8 phoneB = new Phone8();
//锁的存在
new Thread(() -> {
phoneA.sendSms();
}, "A").start();
new Thread(() -> {
phoneB.call();
}, "B").start();
}
}
class Phone8 {
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(8);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
总结
- 同步方法锁对象,可以被多实例绕开
- 静态同步方法锁类,无法被多实例绕开
- 两者混合,不是相同锁
- 不建议使用通过类实例访问静态成员,应该直接使用类访问静态成员
不安全的集合类
List
public class UnSafeList {
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+"("+Thread.currentThread().getName()+")");
}, String.valueOf(i)).start();
}
}
}
-
java.util.ConcurrentModificationException
异常不同线程同时操作了同一 list 索引元素抛出的异常。
-
解决方法
-
集合自带的线程安全的list
List<String list = new Vector<();
-
Collections工具类强行上锁
List<String list =Collections.synchronizedList(new ArrayList<());
-
用
JUC
包下的读写数组CopyOnWriteArrayList
,读写分离List<String list = new CopyOnWriteArrayList<();
-
-
CopyOnWriteArrayList
- 介绍
CopyOnWriteArrayList
,写数组的拷贝,支持高效率并发且是线程安全的, 读操作无锁的ArrayList
。所有可变操作都是通过对底层数组进行一次新的复制来实现。CopyOnWriteArrayList
,适合使用在读操作远远大于写操作的场景里,比如缓存。它不存在扩容的概念,每次写操作都要复制一个副本,在副本的基础上修改后改变 Array 引用。CopyOnWriteArrayList
中写操作需要大面积复制数组,所以性能差。CopyOnWriteArrayList
,慎用 ,因为谁也没法保证CopyOnWriteArrayList
到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,代价高昂。
- 缺点
- 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致
young gc
或者full gc
。- young gc :年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的
GC
机制清理掉(IBM 的研究表明,98% 的对象都是很快消亡的),这个GC
机制被称为Minor GC
或叫Young GC
。 - 年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次
Young GC
后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC
次数也比年轻代少。当年老代内存不足时,将执行Major GC
,也叫Full GC
- young gc :年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的
- 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的, 虽然
CopyOnWriteArrayList
能做到最终一致性, 但是还是没法满足实时性要求;
- 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致
- 总结
CopyOnWriteArrayList
这是一个ArrayList
的线程安全的变体,其原理大概可以通俗的理解为: 初始化的时候只有一个容器,很长一段时间,这个容器数据、数量等没有发生变化的时候,大家 (多个线程),都是读取(假设这段时间里只发生读取的操作) 同一个容器中的数据,所以这样大家读到的数据都是唯一、一致、安全的,但是后来有人往里面增加了一个数据,这个时候CopyOnWriteArrayList
底层实现添加的原理是先 copy 出一个容器(可以简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。
- 介绍
Set
public class UnSafeSet {
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+"("+Thread.currentThread().getName()+")");
}, String.valueOf(i)).start();
}
}
}
-
解决方法
-
用collections工具类强行上锁
Set<Object set = Collections.synchronizedSet(new HashSet<());
-
用
JUC
包下的读写SetCopyOnWriteArraySet
,底层为CopyOnWriteArrayList
Set<String set = new CopyOnWriteArraySet<();
-
Map
public class UnSafeMap {
public static void main(String[] args) {
// 默认等价于什么? new HashMap<>(16,0.75);
Map<String, Object> map = new HashMap<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 5));
System.out.println(map+"("+Thread.currentThread().getName()+")");
}, String.valueOf(i)).start();
}
}
}
-
解决方法
-
Hashtable
Hashtable<Object, Object map = new Hashtable<();
-
用
JUC
包下线程安全的ConcurrentHashMap
Map<String, Object map = new ConcurrentHashMap<();
-
Callable详解
比较推荐的一种线程创建方式。
多线程中提到的两种创建线程的方法。 一种是通过创建 Thread 类,另一种是通过使用 Runnable 创建线程。但是,Runnable 缺少的一项功能是,当线程终止时(即 run()完成时),我们无法使线程返回结果。为了支持此功能,Java 中提供了 Callable 接口。
为了实现Runnable,需要实现不返回任何内容的run()方法,而对于Callable,需要实现在完成时返回结果的call()方法。请注意,不能使用Callable创建线程,只能使用Runnable创建线程。
另一个区别是call()方法可以引发异常,而run()则不能。
为实现Callable而必须重写call方法。
- 线程池启动(前面介绍过)
public class CallableTest implements Callable<Boolean {
@Override
public Boolean call() throws Exception {
System.out.println(Thread.currentThread().getName() + "线程方法被调用");
return true;
}
public static void main(String[] args) {
CallableTest callable = new CallableTest();
//创建执行服务
ExecutorService service = Executors.newFixedThreadPool(1);
//提交执行
Future<Boolean result = service.submit(callable);
//获取结果
boolean isTrue = result.get();
//关闭服务
service.shutdownNow();
}
}
-
FutureTask
启动本质是借助
FutureTask
,包装Callable接口的实现类,然后传递给Thread线程执行 -
Futrue
接口 当call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可以知道该线程返回的结果。为此,可以使用Future对象。将Future视为保存结果的对象–它可能暂时不保存结果,但将来会保存(一旦Callable返回)。因此,Future基本上是主线程可以跟踪进度以及其他线程的结果的一种方式。要实现此接口,必须重写5种方法,但是由于下面的示例使用了库中的具体实现,因此这里仅列出了重要的方法。
-
public boolean cancel(boolean mayInterrupt):用于停止任务。如果尚未启动,它将停止任务。如果已启动,则仅在mayInterrupt为true时才会中断任务。
-
public Object get()抛出InterruptedException,ExecutionException:用于获取任务的结果。如果任务完成,它将立即返回结果,否则将等待任务完成,然后返回结果。
-
public boolean isDone():如果任务完成,则返回true,否则返回false
可以看到Callable和Future做两件事:Callable与Runnable类似,因为它封装了要在另一个线程上运行的任务;而Future用于存储从另一个线程获得的结果。实际上,future也可以与Runnable一起使用。
要创建线程,需要Runnable。为了获得结果,需要future。 Java库具有具体的
FutureTask
类型,该类型实现Runnable和Future,并方便地将两种功能组合在一起。
可以通过为其构造函数提供Callable来创建FutureTask
。然后,将FutureTask
对象提供给Thread的构造函数以创建Thread对象。因此,间接地使用Callable创建线程。 -
-
关系图
-
实现
public class CallableTest implements Callable<Boolean {
@Override
public Boolean call() throws Exception {
System.out.println(Thread.currentThread().getName() + "线程方法被调用");
return true;
}
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//直接包装
new Thread(new FutureTask(new CallableTest())).start();
//lambda表达式
new Thread(new FutureTask<Boolean>(()->{
System.out.println(Thread.currentThread().getName() + "线程方法被调用");
return true;
})).start();
}
}
JUC
常用辅助类
CountDownLatch
见名知意:倒计时锁存器
,阻塞主线程
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch downLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " Go out");
downLatch.countDown();//数量减一
}, String.valueOf(i)
).start();
}
downLatch.await();//等待
System.out.println("Finish");
}
}
CyclicBarrier
见名知意:循环障碍,加法计时器,阻塞所有线程
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("召唤神龙成功");
});
for (int i = 1; i <= 7; i++) {
/**
* 匿名内部类访问局部变量需要加final,lambda同理
*/
final int temp = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "收集到第" + temp + "个龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
Semaphore
见名知意:信号量,可近似看作资源池。
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 得到资源
System.out.println(Thread.currentThread().getName() + "抢到车位👍");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "离开车位😀");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放资源
}
}, String.valueOf(i) + "号车-->").start();
}
}
}
读写锁
ReadWriteLock 接口
所有已知实现类:
ReentrantReadWriteLock
读:可多条线程同时获取数据 共享锁 / 读锁
写:只能单条线程写入 独占锁 / 排它锁 / 写锁
实现目标 读读共享 读写互斥 写写互斥
public class ReadWriteLockDemo {
public static void main(String[] args) {
// MyCache myCache = new MyCache();
MyCacheWithLock myCache =new MyCacheWithLock();
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() -> {
myCache.write(String.valueOf(temp), temp);
}, "线程" + i).start();
}
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() -> {
myCache.read(String.valueOf(temp));
}, "线程" + i).start();
}
}
}
public class MyCacheWithLock {
/**
* 实现目标 读读共享 读写互斥 写写互斥
*/
private volatile Map<String, Object> map = new HashMap<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
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() + "写入OK");
} 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() + "读取OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
阻塞队列
Java Queue接口扩展了Collection接口。Collection接口 externs Iterable接口。
子接口:
BlockingQueue
,Deque
,BlobkingDequeue
一些最常用的Queue实现类是
LinkedList
,ArrayBlickingQueue
,LinkedBlockingQueue
,PriorityQueue
,PriorityBlockingQueue
。
对线程并发处理,线程池,常常用到阻塞队列
- API 1
@Test
public void gcore(){
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")); //异常:IllegalStateException: Queue full
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());
}
- API 2
@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(blockingQueue.poll()); //a
System.out.println(blockingQueue.poll()); //b
System.out.println(blockingQueue.poll()); //c
System.out.println(blockingQueue.poll()); //null
System.out.println(blockingQueue.peek());
}
- API 3
@Test
public void test3(){
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(blockingQueue.poll()); //a
System.out.println(blockingQueue.poll()); //b
System.out.println(blockingQueue.poll()); //c
System.out.println(blockingQueue.poll()); //null
System.out.println(blockingQueue.peek());
}
- API4
@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(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
同步队列
没有容量 == 进去一个元素,必须等待取出来之后,才能继续放一个元素
public class SynchronousQueueTest {
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();
}
}
线程池(※)
概念
-
池化技术
-
在系统开发过程中,我们经常会用到池化技术来减少系统消耗,提升系统性能。
简单点来说,就是提前保存大量的资源,以备不时之需,池化技术就是通过复用来提升性能。
-
-
常见池
- 对象池
- 通过复用对象来减少创建对象、垃圾回收的开销;
- 连接池
- (数据库连接池、Redis 连接池和 HTTP 连接池等)通过复用 TCP 连接来减少创建和释放连接的时间
线程池通过复用线程提升性能
- (数据库连接池、Redis 连接池和 HTTP 连接池等)通过复用 TCP 连接来减少创建和释放连接的时间
- 对象池
-
使用内存池的优点
-
降低资源消耗。这个优点可以从创建内存池的过程中看出,当我们在创建内存池的时候,分配的都是一块块比较规整的内存块,减少内存碎片的产生。
-
提高相应速度。这个可以从分配内存和释放内存的过程中看出。每次的分配和释放并不是去调用系统提供的函数或操作符去操作实际的内存,而是在复用内存池中的内存。
-
方便管理。
-
-
使用内存池的缺点
- 缺点就是很可能会造成内存的浪费,因为要使用内存池需要在一开始分配一大块闲置的内存,而这些内存不一定全部被用到。
三大方法
- 单线程线程池
public static ExecutorService newSingleThreadExecutor()
- 固定线程池
public static ExecutorService newFixedThreadPool(int nThreads)
- 伸缩线程池
public static ExecutorService newCachedThreadPool()
- 时间表线程池(扩展)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
-
实例
-
普通线程
public class MethodTest { 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(); } } }
-
时间线程
public class MethodTest { public static void main(String[] args) throws InterruptedException { ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3); System.out.println("提交时间: " + LocalTime.now()); //延迟3秒钟后执行任务 scheduledThreadPool.schedule(() - { System.out.println("运行时间1: " + LocalTime.now()); }, 3, TimeUnit.SECONDS); //延迟1秒钟后每隔3秒执行一次任务 scheduledThreadPool.scheduleAtFixedRate(() - { System.out.println("运行时间2: " + LocalTime.now()); }, 1, 3, TimeUnit.SECONDS); TimeUnit.SECONDS.sleep(15); scheduledThreadPool.shutdown(); } }
-
七大参数
- 源码分析
//创建单一线程的线程池
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>());
}
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;
}
- 手动创建线程池
public class ParameterTest {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy());
try {
// 最大承载:Deque + max 此处为:5+3=8
// 超过 RejectedExecutionException
for (int i = 1; i <= 9; i++) {
int finalI = i;
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " execute " + finalI + " ok");
});
}
} catch (
Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
四种拒绝策略
- 中止策略
功能:当触发拒绝策略时,直接抛出拒绝执行的异常,中止策略的意思也就是打断当前执行流程
使用场景:这个就没有特殊的场景了,但是一点要正确处理抛出的异常。
ThreadPoolExecutor
中默认的策略就是AbortPolicy
,ExecutorService
接口的系列ThreadPoolExecutor
因为都没有显示的设置拒绝策略,所以默认的都是这个。但是请注意,ExecutorService
中的线程池实例队列都是无界的,也就是说把内存撑爆了都不会触发拒绝策略。当自己自定义线程池实例时,使用这个策略一定要处理好触发策略时抛的异常,因为他会打断当前的执行流程。
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
- 调用者运行策略
功能:当触发拒绝策略时,只要线程池没有关闭,就由提交任务的当前线程处理。
使用场景:一般在不允许失败的、对性能要求不高、并发量较小的场景下使用,因为线程池一般情况下不会关闭,也就是提交的任务一定会被运行,但是由于是调用者线程自己执行的,当多次提交任务时,就会阻塞后续任务执行,性能和效率自然就慢了。
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
- 丢弃策略
功能:直接静悄悄的丢弃这个任务,不触发任何动作
使用场景:如果你提交的任务无关紧要,你就可以使用它 。因为它就是个空实现,会悄无声息的吞噬你的的任务。所以这个策略基本上不用了
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
- 弃老策略
功能:如果线程池未关闭,就弹出队列头部的元素,然后尝试执行
使用场景:这个策略还是会丢弃任务,丢弃时也是毫无声息,但是特点是丢弃的是老的未执行的任务,而且是待执行优先级较高的任务。基于这个特性,我能想到的场景就是,发布消息,和修改消息,当消息发布出去后,还未执行,此时更新的消息又来了,这个时候未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了。因为队列中还有可能存在消息版本更低的消息会排队执行,所以在真正处理消息的时候一定要做好消息的版本比较。
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
}
-
第三方拒绝策略
-
dubbo
中的线程拒绝策略public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy { protected static final Logger logger = LoggerFactory.getLogger(AbortPolicyWithReport.class); private final String threadName; private final URL url; private static volatile long lastPrintTime = 0; private static Semaphore guard = new Semaphore(1); public AbortPolicyWithReport(String threadName, URL url) { this.threadName = threadName; this.url = url; } @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { String msg = String.format("Thread pool is EXHAUSTED!" + " Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," + " Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!", threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(), e.getLargestPoolSize(), e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(), url.getProtocol(), url.getIp(), url.getPort()); logger.warn(msg); dumpJStack(); throw new RejectedExecutionException(msg); } private void dumpJStack() { //省略实现 } }
-
可以看到,当dubbo的工作线程触发了线程拒绝后,主要做了三个事情,原则就是尽量让使用者清楚触发线程拒绝策略的真实原因。
1)输出了一条警告级别的日志,日志内容为线程池的详细设置参数,以及线程池当前的状态,还有当前拒绝任务的一些详细信息。可以说,这条日志,使用dubbo的有过生产运维经验的或多或少是见过的,这个日志简直就是日志打印的典范,其他的日志打印的典范还有spring。得益于这么详细的日志,可以很容易定位到问题所在
2)输出当前线程堆栈详情,这个太有用了,当你通过上面的日志信息还不能定位问题时,案发现场的dump线程上下文信息就是你发现问题的救命稻草。
3)继续抛出拒绝执行异常,使本次任务失败,这个继承了JDK
默认拒绝策略的特性
-
Netty
中的线程池拒绝策略private static final class NewThreadRunsPolicy implements RejectedExecutionHandler { NewThreadRunsPolicy() { super(); } public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { try { final Thread t = new Thread(r, "Temporary task executor"); t.start(); } catch (Throwable e) { throw new RejectedExecutionException( "Failed to start a new thread", e); } } }
Netty中的实现很像JDK中的CallerRunsPolicy,舍不得丢弃任务。不同的是,CallerRunsPolicy是直接在调用者线程执行的任务。而 Netty是新建了一个线程来处理的。
所以,Netty的实现相较于调用者执行策略的使用面就可以扩展到支持高效率高性能的场景了。但是也要注意一点,Netty的实现里,在创建线程时未做任何的判断约束,也就是说只要系统还有资源就会创建新的线程来处理,直到new不出新的线程了,才会抛创建线程失败的异常
-
activeMq
中的线程池拒绝策略new RejectedExecutionHandler() { @Override public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) { try { executor.getQueue().offer(r, 60, TimeUnit.SECONDS); } catch (InterruptedException e) { throw new RejectedExecutionException("Interrupted waiting for BrokerService.worker"); } throw new RejectedExecutionException("Timed Out while attempting to enqueue Task."); } });
activeMq
中的策略属于最大努力执行任务型,当触发拒绝策略时,在尝试一分钟的时间重新将任务塞进任务队列,当一分钟超时还没成功时,就抛出异常
-
pinpoint
中的线程池拒绝策略public class RejectedExecutionHandlerChain implements RejectedExecutionHandler { private final RejectedExecutionHandler[] handlerChain; public static RejectedExecutionHandler build(List<RejectedExecutionHandler> chain) { Objects.requireNonNull(chain, "handlerChain must not be null"); RejectedExecutionHandler[] handlerChain = chain.toArray(new RejectedExecutionHandler[0]); return new RejectedExecutionHandlerChain(handlerChain); } private RejectedExecutionHandlerChain(RejectedExecutionHandler[] handlerChain) { this.handlerChain = Objects.requireNonNull(handlerChain, "handlerChain must not be null"); } @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { for (RejectedExecutionHandler rejectedExecutionHandler : handlerChain) { rejectedExecutionHandler.rejectedExecution(r, executor); } } }
pinpoint的拒绝策略实现很有特点,和其他的实现都不同。他定义了一个拒绝策略链,包装了一个拒绝策略列表,当触发拒绝策略时,会将策略链中的rejectedExecution
依次执行一遍。
最大线程数
-
获取计算资源(支持的线程数)
System.out.println(Runtime.getRuntime().availableProcessors());
-
cpu
密集型- 计算型代码、Bitmap 转换、
Gson
转换 - N+1
- 计算型代码、Bitmap 转换、
-
io
密集型- 文件读写、DB 读写、网络请求
- N*2
ForkJoin
类似大数据中的
MapReduce
,分而治之的思想
-
特点
- 工作窃取
-
底层原理:双端队列,效率高的线程可以获取其他线程的任务执行。
-
使用方法
-
实例化
ForkJoinPool
-
计算类继承
ForkJoinTask
,并实现,作为任务参数 -
计算任务
invoke,submit,execute三个接口可以提交任务运行
-
invoke(ForkJoinTask task)
- 同步,阻塞主进程,有返回值,建议设置超时时间
-
execute(ForkJoinTask<? task)
- 异步,不阻塞主线程,无返回值
-
submit(ForkJoinTask task)
- 异步,get()方法会阻塞主进程,get()方法可以获取返回值和设置超时时间
-
-
-
实例
-
SumTask
(计算类)public class SumTask extends RecursiveTask<Long { private long star; private long end; private long temp = 1000000L; public SumTask(long star, long end) { this.star = star; this.end = end; } @Override protected Long compute() { if ((end - star) < temp) { long sum = 0L; for (long i = star; i < end; i++) { sum += i; } return sum; }else { // 计算平均值 long middle = (star + end) / 2; SumTask task1 = new SumTask(star, middle); SumTask task2 = new SumTask(middle, end); // 拆分任务,把线程压入线程队列 task1.fork(); task2.fork(); return task1.join() + task2.join(); } } }
-
execute
public class Test { private static final long MAX = 20_0000_0000; public static void main(String[] args) throws ExecutionException, InterruptedException { gcore(); } public static void gcore() throws ExecutionException, InterruptedException { long star = System.currentTimeMillis(); ForkJoinPool forkJoinPool = new ForkJoinPool(); forkJoinPool.execute(new SumTask(0L, MAX)); long end = System.currentTimeMillis(); System.out.println("时间:" + (end - star)); } }
-
invoke
public class Test { private static final long MAX = 20_0000_0000; public static void main(String[] args) throws ExecutionException, InterruptedException { test2(); } public static void test2() throws ExecutionException, InterruptedException { long star = System.currentTimeMillis(); ForkJoinPool forkJoinPool = new ForkJoinPool(); Long aLong = forkJoinPool.invoke(new SumTask(0L, MAX)); System.out.println(aLong); long end = System.currentTimeMillis(); System.out.println("时间:" + (end - star)); } }
-
submit
public class Test { private static final long MAX = 20_0000_0000; public static void main(String[] args) throws ExecutionException, InterruptedException { test3(); } public static void test3() throws ExecutionException, InterruptedException { long star = System.currentTimeMillis(); ForkJoinPool forkJoinPool = new ForkJoinPool(); ForkJoinTask<Long submit = forkJoinPool.submit(new SumTask(0L, MAX)); System.out.println(submit.get()); long end = System.currentTimeMillis(); System.out.println("时间:" + (end - star)); } }
-
stream(拓展)
public class Test { private static final long MAX = 20_0000_0000; public static void main(String[] args) throws ExecutionException, InterruptedException { test4(); } public static void test4() { long star = System.currentTimeMillis(); long sum = LongStream.range(0L, 20_0000_0000L).parallel().reduce(0, Long::sum); System.out.println(sum); long end = System.currentTimeMillis(); System.out.println("时间:" + (end - star)); } }
-
CompletableFuture
异步回调
类似Ajax,对未来事件结果的建模。
- 无返回值
public static void TestNoReturn() throws ExecutionException, InterruptedException {
System.out.println(LocalTime.now());
CompletableFuture<Void future = CompletableFuture.runAsync(() - {
System.out.println(Thread.currentThread().getName() + ".....");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(LocalTime.now());
System.out.println(future.get()); //回调
}
- 有返回值
public static void TestWithReturn() throws ExecutionException, InterruptedException {
CompletableFuture<Integer future = CompletableFuture.supplyAsync(() - {
System.out.println(Thread.currentThread().getName() + ".....");
try {
TimeUnit.SECONDS.sleep(2);
int i = 1 / 0;//制造异常
} catch (InterruptedException e) {
e.printStackTrace();
}
return 200;
});
System.out.println(future.whenComplete((t, u) - {
//success 回调
System.out.println("return=" + t); //正常的返回结果
System.out.println("error=" + u); //抛出异常的 错误信息
}).exceptionally((e) - {
//error回调
System.out.println(e.getMessage());
return 404;
}));
}
AQS
AQS全称叫做AbstractQueuedSynchronizer
是可以给我们实现锁的一个「框架」,内部实现的关键就是维护了一个先进先出的队列以及state状态变量
先进先出队列存储的载体叫做Node节点,该节点标识着当前的状态值、是独占还是共享模式以及它的前驱和后继节点等等信息简单理解就是:AQS定义了模板,具体实现由各个子类完成。
总体的流程可以总结为:会把需要等待的线程以Node的形式放到这个先进先出的队列上,state变量则表示为当前锁的状态。
像
ReentrantLock
、ReentrantReadWriteLock
、CountDownLatch
、Semaphore
这些常用的实现类都是基于AQS
实现的
AQS
支持两种模式:独占(锁只会被一个线程独占)和共享(多个线程可同时执行)
替代锁的过程
内存模型
- 问题一
- 详细
- CPU执行速度很快,而从内存读取数据和向内存写入数据的过程慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。
- 解决
- 引入高速缓存,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
- 详细
- 问题二
- 详细
- 著名的缓存一致性问题,即多线程时共享变量的缓存可能不一致。
- 解决
- 通过在总线加LOCK锁。
- 效率低下。
- 通过缓存一致性协议。
- 经典的
MESI
协议(Intel):MESI
协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
- 经典的
- 通过在总线加LOCK锁。
- 详细
原子性&可见性&有序性
- 原子性
一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
- 可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 有序性
程序执行的顺序按照代码的先后顺序执行。
Java内存模型(JMM
)
Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
-
三大特性(
JMM
)-
原子性
- 在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
- Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
-
可见性
- 对于可见性,Java提供了volatile关键字来保证可见性。
- 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
- 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
-
有序性
- 在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
- 在Java里面,可以通过volatile关键字来保证一定的“有序性”。
-
-
八种操作
- Read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用;
- load(载入):作用于工作内存的变量,它把 read 操作从主存中变量放入工作内存中;
- Use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
- assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;
- store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的 write 使用;
- write(写入):作用于主内存中的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中;
- lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态;
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
-
操作规定
- 不允许 read 和 load、store 和 write 操作之一单独出现。即使用了 read 必须 load,使用了 store 必须 write
- 不允许线程丢弃他最近的 assign 操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有 assign 的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施 use、store 操作之前,必须经过 assign 和 load 操作
- 一个变量同一时间只有一个线程能对其进行 lock。多次 lock 后,必须执行相同次数的 unlock 才能解锁
- 如果对一个变量进行 lock 操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新 load 或 assign 操作初始化变量的值
- 如果一个变量没有被 lock,就不能对其进行 unlock 操作。也不能 unlock 一个被其他线程锁住的变量
- 对一个变量进行 unlock 操作之前,必须把此变量同步回主内存
Volatile关键字
相当于轻量化的synchronized
-
特性
-
不保证原子性
public static void main(String[] args) { Atomicity(); } public static void add() { number++; } public static void Atomicity() { for (int i = 1; i <= 20; i++) { new Thread(() - { for (int j = 1; j <= 1000; j++) { add(); } }).start(); } while (Thread.activeCount() 2) { Thread.yield(); } System.out.println(Thread.currentThread().getName() + ",num=" + number); }
-
使用原子类解决(
CAS
)public static void main(String[] args) { Atomicity(); } public static void atomicAdd(){ atomicNumber.incrementAndGet(); } public static void Atomicity() { for (int i = 1; i <= 20; i++) { new Thread(() - { for (int j = 1; j <= 1000; j++) { atomicAdd(); } }).start(); } while (Thread.activeCount() 2) { Thread.yield(); } System.out.println(Thread.currentThread().getName() + ",num=" + atomicNumber); }
-
-
保证可见性
public static void main(String[] args) { Visibility(); } public static void Visibility() { new Thread(() - { while (number == 0) { } }).start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } number = 1; System.out.println(number); }
-
有序性
-
指令重排
源代码– 编译器优化重排– 指令并行也可能会重排– 内存系统也会重排– 执行
处理器在进行指令重排的时候,会考虑数据之间的依赖性
-
volatile 禁止指令重排
volatile 会加内存屏障,这个内存屏障可以保证在这个屏障中的指令顺序。
-
内存屏障
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
-
-
-
使用场景
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
- 原子类
乐观锁
之前为了减轻系统的负担,我们使用简易的
Volatile
,有一个很突出的问题,无法保证原子性,我们选择乐观锁解决这个问题。乐观锁有两个实现机制
CAS
机制(CAS
并不是自旋锁,自旋锁是锁升级中的一种状态。)版本号机制(详情参考
mybatis-plus
)
CAS
机制
CAS
是英文单词Compare and Swap的缩写,翻译过来就是比较并交换。
CAS
有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
-
非阻塞算法 (nonblocking algorithms)
一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
现代的CPU提供了特殊的指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而
compareAndSet()
就用这些代替了锁定。 -
实例
- 在 Java 的原子类中,有对
cas
的封装进行应用:此处用AtomicInteger
举例
- 在 Java 的原子类中,有对
public class Test {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
atomicInteger.getAndIncrement();
}
}
-
源码
-
AtomicInteger.getAndIncrement()
调用unsafe.getAndAddInt()
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
-
unsafe.getAndAddInt()
调用CAS
核心方法this.compareAndSwapInt()
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { ///获取内存值,这是内存值已经是旧的,假设我们称作期望值E var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); //var5是期望值,var5 + var4是要更新的值 //这个操作就是调用CAS的JNI,每个线程将自己内存里的内存值M //与var5期望值E作比较,如果相同将内存值M更新为var5 + var4,否则做自旋操作 return var5; }
-
-
缺点:
-
CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
-
不能保证代码块的原子性
CAS
机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。 -
ABA
问题
-
ABA
问题
如果另一个线程修改 V 值假设原来是 A,先修改成 B,再修改回成 A。当前线程的
CAS
操作无法分辨当前 V 值是否发生过变化。
- 问题
public class Test {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
atomicInteger.compareAndSet(2020, 2021);
atomicInteger.compareAndSet(2021, 2020);
System.out.println(atomicInteger.compareAndSet(2020, 6666));
System.out.println(atomicInteger.get());
}
}
-
解决
-
原子引用
一个
AtomicStampedReference
维护对象引用以及整数 “印记”,可以原子更新。
-
实现注意事项:此实现通过创建表示 “boxed”[引用,整数] 对的内部对象来维护加盖引用
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
public class Test {
public static void main(String[] args) {
AtomicStampedReference<Integer intAtomicStampedReference = new AtomicStampedReference<(1, 1);
new Thread(()-{
System.out.println("操作前stamp:A1=="+ intAtomicStampedReference.getStamp());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException ignored) {} intAtomicStampedReference.compareAndSet(1,2,intAtomicStampedReference.getStamp(),intAtomicStampedReference.getStamp()+1);
System.out.println("操作后stamp:A2=="+ intAtomicStampedReference.getStamp()); intAtomicStampedReference.compareAndSet(2,1,intAtomicStampedReference.getStamp(),intAtomicStampedReference.getStamp()+1);
System.out.println("回滚操作stamp:A3=="+ intAtomicStampedReference.getStamp());
System.out.println("假装是 x="+intAtomicStampedReference.getReference());
},"A").start();
new Thread(()-{
int stamp = intAtomicStampedReference.getStamp(); // 获得版本号
System.out.println("操作前stamp:B="+ stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException ignored) {}
System.out.println("操作是否成功:"+intAtomicStampedReference.compareAndSet(1, 6,
stamp, stamp + 1));
System.out.println("操作后stamp:b2="+intAtomicStampedReference.getStamp());
System.out.println("此时的值:"+intAtomicStampedReference.getReference());
},"B").start();
}
}
至此,替代锁的过程所有问题全部解决,我们的解决方案volatile
和CAS
,这也是实现JUC
的基石。
锁详解
悲观锁&乐观锁
锁的一种宏观分类方式是悲观锁和乐观锁。悲观锁与乐观锁并不是特指某个锁(Java中没有哪个Lock实现类就叫PessimisticLock
或OptimisticLock
),而是在并发情况下的两种不同策略。
悲观锁(Pessimistic Lock), 就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放。
乐观锁(Optimistic Lock), 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,不会上锁!但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作)。
悲观锁阻塞事务,乐观锁回滚重试,它们各有优缺点,不要认为一种一定好于另一种。像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
synchronized锁升级
偏向锁 → 轻量级锁 → 重量级锁
前面提到,synchronized关键字就像是汽车的自动档,现在详细讲这个过程。一脚油门踩下去,synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁,就像自动换挡一样。那么自旋锁在哪里呢?这里的轻量级锁就是一种自旋锁。
初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS
修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。
在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS
修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。
显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS
修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。在JDK1.6
之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。
一个锁只能按照 偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。
感谢评论区酷帅俊靓美的问题:
偏向锁的一个特性是,持有锁的线程在执行完同步代码块时不会释放锁。那么当第二个线程执行到这个synchronized代码块时是否一定会发生锁竞争然后升级为轻量级锁呢?
线程A第一次执行完同步代码块后,当线程B尝试获取锁的时候,发现是偏向锁,会判断线程A是否仍然存活。如果线程A仍然存活,将线程A暂停,此时偏向锁升级为轻量级锁,之后线程A继续执行,线程B自旋。但是如果判断结果是线程A不存在了,则线程B持有此偏向锁,锁不升级。
还有人对此有疑惑,我之前确实没有描述清楚,但如果要展开讲,涉及到太多新概念,可以新开一篇了。更何况有些太底层的东西,我没读过源码,没有自信说自己一定是对的。其实在升级为轻量级锁之前,虚拟机会让线程A尽快在安全点挂起,然后在它的栈中“伪造”一些信息,让线程A在被唤醒之后,认为自己一直持有的是轻量级锁。如果线程A之前正在同步代码块中,那么线程B自旋等待即可。如果线程A之前不在同步代码块中,它会在被唤醒后检查到这一情况并立即释放锁,让线程B可以拿到。这部分内容我之前也没有深入研究过,如果有说的不对的,请多多指教啊!
可重入锁(递归锁)
可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK
提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。如果你需要不可重入锁,只能自己去实现了。网上不可重入锁的实现真的很多,就不在这里贴代码了。99%的业务场景用可重入锁就可以了,剩下的1%是什么呢?我也不知道,谁可以在评论里告诉我?
公平锁&非公平锁
如果多个线程申请一把公平锁,那么当锁释放的时候,先申请的先得到,非常公平。显然如果是非公平锁,后申请的线程可能先获取到锁,是随机或者按照其他优先级排序的。
对于Lock实现类ReentrantLock
类而言,通过构造函数传参可以指定该锁是否是公平锁,默认是非公平锁。一般情况下,非公平锁的吞吐量比公平锁大,如果没有特殊要求,优先使用非公平锁。
对于synchronized而言,它也是一种非公平锁,但是并没有任何办法使其变成公平锁。
可中断锁
可中断锁,字面意思是“可以响应中断的锁”。
这里的关键是理解什么是中断。Java并没有提供任何直接中断某线程的方法,只提供了中断机制。何谓“中断机制”?线程A向线程B发出“请你停止运行”的请求(线程B也可以自己给自己发送此请求),但线程B并不会立刻停止运行,而是自行选择合适的时机以自己的方式响应中断,也可以直接忽略此中断。也就是说,Java的中断不能直接终止线程,而是需要被中断的线程自己决定怎么处理。这好比是父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。
回到锁的话题上来,如果线程A持有锁,线程B等待获取该锁。由于线程A持有锁的时间过长,线程B不想继续等待了,我们可以让线程B中断自己或者在别的线程里中断它,这种就是可中断锁。
在Java中,synchronized就是不可中断锁,而Lock的实现类都是可中断锁,可以简单看下Lock接口。
/* Lock接口 */
public interface Lock {
void lock(); // 拿不到锁就一直等,拿到马上返回。
void lockInterruptibly() throws InterruptedException; // 拿不到锁就一直等,如果等待时收到中断请求,则需要处理InterruptedException。
boolean tryLock(); // 无论拿不拿得到锁,都马上返回。拿到返回true,拿不到返回false。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 同上,可以自定义等待的时间。
void unlock();
Condition newCondition();
}
读写锁&共享锁&互斥锁
读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁)。看下Java里的ReadWriteLock
接口,它只规定了两个方法,一个返回读锁,一个返回写锁。
记得之前的乐观锁策略吗?所有线程随时都可以读,仅在写之前判断值有没有被更改。
读写锁其实做的事情是一样的,但是策略稍有不同。很多情况下,线程知道自己读取数据后,是否是为了更新它。那么何不在加锁的时候直接明确这一点呢?如果我读取值是为了更新它(SQL
的for update就是这个意思),那么加锁的时候就直接加写锁,我持有写锁的时候别的线程无论读还是写都需要等待;如果我读取数据仅为了前端展示,那么加锁时就明确地加一个读锁,其他线程如果也要加读锁,不需要等待,可以直接获取(读锁计数器+1)。
虽然读写锁感觉与乐观锁有点像,但是读写锁是悲观锁策略。因为读写锁并没有在更新前判断值有没有被修改过,而是在加锁前决定应该用读锁还是写锁。乐观锁特指无锁编程,如果仍有疑惑可以再回到第一、二小节,看一下什么是“乐观锁”。
JDK
提供的唯一一个ReadWriteLock
接口实现类是ReentrantReadWriteLock
。看名字就知道,它不仅提供了读写锁,而是都是可重入锁。 除了两个接口方法以外,ReentrantReadWriteLock
还提供了一些便于外界监控其内部工作状态的方法,这里就不一一展开。
总结
修改,之前认为偏向锁和轻量级锁是乐观锁,重量级锁和Lock实现类为悲观锁,网上很多资料对这些概念的表述也很模糊,各执一词。
结论:
我们在Java里使用的各种锁,几乎全都是悲观锁。synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。JDK
提供的Lock实现类全是悲观锁。其实只要有“锁对象”出现,那么就一定是悲观锁。因为乐观锁不是锁,而是一个在循环里尝试CAS
的算法。
那JDK
并发包里到底有没有乐观锁呢?
有。java.util.concurrent.atomic
包里面的原子类都是利用乐观锁实现的。
为什么网上有些资料认为偏向锁、轻量级锁是乐观锁?理由是它们底层用到了CAS
?或者是把“乐观/悲观”与“轻量/重量”搞混了?其实,线程在抢占这些锁的时候,确实是循环+CAS
的操作,感觉好像是乐观锁。但问题的关键是,我们说一个锁是悲观锁还是乐观锁,总是应该站在应用层,看它们是如何锁住应用数据的,而不是站在底层看抢占锁的过程。如果一个线程尝试获取锁时,发现已经被占用,它是否继续读取数据,等后续要更新时再决定要不要重试?对于偏向锁、轻量级锁来说,显然答案是否定的。无论是挂起还是忙等,对应用数据的读取操作都被“挡住”了。从这个角度看,它们确实是悲观锁。
参考链接
- 狂神说_JUC并发编程_1
- 狂神说_JUC并发编程_2
- Java并发编程:Lock
- ReentrantLock中lock(),tryLock(),lockInterruptibly()的区别
- Java线程的6种状态及切换(透彻讲解)
- 什么是Java虚假唤醒及如何避免虚假唤醒?《多线程学习之十四》
- notify丢失、虚假唤醒
- 简单理解Callable接口
- Callable接口及Futrue接口详解
- Java 线程池 8 大拒绝策略,面试必问!
- BlockingQueue(阻塞队列)详解
- 浅谈线程池之submit方法和execute方法
- ForkJoinPool invoke、execute和submit区别
- Java并发编程:volatile关键字解析
- JMM概述
- 单例模式
- 什么是CAS机制?
- 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现!