Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性
1、共享带来的问题
如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
而对应 i-- 也是类似:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
多线程下访问共享资源,因为分时系统导致的数据不一致等安全问题
临界区Critical Section
一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
例如,下面代码中的临界区
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
2、synchronized 解决方案
*应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
synchronized即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
注意
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronized位置
①修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁。
//synchronized加在成员方法上,锁的是this对象;不是锁这个方法,synchronized只能锁对象
class Test{
public synchronized void test() {}
}
等价于
class Test{
public void test() {
synchronized(this) {}
}
}
②修饰静态方法:给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得当前类对象的锁。这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。Synchronized关键字加到static 静态方法和 synchronized(类.class)代码块上都是是给 Class 类上锁。
静态 synchronized 方法和非静态 synchronized 方法之间的调用互斥么?不互斥!如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
class Test{
public synchronized static void test() {}
}
等价于
class Test{
public void test() {
synchronized(Test.class) {}
}
}
③修饰代码块(锁指定对象/类)
- synchronized(object) 表示进入同步代码库前要获得 给定对象的锁
- synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁
synchronized(this) {}
线程8锁
3、变量的线程安全分析
成员变量和静态变量是否线程安全?
-
如果它们没有共享,则线程安全
-
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
-
局部变量是线程安全的
public static void test1() {
int i = 10;
i++;
}
每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享。
局部变量的i++只有一行字节码,不同于静态变量的i++。
当list不是成员变量时:
创建2个线程,然后每个线程去调用method1,如果method1还没把数据放入,method2就要取出数据,此时集合为空,会报错。
原因:无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量,method3 与 method2 分析相同
将 list 修改为局部变量,放到方法内
分析:
- list 是局部变量,每个线程调用时会创建其不同实例,没有共享
- 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
- method3 的参数分析与 method2 相同
-
但局部变量引用的对象则未必
-
如果该对象没有逃离方法的作用访问,它是线程安全的
-
如果该对象逃离方法的作用范围(return),需要考虑线程安全
-
例1:此时会有线程安全问题。
分析:线程1创建一个list,调用method3时,内部又有新的线程访问到了这个list。
例2:从下面的例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】
4、常见线程安全类
String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent包下的类。
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
它们的每个方法是原子的;但注意它们多个方法的组合不是原子的。
5、变量的线程安全案例分析
例1:
例2:
例3:
例4:
例5:
例6:
5.1 卖票-锁一个
@Slf4j(topic="c.ExerciseSell")
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
//模拟多人买票
TicketWindow window = new TicketWindow(100000);
//所有线程的集合
List<Thread> threadList = new ArrayList<>();
//卖出的票数统计
List<Integer> amountList = new Vector<>();
for(int i=0;i<20000;i++){
Thread thread = new Thread(()->{
int amount = window.sell(randomAmount());//买票
try {
Thread.sleep(randomAmount());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
amountList.add(amount);
});
threadList.add(thread);//不用安全的List,是因为threadList只会被主线程使用,不会被多个线程所共享
thread.start();
}
for (Thread thread :threadList) {
thread.join();
}
log.debug("余票:{}",window.getCount());
log.debug("卖出的票数:{}",amountList.stream().mapToInt(i->i).sum());
}
static Random random = new Random();
public static int randomAmount(){
return random.nextInt(5)+1;
}
}
class TicketWindow{
private int count;
public TicketWindow(int count){
this.count = count;
}
public int getCount(){
return count;
}
public int sell(int amount){
if(this.count >= amount){
this.count -= amount;
return amount;
}else{
return 0;
}
}
}
临界区:多个线程对共享变量有读写操作。
在sell方法中存在对共享变量的读写操作,因此只需要在方法上加synchronized:
public synchronized int sell(int amount){
if(this.count >= amount){
this.count -= amount;
return amount;
}else{
return 0;
}
}
5.2 转账-锁两个
这道题的难点在于有2个共享变量,一个是a的账户中的money,一个是b的账户中的money。
@Slf4j(topic="c.ExerciseTransfer")
public class ExerciseTransfer1{
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
a.transfer(b,randomAmount());
}
},"t1");
Thread t2 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
b.transfer(a,randomAmount());
}
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("total:{}",(a.getMoney()+b.getMoney()));
}
static Random random = new Random();
public static int randomAmount(){return random.nextInt(100)+1;}
}
class Account {
private int money;
public Account(int money){
this.money = money;
}
public int getMoney(){
return money;
}
public void setMoney(int money){
this.money = money;
}
public void transfer(Account target,int amount){
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);//自身余额,减去转账金额
target.setMoney(target.getMoney() + amount);//对方余额加上转账金额
}
}
}
偷懒的方法加入下面:synchronized(Account.class),相当于锁住两个账户的临界资源,缺点是n个账户只能有2个账户进行交互。
public void transfer(Account target,int amount){
synchronized(Account.class) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);//自身余额,减去转账金额
target.setMoney(target.getMoney() + amount);//对方余额加上转账金额
}
}
}
6、Monitor 概念
Java对象在内存中通常由两部分组成,一部分是它的对象头,另一部分是它对象中的成员变量。
6.1 Java对象头
以 32 位虚拟机为例
在32位虚拟机中,int在内存中占4个字节;Integer对象是它的对象头+还得有个value用来存储这个int整型,即8+4。
6.2 Monitor原理
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针
Monitor 结构如下
- 刚开始Monitor中Owner为null
- 当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
- 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryListBLOCKED
- Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争的时是非公平的
- 图中WaitSet中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程,后面讲wait-notify时会分析
【注意】①synchronized 必须是进入同一个对象的 monitor 才有上述的效果②不加 synchronized 的对象不会关联监视器,不遵从以上规则
7、轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1() {
synchronized(obj) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
Object分为对象头和对象体。
- 对象头由2部分组成,Mark Word(包含哈希码、分代年龄、状态位)和Klass Word(类型指针)。
- 对象体(存储成员变量的信息)。
原理:
①线程0会在线程的栈帧里生成锁记录(Lock Record)对象,包含对象指针(Object reference)和要加锁记录地址。对象指针是为了后期加锁之后,记录对象(Object)的地址。
② 加锁之后,要让锁记录中对象指针Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录。即让锁记录里的“lock record 地址 00”和锁对象里的“Hashcode Age Bias 01”进行交换。交换是为了表示加锁。
这么做的意义是:对对象(Object)来说,它能知道是哪个线程锁住了自己。对线程来说它能知道锁对象的信息。
③ 如果 cas 失败,有两种情况
- 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是当前线程自己又执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数,新的锁记录的原先的地址处会为null【上述代码是这种情况】
④解锁。
- 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重 入计数减一。
- 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,此时会进行锁清除,使用 cas 将 Mark Word 的值恢复给对象头,即线程会把Mark Word(“Hashcode Age Bias 01”)这部分内容还给Object,Object会把锁记录地址(“lock record 地址 00”)还给线程。
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
8、锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}
如果Thread1想加锁,但由于锁对象(Object)已经被Thread0加锁。
Thread1的锁记录地址(“lock record 地址00”)无法与Object的Mark Word进行替换,因此会进入锁膨胀的阶段。
所以Object 对象申请Monitor 锁,让 Object 指向重量级锁地址。 Monitor的Owner会指向线程0,然后线程1会挂载到EntryList上。
当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值(“Hashcode Age Bias 01”)恢复给对象头,失败(因为Object指向的是Monitor的地址,且后两位也变成1 0了)。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程。
9、自选优化-重量级锁竞争
自旋:多次cas尝试修改Mark Word,让Mark Word指向Lock Record
重量级锁竞争的时候(线程2请求加锁,此时线程1已经上了锁),还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞(避免上下文切换)。
【注】
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- Java 7 之后不能控制是否开启自旋功能
10、偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0
- 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
- 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值
偏向锁可以理解为:某个锁对象“偏向于”只给某个线程使用。
如果有其它线程来使用了锁对象,偏向锁会变成轻量锁。
如果线程之间存在竞争关系,轻量锁就会编程重量锁。
10.1 撤销-对象从可偏向变成不可偏向
调用对象hashcode()
调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销。【如果处于偏向锁状态,markword里最多也就存一个这个线程id,再想存31位的这个哈希码已经存不下了。所以当一个可偏向的对象,调用hashcode(),就会撤销这个对象的偏向状态。】
- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
其它线程使用对象
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。
这里可以这么理解,第2个线程运行到wait方法处会进行等待,一直等到线程1调用notify方法才会继续往下执行,拿到线程1释放的锁。会发现会从101结尾(偏向锁),变成000(轻量级)
调用wait/notify
wait/notify方法只有重量级锁才有
10.2 批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍然有机会重新偏向T2,重偏向会重置对象的Thread ID。
假如原本某个锁对象是偏向线程1,假如后面有线程2来访问,此时要注意:线程2每来执行一次,都是让锁对象从偏向锁变为轻量级锁(而这个锁对象仍然是线程1的偏向锁)。但当撤销偏向锁阈值超过20次之后,jvm会觉得,我是不是偏向错了(不应该偏向给线程1),于是会在给这些对象加锁时重新偏向至加锁线程。
10.3 批量撤销
当撤销偏向锁阈值超过40次后,jvm会觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
调用park()方法是用于阻塞某个线程,调用unpark(线程名)用来唤醒某个线程。
11、wait/notify
wait方法可以理解为让线程进入休息室等待,然后让其它线程继续工作。当另一个线程调用notify会将wait方法的线程唤醒。
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
11.1 API介绍
- obj.wait() 会释放对象的锁,让进入 object 监视器的线程到 waitSet 等待,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止。
无参的wait,默认传入0,表示无限等待。 - wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify
带参的wait,比如wait(1000),就是只等待1秒,如果等不到唤醒,就继续往下执行。 - obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
- obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
【注】
要注意一点,某个线程要先成为Owner,才有资格进入WaitSet。只有某个线程成为Owner,才有资格唤醒WaitSet中的线程。
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法。
@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t2").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t1").start();
// 主线程两秒后执行
sleep(0.5);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
//obj.notify(); // 唤醒obj上一个线程
obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}
11.2 wait和sleep
1) sleep 是 Thread 方法,而 wait 是 Object 的方法
2) sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
3) sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
4) 它们 状态 TIMED_WAITING
11.3 正确姿势-对比15.5
step1
某个线程调用sleep,会导致仍然带着锁没释放,别的线程会被阻塞,需要干等着,导致效率低。
@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep1 {
static final Object room = new Object();
static boolean hasCigarette = false; // 有没有烟
static boolean hasTakeout = false;
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
sleep(2);
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
sleep(1);
new Thread(() -> {
// 这里能不能加 synchronized (room)?
synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
}
}, "送烟的").start();
}
}
step2
step3 虚假唤醒
@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep3 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
// 虚假唤醒
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
}
}
step4
step5 notifyAll(while + wait)
总结
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}
12、同步模式之保护性暂停
即 Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
- JDK 中,join 的实现、Future 的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
12.1 实现
t1等待GuardedObject中response的值,t2为response赋值,会通知t1。下面程序的思路是:t2线程会调用Downloader的download方法下载资源,complete方法用于给成员变量赋值,然后通知所有线程。t1线程会调用get方法获取成员变量的值,如果成员变量没有值就wait等待,获取到值后打印代码行数。
@Slf4j(topic = "c.Test20")
public class Test15 {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(()->{
//等待结果
log.debug("等待结果");
List<String> list = (List<String>) guardedObject.get();
log.debug("结果大小:{}",list.size());
},"t1").start();
new Thread(()->{
log.debug("执行下载");
try {
List<String> list = Downloader.download();
guardedObject.complete(list);
} catch (IOException e) {
throw new RuntimeException(e);
}
},"t2").start();
}
}
class GuardedObject{
//结果
private Object response;
//获取结果
public Object get(){
synchronized (this){
while(response==null){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
return response;
}
}
//产生结果
public void complete(Object response){
synchronized (this){
//给结果成员变量赋值
this.response = response;
this.notifyAll();
}
}
}
问题1-超时事件后如何退出while循环
判断当前时间是否大于所设定的超时来判断
问题2-虚假唤醒问题
假如wait方法中传入的参数是timeout,假如线程在前一次被唤醒,参数还没准备好,在此休眠仍然会有2秒的超时时间,不符合要求。必须是timeout减去之前经过的时间。
测试
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(()->{
log.debug("begin");
Object response = guardedObject.get(2000);
log.debug("结果是:{}",response);
},"t1").start();
new Thread(()->{
log.debug("begin");
Sleeper.sleep(1);
guardedObject.complete(null);
},"t2").start();
}
12.2 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;
}
}
}
12.3 解耦等待和生产
如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理。
@Slf4j(topic = "c.Test20")
public class Test15 {
public static void main(String[] args) {
for(int i=0;i<3;i++){
new People().start();
}
Sleeper.sleep(1);
for(Integer id : Mailboxes.getIds()){
new Postman(id,"内容"+id).start();
}
}
}
@Slf4j(topic="c.People")
class People extends Thread{
@Override
public void run() {
//收信
GuardedObject guardedObject = Mailboxes.createGuardedObject();
log.debug("开始收信 id:{}",guardedObject.getId());
Object mail = guardedObject.get(5000);
log.debug("收到信 id:{} , 内容:{}",guardedObject.getId(),mail);
}
}
@Slf4j(topic="c.Postman")
class Postman extends Thread{
private int id;
private String mail;
public Postman(int id,String mail){
this.id = id;
this.mail = mail;
}
@Override
public void run() {
GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
log.debug("开始收信 id:{},内容:{}",id,mail);
guardedObject.complete(mail);
}
}
class Mailboxes{
private static Map<Integer,GuardedObject> boxes = new Hashtable<>();
private static int id = 1;
//产生唯一id
private static synchronized int generateId(){
return id++;
}
public static GuardedObject getGuardedObject(int id){
return boxes.remove(id);
}
public static GuardedObject createGuardedObject(){
GuardedObject go = new GuardedObject(generateId());
boxes.put(go.getId(),go);
return go;
}
public static Set<Integer> getIds(){
return boxes.keySet();
}
}
class GuardedObject{
//新增 id 用来标识 Guarded Object
private int id;
public GuardedObject(int id){
this.id = id;
}
public int getId(){
return id;
}
//结果
private Object response;
//获取结果
public Object get(long timeout){
synchronized (this){
//开始时间
long begin = System.currentTimeMillis();
//经历的时间
long passedTime = 0;
while(response==null){
//经历的时间超过最大等待时间,退出循环
if(passedTime>=timeout){
break;
}
try {
this.wait(timeout-passedTime);//防止虚假唤醒,唤醒之后结果数据还没准备好。
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//求得经历时间
passedTime = System.currentTimeMillis() - begin;
}
return response;
}
}
//产生结果
public void complete(Object response){
synchronized (this){
//给结果成员变量赋值
this.response = response;
this.notifyAll();
}
}
}
12.4 生产者消费者
保护性暂停是同步的,生产者/消费者是异步的:
- 与前面的保护性暂停中的GuardObject不同,不需要产生结果和消费结果的线程一一对应(比如之前的保护性暂停中要求1个居民要配1个快递员,如果有几百个居民,则需要配几百个快递员)。
- 消费队列可以用来平衡生产和消费的线程资源。
- 生产者仅负责产生结果数据,不关心数据如何处理,消费者专心处理结果数据。
- 消息队列有容量限制,满时不再加入数据,空时不会再消耗数据。
- JDK中各种阻塞队列,采用的是这种模式。
【实现】
- 线程之间通信id很重要,线程之间不知道,id作为桥梁,可以检查消息受到了没有,因此设置一个Message类,在类里加入id属性。
- 在Message类前加final(不能有子类),仅有get方法,因此是线程安全的。
- 双向队列在Java里的实现是LinkedList
@Slf4j(topic = "c.Test21")
public class Test16 {
public static void main(String[] args) {
MessageQueue queue = new MessageQueue(2);
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(()->{
queue.put(new Message(id,"值"+id));
},"生产者"+i).start();
}
new Thread(()->{
while(true) {
sleep(1);
Message message = queue.take();
}
},"消费者").start();
}
}
@Slf4j(topic = "c.MessageQueue")
class MessageQueue{
//消息的队列集合
private LinkedList<Message> list = new LinkedList<>();
//队列容量
private int capcity;
public MessageQueue(int capcity){
this.capcity=capcity;
}
//获取消息
public Message take(){
//检查队列是否为空
synchronized (list) {
while (list.isEmpty()) {
try {
log.debug("队列为空,消费者线程等待");
list.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//从队列头部获取消息返回。
Message message = list.removeFirst();
log.debug("已消费消息{}",message);
list.notifyAll();
return message;
}
}
//存入消息
public void put(Message message){
synchronized (list){
//检查队列是否已满
while(list.size()==capcity){
try {
log.debug("队列为满,生产者线程等待");
list.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//将消息加入队列尾部
list.addLast(message);
log.debug("已生产消息{}",message);
list.notifyAll();
}
}
}
final class Message{
private int id;
private Object value;
public Message(int id,Object value){
this.id=id;
this.value=value;
}
public int getId(){
return id;
}
public Object getValue(){
return value;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", value=" + value +
'}';
}
}
13、多把不相干的锁
一间大屋子有两个功能:睡觉、学习,互不相干。 现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低。
public class TestMultiLock {
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
new Thread(() -> {
bigRoom.study();
},"小南").start();
new Thread(() -> {
bigRoom.sleep();
},"小女").start();
}
}
@Slf4j(topic = "c.BigRoom")
class BigRoom {
public void sleep() {
synchronized (this) {
log.debug("sleeping 2 小时");
Sleeper.sleep(2);
}
}
public void study() {
synchronized (this) {
log.debug("study 1 小时");
Sleeper.sleep(1);
}
}
}
解决方法是准备多个房间(多个对象锁)。
将锁的粒度细分
- 好处,是可以增强并发度
- 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁
14、活跃性
活跃性是指线程内的代码本来是有限的,但是由于某种原因,线程代码一直运行不完
14.1 死锁-各自持有一把锁,但还想获得对方的锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
t1线程获得A对象锁,接下来想获取B对象的锁;t2线程获得B对象锁,接下来想获取A对象的锁。
@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
public static void main(String[] args) {
test1();
}
private static void test1() {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}
定位死锁
检测死锁可以用jconsole工具,或者使用.jps定位进程id,再用jstack定位死锁。
哲学家就餐
如果所有哲学家都拿着一双筷子,死锁发生
public class TestDeadLock {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子
synchronized (left) {
// 尝试获得右手筷子
synchronized (right) {
eat();
}
}
}
}
Random random = new Random();
private void eat() {
log.debug("eating...");
Sleeper.sleep(0.5);
}
}
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
14.2 活锁-两个线程互相改变对方的结束条件
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。
@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
sleep(0.2);
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
sleep(0.2);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
}
14.3 饥饿
一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束
15、ReentrantLock
与 synchronized 一样,都支持可重入。相对于 synchronized 它具备如下特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
15.1 案例
@Slf4j(topic = "c.TestReentrant")
public class TestReentrant {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
log.debug("execute method1");
method2();
} finally {
lock.unlock();
}
}
public static void method2() {
lock.lock();
try {
log.debug("execute method2");
method3();
} finally {
lock.unlock();
}
}
public static void method3() {
lock.lock();
try {
log.debug("execute method3");
} finally {
lock.unlock();
}
}
}
15.2 可打断-reentrantLock.lockInterruptibly()
reentrantLock.lockInterruptibly()可中断模式
可打断:在等待锁的过程中,其它线程可以用interrupt的方法终止线程的等待,防止死锁。
@Slf4j(topic="c.Test18")
public class Test18 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
//如果没有竞争那么此方法会获取lock对象锁
//如果有竞争就进入阻塞队列,可以被其它线程用interrupt方法打断
log.debug("尝试获取锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("没有获得锁,返回");
return;
}
try{
log.debug("获取到锁");
}finally{
lock.unlock();
}
}, "t1");
lock.lock();
t1.start();
try {
sleep(1);
t1.interrupt();
log.debug("执行打断");
} finally {
lock.unlock();
}
}
}
注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断
15.3 锁超时-reentrantLock.tryLock()
某线程尝试获得锁,如果未获取到,不会立刻死亡,会等待一段时间,如果锁仍未释放,就放弃等待,表示锁获取失败。避免线程无限次等待,避免死锁。
- tryLock() 立刻失败
- tryLock(2, TimeUnit.MINUTES.SECONDS) 超时失败
@Slf4j(topic="c.Test19")
public class Test19 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
log.debug("尝试获得锁");
try {
if(!lock.tryLock(2, TimeUnit.MINUTES.SECONDS)){
log.debug("获取不到锁");
return ;
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("获取不到锁");
return;
}
try{
log.debug("获得到锁");
}finally{
lock.unlock();
}
},"t1");
lock.lock();
log.debug("获得到锁");
t1.start();
sleep(1);
lock.unlock();
log.debug("释放了锁锁");
}
}
解决哲学界就餐问题
15.4 公平锁
ReentrantLock 默认是不公平的。公平锁一般没有必要,会降低并发度
15.5 条件变量-对比11.3
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待。
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
使用要点
- 在调用awai()方法前需要先获得锁
- 调用条件变量的await()会释放锁,进入conditionObject等待,(相当于进入休息室等待)。
- 调用条件变量的signal()会唤醒线程,唤醒之后会重新竞争lock锁。
- 竞争lock锁成功后会从awit后继续执行
例子-对比11.3
@Slf4j(topic = "c.TestCondition")
public class TestCondition {
static boolean hasCigarette = false;
static boolean hasTakeout = false;
static ReentrantLock ROOM = new ReentrantLock();
// 等待烟的休息室
static Condition waitCigaretteQueue = ROOM.newCondition();
// 等待外卖的休息室
static Condition waitTakeoutQueue = ROOM.newCondition();
public static void main(String[] args) {
new Thread(() -> {
ROOM.lock();
try{
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
waitCigaretteSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
}finally {
ROOM.unlock();
}
}, "小南").start();
new Thread(() -> {
ROOM.lock();
try{
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
waitTakeoutSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
}finally {
ROOM.unlock();
}
}, "小女").start();
sleep(1);
new Thread(() -> {
ROOM.lock();
try {
hasTakeout = true;
waitTakeoutSet.signal();
} finally {
ROOM.unlock();
}
}, "送外卖的").start();
new Thread(() -> {
ROOM.lock();
try {
hasCigarette = true;
waitCigaretteSet.signal();
} finally {
ROOM.unlock();
}
}, "送烟的").start();
}
}
16、固定运行顺序
使用wait和notify
@Slf4j(topic="c.Test21")
public class Test21 {
static final Object lock = new Object();
//表示t2是否运行过
static boolean t2runned = false;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
synchronized (lock) {
while(!t2runned) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("1");
}
},"t1");
Thread t2 = new Thread(()->{
synchronized (lock){
if(!t2runned){
t2runned=true;
log.debug("2");
lock.notify();
}
}
},"t2");
t1.start();
t2.start();
}
}
使用park&unpark
@Slf4j(topic="c.Test23")
public class Test23 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
LockSupport.park();
log.debug("1");
}, "t1");
t1.start();
new Thread(()->{
log.debug("2");
LockSupport.unpark(t1);
},"t2").start();
}
}
17、交替输出
线程1输出5次a,线程2输出5次b,线程3输出5次c。输出结果为abcabcabcabcabcabc。
wait notify
@Slf4j(topic="c.Test22")
public class Test22 {
public static void main(String[] args) {
WaitNotify wn = new WaitNotify(1,5);
new Thread(()->{
wn.print("a",1,2);
}).start();
new Thread(()->{
wn.print("b",2,3);
}).start();
new Thread(()->{
wn.print("c",3,1);
}).start();
}
}
/*
输出内容 等待标记 下一个标记
a 1 2
b 2 3
c 3 1
*/
class WaitNotify{
public void print(String str,int waitFlag,int nextFlag){
for(int i=0;i<loopNumber;i++){
synchronized(this){
while(flag != waitFlag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(str);
flag=nextFlag;
this.notifyAll();
}
}
}
//等待标记
private int flag;
//循环次数
private int loopNumber;
public WaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
}
ReentrantLock-await&signal
public class Test24 {
public static void main(String[] args) throws InterruptedException {
AwaitSignal awaitSignal = new AwaitSignal(5);
Condition a = awaitSignal.newCondition();
Condition b = awaitSignal.newCondition();
Condition c = awaitSignal.newCondition();
new Thread(()->{
awaitSignal.print("a",a,b);
}).start();
new Thread(()->{
awaitSignal.print("b",b,c);
}).start();
new Thread(()->{
awaitSignal.print("c",c,a);
}).start();
Thread.sleep(1000);
awaitSignal.lock();
try{
System.out.println("开始...");
a.signal();
}finally{
awaitSignal.unlock();
}
}
}
class AwaitSignal extends ReentrantLock{
private int loopNumber;
public AwaitSignal(int loopNumber){
this.loopNumber = loopNumber;
}
//参数1打印内容,参数2进入哪一间休息室,参数3下一间休息室
public void print(String str,Condition current,Condition next){
for(int i=0;i<loopNumber;i++){
lock();
try{
current.await();
System.out.println(str);
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
unlock();
}
}
}
}
park&unpark
public class Test25 {
static Thread t1;
static Thread t2;
static Thread t3;
public static void main(String[] args) {
ParkUnpark pu = new ParkUnpark(5);
t1 = new Thread(()->{
pu.print("a",t2);
});
t2 = new Thread(()->{
pu.print("b",t3);
});
t3 = new Thread(()->{
pu.print("c",t1);
});
t1.start();
t2.start();
t3.start();
LockSupport.unpark(t1);
}
}
class ParkUnpark{
private int loopNumber;
public ParkUnpark(int loopNumber){
this.loopNumber = loopNumber;
}
public void print(String str,Thread next){
for(int i=0;i<loopNumber;i++){
LockSupport.park();
System.out.println(str);
LockSupport.unpark(next);
}
}
}