java线程(三)syschronized同步原理

作用

  • 保证原子性(同步代码中的执行不受其他线程干扰),可见性(同步代码中修改后的数据,退出同步后,对其他线程立即可见),有序性(多条线程有序执行)

用法

  • 修饰静态方法,相当于对类的class对象加锁。
  • 修饰实例方法,相当于对当前实例对象加锁。
  • 同步代码块,可以自由选定加锁对象。

知识点

  • jdk1.6之前单纯通过monitor实现锁,但因为需要切换内核态执行线程阻塞和线程唤醒等调用系统函数,性能比较差。jdk1.6之后做了优化,锁分为偏向锁、轻量锁、重量锁,底层根据不同的锁状态实现不同的同步机制,其中重量锁即通过monitor实现。1.6之后与reentrantlock性能差不多,synchronized使用相对比较简单,但是synchronized只支持非公平锁,且等锁不可中断。
  • 非公平锁。没有先来后到,线程相互竞争,依赖系统调度。非公平锁可能会出现饥饿问题,低优先级的线程一直在阻塞等锁,得不到cpu执行机会,获得锁的线程一直占用锁,或者是wait之后一直没有被唤醒。解决饥饿问题可以用公平锁
  • 获取锁的阻塞状态不可中断,因此出现死锁不可中断。

对象组成

  • 实例数据。存储类的属性数据,包括父类的数据。
  • 填充数据。虚拟机要求对象的起始地址必须是8字节的整数倍,如果对象实例数据不是8字节的整数倍,则会自动填充数据补全。
  • 对象头。分为三个部分,mark word存储对象的hashcode、是否是偏向锁、锁标记位、分代年龄等(如果是偏向锁,还会包含一个偏向的线程id,重量级锁包含指向monitor的指针)。klass 指向对象的类信息的指针(当类被加载的时候,jvm会为该类创建instantKlass对象存储在方法区)。如果是数组类型,还会存储数组长度。
  • 当对象锁的状态是偏向锁时,mark word存储的是偏向的线程id。当对象的状态是轻量锁时,mark word存储的是线程栈中的lock record指针。当对象锁的状态是重量级锁时,存的是指向堆中的monitor对象指针。

monitor

  • monitor是一种程序结构,synchronized基于monitor对象实现同步,每个对象的对象头包含一个指针指向monitor对象,通过一系列机制实现多线程互斥访问共享资源。

HotSpot中monitor结构中的几个主要属性

  • head  存储锁对象的mark word
  • count  记录线程获取锁的次数
  • waiters  处于wait状态的线程数
  • waiterSet  获得锁后进入wait状态的线程集合。
  • recursions  重入次数
  • cxq  竞争队列、所有请求锁的线程都会进入此队列,单向链表结构。
  • EntryList  等待获取锁的线程集合。cxq中的线程获取竞争锁的资格时,会进入此队列。
  • object  指向该monitor的锁对象
  • owner  获取monitor的线程

ObjectWaiter 

  • 线程进入cxq队列会被封装成一个ObjectWaiter对象。以下有些涉及到队列操作的,直接写成线程,方便理解,实际上是ObjectWaiter对象。

mark word 在32位虚拟机的组成,mark word在32位虚拟机的长度是32bit  在64位虚拟机是64bit

加锁字节码层面的原理

  • 同步代码块的原理。通过在代码块前插入monitorenter加锁指令,在代码块后插入monitorexit解锁指令。包含两个monitorexit解锁指令,一个是用于异常抛出时释放锁,相当于隐式的try finally。
  • 同步方法的原理。字节码文件上,会采用ACC_SYNCHRONIZED表示符指定方法同步。方法级别的同步是隐式的,当某个线程要访问某个方法时,会检查是否存在ACC_SYNCHRONIZED标志,如果存在则需要获取锁,执行之后再释放锁。其他线程会因为无法获取锁被阻塞。如果方法出异常,那么异常被抛出方法外前,锁会自动释放。静态方法的加锁对象是类的class对象,实例方法的加锁对象是当前实例的对象。
  • 底层获取锁的逻辑是一致的。

对象头锁状态

