浅谈synchronized和volatitle实现线程安全的策略

编程之家收集整理的这篇文章主要介绍了浅谈synchronized和volatitle实现线程安全的策略编程之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

搜索热词

什么是线程不安全

我对线程安全的理解就是多个线程同时操作一个共享变量时会产生意料之外的情况,这种情况就是线程不安全。注意:只有写操作才可能出现线程不安全,对共享变量只进行读操作线程是绝对安全的。@H_404_3@

具体线程不安全的例子有一个很经典的就是两个线程都对一个共享变量x=0执行100次自增操作,但是x的结果并非200@H_404_3@

因此线程不安全的条件是:多线程 + 共享变量 + 写操作@H_404_3@

Java的内存模型

你可能会好奇线程是如何获取共享变量的,这就为你解答@H_404_3@

Java线程之间的通信由Java内存模型(简称JMM)控制,从抽象的角度来说,JMM定义了线程和主内存之间的抽象关系。JMM的抽象示意图如图所示:@H_404_3@

image-20210417162759801@H_404_3@

从图中我们可以看到:@H_404_3@

  • 共享变量存在于主内存中,也就是堆内存
  • 每一个线程都保存了一份该线程使用到的共享变量的副本
  • 线程读取共享变量优先从本地内存(也就是栈内存)中读取,写共享变量先写到栈内存,再写入堆内存
  • 线程之间对共享变量的通信只能通过堆内存

以上只是Java内存模型的抽象图,实际上线程的工作模型是这样的,栈内存即是两个缓冲区@H_404_3@

image-20210417163527051@H_404_3@@H_404_3@

接下来看一个线程不安全的例子:@H_404_3@

假设线程A、B操作同一个共享变量X,初始两级Cache都为空@H_404_3@

  1. 线程A想要读取X的值,由于两级Cache都没有命中,因此加载堆内存中的X=0,并缓存到两个Cache中
  2. 线程A修改X的值为1,为为两个Cache刷新X,再刷新到堆内存
  3. 线程B想要获取X的值,一级缓存没有获取到,二级缓存命中,读取到X=1
  4. 线程B想要修改X的值为2,先刷新自,己的一级缓存为2,再刷新二级缓存和堆内存中的X为2。目前为止一切正常,接下来重点来了
  5. 线程A想要读取X的值,一级缓存命中此时X=1,但是堆内存中的X=2。可以看到线程B写入的共享变量对X不可见,出现了线程不安全的情况。

由于Java内存机制就是这样设计的,因此多个线程操作同一个变量会产生不安全的问题,volatitle关键字这是被设计出来解决这一问题的,它只能用于单个变量。@H_404_3@

volatile解决共享变量线程不安全的策略

还是接着上面这个例子,我们这样定义X@H_404_3@

volatle int X=0@H_404_3@

volatile的内存语义是:@H_404_3@

当一个线程对volatile修饰的变量进行写操作时,JMM会立即将该线程对应的栈内存中的副本的值刷新到堆内存中;当一个线程对volatile修饰的变量进行读时,JMM会清空此变量的一二级缓存,直接从堆内存中读取共享变量的值。@H_404_3@

volatile可以当作一个轻量级的锁来使用,但volatile仅仅只能保证共享变量内存的可见性,不能保证操作共享变量的原子性,而锁(如synchronized)能保证整段锁范围内的代码具有原子性。@H_404_3@

synchronized与锁

首先要明确的是synchronized不是锁,锁都是基于对象的(Object的子类),Java中的每一个对象都可以作为一个锁。@H_404_3@

synchronized是Java的一个关键字,保证临界区内的代码同一时刻只能有一个线程执行。@H_404_3@

线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码或者抛出异常后或者在同步块内调用了该内置锁资源的wait系列方法时释放该内置锁。内置锁是排它锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。@H_404_3@

sysnchronized的内部锁可以是:@H_404_3@

  • 当前类的class字节码对象:this.getClass
  • 当前类的一个实例:this
  • 一个Object对象

