1. 前提
所谓 JUC,其实就是 java.until.concurrent 工具包的简称。在阅读本篇博客之前,可以阅读 【Java基础】多线程 回顾以下基础知识点。
进程、线程
Java的1个进程默认有2个线程:main、GC
并发、并行
并发编程的本质:充分利用CPU的资源
线程状态
- NEW:新生
- RUNNABLE:运行
- BLOCKED:阻塞
- WAITING:无限等待
- TIME_WAITING:超时等待
- TERMINATED:终止
wait 和 sleep 的区别
1、来自不同的类
wait => Object
sleep => Thread
2、关于CPU资源和锁资源的释放
wait会释放锁,会释放CPU资源
sleep睡觉了,抱着锁睡觉,不会释放锁!但是会释放CPU资源!
3、使用的范围不同
wait 必须在同步方法或者同步代码块中
sleep 可以在任何地方睡觉
2. Lock锁(重点)
2.1 传统的synchronized
//祖传卖票例子
public class SaleTicketDemo01 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
//启动3个线程
new Thread(()->{
for (int i = 1; i < 40; i++) {
ticket.sale();
}
},"A").start();
new Thread(()->{
for (int i = 1; i < 40; i++) {
ticket.sale();
}
},"B").start();
new Thread(()->{
for (int i = 1; i < 40; i++) {
ticket.sale();
}
},"C").start();
}
}
//资源类 OOP
class Ticket {
//属性
private int nums = 30;
//方法
public synchronized void sale(){
if(nums > 0){
System.out.println(Thread.currentThread().getName() + "拿到了第" + nums-- + "张票");
}
}
}
运行结果:
A拿到了第30张票
A拿到了第29张票
A拿到了第28张票
A拿到了第27张票
A拿到了第26张票
B拿到了第25张票
B拿到了第24张票
B拿到了第23张票
B拿到了第22张票
B拿到了第21张票
B拿到了第20张票
B拿到了第19张票
B拿到了第18张票
B拿到了第17张票
B拿到了第16张票
B拿到了第15张票
B拿到了第14张票
B拿到了第13张票
B拿到了第12张票
B拿到了第11张票
B拿到了第10张票
B拿到了第9张票
B拿到了第8张票
B拿到了第7张票
B拿到了第6张票
B拿到了第5张票
B拿到了第4张票
B拿到了第3张票
B拿到了第2张票
B拿到了第1张票
2.2 Lock接口
所有已知实现类:
- ReentrantLock 可重入锁(常用)
- ReentrantReadWriteLock.ReadLock 读锁
- ReentrantReadWriteLock.WriteLock 写锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//祖传卖票例子
public class SaleTicketDemo02 {
public static void main(String[] args) {
Ticket2 ticket2 = new Ticket2();
//启动3个线程
new Thread(() -> { for (int i = 1; i < 40; i++) ticket2.sale(); }, "A").start();
new Thread(() -> { for (int i = 1; i < 40; i++) ticket2.sale(); }, "B").start();
new Thread(() -> { for (int i = 1; i < 40; i++) ticket2.sale(); }, "C").start();
}
}
//Lock锁
class Ticket2 {
private int nums = 30;
//1. 创建锁
Lock lock = new ReentrantLock();
public void sale(){
//2. 加锁
lock.lock();
try{
//3. 业务代码
if(nums > 0){
System.out.println(Thread.currentThread().getName() + "拿到了第" + nums-- + "张票");
}
} finally {
//4. 解锁
lock.unlock();
}
}
}
2.3 Synchronized、Lock 的区别
- Synchronized 内置的 Java 关键字,Lock 是一个 Java 类;
- Synchronized 无法得知是否获取锁成功,Lock可以判断是否获取了锁;
- Synchronized 会自动释放锁,Lock必须手动释放锁!
- Synchronized 如果线程获取不到锁,就傻傻地等;Lock锁就不一定会等待下去;Lock的
tryLock()
方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。 - Synchronized 可重入锁,不可以中断的,非公平锁;Lock 可重入锁,可以中断锁,默认非公平锁(可以自己设置);
- Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!
3. 生产者和消费者问题
3.1 生产者和消费者问题(Synchronized版本)
/**
* 线程之间的通信问题:生产者和消费者问题! 等待唤醒,通知唤醒
* 线程交替执行 A B 操作同一个变量 num = 0
* A num+1
* B num-1
*/
public class A {
public static void main(String[] args) {
Data data = new Data();
//生产者1号
new Thread(()->{
try {
for (int i = 0; i < 3; i++) {
data.increment();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
//生产者2号
new Thread(()->{
try {
for (int i = 0; i < 3; i++) {
data.increment();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"B").start();
//消费者1号
new Thread(()->{
try {
for (int i = 0; i < 3; i++) {
data.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"C").start();
//消费者2号
new Thread(()->{
try {
for (int i = 0; i < 3; i++) {
data.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"D").start();
}
}
// 判断是否需要等待 -> 业务 -> 通知!!!!
class Data { // 数字 资源类
private int num = 0;
// +1
public synchronized void increment() throws InterruptedException {
// 第一步 判断是否需要等待
while(num != 0) {
//等待
this.wait();
}
//第二步 业务代码
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
//第三步 通知其他线程,我 +1 完毕了
this.notifyAll();
}
// -1
public synchronized void decrement() throws InterruptedException {
// 第一步 判断是否需要等待
while (num == 0) {
//等待
this.wait();
}
//第二步 业务代码
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
//第三步 通知其他线程,我 -1 完毕了
this.notifyAll();
}
}
运行结果:
A=>1
C=>0
B=>1
C=>0
A=>1
C=>0
B=>1
D=>0
A=>1
D=>0
B=>1
D=>0
3.2 虚假唤醒
在不被通知、中断或者超时的情况下,线程也可以被唤醒,即所谓的虚假唤醒。
把上面代码的 increment()
和 decrement()
方法中的 while 改为 if,就会出现虚假唤醒情况。执行结果如下:
A=>1
C=>0
B=>1
A=>2
B=>3
C=>2
C=>1
D=>0
B=>1
A=>2
D=>1
D=>0
为什么 while 改为 if 之后就会出现虚假唤醒情况呢?
在上述代码中,有两个生产者线程:A和B,两个消费者线程:C和D。正常情况下,num不是0就是1。但如果使用 if,会出现像下面这种情况:
- 一开始 num 是0,A抢到了锁,开始生产;B、C、D都处于等待状态。
- A生产完毕,num变为1,唤醒所有处于等待状态的线程。
- B、C、D都被唤醒,然后去抢锁,B抢到了,C、D没抢到会继续等待。A如果还想继续生产,也会进入等待。
- 这时候 num == 1,照理说B应该是无法继续生产的,因为缓冲区已经放满了,但由于使用的是 if 语句,B之前是判断了 if 条件才会 wait() ,现在被唤醒了,并不会再回头判断 if 条件,而是继续往下执行,所以B会继续生产,从而出现了num为2的情况。
总结
发生虚假唤醒的原因:在 if 块中使用 wait()
方法。
解决方法:凡是有 wait()
的地方,都使用while循环来做。
3.3 生产者和消费者问题(JUC版本)
通过 Lock 找到 Condition
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class B {
public static void main(String[] args) {
Data2 data = new Data2();
//生产者1号
new Thread(()->{
try {
for (int i = 0; i < 3; i++) {
data.increment();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
//生产者2号
new Thread(()->{
try {
for (int i = 0; i < 3; i++) {
data.increment();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"B").start();
//消费者1号
new Thread(()->{
try {
for (int i = 0; i < 3; i++) {
data.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"C").start();
//消费者2号
new Thread(()->{
try {
for (int i = 0; i < 3; i++) {
data.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"D").start();
}
}
class Data2 { // 数字 资源类
private int num = 0;
//1. 创建锁
Lock lock = new ReentrantLock();
//2. 获取condition实例
Condition condition = lock.newCondition();
// 生产
public void increment() throws InterruptedException {
//3. 加锁
lock.lock();
try {
//4. 业务代码
while (num != 0) {
//等待
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
//通知其他线程,我 +1 完毕了
condition.signalAll();
} finally {
//5. 解锁
lock.unlock();
}
}
// 消费
public void decrement() throws InterruptedException {
lock.lock();
try {
while (num == 0) {
//等待
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
// 通知其他线程,我 -1 完毕了
condition.signalAll();
} finally {
lock.unlock();
}
}
}
运行结果:
A=>1
C=>0
A=>1
C=>0
A=>1
C=>0
B=>1
D=>0
B=>1
D=>0
B=>1
D=>0
3.4 Condition 精准地通知和唤醒线程
轮流调用A、B、C三个线程。
代码写法模板:
- 创建锁:
Lock lock = new ReentrantLock();
- 创建Condition实例:
Condition condition = lock.newCondition();
- 上锁:
lock.lock();
- 编写业务代码:判断等待
condition.await();
—> 业务代码 —> 唤醒condition.signal()
- 解锁:
lock.unlock()
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* A执行完调用B,B执行完调用C,C执行完调用A
*/
public class C {
public static void main(String[] args) {
Data3 data3 = new Data3();
new Thread(()->{ for (int i = 0; i < 5; i++) data3.printA(); },"A").start();
new Thread(()->{ for (int i = 0; i < 5; i++) data3.printB(); },"B").start();
new Thread(()->{ for (int i = 0; i < 5; i++) data3.printC(); },"C").start();
}
}
class Data3 {
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int number = 1;//1A 2B 3C
public void printA() {
lock.lock();
try{
while (number != 1){
condition1.await();//A沉睡
}
System.out.println(Thread.currentThread().getName() + "-> B");
number = 2;
condition2.signal();//唤醒B
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try{
while (number != 2){
condition2.await();
}
System.out.println(Thread.currentThread().getName() + "-> C");
number = 3;
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try{
while (number != 3){
condition3.await();
}
System.out.println(Thread.currentThread().getName() + "-> A");
number = 1;
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
运行结果:
A-> B
B-> C
C-> A
A-> B
B-> C
C-> A
A-> B
B-> C
C-> A
A-> B
B-> C
C-> A
A-> B
B-> C
C-> A
4. 锁
4.2 结论
如何判断锁的是谁?
- 普通同步方法 -----> 当前实例对象
- 静态同步方法 -----> 当前类的Class对象
- 同步方法块 -----> synchronized括号里配置的对象
理解结论,就可以回答出来下面的8个问题了。
4.2 看代码回答问题
import java.util.concurrent.TimeUnit;
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{phone.send();},"A").start();
TimeUnit.SECONDS.sleep(1);//休眠1秒
new Thread(()->{phone.call();},"B").start();
}
}
class Phone {
public synchronized void send(){
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
第1问:标准情况下,是先打印 “发短信” 还是 “打电话”?
答:先发短信,因为线程A最先获得锁,所以它最先打印。
import java.util.concurrent.TimeUnit;
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{phone.send();},"A").start();
TimeUnit.SECONDS.sleep(1);//休眠1秒
new Thread(()->{phone.call();},"B").start();
}
}
class Phone {
public synchronized void send(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
第2问:send方法延迟4秒,最先打印出来的是什么?
答:发短信。因为最先获得锁的是线程A,并且sleep不会释放锁。
import java.util.concurrent.TimeUnit;
public class Test2 {
public static void main(String[] args) throws InterruptedException {
Phone2 phone = new Phone2();
new Thread(()->{phone.send();},"A").start();
TimeUnit.SECONDS.sleep(1);//休眠1秒
new Thread(()->{phone.hello();},"B").start();
}
}
class Phone2 {
public synchronized void send(){
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");
}
}
第3问:增加了一个普通方法后,最先打印出来什么?
答:Hello。因为hello()没有锁,不是同步方法。
import java.util.concurrent.TimeUnit;
public class Test2 {
public static void main(String[] args) throws InterruptedException {
Phone2 phone1 = new Phone2();
Phone2 phone2 = new Phone2();
new Thread(()->{phone1.send();},"A").start();
TimeUnit.SECONDS.sleep(1);//休眠1秒
new Thread(()->{phone2.call();},"B").start();
}
}
class Phone2 {
public synchronized void send(){
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");
}
}
第4问:两个Phone对象,线程A执行phone1的发短信,线程B执行phone2的打电话,最先打印出来的是什么?
答:打电话。因为synchronized锁的是方法的调用者,也就是Phone对象。所以这里有两把锁,线程A和线程B用的不是同一把锁。
import java.util.concurrent.TimeUnit;
public class Test3 {
public static void main(String[] args) throws InterruptedException {
Phone3 phone = new Phone3();
new Thread(()->{phone.send();},"A").start();
TimeUnit.SECONDS.sleep(1);//休眠1秒
new Thread(()->{phone.call();},"B").start();
}
}
class Phone3 {
public static synchronized void send(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call(){
System.out.println("打电话");
}
}
第5问:两个静态的同步方法,一个对象,最先打印什么?
答:发短信。
import java.util.concurrent.TimeUnit;
public class Test3 {
public static void main(String[] args) throws InterruptedException {
Phone3 phone1 = new Phone3();
Phone3 phone2 = new Phone3();
new Thread(()->{phone1.send();},"A").start();
TimeUnit.SECONDS.sleep(1);//休眠1秒
new Thread(()->{phone2.call();},"B").start();
}
}
class Phone3 {
public static synchronized void send(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call(){
System.out.println("打电话");
}
}
第6问:两个静态的同步方法,两个对象,最先打印出来谁?
答:发短信。
import java.util.concurrent.TimeUnit;
public class Test4 {
public static void main(String[] args) throws InterruptedException {
Phone4 phone = new Phone4();
new Thread(()->{phone.send();},"A").start();
TimeUnit.SECONDS.sleep(1);//休眠1秒
new Thread(()->{phone.call();},"B").start();
}
}
class Phone4 {
public static synchronized void send(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
第7问:一个静态同步方法,一个普通同步方法,一个对象,最先打印出来是什么?
答:打电话。这里有两把锁,一把锁的是实例对象,一把锁的是 class 类。
import java.util.concurrent.TimeUnit;
public class Test4 {
public static void main(String[] args) throws InterruptedException {
Phone4 phone1 = new Phone4();
Phone4 phone2 = new Phone4();
new Thread(()->{phone1.send();},"A").start();
TimeUnit.SECONDS.sleep(1);//休眠1秒
new Thread(()->{phone2.call();},"B").start();
}
}
class Phone4 {
public static synchronized void send(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
第8问:一个静态同步方法,一个普通同步方法,两个个对象,最先打印出来是什么?
答:打电话。
5. 集合类不安全
5.1 List 不安全
CopyOnWriteArrayList
CopyOnWriteArrayList
是 JUC 提供的一个并发容器,它是 线程安全 且 读操作无锁 的ArrayList。写操作则通过创建底层数组的新副本来实现,是一种 读写分离 的并发策略。
我们都知道,集合框架中的ArrayList是非线程安全的,Vector虽是线程安全的,但由于简单粗暴的 Synchronized 锁同步机制,性能较差。而CopyOnWriteArrayList则提供了另一种不同的并发处理策略(比较适用于读多写少的并发场景)。
CopyOnWriteArrayList 写操作的逻辑很简单,先将原容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。当然此过程是要加 Lock 锁的,并且在写操作期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。
package test;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
//java.util.ConcurrentModificationException 并发修改异常
public class TestList {
public static void main(String[] args) {
//并发下 ArrayList 是不安全的
//List<String> list = new ArrayList<>();
/**
* 解决方法:
* 1.List<String> list = new Vector<>();
* 2.List<String> list = Collections.synchronizedList(new ArrayList<>());
* 3.List<String> list = new CopyOnWriteArrayList<>();
*/
//CopyOnWrite 写入时复制 COW 计算机程序设计领域的一种优化策略
// 读写分离
// CopyOnWriteArrayList 比 Vector 厉害在哪里?
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
5.2 Set 不安全
CopyOnWriteArraySet
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
public class TestSet {
public static void main(String[] args) {
/**
* 使用 HashSet 会抛出 java.util.ConcurrentModificationException
* Set<String> set = new HashSet<>();
*
* 解决方法:
* 1、Set<String> set = Collections.synchronizedSet(new HashSet<>());
* 2、Set<String> set = new CopyOnWriteArraySet<>();
*/
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,3));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
HashSet
HashSet 的底层是 HashMap,HashMap的底层是数组加链组或者红黑树。
public HashSet(){
map = new HashMap<>();
}
// 添加 set 本质就是添加 map key,是无法重复的
public boolean add(E e){
return map.put(e,PRESET)==null;
}
// PRESET 是一个不变值
private static final Object PRESET = new Object();
5.3 Map 不安全
ConcurrentHashMap
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class TestMap {
public static void main(String[] args) {
// 报错 java.util.ConcurrentModificationException
// Map<String,String> map = new HashMap<>(); 等价于 new HashMap<>(16,0.75)
// Map<String,String> map = Collections.synchronizedMap(new HashMap<String,String>());
Map<String,String> map = new ConcurrentHashMap<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,3));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
6. Callable
特点:
- 可以有返回值
- 可以抛出异常
- 使用的方法是
call()
,而不是run()
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
FutureTask<Integer> futureTask = new FutureTask<>(myThread);
new Thread(futureTask,"A").start();
new Thread(futureTask,"B").start(); //结果会被缓存,效率高,最终只打印一个 "call()被执行"
Integer res = futureTask.get(); // 获取Callable返会结果,这个get方法可能会产生阻塞!!!
// 可以把 get方法 放到最后执行,或者使用异步通信来处理!
System.out.println(res);
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() {
System.out.println("call()被执行");
// 假如这里是耗时的操作,那get方法可能会产生阻塞
return 1024;
}
}
运行结果:
call()被执行
1024
7. 常用辅助类
7.1 CountDownLatch
- countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
- 是通过一个减法计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器就调用
countDown()
,然后值就 -1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程await()
就会被唤醒,继续往下执行。
重要源码如下:
//构造器,参数count为计数值
public CountDownLatch(int count) {
};
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException {
};
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
};
//将count值减1
public void countDown() {
};
使用示例如下:
import java.util.concurrent.CountDownLatch;
/**
* 假如教室里有6个人,只有当6个人全部走了,才能关门
* 写代码模拟这个过程
*
* 计数器
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 总数是6
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "go out!");
countDownLatch.countDown(); // 每走一个人,数量 -1
},String.valueOf(i)).start();
}
// 等待计数器归零,然后再向下执行
// 因为主线程开启了6个线程,循环就结束了,就会把门关上,这时6个副线程可能还没执行完,所以主线程需要等待一下
countDownLatch.await();
System.out.println("Close door!");
}
}
执行结果:
2go out!
3go out!
1go out!
4go out!
6go out!
5go out!
Close door!
7.2 CyclicBarrier
加法计数器,它的作用就是会让所有线程都等待完成后才会继续下一步行动。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
/**
* 召集 7 颗龙珠召唤神龙
*/
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
// 召唤龙珠的线程
// 也就是说,当线程数量达到7的时候,就会执行这个Lambda表达式
System.out.println("召唤神龙成功!");
});
for (int i = 1; i <= 7; i++) {
final int temp = i; // lambda 表达式里操作不到i,所以需要定义一个final变量
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "收集第" + temp + "颗龙珠");
try {
cyclicBarrier.await(); // 等待
System.out.println("线程集齐了才会继续执行" + temp);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
执行结果:
1收集第1颗龙珠
5收集第5颗龙珠
4收集第4颗龙珠
2收集第2颗龙珠
3收集第3颗龙珠
6收集第6颗龙珠
7收集第7颗龙珠
召唤神龙成功!
线程集齐了才会继续执行7
线程集齐了才会继续执行5
线程集齐了才会继续执行4
线程集齐了才会继续执行3
线程集齐了才会继续执行6
线程集齐了才会继续执行2
线程集齐了才会继续执行1
CyclicBarrier 与 CountDownLatch 区别
- CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的
- CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的。
7.3 Semaphore
- Semaphore也是一个线程同步的辅助类,可以维护当前访问自身的线程个数,并提供了同步机制,实现多个共享资源的互斥使用。
- 使用Semaphore可以控制同时访问资源的最大线程个数。
Semaphore的常用方法:
// 申请资源,在获取到资源之前一直将线程阻塞
void acquire();
// 释放资源
void release();
// 返回当前可用资源的个数
int availablePermits();
// 查询是否有线程正在等待获取资源
boolean hasQueuedThreads();
使用示例:
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
/**
* 现在有3个停车位,6辆车,模拟停车过程
* @param args
*/
public static void main(String[] args) {
// 参数表示可用资源个数,限流
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(2);
System.out.println(Thread.currentThread().getName() + "离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放资源
}
},String.valueOf(i)).start();
}
}
}
执行结果:
1抢到了车位
2抢到了车位
3抢到了车位
3离开了车位
4抢到了车位
1离开了车位
5抢到了车位
2离开了车位
6抢到了车位
4离开了车位
5离开了车位
6离开了车位
8. 读写锁
- 写锁:独占锁、排它锁,一次只能被一个线程占有
- 读锁:共享锁,多个线程可以同时占有
不添加读写锁时:
import java.util.HashMap;
import java.util.Map;
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 写线程
for (int i = 1; i <= 5; i++) {
final int tempt = i;
new Thread(()->{
myCache.put(tempt+"", tempt+"");
},String.valueOf(i)).start();
}
// 读线程
for (int i = 1; i <= 5; i++) {
final int tempt = i;
new Thread(()->{
myCache.get(tempt+"");
},String.valueOf(i)).start();
}
}
}
/**
* 自定义缓存
* 可读、可写
* 无锁的情况
*/
class MyCache {
private volatile Map<String,Object> map = new HashMap<>();
// 写
public void put(String key, Object value) {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入成功");
}
// 读
public void get(String key) {
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取成功");
}
}
执行结果:
添加读写锁后:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCacheLock myCache = new MyCacheLock();
// 写线程
for (int i = 1; i <= 5; i++) {
final int tempt = i;
new Thread(()->{
myCache.put(tempt+"", tempt+"");
},String.valueOf(i)).start();
}
// 读线程
for (int i = 1; i <= 5; i++) {
final int tempt = i;
new Thread(()->{
myCache.get(tempt+"");
},String.valueOf(i)).start();
}
}
}
/**
* 自定义缓存
* 可读、可写
* 有锁的情况
*/
class MyCacheLock {
private volatile Map<String,Object> map = new HashMap<>();
// 读写锁,更加细粒度的控制
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 写,写入的时候,只希望同时只有一个线程写
public void put(String key, Object value) {
readWriteLock.writeLock().lock(); // 加上写锁
try {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入成功");
}finally {
readWriteLock.writeLock().unlock(); // 解锁
}
}
// 读,所有人都可以读
public void get(String key) {
readWriteLock.readLock().lock(); // 加上读锁
try{
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取成功");
}finally {
readWriteLock.readLock().unlock();
}
}
}
执行结果:
1写入1
1写入成功
4写入4
4写入成功
3写入3
3写入成功
5写入5
5写入成功
2写入2
2写入成功
1读取1
2读取2
2读取成功
5读取5
5读取成功
3读取3
1读取成功
4读取4
4读取成功
3读取成功
9. 阻塞队列
队列比较好理解,数据结构中我们都接触过,先进先出(FIFO)的一种数据结构,那什么是阻塞队列呢?从名字可以看出阻塞队列其实也就是队列的一种特殊情况。它具有如下特点:
- 当阻塞队列满了,往队列添加元素的操作将会被阻塞。
- 当阻塞队列为空时,从队列中获取元素的操作将会被阻塞。
阻塞队列常用的应用场景: 多线程并发处理,线程池!常用于生产者和消费者场景,生产者是往队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列正好是生产者存放、消费者来获取的容器。
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add() | offer() | put() | offer(,,) |
移除 | remove() | poll() | take() | poll(,) |
获取队首元素 | element() | peek() | - | - |
使用示例:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
public class ArrayBlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
//test1();
//test2();
//test3();
test4();
}
/**
* 抛出异常
*/
public static void test1(){
// 队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
// 超出队列大小 ,抛出异常 java.lang.IllegalStateException: Queue full
// System.out.println(blockingQueue.add("d"));
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
// 抛出异常 java.util.NoSuchElementException
// System.out.println(blockingQueue.remove());
}
/**
* 有返回值,不抛出异常
*/
public static void test2(){
// 队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("d")); // 返回false,不抛出异常
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll()); // 返回null,不抛出异常
}
/**
* 阻塞 等待
*/
public static void test3() throws InterruptedException {
// 队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(3);
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
//blockingQueue.put("d"); // 傻傻地等待,直到有位置给自己进去
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take()); // 取不到元素就一直阻塞
}
/**
* 超时 等待
*/
public static void test4() throws InterruptedException {
// 队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("d",2,TimeUnit.SECONDS)); // 等待超过2秒就退出,返回false
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll(2,TimeUnit.SECONDS)); // 等待超过2秒就退出,返回null
}
}
10. 同步队列
SynchronousQueue
不能自定义容量,它的容量大小固定为1,也就是说一旦 put 进去一个元素,必须从里面先 take 取出来,否则不能再 put 进去值!
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
public class SynchronousQueueDemo {
public static void main(String[] args) {
// 同步队列
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName() + "put 1");
blockingQueue.put("1");
System.out.println(Thread.currentThread().getName() + "put 2");
blockingQueue.put("2");
System.out.println(Thread.currentThread().getName() + "put 3");
blockingQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "get" + blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "get" + blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "get" + blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T2").start();
}
}
11. 线程池(重点)
11.1 池化技术
程序在运行的时候会占用系统的资源,池化技术就是用来优化资源的使用,常见的池化技术有线程池、连接池、内存池、对象池等等。
池化技术简单来说,就是事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我。
线程池的好处:
- 降低系统消耗:重复利用已经创建的线程降低线程创建和销毁造成的资源消耗。
- 提高响应速度: 当任务到达时,任务不需要等到线程创建就可以立即执行。
- 方便线程的管理: 可以通过设置合理分配、调优、监控。
线程复用、可以控制最大并发数、管理线程
线程池处理流程
假设线程池的最大容量为5个,其中核心线程池容量为3个,非核心线程池容量为2个。工作队列的容量大小为2个。
一开始,线程池里的线程个数为0,这时候任务1来了,线程池管理者就会创建1个线程,用来执行任务1,接着任务2、3也来了,线程池管理者就会再创建两个线程,用来执行任务2和3。此时,线程池里一共3个线程,也就是核心线程池满了。
这时候,又来一个任务4,但是核心线程池已经满了,管理者就会让它到工作队列进行排队,任务5来了,同样也是在工作队列里等待。此时,等待队列已满。
这时候任务1执行完了,释放了一个线程1,就可以让任务4出队被处理。然后,任务6和7来了。此时,三个核心线程都在工作,工作队列还有一个空位。任务6去了工作队列排队,任务7没有位置坐。因为线程池最大可以放5个线程,只有核心线程池3个满了,还有两个位置可以放非核心线程,所以管理者会创建一个非核心线程,然后任务5出队,任务7入队。
直到线程池全满(5个),工作队列也满,新来的任务才会无法被处理,这时采用拒绝策略拒绝新来的任务。优先使用线程池中的核心线程池。
11.2 三大方法
// 单个线程
Executors.newSingleThreadExecutor();
// 创建一个固定大小的线程池池
Executors.newFixedThreadPool(5);
// 线程池大小不固定,遇强则强,遇弱则弱
Executors.newCachedThreadPool();
使用示例:
package pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo01 {
public static void main(String[] args) {
// 单个线程
//ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 创建一个固定大小的线程池池
// ExecutorService threadPool = Executors.newFixedThreadPool(5);
// 线程池大小不固定,遇强则强,遇弱则弱
ExecutorService threadPool = Executors.newCachedThreadPool();
try {
for (int i = 1; i <= 50; i++) {
// 使用了线程池之后,使用线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + "ok");
});
}
} finally {
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
阿里巴巴开发手册建议:线程池不允许使用 Excutors 去创建,而是通过 ThreadPoolExecutor的方式(下一小节会讲到),这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
(1)FixedThreadPool
和SingThreadPool
:允许的请求队列长度为Integer.MAX_VALUE
,可能会堆积大量的请求,从而导致OOM
(2)CachedThreadPool
:允许创建的线程数量最大为Integer.MAX_VALUE
,可能会创建大量的线程,从而导致OOM
11.3 7大参数
三大方法的源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, // 21亿 OOM
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
观察源码可以发现,这几种方法的本质都是 new 一个 ThreadPoolExecutor 对象,我们来看一下 ThreadPoolExecutor 的源码(构造器有7个参数):
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;
}
11.4 4种拒绝策略
import java.util.concurrent.*;
/**
* 1、new ThreadPoolExecutor.AbortPolicy() // 默认拒绝策略,如果阻塞队列满了,还有任务要进来,就不处理这个任务,并抛出异常
* 2、new ThreadPoolExecutor.CallerRunsPolicy() // 谁调用你的,就让谁去处理,所以多出来的任务会被main线程处理
* 3、new ThreadPoolExecutor.DiscardPolicy() // 队列满了,丢掉任务,不会抛出异常
* 4、new ThreadPoolExecutor.DiscardOldestPolicy() // 队列满了,尝试去和最早的任务竞争,也不会抛出异常!
*/
public class Demo01 {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy() // 队列满了,尝试去和最早的任务竞争,也不会抛出异常!
);
try {
for (int i = 1; i <= 10; i++) {
// 使用了线程池之后,使用线程池来创建线程
// 也就是使用线程池执行10次Runnable接口
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + "ok");
});
}
} finally {
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
11.5 CPU密集型、IO密集型(调优)
线程池的最大容量 maximumPoolSize
应该如何设置?
- CPU密集型:
- 电脑的CPU是几核的,就设置为几,可以保持CPU的效率最高!
- 获取电脑CPU核数:
Runtime.getRuntime().availableProcessors()
- IO密集型:
- 假设程序中有 n 个十分消耗 IO 资源的线程,可以设置 > n 的线程池容量,一般设置为 2n。
- 比如程序中有 5 个消耗IO的线程,那线程池最大容量可以设置为 10。
12. 四大函数式接口(必须掌握)
新时代程序员必须会的4种操作:Lambda表达式、链式编程、函数式接口、Stream流计算
函数式接口:只有一个抽象方法的接口。
比如:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
函数式接口的好处:简化编程模型,在新版本的框架底层中大量应用!
12.1 Function 函数型接口
有一个输入参数,有一个输出结果!
可以作为工具类,输出处理输入参数后的结果值
源码:
@FunctionalInterface
public interface Function<T, R> {
// 输入 T,返回 R
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
使用示例:
import java.util.function.Function;
public class Demo01 {
public static void main(String[] args) {
/*
写法1:匿名内部类
Function<String,String> function = new Function<String,String>() {
@Override
public String apply(String str) {
return str;
}
};*/
// 写法2:Lambda表达式
Function<String,String> function = (str)->{ return str; };
System.out.println(function.apply("asd"));
}
}
12.2 Predictate 断定型接口
有一个输入参数,返回值只能是 布尔值!
源码:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
使用示例:
package test;
import java.util.function.Predicate;
public class Demo02 {
public static void main(String[] args) {
// 判断字符串是否为空
Predicate<String> predicate = new Predicate<String>() {
@Override
public boolean test(String s) {
return s.isEmpty();
}
};
System.out.println(predicate.test("acd"));
}
}
12.3 Consumer 消费型接口
只有输入,没有返回值!
源码:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
使用示例:
package test;
import java.util.function.Consumer;
public class Demo03 {
public static void main(String[] args) {
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
consumer.accept("add");
}
}
12.4 Supplier 供给型接口
没有参数,只有返回值!
源码:
@FunctionalInterface
public interface Supplier<T> {
T get();
}
使用示例:
import java.util.function.Supplier;
public class Demo04 {
public static void main(String[] args) {
Supplier<String> supplier = new Supplier<String>() {
@Override
public String get() {
return "aaaa";
}
};
System.out.println(supplier.get());
}
}
13. Stream 流式计算
什么是 Stream 流式计算
大数据:存储 + 计算
- 存储:使用集合、MySQL…
- 计算:使用流
使用示例:
// User类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
// 有参,无参构造,get、set、toString方法
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private int id;
private String name;
private int age;
}
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
/**
* 现有5个用户,筛选:
* 1、ID 必须是偶数
* 2、年龄必须大于23岁
* 3、用户名转为大写字母
* 4、用户名字母与倒着排序
* 5、只输出一个用户
*/
public class Test {
public static void main(String[] args) {
User u1 = new User(1,"a",21);
User u2 = new User(2,"b",22);
User u3 = new User(3,"c",23);
User u4 = new User(4,"d",24);
User u5 = new User(6,"e",25);
// 集合就是起存储作用
List<User> list = Arrays.asList(u1, u2, u3, u4, u5);
// 计算交给 Stream 流
// Lambda表达式、链式编程、函数式接口、Stream流计算
list.stream()
.filter(user -> { return user.getId()%2 == 0; }) // 1、得到的数据流只剩下ID为偶数的用户
.filter(user -> { return user.getAge()>23; }) // 2、
.map(user -> { return user.getName().toUpperCase(); }) // 3、这一步得到 D、E
.sorted((user1,user2)->{ return user2.compareTo(user1);}) // 4、
.limit(1) // 5、
.forEach(System.out::println);
}
}
执行结果:
E
14. ForkJoin
什么是 ForkJoin
Java7 提供的一个用于并行执行任务的框架,把一个大任务分割成若干个小任务,最终汇总每个小任务的结果后得到大任务结果的框架。这种做法可以提高效率,特别适合大数据量!
ForkJoin 特点:工作窃取算法
是指某个线程从其他队列里窃取任务来执行。当大任务被分割成小任务时,有的线程可能提前完成任务,此时闲着不如去帮其他没完成工作线程。此时可以去其他队列窃取任务,为了减少竞争,通常使用双端队列,被窃取的线程从头部拿,窃取的线程从尾部拿任务执行。
工作窃取算法的优缺点:
- 优点:充分利用线程进行并行计算,减少了线程间的竞争。
- 缺点:有些情况下还是存在竞争,比如双端队列中只有一个任务。这样就消耗了更多资源。
ForkJoin 的用法
例题:计算求和,例如计算 1+2+3+…+10000 的值。
先来说一下逻辑思路:
- 通过
ForkJoinPool
来执行任务,它有一个方法是void execute(ForkJoinTask<?> task)
,用来为异步执行给定任务的排列。 - 所以我们需要先创建
ForkJoinTask
任务,作为 execute() 的参数。已知它有三个子类CountedCompleter<T>
、RecursiveAction<V>
(递归事件,没有返回值)、RecursiveTask<V>
(递归任务,有返回值),这里我们需要返回计算值,所以选择用RecursiveTask<V>
。 - 自定义一个任务类,该任务类继承
RecursiveTask<V>
,需要重写<V> compute()
方法。
任务类代码:
package forkJoin;
import java.util.concurrent.RecursiveTask;
/**
* 求和计算的任务!
* 从 start 开始,一直加到 end
*/
public class ForkJoinDemo extends RecursiveTask<Long> {
private long start; // 1
private long end; // 100000
// 临界值
private long temp = 10000L;
// 构造器
public ForkJoinDemo(long start, long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if((end - start) <= temp){
// 区间长度没达到临界值,就正常计算
long sum = 0L;
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
//分支合并计算,使用 ForkJoin
//中间值
long middle = (start + end) / 2;
// 将整个任务拆分为两个小任务
ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
task1.fork(); // 拆分任务,把任务压入线程队列
ForkJoinDemo task2 = new ForkJoinDemo(middle + 1, end);
task2.fork(); // 拆分任务,把任务压入线程队列
return task1.join() + task2.join();
}
}
}
测试代码:
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;
public class Test {
// 普通求和计算
public static void test1(){
long sum = 0L;
long start = System.currentTimeMillis();
for (long i = 1L; i <= 10_0000_0000L; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("普通方法所需时间:" + (end - start) + "ms,结果是:" + sum);
}
// 使用 ForkJoin
public static void test2() throws ExecutionException, InterruptedException {
// 开始运行时间
long start = System.currentTimeMillis();
//1. 创建线程池
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 2. 创建任务
ForkJoinTask<Long> task = new ForkJoinDemo(1L,10_0000_0000L);
// 3. 提交任务
ForkJoinTask<Long> submit = forkJoinPool.submit(task);
// 4. 得到任务结果
Long sum = submit.get();
// 结束时间
long end = System.currentTimeMillis();
System.out.println("ForkJoin 方法所需时间:" + (end - start) + "ms,结果是:" + sum);
}
// 使用 Stream
public static void test3() {
// 开始运行时间
long start = System.currentTimeMillis();
long sum = LongStream.rangeClosed(1L,10_0000_0000L).parallel().reduce(0,Long::sum);
// 结束时间
long end = System.currentTimeMillis();
System.out.println("ForkJoin 方法所需时间:" + (end - start) + "ms,结果是:" + sum);
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
test1();
test2();
test3();
}
}
执行结果:
普通方法所需时间:505ms,结果是:500000000500000000
ForkJoin 方法所需时间:646ms,结果是:500000000500000000
Stream 方法所需时间:349ms,结果是:500000000500000000
15. 异步回调
Future 设计的初衷:对将来的某个事件的结果进行建模
使用示例:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* 异步调用: CompletableFuture
* 异步执行
* 成功回调
* 失败回调
* 作用类似 Ajax
*/
public class Demo01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*
// 没有返回值的 runAsync 异步调用
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "runAsync => Void");
});
System.out.println("11111");
completableFuture.get();//阻塞 直到获取执行结果
*/
// 有返回值的 supplyAsync 异步调用
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName() + "supplyAsync => Integer");
int n = 10 / 0;
return 1024;
});
completableFuture.whenComplete((t,u)->{ // 成功回调时
System.out.println("t => " + t); // t 代表正常的返回结果
System.out.println("u => " + u); // u 为null 代表没有返回错误,有错误就会打印错误信息
}).exceptionally((e)->{ // 失败回调时
System.out.println(e.getMessage());
return 404; // 加了 int n = 10/0; 之后,就可以获取到失败回调的结果
}).get();
}
}
16. JMM
什么是 JMM
JMM 即为 JAVA 内存模型(Java Memory Model),这是一个理论上的模型,是一种概念,不是实际存在的东西。
内存模型:描述了程序中各个变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节。
内存模型可以理解为在特定的操作协议下,对特定的内存或者高速缓存进行读写访问的过程抽象描述。不同架构下的物理机拥有不一样的内存模型,Java虚拟机是一个实现了跨平台的虚拟系统,因此它也有自己的内存模型,即Java内存模型。 因此它不是对物理内存的规范,而是在虚拟机基础上进行的规范从而实现平台一致性,以达到Java程序能够“一次编写,到处运行”。
因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。
作用:缓存一致性协议,用于定义数据读写的规则。
内存划分
JMM规定了内存主要划分为主内存和工作内存两种。JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存。
此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。
内存交互操作(8种原子操作)
JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。因为JMM制定了一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程。
关于主内存和工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存,JMM 定义了8中操作来完成,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
- lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则:
- 不允许 read 和 load、store 和 write 操作之一单独出现(即不允许一个变量从主存读取了但是工作内存不接受,或者从工作内存发起会写了但是主存不接受的情况),以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
- 不允许线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化(load或assign)的变量。换句话说就是对变量实施use、store操作之前,必须经过assign和load操作。
- 一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存(执行store和write操作)。
Java内存模型的三个特征
并发程序要正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。JMM就是围绕着在并发过程中如何处理这三个特性而建立的模型。
- 原子性:一个操作或多个操作要么全部执行完成且执行过程不被中断,要么就不执行。
- 可见性:当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 有序性:程序执行的顺序按照代码的先后顺序执行。
17. Volatile
请你谈谈对 volatile 的理解
volatile 是Java虚拟机提供的轻量级的同步机制。
1、保证可见性
2、不保证原子性
3、禁止指令重排
1、保证可见性
import java.util.concurrent.TimeUnit;
public class JMMDemo {
private static int num = 0;
public static void main(String[] args){ // main线程
new Thread(()->{ // 线程1对主内存的变化是不知道的
while(num==0){
}
}).start();
// main 线程睡眠1秒,确保线程1开始执行
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
运行上面的代码,执行结果将会输出1,但是程序并没有停止,因为线程1还在死循环中。虽然main线程修改了num的值,但是线程1对此变化并不知情,它用到的还是自己工作内存的变量副本。
只需要在定义num的时候加上 volatile
修饰符就可以解决这个问题,因为 volatile
可以保证变量的可见性。
private volatile static int num = 0;
2、不保证原子性
public class VDemo02 {
// volatile 不保证原子性
private volatile static int num = 0;
public static void add(){
num++;
}
public static void main(String[] args) {
// 理论上 num 结果应该为4万
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 2000; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){ // main gc
Thread.yield();
}
System.out.println(num);
}
}
执行结果输出:
37828
如果不加 lock 和 synchronized,怎么保证原子性?可以使用原子类
import java.util.concurrent.atomic.AtomicInteger;
public class VDemo02 {
// volatile 不保证原子性
// AtomicInteger 原子类的 Integer,可以用来解决原子性
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
//num++; // 不是一个原子性操作
num.getAndIncrement(); // AtomicInteger 的 +1 方法 CAS
}
public static void main(String[] args) {
// 理论上 num 结果应该为4万
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 2000; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){ // main gc
Thread.yield();
}
System.out.println(num);
}
}
3、避免指令重排
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。也就是说,程序实际执行代码的顺序与我们书写的代码顺序不一定一致。
- 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
处理器在进行指令重排的时候,考虑:数据之间的依赖性!
有volatile
修饰的变量,是借助内存屏障来解决可见性跟重排序的问题。
内存屏障(一种CPU指令)
内存屏障的作用
- 禁止指令重排:在有内存屏障的地方,会禁止指令重排序,即屏障下面的代码不能跟屏障上面的代码交换执行顺序。
- 保证可见性:在有内存屏障的地方,线程修改完共享变量以后会马上把该变量从本地内存写回到主内存,并且让其他线程本地内存中该变量副本失效(使用MESI协议)
内存屏障分类
- 按照可见性保障来划分
- 加载屏障(Load Barrier):StoreLoad屏障可充当加载屏障,作用是使用load 原子操作,刷新处理器缓存,即清空无效化队列,使处理器在读取共享变量时,先从主内存或其他处理器的高速缓存中读取相应变量,更新到自己的缓存中
- 存储屏障(Store Barrier):StoreLoad屏障可充当存储屏障,作用是使用 store 原子操作,冲刷处理器缓存,即将写缓冲器内容写入高速缓存中,使处理器对共享变量的更新写入高速缓存或者主内存中
- 这两个屏障一起保证了数据在多处理器之间是可见的。
- 按照有序性保障来划分
- 获取屏障(Acquire Barrier):相当于LoadLoad屏障与LoadStore屏障的组合。在读操作后插入,禁止该读操作与其后的任何读写操作发生重排序;
- 释放屏障(Release Barrier):相当于LoadStore屏障与StoreStore屏障的组合。在一个写操作之前插入,禁止该写操作与其前面的任何读写操作发生重排序。
- 这两个屏障一起保证了临界区中的任何读写操作不可能被重排序到临界区之外。
在每个volatile变量,在读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;如图:
在每个volatile变量,在写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;如图:
18、深入理解CAS
什么是CSA机制
- 悲观锁是将资源锁住,等获得锁的线程释放锁之后,下一个线程才可以访问。
- 乐观锁是对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁(这使得多个任务可以并行的对数据进行操作),只有到数据提交的时候才通过一种机制来验证数据是否存在冲突(一般实现方式是通过加版本号然后进行版本号的对比方式实现)。
既然都有了悲观锁,为啥还需要一个乐观锁?
因为锁的获取和释放是要花费一定代价的,如果在线程数目特别少的时候,可能根本就不会有别的线程来操作数据,此时你还要获取锁和释放锁,可以说是一种浪费。
CAS机制就是对乐观锁的一种实现,它是 Compare And Swap 的缩写,即我们所说的比较交换。在并发很小的情况下,数据被意外修改的概率很低,但是又存在这种可能性,此时就用CAS。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
举个例子:
- 在内存地址V当中,存储着值为10的变量。
- 此时线程1想要把变量的值增加1。对线程1来说,内存地址内的值V=10,旧的预期值A=10,要修改的新值B=11。
- 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11,也就是此时V=11。
- 线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。
- 线程1重新获取内存地址V的当前值,并修改期望值A,然后尝试重新提交。。 此时对线程1来说,V=11,A=11,B=12。 这个过程被称为自旋。
- 这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。 线程1进行Swap,把地址V的值替换为B,也就是12。
AtomicInteger类的 getAndIncrement() 方法就是应用了 CAS机制
// AtomicInteger类的 getAndIncrement() 方法源码
public final int getAndIncrement() {
//第一个参数当前对象地址,第二个参数数据偏移量,第三个参数每次指定默认加1
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// Unsafe类的getAndAddInt() 方法源码
public final int getAndAddInt(Object var1, long var2, int var4) {
//这个方法使用的就是CAS,核心在于循环比较内存里面的值和当前值是否相等,如果相等就用新值覆盖
int var5;
do {
//var5 就是内存地址中的值
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//通过var1, var2重新获取内存中的值,如果这个值还是我期望的var5,我就让它加上var4,也就是加1
return var5;
}
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
// 如果atomicInteger的值是我所期望的2020,我才会将其更新为2021,否则不更新
System.out.println(atomicInteger.compareAndSet(2020, 2021)); //输出true
System.out.println(atomicInteger.get()); //输出2021
System.out.println(atomicInteger.compareAndSet(2020, 2022));//输出false
System.out.println(atomicInteger.get()); //输出2021
}
}
Unsafe类
Unsafe类可以帮我们直接去操作硬件资源,这是借助 Java 的 jit 来进行的,官方不推荐使用,因为不安全。例如你使用 unsafe 创建一个超级大的数组,但是这个数组 JVM 是不管理的,只能你自己操作,容易oom,也不利于资源的回收。
CAS存在的问题
- ABA问题
- 循环时间长消耗资源大
- 只能保证一个共享变量的原子操作
19. 原子引用解决ABA问题
什么是ABA问题
因为CAS需要在操作值的时候,检查值有没有变化,如果没有变化则更新。如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检测时会发现值没有发生变化,其实是变过的。
本质:ABA问题的根本在于cas在修改变量的时候,无法记录变量的状态,比如修改的次数,否修改过这个变量。这样就很容易在一个线程将A修改成B时,另一个线程又会把B修改成A,造成CAS多次执行的问题。
解决方法:添加版本号,每次更新的时候追加版本号
从JDK1.5开始,Atomic包提供了一个类AtomicStampedReference来解决ABA的问题。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
public class CASDemo {
public static void main(String[] args) {
//这里有个坑。把下面代码中的参数20全部改为大于127的数字就可以发现坑,这里解释一下坑存在的原因:
//1. 看compareAndSet的源码,里面是使用 == 进行比较的。
//2. 由于new的时候声明泛型肯定是装箱类,这个时候传入值类型将会自动装箱
//3. 自动装箱的后果就是地址不一致,使用 == 判断的结果就为false
//4. Integer 会有 IntegerCache,缓存使用范围在 -127~127,在这个范围内的会复用对象,所以==不会出现问题,但是使用>127的数字就会出现问题
//5. 总结:AtomicStampedReference 如果泛型是一个包装类,注意对象的引用问题
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(20,1);
new Thread(()->{
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("a0---->" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 第一次比较交换
// 如果atomicInteger的值是我所期望的2020,我才会将其更新为2021,否则不更新
System.out.println("a尝试第1次比较交换:"+atomicStampedReference.compareAndSet(20, 21,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("a1---->" + atomicStampedReference.getStamp());
// 第二次比较交换
System.out.println("a尝试第2次比较交换:"+atomicStampedReference.compareAndSet(21, 20,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("a2---->" + atomicStampedReference.getStamp());
},"a").start();
//跟乐观锁的原理相同
new Thread(()->{
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("b0---->" + stamp);
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("b尝试第1次比较交换:"+atomicStampedReference.compareAndSet(20, 66,
stamp, stamp+ 1));
System.out.println("b1---->" + atomicStampedReference.getStamp());
},"b").start();
}
}
注意:Integer 使用了对象缓存机制,默认范围是 -128~127,推荐使用静态工厂方法 valueOf 获取对象实例,而不是 new,因为 valueOf 使用缓存,会复用已有对象,而 new 一定会创建新的对象,分配新的空间。
20. 各种锁的理解
20.1 公平锁、非公平锁
- 公平锁:十分公平,遵循先来后到 FIFO 原则。
- 非公平锁:十分不公平,可以插队。(默认都是非公平锁)
20.2 可重入锁
可重入锁,也叫做递归锁。可重入就是说某个线程已经获得某个锁,可以再次获取相同的锁而不会出现死锁。例如一个线程在执行一个带锁的方法1,该方法中又调用了另一个需要相同锁的方法2,则该线程可以直接执行调用的方法2,而无需重新获得锁。
实际应用场景:数据库事务操作,add操作将会获取锁,若一个事务当中多次add,就应该允许该线程多次进入该临界区。
在JAVA环境下,ReentrantLock
和synchronized
都是可重入锁。
synchronized 例子
在一个类中,如果synchronized方法1调用了synchronized方法2,方法2是可以正常执行的,这说明synchronized是可重入锁。
public class Demo01 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sms();
},"A").start();
new Thread(()->{
phone.sms();
},"B").start();
}
}
class Phone{
// 线程一旦获得了sms()的锁,就会自动获得方法里面的call()的锁
public synchronized void sms(){
System.out.println(Thread.currentThread().getName()+"=>sms");
call(); // 这里也有锁
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName()+"=>call");
}
}
结果输出:线程A的两个方法总是先后一起执行的,中间不会有线程B的插入,也说明了 synchronized 是可重入锁。
A=>sms
A=>call
B=>sms
B=>call
Lock 例子
与synchronized不同,Lock 需要手动释放锁,并且加锁次数和释放锁的次数要一样。
package lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo02 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
new Thread(()->{
phone.sms();
},"A").start();
new Thread(()->{
phone.call();
},"B").start();
}
}
class Phone2{
Lock lock = new ReentrantLock();
// 线程一旦获得了sms()的锁,就会自动获得方法里面的call()的锁
// 因为他们使用的是同一个锁对象
public void sms(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"=>sms");
TimeUnit.SECONDS.sleep(1);
call(); // 这里也有锁
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void call(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"=>call");
}finally {
lock.unlock();
}
}
}
结果输出:线程A的两个方法总是先后一起执行的,中间不会有线程B的插入,也说明了 Lock 是可重入锁。
A=>sms
A=>call
B=>call
注意:这里 sms() 和 call() 使用的是同一个锁对象,都是lock实例,如果两者使用的不是同一个锁实例对象,比如 sms() 使用的是 lock 对象,call() 使用的是 lock1 对象,那会怎么样呢?
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo02 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
new Thread(()->{
phone.sms();
},"A").start();
new Thread(()->{
phone.call();
},"B").start();
}
}
class Phone2{
Lock lock = new ReentrantLock();
Lock lock1 = new ReentrantLock();
// 线程一旦获得了sms()的锁,就会自动获得方法里面的call()的锁
public void sms(){
lock.lock(); //用的是 lock 锁对象
try {
System.out.println(Thread.currentThread().getName()+"=>sms");
TimeUnit.SECONDS.sleep(1);
call();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void call(){
lock1.lock(); // 用的是lock1锁对象
try {
System.out.println(Thread.currentThread().getName()+"=>call");
}finally {
lock1.unlock();
}
}
}
执行结果:可以发现线程B插入到了线程A的执行,这说明想要实现可重入,那两个方法必须使用的是相同的锁对象。
A=>sms
B=>call
A=>call
可重入锁的实现原理是怎么样的?
- 加锁时,需要判断锁是否已经被获取。如果已经被获取,则判断获取锁的线程是否是当前线程。如果是当前线程,则给获取次数加1。如果不是当前线程,则需要等待。
- 释放锁时,需要给锁的获取次数减1,然后判断,次数是否为0了。如果次数为0了,则需要调用锁的唤醒方法,让锁上阻塞的其他线程得到执行的机会。
20.2 自旋锁
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断地判断锁是否能够被成功获取,直到获取到锁才会退出循环。
Java如何实现自旋锁,下面看一个简单的例子:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* 自定义自旋锁
*/
public class SpinlockDemo {
// Integer 默认值 0
// Thread 默认值 null
AtomicReference<Thread> atomicReference = new AtomicReference<>();
// 加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "==> myLock");
// 自旋锁
while (!atomicReference.compareAndSet(null,thread)){
}
}
// 解锁
public void myUnLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "==> myUnLock");
atomicReference.compareAndSet(thread,null);
}
public static void main(String[] args) {
SpinlockDemo lock = new SpinlockDemo();
new Thread(()->{
lock.myLock();
try {
System.out.println("T1在使用锁");
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.myUnLock();
}
},"T1").start();
new Thread(()->{
lock.myLock();
try {
System.out.println("T2在使用锁");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.myUnLock();
}
},"T2").start();
}
}
执行结果:
T1==> myLock
T2==> myLock
T2在使用锁
T2==> myUnLock
T1在使用锁
T1==> myUnLock
20.3 死锁
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁,这就容易造成死锁。
Java代码实现死锁例子:
import java.util.concurrent.TimeUnit;
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new MyThread(lockA,lockB),"T1").start();
new Thread(new MyThread(lockB,lockA),"T2").start();
}
}
class MyThread implements Runnable{
private String lockA;
private String lockB;
public MyThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+" already get:"+lockA+" ===>want to get "+lockB);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+" already get:"+lockB+" ===>want to get "+lockA);
}
}
}
}
执行结果:
怎么进行死锁排查,解决问题
1、使用jps -l
定位进程号
2、使用jstack 进程号
找到死锁问题
死锁相关信息:
21. 谈谈对线程安全的理解
线程安全
目前主流操作系统都是多任务的,即多个进程同时进行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的内存空间,这是由操作系统保障的。
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。
所以线程安全不是指线程的安全,而是指内存的安全。在堆内存中的数据由于可以被任何线程访问到,在没有限制的情况下存在被意外修改的风险。
解决方案1:数据私有
操作系统会为每个线程分配属于它自己的内存空间,通常称为栈内存,其它线程无权访问。这也是由操作系统保障的。
如果一些数据只有某个线程会使用,其它线程不能操作也不需要操作,这些数据就可以放入线程的栈内存中。较为常见的就是局部变量。
解决方案2:复制数据
要让公共区域堆内存中的数据对于每个线程都是安全的,那就每个线程都拷贝它一份,每个线程只处理自己的这一份拷贝而不去影响别的线程的拷贝,这不就安全了嘛。
需要说明的是所有副本数据都还是存储在公共区域堆内存里的,经常听到的“线程本地”,是从逻辑从属关系上来讲的,这些数据和线程一一对应,仿佛成了线程自己“领地”的东西了。其实从数据所在“位置”的角度来讲,它们都位于公共的堆内存中,只不过被线程认领了而已。
解决方案3:数据仅读
数据只能读取,不能修改,就可以保证线程安全。其实就是常量或只读变量,它们对于多线程是安全的,想改也改不了。
(最佳)解决方案4:(互斥)锁
如果公共区域(堆内存)的数据,要被多个线程操作时,为了确保数据的安全(或一致)性,需要在数据旁边放一把锁,要想操作数据,先获取锁再说吧。