03-共享模型之管程

3.1 共享带来的问题

3.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

3.1.2 Java代码体现

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

static int counter = 0;
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            counter++;
        }
    }, "t1");
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            counter--;
        }
    }, "t2");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    log.debug("{}",counter);
}

3.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
如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:image.png
但多线程下这 8 行代码可能交错运行:
出现负数的情况:
image.png
出现正数的情况:
image.png

3.1.4 临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    1. 多个线程读共享资源其实也没有问题
    2. 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

例如,下面代码中的临界区

static int counter = 0;
static void increment() 
// 临界区
{ 
 counter++; 
}
static void decrement() 
// 临界区
{ 
 counter--;
}

3.1.5 竞态条件 Race Condition

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

3.2 synchronized 解决方案

3.2.1 应用之互斥

3.2.1.1 悲观互斥

互斥实际是悲观锁的思想
例如,有下面取款的需求

interface Account {
	// 获取余额
	Integer getBalance();
	// 取款
	void withdraw(Integer amount);
} 

/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(Account account) {
	List<Thread> ts = new ArrayList<>();
	for (int i = 0; i < 1000; i++) {
		ts.add(new Thread(() -> {
			account.withdraw(10);
		}));
	}
	long start = System.nanoTime();
	ts.forEach(Thread::start);
	ts.forEach(t -> {
		try {
			t.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	});
	long end = System.nanoTime();
	System.out.println(account.getBalance()
					   + " cost: " + (end-start)/1000_000 + " ms");
}

//用互斥来保护
class AccountSync implements Account {
	private Integer balance;
	public AccountSync(Integer balance) {
		this.balance = balance;
	}
	@Override
	public Integer getBalance() {
		synchronized (this) {
			return this.balance;
		}
	}
	@Override
	public void withdraw(Integer amount) {
		synchronized (this) {
			this.balance -= amount;
		}
	}
}

3.2.1.2 乐观重试

另外一种是乐观锁思想,它其实不是互斥

class AccountCas implements Account {
	private AtomicInteger balance;
	public AccountCas(int balance) {
		this.balance = new AtomicInteger(balance);
	}
	@Override
	public Integer getBalance() {
		return balance.get();
	}
	@Override
	public void withdraw(Integer amount) {
		while(true) {
			// 获取余额的最新值
			int prev = balance.get();
			// 要修改的余额
			int next = prev - amount;
			// 真正修改
			if(balance.compareAndSet(prev, next)) {
				break;
			}
		}
	}
}

3.2.2 synchronized

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

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

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

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

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
  • 一般synchronized关键字修饰的对象都应该是常量 final修饰

语法:

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

代码解决:

static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
	 Thread t1 = new Thread(() -> {
		 for (int i = 0; i < 5000; i++) {
			 synchronized (room) {
				 counter++;
			 }
		 }
	 }, "t1");
	 Thread t2 = new Thread(() -> {
		 for (int i = 0; i < 5000; i++) {
			 synchronized (room) {
				 counter--;
			 }
		 }
	 }, "t2");
	 t1.start();
	 t2.start();
	 t1.join();
	 t2.join();
	 log.debug("{}",counter);
}

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

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

image.pngimage.png

3.2.3 思考

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

  • 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性 一样的
  • 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?-- 锁不同对象 达不到效果
  • 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象 达不到效果

3.2.4 面向对象改进

把需要共享的对象放入一个类中

class Room {
	int value = 0;
	public void increment() {
		synchronized (this) {
			value++;
		 }
	 }
	public void decrement() {
		synchronized (this) {
			value--;
		}
	}
	public int get() {
		synchronized (this) {
			return value;
		}
	}
}

@Slf4j
public class Test1 {
 
	public static void main(String[] args) throws InterruptedException {
		Room room = new Room();
		Thread t1 = new Thread(() -> {
			for (int j = 0; j < 5000; j++) {
				room.increment();
			}
		}, "t1");
		Thread t2 = new Thread(() -> {
			for (int j = 0; j < 5000; j++) {
				room.decrement();
			}
		}, "t2");
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		log.debug("count: {}" , room.get());
	}
}

3.3 方法上的synchronized

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



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


//不加 synchronized 的方法(对多线程共享变量不安全)
//不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)
//Test.class对象JVM中单例 

线程八锁问题:
考察 synchronized 锁住的是哪个对象

问题一: 1 2 或者 2 1
@Slf4j(topic = "c.Number")
class Number{

    public synchronized void a(){
        log.debug("1");
    }

    public synchronized void b(){
        log.debug("2");
    }
	public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(()->{n1.a();}).start();
        new Thread(()->{n1.b();}).start();
    }
}

问题二:1s后1 2 或者 2 1s后1
@Slf4j(topic = "c.Number")
class Number{

    public synchronized void a(){
        sleep(1);
        log.debug("1");
    }

    public synchronized void b(){
        log.debug("2");
    }
	public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(()->{n1.a();}).start();
        new Thread(()->{n1.b();}).start();
    }
}

问题三:3 1s后1 2 或者 3 2 1s后1 或者 2 3 1s后1 或者 2 1s后1 3 或者 1s后1 3 2 或者 1s后1 2 3 
@Slf4j(topic = "c.Number")
class Number{

    public synchronized void a(){
        sleep(1);
        log.debug("1");
    }

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

    public synchronized void c(){
        log.debug("3");
    }
	public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(()->{n1.a();}).start();
        new Thread(()->{n1.b();}).start();
        new Thread(()->{n1.c();}).start();
    }
}

问题四: 2 1s后1 
@Slf4j(topic = "c.Number")
class Number{

    public synchronized void a(){
        sleep(1);
        log.debug("1");
    }

    public synchronized void b(){
        log.debug("2");
    }
	
	public static void main(String[] args) {
		// 锁不同的对象
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(()->{n1.a();}).start();
        new Thread(()->{n2.b();}).start();
    }

}

问题五:2 1s后1 
@Slf4j(topic = "c.Number")
class Number{

    public static synchronized void a(){
        sleep(1);
        log.debug("1");
    }

    public synchronized void b(){
        log.debug("2");
    }
	
	public static void main(String[] args) {
		// 锁不同对象 一个实例对象 一个类对象
        Number n1 = new Number();
        new Thread(()->{n1.a();}).start();
        new Thread(()->{n1.b();}).start();
    }

}

问题六:2 1s后1 或者 1s后1 2
@Slf4j(topic = "c.Number")
class Number{

    public static synchronized void a(){
        sleep(1);
        log.debug("1");
    }

    public static synchronized void b(){
        log.debug("2");
    }
	
	public static void main(String[] args) {
		//都是锁类对象
        Number n1 = new Number();
        new Thread(()->{n1.a();}).start();
        new Thread(()->{n1.b();}).start();
    }

}

问题七: 2 1s后1
@Slf4j(topic = "c.Number")
class Number{

    public static synchronized void a(){
        sleep(1);
        log.debug("1");
    }

    public synchronized void b(){
        log.debug("2");
    }
	public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(()->{n1.a();}).start();
        new Thread(()->{n1.b();}).start();
    }
	
}

问题八:1s后1 2 或者 2 1s后1
@Slf4j(topic = "c.Number")
class Number{

    public static synchronized void a(){
        sleep(1);
        log.debug("1");
    }

    public static synchronized void b(){
        log.debug("2");
    }
	
	public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(()->{n1.a();}).start();
        new Thread(()->{n1.b();}).start();
    }


}

3.4 变量的线程安全分析

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

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

局部变量是否线程安全?

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

3.4.1 局部变量线程安全分析

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
局部变量的引用稍有不同,先看一个成员变量的例子

class ThreadUnsafe {
	ArrayList<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);
	}
}


