线程的安全性分析
一、并发编程问题的源头
可见性,由多核cpu缓存导致的,每个cpu访问各自的缓存,不同cpu缓存中的数据不可见。(缓存用于提高I/O存储速度)
原子性,线程切换导致的,Java中一条代码对应底层多条代码(在多条代码的执行过程中,线程切换了。)。
有序性,编译器编译代码时会改变代码的顺序(单线程没有什么影响,多线程影响比较大)
1. 可见性问题
多核cpu访问各自缓存所导致的
不同cpu在执行线程时,cpu访问的数据是各自cpu中的缓存导致
下图中:线程A对于变量X的操作,对于线程B不可见。不具备可见性。
当stop没有volatile修饰时,主线程中修改stop的值是不会影响子线程中的stop的值。
public class VisableDemo {
// public static boolean stop=false;
// volatile可以解决可见性、[有序性]
public volatile static boolean stop=false;
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
int i=0;
while(!stop){
i++;
}
System.out.println("线程结束: result:"+i);
});
thread.start();
System.out.println("begin start thread");
Thread.sleep(1000);
stop=true; //主线程中修改stop的值,使得上面线程可以执行结束
}
}
2. 原子性问题
由线程切换导致的
count++在Java中可能只是一行指令,但是底层可能是有多条指令的。
public class AtomicDemo {
public static int count=0;
public static void incr(){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++; //count++并不是一个原子操作:count++ (只会由一个线程来执行)
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
// 创建1000个线程
new Thread(AtomicDemo::incr).start();
}
// 为了测试显示的效果做了一个延时,等待子线程执行完毕。
Thread.sleep(4000);
System.out.println("result:"+i);
}
}
输出结果:一定是小于等于1000的值
查看count++源码
IDEA点击到文件对应的字节码.class,邮件open in terminal
javap -v AtomicDemo.class
count++ 对应代码如下:并不是一个原子指令,要么同时成功要么同时失败。
12: getstatic
15: iconst_1
16: iadd
17: putstatic
3. 编译器带来的有序性问题
例如:final域问题
在构造函数中对非final修饰的变量赋值。
这个赋值操作会被编译器放在构造函数之外。
二、Java内存模型------Java如何解决可见性有序性问题
Java内存模型(Java Memory Model):Java 解决可见性、原子性、有序性的一套软件机制。
volatile、synchronized、final关键字
Happens-Before原则(告诉你哪些场景不会存在可见性问题)
三、synchronized关键字
原子性:解决方案(Synchronized、AtomicXXX、Lock)
可见性:解决方案(Synchronized、Volatile)
有序性:解决方案(Synchronized、Volatile)
synchronized:同一时刻单线程执行的代码不存在原子性问题。(指令重排序也无所谓)
synchronized修饰的代码同一时刻只能由一个线程执行,虽然不能解决有序性,但是可以解决有序性问题,因为只有一个线程。
synchronized修饰的范围:
- 修饰实例方法:锁的是同一个对象
- 修饰静态方法:锁的对象是类
- 修饰代码块:可以指定锁(对象或者类)
3.1 抢占锁的本质是:互斥。
如何实现互斥?
- 共享资源
- 可以是一个标记,0 无锁 1 有锁。
Object lock = new Object;
public void m2() {
// 代码块
synchronied (lock) {
}
}
在lock中存放了锁的相关信息
3.2 MarkWord对象头
3.3 锁主要存在的四种状态:
- 无锁状态
- 偏向锁状态:在没有线程竞争的情况下,线程A进入同步代码块,锁偏向线程A,A线程下次再进来的时候就不需要抢占锁。
- 轻量级锁状态:存在多个线程抢占锁,在偏向锁的基础上,将锁升级。(无锁状态—优化机制)
- 避免线程阻塞,通过自旋锁阻塞。
- 重量级锁状态。
3.4 CAS机制
以下操作必须是原子的
修改锁的标记
修改线程指针的指向
CompareAndSwap(old、except、update)
old:ThreadA
except:ThreadB
update: ThreadC
四、Volatile关键字
volatile: 可以用来解决可见性、有序性。
可见性的本质:
cpu处理速度快,内存磁盘读取速度慢,导致cpu资源的浪费。引入CPU增加高速缓存。
五、final域:防止指令重排序,解决可见性问题
一旦将引用申明成final,将不能改变这个引用。
参考ppt
对于final域,编译器和处理器要遵守两个重排序规则。
在构造函数内对一个final域的写入(构造函数中对final修饰的变量赋值)
与随后把这个被构造对象的引用赋值给一 个引用变量,这两个操作之间不能重排序。
初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操 作之间不能重排序。
这两个规则,可以防止指令重排序,来解决可见性问题。
1 写final域的重排序规则
i的值可能等于0,但j的值一定等于2
写普通变量i的操作,被编译器重排序到构造函数之外。
2 读final域的重排序规则
在一个线程中,初次读对象引用与初次读该对象包含的final 域,
JMM禁止处理器重排序这两个操作,编译器会在读final 域操作的前面插入一个LoadLoad屏障。
上面代码中,读普通域i代码,可以在写普通域i之前
六、Happens-Before规则
参考ppt
- 程序顺序性规则:不管程序如何重排序,单线程的执行结果一定不会发生变化。
- 监视器锁规则:一个锁的解锁,在后序这个锁的枷锁之前。
- Volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读
- 传递性规则:A happen before B B happens befoe C 那么 A 在 C之前。
- start规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的 ThreadB.start()操作happens-before于线程B中的任意操作
- join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意 操作happens-before于线程A从ThreadB.join()操作成功返回
七、原子类Automic
Synchronized能够保证原子性问题,因为加了Synchronized的代码,同一时刻只能由一个线程执行所以不存在原子性问题。
八、ThreadLocal的实现原理
每个线程独立存储数据的空间
public class ThreadLocalDemo {
private static Integer num=0;
public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
protected Integer initialValue(){
return 0; //初始值
}
};
public static final ThreadLocal<Integer> local1 = new ThreadLocal<Integer>();
public static void main(String[] args) {
Thread[] threads = new Thread[5];
//希望每个线程都拿到的是0
for (int i = 0; i < 5; i++) {
threads[i]=new Thread(()->{
// num+=5;
int num=local.get(); //拿到初始值
local1.get();
num += 5;
local.set(num);
System.out.println(Thread.currentThread().getName()+"->"+num);
},"Thread-"+i);
}
for(Thread thread:threads){
thread.start();
}
}
}