Java JUC

Java JUC

1、Java JUC 简介

在 Java 5.0 提供了 java.util.concurrent (简称 JUC )包,在此包中增加了在并发编程中很常用 的实用工具类,用于定义类似于线程的自定义子 系统,包括线程池、异步 IO 和轻量级任务框架。 提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现等。

2、volatile 关键字-内存可见性

内存可见性问题

  1. Java 内存模型规定,对于多个线程共享的变量,存储在主内存当中。
  2. 每个线程都有自己独立的工作内存,并且线程只能访问自己的工作内存,不可以访问其它线程的工作内存。
  3. 工作内存中保存了主内存中共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中,其 JVM 模型大致如下图。

image-20200804214353669


JVM 模型规定:

  1. 线程对共享变量的所有操作必须在自己的内存中进行,不能直接从主内存中读写。
  2. 不同线程之间无法直接访问其它线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
  3. 这样的规定可能导致得到后果是:线程对共享变量的修改没有即时更新到主内存,或者线程没能够即时将共享变量的最新值同步到工作内存中,从而使得线程在使用共享变量的值时,该值并不是最新的。这就引出了内存可见性。

内存可见性(Memory Visibility)

  1. 内存可见性(Memory Visibility)是指当某个线程正在使用对象状态,而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。
  2. 可见性错误是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。

代码示例

  • 代码
public class TestVolatile {

	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo();
		new Thread(td).start();

		while (true) {
			if (td.isFlag()) {
				System.out.println("------------------");
				break;
			}
		}

	}

}

class ThreadDemo implements Runnable {

	private  boolean flag = false;

	@Override
	public void run() {

		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
		}

		flag = true;

		System.out.println("flag=" + isFlag());

	}

	public boolean isFlag() {
		return flag;
	}

	public void setFlag(boolean flag) {
		this.flag = flag;
	}

}
  • 虽然 td 线程已经将 flag 标志位修改为 true,但是由于 main 线程中并没有从主存中更新 flag 变量的值,导致程序无法停止

image-20200804214931318

解决办法

  1. 加同步锁:加同步锁会强制 main 线程去更新 flag 标志位的值
  2. 或者使用 volatile 关键字

volatile 关键字

  1. Java 提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其它线程。可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同。
  2. 当把共享变量声明为 volatile 类型后,线程对该变量修改时会将该变量的值立即刷新回主内存,同时会使其它线程中缓存的该变量无效,从而其它线程在读取该值时会从主内中重新读取该值(参考缓存一致性)。因此在读取 volatile 类型的变量时总是会返回最新写入的值。
  3. volatile屏蔽掉了JVM中必要的代码优化(指令重排序),所以在效率上比较低

代码示例

  • 代码:在 flag 变量前加上 volatile 关键字
/*
 * 一、volatile 关键字:当多个线程进行操作共享数据时,可以保证内存中的数据可见。
 * 					  相较于 synchronized 是一种较为轻量级的同步策略。
 * 
 * 注意:
 * 1. volatile 不具备“互斥性”
 * 2. volatile 不能保证变量的“原子性”
 */
public class TestVolatile {

	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo();
		new Thread(td).start();

		while (true) {
			if (td.isFlag()) {
				System.out.println("------------------");
				break;
			}
		}

	}

}

class ThreadDemo implements Runnable {

	private volatile boolean flag = false;

	@Override
	public void run() {

		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
		}

		flag = true;

		System.out.println("flag=" + isFlag());

	}

	public boolean isFlag() {
		return flag;
	}

	public void setFlag(boolean flag) {
		this.flag = flag;
	}

}
  • 程序成功停止
flag=true
------------------

volatile关键字最主要的作用是:

  1. 保证变量的内存可见性
  2. 局部阻止重排序的发生

可以将 volatile 看做一个轻量级的锁,但是又与 锁有些不同:

  1. 对于多线程,不是一种互斥关系
  2. 不能保证变量状态的“原子性操作“

3、原子变量-CAS算法

i++ 原子性问题

i++ 的步骤

i++ 的原子性问题:i++ 的操作实际上分为三个步骤:读-改-写

  1. 先将 i 的值赋值给一个临时变量 temp
  2. 将 i 的值自增 1
  3. 将临时变量 temp 的值再赋值给 i
int i = 10;
i = i++; //10

// 以上操作被分解为如下三个步骤
int temp = i;
i = i + 1;
i = temp;

代码演示原子性问题

  • 代码
public class TestAtomicDemo {

	public static void main(String[] args) {
		AtomicDemo ad = new AtomicDemo();
		
		for (int i = 0; i < 10; i++) {
			new Thread(ad).start();
		}
	}
}

class AtomicDemo implements Runnable{
	
	private volatile int serialNumber = 0;
	
	@Override
	public void run() {	
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
		}

		System.out.println(Thread.currentThread().getName() + ":" + getSerialNumber());
	}

	public int getSerialNumber() {
		return serialNumber++;
	}
    
}
  • 程序运行:出现了重复数据
Thread-9:1
Thread-2:2
Thread-4:0
Thread-3:3
Thread-6:4
Thread-0:1
Thread-7:2
Thread-1:6
Thread-5:5
Thread-8:0

分析问题

  1. 如果两个线程:线程 A 和线程 B ,同时从主存存中拿到了 serialNumber 的值
  2. 线程 A 比较牛批,他先将 serialNumber 的值加 1 ,然后写回主存中
  3. 线程 B 是个懒惰胚,他将在自己缓存中的 serialNumber 的值加 1 ,然后写回主存中,所以就出现了重复数据
  4. 问题的原因:serialNumber++ 可拆分成更细的操作,如果把 serialNumber++ 操作当做一个整体,这就叫原子操作

  • 代码
/*
 * 一、i++ 的原子性问题:i++ 的操作实际上分为三个步骤“读-改-写”
 * 		  int i = 10;
 * 		  i = i++; //10
 * 
 * 		  int temp = i;
 * 		  i = i + 1;
 * 		  i = temp;
 * 
 * 二、原子变量:在 java.util.concurrent.atomic 包下提供了一些原子变量。
 * 		1. volatile 保证内存可见性
 * 		2. CAS(Compare-And-Swap) 算法保证数据变量的原子性
 * 			CAS 算法是硬件对于并发操作的支持
 * 			CAS 包含了三个操作数:
 * 			①内存值  V
 * 			②预估值  A
 * 			③更新值  B
 * 			当且仅当 V == A 时, V = B; 否则,不会执行任何操作。
 */
public class TestAtomicDemo {

	public static void main(String[] args) {
		AtomicDemo ad = new AtomicDemo();
		
		for (int i = 0; i < 10; i++) {
			new Thread(ad).start();
		}
	}
	
}

class AtomicDemo implements Runnable{
	private AtomicInteger serialNumber = new AtomicInteger(0);

	@Override
	public void run() {	
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
		}

		System.out.println(Thread.currentThread().getName() + ":" + getSerialNumber());
	}

	public int getSerialNumber() {
		return serialNumber.getAndIncrement();
	}	
}
  • 程序运行结果
Thread-1:2
Thread-2:6
Thread-9:5
Thread-0:1
Thread-6:4
Thread-5:0
Thread-8:7
Thread-4:9
Thread-7:8
Thread-3:3

原子变量

  1. 实现全局自增id最简单有效的方式是什么?java.util.concurrent.atomic包定义了一些常见类型的原子变量。这些原子变量为我们提供了一种操作单一变量无锁(lock-free)的线程安全(thread-safe)方式。

  2. 实际上该包下面的类为我们提供了类似volatile变量的特性(保证内存可见性),同时还提供了诸如boolean compareAndSet(expectedValue, updateValue)的功能。

  3. 不使用锁实现线程安全听起来似乎很不可思议,这其实是通过CPU的compare and swap指令实现的,由于硬件指令支持当然不需要加锁了。

  4. 实现原子操作的核心方法为:boolean compareAndSet(expectedValue, updateValue)


原子变量的使用

  1. 原子变量类的命名类似于AtomicXxx,例如,AtomicInteger类用于表示一个int变量。
    • AtomicBoolean 、 AtomicInteger 、 AtomicLong 、 AtomicReference
    • AtomicIntegerArray 、 AtomicLongArray
    • AtomicMarkableReference
    • AtomicReferenceArray
    • AtomicStampedReference
  2. 类 AtomicBoolean、 AtomicInteger、 AtomicLong 和 AtomicReference 的实例各自提供对相应类型单个变量的访问和更新。每个类也为该类型提供适当的实用工具方法。
  3. AtomicIntegerArray、 AtomicLongArray 和 AtomicReferenceArray 类进一步扩展了原子操作,对这些类型的数组提供了支持。这些类在为其数组元素提供 volatile 访问语义方面也引人注目,这对于普通数组来说是不受支持的。