static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 2000;
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();
	}
}
存在一种情况是,如果线程2还未add,线程1remove就会报错:
Exception in thread "Thread1" java.lang.ArrayIndexOutOfBoundsException: -1

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

image.png
将list改为局部变量便不会有上述问题了

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

  • 情况1:有其它线程调用 method2 和 method3
  • 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
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);
	}
}
class ThreadSafeSubClass extends ThreadSafe{
	@Override
	public void method3(ArrayList<String> list) {
		new Thread(() -> {
			list.remove(0);
		}).start();
	}
	
	public static void main(String[] args) {
        // 线程不安全
        ThreadSafe test = new ThreadSafeSubClass();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "Thread" + i).start();
        }
    }
}

:::info
从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】
对于存在会发生线程共享问题的方法禁止方法的继承,一般是对于逃逸出方法作用区的不安全变量如SimpleDateFormat类型的。体会下String类型为什么是final的。

String类型是一个final类,一旦给String类型赋值之后呢地址就确定了,重新赋值会产生新的地址,所以String类是一个线程安全的类,毕竟地址都不一样了,也就不存在线程安全问题了。String只能读不能写
:::

3.4.2 常见线程安全类

  • String final修饰 只有读没有写操作
  • Integer final修饰 只有读没有写操作
  • StringBuffer final修饰相关方法有synchronized
  • Random 只有读 相干域都是finnal的无法写
  • Vector 相关方法有synchronized
  • Hashtable 相关方法有synchronized
  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的并不是重新赋值

Hashtable table = new Hashtable();
new Thread(()->{
	table.put("key", "value1");
}).start();
new Thread(()->{
	table.put("key", "value2");
}).start();
  • 它们的每个方法是原子的但注意它们多个方法的组合不是原子的,会有指令交错现象的发生

3.4.3 线程安全类方法的组合

image.png

3.4.4 不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。发生线程安全肯定是读写操作。

public final class Immutable {

    private final int value;

    public Immutable(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
    
    public Immutable set(int value){
        // 产生一个新对象
        return new Immutable(value);
    }
}

3.4.5 实例分析

实例一:

public class MyServlet extends HttpServlet {
	// 是否安全?  不安全 可使用ConcurrentHashMap
	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) {
		// 使用上述变量
	}
}

实例二:

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

实例三:

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

实例四:

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) {
			// ...
		}
	}
}

实例五:

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

实例六:

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() {
		// UserDaow为局部变量  线程安全  但是不建议这样写
		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();
	}
}

实例七:

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 的行为是不确定的,可能导致不安全的发生,被称之为外星方法
// SimpleDateFormat线程不安全 可以看代码format中操作了成员变量calendar
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();
	}
}

实例八:

private static Integer i = 0;
public static void main(String[] args) throws InterruptedException {
	List<Thread> list = new ArrayList<>();
	for (int j = 0; j < 2; j++) {
		Thread thread = new Thread(() -> {
			for (int k = 0; k < 5000; k++) {
				//线程不安全 这里无法锁住i 因为Integer是final类 每次i++相当于重新赋值
				//synchronized的对象一定要是个final引用对象
				synchronized (i) {
					i++;
					// 1. 拆箱 Interer->int
					// 2. 自增
					// 3. i = new Integer(i);
				}
			}
		}, "" + j);
		list.add(thread);
	}
	list.forEach(t -> t.start());
	list.forEach(t -> {
		try {
			t.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	});
	log.debug("{}", i);
}

3.4.6 练习

卖票-加深synchronized锁对象的理解

@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            log.debug("第{}次测试", i);
            solve();
            log.debug("===========");
        }
    }


    public static void solve() {
        int initCount = 200000;
        TicketWindow ticketWindow = new TicketWindow(initCount);
        List<Thread> list = new ArrayList<>();
        List<Integer> sellCount = new Vector<>();
        for (int i = 0; i < 20000; i++) {
            Thread t = new Thread(() -> {
                // 主要分析这里的竞态条件
                int sell = ticketWindow.sell(randomAmount());
                //这里Vector保证线程安全
                sellCount.add(sell);
            });
            list.add(t);
            t.start();
        }

        list.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        int sellCountResult = sellCount.stream().mapToInt(Integer::intValue).sum();
        log.debug("结果:" + (initCount == (sellCountResult + ticketWindow.getCount())));
    }

    static Random random = new Random();
    public static int randomAmount() {
        return random.nextInt(5) + 1;
    }

}


class TicketWindow {

    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    public /*synchronized*/ int getCount() {
        return count;
    }

    public synchronized int sell(int amount) {
        if (this.count >= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }

}

转账-加深synchronized锁类对象的理解

@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {

    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);
        // 开启两个线程 模拟两个账户相互转账1000次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("after transfer total:{}", (a.getMoney() + b.getMoney()));
    }


    static Random random = new Random();

    public static int randomAmount() {
        return random.nextInt(100) + 1;
    }
}


class Account {
    private int money;

    public Account(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public /*synchronized*/ void transfer(Account target, int amount) {
        // 涉及到两个对象的竞态条件 要用锁类对象的方式
        synchronized (Account.class){
            if (this.money > amount) {
                this.setMoney(this.getMoney() - amount);
                target.setMoney(target.getMoney() + amount);
            }
        }
    }
}

3.5 Monitor

3.5.1 Java对象头

在 JVM 中,Java对象保存在堆中时,由以下三部分组成:

