synchronzied是JVM内置的同步器,用于实现多线程环境下临界资源的同步互斥访问。
设计同步器的意义
在多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之为临界资源,这种资源可能是:对象、变量、文件等。
共享:资源可以由多个线程同时访问。
可变:资源可以在其生命周期内被修改。
引出的问题:由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问。
如何解决线程安全问题
实际上,所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源。即在同一时刻,只能由一个线程访问临界资源,也称作同步互斥访问。
Java中提供了两种方式来实现同步互斥访问:synchronized和Lock。
synchronized是JVM级别的同步器,Lock是基于AQS(抽象队列同步器)实现的API级别的同步器。
同步器的本质就是加锁。
加锁目的:序列化访问临界资源,即同一时刻只有一个线程访问临界资源(同步互斥访问)。
不过有一点需要区别的是:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量在每个线程的私有栈中,因为不具有共享性,不会导致线程安全问题。
synchronized原理详解
synchronized内置锁是一种对象锁,锁的是对象而非引用,作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。
加锁的方式
- synchronized修饰实例方法,锁是当前实例对象
- synchronized修饰类方法,锁是当前类的Class对象
- synchronized修饰代码块,锁是括号里面指定的对象
synchronized底层原理
synchronized是JVM内置同步互斥锁,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex Lock(互斥锁)实现,它是一个重量级锁,性能较低。当然,JVM内置锁在JDK 1.5之后版本做了重大的优化,如锁粗化、锁消除、轻量级锁、偏向锁等技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平。synchronized关键字被编译成字节码后会被翻译成monitorenter和monitorexit两条指令,分别在同步代码块的起始位置与结束位置。
每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下图所示:
Monitor监视器锁
任何一个对象都有一个Monitor与之关联,当一个Monitor被持有后,它将处于锁定状态。
synchronized在JVM里的实现都是基于进入和退出Monitor对象过来实现方法同步和代码同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。
什么是Monitor
可以理解为一个同步工具,它可以描述为一种同步机制,它通常被描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁,也就是通常说的synchronized的对象锁,MarkWork锁标志位为10,指针指向的是Monitor对象的起始地址。
MonitorEnter
每个对象都是一个监视器锁,当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
- 如果本线程已经占有monitor,只是重入monitor,则monitor的进入数加1;
- 如果其他线程已经占有了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,在重新尝试获取monitor的所有权。
MonitorExit
- 执行monitorexit的线程必须是持有monitor的线程
- 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再持有monitor,其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
看一个同步方法:
publicclassSynchronizedMethod{publicsynchronizedvoidmethod(){
System.out.println("Hello World!");
}
}
反编译结果:
从编译的结果来看,方法的同步并没有通过monitorenter和monitorexit指令来完成,不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标识符,JVM就是根据该标识符来实现方法的同步的。
当方法调用时,调用指令会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法体执行完成之后再释放monitor,在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起,等待重新调度,会导致用户态和内核态两个态之间来回切换,对性能有较大影响。
什么是Monitor
在Java HopSpot虚拟机中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):
ObjectMonitor(){_header=NULL;_count=0;//记录个数_waiters=0,_recursions=0;_object=NULL;_owner=NULL;_WaitSet=NULL;//处于wait状态的线程,会被加入到_WaitSet_WaitSetLock=0;_Responsible=NULL;_succ=NULL;_cxq=NULL;FreeNext=NULL;_EntryList=NULL;//处于等待锁block状态的线程,会被加入到该列表_SpinFreq=0;_SpinClock=0;OwnerIsThread=0;}
ObjectMonitor中有两个队列,_WaitSet(等待池)和_EntryList(锁池),用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象),_Owner执行过持有ObjectMonitor的对象,当多个线程同时访问一段同步代码时:
- 首先会进入_EntryList队列,当线程获取到对象的Monitor后,将monitor中的_Owner设置为当前线程个,同时monitor中的计数器加1;
- 若线程调用wait方法,将释放当前持有的monitor,_Owner变量恢复成null,count自减1,同时该线程进入_WaitSet队列中等待被唤醒;
- 若当前线程执行完毕,_Owner变量恢复成null,并复位count的值,以便其他线程进入获取monitor(锁)。
同时,Monitor对象存储在Java对象的对象头Mark Word中(存储指针指向),synchronized锁便是通过这种方式获取的。也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法也会使用到Monitor锁对象,所以必须在同步代码块中使用。
监视器有两种同步方式:互斥与协作。
多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。
那么有个问题来了,我们知道synchronized加锁加在对象上,对象是如何记录锁状态的呢?答案是锁状态是被记录在每个对象的对象头(Mark Word)中,下面我们一起来认识对象的内存布局。
对象的内存布局
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头、实例数据、对齐填充。
- 对象头:存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、偏向线程ID等。
- 实例数据:存储类的属性、数据信息,包括父类的属性信息
- 对齐填充:由于虚拟机要求对象的起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。
锁优化
JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁轻量级锁等技术来减少锁操作的开销。
锁主要存在四种状态,无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级为重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。从JDK1.6默认是开启偏向锁和轻量级锁的,可以通过-XX:UseBiasedLocking来禁用偏向锁。
偏向锁
偏向锁是Java 1.6 之后引入的,它是一种针对加锁操作优化手段。经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,当线程再次请求锁是,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而提升了程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这种场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,值得注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
轻量级锁
如果偏向锁失败,虚拟机并不会立即升为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。轻量级锁能够提升程序性能的依据是对绝大部分的锁,在整个同步周期内都不存在竞争,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。锁消除的依据是逃逸分析的数据支持。
锁消除,前提是程序必须运行在server模式后,同时必须开启逃逸分析。
-XX:+DoEscapeAnalysis开启逃逸分析-XX:+EliminateLocks表示开启锁消除
使用逃逸分析,编译器可以对代码做如下优化:
- 同步省略。如果一个对象发现只能被同一个线程访问到,那么对于这个对象的操作可以不考虑同步。
- 将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配,而不是堆分配;
- 分离对象或者标量替换。有的对象可能不需要作为一个连续的内存结构也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
是不是所有的对象和数组都会在堆内存分配空间?不一定。
在Java代码运行时,通过JVM参数可指定是否开启逃逸分析。
-XX:+DoEscapeAnalysis:开启逃逸分析
-XX:-DoEscapeAnalysis:关闭逃逸分析
声明:本文部分素材转载自互联网,如有侵权立即删除 。
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需!
7. 如遇到加密压缩包,请使用WINRAR解压,如遇到无法解压的请联系管理员!
8. 精力有限,不少源码未能详细测试(解密),不能分辨部分源码是病毒还是误报,所以没有进行任何修改,大家使用前请进行甄别
丞旭猿论坛
暂无评论内容