Java并发- 4 共享模型管程👦🏻

本章内容

  • 共享问题
  • synchronized
  • 线程安全分析
  • Monitor
  • wait/notify
  • 线程状态转换
  • 活跃性
  • Lock

4.1. 共享带来的问题

4.1.1. 小故事

  • 老王(操作系统)有一个功能强大的算盘(CPU),现在想把它租出去,赚一点外快

image.png

  • 小南、小女(线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用
  • 但小南不能一天24小时使用算盘,他经常要小憩一会(sleep),又或是去吃饭上厕所(阻塞 io 操作),有时还需要一根烟,没烟时思路全无(wait)这些情况统称为(阻塞)

image.png

  • 在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算
  • 另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平
  • 于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ]
  • 这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然
  • 最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了一个笔记本(主存),把一些中间结果先记在本上
  • 计算流程是这样的

image.png

  • 但是由于分时系统,有一天还是发生了事故
  • 小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果
  • 老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1...] 不甘心地到一边待着去了(上下文切换)
  • 老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果 -1 写入笔记本
  • 这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写入了笔记本

image.png

  • 小南和小女都觉得自己没做错,但笔记本里的结果是 1 而不是 0。

4.1.2. Java 的体现

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

package com.beatshadow.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

/**
 * 
 * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
 * @since : 2020/4/28 19:53
 */
@Slf4j
public class Example1 {
    private static int count = 0 ;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5000 ; i++) {
                count++;
            }
        }, "t1");

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5000 ; i++) {
                count--;
            }
        }, "t2");
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug(String.valueOf(count));
    }
}

4.1.3. 问题分析

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析
例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic     i  // 获取静态变量i的值
iconst_1         // 准备常量1
iadd             // 自增
putstatic     i  // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

getstatic     i  // 获取静态变量i的值
iconst_1         // 准备常量1
isub             // 自减
putstatic     i  // 将修改后的值存入静态变量i




而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1588075731844-74badb1a-21dc-4f2f-bf6d-518512ced587.png#align=left&display=inline&height=221&margin=%5Bobject%20Object%5D&name=image.png&originHeight=295&originWidth=621&size=11742&status=done&style=shadow&width=466)
如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题: ![](https://cdn.nlark.com/yuque/__mermaid_v3/8cb42be6ad8ccdcb3a645d53a51bdb7f.svg#lake_card_v2=eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG5wYXJ0aWNpcGFudCB0MSBhcyDnur_nqIsxXG5wYXJ0aWNpcGFudCBpIGFzIHN0YXRpYyBpXG5cdGkgLT4-IHQxIDogZ2V0c3RhdGljIGkg6K-75Y-WIDBcblx0dDEgLT4-IHQxIDogaWNvbnN0XzEg5YeG5aSH5bi45pWwIDFcblx0dDEgLT4-IHQxIDogaWFkZCDliqDms5UsIOe6v-eoi-WGhSBpID0gMVxuXHR0MSAtPj4gaSA6IHB1dHN0YXRpYyBpIOWGmeWFpSAxXG5cdGkgLT4-IHQxIDogZ2V0c3RhdGljIGkg6K-75Y-WIDFcblx0dDEgLT4-IHQxIDogaWNvbnN0XzEg5YeG5aSH5bi45pWwIDFcblx0dDEgLT4-IHQxIDogaXN1YiDlh4_ms5UsIOe6v-eoi-WGhSBpID0gMFxuXHR0MSAtPj4gaSA6IHB1dHN0YXRpYyBpIOWGmeWFpSAwIiwidHlwZSI6Im1lcm1haWQiLCJtYXJnaW4iOnRydWUsImlkIjoibFpIb2YiLCJ1cmwiOiJodHRwczovL2Nkbi5ubGFyay5jb20veXVxdWUvX19tZXJtYWlkX3YzLzhjYjQyYmU2YWQ4Y2NkY2IzYTY0NWQ1M2E1MWJkYjdmLnN2ZyIsImhlaWdodCI6NDgwLCJjYXJkIjoiZGlhZ3JhbSJ9)但多线程下这 8 行代码可能交错运行:
出现负数的情况: ![](https://cdn.nlark.com/yuque/__mermaid_v3/58f2cf0f423982ea8f6b2dcf36f70f2f.svg#lake_card_v2=eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG5wYXJ0aWNpcGFudCB0MSBhcyDnur_nqIsxXG5wYXJ0aWNpcGFudCB0MiBhcyDnur_nqIsyXG5wYXJ0aWNpcGFudCBpIGFzIHN0YXRpYyBpXG5cdGkgLT4-IHQyIDogZ2V0c3RhdGljIGkg6K-75Y-WIDBcblx0dDIgLT4-IHQyIDogaWNvbnN0XzEg5YeG5aSH5bi45pWwIDFcblx0dDIgLT4-IHQyIDogaXN1YiDlh4_ms5UsIOe6v-eoi-WGhSBpID0gLTFcblx0dDIgLS0-PiB0MSA6IOS4iuS4i-aWh-WIh-aNolxuXHRcblx0aSAtPj4gdDEgOiBnZXRzdGF0aWMgaSDor7vlj5YgMFxuXHR0MSAtPj4gdDEgOiBpY29uc3RfMSDlh4blpIfluLjmlbAgMVxuXHR0MSAtPj4gdDEgOiBpYWRkIOWKoOazlSwg57q_56iL5YaFIGkgPSAxXG5cdHQxIC0-PiBpIDogcHV0c3RhdGljIGkg5YaZ5YWlIDFcblx0XG5cdHQxIC0tPj4gdDIgOiDkuIrkuIvmlofliIfmjaJcdFxuXHR0MiAtPj4gaSA6IHB1dHN0YXRpYyBpIOWGmeWFpSAtMVxuXHQiLCJ0eXBlIjoibWVybWFpZCIsIm1hcmdpbiI6dHJ1ZSwiaWQiOiJCWWV4TCIsInVybCI6Imh0dHBzOi8vY2RuLm5sYXJrLmNvbS95dXF1ZS9fX21lcm1haWRfdjMvNThmMmNmMGY0MjM5ODJlYThmNmIyZGNmMzZmNzBmMmYuc3ZnIiwiaGVpZ2h0Ijo0NjIsImNhcmQiOiJkaWFncmFtIn0=)出现正数的情况: ![](https://cdn.nlark.com/yuque/__mermaid_v3/a4b7939637a4c7841db1c66e69c5be04.svg#lake_card_v2=eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG5wYXJ0aWNpcGFudCB0MSBhcyDnur_nqIsxXG5wYXJ0aWNpcGFudCB0MiBhcyDnur_nqIsyXG5wYXJ0aWNpcGFudCBpIGFzIHN0YXRpYyBpXG5cdGkgLT4-IHQxIDogZ2V0c3RhdGljIGkg6K-75Y-WIDBcblx0dDEgLT4-IHQxIDogaWNvbnN0XzEg5YeG5aSH5bi45pWwIDFcblx0dDEgLT4-IHQxIDogaWFkZCDliqDms5UsIOe6v-eoi-WGhSBpID0gMVxuXHR0MSAtLT4-IHQyIDog5LiK5LiL5paH5YiH5o2iXG5cdGkgLT4-IHQyIDogZ2V0c3RhdGljIGkg6K-75Y-WIDBcblx0dDIgLT4-IHQyIDogaWNvbnN0XzEg5YeG5aSH5bi45pWwIDFcblx0dDIgLT4-IHQyIDogaXN1YiDlh4_ms5UsIOe6v-eoi-WGhSBpID0gLTFcblx0dDIgLT4-IGkgOiBwdXRzdGF0aWMgaSDlhpnlhaUgLTFcblx0dDIgLS0-PiB0MSA6IOS4iuS4i-aWh-WIh-aNolxuXHR0MSAtPj4gaSA6IHB1dHN0YXRpYyBpIOWGmeWFpSAxIiwidHlwZSI6Im1lcm1haWQiLCJtYXJnaW4iOnRydWUsImlkIjoicVJCRDAiLCJ1cmwiOiJodHRwczovL2Nkbi5ubGFyay5jb20veXVxdWUvX19tZXJtYWlkX3YzL2E0Yjc5Mzk2MzdhNGM3ODQxZGIxYzY2ZTY5YzViZTA0LnN2ZyIsImhlaWdodCI6NDgwLCJjYXJkIjoiZGlhZ3JhbSJ9) ### 4.1.4. 临界区 Critical Section
  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
    例如,下面代码中的临界区
static int counter = 0;

static void increment() 
// 临界区
{   
    //++既有读,又有写,而且counter为共享资源
    counter++;
}

static void decrement() 
// 临界区
{    
    counter--;
}

4.1.5. 竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了**竞态条件。


为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,****Lock
  • 非阻塞式的解决方案:****原子变量

4.2. synchronized 解决方案

本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

注意> 虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

4.2.1. 语法

synchronized(对象) // 线程1, 线程2(blocked)
{
	临界区
}

4.2.2. 解决

package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/16 23:11
 */
@Slf4j
public class Example2 {
    private static int count = 0;

    private static Object lock = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock) {
                for (int i = 0; i < 5000; i++) {
                    count++;
                }
            }
        }, "t1");

        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                for (int i = 0; i < 5000; i++) {
                    count--;
                }
            }
        }, "t2");
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug(String.valueOf(count));
    }

}

4.2.3. 理解

image.png
你可以做这样的类比:

  • synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人
  • 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码
  • 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了
  • 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
  • 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码

用图来表示

4.2.4. 思考

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
为了加深理解,请思考下面的问题:

  • 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性
    • 只对count++或者count--加锁,只是对其中的4行代码加锁,如果加在for循环外,那其实是对5000*4=20000行代码加了说,虽然结果没有问题,但是效率不高。
  • 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?-- 锁对象【错误】
    • 因为使用的锁对象不同,故而在运行t1的时候,t2并不会阻塞。
  • 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象【错误】
    • t1执行的时候,t2并没有阻塞。

4.2.5. 面向对象改造

package com.bloom.concurrent.chapter4;

import java.util.concurrent.TimeUnit;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/16 23:16
 */
public class Example3 {

    public static void main(String[] args) throws InterruptedException {
        Room room = new Room() ;
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                room.add();
            }
        }, "td1");

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                room.subtract();
            }
        }, "td2");

        thread1.start();
//        thread1.join();
        thread2.start();
//        thread2.join();
        TimeUnit.SECONDS.sleep(1);
        System.out.println(room.getCount());
    }

}

class Room{
    private int count = 0;