  • 对象头(object header):包括了关于堆对象的布局、类型、GC状态、同步状态和标识哈希码的基本信息。Java对象和vm内部对象都有一个共同的对象头格式。
  • 实例数据(Instance Data):主要是存放类的数据信息,父类的信息,对象字段属性信息。
  • 对齐填充(Padding):为了字节对齐,填充的数据,不是必须的。

image.png
以32为虚拟机为例
普通对象由32位mark word,32位class对象指针组成

|--------------------------------------------------------------| 
| 						Object Header (64 bits)				   |
|------------------------------------|-------------------------| 
| 				 Mark Word (32 bits) | Klass Pointer (32 bits) |
|------------------------------------|-------------------------|

数组对象:
多了32位存储数组长度

|---------------------------------------------------------------------------------|
|							 Object Header (96 bits) 							  |
|--------------------------------|-----------------------|------------------------|
| 		 Mark Word(32bits) 		 |Klass Pointer(32bits)  | array length(32bits)   |
|--------------------------------|-----------------------|------------------------|

其中 32位Mark Word 结构为

锁状态(State)25bit4bit1bit2bit
23bit2bit是否偏向锁biased_lock锁标志位
无锁态(Normal)对象hashcode:25分代年龄age001
偏向锁(Biased)线程id:23epoch:2分代年龄age101
轻量级锁(Lightweight Locked)指向栈中锁记录的指针(ptr_to_lock_record):6200
重量级锁(Heavyweight Locked)指向互斥锁(重量级锁)的指针(ptr_to_heavyweight_monitor):6210
GC标记(Marked for GC)11

其中64位Mark Word 结构为

锁状态(State)56bit1bit4bit1bit2bit
是否偏向锁biased_lock锁标志位
无锁态(Normal)unused:25bit对象hashcode:31bitunused分代年龄age001
偏向锁(Biased)线程id:52bitepoch:2bitunused分代年龄age101
轻量级锁(Lightweight Locked)指向栈中锁记录的指针(ptr_to_lock_record):6200
重量级锁(Heavyweight Locked)指向互斥锁(重量级锁)的指针(ptr_to_heavyweight_monitor):6210
GC标记(Marked for GC)11
  • 对象头
    • Klass Pointer 即类型指针,是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
    • Mark word
      • 锁标志位(lock):区分锁状态,11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
      • biased_lock:是否偏向锁,由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
      • 分代年龄(age):表示对象被GC的次数,当该次数到达阈值15的时候,对象就会转移到老年代。
      • 对象的hashcode(hash):运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在重量锁,hashcode会被转移到Monitor中。
      • 偏向锁的线程ID(JavaThread):偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。
      • epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
      • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针。
      • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针。
  • 实例数据
    • 如果对象有属性字段,则这里会有数据信息。如果对象无属性字段,则这里就不会有数据。根据字段类型的不同占不同的字节,例如boolean类型占1个字节,int类型占4个字节等等;
  • 对齐数据
    • 对象可以有对齐数据也可以没有。默认情况下,Java虚拟机堆中对象的起始地址需要对齐至8的倍数。如果一个对象用不到8N个字节则需要对其填充,以此来补齐对象头和实例数据占用内存之后剩余的空间大小。如果对象头和实例数据已经占满了JVM所分配的内存空间,那么就不用再进行对齐填充了。
    • 所有的对象分配的字节总SIZE需要是8的倍数,如果前面的对象头和实例数据占用的总SIZE不满足要求,则通过对齐数据来填满。
    • 为什么要对齐数据?字段内存对齐的其中一个原因,是让字段只出现在同一CPU的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。这两种情况对程序的执行效率而言都是不利的。其实对其填充的最终目的是为了计算机高效寻址。
  • epoch在批量重偏向的使用过程
    • 首先引入一个概念epoch,其本质是一个时间戳,代表了偏向锁的有效性,epoch存储在可偏向对象的MarkWord中。除了对象中的epoch,对象所属的类class信息中,也会保存一个epoch值。
    • 每当遇到一个全局安全点时(这里的意思是说批量重偏向没有完全替代了全局安全点,全局安全点是一直存在的),比如要对class C 进行批量再偏向,则首先对 class C中保存的epoch进行增加操作,得到一个新的epoch_new
    • 然后扫描所有持有 class C 实例的线程栈,根据线程栈的信息判断出该线程是否锁定了该对象,仅将epoch_new的值赋给被锁定的对象中,也就是现在偏向锁还在被使用的对象才会被赋值epoch_new。
    • 退出安全点后,当有线程需要尝试获取偏向锁时,直接检查 class C 中存储的 epoch 值是否与目标对象中存储的 epoch 值相等, 如果不相等,则说明该对象的偏向锁已经无效了(因为(3)步骤里面已经说了只有偏向锁还在被使用的对象才会有epoch_new,这里不相等的原因是class C里面的epoch值是epoch_new,而当前对象的epoch里面的值还是epoch),此时竞争线程可以尝试对此对象重新进行偏向操作。

使用Java代码打印对象头信息:

public class D {

    public static void main(String[] args) {
        D d = new D();
        System.out.println(ClassLayout.parseInstance(d).toPrintable());
    }

}

打印的数据:
com.java.simple.study.juc.p3.D 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)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

此对象目前的对象头为无锁状态 8字节二进制倒序显示
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

image.png

3.5.2 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 时会分析
    :::info
  1. synchronized 必须是进入同一个对象的 monitor 才有上述的效果(想想之前的Integer例子)
  2. 不加 synchronized 的对象不会关联监视器,不遵从以上规则
    :::

3.5.3 synchronized 原理

static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
	synchronized (lock) {
		counter++;
	}
}


