线程安全指的是代码若是串行执行和并发执行的结果完全一致,就称为该代码是线程安全的。
若多个线程串行执行(单线程执行)的结果和并发执行的结果不同,就称为线程不安全。
顺序执行:
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t1.join();
t2.start();
t2.join();
System.out.println("两个搬砖人都已经执行结束");
System.out.println(counter.count);
}
并发执行:
public class ThreadUnsafeDemo {
private static class Counter{
int count = 0;
void increase(){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("两个搬砖人都已经执行结束");
System.out.println(counter.count);
}
}
顺序执行和并发执行的结果并不一致,而且同一段代码(并发),每次执行的结果也都不相同。
JMM(Java Memory Model):java 内存模型:描述多线程场景下,线程的工作内存(CPU的高速缓存和寄存器)和主内存的关系
每个线程都有自己的工作内存,每次读取共享变量(类中的成员变量、静态变量、常量都属于共享变量,在堆中和方法区中存储的变量),不是线程的局部变量,都是先从主内存中将变量加载到自己的工作内存,之后关于此变量的所有操作都是在自己的工作内存中进行,然后写会主内存。
原子性 :该操作对应CPU的一条指令,这个操作不会被中断,要么全部执行,要么全都不被执行,不会存在中间状态。这个操作就是一个原子性操作。
int a = 10 => 直接将常量10赋值给a变量,要么没赋值,要么赋值成功,原子性
a += 10 => a = a+ 10 先要读取当前变量a 的值,再将 a + 10计算,最后将计算得出的值重新赋值给a变量(对应3个原子性操作,这条指令是不原子性的)
可见性 :一个线程对共享变量的修改,能够及时的被其他线程看到,这种特性称为可见性(synchronized-上锁 、 volatile关键字 、 final关键字)
指令重排: 代码的书写顺序不一定就是最终JVM或者CPU的执行顺序。(编译器和CPU会对指令优化 -——>前提 : 保证代码的逻辑正确)
在单线程场景下指令重排没什么问题,但是在多线程场景下就有可能因为指令重排导致错误(一般就是对象还未初始化完成就被别的线程给用了)
要确保一段代码的线程安全,需要同时满足可见性、原子性和防止指令重排。
是否会导致线程不安全,一定要注意,多个线程是否在操作同一个共享变量。
1、increase()方法中的count ++ 操作不是一个原子性的操作。
首先,线程会从主内存中读取count的值到自己的工作内存中;
然后,计算 count++;
最后,将 count的值写会主内存。
2、此时是多个线程同时操作同一个共享变量(count)
3、线程对共享变量的修改不能及时被其他线程看到(主内存 和 工作内存)。
4、其中一种可能性(以结果为66211为例)
(1) t1 和 t2 在线程启动时,会将主内存中的 count 值读取到自己的工作内存中,t1先于t2启动,先从主内存中读取count的值 t1.count = 0,此时t2还没有启动。
(2)之后t1开始执行自己的run方法,假设 t1 执行循环16211次,此时 t1.count =16211,线程 t1 将16211 写回主内存中,这时t2才启动,从主内存中读到 count = 16211。
(3)然后 t1 、t2都各自在执行自己的run方法,(解设期间t1、t2都没有再写会主内存),t1 在执行直到最后,把t1.count = 5000,写回内存。
(4)但是 t2 在拿到初始值16211后,一直在读取自己工作内存中的值,而 t1.count = 5000这个值对于 t2 来说并不可见,在 t2 执行完之后,就会把最终值66211写回主内存,把之前t1写回的5000给覆盖了。
解决线程安全问题,就是保证原子性和可见性。Synchronized 关键字就能同时满足原子性和可见性。
private static class Counter{
int count = 0;
synchronized void increase(){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("两个搬砖人都已经执行结束");
System.out.println(counter.count);
}
(1)synchronized 的三大特性
synchronized 会起到互斥效果(metex lock), 某个线程执行到某个对象的 synchronized 中时(获取到该对象的锁), 其他线程如果也要获取同一个对象的锁,就会处在阻塞等待状态.
当给increase方法加上synchronized关键字,所有进入该方法的线程都需要获取当前counter对象的 “锁”,获取成功才能进入,获取失败,就会进入阻塞状态。
正因为increase方法上上锁处理,多个线程在执行increase方法时,其实是在排队进入,同一时刻只可能有一个线程进入increase方法,执行对count属性的操作。----保证了线程安全
线程执行synchronized代码块的流程:
a. 获取对象锁
b. 从主内存拷贝变量值到工作内存中
c. 执行代码
d. 将更改后的值写会主内存
e. 释放对象锁
因为从a - e 只有一个线程能执行,其他线程都在阻塞。synchronized保证互斥,同一时刻只有一个线程能够获取到这个对象的锁,这就保证了原子性(synchronized修饰的代码块全部执行结束后才会释放锁) 和可见性(操作完毕将共享变量的值写回主内存后才释放锁,其它线程获取锁的时候,这内存中的值一定是更改后的)。
可重入:获取到对象锁的线程可以再次加锁,这种操作就称为可重入。(Java中线程安全锁都是可重入的(包括java.concurrent.lock))
在Java内部,每个Java对象都有一块内存(对象头),描述当前对象的“锁”信息。信息包含当前对象被哪个线程持有,以及一个计数器-记录当前对象被上锁的次数。
I. 若线程1 需要进入当前对象的同步代码块(synchronized),此时当前对象的对象头中没有锁信息,线程1是第一个获取锁的线程,进入同步代码块,对象头修改持有线程为线程1,计数器从 0 -> 1。当线程1 在同步代码块中再次调用当前对象的其他同步方法,计数器的值再次+1,说明此时对象锁被线程1获取了两次。
II. 若线程2需要进入当前对象的同步代码块,此时当前对象的对象头持有线程为线程1,且计数器值不为0,线程2就会进入阻塞状态,一直等到线程1释放锁为止(直到计数器值为0,才叫真正释放锁)。
synchronized修饰类中的成员方法,则锁的对象就是当前类的对象。当前这个方法是通过哪个对象调用的,synchronized锁的就是哪个对象。
public class Reentrant {
public static void main(String[] args) {
Counter counter1 = new Counter();
Thread t1 = new Thread(() -> {
counter1.increase();
});
t1.start();
Counter counter2 = new Counter();
Thread t2 = new Thread(() -> {
// counter2.increase();
counter1.increase();
});
t2.start();
}
private static class Counter {
int val;
// 锁的是当前Counter对象
synchronized void increase() {
val ++;
}
}
}
情况1:
情况2:
synchronized修饰的类中的静态方法,锁的是当前这个类的class对象(全局唯一,相当于把这个类锁了,同一时刻只能有一个线程访问这个方法(无论有几个对象))
public class Reentrant {
public static void main(String[] args) {
Counter counter1 = new Counter();
Counter counter2 = new Counter();
Counter counter3 = new Counter();
Thread t1 = new Thread(() -> {
counter1.increase2();
},"t1");
Thread t2 = new Thread(() -> {
counter2.increase2();
},"t2");
Thread t3 = new Thread(() -> {
counter3.increase2();
},"t3");
t1.start();
t2.start();
t3.start();
}
private static class Counter {
// 当synchronized修饰静态方法,则相当于将Counter这个类的所有对象都给锁了(其实锁的Counter类的class对象,全局唯一)
synchronized static void increase2() {
while (true) {
System.out.println(Thread.currentThread().getName() + "获取到了锁~~");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
private static class Counter {
void increase3() {
//.....很多代码
// 同步代码块,进入同步代码块,必须获取到指定的锁
// this表示当前对象引用 ~~,锁的就是当前对象
// 若锁的是class对象,全局唯一
synchronized (this) {
while (true) {
System.out.println(Thread.currentThread().getName() + "获取到当前对象的锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
volatile 关键字可以保证共享变量可见性(强制线程读写主内存的变量值)。相较于普通的共享变量,使用volatile可以保证共享变量的可见性。
a. 当线程读取的是volatile关键字时,线程直接从主内存中读取该值到工作内存中(无论当前工作内存中是否已经有该值)
b. 当线程写的是volatile关键字时,将当前修改后的变量值(工作内存中的)立即刷新到主内存中,且其他正在读取此变量值的线程会等待(不是阻塞),直到写回主内存操作完成,保证读的一定是刷新后的主内存值。(对于同一个volatile变量,它的写操作一定发生在它的读操作之前,保证读到数据一定是主内存中刷新后的数据)
线程直接从主内存中读取该值到工作内存中(无论当前工作内存中是否已经有该值)
public class Volatile {
private static class Counter {
volatile int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
// volatile变量每次都读写主内存
while (counter.flag == 0) {
// 一直循环..
}
System.out.println(counter.flag + "退出循环");
});
t1.start();
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请改变flag的值");
counter.flag = scanner.nextInt();
});
t2.start();
}
}
注意:volatile只保证可见性,但无法保证原子性 ,因此,如果代码不是原子性操作,仍然不是线程安全的!!!
使用volatile关键字修饰的变量,相当于一个内存屏障。
final修饰的常量一定是可见的,因为常量在定义时就要赋值,且赋值后无法修改!