线程安全问题

1.线程安全问题引入

首先看下面代码:

class Counter{
    public int count = 0;

    public void increase(){
        count++;
    }
}


public class demo1 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() ->{
            for(int i = 0; i < 50000; i++){
                counter.increase();
            }
        });

        Thread t2 = new Thread(() ->{
            for(int i = 0; i < 50000; i++){
                counter.increase();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(counter.count);
    }
}

 我们会想,两个线程,针对同一个变量,进行循环自增,预期结果应该是10w,那么是不是呢?

显而易见,并不是10w,并且每次的运行结果都不同,这是为什么呢?

看这个操作:

这个操作本质上是三个步骤(CPU指令的角度):

而如果按照上述的操作,在两个线程,或者更多个线程并发执行的情况下,就可能出现问题:

虽然是自增两次,但是由于两个线程并发执行,就可能在一定的执行顺序下,导致运算的中间结果被覆盖了.

那么在这5w次的循环过程中,有多少次这两个线程执行是"串行的",有多少次会出现覆盖结果呢?

不确定!!!线程的调度是随机的,抢占式执行的过程

所以此处的结果就会出现问题,而且这个错误的结果一定小于10w

简单列举几种错误:

2.线程安全问题的原因

1) [根本原因]多个线程之间的调度是"随机的",操作系统使用"抢占式"策略来调度                       

(与单线程相比,多线程下,代码的执行顺序产生了更多的变化)

2) 多个线程同时修改同一个变量,容易产生线程安全问题(注意这三点)               

3) 进行的修改,不是"原子的".如果修改操作,能够按照原子的方式来完成,此时就不会有线程安全问题                                                                               

("count++"不是原子的,"="直接赋值,视为原子,"if="先判定,再赋值,也不是原子的)                      

4) 内存可见性

一个线程在读,另一个线程在改(类似于事务的不可重复读),也会出问题

5) 指令重排序

编译器可能觉得你写的代码不够高效,所以将代码的执行顺序进行了调整(保持逻辑不变的情况下),来加快程序的执行效率

3.synchronized锁

  • 把一组操作,打包成一个"原子"的操作,让多个线程,同一时刻只有一个线程能使用这个变量
  • synchronized修饰的是静态方法,和具体的对象无关,是和类有关的

3.1 代码加锁操作

如下,对上述代码进行修改:

class Counter{
    public int count = 0;

    /*synchronized public void increase(){
        count++;
    }*/ //法一

    public void increase(){
        synchronized (this){
            count++;
        }
    }  //法二
}


public class demo1 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() ->{
            for(int i = 0; i < 50000; i++){
                counter.increase();
            }
        });

        Thread t2 = new Thread(() ->{
            for(int i = 0; i < 50000; i++){
                counter.increase();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(counter.count);
    }
}

运行结果如下:

 3.2 synchronized相关注意事项

  • 注:Java中引入synchronized关键字进行加锁操作

  • synchronized进行加锁解锁,是以"对象"为维度进行展开的

现在我们思考一下, 通过加锁操作把并发执行=>串行执行了,那么多线程还要存在的意义嘛?

  • 这里的串行执行并不是针对整体,只针对了count++这件事,而for循环并没有加锁
  • for循环中操作的变量i是栈上的一个局部变量,两个线程,有两个独立的栈空间,也就是完全不同的变量,即两个线程中的i不是同一个变量,两个线程修改两个不同的变量,没有线程安全问题,所以也就不需要给i加锁
  • 不过,在这两个线程中,一部分代码是串行执行的,一部分是并发执行的,所以比存粹的串行执行效率要高

  • 加锁是为了互斥使用资源(互斥的修改变量),synchronized每次加锁,也是针对每个特定的对象加锁!!!

  •  具体针对哪个对象加锁不重要,重要的是,两个线程是不是针对同一个对象!!! 

如下解释:

 那如果是两个线程针对不同的对象加锁呢?

class Counter{
    public int count = 0;
    private Object locker = new Object();


    public void increase(){
        synchronized (this){
            count++;
        }
    }

    public void increase2(){
        synchronized (locker){
            count++;
        }
    }
}


public class demo1 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() ->{
            for(int i = 0; i < 50000; i++){
                counter.increase();
            }
        });

        Thread t2 = new Thread(() ->{
            for(int i = 0; i < 50000; i++){
                counter.increase2();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(counter.count);
    }
}

 

可以知道,如果要是两个线程针对不同的对象加锁,此时,就不会有阻塞等待,也就不会让两个线程按照串行的方式进行count++,也就仍然会存在线程安全问题

此时仍然是针对同一个对象加锁,会解决线程安全问题

所以具体针对哪个对象加锁不重要,重要的是,两个线程是不是针对同一个对象!!!

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值