对应的字节码为
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: getstatic     #2                  // <- lock引用 (synchronized开始)
         3: dup
         4: astore_1                          // lock引用 -> slot 1
         5: monitorenter					// 将 lock对象 MarkWord 置为 Monitor 指针
         6: getstatic     #3                  // <- i
         9: iconst_1							// 准备常数 1
        10: iadd								// +1
        11: putstatic     #3                  // -> i
        14: aload_1                         //<- lock引用
        15: monitorexit                    // 将 lock对象 MarkWord 重置, 唤醒 EntryList  
        16: goto          24
        19: astore_2                      // e -> slot 2  6-16行的异常处理
        20: aload_1                        // <- lock引用
        21: monitorexit                   // 将 lock对象 MarkWord 重置, 唤醒 EntryList
        22: aload_2                     // <- slot 2 (e)
        23: athrow                     // throw e
        24: return
      Exception table:
         from    to  target type
             6    16    19   any
            19    22    19   any
      LineNumberTable:
        line 13: 0
        line 14: 6
        line 15: 14
        line 16: 24
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      25     0  args   [Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 19
          locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

:::info
方法级别的 synchronized 不会在字节码指令中有所体现
:::

3.5.4 小故事

故事角色

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

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

3.5.5 轻量级锁

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

static final Object obj = new Object();
public static void method1() {
	synchronized( obj ) {
		// 同步块 A
		method2();
	}
}
public static void method2() {
	synchronized( obj ) {
		// 同步块 B
	}
}
  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

image.png

  • 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录

image.png

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

image.png

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

image.png

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

image.png

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

3.5.6 锁膨胀

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

static Object obj = new Object();
public static void method1() {
	synchronized( obj ) {
		// 同步块
	}
}
  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
  • image.png
  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
    • 然后自己进入 Monitor 的 EntryList BLOCKED

image.png

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

3.5.7 自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免直接进入EntryLIst阻塞。
自旋重试成功的情况
image.png
自旋重试失败的情况
image.png

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

3.5.8 偏向锁

3.5.8.1 介绍

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

static final Object obj = new Object();
public static void m1() {
	synchronized( obj ) {
		// 同步块 A
		m2();
	}
}
public static void m2() {
	synchronized( obj ) {
		// 同步块 B
		m3();
	}
}
public static void m3() {
	synchronized( obj ) {
		// 同步块 C
	}
}

image.png
image.png
一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值即最后 3 位为 101,这时它的 thread、epoch、age 都为 0 (JVM参数-XX:-UseBiasedLocking禁用偏向锁
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以使用JVM 参数** -XX:BiasedLockingStartupDelay=0** 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为即最后 3 位为 001,这时它的 hashcode、 age 都为 0,第一次用到 hashcode 时才会赋值

3.5.8.2 验证

  1. 测试偏向锁延迟特性以及禁用
public class D {

    public static void main(String[] args) {
        //testDelay();
        testForbidDelay();
    }

    public static void testForbidDelay(){
        // jvm参数 -XX:BiasedLockingStartupDelay=0 延迟设为0秒不延迟  默认4秒
        // -XX:-UseBiasedLocking 关闭偏向锁
        System.out.println(ClassLayout.parseInstance(new D()).toPrintable());
        System.out.println(ClassLayout.parseInstance(new D()).toPrintable());
    }

    public static void testDelay(){
        //00000001
        System.out.println(ClassLayout.parseInstance(new D()).toPrintable());
        // 延迟几秒后 对象才会编程偏向锁
        Sleeper.sleep(4);
        //00000101
        System.out.println(ClassLayout.parseInstance(new D()).toPrintable());
    }

}

  1. 查看偏向锁对象头信息
public static void testLockObjectHeader(){
	D d = new D();
	ClassLayout classLayout = ClassLayout.parseInstance(d);
	new Thread(() -> {
		log.debug("synchronized 前");
		System.out.println(classLayout.toPrintable());
		synchronized (d) {
			log.debug("synchronized 中");
			System.out.println(classLayout.toPrintable());
		}
		log.debug("synchronized 后");
		System.out.println(classLayout.toPrintable());
	}, "t1").start();
    }
}

image.png
:::info
处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
:::

  1. 上述代码在偏向锁开关设为禁用后结果如下

image.png
:::info
无锁状态<->轻量级锁状态
正常状态对象一开始是没有 hashCode 的,第一次调用才生成
:::

3.5.8.3 偏向锁撤销

  1. 调用对象 hashCode

调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销。因为没有足够的空间去存hashcode了只能撤销(看下偏向锁mark word示意图)

  • 轻量级锁会在锁记录中记录 hashCode
  • 重量级锁会在 Monitor 中记录 hashCode

在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking

public static void testLockObjectHeader(){
        D d = new D();
        d.hashCode();
        ClassLayout classLayout = ClassLayout.parseInstance(d);
        new Thread(() -> {
            log.debug("synchronized 前");
            System.out.println(classLayout.toPrintable());
            synchronized (d) {
                log.debug("synchronized 中");
                System.out.println(classLayout.toPrintable());
            }
            log.debug("synchronized 后");
            System.out.println(classLayout.toPrintable());
        }, "t1").start();
    }

image.png
:::info
可以看出 调用hashcode后 变成了轻量级锁
:::

  1. 其它线程使用对象

当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁(使用时间交错)

public static void testThreadLock() {
	Dog dog = new Dog();
	Thread t1 = new Thread(() -> {
		synchronized (dog) {
			log.debug(ClassLayout.parseInstance(dog).toPrintable());
		}
		synchronized (TestBiased.class) {
			TestBiased.class.notify();
		}
	}, "t1");
	t1.start();
	
	Thread t2 = new Thread(() -> {
		synchronized (TestBiased.class) {
			try {
				TestBiased.class.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		log.debug(ClassLayout.parseInstance(dog).toPrintable());
		synchronized (dog) {
			log.debug(ClassLayout.parseInstance(dog).toPrintable());
		}
		log.debug(ClassLayout.parseInstance(dog).toPrintable());
	}, "t2");
	t2.start();
}

:::info
101(t1) -> 101(t2) -> 000(t2) -> 001(t2)
:::

  1. 调用 wait/notify(重量级锁)
public static void testWaitNotify(){
        Dog dog = new Dog();
        new Thread(()->{
            // 偏向锁 101
            log.debug(ClassLayout.parseInstance(dog).toPrintable());
            synchronized (dog){
                // 偏向锁 101
                log.debug(ClassLayout.parseInstance(dog).toPrintable());
                try {
                    // 等待t2线程唤醒
                    dog.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //变成了 重量级锁010  (wait notify 自动会变成重量级锁)
                log.debug(ClassLayout.parseInstance(dog).toPrintable());
            }
        },"t1").start();


        new Thread(()->{
            try {
                Thread.sleep(6000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (dog){
                log.debug("notify");
                dog.notify();
            }
        },"t2").start();

    }
  1. 批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象头的 Thread ID。
当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至
加锁线程。

public static void testReBiased() {
        Vector<Dog> list = new Vector<>();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                Dog dog = new Dog();
                list.add(dog);
                synchronized (dog) {
                    log.debug(i + "");
                    // 此时都会偏向t1线程
                    log.debug(ClassLayout.parseInstance(dog).toPrintable());
                }
            }
            synchronized (list){
                list.notify();
            }
        }, "t1");
        t1.start();

        Thread t2 = new Thread(()->{
            synchronized (list){
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("----------------------------------");
            for (int i = 0; i < 30; i++) {
                if(i==19){
                    log.debug("注意了!!!!到20了");
                }
                Dog dog = list.get(i);
                log.debug(i+"");
                //还是偏向t1的 (当到达阈值时偏向t2)
                log.debug(ClassLayout.parseInstance(dog).toPrintable());
                synchronized (dog) {
                    // 有线程竞争->轻量级锁  (当到达阈值时偏向t2的偏向锁)
                    log.debug(ClassLayout.parseInstance(dog).toPrintable());
                }
                // 正常无锁状态  (当到达阈值时偏向t2的偏向锁)
                log.debug(ClassLayout.parseInstance(dog).toPrintable());
            }
        },"t2");
        t2.start();

    }
  1. 批量撤销

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

static Thread t1,t2,t3;
public static void batchReset() throws InterruptedException {
	Vector<Dog> list = new Vector<>();
	int loopNumber = 39;
	t1 = new Thread(()->{
		for(int i=0;i<loopNumber;i++){
			Dog d = new Dog();
			list.add(d);
			synchronized (d){
				log.debug(i+"");
				log.debug(ClassLayout.parseInstance(d).toPrintable());
			}
		}
		LockSupport.unpark(t2);
	},"t1");

	t2 = new Thread(()->{
		LockSupport.park();
		log.debug("---------------------------");
		for(int i=0;i<loopNumber;i++){
			Dog d = list.get(i);
			log.debug(i+"");
			log.debug(ClassLayout.parseInstance(d).toPrintable());
			synchronized (d){
				log.debug(ClassLayout.parseInstance(d).toPrintable());
			}
			log.debug(ClassLayout.parseInstance(d).toPrintable());
		}
		LockSupport.unpark(t3);
	},"t2");

	t3 = new Thread(()->{
		LockSupport.park();
		log.debug("---------------------------");
		for(int i=0;i<loopNumber;i++){
			Dog d = list.get(i);
			log.debug(i+"");
			log.debug(ClassLayout.parseInstance(d).toPrintable());
			synchronized (d){
				log.debug(ClassLayout.parseInstance(d).toPrintable());
			}
			log.debug(ClassLayout.parseInstance(d).toPrintable());
		}
	},"t3");

	t1.start();
	t2.start();
	t3.start();
	t3.join();
	log.debug(ClassLayout.parseInstance(new Dog()).toPrintable());
}

3.5.9 锁消除

JIT编译器在编译的时候,进行逃逸分析。分析synchronized锁对象是不是只可能被一个线程加锁,不存在其他线程来竞争加锁的情况。这时就可以消除该锁了,提升执行效率。
编译就不用加入monitorenter和monitorexit指令。

@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class NoSyncBenchmark {
	
	static int x = 0;
	
	@Benchmark
	public void a() {
		x++;
	}
	
	@Benchmark
	public void b() {
		// 代码都是人写出来的  有的可能会写出这样的代码
		// 锁对象对临界区代码根本无效 非共享 所以jvm的即时编译去JIT会优化它
		// 即方法a、b执行效率其实差不多
		Object o = new Object();
		synchronized (o) {
			x++;
		}
	}
	
	public static void main(String[] args) throws RunnerException {
		Options opt = new OptionsBuilder()
			.include(MultiBenchMark.class.getSimpleName())
			.resultFormat(ResultFormatType.JSON).build();
		new Runner(opt).run();
	}
	
}

验证结果如下:

Benchmark          Mode  Cnt  Score   Error  Units
NoSyncBenchmark.a  avgt    5  1.957 ± 2.052  ns/op
NoSyncBenchmark.b  avgt    5  1.819 ± 1.121  ns/op

3.5.10 锁粗化

JIT编译时,发现一段代码中频繁的加锁释放锁,会将前后的锁合并为一个锁,避免频繁加锁释放锁。

synchronized(this){
}
synchronized(this){
}
synchronized(this){
}

3.6 wait-notify

3.6.1 为什么需要wait-notify

小故事 - 为什么需要 wait

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

image.png

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

image.png

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

3.6.2 原理

image.png

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

3.6.3 API介绍

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待,释放对象的锁,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止
  • obj.wait(long n) 有时限的等待, 到 n 毫秒后结束等待或是被notify,被notify后不会继续等到n毫秒结束,直接向下运行
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒,存在虚假唤醒问题
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法,否则会出现java.lang.IllegalMonitorStateException。

@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {

    final static Object obj = new Object();

    public static void main(String[] args) {
        new Thread(()->{
            synchronized (obj){
                log.debug("t1执行...");
                try {
                    obj.wait(); // 先释放obj锁 然后让线程在此一直等待(不占用CPU时间片)
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("t1其它代码");
            }
        },"t1").start();

        new Thread(()->{
            synchronized (obj){
                log.debug("t2执行...");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("t2其它代码");
            }
        },"t2").start();
        // 主线程两秒后执行
        Sleeper.sleep(2);
        log.debug("唤醒obj上的其它线程");
        //Exception in thread "main" java.lang.IllegalMonitorStateException
        // 未获得到锁不能调用
        //obj.notifyAll();
        synchronized (obj){
            //obj.notify(); // 随机唤醒一个线程
            obj.notifyAll(); //唤醒obj上所有线程
        }
    }

}

3.6.4 wait-notify的正确使用

:::info
sleep(long n) 和 wait(long n) 的区别

  • sleep 是 Thread 方法,而 wait 是 Object 的方法
  • sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  • sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
  • 它们状态 TIMED_WAITING
    :::
    感受下列代码的优劣及变化,体会wait-notify的使用
    step1:
@Slf4j(topic = "c.TestCorrectPostureStep1")
public class TestCorrectPostureStep1 {

    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    // 不会释放锁
                    Sleeper.sleep(2);
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活");
                }
            }
        }, "小南").start();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                // 必须等待锁释放 2s后才可以开始干活
                synchronized (room) {
                    log.debug("可以开始干活了");
                }
            }, "其他人").start();
        }

        Sleeper.sleep(1);
        new Thread(()->{
            // 这里不能synchronized 否则就永远送不到烟了。。因为sleep不会释放锁
            hasCigarette = true;
            log.debug("烟送到了哦!");
        },"送烟的").start();
    }

}


  • 其它干活的线程,都要一直阻塞,效率太低
  • 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
  • 加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加synchronized 就好像 main 线程是翻窗户进来的
  • 解决方法,使用 wait - notify 机制