CAS 算法

  1. CAS (Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。
  2. CAS 是一种无锁的非阻塞算法的实现。
  3. CAS 包含了 3 个操作数:
    • 需要读写的内存值 V
    • 进行比较的值 A
    • 拟写入的新值 B
  4. 当且仅当 V 的值等于 A 时, CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作。

image-20200804225304126


代码举例

  • JAVA 模拟 CAS 算法
  • 读取内存值 get() 、比较值 compareAndSwap() 、设置新值 compareAndSet() 这三个方法分别独立,互不干扰
/*
 * 模拟 CAS 算法
 */
public class TestCompareAndSwap {

	public static void main(String[] args) {
		final CompareAndSwap cas = new CompareAndSwap();
		
		for (int i = 0; i < 10; i++) {
			new Thread(new Runnable() {				
				@Override
				public void run() {
					int expectedValue = cas.get();
					boolean b = cas.compareAndSet(expectedValue, (int)(Math.random() * 101));
					System.out.println(b);
				}
			}).start();
		}
		
	}
	
}

class CompareAndSwap{
	private int value;
	
	//获取内存值
	public synchronized int get(){
		return value;
	}
	
	//比较
	public synchronized int compareAndSwap(int expectedValue, int newValue){
		int oldValue = value;
		
		if(oldValue == expectedValue){
			this.value = newValue;
		}
		
		return oldValue;
	}
	
	//设置
	public synchronized boolean compareAndSet(int expectedValue, int newValue){
		return expectedValue == compareAndSwap(expectedValue, newValue);
	}
}

  • 程序运行结果
false
true
true
false
false
false
false
false
false
true

4、ConcurrentHashMap 锁分段机制

ConcurrentHashMap 简介

  1. Java 5.0 在 java.util.concurrent 包中提供了多种并发容器类来改进同步容器的性能。
  2. ConcurrentHashMap 同步容器类是Java 5 增加的一个线程安全的哈希表。对与多线程的操作,介于 HashMap 与 Hashtable 之间。
  3. ConcurrentHashMap 内部采用“锁分段”机制替代 Hashtable 的独占锁。进而提高性能。
  4. 此包还提供了设计用于多线程上下文中的 Collection 实现:
    • 并发容器类:ConcurrentHashMap、 ConcurrentSkipListMap、 ConcurrentSkipListSet、
      CopyOnWriteArrayList 和 CopyOnWriteArraySet。
    • 当期望许多线程访问一个给定 collection 时, ConcurrentHashMap 通常优于同步的 HashMap,ConcurrentSkipListMap 通常优于同步的 TreeMap。
    • 当期望的读数和遍历远远大于列表的更新数时, CopyOnWriteArrayList 优于同步的 ArrayList。

锁粒度与锁分段

锁粒度

  1. 减小锁粒度是指缩小锁定对象的范围,从而减小锁冲突的可能性,从而提高系统的并发能力。
  2. 减小锁粒度是一种削弱多线程锁竞争的有效手段,这种技术典型的应用是ConcurrentHashMap(高性能的HashMap)类的实现。
  3. 对于 HashMap 而言,最重要的两个方法是 get 与 set 方法,如果我们对整个 HashMap 加锁,可以得到线程安全的对象,但是加锁粒度太大(锁了整个 HashMap吗?)。
  4. Segment 的大小也被称为 ConcurrentHashMap 的并发度。

锁分段

  1. ConcurrentHashMap,它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下 一个 ConcurrentHashMap 被进一步细分为 16 个段,即为锁的并发度。

  2. 如果需要在 ConcurrentHashMap 中添加一个新的表项,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该表项应该存放在哪个段中,然后对该段加锁,并完成 put 操作。

  3. 在多线程环境中,如果多个线程同时进行put操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行。

测试 CopyOnWriteArrayList

  • 代码:使用 Collections 封装线程安全的 ArrayList 类
public class TestCopyOnWriteArrayList {

	public static void main(String[] args) {
		HelloThread ht = new HelloThread();
		
		for (int i = 0; i < 10; i++) {
			new Thread(ht).start();
		}
	}
	
}

class HelloThread implements Runnable{
	
	private static List<String> list = Collections.synchronizedList(new ArrayList<String>());
		
	static{
		list.add("AA");
		list.add("BB");
		list.add("CC");
	}

	@Override
	public void run() {		
		Iterator<String> it = list.iterator();
		
		while(it.hasNext()){
			System.out.println(it.next());		
			list.add("AA");
		}
	
	}
	
}
  • 程序运行结果:并发修改异常
Exception in thread "Thread-7" Exception in thread "Thread-9" Exception in thread "Thread-1" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
	at java.util.ArrayList$Itr.next(ArrayList.java:851)
	at com.atguigu.juc.HelloThread.run(TestCopyOnWriteArrayList.java:44)
	at java.lang.Thread.run(Thread.java:748)

解决并发修改异常

  • 代码:使用 CopyOnWriteArrayList 类
/*
 * CopyOnWriteArrayList/CopyOnWriteArraySet : “写入并复制”
 * 注意:添加操作多时,效率低,因为每次添加时都会进行复制,开销非常的大。并发迭代操作多时可以选择。
 */
public class TestCopyOnWriteArrayList {

	public static void main(String[] args) {
		HelloThread ht = new HelloThread();
		
		for (int i = 0; i < 10; i++) {
			new Thread(ht).start();
		}
	}
	
}

class HelloThread implements Runnable{
		
	private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
	
	static{
		list.add("AA");
		list.add("BB");
		list.add("CC");
	}

	@Override
	public void run() {		
		Iterator<String> it = list.iterator();
		
		while(it.hasNext()){
			System.out.println(it.next());		
			list.add("AA");
		}
	
	}
	
}
  • 分析:为什么并发添加不会报错呢?
    • 这样不会报错,正常运行。因为在每次写入时,都会在底层完成一次复制,复制一份新的列表,然后再进行添加。每次写入都会复制。不会造成并发修改异常,但是效率较低。
    • 添加操作多时,效率低,因为每次添加时都会进行复制,开销非常的大。并发迭代操作多时可以选择。

5、CountDownLatch 闭锁

闭锁的概念

  1. CountDownLatch(闭锁):一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。CountDown(倒数)latch(锁)

  2. 闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活动直到其他活动都完成才继续执行:

    • 确保某个计算在其需要的所有资源都被初始化之后才继续执行;
    • 确保某个服务在其依赖的所有其他服务都已经启动之后才启动;
    • 等待直到某个操作所有参与者都准备就绪再继续执行。

CountDownLatch 使用步骤

  1. 使用用给定的计数初始化 CountDownLatch
  2. 调用了 countDown() 方法,计数将减 1
  3. 在当前计数到达零之前,await 方法会一直受阻塞
  4. 计数到达零时,await 方法将不再阻塞,且计数无法被重置
  5. 适用于 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。

CountDownLatch 代码示例

  • 代码:使用 CountDownLatch 保证并发的线程都执行完毕
/*
 * CountDownLatch :闭锁,在完成某些运算是,只有其他所有线程的运算全部完成,当前运算才继续执行
 */
public class TestCountDownLatch {

	public static void main(String[] args) {
		final CountDownLatch latch = new CountDownLatch(50);
		LatchDemo ld = new LatchDemo(latch);

		long start = System.currentTimeMillis();

		for (int i = 0; i < 50; i++) {
			new Thread(ld).start();
		}

		try {
			latch.await();
		} catch (InterruptedException e) {
		}

		long end = System.currentTimeMillis();

		System.out.println("耗费时间为:" + (end - start));
	}

}

class LatchDemo implements Runnable {

	private CountDownLatch latch;

	public LatchDemo(CountDownLatch latch) {
		this.latch = latch;
	}

	@Override
	public void run() {

		try {
			for (int i = 0; i < 50000; i++) {
				if (i % 2 == 0) {
					System.out.println(i);
				}
			}
		} finally {
			latch.countDown(); // 如果 latch 不减为 0 ,主线程无法停止,所以这段代码必须执行,将其放在 finally 中
		}

	}

}
  • 并发执行 50 个线程的耗时
耗费时间为:6766

6、实现 Callable 接口

Callable 接口

  1. Callable 接口类似于 Runnable,两者都可以指定多线程操作的执行逻辑。
  2. 相较于 Runnable ,Callable 接口有返回结果,并且可以抛出经过检查的异常。
  3. Callable 需要依赖 FutureTask , FutureTask 也可以用作闭锁。

创建线程的四种方式

  1. 无返回:
    • 实现Runnable接口,重写run();
    • 继承Thread类,重写run();
  2. 有返回:
    • 实现Callable接口,重写call()
    • 利用线程池

Callable 代码示例

  • Callable 使用示例
  1. 创建一个类(ThreadDemo)实现 Callable 接口,并重写 call() 方法,返回计算结果
  2. 创建 FutureTask 类的实例,用于封装 ThreadDemo 类的实例
  3. 将 FutureTask 类的实例作为参数,传递给 Thread 类的构造器
  4. 调用 FutureTask 类的 get() 方法,实现闭锁,并获取计算结果
  • 代码
/*
 * 一、创建执行线程的方式三:实现 Callable 接口。 相较于实现 Runnable 接口的方式,方法可以有返回值,并且可以抛出异常。
 * 
 * 二、执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。  FutureTask 是  Future 接口的实现类
 */
public class TestCallable {

	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo();

		// 1.执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。
		FutureTask<Integer> result = new FutureTask<>(td);

		new Thread(result).start();

		// 2.接收线程运算后的结果
		try {
			Integer sum = result.get(); // FutureTask 可用于 闭锁
			System.out.println(sum);
		} catch (InterruptedException | ExecutionException e) {
			e.printStackTrace();
		}
	}

}