    //加法
    public void add(){
        synchronized (this){
            count ++ ;
        }
    }
    //减法
    public void subtract(){
        synchronized (this){
            count -- ;
        }
    }
    //获取
    public int getCount(){
        synchronized (this){
            return count ;
        }
    }
}

4.3. synchronized加在方法上

4.3.1. 加在成员方法上(锁的是this对象)

class Test{
	public synchronized void test() {
	
	}
}
等价于
class Test{
	public void test() {
		synchronized(this) {
		
		}
	}
}

4.3.2. 加在静态方法上(锁的是类对象)

class Test{
	public synchronized static void test() {

	}
}
等价于
class Test{
	public static void test() {
		synchronized(Test.class) {
			
		}
	}
}

4.3.3. 不加 synchronized 的方法

不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的),不保证原子性。

4.3.4. 所谓的“线程八锁”

其实就是考察 synchronized 锁住的是哪个对象

  1. 情况1
package com.bloom.concurrent.chapter4;

import com.sun.org.apache.bcel.internal.generic.NEW;
import lombok.extern.slf4j.Slf4j;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/17 08:51
 */
@Slf4j
public class TestThread8Monitor_1 {
    public static void main(String[] args) {
        Number1 number1 = new Number1();
        Thread t1 = new Thread(() -> {
            number1.a();
        }, "t1");

        Thread t2 = new Thread(() -> {
            number1.b();
        }, "t2");
        t1.start();
        t2.start();
    }
    // 1 2 或者 2 1


}

@Slf4j
class Number1 {
    public synchronized void a() {
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }
}
  1. 情况2
package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import static java.lang.Thread.sleep;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/17 08:55
 */
@Slf4j
public class TestThread8Monitor_2 {

    public static void main(String[] args) {
        Number2 n1 = new Number2();
        new Thread(()->{
            try {
                n1.a();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{ n1.b(); }).start();
    }

    // 1s 1 2 或 2 1s 1

}

@Slf4j(topic = "c.Number")
class Number2{
    public synchronized void a() throws InterruptedException {
        sleep(1000);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}
  1. 情况3
package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/17 08:57
 */
public class TestThread8Monitor_3 {

    public static void main(String[] args) {
        Number3 n1 = new Number3();
        new Thread(() -> {
            try {
                n1.a();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(() -> {
            n1.b();
        }).start();
        new Thread(() -> {
            n1.c();
        }).start();
    }

    //3  1s 1 2
    //2  3 1S 1
    //3  2 1s 1

}

@Slf4j(topic = "c.Number")
class Number3 {
    public synchronized void a() throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }

    public void c() {
        log.debug("3");
    }
}

  1. 情况4
package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/17 08:59
 */
public class TestThread8Monitor_4 {

    public static void main(String[] args) {
        Number4 n1 = new Number4();
        Number4 n2 = new Number4();
        new Thread(()->{
            try {
                n1.a();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{ n2.b(); }).start();
    }

    // 2  1S 1
}

@Slf4j(topic = "c.Number")
class Number4{
    public synchronized void a() throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}

  1. 情况5
package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/17 09:01
 */
@Slf4j
public class TestThread8Monitor_5 {

    public static void main(String[] args) {
        Number5 n1 = new Number5();
        new Thread(()->{
            try {
                n1.a();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{ n1.b(); }).start();
    }

    // 2 1S 1
}

@Slf4j(topic = "c.Number")
class Number5{
    public static synchronized void a() throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}

  1. 情况6
package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/17 09:02
 */
@Slf4j
public class TestThread8Monitor_6 {

    public static void main(String[] args) {
        Number6 number6 = new Number6() ;
        new Thread(() -> {
            try {
                number6.a();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(() -> {
            number6.b();
        }).start();

        // 2 1S 1
        // 1S 1 2
    }
}

@Slf4j(topic = "c.Number")
class Number6{
    public static synchronized void a() throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        log.debug("1");
    }
    public static synchronized void b() {
        log.debug("2");
    }
}
package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/17 09:02
 */
@Slf4j
public class TestThread8Monitor_6 {

    public static void main(String[] args) {
        Number6 number6 = new Number6() ;
        new Thread(() -> {
            try {
                number6.a();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(() -> {
            number6.b();
        }).start();

        // 2 1S 1
        // 1S 1 2
    }
}

@Slf4j(topic = "c.Number")
class Number6{
    public static synchronized void a() throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        log.debug("1");
    }
    public static synchronized void b() {
        log.debug("2");
    }
}

  1. 情况7
package com.beatshadow.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import static java.lang.Thread.sleep;

/**
 * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
 * @since : 2020/4/28 21:22
 */
@Slf4j
public class TestThread8Monitor_7 {

    public static void main(String[] args) {
        Number7 n1 = new Number7();
        Number7 n2 = new Number7();
        new Thread(()->{
            try {
                n1.a();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{ n2.b(); }).start();
    }

    // 2 1S 1
}

@Slf4j(topic = "c.Number")
class Number7{
    public static synchronized void a() throws InterruptedException {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}
  1. 情况8
package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/17 09:06
 */
@Slf4j
public class TestThread8Monitor_8 {

    public static void main(String[] args) {
        Number8 n1 = new Number8();
        Number8 n2 = new Number8();
        new Thread(()->{
            try {
                n1.a();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{ n2.b(); }).start();
    }

    // 2 1S 1
    //1 1S 2
}

@Slf4j(topic = "c.Number")
class Number8{
    public static synchronized void a() throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        log.debug("1");
    }
    public static synchronized void b() {
        log.debug("2");
    }
}

4.4. 变量的线程安全分析

4.4.1. 成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

4.4.2. 局部变量是否线程安全?

  • 局部变量是线程安全的
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

4.4.3. 局部变量的线程安全

public static void test1() {
    int i = 10;
    i++;
}

每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享

public static void test1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=0
         0: bipush        10
         2: istore_0
         3: iinc          0, 1
         6: return
      LineNumberTable:
        line 10: 0
        line 11: 3
        line 12: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            3       4     0     i   I

如图:
image.png

4.4.3.1. 成员变量的例子

package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/17 09:12
 */
@Slf4j
public class TestThreadSafe {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "Thread" + i).start();
        }
    }
}
class ThreadUnsafe {
    List<String> list = new ArrayList<>();
    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
            // { 临界区, 会产生竞态条件
            method2();
            method3();
            // } 临界区
        }
    }

    private void method2() {
        list.add("1");
    }

    private void method3() {
        list.remove(0);
    }
}

其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:
image.png
分析:

  • 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
  • method3 与 method2 分析相同

image.png

4.4.3.2. 改为局部变量

package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/17 09:12
 */
@Slf4j
public class TestThreadSafe {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
      /*  ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "Thread" + i).start();
        }*/
        ThreadSafe test2 = new ThreadSafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test2.method1(LOOP_NUMBER);
            }, "Thread" + i).start();
        }
    }
}
class ThreadUnsafe {
    List<String> list = new ArrayList<>();
    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
            // { 临界区, 会产生竞态条件
            method2();
            method3();
            // } 临界区
        }
    }

    private void method2() {
        list.add("1");
    }

    private void method3() {
        list.remove(0);
    }
}

class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }

    private void method2(ArrayList<String> list) {
        list.add("1");
    }

    private void method3(ArrayList<String> list) {
        list.remove(0);
    }
}

那么就不会有上述问题了
分析:

  • list 是局部变量,每个线程调用时会创建其不同实例,没有共享
  • 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
  • method3 的参数分析与 method2 相同

image.png

4.4.3.3. 局部变量——暴露引用

方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?

  • 情况1:有其它线程调用 method2 和 method3
    • 不会有线程安全问题
  • 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/17 09:12
 */
@Slf4j
public class TestThreadSafe {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;

    public static void main(String[] args) {
      /*  ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "Thread" + i).start();
        }
        ThreadSafe test2 = new ThreadSafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test2.method1(LOOP_NUMBER);
            }, "Thread" + i).start();
        }*/
        ThreadSafeSubClass test2 = new ThreadSafeSubClass();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test2.method1(LOOP_NUMBER);
            }, "Thread" + i).start();
        }

    }
}

class ThreadUnsafe {
    List<String> list = new ArrayList<>();

    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
            // { 临界区, 会产生竞态条件
            method2();
            method3();
            // } 临界区
        }
    }

    private void method2() {
        list.add("1");
    }

    private void method3() {
        list.remove(0);
    }
}

class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }

    public void method2(ArrayList<String> list) {
        list.add("1");
    }

    public void method3(ArrayList<String> list) {
        list.remove(0);
    }
}

class ThreadSafeSubClass extends ThreadSafe {
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

运行结果:(出错)


因为产生了共享资源,所谓的共享资源就是两个不同的线程访问了同一资源。
这时候我们把代码改成final、private,让其不能被继承,也是可以保证线程安全的。

从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】

4.5. 线程安全类

4.5.1. 常见线程安全类

  • 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();
  • 它们的每个方法是原子的
  • 注意它们多个方法的组合不是原子的,见后面分析

4.5.2. 线程安全类方法的组合

分析下面代码是否线程安全?

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
    table.put("key", value);
}

4.5.3. 不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。
有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?
查看substring源码:

    public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        //创建了一个新的String对象
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }
    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        //对原有数据进行了copy,然后放入到新的String对象中
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

后续会介绍不可变类的设计思想。

4.5.4. 实例分析

4.5.4.1. 例1

public class MyServlet extends HttpServlet {
    // 是否安全?
    Map<String,Object> map = new HashMap<>();
    // 是否安全?
    String S1 = "...";
    // 是否安全?
    final String S2 = "...";
    // 是否安全?
    Date D1 = new Date();
    // 是否安全?
    final Date D2 = new Date();
    
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        // 使用上述变量
    }
}
  • map 线程不安全
  • S1 线程安全
  • S2 线程安全
  • D1 线程不安全
  • D2 线程不安全,虽然加了final,只能保证地址引用是唯一的,但是Date对象不是不可变类,它的属性值还是可以发生变化的。

4.5.4.2. 例2

public class MyServlet extends HttpServlet {
    // 是否安全?
    private UserService userService = new UserServiceImpl();
    
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}

public class UserServiceImpl implements UserService {
    // 记录调用次数
    private int count = 0;
    
    public void update() {
        // ...
        count++;
    }
}

因为Servlet是单例对象。故而是线程共享的,那么MyServlet也是线程共享的,故而userService也是线程不安全的,上述程序应该避免在UserServiceImpl中定义成员变量,使用局部变量的方式去控制线程安全,或者去加互斥锁。

4.5.4.3. 例3

@Aspect
@Component
public class MyAspect {
    // 是否安全?
    private long start = 0L;
    
