1.造成线程不安全的常见5点因素
2.如何解决线程不安全
线程不安全,就是在多线程运行的结束后,结果或者过程并不按照我们预期的那样执行,则为线程不安全,即产生了BUG
出现以下5种情况,一般都会造成线程不安全
1.抢占式执行
2.两个线程修改同一个变量
3.修改的操作不是原子的
4.内存可见性问题
5.指令重排序
举一个线程不安全的代码例子
定义一个count变量,让它从0自增到10w,创建两个线程,每个线程自增5w次,按照这个逻辑,count可以自增到10w吗?
//定义Counter类,让count自增10w次
class Counter{
public int count;
public void increase(){
count++;
}
}
public class Demo1 {
//实例化Counter对象
public static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 50000; i++){
counter.increase();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 50000; i++){
counter.increase();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("count: " + counter.count);
}
}
由代码运行的结果来看,并没有达到10w次,因此出现了线程不安全问题
造成该结果的原因是线程的抢占式执行
这里返回解释一下会导致线程不安全的5种情况
1.抢占式执行:我们使用多线程时,线程的调度执行过程是由系统内核来操作的,谁先调度执行完全看内核心情,因此视为“伪随机”,这是我们程序员无能为力的问题,这也是导致线程不安全的罪魁祸首
2.多个线程修改一个变量:由于多线程会抢占式执行,当多个线程修改同一个变量时,其中一个线程的计算过程还没完整结束,另外一个线程就开始调用了一个不完整的计算结果,从而导致最终的结果不准确(可调整代码来规避,但是针对这一点下手普适性不高)
3.修改操作不是原子的:
原子表示不可分割的最小单位
像count++这种操作,本质上是3个cpu指令来完成的;load,add,save(这三个操作单独拎出来都可以说是原子的)
cpu执行指令,都是以“一个指令”为单位进行执行,一个指令相当于cpu上的“最小单位”,不能说把指令(load/add/save)执行一半就把线程调度走了
综上所述:我们一般着眼于第三点来反制线程不安全,即把多个操作通过特殊手段,打包成一个原子操作,也就是说把零散的指令组合成一个完整的指令,因为cpu一次执行一个完整指令,从而避免了一个线程还没执行完所有应该执行的指令就被调度走了
以上3点是线程不安全的核心原因
4.内存可见性问题:JVM的代码优化引入BUG
编译器很聪明,它会自己帮我们进行代码优化提升效率,但在多线程情况下,编译器容易出现误判
除非能保证优化后,逻辑与之前的逻辑是等价的,否则就会出现BUG
编译器优化对单线程没有影响,并且会提升效率,而面对多线程,可能导致第5点问题,也就是
5.指令重排序
我们重点在于解决第三点,从第三点入手反制
也就是用特殊手段让count++变成原子的 -------”加锁“
即使用synchronized关键字来时代码进行“加锁”
返回到用两个线程自增count10w次的代码,也就是让每个线程都独立完成自己的任务后,另外的线程才开始调度执行,所以我们要对线程1进行加锁,当线程1正在运行的时候,线程2乖乖的阻塞等待带xianc1解锁,线程1解锁后线程2开始执行调度,线程2也要进行加锁,所以线程2在执行调度的时候,线程1反过来阻塞等待
因此线程1与线程2会产生“锁竞争”,也就是线程1还没解锁,线程2不动,线程2执行时还没解锁,线程1不动,当然,哪个线程先执行由内核说了算,谁先执行,另外的就阻塞等待
具体做法:在count++之前加锁,++完后解锁,count在加锁和解锁之间进行++,别的线程想修改,改不了(别的线程只能阻塞等待,阻塞等待的线程状态为BLOCKED)
下面研究如何给代码加锁-----synchronized
(1)syn的最基本用法:使用syn关键字来修饰一个普通方法,当进入方法时,加锁,方法执行完后,解锁(看起来就像串行了)
注:锁竞争会多线程存在串行,但是也仅仅是加锁的部分串行,其他代码都还在并发执行
如图所示,在increase方法进行加锁,当两个线程同时调用到increase方法时,则加锁,另外一个线程阻塞等待
这样就避免了由于线程的抢占式执行而产生的bug
以上是syn的一种用法
(2)syn可以修饰代码块:用于在一个方法中有些代码需要加锁有些不需要
而要完成这种做法,我们要明确锁对象,由于Java的特殊性,锁对象可以任意选择,只要线程之间都是通过这个锁对象来对代码进行加锁,产生了锁竞争即可
一般我们用this来当锁对象,含义就是此时调用的对象,谁调用就对谁进行加锁,所以我们对代码进行修改一下
用于我们补充了锁对象为this,并且两个线程中的counter对象是同一个东西,都调用了increase,所以会产生锁竞争
(3).第三种用法则是对静态的东西下手,用syn来修饰静态方法,因为静态的就一份,不同的对象来调用静态的东西,这个东西永远是这个东西,因此也会产生锁竞争
三种syn的写法介绍完毕,简单来说就是要使其产生锁竞争,这是最重要的
就相当于两个男生追一个妹子 ,一个追到了,另外一个就要阻塞等待~