class ThreadDemo implements Callable<Integer> {

	@Override
	public Integer call() throws Exception {
		int sum = 0;

		for (int i = 0; i <= 100; i++) {
			sum += i;
		}

		return sum;
	}

}
  • 程序运行结果
5050
------------------------------------

7、Lock 同步锁

显示锁 Lock 简介

  1. 在 Java 5.0 之前,协调共享对象的访问时可以使用的机制只有 synchronized 和 volatile 。 Java 5.0 后增加了一些新的机制,但并不是一种替代内置锁的方法,而是当内置锁不适用时,作为一种可选择的高级功能。
  2. ReentrantLock 实现了 Lock 接口,并提供了与synchronized 相同的互斥性和内存可见性。但相较于 synchronized 提供了更高的处理锁的灵活性。

解决多线程安全问题的三种方式

  1. jdk 1.5 前:synchronized 隐式锁
    • 同步代码块
    • 同步方法
  2. jdk 1.5 后:
    • 同步锁 Lock:显式锁
    • 注意:是一个显示锁,需要通过 lock() 方法上锁,必须通过 unlock() 方法进行释放锁

Lock 代码示例

  • 代码:但凡执行了 lock() 方法,unlock() 方法就一定要执行,否则会造成死锁
/*
 * 一、用于解决多线程安全问题的方式:
 * 
 * synchronized:隐式锁
 * 1. 同步代码块
 * 
 * 2. 同步方法
 * 
 * jdk 1.5 后:
 * 3. 同步锁 Lock
 * 注意:是一个显示锁,需要通过 lock() 方法上锁,必须通过 unlock() 方法进行释放锁
 */
public class TestLock {
	
	public static void main(String[] args) {
		Ticket ticket = new Ticket();
		
		new Thread(ticket, "1号窗口").start();
		new Thread(ticket, "2号窗口").start();
		new Thread(ticket, "3号窗口").start();
	}

}

class Ticket implements Runnable{
	
	private int tick = 100;
	
	private Lock lock = new ReentrantLock();

	@Override
	public void run() {
		while(true){
			
			lock.lock(); //上锁
			
			try{
				if(tick > 0){
					try {
						Thread.sleep(200);
					} catch (InterruptedException e) {
					}
					
					System.out.println(Thread.currentThread().getName() + " 完成售票,余票为:" + --tick);
				}
			}finally{
				lock.unlock(); //必须执行, 因此放在finally中 释放锁
			}
		}
	}
	
}
  • 程序运行结果
1号窗口 完成售票,余票为:99
1号窗口 完成售票,余票为:98
1号窗口 完成售票,余票为:97
1号窗口 完成售票,余票为:96
1号窗口 完成售票,余票为:95
1号窗口 完成售票,余票为:94
1号窗口 完成售票,余票为:93
1号窗口 完成售票,余票为:92
1号窗口 完成售票,余票为:91
1号窗口 完成售票,余票为:90
1号窗口 完成售票,余票为:89
1号窗口 完成售票,余票为:88
1号窗口 完成售票,余票为:87
1号窗口 完成售票,余票为:86
1号窗口 完成售票,余票为:85
1号窗口 完成售票,余票为:84
1号窗口 完成售票,余票为:83
1号窗口 完成售票,余票为:82
1号窗口 完成售票,余票为:81
1号窗口 完成售票,余票为:80
1号窗口 完成售票,余票为:79
1号窗口 完成售票,余票为:78
1号窗口 完成售票,余票为:77
1号窗口 完成售票,余票为:76
2号窗口 完成售票,余票为:75
2号窗口 完成售票,余票为:74
2号窗口 完成售票,余票为:73
2号窗口 完成售票,余票为:72
2号窗口 完成售票,余票为:71
2号窗口 完成售票,余票为:70
2号窗口 完成售票,余票为:69
2号窗口 完成售票,余票为:68
2号窗口 完成售票,余票为:67
2号窗口 完成售票,余票为:66
2号窗口 完成售票,余票为:65
2号窗口 完成售票,余票为:64
2号窗口 完成售票,余票为:63
2号窗口 完成售票,余票为:62
2号窗口 完成售票,余票为:61
2号窗口 完成售票,余票为:60
2号窗口 完成售票,余票为:59
2号窗口 完成售票,余票为:58
2号窗口 完成售票,余票为:57
2号窗口 完成售票,余票为:56
2号窗口 完成售票,余票为:55
2号窗口 完成售票,余票为:54
2号窗口 完成售票,余票为:53
2号窗口 完成售票,余票为:52
2号窗口 完成售票,余票为:51
2号窗口 完成售票,余票为:50
2号窗口 完成售票,余票为:49
2号窗口 完成售票,余票为:48
2号窗口 完成售票,余票为:47
2号窗口 完成售票,余票为:46
2号窗口 完成售票,余票为:45
2号窗口 完成售票,余票为:44
2号窗口 完成售票,余票为:43
2号窗口 完成售票,余票为:42
2号窗口 完成售票,余票为:41
2号窗口 完成售票,余票为:40
2号窗口 完成售票,余票为:39
2号窗口 完成售票,余票为:38
2号窗口 完成售票,余票为:37
2号窗口 完成售票,余票为:36
2号窗口 完成售票,余票为:35
2号窗口 完成售票,余票为:34
2号窗口 完成售票,余票为:33
2号窗口 完成售票,余票为:32
2号窗口 完成售票,余票为:31
2号窗口 完成售票,余票为:30
2号窗口 完成售票,余票为:29
2号窗口 完成售票,余票为:28
2号窗口 完成售票,余票为:27
2号窗口 完成售票,余票为:26
2号窗口 完成售票,余票为:25
2号窗口 完成售票,余票为:24
2号窗口 完成售票,余票为:23
2号窗口 完成售票,余票为:22
2号窗口 完成售票,余票为:21
2号窗口 完成售票,余票为:20
2号窗口 完成售票,余票为:19
2号窗口 完成售票,余票为:18
2号窗口 完成售票,余票为:17
2号窗口 完成售票,余票为:16
2号窗口 完成售票,余票为:15
2号窗口 完成售票,余票为:14
2号窗口 完成售票,余票为:13
2号窗口 完成售票,余票为:12
2号窗口 完成售票,余票为:11
2号窗口 完成售票,余票为:10
2号窗口 完成售票,余票为:9
2号窗口 完成售票,余票为:8
2号窗口 完成售票,余票为:7
2号窗口 完成售票,余票为:6
2号窗口 完成售票,余票为:5
2号窗口 完成售票,余票为:4
2号窗口 完成售票,余票为:3
2号窗口 完成售票,余票为:2
2号窗口 完成售票,余票为:1
2号窗口 完成售票,余票为:0

8、生产者与消费者

生产者消费者问题

  • 代码
/*
 * 生产者和消费者案例
 */
public class TestProductorAndConsumer {