以下锁的级别从低到高,随着竞争状态越激烈,级别就会升的越高,锁的状态可以升级,也可以降级(1.8 openJDK 降级代码,并发编程艺术说不可降级,可能是版本不同)。锁升级的过程即为锁膨胀。线程获取锁时,根据当前对象头的锁状态,采取不同的锁机制。由于偏向锁和轻量锁不涉及到线程阻塞、线程唤醒等系统函数调用,因此当调用wait、notify等函数时会先将锁膨胀到重量级锁。

  • 无锁状态
  • 偏向锁状态(适合单线程访问同步块)。偏向锁的作用是省略cas操作。有写情况同步块的锁总是由同一个线程多次获取,为了降低获取锁的代价引入偏向锁,即该锁偏向某个线程,该线程下次获取锁只需判断偏向当前线程即可。
  1. 偏向锁结构。偏向锁线程ID、epoch。偏向锁线程id: 指定偏向的线程,默认是0,表示没有偏向任何线程,也叫匿名偏向。epoch 批量重偏向的次数。
  2. lock record。 偏向锁和轻量锁都包含lock record,不同的是偏向锁对象头没有包含指向lock record的指针lock record 包含Displaced Mark Word、object reference。Displaced Mark Word存储锁对象的对象头中的mark word和object reference 是指向锁对象的指针。
  3. 加锁。当一个线程第一次访问同步块并获取锁时,会通过cas设置对象头和栈帧中存储锁偏向的线程id,cas成功后,会往线程栈中新增一条lock record(基于线程栈操作,lock record机制参考轻量级锁,偏向锁通过lock record可用于后续判断线程是否处于同步块中)。该线程之后再进入该同步块,只需要测试加锁的对象头是否存储指向当前线程的偏向锁,如果测试成功,插入一条lock record后,则线程获取锁成功。如果当前锁已经偏向其他线程,一般情况是会直接升级锁,除非遇到批量重偏向的情况。
  4. 锁重入。锁重入与轻量级锁类似,也是往当前线程的线程栈中插入一条Displaced Mark Word为空的lock record。详见轻量锁。因为操作的是线程私有的栈,所以不需要cas。
  5. 偏向锁撤销。撤销的策略是竞争时才撤销,即当其他线程尝试获取偏向锁才释放锁,且需要等到jvm的safepoint(gc时会让所有线程阻塞的停顿点)才开始撤销。当其他线程尝试获取偏向锁时,检查该线程是否处于活跃状态并且处于同步块中(此时会先遍历jvm中维护的一个处于存活状态的线程集合,与对象头中的线程id比较),如果不处于同步代码块中则将锁状态设置为无锁状态并进行锁升级,如果处于同步代码块中则进行锁升级。
  6. 锁释放。释放线程栈中的lock record。
  7. 锁膨胀。只要发生多线程竞争锁,不管竞争时占用偏向锁的线程是否处于活跃状态,都会升级锁,除非jvm认为该对象的锁偏向有问题,进行了批量重偏向,否则偏向锁上的线程id不会重偏向到其他线程。锁膨胀会升级对象锁结构,添加一个指向lock record的指针。
  8. 批量重偏向(bulk rebias)。jvm为每一个类维护一个偏向锁撤销计数器,记录这个类对应的对象作为锁时,被撤销的次数。当一个类的对象撤销的次数达到20次时,jvm会认为这个类的对象的偏向锁有问题,会进行批量重偏向。批量重偏向需要等到gc停顿点,因为需要操作其他线程栈。每个类都有对应的一个epoch值,表示批量重偏向的次数,该类的对象被创建时,对象头上的epoch就是class的epoch值。批量重偏向时,会将class的epoch值加1,同时会遍历所有线程栈,通过lock record找到占有该对象的偏向锁并且所有处于同步块中的线程,更新处于同步块中的线程中的epoch(即不会重偏向还处于同步代码块中的线程)。其他线程获取偏向锁时,如果发现对象的epoch值和类上的epoch不一致,那么就会重偏向当当前线程。
  9. 批量撤销(bulk revoke):当一个类的对象锁重偏向到40次以后,jvm认为该类的使用场景存在多线程竞争,将该类标记为不可偏向。
  • 轻量级锁状态(线程交替执行,没有竞争锁)。
  1. 轻量锁结构 。锁对象头包含一个指向lock record的指针。
  2. 加锁。线程进入同步块时,会在线程独享的线程栈帧中创建一个lock record。通过cas将该线程创建lock record的地址存储在对象头中,如果对象处于无锁状态则修改成功。如果cas失败是其他线程拥有锁则说明发生竞争,自旋重试,重试一定程度后将锁膨胀成重量锁。
  3. 锁重入。如果cas失败判断是不是当前线程已经拥有这个锁,如果是则再往线程栈插入一条lock record,这条lock record的Displaced Mark Word设置为空,起到重入计数的作用。即有多少条lock record的object reference指向当前线程,并且Displaced Mark Word为空,则说明重入多少次。
  4. 解锁。遍历线程栈中的lock record,将匹配当前对象锁的lock record释放(包括第一次获取锁和重入的记录),最后将第一次加锁的lock record上的mark word 通过 cas设置回对象头,cas失败需要进行锁升级。
  5. 轻量锁膨胀到重量锁的过程。先申请monitor,初始化monitor对象,将状态设置成膨胀中状态(多线程膨胀时,判断处于膨胀状态则进入忙等待)。将monitor的head设置成锁对象的mark work,owner设置成lock record,object设置成锁对象。设置锁的状态为重量级锁,将锁的mark word的锁指针指向monitor。
  • 重量级锁状态(适合同步块执行速度较慢,多线程同步竞争资源。依赖操作系统的同步函数( 如linux的futex)进行线程的阻塞和唤醒,执行特权指令需要从用户态切换到内核态,进程的上下文切换成本高)。重量级锁依赖monitor实现,加锁对象的对象头包含指向monitor监视器对象的指针,进入代码需要通过monitor,一个monitor同时只有被一个线程占有,其他线程未获取锁则进入monitor维护的队列阻塞等待唤醒。