    @Before("execution(* *(..))")
    public void before() {
        start = System.nanoTime();
    }
    
    @After("execution(* *(..))")
    public void after() {
        long end = System.nanoTime();
        System.out.println("cost time:" + (end-start));
    }
}
  • 这是Spring中的一个切面类,Spring中没有强调的对象都是单例的,故而,start是线程共享的,会造成线程不安全
  • 改法尝试:
    • 将MyAspect设置为多例对象,那么无法保证前置通知和后置通知的对象是同一个 (不可取);
    • 使用环绕通知替换前置通知和后置通知,并将start变量改为局部变量(可取)。

4.5.4.4. 例4

public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl();
    
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}

public class UserServiceImpl implements UserService {
    // 是否安全
    private UserDao userDao = new UserDaoImpl();
    
    public void update() {
        userDao.update();
    }
}

public class UserDaoImpl implements UserDao { 
    public void update() {
        String sql = "update user set password = ? where username = ?";
        // 是否安全
        try (Connection conn = DriverManager.getConnection("","","")){
            // ...
        } catch (Exception e) {
            // ...
        }
    }
}

UserDaoImpl虽然是线程共享的,但是其中没有全局变量,而是一个单独的一个方法,故而是线程安全的,所以整体是线程安全的。

4.5.4.5. 例5

public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl();
    
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}

public class UserServiceImpl implements UserService {
    // 是否安全
    private UserDao userDao = new UserDaoImpl();
    
    public void update() {
        userDao.update();
    }
}

public class UserDaoImpl implements UserDao {
    // 是否安全
    private Connection conn = null;
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","");
        // ...
        conn.close();
    }
}

线程不安全的,因为Connection conn被作为了共享变量。

4.5.4.6. 例6

public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl();
    
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}

public class UserServiceImpl implements UserService {    
    public void update() {
        UserDao userDao = new UserDaoImpl();
        userDao.update();
    }
}

public class UserDaoImpl implements UserDao {
    // 是否安全
    private Connection = null;
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","");
        // ...
        conn.close();
    }
}

虽然Connection 不是线程安全的,但是每次在进行userDao.update()调用的时候,使用的都是new出来的新的对象。
4.5.4.7. 例7

public abstract class Test {
    
    public void bar() {
        // 是否安全
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        foo(sdf);
    }
    
    public abstract foo(SimpleDateFormat sdf);
    
    
    public static void main(String[] args) {
        new Test().bar();
    }
}

其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法