	public static void main(String[] args) {
		Clerk clerk = new Clerk();

		Productor pro = new Productor(clerk);
		Consumer cus = new Consumer(clerk);

		new Thread(pro, "生产者 A").start();
		new Thread(cus, "消费者 B").start();
	}

}

//店员
class Clerk {
	private int product = 0;

	// 进货
	public synchronized void get() {
		if (product >= 10) {
			System.out.println("产品已满!");
		} else {
			System.out.println(Thread.currentThread().getName() + " : " + ++product);
		}
	}

	// 卖货
	public synchronized void sale() {
		if (product <= 0) {
			System.out.println("缺货!");
		} else {
			System.out.println(Thread.currentThread().getName() + " : " + --product);
		}
	}
}

//生产者
class Productor implements Runnable {
	private Clerk clerk;

	public Productor(Clerk clerk) {
		this.clerk = clerk;
	}

	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			clerk.get();
		}
	}
}

//消费者
class Consumer implements Runnable {
	private Clerk clerk;

	public Consumer(Clerk clerk) {
		this.clerk = clerk;
	}

	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			clerk.sale();
		}
	}
}
  • 程序运行结果
生产者 A : 1
生产者 A : 2
生产者 A : 3
消费者 B : 2
消费者 B : 1
消费者 B : 0
缺货!
缺货!
缺货!
缺货!
缺货!
缺货!
缺货!
缺货!
缺货!
缺货!
缺货!
缺货!
缺货!
缺货!
缺货!
缺货!
缺货!
生产者 A : 1
生产者 A : 2
生产者 A : 3
生产者 A : 4
生产者 A : 5
生产者 A : 6
生产者 A : 7
生产者 A : 8
生产者 A : 9
生产者 A : 10
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!

问题分析:

  1. 生产者过快:可能出现数据丢失(生产之后发现“仓库”已满,只能将当前生产的产品丢弃)
  2. 消费者过快:可能出现错误数据(从“仓库”里取产品,发现根本没有产品)

生产者消费者:使用等待唤醒解决生产过快或者消费过快的问题

  • 代码:使用 wait() 方法和 notifyAll() 方法实现等待唤醒
/*
 * 生产者和消费者案例
 */
public class TestProductorAndConsumer {

	public static void main(String[] args) {
		Clerk clerk = new Clerk();

		Productor pro = new Productor(clerk);
		Consumer cus = new Consumer(clerk);

		new Thread(pro, "生产者 A").start();
		new Thread(cus, "消费者 B").start();
	}

}

//店员
class Clerk {
	private int product = 0;

	// 进货
	public synchronized void get() {
		if (product >= 10) {
			System.out.println("产品已满!");
			// 生产者已满,则停止生产,等待消费者唤醒
			try {
				this.wait();
			} catch (InterruptedException e) {
			}
		} else {
			System.out.println(Thread.currentThread().getName() + " : " + ++product);
			// 生产了新产品,唤醒消费者
			this.notifyAll();
		}
	}

	// 卖货
	public synchronized void sale() {
		if (product <= 0) {
			System.out.println("缺货!");
			// 生产者缺货,则停止消费,等待生产者唤醒
			try {
				this.wait();
			} catch (InterruptedException e) {
			}
		} else {
			System.out.println(Thread.currentThread().getName() + " : " + --product);
			// 消费了产品,唤醒生产者
			this.notifyAll();
		}
	}
}

//生产者
class Productor implements Runnable {
	private Clerk clerk;

	public Productor(Clerk clerk) {
		this.clerk = clerk;
	}

	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			clerk.get();
		}
	}
}

//消费者
class Consumer implements Runnable {
	private Clerk clerk;

	public Consumer(Clerk clerk) {
		this.clerk = clerk;
	}

	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			clerk.sale();
		}
	}
}
  • 程序运行结果
生产者 A : 1
生产者 A : 2
生产者 A : 3
生产者 A : 4
生产者 A : 5
生产者 A : 6
消费者 B : 5
消费者 B : 4
消费者 B : 3
消费者 B : 2
消费者 B : 1
消费者 B : 0
缺货!
生产者 A : 1
生产者 A : 2
生产者 A : 3
生产者 A : 4
生产者 A : 5
生产者 A : 6
生产者 A : 7
生产者 A : 8
生产者 A : 9
生产者 A : 10
产品已满!
消费者 B : 9
消费者 B : 8
消费者 B : 7
消费者 B : 6
消费者 B : 5
消费者 B : 4
消费者 B : 3
消费者 B : 2
消费者 B : 1
消费者 B : 0
缺货!
生产者 A : 1
生产者 A : 2
生产者 A : 3
消费者 B : 2
消费者 B : 1
  • 结果分析:
    • 生产者已满,则停止生产,等待消费者消费产品
    • 生产者缺货,则停止消费,等待生产者生产产品

上述代码中存在的问题

  • 代码
    • 将生产者的上限改为 1 ,即 product >= 1 时表示生产者已满
    • 降低生产者的生产速度
/*
 * 生产者和消费者案例
 */
public class TestProductorAndConsumer {

	public static void main(String[] args) {
		Clerk clerk = new Clerk();

		Productor pro = new Productor(clerk);
		Consumer cus = new Consumer(clerk);

		new Thread(pro, "生产者 A").start();
		new Thread(cus, "消费者 B").start();
	}

}

//店员
class Clerk {
	private int product = 0;

	// 进货
	public synchronized void get() {
		if (product >= 1) {
			System.out.println("产品已满!");
			// 生产者已满,则停止生产,等待消费者唤醒
			try {
				this.wait();
			} catch (InterruptedException e) {
			}
		} else {
			System.out.println(Thread.currentThread().getName() + " : " + ++product);
			// 生产了新产品,唤醒消费者
			this.notifyAll();
		}
	}

	// 卖货
	public synchronized void sale() {
		if (product <= 0) {
			System.out.println("缺货!");
			// 生产者缺货,则停止消费,等待生产者唤醒
			try {
				this.wait();
			} catch (InterruptedException e) {
			}
		} else {
			System.out.println(Thread.currentThread().getName() + " : " + --product);
			// 消费了产品,唤醒生产者
			this.notifyAll();
		}
	}
}

//生产者
class Productor implements Runnable {
	private Clerk clerk;

	public Productor(Clerk clerk) {
		this.clerk = clerk;
	}

	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
			}
			clerk.get();
		}
	}
}

//消费者
class Consumer implements Runnable {
	private Clerk clerk;

	public Consumer(Clerk clerk) {
		this.clerk = clerk;
	}

	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			clerk.sale();
		}
	}
}
  • 程序无法停止,从运行结果可以看出,生产者线程生产了一个产品,消费者并没有消费

image-20200805222250336

缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
生产者 A : 1
产品已满!

分析:

  • 现在生产者比消费者者慢,消费者过来,看到没货,就在那儿等着,可以等到产品生产出来,没有使用就走了。。。可以看到上述结果中消费者并没有消费够 20 次
// 卖货
public synchronized void sale() {
	if (product <= 0) {
		System.out.println("缺货!");
		// 生产者缺货,则停止消费,等待生产者唤醒
		try {
			this.wait();
		} catch (InterruptedException e) {
		}
	} else {
		System.out.println(Thread.currentThread().getName() + " : " + --product);
		// 消费了产品,唤醒生产者
		this.notifyAll();
	}
}
  • 当消费者 wait() 时,生产者生产了一个产品之后,将其唤醒后,消费者都没消费,就跑了,这问题出在 else 身上

解决上述代码中的问题

  • 代码:删除 else ,让消费者唤醒后,将产品消费了再走~~~,钱都付了,哪有不吃的道理
/*
 * 生产者和消费者案例
 */
public class TestProductorAndConsumer {

	public static void main(String[] args) {
		Clerk clerk = new Clerk();

		Productor pro = new Productor(clerk);
		Consumer cus = new Consumer(clerk);

		new Thread(pro, "生产者 A").start();
		new Thread(cus, "消费者 B").start();
	}

}

//店员
class Clerk {
	private int product = 0;

	// 进货
	public synchronized void get() {
		if (product >= 1) {
			System.out.println("产品已满!");
			// 生产者已满,则停止生产,等待消费者唤醒
			try {
				this.wait();
			} catch (InterruptedException e) {
			}
		}
		System.out.println(Thread.currentThread().getName() + " : " + ++product);
		// 生产了新产品,唤醒消费者
		this.notifyAll();
	}