wait和notify方法只能用于synchronized同步代码块内@H_404_3@

synchronized的内存语义

volatile不同@H_404_3@

进入synchronized块的内存语义是把再synchronized块内使用到的所有共享变量从栈内存中清空,这样就只能从堆内存只读取,保证了内存可见性。退出synchronized块的内存语义是把synchronized块内对共享变量的修改刷新到堆内存。@H_404_3@

仔细想想,这其实也是一个加锁和解锁的过程,保证共享变量修改的可见性。@H_404_3@

总结

  • volatile仅能保证单个共享变量内存的可见性,不能保证原子性。而synchronized既可保证同步块内所有共享变量的内存可见性,又能保证其操作的原子性。@H_404_3@

  • volatile是一个轻量级的保证内存可见性的关键字,实际上并没有加锁。因此它的性能很高。@H_404_3@

  • synchronized是一个重量级的锁,可以用在代码块、普通方法以及静态方法上。用在代码块时锁就是synchronized(~)内的对象,用在普通方法时锁就是this,用在静态方法时锁就是this.getClass()@H_404_3@

  • synchronized保证同步块内代码的原子性,因为要进行线程上下文切换,性能较低。不过优化过后性能也还可以。@H_404_3@

参考

总结

以上是编程之家为你收集整理的浅谈synchronized和volatitle实现线程安全的策略全部内容,希望文章能够帮你解决浅谈synchronized和volatitle实现线程安全的策略所遇到的程序开发问题。

如果觉得编程之家网站内容还不错,欢迎将编程之家网站推荐给程序员好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您喜欢寻找一群志同道合、互帮互助的学习伙伴,可以点击下方链接加入:
编程之家官方1群:1065694478(已满)
编程之家官方2群:163560250(已满)
编程之家官方3群:312128206(已满)
编程之家官方4群:230427597

相关文章

猜你在找的Java相关文章

什么是线程不安全 我对线程安全的理解就是多个线程同时操作一个共享变量时会产生意料之外的情况,这种情况就是线程不安全。注意:只有写操作才可能出现线程不安全,对共享变量只进行读操作线程是绝对安全的。 具体
为什么要使用线程池 创建/销毁线程需要消耗系统资源,线程池可以复用已创建的线程。 控制并发的数量。并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃。(主要原因) 可以对线程做统一管理。 JUC
参考资料: JVM虚拟机入门教程-陈树义 JavaGuide 《深入理解Java虚拟机》 面经: 个人整理 - Java 后端面试题 - JVM 篇 阿里 Java 实习十轮面试面经 1. 讲一下JV
代码如下:/*  练习把一个整数逆序输出  分别把个位,十位,百位,千位等各位的数字取出来*/import java.util.Scanner;class Demo18 { public static void main(String[] args)  { Scanner sc=new Scanner(System.in); System.out.println("请输入:"); int n...
代码如下:/*  练习限制用户登陆的次数*/import java.util.Scanner;class Demo16 { public static void main(String[] args)  { Scanner sc=new Scanner(System.in); String name=""; String pwd=""; for(int i=1;i<=5;i++){ ...
代码如下:/*  从键盘上输入正数和附属,分别统计正数和负数的个数并计算所有数的和。输入0表示结束*/import java.util.Scanner;class Demo15{ public static void main(String[] args)  { Scanner sc=new Scanner(System.in); int num=1,t=0,f=0,sum=0; while...
代码如下:/*  多分支练习彩票游戏,随机生成两个一位数的随机数,提示用户输入猜测的数字  如果完全匹配奖金10000,只匹配数字没有匹配顺序奖金3000,只匹配一个数字奖金1000  例:若生成的随机数为18,如果用户输入18奖金10000;如果用户输入81奖金3000;如果用户输入16奖金1000*/import java.util.Scanner;class Demo13{ public s...
代码如下:/*  练习简易计算器*/import java.util.Scanner;class Demo12 { public static void main(String[] args)  { Scanner sc=new Scanner(System.in); System.out.println("请输入第一个数字:"); int a=sc.nextInt(); System.o...