Synchronized
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
- 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
- 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
java对象内存结构
Mark Word结构
Mark Word (64bits) | State |
---|---|
unused:25 | hashcode:31 | unused:1 | age:4bits | biassed_lock:0 | 01 | Normal |
thread: 54 | epoch:2 | unused:1 | age:4 | biassed_lock:0 | 01 | Biassed |
pre_to_lock_record:62 | 00 | Lightweight Locked |
prt_to_heavyweight_monitor:62 | 10 | heavyweight Locked |
-
hashcode: 对象的hashcode
-
age: 对象年龄, 决定是否去老年代
-
biassed_lock: 偏向锁标志, 0表示非偏向锁, 1表示偏向锁
-
Biassed: 偏向锁
-
thread: 线程id
Monitor(锁)
每个JAVA对象都可以关联一个Monitor对象, 如果使用synchonized给对象上锁(重量级)之后, 该对象头的Mark Word就被设置指向Monitor对象的指针, Mark Word中原本的信息如hashcode就会被存到Monitor对象中, 在释放锁的时候, monitor会将这些属性还原到Mark Word中
-
刚开始监视器中所有者为NULL
-
当T1执行synchronized(obj)就会将Monitor的所有者Owner置为T1,Monitor中只能有一个Owner
-
当T1上锁后,T2/T3/T4也来执行临界区代码(synchonized(obj))就会进入Monitor的EntryList中阻塞
-
T1执行完临界区代码, 就会唤醒EntryList中的线程, 来进行锁竞争, 竞争时是非公平的.
-
图中waitSet中的T9/T0是之前获得过锁的, 但是不满足进入WATING状态的线程, 跟wait-notify有关
注意:
没有添加synchonized关键字的对象不会关联Monitor
synchonized-轻量级锁
当synchonized为轻量级锁的时候,其锁创建过程如下
-
每个线程创建时都会创建一个lock record对象, 内部存储锁对象的Mark Word
-
当thread0执行到sychonized代码块时, lock record对象中的object reference将指向锁对象, 同时采用cas操作, 将obj锁对象中的mark word与lock record对象中的lock record 地址 00进行交换
-
cas成功, 其状态如下; 锁状态00. 表示轻量级锁
-
cas失败, 则有两种情况
-
如果是其他线程已经持有了该obj锁对象, 表示有竞争, 会进入锁膨胀
-
如果是thread0锁冲入导致cas失败, 那么在栈帧中再添加一个lock record作为锁重入计数
-
-
当退出synchonized代码块时,进行锁释放. 重入几次, 就要释放几次
- 释放成功, 轻量级锁成功释放
- 释放失败, 说明轻量级锁进入锁膨胀, 要进入重量级锁解锁流程
锁膨胀
当轻量级锁存在锁竞争的情况时, thread1线程竞争失败, 进入所膨胀流程.
-
为obj锁对象申请monitor锁, 让obj对象的mark word指向monitor地址
-
然后thread1线程进入monitor的entryList中进行阻塞等待
注意: 轻量级锁没有阻塞等待队列
- thread0 释放轻量级锁时, cas失败, 进入重量级锁释放流程. 将monitor中的owner置为null, 唤醒entryList中等待线程进行锁竞争
重量级锁-自旋优化
- 当monitor存在多线程锁竞争的时候, 竞争线程不会立即进入EntryList队列中阻塞, 他会重试多次获取锁, 如果重试的时候获取到了monitor, 则进入临界代码块. 自旋获取失败, 进入monitor 的entryList中阻塞等待
- java6之后自旋是自适应的, 比如之前的一次锁竞争中自旋成功了, 那么认为当前自旋获取锁的概率大, 就会多进行几次自旋, 反之就少自旋, 甚至不自旋
偏向锁
偏向锁, 锁对象的mark word中记录的是线程id, 当线程重入加锁时, 省去了判断mark word中的地址是否为锁记录的地址.
一个对象创建时:
-
如果开启了偏向锁(默认开启) ,那么对象创建后,markword值为0x05即最后3位为101, 这时它的thread、epoch、 age 都为0
-
偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数
-XX:BiasedLockingStartupDelay=e来禁用延迟
-
如果没有开启偏向锁,那么对象创建后,markword 值为0x01即最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值
偏向锁-撤销偏向
会撤销偏向的场景
- 当调用锁对象的hashcode方法时, 偏向锁会撤销, 将mark word中的线程id替换为正常对象的信息(可看mark word结构). 即将biassed状态撤销为normal状态
- 当多个线程都要使用偏向锁时(不是竞争也会升级), 偏向锁升级为轻量级锁.
- 调用wait/notify; 这两个方法只有重量级锁, 不管当前用的什么锁, 都会升级重量级锁
偏向锁-批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID
当撤销偏向锁阈值超过20次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程
偏向锁-批量撤销
当撤销偏向锁阈值超过40次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有
对象都会变为不可偏向的,新建的对象也是不可偏向的
Synchronized锁升级过程
伪代码:
private Object lock = new Object();
synchronized (lock) {
// 代码逻辑
}
当java程序第一次运行一个上述synchronized代码块儿时, 假设当前线程为t0
, 锁变化如下
- 加偏向锁,
lock
对象的mark word记录t0线程ID, 将锁标志位修改为01
表示当前为偏向锁. 如果后续执行该代码块的线程仍然为t0线程, 那么只需要比较lock对象中的mark word存的是不是t0线程的ID即可, 不用像轻量级锁那样获取地址值进行比较. - 当访问代码块的线程为t1线程时, lock对象的mark word中线程ID与当前线程不相等, 升级为轻量级锁, 并且将lock对象的mark word存入t1线程的栈帧中的
lock record
对象, 将lock record对象的指针存入lock对象的mark word, 完成交换. - 在场景2的基础上, 又来了线程t3要执行同步代码块, 此时t2没有执行完成, t3无法获得锁, 锁升级为重量级锁, lock对象的mark word记录monitor的地址值, 设置t3线程进入monitor的entryList中等待t2线程释放锁.
锁消除
锁消除指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。锁消除的依据是逃逸分析的数据支持,如 StringBuffer 的 append() 方法,或 Vector 的 add() 方法,在很多情况下是可以进行锁消除的,比如以下这段代码:
public String method() {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 10; i++) {
sb.append("i:" + i);
}
return sb.toString();
}
StringBuffer对象是局部变量, 且不会从该方法中逃逸出去. 所以StringBuffer对象是没有并发环境的, 所以JVM对其进行了锁消除. 编译时, 将StringBuffer转换成了StringBuilder
锁粗化
锁粗化是指,将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
public String method() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
// 伪代码:加锁操作
sb.append( + i);
// 伪代码:解锁操作
}
return sb.toString();
}
在循环中每次都需要加锁, 浪费性能, 将锁放到循环外, 只用加锁一次
public String method() {
StringBuilder sb = new StringBuilder();
// 伪代码:加锁操作
for (int i = 0; i < 10; i++) {
sb.append( + i);
}
// 伪代码:解锁操作
return sb.toString();
}
wait-notyfy
- obj.wait() 让进入object监视器的线程到waitSet等待
- obj.wait(long n) n毫秒之后, 自动唤醒
- obj .notify()在object. 上正在waitSet等待的线程中挑一 个唤醒
- obj . notifyAll()让object. 上正在waitSet等待的线程全部唤醒
- 它们都是线程之间进行协作的手段,都属于Object对象的方法。obj对象必须被线程锁住,才能调用这几个方法
- wait调用之后, 会释放锁. 其他线程可获取当前锁进入临界代码区
如果没有synchonized(obj), 直接执行obj.wait()会报错.
public class Test7 {
private static final Object obj = new Object();
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj) {
try {
System.out.println("执行wait");
obj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("执行释放");
}
});
}
}
sleep与wait方法的区别
- sleep是Thread类的静态方法; wait是Object类的普通方法
- sleep不释放锁; wait释放锁
- wait需要synchonized才能使用
同步模式-保护性暂停
概念: 即Guarded Suspension,用在一个线程等待另一个线程的执行结果
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者消费者)
- JDK中, join的实现、Future的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
保护性暂停案例, 这也是join()方法的实现原理
package com.heima.test;
import java.io.Serializable;
import java.util.List;
public class GuardedObject implements Serializable {
private List<String> response;
public List<String> getResponse() {
return response;
}
public List<String> get(long n) throws InterruptedException {
synchronized (this) {
long start = System.currentTimeMillis();
long passed = 0L;
while (null == response) {
passed = System.currentTimeMillis() - start;
System.out.println("获取进入等待,等待时间:" + n);
wait(n - passed);
}
return response;
}
}
public void complete(List<String> list) {
synchronized (this) {
response = list;
System.out.println("唤醒其他线程");
this.notifyAll();
}
}
}
public class Test7 {
public static void main(String[] args) {
GuardedObject guarded = new GuardedObject();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
List<String> list = guarded.get(2000);
if (null != list) {
list.forEach(System.out::println);
} else {
System.out.println("null");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread t2 = new Thread(() -> {
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL("http://www.baidu.com").openConnection();
} catch (IOException e) {
throw new RuntimeException(e);
}
List<String> list = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
list.add(line);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
guarded.complete(list);
}, "t2");
t.start();
t2.start();
}
}