	// 卖货
	public synchronized void sale() {
		if (product <= 0) {
			System.out.println("缺货!");
			// 生产者缺货,则停止消费,等待生产者唤醒
			try {
				this.wait();
			} catch (InterruptedException e) {
			}
		}
		System.out.println(Thread.currentThread().getName() + " : " + --product);
		// 消费了产品,唤醒生产者
		this.notifyAll();
	}
}

//生产者
class Productor implements Runnable {
	private Clerk clerk;

	public Productor(Clerk clerk) {
		this.clerk = clerk;
	}

	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
			}
			clerk.get();
		}
	}
}

//消费者
class Consumer implements Runnable {
	private Clerk clerk;

	public Consumer(Clerk clerk) {
		this.clerk = clerk;
	}

	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			clerk.sale();
		}
	}
}
  • 程序运行结果

image-20200805223435433

缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 B : 0

生产者消费者:虚假唤醒

  • 代码:再来一对生产者消费者线程
public static void main(String[] args) {
    Clerk clerk = new Clerk();

    Productor pro = new Productor(clerk);
    Consumer cus = new Consumer(clerk);

    new Thread(pro, "生产者 A").start();
    new Thread(cus, "消费者 B").start();

    new Thread(pro, "生产者 C").start();
    new Thread(cus, "消费者 D").start();
}
  • 程序运行结果
缺货!
缺货!
生产者 A : 1
消费者 D : 0
缺货!
消费者 B : -1
缺货!
生产者 C : 0
消费者 B : -1
缺货!
消费者 D : -2
缺货!
消费者 B : -3
缺货!
消费者 D : -4
缺货!
消费者 B : -5
缺货!
消费者 D : -6
缺货!
消费者 B : -7
缺货!
消费者 D : -8
缺货!
消费者 B : -9
缺货!
消费者 D : -10
缺货!
消费者 B : -11
缺货!
消费者 D : -12
缺货!
消费者 B : -13
缺货!
消费者 D : -14
缺货!
消费者 B : -15

分析原因:

  • 消费者消费的速度比生产者生产的速度快,这就说明同时可能会有好几个消费者进行 if 条件中,等待着生产者生产产品
  • 生产者生产产品之后,唤醒所有消费者,好家伙,本来生产者只生产了一个产品,消费者才不管你这么多,他蛮不讲理,唤醒之后有起床气,必须要消费
  • 这其实就叫虚假唤醒,解决办法:唤醒之后再做一次判断
// 卖货
public synchronized void sale() {
	if (product <= 0) {
		System.out.println("缺货!");
		// 生产者缺货,则停止消费,等待生产者唤醒
		try {
			this.wait();
		} catch (InterruptedException e) {
		}
	}
	System.out.println(Thread.currentThread().getName() + " : " + --product);
	// 消费了产品,唤醒生产者
	this.notifyAll();
}

生产者消费者:最终版

  • 先来看看 API 文档

image-20200805223753604

  • 代码:将 if 改为 while,每次生产者、消费者被唤醒之后,重新判断是否应该继续生产或者消费
/*
 * 生产者和消费者案例
 */
public class TestProductorAndConsumer {

	public static void main(String[] args) {
		Clerk clerk = new Clerk();

		Productor pro = new Productor(clerk);
		Consumer cus = new Consumer(clerk);

		new Thread(pro, "生产者 A").start();
		new Thread(cus, "消费者 B").start();
		
		new Thread(pro, "生产者 C").start();
		new Thread(cus, "消费者 D").start();
	}

}

//店员
class Clerk {
	private int product = 0;

	// 进货
	public synchronized void get() {
		while (product >= 1) {
			System.out.println("产品已满!");
			// 生产者已满,则停止生产,等待消费者唤醒
			try {
				this.wait();
			} catch (InterruptedException e) {
			}
		}
		System.out.println(Thread.currentThread().getName() + " : " + ++product);
		// 生产了新产品,唤醒消费者
		this.notifyAll();
	}

	// 卖货
	public synchronized void sale() {
		while (product <= 0) {
			System.out.println("缺货!");
			// 生产者缺货,则停止消费,等待生产者唤醒
			try {
				this.wait();
			} catch (InterruptedException e) {
			}
		}
		System.out.println(Thread.currentThread().getName() + " : " + --product);
		// 消费了产品,唤醒生产者
		this.notifyAll();
	}
}

//生产者
class Productor implements Runnable {
	private Clerk clerk;

	public Productor(Clerk clerk) {
		this.clerk = clerk;
	}

	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
			}
			clerk.get();
		}
	}
}

//消费者
class Consumer implements Runnable {
	private Clerk clerk;

	public Consumer(Clerk clerk) {
		this.clerk = clerk;
	}

	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			clerk.sale();
		}
	}
}
  • 哦了~~~
缺货!
缺货!
生产者 A : 1
消费者 D : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 C : 1
消费者 D : 0
缺货!
缺货!
生产者 A : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 D : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 D : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 C : 1
消费者 D : 0
缺货!
缺货!
生产者 A : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 D : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 C : 1
消费者 D : 0
缺货!
缺货!
生产者 A : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 D : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 D : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 D : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 D : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 C : 1
消费者 D : 0
缺货!
缺货!
生产者 A : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 D : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 D : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 C : 1
消费者 D : 0
缺货!
缺货!
生产者 A : 1
消费者 B : 0
缺货!
缺货!
生产者 C : 1
消费者 D : 0
缺货!
缺货!
生产者 A : 1
消费者 B : 0
缺货!
缺货!
生产者 C : 1
消费者 D : 0
缺货!
缺货!
生产者 A : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 D : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 D : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 D : 0
缺货!
生产者 C : 1
消费者 B : 0

9、Condition 控制线程通信

Condition 介绍

  1. Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。
  2. 为了避免兼容性问题, Condition 方法的名称与对应的 Object 版本中的不同。在 Condition 对象中,与 wait、 notify 和 notifyAll 方法对应的分别是await、 signal 和 signalAll。
  3. Condition 实例实质上被绑定到一个锁上。要为特定 Lock 实例获得 Condition 实例,请使用其 newCondition() 方法。

用我自己的话总结

  1. 在某些情况下,可使用显示锁 Lock 替代隐式锁 synchronized
  2. synchronized 可以锁住一个对象,在 synchronized (obj) {} 括号之间的代码,则称之为同步代码块
  3. synchronized 也可以加在实例方法上,表示锁住当前实例对象,相当于 synchronized (this) {}
  4. synchronized 也可以加在静态方法上,表示锁住当前 Class,相当于 synchronized (class) {}
  5. 在遇到线程通信问题时,synchronized 可以通过 obj.wait()、obj.notify()、obj.notifyAll() 方法控制线程通信
  6. Lock 实例可以调用 newCondition() 方法,获取 Condition 类的实例,其 await()、signal() 和 signalAll() 方法就相当于 Object 类的 wait()、notify() 和 notifyAll() 方法

Condition 解决生产着消费者问题

  • 代码:去掉方法上的 synchronized 关键字,使用 Lock 实现线程同步,使用 Condition 完成线程通信
/*
 * 生产者消费者案例:
 */
public class TestProductorAndConsumerForLock {

	public static void main(String[] args) {
		Clerk clerk = new Clerk();

		Productor pro = new Productor(clerk);
		Consumer con = new Consumer(clerk);

		new Thread(pro, "生产者 A").start();
		new Thread(con, "消费者 B").start();

		 new Thread(pro, "生产者 C").start();
		 new Thread(con, "消费者 D").start();
	}

}

class Clerk {
	private int product = 0;

	private Lock lock = new ReentrantLock();
	private Condition condition = lock.newCondition();

	// 进货
	public void get() {
		lock.lock();

		try {
			while (product >= 1) { // 为了避免虚假唤醒,应该总是使用在循环中。
				System.out.println("产品已满!");

				try {
					condition.await();
				} catch (InterruptedException e) {
				}

			}
			
			System.out.println(Thread.currentThread().getName() + " : " + ++product);
			condition.signalAll();
		} finally {
			lock.unlock();
		}

	}

	// 卖货
	public void sale() {
		lock.lock();

		try {
			while (product <= 0) {
				System.out.println("缺货!");

				try {
					condition.await();
				} catch (InterruptedException e) {
				}
			}

			System.out.println(Thread.currentThread().getName() + " : " + --product);
			condition.signalAll();
		} finally {
			lock.unlock();
		}
	}
}

// 生产者
class Productor implements Runnable {

	private Clerk clerk;

	public Productor(Clerk clerk) {
		this.clerk = clerk;
	}

	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

