Administrator
发布于 2024-02-04 / 7 阅读 / 0 评论 / 0 点赞

JAVA 线程不安全问题以及相关解决方案

JAVA 线程不安全问题以及相关解决方案

造成线程不安全的常见5点原因

线程不安全,就是在多线程运行结束后,结果或者过程并没有按照我们的预期那样执行,则为线程不安全,即产生了BUG

出现以下5种情况,一般都会照成线程不安全

  1. 抢占式执行

    我们使用多线程时,线程的调度执行过程是由系统内核来操作的,谁先调度执行完全看内核心情,因此视为“伪随机”,这是我们程序员无能为力的问题,这也是导致线程不安全的罪魁祸首

  2. 多个线程修改同一个变量

    由于多线程会抢占式执行,当多个线程修改同一个变量时,其中一个线程的计算过程还没完整结束,另外一个线程就开始调用了一个不完整的计算结果,从而导致最终的结果不准确(可调整代码来规避,但是针对这一点下手普适性不高

  3. 修改的操作不是原子的

    原子表示不可分割的最小单位 像count++这种操作,本质上是3个cpu指令来完成的;load,add,save(这三个操作单独拎出来都可以说是原子的)cpu执行指令,都是以“一个指令”为单位进行执行,一个指令相当于cpu上的“最小单位”,不能说把指令(load/add/save)执行一半就把线程调度走了综上所述:我们一般着眼于第三点来反制线程不安全,即把多个操作通过特殊手段,打包成一个原子操作,也就是说把零散的指令组合成一个完整的指令,因为cpu一次执行一个完整指令,从而避免了一个线程还没执行完所有应该执行的指令就被调度走了

  4. 内存可见性问题

    JVM的代码优化引入BUG 编译器很聪明,它会自己帮我们进行代码优化提升效率,但在多线程情况下,编译器容易出现误判 除非能保证优化后,逻辑与之前的逻辑是等价的,否则就会出现BUG 编译器优化对单线程没有影响,并且会提升效率,而面对多线程,可能导致第5点问题,也就是 “指令重排序”

  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);
    }
}

1-ewzg.png由代码运行的结果来看,并没有达到10w次,因此出现了线程不安全问题 造成该结果的原因是线程的抢占式执行;

我们重点在于解决第三点,从第三点入手反制

也就是用特殊手段让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关键字来修饰一个普通方法,当进入方法时,加锁,方法执行完后,解锁(看起来就像串行了)

注:锁竞争会多线程存在串行,但是也仅仅是加锁的部分串行,其他代码都还在并发执行

2-vgmb.png如图所示,在increase方法进行加锁,当两个线程同时调用到increase方法时,则加锁,另外一个线程阻塞等待

这样就避免了由于线程的抢占式执行而产生的bug

3-brzl.png2、syn可以修饰代码块:用于在一个方法中有些代码需要加锁有些不需要

而要完成这种做法,我们要明确锁对象,由于Java的特殊性,锁对象可以任意选择,只要线程之间都是通过这个锁对象来对代码进行加锁,产生了锁竞争即可一般我们用this来当锁对象,含义就是此时调用的对象,谁调用就对谁进行加锁,所以我们对代码进行修改一下

4-llyr.png5-dbuq.png用于我们补充了锁对象为this,并且两个线程中的counter对象是同一个东西,都调用了increase,所以会产生锁竞争
6-hmhm.png

3、用syn来修饰静态方法,因为静态的就一份,不同的对象来调用静态的东西,这个东西永远是这个东西,因此也会产生锁竞争

7-nejf.png8-fmwy.png


评论