step2:

@Slf4j(topic = "c.TestCorrectPostureStep2")
public class TestCorrectPostureStep2 {

    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        // 会释放锁  等够2s/被打断/被唤醒
                        room.wait(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活");
                }
            }
        }, "小南").start();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (room) {
                    log.debug("可以开始干活了");
                }
            }, "其他人").start();
        }

        Sleeper.sleep(1);
        new Thread(()->{
            synchronized (room){
                hasCigarette = true;
                log.debug("烟送到了哦!");
                room.notify();
            }
        },"送烟的").start();
    }

}
  • 解决了其它干活的线程阻塞的问题
  • 但如果有其它线程也在等待条件呢?

step3:

@Slf4j(topic = "c.TestCorrectPostureStep3")
public class TestCorrectPostureStep3 {

    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活");
                }
            }
        }, "小南").start();

        new Thread(() -> {
            synchronized (room) {
                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();

        Sleeper.sleep(1);
        new Thread(()->{
            synchronized (room){
                hasTakeout = true;
                log.debug("外卖送到了哦!");
                room.notify();
            }
        },"送外卖的").start();
    }

}
  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】
  • 解决方法,改为 notifyAll

step4:

@Slf4j(topic = "c.TestCorrectPostureStep4")
public class TestCorrectPostureStep4 {

    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活");
                }
            }
        }, "小南").start();

        new Thread(() -> {
            synchronized (room) {
                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();

        Sleeper.sleep(1);
        new Thread(()->{
            synchronized (room){
                hasTakeout = true;
                log.debug("外卖送到了哦!");
                room.notifyAll();
            }
        },"送外卖的").start();
    }

}
  • 用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了
  • 解决方法,用 while(条件变量) + wait,当条件不成立,再次 wait

step5:正确的使用姿势——被错误唤醒的线程继续wait

@Slf4j(topic = "c.TestCorrectPostureStep5")
public class TestCorrectPostureStep5 {

    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                while (!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) {
                log.debug("外卖送到没?[{}]", hasTakeout);
                while (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活");
                }
            }
        }, "小女").start();

        Sleeper.sleep(1);
        new Thread(()->{
            synchronized (room){
                hasTakeout = true;
                log.debug("外卖送到了哦!");
                room.notifyAll();
            }
        },"送外卖的").start();
    }

}

synchronized(lock) {
	while(条件不成立) {
		lock.wait();
	}
	// 干活
}
//另一个线程
synchronized(lock) {
	lock.notifyAll();
}

3.6.5 同步模式之保护性暂停

定义:
即 Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
  • JDK 中,join 的实现、Future 的实现,采用的就是此模式
  • 因为要等待另一方的结果,因此归类到同步模式
  • image.png

实现:

@Slf4j(topic = "c.TestGuardedObject")
public class TestGuardedObject {