			clerk.get();
		}
	}
}

// 消费者
class Consumer implements Runnable {

	private Clerk clerk;

	public Consumer(Clerk clerk) {
		this.clerk = clerk;
	}

	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			clerk.sale();
		}
	}

}
  • 程序运行结果
缺货!
缺货!
生产者 C : 1
产品已满!
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
产品已满!
消费者 B : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 B : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 B : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 B : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 B : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 B : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 B : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 B : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
缺货!
生产者 A : 1
消费者 B : 0
缺货!
缺货!
生产者 C : 1
消费者 B : 0
缺货!
生产者 A : 1
消费者 D : 0
缺货!
生产者 C : 1
消费者 D : 0
缺货!
生产者 A : 1
消费者 D : 0
缺货!
生产者 C : 1
消费者 D : 0
缺货!
生产者 A : 1
消费者 D : 0
缺货!
生产者 C : 1
消费者 D : 0
缺货!
生产者 A : 1
消费者 D : 0
缺货!
生产者 C : 1
消费者 D : 0
缺货!
生产者 A : 1
消费者 D : 0
缺货!
生产者 C : 1
消费者 D : 0
缺货!
生产者 A : 1
消费者 D : 0
缺货!
生产者 C : 1
消费者 D : 0
缺货!
生产者 A : 1
消费者 D : 0
缺货!
生产者 C : 1
消费者 D : 0
缺货!
生产者 A : 1
消费者 D : 0
缺货!
生产者 C : 1
消费者 D : 0
缺货!
生产者 A : 1
消费者 D : 0
缺货!
生产者 C : 1
消费者 D : 0
缺货!
生产者 A : 1
消费者 D : 0
缺货!
生产者 C : 1
消费者 D : 0

10、线程按序交替

面试题:线程交替打印

  1. 题目描述:编写一个程序,开启 3 个线程,这三个线程的 ID 分别为 A、 B、 C,每个线程将自己的 ID 在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。如: ABCABCABC…… 依次递归
  2. 我们来点不一样的:我们控制每个线程打印字符的次数,每个线程打印 3 次:AAABBBCCC…

代码示例

  • 代码:使用 Lock 和 Condition 控制线程通信
    • condition1 控制线程 A 的休眠与唤醒
    • condition2 控制线程 B 的休眠与唤醒
    • condition3 控制线程 C 的休眠与唤醒
/*
 * 编写一个程序,开启 3 个线程,这三个线程的 ID 分别为 A、B、C,每个线程将自己的 ID 在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。
 *	如:ABCABCABC…… 依次递归
 */
public class TestABCAlternate {

	public static void main(String[] args) {
		AlternateDemo ad = new AlternateDemo();

		new Thread(new Runnable() {
			@Override
			public void run() {

				for (int i = 1; i <= 20; i++) {
					ad.loopA(i);
				}

			}
		}, "A").start();

		new Thread(new Runnable() {
			@Override
			public void run() {

				for (int i = 1; i <= 20; i++) {
					ad.loopB(i);
				}

			}
		}, "B").start();

		new Thread(new Runnable() {
			@Override
			public void run() {

				for (int i = 1; i <= 20; i++) {
					ad.loopC(i);

					System.out.println("-----------------------------------");
				}

			}
		}, "C").start();
	}

}

class AlternateDemo {

	private int number = 1; // 当前正在执行线程的标记
	private int singleLoopCount = 1; // 循环计数器

	private Lock lock = new ReentrantLock();
	private Condition condition1 = lock.newCondition();
	private Condition condition2 = lock.newCondition();
	private Condition condition3 = lock.newCondition();

