JUC

Java线程

创建和运行线程

方法一:直接使用Thread

1
2
3
4
5
6
7
// 创建线程对象,并指定线程名
Thread t = new Thread("t1"){
public void run(){
//要执行的任务
}
}
t.start

方法二:使用Runnable配合Thread

1
2
3
4
5
6
7
Runnable runnable = new Runnable (){
public void run(){
//要执行的任务
}
}
Thread t = new Thread(runnable);
t.start;

Thread与Runnable的关系

Runnable作为Thread构造器的参数传入,并赋值给Thread的成员变量target,最后Thread会执行如下 run 方法

1
2
3
4
5
6
@Override
public void run() {
if (target != null) {
target.run();
}
}

更推荐使用第二种方式创建线程,因为

  • 用Runnable更容易与线程池等高级API配合
  • 用Runnable让任务类脱离了Thread继承体系,更灵活

方法三:FutureTask配合Thread

FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//创建任务对象
//泛型为返回类型
FutrueTask<Integer> task = new FutrueTask<>(new Callable<Integer>{
//要执行的任务
return ...;
});

//参数1:任务对象
//参数2:线程名
new Thread(task,"t").start();

//主线程阻塞,同步等待 task 执行完毕的结果
//get()用于返回任务结果
Integer result = task.get();

查看线程运行情况工具

jconsole远程监控

  • 需要以如下方式运行java类
1
java -Djava.rmi.server.hostna,e='ip地址'    -Dcom.sun.management.jmxremote-Dcom.sun.management.jmxremote.port=8080(连接端口)-Dcom.sun.management.jmxremote.ssl=false(是否安全连接)-Dcom.sun.management.jmxremote.authenticate=false(是否认证) Test(java类名)

线程运行原理

栈与栈帧

JVM中由堆、栈、方法区所组成,其中栈内存就是给线程使用的,每个线程启动后,虚拟机就会为其配fe一块栈内存,互不干扰

  • 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程上下文切换(Thread Context Switch)

因为以下一些原因导致CPU不再执行当前的线程,转而执行另一个线程的代码

  • 线程的cpu时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用sleep、yield、wait、join、park、synchronized、lock等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器,它的作用就是记住下一条jvm指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等

Image

Image

大部分方法已经学习过,就不再赘述

两阶段终止模式

在一个线程 t1 中如何“优雅”的终止线程 t2? 这里的“优雅”指的是给 t2 处理终止后需要处理的事务,如释放资源

Image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class TwoPhaseTermination {
private Thread thread;

//启动线程
public void start(){
thread = new Thread(()->{
while (true){
Thread current = Thread.currentThread();
if (current.isInterrupted()){
System.out.println("释放资源");
break;
}
try {
Thread.sleep(1000);
System.out.println("执行任务");
} catch (InterruptedException e) {
//如果在睡眠时被打断会抛出异常,并将打断标志设为false
e.printStackTrace();
//重新设置打断标记
current.interrupt();
}
}
});

thread.start();
}

public void stop(){
thread.interrupt();
}
}

主线程与守护线程