重量级锁通过monitor实现互斥执行的流程

  1. 获取锁。当一个线程访问一段同步的代码,先根据对象头的指针找到对象的monitor监视器(如果没有则会去jvm中申请,并在锁对象的对象头放入一个指向该monitor的指针),先尝试cas获取锁(cas设置monitor的owner为当前线程),如果尝试后还没拿到锁将线程封装成ObjectWaiter进入cxq 竞争队列头部(循环cas插入队列,每次插入失败会再尝试获取锁,降低进入cxq的频率),进入队列后将当前线程通过park函数暂停,当monitor没有被锁占用或者在解锁的时候(即owner为空的时候),默认策略cxq中的线程会移到entryList,并唤醒entryList的tou线程(不同策略唤醒的方式不同,有的策略是直接唤醒cxq中的线程),该线程成为准继承人,通过cas尝试设置monitor的_owner参数修改成该线程(有可能失败,其他线程刚进同步块也会cas竞争),并将_count参数修改成0。
  2. 锁重入。在获取锁的情况下再次获取锁,_count参数会自增,每次退出锁_count减1。当_count减为0时,_owner参数置空,释放锁。
  3. wait等待。如果线程获取锁之后,调用wait方法,将_owner设置为空,_count置为0,进入_waiterSet等待队列等待被唤醒。
  4. 释放锁。如果_owner不是当前线程,而是lock record则把_owner改成当前线程,重入次数置为0,此时会先释放锁,将_owner置为0,如果有线程正好处于cas尝试获取锁,那么此时该线程获取到锁,如果没有线程在竞争,那么当前线程会重新尝试一次cas竞争锁,如果获取到锁会先判断当前有没有一个醒着的准继承人(可能之前成为准继承人后cas没抢到锁),如果有则不需要唤醒线程,否则则根据不同的策略唤醒处于cxq和entryList中的某个线程。默认的策略是会将cxq中的线程移到entryList,然后唤醒entryList中的第一个线程。唤醒后的线程cas竞争锁。(这也是不公平锁的体现,不是先进来的线程就先获取到锁)

重量级锁唤醒线程的策略

  1. QMode = 2且cxq非空:取cxq队列队首的ObjectWaiter对象,唤醒该线程。
  2. QMode = 3且cxq非空:把cxq队列插入到EntryList的尾部;将EntryList头部的线程唤醒。
  3. QMode = 4且cxq非空:把cxq队列插入到EntryList的头部;将EntryList头部的线程唤醒。
  4. QMode = 0且cxq非空(默认策略),将cxq队列全部的线程放到EntryList,将EntryList头部的线程唤醒。默认策略下,由于线程进入cxq是从头部插入,迁移到EntryList是按照cxq原顺序插入,唤醒是唤醒头部的线程。因此后面到的线程会被先唤醒(非公平的体现之一)。(如果都cas没有成功进入到了cxq队列的情况下)

重量级锁申请 monitor 流程

       如果加锁对象的对象头没有指向一个monitor对象需要申请,monitor并不是随着对象创建生成,而是每个线程会维护两个monitor对象列表,已使用的monitor对象列表usedList、未使用的monitor对象列表freeList,当一个线程需要申请锁时,会在线程私有的free列表申请,如果没有可以申请,则需要到jvm维护的global free list中申请,从global free list会申请到一批monitor到线程私有的freeList,申请到的monitor初始化到堆中。monitor的head存放锁对象的mark word,object字段存储锁对象,owenr设置为null(如果是轻量锁膨胀则设置为lock record)。

总结

偏向锁: 只有单线程,不会出现多线程进入一个同步方法。比如使用jdk提供的同步方法,同个线程循环调用这个方法,只会有单线程。

轻量级锁: 多线程交替执行,多线程不会同时进入一个同步方法,线程交替进入同步方法的情况。比如有个方法加了同步,但是每次都是单个请求处理完才会有下一个请求,每次请求发起的线程不一样,但是交替执行的情况。

重量级锁: 多线程竞争资源执行,需要阻塞、唤醒等操作。

锁消除

  • Jit即时编译器会进行逃逸分析,检测加了同步的代码,但是不可能出现共享数据竞争的锁进行锁消除。既同步代码中没有对线程共享数据进行读写,那么就都是线程私有的,可以消除锁。

锁粗化

  • Jit对代码中出现连续对同一个对象进行加锁,甚至在对同一对象在循环中加锁,会进行锁粗化,扩大锁范围,避免频繁加锁解锁。

你可能感兴趣的:(Java基础)