    public static void main(String[] args) {
        GuardedObject guardedObject = new GuardedObject();
        new Thread(() -> {
            try {
                List<String> download = Downloader.download();
                log.debug("download complete");
                guardedObject.complete(download);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
        log.debug("waiting....");
        // 主线程阻塞等待
        Object response = guardedObject.get();
        log.debug("get response size:[{}]", ((List<String>) response).size());
    }


}

class GuardedObject {
    private Object response;
    private final Object lock = new Object();

    public Object get() {
        synchronized (lock) {
            while (response == null) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return response;
        }
    }

    public void complete(Object response) {
        synchronized (lock) {
            this.response = response;
            lock.notifyAll();
        }
    }

}

带超时版GuardedObject
实现:

@Slf4j(topic = "c.TestGuardedObject")
public class TestGuardedObjectV2 {

    public static void main(String[] args) {
        GuardedObjectV2 guardedObject = new GuardedObjectV2();
        new Thread(() -> {
            Sleeper.sleep(1);
            guardedObject.complete(null);
            Sleeper.sleep(1);
            guardedObject.complete(Arrays.asList("a", "b", "c"));
        }).start();
        // 测试非超时
        //Object response = guardedObject.get(2500);
        // 测试超时
        Object response = guardedObject.get(1500);
        if (response != null) {
            log.debug("get response size:[{}]", ((List<String>) response).size());
        } else {
            log.debug("cant get response");
        }
    }


}

@Slf4j(topic = "c.GuardedObjectV2")
class GuardedObjectV2 {
    private Object response;
    private final Object lock = new Object();

    public Object get(long mills) {
        synchronized (lock) {
            // 1. 记录最初时间
            long begin = System.currentTimeMillis();
            // 2. 已经经历的时间
            long timePassed = 0;
            while (response == null) {
                // 4. 假设mills是1000 结果在400时唤醒了,那么还有600要等
                long waitTime = mills - timePassed;
                log.debug("waitTime:{}", waitTime);
                if (waitTime <= 0) {
                    log.debug("break...");
                    break;
                }
                try {
                    lock.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 3. 如果提前被唤醒 这时已经经历的时间为400
                timePassed = System.currentTimeMillis() - begin;
                log.debug("timePassed:{},object is null:{}", timePassed, response == null);
            }
            return response;
        }
    }

    public void complete(Object response) {
        synchronized (lock) {
            this.response = response;
            lock.notifyAll();
        }
    }

}

多任务版 GuardedObject
图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右
侧的 t1,t3,t5 就好比邮递员, 如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理
image.png

@Slf4j(topic = "c.TestGuardedObject")
public class TestGuardedObjectV3 {
	
	public static void main(String[] args) {
		for (int i = 0; i < 3; i++) {
			new People().start();
		}
		Sleeper.sleep(1);
		for (Integer id : Features.getIds()) {
			new Postman(id, "内容:" + id).start();
		}
	}
	
	
}

class GuardedObjectV3 {
	// 标识  可以被多个线程使用
	private int id;
	private Object response;
	
	public GuardedObjectV3(int id) {
		this.id = id;
	}
	
	public int getId() {
		return id;
	}
	
	public Object get(long mills) {
		synchronized (this) {
			// 1. 记录最初时间
			long begin = System.currentTimeMillis();
			// 2. 已经经历的时间
			long timePassed = 0;
			while (response == null) {
				// 4. 假设mills是1000 结果在400时唤醒了,那么还有600要等
				long waitTime = mills - timePassed;
				if (waitTime <= 0) {
					break;
				}
				try {
					this.wait(waitTime);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				// 3. 如果提前被唤醒 这时已经经历的时间为400
				timePassed = System.currentTimeMillis() - begin;
			}
			return response;
		}
	}
	
	public void complete(Object response) {
		synchronized (this) {
			this.response = response;
			this.notifyAll();
		}
	}
	
}

class Features {
	private static Map<Integer, GuardedObjectV3> FEATURES = new ConcurrentHashMap<>();
	private static int id = 1;
	
	private static synchronized int generateId() {
		return id++;
	}
	
	public static GuardedObjectV3 getGuardedObject(int id) {
		return FEATURES.remove(id);
	}
	
	public static GuardedObjectV3 createGuardedObject() {
		GuardedObjectV3 guardedObjectV3 = new GuardedObjectV3(generateId());
		FEATURES.put(guardedObjectV3.getId(), guardedObjectV3);
		return guardedObjectV3;
	}
	
	public static Set<Integer> getIds() {
		return FEATURES.keySet();
	}
	
}

@Slf4j(topic = "c.People")
class People extends Thread {
	@Override
	public void run() {
		GuardedObjectV3 guardedObject = Features.createGuardedObject();
		log.debug("开始收信 id:{}", guardedObject.getId());
		Object text = guardedObject.get(5000);
		log.debug("收到信 id:{}, 内容:{}", guardedObject.getId(), text);
	}
}

@Slf4j(topic = "c.Postman")
class Postman extends Thread {
	private int id;
	private String text;
	
	public Postman(int id, String text) {
		this.id = id;
		this.text = text;
	}
	
	@Override
	public void run() {
		GuardedObjectV3 guardedObject = Features.getGuardedObject(id);
		log.debug("送信id:{},内容:{}", id, text);
		guardedObject.complete(guardedObject);
	}
}

3.6.6 join原理

调用者轮询检查线程 alive 状态
t1.join等价于

synchronized (t1) {
	// 调用者线程进入 t1 的 waitSet 等待, 直到 t1 运行结束
	while (t1.isAlive()) {
		t1.wait(0);
	}
}
// 所有线程死的时候会调用自己的notifyAll方法
// As a thread terminates the {@code this.notifyAll} method is invoked

3.6.7 异步模式之生产者消费者

定义

  • 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
  • 消费队列可以用来平衡生产和消费的线程资源
  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
  • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
  • JDK 中各种阻塞队列,采用的就是这种模式

image.png
实现

@Slf4j(topic = "c.TestProducerConsumer")
public class TestProducerConsumer {

    public static void main(String[] args) {
        MessageQueue messageQueue = new MessageQueue(2);
        for (int i = 0; i < 4; i++) {
            int id = i;
            new Thread(() -> {
                try {
                    log.debug("download...");
                    List<String> download = Downloader.download();
                    log.debug("try put message id:{}", id);
                    messageQueue.put(new Message(id, download));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }, "生产者" + i).start();
        }

        //消费者线程
        new Thread(() -> {
            while (true) {
                Message message = messageQueue.take();
                List<String> respList = (List<String>) message.getMessage();
                log.debug("take message id:{}, message resp size:{}", message.getId(), respList.size());
            }
        }, "消费者").start();
    }

}

@Data
@AllArgsConstructor
class Message {
    private int id;
    private Object message;
}

@Slf4j(topic = "c.MessageQueue")
class MessageQueue {
    private final LinkedList<Message> queue;

    private final int capacity;

    public MessageQueue(int capacity) {
        this.capacity = capacity;
        queue = new LinkedList<>();
    }

    public Message take() {
        synchronized (queue) {
            while (queue.isEmpty()) {
                log.debug("queue empty...");
                try {
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            Message message = queue.removeFirst();
            queue.notifyAll();
            return message;
        }
    }

    public void put(Message message) {
        synchronized (queue) {
            while (queue.size() == capacity) {
                log.debug("queue limit capacity, wait");
                try {
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            queue.addLast(message);
            queue.notifyAll();
        }
    }
}

3.7 Park & Unpark

3.7.1 基本使用

  • LockSupport.park(); 暂停当前线程
  • LockSupport.unpark(暂停线程对象); 恢复某个线程的运行
  • 底层是用unsafe的本地方法

先park再unpark:

@Slf4j(topic = "c.TestLockSupport")
public class TestLockSupport {

    public static void main(String[] args) {
        testParkUnpark();
    }

    public static void testParkUnpark(){
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            Sleeper.sleep(1);
            log.debug("park...");
            LockSupport.park();
            log.debug("resume...");
        }, "t1");
        t1.start();
        Sleeper.sleep(2);
        log.debug("unpark...");
        LockSupport.unpark(t1);
    }

}

20:56:22.148 c.TestLockSupport [t1] - start...
20:56:23.156 c.TestLockSupport [t1] - park...
20:56:24.154 c.TestLockSupport [main] - unpark...
20:56:24.154 c.TestLockSupport [t1] - resume...

先unpark再park

@Slf4j(topic = "c.TestLockSupport")
public class TestLockSupport {

    public static void main(String[] args) {
        //testParkUnpark();
        testUnParkPark();
    }

    public static void testUnParkPark(){
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            Sleeper.sleep(2);
            log.debug("park...");
			// 不能park住
            LockSupport.park();
            log.debug("resume...");
        }, "t1");
        t1.start();

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

特点:
与 Object 的 wait & notify 相比

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

3.7.2 原理

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

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

image.png

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

image.png

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

image.png

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 当前线程调用 Unsafe.park() 方法
  3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
  4. 设置 _counter 为 0

3.8 重新理解线程状态转换

image.png

  • NEW --> RUNNABLE
    • 线程对象创建后调用start方法
    • 由CPU分配时间片调度,被调度的运行状态(包括IO阻塞等),没有被调度为可运行状态
    • 不可逆
  • RUNNABLE <–> WAITING
    • t 线程用 synchronized(obj) 获取了对象锁后
      • 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
      • 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
        • 竞争锁成功,t 线程从 WAITING --> RUNNABLE
        • 竞争锁失败,t 线程从 WAITING --> BLOCKED
@Slf4j(topic = "c.TestThreadStatus")
public class TestThreadStatus {

    final static Object obj = new Object();

    public static void main(String[] args) {
        new Thread(()->{
            synchronized (obj){
                log.debug("执行...");
                try {
                    obj.wait();
					// 在线程维度debug 查看线程状态
                    Thread thread = Thread.currentThread();
                    log.debug("状态:{}", thread.getState());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码...");
            }
        },"t1").start();

        new Thread(()->{
            synchronized (obj){
                log.debug("执行...");
                try{
                    obj.wait();
					// 在线程维度debug 查看线程状态
                    Thread thread = Thread.currentThread();
                    log.debug("状态:{}", thread.getState());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码...");
            }
        },"t2").start();
        Sleeper.sleep(0.5);
        log.debug("唤醒obj上其它线程");
        synchronized (obj){
			// 在线程维度debug 查看线程状态
            obj.notifyAll();
        }
    }

}

  • RUNNABLE <–> WAITING
    • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
      • 注意是当前线程在t 线程对象的监视器上等待
    • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE
  • RUNNABLE <–> WAITING
    • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
    • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE
  • 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
  • RUNNABLE <–> TIMED_WAITING
    • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
      • 注意是当前线程在t 线程对象的监视器上等待
    • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从TIMED_WAITING --> RUNNABLE
  • RUNNABLE <–> TIMED_WAITING
    • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
    • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE
  • RUNNABLE <–> TIMED_WAITING
    • 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
    • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从TIMED_WAITING–> RUNNABLE
  • RUNNABLE <–> BLOCKED
    • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
    • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED
  • RUNNABLE <–> TERMINATED
    • 当前线程所有代码运行完毕,进入 TERMINATED
    • 不可逆
    • 会调用当前线程上的notifyAll来唤醒所有其它线程(join方法原理)

3.9 多把锁

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

public class TestMultiLock {

    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(()->{
            bigRoom.sleep();
        },"小南").start();
        new Thread(()->{
            bigRoom.study();
        },"小女").start();
    }

}

@Slf4j(topic = "c.BigRoom")
class BigRoom {
    public void sleep(){
        synchronized (this){
            log.debug("sleep 2 小时");
            Sleeper.sleep(2);
        }
    }

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

}

21:49:45.922 c.BigRoom [小南] - sleep 2 小时
21:49:47.938 c.BigRoom [小女] - sleep 1 小时

改进:

public class TestMultiLock {

    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(()->{
            bigRoom.sleep();
        },"小南").start();
        new Thread(()->{
            bigRoom.study();
        },"小女").start();
    }

}

@Slf4j(topic = "c.BigRoom")
class BigRoom {
    private final Object sleepRoom = new Object();
    private final Object studyRoom = new Object();

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

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

}

21:51:27.669 c.BigRoom [小女] - sleep 1 小时
21:51:27.669 c.BigRoom [小南] - sleep 2 小时

将锁的粒度细分

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

3.10 活跃性

3.10.1 死锁

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

@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {

    private static final Object A = new Object();
    private static final Object B = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                log.debug("lock A");
                Sleeper.sleep(1);
                synchronized (B) {
                    log.debug("lock B");
                    log.debug("操作。。。");
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (B) {
                log.debug("lock B");
                Sleeper.sleep(0.5);
                synchronized (A) {
                    log.debug("lock A");
                    log.debug("操作。。。");
                }
            }
        }, "t2");

        t1.start();
        t2.start();

    }

}

定位死锁

  • 使用jdk jconsole可视化工具

image.png
image.png
image.png

  • jps定位进程id,jstack定位原因
    • jps

image.png

  • jstack 10924

image.png

  • 避免死锁要注意加锁顺序
  • 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

3.10.2 哲学家就餐问题

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

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

代码描述:

public class TestDeadLock {

    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("c1");
        Chopstick c2 = new Chopstick("c2");
        Chopstick c3 = new Chopstick("c3");
        Chopstick c4 = new Chopstick("c4");
        Chopstick c5 = new Chopstick("c5");

        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里斯多德", c3, c4).start();
        new Philosopher("赫拉克利克", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();
    }

}

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
    private final Chopstick left;
    private final 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();
                }
                // 释放右手筷子
            }
            // 释放左手筷子
        }
    }
}


class Chopstick {
    private final String name;

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