public void foo(SimpleDateFormat sdf) {
    String dateStr = "1999-10-11 00:00:00";
    for (int i = 0; i < 20; i++) {
        new Thread(() -> {
            try {
                sdf.parse(dateStr);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

请比较 JDK 中 String 类的实现【String是开闭原则的最好体现者

4.5.5. 是否存在线程安全问题

4.5.5.1. 卖票

package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;
import java.util.concurrent.TimeUnit;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/17 09:33
 */
@Slf4j
public class ExerciseSell {
    public static void main(String[] args) {
        TicketWindow ticketWindow = new TicketWindow(2000);
        List<Thread> list = new ArrayList<>();
        // 用来存储买出去多少张票
        List<Integer> sellCount = new Vector<>();
        for (int i = 0; i < 2000; i++) {
            Thread t = new Thread(() -> {
                // 分析这里的竞态条件
                int count = ticketWindow.sell(randomAmount());
                try {
                    TimeUnit.SECONDS.sleep(randomAmount());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                sellCount.add(count);
            });
            list.add(t);
            t.start();
        }
        list.forEach((t) -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // 买出去的票求和
        log.debug("selled count:{}",sellCount.stream().mapToInt(c -> c).sum());
        // 剩余票数
        log.debug("remainder count:{}", ticketWindow.getCount());
    }
    // Random 为线程安全
    static Random random = new Random();
    // 随机 1~5
    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;
        }
    }
}

测试问题:
有时候线程之间没有产生指令的混乱,不好演示,所以可以采用下面两中方式:

  • 方式一:休眠一个随机的时间,增加上下文切换的几率:

image.png
发现以下问题:售出的票数+剩余的票数=2002>2000。

09:38:31 [main] com.bloom.concurrent.chapter4.ExerciseSell - selled count:2002
09:38:31 [main] com.bloom.concurrent.chapter4.ExerciseSell - remainder count:0
  • 方式二:如果通过上述修改还不能演示出来问题所在,进行此操作。
    • 测试脚本:(目的是为了多运行几次)【window下】
for /L %n in (1,1,10) do java -cp "
.;C:\Users\manyh\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;
C:\Users\manyh\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;
C:\Users\manyh\.m2\repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar"
com.bloom.concurrent.chapter4.ExerciseSell

说明线程是不安全的,那么哪个地方是不安全的呢?

  1. 考虑临界区,根据临界区的定义,可以定位到如下位置:

image.png

  1. sellCount是一个线程安全的集合,那么问题只有能出现在sell方法
  2. 参看sell方法,发现count是共享变量,并且sell方法没有做互斥处理。

image.png
解决方案:将sell方法设定为synchronized方法。

4.5.5.2. 转账

package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import java.util.Random;

/**
 * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
 * @since : 2020/4/29 02:50
 */
@Slf4j
public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        //账户A 1000
        Account a = new Account(1000);
        //账户B 1000
        Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                //A转账给B
                a.transfer(b, randomAmount());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            //B转账给A
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 查看转账2000次后的总金额
        log.debug("total:{}", (a.getMoney() + b.getMoney()));
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~100
    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);
        }
    }
}

运行结果:
image.png
说明线程不安全。
分析:考虑谁是共享变量,发现A和B的account是都是共享变量,如何锁住共同的共享变量呢?可以使用下述方式:
image.png

4.6. Monitor原理

Java对象在内存中是由两部分组成:对象头和对象的成员变量。
HopSpot虚拟机的对象头(Object Header)分为两部分,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄(Generational GC Age)等。这部分数据的长度在32位和64位的Java虚拟机中分别占用32个或64个比特,官方称它为“Mark Word”。这部分是实现轻量级锁和偏向锁的关键。另一部分用于存储指向方法区对象类型数据的指针,如果是数组对象,还有一个额外的部分用于存储数组长度。

  1. 普通对象
|--------------------------------------------------------------|
|                     Object Header (64 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (32 bits)         |    Klass Word (32 bits) |
|------------------------------------|-------------------------|
  1. 数组对象
|---------------------------------------------------------------------------------|
|                                 Object Header (96 bits)                         |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|
  1. 32 位虚拟机 Mark Word 结构为
|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
|  hashcode:25         | age:4 | biased_lock:0 | 01     |       Normal       |
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | 01     |       Biased       |
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | 00     | Lightweight Locked |
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | 10     | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                              | 11     |    Marked for GC   |
|-------------------------------------------------------|--------------------|
  1. 64 位虚拟机 Mark Word
|--------------------------------------------------------------------|--------------------|
|                        Mark Word (64 bits)                         |       State        |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01    |       Normal       |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2     | unused:1 | age:4 | biased_lock:1 | 01    |       Biased       |
|--------------------------------------------------------------------|--------------------|
|             ptr_to_lock_record:62                          | 00    | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
|             ptr_to_heavyweight_monitor:62                  | 10    | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
|                                                            | 11    |    Marked for GC   |
|--------------------------------------------------------------------|--------------------|

参考资料
https://stackoverflow.com/questions/26357186/what-is-in-java-object-header【stackoverflow】
https://blog.csdn.net/scdn_cp/article/details/86491792【csnd】

  1. Monitor
    Monitor被翻译为监视器管程
    每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针。
    Monitor结构如下所示:
    image.png
  • 刚开始Monitor中Owner为null;
  • 当Thread-2执行了synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只有一个Owner;
  • 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED ;
  • Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争是非公平的;
  • 图中WaitSet中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程,后面讲wait-notify时会分析;

注意:

  • synchronized必须是进入同一对象的monitor才有上述结果
  • 不加synchronized的对象不会关联监视器,不遵守以上规则

4.7. synchronized原理

  1. 演示代码
package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/24 11:13
 */
@Slf4j
public class Example5 {

    private final static Object lock = new Object();

    static int num = 0;

    public static void main(String[] args) {
        synchronized (lock){
            num++;
        }

    }
}

  1. 对应的字节码为:

由以下可知:synchronized中的代码出错了也可以释放锁****。

// class version 52.0 (52)
// access flags 0x21
public class com/bloom/concurrent/chapter4/Example5 {

  // compiled from: Example5.java

  // access flags 0x1A
  private final static Lorg/slf4j/Logger; log

  // access flags 0x1A
  private final static Ljava/lang/Object; lock

  // access flags 0x8
  static I num

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 11 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/bloom/concurrent/chapter4/Example5; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
    TRYCATCHBLOCK L0 L1 L2 null
    TRYCATCHBLOCK L2 L3 L2 null
   L4
    LINENUMBER 18 L4
    GETSTATIC com/bloom/concurrent/chapter4/Example5.lock : Ljava/lang/Object;
    DUP
    ASTORE 1
    MONITORENTER
   L0
    LINENUMBER 19 L0
    GETSTATIC com/bloom/concurrent/chapter4/Example5.num : I
    ICONST_1
    IADD
    PUTSTATIC com/bloom/concurrent/chapter4/Example5.num : I
   L5
    LINENUMBER 20 L5
    ALOAD 1
    MONITOREXIT
   L1
    GOTO L6
   L2
   FRAME FULL [[Ljava/lang/String; java/lang/Object] [java/lang/Throwable]
    ASTORE 2
    ALOAD 1
    MONITOREXIT
   L3
    ALOAD 2
    ATHROW
   L6
    LINENUMBER 22 L6
   FRAME CHOP 1
    RETURN
   L7
    LOCALVARIABLE args [Ljava/lang/String; L4 L7 0
    MAXSTACK = 2
    MAXLOCALS = 3

  // access flags 0x8
  static <clinit>()V
   L0
    LINENUMBER 10 L0
    LDC Lcom/bloom/concurrent/chapter4/Example5;.class
    INVOKESTATIC org/slf4j/LoggerFactory.getLogger (Ljava/lang/Class;)Lorg/slf4j/Logger;
    PUTSTATIC com/bloom/concurrent/chapter4/Example5.log : Lorg/slf4j/Logger;
   L1
    LINENUMBER 13 L1
    NEW java/lang/Object
    DUP
    INVOKESPECIAL java/lang/Object.<init> ()V
    PUTSTATIC com/bloom/concurrent/chapter4/Example5.lock : Ljava/lang/Object;
   L2
    LINENUMBER 15 L2
    ICONST_0
    PUTSTATIC com/bloom/concurrent/chapter4/Example5.num : I
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 0
}

synchronized的工作方式是让每个对象都关联一个monitor,而monitor是真正的锁,但是monitor锁是由操作系统提供的,要使用它成本是比较高的,如果是每次进入synchronized都需要获取monitor锁,对于程序运行的性能是有影响的,基于这个理由,Java6开始,对synchronized获取锁的方式进行了一些改进,进行了一些优化,从直接使用monitor锁改成了还可以使用轻量级锁、偏向锁等来进行优化。

4.8. synchronized原理进阶

4.8.1. 小故事

故事角色

  • 老王 - JVM
  • 小南 - 线程
  • 小女 - 线程
  • 房间 - 对象
  • 房间门上 - 防盗锁 - Monitor
  • 房间门上 - 小南书包 - 轻量级锁
  • 房间门上 - 刻上小南大名 - 偏向锁
  • 批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
  • 不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向

小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样,即使他离开了,别人也进不了门,他的工作就是安全的。
但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女晚上用。每次上锁太麻烦了,有没有更简单的办法呢?
第一次优化:【轻量级锁】
小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式。
后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。
第二次优化:【偏向锁】
于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。
同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字
后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包。

4.8.2. 轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是synchronized。
假设有两个方法同步块,利用同一个对象加锁。

public static void method1(){
        synchronized (object){
            //同步块A
            method2();
        }
    }

private static void method2() {
        synchronized (object){
            //同步块B
        }
    }
  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的MarkWord。
    • lock record:
      • Mark Word
      • 对象的指针

image.png

  • 让锁记录中的Object reference指向锁对象,并尝试用CAS替换Object的Mark Word,将Mark Word的值存入锁记录。
    • Lock Record 中的状态是00 ,对象中的状态是01;【参见:Mark Word】(00表示:Normal ,01表示:轻量级锁。)

image.png

  • 如果CAS替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁,这时图示如下:

image.png

  • 如果CAS替换失败,有两种情况:
    • 如果是其他线程已经持有了该Object的轻量级锁,这是表明有竞争,进入锁膨胀过程;
    • 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数。

image.png

  • 当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这是重置锁记录,表示重入计数减一。

image.png

  • 当退出synchronized代码块(解锁时)锁记录的值不为null,这时使用CAS将Mark Word的值恢复给对象(还原工作)
    • 成功,则表示解锁成功;
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

4.8.3. 锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这是一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这是需要进行锁膨胀,将轻量级锁变为重量级锁。

    private final static Object object = new Object();

    public static void method1(){
        synchronized (object){
            //同步块A
            method2();
        }
    }
  • 当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁,如下图所示:
           
  • 这时Thread-1加轻量级锁失败,进入锁膨胀流程:
    • 即为Object对象申请Monitor锁,让Object指向重量级锁地址
    • 然后自己进入Monitor的EntityList BLOCKED(因为Thread-1没有竞争到锁)

image.png

  • 当Thread-0退出同步块解锁时,使用CAS将Mark Word的值恢复给对象头,失败,这时会进入重量级锁解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null(表示此是没有主人),唤醒EntryList中的BLOCKED线程。

4.8.4. 自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这是当前线程就可以避免阻塞。【因为阻塞要发生上下文切换】
自旋成功的情况:
image.png
自旋失败的情况:
image.png

  • 在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋的可能性会很高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
  • Java7之后不能控制是否开启自旋锁功能。

4.8.5. 偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次发现锁重入时仍然需要执行CAS操作。
Java6中引入了偏向锁来做进一步优化;只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有。
例如:

private final static Object object = new Object();

    public static void method1(){
        synchronized (object){
            //同步块A
            method2();
        }
    }

    private static void method2() {
        synchronized (object){
            //同步块B
            method3();
        }
    }

    private static void method3(){
        synchronized (object){
            //同步块C
        }
    }

image.png
image.png

4.8.5.1. 偏向状态

回忆一下对象头格式:
image.png
一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,mark word值为0x05即后三位为101,这是它的thread、epoch、age都为0;
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数:-XX:BiasedLockingStartupDelay=0来禁止延迟;
  • 如果没有开启偏向锁,那么对象创建后,Mark word只为0x01即后三位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值。
4.8.5.1.1. 验证延迟性
  1. 代码:
package com.bloom.concurrent.chapter4;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

import java.util.concurrent.TimeUnit;

/**
 * @description: *-XX:+UseBiasedLocking 【默认开启】-XX:BiasedLockingStartupDelay=0   【取消延时】
 * @author: teago
 * @time: 2020/5/24 13:07
 */
@Slf4j
public class Example6 {
    public static void main(String[] args) throws Exception {
        Dog dog = Dog.builder().age(1).name("Tom").sex(true).build();
        String toPrintable = ClassLayout.parseInstance(dog).toPrintable();
        log.info(toPrintable);
        TimeUnit.SECONDS.sleep(5);
        log.info("延迟5秒打印结果如下所示:");
        String printable2 = ClassLayout.parseInstance(new Dog()).toPrintable();
        System.out.println(printable2);
    }
}

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
class Dog {
    private Integer age;   //4
    private String name;   //4
    private boolean sex;   //1
    // 9
}

  1. 运行结果
Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
13:14:26 [main] com.bloom.concurrent.chapter4.Example6 - com.bloom.concurrent.chapter4.Dog object internals:
 OFFSET  SIZE                TYPE DESCRIPTION                               VALUE
      0     4                     (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                     (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                     (object header)                           2a 67 01 f8 (00101010 01100111 00000001 11111000) (-134125782)
     12     1             boolean Dog.sex                                   true
     13     3                     (alignment/padding gap)                  
     16     4   java.lang.Integer Dog.age                                   1
     20     4    java.lang.String Dog.name                                  (object)
Instance size: 24 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

13:14:31 [main] com.bloom.concurrent.chapter4.Example6 - 延迟5秒打印结果如下所示:
com.bloom.concurrent.chapter4.Dog object internals:
 OFFSET  SIZE                TYPE DESCRIPTION                               VALUE
      0     4                     (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4                     (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                     (object header)                           2a 67 01 f8 (00101010 01100111 00000001 11111000) (-134125782)
     12     1             boolean Dog.sex                                   false
     13     3                     (alignment/padding gap)                  
     16     4   java.lang.Integer Dog.age                                   null
     20     4    java.lang.String Dog.name                                  null
Instance size: 24 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total


第一次打印为:1,不是默认开启的吗?不应该是5吗?其实是因为偏向锁默认是延迟的。
故而:中间让程序睡眠了5秒。
第二次打印为:5,说明了上述结论。
当然可以使用下述参数取消偏向锁延迟:

-XX:+UseBiasedLocking 【默认开启】
-XX:BiasedLockingStartupDelay=0   【取消延时】

4.8.5.1.2. 验证偏向锁原理
  1. 代码
package com.bloom.concurrent.chapter4;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/24 13:17
 */
@Slf4j
public class TestBiased2 {

    public static void main(String[] args) {
        Student student = new Student();
        log.debug("执行synchronized前");
        System.out.println(ClassLayout.parseInstance(student).toPrintable());

        synchronized (student) {
            log.debug("执行synchronized中");
            System.out.println(ClassLayout.parseInstance(student).toPrintable());
        }
        log.debug("执行synchronized后");
        System.out.println(ClassLayout.parseInstance(student).toPrintable());
    }
}


@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class Student {
    private Integer id;
    private String name;
}

  1. 运行结果

image.png

4.8.5.1.3. 禁用偏向锁

添加VM参数:-XX:-UseBiasedLocking禁用偏向锁。
禁用偏向锁之后,默认就变成了轻量级锁。
image.png

4.8.5.1.4. 调用hashCode()
  1. 测试代码:
package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

/**
 * @description: -XX:BiasedLockingStartupDelay=0
 * @author: teago
 * @time: 2020/5/24 13:32
 */
@Slf4j
public class TestBiased3 {
    public static void main(String[] args) {
        Emp emp = new Emp();
        String printable1 = ClassLayout.parseInstance(emp).toPrintable();
        log.info(printable1);
        //调用hashcode
        emp.hashCode();
        String printable2 = ClassLayout.parseInstance(emp).toPrintable();
        log.info(printable2);
    }
}


class Emp {

}
  1. 运行结果:

image.png
发现偏向锁被撤销掉了,因为:

  • 有偏向锁的时候,54位存储的是线程ID,
  • 调用了hashCode()之后,hashCode需要31位,此时Mark Word中没有位置了,故而需要删除thread占用的位置来存放hashCode。
  • 那为什么轻量级锁或重量级锁调用hashCode的时候不会有这样的问题呢?
    • 轻量级锁是将这些信息存放在锁记录中的。
    • 重量级锁将这些信息存储在monitor对象中。
    • 对于轻量级锁没有地方存储这些信息。

image.png

4.8.5.2. 撤销偏量级锁

  1. 调用hashCode(),见【4.8.5.1.4. 调用hashCode()
  2. 其他线程使用
  • 当其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。

程序设置前提:偏向锁和轻量级锁的前提就是线程没有交错执行。故而代码中要使用wait()和notify()。

package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

/**
 * @description:-XX:BiasedLockingStartupDelay=0
 * @author: teago
 * @time: 2020/5/24 13:38
 */
@Slf4j
public class TestBiased4 {
    public static void main(String[] args) {
        Dept dept = new Dept() ;

        new Thread(()->{
            printInfo(dept);

            //这是为了让线程td1和td2交互执行,原因是:偏向锁和轻量级锁的前提就是线程没有交错执行。
            synchronized (TestBiased4.class){
                TestBiased4.class.notify();
            } },"td1").start();

        new Thread(()->{

            synchronized (TestBiased4.class){
                try {
                    TestBiased4.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
            printInfo(dept);
            synchronized (TestBiased4.class){
                TestBiased4.class.notify();
            } },"td2").start();

    }

    private static void printInfo(Dept dept) {
        log.debug("执行synchronized前");
        System.out.println(ClassLayout.parseInstance(dept).toPrintable());
        synchronized (dept) {
            log.debug("执行synchronized中");
            System.out.println(ClassLayout.parseInstance(dept).toPrintable());
        }
        log.debug("执行synchronized后");
        System.out.println(ClassLayout.parseInstance(dept).toPrintable());
    }
}

class Dept{

}

代码运行结果:

td1 td2
执行synchronized前 101【偏向锁】 101【偏向锁】
执行synchronized中 101【偏向锁】 000【轻量级锁】
执行synchronized后 101【偏向锁】 001【无锁】
[td1] com.bloom.concurrent.chapter4.TestBiased4 - 执行synchronized前
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
com.bloom.concurrent.chapter4.Dept object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           2a 67 01 f8 (00101010 01100111 00000001 11111000) (-134125782)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

13:39:34 [td1] com.bloom.concurrent.chapter4.TestBiased4 - 执行synchronized中
com.bloom.concurrent.chapter4.Dept object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 50 16 92 (00000101 01010000 00010110 10010010) (-1844031483)
      4     4        (object header)                           fe 7f 00 00 (11111110 01111111 00000000 00000000) (32766)
      8     4        (object header)                           2a 67 01 f8 (00101010 01100111 00000001 11111000) (-134125782)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

13:39:34 [td1] com.bloom.concurrent.chapter4.TestBiased4 - 执行synchronized后
com.bloom.concurrent.chapter4.Dept object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 50 16 92 (00000101 01010000 00010110 10010010) (-1844031483)
      4     4        (object header)                           fe 7f 00 00 (11111110 01111111 00000000 00000000) (32766)
      8     4        (object header)                           2a 67 01 f8 (00101010 01100111 00000001 11111000) (-134125782)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

13:39:34 [td2] com.bloom.concurrent.chapter4.TestBiased4 - 执行synchronized前
com.bloom.concurrent.chapter4.Dept object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 50 16 92 (00000101 01010000 00010110 10010010) (-1844031483)
      4     4        (object header)                           fe 7f 00 00 (11111110 01111111 00000000 00000000) (32766)
      8     4        (object header)                           2a 67 01 f8 (00101010 01100111 00000001 11111000) (-134125782)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

13:39:34 [td2] com.bloom.concurrent.chapter4.TestBiased4 - 执行synchronized中
com.bloom.concurrent.chapter4.Dept object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           80 f8 9a 0f (10000000 11111000 10011010 00001111) (261814400)
      4     4        (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
      8     4        (object header)                           2a 67 01 f8 (00101010 01100111 00000001 11111000) (-134125782)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

13:39:34 [td2] com.bloom.concurrent.chapter4.TestBiased4 - 执行synchronized后
com.bloom.concurrent.chapter4.Dept object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           2a 67 01 f8 (00101010 01100111 00000001 11111000) (-134125782)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

  1. 调用wait/notify
  • 也会撤销偏向锁状态,因为wait/notify只有重量级锁有。

4.8.5.3. 批量重偏向

如果对象虽然被多个线程访问,当没有竞争,这时偏向了线程td1的对象人有机会重新偏向td2,重偏向会重置对象的Thread ID。
当撤销偏向锁阀值超过20次后,JVM会这样觉得:我是不是偏向错了呢,于是会给这些对象加锁时重新偏向至加锁线程。
观察一下代码运行结果发现:

  1. 刚开始偏向锁中的thread 为 td1 ;
  2. td2运行的时候,前20条打印结果发现,偏向锁变为轻量级锁,然后synchronized执行之后,锁状态变为normal;
  3. 20次之后,JVM发现撤销偏向状态影响性能,可以重新设置偏向锁的线程id。
package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

import java.util.Vector;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/24 13:43
 */
@Slf4j
public class TestBiased5 {
    public static void main(String[] args) {
        Vector<Teacher> teacherVector = new Vector<>();
        new Thread(()->{
            for (int i = 0; i < 30 ; i++) {
                Teacher teacher = new Teacher();
                teacherVector.add(teacher);
                log.debug("执行synchronized前");
                System.out.println(ClassLayout.parseInstance(teacher).toPrintable());
                synchronized (teacher) {
                    log.debug("执行synchronized中");
                    System.out.println(ClassLayout.parseInstance(teacher).toPrintable());
                }
                log.debug("执行synchronized后");
                System.out.println(ClassLayout.parseInstance(teacher).toPrintable());

                synchronized (teacherVector){
                    teacherVector.notify();
                }
            }
        },"td1").start();

        new Thread(()->{
            synchronized (teacherVector){
                try {
                    teacherVector.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            for (int i = 0; i < 30 ; i++) {
                Teacher teacher = teacherVector.get(i);
                log.debug("第"+i+"......执行synchronized前");
                System.out.println(ClassLayout.parseInstance(teacher).toPrintable());
                synchronized (teacher) {
                    log.debug("第"+i+"......执行synchronized中");
                    System.out.println(ClassLayout.parseInstance(teacher).toPrintable());
                }
                log.debug("第"+i+"......执行synchronized后");
                System.out.println(ClassLayout.parseInstance(teacher).toPrintable());
            }
        },"td2").start();
    }
}

class Teacher{

}

4.8.5.4. 批量撤销

当撤销偏向锁阀值超过40次,JVM会这样觉得:自己确实偏向错了,根本不应该偏向,于是整个类的所有对象都不会变为不可偏向的,新建的对象也是不可偏向的

  • 要求:调整 loopNumber =39 -----> 38, 观察main线程新创建的对象。
package com.bloom.concurrent.chapter4;

import org.openjdk.jol.info.ClassLayout;

import java.util.Vector;
import java.util.concurrent.locks.LockSupport;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/24 13:48
 */
public class TestBiased6 {


    static Thread thread1 ,thread2 , thread3 ;
    public static void main(String[] args) {
        int  loopNumber = 39 ;
        Vector<Tiger> tigerVector = new Vector<>() ;
        thread1 = new Thread(()->{
            for (int i = 0; i < loopNumber ; i++) {
                Tiger tiger = new Tiger() ;
                tigerVector.add(tiger);
                synchronized (tiger){
                    System.out.println(Thread.currentThread().getName()+"......"+ClassLayout.parseInstance(tiger).toPrintable());
                }
            }
            LockSupport.unpark(thread2);
        });
        thread1.start();

        //thread2
        thread2 = new Thread(()->{
            LockSupport.park();
            for (int i = 0; i < loopNumber ; i++) {
                Tiger tiger = tigerVector.get(i);
                synchronized (tiger){
                    System.out.println(Thread.currentThread().getName()+"......"+ ClassLayout.parseInstance(tiger).toPrintable());
                }
            }
            LockSupport.unpark(thread3);
        });
        thread2.start();

        //thread3
        thread3 = new Thread(()->{
            LockSupport.park();
            for (int i = 0; i < loopNumber ; i++) {
                Tiger tiger = tigerVector.get(i);
                synchronized (tiger){
                    System.out.println(Thread.currentThread().getName()+"......"+ClassLayout.parseInstance(tiger).toPrintable());
                }
            }
        });
        thread3.start();

        try {
            thread3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //观察新建对象偏向情况
        System.out.println(Thread.currentThread().getName()+"......"+ClassLayout.parseInstance(new Tiger()).toPrintable());
    }


}

class  Tiger {

}


4.9. wait/notify

4.9.1. 小故事 - 为什么需要wait

  • 由于条件不满足,小南不能继续进行计算
  • 但小南如果一直占用着锁,其它人就得一直阻塞,效率太低

14.png

  • 于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开,其它人可以由老王随机安排进屋
  • 直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)

15.png

  • 小南于是可以离开休息室,重新进入竞争锁的队列

16.png

4.9.2. wait/notify原理

image.png

  • Owner线程发现条件不足,调用wait方法,即可进入WaitSet变为WAITING状态;
  • BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片;
  • BLOCKED线程会在Owner线程释放锁时唤醒;
  • WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后不意味着立刻获得锁,仍需进入EntryList重现竞争。

4.9.3. wait&notify相关API

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
验证:

  1. 在没有获得对象锁的前提下调用wait方法
package com.bloom.concurrent.chapter4;

/**
 * @description:
 *  * wait/notify 演示
 *  * 在没有获得对象锁的前提下调用wait方法
 * @author: teago
 * @time: 2020/5/24 13:55
 */
public class Example7 {
    public static void main(String[] args) {
        Object object = new Object();
        try {
            object.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

Exception in thread "main" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at at com.bloom.concurrent.chapter4.Example7.main(Example7.java:14)

修改:

package com.bloom.concurrent.chapter4;


/**
 * @description:
 *  * wait/notify 演示
 *  
 * @author: teago
 * @time: 2020/5/24 13:55
 */
public class Example8 {
    public static void main(String[] args) {
        Object o = new Object();
        synchronized (o){
            try {
                o.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

notify也是这样的,在没有获取对象锁的时候,也会出现上述一次。

  1. notify()和notifyAll()的区别
  • notify() 挑选当前锁中的其中的一个唤醒
  • notifyAll() 唤醒当前锁所有线程
package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * wait/notify 演示
 *      验证notify和notifyAll的区别
 * @author : teago
 * @time: 2020/5/24 14:15
 */
@Slf4j
public class Example9 {

    public static void main(String[] args) {
        Object object = new Object() ;

        new Thread(()->{
            log.debug("执行");
            synchronized (object){
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("执行其他代码");
        }).start();


        new Thread(()->{
            log.debug("执行");
            synchronized (object){
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("执行其他代码");
        }).start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("执行");
        synchronized (object){

           //object.notify();
            object.notifyAll();
        }
    }

}

  1. wait()、wait(long)、wait(long ,int)的区别
  • wait()
    public final void wait() throws InterruptedException {
        wait(0);
    }
  • wait(long)
public final native void wait(long timeout) throws InterruptedException;

  • wait(long,int)
    public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
    }

4.9.4. wait/notify的正确使用姿势

4.9.4.1. wait和sleep

  1. 区别
  • sleep是Thread的静态方法,wait是Object方法;
  • sleep不需要强制和synchronized使用,但wait需要和synchronized一起用;
  • sleep在睡眠的同时,不会释放对象锁,但是wait在等待的时候会释放对象锁。
  1. 共同点
  • 线程状态都是TIMEWAITING。(有实现的等待)

4.9.4.2. step1

思考下面的实现行吗,为什么?

new Thread(() -> {
    synchronized (room) {
        log.debug("有烟没?[{}]", hasCigarette);
        if (!hasCigarette) {
            log.debug("没烟,先歇会!");
            try {
                room.wait(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        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) {
        hasCigarette = true;
        log.debug("烟到了噢!");
        room.notify();
    }
}, "送烟的").start();
  • 其它干活的线程,都要一直阻塞,效率太低
  • 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
  • 加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加 synchronized 就好像 main 线程是翻窗户进来的
  • 解决方法,使用 wait - notify 机制

4.9.2.3. step2

思考下面的实现行吗,为什么?

new Thread(() -> {
    synchronized (room) {
        log.debug("有烟没?[{}]", hasCigarette);
        if (!hasCigarette) {
            log.debug("没烟,先歇会!");
            try {
                room.wait(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        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) {
        hasCigarette = true;
        log.debug("烟到了噢!");
        room.notify();
    }
}, "送烟的").start();
  • 解决了其它干活的线程阻塞的问题
  • 但如果有其它线程也在等待条件呢?notify会不会错误的叫醒了其他线程。

4.9.2.4. step3

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.notify();
    }
}, "送外卖的").start();
  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒
  • 解决方法,改为 notifyAll

4.9.2.5. step4

new Thread(() -> {
    synchronized (room) {
        hasTakeout = true;
        log.debug("外卖到了噢!");
        room.notifyAll();
    }
}, "送外卖的").start();
  • 用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了
  • 解决方法,用 while + wait,当条件不成立,再次 wait

4.9.2.6. step5

while (!hasCigarette) {
    log.debug("没烟,先歇会!");
    try {
        room.wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

4.9.4.7. 总结

synchronized(lock) {
    while(条件不成立) {
        lock.wait();
    }
    // 干活
}

//另一个线程
synchronized(lock) {
    lock.notifyAll();
}

4.10. 多线程设计模式——同步模式之保护性暂停

4.11. join原理

  1. 源码解析
  2. 参见多线程设计模式【1.3. 增加超时

4.12. 多线程设计模式——生产者和消费者模式

4.13. park和unpark

4.13.1. 基本使用

它们是 LockSupport 类中的方法

// 暂停当前线程
LockSupport.park(); 

// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

先 park 再 unpark

Thread t1 = new Thread(() -> {
    log.debug("start...");
    sleep(1);
    log.debug("park...");
    LockSupport.park();
    log.debug("resume...");
},"t1");
t1.start();

sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);

输出

18:42:52.585 c.TestParkUnpark [t1] - start...
18:42:53.589 c.TestParkUnpark [t1] - park...
18:42:54.583 c.TestParkUnpark [main] - unpark...
18:42:54.583 c.TestParkUnpark [t1] - resume...

先 unpark 再 park

Thread t1 = new Thread(() -> {
    log.debug("start...");
    sleep(2);
    log.debug("park...");
    LockSupport.park();
    log.debug("resume...");
}, "t1");
t1.start();

sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);

输出

18:43:50.765 c.TestParkUnpark [t1] - start...
18:43:51.764 c.TestParkUnpark [main] - unpark...
18:43:52.769 c.TestParkUnpark [t1] - park...
18:43:52.769 c.TestParkUnpark [t1] - resume...

4.13.2. 特点

与 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify

4.13.3. park & unpark原理

每个线程都有自己的一个Parker对象,由三部分组成_counter,_cond和_mutex打个比喻。

  • 线程就像一个旅人,Parker就像他随身携带的背包,条件变量【_cond】就好比背包中的帐篷,_counter就好比书包中的备用干粮(0为耗尽,1为充足);
  • 调用park就时要看需不需要停下来歇息
    • 如果备用干粮耗尽,那么钻进帐篷歇息
    • 如果备用干粮充足,那么不需要停留,继续前进
  • 调用unpark,就好比令干粮充足
    • 如果这时线程还在帐篷,就唤醒让他继续前进
    • 如果这时线程还在运行,那么下次他调用park时,仅是消耗掉备用干粮,不需停留继续前进
      • 因为背包空间有限,多次调用unpark仅会补充一份备用干粮

image.png

  1. 当前线程调用Unsafe.park()方法
  2. 检测_conter,本情况为0,这时,获得_mutex互斥锁
  3. 线程进入_cond条件变量阻塞
  4. 设置_counter=0

image.png

  1. 调用Unsafe.unpark(Thread_0)方法,设置_conter为1
  2. 唤醒_cond条件变量中的Thread_0
  3. Thread_0恢复运行
  4. 设置_counter为0

4.14. 重新理解线程状态转换

8.png
假设有线程 Thread t

4.14.1. 情况 1 NEW --> RUNNABLE

  • 当调用 t.start() 方法时,由 NEW --> RUNNABLE

4.14.2. 情况 2 RUNNABLE <--> WAITING

t 线程用  synchronized(obj) 获取了对象锁后

  • 调用 obj.wait() 方法时,t 线程RUNNABLE --> WAITING
  • 调用 obj.notify()obj.notifyAll()t.interrupt()
    • 竞争锁成功,t 线程从  WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从  WAITING --> BLOCKED
public class TestWaitNotify {
    final static Object obj = new Object();

    public static void main(String[] args) {

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码...."); // 断点
            }
        },"t1").start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码...."); // 断点
            }
        },"t2").start();
        
        sleep(0.5);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
            obj.notifyAll(); // 唤醒obj上所有等待线程  断点
        }
    }
}

4.14.3. 情况 3 RUNNABLE <--> WAITING

  • 当前线程调用 t.join() 方法时,当前线程RUNNABLE --> WAITING
    • 注意是当前线程t 线程对象的监视器上等待
  • t 线程运行结束,或调用了当前线程interrupt() 时,当前线程WAITING --> RUNNABLE

4.14.4. 情况 4 RUNNABLE <--> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt(),会让目标线程从 WAITING --> RUNNABLE

4.14.5. 情况 5 RUNNABLE <--> TIMED_WAITING

t 线程用  synchronized(obj) 获取了对象锁后

  • 调用 obj.wait(long n) 方法时,t 线程RUNNABLE --> TIMED_WAITING
  • t 线程等待时间超过了 n 毫秒,或调用 obj.notify()obj.notifyAll()t.interrupt()
    • 竞争锁成功,t 线程从  TIMED_WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从  TIMED_WAITING --> BLOCKED

4.14.6. 情况 6 RUNNABLE <--> TIMED_WAITING

  • 当前线程调用 t.join(long n) 方法时,当前线程RUNNABLE --> TIMED_WAITING
    • 注意是当前线程t 线程对象的监视器上等待
  • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程interrupt() 时,当前线程TIMED_WAITING --> RUNNABLE

4.14.7. 情况 7 RUNNABLE <--> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n),当前线程从 RUNNABLE --> TIMED_WAITING
  • 当前线程等待时间超过了 n 毫秒,当前线程从  TIMED_WAITING --> RUNNABLE

4.14.8. 情况 8 RUNNABLE <--> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos)LockSupport.parkUntil(long millis) 时,当前线程RUNNABLE --> TIMED_WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt(),或是等待超时,会让目标线程从 TIMED_WAITING--> RUNNABLE

4.14.9. 情况 9 RUNNABLE <--> BLOCKED

  • t 线程用  synchronized(obj) 获取了对象锁时如果竞争失败,从  RUNNABLE --> BLOCKED
  • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED  的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE,其它失败的线程仍然  BLOCKED

4.14.10. 情况 10 RUNNABLE <--> TERMINATED

当前线程所有代码运行完毕,进入TERMINATED

4.15. 多把锁

4.15.1. 多把不相干的锁

一间大屋子有两个功能:睡觉、学习,互不相干。
现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低
解决方法是准备多个房间(多个对象锁)
例如

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);
        }
    }

}

执行

BigRoom bigRoom = new BigRoom();
new Thread(() -> {
    bigRoom.compute();
},"小南").start();
new Thread(() -> {
    bigRoom.sleep();
},"小女").start();

某次结果

12:13:54.471 [小南] c.BigRoom - study 1 小时
12:13:55.476 [小女] c.BigRoom - sleeping 2 小时

改进

class BigRoom {

    private final Object studyRoom = new Object();
    private final Object bedRoom = new Object();

    public void sleep() {
        synchronized (bedRoom) {
            log.debug("sleeping 2 小时");
            Sleeper.sleep(2);
        }
    }

    public void study() {
        synchronized (studyRoom) {
            log.debug("study 1 小时");
            Sleeper.sleep(1);
        }
    }

}

某次执行结果

12:15:35.069 [小南] c.BigRoom - study 1 小时
12:15:35.069 [小女] c.BigRoom - sleeping 2 小时

将锁的粒度细分

  • 好处,是可以增强并发度
  • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

4.16. 线程活跃性

4.16.1. 死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
t1 线程  获得 A对象 锁,接下来想获取 B对象的锁
t2 线程 获得 B对象 锁,接下来想获取 A对象的锁

4.161.1. 死锁现象

例:

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();

执行结果:(陷入死锁)

12:22:06.962 [t2] c.TestDeadLock - lock B
12:22:06.962 [t1] c.TestDeadLock - lock A

4.16.1.2. 定位死锁

  • 检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:

4947 RemoteMavenServer36
5769 Jps
3868
5631 Launcher
 haoyu@haodeMacBook-Pro  ~  jstack  3868
2020-05-24 14:20:55
Full thread dump OpenJDK 64-Bit Server VM (11.0.6+8-b765.25 mixed mode):

Threads class SMR info:
_java_thread_list=0x0000600006c953c0, length=66, elements={
0x00007fee84861000, 0x00007fee84853000, 0x00007fee8500c800, 0x00007fee85013800,
0x00007fee85816000, 0x00007fee85817000, 0x00007fee8501b800, 0x00007fee85835800,
0x00007fee85014800, 0x00007fee8400d800, 0x00007fee84902800, 0x00007fee850ab800,
0x00007fee84812000, 0x00007fee44012800, 0x00007fee8591b800, 0x00007fee849de800,
0x00007fee85443800, 0x00007fee86174000, 0x00007fee8629e000, 0x00007fee34022800,
0x00007fee04020000, 0x00007fee866cc800, 0x00007fee84bec000, 0x00007fee84c11800,
0x00007fee843bf800, 0x00007fee845aa800, 0x00007fee8552a000, 0x00007fee44844800,
0x00007fee06c30000, 0x00007fee86485000, 0x00007fee06a20000, 0x00007fee06a56800,
0x00007fee22254000, 0x00007fee20f1a800, 0x00007fee221dc000, 0x00007fee22a8f000,
0x00007fee6400c800, 0x00007fee20e68800, 0x00007fee20ad7800, 0x00007fee3482f800,
0x00007fee2302f000, 0x00007fee8542a800, 0x00007fee22088000, 0x00007fee851d3000,
0x00007fee854c1000, 0x00007fee4487f000, 0x00007fee21afb000, 0x00007fee4486a000,
0x00007fee219b2800, 0x00007fee44809800, 0x00007fee04016800, 0x00007fee863a3000,
0x00007fee22087000, 0x00007fee34887000, 0x00007fee440bd000, 0x00007fee24054000,
0x00007fee0400b000, 0x00007fee229d2000, 0x00007fee20bd7800, 0x00007fee22cfe800,
0x00007fee23325000, 0x00007fee2077f800, 0x00007fee2077d000, 0x00007fee64436000,
0x00007fee2030d000, 0x00007fee64571800
}

"Reference Handler" #2 daemon prio=10 os_prio=31 cpu=95.10ms elapsed=12102.11s tid=0x00007fee84861000 nid=0x3603 waiting on condition  [0x0000700007bde000]
   java.lang.Thread.State: RUNNABLE
	at java.lang.ref.Reference.waitForReferencePendingList(java.base@11.0.6/Native Method)
	at java.lang.ref.Reference.processPendingReferences(java.base@11.0.6/Reference.java:241)
	at java.lang.ref.Reference$ReferenceHandler.run(java.base@11.0.6/Reference.java:213)

"Finalizer" #3 daemon prio=8 os_prio=31 cpu=46.12ms elapsed=12102.11s tid=0x00007fee84853000 nid=0x4403 in Object.wait()  [0x0000700007ce1000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(java.base@11.0.6/Native Method)
	- waiting on <no object reference available>
	at java.lang.ref.ReferenceQueue.remove(java.base@11.0.6/ReferenceQueue.java:155)
	- waiting to re-lock in wait() <0x00000007ab8b9f88> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(java.base@11.0.6/ReferenceQueue.java:176)
	at java.lang.ref.Finalizer$FinalizerThread.run(java.base@11.0.6/Finalizer.java:170)

4.16.1.3. 哲学家就餐问题(演示)

79.png
有五位哲学家,围坐在圆桌旁。

  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
  • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
  • 如果筷子被身边的人拿着,自己就得等待

筷子类

  class Chopstick {
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

哲学家类

class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(1);
    }
    
    @Override
    public void run() {
        while (true) {
            // 获得左手筷子
            synchronized (left) {
                // 获得右手筷子
                synchronized (right) {
                    // 吃饭
                    eat();
                }
                // 放下右手筷子
            }
            // 放下左手筷子
        }
    }
}

就餐

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();


![image.png](https://cdn.nlark.com/yuque/0/2020/png/255340/1590309513666-5a041996-ecce-4451-a5a7-5d4c9d7c3d17.png#align=left&display=inline&height=1500&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1500&originWidth=1800&size=206544&status=done&style=none&width=1800)
**这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题**,除了死锁以外,还有活锁和饥饿者两种情况。
**解决:使用RenntrantLock。** ### 4.16.2. 活锁 活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束, ```java 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();
}

}

**解决:错开交错执行的时间,让线程随机睡眠。**
<a name="nSgPq"></a>
### 4.16.3. 饥锁
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题<br />下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题。
![](https://cdn.nlark.com/yuque/__mermaid_v3/ef5763b00ab50a5f593f7dfc6a331ec2.svg#lake_card_v2=eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG5wYXJ0aWNpcGFudCB0MSBhcyDnur_nqIsxXG5wYXJ0aWNpcGFudCB0MiBhcyDnur_nqIsyXG5wYXJ0aWNpcGFudCBhIGFzIOWvueixoUFcbnBhcnRpY2lwYW50IGIgYXMg5a-56LGhQlxuXHR0MSAtLT4-IGEgOiDlsJ3or5Xojrflj5bplIFcblx0Tm90ZSBvdmVyIHQxLGE65oul5pyJ6ZSBXG5cdHQyIC0tPj4gYiA6IOWwneivleiOt-WPlumUgVxuXHROb3RlIG92ZXIgdDIsYjrmi6XmnInplIFcblx0dDEgLS14IGIgOiDlsJ3or5Xojrflj5bplIFcblx0dDIgLS14IGEgOiDlsJ3or5Xojrflj5bplIEiLCJ0eXBlIjoibWVybWFpZCIsIm1hcmdpbiI6dHJ1ZSwiaWQiOiJEZTU1dSIsInVybCI6Imh0dHBzOi8vY2RuLm5sYXJrLmNvbS95dXF1ZS9fX21lcm1haWRfdjMvZWY1NzYzYjAwYWI1MGE1ZjU5M2Y3ZGZjNmEzMzFlYzIuc3ZnIiwiY2FyZCI6ImRpYWdyYW0ifQ==)顺序加锁的解决方案
![](https://cdn.nlark.com/yuque/__mermaid_v3/3f36a5bf0a653e34a5f7546cdb638488.svg#lake_card_v2=eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG5wYXJ0aWNpcGFudCB0MSBhcyDnur_nqIsxXG5wYXJ0aWNpcGFudCB0MiBhcyDnur_nqIsyXG5wYXJ0aWNpcGFudCBhIGFzIOWvueixoUFcbnBhcnRpY2lwYW50IGIgYXMg5a-56LGhQlxuXHR0MSAtLT4-IGEgOiDlsJ3or5Xojrflj5bplIFcblx0Tm90ZSBvdmVyIHQxLGE65oul5pyJ6ZSBXHRcblx0dDIgLS14IGEgOiDlsJ3or5Xojrflj5bplIFcblx0dDIgLS0-PiBhIDog6Zi75aGeXG5cdHQxIC0tPj4gYiA6IOWwneivleiOt-WPlumUgVxuXHROb3RlIG92ZXIgdDEsYjrmi6XmnInplIEiLCJ0eXBlIjoibWVybWFpZCIsIm1hcmdpbiI6dHJ1ZSwiaWQiOiJnTUpPWiIsInVybCI6Imh0dHBzOi8vY2RuLm5sYXJrLmNvbS95dXF1ZS9fX21lcm1haWRfdjMvM2YzNmE1YmYwYTY1M2UzNGE1Zjc1NDZjZGI2Mzg0ODguc3ZnIiwiY2FyZCI6ImRpYWdyYW0ifQ==)演示饥饿现象:

- 修改标注位置

![image.png](https://cdn.nlark.com/yuque/0/2020/png/285619/1588582933018-ce0769cd-7af9-4bbe-b26e-69ca678e9a7d.png#align=left&display=inline&height=299&margin=%5Bobject%20Object%5D&name=image.png&originHeight=598&originWidth=1228&size=150164&status=done&style=shadow&width=614)<br />**如何解决饥饿现象,使用ReentrantLock.**
<a name="GXs3b"></a>
## 4.17. ReentrantLock
<a name="EF30i"></a>
### 4.17.1. 简介
相对于 synchronized 它具备如下特点

- 可中断
- 可以设置超时时间
- 可以设置为公平锁【防止线程饥饿】
- 支持多个条件变量【synchronized中只有一个waitset,ReentrantLock有多个waitset】

与 synchronized 一样,都支持可重入<br />基本语法:<br />synchronized是关键字级别保护临界区,ReentrantLock是对象级别保护临界区。
```java
        ReentrantLock reentrantLock = new ReentrantLock();
		//	 获取锁
        reentrantLock.lock();
        try{
			// 临界区
        }catch (Exception e){

        }finally {
            //释放锁
            reentrantLock.unlock();
        }

4.17.1. 可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。

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();
    }
}

输出结果

17:59:11.862 [main] c.TestReentrant - execute method1
17:59:11.865 [main] c.TestReentrant - execute method2
17:59:11.865 [main] c.TestReentrant - execute method3

4.17.2. 可打断

可打断性可以防止死锁的发生。
被动打断:reentrantLock.lockInterruptibly();避免死锁的发生,这是一个被动的,需要调用thread.interrupted();
主动打断:使用锁超时【ReentrantLock#tryLock(long, TimeUnit)】到了时间还么有获取时间,也会被打断。【参见:锁超时
⚠️:ReentrantLock#tryLock()不可被打断。【参加:锁超时

ReentrantLock lock = new ReentrantLock();

Thread t1 = new Thread(() -> {
    log.debug("启动...");
    try {
        lock.lockInterruptibly();
    } catch (InterruptedException e) {
        e.printStackTrace();
        log.debug("等锁的过程中被打断");
        return;
    }
    try {
        log.debug("获得了锁");
    } finally {
        lock.unlock();
    }
}, "t1");


lock.lock();
log.debug("获得了锁");
t1.start();
try {
    sleep(1);
    t1.interrupt();
    log.debug("执行打断");
} finally {
    lock.unlock();
}

输出结果

16:43:07 [main] com.bloom.concurrent.chapter4.ReentrantLockDemo - 获得了锁
16:43:07 [t1] com.bloom.concurrent.chapter4.ReentrantLockDemo - 启动...
16:43:07 [main] com.bloom.concurrent.chapter4.ReentrantLockDemo - 执行打断
16:43:07 [t1] com.bloom.concurrent.chapter4.ReentrantLockDemo - 等锁的过程中被打断
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at com.bloom.concurrent.chapter4.ReentrantLockDemo.lambda$main$0(ReentrantLockDemo.java:22)
	at java.lang.Thread.run(Thread.java:748)

4.17.3. 锁超时

主动打断,其中包含两个方法重载。

4.17.3.1. ReentrantLock#tryLock()

  1. 是否可打断测试
ReentrantLock lock = new ReentrantLock();

Thread t1 = new Thread(() -> {
    log.debug("启动...");
    lock.lock();
    try {
        log.debug("获得了锁");
    } finally {
        lock.unlock();
    }
}, "t1");


lock.lock();
log.debug("获得了锁");
t1.start();
try {
    sleep(1);
    t1.interrupt();
    log.debug("执行打断");
    sleep(1);
} finally {
    log.debug("释放了锁");
    lock.unlock();
}

运行结果:

18:06:56.261 [main] c.TestInterrupt - 获得了锁
18:06:56.265 [t1] c.TestInterrupt - 启动...
18:06:57.266 [main] c.TestInterrupt - 执行打断 // 这时 t1 并没有被真正打断, 而是仍继续等待锁
18:06:58.267 [main] c.TestInterrupt - 释放了锁
18:06:58.267 [t1] c.TestInterrupt - 获得了锁

结论:
不可以被打断。

  1. 无参,等待锁的时间为零

4.17.3.2. ReentrantLock#tryLock(long, TimeUnit)

  1. 演示释放可以被打断
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
    log.debug("启动...");
    try {
        if (!lock.tryLock(1, TimeUnit.SECONDS)) {
            log.debug("获取等待 1s 后失败,返回");
            return;
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    try {
        log.debug("获得了锁");
    } finally {
        lock.unlock();
    }
}, "t1");

lock.lock();
log.debug("获得了锁");
t1.start();
try {
    sleep(2);
} finally {
    lock.unlock();
}

运行结果:

19:55:08 [t1] com.beatshadow.concurrent.chapter4.Example24 - 尝试获取锁
19:55:08 [t1] com.beatshadow.concurrent.chapter4.Example24 - 被打断
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.tryAcquireNanos(AbstractQueuedSynchronizer.java:1245)
	at java.util.concurrent.locks.ReentrantLock.tryLock(ReentrantLock.java:442)
	at com.beatshadow.concurrent.chapter4.Example24.lambda$main$0(Example24.java:22)
	at java.lang.Thread.run(Thread.java:748)

  1. 演示超时
package com.beatshadow.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 验证{@link ReentrantLock#tryLock(long, TimeUnit)} 的可打断性
 * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
 * @since : 2020/5/4 19:08
 */
@Slf4j
public class Example25 {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        Thread thread = new Thread(() -> {

            log.debug("尝试获取锁");
            try {
                //如果超时,则获取不到锁
                if (!reentrantLock.tryLock(6,TimeUnit.SECONDS)) {
                    log.debug("获取不到锁");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("被打断");
                return;
            }

            try{
                log.debug("获取到锁");
            }finally {
                reentrantLock.unlock();
            }
        }, "t1");
        //让主线程获取锁
        log.debug("获取锁");
        reentrantLock.lock();
        thread.start();
        //两秒钟后,解锁
        try {
            TimeUnit.SECONDS.sleep(2);
            log.debug("解锁");
            reentrantLock.unlock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

运行结果:

19:56:10 [main] com.beatshadow.concurrent.chapter4.Example25 - 获取锁
19:56:10 [t1] com.beatshadow.concurrent.chapter4.Example25 - 尝试获取锁
19:56:12 [main] com.beatshadow.concurrent.chapter4.Example25 - 解锁
19:56:12 [t1] com.beatshadow.concurrent.chapter4.Example25 - 获取到锁

改变条件:让主线程睡上7秒,运行结果如下所示:

19:57:11 [main] com.beatshadow.concurrent.chapter4.Example25 - 获取锁
19:57:11 [t1] com.beatshadow.concurrent.chapter4.Example25 - 尝试获取锁
19:57:17 [t1] com.beatshadow.concurrent.chapter4.Example25 - 获取不到锁
19:57:18 [main] com.beatshadow.concurrent.chapter4.Example25 - 解锁

4.17.3.3. 锁超时解决哲学家就餐问题

4.16.1.3. 哲学家就餐问题】是一个死锁的问题,但是给出的一个解决方案造成了极锁,现在尝试使用锁超时解决死锁问题。

  1. 让筷子作为锁对象,又想使用ReentrantLock,那么需要extends ReentrantLock
  2. 修改Philosopher2对象中的run方法。
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) {
            // 尝试获得左手筷子
            if (left.tryLock()) {
                try {
                    // 尝试获得右手筷子
                    if (right.tryLock()) {
                        try {
                            eat();
                        } finally {
                            right.unlock();
                        }
                    }
                } finally {
                    left.unlock();
                }
            }
        }
    }

    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(1);
    }
}

4.17.4. 锁公平

synchronized是不公平锁,ReentrantLock默认也是不公平锁,但是我们可以设置ReentrantLock为公平锁。
所谓公平性就是:在释放锁之后,阻塞队列中的等待线程不是一拥而上的,而是按照先进入队列的顺序先获得到锁。
改为公平锁后

ReentrantLock lock = new ReentrantLock(true);

公平锁的本意是解决饥锁问题的,但是实际上公平锁一般没有必要,会降低并发度****,后面分析源码时会讲解。
使用tryLock更好。

4.17.5. 条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行`

主要方法:

  • static ReentrantLock reentrantLock = new ReentrantLock();
  • static Condition cigaretteCondition = reentrantLock.newCondition();
  • reentrantLock.lock();
  • reentrantLock.unlock();
  • takeoutCondition.await();
  • takeoutCondition.signal();
static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitbreakfastQueue = lock.newCondition();
static volatile boolean hasCigrette = false;
static volatile boolean hasBreakfast = false;

public static void main(String[] args) {
    new Thread(() -> {
        try {
            lock.lock();
            while (!hasCigrette) {
                try {
                    waitCigaretteQueue.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("等到了它的烟");
        } finally {
            lock.unlock();
        }
    }).start();

    new Thread(() -> {
        try {
            lock.lock();
            while (!hasBreakfast) {
                try {
                    waitbreakfastQueue.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("等到了它的早餐");
        } finally {
            lock.unlock();
        }
    }).start();

    sleep(1);
    sendBreakfast();
    sleep(1);
    sendCigarette();
}

private static void sendCigarette() {
    lock.lock();
    try {
        log.debug("送烟来了");
        hasCigrette = true;
        waitCigaretteQueue.signal();
    } finally {
        lock.unlock();
    }
}

private static void sendBreakfast() {
    lock.lock();
    try {
        log.debug("送早餐来了");
        hasBreakfast = true;
        waitbreakfastQueue.signal();
    } finally {
        lock.unlock();
    }
}

4.18. 同步模式之顺序控制

4.18.1. wait&notify

package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/24 16:50
 */
@Slf4j
public class Demo01 {
    static Object o = new Object();
    static boolean checked = false;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (o) {
                while (!checked) {
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.info("1");
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (o) {
                log.debug("2");
                checked = true;
                o.notify();

            }
        }, "t2");
        t1.start();
        t2.start();
    }

}

4.18.2. ReentrantLock

package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/24 16:54
 */
@Slf4j
public class Demo2 {
    static ReentrantLock reentrantLock = new ReentrantLock();
    static Condition condition = reentrantLock.newCondition() ;
    static boolean checked = false ;
    public static void main(String[] args) {
        new Thread(()->{
            reentrantLock.lock();
            try{
                while (!checked){
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("1");
            }finally {
                reentrantLock.unlock();
            }
        },"t1").start();

        new Thread(()->{
            reentrantLock.lock();
            try{
                log.debug("2");
                checked = true ;
                condition.signal();
            }finally {
                reentrantLock.unlock();
            }
        },"t2").start();
    }



}

4.18.3. park&unpark

package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.LockSupport;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/24 16:56
 */
@Slf4j
public class Demo03 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            LockSupport.park();
            log.debug("1");
        }, "t1");
        Thread t2 = new Thread(() -> {
            log.debug("2");
            LockSupport.unpark(t1);
        }, "t2");
        t1.start();
        t2.start();

    }
}

4.19. 同步模式之交替输出

线程1输出A5次,线程2输出B5次,线程3输出C5次,现在要求输出“abcabcabcabcabc”怎么实现。

4.19.1. wait/notify

package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/24 17:01
 */
@Slf4j
public class Demo04 {
    public static void main(String[] args) {
        SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
        new Thread(() -> {
            syncWaitNotify.print(1, 2, "a");
        }).start();
        new Thread(() -> {
            syncWaitNotify.print(2, 3, "b");
        }).start();
        new Thread(() -> {
            syncWaitNotify.print(3, 1, "c");
        }).start();

    }
}


class SyncWaitNotify {
    private int flag;
    private int loopNumber;
    public SyncWaitNotify(int flag, int loopNumber) {
        this.flag = flag;
        this.loopNumber = loopNumber;
    }
    public void print(int waitFlag, int nextFlag, String str) {
        for (int i = 0; i < loopNumber; i++) {
            synchronized (this) {
                while (this.flag != waitFlag) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print(str);
                flag = nextFlag;
                this.notifyAll();
            }
        }
    }
}

4.19.2. await/signal

package com.bloom.concurrent.chapter4;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/24 17:11
 */
public class Demo05 {
    public static void main(String[] args) {
        SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
        new Thread(() -> {
            syncWaitNotify.print(1, 2, "a");
        }).start();
        new Thread(() -> {
            syncWaitNotify.print(2, 3, "b");
        }).start();
        new Thread(() -> {
            syncWaitNotify.print(3, 1, "c");
        }).start();
    }
}


@Slf4j
class AwaitSignal extends ReentrantLock {

    public void start(Condition first) {
        this.lock();
        try {
            log.debug("start");
            first.signal();
        } finally {
            this.unlock();
        }
    }
    public void print(String str, Condition current, Condition next) {
        for (int i = 0; i < loopNumber; i++) {
            this.lock();
            try {
                current.await();
                log.debug(str);
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                this.unlock();
            }
        }
    }
    // 循环次数
    private int loopNumber;
    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }
}
Demo

4.19.3. park/unpark

package com.bloom.concurrent.chapter4;

import java.util.concurrent.locks.LockSupport;

/**
 * @description:
 * @author: teago
 * @time: 2020/5/24 17:45
 */
public class Demo06 {
    public static void main(String[] args) {
        SyncPark syncPark = new SyncPark(5);
        Thread t1 = new Thread(() -> {
            syncPark.print("a");
        });
        Thread t2 = new Thread(() -> {
            syncPark.print("b");
        });
        Thread t3 = new Thread(() -> {
            syncPark.print("c\n");
        });
        syncPark.setThreads(t1, t2, t3);
        syncPark.start();
    }
}
class SyncPark {
    private int loopNumber;
    private Thread[] threads;
    public SyncPark(int loopNumber) {
        this.loopNumber = loopNumber;
    }
    public void setThreads(Thread... threads) {
        this.threads = threads;
    }
    public void print(String str) {
        for (int i = 0; i < loopNumber; i++) {
            LockSupport.park();
            System.out.print(str);
            LockSupport.unpark(nextThread());
        }
    }
    private Thread nextThread() {
        Thread current = Thread.currentThread();
        int index = 0;
        for (int i = 0; i < threads.length; i++) {
            if(threads[i] == current) {
                index = i;
                break;
            }
        }
        if(index < threads.length - 1) {
            return threads[index+1];
        } else {
            return threads[0];
        }
    }
    public void start() {
        for (Thread thread : threads) {
            thread.start();
        }
        LockSupport.unpark(threads[0]);
    }
}

4.20. 本章小结

本章我们需要重点掌握的是

  • 分析多线程访问共享资源时,哪些代码片段属于临界区
  • 使用 synchronized 互斥解决临界区的线程安全问题
    • 掌握 synchronized 锁对象语法
    • 掌握 synchronzied 加载成员方法和静态方法语法
    • 掌握 wait/notify 同步方法
  • 使用 lock 互斥解决临界区的线程安全问题
    • 掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量
  • 学会分析变量的线程安全性、掌握常见线程安全类的使用
  • 了解线程活跃性问题:死锁、活锁、饥饿
  • 应用方面
    • 互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
    • 同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果
  • 原理方面
    • monitor、synchronized 、wait/notify 原理
    • synchronized 进阶原理
    • park & unpark 原理
  • 模式方面
    • 同步模式之保护性暂停
    • 异步模式之生产者消费者
    • 同步模式之顺序控制
posted @ 2020-05-24 19:28  teago  阅读(541)  评论(0)    收藏  举报