java 并发包 -synchronized

来源:互联网 发布:linux双机热备方案 编辑:程序博客网 时间:2024/06/05 23:57

首先先说明一点的是 synchronized并不是并发包里的,因为synchronized也用于高并发的情况,所以放到这里介绍。
synchronized 是可以方法、代码块的锁,同时只能由一个线程进行占有,并且使得变量具有内存可见性。怎么用就不多说了,大家都会用。但是synchronized的原理,就不一定都知道了。
synchronized 分为
1 偏向锁
2 轻量级锁
3 重量级锁
再介绍锁之前,先来了解下下两个概念:对象头和monitor。

什么是对象头?
首先 对象在内存中存储的布局可以分为三个区域:对象头(Header),实例数据(Instance Data),和对齐填充(Padding)。
对象头又分为两部分
第一部分(Mark Word):用于存储对象自身的运行时数据,如哈希吗、GC分代年龄、锁状态标识、线程持有锁、偏向线程ID、偏向时间戳等。

Mark Word被设计为非固定的数据结构,以便在及小的空间内存储更多的信息。比如:在32位的hotspot虚拟机中:如果对象处于未被锁定的情况下。mark word 的32bit空间中有25bit存储对象的哈希码、4bit存储对象的分代年龄、2bit存储锁的标记位、1bit固定为0。而在其他的状态下(轻量级锁、重量级锁、GC标记、可偏向)下对象的存储结构为

这里写图片描述
另一部分:是类型指针。

什么是monitor?
monitor是线程私有的数据结构,每一个线程都有一个可用monitor列表,同时还有一个全局的可用列表,内部包含如下几个部分:
Owner:初始时为NULL表示当前没有任何线程拥有该monitor,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor失败的线程。
RcThis:表示blocked或waiting在该monitor上的所有线程的个数。
Nest:用来实现重入锁的计数。
HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值:0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。
那么monitor的作用是什么呢?在 java 虚拟机中,线程一旦进入到被synchronized修饰的方法或代码块时,指定的锁对象通过某些操作将对象头中的LockWord指向monitor 的起始地址与之关联,同时monitor 中的Owner存放拥有该锁的线程的唯一标识,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码。

偏向锁:
当线程访问同步方法method1时,会在对象头(SynchronizedTest.class对象的对象头)和栈帧的锁记录中存储锁偏向的线程ID,下次该线程在进入method2,只需要判断对象头存储的线程ID是否为当前线程,而不需要进行CAS操作进行加锁和解锁(因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟)。

public class SynchronizedTest {    private static Object lock = new Object();    public static void main(String[] args) {        method1();        method2();    }    synchronized static void method1() {}    synchronized static void method2() {}}

轻量级锁
线程可以通过两种方式锁住一个对象:

通过膨胀一个处于无锁状态(状态位001)的对象获得该对象的锁;
对象处于膨胀状态(状态位00),但LockWord指向的monitor的Owner字段为NULL,则可以直接通过CAS原子指令尝试将Owner设置为自己的标识来获得锁。
获取锁(monitorenter)的大概过程:

对象处于无锁状态时(LockWord的值为hashCode等,状态位为001),线程首先从monitor列表中取得一个空闲的monitor,初始化Nest和Owner值为1和线程标识,一旦monitor准备好,通过CAS替换monitor起始地址到LockWord进行膨胀。如果存在其它线程竞争锁的情况而导致CAS失败,则回到monitorenter重新开始获取锁的过程即可。
对象已经膨胀,monitor中的Owner指向当前线程,这是重入锁的情况(reentrant),将Nest加1,不需要CAS操作,效率高。
对象已经膨胀,monitor中的Owner为NULL,此时多个线程通过CAS指令试图将Owner设置为自己的标识获得锁,竞争失败的线程则进入第4种情况。
对象已经膨胀,同时Owner指向别的线程,在调用操作系统的重量级的互斥锁之前自旋一定的次数,当达到一定的次数如果仍然没有获得锁,则开始准备进入阻塞状态,将rfThis值原子加1,由于在加1的过程中可能被其它线程破坏对象和monitor之间的联系,所以在加1后需要再进行一次比较确保lock word的值没有被改变,当发现被改变后则要重新进行monitorenter过程。同时再一次观察Owner是否为NULL,如果是则调用CAS参与竞争锁,锁竞争失败则进入到阻塞状态。

释放锁(monitorexit)的大概过程:

检查该对象是否处于膨胀状态并且该线程是这个锁的拥有者,如果发现不对则抛出异常。
检查Nest字段是否大于1,如果大于1则简单的将Nest减1并继续拥有锁,如果等于1,则进入到步骤3。
检查rfThis是否大于0,设置Owner为NULL然后唤醒一个正在阻塞或等待的线程再一次试图获取锁,如果等于0则进入到步骤4。
缩小(deflate)一个对象,通过将对象的LockWord置换回原来的HashCode等值来解除和monitor之间的关联来释放锁,同时将monitor放回到线程私有的可用monitor列表。

public class SynchronizedTest implements Runnable {    private static Object lock = new Object();    public static void main(String[] args) {        Thread A = new Thread(new SynchronizedTest(), "A");        A.start();        Thread B = new Thread(new SynchronizedTest(), "B");        B.start();    }    @Override    public void run() {        method1();        method2();    }    synchronized static void method1() {}    synchronized static void method2() {}}

重量级锁

当锁处于这个状态下,其他线程试图获取锁都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程。
总结:
1、可以只对需要同步的使用
2、与wait()/notify()/nitifyAll()一起使用时,比较方便
这部分涉及到的jvm的内容,如果熟悉jvm的对象创建及存储,应该能很容易明白
转载地址

原创粉丝点击