写在前面
本文一起看下线程安全相关内容。
1:重要的概念
1.1:竞态条件
多个线程竞争同一资源,如果是对多个线程访问资源的顺序敏感(即导致非预期结果)
,则该资源就是竞态条件。
1.2:临界区
会导致竞态条件发生的区域叫做临界区。
如果是出现了竞态条件,则需要对临界区的代码通过锁做好并发控制,否则会导致程序的错误。
2:并发相关的性质
并发相关的性质有,原子性,可见性,有序性,分别看下。如果是能够满足这3个特性,我们就可以认为程序时多线程安全的。
2.1:原子性
原子性的意思是,一组不可被终中断的操作,要么执行,要么不执行。如下:
x = 1
赋值操作,是原子的
x++
读取变量,然后+1,然后赋值,其中读取变量是原子的,赋值也是原子的,但整体不是原子的
y=x
读取x的值,是原子的,然后赋值给y是原子的,但是整体不是原子的
x=x+1
读取x的值,是原子的,对x值+1,将结果赋值给x是原子的,但是整体不是原子的
对于基础数据类型的读取,和赋值操作是原子,一旦一个操作有这两个操作组成则就不是原子的了,一般实现原子的方式是synchronized关键字,Lock上锁。
2.2:可见性
默认情况下每个线程都是读取自己内存的数据副本,而不会从主内存中读取数据,所以默认的其他线程的修改,本线程是读不到的,想要读到最新的修改,只要不读取线程内存的副本而改为读取主内存就行了,java提供了volatile关键字来实现这个要求。即通过volatile关键字就可以实现可见性。
除了volatile关键字之外,通过synchronized关键字和Lock也可以实现可见性,此时线程只能串行执行,并且在释放锁之前,会将所有的修改都刷新到主内存中,这样当其他线程获取锁时,第一次肯定是从主内存读取数据的,就能实现可见性,但是这种串行化的方式会损失性能。
注意:volatile关键字无法实现原子性,即只能保证每次都从主存中读取数据。
2.3:有序性
happen-before有以下两个层次的含义:
1:如果是A happen-before B,则A肯定在B之前执行
2:如果A happen-before B,则B能够看到A的修改,即数据是可见的,这由JMM机制保证
因此happen-before一组保证了某些条件下保证有序性的规则,也是一种在特定的条件下满足数据可见性的规则。happen-before规则一共有8中分别看下。
2.3.1:程序次序规则
一个线程内,前面的程序在后面的程序执行执行,如下图:
含义:
1:一个线程,前面的操作A happen-before 后面的操作B,即 前面的操作A 肯定在 后面的操作B 之前执行
2:一个线程,前面的操作A happen-before 后面的操作B,即 前面的操作A 产生的修改 肯定在 后面的操作B 中可见
描述的是一个线程内的先后顺序。
2.3.2:锁定规则
对于一个锁的unlock操作先行发生于对同一个锁的的lock操作,即unlock发生在后续的lock之前,注意这里的锁是synchronized,如下:
含义:
1:单个或多个线程内,某线程对Lock的unlock操作A happen-before 后续的某个线程对Lock的lock操作B,这里其实已经强调了先后关系,所以happen-before就有点废话了
2:单个或多个线程内,某线程对Lock的unlock操作A happen-before 后续的某个线程对Lock的lock操作B,则 某线程对Lock的unlock操作A 产生的修改 肯定在 后续的某个线程对Lock的lock操作B 中可见
2.3.3:volatile规则
对一个volatile变量的写操作A 先行发生于 后续对这个volatile变量的读操作B。
含义:
1:单个或多个线程内,对一个volatile变量的写操作A hanppen-before 后续对这个volatile变量的读操作B,这里已经指明后续了,所以happen-before就有点废话了
2:单个或多个线程内,对一个volatile变量的写操作A hanppen-before 后续对这个volatile变量的读操作B,则 对一个volatile变量的写操作A
2.3.4:传递规则
A 先行于 B ,B 先行于 C,则 先行于 C。该规则的最大作用是可以实现灵活的数据可见性,如下代码:
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里x会是多少呢?
}
}
}
假定如下的线程执行顺序:
首先根据程序次序规则,线程A的x = 42
happen-before 线程A的v = true;
,其次根据volatile规则,线程A的写volatile变量v=true
happen before 线程B的读volatile变量v
,因此线程A的x = 42
happen before 线程B的读volatile变量v
,因此线程B能够读取到线程A修改的变量值,即线程B能够读取到v=true,x=42,所以线程B执行最终会输出42
。
2.3.5:线程启动规则
Thread对象的start方法先行于Thread的run方法的内容。
含义:
1:Thread对象的start方法 happen-before Thread的run方法的内容,即Thread run方法在Thread start方法之后执行
1:Thread对象的start方法 happen-before Thread的run方法的内容,即Thread run方法可以看到Thread start方法线程产生的修改
2.3.6:线程中断规则
对线程interrupt方法的调用,先行于线程本身对interrupt异常的捕获逻辑。
含义:
1:对线程的interrupt方法调用 happen before 线程的InterrupttedException的捕获逻辑,即线程的interrupt方法调用 在 线程的InterrupttedException的捕获逻辑 之前执行
1:对线程的interrupt方法调用 happen before 线程的InterrupttedException的捕获逻辑,即 线程的InterrupttedException的捕获逻辑 可以看到 线程的interrupt方法调用 的线程产生的修改
2.3.7:线程终结检测规则
线程中的所有操作 先行发生于 线程状态的检测,如下图:
考虑这样的场景,线程B的执行需要依赖于线程A的操作产生的修改,则可以在线程B中执行A.join等待线程B执行完毕,然后在执行A.isAlive检测线程A的状态,使之发生happen-before,则能保证在线程B中看到线程A的修改,如下:
含义:
1:线程A中的所有操作 happen-before 对于线程A的状态检测,即对线程A的所有操作,发生在对于线程A的状态检测之前
2:线程A中的所有操作 happen-before 对于线程A的状态检测,即对线程A的状态检测后的操作,能够看到线程A所有操作已经产生的修改
2.3.8:对象终结规则
一个对象的初始化先行发生于一个对象finalize()方法的调用,如下:
含义:
1:对象A的初始化 happen-before 对象A的finalize方法调用,即对象A的finalize方法调用发生在对象A的初始化操作之前
2:对象A的初始化 happen-before 对象A的finalize方法调用,即对象A的finalize方法中可以看到对象A的初始化产生的修改
3:一个例子
如下两个线程修改同一个变量产生数据错误的例子,代码:
public class Counter {
private int sum = 0;
public void incr() {
sum = sum + 1;
}
public int getSum() {
return sum;
}
public static void main(String[] args) throws InterruptedException {
int loop = 10_0000;
// test single thread
Counter counter = new Counter();
for (int i = 0; i < loop; i++) {
counter.incr();
}
System.out.println("single thread: " + counter.getSum());
// test multiple threads
final Counter counter2 = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < loop / 2; i++) {
counter2.incr();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < loop / 2; i++) {
counter2.incr();
}
});
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println("multiple threads: " + counter2.getSum());
}
}
正常情况下,单线程+1和多线程+1的最终的都应该是100000,但事实不是,因为多线程因为存在可见性的问题,会小于该值,运行如下:
single thread: 100000
multiple threads: 54491
当然这个小于100000的值具体是多少,是不确定,但肯定小于100000,原因是存在从线程本地读取副本,而无法读到最新修改的情况。根本原因是存在竞态条件private int sum = 0
,因此我们只需要让竞态条件所形成的临界区加锁,就行了,串行访问,这样,每次sum+1后都会将最新的修改刷到主存,每次读取也都主存中读取最新的值,修改incr方法如下:
public synchronized void incr() {
sum = sum + 1;
}
再次运行:
single thread: 100000
multiple threads: 100000
在方法上加synchronized关键字其实就是在this对象上的修改标记字对应的锁状态字节对应的值,不同的加锁方式和对应的加锁方式参考下图:
4:其他相关知识点
4.1:volatile
特点如下:
1:每次读都强制从主存中读
2:适用于单线程写,多线程读的场景
3:能不用就不用,不确定也不用
4:替代方案Atomic原子类(实现最终的一致性)
6:内存屏障,组织指令重排序
对于6,可参考如下代码:
int a = 0;
int b = 9;
volatile boolean isRight = false;
a = 999; // 语句1
b = 888; // 语句2
isRight = true; // 语句3
b = a - 1; // 语句4
a = a + b; // 语句5
这里语句3
有以下几个语义:
1:语句4,语句5不会排到语句1,语句2的前面
2:语句1,语句2的修改对语句3,语句4,语句5是可见的
4.2:final
final本身能够提供最大程度的数据安全,因此,在程序中最大限度的使用final是个好习惯。
写在后面
参考文章列表
Java 对象结构 。
happen-before原则 。
happens-before是什么?JMM最最核心的概念,看完你就懂了 。