并发编程之synchronized

  在说并发同步器synchronized之前我们先来说一下为什么会用到并发同步器。

并发时会出现多个线程同时访问一个共享可变资源,这个共享的可变资源称之为临界资源。这个资源可以是对象、变量或者文件。共享说明资源可以由多个线程同时访问;可变说明资源的生命周期内是可以修改的。由于线程在执行的过程是不可控的,所以为了保证这个临界资源的修改能够其他线程及时访问到,避免线程安全问题,要对临界资源进行序列化的访问,即在同一时刻只能由1个线程访问共享资源,即同步互斥访问。

Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock

同步器的本质就是加锁

加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)不过有一点需要区别的是:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。java中的锁按照性质可以分为显示锁和隐式锁,ReetrantLock和synchronized,  synchronized属于隐式锁,ReetrantLock属于显示锁(这次文章主要来讲我们的synchronized)。

隐式锁:Synchonized加锁机制,jvm内置锁,不需要手动加锁和解锁,jvm会自动加锁和解锁。

显式锁:ReentrantLock,实现juc里Lock,实现是基于AQS实现,需要通过手动ReentrantLock lock()加锁和unlock()解锁,ReentrantLock lock(),unlock();

synchronized原理解析

  synchronized内置锁是一种对象锁(锁的是对象而不是引用),作用粒度是对象,可以用来实现对临界资源的互斥访问,是可重入的。

加锁方式:

  同步代码块synchonized(Object object):锁括号里面的对象,几乎不可以跨方法进行加锁,跨方法加锁的话可以使用UnsafeInstance.reflectGetUnsafe().monitorEnter(object)进行加锁,UnsafeInstance.reflectGetUnsafe().monitorExit(object)进行解锁,这样的底层方式,可以通过虚拟机进行优化。Synchonized在进行字节码编译的时候会翻译成monitorEnter,monitorExit.也可以使用ReetrantLock,但是用这个是跨越虚拟机的,无优化空间。

同步实例方法static Synchonized:加锁加在类对象上;

同步类方法 ,不加static,单独使用synchronized:加锁加在this当前类对象上,当前实例对象上。当前bean由容器管理,当前bean的作用域必须是单例模式

lock操作和Unlock操作对应monitorEnter和monitorExit   

说明:synchonized保证不同的线程之间看到的是有序的执行,但是synchonized包裹的代码段内指令的有可能发生指令重排

synchronized底层原理

synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较然,JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(LightweighLocking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spin技术来减少锁操作的开销,,内置锁的并发性能已经基本与Lock持平。synchronized 如何实现解锁和开锁的呢,我们来看一下synchronized代码执行的时候会编译成字节码后被翻译成monitorenter 和 monitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置。 

其结构如下:


synchronized的粗略执行过程如下:


多线程在进行竞争锁的时候,详细流程如下:

  我们来详细讲解一下上面的图。由上图所知每个对象都有一个Monitor对象,synchronized是对象锁,当多个线程通过Monitor.enter来竞争这个锁了时候,我只能将锁分配给其中一个线程,该线程获取到该锁就会进入代码块进行执行,其余没有获取到该锁的线程就会进入同步队列里面(waitSet队列)中进行阻塞等待,等争取到锁的线程执行完成之后通过Monitor.exit释放锁,同时唤醒队列中的线程去竞争锁,如此反复,知道线程执行完成为止。

  找到jvm底层的ObjectMonitor.hpp代码,可以看到里面有定义以下信息:

header:对象头,

count:记录加锁次数,锁重入的时候使用(synchronized是可以重入的锁),假如当给一个对象多次加锁的时候,每加一次锁count就进行+1操作,当我解锁一次count就进行-1操作,0+1+1+1-1-1直到最后该值的结果为0,就说明完成了多次加锁和解锁操作。

waiters:当前有多少处于wait状态的线程

owner:当前持有ObjectMonitor对象的线程

WaitSet:处于wait状态的线程,会被加入到WaitSet中

EntryList:处于等待加锁的block状态的线程,会被加到该队列。


   由上述内容可以知道synchronized是个对象锁,在进行加锁、解锁的时候,我需要一个地方来进行记录,那对象把这些信息存放在哪里呢?这时候我们就需要来了解一下对象内存结构。对象的内存结构分为三个部分:对象头、实例数据、对齐填充位,如下图所示(图中的XXBit是根据32位操作系统来进行分配的)

每次实例化一个对象的时候就会开辟一个像上面的空间来存储对象信息。 

MetaData:元数据指针指向实例对象的class对象,这就是我们可以通过Class来获取对象的原因。

实例数据:里面就是创建对象时,对象中的变量、方法等。

对齐填充位:对象的大小必须是8字节的整数倍(强制规定,没有为啥)。

其余的从上图中都可以一目了然,不需要多解释。那么实例对象存储在内存中的哪个区域呢?理论上来讲如果实例对象存储在堆区的时候,那么实例对象内存存储在堆区,实例的引用存储在栈上,实例的元数据存储在方法区或者元空间;那么对象一定是存储在堆区的吗?不一定。当没有发生实例对象线程逃逸的时候,对象是存储在堆区的,否则的话就是存储在线程栈上。

    线程逃逸(对象逃出当前线程): 这个对象甚至可能被其它线程访问到,例如赋值给类变量或可以在其它线程中访问的实例变量

关闭逃逸分析,同时调大堆空间,避免堆内GC的发生,如果有GC信息将会被打印出来

VM运行参数:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError

开启逃逸分析

VM运行参数:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError

 有了上述内容的铺垫,接下来我们要说一下锁的自我膨胀,也就是线程从无锁-偏向锁-轻量级锁-重量级锁的升级过程,该过程不可逆。下图中的Java锁分类了解一下:

线程的膨胀是从无锁,升级到偏向锁,再升级到轻量级锁,再升级到重量级锁。

偏向锁是在单线程访问的情况下使用。此时没有必要向底层申请互斥访问。

先来说一下轻量级锁。轻量级锁就是在线程交替执行,竞争不激烈的情况下使用的,当T1和T2在执行的时候,T1先获得锁执行同步块,T1还没有执行完成,T2就开始执行,需要与T1竞争锁,但是T2与T1竞争锁的时间比较短,所以JVM会让T2自旋(空循环)等T1执行完同步块释放锁。自旋不同于阻塞,自旋不会放弃CPU使用权。java1.7以后自适应自旋。

锁的膨胀到底是怎么回事呢,我们先来看一下MardWord随着锁升级的变化:

具体的膨胀变化过程如下:

无锁-偏向锁: 当线程无锁的状态的是偏向状态是0的时候,修改markWord升级为偏向锁.

偏向锁-轻量级锁:T1获得偏向锁在执行同步块的时候,T2线程启动了,启动的时候先检查markWord中的偏向线程ID是否是自己,不是自己的话就去尝试修改ThreadID,使其指向自己,修改失败,就要求撤销偏向线程锁,等到线程T1到达安全点时(此时该线程不一定执行完成)检查线程T1的运行状态是否退出同步块,如果退出,则修改偏向状态为0,线程T2从无锁到偏向锁,将ThreadID更改为T2的ID。如果线程T1没有退出同步块,那么将偏向锁升级为轻量级锁。

轻量级锁-重量级锁:升级为轻量级锁的时候在当前线程栈中开辟一块LockRecord,同时复制markWord到LockRecord中,owner为该线程的栈指针。然后T1,T2同时让CAS修改MarkWord,假设T1修改成功,T2修改失败,T1将markWord中的修改处修改为指向线程栈中开辟的LockRecord的地址,LockRecord中的owner指向MarkWord的头,T1修改成功进入同步块的执行,T2进入自旋。T2自旋完成以后再尝试修改MarkWord,如果修改失败以后,T1还没有释放,则锁进入重量级锁。在进行升级之前会调用Pthread,向底层申请互斥量,此时就有用户态和内核态的转换,这个是一个耗时耗资源的操作。将MarkWord的头地址进行更改,T2进入阻塞。T1执行完了,要释放轻量级锁,发现MarkWord的指针是指向重量级Monitor的指针,这时候T1在释放锁的同时还需要去唤醒阻塞的线程。

以上就是synchronized的原理以及锁的膨胀过程,如有错误,欢迎小伙伴们指出,谢谢.

你可能感兴趣的:(并发编程之synchronized)