文章目录
学习目标
1.线程相关概念
2.线程安全
3.线程通讯
4.线程锁
5.Callable接口
6.并发容器类
7.JUC四大辅助类
8.阻塞队列
9.ThreadPool 线程池
10.Fork/Join 框架
11.CompletableFuture
12.JMM内存模型
13.CAS算法与原子类
一、多线程相关概念
1.1 节 进程与线程
说到多线程,我们最应该先了解的两个概念为:线程和进程
进程:是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。理解起来非常难受,那我们就可以简单理解为:进程是程序的实体,就好比对象与类的关系,进程就是对象;我们还需要进行记忆的就是:进程是线程的容器!
线程:线程是操作系统能够进行运算调度的最小单位,而进程则是基本单位;线程可以理解为是进程中的实际运作单位,一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
总结来说:
进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程:资源分配的最小单位。
线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程:程序执行的最小单位。
1.2 节 并发与并行
串行表示所有任务都一一按先后顺序进行。串行意味着必须先装完一车柴才能运送这车柴,只有运送到了,才能卸下这车柴,并且只有完成了这整个三个步骤,才能进行下一个步骤。
串行是一次只能取得一个任务,并执行这个任务。
1.2.2 并行模式
通俗来讲,就是有多个线程去同时执行多个任务,按照字面意思来讲就是同时进行嘛!
1.2.3 并发
按照字面意思来讲就是一起发生的。并发的重点在于它是一种现象, 并发描述的是多进程同时运行的现象。
要解决大并发问题,通常是将大任务分解成多个小任务, 由于操作系统对进程的调度是随机的,所以切分成多个小任务后,可能会从任一小任务处执行。这可能会出现一些现象:
-
可能出现一个小任务执行了多次,还没开始下个任务的情况。这时一般会采用队列或类似的数据结构来存放各个小任务的成果
-
可能出现还没准备好第一步就执行第二步的可能。这时,一般采用多路复用或异步的方式,比如只有准备好产生了事件通知才执行某个任务。
-
可以多进程/多线程的方式并行执行这些小任务。也可以单进程/单线程执行这些小任务,这时很可能要配合多路复用才能达到较高的效率
1.2.4 小结(重点)
-
并发:同一时刻多个线程在访问同一个资源,多个线程对一个点 ; 例子:春运抢票 电商秒杀...
-
并行:多项工作一起执行,之后再汇总; 例子:泡方便面,电水壶烧水,一边撕调料倒入桶中
1.3 节 管程
管程(monitor)是保证了同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现).但是这样并不能保证进程以设计的顺序执行
JVM 中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程(monitor)对象,管程(monitor)会随着 java 对象一同创建和销毁
执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程
1.4 节 什么是JUC
JUC就是 java.util .concurrent 工具包的简称。这是一个处理线程的工具包,JDK 1.5 开始出现的。
可以看出JUC包含三个部分
-
并发相关类
-
原子操作类
-
锁相关类
第二章 线程安全
2.1 节 Synchronized
2.1.1 Synchronized 复习
synchronized 是 Java 中的关键字,是一种同步锁。它修饰的对象有以下几种:
-
修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{} 括起来的代码,作用的对象是调用这个代码块的对象;
-
修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
-
修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
-
修饰一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用主的对象是这个类的所有对象
注意:
虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定义的一部分,因此,synchronized 关键字不能被继承。如果在父类中的某个方法使用了 synchronized 关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上 synchronized 关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此, 子类的方法也就相当于同步了。
为大家提供一个经典案例:
2.1.2 卖票案例
package com.xxx.lock;
// 第一步创建资源类 ,定义属性和操作方法
public class TickerDemo {
//票数
private int number = 100;
//定义卖票方法
public void sale(){
synchronized (this){ //此处的this锁定的对象就是调用它的对象,也就是TickerDemo的实例
if(number>0){
System.out.println(Thread.currentThread().getName()+":卖出"+(number--)+"剩余:"+number);
}
}
}
//第二步编写线程
public static void main(String[] args) {
TickerDemo tickerDemo = new TickerDemo();
new Thread(()->{
for (int i=0;i<=100;i++){
tickerDemo.sale();;
}
},"AA").start();
new Thread(()->{
for (int i=0;i<=100;i++){
tickerDemo.sale();;
}
},"BB").start();
new Thread(()->{
for (int i=0;i<=100;i++){
tickerDemo.sale();;
}
},"CC").start();
}
}
注意:一定要注重线程代码的编写流程
-
编写多线程控制的共享资源
-
编写多线程代码
2.2 节 Lock锁
Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock 提供了比 synchronized 更多的功能。
Lock 与的 Synchronized 区别
-
Lock 不是 Java 语言内置的,synchronized 是 Java 语言的关键字,因此是内置特性。Lock 是一个类,通过这个类可以实现同步访问;
-
Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户去手动释放锁,当synchronized 方法或者 synchronized 代码块执行完之后, 系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
Lock接口如下:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
2.2.1 Lock
lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。 采用 Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用 Lock 必须在 try{}catch{}块中进行,并且将释放锁的操作放在finally 块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用 Lock 来进行同步的话,是以下面这种形式去使用的:
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
2.2.2 ReentrantLock
ReentrantLock,意思是“可重入锁”,关于可重入锁的概念将在后面讲述。 ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更多的方法。下面通过一些实例看具体看一下如何使用。
示例:
package com.xxx.lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Lock锁演示卖票
*/
// 第一步 创建资源类,定义属性和操作方法
public class LockTicketDemo {
//票数
private int number = 100;
//创建可重入锁
private final ReentrantLock lock = new ReentrantLock(true);
//卖票方法
public void sale(){
//上锁
lock.lock();
try {
if(number>0){
System.out.println(Thread.currentThread().getName()+":卖出"+(number--)+"剩余:"+number);
}
} finally {
//解锁
lock.unlock();
}
}
public static void main(String[] args) {
//第二步创建多线程
LockTicketDemo lockTicketDemo = new LockTicketDemo();
new Thread(()-> {
for (int i = 0; i < 100; i++) {
lockTicketDemo.sale();
}
},"AA").start();
new Thread(()-> {
for (int i = 0; i < 100; i++) {
lockTicketDemo.sale();
}
},"BB").start();
new Thread(()-> {
for (int i = 0; i < 100; i++) {
lockTicketDemo.sale();
}
},"CC").start();
}
}
2.2.3 小节
Lock 和 synchronized 有以下几点不同:
-
Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现;
-
Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用synchronized 时,等待的线程会一直等待下去,不能够响应中断;
-
Lock 不是 Java 语言内置的,synchronized 是 Java 语言的关键字,因此是内置特性。Lock 是一个类,通过这个类可以实现同步访问;
-
Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户去手动释放锁,当synchronized 方法或者 synchronized 代码块执行完之后, 系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
第三章 线程通讯
3.1 节 Synchronized
线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。我们来基本一道面试常见的题目来分析
场景:两个线程,一个线程对当前数值加 1,另一个线程对当前数值减 1,要求用线程间通信实现
编写思路:
-
创建资源类,定义属性和方法
-
在方法内进行判断,什么时候等待,什么时候唤醒
-
创建多个线程,调用共享资源类
3.1.1 线程通讯案例
先看示例,我们先用两个线程来完成:
package com.xxx.communicate;
/**
* 通讯案例
*/
//第一步 创建资源类 ,定义属性和操作方法
public class CommunicateDemo1 {
//初始值
private int number = 0;
//+1方法
public synchronized void incr() throws InterruptedException {
//第二步 判断 何时等待,何时唤醒
if(number!=0){ //判断number 值是否0,如果不是0,等待
this.wait(); // 线程等待,必须写在while循环中,否则造成虚假唤醒
}
//如果number值0,就+1
number++;
System.out.println(Thread.currentThread().getName()+":"+number);
//当前线程工作为,唤醒其他线程
this.notifyAll();
}
//-1的方法
public synchronized void decr() throws InterruptedException {
//第二步 判断 何时等待,何时唤醒
if(number!=1){
this.wait();// 线程等待,必须写在while循环中,否则造成虚假唤醒
}
//如果number值1,就-1
number--;
System.out.println(Thread.currentThread().getName()+":"+number);
//通知其他线程
this.notifyAll();
}
public static void main(String[] args) {
//创建多线程,操作共享数据
CommunicateDemo1 communicateDemo1 = new CommunicateDemo1();
//创建线程
new Thread(()->{
while(true){
try {
communicateDemo1.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
while(true){
try {
communicateDemo1.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
new Thread(()->{
// while(true){
// try {
// communicateDemo1.incr();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// },"CC").start();
//
// new Thread(()->{
// while(true){
// try {
// communicateDemo1.decr();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// },"DD").start();
}
}
我们会发现没有一点问题,准确的很,但是若是将我们的注释打开,变成四个线程同时操作呢?
你会发现,数据开始混乱,简直是超乎想象!
分析过后,我们会发现,导致这种原因是:虚假唤醒!那么什么叫虚假唤醒?为什么叫做虚假唤醒?
那么就来解释一下,其实在官方的Api有明确说明,大体意思就是:在进行线程进行通讯中,必须要使用循环来进行,这是为什么?
其实很好理解,由线程的生命周期就可以明白:线程调用wait()之后,会陷入阻塞状态,等待之后,才会继续执行,而且由于加了Synchronized锁,所以当一个线程陷入阻塞之后,其他等待的线程会立刻去抢到CPU资源进行执行。
那么两个线程为何就不会出现这个问题呢?这是因为,当A线程进行阻塞的时候,另一个线程就会执行,这时候他执行的条件一定是符合判断条件的,因此无需再次判断,他就是准确的;而当多个线程出现时,这时候由于线程抢占资源的随机性,我们无法确保A线程结束之后,进行的一定是与A判断相反的其他线程,而有可能是与其判断条件相同的线程,虽然会进行阻塞,但是当判断条件相反的线程执行了notifyAll()之后,会全部唤醒,而此刻由于是if判断,所以会直接向下运行,也就是为进行条件判断就执行,这就会导致与预想的结果不一样,就会出现我们所说的“虚假唤醒”。
了解为什么出错,那么就很好解决了,我们只需要在每个线程被唤醒之后,继续判断其符合条件即可,使用while()循环即可完成
package com.xxx.communicate;
/**
* 通讯案例
*/
//第一步 创建资源类 ,定义属性和操作方法
public class CommunicateDemo1 {
//初始值
private int number = 0;
//+1方法
public synchronized void incr() throws InterruptedException {
//第二步 判断 何时等待,何时唤醒
while(number!=0){ //判断number 值是否0,如果不是0,等待
this.wait(); // 线程等待,必须写在while循环中,否则造成虚假唤醒
}
//如果number值0,就+1
number++;
System.out.println(Thread.currentThread().getName()+":"+number);
//当前线程工作为,唤醒其他线程
this.notifyAll();
}
//-1的方法
public synchronized void decr() throws InterruptedException {
//第二步 判断 何时等待,何时唤醒
while (number!=1){
this.wait();// 线程等待,必须写在while循环中,否则造成虚假唤醒
}
//如果number值1,就-1
number--;
System.out.println(Thread.currentThread().getName()+":"+number);
//通知其他线程
this.notifyAll();
}
public static void main(String[] args) {
//创建多线程,操作共享数据
CommunicateDemo1 communicateDemo1 = new CommunicateDemo1();
//创建线程
new Thread(()->{
while(true){
try {
communicateDemo1.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
while(true){
try {
communicateDemo1.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
new Thread(()->{
while(true){
try {
communicateDemo1.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();
new Thread(()->{
while(true){
try {
communicateDemo1.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"DD").start();
}
}
可以看到之前的问题完美解决!!
3.2 节 Lock锁
3.2.1 newCondition
关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock 锁的 newContition()方法返回 Condition 对象,Condition 类也可以实现等待/通知模式。
用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以进行选择性通知, Condition 比较常用的两个方法:
-
await()会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重新获得锁并继续执行。
-
signal()用于唤醒一个等待的线程。
注意:在调用 Condition 的 await()/signal()方法前,也需要线程持有相关的 Lock 锁,调用 await()后线程会释放这个锁,在 singal()调用后会从当前Condition 对象的等待队列中,唤醒一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行。
3.2.2 线程通讯案例
接下来我们用newConditon完成线程通讯案例。
示例:
package com.xxx.communicate;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 通讯案例
*/
//第一步 创建资源类 ,定义属性和操作方法
public class CommunicateDemo2 {
//初始值
private int number = 0;
//声明锁
private Lock lock = new ReentrantLock();
//声明钥匙
private Condition condition = lock.newCondition();
//+1方法
public void incr() throws InterruptedException {
try {
//第二步 判断 何时等待,何时唤醒
lock.lock();
while(number!=0){ //判断number 值是否0,如果不是0,等待
condition.await(); // 线程等待,必须写在while循环中,否则造成虚假唤醒
}
//如果number值0,就+1
number++;
System.out.println(Thread.currentThread().getName()+":"+number);
//当前线程工作为,唤醒其他线程
condition.signalAll();
} finally {
lock.unlock();
}
}
//-1的方法
public void decr() throws InterruptedException {
try {
lock.lock();
//第二步 判断 何时等待,何时唤醒
while (number!=1){
condition.await();// 线程等待,必须写在while循环中,否则造成虚假唤醒
}
//如果number值1,就-1
number--;
System.out.println(Thread.currentThread().getName()+":"+number);
//通知其他线程
condition.signalAll();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
//创建多线程,操作共享数据
CommunicateDemo2 communicateDemo1 = new CommunicateDemo2();
//创建线程
new Thread(()->{
while(true){
try {
communicateDemo1.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
while(true){
try {
communicateDemo1.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
new Thread(()->{
while(true){
try {
communicateDemo1.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();
new Thread(()->{
while(true){
try {
communicateDemo1.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"DD").start();
}
}
此处不再解释,大家自行领会
3.2.3 线程定制化通讯
问题: A 线程打印 5 次 A,B 线程打印 10 次 B,C 线程打印 15 次 C,按照 此顺序循环 10 轮
实现:
package com.xxx.communicate;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 线程定制化通讯
*/
//第一步 创建资源类 ,定义属性和操作方法
public class CommunicateDemo3 {
//初始值
private int number = 0;
//声明锁
private Lock lock = new ReentrantLock();
//声明钥匙A
private Condition conditionA = lock.newCondition();
//声明钥匙B
private Condition conditionB = lock.newCondition();
//声明钥匙C
private Condition conditionC = lock.newCondition();
/**
* A打印5次
*/
public void printA(int j){
try {
lock.lock();
while(number!=0){
conditionA.await();
}
System.out.println(Thread.currentThread().getName()+"输出A,第"+j+"轮开始");
//输出5次A
for(int i=0;i<5;i++){
System.out.println("A");
}
//开始打印B
number = 1;
//唤醒B
conditionB.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* B打印10次
*/
public void printB(int j){
try {
lock.lock();
while(number!=1){
conditionB.await();
}
System.out.println(Thread.currentThread().getName()+"输出B,第"+j+"轮开始");
//输出10次B
for(int i=0;i<10;i++){
System.out.println("B");
}
//开始打印C
number = 2;
//唤醒C
conditionC.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* C打印15次
*/
public void printC(int j){
try {
lock.lock();
while(number!=2){
conditionC.await();
}
System.out.println(Thread.currentThread().getName()+"输出C,第"+j+"轮开始");
//输出15次C
for(int i=0;i<15;i++){
System.out.println("C");
}
//开始打印A
number = 0;
//唤醒A
conditionA.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
CommunicateDemo3 communicateDemo3 = new CommunicateDemo3();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
communicateDemo3.printA(i);
}
},"A").start();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
communicateDemo3.printB(i);
}
},"B").start();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
communicateDemo3.printC(i);
}
},"C").start();
}
}
这个是我之前学习的一个盲点,还可以定制化唤醒,在多线程定制中起着重大作用
第四章 线程锁
4.1 节 可重入锁
答:举例来说明锁的可重入性,示例如下:
package com.xxx.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 可重入锁
*/
public class ReentrantLockDemo {
Lock lock = new ReentrantLock();
public void outer(){
try {
lock.lock();
System.out.println("out外层加锁代码");
inner();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void inner(){
try {
lock.lock();
System.out.println("inner内层枷锁代码");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
new Thread(()->{
reentrantLockDemo.outer();
},"A").start();
}
}
outer 中调用了 inner,outer 先锁住了 lock,这样 inner 就不能再获取 lock。其实调用 outer 的线程已经获取了 lock 锁,但是不能在 inner 中重复利用已经获取的锁资源,这种锁即称之为 不可重入锁。可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。synchronized、ReentrantLock 都是可重入的锁,可重入锁相对来说简化了并发编程的开发。
4.2 节 公平锁与非公平锁
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
-
优点:所有的线程都能得到资源,不会饿死在队列中。
-
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
-
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
-
缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
卖票示例演示公平锁与非公平锁,当创建new ReentrantLock(false) 传入false时,此时底层采用非公平锁,几乎所有任务都被线程A执行。
/**
* Lock锁演示卖票
*/
// 第一步 创建资源类,定义属性和操作方法
public class LockTicketDemo {
//票数
private int number = 100;
//创建可重入锁
private final ReentrantLock lock = new ReentrantLock(false);
//卖票方法
public void sale(){
//上锁
lock.lock();
try {
if(number>0){
System.out.println(Thread.currentThread().getName()+":卖出"+(number--)+"剩余:"+number);
}
} finally {
//解锁
lock.unlock();
}
}
public static void main(String[] args) {
//第二步创建多线程
LockTicketDemo lockTicketDemo = new LockTicketDemo();
new Thread(()-> {
for (int i = 0; i < 100; i++) {
lockTicketDemo.sale();
}
},"AA").start();
new Thread(()-> {
for (int i = 0; i < 100; i++) {
lockTicketDemo.sale();
}
},"BB").start();
new Thread(()-> {
for (int i = 0; i < 100; i++) {
lockTicketDemo.sale();
}
},"CC").start();
}
}
将private final ReentrantLock lock = new ReentrantLock(true);代码改为true后,采用公平锁,我们发现每个线程都有机会执行卖票方法。
接下来我们查看底层源码
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
其中FairSync 为公平锁 ,MonfairSync()为非公平锁。
4.3 节 死锁
死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。产生死锁的必要条件:
-
互斥条件:所谓互斥就是进程在某一时间内独占资源。
-
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
-
不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
-
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
示例:
package com.xxx.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//死锁演示
public class DeadLock {
public static void main(String[] args) {
//创建两个锁
Lock lockA = new ReentrantLock();
Lock lockB = new ReentrantLock();
//创建两个线程
new Thread(()->{
try {
lockA.lock();
System.out.println(Thread.currentThread().getName()+"A,执行代码,需要获取B锁,继续往下执行");
Thread.sleep(1000);
lockB.lock();
System.out.println(Thread.currentThread().getName()+"A,执行代码,已经获得B锁");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockA.unlock();
lockB.unlock();
}
},"A").start();
new Thread(()->{
try {
lockB.lock();
System.out.println(Thread.currentThread().getName()+"B,执行代码,需要获取A锁,继续往下执行");
Thread.sleep(1000);
lockA.lock();
System.out.println(Thread.currentThread().getName()+"B,执行代码,已经获得A锁");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockB.unlock();
lockA.unlock();
}
},"B").start();
}
}
4.4 节 读写锁
读写锁(ReadWriteLock )也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成 2 个锁来分配给线程,从而使得多个线程可以同时进行读操作。
ReentrantReadWriteLock 实现了 ReadWriteLock 接口。
ReentrantReadWriteLock 里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和 writeLock()用来获取读锁和写锁。
下面通过几个例子来看一下 ReentrantReadWriteLock 具体用法。
package com.xxx.lock;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
//资源类
class MyCache {
//创建map集合
private volatile Map<String,Object> map = new HashMap<>();
//创建读写锁对象
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
//放数据
public void put(String key,Object value) {
//添加写锁
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+" 正在写操作"+key);
//暂停一会
Thread.sleep(300);
//放数据
map.put(key,value);
System.out.println(Thread.currentThread().getName()+" 写完了"+key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放写锁
rwLock.writeLock().unlock();
}
}
//取数据
public Object get(String key) {
//添加读锁
rwLock.readLock().lock();
Object result = null;
try {
System.out.println(Thread.currentThread().getName()+" 正在读取操作"+key);
//暂停一会
Thread.sleep(300);
result = map.get(key);
System.out.println(Thread.currentThread().getName()+" 取完了"+key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放读锁
rwLock.readLock().unlock();
}
return result;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) throws InterruptedException {
MyCache myCache = new MyCache();
//创建线程放数据
for (int i = 1; i <=5; i++) {
final int num = i;
new Thread(()->{
myCache.put(num+"",num+"");
},String.valueOf(i)).start();
}
Thread.sleep(300);//确保全部写完之后再进行读取,
//创建线程取数据
for (int i = 1; i <=5; i++) {
final int num = i;
new Thread(()->{
myCache.get(num+"");
},String.valueOf(i)).start();
}
}
}
读操作是不会线程被阻塞的,可以看到源代码是读取的时候,先打印一句再睡眠,若是锁阻塞的话,应该是等待下一句话读完之后下一个线程才会进来,现在很明显不是,这就说明了读操作是不会被阻塞的;而写操作很显然不是,它只有一个线程可以进行写操作,在这个线程写的时候,其它线程不可进行操作,包括读锁!这个问题就会联想到并发问题,但是在这里不去讲解,感兴趣的大家可以查阅资料。
4.5 节 小节
-
可重入锁
-
公平锁与非公平锁
-
死锁
-
读写锁
第五章 Callable接口
Callable 接口 是 java.util.concurrent.下的一个泛型接口 , 只有一个call () 方法 , 它是有返回值的 , 我们可以获取多线程执行的结果 , 使用 Callable接口 和 FutureTask 的组合 , 可以实现利用 FutureTask 来跟踪异步计算的结果 示例:
/**
* 开启一个线程计算1-100的和
*/
public class CallableDemo implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <=100 ; i++) {
sum+=i;
}
return sum;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask(new CallableDemo());
new Thread(futureTask,"A").start();
Integer result = futureTask.get();
System.out.println(result);
}
}
其实我觉得最重要的还是弄清楚为什么new Thread()可以传入FutureTask类型的参数。
可以看到构造方法中并没有可以传入FutureTask类型的参数,那么我们就需要去看FutureTask的源码了。
看到这里,我相信大家都已经了解到了,FutureTask实现了RunnableFuture接口,而RunnableFuture又继承了Runnable接口,那么FutureTask就相当于是间接实现了Runnable接口,一切就都说得通了