Java并发专题程序员

Java并发之synchronized关键字

2018-12-12  本文已影响26人  第四单元

一.基本使用和语义

synchronized可以用于修饰方法或代码块。修饰代码块时锁是后面括号里的对象。修饰方法分为实例方法和静态方法。修饰实例方法时锁是调用方法的对象;修饰静态方法时锁是类对象。再解释一下这个类对象。在java中'一切'都是对象,连对象的模板——类本身也是对象,存储在虚拟机运行时数据区域的方法区。

synchronized是java从语言层面提供的,通过‘互斥同步’来实现并发正确性的机制。它的语义是只有获得锁之后才能执行相应的代码,每次只能有一个线程获取锁。需要注意的是对一个线程来说synchronized同步块是可重入的,对不同线程是互斥的。具体表现如,一个线程获取到synchronized锁进入到一个实例方法中后,其它线程则不能再进入同一个方法或本对象的其它实例方法,因为它们都由同一个锁绑定。同时获取到锁的线程还可以继续调用其它synchronized方法,不会出现自己把自己锁死的情况。

原子性:
基本数据类型的访问读写是具备原子性的;
synchronized反映到字节码的层面是monitorenter和monitorexit。在synchronized块之间的操作也具备原子性。

可见性:
可见性是指一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
volatile、synchronized、final可以实现可见性。
volatile通过变量修改会将新值立即同步回主内存,来保证可见性;
synchronized通过‘对一个变量执行unlocak操作之前,必须先把此变量同步回主内存中’保证可见性。
final:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this的引用传递出去,那在其它线程中就能看见final字段的值

有序性:
volatile、synchronized
volatile本身有禁止指令重排序,保证在其之前的语句在其之前执行,其之后的语句在其之后执行。
synchronized:‘一个变量在同一时刻只允许一条线程对其进行lock操作’,这条规则则决定了持有同一个锁的两个同步块只能串行进入。

二.实现原理

Java的线程是映射到操作系统的线程之上的,也就是说线程的调度、阻塞、唤醒等都需要操作系统来帮忙完成,这就涉及到用户态到内核态的切换。

Java中的每个对象都有作为锁的潜质,这是通过Java对象的对象头实现的。Java对象的存储结构由三部分组成对象头、实例数据和填充数据。其中对象头信息又包括Mark Word、类元信息。其中类元信息是指向对象所属类对象的指针。

Mark Word中是一些状态信息,包括锁标志位:重量级锁(synchronized),偏向锁,轻量级锁和GC标记。当处于重量级锁10状态时就是有线程获取了synchronized锁,这时Mark word中还有记录指向monitor对象的指针。对于每一个对象都有一个monitor对象与之对应。monitor对象是实现synchronized的关键。它是使用操作系统互斥量来实现的。维持一个Entry_list记录阻塞的线程队列和一个Wait_list记录调用了wait()方法的队列。

三.锁优化

synchronized是基于互斥同步的,这种实现设计到线程的阻塞,而挂起和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力。所以有了以下JVM的优化措施。

3.1自旋锁与自适应锁

获取锁的的时候,如果不成功,不是马上放弃处理时间进入阻塞状态,而是自己循环等待一下。因为阻塞的代价较大,而且大多数时候锁很快就能释放,自旋等待容易等到锁的释放。

自适应的自旋锁,自旋的时间不再固定,如果上一次自旋获得了锁,那么下一次还倾向于认为能自旋成功,所以可以多等一会。反之,如果对于某个锁自旋很少成功,那么之后获取这个锁时倾向于减少等待时间甚至取消自旋。

3.2锁消除

数据只有在被多线程访问时才可能有并发问题,如果JVM判断出在一段代码中,堆上的所有数据都不会逃逸出去而被其他线程访问到,那就可以把它们当做栈上的数据对待,认为它们是线程私有的,同步加锁就不需要了。
eg.jdk1.5之前,String的加法操作会转换为StringBuffer的append()方法,这个方法是用synchronized修饰。而有时候这是不必要的。

3.3锁粗化

消除频繁加锁的影响,如在循环加锁,可以移到循环外部,提交效率。

3.4轻量级锁

实践表明,大多数情况下同步代码并不存在多线程竞争。这种情况下再每次都使用重量级锁的互斥量,开销就很大。这里的轻量是和使用操作系统互斥量来实现的传统锁而言的。MarkWord锁标志位为00时表示处于轻量级锁状态。(锁标志位:00轻量级锁,01未锁定\可偏向,10重量级锁,11GC标志。)

过程:
在代码进入同步块时,如果此同步对象没有被锁定(标志位01),虚拟机首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储MarkWord的拷贝。
然后,虚拟机将使用CAS操作尝试将对象的mark word更新为指向Lock Record的指针。如果成功,则该线程就拥有了该对象的锁,且对象处于轻量级锁定的状态。执行完同步块代码后释放锁即可,整个过程不涉及操作系统互斥量等。如果一直没有线程竞争,就会每次都使用轻量级锁。
如果CAS操作失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步快继续执行。否则,说明这个锁被其它线程抢占了。那么轻量级锁就要膨胀为重量级锁。
解锁过程也是通过CAS操作来进行的,如果mark word仍然指向这线程的锁记录,那就用CAS操作把当前对象的mark word和栈帧中的displaced mark word换回来,如果替换成功,整个同步过程就完成了。如果失败,说明有其他线程获取过该锁,那就要在释放锁的同时,唤醒被挂起的线程。

3.5偏向锁

偏向锁的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

过程:
当锁对象第一次被线程获取的时候,虚拟机会把对象头中的标志位设置为01,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作。当有另一个线程尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向后恢复到未锁定或轻量级锁定的状态。

上一篇下一篇

猜你喜欢

热点阅读