JUC并发编程
1. 什么是JUC
JUC 是在 JDK 1.5 开始出现的。下面一起来看看它怎么使用。
2. 线程和进程
2-1. 进程:正在进行中的程序(直译),一个进程至少包含一个或多个线程。
2-2. 线程:进程中一个负责程序执行的控制单元(执行路径),每一个线程都有自己运行的内容,这个内容可以称之为线程要执行的任务;
2-3. 并发、并行1、Java默认有几个线程?
答:2个,main主线程、GC线程
2、Java 真的可以开启线程吗?
答:不可以,只能通过本地方法调用底层的C++去操作
public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } } // 本地方法,底层的C++ ,Java 无法直接操作硬件 private native void start0();
- CPU一核,模拟出多条线程,快速交替
- CPU多核,多个线程可以同时执行
// 获取CPU核数 // CPU密集型、IO密集型 Runtime.getRuntime().availableProcessors();
并发编程的本质:充分利用CPU的资源
public enum State { // 新生 NEW, // 运行 RUNNABLE, // 阻塞 BLOCKED, // 等待(死等,也是阻塞的等待) WAITING, // 超时等待(指定的时间内等待,过时不候) TIMED_WAITING, // 终止 TERMINATED; }
① 来自不同的类: - sleep来自Thread类;
- wait来自Thread类; ② 是否释放锁: - sleep方法不会释放锁;
- wait会方法释放锁,使得其他线程可以使用同步控制块或者方法(锁代码块和方法锁); ③ 使用范围: - sleep可以在任何地方使用;
- wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用; ④ 异常捕获: - sleep必须捕获异常;
- wait,notify和notifyAll不需要捕获异常;
3. Lock锁同步
在JDK1.5之前,解决多线程安全问题有两种方式:synchronized 隐式锁
- 同步代码块、同步方法
在JDK1.5之后,出现了更加灵活的方式:Lock 显式锁
- 同步锁
3-1. Lock需要通过lock()方法上锁,通过unlock()方法释放锁。为了保证锁能释放,所有unlock方法一般放在finally中去执行。
Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }
○ ReentrantLock:默认为非公平锁
公平锁:十分公平:可以先来后到
非公平锁:十分不公平:可以插队 (默认)

