线程安全问题的原因和解决方案

文章讲述了多线程环境下为何会出现线程安全问题,主要原因是并发执行的不确定性、非原子操作、内存可见性和指令重排序。synchronized关键字用于解决线程安全问题,通过加锁确保同一时间只有一个线程执行特定代码,实现可重入锁机制。文章还提到Java标准库中的一些线程安全类,如StringBuffer和ConcurrentHashMap。
摘要由CSDN通过智能技术生成

目录

为什么会有线程安全问题

线程安全问题的原因(这里只讲述5个典型的原因)

synchronized关键字

synchronized使用方法:明确对哪个方法进行加锁

synchronized是可重入锁

java标准库中的线程安全类


为什么会有线程安全问题

多线程的抢占式执行,带来的随机性

如果没有多线程,此时程序代码执行顺序就是固定的只有那么一条路,代码顺序是固定的,那么程序结果就是固定的,那么线程肯定就是安全的

但是如果有了多线程,此时抢占执行下,代码执行顺序,就从一种情况变成了无数种情况  所以我们就需要保证这无数种线程调度顺序的情况下,代码的执行顺序结果都是正确的,才会线程安全

此时 我们可以写一个这样的代码 使用两个线程对count进行++操作 ,根据我们的推想运行结果应该为10 0000 但是实际运行结果却是不唯一的 此时的代码就是线程不安全的

class Counter{
    public int count;
     public  void add1(){
        count++;
    }
}

public class Threaddemo13 {
    public static void main(String[] args) {
          Counter counter = new Counter();
          Thread t1 = new Thread(() ->{
             for(int i = 0;i < 50000;i++){
                 counter.add1();
             }
          });
        Thread t2 = new Thread(() ->{
            for(int i = 0;i < 50000;i++){
                counter.add1();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
     System.out.println(counter.count);
    }
}

而为什么程序会出现这个情况呢?

++操作本质上可以分为三步 

1.load操作:先把内存中的值操作,读取到CPU的寄存器中

2.add操作:把CPU寄存器里的数值进行+1操作(寄存器是CPU中重要的组成部分,也能存数据,空间更小,访问速度更快,CPU中进行的运算都是针对寄存器中的数据进行的)

3.save操作:把得到的结果写回到内存中去

而如果两个线程并发执行count++,此时就相当于两组load add save进行执行,此时不同的线程调度顺序,就可能会产生一些结果上的差异,如图列举所示,像这样的调度情况有无数种。

 第一种情况是线程安全的  执行过程  首先内存中有一个变量count == 0  而t1进行了load操作 那么t1线程中CPU寄存器中count的值记为0 ,再接着t1执行add操作 t1CPU寄存器中的count值变为1,t1再进行save操作 将count == 1放回到内存中去,此时t2再进行读操作的时候 count的值已经变成了1 再将它进行add和save操作 而最后count == 2;这里count自增两次,结果为2

第二中情况是不安全的 首先内存中有一个变量count == 0  ,t1线程会进行load操作 t1线程中CPU寄存器中count的值记为0,再接着t2也会进行load操作,t2线程中的CPU寄存器中count的值也会记为0,接下来t1进行add和save,t1里count变为1并且将count == 1 存到内存中去,再接下来t2进行add和save,t2里count变为1并且将count == 1 存到内存中去,而这里count自增两次 结果为1

线程安全问题的原因(这里只讲述5个典型的原因)

1.抢占式执行,随机调度【根本原因】 

2.代码结构:多个线程同时修改同一个变量 (比如说String 是不可变对象,不可变对象,天然是线程安全的)

3.原子性:指单个指令无法再进一步拆分了。 如果上面的++操作也是原子的,那么此时线程就是安全的,但是count++ 这里可以拆分load,add,save三个操作 所以++操作不是原子的 可以用加锁来讲非原子的改为原子的从而解决线程安全问题

4.内存可见性问题: 一边读一边写 也可能出现问题 可能此处读的结果不符合预期

5.指令重排序:编译器再保持逻辑不变的情况下 自己调整了你代码的执行顺序 从而加快看程序的执行效率,就是编译器再优化你代码的时候优化出bug了(单线程)

synchronized关键字

关于加锁(解决线程安全问题)

synchronized修饰add方法 对counter这个对象加锁  其他线程可以使用这个对象里没加锁的方法

首先我们要明确加锁对象

如果两个线程对同一个对象进行加锁,就会出现锁竞争/锁冲突。比如说我和小明去银行自助取款机上取钱,一个自动取款机只能服务一个人,当小明进去,就相当于他对自动取款机加锁,而我作为另一个要获取锁的线程,那么我只能在外面阻塞等待,等到解锁也就是小明从自助取款机里出来,我才能尝试加锁,而当小明进去后,就算小明没有使用自助取款机,但是他没有释放锁,只要他不释放锁,我就得仍然得在外面阻塞等待 就好比例如第二种情况虽然t1这会没在CPU上执行,但是没有释放锁,t2仍然得阻塞等待

 

如果两个线程针对不同对象加锁,此时不会发生锁竞争/锁冲突,这两线程都能各自获取到锁,此时不会发生阻塞等待了 就好比我和小明一人使用一个自助取款机 我不需要等待他使用完再使用。

当一个线程加锁,一个线程不加锁,那么就没有锁竞争,相当于没有加锁

加锁本质是把并发,变成了串行能够获取到锁(先到先得)另一个线程阻塞等待,等待到上一个线程解锁,它才能获取成功

synchronized使用方法:明确对哪个方法进行加锁

1.修饰方法:进入方法就加锁 离开方法就解锁

synchronized public void add1(){
  //用synchronized修饰刚才的add1方法,对counter这个对象加锁 
  //此时运行结果就为10 0000  
        count++;
    }

 1)修饰普通方法 :修饰普通方法,锁对象就是this

 2)修饰静态方法:修饰静态方法,锁对象就是类对象(Counter.clss)

2.修饰代码块://进入代码块就加锁 出了代码块就解锁

public void add2(){
        synchronized (this){
            //修饰代码块
            count++;
        }
    }

synchronized是可重入锁

一个线程针对同一个对象,连续加锁两次,没有问题就是可重入锁

java标准库中的线程安全类

   如果多个线程操作同一个集合类,就要考虑到线程安全问题

像Arraylist ,LinkedList,HashMap,TreeMap等等这些类中没有加锁 ,像StringBuffer,ConcurrentHashMap这些类中已经内置了synchronized,更安全一点 ,但是加锁这个操作是会有额外的时间开销,那么如果没有线程安全问题就不需要加锁,但是像StringBuffer这样强行加锁反而是浪费时间。String类里是不可变对象,虽然没有加锁,那它也是安全的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值