JUC
JUC是Java.util.concurrent工具包的简称,其是一个处理线程的工具包,在JDK1.5之后出现
我们的学习过程中需要引入下面的依赖,其中logback是log4j的一种具体实现
下面是logback.xml的具体配置
进程与线程
接着我们来看看进程和线程之间的区别
线程存在于进程之中,且线程比进程更加轻量,切换的成本也更低
并行与并发
在单核CPU的情况下,所有的线程都是串行执行的,其操作系统中的任务调度器会将cpu的时间片分给不同线程使用,通过不断的切换线程给人一种同时执行的感觉
通常,我们会将这种线程轮流使用CPU的做法称为并发,也就是concurrent
而并行指的是有在多核 cpu下,每 核(core) 都可以调度运行线程,这时候线程可以是并行的
下面是对并行和并发的描述和解释
异步回调
对应在我们的Java程序中有同步和异步的两种方法调用,需要等待结果返回的则是同步,反之则是异步
一般来说,我们推荐将一个需要较久时间处理的业务需求设置为异步
利用异步可以提高程序运行的效率
但是前提是CPU本身是多核的,如果是单核的情况下那么并不能提升效率,甚至能会降低效率
IO操作并不会占用cpu,但是IO分为阻塞IO,其会令线程一直等待直到IO结束,没能充分利用线程,因此后面也会有非阻塞IO和异步IO的优化
线程
创建有三种方法,第一种是直接使用Thread创建,重写其下的run方法即可
第二种方式是使用Runnable配合Thread,可以将线程和任务的代码分离开来,创建Runnable对象并重写run方法然后将该对象传入给Thread的构造方法中
当然,还可以使用lambda来精简我们的代码
实际上Thread是把Runnable对象进行进一步的包装,其内部执行线程会调用Runnable对象的run方法
第三种的方法是创建一个FutureTask对象,重写其下run方法,然后放入到线程中
FutureTask对象继承了Runnable对象和Future对象,可以返回结果,而Runnable对象则不能,其有获得返回的结果的方法,调用该方法时代码会阻塞在此直到该线程返回结果为止
多个线程同时运行时交替执行,且谁先谁后不受我们的控制
在不同的系统下,我们有对应的方法来查看进程和线程
我们还可以使用jconsole来远程监控我们的java类
JVM中有栈堆方法区,每个线程启动后会为其分配一个栈内存,栈由多个栈帧组成
cpu会因为某些原因转而执行另外的线程,此时会发生线程上下文切换(Thread Context Switch)
常见方法
下面是我们在线程中会使用的常见方法
run和start的方法区别在于前者不会开启新线程,后者会
sleep会让当前线程进入阻塞状态,同时我们推荐使用TimeUnit的sleep方法来获得更好的可读性
线程中有线程优先级的设计,优先级高的线程会让调度器优先调用,不会在CPU空闲时几乎没用
sleep、wait、join都会令线程进入阻塞状态,而使用interrupt可以打算该状态
如果对正常运行的线程使用该方法,那么其不会打断当前的状态,不过会将其打断标记设置为true
两阶段终止模式
两阶段中止模式指的是在一个线程T1中优雅终止线程T2,优雅指的是终止T2前给T2做对应的资源处理的操作,比方说T2中可能占有了一些共享锁,如果此时不先对这些锁进行释放就结束该线程,那么就没有线程可以获得该锁了
实现两阶段终止模式的思路是设置两个线程,监控线程每次监控有无打断,若打断则料理后事,反之则睡眠一段时间并执行监控记录,期间若无异常则进行监控,反之则设置打断标记
下面是其代码,这里值得一提的是如果在睡眠过程中发生了线程的打断此时,会抛出异常,这时打断操作将无法正常将打断标记设置为true,因此我们在catch中需要重新进行一次打断操作
LockSupport.park()方法会将线程挂起,不再执行线程下面的代码,可以使用interrupt()方法打断线程的park状态,该操作会将打断标记设置为true,而当打断标记为true时,park方法将不再有效
我们可以使用interrupted()方法来清空打断状态
下面是一些不推荐的方法
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守 护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束,设置一个线程为守护线程的方法是setDaemon()
线程状态
线程的状态有五种状态和六种状态两种区分方式,五种状态从操作层面来描述线程的状态的
运行状态指的是获取到了CPU的时间片,正在运行的状态。阻塞状态指的是调用了阻塞API线程进入阻塞的状态,阻塞状态的转换同样通过API,会转换为可运行状态
六种状态是从 Java API 层面来描述的,也就是Thread.State中的六种状态
五种状态中的运行状态、阻塞状态和可运行状态在这里都是RUNNABLE状态,这里的BLOCKED阻塞状态指的是线程无法获取到锁的状态,而之前的阻塞状态指的是线程因为调用了对应的API而不需要使用CPU时间片的状态
WAITING指的是线程进入等待的状态,调用了线程的JOIN的方法会进入该状态,而TIME_WAITING指的是线程等待指定的时间,调用sleep会进入该状态
TERMINATED指的是线程结束了的状态,线程代码运行完后进入该状态
最后我们来看看本章小结
共享问题
先来看看下面的案例
按理说结果应该为0,但实际得到的结果却并不为0,之所以会这样是因为在JVM中,字节码对i++和i--的字节码指令如下
而当线程1中准备存入结果时发生了上下文切换时,最后的结果不会是两者相加的结果,而是线程1的结果,因为最终是线程1将自己的计算结果存入到内存中
同理也会有线程2的结果的情况,正是因为如此所以会导致最终结果的不符合预期
如果一段代码存在多个线程对共享资源的多线程读写操作,那么该代码块就称为临界区
如果多个线程在临界区内执行,导致最终执行的结果无法预测,此时称该临界区发生了竞态条件
synchronized
为了避免竞态条件发生,我们可以使用synchronized
那么我们可以将代码改造如下
synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断,synchronized关键字会令给对应代码块上锁,使得只有对应线程执行完任务之后才能令另外的线程进入并执行
下面是具体的举例说明
下面三种情况里,后两个都无法正确给代码块进行保护
当然,我们可以对该代码进行改良,将需要保护的变量都放入一个类中,以后只需要new出该对象并调用对应的方法即可,就不用自己再加对应的synchronized关键字了,因为其本身已经实现了的
放到方法中的synchronized关键字相当于是锁住了整个对象
不加synchronized的方法不需要获取锁,其不会遵守对应的锁规则,会直接运行其代码
synchronized修饰的方法中如果加入了static关键字,则其锁住的是类对象。锁住类对象时,如果两个方法一个是类锁的方法,一个是锁住实例的方法,那么这两个方法由于锁住的对象并不是同一个对象因此不会产生排斥的情况
简而言之我们看synchronized修饰的方法会不会产生排斥只要看两个synchronized锁住的对象是不是同一个对象即可
线程安全分析
成员变量和静态变量如何没有共享或者被共享了却只有读操作都是线程安全的,但一旦有读写操作,则为线程不安全
局部变量都是线程安全的,但是局部变量引用的对象则未必是线程安全的。如果其没有逃离方法的作用访问,则其是线程安全的,反之则是线程不安全的
比如下面的例子,由于局部变量引用了成员变量,此时在多个线程的调用下就可能产生角标问题
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
// { 临界区, 会产生竞态条件
method2();
method3();
// } 临界区
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
如果将list修改为局部变量,此时就不存在上述问题
还有一个问题,那就是在list为局部变量的情况下如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?
答案是不会有,因为如果list为局部变量,method方法必然要传入list,那么此时对list的修改必然是传入的list,此时不会产生线程安全问题
但如果在list为局部变量同时方法修饰符为public的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
public void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
此时其子类创建的线程中对list的移除操作里操作的list其实是父类的list,此时仍然会有线程不安全的问题
解决的方法也很简单,只需要将public改为private即可,这也侧面说明了修饰符是有保证线程安全的作用的,如果我们希望当前的对象或者方法不被子类调用而发生线程安全问题,就需要善用final和private关键字
常见的线程安全类有以下类,它们的每个方法都是线程安全的
它们的每个方法都是原子的,但是如果它们的方法组合并不是原子的,如果我们想要保证其方法的组合的原子性,则需要在外继续加上synchronized关键字组成代码块
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
注意HashMap并不是线程安全的,HashTable才是。如果一个对象会被其他的线程使用,那么就要考虑其线程安全性的问题。一般来说,如果一个被多线程共享的对象其下不存在任何的可修改成员属性,那么其就是线程安全的,其上层对其方法的调用也是线程全的
如果被多线程的共享的对象存在可修改的属性,那么其就是线程不安全的,但如果上层每次调用该对象时都new一个新对象,那么就可以解决线程不安全的问题,但是这样所花费的开销很大
解决这个问题的方式当然是将成员变量修改为局部变量,我们推荐将存在线程安全问题的对象的成员变量修改为局部变量
如果一个抽象类中存在对局部变量,而该局部变量调用到一个抽象方法中,那么仍然存在线程不安全问题,因为其子类可能会重写对应的方法并修改该变量可能导致不安全的发生,被称之为外星方法,这也是为什么JDK中的String类由final修饰
synchronized原理
在Java中,任何对象都有对象头,普通对象的对象头一共是64比特大小,分别由标记单词和指向文件类型的指针组成,都占32位比特
而数组对象则在此基础上多了一个array length数据,同样是32位比特
64位虚拟机下Mark Word的结构如下,左边内容下的左边为标记名,右边为大小,右边内容代表左边内容的数据存储的状态
比如Normal状态下左边内容分别是hash值,分代变量,偏向锁和加锁状态
Monitor被翻译为监视器或管程,每一个Java对象都可以关联一个管程对象,如果使用synchronized给对象上了重量级锁之后,该对象头的Mark Word就会指向Monitor对象
Monitor中有三个属性,分别是Owner、EntryList和WaitSet,当一个线程进入到synchronized中时,其会将给线程设置到Owner中,此时如果另外的线程想要进入,就会被设置到EntryList中进入阻塞状态等待,当目前的线程运行结束之后再执行
值得一提的是,在EntryList中的阻塞线程执行的顺序并不是公平的,而是依靠用户自己设定的规则来执行的
同时不加synchronized关键字的线程不会关联管程,也就不会遵从上述的规则
synchronized构造下面的代码
其内部的字节码如下,可以看到其进行了相应的操作来执行线程与管程之间的相关操作并采用死循环的方式来确保一定会释放锁
接着我们来讲synchronized中的各种锁,首先我们来看看下面的故事
首先是轻量级锁,如果一个对象有多线程访问,但是访问时间是错开的,那么可以使用轻量级锁来优化
轻量级锁创建时会在线程中的栈帧中包含一个锁结构的记录,值得一提的是轻量级锁在代码层面上是看不到的,其是JVM内部的操作,加锁时优先加轻量级锁,而后是重量级锁
该锁结构的记录会令Object reference指向锁对象,并尝试用cas替换Object中的Mark Word,当然我们还没学过cas,我们可以简单理解为其就是将线程栈帧中的锁记录的结构也就是 lock record 的地址 00 与对象地址中的 Hashcode Age Bias 01进行交换
替换成功则表示该线程给该对象进行了加锁,此时Object中最后的数据为00
若失败则说明其他线程已经持有了该对象的轻量级锁,此时会进入锁碰撞过程,若是自己的线程再次加锁,则会执行锁重入,再次添加一条Lock Record在栈帧中,代表此时锁重入数+1
当解锁时如果有Hashcode Age Bias 01为null的锁记录,代表有重入,此时重置所记录,表示重入数-1
当不为null时将Mark Word的值返回给对象,若成功则解锁成功,失败则进入重量级解锁流程
如果在尝试加入轻量级锁的过程中,CAS操作无法成功,则说明其他线程已经占有了该对象的轻量级锁,此时需要进入锁膨胀,将轻量级锁变为重量级锁
加锁失败之后会将往对象绑定Monitor地址,同时将线程放入其下的EntryList队列中
重量级锁竞争时,还存在自旋优化,其指的是当前线程需要竞争锁时,不会立刻进入阻塞队列,而是会先进行自旋重试三次,期间拿到则执行同步代码块,拿不到再进入阻塞队列
这里值得一提的是自旋会占用CPU时间,因此只有多核CPU的自旋才能发挥优势,不然就是白搭
轻量级锁每次重入时都需要执行CAS操作,花费仍然较大,为了解决这个问题可以使用偏向锁,其会在第一次使用CAS时将线程ID设置到对象的Mark Word头中,之后线程只要判断这个id是否是自己的即可,若是则不发生竞争
可以看到上面的代码如果使用轻量级锁,每次都需要替换对应的markword,开销较大
但如果使用偏向锁则只需要判断线程id是否是自己的即可
偏向锁中存在偏向状态,当开启偏向锁后markword最后三位为101,且thread、epoch、age的值都为0。偏向锁也是默认延迟的,不会再程序启动时就立即生效
如果没有开启偏向锁,则markword的值最后三位为001且hashcode、age都为0
处于偏向锁的对象解锁之后,线程id仍然存储在对象头中,直到有新的线程对该对象加入了偏向锁为止
我们可以添加VM参数-XX:UseBiasedLocking来禁用偏向锁
调用对象的hashCode方法会令对象取出偏向锁,因此偏向锁存储的数据时没有空间再存储哈希值了,因此如果调用哈希值则会撤销偏向锁
当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
嗲用wait\notify方法也会导致偏向锁的撤销,因为这两个方法只有重量级锁才能调用
批量重定向指的是对象虽然被多个线程访问,但是其偏向T1线程的对象仍然有机会偏向T2,这里的偏向指的是对象中存储的线程id,也就是偏向锁,如果撤销偏向锁的次数超过20次之后,JVM就会将之后的所有对象加锁时重新偏向到目前需要加锁的线程
如果撤销次数到达了40,那么其会将该类的所有对象都变为不可偏向的,同时新建的对象也是不可偏向的,也就是移除所有的偏向锁
wait/notify
我们之所以需要wait,是因为我们有时候我们的线程在处理业务需求时需要其他的数据才能进行,但是此时我们的线程正占有锁,不能让他一直等待,此时我们可以调用其wait方法,令其暂时进入等待区等待同时释放其占有的锁给其他线程使用
进入等待室,也就是WaitSet的线程可以通过notify方法唤醒,但是唤醒之后其会进入阻塞列表中重新获得锁之后再执行自己的业务代码
wait和notify方法是每个对象都有的方法,但是我们必须要先获得该对象的锁才能调用这几个方法,也就是说我们必须令其在synchronized代码块中我们才可以调用该方法,不然会抛异常
sleep是Thread的方法,wait是Object的方法,后者需要配合synchronized一起使用,而前者不需要。前者不会释放对象锁,而后者会
我们推荐在需要线程获得更高效率的时候使用wait方法来实现线程的暂停,同时它们两者作用于线程后都会令线程进入TIMED_WAITING状态
对于需要唤醒的线程,如果有多个,我们可以每次唤醒时调用notifyAll方法,这样来唤醒所有等待的线程,这样就不会唤醒错误的线程。线程进入等待状态时推荐使用wait方法,线程等待所需数据的代码推荐使用while配合wait方法构建
保护性暂停
如果一个线程需要另一个线程传递过来的结果才能继续执行其业务,那么关联这两者的类就是GuardedObject
根据上面的内容我们可以写入其关联类如下,其中我们给其构建对应的方法
然后是测试该方法的类
如果我们还希望在这个关联类中加入等待超时的设定,那么我们就需要往get方法中传入对应的时间,然后while循环中我们再继续统计用时,如果同时超过指定时间我们就跳出循环,否则我们就继续让线程等待指定时间减去已知时间的差
上面我们所构建的关联类的逻辑就成为保护性暂停,而join方法的原理也是基于此
下面我们要做一个例子,这个例子里我们有居民线程来等待结果,同时有邮递员线程来送信,而信箱就是我们的中间类
下面是我们构造的代码
import lombok.extern.slf4j.Slf4j;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
@Slf4j(topic = "c.Test20")
public class Test20 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new People().start();
}
Sleeper.sleep(1);
for (Integer id : Mailboxes.getIds()) {
new Postman(id, "内容" + id).start();
}
}
}
@Slf4j(topic = "c.People")
class People extends Thread{
@Override
public void run() {
// 收信
GuardedObject guardedObject = Mailboxes.createGuardedObject();
log.debug("开始收信 id:{}", guardedObject.getId());
Object mail = guardedObject.get(5000);
log.debug("收到信 id:{}, 内容:{}", guardedObject.getId(), mail);
}
}
@Slf4j(topic = "c.Postman")
class Postman extends Thread {
private int id;
private String mail;
public Postman(int id, String mail) {
this.id = id;
this.mail = mail;
}
@Override
public void run() {
GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
log.debug("送信 id:{}, 内容:{}", id, mail);
guardedObject.complete(mail);
}
}
class Mailboxes {
private static Map<Integer, GuardedObject> boxes = new Hashtable<>();
private static int id = 1;
// 产生唯一 id
private static synchronized int generateId() {
return id++;
}
public static GuardedObject getGuardedObject(int id) {
return boxes.remove(id);
}
public static GuardedObject createGuardedObject() {
GuardedObject go = new GuardedObject(generateId());
boxes.put(go.getId(), go);
return go;
}
public static Set<Integer> getIds() {
return boxes.keySet();
}
}
// 增加超时效果
class GuardedObject {
// 标识 Guarded Object
private int id;
public GuardedObject(int id) {
this.id = id;
}
public int getId() {
return id;
}
// 结果
private Object response;
// 获取结果
// timeout 表示要等待多久 2000
public Object get(long timeout) {
synchronized (this) {
// 开始时间 15:00:00
long begin = System.currentTimeMillis();
// 经历的时间
long passedTime = 0;
while (response == null) {
// 这一轮循环应该等待的时间
long waitTime = timeout - passedTime;
// 经历的时间超过了最大等待时间时,退出循环
if (timeout - passedTime <= 0) {
break;
}
try {
this.wait(waitTime); // 虚假唤醒 15:00:01
} catch (InterruptedException e) {
e.printStackTrace();
}
// 求得经历时间
passedTime = System.currentTimeMillis() - begin; // 15:00:02 1s
}
return response;
}
}
// 产生结果
public void complete(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
this.notifyAll();
}
}
}
我们这里首先构造一个中间关联类,然后提供给其对应获得和存入结果的方法,接着我们创建一个邮箱类,邮箱类中提供id和中间关联类的,我们这里获取中间关联类时同时也需要将其对应的中间资源类释放,因此我们需要调用remove方法,具体的逻辑是创建对应的用户和邮递员类的线程,然后用户线程中创建对应的中间类并返回,然后通过中间类获取结果,邮递员则获取中间类并移除中间类然后通过中间类放入信件,我们这里同时需要id作为唯一标识,因此中间类和邮箱类中都有id属性
我们这里要注意我们这里每一个用户对应一个邮递员,如果我们需要做一个邮递员对应多个用户的案例,则需要构造生产者消费者模式
生产者消费者的模式是有多个消费者消费数据,而有一个生产者不断生产数据,JDK中的各种阻塞队列就是采用这种模式
一般来说此时我们推荐我们的消息队列的中间关联类中的属性需要是一个队列,而且我们的数据需要有唯一标识,提供take和put方法,先做好队列空与满的判断然后在测试方法中开启对应的线程即可
park方法可以令线程停止,而使用unpark方法可以唤醒线程,即使unpark在前park在后也一样能发挥效果
每个线程都有自己的Parker对象,其由_counter _cond _mutex三部分组成,调用park方法会令线程停止,而嗲用unpark方法会令_counter计数+1,多次调用该方法也只会令该计数+1,同时调用park方法时若counter有值,则不会令线程停止
其图示过程如下,这里我们是调用park
然后是先调用unpark然后再调用park方法的过程
线程状态转换
线程的状态按线程API来分类就一共有六种状态,而这些状态是可以互相转换的
当线程调用start()方法时,线程的状态就是NEW转化到RUNNABLE
而当其调用sleep或者wait方法时,就从Runnble状态进入Waiting状态,调用notify、notifyall、interrupt方法时,线程会竞争锁,成功则进入Runnable状态,反之则进入Blocked状态
这里我们值得一提的是join方法会令调用该方法时的当前线程等待直到方法代表的该线程执行完毕之后再唤醒当前线程
锁
我们锁对象时我们推荐锁具体的小对象,而不要锁大对象,这样效率会更高,当然小对象可以自己去创建,如果要锁多个线程的小对象,必须保证其下的业务代码是互不相干的
不然轻则会导致效率降低,重则会导致死锁
下面是死锁的案例代码
检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁
死锁的经典案例就是哲学家就餐问题
其代码如下
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
//哲学家类
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
private void eat() {
log.debug("eating...");
Sleeper.sleep(1);
}
@Override
public void run() {
while (true) {
// 获得左手筷子
synchronized (left) {
// 获得右手筷子
synchronized (right) {
// 吃饭
eat();
}
// 放下右手筷子
}
// 放下左手筷子
}
}
//主方法
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}
最终这五个线程都会停止,因为五个线程每个线程都会正好拿到一个筷子,这样就会令五个人都无法执行自己的业务代码
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,比方说一个线程执行的业务是令一个值自减到0结束,而另一个是自增到100结束,那么这两个线程同时执行时将不会有停止的时候
饥饿指的是一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束的情况
ReentrantLock
ReentrantLock对比于synchronized,具有可中断、可以设置超时时间、可以设置公平锁、支持可重入和多个条件变量等优势
使用其时,需要先定义static ReentrantLock lock = new ReentrantLock(),然后调用该对象的lock方法代表进行上锁
ReentrantLock支持可重入,同一个线程可以对在上锁之后再次上锁
示例代码如下
public class Main {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
log.debug("execute method1");
method2();
} finally {
lock.unlock();
}
}
public static void method2() {
lock.lock();
try {
log.debug("execute method2");
method3();
} finally {
lock.unlock();
}
}
public static void method3() {
lock.lock();
try {
log.debug("execute method3");
} finally {
lock.unlock();
}
}
}
同时ReentrantLock也是可以打断的,在synchronized中,线程如果没有获取到锁,那么就会一直等待,但是在ReentrantLock中可以设置令其等待不到之后就不等了,示例代码如下
public class Main {
ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("等锁的过程中被打断");
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(1);
t1.interrupt();
log.debug("执行打断");
} finally {
lock.unlock();
}
}
}
可以看到我们这里加锁时调用lockInterruptibly方法,如果不是调用该方法加锁而是使用lock方法加锁,则后面无法使用interrupt方法进行打断,同时我们的锁一定要finally中释放出来
同时还支持锁超时,调用其tryLock()方法即可,内部有两个参数,第一个是数值,第二个是TimeUnit中指定的时间单位
public class Main {
ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
log.debug("获取等待 1s 后失败,返回");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(2);
} finally {
lock.unlock();
}
}
}
下面是我们用ReentrantLock来解决哲学家问题的代码
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
public class Main {
//主方法
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}
//哲学家类
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子
if (left.tryLock()) {
try {
// 尝试获得右手筷子
if (right.tryLock()) {
try {
eat();
} finally {
right.unlock();
}
}
} finally {
left.unlock();
}
}
}
}
private void eat() {
log.debug("eating...");
Sleeper.sleep(1);
}
}
ReentrantLock默认是使用不公平锁,也就是线程获得锁的顺序并不会根据进入等候室的时间先后来,而是谁能抢到就由谁获得,我们要将其改为公平锁只需要在创建ReentranLock对象是传入true参数即可,但是我们并不推荐将其改为公平锁,因为会降低并发数
synchronized中也有条件变量,就是我们讲原理的时候的waitSet休息室,当线程不满足条件无法执行业务需求时就进入waitSet休息室中等待。而ReentrantLock中支持多个条件变量,可以理解为其支持多个自定义的休息室,唤醒时也可以按休息室来唤醒
要自定义休息室,就需要使用ReentrantLock中的newCondition方法,其会返回一个Condition对象,这就是休息室对象
调用休息室的await方法会释放锁,令其进入conditionObject中等待,但是必须要先获得锁才能调用该方法,线程被唤醒后会重新竞争锁,竞争成功后从await中继续执行
下面是我们使用ReentrantLock重新改造外卖抽烟案例的代码
public class Main {
static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitbreakfastQueue = lock.newCondition();
static volatile boolean hasCigrette = false;
static volatile boolean hasBreakfast = false;
public static void main(String[] args) {
new Thread(() -> {
try {
lock.lock();
while (!hasCigrette) {
try {
waitCigaretteQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("等到了它的烟");
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
try {
lock.lock();
while (!hasBreakfast) {
try {
waitbreakfastQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("等到了它的早餐");
} finally {
lock.unlock();
}
}).start();
sleep(1);
sendBreakfast();
sleep(1);
sendCigarette();
}
private static void sendCigarette() {
lock.lock();
try {
log.debug("送烟来了");
hasCigrette = true;
waitCigaretteQueue.signal();
} finally {
lock.unlock();
}
}
private static void sendBreakfast() {
lock.lock();
try {
log.debug("送早餐来了");
hasBreakfast = true;
waitbreakfastQueue.signal();
} finally {
lock.unlock();
}
}
}
我们需要注意被volatile修饰的对象是线程共享的
顺序控制
如果我们需要实现两个线程按照一定的顺序来打印内容,比如说一定要令其ababab这样打印,此时就需要使用到线程的顺序控制
要实现我们的需求,我们可以写入我们的代码如下,设置一个锁对象和标记并用wait和notify方法来实现我们的需求
@Slf4j(topic = "c.Test25")
public class Test25 {
static final Object lock = new Object();
// 表示 t2 是否运行过
static boolean t2runned = false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock) {
while (!t2runned) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("1");
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (lock) {
log.debug("2");
t2runned = true;
lock.notify();
}
}, "t2");
t1.start();
t2.start();
}
}
当然,只是实现这样的需求的话使用park和unpark的方法也是可以实现的
@Slf4j(topic = "c.Test26")
public class Test26 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
LockSupport.park();
log.debug("1");
}, "t1");
t1.start();
new Thread(() -> {
log.debug("2");
LockSupport.unpark(t1);
},"t2").start();
}
}
如果我们要实现多线程的交替循环输出,那么需要新建一个标记对象,对象内存有等待标记和下一个标记并提供打印方法,方法获取当前的锁并判断当前标记和传入标记是否一直,若是则打印需要的值并更改标记并唤醒其他线程,反之则进入等待
@Slf4j(topic = "c.Test27")
public class Test27 {
public static void main(String[] args) {
WaitNotify wn = new WaitNotify(1, 5);
new Thread(() -> {
wn.print("a", 1, 2);
}).start();
new Thread(() -> {
wn.print("b", 2, 3);
}).start();
new Thread(() -> {
wn.print("c", 3, 1);
}).start();
}
}
/*
输出内容 等待标记 下一个标记
a 1 2
b 2 3
c 3 1
*/
class WaitNotify {
// 打印 a 1 2
public void print(String str, int waitFlag, int nextFlag) {
for (int i = 0; i < loopNumber; i++) {
synchronized (this) {
while(flag != waitFlag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(str);
flag = nextFlag;
this.notifyAll();
}
}
}
// 等待标记
private int flag; // 2
// 循环次数
private int loopNumber;
public WaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
}
当然,我们可以说使用使用ReentrantLock来实现该案例,我们这个案例就比较复杂,因为我们这里要考虑到我们代码的通用性。首先我们创建AwaitSingal2对象,继承ReentrantLock,其下有loopNumber值和Map<Thread, Condition[]>作为成员变量并提供构造方法,开启方法中我们创建和线程数同样多的Condition数组然后进行初始化,接着for循环将对应的Conditon对象放入到map对象中,接着开启线程,睡眠,锁住对象之后唤醒第一个对象,最后释放锁
打印方法中会根据当前的线程对象得到Condition数组,令第一个进入等待,打印字符之后会唤醒下一个
我们在主方法中传入我们想要按顺序打印的任意个线程并调用AwaitSingal2其下的print方法就可以通过其自动装配启动来顺序打印字符串
@Slf4j(topic = "c.Test28")
public class Test28 {
public static void main(String[] args) {
AwaitSignal2 as = new AwaitSignal2(3);
as.start(new Thread(() -> {
as.print("a");
}), new Thread(() -> {
as.print("b");
}), new Thread(() -> {
as.print("c");
}), new Thread(() -> {
as.print("d");
}));
}
}
@Slf4j(topic = "c.AwaitSignal")
class AwaitSignal2 extends ReentrantLock {
private Map<Thread, Condition[]> map = new HashMap<>();
public void start(Thread... threads) {
Condition[] temp = new Condition[threads.length];
for (int i = 0; i < threads.length; i++) {
temp[i] = this.newCondition();
}
for (int i = 0; i < threads.length; i++) {
Condition current = temp[i];
Condition next;
if (i == threads.length - 1) {
next = temp[0];
} else {
next = temp[i + 1];
}
map.put(threads[i], new Condition[]{current, next});
}
for (Thread thread : map.keySet()) {
thread.start();
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.lock();
try {
map.get(threads[0])[0].signal();
} finally {
this.unlock();
}
}
public void print(String str) {
for (int i = 0; i < loopNumber; i++) {
this.lock();
try {
Condition[] conditions = map.get(Thread.currentThread());
conditions[0].await();
log.debug(str);
conditions[1].signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.unlock();
}
}
}
// 循环次数
private int loopNumber;
public AwaitSignal2(int loopNumber) {
this.loopNumber = loopNumber;
}
}
当然我们也可以使用park方法来实现,不过这样构造的代码失去了通用性
@Slf4j(topic = "c.Test31")
public class Test31 {
static Thread t1;
static Thread t2;
static Thread t3;
public static void main(String[] args) {
ParkUnpark pu = new ParkUnpark(5);
t1 = new Thread(() -> {
pu.print("a", t2);
});
t2 = new Thread(() -> {
pu.print("b", t3);
});
t3 = new Thread(() -> {
pu.print("c", t1);
});
t1.start();
t2.start();
t3.start();
LockSupport.unpark(t1);
}
}
class ParkUnpark {
public void print(String str, Thread next) {
for (int i = 0; i < loopNumber; i++) {
LockSupport.park();
System.out.print(str);
LockSupport.unpark(next);
}
}
private int loopNumber;
public ParkUnpark(int loopNumber) {
this.loopNumber = loopNumber;
}
}
最后我来看看本章小结
Voliatile
JMM指的是Java Memory Model,其体现在原子性、可见性、有序性这三点上
如果我们启动如下的程序,那么t线程并不会如预期般停止,而是会继续运行
@Slf4j(topic = "c.Test31")
public class Test31 {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
sleep(1);
run = false; // 线程t不会如预想的停下来
}
}
而之所以会发生这种情况,是因为JVM中存在JIT编译器,其会认为run的属性值一直重复读取,那干脆将其放入到高速缓存中,这样可以提高效率
但是这样却造成了我们的程序失去了可见性,即使我们改变了主内存中的值,由于高速缓存中的值没有改变,此时我们的t线程就仍然不会停止
解决方法也非常简单,就是加入voliatile关键字,其代表的意思是易变的,被其修饰的变量是线程共享的,是必须到主存中获取的
volatile关键字只能保证可见性,但是不能保证其原子性,简单来说就是即使添加了volatile关键字,我们仍然不能就这样就解决之前的两个线程一个i++一个i--的例子,其只能保证每个线程看到的值都是最新值,但是无法解决指令交错的问题
synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低
JVM会在不影响正确性的前提下调整语句的执行顺序,我们称之为指令重排,其会影响多线程下的结果正确性
其作用是可以提高项目的吞吐量,虽然同一个线程的执行时间不变,但是可以在执行第一个线程的第二个步骤的时候同时执行第二个线程的第一个步骤
使用voliatile关键字可以避免指令重排,但是其只能让代码里被voliatile修饰的属性的上面的代码不会发生重排,往下的就不行
原理
其保证可见性的原理时在对voliatile修饰的变量赋值之前加入一个写屏障,该屏障会将该变量之前的所有对共享变量的改动都同步到主存中
在读时会加入一个读屏障,该屏障会将该变量之下的所有共享变量都读取主存中的最新数据
下面是线程交互的过程图
同时对于有序性的保证在于写屏障会保证指令重排序时不会讲写屏障之前的代码排在写屏障之后,而读屏障会保证重排序时不会将读屏障之后的代码排在读屏障之前
原理图
但是还是要记住,voliatile只能保证可见性和有序性,但是不能解决指令交错问题
双重检查锁
下面是著名的双重检查锁,这样的检查锁能够提高在多线程下的效率,如果有两个线程进入该代码,第一个线程进入synchronized中,第二个线程还在外面if语句中,此时正好第一个线程创建对象完成,第二个线程进入到同步代码块中,里面的if语句仍然会阻止该线程再次创建对象,同时第三个线程进入时会直接在第一个if语句中被阻断
但在多线程的环境下,由于指令重排,上面的代码是有问题的
那么这样的话就可能会导致一种情况,这种情况下t1线程先给INSTANCE赋值,第二个线程进入判断不为空直接返回该对象并使用,但是此时第二个线程还没有调用构造方法,此时就会报空指针异常,这种情况是我们完全不能接受的
要解决这个问题也非常简单,给这个单例对象加入voliatile关键字即可
最后我们提一嘴,synchronized并不是说可以保证有序性、原子性和可见性,它是可以保证如果其代码块内的属性完全只在其中使用,那么就不用担心有序性原子性和可见性的问题,并不是在synchronized代码块里的代码就不会发生指令重排,而我们的案例里INSTANCE对象在外面里面都被使用了,因此仍然会有原子性的问题
happens-before
其规定了共享变量的写操作对其他线程的读操作可见的一套规则总结,直接结论和代码即可
@Slf4j(topic = "c.Test31")
public class Test31 {
static int x;
static Object m = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized(m) {
x = 10;
}
},"t1").start();
new Thread(()->{
synchronized(m) {
System.out.println(x);
},"t2").start();
}
}
注意我们这里变量都是指成员变量或静态成员变量
习题
本节我们来学习一些案例的问题
下面的方法显然在多线程中会存在问题
下面我们将单例模式实现时存在的问题
- 加final是为了防止子类重写方法破坏其原有的规范实现
- 加入readResolve方法令其直接返回单例对象,这样序列化时发现有该方法会直接返回该方法对象
- 设置为私有可以避免其他人直接调用构造方法来创建对象,但是并不能防止反射创建新实例
- 这样初始化能保证其线程安全
- 提供饿汉式实现,可以创建时加入我们的自定义逻辑,这样做支持泛型创建
但如果使用枚举类,那么上面的问题全都不复存在,需要初始化逻辑可以在枚举类中创建构造方法
下面的第一个实现虽然可以,但是效率很低,我们推荐第二种双重检查锁的实现方式
下面也是我们推荐的懒汉式单例模式的经典实现,由于类的加载本身就是懒汉式的,因此内部的单例类没有被使用时就不会被创建,但是一旦调用了对应的方法时,就会创建该对象,不会有并发问题
CAS
有如下需求,需要保证 account.withdraw 取款方法的线程安全
我们能够想到的简单的解决方法当然是在withdraw的实现方法中加入synchronized关键字,但是加锁必然会导致效率的降低
为了解决这个问题,我们可以使用CAS思路来实现无锁解决多线程冲突问题
那么我们可以写入我们的代码如下,首先我们要将类属性更改为AtomicInteger,其是JDK提供给我们的没有synchronized修饰的对象
在减去金额的方法里我们首先获得当前的数额,然后得到减去后的数额,调用其compareAndSet()方法,如果该方法执行成功则跳出死循环,反之则继续执行
class AccountSafe implements Account {
private AtomicInteger balance;
public AccountSafe(Integer balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return balance.get();
}
@Override
public void withdraw(Integer amount) {
while (true) {
int prev = balance.get();
int next = prev - amount;
if (balance.compareAndSet(prev, next)) {
break;
}
}
// 可以简化为下面的方法
// balance.addAndGet(-1 * amount);
}
}
其原理是当我们的线程1执行时会将当前余额和目标余额传入并与当前的余额进行对比,如果当前余额已经被其他线程修改,则返回false并重新获取最新值再次执行,如果没有被修改则成功执行
AtomicInteger内部的compareAndSet内部的方法执行流程就是如此
在AtomicInteger对象中的最重要的属性数据是用了voliatile修饰的,这是为了保证该变量的可见性,CAS必须借助voliatile才能实现比较并交换的效果
无锁情况下即使重试失败线程也还在运行,不需要付出线程上下文切换的代价,但是必须是在多核CPU下CAS才有实现的价值,而且线程数推荐和CPU数一致
CAS是基于乐观锁的思想,而synchronized是基于悲观锁的思想,前者体现的是无锁和无阻塞并发的思想
JUC并发还提供了其他的原子基本属性对象和对应的方法
原子引用指的是可以保护我们的自定义的引用对象类型的线程安全的类
下面是小数的数据的业务需求的不安全实现,我们这里执行业务需求时存在线程安全问题
class DecimalAccountUnsafe implements DecimalAccount {
BigDecimal balance;
public DecimalAccountUnsafe(BigDecimal balance) {
this.balance = balance;
}
@Override
public BigDecimal getBalance() {
return balance;
}
@Override
public void withdraw(BigDecimal amount) {
BigDecimal balance = this.getBalance();
this.balance = balance.subtract(amount);
}
}
下面是使用原子对象AtomicReference修饰该类的BigDecimal属性,这样就相当于是将其保护起来,此时再用其执行对应的需求即可
同样在执行业务需求的代码上构建while循环并调用compareAndSet方法来解决线程安全问题
class DecimalAccountSafeCas implements DecimalAccount {
AtomicReference<BigDecimal> ref;
public DecimalAccountSafeCas(BigDecimal balance) {
ref = new AtomicReference<>(balance);
}
@Override public BigDecimal getBalance() {
return ref.get();
}
@Override
public void withdraw(BigDecimal amount) {
while (true) {
BigDecimal prev = ref.get();
BigDecimal next = prev.subtract(amount);
if (ref.compareAndSet(prev, next)) {
break;
}
}
}
}
然而原子对象提供的compareAndSet也有缺陷,那就是如果两个线程同时对一个数据进行了修改最终令其结果与未修改前的结果一样,此时该方法是不能判断出该属性已经被其他线程所修改了的,为了解决这个问题我们可以往我们的属性封装的对象中加入一个版本号,只要有线程对其进行了修改,我们就令版本号自增
此时我们就需要用到AtomicStampedReference对象,该对象封装对象时需要指定版本号,在对数据更新之后可以自定义版本号的更新方案,调用其compareAndSet方法时,需要指定之前的值和要改动的值,以及期待的版本号和成功更新之后版本号的更新方案
public class Main {
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 获取值 A
String prev = ref.getReference();
// 获取版本号
int stamp = ref.getStamp();
log.debug("版本 {}", stamp);
// 如果中间有其它线程干扰,发生了 ABA 现象
other();
sleep(1);
// 尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
}
private static void other() {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t1").start();
sleep(0.5);
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t2").start();
}
}
当然有的时候我们并不关心我们的对象更改了几次,我们只关系其是否更改过了,因此就有了对象AtomicMarkableReference
我们可以看到我们这里封装时还需要指定一开始的标记,调用其compareAndSet方法同样是指定修改之前和之后的对象并指定预期的标记和修改后的标记
@Slf4j
public class TestABAAtomicMarkableReference {
public static void main(String[] args) throws InterruptedException {
GarbageBag bag = new GarbageBag("装满了垃圾");
// 参数2 mark 可以看作一个标记,表示垃圾袋满了
AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
log.debug("主线程 start...");
GarbageBag prev = ref.getReference();
log.debug(prev.toString());
new Thread(() -> {
log.debug("打扫卫生的线程 start...");
bag.setDesc("空垃圾袋");
while (!ref.compareAndSet(bag, bag, true, false)) {}
log.debug(bag.toString());
}).start();
Thread.sleep(1000);
log.debug("主线程想换一只新垃圾袋?");
boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
log.debug("换了么?" + success);
log.debug(ref.getReference().toString());
}
}
然而,如果我们的数据本身是一个数组,那么我们上面的对象只能保证这个数组对象本身的地址不会发生多线程问题,但是却无法保证数组对象内部的具体的数组下标对应的对象发生多线程问题,因此有了下面的保证数组不发生问题的对象
我们来看看证明上面的论证正确性的代码,对应的传入对象有提供者、函数和消费者,并对提供的数组执行对应的业务,可以看到我们这里就是将其对应的下标的对象的值进行自增
@Slf4j
public class TestABAAtomicMarkableReference {
/**
参数1,提供数组、可以是线程不安全数组或线程安全数组
参数2,获取数组长度的方法
参数3,自增方法,回传 array, index
参数4,打印数组的方法
*/
// supplier 提供者 无中生有 ()->结果
// function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
// consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
private static <T> void demo(
Supplier<T> arraySupplier,
Function<T, Integer> lengthFun,
BiConsumer<T, Integer> putConsumer,
Consumer<T> printConsumer ) {
List<Thread> ts = new ArrayList<>();
T array = arraySupplier.get();
int length = lengthFun.apply(array);
for (int i = 0; i < length; i++) {
// 每个线程对数组作 10000 次操作
ts.add(new Thread(() -> {
for (int j = 0; j < 10000; j++) {
putConsumer.accept(array, j%length);
}
}));
}
ts.forEach(t -> t.start()); // 启动所有线程
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}); // 等所有线程结束
printConsumer.accept(array);
}
public static void main(String[] args) {
demo(
()->new int[10],
(array)->array.length,
(array, index) -> array[index]++,
array-> System.out.println(Arrays.toString(array))
);
demo(
()-> new AtomicIntegerArray(10),
(array) -> array.length(),
(array, index) -> array.getAndIncrement(index),
array -> System.out.println(array)
);
}
}
可以看到解决的方案很简单,直接new一个对应的AtomicIntegerArray对象数组即可
其也提供了字段更新器,让我们只针对某些引用类型的属性进行多线程的保护,但注意该属性必须配合voliatile关键字
创建该对象时需要指定需要保护的属性的类型,然后指定属性名即可,修改时首先需要传入被保护的类,然后是期待的目前属性的值然后是修改之后的值
public class Test5 {
private volatile int field;
public static void main(String[] args) {
AtomicIntegerFieldUpdater fieldUpdater =AtomicIntegerFieldUpdater.newUpdater(Test5.class, "field");
Test5 test5 = new Test5();
fieldUpdater.compareAndSet(test5, 0, 10);
// 修改成功 field = 10
System.out.println(test5.field);
// 修改成功 field = 20
fieldUpdater.compareAndSet(test5, 10, 20);
System.out.println(test5.field);
// 修改失败 field = 20
fieldUpdater.compareAndSet(test5, 10, 30);
System.out.println(test5.field);
}
}
LongAdder
AtomicLong 与 LongAdder都可以用于做Long类型的属性的多线程封装,但是后者的效率比前者高得多,因为后者是由大神专门制作用于Long类型数据在多线程中的自增的
其性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能
LongAdder属性有下面这些,其中cells是累加单元数组cell是具体的累加单元对象,base是存累加值的属性,cellsBusy是代表锁
cellsBusy锁用于创建或扩容时,其实现原理时锁住时令线程其他将其值改为1代表加锁,如果失败则进入死循环,反之则成功加锁,此时其他线程都在循环,只有加锁的线程没有,调用解锁方法是直接将值改为0即可,因为其他线程都在循环,能执行该方法的线程必然是有锁线程,当然要注意我们下面的代码只是演示,实际这个代码是在底层做的,我们实际的实践中千万别这样
累加单元Cell的源码如下,其下有被volatile修饰的value变量,其增加的方法也是使用新值和增加值来进行不断重试来新增的
为什么这里要特别划分出累加单元来进行计算呢?这就得从缓存说起了。一般来说我们的CPU核心下都有三级缓存,最后是内存
缓存的速度比内存要快得多,但是其为了保证数据的一致性,需要不定时地更新或者令某部分的数据失效
CPU核心一般都对应一个累加单元,一个Cell占据24个字节,一个缓存行都可以存下两个2cell,这样就导致两个CPU无论谁修改成功都会导致对方的缓存行失效,这样必定会大大降低效率
像上述中一个缓存行存放多个cell的情况我们称之为是伪共享
因此在Cell类上增加了Contended注解,其会在使用此注解的对象或字段上前后各增加128字节大小的padding,从而令一个Cell占用一个缓存行
add方法的流程是首先判断累加单元数组cells是否被创建,没有说明没发生过竞争此时进行累加,累加肯定会失败因为cells没创建,此时返回的结果进行取反进入到if语句中执行longAccumulate方法,其会初始化cells
若cells已经创建,但是当前线程对应的cell并没有创建,此时会直接进入longAccumulate,若已经创建之后累加失败也进入
public void add(long x) {
// as 为累加单元数组
// b 为基础值
// x 为累加值
Cell[] as; long b, v; int m; Cell a;
// 进入 if 的两个条件
// 1. as 有值, 表示已经发生过竞争, 进入 if
// 2. cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if
if ((as = cells) != null || !casBase(b = base, b + x)) {
// uncontended 表示 cell 没有竞争
boolean uncontended = true;
if (
// as 还没有创建
as == null || (m = as.length - 1) < 0 ||
// 当前线程对应的 cell 还没有
(a = as[getProbe() & m]) == null ||
// cas 给当前线程的 cell 累加失败 uncontended=false ( a 为当前线程的 cell )
!(uncontended = a.cas(v = a.value, v + x))
) {
// 进入 cell 数组创建、cell 创建的流程
longAccumulate(x, null, uncontended);
}
}
}
下面是该方法的执行流程
然后我们来看看longAccumulate方法的源码,其首先会判断cells是否为null,若是则且没有线程进行加锁且其对应的cell还没有创建,则此时进行加锁初始化长度为2的cells并填充一个cell到随机位置,若加锁失败说明已经有线程完成了创建,此时直接执行累加操作即可
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
int h;
// 当前线程还没有对应的 cell, 需要随机生成一个 h 值用来将当前线程绑定到 cell
if ((h = getProbe()) == 0) {
// 初始化 probe
ThreadLocalRandom.current();
// h 对应新的 probe 值, 用来对应 cell
h = getProbe();
wasUncontended = true;
}
// collide 为 true 表示需要扩容
boolean collide = false;
for (;;) {
Cell[] as; Cell a; int n; long v;
// 已经有了 cells
if ((as = cells) != null && (n = as.length) > 0) {
// 还没有 cell
if ((a = as[(n - 1) & h]) == null) {
// 为 cellsBusy 加锁, 创建 cell, cell 的初始累加值为 x
// 成功则 break, 否则继续 continue 循环
}
// 有竞争, 改变线程对应的 cell 来重试 cas
else if (!wasUncontended)
wasUncontended = true;
// cas 尝试累加, fn 配合 LongAccumulator 不为 null, 配合 LongAdder 为 null
else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
// 如果 cells 长度已经超过了最大长度, 或者已经扩容, 改变线程对应的 cell 来重试 cas
else if (n >= NCPU || cells != as)
collide = false;
// 确保 collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了
else if (!collide)
collide = true;
// 加锁
else if (cellsBusy == 0 && casCellsBusy()) {
// 加锁成功, 扩容
continue;
}
// 改变线程对应的 cell
h = advanceProbe(h);
}
// 还没有 cells, 尝试给 cellsBusy 加锁
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
// 加锁成功, 初始化 cells, 最开始长度为 2, 并填充一个 cell
// 成功则 break;
}
// 上两种情况失败, 尝试给 base 累加
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
}
}
上面的流程图如下
循环时若已经存在cell且线程对应的cell没创建,则进入加锁并创建cell,加锁成功且对应的槽位为空此时执行给cell赋予对应对象的操作,反之则继续循环
如果cells存在且cell也已经春节,此时会执行累加操作,若失败则检查数组长度是否超过CPU上限,若是则改变线程对应的cell,反之则进行加锁,加锁失败也改变线程对应的cell,成功则扩容
循环方案中as不为空的情况下无法解决的问题最终都是进入advanceProbe()方法,也就是改变当前线程对应的cell的方法
数组长度超过了CPU数或者是扩容了之后都会令collide的值最终为ture,这样能防止下次循环代码执行到扩容的位置
最后执行累加,若成功则退出循环
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
Cell[] as; Cell a; int n; long v;
if ((as = cells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == as) { // Expand table unless stale
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = advanceProbe(h);
}
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
最后是获取所有的累加单元的值的和的结果的sum方法,无非就是遍历累加而已
Unsafe
Unsafe对象提供了非常底层的操作内存和线程的方法,其不能直接调用,只能通过反射获得
Unsafe对象的属性中有存在其自身对象的属性,因此我们可以通过反射获得该属性,也就是上面的代码,别忘了要打破封装,由于其是静态变量是与类相关联的,直接传入null即可获得对应的属性
注意该类叫Unsafe不是说其是线程不安全的类,其意思是程序员由于该类比较底层,程序员调用该类时容易发生安全问题
当然我们也可以使用Unsafe来实现CAS操作,首先需要获得Unsafe对象,获得的方法就是我们上面讲过的反射,然后通过反射获得成员变量的属性,调用unsafe对象中的objectFieldOffset方法并传入对应的属性名就可以获得其偏移量,调用其compareAndSwapxxx方法可以替换成员变量的值,int和long用于替换对应的基本类型,其他引用类型均使用Object方法进行替换
其需要传入四个参数,第一个是要修改的属性的所在对象,第二个是该属性的偏移量,第三个是属性的当前值,第四个是属性修改后的值
接着我们用Unsafe来实现之前线程安全的原子整数 Account 实现,静态代码块中获得unsafe对象,自减方法中我们用while搭配unsafe的compareAndSwapxxx方法来实现循环尝试
class AtomicData {
private volatile int data;
static final Unsafe unsafe;
static final long DATA_OFFSET;
static {
unsafe = UnsafeAccessor.getUnsafe();
try {
// data 属性在 DataContainer 对象中的偏移量,用于 Unsafe 直接访问该属性
DATA_OFFSET = unsafe.objectFieldOffset(AtomicData.class.getDeclaredField("data"));
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
public AtomicData(int data) {
this.data = data;
}
public void decrease(int amount) {
int oldValue;
while(true) {
// 获取共享变量旧值,可以在这一行加入断点,修改 data 调试来加深理解
oldValue = data;
// cas 尝试修改 data 为 旧值 + amount,如果期间旧值被别的线程改了,返回 false
if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue - amount)) {
return;
}
}
}
public int getData() {
return data;
}
}
这样我们的用Unsafe来实现之前线程安全的原子整数 Account 实现案例也写好了
不可变类
不可变类是线程安全的,而可变类则不是,比如JDK8提供的不可变日期类DateTimeFormatter
String类的设计就是不可变的
该类和类中所有属性都被final修饰,保证其子类不能破坏其不可变性
同时其还有保护性拷贝这一特性,简而言之就是任何与字符串相关的修改方法本身都不是对字符串的改造,而是将新创建一个字符串并将该字符串作为类中的属性,这样调用其任何方法返回或者是创建的对象都是新对象而不是对原来对象的改造,这样就保证了其原对象的不可变性
没有任何成员变量的类是线程安全的,同时其可以称之为无状态
BigDecimal也是不可变类,但为什么前面的例子里我们还需要调用对应的对象对其进行封装保护呢?这是因为虽然每个方法都是线程安全的,但是我的业务需求却是方法的结合,此时不能保证其操作的线程安全,因此我们需要另外对其进行封装保护
享元模式是23种设计模式中的其中一种,其代表的设计就是各种常量池或者是缓存池
CAS锁适用于占用锁或业务时间较短的业务,若较长因为线程不断循环导致CPU时间片被无效占用反而导致效率降低
线程池
接着我们来做一个自定义线程池,先来看看我们线程池的结构图
可以看到我们线程池里有许多线程,这里存在的最大数量的线程是我们设置的,我们这里采用的创建线程的方式是懒汉式,只有需要线程时我们才创建,如果有过多的任务需要处理就将任务放入到阻塞队列中
按照这个思路我们可以写入我们的代码如下,我们首先写阻塞队列的代码
@Slf4j(topic = "c.TestPool")
public class TestPool {
public static void main(String[] args) {
ThreadPool threadPool = new ThreadPool(1,
100, TimeUnit.MILLISECONDS, 1,addStrategy.THROWS);
for (int i = 0; i < 3; i++) {
int j = i;
threadPool.execute(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}", j);
});
}
}
}
@FunctionalInterface // 拒绝策略
interface RejectPolicy<T> {
void reject(BlockingQueue<T> queue, T task);
}
enum addStrategy{
//一直等待
WAITING,
//过时不候
TIME_WAITING,
//调用者放弃任务执行
GIVE_UP,
//调用者抛出异常
THROWS,
//调用者自己执行任务
RUN_BY_SELF;
}
@Slf4j(topic = "c.ThreadPool")
class ThreadPool {
// 任务队列
private BlockingQueue<Runnable> taskQueue;
// 线程集合
private HashSet<Worker> workers = new HashSet<>();
// 核心线程数
private int coreSize;
// 获取任务时的超时时间
private long timeout;
private TimeUnit timeUnit;
private addStrategy strategy;
// 执行任务
public void execute(Runnable task) {
// 当任务数没有超过 coreSize 时,直接交给 worker 对象执行
// 如果任务数超过 coreSize 时,加入任务队列暂存
synchronized (workers) {
if(workers.size() < coreSize) {
Worker worker = new Worker(task);
log.debug("新增 worker{}, {}", worker, task);
workers.add(worker);
worker.start();
} else {
// taskQueue.put(task);
// 1) 死等
// 2) 带超时等待
// 3) 让调用者放弃任务执行
// 4) 让调用者抛出异常
// 5) 让调用者自己执行任务
// taskQueue.tryPut(rejectPolicy, task);
switch (strategy){
case WAITING:taskQueue.tryPut((queue, task1) -> {queue.put(task1);},task);break;
case TIME_WAITING:taskQueue.tryPut((queue, task1) -> {queue.offer(task1,1500,TimeUnit.MILLISECONDS);},task);break;
case GIVE_UP:log.debug("放弃{}",task);break;
case THROWS:throw new RuntimeException("任务执行失败 "+task);
case RUN_BY_SELF:task.run();break;
default:
}
}
}
}
public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity,addStrategy strategy) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.taskQueue = new BlockingQueue<>(queueCapcity);
this.strategy = strategy;
}
class Worker extends Thread{
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
// 执行任务
// 1) 当 task 不为空,执行任务
// 2) 当 task 执行完毕,再接着从任务队列获取任务并执行
// while(task != null || (task = taskQueue.take()) != null) {
while(task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
try {
log.debug("正在执行...{}", task);
task.run();
} catch (Exception e) {
e.printStackTrace();
} finally {
task = null;
}
}
synchronized (workers) {
log.debug("worker 被移除{}", this);
workers.remove(this);
}
}
}
}
@Slf4j(topic = "c.BlockingQueue")
class BlockingQueue<T> {
// 1. 任务队列
private Deque<T> queue = new ArrayDeque<>();
// 2. 锁
private ReentrantLock lock = new ReentrantLock();
// 3. 生产者条件变量
private Condition fullWaitSet = lock.newCondition();
// 4. 消费者条件变量
private Condition emptyWaitSet = lock.newCondition();
// 5. 容量
private int capcity;
public BlockingQueue(int capcity) {
this.capcity = capcity;
}
// 带超时阻塞获取
public T poll(long timeout, TimeUnit unit) {
lock.lock();
try {
// 将 timeout 统一转换为 纳秒
long nanos = unit.toNanos(timeout);
while (queue.isEmpty()) {
try {
// 返回值是剩余时间
if (nanos <= 0) {
return null;
}
nanos = emptyWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
} finally {
lock.unlock();
}
}
// 阻塞获取
public T take() {
lock.lock();
try {
while (queue.isEmpty()) {
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
} finally {
lock.unlock();
}
}
// 阻塞添加
public void put(T task) {
lock.lock();
try {
while (queue.size() == capcity) {
try {
log.debug("等待加入任务队列 {} ...", task);
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
} finally {
lock.unlock();
}
}
// 带超时时间阻塞添加
public boolean offer(T task, long timeout, TimeUnit timeUnit) {
lock.lock();
try {
long nanos = timeUnit.toNanos(timeout);
while (queue.size() == capcity) {
try {
if(nanos <= 0) {
return false;
}
log.debug("等待加入任务队列 {} ...", task);
nanos = fullWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
return true;
} finally {
lock.unlock();
}
}
public int size() {
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}
public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
lock.lock();
try {
// 判断队列是否满
if(queue.size() == capcity) {
rejectPolicy.reject(this, task);
} else { // 有空闲
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
}
} finally {
lock.unlock();
}
}
}
阻塞队列里我们需要任务队列,ReentrantLock锁对象和生产者和消费者的休息室和容量
我们提供带超时和不带超时的获取任务队列中的任务的方法,不提供超时的方法用while结合队列判空,在队列为空时令当前线程到消费者休息室等待即可,不为空时移除队列的任务对象并唤醒生产者休息室中的线程,而至于超时等待的方法无非是提供时间对象并利用TimeUnit对象转换为纳秒然后调用休息室的awaitNanos传入纳秒对象即可,其会返回剩余的时间对象,每次循环判断剩余时间是否小于0,若是则退出循环即可
超时和不带超时的添加方法也是依葫芦画瓢即可,还有返回阻塞队列的任务数的方法,最后我们还有一个设定多余的任务的等待策略的tryput方法,这个我们后面再讲
然后是我们的线程池对象,首先我们线程池对象里有阻塞队列、核心线程数、超时时间、TimeUnit时间转换对象以及指定线程等待策略的枚举类。其下还有内部类Worker,该类继承Thread,封装Runnable对象,重写其run方法,如果task线程不为null或者是阻塞队列中获取的任务对象不为null,此时就说明有任务可以执行(我们这里每一个任务可以简单理解为一个线程),执行调用其run方法,执行完毕之后将task赋值为null,全部执行完毕确定内部没有需要执行的任务时退出循环并移除该woker
而对于超过了线程池中线程数量的任务我们当然需要令其进入阻塞队列中等待,但如果阻塞队列也满了,该线程是要继续等待还是直接退出呢?这里就是等待策略,我们将的等待策略有很多种,我们可以自己先做好,然后让用户来自己选择使用何种等待策略
先来实现修改等待策略,这里我们不要写死,因此我们推荐将权限下放到一个新类中,因此我们可以新创建一个策略类接口
@FunctionalInterface // 拒绝策略
interface RejectPolicy<T> {
void reject(BlockingQueue<T> queue, T task);
}
其下提供一个拒绝策略的方法,提供阻塞接口和线程对象本身,由于只有一个方法因此可以加入对应的lambda表达式
enum addStrategy{
//一直等待
WAITING,
//过时不候
TIME_WAITING,
//调用者放弃任务执行
GIVE_UP,
//调用者抛出异常
THROWS,
//调用者自己执行任务
RUN_BY_SELF;
}
然后我们设置不同的策略的枚举类,接着在阻塞队列中设置调用等待策略的方法,其下判断阻塞队列中的任务数量是否已经超过容量,若超过则执行拒绝策略,反之则加入任务队列
然后我们在线程池中的execute方法中写入我们的任务执行逻辑,执行任务时加锁,首先判断worker数量是否小于线程核心数,若是则创建Woker对象并将其加入到Wokers集合中然后开启该线程,反之们则进入switch的执行策略,通过不同的枚举类来执行不同的策略,采用lambda表达式的方式在调用方法时直接写入拒绝策略的逻辑
最后我们在测试的主方法中只需要先设定线程池然后通过for循环调用线程池的execute方法即可
我们这里创建线程是懒汉式的,只有外面的线程创建并传给我们,我们才会将该线程加入到线程池中并使用,否则我们自己不主动创建