1.专业术语
临界区Critical Section
- 一个查询运行多个线程本身没有问题
- 问题在于多个线程访问共享资资源
- 多个线程读共享资源时也不会发生问题
- 在多个线程对共享操作发生指令交错时就会发生问题
- 一段代码如果存在共享资源的多线程读写操作,称这段代码块为临界区
竞态条件Race Condition
多个线程在临界区执行,由于代码的执行顺序不同而导致结果无法预测,称之为竞态条件
2.synchronized解决方案
应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
阻塞式的解决方案: synchronized,Lock
非阻塞式的解决方案: 原子变量
synchronized语法:
synchronized(同一对象) // 线程1, 线程2(blocked)
{
临界区
}
建议锁对象前可以加上final保证锁住的引用对象不会变换
synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码是不可分割的,不会被线程切换所打断。
优化:
- 将数据抽象成一个类,并将数据的操作作为这个类的方法,这么设计可以和容易做到同步,只要在方法上加”synchronized“
- 将 Runnable 对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数
据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各
个 Runnable 对象调用外部类的这些方法。
synchronized加在方法上:
class Test{
public synchronized void test() {
}
}
//等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
class Test{
public synchronized static void test() {
}
}
//等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
3.变量的线程安全分析
成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则不一定
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
对象行为不确定的方法,可能导致不安全发生,被称之为外星方法,如果不想向外暴露,可以修饰为private或者final
举例:经典的String类型使用了final修饰,不可变,保证了线程安全
局部变量线程安全分析
链接: JVM内存结构.
// 线程安全
public static void test1() {
StringBuilder sb = new StringBuilder();
sb.append("a");
sb.append("b");
System.out.println(sb.toString());
}
// 线程不安全,引用了外部StringBuilder
public static void test2(StringBuilder sb) {
sb.append("a");
sb.append("b");
System.out.println(sb.toString());
}
// 线程不安全,返回了一个引用,其他对象可能拿到该引用区修改它
public static StringBuilder test3() {
StringBuilder sb = new StringBuilder();
sb.append("a");
sb.append("b");
return sb;
}
private和final也可以在线程安全中起到作用,防止方法重写,类继承时子类无法重写。(开闭原则CLOSE)
常见的线程安全的类
String、Integer、StringBuffer、Random、Vector、HashTable、java.util.concurrent包下的类等
这里说它们是线程安全的是指:多个线程调用它们的同一个实例中的某个方法是,是线程安全的。
它们的每个方法是原子的,但是多个方法组合在一起就不是原子的了
举例:
如果有多个需要保护的对象可以锁住类,但是效率不佳
class Account {
private int money;
public Account(int money) {this.money = money;}
public int getMoney() {return this.money;}
public void setMoney(int money) {this.money = money;}
// 模拟转账
public void transfer(Account target, int amount) {
synchronized (Account.class) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
4.Minitor
4.1.Java对象头
Java对象通常由两部分组成,对象头和成员变量。
以32位虚拟机为例:
普通对象
数组对象
MarkWord结构
4.2.Monitor(监视器锁)
Monitor 被翻译为监视器锁或管程,每个Java对象都可以关联一个 Monitor对象,如果使用 sychronized 给对象上锁(重量级)之后,该对象头的Mark Word中就被设置为指向Monitor对象。
Minitor结构:
- 刚开始是Moinitor为NULL
- 当Thread执行到synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Moinitor只能有一个所有者
- 在Thread上锁过程中,如果Thread-3、Thread-4、Thread-5也执行到synchronized(obj),就会进入到EntryList BLOCKED
- Thread-2同步代码块执行完毕,就会唤醒EntryList中等待线程来竞争锁,竞争时是非公平的
- 图中WaitSet中的Thread-0、Thread-1是之前获得过锁,但条件不满足而放弃锁进入WAITING等待状态的线程(详细见本篇后文)
注意:
- synchronized 必须是进入同一个对象的 Monitor 才会有以上效果
- 不加 sychronized 对象不会关联 Monitor,不尊遵从以上规则
字节码角度理解synchronized加锁原理:
对应字节码为:
5.sychronized锁优化
链接: JVM内存模型.
6.wait与notify
为什么需要wait ?
某个线程由于条件不满足不能继续进行运算,但是却又不能一直占用着锁,其他线程一直阻塞,影响效率,当满足条件后调用notify方法重新唤醒线程,进入等待队列。
6.1 wait/notify原理
- Owner线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU
- BLOCKED线程会在Owener线程释放锁时被唤醒
- WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后进入EntryList重新竞争
6.2.API介绍
- obj.wait() 让进入object监视器的线程到waitSet等待
- obj.wait(long timeout) 让进入object监视器的线程到waitSet等待一定时间
- obj.notify() 在object上正在waitSet等待的某个线程唤醒(随机)
- obj.notifyAll() 让obj上正在waitSet等待的线程全部唤醒
它们都是线程间进行协作通信的方法,都属于Object的方法,必须获得此对象的锁才能调用这几个方法,否则会抛出异常
6.3.正确的使用wait notify
sleep与wait的区别:
- sleep是Thread的static方法,wait是Object的方法
- sleep不需要强制和synchronized配合使用,但wait需要
- sleep在睡眠时不会释放锁对象,但wait会释放锁对象
多个线程时notify可能导致唤醒错误(唤醒的对象不对(虚假唤醒))
notifyAll 只能解决部分虚假唤醒问题(全部唤醒,但有对象条件仍不满足,导致无法继续执行逻辑)
正确方法:
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
doSomething();
}
//另一个线程
synchronized(lock) {
while(条件不成立) {
lock.notifyAll();
}
}
6.4.设计模式保护性暂停
场景:用于一个线程等待另一个线程的执行结果
要点:
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
- 如果有结果不断从一个线程到另一个线程,不能使用此模式,可以使用消息队列
- JDK中,join的实现、Future的实现,采用的就是此模式
- 因为要等到另一方的结果,因此归类到同步模式
示例:
一个线程等待另一个线程给某个对象赋值
增加超时效果:
保护性暂停应用之join源码:
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
6.5.park与unpark
Park与UnPark:
它们是LockSupport类中的方法
// 暂停当前线程
LookSupport.patk();
//恢复线程的运行
LockSuport.unpark(暂停线程对象);
public static void main(String[] args) {
Thread t0 = new Thread(new Runnable() {
@Override
public void run() {
Thread current = Thread.currentThread();
log.info("{},开始执行!",current.getName());
for(;;){//spin 自旋
log.info("准备park住当前线程:{}....",current.getName());
LockSupport.park();
log.info("当前线程{}已经被唤醒....",current.getName());
}
}
},"t0");
t0.start();
try {
Thread.sleep(5000);
log.info("准备唤醒{}线程!",t0.getName());
LockSupport.unpark(t0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
-----------------------------------------------------------------------
执行结果
21:55:14.621 [t0] INFO com.zhe.lock.Thread_LockSupport - t0,开始执行!
21:55:14.637 [t0] INFO com.zhe.lock.Thread_LockSupport - 准备park住当前线程:t0....
21:55:19.636 [main] INFO com.zhe.lock.Thread_LockSupport - 准备唤醒t0线程!
21:55:19.636 [t0] INFO com.zhe.lock.Thread_LockSupport - 当前线程t0已经被唤醒....
21:55:19.636 [t0] INFO com.zhe.lock.Thread_LockSupport - 准备park住当前线程:t0....
特点:
与Object的wait & notify相比
- wait,notify和notifyAll必须配合Object Monitor一起使用,而park,unpark不必
- park和unpark是以线程为单位来【阻塞】和【唤醒】线程,而notify只能随机唤醒一个,notifyAll只能唤醒所有,不能【精确】唤醒
- park和unpark可以先unpark而wait和notifyl不能先notify,先unpark也能恢复线程的运行
原理:
每个线程都有自己的Parker对象,由三部分组成_counter,_cond和_mutex。
线程就像一个旅人,Parker就是他随身携带的背包,
条件变量_cond就好比背包中的帐篷。_counter就好比背
包中的备用干粮(0为耗尽,1为充足)。
调用park就是看需不需要停下来休息
如果备用干粮耗尽,你们钻进帐篷休息
如果备用干粮充足,你们不需要停留,继续其前进
调用unpark就好比干粮充足
如果这时还在帐篷,就唤醒他继续前进
如果这时线程还在运行,你们他下次调用park时,仅是
消耗掉备用干粮,不需要停留,继续前进
因为背包空间有限,多次调用unpark仅会补充一份备用
干粮
调用park:
1.当前线程调用UnSafe.park()方法
2.检查counter,本情况为0,这时获得_mutex互斥锁
3.线程进入_cond条件变量阻塞
4.设置_counter = 0
先调用park后调用unpark:
1.调用Unsafe.unpark(Thread_0)方法,设置_counter为1
2.唤醒_cond条件变量中的Thread_0
3.Thread_0恢复运行
4.设置_counter为0
先调用unpark后调用park:
1.调用Unsafe.unpark(Thread_0)方法,设置_counter为1
2.当前线程调用Unsafe.park()方法
3.检查_counter,本情况为1,这时线程无需阻塞,继续运行
4.设置_counter为0