JUC并发-共享模型-Monitor管程/监视器-悲观锁(阻塞)

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 结构如下

  1. 刚开始Monitor中Owner为null
  2. 当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
  3. 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryListBLOCKED
  4. Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争的时是非公平的
  5. 图中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。以后只要不发生竞争,这个对象就归该线程所有。

一个对象创建时:

  1. 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0
  2. 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
  3. 如果没有开启偏向锁,那么对象创建后,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方法的线程唤醒。

  1. Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  2. BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  3. BLOCKED 线程会在 Owner 线程释放锁时唤醒
  4. 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);
        }
    }
}
  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值