	/**
	 * @param totalLoop : 循环第几轮
	 */
	public void loopA(int totalLoop) {
		lock.lock();

		try {
			// 1. 判断
			if (number != 1) {
				condition1.await();
			}

			// 2. 打印
			for (int i = 1; i <= singleLoopCount; i++) {
				System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop);
			}

			// 3. 唤醒
			number = 2;
			condition2.signal();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void loopB(int totalLoop) {
		lock.lock();

		try {
			// 1. 判断
			if (number != 2) {
				condition2.await();
			}

			// 2. 打印
			for (int i = 1; i <= singleLoopCount; i++) {
				System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop);
			}

			// 3. 唤醒
			number = 3;
			condition3.signal();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void loopC(int totalLoop) {
		lock.lock();

		try {
			// 1. 判断
			if (number != 3) {
				condition3.await();
			}

			// 2. 打印
			for (int i = 1; i <= singleLoopCount; i++) {
				System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop);
			}

			// 3. 唤醒
			number = 1;
			condition1.signal();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

}
  • 程序运行结果
A	1	1
B	1	1
C	1	1
-----------------------------------
A	1	2
B	1	2
C	1	2
-----------------------------------
A	1	3
B	1	3
C	1	3
-----------------------------------
A	1	4
B	1	4
C	1	4
-----------------------------------
A	1	5
B	1	5
C	1	5
-----------------------------------
A	1	6
B	1	6
C	1	6
-----------------------------------
A	1	7
B	1	7
C	1	7
-----------------------------------
A	1	8
B	1	8
C	1	8
-----------------------------------
A	1	9
B	1	9
C	1	9
-----------------------------------
A	1	10
B	1	10
C	1	10
-----------------------------------
A	1	11
B	1	11
C	1	11
-----------------------------------
A	1	12
B	1	12
C	1	12
-----------------------------------
A	1	13
B	1	13
C	1	13
-----------------------------------
A	1	14
B	1	14
C	1	14
-----------------------------------
A	1	15
B	1	15
C	1	15
-----------------------------------
A	1	16
B	1	16
C	1	16
-----------------------------------
A	1	17
B	1	17
C	1	17
-----------------------------------
A	1	18
B	1	18
C	1	18
-----------------------------------
A	1	19
B	1	19
C	1	19
-----------------------------------
A	1	20
B	1	20
C	1	20
-----------------------------------

我的代码

  • 代码:线程 A B C 共用一个 condition 进行通信,使用 while 循环避免虚假唤醒
/*
 * 编写一个程序,开启 3 个线程,这三个线程的 ID 分别为 A、B、C,每个线程将自己的 ID 在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。
 *	如:ABCABCABC…… 依次递归
 */
public class TestABCAlternate {

	public static void main(String[] args) {
		AlternateDemo ad = new AlternateDemo();

		new Thread(new Runnable() {
			@Override
			public void run() {

				for (int i = 1; i <= 20; i++) {
					ad.loopA(i);
				}

			}
		}, "A").start();

		new Thread(new Runnable() {
			@Override
			public void run() {

				for (int i = 1; i <= 20; i++) {
					ad.loopB(i);
				}

			}
		}, "B").start();

		new Thread(new Runnable() {
			@Override
			public void run() {

				for (int i = 1; i <= 20; i++) {
					ad.loopC(i);

					System.out.println("-----------------------------------");
				}

			}
		}, "C").start();
	}

}

class AlternateDemo {

	private int number = 1; // 当前正在执行线程的标记
	private int singleLoopCount = 1; // 循环计数器

	private Lock lock = new ReentrantLock();
	private Condition condition = lock.newCondition();

	/**
	 * @param totalLoop : 循环第几轮
	 */
	public void loopA(int totalLoop) {
		lock.lock();

		try {
			// 1. 判断
			while (number != 1) {
				condition.await();
			}

			// 2. 打印
			for (int i = 1; i <= singleLoopCount; i++) {
				System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop);
			}

			// 3. 唤醒
			number = 2;
			condition.signalAll();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void loopB(int totalLoop) {
		lock.lock();

		try {
			// 1. 判断
			while (number != 2) {
				condition.await();
			}

			// 2. 打印
			for (int i = 1; i <= singleLoopCount; i++) {
				System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop);
			}

			// 3. 唤醒
			number = 3;
			condition.signalAll();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void loopC(int totalLoop) {
		lock.lock();

		try {
			// 1. 判断
			while (number != 3) {
				condition.await();
			}

			// 2. 打印
			for (int i = 1; i <= singleLoopCount; i++) {
				System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop);
			}

			// 3. 唤醒
			number = 1;
			condition.signalAll();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

}
  • 程序运行结果
A	1	1
B	1	1
C	1	1
-----------------------------------
A	1	2
B	1	2
C	1	2
-----------------------------------
A	1	3
B	1	3
C	1	3
-----------------------------------
A	1	4
B	1	4
C	1	4
-----------------------------------
A	1	5
B	1	5
C	1	5
-----------------------------------
A	1	6
B	1	6
C	1	6
-----------------------------------
A	1	7
B	1	7
C	1	7
-----------------------------------
A	1	8
B	1	8
C	1	8
-----------------------------------
A	1	9
B	1	9
C	1	9
-----------------------------------
A	1	10
B	1	10
C	1	10
-----------------------------------
A	1	11
B	1	11
C	1	11
-----------------------------------
A	1	12
B	1	12
C	1	12
-----------------------------------
A	1	13
B	1	13
C	1	13
-----------------------------------
A	1	14
B	1	14
C	1	14
-----------------------------------
A	1	15
B	1	15
C	1	15
-----------------------------------
A	1	16
B	1	16
C	1	16
-----------------------------------
A	1	17
B	1	17
C	1	17
-----------------------------------
A	1	18
B	1	18
C	1	18
-----------------------------------
A	1	19
B	1	19
C	1	19
-----------------------------------
A	1	20
B	1	20
C	1	20
-----------------------------------

11、ReadWriteLock 读写锁

读-写锁:ReadWriteLock

  1. ReadWriteLock 维护了一对相关的锁,一个用于只读操作, 另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。

  2. ReadWriteLock 读取操作通常不会改变共享资源,但执行写入操作时,必须独占方式来获取锁。对于读取操作占多数的数据结构。 ReadWriteLock 能提供比独占锁更高的并发性。而对于只读的数据结构,其中包含的不变性可以完全不需要考虑加锁操作。


我的总结

  1. 读-写锁 = 读锁 + 写锁
  2. 读锁可以多个线程并发持有,而写锁只能有一个线程独占
  3. 读-写锁 是一把乐观锁

代码示例

  • 代码:ReentrantReadWriteLock 类中有一把读锁和一把写锁,分别用于读操作和写操作
/*
 * 1. ReadWriteLock : 读写锁
 * 
 * 写写/读写 需要“互斥”
 * 读读 不需要互斥
 * 
 */
public class TestReadWriteLock {

	public static void main(String[] args) {
		ReadWriteLockDemo rw = new ReadWriteLockDemo();

		new Thread(new Runnable() {

			@Override
			public void run() {
				rw.set((int) (Math.random() * 101));
			}
		}, "Write:").start();

		for (int i = 0; i < 100; i++) {
			new Thread(new Runnable() {

				@Override
				public void run() {
					rw.get();
				}
			}, "Read:").start();
		}
	}

}

class ReadWriteLockDemo {

	private int number = 0;

	private ReadWriteLock lock = new ReentrantReadWriteLock();

	// 读
	public void get() {
		lock.readLock().lock(); // 上锁

		try {
			System.out.println(Thread.currentThread().getName() + " : " + number);
		} finally {
			lock.readLock().unlock(); // 释放锁
		}
	}

	// 写
	public void set(int number) {
		lock.writeLock().lock();

		try {
			System.out.println(Thread.currentThread().getName() + ":" + number);
			this.number = number;
		} finally {
			lock.writeLock().unlock();
		}
	}
}
  • 程序运行结果
Read: : 0
Read: : 0
Read: : 0
Read: : 0
Write::17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17
Read: : 17

12、线程八锁

线程八锁的关键

  1. 非静态方法的锁默认为 this ,静态方法的锁为 对应的 Class 实例
  2. 某一个时刻内,只能有一个线程持有锁,无论几个方法

1. 两个普通同步方法,两个线程,标准打印, 打印? //one two

  • 我感觉有可能会是 two one ,因为线程 start() 之后,具体的执行时间是不确定,但是运行了好多次都没有出现这种情况,说明基本上都先执行了 getOne() 方法,即 getOne() 方法先拿到了锁
public class TestThread8Monitor {

	public static void main(String[] args) {
		Number number = new Number();

		new Thread(new Runnable() {
			@Override
			public void run() {
				number.getOne();
			}
		}).start();

		new Thread(new Runnable() {
			@Override
			public void run() {
				number.getTwo();
			}
		}).start();
	}

}

class Number {

	public synchronized void getOne() {
		
		System.out.println("one");
	}

	public synchronized void getTwo() {
		System.out.println("two");
	}
}

2. 新增 Thread.sleep() 给 getOne() ,打印? //one two

  • 那还是说明先执行了 getOne() 方法,即 getOne() 方法先拿到了锁
public class TestThread8Monitor {

	public static void main(String[] args) {
		Number number = new Number();

		new Thread(new Runnable() {
			@Override
			public void run() {
				number.getOne();
			}
		}).start();

		new Thread(new Runnable() {
			@Override
			public void run() {
				number.getTwo();
			}
		}).start();
	}

}

class Number {

	public synchronized void getOne() {
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
		}
		System.out.println("one");
	}

	public synchronized void getTwo() {
		System.out.println("two");
	}
}

3. 新增普通方法 getThree() , 打印? //three one two

  • 非同步方法说:同步方法关我毛事,我又不抢他们的锁
public class TestThread8Monitor {

	public static void main(String[] args) {
		Number number = new Number();

		new Thread(new Runnable() {
			@Override
			public void run() {
				number.getOne();
			}
		}).start();

		new Thread(new Runnable() {
			@Override
			public void run() {
				number.getTwo();
			}
		}).start();
		new Thread(new Runnable() {
			@Override
			public void run() {
				number.getThree();
			}
		}).start();
	}

}

class Number {

	public synchronized void getOne() {
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
		}
		System.out.println("one");
	}

	public synchronized void getTwo() {
		System.out.println("two");
	}
	
	public  void getThree() {
		System.out.println("three");
	}
}

4. 两个普通同步方法,两个 Number 对象,打印? //two one

  • getTwo() 方法对 getOne() 说:我两的 this 对象不同,所以我们不用抢同一把锁了,我就不用等你了
public class TestThread8Monitor {

	public static void main(String[] args) {
		Number number = new Number();
		Number number2 = new Number();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				number.getOne();
			}
		}).start();

		new Thread(new Runnable() {
			@Override
			public void run() {
				number2.getTwo();
			}
		}).start();
	}

}

class Number {

	public synchronized void getOne() {
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
		}
		System.out.println("one");
	}

	public synchronized void getTwo() {
		System.out.println("two");
	}
	
	public  void getThree() {
		System.out.println("three");
	}
}

5. 修改 getOne() 为静态同步方法,一个 Number 对象,打印? //two one

  • 同上:getOne() 方法锁的 Number.class 对象,getTwo() 方法锁的 this 对象,同样也是两个不同的对象
public class TestThread8Monitor {

	public static void main(String[] args) {
		Number number = new Number();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				number.getOne();
			}
		}).start();

		new Thread(new Runnable() {
			@Override
			public void run() {
				number.getTwo();
			}
		}).start();
	}

}

class Number {

	public static synchronized void getOne() {
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
		}
		System.out.println("one");
	}

	public synchronized void getTwo() {
		System.out.println("two");
	}
	
	public  void getThree() {
		System.out.println("three");
	}
}

6. 修改两个方法均为静态同步方法,一个 Number 对象? //one two

  • 两个方法锁的都是 Number.class ,所以 getTwo() 方法会等 getOne() 方法释放锁
public class TestThread8Monitor {

	public static void main(String[] args) {
		Number number = new Number();

		new Thread(new Runnable() {
			@Override
			public void run() {
				number.getOne();
			}
		}).start();

		new Thread(new Runnable() {
			@Override
			public void run() {
				number.getTwo();
			}
		}).start();
	}

}

class Number {

	public static synchronized void getOne() {
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
		}
		System.out.println("one");
	}

	public static synchronized void getTwo() {
		System.out.println("two");
	}

	public void getThree() {
		System.out.println("three");
	}
}

7. 一个静态同步方法,一个非静态同步方法,两个 Number 对象? //two one

  • 两个方法锁的对象不同 ,所以 getTwo() 方法不用等待 getOne() 方法释放锁
public class TestThread8Monitor {

	public static void main(String[] args) {
		Number number = new Number();
		Number number2 = new Number();

		new Thread(new Runnable() {
			@Override
			public void run() {
				number.getOne();
			}
		}).start();

		new Thread(new Runnable() {
			@Override
			public void run() {
				number2.getTwo();
			}
		}).start();
	}

}

class Number {

	public static synchronized void getOne() {
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
		}
		System.out.println("one");
	}

	public synchronized void getTwo() {
		System.out.println("two");
	}

	public void getThree() {
		System.out.println("three");
	}
}

