目录
(2)修饰静态方法: 锁的 Synchronize_static 类的对象
一、多线程的安全问题
1、观察线程不安全
/**
* 观察多线程的安全问题
*/
public class ThreadSafeorUnsafe {
public 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();
//主线程走到此处,t1和t2都已经执行结束,理性状态是count和等于1万
System.out.println("t1和t2已经执行完毕~~");
System.out.println(counter.count);
}
}
此时的代码跑好多次都不是1万,并且每次都不一样
2、线程安全的概念
线程安全就是代码的串行和并行执行的结果是完全一致的,就说明当前线程是安全的
多个线程串行执行的结果和并行执行的结果不同,这就是线程不安全
第一次的是并行,会出现结果并不是理性的一万
此时改为串行执行后,就是1万了
所谓的线程安全问题就是在多线程并发的场景下,实际运行结果和单线程下的预期运行结果不符所出现的问题
3、JMM
JMM--java的内存模型,描述多线程场景下java的线程内存(CPU的高速缓存和寄存器和主内存的关系),注意这个和JVM的呢村区域划分不是一个概念。
每个线程都有自己的工作内存,每次读取变量(共享变量,不是线程的全局变量)都是先从主内存将变量加载到自己的工作内存中,之后关于此变量的所有操作都是在自己的工作内存中进行,然后写会主内存。
共享变量:类中的成员变量,静态变量,常量都属于共享变量,也就是在堆和方法区中存储的变量
4、多线程的安全保证
一段代码要保证是线程安全的(多线程场景下和单线程场景下的运行结果保持一致),需要同时满足以下三个特性:
(1)原子性
该操作对应CPU的一条指令,这个操作不会被中断,要么全部执行,要么就不执行,不会存在中间状态,这种操作就是一个原子性操作。
例如:
①对于int a=10 这个操来说,就是直接将变量赋值给a变量,要么赋值成功,要么就没有赋值
②对于a+=10这个操作来说,先要读取当前变量的值,再将a+10计算,最后将计算得出的值重新赋值给a变量(对应了三个原子性操作)
(2)可见性
一个线程对共享变量的修改,能够及时被其他线程看到,这种特性就是可见性
两个小问题
①为啥需要整这么多内存?
实际并没有这么多 "内存". 这只是 Java 规范中的一个术语, 是属于 "抽象" 的叫法. 所谓的 "主内存" 才是真正硬件角度的 "内存". 而所谓的 "工作内存", 则是指 CPU 的寄存器和高速缓存.
② 为啥要这么麻烦的拷来拷去?
因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级, 也 就是几千倍, 上万倍).
比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果 只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问 内存了. 效率就大大提高了. 那么接下来问题又来了, 既然访问寄存器速度这么快, 还要内存干啥?? 答案就是:CPU很贵 。
(3)防止指令重排
代码的书写顺序不一定就是最终JVM或者CPU的执行顺序
一段代码是这样的:
1. 去前台取下 U 盘
2. 去教室写 10 分钟作业
3. 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问 题,可以少跑一次前台。这种叫做指令重排序
对于单线程来说,指令重排是不会出现太大的问题的,但是在多线程的场景下,就有可能因为指令重排导致错误,一般就是对象还没初始化就被别的线程给用了
二、多线程安全问题的解决
1、多线程对象不同时
当多个线程使用了不同的线程对象时,多个线程之间就互不影响,因此就不会产生线程安全问题,是否会导致线程不安全,一定要注意,多个线程是否在操作同一个变量
2、synchronized关键字的使用
要确保一段代码的线程安全性,就需要同时满足原子性,可见性和防止指令重排现象。
synchronized这个关键字就可以同时满足这三个条件。