c#发展

注册

 

发新话题 回复该主题

synchronized原理剖析 [复制链接]

1#

并发编程存在什么问题?

1可见性

可见性:是指当一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的最新值。

案例演示:一个线程A根据boolean类型的标记flag,while死循环;另一个线程B改变这个flag变量的值;那么线程A并不会停止循环。

java

/**案例演示:一个线程对共享变量的修改,另一个线程不能立即得到最新值*/publicclassTest01Visibility{//多个线程都会访问的数据,我们称为线程的共享数据privatestaticbooleanflag=false;publicstaticvoidmain(String[]args)throwsInterruptedException{//t1线程不断的来读取run共享变量的取值Threadt1=newThread(()-{while(flag){      }});t1.start();Thread.sleep();//t2线程对该共享变量的取值进行修改Threadt2=newThread(()-{flag=false;System.out.println("时间到,线层2设置为false");});t2.start();//可以观测得到t2线程对flag共享变量的修改,t1线程并不能够读取到更改了之后的值,导致程序不能停止;//这就出现了可见性问题}}

解决可见性:

Ⅰ.在共享变量前面加上volatile关键字修饰;

Q:为什么volatile关键字能保证可见性?

volatile的底层实现原理是内存屏障(MemoryBarrier),保证了对volatile变量的写指令后会加入写屏障,对volatile变量的读指令前会加入读屏障。

写屏障(sfence)保证在写屏障之前的,对共享变量的改动,都同步到主存当中;

读屏障(lfence)保证在读屏障之后,对共享变量的读取,加载的是主存中最新数据;

为什么volatile关键字能解决有序性看下文有序性部分。

Ⅱ.在死循环内写一个synchronized同步代码块,因为synchronized同步时会对应JMM中的lock原子操作,lock操作会刷新工作内存中的变量的值,得到共享内存(主内存)中最新的值,从而保证可见性。

Q:为什么synchronized同步代码块能保证可见性?

synchronized同步的时候会对应8个原子操作当中的lock与unlock这两个原子操作,lock操作执行时该线程就会去主内存中获取到共享变量最新值,刷新工作内存中的旧值,从而保证可见性。

java

Threadt1=newThread(()-{while(run){synchronized(obj){    //死循环内加一个同步代码块}}});t1.start();//或者Threadt1=newThread(()-{while(run){//输出语句也能保证可见性?//因为PrintStream.java中的println(booleanx)方法中也使用到了synchronized,synchronized能保证可见性System.out.println();}});t1.start();

小结:

可见性(Visibility):是指当一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的最新值。

synchronized可以保证可见性,但缺点是synchronized锁属于重量级操作,性能相对更低。

2原子性

原子性(Atomicity):在一次或多次操作中,要么所有的操作都执行,并且不会受其他因素干扰而中断,要么所有的操作都不执行;

案例演示:5个线程各执行次i++操作:

java

/**案例演示:5个线程各执行次i++;*/publicclassTest02Atomicity{privatestaticintnumber=0;publicstaticvoidmain(String[]args)throwsInterruptedException{//5个线程都执行次i++Runnableincrement=()-{for(inti=0;i;i++){number++;}};//5个线程ArrayListThreadts=newArrayList();for(inti=0;i5;i++){Threadt=newThread(increment);t.start();ts.add(t);}for(Threadt:ts){t.join();}/*最终的效果即,加出来的效果不是,可能会少于那么原因就在于i++并不是一个原子操作下面会通过java反汇编的方式来进行演示和分析,这个i++其实有4条指令*/System.out.println("number="+number);}}

Idea中找到target目录,找到当前java文件的字节码.class文件,该目录下打开cmd,输入javap-p-vxxx.class,得到字节码指令,其中,number++对应的字节码指令为:

java

9etstatic#18//Fieldnumber:I  获取静态变量的值12:iconst_1  //准备一个常量:iadd    //让静态变量和1做相加操作14:putstatic#18//Fieldnumber:I    把相加后的结果赋值给静态变量

number++是由四条字节码指令组成的,那么在一个线程下是没有问题的,但如果是放在多线程的情况下就有问题,比如线程A在执行13:iadd前,CPU又切换到另外一个线程B,线程B执行了9etstatic,就会导致两次number++,但实际上只加了1。

这个问题的原因就在于让两个线程来进行操作number++,而number++的字节码指令又是多条指令(4条指令),其中一个线程执行到一半时,CPU又切换到另外一个线程,另外一个线程来执行,读取到的值依然跟另一个线程一样,即第二个线程干扰了第一个线程的执行从而导致执行结果的错误,没有保证原子性。

解决原子性:

synchronized可以保证number++的原子性。synchronized能够保证在同一时刻最多只有一个线程执行该段代码,已保证并发安全的效果。

java

synchronized(obj){number++;}

加了synchronized同步代码块后,每次运行的结果都是.

Idea中找到target目录,找到当前java文件的字节码.class文件,该目录下打开cmd,输入javap-p-vxxx.class,得到字节码指令,其中,num++对应的字节码指令还是中间的四条,不过上下新增了几条指令(后文会讲到):

java

14:monitorenter15etstatic#18//Fieldnumber:I18:iconst_:iadd20:putstatic#18//Fieldnumber:I23:aload_:monitorexit

小结:

原子性(Atomicity):在一次的操作或多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。

原子性可以通过synchronized同步代码块或ReentrantLock来解决。

3有序性

有序性(Ordering):是指程序代码在执行过程中的先后顺序,由于java在编译器以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码的顺序。

Q:为什么要重排序?

一般会认为编写代码的顺序就是代码最终的执行顺序,那么实际上并不一定是这样的,为了提高程序的执行效率,java在编译时和运行时会对代码进行优化(JIT即时编译器),会导致程序最终的执行顺序不一定就是编写代码时的顺序。

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段;

解决有序性:

Ⅰ.可以使用synchronized同步代码块来保证有序性;

Q:synchronized保证有序性的原理是?

加了synchronized,依然会发生指令重排序(可以看看DCL单例模式),只不过,由于存在同步代码块,可以保证只有一个线程执行同步代码块当中的代码,也就能保证有序性。

Ⅱ.除了可以使用synchronized来进行解决,还可以给共享变量加volatile关键字来解决有序性问题。

volatile如何保证有序性的?

写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后;

读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前;

总结

synchronized可以保证原子性、有序性和可见性,而volatile只能保证有序性和可见性;

synchronized是个重量级锁,应尽量少使用;

Java内存模型

定义

java内存模型(即javaMemoryModel,简称JMM),主要分成两部分来看,一部分叫做主内存,另一部分叫做工作内存。

java当中的共享变量;都放在主内存当中,如类的成员变量(实例变量),还有静态的成员变量(类变量),都是存储在主内存中的。每一个线程都可以访问主内存;

每一个线程都有其自己的工作内存,当线程要执行代码的时候,就必须在工作内存中完成。比如线程操作共享变量,它是不能直接在主内存中操作共享变量的,只能够将共享变量先复制一份,放到线程自己的工作内存当中,线程在其工作内存对该复制过来的共享变量处理完后,再将结果同步回主内存中去。

主内存

主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存;

共享变量主要包括类当中的成员变量,以及一些静态变量等。局部变量是不会出现在主内存当中的,因为局部变量只能线程自己使用;

工作内存

每一个线程都有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有读写操作都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量;

线程对共享变量的操作都是对其副本进行操作,操作完成之后再同步回主内存当中去;

作用

主要目的就是在多线程对共享变量进行读写时,来保证共享变量的可见性、有序性、原子性;在编程当中是通过两个关键字synchronized和volatile来保证共享变量的三个特性的。

主内存与工作内存如何交互

一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存的呢?

Java内存模型中定义了上图中的8种操作(橙色箭头)来完成,虚拟机实现时必须保证每一种操作都是原子的、不可再分的。

举个:

java

假设现在线程1想要来访问主内存当中的共享变量x,即当前主内存中的共享变量x的取值为booleanx=true;线程1首先会做一个原子操作叫做Read,读取主内存当中的共享变量x的取值,即booleanx=true;接下来就是Load操作,把在主内存中读取到的共享变量加载到了工作内存当中(副本);接着执行Use操作,如果线程1需要对共享变量x进行操作,即会取到从主内存中加载过来的共享变量x的取值去进行一些操作;操作之后会有一个新的结果返回,假设令这个共享变量的取值变为false,完成Assign操作,即给共享变量x赋新值;操作完成之后;就需要同步回主内存,首先会完成一个Store的原子操作,来保存这个处理结果;接着执行Write操作,即在工作内存中,Assign赋值给共享变量的值同步到主内存当中,主内存中共享变量取值x由true更改为false。--------------------------------另外还有两个与锁相关的操作,Lock与unlock,比如说加了synchronized,才会产生有lock与unlock操作;如果对共享变量的操作没有加锁,那么也就不会有lock与unlock操作。

注意:

如果对共享变量执行lock操作,该线程就会去主内存中获取到共享变量的最新值,刷新工作内存中的旧值,保证可见性;(加锁说明要对这个共享变量进行写操作了,先刷新旧值,再操作新值)

对共享变量执行unlock操作,必须先把此变量同步回主内存中,再执行unlock;(因为对共享变量释放锁,接下来其他线程就能访问到这个共享变量,就必须使这个共享变量呈现的是最新值)

这两点就是synchronized为什么能保证“可见性”的原因。

小结

主内存与工作内存之间的数据交互过程(即主内存与工作内存的交互是通过这8个原子操作来保证数据的正确性的):

lock→read→load→use→assign→store→write→unlock

synchronized的特性

synchronized作为悲观锁,具有两个特性,一个是可重入性,一个是不可中断性。

1可重入

定义

指的是同一个线程的可以多次获得同一把锁(一个线程可以多次执行synchronized,重复获取同一把锁)。

java

/*可重入特性指的是同一个线程获得锁之后,可以再次获取该锁。*/publicclassDemo01{publicstaticvoidmain(String[]args){RunnablesellTicket=newRunnable(){

Overridepublicvoidrun(){synchronized(Demo01.class){System.out.println("我是run");test01();}}publicvoidtest01(){synchronized(Demo01.class){System.out.println("我是test01");}}};newThread(sellTicket).start();newThread(sellTicket).start();}}

原理

synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁,每重入一次,计数器就+1,在执行完一个同步代码块时,计数器数量就会减1,直到计数器的数量为0才释放这个锁。

优点

可以避免死锁(如果不能重入,那就不能再次进入这个同步代码块,导致死锁);

更好地封装代码(可以把同步代码块写入到一个方法中,然后在另一个同步代码块中直接调用该方法实现可重入);

2不可中断

定义

线程A获得锁后,线程B要想获得锁,必须处于阻塞或等待状态。如果线程A不释放锁,那线程B会一直阻塞或等待,阻塞等待过程中,线程B不可被中断。

synchronized是不可中断的,处于阻塞状态的线程会一直等待锁。

案例演示

java

publicclassDemo02_Uninterruptible{privatestaticObjectobj=newObject();    //定义锁对象publicstaticvoidmain(String[]args){//1.定义一个RunnableRunnablerun=()-{//2.在Runnable定义同步代码块;同步代码块需要一个锁对象;synchronized(obj){//打印是哪一个线程进入的同步代码块Stringname=Thread.currentThread().getName();System.out.println(name+"进入同步代码块");Thread.sleep();}};//3.先开启一个线程来执行同步代码块Threadt1=newThread(run);t1.start();//保证第一个线程先去执行同步代码块Thread.sleep();/**4.后开启一个线程来执行同步代码块(阻塞状态)到时候第二个线程去执行同步代码块的时候,锁已经被t1线程锁获取得到了;所以线程t2是无法获取得到Objectobj对象锁的;那么也就将在同步代码块外处于阻塞状态。*/Threadt2=newThread(run);t2.start();/**5.停止第二个线程;观察此线程t2能否被中断;*/System.out.println("停止线程前");t2.interrupt();  //通过interrupt()方法给t2线程进行强行中断System.out.println("停止线程后");//最后得到两个线程的执行状态System.out.println(t1.getState());  //TIMED_WAITINGSystem.out.println(t2.getState());  //BLOCKED}}//运行结果:Thread-0进入同步代码块停止线程前停止线程后TIMED_WAITINGBLOCKED    //t2的状态依然为BLOCKED,说明synchronized是不可被中断的

结果分析:

通过interrupt()方法让t2线程强行中断,最后打印t2的状态,依然为BLOCKED,即线程不可中断。

对比ReentrantLock

ReentrantLock的lock方法是不可中断的,tryLock方法是可中断的。

Ⅰ.演示ReentrantLock不可中断:

java

publicclassDemo03_Interruptible{//创建一个Lock对象privatestaticLocklock=newReentrantLock();publicstaticvoidmain(String[]args)throwsInterruptedException{test01();}//演示Lock不可中断publicstaticvoidtest01(){Runnablerun=()-{Stringname=Thread.currentThread().getName();try{lock.lock();  //lock()无返回值System.out.println(name+"获得锁,进入锁执行");Thread.sleep();}catch(InterruptedExceptione){e.printStackTrace();}finally{lock.unlock();  //unlock也是没有返回值的System.out.println(name+"释放锁");}};Threadt1=newThread(run);t1.start();Thread.sleep();Threadt2=newThread(run);t2.start();System.out.println("停止t2线程前");t2.interrupt();System.out.println("停止t2线程后");Thread.sleep();System.out.println(t1.getState());System.out.println(t2.getState());}}--------------------------------------------------运行效果:Thread-0获得锁,进入锁执行停止t2线程前停止t2线程后TIMED_WAITING  //t1线程在临界区睡ms,有时限的等待WAITING    //t2线程处于等待状态,WAITING

Ⅱ.演示ReentrantLock可中断:

java

publicclassDemo03_Interruptible{privatestaticLocklock=newReentrantLock();publicstaticvoidmain(String[]args)throwsInterruptedException{test02();}//演示Lock可中断publicstaticvoidtest02()throwsInterruptedException{Runnablerun=()-{Stringname=Thread.currentThread().getName();booleanb=false;try{b=lock.tryLock(3,TimeUnit.SECONDS);//说明尝试获取得到了锁;则进入if块当中if(b){System.out.println(name+"获得锁,进入锁执行");Thread.sleep();}else{//没有获取得到锁执行else,证明了Lock.tryLock()是可中断的;System.out.println(name+"在指定时间内没有获取得到锁则做其他操作");}}catch(InterruptedExceptione){e.printStackTrace();}finally{if(b){  //得到了锁才释放锁lock.unlock();System.out.println(name+"释放锁");}}};Threadt1=newThread(run);t1.start();Threadt2=newThread(run);t2.start();}}--------------------------------------------------------代码执行效果:Thread-0获得锁,进入锁执行Thread-1在指定时间没有得到锁做其他操作

小结

synchronized和ReentrantLock都是可重入锁;

synchronized获取不到锁会阻塞等待,该过程不可中断,而ReentrantLock的lock方法不可中断,tryLock方法是可中断的。

synchronized底层

首先通过javap反汇编的方式来学习synchronized原理:

举个例子:

java

publicclassDemo01{//依赖的锁对象privatestaticObjectobj=newObject();

Overridepublicvoidrun(){for(inti=0;i;i++){//synchronized同步代码块;且在代码块当中做了简单的打印操作;//重点是看synchronized在反汇编之后形成的字节码指令synchronized(obj){System.out.println("1");}}}//编写了一个synchronized修饰的方法//synchronized修饰代码块与synchronized修饰方法反汇编之后的结果是不太一样的;publicsynchronizedvoidtest(){System.out.println("a");}};//代码写好之后让idea编译得到字节码文件;//编译好的字节码文件目录:工程名/target/classes/xxx/demo04_synchronized_monitor/Demo01.class

找到target目录下的.class文件,cmd下输入javap-p-vxxx.class进行反编译,得到字节码指令:

java

publicstaticvoidmain(java.lang.String[]);descriptor[Ljava/lang/String;)Vflags:ACC_PUBLIC,ACC_STATICCode:stack=2,locals=3;args_size=10etstatic#2//Fieldobjjava/lang/Object;3up4:astore_15:monitorenter6etstatic#3//Fieldjava/lang/System.outjava/io/PrintStream;9:1dc#4//String:invokevirtual#5//Methodjava/io/PrintStream.printlnLjava/lang/String;)V14:aload_:monitorexit16:goto:astore_:aload_:monitorexit  //这个意思是说当同步代码块内出现异常时,会自动帮我们释放锁22:aload_:athrow24:returnExceptiontableromtotargettypeanyany

monitorenter

每一个synchronized锁对象都会和一个监视器monitor关联,监视器被占用时会被锁住,其他线程无法来获取该monitor(这个monitor才是真正的锁),当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权(即尝试去获取这把锁;有可能获取到,也有可能获取不到),过程如下:

若monitor的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者);

若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1;

若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权;

monitor内部有两个重要的成员变量:

owner:拥有这把锁的线程;

recursions:记录线程拥有锁的次数

monitorexit

能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。

执行monitorexit时会将monitor的进入数(已重入次数)减1,当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

monitorexit插入在方法结束处和异常处(查看上面的字节码指令会发现有两个monitorexit),jvm保证每个monitorenter必须有一个对应的monitorexit。

Q:synchronized代码块内出现异常会释放锁吗?

A:会自动释放锁,查看字节码指令可以知道,monitorexit插入在方法结束处和异常处。从Exceptiontable异常表中也可以看出。

java

Exceptiontableromtotargettypeanyany//from...to:指的是从哪一行到哪一行;即指的是6~16行或者19~22行之间的字节码指令,//出现了异常则会去执行19行及以后的代码,19后有个monitorexit指令,说明若在同步代码块当中出现了异常,monitor会自动帮助释放锁

同步方法

上面介绍的是synchronized同步代码块内的情况,当synchronized修饰方法时,查看反编译后的字节码指令,可以看到同步方法在反汇编后,会增加ACC_SYNCHRONIZED,会隐式地调用monitorenter和monitorexit,即在执行同步方法之前会调用monitorenter,在执行完同步方法后会调用monitorexit;

源码:

java

publicsynchronizedvoidtest(){System.out.println("a");}

javapublicsynchronizedvoidtest();descriptor)Vflags:ACC_PUBLIC,ACC_SYNCHRONIZEDCode:stack=2,locals=1,args_size=10:getstatic#3//Fieldjava/lang/System.outjava/io/PrintStream;3:1dc#6//Stringa5:invokevirtual#5//Methodjava/io/PrintStream.println(Ljava/lang/String;)VLineNUmberTablene13:0line14:0LocalVariableTalbe:StartLengthSlotNameSignaturethisL

分享 转发
TOP
发新话题 回复该主题