8. 两个静态同步方法,两个 Number 对象? //one two

  • 虽然是两个对象,但锁的都是 Number.class 这个对象
public class TestThread8Monitor {

	public static void main(String[] args) {
		Number number = new Number();
		Number number2 = new Number();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				number.getOne();
			}
		}).start();

		new Thread(new Runnable() {
			@Override
			public void run() {
				number2.getTwo();
			}
		}).start();
	}

}

class Number {

	public static synchronized void getOne() {
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
		}
		System.out.println("one");
	}

	public static synchronized void getTwo() {
		System.out.println("two");
	}
	
	public  void getThree() {
		System.out.println("three");
	}
}

线程 8 锁结论

  1. 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用
    其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻
    内,只能有唯一一个线程去访问这些synchronized方法
  2. 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的
    synchronized方法
  3. 所有的非静态同步方法用的都是同一把锁——实例对象本身,也就是说如果一个实
    例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获
    取锁的方法释放锁后才能获取锁,可是别的实例对象的非静态同步方法因为跟该实
    例对象的非静态同步方法用的是不同的锁,所以无需等待该实例对象已获取锁的非
    静态同步方法释放锁就可以获取他们自己的锁。
  4. 所有的静态同步方法用的也是同一把锁——类对象本身,这两把锁是两个不同的对
    象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。但是一旦一个
    静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取
    锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同
    步方法之间,只要它们同一个类的实例对象!

13、线程池

线程池简介

  1. 第四种获取线程的方法:线程池,一个 ExecutorService,它使用可能的几个池线程之一执行每个提交的任务,通常使用 Executors 工厂方法配置。
  2. 线程池可以解决两个不同问题:由于减少了每个任务调用的开销,它们通常可以在执行大量异步任务时提供增强的性能,并且还可以提供绑定和管理资源(包括执行任务集时使用的线程)的方法。每个 ThreadPoolExecutor 还维护着一些基本的统计数据,如完成的任务数。
  3. 为了便于跨大量上下文使用,此类提供了很多可调整的参数和扩展钩子 (hook)。但是,强烈建议程序员使用较为方便的 Executors 工厂方法 :
    • Executors.newCachedThreadPool()(无界线程池,可以进行自动线程回收)
    • Executors.newFixedThreadPool(int)(固定大小线程池)
    • Executors.newSingleThreadExecutor()(单个后台线程)
    • 它们均为大多数使用场景预定义了设置。

线程池的体系结构

/*
 * 一、线程池:提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提高了响应的速度。
 * 
 * 二、线程池的体系结构:
 * 	java.util.concurrent.Executor : 负责线程的使用与调度的根接口
 * 		|--**ExecutorService 子接口: 线程池的主要接口
 * 			|--ThreadPoolExecutor 线程池的实现类
 * 			|--ScheduledExecutorService 子接口:负责线程的调度
 * 				|--ScheduledThreadPoolExecutor :继承 ThreadPoolExecutor, 实现 ScheduledExecutorService
 * 
 * 三、工具类 : Executors 
 * ExecutorService newFixedThreadPool() : 创建固定大小的线程池
 * ExecutorService newCachedThreadPool() : 缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。
 * ExecutorService newSingleThreadExecutor() : 创建单个线程池。线程池中只有一个线程
 * 
 * ScheduledExecutorService newScheduledThreadPool() : 创建固定大小的线程,可以延迟或定时的执行任务。
 */

代码示例

  • 代码
public class TestThreadPool {
	
	public static void main(String[] args) throws Exception {
		//1. 创建线程池
		ExecutorService pool = Executors.newFixedThreadPool(5);		
		List<Future<Integer>> list = new ArrayList<>();
		
		for (int i = 0; i < 10; i++) {
            // 2. 为线程池中的线程分配任务
			Future<Integer> future = pool.submit(new Callable<Integer>(){
				@Override
				public Integer call() throws Exception {
					int sum = 0;					
					for (int i = 0; i <= 100; i++) {
						sum += i;
					}					
					return sum;
				}				
			});
			list.add(future);
		}
		
        // 3. 关闭线程池
		pool.shutdown();
		
		for (Future<Integer> future : list) {
			System.out.println(future.get());
		}
	}
}
  • 程序运行结果
5050
5050
5050
5050
5050
5050
5050
5050
5050
5050

14、线程调度

线程调度:ScheduledExecutorService

  • 代码一个:ScheduledExecutorService,可安排在给定的延迟后运行或定期执行的命令。
/*
 * ScheduledExecutorService newScheduledThreadPool() : 创建固定大小的线程,可以延迟或定时的执行任务。
 */
public class TestScheduledThreadPool {

	public static void main(String[] args) throws Exception {
		ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
		
		for (int i = 0; i < 5; i++) {
			Future<Integer> result = pool.schedule(new Callable<Integer>(){
				@Override
				public Integer call() throws Exception {
					int num = new Random().nextInt(100);//生成随机数
					System.out.println(Thread.currentThread().getName() + " : " + num);
					return num;
				}
				
			}, 1, TimeUnit.SECONDS);
			
			System.out.println(result.get());
		}
		
		pool.shutdown();
	}
	
}
  • 程序运行结果:每隔 1s 输出一次结果
pool-1-thread-1 : 48
48
pool-1-thread-1 : 39
39
pool-1-thread-2 : 35
35
pool-1-thread-1 : 72
72
pool-1-thread-3 : 83
83

15、ForkJoinPool 分支/合并

Fork/Join 框架

  • Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行 join 汇总

image-20200806141516762

Fork/Join 框架与线程池的区别

  1. 采用 “工作窃取”模式(work-stealing):当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,如果一个线程执行完线程队列的任务后,该线程会从一个随机线程的队列的末尾偷取一个任务并把它放在自己的队列中。
  2. 相对于一般的线程池实现, fork/join框架的优势体现在对其中包含的任务的处理方式上.在一般的线程池中, 如果一个线程正在执行的任务由于某些原因无法继续运行, 那么该线程会处于等待状态。
  3. 而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行。 那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行,这种方式减少了线程的等待时间, 提高了性能。

代码示例

  • 代码
public class TestForkJoinPool {

	public static void main(String[] args) {
		Instant start = Instant.now();

		// fork/join 需要 ForkJoinPool 池的支持才能完成计算
		ForkJoinPool pool = new ForkJoinPool();
		// 初始化 fork/join
		ForkJoinTask<Long> task = new ForkJoinSumCalculate(0L, 50000000000L);
		// 提交 fork/join 任务
		Long sum = pool.invoke(task);
		System.out.println(sum);

		Instant end = Instant.now();
		System.out.println("耗费时间为:" + Duration.between(start, end).toMillis());// 9444
	}

	@Test
	public void test1() {
		Instant start = Instant.now();
		
		long sum = 0L;
		for (long i = 0L; i <= 50000000000L; i++) {
			sum += i;
		}
		System.out.println(sum);

		Instant end = Instant.now();
		System.out.println("耗费时间为:" + Duration.between(start, end).toMillis());// 13093
	}

	// java8 新特性
	@Test
	public void test2() {
		Instant start = Instant.now();
		
		Long sum = LongStream.rangeClosed(0L, 50000000000L).parallel().reduce(0L, Long::sum);
		System.out.println(sum);
		
		Instant end = Instant.now();
		System.out.println("耗费时间为:" + Duration.between(start, end).toMillis());// 5285
	}

}

class ForkJoinSumCalculate extends RecursiveTask<Long> {

	private static final long serialVersionUID = -259195479995561737L;

	private long start;
	private long end;

	private static final long THRESHOLD = 1000L; // 临界值

	public ForkJoinSumCalculate(long start, long end) {
		this.start = start;
		this.end = end;
	}

	@Override
	protected Long compute() {
		long length = end - start;

		if (length <= THRESHOLD) {
			// 小于阈值,则直接计算
			long sum = 0L;

			for (long i = start; i <= end; i++) {
				sum += i;
			}

			return sum;
		} else {
			// 大于阈值,则进行拆分
			long middle = (start + end) / 2;

			ForkJoinSumCalculate left = new ForkJoinSumCalculate(start, middle);
			left.fork(); // 进行拆分,同时压入线程队列

			ForkJoinSumCalculate right = new ForkJoinSumCalculate(middle + 1, end);
			right.fork(); // 进行拆分,同时压入线程队列

			// 拆分完成后进行合并
			return left.join() + right.join();
		}
	}
}
  • 结论:还是用 Stream 流方便呀~~~
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值