线程安全问题及关键字synchronized,volatile

这篇文章思维导图如下 :

 

线程安全

什么是线程安全 ?? 什么又是线程不安全呢??

如果是在单线程环境下运行的结果与我们多个线程运行的结果一样,这就是线程安全的,但是由于多个线程并发执行就会导致与我们预期结果(单线程顺序执行的结果)不一样就会导致bug,这就是线程不安全.

  • 线程不安全案例
 static class Counter{
         public  int count = 0;
         public void increase() {
             count++;
         }
    }

    public static void main(String[] args) {
        Counter counter = new Counter();
         Thread t1 = new Thread(()->{
             for(int i =0;i<50000;++i){
                 counter.increase();
             }
         });
         t1.start();
         Thread t2 = new Thread(()->{
             for(int i =0;i<50000;++i){
                 counter.increase();
             }
         });
         t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter.count);
    }
  • 我们创建一个Counter类,类里有increase方法和count变量
  • 创建两个线程对Counter类的count变量累加,两个线程各自累加50000次
  • 使用join等待两个线程都运行完结束

输出结果如下 :

答案与我们预期的答案不一样,如果是在单线程的环境下,两个线程顺序执行答案就是10W,这里因为由于两个线程是并发执行的就导致bug-->让我们的线程不安全.

线程不安全的原因

随机调度/抢占式执行

就如上述情况,多个线程并发执行,由于我们操作系统是随机调度的/抢占式执行,CPU以不确定的方式或者是随机的时间执行,就会导致线程不安全

对于上述案例 count++就分为三个指令

1.从内存中读取数据到CPU 2. 进行数据更新(在CPU进行运算) 3.将数据写回内存

这就导致比如我们完成1+1操作,由于多个线程随机调度这三个指令,就会出现有的没有累加成功,直接写回内存,就导致与我们预期结果不一致-->线程安全问题

多个线程修改同一变量

如上述情景,两个线程共同修改同一个变量count,就会导致线程安全.

如果是其他情况如 : 多个线程修改不同变量 ,多个线程读同一变量,一个线程修改一个变量就不会出现线程安全问题.

非原子性操作

什么是原子性 ???

原子性:原子性指的是一个操作或者多个操作,保证操作都执行或者都不执行,这就是原子性;

比如 = 赋值一次性操作一次就执行这就是原子性的.

如count++,这就是非原子性的,因为++包括三个指令 1.读取内存到CPU2.在CPU进行运算(修改操作) 3.将数据写回内存,包含3个指令,但是在多个线程之间并发执行就可能导致三个指令并不都执行(比如只执行两个操作1.读取内存2.进行++运算,但是没有写回内存,就导致其他线程在进行运算时拿的是旧的值进行计算,导致最终结果错误);

  • 总结

非原子性操作 : 是指一个操作被分成多个指令,由于操作系统的调度,导致有些指令并没有被执行,最终影响结果

内存可见性问题

什么是可见性???

可见性 : 可见性是指一个线程读一个线程写,当一个线程修改同一份共享变量时,另一个线程读取到的是最新值.

而如果一个线程修改同一个共享变量,而另外一个线程读取时感知不到这个变量的值已经被修改就会出现问题 这就是内存可见性问题.

这其实是编译器进行优化导致的,我们知道读取寄存器的速度要比读取CPU速度快的多,这就会导致由于编译器很频繁的读取就会导致读到的结果都是一样的,编译器就会对其进行一些优化操作,将读取内存全部优化成读取寄存器,这样就会导致一个线程修改,而另外一个线程在读取时感知不到,读取不到最新值.

总结:

内存可见性是编译器进行优化导致一个线程修改共享变量,而另外一个线程读取时读取不到最新值,导致结果错误

 指令重排序

这也是一个重要的特性 有序性:

有序性就是指代码的执行顺序,按照先后顺序来执行

比如一个操作实例化对象 Test t = new Test();这个实例化操作其实包含3个方面

1.创建内存空间 2.往这个空间构造对象 3.将对象的引用赋值给该对象

如果是进行2,3这样的顺序,当一个线程读取这个对象为非null时这个对象是一个有效的对象

如果是进行3,2这样的顺序,当一个线程读取这个对象为非null时,由于这个对象并没有构造出对象,所以这个对象是一个无效的对象.

指令重排序这也是一个编译器进行优化导致的问题,对代码的顺序进行修改.

解决线程安全问题

解决上述线程安全问题 可以使用两个关键字synchronized关键字(这个是给对象进行加锁),volatile关键字

synchronized关键字