    @Override
    public String toString() {
        return "Chopstick{" +
                "name='" + name + '\'' +
                '}';
    }
}

执行一会就发现执行不下去了
:::info
21:12:07.762 c.Philosopher [苏格拉底] - eating…
21:12:07.762 c.Philosopher [亚里斯多德] - eating…
21:12:08.778 c.Philosopher [阿基米德] - eating…
21:12:08.778 c.Philosopher [柏拉图] - eating…
21:12:09.790 c.Philosopher [柏拉图] - eating…
21:12:10.802 c.Philosopher [苏格拉底] - eating…
:::
使用jconsole发现死锁
image.png
这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情况。

3.10.3 活锁

活锁出现在两个线程互相改变对方的结束条件,虽然没有死锁但是最后谁也无法结束,例如

@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {

    static volatile int count = 13;

    public static void main(String[] args) {
        new Thread(() -> {
            // 期望减到0 退出循环
            while (count > 0) {
                Sleeper.sleep(0.5);
                count--;
                log.debug("count:{}", count);
            }
        },"t1").start();

        new Thread(() -> {
            // 期望达到20 退出循环
            while (count < 20) {
                Sleeper.sleep(0.5);
                count++;
                log.debug("count:{}", count);
            }
        },"t2").start();
    }

}

3.10.4 饥饿

很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不
易演示,讲读写锁时会涉及饥饿问题。
下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题
image.png
顺序加锁之后
image.png
哲学家就餐问题尝试将锁顺序调换解决死锁问题 但是造成了线程饥饿

public static void main(String[] args) {
        Chopstick c1 = new Chopstick("c1");
        Chopstick c2 = new Chopstick("c2");
        Chopstick c3 = new Chopstick("c3");
        Chopstick c4 = new Chopstick("c4");
        Chopstick c5 = new Chopstick("c5");

        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();
        // 饥饿
        new Philosopher("阿基米德", c1, c5).start();
    }

3.11 ReentrantLock

相对于 synchronized 它具备如下特点

  • 可中断。synchronized 不可被Interrupt打断,
  • 可以设置超时时间。synchronized 获取不到锁会一直在EntryList中blocked
  • 可以设置为公平锁。耗性能,一般不用(可解决线程饥饿问题)
  • 支持多个条件变量。synchronized 只能锁一个对象,等待唤醒都在这个对象上的线程,存在虚假唤醒问题,即线程唤醒之后又blocked,消耗性能(上下文切换)

与 synchronized 一样,都支持可重入
基本语法:

// 获取锁
reentrantLock.lock();
try {
	// 临界区
} finally {
	// 释放锁
	reentrantLock.unlock();
}

3.11.1 可重入

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

@Slf4j(topic = "c.TestReentrantLock")
public class TestReentrantLock {
    static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        m1();
    }

    public static void m1() {
        lock.lock();
        try {
            log.debug("execute m1...");
            m2();
        } finally {
            lock.unlock();
        }
    }

    public static void m2() {
        lock.lock();
        try {
            log.debug("execute m2...");
            m3();
        } finally {
            lock.unlock();
        }
    }

    public static void m3() {
        lock.lock();
        try {
            log.debug("execute m3...");
        } finally {
            lock.unlock();
        }
    }

}

3.11.2 可打断

@Slf4j(topic = "c.TestInterrupt")
public class TestInterrupt {

