1.synchronized
synchronized关键字解决多个线程之间访问资源的同步性。 保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行,此外 synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性),并且有效解决重排序问题 。
在java中,每一个对象有且仅有一个同步锁。这也意味着同步锁是依赖于对象而存在。
在JDK早期版本中synchronized属于重量级锁,效率低下;在Java6 之后,JVM对synchronized做了大量的优化。
保证原子性
synchronized保证只有一个线程拿到锁,能够进入同步代码块中。当第一个线程进入了同步代码块当中即使操作到了一半没有操作完由于CPU切换,切换到了其他线程,其他线程也会由于没有对象锁的缘故无法进入同步代码块当中只能进行等待;即其他线程不会来干扰第一个线程的操作。
package com.xxx.demo02_concurrent_problem;import java.util.ArrayList;/**案例演示:5个线程各执行1000次 i++;*/public class Test01Atomicity{private static int number = 0;public static void main(String[] args) throws InterruptedException{Runnable increment = new Runnable(){@Overridepublic void run(){for(int i = 0; i < 1000; i++){synchronized(Test01Atomicity.class){number++;}}}};ArrayList<Thread> ts = new ArrayList();for( int i = 0; i < 50 ; i++){Thread t = new Thread(increment);t.start();ts.add(t);}for(Thread t : ts){t.join();}System.out.println("number = " + number);}}
保证可见性
synchronized关键字其实会变成8个原子操作当中的lock与unlock原子操作;即8个原子操作(主内存与工作内存之间具体的交互协议)为lock —→read —→ load—→ use—→ assign—→ store—→ write—→ unlock。执行synchronized时,其对应的lock原子操作会刷新工作内存中共享变量的值。
/**
也可以不用进行写 synchronized(obj){} ;可以直接进行打印 System.out.println(flag);也是可以做到解决可见性问题。PrintStream.java中的println(boolean x)方法中也使用到了synchronized,也就导致也会去刷新线程A当中工作内存当中的变量从而去获取主内存当中最新的共享变量的取值。
public void println(boolean x){
synchronized(this){
print(x);
newLine();
}
}
**/
public class Test01Visibility{
// 1. 创建一个共享变量
private static boolean flag = true;
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException{
// 2. 创建一条线程不断读取共享变量
new Tread(()->{
while(flag){
synchronized(obj){
}
}
}).start();
Thread.sleep(2000);// 主线程沉睡两秒
// 3. 创建一条线程修改共享变量
new Thread(()->{
flag = false;
System.out.println("线程修改了变量的值为false")
});
}
}
保证有序性
加synchronized,依然会发生重排序,只不过存在有同步代码块,可以保证只有一个线程执行同步代码块当中的代码,也就能保证有序性。
@JCStressTest
@Outcome(id = {"1" , "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class Test03Ordering{
// 先搞一个对象作为对象锁
private Object obj = new Object();
int num = 0;
boolean ready = true;
// 线程一 执行的代码
// 测试方法actor1(I_Result r)进行添加synchronized
@Actor
public void actor1(I_Result r){
synchronized(obj){
if(ready){
r.r1 = num + num;
}else{
r.r1 = 1;
}
}
}
// 线程二 执行的代码
// 下面的测试方法actor2(I_Result r)同样进行添加synchronized
@Actor
public void actor2(I_Result r){
synchronized(obj){
num = 2;
ready = true;
}
}
}
可重入
java中synchronized是基于原子性的内部锁机制,是可重入的, 可以避免死锁 。
一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法。
当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。注意由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器 recursions 变量仍会加1。
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
static int j=0;
@Override
public void run() {
for(int j=0;j<1000000;j++){
//this,当前实例对象锁
synchronized(this){
i++;
increase();//synchronized的可重入性
}
}
}
public synchronized void increase(){
j++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
不可中断
一个线程获得锁后,另一个线程想要锁必须处于阻塞或等待状态;如果第一个线程不释放锁,第二个线程一直处于阻塞或等待状态,在阻塞或者等待过程中,不可被中断。
所以线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用,即使调用中断线程的方法,也不会生效。
/**
运行效果:程序没有停止运行依然在继续
Thread-0进入同步代码块
停止线程前
停止线程后
TIMED_WAITING
BLOCKED
通过interrupt()方法给t2线程进行强行中断;
最后进行打印t2的状态及State发现状态依然为BLOCKED;
即线程不可中断;**/
public class Demo02_Uninterruptible{
private static Object obj = new Object();//定义锁对象
public static void main(String[] args){
// 1. 定义一个Runnable
Runnable run = ()->{
// 2. 在Runnable定义同步代码块;
// 同步代码块需要一个锁对象;
synchronized(obj){
// 进行打印是哪一个线程进入的同步代码块
String name = Thread.currentThread().getName();
System.out.println(name + "进入同步代码块");
// 需要进行保证不进行退出同步代码块;
// 所以让此线程进行沉睡sleep
Thread.sleep(888888);
}
};
// 3. 先开启一个线程来执行同步代码块
Thread t1 = new Thread(run);
t1.start();
// 沉睡一秒钟;
// 保证第一个线程先去执行同步代码块之后再来创建第二个线程;
Thread.sleep(1000);
/**
4. 后开启一个线程来执行同步代码块(阻塞状态)
到时候第二个线程去执行同步代码块的时候,
锁已经被t1线程锁获取得到了;
所以线程t2是无法获取得到Object obj对象锁的;
那么也就将会在同步代码块外处于阻塞状态。
*/
Thread t2 = new Thread(run);
t2.start();
/** 5. 停止第二个线程;
观察此线程t2能够被中断;
*/
System.out.println("停止线程前");
t2.interrupt();
System.out.println("停止线程后");
// 最后得到两个线程的执行状态
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
public class SynchronizedBlocked implements Runnable{
public synchronized void f() {
System.out.println("Trying to call f()");
while(true) // Never releases lock
Thread.yield();
}
/**
* 在构造器中创建新线程并启动获取对象锁
*/
public SynchronizedBlocked() {
//该线程已持有当前实例锁
new Thread() {
public void run() {
f(); // Lock acquired by this thread
}
}.start();
}
public void run() {
//中断判断
while (true) {
if (Thread.interrupted()) {
System.out.println("中断线程!!");
break;
} else {
f();
}
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedBlocked sync = new SynchronizedBlocked();
Thread t = new Thread(sync);
//启动后调用f()方法,无法获取当前实例锁处于等待状态
t.start();
TimeUnit.SECONDS.sleep(1);
//中断线程,无法生效
t.interrupt();
}
}
SynchronizedBlocked构造函数中创建一个新线程并启动获取调用f()获取到当前实例锁,由于SynchronizedBlocked自身也是线程,启动后在其run方法中也调用了f(),但由于对象锁被其他线程占用,导致t线程只能等到锁,此时我们调用了t.interrupt();但并不能中断线程。
等待唤醒
notify/notifyAll和wait 方法必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常 。
notify/notifyAll和wait方法依赖于monitor对象 而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。
synchronized (obj) {
obj.wait();
obj.notify();
obj.notifyAll();
}
2.三种应用方式
- 修饰实例方法,作用于当前实例加锁
- 修饰静态方法,作用于当前类对象加锁
- 修饰代码块,作用于括号中的对象。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得当前 class 的锁
package com.lymn.juc;
import java.util.concurrent.TimeUnit;
public class Synchronized {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{ phone.sendMessage(); },"A").start();
new Thread(()->{ phone.call(); },"B").start();
}
}
class Phone{
/**
* synchronized 锁的对象是方法的调用者
* 两个方法使用的是同一个锁,哪个先拿到哪个先执行
*/
public synchronized void sendMessage(){ System.out.println(Thread.currentThread().getName() + " sendMessage");
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName() + " call");
}
}
package com.lymn.juc;
import java.util.concurrent.TimeUnit;
public class Synchronized {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{ phone.sendMessage(); },"A").start();
//延时1s
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{ phone.call(); },"B").start();
}
}
class Phone{
/**
* synchronized 锁的对象是方法的调用者
*
*/
public synchronized void sendMessage(){
try {
//延时3秒打印A sendMessage B call
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sendMessage");
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName() + " call");
}
}
package com.lymn.juc;
import java.util.concurrent.TimeUnit;
public class Synchronized1 {
public static void main(String[] args) {
Phone1 phone = new Phone1();
new Thread(()->{
phone.sendMessage();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.hello();
},"B").start();
}
}
class Phone1{
//hello()方法,是普通方法,不受锁的影响
// 线程B执行hello方法,不需要获取锁就能执行,所以也不必等待A执行完释放锁 hello sendMessage
public synchronized void sendMessage(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sendMessage");
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName() + " call");
}
public void hello(){
System.out.println("hello");
}
}
package com.lymn.juc;
import java.util.concurrent.TimeUnit;
public class Synchronized2 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
Phone2 phone1 = new Phone2();
new Thread(()->{
phone.sendMessage();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone1.call();
},"B").start();
}
}
class Phone2{
// synchronized用在方法上,那么锁的是方法的调用者。现在有两个调用者,故先调用call sendMessage
public synchronized void sendMessage(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sendMessage");
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName() + " call");
}
}
package com.lymn.juc;
import java.util.concurrent.TimeUnit;
public class Synchronized3 {
public static void main(String[] args) {
Phone3 phone = new Phone3();
new Thread(()->{
phone.sendMessage();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
},"B").start();
}
}
class Phone3{
// synchronized用在静态方法上,那么锁的就是class对象(模板)sendMessage call
public static synchronized void sendMessage(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sendMessage");
}
public static synchronized void call(){
System.out.println(Thread.currentThread().getName() + " call");
}
}
package com.lymn.juc;
import java.util.concurrent.TimeUnit;
public class Synchronized4 {
public static void main(String[] args) {
Phone4 phone = new Phone4();
new Thread(()->{
phone.sendMessage();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
},"B").start();
}
}
class Phone4{
// synchronized用在静态方法上,那么锁的就是class对象(模板),用在实例方法上,锁调用者,两者互不影响 call sendMessage
public static synchronized void sendMessage(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sendMessage");
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName() + " call");
}
}
package com.lymn.juc;
import java.util.concurrent.TimeUnit;
public class Synchronized5 {
public static void main(String[] args) {
Phone5 phone = new Phone5();
Phone5 phone1 = new Phone5();
new Thread(()->{
phone.sendMessage();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone1.call();
},"B").start();
}
}
class Phone5{
// synchronized用在静态方法上,那么锁的就是class对象(模板),java中每个类只有一个class对象,sendMessage call
public static synchronized void sendMessage(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sendMessage");
}
public static synchronized void call(){
System.out.println(Thread.currentThread().getName() + " call");
}
}
package com.lymn.juc;
import java.util.concurrent.TimeUnit;
public class Synchronized6 {
public static void main(String[] args) {
Phone6 phone = new Phone6();
Phone6 phone1 = new Phone6();
new Thread(()->{
phone.sendMessage();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone1.call();
},"B").start();
}
}
class Phone6{
// synchronized用在静态方法上,那么锁的就是class对象(模板),普通同步方法锁调用者,互不影响 call sendMessage
public static synchronized void sendMessage(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sendMessage");
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName() + " call");
}
}
synchronized扩号后面的对象是一把锁,在java中任意一个对象都可成为锁,简单来说,我们把object比喻是一个key,拥有这个key的线程才能执行这个方法。如果后续的线程想访问当前方法,因为没有key所以不能访问只能在门口等着,等之前的线程把key放回去。所以synchronized锁定的对象必须是同一个,如果是不同对象,就意味着是不同的房间的钥匙,对于访问者来说是没有任何影响的。
3.底层原理
Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现,无论是显式同步(有明确的monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。
Java对象头
在JVM虚拟机中,对象在内存中的存储布局,可以分为三个区域:
- 对象头(Header)
- 实例数据(Instance Data): 存放类的属性数据信息
- 对齐填充(Padding):由于虚拟机要求对象起始地址必须是8字节的整数倍,仅仅是为了字节对齐。

在Hotspot虚拟机当中对象头分为两种:一种是普通对象的对象头即instanceOopDesc;另外一种是描述数组的对象头即arrayOopDesc。
instanceOopDesc的定义在Hotspot源码的 instanceOop.hpp 文件中, arrayOopDesc的定义对应 arrayOop.hpp。 instanceOopDesc继承自oopDesc, oopDesc的定义在Hotspot源码中的 oop.hpp文件中。
class instanceOopDesc : public oopDesc{
public:
// aligned header size.
static int header_size(){return sizeof(instanceOopDesc)/HeapWordSize; }
// If compressed. the offset of the fields of the instance may not be aligned.
static int base_offset_in_bytes(){
/* offset computation code breaks if useCompressedClassPointers
only is true
*/
return (UseCompressedOops && UseCompressedClassPointers) ? klass_gap_offset_in_bytes() : sizeof(instanceOopDesc);
}
static bool contains_field_offset(int offset, int nonstatic_field_size){
int base_in_bytes = base_offset_in_bytes();
return (offset >= base_in_bytes && (offset-base_in_bytes) < nonstatic_field_size = heapOopSize);
}
};
class oopDesc{
friend class VMStructs;
private :
volatile markOop _mark;
union _metadata{
Klass* _klass; # 没有开启指针压缩时的类型指针
narrowKlass _compressed_klass; # 开启了指针压缩
} _metadata;
// Fast access to barrier set. Must be initialized.
static BarrierSet* _bs;
};
普通示例对象中,oopDesc的定义包含两个成员,分别是_mark和_metadata。
_mark表示对象标记、属于markOop类型,即Mark Word,它记录了对象和锁有关的信息。
_metadata表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示普通指针;_compressed_klass表示压缩类指针。
故对象头由两部分组成:一部分用于存储自身的运行时数据,称之为Mark Word;另一部分是类型指针,即对象指向它的类元数据的指针。
| 头对象结构 | 说明 |
|---|---|
| Mark Word | 存储对象的hashCode、锁信息或分代年龄或GC标志等信息 |
| Class Metadata Address | 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。 |
Mark Word
synchronized使用的锁对象是存储在Java对象头里的标记字段。
64位虚拟机下Mark Word是64bit大小的,其存储结构如下:
对象头 = Mark Word + 类型指针(未开启指针压缩的情况下)
在32位系统中,Mark Word = 4 bytes, 类型指针 = 4bytes, 对象头 = 8bytes = 64 bits;
在64位系统中,Mark Word = 8 bytes, 类型指针 = 8bytes, 对象头 = 16bytes = 128 bits
查看java对象布局
<dependency>
<groupId>org.openjdk.jol</groupId>
<aratifactId>jol-core</aratifactId>
<version>0.9</version>
</dependency>
/**
打印对象布局:
com.xxx.demo06_object_layout.LockObj object internals:
-------------------------------------------------------------------
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int LockObj.x 0
-------------------------------------------------------------------
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
**/
class LockObj{
private int x;
}
public class Demo01{
public static void main(String[] args){
LockObj obj = new LockObj();
//parseInstance 解析实例对象;toPrintable进行打印其解析的实例对象信息
ClassLayout.parseInstance(obj).toPrintable().;
}
}
-------------------------------------------------------------------
OFFSET偏移0;SIZE为4表示用了4个字节去进行描述DESCRIPTION对象头object header信息;
OFFSET偏移4;SIZE为4表示用了4个字节也是去进行描述DESCRIPTION与对象头object header相关的内容;
OFFSET偏移8;SIZE为4表示用了4个字节也是去进行描述DESCRIPTION与对象头object header相关的内容;
那么此时来看对象头object header所占了3 * 4 = 12个字节来进行描述与对象头object header相关信息;
与之前所说在64位系统中,Mark Word = 8 bytes,类型指针 = 8bytes,对象头 = 16 bytes = 128 bits;此处存在偏差。
那么这是因为JVM默认自动开启了指针压缩的选项参数;从而也就导致了JVM在默认情况下去进行打印对象布局当中的对象头信息所占字节为12个字节而不是16个字节。
选项-XX:+UseCompressedOops是用来进行开启指针压缩;-XX:-UseCompressedOops为关闭指针压缩。
-------------------------------------------------------------------
再次打印对象布局:
com.xxx.demo06_object_layout.LockObj object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) d8 34 5a 25 (11011000 00110100 01011010 00100101) (626668760)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 4 int LockObj.x 0
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
-------------------------------------------------------------------
对象布局当中object header对象头所占字节为16个字节;其中后面8个字节即第三排和第四排的object header是用来表示Klass pointer;16个对象头信息所占字节数 + LockObject.x int类型变量所占字节数为4个字节 = 当前字节数为 20 个字节;那么当前此时是不满足对象头信息总占字节数需要为8个字节的整数倍这一限制条件的;所以此时就有了填充数据,即最后一排的4个字节加4个字节的填充数据。即:
20 4 (loss due to the next object alignment)
-------------------------------------------------------------------
class LockObj{
private int x;
private boolean b;
}
--------------------------------------------------------
再次打印对象布局:
com.xxx.demo06_object_layout.LockObj object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int LockObj.x 0
16 1 boolean LockObj.b false
17 7 int (loss due to the next object alignment)
Instance size: 24 bytes
--------------------------------------------------------
boolean布尔类型在其中占用到了1个字节数;那么此时累加到字节数即为:12个对象头信息所占字节数 + LockObj当中int类型变量所占字节数4个字节 + LockObj当中boolean类型所占字节数为1个字节 = 总计字节数为17个字节;而17个字节数显然是不满足8字节的整数倍这一说法,所以就存在有最后一排的对齐填充数据占有7个字节进行平衡。即
17 7 int (loss due to the next object alignment)
填充了7个字节之后导致数据实例大小为24个字节满足8字节的整数倍这一说法。
-------------------------------------------------------------
/**
在64位虚拟机下,Mark Word是64bit大小的;其中是存在有31位导致hashCode的;hashCode的取值不是一来就有的;而是先需要在代码层当中使用到了这个hashCode那么它才会去进行保存这个hashCode取值
**/
public class Demo01{
public static void main(String[] args){
LockObj obj = new LockObj();
//调用obj对象的hashCode()方法
obj.hashCode();
System.out.println(Integer.toHexString(obj.hashCode()));
ClassLayout.parseInstance(obj).toPrintable().;
}
}
-----------------------------------------------------------
再次打印对象布局:
6d6f6e28 # 16进制hashCode
com.xxx.demo06_object_layout.LockObj object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 28 6e 6f (00000001 00101000 01101110 01101111) (1869490177)
4 4 (object header) 6d 00 00 00 (01101101 00000000 00000000 00000000) (109)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int LockObj.x 0
16 1 boolean LockObj.b false
17 7 int (loss due to the next object alignment)
Instance size: 24 bytes
-----------------------------------------------------------
Monitor
monitor描述为对象监视器,可以类比为一个特殊的房间,这个房间中有一些被保护的数据,monitor保证每次只能有一个线程能进入这个房间进行访问被保护的数据,进入房间即为持有monitor,退出房间即为释放monitor。
Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;//线程的重入次数
_object = NULL;//存储该monitor的对象
_owner = NULL;//标识拥有该monitor的线程
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
_owner: 初始化为NUll,当有线程占有该monitor时,owner标记为该线程的唯一表示。当线程释放monitor时,owner又恢复到NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的。
_cxq: 竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资源,JVM通过CAS原子指令来修改_cxq队列。 修改前 _cxq的旧值填入了 node的next字段, _cxq指向新值(新线程)。因此 _cxq是一个后进先出的stack(栈)。
_EntryList: _cxq队列中有资格成为候选资源的线程会被移动到该队列中。
_WaitSet: 因为调用wait方法而被阻塞的 线程会被放在该队列中。
ObjectMonitor中有两个队列WaitSet 和 EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象),owner指向持有ObjectMonitor对象的线程。
当多个线程同时访问一段同步代码时,首先会进入 EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。