synchronized可以理解为互斥当在方法或者代码块上加上synchronized关键字相当于给对象加锁,当一个线程执行时也就是使用这一份资源时相当于加锁,而其他线程要想使用这一份资源就要阻塞等待.

就如刚才那个示例加上synchronized关键字就会先让一个线程先执行,而另外一个线程阻塞等待,等第一个线程执行完之后(解锁),另外一个线程在执行.synchronized关键字保证原子性

static class Counter{
         public  int count = 0;
         public synchronized void increase() {
             count++;
         }
    }
    public static void main(String[] args) {
        Counter counter = new Counter();
         Thread t1 = new Thread(()->{
             for(int i =0;i<50000;++i){
                 counter.increase();
             }
         });
         t1.start();
         Thread t2 = new Thread(()->{
             for(int i =0;i<50000;++i){
                 counter.increase();
             }
         });
         t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter.count);
    }

 

synchronized关键字的使用

  • synchronized关键字修饰普通方法

 对普通方法加锁相当于对当前对象this加锁,不同的对象对应着不同的锁,

  • synchronized关键字修饰静态方法

 对静态方法加锁相当于给类对象加锁-->所有对象调用静态方法都是互斥的

  • 对方法的总结

对普通方法加锁相当于对this对象加锁,对静态方法加锁相当于对类对象加锁.

对于普通方法,不同的对象对应着不同的锁,多个线程针对同一对象加锁才会发生锁竞争,多个线程对不同对象加上不会产生锁竞争

对于静态方法,由于类对象时全局的,只有一份,所以所有对象调用静态方法都会产生锁竞争(也就是一个线程使用资源,其他的线程需要阻塞等待)

  • 对代码块进行加锁
    public void method3(){
        synchronized(this){
            //对当前对象加锁
        }
    }
public void method4(){
        synchronized(TestDemo.class){
            //对类对象加锁
        }
    }

示例:

  • 对相同对象加锁会产生锁竞争
public static Object locker = new Object();
    public static void main(String[] args) {
        //对相同对象加锁会产生锁竞争
        Thread t1 = new Thread(()->{
            synchronized(locker){
                System.out.println("t1  --> start");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1  --> end");
            }
        });
        t1.start();
        Thread t2 = new Thread(()->{
            synchronized(locker){
                System.out.println("t2  --> start");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2  --> end");
            }
        });
        t2.start();

    }

 

这就是当对一个对象加锁会产生所竞争,t1线程执行时,t2线程会阻塞等待,当t1执行完之后(释放锁),t2在执行.

  •  对不同对象加锁不产生锁竞争
public static Object locker1 = new Object();
    public static Object locker2 = new Object();
    public static void main(String[] args) {
        //对不同对象加锁不会产生锁竞争
        Thread t1 = new Thread(()->{
            synchronized(locker1){
                System.out.println("t1  --> start");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1  --> end");
            }
        });
        t1.start();
        Thread t2 = new Thread(()->{
            synchronized(locker2){
                System.out.println("t2  --> start");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2  --> end");
            }
        });
        t2.start();

    }

 volatile关键字

static class Counter{
        public int flag =0;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            System.out.println("t1 开始");
            while(counter.flag==0){

            }
            System.out.println("t1 结束");
        });
        t1.start();
        Thread t2 = new Thread(()->{
            Scanner scan = new Scanner(System.in);
            System.out.println("请输入一个值开始t2线程进行修改");
            scan.nextInt();
            counter.flag = 1;
            System.out.println("t2 结束 ");
        });
        t2.start();
    }

 

这就是典型的场景,一个线程在读,一个线程在写,由于要频繁读取,编译器就将读取内存操作优化为读取寄存器操作,导致一个线程在修改时,另一个线程在读取时没有感知到被修改,所以一直在读取旧值导致死循环.

但是当我们加上volatile关键字就可以很好地解决内存可见性问题

 

使用volatile关键字也能够禁止指令重排序,但是volatile不能解决原子性

  • volatile本质就是在编译器进行优化时加了一层"内存屏障",使得编译器在优化,需要重新读取内存,不进行优化,解决了内存可在性问题

synchronized关键字和volatile关键字的区别

  • volatile可以保证内存可在性,禁止指令重排序但是不能解决非原子操作问题,synchronized关键字能够保证原子性
  • volatile关键字只能修饰变量,而synchronized关键字能修饰代码块和方法
  • volatile关键字是解决的是变量在多个线程之间的可见性,synchronized关键字解决的是多个线程之间访问资源的同步性
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值