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

本文分析了一个Java多线程示例中出现的线程不安全问题,通过解释线程调度、内存操作和原子性,以及演示如何使用`synchronized`关键字和锁来确保线程安全,解决并发计数器的竞态条件。
摘要由CSDN通过智能技术生成

  我们先看以下代码:

public class ThreadDemo9 {
    private static int count;
    public static void main(String[] args) throws InterruptedException {
       Thread t1 = new Thread(()->{
           for (int i = 0; i < 10000; i++) {
                   count++;
           }
       });
       Thread t2 = new Thread(()->{
           for (int i = 0; i < 10000; i++) {
                   count++;
           }
       });
       t1.start();
       t2.start();
       t1.join();
       t2.join();
        System.out.println("count = " + count);
    }
}

两个线程同时对count变量加10000次,不出意外结果应该是20000,但实际上结果却是:

为什么?这就要谈到线程在cpu上的执行过程了。也就是线程安全问题。

首先,count++这个操作,站在cpu的角度上看,其实是3个指令。

load:把内存中的数据,加载到寄存器中。

add:把寄存器中的值+1.

save:把寄存器中的值写回到内存。

上述代码中,两个线程并发的进行count++,多线程的执行,是随机调度,抢占式的执行模式。也就是说某个线程执行指令的过程中,当它执行到任何一个指令的时候,都有可能被其他线程把它的cpu给抢占走。

结合上述两点,实际并发执行的时候,两个线程执行指令的相对顺序就可能会存在多种可能。

上面这两种可能是不影响结果的。

但是如果出现这些情况,就可能使3次甚至4次++,只起到1次的效果。

总结:一个线程的save在另一个线程的load之前,就是ok的。

          一个线程的save在另一个线程的load之后,就都是有问题的。

出现线程不安全的原因:

1)线程在系统中是随机调度,抢占式执行的。

2)当前代码中,多个线程同时修改同一个变量。

3)线程针对变量的修改操作,不是原子的。(不可拆分的最小单位,就叫"原子",如果某个代码操作,对应到一个cpu指令,就是原子的,对应到多个,就不是原子的)

count++就不是原子操作 ->3个指令。

4)内存可见性问题,引起的线程不安全。

5)指令重排序,引起的线程不安全。

上述例子就告诉大家,多个线程并发执行的时候,具体指令执行的先后顺序,可能存在无数种情况。我们要保证每一种情况下计算结果都得是对的才行。

解决方案

那么如何解决上述问题?要从原因入手,原因1是系统规定好的,我们无法干预。

原因2,我们的目的就是要多个线程修改同一个变量啊,也无从下手。

原因3是解决线程安全问题最普适的方案。

我们可以通过一些操作,把上述一系列”非原子“的操作,打包成一个”原子“操作。

这个操作就是加锁。

锁本质上也是操作系统提供的功能,内核提供的功能=>通过api给应用程序了。java(JVM)对于这样的系统api又进行了封装。

关于锁,主要的操作是两个方面

1)加锁    t1加上锁之后,t2也尝试进行加锁,就会阻塞等待(都是系统内核控制)(BLOCKER状态)

2)解锁   直到t1解锁了之后,t2才有可能拿到锁(加锁成功)

在一个程序中,锁不一定只有一把,如果你有两个线程,针对不同的代码加锁,不会产生互斥的(也称为锁竞争/锁冲突),只有针对同一段代码加锁,才有互斥。

总结:1)锁涉及到两个核心操作 : 加锁,解锁

2)锁的主要特性:互斥:一个线程获取到锁之后,另一个线程也尝试加这个锁,就会阻塞等待(也叫做锁竞争/锁冲突)

3)代码中,可以创建出多个锁,只有多个线程竞争同一把锁,才会产生互斥,针对不同的锁,则不会。

我们利用锁改进上述代码:

public class ThreadDemo9 {
    private static int count;
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
       Thread t1 = new Thread(()->{
           for (int i = 0; i < 10000; i++) {
               synchronized (locker){
                   count++;
               }
           }
       });
       Thread t2 = new Thread(()->{
           for (int i = 0; i < 10000; i++) {
               synchronized (locker){
                   count++;
               }
           }
       });
       t1.start();
       t2.start();
       t1.join();
       t2.join();
        System.out.println("count = " + count);
    }
}

运行结果:

注:synchronized 后面带上(),()里面就是写的“锁对象”。锁对象的用途有且只有一个,就是用来区分两个线程是否针对同一个对象加锁。如果是,就会出现锁竞争/锁冲突/互斥,就会引起阻塞等待。如果不是,就不会出现锁竞争,也就不会阻塞等待。和对象具体是啥类型和它里面有啥属性,有啥方法,接下来是否要操作这个对象没有任何关系。

synchronized下面跟者{}用于包裹代码块,当进入到代码块,就是给上述()锁对象进行了加锁操作,当出了代码块,就是给上述()锁对象进行了解锁操作。

当加锁的生命周期和方法的生命周期是一样的时候,synchronized还可以直接写到方法上。

第二种写法相当于一进入方法就针对this加锁。锁对象就是this。

synchronized修饰普通方法,是相当于针对this加锁了

synchronized还可以修饰static方法,

synchronized修饰static方法,相当于针对该类的类对象加锁。也就是利用java机制反射,类名.class获得类对象。

以上,关于线程安全问题,希望对你有所帮助。

  • 12
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值