线程同步安全吗,线程安全 java
00-1010 1.为什么需要线程同步?2.如何实现线程同步?2.1.使用volatile关键字?2.2.使用同步关键字。
00-1010什么是线程安全:这意味着在访问被多个线程.期间可以在持续正确地办理手续
00-1010案例:以抢折扣为例说明线程安全问题。
class demo 1 { publicstaticvoidmain(string[]args){//简单模拟20个人抢一个折扣换(inti=0;i20i ){newThread(newThreadDemo())。start();} } }//前十名可以获得优惠,优惠可以通过数字ClassThreaddemomplementsRunnable { privatestatingintegernum=10兑换;@ Overridepublicvoidrun(){ try { thread . sleep(10);} catch(interrupted exception e){ e . printstacktrace();}if(num=0){System.out.println(被抢了,下次再来);返回;} system . out . println(thread . current thread()。 getname()用户抢到的号是: num-);}}}执行结果:出现的问题
可以多次获得相同的优惠号码;折扣数可能得到0和负数,类似于上面提到的超买和超卖并发访问不安全共享变量时的常见问题。
回避问题。
共享变量被设计成不可变的(final)。编程过程不修改共享变量(无修改)。对象被设计成无状态的(stateless)。修改共享变量时使用线程同步(通过锁定)。
00-1010线程同步是指程序中用来控制不同线程之间相对操作顺序的机制。
目录
volatile是轻量级同步的。对于共享变量,线程同步可以通过volatile关键字来实现。
共享变量处理情况:总线将共享变量从主内存复制到线程的私有工作内存,然后将它们交给处理器进行处理。处理完成后,运算结果从工作存储器写回主存储器。内存起到临时缓存数据和指令的作用,线程是私有的,所以会出现缓存不一致的情况。
实现原理:当一个由可变变量修改的共享变量执行写操作,时,会有一行额外的锁前缀指令的汇编代码。锁前缀指令会直接锁定缓存行,起到内存屏障的作用,使处理器立即将缓存写回主存,导致其他处理器的缓存失效,所以需要再次从主存获取最新值。
附上一张图,便于理解。
Java线程需要由操作系统内核线程调度器来调度,而不是直接访问处理器资源。图中只显示了几个关键点。
怎么输出汇编指令
Windows:下载hsdis-amd64.dll提取代码:w5a2,放在目录jdk1.8.0_181jrebinserver下。
在idea工具中配置VM:-xcomp-xx3360 unlockdiagnosticvmotions-xx3360 print Assembly-xx3360 compile Command=compile only,* testvolatile.main
//测试代码publicsclasstestvolatile { privatesticvolatileintnum=0;publistaticvoidmain(String[]args){ num-;system . out . println( result: num );}}执行结果:lock add dword ptr [rsp]
0x00000000
0320606d: lock add dword ptr [rsp],0h ;*putstatic num ; - com.yty.concurrent.synchronizeddemo.TestVolatile::main@5 (line 7)
通过缓存锁定和缓存一致性协议实现可见性,确保多线程程序读写共享变量的时候,每个CPU看到的都是最新值。
MESI 高速缓存一致性协议,处理器使用嗅探技术保证缓存、主内存和其他处理器缓存的数据一致。还有其他的缓存一致性协议,比如:AMD的MOESI协议、Intel的MOSIF协议。
MESI 分别表示:
M(Modify):修改E(Exclusive):独占、互斥S(Shared):共享I(Invalid):无效volatile 关键字的另一个作用是禁止编译器或处理器对指令进行重排序优化。
什么是重排序:重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。说白了,如果在编译或运行期间发生重排序,那么程序就可能不按照编写的顺序执行,会出现意料之外的执行结果。
编译器和处理器不会对存在数据依赖关系的操作做重排序,一般重排序可能发生在没有数据依赖关系的指令之中。就算数据之间没有依赖关系,在多线程场景中若发生指令重排序优化,依然存在影响到最终执行结果的情况。当多线程场景下存在这种情况时,就需要使用volatile关键字等其他手段去禁止编译器和处理器对指令重排序优化。
实现原理是在编译器在生成字节码时,会在指令序列中插入内存屏障禁止在内存屏障前后的指令执行重排序优化。
回来到一开始的案例问题,给线程类的共享变量 private static Integer num = 10; 加上volatile关键字能实现线程安全吗?
private static volatile Integer num = 10;
答案是:不能
原因是:指令【num--】看似一条指令,其实分成三步执行:先获取、再计算、后保存,所以自减和自增都不是原子性操作,而volatile 无法确保其原子性操作,所以使用volatile关键字无法确保该情况下的线程安全,需要使用锁来实现原子性操作。
2.2.使用synchronized关键字
synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,也称为监视器锁。synchronized 同步代码块中的操作被看为原子性操作。同步锁是一种排它锁、独占锁、可重入锁,当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁,并支持锁释放后可以再次获取锁。
使用方式
private Integer num = 1;private static Integer num2 = 1;public synchronized void test1(){ System.out.println("普通方法" + num++);}public static synchronized void test2(){ System.out.println("静态方法" + num2++);}public void test1_1(){ synchronized (this){ System.out.println("锁当前对象"+ num++); } synchronized (TestSynchronized.class){ System.out.println("锁当前类" + num++); } synchronized (num){ System.out.println("锁指定变量" + num++); }}
synchronized 关键字加在普通方法时,是给当前实例对象上锁;
synchronized 关键字加在静态方法时,是给当前Class类对象上锁;
synchronized 关键字加在同步代码块时,是给括号里配置的对象上锁。
注意:这个Class类对象指的是每个类在类加载过程中生成的Class类。
原理简析
在synchronized的同步代码块中,入口处执行了monitorenter,在其出口执行了monitorexit,虚拟机中的每个Object实例都有一个monitor(监视器锁);而同步方法通过字节码flags 标记该方法为ACC_SYNCHRONIZED,表明执行该方法时需要获取到监视器锁才可以执行。两者的本质都是对一个对象的监视器(monitor)的获取和释放。
可以使用javap -c -v xxx.class
查看字节码信息
同步代码块
同步方法
关于一开始的例子,可以写成
public class Demo1 { public static void main(String[] args) { // 简单模拟20人抢优惠 for(int i=0;i<20;i++){ new Thread(new ThreadDemo()).start(); } }}// 前十位可以获取优惠,凭号码兑换优惠class ThreadDemo implements Runnable{ private static Integer num = 10; @Override public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (num){ if(num<=0){ System.out.println("已被抢完,下次再来"); return; } System.out.println(Thread.currentThread().getName()+"用户抢到的号码:"+num--); } }}
或者改为
synchronized (ThreadDemo.class){ if(num<=0){ System.out.println("已被抢完,下次再来"); return; } System.out.println(Thread.currentThread().getName()+"用户抢到的号码:"+num--);}
注意:num 变量是static变量,是属于类的变量,需要锁住变量对象或Class类,单独或组合使用synchronized (this) 和volatile都无法确保其线程安全,这个可自行验证。
案例:对象单例实现
public class SingletonDemo { private static volatile SingletonDemo singletonDemo; public SingletonDemo(){ } public static SingletonDemo getInstance(){ if(singletonDemo==null) synchronized (SingletonDemo.class){ if (singletonDemo==null) singletonDemo = new SingletonDemo(); } return singletonDemo; }}// 测试class Demo{ public static void main(String[] args) { Demo demo = new Demo(); for(int i=0;i<10000;i++){ demo.test(); } } public void test(){ new Thread(()->{ SingletonDemo instance = SingletonDemo.getInstance(); System.out.println(Thread.currentThread().getName()+"="+instance); }).start(); }}
说明:volatile 和 synchronized实现双重锁校验。synchronized 起到指令执行的原子性和同一时间只能单个线程执行;synchronized和volatile都起到内存可见性保证;volatile起到禁止指令重排序,指令重排序导致线程不安全的可能性较小,但存在可能发生。
除了简单易用的synchronized 同步锁之外,还有其他更灵活的锁。
篇幅原因,将在下一篇讲述关于Java中的锁:
以上就是一文搞懂Java中的线程安全与线程同步的详细内容,更多关于Java线程安全 线程同步的资料请关注盛行IT其它相关文章!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。