概念
线程安全的定义是复杂的,但是我们可以这样认为:
如果在多线程环境下代码运行的结果是符合我们要求的,也就是和单线程环境运行的结果一致,那么我们就认为这个程序是线程安全的。
先看一段线程不安全的代码:SUM是一个共享变量。期待结果应为200000,但结果并不是200000,这是由于此时线程不安全导致的。
//有一个共享变量,初始为0,启动20个线程,每个线程循环10000次,每次循环将共享变量++
public class UnsafeThread {
private static int SUM;
public static void main(String[] args){
//for循环只是启动线程
for (int i = 0; i < 20; i++){
new Thread(()->{
for (int j = 0; j<10000;j++){
SUM++;
}
}).start();
}
//所有子线程执行完毕后,打印SUM的值
//idea中,运行方式:run运行写>2;debug运行写>1
while ( Thread.activeCount() > 2) {
//判断线程活跃数,>1表示当前有子线程在运行,所有让main让步
Thread.yield();
//yield()让步,让当前main线程由运行态转变为就绪态
}
System.out.println(SUM);
}
}
线程不安全的原因:
1.原子性
原子性概念:一组操作中不能插入其他修改指令。
不保证原子性所带来的问题:
- 在多个线程并行或并发执行指令中,如果在某个线程执行的过程中,被其他线程修改了内部的共享变量,导致前一个线程的运行结果发生了错误。
- 可以通过加锁操作来保证代码的原子性(synchronized关键字)
一条JAVA语句,不一定就是原子的,也不一定只是一条指令
像上代码中的SUM++,其实就是三步操作完成的:
1.从内存中把数据读到CPU中
2.把SUM里面的数据进行++更新
3.再把数据写回到CPU中
看似是一条代码,但其实有3步,就很有可能存在线程不安全问题。
2.可见性
2.1线程的相关方法栈
- 方法和栈有关
方法进入时,会生成和方法相关的栈信息(入栈)
方法退出时,会消除相关的栈信息(出栈)
线程和方法栈的关系同上
jdk1.7内存模型
JDK1.8内存模型,1.8以后没有方法区,有元数据区
每个线程都具有自己的线程栈,每次方法调用都会生成对应的方法栈。
通过方法栈分析:下代码打印结果为A,B
2.2可见性分析
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到变化,这个就是不可见性带来的线程安全问题。
3.代码顺序性
顺序性就是:
(1)一段代码的执行顺序
(2)指令执行的顺序
什么是重排序? 这是JVM,CPU的优化结果
java程序 —>java进程运行的过程:
javaC命令编译java文件为class字节码文件,进程运行以后,JVM会解释字节码为机器码,交给CPU执行指令。
保证线程安全的方法:
1.synchronized关键字——监视器锁
在最开始线程不安全的代码中加入synchronized关键字,令其线程安全
synchronized语法演示:
//演示synchronized关键字使用方式
public class SynchronizedDemo {
//语法1.在静态方法上:静态同步方法
public synchronized static void test1(){
}
//test1写法等于test1_2
public static void test1_2(){
synchronized (SynchronizedDemo.class){
}
}
//语法2.在实例方法中:实例同步方法
public void test2(){
}
//test2写法等于test2_2
public void test2_2(){
synchronized (this){
}
}
//语法3.在代码块:同步代码快
public static void main(String[] args){
Object o = new Object();
synchronized (o){
}
}
}
1.对象锁,每个对象都有一个对象头,包含锁
2.synchronized是获取对象锁操作;
我要获取这个对象就要知道这个对象的对象锁,如果不知道对象锁,就会发生阻塞
(1)申请对象锁
(2)申请成功的话:
a:持有对象锁,并进入代码块中并执行代码,
b:退出代码块时,是需要归还对象锁的
c:退出代码块归还对象锁后,JVM会将刚才竞争失败的线程唤醒,重新竞争,循环(1)(2)(3)的逻辑
(3)申请失败的话:竞争失败的线程进入一个同步队列中,全部发生阻塞
同步队列:JVM管理synchronize竞争失败线程时,
将失败线程由运行态转变为阻塞态,然后将他们放到一个同步队列的数据结构中去,
当对象锁再次可用再次竞争时,再将之前所有失败线程唤醒(只会
唤醒之前竞争该对象锁失败的线程,竞争别的对象锁的线程也会被存储进同步队列,但是不会被唤醒),(阻塞态转变为被唤醒状态)
(4)对象:普通的java对象,类对象(JVM把类当做对象来管理)
(5)注意事项:
a:多个线程只有对同一个对象申请对象锁时,才会有同步互斥的作用
————同步互斥:同步代码块,在一个时间点,只有一个线程运行
b:多个线程竞争的不同对象的对象锁,不会产生同步互斥作用
(6)对于静态同步方法、实例同步方法都可以转换为同步代码快的写法。
基于上语法,针对代码执行顺序进行分析:
分析不同线程运行是否有同步互斥的效果:
什么时候使用synchronized关键字?
- 线程数量——根据代码,线程数量在动态变化时使用。
- synchronized加锁时,执行同步代码块的时间和任务量越多,对性能的优化越好。
Volatile关键字:
分析加上volatile关键字不会存在线程安全问题: