一、synchronized
1、原理:
- synchronize是线程互斥的,所以能保证共享数据一致性。
- synchronize底层是有 monitorenter 和 monitorexit 两个jvm指令;他能保证任何线程在enter之前从主内存中读取共享数据,在exit将共享数据刷新到主内存。(遵循happens-before原则,管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而"后面"是指时间上的先后顺序。)
可参考 深入synchronize原理
2、如下反编译带有synchronize代码程序:
3、获取 和 释放 对象关联的monitor锁机制
synchronize是不可中断的
只有像sleep、wait、join这些可中断方法才能被中断,然而获取锁阻塞是无法被中断的,如下案例
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();
}
}
结果:Trying to call f()
由于构造中已经有线程抢了锁,没有释放,所以t线程抢锁导致阻塞是无法被中断的;
只有在可中断方法才能被中断(不管是否加synchronize关键字,只要运行到可中断方法就可以捕获中断信号抛出中断异常,获取锁是阻塞无法捕获中断信号的)
4、锁粗化 和 锁消除
// 锁粗化(运行时 jit 编译优化)
// jit 编译后的汇编内容, jitwatch可视化工具进行查看
public class ObjectSyncDemo3 {
int i;
public void test1(Object arg) {
synchronized (this) {
i++;
}
synchronized (this) {
i++;
}
//上面热点代码,会优化如下,因为相同的锁进入两次消耗大量系统资源
/* synchronized (this) {
i++;
i++;
}*/
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000000; i++) {
new ObjectSyncDemo3().test1("a");
}
}
}
// 锁消除(jit)
public class ObjectSyncDemo4 {
public void test1(Object arg) {
// jit 优化, 消除了锁
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("a");
stringBuffer.append(arg);
stringBuffer.append("c");
// System.out.println(stringBuffer.toString());
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000000; i++) {
new ObjectSyncDemo4().test1("123");
}
}
}
分析:热点代码优化导致锁消除,如何保证安全性呢?由于各个线程是操作的栈帧局部变量所以安全
//如下是StringBuffer 的 append方法
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
5、同步关键字加锁原理 及其优化过程
①、锁对象包括Mark word,对象信息及格式,如下图
②cas实现加锁的原理,线程栈的lock record中存入该锁对象的Mark word信息(hash、经历多少次新生代gc、和什么锁状态)
③锁对象加锁优化的步骤如下图:对于只有一个线程多次获取同一个锁,只需要使用偏向锁就可以啦
6、synchronize的缺陷
上图就能说明为什么wait 和 notify 、notifyAll必须写在同步中的原因,因为waitSet在monitor中。
三、同步阻塞 和 异步非阻塞
同步阻塞 和 异步非阻塞 对比
- 同时提交任务,同步处理等待时间较长,可能会出现阻塞;频繁的创建、关闭线程,大量的浪费资源;如果线程创建数量巨大是cpu频繁切换也会降低性能。
- 异步非阻塞是提交任务到队列将立马返回工单,创建合理的线程数量去处理队列任务,然后存入结果集中,可凭工单获取结果集。
无法控制线程停止,可中断方法可以使用interrupted方法中断,但是获取锁阻塞就无法中断了
二、object方法 wait 、notify、notifyAll
wait:
- wait不设置时间,内部是调用wait(0)方法,表示永不超时阻塞。
- 使当前线程进入阻塞,阻塞时间到了自动醒来,或者被其他线程调用该锁对象的notify / notifyAll方法唤醒(通知在此对象的监视器上等待的线程醒来。然后该线程将等到重新获得对监视器的所有权后才能继续执行)。
- 方法必须在同步中使用,因为waitset在monitor中,所以使用必须拥有该对象的monitor以及monitor的所有权才能执行。
- 执行该方法会放弃monitor所有权,进入该锁对象关联的wait set中,其他线程才有机会获取该对象的monitor所有权。
notify:
- 唤醒该锁对象关联的wait set中的任意一个线程,如果没有则被忽略。
- 被唤醒要重新获取到该锁对应monitor的所有权,线程才能执行。
notifyAll
唤醒全部该锁对象的wait set中的线程,然后争抢monitor锁所有权
wait 和 sleep 总结
- 相同点:①都是使线程进入阻塞状态;②都是可中断方法,中断后抛出中断异常;
- 不同点:
①wait是object方法,sleep是Thread方法;
②wait方法必须在同步中才能使用,sleep不用
③wait会释放monitor锁,sleep不会
④因为sleep方法必须指定时间,而wait不用,所以sleep会自动醒来进入runnable状态。