    static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
//        test1();
        test2();
    }

    public static void test2() {
        Thread t1 = new Thread(() -> {
            log.debug("启动...");
            // blocked等待锁时 不可以被打断
            lock.lock();
            try {
                log.debug("t1获得到锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.debug("主线程获取到锁");
        t1.start();
        try {
            Sleeper.sleep(1);
            //打断t1
            t1.interrupt();
            log.debug("执行打断操作");
        } finally {
            lock.unlock();
        }
    }


    public static void test1() {
        Thread t1 = new Thread(() -> {
            log.debug("启动...");
            try {
                // blocked等待锁时 可以被打断 取消等待
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("等锁过程中被打断");
                return;
            }
            try {
                log.debug("t1获得到锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.debug("主线程获取到锁");
        t1.start();
        try {
            Sleeper.sleep(1);
            //打断t1
            t1.interrupt();
            log.debug("执行打断操作");
        } finally {
            lock.unlock();
        }
    }

}

3.11.3 锁超时

@Slf4j(topic = "c.TestTimeout")
public class TestTimeout {
    static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        //test1();
        test2();
    }

    public static void test2() {
        Thread t1 = new Thread(() -> {
            log.debug("启动...");
            try {
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                    log.debug("尝试获取锁等待1s后仍未获取到失败!");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("尝试获取锁被打断");
                return;
            }
            try {
                log.debug("t1获取到了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.debug("主线程获取到了锁");
        t1.start();
        try {
            Sleeper.sleep(2);
        } finally {
            lock.unlock();
        }
    }


    public static void test1() {
        Thread t1 = new Thread(() -> {
            log.debug("启动...");
            if (!lock.tryLock()) {
                log.debug("尝试获取锁 未获取到立刻返回false 快速失败!");
                return;
            }
            try {
                log.debug("t1获取到了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.debug("主线程获取到了锁");
        t1.start();
        try {
            Sleeper.sleep(2);
        } finally {
            lock.unlock();
        }
    }

}

3.11.4 使用tryLock解决哲学家就餐问题

public class TestDeadLock {

    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("c1");
        Chopstick c2 = new Chopstick("c2");
        Chopstick c3 = new Chopstick("c3");
        Chopstick c4 = new Chopstick("c4");
        Chopstick c5 = new Chopstick("c5");

        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里斯多德", c3, c4).start();
        new Philosopher("赫拉克利克", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();
    }

}

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
    private final Chopstick left;
    private final 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) {
            // 获得左手筷子
            if (left.tryLock()) {
                try {
                    // 获得右手筷子
                    if (right.tryLock()) {
                        try {
                            eat();
                        } finally {
                            // 释放右手筷子
                            right.unlock();
                        }
                    }
                } finally {
                    // 释放左手筷子
                    left.unlock();
                }
            }
        }
    }
}

class Chopstick extends ReentrantLock {
    private final String name;

    public Chopstick(String name) {
        // 是否公平锁-->解决饥饿问题  耗性能  不建议使用
        //super(true);
        this.name = name;
    }

    @Override
    public String toString() {
        return "Chopstick{" +
                "name='" + name + '\'' +
                '}';
    }
}

3.11.5 公平锁

ReentrantLock 默认是不公平的,公平锁一般没有必要,会降低并发度。

@Slf4j(topic = "c.TestFair")
public class TestFair {

    // 布尔值控制是否公平锁
    static final ReentrantLock lock = new ReentrantLock(true);

    public static void main(String[] args) {
        // 主线程上锁
        lock.lock();
        for (int i = 0; i < 500; i++) {
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " running...");
                } finally {
                    lock.unlock();
                }
            }, "t" + i).start();
        }
        // 1s后另一个线程加入争抢
        Sleeper.sleep(1);
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " start...");
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "running...");
            } finally {
                lock.unlock();
            }
        }, "强行插入").start();
        /**
         * 非公平锁的情况下 强行插入线程有机会在中间抢到锁
         * 公平锁的情况下  强行插入线程一定是在最后抢到锁
         */
        lock.unlock();
    }
    
}

3.11.6 条件变量

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

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
  • synchronized存在notify具有随机唤醒,notifyAll会唤醒所有的。均会存在虚假唤醒问题,ReentrantLock 条件变量则是精确唤醒

使用要点:

  • Condition#await 前需要获得锁, Object#wait前需要synchronized加锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(signal)(或打断、或超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行
@Slf4j(topic = "c.TestCondition")
public class TestCondition {

    static final ReentrantLock lock = new ReentrantLock();
    static Condition waitCigaretteQueue = lock.newCondition();
    static Condition waitBreakfastQueue = lock.newCondition();
    static volatile boolean hasCigarette = false;
    static volatile boolean hasBreakfast = false;

    public static void main(String[] args) {
        new Thread(() -> {
            lock.lock();
            try {
                while (!hasCigarette){
                    try {
                        waitCigaretteQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        log.debug("等烟过程中被打断");
                        return;
                    }
                }
                log.debug("等到烟了,开始干活了。。。");
            } finally {
                lock.unlock();
            }
        }, "小南").start();

        new Thread(() -> {
            lock.lock();
            try {
                while (!hasBreakfast){
                    try {
                        waitBreakfastQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        log.debug("等早餐过程中被打断");
                        return;
                    }
                }
                log.debug("等到早餐了,开始干活了。。。");
            } finally {
                lock.unlock();
            }
        }, "小女").start();

        Sleeper.sleep(2);
        sendCigarette();
        Sleeper.sleep(2);
        sendBreakfast();
    }

    private static void sendCigarette(){
        lock.lock();
        try{
            log.debug("送烟到【烟等待室】了");
            hasCigarette = true;
            waitCigaretteQueue.signal();
        }finally {
            lock.unlock();
        }
    }

    private static void sendBreakfast(){
        lock.lock();
        try{
            log.debug("送早餐到【早餐等待室】了");
            hasBreakfast = true;
            waitBreakfastQueue.signal();
        }finally {
            lock.unlock();
        }
    }

}

3.12 同步模式之顺序控制

3.12.1 固定运行顺序

比如,必须先 2 后 1 打印
wait-notify版本

// 用来同步锁的对象
static Object obj = new Object();
// 标识t2线程是否运行过
static boolean t2runned = false;

public static void testFixOrderWaitNotify(){
	new Thread(()->{
		synchronized (obj){
			while (!t2runned){
				try {
					obj.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(1);
		}
	}, "t1").start();
	
	new Thread(()->{
		System.out.println(2);
		synchronized (obj){
			t2runned = true;
			obj.notifyAll();
		}
	}, "t2").start();
	
}

Park-Unpark版本
可以看到,实现上很麻烦:
首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该wait
第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决此问题
最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个
可以使用 LockSupport 类的 park 和 unpark 来简化上面的题目:

public static void testFixOrderParkUnpark() {
	Thread t1 = new Thread(() -> {
		LockSupport.park();
		System.out.println(1);
	}, "t1");
	t1.start();
	
	new Thread(() -> {
		System.out.println(2);
		LockSupport.unpark(t1);
        }, "t2").start();
}

3.12.2 交替输出

线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现
wait-notify版本

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);
                this.flag = nextFlag;
                this.notifyAll();
            }
        }
    }

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

}

Lock 条件变量版

class AwaitSignal extends ReentrantLock {

    private int loopNumber;

    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    public void start(Condition first) {
        this.lock();
        try {
            first.signal();
        } finally {
            this.unlock();
        }
    }

    public void print(String str, Condition curr, Condition next) {
        for (int i = 0; i < loopNumber; i++) {
            this.lock();
            try {
                curr.await();
                System.out.print(str);
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                this.unlock();
            }
        }
    }

    public static void main(String[] args) {
        AwaitSignal awaitSignal = new AwaitSignal(5);
        Condition aWaitSet = awaitSignal.newCondition();
        Condition bWaitSet = awaitSignal.newCondition();
        Condition cWaitSet = awaitSignal.newCondition();

        new Thread(() -> {
            awaitSignal.print("a", aWaitSet, bWaitSet);
        }).start();

        new Thread(() -> {
            awaitSignal.print("b", bWaitSet, cWaitSet);
        }).start();

        new Thread(() -> {
            awaitSignal.print("c", cWaitSet, aWaitSet);
        }).start();

        awaitSignal.start(aWaitSet);
    }

}

Park-Unpark版

class SyncPark {
    private final 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());
        }
    }

    public void start() {
        for (Thread thread : threads) {
            thread.start();
        }
        LockSupport.unpark(threads[0]);
    }

    private Thread nextThread() {
        Thread thread = Thread.currentThread();
        int index = 0;
        // 获取当前线程下表
        for (int i = 0; i < threads.length; i++) {
            if (thread == threads[i]) {
                index = i;
                break;
            }
        }
        if (index < threads.length - 1) {
            return threads[index + 1];
        }
        return threads[0];
    }

    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");
        });
        syncPark.setThreads(t1, t2, t3);
        syncPark.start();
    }

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

层巅余落日

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值