1.什么是JUC
1.JUC简介
JUC就是java.util.concurrent工具包的简称,这是一个处理线程的工具包,JDK1.5开始出现的。
2.进程和线程
- 进程:资源分配的最小单位
- 线程:也被称为轻量级进程,系统分配处理器时间资源的基本单元。一个Java程序中默认有两个线程,main线程和GC线程。
- 线程分为用户线程和守护线程,创建的线程默认是用户线程。
1. isDaemon()方法可以判断当前线程是否为守护线程 2. setDaemon(boolean)方法设置当前线程为守护线程
- 用户线程都是独立的,只要还存在运行着的用户线程,则JVM继续运行;当所有用户线程停止运行,守护线程也会随着JVM停止运行。
3.线程的状态
- 线程状态枚举类
A thread state. A thread can be in one of the following states:
1. NEW A thread that has not yet started is in this state.
2. RUNNABLE A thread executing in the Java virtual machine is in this state.
3. BLOCKED A thread that is blocked waiting for a monitor lock is in this state.
4. WAITING A thread that is waiting indefinitely for another thread to perform a particular action is in this state.
5. TIMED_WAITING A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.
6. TERMINATED A thread that has exited is in this state.
public enum State {
NEW, // 新建
RUNNABLE, // 准备就绪
BLOCKED, // 阻塞
WAITING, // 不见不散
TIMED_WAITING, // 过时不候
TERMINATED; // 终结
}
- wait和sleep方法的区别
1. 来自不同的类
sleep是Thread类的静态方法,而wait是Object的方法,任何对象实例都可以调用。
2. 关于锁的释放
sleep不会释放锁,也不需要占用锁。wait会释放锁,调用他的前提是当前线程占有锁。
3. 两者都可以被interrupted方法中断。
4. 使用的范围不同
sleep任何对象都可以调用。
4.管程
管程,又叫做Monitor(监视器),也就是锁。它是一种同步机制,保证同一时间只有一个线程访问被保护数据或者代码。JVM中的同步是根据进入和退出监视器对象(管程对象)来实现的
2.Lock接口
Lock接口的实现类有ReadLock,ReentrantLock,WriteLockd,ReentryReadWriteLock等。
1.synchronized关键字回顾
- 修饰一个代码块,则被修饰的代码块称为同步语句块。
// 同步代码块示例
synchronized(this) {
}
- 修饰一个方法,被修饰的方法称为同步方法。
- 示例:售票员售票
class Ticket { // 票的数量 private int number = 100; // 售票方法 public synchronized void sale() { if (number > 0) { System.out.println(Thread.currentThread().getName() + "正在出售第" + number-- + "张票"); } } } public class SaleTicket { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 60; i++) { ticket.sale(); } } }, "AAA").start(); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 60; i++) { ticket.sale(); } } }, "BBB").start(); } }
- synchronized是一种可重入锁,不可以中断,非公平。(可重入锁又叫做递归锁,可重入的意思就是某个线程获得了锁,再次获取不会出现死锁。)
public class Parent {
public synchronized void test() {
}
}
class Son extends Parent{
public synchronized void test() {
System.out.println(Thread.currentThread().getName() + "running!");
super.test();
}
public static void main(String[] args) {
// 执行test方法时已经获得了锁,在方法中再次获取锁,(锁是一个实例对象)
// 因为synchronized是可重入锁,所以不会发生死锁
new Son().test();
}
}
- synchronized关键字不能被继承。在子类中重写父类中的同步方法,如果不显式的在重写的方法上加上synchronized关键字,则该方法不是同步方法。
2.什么是Lock
- Lock与synchronized的区别:
- 采用synchronized不需要用户去手动释放锁,当同步方法或者同步代码块执行完之后或者发生异常,系统会自动让线程释放对锁的占用。而使用Lock需要用户去手动释放锁,如果没有主动释放锁,就可能导致出现死锁的现象。为了避免在发生异常时无法释放锁,需要在finally块中释放锁。
- synchronized无法获取锁的状态,Lock可以判断(比如说是否获取到了锁)。
- Lock是一个接口,而synchronized是Java中的关键字,它是内置的语言实现。
- synchronized是一种可重入锁,不可以中断,非公平。Lock,可重入锁,可以判断锁,设置是否公平。
// 使用可重入锁的示例,ReentrantLock是实现Lock的一个接口 class Ticket { // 创建可重入锁 private final ReentrantLock reentrantLock = new ReentrantLock(); private int number = 30; public void sale() { // 上锁 reentrantLock.lock(); try { if (number > 0) { System.out.println(Thread.currentThread().getName() + "正在出售" + number-- + "张票"); } } finally { // 解锁 reentrantLock.unlock(); } } } public class SaleTicket { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(()-> { for (int i = 0; i < 60; i++) { ticket.sale(); } }, "AAA").start(); new Thread(()-> { for (int i = 0; i < 60; i++) { ticket.sale(); } }, "BBB").start(); } }
- 创建线程的三种方式:
- 继承自Thread类
- 实现Runnable接口
- 使用Callable接口
- 使用线程池
3.线程间通信
1.使用wait/notify或者notifyAll方法
- 示例:两个线程,一个线程对初始值为0的值加一,另一个线程减一,交替进行。(生产者消费者问题)
class Share {
// 初始值为1
private int value = 0;
// 加一的方法
public synchronized void add() throws InterruptedException {
while (value != 0) {
this.wait();
}
value++;
System.out.println(Thread.currentThread().getName() + ":" + value);
// 唤醒其他线程
this.notifyAll();
}
// 减一的方法
public synchronized void subtract() throws InterruptedException {
while (value != 1) {
// wait方法在哪里睡在哪里醒
this.wait();
}
value--;
System.out.println(Thread.currentThread().getName() + ":" + value);
// 唤醒其他线程
this.notifyAll();
}
}
public class ThreadDemo {
public static void main(String[] args) {
Share share = new Share();
new Thread(()->{
for (int i = 0; i < 30; i++) {
try {
share.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AAA").start();
new Thread(()->{
for (int i = 0; i < 30; i++) {
try {
share.subtract();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BBB").start();
}
}
- 为了防止虚假唤醒问题,条件需要加到while循环中,而不能加到if判断中,因为if只进行了一次判断,就接着往下执行(虚假唤醒即唤醒了不该唤醒的线程来执行)
2.使用Condition接口中的await/signal或者signalAll方法
- 示例:多个线程交替对一个初始值为0的变量加一减一
class Share {
private int value = 0;
private final Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
// 加一的方法
public void add() throws InterruptedException {
// 上锁
lock.lock();
try {
// 不能使用if做判断,防止虚假唤醒的问题
while (value != 0) {
condition.await();
}
value++;
System.out.println(Thread.currentThread().getName() + ":" + value);
// 唤醒其他线程
condition.signalAll();
} finally {
// 释放锁
lock.unlock();
}
}
// 减一的方法
public void subtract() throws InterruptedException {
lock.lock();
try {
while (value != 1) {
condition.await();
}
value--;
System.out.println(Thread.currentThread().getName() + ":" + value);
condition.signalAll();
} finally {
lock.unlock();
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Share share = new Share();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AAA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.subtract();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BBB").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "CCC").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.subtract();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "DDD").start();
}
}
4.线程间定制化通信
- 示例:AA线程打印5次,BB线程打印10次,CC打印15次,一共进行10轮。(根据标志位决定打印5次、10次、15次)
class Share {
// 1:表示AA线程,2:BB,3:CC
private int falg = 1;
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void print5(int loop) throws InterruptedException {
lock.lock();
try {
while (falg != 1) {
c1.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ",i的值:" + i + "轮数:" + loop);
}
falg = 2;
// 唤醒BB线程
c1.signal();
} finally {
lock.unlock();
}
}
public void print10(int loop) throws InterruptedException {
lock.lock();
try {
while (falg != 2) {
c2.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ",i的值:" + i + "轮数:" + loop);
}
falg = 3;
// 唤醒CC线程
c2.signal();
} finally {
lock.unlock();
}
}
public void print15(int loop) throws InterruptedException {
lock.lock();
try {
while (falg != 3) {
c3.await();
}
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName() + ",i的值:" + i + "轮数:" + loop);
}
// 唤醒AA线程
falg = 1;
c3.signal();
} finally {
lock.unlock();
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Share share = new Share();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.print5(i + 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AAA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.print10(i + 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BBB").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.print15(i + 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "CCC").start();
}
}
5.集合的线程安全
1.集合ArrayList线程不安全演示及解决方案
- 演示
// java.util.ConcurrentModificationException
List<String> list = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
new Thread(()-> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}).start();
}
- 解决方案
- 使用Vector类:其add方法为同步方法。
- 使用Collections工具类的synchronizedList方法
List<String> list = Collections.synchronizedList(new ArrayList<>());
- 使用CopyOnWriteArrayList类
- 示例
// CopyOnWriteArrayList使用的写时复制技术。 List<String> list = new CopyOnWriteArrayList<>();
- 核心原理
// add方法加锁了,所有是线程安全的 public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { // 原数组 Object[] elements = getArray(); int len = elements.length; // 根据原数组复制出一个新数组 Object[] newElements = Arrays.copyOf(elements, len + 1); // 将待添加的元素添加到新数组末尾 newElements[len] = e; // 将新数组覆盖原数组 setArray(newElements); return true; } finally { lock.unlock(); } }
2.HashSet线程不安全演示及解决方案
- 演示:
// java.util.ConcurrentModificationException
Set<String> set = new HashSet<>();
for (int i = 1; i <= 100; i++) {
new Thread(()-> {
set.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(set);
}).start();
}
- 解决方案
- 使用CopyOnWriteArraySet类
Set<String> set = new CopyOnWriteArraySet<>();
3.HashMap线程不安全演示及解决方案
- 演示
// java.util.ConcurrentModificationException
Map<String, String> map = new HashMap<>();
for (int i = 1; i <= 100; i++) {
int finalI = i;
new Thread(()-> {
String key = String.valueOf(finalI);
map.put(key, UUID.randomUUID().toString().substring(0, 8));
System.out.println(map);
}).start();
}
- 解决方案
- 使用ConcurrentHashMap类
Map<String, String> map = new ConcurrentHashMap<>();
- 使用Colletions工具类中的方法
6.多线程锁
1.synchronized锁的特点
- Synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现如下三种形式:
- 对于普通同步方法,锁是当前调用同步方法的实例对象
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步代码块,锁是SynChronized括号里配置的对象。
2.公平锁和非公平锁
- 非公平锁:使用
ReentrantLock()
或者ReentrantLock(false)
构造函数创建的是非公平锁。效率高。 - 公平锁:使用
ReentrantLock(true)
构造函数创建的是公平锁。效率相对较低。
3.可重入锁(也可以叫做递归锁)
synchronized内部是可重入锁(隐式),Lock也是可重入锁(显式)。
// Thread-0外层
// Thread-0中层
// Thread-0内层
Object o = new Object();
new Thread(()-> {
synchronized (o) {
System.out.println(Thread.currentThread().getName() + "外层");
synchronized (o) {
System.out.println(Thread.currentThread().getName() + "中层");
synchronized (o) {
System.out.println(Thread.currentThread().getName() + "内层");
}
}
}
}).start();
4.死锁
- 什么是死锁:两个或者两个以上线程在执行过程中,因为争夺资源而造成一种互相等待的现象,如果没有外力干涉,他们无法再执行下去。
- 产生死锁原因:
- 系统资源不足
- 进程运行推进顺序不合适
- 资源分配不当
- 死锁的演示
Object o1 = new Object();
Object o2 = new Object();
new Thread(() -> {
synchronized (o1) {
System.out.println("线程AAA拥有锁o1,试图获取锁o2");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("已经获取到锁o2");
}
}
}, "AAA").start();
new Thread(() -> {
synchronized (o2) {
System.out.println("线程BBB拥有锁o2,试图获取锁o1");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("已经获取到锁o1");
}
}
}, "BBB").start();
- 死锁的验证:使用jps工具和jstack工具
7.Callable接口
1.Callable接口和Runable接口的区别
创建线程的第三种方案:实现Callable接口,重写带有返回值的call方法。Callable接口的特点:
- 实现Runable接口,需要实现没有返回值的run方法。而对于Callable接口,需要实现返回结果的call方法。
- call方法可以引发异常,而run方法不行。
2.Runnable实现类之一FutureTask
- 实现Callable接口创建线程的示例1
class MyThread implements Callable {
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName());
return 1024;
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
// FutureTask的一个构造函数接收Callable实现类对象
FutureTask<Integer> futureTask1 = new FutureTask<>(new MyThread());
// 使用Lambda表达式
FutureTask<Integer> futureTask2 = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName());
return 2048;
});
// 创建线程
new Thread(futureTask2).start();
new Thread(futureTask1).start();
// FutureTask的get方法获取call方法的返回值
try {
System.out.println(futureTask2.get());
System.out.println(futureTask1.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
- 实现Callable接口的示例2:第一个线程计算1+2+...+10;第二个线程计算11+12...20
public static void main(String[] args) {
FutureTask<Integer> task1 = new FutureTask<>(() -> {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
return sum;
});
FutureTask<Integer> task2 = new FutureTask<>(() -> {
int sum = 0;
for (int i = 11; i <= 20; i++ ) {
sum += i;
}
return sum;
});
new Thread(task1).start();
new Thread(task2).start();
// 主线程汇总
try {
int sum = task1.get() + task2.get();
System.out.println("sum:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
总结:创建线程的四种方式:
- 继承Thread类
- 实现Runable接口
- 实现Callable接口
- 线程池
8.JUC中强大的辅助类
循环栅栏,减法计数器,信号灯
1.CountDownLatch:减少计数
- CountDownLatch类可以设置一个计数器,然后通过countDown方法来进行减一的操作,使用await方法等待计数器不大于0(等待计数器为0),然后继续执行await方法之后的语句。该类主要有两个方法:
- await:当一个或者多个线程调用await方法时,这些线程会阻塞。
- countDown:调用countDown方法会将计数器减一(调用countDown方法的线程不会阻塞 ),当计数器的值为0时,因为await方法阻塞的线程会被唤醒,继续执行。
- 示例:六个同学陆续离开教室后,班长锁门
// 创建CountDownLatch对象,设置初始值
CountDownLatch count = new CountDownLatch(6);
// 六个同学陆续离开教室
for (int i = 1; i <= 6; i++ ) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "号同学离开了教室");
count.countDown();
}).start();
}
// 当计数器为0时,主线程main被唤醒,继续向下执行
// 当计数器不为0时,await方法等待计数器归零,主线程阻塞
count.await();
System.out.println(Thread.currentThread().getName() + "班长离开了教师");
2.CyclicBarrier:循环栅栏
- CyclicBarrier的构造方法的第一个参数是目标障碍数,每次执行CyclicBarrier一次障碍数会加一,如果达到了目标障碍数,就会执行cyclicBarrier.await之后的语句。
- 示例:集齐七颗龙珠就可以召唤神龙
private static final int NUMBER = 7;
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, () -> {
System.out.println("七颗龙珠集齐完毕,可以召唤神龙");
});
for (int i = 1; i <= 7; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "龙珠被收集到了");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
3.semaph:信号灯
- 作用:多个共享资源互斥的使用,并发限流,控制最大的线程数。
- 示例:六辆汽车,停三个停车位
// 创建Semaphore,设置许可数量
Semaphore semaphore = new Semaphore(3);
// 模拟六辆汽车
for (int i = 1; i <= 6; i++) {
new Thread(()-> {
try {
//抢占
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢占到了车位");
// 设置随机停车时间
TimeUnit.SECONDS.sleep(new Random(5).nextLong());
System.out.println(Thread.currentThread().getName() + "离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放
semaphore.release();
}
}, String.valueOf(i)).start();
}
9.读写锁ReentrantReadWriteLock
- 读锁:共享锁,会发生死锁。比如说两个线程,1线程修改,需要等到2线程读之后。2线程修改,需要等到1线程读之后。
- ReentrantReadWriteLock的ReadLock()方法获取读锁
- 写锁:独占锁,写入的时候只有一个线程写,会发生死锁。
- ReentrantReadWriteLock的WriteLock()方法获取写锁
// 资源类
class Cache {
// 创建Map集合
private Map<String, Object> map = new HashMap<>();
// 创建读写锁
ReadWriteLock lock = new ReentrantReadWriteLock();
// 放数据
public void put(String key, Object value) {
// 上锁
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在执行写操作" + key);
TimeUnit.SECONDS.sleep(1);
// 放数据
System.out.println(Thread.currentThread().getName() + "写完了" + key);
map.put(key, value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.writeLock().unlock();
}
}
// 取数据
public Object get(String key) {
Object result = null;
// 上锁
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在执行读操作" + key);
TimeUnit.SECONDS.sleep(1);
result = map.get(key);
System.out.println(Thread.currentThread().getName() + "读完了" + key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.readLock().unlock();
}
return result;
}
}
public class ReadWriteDemo {
public static void main(String[] args) {
Cache cache = new Cache();
// 创建线程放数据
for (int i = 1; i <= 5; i++) {
int finalI = i;
new Thread(() -> {
cache.put(finalI + "",finalI);
},String.valueOf(i)).start();
}
// 创建线程取数据
for (int i = 1; i <= 5; i++) {
int finalI = i;
new Thread(() -> {
cache.get(finalI + "");
},String.valueOf(i)).start();
}
}
}
- 读写锁ReentrantReadWriteLock:一个资源可以被多个线程访问,或者可以被一个写线程访问,但是不能同时存在读写线程,读写互斥,读读共享的。
- 读写锁的演变如下:
- 无锁:多个线程抢夺资源
- 添加锁:比如说使用synchronized和ReentrantLock,都是独占的,读读、读写、写写,每次只能来一个线程操作。
- 读写锁:比如ReentrantReadWriteLock,对于读读操作可以共享(同时多个线程进行读操作),提升性能。写写操作每次只能来一个线程。缺点是容易造成锁饥饿,一直读没有写操作。
- 读写锁的降级:将写入锁降为读锁。JDK8说明:获取写锁、再获取读锁、然后释放写锁、最后释放读锁。读锁不能升级为写锁。
// 可重入读写锁对象
ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
// 锁降级
// 1.获取写锁
rwlock.writeLock().lock();
System.out.println("write...");
// 2.获取读锁
rwlock.readLock().lock();
System.out.println("read...");
// 3.释放写锁
rwlock.writeLock().unlock();
// 4.释放读锁
rwlock.readLock().unlock();
10.阻塞队列BlockingQueue
1.阻塞队列的概述
阻塞队列也是一个队列。试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素;试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空。
2.阻塞队列的分类
BlockingQueue接口提供了很多实现类。
- ArrayBlockingQueue:生产者和消费者获取数据,都是共用同一个锁对象,两者无法并行运行。底层由数组实现的有界阻塞双向队列。
- LinkedBlockingQueue:在生产者和消费者端分别采用独立的锁来控制数据同步,这样在高并发的情况下生产者和消费者可以并行的操作队列中的数据,提高队列的并发性能。基于链表实现的有界阻塞队列。
- DelayQueue:当指定的延迟时间到了,才能从队列中获取元素。因为这个队列是无界的,往队列中插入数据的操作不会阻塞,只有获取数据的操作才会阻塞。一个使用优先级队列实现的延迟无界阻塞队列。
- PriorityBlockingQueue:这个队列不会阻塞数据生产者,而可能会在没有可消费的数据时,阻塞数据的消费者。一个支持优先级排序的无界阻塞队列。优先级的判断通过构造函数传入的Comparator实现类对象决定。
- SynchronousQueue:一种无缓冲的不存储元素的阻塞队列。 该同步队列没有容量,进去一个元素,必须等待取出来后,才能再往里面放一个元素。
BlockingQueue<String> queue = new SynchronousQueue<>();
new Thread(() -> {
try {
queue.put("111");
System.out.println(Thread.currentThread().getName() + ":put1");
queue.put("222");
System.out.println(Thread.currentThread().getName() + ":put2");
queue.put("333");
System.out.println(Thread.currentThread().getName() + ":put3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "AAA").start();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + ":" + queue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + ":" + queue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + ":" + queue.take());
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "BBB").start();
- LinkedTransferQueue:一个由链表组成的无界阻塞队列。
3.阻塞队列的核心方法
BlockingQueue queue = new ArrayBlockingQueue(3);
System.out.println(queue.add(1));
System.out.println(queue.add(2));
System.out.println(queue.add(3));
// IllegalStateException
// System.out.println(queue.add(4));
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
// NoSuchElementException
System.out.println(queue.remove());
11.线程池ThreadPool
Java中的线程池由Executor框架实现的,主要由以下类实现:
1.线程池的使用方式(三大方法)
- 一池N线程:使用
Executors.newFixedThreadPool(int)
创建,缺点:底层由LinkedBlockingQueue实现,允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
// 一池五线程
ExecutorService threadPool = Executors.newFixedThreadPool(5);
// 10个顾客请求
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "正在办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
// pool-1-thread-1正在办理业务
// pool-1-thread-4正在办理业务
// pool-1-thread-2正在办理业务
// pool-1-thread-3正在办理业务
// pool-1-thread-5正在办理业务
// pool-1-thread-3正在办理业务
// pool-1-thread-2正在办理业务
// pool-1-thread-4正在办理业务
// pool-1-thread-1正在办理业务
// pool-1-thread-5正在办理业务
- 一个任务一个任务执行,一池一线程:使用
Executors.newSingleThreadExecutor()
创建,缺点:==底层由LinkedBlockingQueue实现,允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
// 一池一线程
ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 10个顾客请求
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "正在办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
// pool-1-thread-1正在办理业务
// pool-1-thread-1正在办理业务
// pool-1-thread-1正在办理业务
// pool-1-thread-1正在办理业务
// pool-1-thread-1正在办理业务
// pool-1-thread-1正在办理业务
// pool-1-thread-1正在办理业务
// pool-1-thread-1正在办理业务
// pool-1-thread-1正在办理业务
// pool-1-thread-1正在办理业务
- 线程池根据需求创建线程,可扩容,遇强则强:使用
Executors.newCachedThreadPool()
创建,缺点:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
// 一池可扩容线程
ExecutorService threadPool = Executors.newCachedThreadPool();
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "正在办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
// pool-1-thread-10正在办理业务
// pool-1-thread-7正在办理业务
// pool-1-thread-3正在办理业务
// pool-1-thread-9正在办理业务
// pool-1-thread-1正在办理业务
// pool-1-thread-2正在办理业务
// pool-1-thread-8正在办理业务
// pool-1-thread-5正在办理业务
// pool-1-thread-6正在办理业务
// pool-1-thread-4正在办理业务
2.ThreadPoolExecutor构造函数的七个参数
ThreadPoolExecutor(int corePoolSize, //(核心线程数即最小线程数)
int maximumPoolSize,// 最大线程数量
long keepAliveTime, // 线程存活时间
TimeUnit unit, // 存活的时间单位
BlockingQueue<Runnable> workQueue,// 等待队列,存放已提交但未执行的任务
ThreadFactory threadFactory, // 创建线程的工厂
RejectedExecutionHandler handler) // 阻塞队列满后的拒绝策略
3.线程池的工作流程和拒绝策略
- JDK内置的拒绝策略如下:
- AbortPolicy:线程池的默认拒绝策略。丢弃任务,并且抛出
RejectedExecutionException
异常。 - DiscardPolicy:直接丢弃任务,不做任何处理包括抛出异常。
- DiscardOldestPolicy:当触发拒绝策略时,只要线程池还没有关闭,将丢弃阻塞队列中等待时间最长的一个任务,加入新任务。
- CallerRunsPolicy:将某些任务回退给调用者。
- AbortPolicy:线程池的默认拒绝策略。丢弃任务,并且抛出
- 何时触发拒绝策略:当提交的任务数大于corePoolSize的时候,会先将任务放到阻塞队列中,当阻塞队列饱和后,会扩充线程池中的线程数,直到达到maximumPoolSize最大线程数配置。此时再有任务到来,就会触发线程池的拒绝策略。
- 线程池的工作流程:
4.自定义线程池
- 阿里巴巴开发手册建议创建线程池使用ThreadPoolExecutor构造函数实现,自定义一个线程池示例如下:
ThreadPoolExecutor executor = new ThreadPoolExecutor(2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
try {
for (int i = 0; i < 10; i++) {
executor.execute(()-> {
System.out.println(Thread.currentThread().getName() + "正在办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
12.Fork/Join分支合并框架
- Fork/Join框架的特点:可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果。
- Fork:将一个复杂任务进行分拆,大事化小
- Join:将分拆任务的结果进行合并
- 在Java中的Fork/Join框架中,使用两个类完成上述操作。
Future
- Future接口定义了操作异步任务执行的一些方法,比如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。Future接口可以为主线程开一个分支任务,专门为主线程处理耗时和费力的复杂业务。它是Java5新加的一个接口,提供了一种异步并行计算的功能。
FutureTask
-
FutureTask是Future的一个实现类,FutureTask的继承体系图如下
-
FutureTask的简单示例
/**
* 三个任务开启多个异步任务线程处理耗时比只有一个main线程处理三个任务少很多
*/
public class FutureTaskDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
long startTime = System.currentTimeMillis();
FutureTask<String> task1 = new FutureTask<String>(()->{
TimeUnit.MILLISECONDS.sleep(500);
return "task1 over!";
});
threadPool.submit(task1);
FutureTask<String> task2 = new FutureTask<String>(()->{
TimeUnit.MILLISECONDS.sleep(300);
return "task2 over!";
});
threadPool.submit(task2);
FutureTask<String> task3 = new FutureTask<String>(()->{
TimeUnit.MILLISECONDS.sleep(200);
return "task3 over!";
});
threadPool.submit(task3);
System.out.println(task1.get());
System.out.println(task2.get());
System.out.println(task3.get());
long endTime = System.currentTimeMillis();
System.out.println("cost time:" + (endTime - startTime)); // 537
threadPool.shutdown();
}
}
- FutureTask的缺点
- 主线程调用FutureTask的get方法获取异步任务的处理结果时可能会阻塞主线程main的执行。解决方法:使用get方法的重载版本
// 过期不候,超过3秒抛出超时异常,这样防止阻塞主线程main的执行 task.get(3, TimeUnit.SECONDS)
- 主线程调用isDone方法判断异步任务是否执行完毕再取值,会耗费无谓的CPU资源,而且未必可以及时的得到计算结果。
public class FutureTaskBlock { public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException { FutureTask<String> task = new FutureTask<String>(()->{ System.out.println("executing!"); TimeUnit.SECONDS.sleep(5); return "task over!"; }); Thread thread = new Thread(task, "thread1"); thread.start(); while (true) { if (task.isDone()) { System.out.println(task.get()); break; } else { TimeUnit.MILLISECONDS.sleep(500); System.out.println("正在处理中!"); } } System.out.println("主线程" + Thread.currentThread().getName() + "执行完毕!"); } }
CompletableFuture(和FutureTask相比,推荐使用)
-
JDK8出现的Completable提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。Completable的继承图如下:
-
CompletionStage接口:代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另一个阶段。
-
CompletableFuture中创建异步任务的四大静态方法
- 没有返回值的异步任务使用runAsync()方法
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> { // 不指定线程池则使用默认的,ForkJoinPool.commonPool-worker-9 System.out.println(Thread.currentThread().getName()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } }); // null System.out.println(completableFuture.get());
- 有返回值的异步任务使用supplyAsync()方法
ExecutorService threadPool = Executors.newFixedThreadPool(3); CompletableFuture<Object> completableFuture = CompletableFuture.supplyAsync(()-> { // 指定线程池,pool-1-thread-1 System.out.println(Thread.currentThread().getName()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } return "test"; }, threadPool); // test System.out.println(completableFuture.get());
-
CompletableFuture是Future的增强版,能够减少阻塞和轮询。
ExecutorService threadPool = Executors.newFixedThreadPool(3);
try {
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()-> {
int result = ThreadLocalRandom.current().nextInt(10);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("一秒钟以后出结果:" + result);
// 模拟异常
if (result > 2) {
int i = result / 0;
}
return result;
}, threadPool).whenComplete((v, e) -> {
if (e == null) {
System.out.println("计算完成,更新系统" + v);
}
}).exceptionally(e -> {
e.printStackTrace();
System.out.println("异常情况:" + e.getCause() + "\t" + e.getMessage());
return null;
});
System.out.println("主线程先去执行其他任务");
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
- 消费处理结果:使用thenAccept方法可以接收任务的处理结果,并消费处理。
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> {
int number = 0;
System.out.println("执行加和任务!");
for (int i = 1; i <= 10; i++) {
number += i;
}
return number;
}).thenApply(number -> number * number)
.thenAccept(number -> {
System.out.println("子线程全部处理完成,最后结果为:" + number);
});
}
- 异常处理:handle方法和exceptionally都可以异常处理。
- 结果合并:
- thenCompose方法用于合并两个有依赖关系的CompletableFutures的执行结果。
- thenCombine合并两个没有依赖关系的CompletableFutures的执行结果。
- 合并多个任务的结果使用allOf和anyOf方法
- CompletableFuture的优点
- 异步任务结束时和异步任务出错时,会自动回调某个对象的方法,通过回调可以在主线程中得到异步任务的执行状态或者异常信息等。
- 主线程设置好回调之后,不再关心异步任务的执行,异步任务之间可以顺序执行。异步任务运行在与主线程分离的其他线程中。
13.volatile
- 保证可见性
// 不加volatile关键字,程序陷入死循环
// 加上volatile关键字,保证可见性,即主线程main对于number的修改
// 线程AAA可见
private volatile static int number = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(()-> {
while (number == 0) {
}
}, "AAA").start();
TimeUnit.SECONDS.sleep(1);
number = 2;
System.out.println(number);
}
- 不保证原子性
// volatile不能保证原子性,所以最终结果可能不是20000(synchronized关键字可以)
private volatile static int number = 0;
public static void add() {
number++;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 20; i++) {
new Thread(()-> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
// 上面20个线程还未执行完
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(number);
}
扩展:如果不加lock和使用synchronized关键字,如何保证原子性? 使用原子类。
3. 禁止指令重排:原理是内存屏障
14.单例模式
单例模式是设计模式之一,指的是一个类在运行期间只有一个实例。
1.饿汉式单例
- 优点:线程安全
- 缺点:可能造成资源浪费,因为可能单例对象创建好却没有使用。
class Singleton {
private static Singleton instance = new Singleton();
// 构造函数私有化
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
2.懒汉式单例
- 优点:不会造成资源的浪费。
- 缺点:线程不安全,可能会创建多个实例。
class Singleton{
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
// 线程不安全,两个线程创建的实例不一样
public class SingletonTest {
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(()-> {
System.out.println(Singleton.getInstance());
}).start();
}
}
}
- 保证懒汉式单例的线程安全:使用双重检测锁(DCL,Double Checked Lock)
- volatile关键字的作用:volatile关键字防止CPU指令重排,因为
instance = new Singleton();
不是一个原子操作。由于存在指令重排,线程AAA可能在执行new操作时,可能先将分配的内存空间地址赋值给引用变量,再调用构造函数初始化实例对象。这个时候时间片结束切换到线程BBB执行,执行if (instance == null)
判断不为空将会得到一个已经分配内存地址但是还没有初始化的对象。 - 指令重排
// 完成创建一个对象的步骤 // 1.在堆上分配空间创建一个新的未初始化的对象 new //2.调用实例初始化方法进行对象初始化 invokespecial // 3.将步骤1中分配的内存空间的地址赋值给引用变量 putstatic // 指令重排就是23两个步骤可能重排
- volatile关键字的作用:volatile关键字防止CPU指令重排,因为
class Singleton {
private static volatile Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
public class SingletonTest {
public static void main(String[] args) {
new Thread(()-> {
System.out.println(Thread.currentThread().getName() + Singleton.getInstance());
}, "AAA").start();
new Thread(()-> {
System.out.println(Thread.currentThread().getName() + Singleton.getInstance());
}, "BBB").start();
}
}
3.使用静态内部类
- 使用静态内部类创建单例和饿汉式单例的区别:
- 不同点:静态内部类不会造成资源的浪费,因为只有在调用getInstance()方法时,才会装载内部类从而完成实例的初始化。
- 相同点:线程安全
class Singleton {
// 静态内部类
private static class InnerSingleton {
private static final Singleton instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return InnerSingleton.instance;
}
}
4.使用枚举类
- 使用枚举类创建单例,不仅线程安全,而且不会造成资源的浪费。
class Singleton {
private enum SingletonEnum {
// SingletonEnum类型的对象
INSTANCE;
private final Singleton instance;
SingletonEnum() {
instance = new Singleton();
}
private Singleton getInstance() {
return instance;
}
}
public static Singleton getInstance() {
return SingletonEnum.INSTANCE.getInstance();
}
}
推荐使用4方式创建单例,因为反射不能破坏枚举