public class SaleTicketDemo01 { public static void main(String[] args) { // 并发:多线程操作同一个类,把资源类丢入线程就可以了 Ticket ticket = new Ticket(); // @FunctionalInterface 函数式接口,jdk1.8 lambda表达式 (参数)->{ 代码 } new Thread(() -> { for (int i = 0; i < 30; i++) ticket.sale(); }, "001").start(); new Thread(() -> { for (int i = 0; i < 30; i++) ticket.sale(); }, "002").start(); new Thread(() -> { for (int i = 0; i < 30; i++) ticket.sale(); }, "003").start(); } } // 资源类 OOP class Ticket { private int ticket = 30; // synchronized 本质: 队列,锁 public synchronized void sale() { if (ticket > 0) { System.out.println("窗口 " + Thread.currentThread().getName() + " 完成售票,余票为:" + (--ticket)); } } }
多个线程同时操作共享数据ticket,所以会出现线程安全问题。会出现同一张票卖了好几次或者票数为负数的情况。以前用同步代码块和同步方法解决,现在看看用同步锁怎么解决。
● Lock接口:直接创建lock对象,然后用lock()方法上锁,最后用unlock()方法释放锁即可。
public class SaleTicketDemo02 { public static void main(String[] args) { // 并发:多线程操作同一个类,把资源类丢入线程就可以了 Ticket2 ticket2 = new Ticket2(); new Thread(() -> { for (int i = 0; i < 30; i++) ticket2.sale(); }, "001").start(); new Thread(() -> { for (int i = 0; i < 30; i++) ticket2.sale(); }, "002").start(); new Thread(() -> { for (int i = 0; i < 30; i++) ticket2.sale(); }, "003").start(); } } // 资源类 class Ticket2 { private int ticket = 30; private Lock lock = new ReentrantLock(); // 1.创建lock锁 public void sale() { lock.lock(); // 2.加锁 try { if (ticket > 0) { System.out.println("Lock窗口 " + Thread.currentThread().getName() + " 完成售票,余票为:" + (--ticket)); } } finally { lock.unlock(); // 3.解锁 } } }
3-3. Synchronized 和 Lock 的区别
① Synchronized 内置的 Java 关键字;Lock 是一个 Java 类
② Synchronized 无法判断获取锁的状态;Lock 可以判断是否获取到了锁
③ Synchronized 会自动释放;Lock 必须要手动释放锁!如果不释放 会产生死锁
④ Synchronized 线程 1(获得锁,阻塞),线程 2(等待获取,死等);Lock 锁就不一定会等待下去
⑤ Synchronized 可重入锁,不可以中断的,非公平;Lock 可重入锁,可以判断锁,非公平(可以自己设置)
⑥ Synchronized 适合锁少量的代码同步问题;Lock 适合锁大量的同步代码!
4. 等待唤醒机制
4-1. 虚假唤醒问题
生产消费模式是等待唤醒机制的一个经典案例,看下面的代码:
public class TestProductorAndConsumer { public static void main(String[] args) { Data data = new Data(); new Thread(() -> { for (int i = 0; i < 10; i++) data.decrement(); }, "生产者01").start(); new Thread(() -> { for (int i = 0; i < 10; i++) data.increment(); }, "消费者02").start(); new Thread(() -> { for (int i = 0; i < 10; i++) data.decrement(); }, "生产者03").start(); new Thread(() -> { for (int i = 0; i < 10; i++) data.increment(); }, "消费者04").start(); } } class Data { private int product = 0; // 共享数据 // product +1 public synchronized void increment() { if (product != 0) { try { this.wait(); // 等待 } catch (InterruptedException e) { e.printStackTrace(); } } product++; System.out.println(Thread.currentThread().getName() + " => " + product); // 通知其他线程,我 +1 完毕了 this.notifyAll(); } // product -1 public synchronized void decrement() { if (product == 0) { try { this.wait(); // 等待 } catch (InterruptedException e) { e.printStackTrace(); } } product--; System.out.println(Thread.currentThread().getName() + " => " + product); // 通知其他线程,我 -1 完毕了 this.notifyAll(); } }
● 问题存在:当存在 2 个以上的线程时,可能会产生虚假唤醒!
● 解决办法:将 if 改为 while 判断
// product +1 public synchronized void increment() throws InterruptedException { while (product != 0) this.wait(); // 等待 product++; System.out.println(Thread.currentThread().getName() + " => " + product); // 通知其他线程,我 +1 完毕了 this.notifyAll(); } // product -1 public synchronized void decrement() throws InterruptedException { while (product == 0) this.wait(); // 等待 product--; System.out.println(Thread.currentThread().getName() + " => " + product); // 通知其他线程,我 -1 完毕了 this.notifyAll(); }
只需要把 if 改成 while,每次都再去判断一下,就可以了。
4-2. 用Lock锁实现等待唤醒
public class TestProductorAndConsumer2 { public static void main(String[] args) { Data2 data = new Data2(); new Thread(() -> { for (int i = 0; i < 10; i++) data.decrement(); }, "生产者01").start(); new Thread(() -> { for (int i = 0; i < 10; i++) data.increment(); }, "消费者02").start(); new Thread(() -> { for (int i = 0; i < 10; i++) data.decrement(); }, "生产者03").start(); new Thread(() -> { for (int i = 0; i < 10; i++) data.increment(); }, "消费者04").start(); } } class Data2 { private int product = 0; // 共享数据 private Lock lock = new ReentrantLock(); // 创建锁对象 Condition condition = lock.newCondition(); // 获取锁的监视器对象 // product +1 public void increment() { lock.lock(); // 加锁 try { while (product != 0) condition.await(); // 等待 product++; System.out.println("Lock" + Thread.currentThread().getName() + " => " + product); condition.signalAll();// 通知其他线程,我 +1 完毕了 } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); // 释放锁 } } // product -1 public void decrement() { lock.lock(); // 加锁 try { while (product == 0) condition.await(); // 等待 product--; System.out.println("Lock" + Thread.currentThread().getName() + " => " + product); // 通知其他线程,我 -1 完毕了 condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); // 释放锁 } } }
使用 lock 同步锁,就不需要 sychronized 关键字了,需要创建 lock 对象和 condition 实例。condition 的 await() 方法、signal() 方法和 signalAll() 方法分别与 wait() 方法、 notify() 方法和 notifyAll() 方法对应。
4-3. 线程按序交替
首先来看一道题:
编写一个程序,开启 3 个线程,这三个线程的 ID 分别为 A、B、C, 每个线程将自己的 ID 在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。 如:ABCABCABC…… 依次递归
线程本来是抢占式进行的,要按序交替,所以必须实现线程通信,
那就要用到等待唤醒。可以使用同步方法,也可以用同步锁。
代码实现:
public class TestLoopPrint { public static void main(String[] args) { AlternationDemo alternationDemo = new AlternationDemo(); new Thread(()->{for (int i = 0; i < 10; i++) alternationDemo.loopA(); },"线程A").start(); new Thread(()->{for (int i = 0; i < 10; i++){alternationDemo.loopB();}},"线程B").start(); new Thread(()->{for (int i = 0; i < 10; i++){alternationDemo.loopC();}},"线程C").start(); } } class AlternationDemo { // 当前正在执行的线程的标记 private int number = 1; // 创建锁对象,并且使用该锁创建三个监视器 private Lock lock = new ReentrantLock(); Condition condition_1 = lock.newCondition(); Condition condition_2 = lock.newCondition(); Condition condition_3 = lock.newCondition(); public void loopA() { lock.lock(); try { while (number != 1) { // 判断 condition_1.await(); // 等待 } System.out.println(Thread.currentThread().getName() + " => AAAAA"); // 唤醒指定的监视器2 number = 2; condition_2.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void loopB() { lock.lock(); try { while (number != 2) { // 判断 condition_2.await(); // 等待 } System.out.println(Thread.currentThread().getName() + " => BBBBBB"); // 唤醒指定的监视器3 number = 3; condition_3.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void loopC() { lock.lock(); try { while (number != 3) { // 判断 condition_3.await(); // 等待 } System.out.println(Thread.currentThread().getName() + " => CCCCCCC"); // 唤醒指定的监视器1 number = 1; condition_1.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }
5. 锁的8种问题
按照普通的情况访问同步方法,查看输出
public class Test1 { public static void main(String[] args) { Phone1 phone = new Phone1(); new Thread(() -> {phone.sendSms(); }, "A").start(); try { // 睡眠2秒 TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { phone.call(); }, "B").start(); } } class Phone1 { public synchronized void sendSms() { System.out.println("发短信》》》"); } public synchronized void call() { System.out.println("打电话!!!"); } }
- 执行结果:1、发短信 2、打电话
2)在其中一种方法中添加sleep方法访问
在sendSms方法中添加sleep,查看修改后的打印顺序
class Phone1 { public synchronized void sendSms() { try { TimeUnit.SECONDS.sleep(4); // 睡4秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信》》》"); } public synchronized void call() { System.out.println("打电话!!!"); } }
- 执行结果: 1、发短信 2、打电话
synchronized 锁的是对象this,两个方法用的使用一个锁,所以谁先拿到谁先执行
在Phone类中添加hello方法,同时线程修改为访问同步方法和普通方法
public class Test2 { public static void main(String[] args) { Phone2 phone = new Phone2(); // 有锁 new Thread(() -> { phone.sendSms(); }, "A").start(); try { // 睡眠2秒 TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } // 没锁 new Thread(() -> { phone.hello(); }, "B").start(); } } class Phone2 { public synchronized void sendSms() { try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信》》》"); } public synchronized void call() { System.out.println("打电话!!!"); } // 这里没有锁,不是同步方法,不受锁的影响 public void hello() { System.out.println("hello!"); } }
- 执行结果:1、hello 2、发短信
4)执行时创建两个不同对象,通过不同对象访问加锁的方法
在创建线程时,通过不同对象执行同步方法,查看执行结果
public class Test2 { public static void main(String[] args) { // 两个对象,两个调用者,两把锁! Phone2 phone1 = new Phone2(); Phone2 phone2 = new Phone2(); // 有锁 new Thread(() -> { phone1.sendSms(); }, "A").start(); try { // 睡眠2秒 TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } // 有锁 new Thread(() -> { phone2.call(); }, "B").start(); } } class Phone2 { // synchronized 是 对象锁this public synchronized void sendSms() { try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信》》》"); } public synchronized void call() { System.out.println("打电话!!!"); } // 这里没有锁 public void hello() { System.out.println("hello!"); } }
- 执行结果:1、打电话 2、发短信
synchronized 锁的是对象this。两个对象,两个调用者,两把锁!所以互不受影响
5)将加锁的方法改为静态方法,同一个对象执行
两个同步方法都改为静态方法,通过同一个对象执行方法,查看执行结果
public class Test3 { public static void main(String[] args) { Phone3 phone = new Phone3(); new Thread(() -> { phone.sendSms(); }, "A").start(); try { // 睡眠2秒 TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { phone.call(); }, "B").start(); } } class Phone3 { // static synchronized 静态方法锁 public static synchronized void sendSms() { try { TimeUnit.SECONDS.sleep(4); // 睡眠4秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信》》》"); } public static synchronized void call() { System.out.println("打电话!!!"); } }
- 执行结果: 1、发短信 2、打电话
static synchronized 静态同步方法,类一加载就有了,所以锁的是Class
6)通过两个对象访问静态同步方法
public class Test3 { public static void main(String[] args) { // 两个对象的Class类模板只有一个,static,锁的是Class Phone3 phone1 = new Phone3(); Phone3 phone2 = new Phone3(); new Thread(() -> { phone1.sendSms(); }, "A").start(); try { // 睡眠2秒 TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { phone2.call(); }, "B").start(); } } class Phone3 { // static synchronized 静态方法,类一加载就有了,所以锁的是Class public static synchronized void sendSms() { try { TimeUnit.SECONDS.sleep(4); // 睡眠4秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信》》》"); } public static synchronized void call() { System.out.println("打电话!!!"); } }
- 执行结果:1、打电话 2、发短信
两个对象的Class类模板只有一个,锁的是Class,只有一把锁
7)一个静态同步方法 一个普通同步方法,通过同一个对象执行
其中的一个方法去掉静态关键字,查看执行结果
public class Test4 { public static void main(String[] args) { Phone4 phone = new Phone4(); new Thread(() -> { phone.sendSms(); }, "A").start(); try { // 睡眠2秒 TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { phone.call(); }, "B").start(); } } class Phone4 { // 静态同步方法 锁Class public static synchronized void sendSms() { try { // 睡眠4秒 TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信》》》"); } // 普通同步方法 锁this public synchronized void call() { System.out.println("打电话!!!"); } }
- 执行结果:1、打电话 2、发短信
静态同步锁 Class,普通同步锁 this,两把不同的锁,所以互不受影响
8)一个静态同步方法 一个普通同步方法,两个对象执行
public class Test4 { public static void main(String[] args) { Phone4 phone1 = new Phone4(); Phone4 phone2 = new Phone4(); new Thread(() -> { phone1.sendSms(); }, "A").start(); try { // 睡眠2秒 TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { phone2.call(); }, "B").start(); } } class Phone4 { // 静态同步方法 锁Class public static synchronized void sendSms() { try { // 睡眠4秒 TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信》》》"); } // 普通同步方法 锁this public synchronized void call() { System.out.println("打电话!!!"); } }
- 执行结果:1、打电话 2、发短信
两个对象 两把不同的锁,一个Class 一个this
● 总结:
① 对象锁:使用 synchronized 修饰非静态的方法以及 synchronized(this) 同步代码块使用的锁是对象锁。
② 类锁:使用 synchronized 修饰静态的方法以及 synchronized(class) 同步代码块使用的锁是类锁。
③ 私有锁:在类内部声明一个私有属性如 private Object lock,在需要加锁的同步块使用 synchronized(lock)
它们的特性:
- 对象锁具有可重入性。
- 当一个线程获得了某个对象的对象锁,则该线程仍然可以调用其他任何需要该对象锁的 synchronized 方法或 synchronized(this) 同步代码块。
- 当一个线程访问某个对象的一个 synchronized(this) 同步代码块时,其他线程对该对象中所有其它 synchronized(this) 同步代码块的访问将被阻塞,因为访问的是同一个对象锁。
- 每个类只有一个类锁,但是类可以实例化成对象,因此每一个对象对应一个对象锁。
- 普通方法与同步方锁法无关。
- 类锁和对象锁不会产生竞争。
- 私有锁和对象锁也不会产生竞争。
- 使用私有锁可以减小锁的细粒度,减少由锁产生的开销。
6. 集合类不安全
多个线程共同操作一个线程不安全的类时报并发修改异常
6-1. List - 线程不安全
public class ListTest { public static void main(String[] args) { // List<String> list = new Vector<>(); // List<String> list = Collections.synchronizedList(new ArrayList<>()); List<String> list = new CopyOnWriteArrayList<>(); for (int i = 0; i < 10; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString().substring(0, 5)); System.out.println(Thread.currentThread().getName() + " - " + list); }, String.valueOf(i)).start(); } } }
① 使用 Vector 对象,实际上是加了 Synchronized 同步,实现同步需要很高的花费,效率较低
② 使用 Collections 集合工具类,给线程不安全的集合添加一层封装
③ 写入时复制 CopyOnWriteArrayList,加了写锁的集合,锁住的是整个对象,但读操作可以并发执行
- 写入时复制:这种集合将数据放在固定的数组中,任何数据的改变,都会重新创建一个新的数组来记录值。
- 这种集合被设计用在,读多写少的时候推荐使用!
6-2. Set - 线程不安全
public class SetTest { public static void main(String[] args) { // Set<String> set = Collections.synchronizedSet(new HashSet<>()); Set<String> set = new CopyOnWriteArraySet<>(); for (int i = 0; i < 10; i++) { new Thread(() -> { set.add(UUID.randomUUID().toString().substring(0, 5)); System.out.println(Thread.currentThread().getName() + " - " + set); }, String.valueOf(i)).start(); } } }
● 解决方案:
① 使用 Collections 集合工具类,给线程不安全的集合添加一层封装
② CopyOnWriteArraySet 写入时复制,读多写少的时候推荐使用
● HashSet 底层:
// HashSet是基于HashMap实现的 public HashSet() { map = new HashMap<>(); } // add set 本质就是 map key 是无法重复的! public boolean add(E e) { return map.put(e, PRESENT)==null; } // PRESENT 是用来填充 map 中的 value,定义为 Object 类型。 private static final Object PRESENT = new Object();
6-3. Map - 线程不安全
public class MapTest { public static void main(String[] args) { // Map<String, String> map = Collections.synchronizedMap(new HashMap<>()); Map<String, String> map = new ConcurrentHashMap<>(); for (int i = 0; i < 30; i++) { int finalI = i; new Thread(() -> { map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 5)); System.out.println(Thread.currentThread().getName() + " - " + map); }, String.valueOf(i)).start(); } } }
解决方案:
① 使用 Collections 集合工具类,给线程不安全的集合添加一层封装
② 使用线程安全的 hash 表 ConcurrentHashMap,注意:1.7前采用是锁分段机制,1.8后采用了CAS算法
1、Map 是这样用的吗? 不是,工作中不使用 HashMap 2、默认等价于什么? new HashMap<>(16, 0.75f) - 初始容量:static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 - 加载因子:static final float DEFAULT_LOAD_FACTOR = 0.75f
7. 创建线程的方式 --- 实现Callable接口
● Callable:类似于Runnable

- call 方法有返回值,并且能够抛出异常。
- 有缓存,结果可能需要等待,会阻塞!
● FutureTask:获取 Callable 任务的返回值

使用 Future 我们可以得知 Callable 的运行状态,以及获取 Callable 执行完后的返回值。
Future 的方法介绍:
- get() :阻塞式,用于获取 Callable/Runnable 执行完后的返回值。
- 带时间参数的get()重载方法用于最多等待的时间后,如仍未返回则线程将继续执行。
- cancel() :撤销正在执行 Callable 的 Task。
- isDone():是否执行完毕。
- isCancelled():任务是否已经被取消。
● 使用实例:
public class CallableTest { public static void main(String[] args) { CallableDemo callableDemo = new CallableDemo(); for (int i = 0; i < 10; i++) { // 执行callable方式,需要FutureTask实现类的支持,用来接收运算结果 FutureTask<Integer> task = new FutureTask<>(callableDemo); // 执行线程任务 new Thread(task, String.valueOf(i)).start(); // 接收线程运算结果 try { // 需要等到线程执行完调用get方法才会执行,也可以用于闭锁操作 System.out.println(task.get()); } catch (Exception e) { e.printStackTrace(); } } } } class CallableDemo implements Callable<Integer> { @Override public Integer call() throws Exception { System.out.println(Thread.currentThread().getName() + " => call()"); TimeUnit.SECONDS.sleep(1); // 睡眠1秒 return new Random().nextInt(100); } }
Callable接口和实现Runable接口的区别就是,Callable带泛型,其call方法有返回值。使用的时候,需要用FutureTask来接收返回值。而且它也要等到线程执行完调用get方法才会执行,也可以用于闭锁操作。
8. 读写锁 - ReadWriterLock
我们在读数据的时候,可以多个线程同时读,不会出现问题,但是写数据的时候,如果多个线程同时写数据,那么到底是写入哪个线程的数据呢?
所以,如果有两个线程,读-写 和 写-写 需要互斥,读-读 不需要互斥。这个时候可以用读写锁。
独占锁(写锁):一次只能被一个线程占有
共享锁(读锁):多个线程可以同时占有
代码实现:
public class ReadWriteLockDemo { public static void main(String[] args) { RWLTest rwlTest = new RWLTest(); // 写入 for (int i = 0; i < 5; i++) { int finalI = i; new Thread(() -> { rwlTest.set(String.valueOf(finalI), finalI + ""); }, String.valueOf(i)).start(); } // 读取 for (int i = 0; i < 5; i++) { int finalI = i; new Thread(() -> { rwlTest.get(String.valueOf(finalI)); }, String.valueOf(i)).start(); } } } class RWLTest { private volatile Map<String, Object> map = new HashMap<>(); private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // 读(可以多个线程同时操作) public void get(String key) { readWriteLock.readLock().lock();// 上锁 try { System.out.println(Thread.currentThread().getName() + " -> 读取 key[" + key + "]"); Object obj = map.get(key); System.out.println(Thread.currentThread().getName() + " -> 获得 value[" + obj + "]"); } finally { readWriteLock.readLock().unlock();// 释放锁 } } // 写(一次只能有一个线程操作) public void set(String key, Object value) { readWriteLock.writeLock().lock();// 上锁 try { System.out.println(Thread.currentThread().getName() + " -> 写入 key[" + key + "]"); map.put(key, value); System.out.println(Thread.currentThread().getName() + " -> 写入成功!!!"); } finally { readWriteLock.writeLock().unlock();// 释放锁 } } }
执行结果:

9. 阻塞队列 - BlockingQueue
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。
① 支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。
② 支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。
9-1. 阻塞队列的四种方法
| 方法\处理方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞等待 | 超时等待 |
|---|---|---|---|---|
| 插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
| 移除方法 | remove() | poll() | take() | poll(time,unit) |
| 检查队首方法 | element() | peek() | 不可用 | 不可用 |
● 抛出异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException异常 。
● 有返回值,不抛出异常:插入方法会返回是否成功,成功则返回true。移除方法,则是从队列里拿出一个元素,如果没有则返回null
● 阻塞等待:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用。
● 超时等待:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出等待。
代码演示:
public class BlockingQueueTest { public static void main(String[] args) throws InterruptedException { // test01(); // test02(); // test03(); test04(); } /* 抛出异常 */ public static void test01() { // 队列的大小 ArrayBlockingQueue<String> queue = new ArrayBlockingQueue(3); System.out.println(queue.add("a")); System.out.println(queue.add("b")); System.out.println(queue.add("c")); // System.out.println(queue.add("d")); // 队列已满,抛出异常 IllegalStateException System.out.println("队首 = " + queue.element()); // 检测队首元素 System.out.println("=================="); System.out.println(queue.remove()); System.out.println(queue.remove()); System.out.println(queue.remove()); // System.out.println(queue.remove()); // 队列置空,没有元素,抛出异常 NoSuchElementException } /* 有返回值,不抛出异常 */ private static void test02() { ArrayBlockingQueue queue = new ArrayBlockingQueue(3); System.out.println(queue.offer("111")); System.out.println(queue.offer("222")); System.out.println(queue.offer("333")); System.out.println(queue.offer("444")); // false,不抛出异常 System.out.println("队首 = " + queue.peek()); // 检测队首元素 System.out.println("======================================"); System.out.println(queue.poll()); System.out.println(queue.poll()); System.out.println(queue.poll()); System.out.println(queue.poll()); // null,不抛出异常 } /* 等待,阻塞(一直阻塞) */ private static void test03() throws InterruptedException { ArrayBlockingQueue queue = new ArrayBlockingQueue(3); // 一直阻塞 queue.put("AAA"); queue.put("BBB"); queue.put("CCC"); // queue.put("DDD"); // 队列已满,等待添加 -> 阻塞 System.out.println(queue.take()); System.out.println(queue.take()); System.out.println(queue.take()); // System.out.println(queue.take()); // 队列为空,等待获取 -> 阻塞 } /* 有返回值,不抛出异常,等待,阻塞(等待超时) */ private static void test04() throws InterruptedException { ArrayBlockingQueue queue = new ArrayBlockingQueue(3); System.out.println(queue.offer("A1", 2, TimeUnit.SECONDS)); System.out.println(queue.offer("B2", 2, TimeUnit.SECONDS)); System.out.println(queue.offer("C3", 2, TimeUnit.SECONDS)); System.out.println(queue.offer("D4", 2, TimeUnit.SECONDS)); // 等待超过两秒,就返回false System.out.println("================================"); System.out.println(queue.poll(2, TimeUnit.SECONDS)); System.out.println(queue.poll(2, TimeUnit.SECONDS)); System.out.println(queue.poll(2, TimeUnit.SECONDS)); System.out.println(queue.poll(2, TimeUnit.SECONDS)); // 等待超过两秒,就返回null } }
9-2. Java里的阻塞队列
JDK7提供了7个阻塞队列。分别是:
① ArrayBlockingQueue:是用数组实现的有界阻塞队列,初始化时必须传入容量,是FIFO队列,支持公平和非公平锁。
② LinkedBlockingQueue:是用链表实现的有界阻塞队列,初始化时如果没有传入容量,则容量时Intger.MAX_VALUE,是FIFO队列,因为入队和出队方法各是一把锁,所以一般情况下并发性能优于ArrayBlockingQueue。
③ LinkedBlockingDeque:是双向链表实现的有界阻塞队列,初始化时如果没有传入容量,则容量时Intger.MAX_VALUE,可以实现FIFO队列,也可以实现LIFO队列。
④ PriorityBlockingQueue:是数组实现的具有优先级的无界阻塞队列,有两个注意事项,
- 该队列是具有优先级的,不是按先进先出或者后进先出,是按优先级的高低,所以入队的对象必须是实现了Comparable接口的。
- 队列虽然逻辑上是无界的,但因为是数组实现的,不能无限扩容,作者在代码里限制了,最大容量是Intger.MAX_VALUE-8。
⑤ DelayQueue:延迟队列可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素, 元素必须是Delayed的子类
⑥ SynchronousQueue:不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。支持公平锁和非公平锁。
⑦ LinkedTransferQueue:由链表结构组成的无界阻塞队列,对比LinkedBlockingQueue除了有put、take等方法外,还提供了transfer方法,该方法会阻塞线程,直到元素被消费,才返回。
上面列举了阻塞队列的异同点,我们可以根据自己的业务场景选择合适的阻塞队列。
简单介绍下其中两个队列:
9-2-1. ArrayBlockingQueue
public interface BlockingQueue<E> extends Queue<E> { //入队,入队失败会抛出异常 boolean add(E e); //入队,入队成功返回true,否则返回false boolean offer(E e); //入队,入队成功返回,否则进行等待 void put(E e) throws InterruptedException; //入队,不成功等待一段时间,仍然不能入队成功返回false,入队成功返回true boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException; //出队,队列为空进行等待,否则将元素出队 E take() throws InterruptedException; //出队,队列为空进行一段时间的等待,仍然为空返回null,否则将元素出队 E poll(long timeout, TimeUnit unit) throws InterruptedException; //返回剩余余量 int remainingCapacity(); //将某个元素出队,存在返回true,否则返回false boolean remove(Object o); //判断是否包含某个元素 public boolean contains(Object o); //将队列中所有可用元素出队到集合C中 int drainTo(Collection<? super E> c); //将队列中最多maxElements个可用元素出队到集合C中 int drainTo(Collection<? super E> c, int maxElements); }
ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列。
什么是公平的访问队列?
所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
访问者的公平性是使用可重入锁实现的,代码如下:
public ArrayBlockingQueue(int capacity, boolean fair) { if (capacity <= 0) throw new IllegalArgumentException(); this.items = new Object[capacity]; lock = new ReentrantLock(fair); notEmpty = lock.newCondition(); notFull = lock.newCondition(); }
9-2-2. SynchronousQueue
SynchronousQueue 是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。
适用场景:线程之间数据传递,一个线程使用过的数据,传给另外一个线程使用。
声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。
public SynchronousQueue() { this(false); } public SynchronousQueue(boolean fair) { transferer = fair ? new TransferQueue<E>() : new TransferStack<E>(); }
公平模式和非公平模式的区别:
- 公平模式:采用公平锁,在获取锁时,增加了isFirst(current)判断,当且仅当,等待队列为空或当前线程是等待队列的头结点时,才可尝试获取锁。
- 非公平模式(默认):采用非公平锁,那些尝试获取锁且尚未进入等待队列的线程会和等待队列head结点的线程发生竞争。
代码演示:
/** * 同步队列 - SynchronousQueue * 和其它的BlockingQueue不一样,SynchronousQueue不存储元素 * put一个元素,必须take取出一个元素出来,否则不能再put进去 */ public class SynchronousQueueDemo { public static void main(String[] args) { SynchronousQueue queue = new SynchronousQueue(); new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + " Put 111"); queue.put("111"); System.out.println(Thread.currentThread().getName() + " Put 222"); queue.put("222"); System.out.println(Thread.currentThread().getName() + " Put 333"); queue.put("333"); } catch (InterruptedException e) { e.printStackTrace(); } }, "T1").start(); new Thread(() -> { try { TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName() + " - Take " + queue.take()); TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName() + " - Take " + queue.take()); TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName() + " - Take " + queue.take()); } catch (InterruptedException e) { e.printStackTrace(); } }, "T2").start(); } }
10. 线程池
线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
● 线程池工作机制:
① 在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。
② 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
● 为什么使用?
多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过渡消耗系统资源,以及过渡切换线程的危险,从而可能导致系统资源的崩溃。这时,线程池就是最好的选择了。
10-1. 线程池的好处
① 降低系统资源消耗。通过重用已存在的线程,降低线程创建和销毁造成的消耗;
② 提高系统响应速度。当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
③ 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
④ 提供更强大的功能。延时执行、定时循环执行的策略等
10-2. java中提供的线程池
Executors类提供了4种不同的线程池:newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor、newScheduledThreadPool
① newCachedThreadPool():创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
可缓存线程池:
- 线程数无限制
- 有空闲线程则复用空闲线程,若无空闲线程则新建线程
- 终止并从缓存中移除那些已有 60 秒钟未被使用的线程
- 一定程序减少频繁创建/销毁线程,减少系统开销
创建方法:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
源码:
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
示例代码:
public class NewCachedThreadPoolTest { public static void main(String[] args) { // 创建一个可缓存线程池 ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { try { // sleep可明显看到使用的是线程池里面以前的线程,没有创建新的线程 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } cachedThreadPool.execute(new Runnable() { public void run() { // 打印正在执行的缓存线程信息 System.out.println(Thread.currentThread().getName() + "正在被执行"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } } }
② newFiexedThreadPool(int Threads):创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
定长线程池:
- 可控制线程最大并发数(同时执行的线程数)
- 超出的线程会在队列中等待
创建方法:
//nThreads => 最大线程数即 maximumPoolSize ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads); //threadFactory => 创建线程的方 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads, ThreadFactory threadFactory);
源码:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
示例代码:
public class NewFixedThreadPoolTest { public static void main(String[] args) { // 创建一个可重用固定个数的线程池 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3); for (int i = 0; i < 10; i++) { fixedThreadPool.execute(new Runnable() { public void run() { try { // 打印正在执行的缓存线程信息 System.out.println(Thread.currentThread().getName() + "正在被执行"); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } } }
③ SingleThreadExecutor():创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
适用于需要保证顺序执行各个任务。
单线程化的线程池:
- 有且仅有一个工作线程执行任务
- 所有任务按照指定顺序执行,即遵循队列的入队出队规则
创建方法:
ExecutorService singleThreadPool = Executors.newSingleThreadPool();
源码:
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
示例代码:
public class NewSingleThreadExecutorTest { public static void main(String[] args) { //创建一个单线程化的线程池 ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); for (int i = 0; i < 10; i++) { final int index = i; singleThreadExecutor.execute(new Runnable() { public void run() { try { //结果依次输出,相当于顺序执行各个任务 System.out.println(Thread.currentThread().getName() + "正在被执行,打印的值是:" + index); Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } } }
④ newScheduledThreadPool(int corePoolSize):创建一个定长线程池,支持定时及周期性任务执行。
适用于执行延时或者周期性任务。
定长线程池:
- 支持定时及周期性任务执行。
创建方法:
//nThreads => 最大线程数即maximumPoolSize ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);
源码:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } //ScheduledThreadPoolExecutor(): public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue()); }
示例代码:
public class NewScheduledThreadPoolTest { public static void main(String[] args) { //创建一个定长线程池,支持定时及周期性任务执行——延迟执行 ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5); //延迟1秒执行 /*scheduledThreadPool.schedule(new Runnable() { public void run() { System.out.println("延迟1秒执行"); } }, 1, TimeUnit.SECONDS);*/ //延迟1秒后每3秒执行一次 scheduledThreadPool.scheduleAtFixedRate(new Runnable() { public void run() { System.out.println("延迟1秒后每3秒执行一次"); } }, 1, 3, TimeUnit.SECONDS); } }
10-3. ThreadPoolExecutor(重要)
在阿里巴巴开发手册中,明确规定线程池不允许使用 Executors 去创建,所以我们需要使用 ThreadPoolExecutor 的方式创建线程池。

● ThreadPoolExecutor提供了四个构造函数:
// 五个构造参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } // 六个构造参数 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler); } // 六个构造参数 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler); } // 七个构造参数,本质的ThreadPoolExecutor() public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小 int maximumPoolSize,// 最大核心线程池大小 long keepAliveTime, // 超时了没有人调用就会释放 TimeUnit unit, // 超时单位 BlockingQueue<Runnable> workQueue, // 阻塞队列 ThreadFactory threadFactory, // 线程工厂:创建线程的,一般不用动 RejectedExecutionHandler handler){ // 拒绝策略 if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
● 线程池的主要参数:
① int corePoolSize:该线程池中核心线程数最大值
线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程
核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。
如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉
② int maximumPoolSize:该线程池中线程总数最大值
线程总数 = 核心线程数 + 非核心线程数。
③ long keepAliveTime:该线程池中非核心线程闲置超时时长
当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
如果设置allowCoreThreadTimeOut = true,则会作用于核心线程
④ TimeUnit uni:keepAliveTime的超时单位,TimeUnit是一个枚举类型
NANOSECONDS : 1微毫秒 = 1微秒 / 1000
MICROSECONDS : 1微秒 = 1毫秒 / 1000
MILLISECONDS : 1毫秒 = 1秒 /1000
SECONDS : 秒
MINUTES : 分
HOURS : 小时
DAYS : 天
⑤ BlockingQueue<Runnable> workQueue:阻塞队列
该线程池中的任务队列:维护着等待执行的Runnable对象
当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务
⑥ ThreadFactory threadFactory:创建线程的方式(一般用不上)
这是一个接口,你new他的时候需要实现他的 Thread newThread(Runnable r) 方法
⑦ RejectedExecutionHandler handler:拒绝策略,线程池和队列都满了,再加入线程会执行此策略。
这玩意儿就是抛出异常专用的,比如上面提到的两个错误发生了,就会由这个handler抛出异常,你不指定他也有个默认的
○ 代码演示
public class Demo01 { public static void main(String[] args) { ExecutorService threadExecutor = new ThreadPoolExecutor( 2, 5, 3, TimeUnit.SECONDS, new LinkedBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() // 丢弃任务,并抛出RejectedExecutionException异常 【 默认 】 ); try { // 最大承载:maximumPoolSize + workQueue,超过则抛出 RejectedExecutionException 异常 for (int i = 0; i < 9; i++) { // 使用线程池创建线程 threadExecutor.execute(() -> { System.out.println(Thread.currentThread().getName() + " => OK"); }); } } finally { // 线程池用完,程序结束,关闭线程池 threadExecutor.shutdown(); } } }
10-4. 线程池的四种拒绝策略
① ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。
如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
② ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务。
③ ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
使用此策略,可能会使我们无法发现系统的异常状态。
建议是一些无关紧要的业务采用此策略。
例如,本人的博客网站统计阅读量就是采用的这种拒绝策略。
④ ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
此拒绝策略,是一种喜新厌旧的拒绝策略。
是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
10-5. 如何配置线程池
● CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
● IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
● 混合型任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。
因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失
11. 异步函数式编程 - CompletableFuture
使用 Future 获得异步执行结果时,要么调用阻塞方法 get() ,要么轮询看 isDone() 是否为 true ,这两种方法都不是很好,因为主线程也会被迫等待。
从 Java 8 开始引入了 CompletableFuture ,它针对 Future 做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。
11-1. 创建 CompletableFuture 对象
CompletableFuture 提供了四个静态方法用来创建 CompletableFuture 对象
- public static CompletableFuture<Void> runAsync(Runnable runnable)
- public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
- public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
- public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
CompletableFuture<String> future = CompletableFuture.supplyAsync(()->{ return "hello world"; }); System.out.println(future.get()); //阻塞的获取结果 ''helllo world"
11-2. Demo
public class Demo01 { public static void main(String[] args) throws ExecutionException, InterruptedException { // 没有返回值的 runAsync 异步回调 // runAsync(); // 有返回值的 supplyAsync 异步回调 supplyAsync(); } // 没有返回值的 runAsync 异步回调 public static void runAsync() throws ExecutionException, InterruptedException {
// 创建异步执行任务 CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " runAsync => Void"); }); System.out.println("11111111"); // 获取阻塞执行结果 future.get(); } // 有返回值的 supplyAsync 异步回调 public static void supplyAsync() throws ExecutionException, InterruptedException { // 创建异步执行任务: CompletableFuture<Integer> supplyAsync = CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName() + " runAsync => Void"); // int i = 10 / 0; // 异常调用失败回调 return 2048; }); // 成功和失败的回调 Integer result = supplyAsync // 如果执行成功: .whenComplete((t, u) -> { System.out.println("t => " + t); // 正常的返回结果 System.out.println("u => " + u); // 错误描述信息:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero }) // 如果执行异常: .exceptionally((e) -> { System.out.println(e.getMessage()); return 404; // 错误的返回结果 }) // 获取执行结果 .get(); System.out.println("result => " + result); } }
12 深入单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
多种单例的特性:
| 单例模式 | 是否推荐 | 懒加载 | 反序列化单例 | 反射单例 | 克隆单例 | 性能、失效问题 |
| 饿汉式 | Eager加载推荐 | x | x | x | x | 类加载时就初始化,浪费内存 |
| 懒汉式(同步方法) | x | √ | x | x |
x |
存在性能问题,每次获取示例都会进行同步 |
| 双重检测锁(DCL) | 可用 | √ | x | x | x |
JDK < 1.5 失效 安全且在多线程情况下能保持高性能 |
| 静态内部类 | 推荐 | √ | x | x | x | 和双检锁差不多,但这种方式只适用于静态域的情况 |
| 枚举 | 最推荐 | x | √ | √ | √ |
JDK < 1.5 不支持 自动支持序列化机制,绝对防止多次实例化 |
12-1. 单例模式的几种实现方式
① 饿汉式:该模式在类被加载时就会实例化一个对象。
该模式能简单快速的创建一个单例对象,而且是线程安全的(只在类加载时才会初始化,以后都不会)。但它有一个缺点,就是不管你要不要都会直接创建一个对象,会消耗一定的性能(当然很小很小,几乎可以忽略不计,所以这种模式在很多场合十分常用而且十分简单)
// 饿汉式单例 public class Hungry { // 在类装载时就实例化 private static Hungry HUNGRY = new Hungry(); // 私有化构造方法 private Hungry() { } // 提供方法让外部获取实例 public static Hungry getInstance() { return HUNGRY; } }
这种做法很方便的帮我们解决了多线程实例化的问题,但是缺点也很明显。
因为这句代码 private static Hungry HUNGRY = new Hungry(); 的关系,所以该类一旦被jvm加载就会马上实例化!
那如果我们不想用这个类怎么办呢? 是不是就浪费了呢?既然这样,我们来看下替代方案! 懒汉式。
② 懒汉式:该模式只在你需要对象时才会生成单例对象(比如调用getInstance方法)
这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
public class LazyMan { private static LazyMan LAZY_MAN; private LazyMan() { } public synchronized static LazyMan getInstance() { if (LAZY_MAN== null) return new LazyMan(); return LAZY_MAN; } }
从线程安全性上讲,不加同步的懒汉式是线程不安全的,比如说:有两个线程,一个是线程A,一个是线程B,它们同时调用getInstance方法,那就可能导致并发问题。
所以只要加上 Synchronized 即可,但是这样一来,会降低整个访问的速度,而且每次都要判断,也确实是稍微慢点。
那么有没有更好的方式来实现呢?双重检查加锁,可以使用“双重检查加锁”的方式来实现,就可以既实现线程安全,又能够使性能不受到大的影响。
③ 双检锁/双重校验锁(DCL,即 double-checked locking):这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
双重检查加锁机制 并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查 。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
public class DoubleCheck { private volatile static DoubleCheck DOUBLE_CHECK; private DoubleCheck() { } public static DoubleCheck getInstance() { // 先检查实例是否存在,如果不存在才进入下面同步块 if (DOUBLE_CHECK == null) {
// 同步块,线程安全的创建实例 synchronized (DoubleCheck.class) { // 再次检查实例是否存在,如果不存在则创建实例
if (DOUBLE_CHECK == null) return new DoubleCheck(); } } return DOUBLE_CHECK; } }
volatile关键字:将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
注意:在Java1.4及以前版本中,很多JVM对于volatile关键字的实现有问题,会导致双重检查加锁的失败,因此双重检查加锁的机制只能用在Java5及以上的版本。
这种实现方式既可使实现线程安全的创建实例,又不会对性能造成太大的影响,它 只在第一次创建实例的时候同步,以后就不需要同步了,从而加快运行速度。
但 由于 volatile 关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高 ,因此一般建议,没有特别的需要,不要使用。
也就是说,虽然可以使用双重加锁机制来实现线程安全的单例,但并不建议大量采用,根据情况来选用吧。
④ 静态内部类:采用类级内部类,在这个类级内部类里面去创建对象实例,只要不使用到这个类级内部类,那就不会创建对象实例。
这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
当 getInstance 方法第一次被调用的时候,它第一次读取 SingletonHolder.instance ,导致 SingletonHolder 类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建 Singleton 的实例,由于是静态的域,因此只会被虚拟机在装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。
public class Holder { // 私有化构造器,保证外部的类不能通过构造器来实例化 private Holder() { } /** * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例 * 没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载 */ private static class InnerClass { // 静态初始化器,由JVM来保证线程安全 private static final Holder HOLDER = new Holder(); } // 获取单例对象实例 public static final Holder getInstance() { return InnerClass.HOLDER; } }
这个模式的优势在于,getInstance 方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。
⑤ 枚举:枚举实现单例是最为推荐的一种方法,因为它更简洁并且就算通过序列化,反射等也没办法破坏单例性
- Java的枚举类型实质上是功能齐全的类,因此可以有自己的属性和方法;
- Java枚举类型的基本思想:通过公有的静态final域为每个枚举常量导出实例的类;
- 从某个角度讲,枚举是单例的泛型化,本质上是单元素的枚举;
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
public enum EnumSingle { INSTANCE; public EnumSingle getInstance() { return INSTANCE; } }
使用枚举来实现单实例控制,会更加简洁,而且无偿的提供了序列化的机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。
13. 公平锁、非公平锁、重入锁、自旋锁
13-1. 公平锁、非公平锁
public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
1)是什么
公平锁就是先来后到、非公平锁就是允许加塞, Lock lock = new ReentrantLock(Boolean fair); 默认非公平。
-
公平锁:多个线程按照申请锁的顺序来获取锁,类似排队打饭。
-
非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者节现象。
2)两者区别
-
公平锁:Threads acquire a fair lock in the order in which they requested it
公平锁,就是很公平,在并发环境中,每个线程在获取锁时,会先查看此锁维护的等待队列,如果为空,或者当前线程就是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
-
非公平锁:a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lock happens to be available when it is requested.
非公平锁比较粗鲁,上来就直接尝试占有额,如果尝试失败,就再采用类似公平锁那种方式。
3)other
对 Java ReentrantLock 而言,通过构造函数指定该锁是否公平,默认是非公平锁,非公平锁的优点在于吞吐量比公平锁大。
对 Synchronized 而言,是一种非公平锁。
15-2. 重入锁(递归锁)
1)递归锁是什么
指的时同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块
2)ReentrantLock / Synchronized 就是一个典型的可重入锁
3)可重入锁最大的作用是避免死锁
4)代码示例
● synchronized
public class Demo01 { public static void main(String[] args) { Phone phone = new Phone(); new Thread(() -> { phone.sms(); }, "A").start(); new Thread(() -> { phone.call(); }, "B").start(); } } class Phone { // 锁 1 public synchronized void sms() { System.out.println(Thread.currentThread().getName() + " => sms"); try { // TimeUnit.SECONDS.sleep(3); // 线程睡眠,锁不释放 // wait(); // 线程等待,释放锁 } catch (Exception e) { e.printStackTrace(); } call(); // 锁2 - 这里也有锁 } // 锁 2 public synchronized void call() { System.out.println(Thread.currentThread().getName() + " => call"); // notify(); // 唤醒 A } }
● ReentrantLock
public class Demo02 { public static void main(String[] args) { Phone2 phone2 = new Phone2(); new Thread(() -> { phone2.sms(); }, "A").start(); new Thread(() -> { phone2.call(); }, "B").start(); } } class Phone2 { Lock lock = new ReentrantLock(); public void sms() { lock.lock(); // 🔒1 细节问题:lock.lock(); lock.unlock(); - lock锁必须配对(成对出现),否则会死在里面 lock.lock(); // 🔒2 try { System.out.println(Thread.currentThread().getName() + " => sms"); TimeUnit.SECONDS.sleep(3); // 睡眠3秒 call(); // 也有锁 } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); // 🔒1 lock.unlock(); // 🔒2 } } public void call() { lock.lock(); try { System.out.println(Thread.currentThread().getName() + " => call"); } finally { lock.unlock(); } } }
13-3. 自旋锁
自旋锁(Spin lock) 自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是 否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。其作用是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远 高于互斥锁。虽然它的效率比互斥锁高,但是它也有些不足之处:
- 自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。
- 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等。
因此我们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。
1)加锁原理
线程一直是 running(加锁 ---> 解锁) ,死循环检测锁的标志位,机制不复杂。 互斥锁属于 sleep-waiting 类型的锁。例如在一个双核的机器上有两个线程 (线程 A 和线程 B ) ,它们分别运行在 Core0 和 Core1 上。假设线程 A 想要通过 pthread_mutex_lock 操作去得到一个临界区的锁,而此时这个锁正被线程 B 所持有,那么线程 A 就会被阻塞 (blocking),Core0 会在此时进行上下文切换 (Context Switch) 将线程 A 置于等待队列中,此时 Core0 就可以运行其他的任务 (例如另一个线程 C ) 而不必进行忙等待。而自旋锁则不然,它属于 busy-waiting 类型的锁,如果线程 A 是使用 pthread_spin_lock 操作去请求锁,那么线程 A 就会一直在 Core0 上进行忙等待并不停的进行锁请求,直到得到这个锁为止。
2)应用
因为自旋锁是死循环检测,加锁全程消耗 cpu ,起始开销虽然低于互斥锁,但是随着持锁时间,加锁的开销是线性增长。
所有自旋锁主要用在临界区持锁时间非常短且CPU资源不紧张的情况下,自旋锁一般用于多核的服务器。
3)spinlock
自旋尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
4)手写自旋锁
public class SpinLockDemo { public static void main(String[] args) { // 底层使用的自旋锁 CAS SpinLock spinLock = new SpinLock(); new Thread(() -> { spinLock.myLock(); // 1 - A1获取锁 try { TimeUnit.SECONDS.sleep(3); // 持锁睡眠 } catch (InterruptedException e) { e.printStackTrace(); } finally { spinLock.myUnLock(); // 3 - 释放锁 } }, "A1").start(); try { TimeUnit.SECONDS.sleep(1); // 让出线层执行权 } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { spinLock.myLock(); // 2 - 自旋 等待A1释放锁,A1释放锁 S1获得 try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } finally { spinLock.myUnLock(); // 4 - 释放S1锁 } }, "S1").start(); } } class SpinLock { // 原子引用线程 AtomicReference<Thread> reference = new AtomicReference<>(); // 加锁 public void myLock() { Thread thread = Thread.currentThread(); // 获取当前的线程 System.out.println(Thread.currentThread().getName() + " => myLock"); // 自旋,期望值不为null -> 自旋 while (!reference.compareAndSet(null, thread)) { } } // 解锁 public void myUnLock() { Thread thread = Thread.currentThread(); // 获取当前的线程 System.out.println(Thread.currentThread().getName() + " => myUnLock"); reference.compareAndSet(thread, null); // 解锁 / 期望 !null 更新 null } }
14. 死锁编码及定位分析
14-1. 死锁是什么
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
graph TD threadA(线程A) threadB(线程B) lockA((锁A)) lockB((锁B)) threadA--持有-->lockA threadB--试图获取-->lockA threadB--持有-->lockB threadA--试图获取-->lockB
14-2. 产生死锁的主要原因
- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
14-3. 死锁示例
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去,
public class DeadLockDemo { public static void main(String[] args) { String lockA = "lockA"; String lockB = "lockB"; new Thread(new HoldThread(lockA,lockB),"Thread-AAA").start(); new Thread(new HoldThread(lockB,lockA),"Thread-BBB").start(); } } class HoldThread implements Runnable { private String lockA; private String lockB; public HoldThread(String lockA, String lockB) { this.lockA = lockA; this.lockB = lockB; } @Override public void run() { synchronized (lockA) { System.out.println(Thread.currentThread().getName() + "\t自己持有:" + lockA + "\t尝试获得:" + lockB); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockB) { System.out.println(Thread.currentThread().getName() + "\t自己持有:" + lockB + "\t尝试获得:" + lockA); } } } }
14-4. 解决死锁问题
1)使用 jps -l 定位进程号
2)jstack 进程号 找到死锁查看
3)jdk自带的 jconsole 工具

浙公网安备 33010602011771号