由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因 。
同步语句块原理
同步语句块实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。
当执行monitorenter指令时,当前线程将试图获取objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor,重入时计数器的值也会加 1。
倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕。即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。
synchronized(obj)真正的锁并非是这个传入进来的该对象obj,而是该对象obj会去关联一个monitor,这个monitor才是真正的锁。对象monitor并非是手动使用代码进行创建的,而是JVM会去进行检查该对象obj是否有进行关联monitor对象,没有关联那么就会去创建一个与之关联的monitor对象,而且该monitor对象还需要进行注意的是monitor并不是一个java对象,而是一个C++对象。
为了保证在方法(包含同步语句块)异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。
//javap反编译后得到字节码如下
public void test2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter //进入同步方法
4: aload_1
5: monitorexit //退出同步方法
6: goto 14
9: astore_2
10: aload_1
11: monitorexit //退出同步方法
12: aload_2
13: athrow
14: return
Exception table:
from to target type
4 6 9 any
9 12 9 any
LineNumberTable:
line 9: 0
line 10: 4
line 11: 14
LocalVariableTable:
Start Length Slot Name Signature
0 15 0 this Lcom/lymn/juc/SynchronziedTest01;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 9
locals = [ class com/lymn/juc/SynchronziedTest01, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
同步方法原理
synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的是ACC_SYNCHRONIZED标识,该标识指明该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法。
当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor, 然后再执行方法,最后方法完成后(无论是否异常)释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。
public synchronized void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/lymn/juc/SynchronziedTest01;
public static synchronized void test3();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 13: 0
}
监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。
Java 6之后, Java官方对从JVM层面对synchronized较大优化,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。
synchronized锁优化
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
无锁
没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
偏向锁
偏向锁是Java6之后加入的新锁,经过研究发现,大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。
锁对象第一次被线程持有时,虚拟机通过CAS把获取到这个锁的线程ID记录到对象头Mark World中,操作成功则成功获取偏向锁,对象头中的锁标志位设置为01, 且偏向锁设置为1 。持有偏向锁的线程每次执行到这段同步代码时,不需要任何同步操作,这项优化称为偏向锁。
偏向锁倒数第三位会变成1;另外由前面56位当中的前54位会来保存偏向锁的id;剩下两位用来保证Epoch即时间。
public class Demo01{
public static void main(String[] args){
MyThread mt = new MyThread();
mt.start();
}
}
class MyThread extends Thread{
static Object obj = new Object();
@Override
public void run(){
for( int i = 0; i < 5; i++){
synchronized(obj){
// ...
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
}
}
------------------------------------------------------------
打印结果:
com.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) d8 ed 44 29 (11011000 11101101 01000100 00101001) (692383192)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 int LockObj.x 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
------------------------------------------------------------
分析:
d8 11011000代表的就是是否是偏向锁,即锁的标志位;最后三位即000;那么000不是属于偏向锁啊。原因在于:偏向锁虽然在jdk1.6的时候偏向锁是开启的,但是这个默认开启的偏向锁并不是立马可以进行使用的,所以这个时候又需要添加一个JVM参数即在VM options处填入参数:-XX:BiasedLockingStartupDelay=0,让其原始值为0即程序一启动那么偏向锁就生效。-XX:-UseBiasedLocking=false参数关闭偏向锁
-------------------------------------------------------------------
再次运行打印结果:
com.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 90 61 27 (00000101 10010000 01100001 00100111) (660705285)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 int LockObj.x 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
-----------------------------------------------------------------
当有其它线程尝试获取对象的锁时,终止偏向模式,同时根据锁是否处于锁定状态,撤销偏向锁恢复到无锁或轻量级锁状态。
偏向锁适用于一个线程反复获得同一个锁的情况。
轻量级锁
轻量级锁是 JDK1.6之中加入的 新型锁机制,“轻量级”是相对于使用monitor的传统锁而言的,
因此传统的锁机制就称为“重量级”锁。轻量级锁并不是用来代替重量级锁的,而是在多线程交替执行同步块的情况下,能够尽量避免重量级锁引起的性能消耗。
当关闭偏向锁功能 或者 多个线程竞争偏向锁导致偏向锁升级为轻量级锁。 获取锁步骤如下:
- 判断当前对象是否处于无锁状态,如果是JVM首先将在当前线程的栈帧中建立一个所记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,将对象的Mark Word赋值到栈帧中的Lock Record中,将Lock Record的owner指向当前对象。
- JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针;如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作。
- 如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象,直接执行同步代码;否则需膨胀成重量级锁。


class MyThread extends Thread{
static Object obj = new Object();
@Override
public void run(){
for( int i = 0; i < 5; i++){
synchronized(obj){
// ...
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
}
}
-----------------------------------------------------------
synchronized当中的obj即为锁对象(对象锁)
当前具有两个A、B线程,假设线程A来先进行执行run()方法,即此时运行的run()方法即为一个栈帧。假设此时线程A要进入到同步代码块之中,那么这个时候就要升级为轻量级锁,因为存在有多个锁竞争。
首先在栈中运行执行run()方法的该栈帧当中会创建一个叫做Lock Record的锁记录空间(这块空间内存放displaced hdr以及owner),当前obj锁对象的状态为无锁状态,即会将对象当中的无锁状态当中的hashCode、分代年龄以及锁标记赋值到栈帧当中Lock record中创建的displaced hdr当中,另外还会将栈帧当中的Lock record当中还会进行创建的owner指向obj,即对象锁,还会将锁标志位的数值修改成00。
-----------------------------------------------------------
轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:
- 取出在获取轻量级锁 保存在Displaced Mark Word中的数据。
- 用CAS操作 将取出的数据 替换当前对象的Mark Word中,如果成功,则说明释放锁成功。
- 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要将轻量级锁需要膨胀升级为重量级锁。
轻量级锁的性能之所以高是因为在绝大部分情况下,这个同步代码块不存在有竞争的状况,线程之间交替执行。
自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
自旋锁在JDK 1.4.2中就已经引入,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启。
自旋锁在JDK1.6中是默认开启的, 默认自旋次数是10次,可以使用参数-XX:PreBlockSpin修改默认值。
自旋次数改多了浪费资源,改少了又没有抢到锁也是浪费资源,改成多少合适呢? 因此在JDK6中引入了自适应自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机 就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100次循环。另外如果对于某个锁自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。
重量级锁
重量级锁是由monitor来进行实现的;当一个线程来进行竞争monitor锁如果没有竞争到那么线程就会进入阻塞状态;当其他线程将锁释放的时候会来进行唤醒那些处于阻塞状态的线程有机会去竞争锁。
线程的阻塞和唤醒是需要CPU从用户态切换至内核态。
锁粗化
JVM会探测到一连串细小的操作都是用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,那么这样只需要加一次锁即可。即将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
public class Demo01{
public static void main(String[] args){
StringBuffer sb = new StringBuffer();
for(int i = 0; i< 100 ; ++){
sb.append("aa");
}
System.out.println(sb.toString());
}
}
❝举个例子,买门票进动物园。老师带一群小朋友去参观,验票员如果知道他们是个集体,就可以把他们看成一个整体(锁租化),一次性验票过,而不需要一个个找他们验票。❞
锁消除
锁消除是虚拟机另外一种锁的优化。虚拟机在JIT编译时(即时编译),对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,
同步加锁自然就无需进行。
public class Demo01{
public static void main(String[] args){
concatString("aa" , "bb" , "cc");
}
public static String concatString(String s1, String s2, String s3){
return new StringBuffer().append(s1).append(s2).append(s3).toString();
}
}
-----------------------------------------------------------
StringBuffer.append()方法使用了synchronized进行同步,
@Override
public synchronized StringBuffer append(String str){
toStringCache = null;
super.append(str);
return this;
}
-----------------------------------------------------------
那么此时可以看到StringBuffer的append()方法使用了synchronized进行了同步处理,
而concatString当中进行调用了三次append()方法,也就是表明会进行执行三次这个同步方法。
同步方法它所使用的对象锁即new StringBuffer(),new StringBuffer()是concatString()
方法当中所new出来的局部变量,并没有逃逸出concatString()这个方法。
如果有多线程来进行执行,那么每个线程也都是获取拿到不同的锁,根本不存在有竞争。
那么append()方法的同步代码块synchronized就没有必要了,会自动进行消除掉这个同步代码块
synchronized。即完成锁消除
---------------------------------------------------
public StringBuffer append(String str){
toStringCache = null;
super.append(str);
return this;
}
---------------------------------------------------
4.用法
减少synchronized的范围
同步代码块中尽量短,减少同步代码块中代码的执行时间,减少锁的竞争。
synchronized(Demo01.class){
System.out.println("aaa");
}
降低synchronized锁的粒度
两个方法没有任何业务关联,尽量不要使用类名.class这样的锁。
HashTable:锁定整个哈希表,一个操作正在进行时,其他操作也同时锁定,效率低下;
ConcurrentHashMap:局部锁定,只锁定桶,当对当前元素锁定时,其他元素不锁定;
LinkedBlockingQueue:入队和出队使用不同的锁,相对于读写只有一个锁效率要高。
public class Demo01{
public static void main(String[] args){
Hashtable hs = new Hashtable();
hs.put("aa","bb");
hs.put("xx","yy");
hs.get("a");
hs.remove("b");
}
public void test01(){
synchronized(Demo01.class){
}
}
public void test02(){
synchronized(Demo01.class){
}
}
读写分离
读取时不加锁,写入和删除时加锁
ConcurrentHashMap,CopyOnWriteArrayList和ConyOnWriteSet