默认情况下,Java进程需要等待所有线程执行完才会结束,有一种特殊的线程叫守护线程,只要其他非守护线程执行完,即使守护线程没执行完,也会强制结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void main(){
Thread t = new Thread(()->{
//执行任务
try {
System.out.println("子线程开始执行");
Thread.sleep(2000);
System.out.println("子线程执行结束");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"daemon");

//设置该线程为守护线程
t.setDaemon(true);
t.start();


try {
Thread.sleep(1000);
System.out.println("主线程执行结束");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

应用场景:

  • 垃圾回收器线程就是一种守护线程
  • java 守护线程通常可用于开发一些为其它用户线程服务的功能。比如说心跳检测,事件监听等。

线程状态

以操作系统层面来说有五种状态

Image

以Java层面来说有六种状态

Image

线程安全

两个线程分别对一个变量进行同样次数的自增和自减,期望结果应该为0,而实际上结果并不唯一为什么呢?因为对于++操作而言,实际会产生如下的JVM字节码指令

1
2
3
4
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

而Java的内存模型如下,完成静态变量的自增、自减需要在主存和工作内存中进行数据交换

image-20220423120707424

多线程情况下可能会产生如下的运行情况

image-20220423120930220

为了避免共享资源的安全问题,有多种手段可以达到目的

  • 阻塞式解决方案:synchronized、Lock
  • 非阻塞式的解决方案:原子变量

synchronized

synchronized实际是使用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程所打断

synchronized修饰代码块和修饰方法都是锁住实例对象

synchronized修饰静态方法上是锁住这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class test1 {
public static int num = 0;
static Object lock = new Object();

public static void main(String[] args) {
Thread t1 = new Thread(()->{
for (int i=0; i < 1000;i++){
//为num上锁
synchronized (lock){
num++;
}
}
});

Thread t2 = new Thread(()->{
for (int i=0; i < 1000;i++){
//为num上锁
synchronized (lock){
num--;
}
}
});
}
}

image-20220423143526362

生产者消费者问题(管程法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
public class Test {
public static void main(String[] args) {
SynContainer container = new SynContainer();

new Productor(container).start();
new Consumer(container).start();
}
}

//生产者
class Productor extends Thread{
SynContainer container;

public Productor(SynContainer container){
this.container = container;
}

@Override
public void run() {
for (int i = 0; i<100 ; i++)
container.push(new Chicken(i));
}
}

//消费者
class Consumer extends Thread{
SynContainer container;

public Consumer(SynContainer container){
this.container = container;
}

@Override
public void run() {
for (int i = 0 ; i<100 ; i++){
container.pop();
}
}
}

//产品
class Chicken{
int id ; //产品编号

public Chicken(int id){
this.id = id;
}
}

//缓冲区
class SynContainer{
//容器大小
Chicken[] chickens = new Chicken[10];
//容器计数器
int count = 0;

//生产者放入产品
public synchronized void push(Chicken chicken){
//如果容器满了,就需要等待消费者消费
//最好不要用if,应该用while,否则当有多个消费者的时候,会出现脏判断的
while (count == chickens.length){
try {
this.wait();//阻塞此线程,并释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果容器没满,生产者放入产品
chickens[count] = chicken;
count++;

//通知消费者消费产品
this.notifyAll(); //唤醒其他等待线程
}

//消费者消费产品
public synchronized Chicken pop(){
//判断能否消费
if (count == 0){
//等待生产者生产,消费者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

//如果可以消费
count--;
Chicken chicken = chickens[count];

//通知生产者生产
this.notifyAll();

return chicken;
}
}

Monitor

概念:

Monitor被翻译为监视器管程,由操作系统提供

每个Java对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级锁)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针

Monitor结构如下

image-20220423170730098

当 Thread-2 获得锁时,Monitor 的 Owner 属性会指向 Thread-2,当还有其他的线程要去获得锁时会进入 EntryList 阻塞队列 ,当 Thread-2 执行完后会唤醒 EntryList 中阻塞的线程,并选择一个获得锁

从字节码层面看

主要的就是 monitorenter 和 monitorexit 两条指令,来决定进入同步代码块和退出同步代码块

image-20220423174219002

锁膨胀机制

引子:

小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样,即使他离开了,别人也进不了门,他的工作就是安全的。

但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女晚上用。每次上锁太麻烦了,有没有更简单的办法呢?

小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式。(轻量级锁)

后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。

于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。(偏向锁)

同学们都放假回老家了,小南就膨胀了,在20个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字

后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包

偏向锁

轻量级锁在没有竞争时,每次重入(就是在同步代码块中再调用同步代码块)仍然需要执行cas操作

所以引入了偏向锁来做进一步的优化:只有第一次使用cas将线程ID设置到对象的 Mark Word 头,之后发现这个线程ID是自己的就表示没有竞争,不用重新cas。以后只要不发生竞争,这个对象就归该线程所有

使用场景: 偏向锁主要用来优化同一线程多次申请同一个锁的竞争

Image

  • 当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
  • 当调用 wait/notify 时,会撤销偏向锁

轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化,轻量级锁对使用者来说是透明的,语法仍然是 synchronized

  • 创建一个锁记录对象,让锁记录中的Object reference指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word的值存入锁记录中

Image

  • 成功后图示如下 00 代表轻量级锁,01代表无锁

Image

  • 如果 cas 失败则有两种情况
    • 如果是其他线程已经持有了该Object的轻量级锁,表示有竞争,进入锁膨胀过程
    • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

Image

  • 当退出同步代码块时,如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

  • 如果没有则使用 cas 将 Mark Word 的值恢复给对象头

    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀,或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

当锁对象已经加上了轻量级锁,再来了一个线程要加轻量级锁的时候就会失败,这时候进行锁膨胀

Image

  • 为Object对象申请 Monitor 锁,让Object指向重量级锁地址
  • 然后自己进入 Monitor 的 EntryList 中

Image

  • 当 Thread-0 退出时会失败,并进入重量级锁的解锁流程

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步代码块,释放了锁),这时当前线程就可以避免阻塞,减少线程上下文切换(自旋就是进入一段时间的循环)

自旋成功情况:

Image


加锁顺序:偏向锁 ==> 轻量级锁 ==> 重量级锁(Monitor)

锁消除

像如下代码,obj锁对象是局部变量,其他线程也访问不到,这样的锁是无意义的,JIT Java即时编译器,就会将锁消除掉,以提高性能

1
2
3
4
5
6
public void b() throws Exception{
Object obj = new Object();
synchronized(obj){
x++;
}
}

wait-ify

  • Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
  • BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
  • BLOCKED线程会在Owner线程释放锁时唤醒
  • WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList重新竞争

API介绍

  • obj.wait()让进入object 监视器的线程到 waitSet 等待
  • obj.notify()在object上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll()让object上正在 waitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于Object对象的方法。必须获得此对象的锁,才能调用这几个方法

sleep()和wait()的区别

  • sleep是Thread方法,而wait是Object的方法
  • sleep不需要强制和synchronized 配合使用,但wait 需要和synchronized一起用
  • sleep在睡眠的同时,不会释放对象锁的,但wait在等待的时候会释放对象锁。

注:可以使用while来解决虚假唤醒问题

同步模式之保护性暂停

定义

  • 用于一个线程等待另一个线程的执行结果

要点

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
  • JDK中,join的实现、Future的实现,采用的就是此模式
  • 因为要等待另一方的结果,因此归类到同步模式

Image

代码实例:

Image Image

join原理也是采用了同步模式之保护性暂停

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
//同步模式之保护性暂停
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}

异步模式之生产者 / 消费者

要点:

  • 与之前的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
  • 消费队列可以用来平衡生产和消费的线程资源
  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
  • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
  • JDK中各种阻塞队列,采用的就是这种模式

image-20220427195925153

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class MessageQueue{
//消息队列集合
private LinkedList<Message> list = new LinkedList<>();
//队列容量
private int capcity;

public int getCapcity() {
return capcity;
}

//获取消息
public Message take(){
//检查队列是否为空
synchronized (list){
while (list.isEmpty()){
try {
list.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
//从队列头部获取消息并返回
Message message = list.removeFirst();
list.notifyAll();
return message;
}
}

//存入消息
public void put(Message message){
synchronized (list){
while (list.size() == capcity){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//放入消息到尾部
list.addLast(message);
list.notifyAll();
}
}
}

final class Message{
private int id;
private Object value;

public Message(int id, Object value) {
this.id = id;
this.value = value;
}

public int getId() {
return id;
}

public Object getValue() {
return value;
}

@Override
public String toString() {
return "Message{" +
"id=" + id +
", value=" + value +
'}';
}
}

Park & Unpark

基本使用

它们是 LockSupport 类中的方法

1
2
3
4
5
// 暂停当前线程
LockSupport.park();

// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

例:先unpark再park

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TestParkUnpark {
public static void main(String[] args) {
Thread t1 = new Thread(() ->{
log.debug("start...");
sleep(2);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, name: "t1");
t1.start();

sleep(1);
Log.debug("unpark...");
LockSupport.unpark(t1);
}
}

//start...
//unpark...
//park...
//resume...
//结论:unpark可以在park之前调用,调用之后会在下一次park之后恢复线程运行

特点:

与Object的 wait & notify相比

  • wait,notify和notifyAll必须配合Object Monitor一起使用,而unpark不必
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
  • park & unpark可以先unpark,而wait & notify不能先notify

原理

每个线程都有自己的一个Parker对象,由三部分组成_counter , _cond 和 _mutex 打个比喻

  • 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter就好比背包中的备用干粮〔0为耗尽,1为充足)
  • 调用park就是要看需不需要停下来歇息
    • 如果备用干粮耗尽,那么钻进帐篷歇息
    • 如果备用干粮充足,那么不需停留,继续前进
  • 调用unpark,就好比令干粮充足
    • 如果这时线程还在帐篷,就唤醒让他继续前进
    • 如果这时线程还在运行,那么下次他调用park时,仅是消耗掉备用干粮,不需停留继续前进
      • 因为背包空间有限,多次调用unpark 仅会补充一份备用干粮

调用park

image-20220428110109324

调用unpark

image-20220428110652050


活跃性

死锁

当一个线程需要同时获取多把锁时,就容易发生死锁

死锁条件

  1. 请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
  2. 不可剥夺:进程已获得的资源,在末使用完之前,不能强行剥夺;
  3. 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系;
  4. 互斥:一个资源每次只能被一个进程使用;

活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束

可以使用随机睡眠时间预防活锁的发生

饥饿

一个线程由于优先级太低,始终得不到CPU调度执行


ReentrantLock

特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁(先进先出)
  • 支持多个条件变量

与synchronized一样,都支持可重入

基本语法

1
2
3
4
5
6
7
8
//获取锁
reentrantLock.lock();
try{
//临界区
}finally{
//释放锁
reentrantLock.unlock();
}

可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获得这把锁,如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Test{
private static ReentrantLock lock = new ReentrantLock();
main{
//获取锁
lock.lock();
try{
//临界区
m1();
}finally{
//释放锁
lock.unlock();
}
}

public static void m1(){
//获取锁
lock.lock();
try{
//临界区
m2();
}finally{
//释放锁
lock.unlock();
}
}

public static void m2(){
//获取锁
lock.lock();
try{
//临界区
}finally{
//释放锁
lock.unlock();
}
}
}

可打断

可以将运行中的线程打断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test1 {
private static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
Thread t1 = new Thread(()->{
try {
//如果没有竞争,此方法回获取lock对象锁
//如果有竞争则进入阻塞队列,可以被其他线程用interruput方法打断
lock.lockInterruptibly();
}catch (InterruptedException e){
e.printStackTrace();
return;
}finally {
lock.unlock();
}
});

t1.start();

//主线程打断t1,如果使用的是lock()方法,是不能打断的
t1.interrupt();
}
}

公平锁

synchronized是不公平锁,ReentrantLock 默认是不公平锁,不公平锁的意思就是,当一个线程释放锁的时候,阻塞的线程都去抢锁,谁抢到了,谁就持有锁,不会按阻塞队列先后顺序持有锁

可以通过构造方法来指定是否为公平锁

1
2
3
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

条件变量

synchronized中也有条件变量,就是waitSet,当条件不满足时进入waitSet等待,ReentrantLock 的条件变量比 synchronized 强大的地方在于,它是支持多个条件变量的

  • await前需要获得锁
  • await执行后,会释放锁,进入conditionObject等待
  • await的线程被唤醒(或打断、或超时)取重新竞争lock 锁
  • 竞争lock锁成功后,从await后继续执行
1
2
3
4
5
6
7
8
9
//创建条件变量
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();

lock.lock();

condition1.await(); // 对应wait
condition1.signal(); // 对应notify
condition1.signalAll(); // 对应notifyAll

固定运行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) {
Thread t1 = new Thread(()->{
synchronized (lock){
while (t2runned){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("1");
}
});

Thread t2 = new Thread(()->{
synchronized (lock){
t2runned = true;
System.out.println("2");
lock.notifyAll();
}
});

t1.start();
t2.start();
}

volatile

volatile关键字可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存

  • volatile适合一写多读的情况

  • volatile能保证可见性,但不能保证原子性

volatile只有写操作是原子性的,也就是数据操作完成后会立刻刷新到主内存中。但是被volatile修饰的变量在读的时候可能会被多个线程读。

可见性与Java的内存模型有关,模型采用缓存与主存的方式对变量进行操作,也就是说,每个线程都有自己的缓存空间,对变量的操作都是在缓存中进行的,之后再将修改后的值返回到主存中,这就带来了问题,有可能一个线程在将共享变量修改后,还没有来的及将缓存中的变量返回给主存中,另外一个线程就对共享变量进行修改,那么这个线程拿到的值是主存中未被修改的值,这就是可见性的问题。

volatile很好的保证了变量的可见性,变量经过volatile修饰后,对此变量进行写操作时,汇编指令中会有一个LOCK前缀指令,这个不需要过多了解,但是加了这个指令后,会引发两件事情:

  • 将当前处理器缓存行的数据写回到系统内存
  • 这个写回内存的操作会使得在其他处理器缓存了该内存地址无效

什么意思呢?意思就是说当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值,这就保证了可见性。

从上述这段话中能看出只有在读取的时候才能保证读取到的是最新值

可见性

可见性即“内存可见性”。一个共享的变量才有资格谈可见性。

首先我们要清楚一件事,当一个共享变量被多个线程拿去使用的时候,是将公共位置的值拷贝一份拿到各个线程的私有空间中去使用的。这样就有一个问题,其中某个线程对共享变量进行了修改,但其他线程中的共享变量却还是原来的值。所谓的可见性,就是当共享变量在某个线程中变化时,其他线程在使用该共享变量前能得到变化后的值,仿佛这个变量的一动一静所有线程都能察觉一样。这就是可见性。

原子性

Java中,对基本数据类型的读取和赋值操作是原子性操作,所谓原子性操作就是指这些操作是不可中断的,要做一定做完,要么就没有执行。

防止指令重排

JVM会在不影响正确性的前提下,调整语句的执行顺序

例如:

1
2
int i = ...;
int j = ...;

指令重排后:

1
2
int j = ...;
int i = ...;

但是多线程情况下,指令重排可能回影响正确性,例如new一个对象时并不是原子操作,分为三步

  • 分配内存空间
  • 执行构造方法,初始化对象
  • 把这个对象指向这个空间

由于不是原子性操作,可能会造成指令重排的问题,例如先将这个对象指向这个空间,再去初始化对象

情况:一个线程A执行代码,在new操作时发生了指令重排,导致先将对象指向分配的空间,再去初始化对象;这时线程B进入发现这个对象已经指向了一个空间,它就会认为这个对象!=null,但实际上这个对象可能还未完成初始化对象


volatile原理

volatile的底层实现原理是内存屏障

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令后会加入读屏障

如何保证可见性

写屏障保证在该屏障之前的,对共享变量的改动都同步到主存中

1
2
3
num = 2;
ready = ture; //ready 是 volatile 赋值,带写屏障
//写屏障 将改动的数据同步到主存中

而读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中最新的数据

1
2
3
4
5
//读屏障 工作线程中的ready失效,读取主存中的最新数据
//ready 是 volatile 读取值,带读屏障
if(ready){
...
}

如何保证有序性

写屏障会确保指令重排时,不会将写屏障之前的代码排在写屏障之后

1
2
3
num = 2;
ready = ture; //ready 是 volatile 赋值,带写屏障
//写屏障 将改动的数据同步到主存中

读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

1
2
3
4
5
//读屏障 工作线程中的ready失效,读取主存中的最新数据
//ready 是 volatile 读取值,带读屏障
if(ready){
...
}

happens-before规则

happens-before规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before规则,JMM并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见

具体的定义为:

  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  • 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

具体的规则:

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  • start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  • Join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  • 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
  • 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。

CAS

Java中有一个AtomicInteger类型,其作用与加了volatile修饰的基本类型int的效果是一样的,但是其实现却不是靠加锁来实现。那么他是如何实现的呢?

其中的关键就是cas,全称是CompareAndSwap,它是一条CPU并发原语。用来判断内存某个位置的值是否为预期值,如果是则改为更新的值,这个过程是原子的。AtomicInteger中对应着compareAndSet方法

例如:

1
2
3
4
5
6
7
8
9
10
public void withdraw( Integer amount) {
while (true) {
int prev = balance.get(); //balance为AtomicInteger类型
int next = prev - amount;
//比较并设置值
if (balance.compareAndSet(prev, next)) {
break;
}
}
}

上述代码在多线程情况下就会执行如下流程

image-20220502114325576

实际上就是不断尝试,直到要修改的值与最新值一致的时候才进行修改

CAS与volatile

CAS必须借助volatile才能读取到共享变量的最新值,来实现比较并交换的效果

AtomicInteger源码中存储值的value也是volatile修饰的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AtomicInteger extends Number implements java.io.Serializable {
/**
private static final long serialVersionUID = 6214790243416807050L;

// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
**/
private volatile int value;

为什么CAS效率高

  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻
  • 线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速…恢复到高速运行,代价比较大
  • 但无锁情况下,因为线程要保持运行,需要额外CPU的支持,CPU在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

CAS的特点

  • CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
  • synchronized是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
  • CAS体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
    • 因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

CAS的缺点

  • 如果循环时间长,CPU开销很大
  • 只能保证一个共享变量的原子操作,对于多个共享变量操作时,循环CAS无法保证操作的原子性,这时候只能使用锁来保证
  • 会有ABA问题

ABA

CAS算法是取出内存中某时刻的数据并在当下时刻比较并替换,这中间会有一个时间差导致数据变化。比如说有两个线程,一快一慢,同时从主内存中取变量A,快线程将变量改为B并写入主内存,然后又将B从主内存中取出再改为A写入主内存,这时候慢线程刚完成了工作,使用CAS算法,发现预期值还是A,然后慢线程将自己的结果写入主内存。虽然慢线程操作成功,但是这个过程可能是有问题的


Unsafe

Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe对象不能直接调用,只能通过反射获得

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Test1 {

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
//theUnsafe是单例的
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);

//获取域的偏移地址,对内存操作
long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));

Teacher t = new Teacher();

//执行cas操作
//参数1:要操作的对象
//参数2:字段的偏移地址
//参数3:旧值
//参数4:新值
unsafe.compareAndSwapInt(t,idOffset,0,1);
unsafe.compareAndSwapObject(t,nameOffset,null,"jjw");
}
}

class Teacher{
volatile int id;
volatile String name;
}

模拟原子整数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class MyAtomicInteger implements Account {
private volatile int value;
private static final long vaLueOffset;
private static final Unsafe UNSAFE;
static {
UNSAFE = UnsafeAccessor.getUnsafe();
try{
valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
}catch (NoSuchFieldException e)i
e.printstackTrace();
throw new RuntimeException(e);
}
}

public int getValue() {
return value;
}

public void decrement(int amount) {
while(true) {
int prev = this.value;
int next = prev - amount;
if (UNSAFE.compareAndSwapInt(this,valueOffset,prev,next){
break;
}
}
}

}