一、JUC概述
1.1、JUC简介
简单来说JUC就是java.util.concurrent并发包,是处理线程的工具包,从Java1.5开始出现的。
1.2、线程和进程
📗进程和线程是什么?
- 进程是计算机分配资源的基本单位,它是一个具有独立功能的程序,例如QQ运行起来就是一个大的进程,这可以在任务管理器中看到;
- 线程是程序执行的最小单位,一个进程里面可以有很多个线程,这些线程共享进程中的资源。
📗进程和线程举例?
- 例如QQ运行起来就是一个大的进程,然后里面有天气预报,我们可以同时进行文字聊天和视频,这些都是小的线程
1.3、线程的状态
1.3.1、线程在JVM中的6种状态
NEW(新建)
A thread that has not yet started is in this state. 没有开始执行的线程处于这种状态
RUNNABLE(准备就绪)
A thread executing in the Java virtual machine is in this state. 在JVM中执行的线程处于这种状态
BLOCKED(阻塞)
A thread that is blocked waiting for a monitor lock is in this state.
因为等待监视器锁而处于等待的线程处于这种状态
WAITING(不见不散)
A thread that is waiting indefinitely for another thread to perform a particular action is in this state. 等待另一个线程执行某些特别操作的线程处于这种状态。补充:实际上,这个线程等待的条件称为条件谓词,这个线程等待的位置称为条件队列。来源大牛的书《Java Concurrency in Proactice》
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. 等待另一个线程执行某些特别操作的线程(有时间限制)处于这种状态
TERMINATED(结束)
A thread that has exited is in this state. 一个已经执行完毕的线程处于这种状态
如果你想查看这几种状态,有以下两种方法:
1、直接查看jdk的api文档
,然后在文档右上角索引
处搜索Thread.Stete
,就可以查看了,获取jdk1.8的api文档点击我
2、在IDEA中输入Thread.State
,然后按住Ctrl键,在State
上点击鼠标左键就可以查看线程的几种状态了
1.3.2、线程在JVM中的状态转换
JDK源码的注释中详细的描述了不同状态之间在哪些条件下进行转换,本文中我用一个图来进行表示。 由于CPU的时间片轮转机制,处于Runnable状态的线程可以分为两种:Ready(就绪)和Running(运行)。因此,我们的状态图中有7种状态节点。 如下图:
上图中展示了线程在不同状态之间的转换情况,在调用与线程有关的方法后,线程会进入不同的线程状态,这些状态之间某些是双向的,比如WAITING和RUNNING之间可以循环的进行切换。而有些是单向的,比如终止后不能再次进入终止状态。针对上面的图,可以询问的面试点有很多。比如线程的监视器锁机制、比如线程协同的机制等等,读者要结合图片仔细研究。
1.3.3、使用例子展示线程的6种状态
代码:
public class Test {
private static Object lock = new Object();
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
synchronized (lock) {
// 带锁睡眠
TimeUnit.SECONDS.sleep(3);
// 不带锁睡眠
lock.wait();
}
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
synchronized (lock) {
// 带锁睡眠
TimeUnit.SECONDS.sleep(3);
// 唤醒其他锁
lock.notifyAll();
}
}
}, "t2");
// 此时t1已经创建
System.out.println(t1.getName() + ":" + t1.getState());
// 开启t1、t2线程
t1.start();
t2.start();
// 此时t1正在运行,但是没有获取锁睡眠
System.out.println(t1.getName() + ":" + t1.getState());
// 主线程睡眠1秒,让t1带着锁睡眠
TimeUnit.SECONDS.sleep(1);
// 此时t1带锁睡眠中
System.out.println(t1.getName() + ":" + t1.getState());
// 此时t2被阻塞
System.out.println(t2.getName() + ":" + t2.getState());
// 主线程睡眠4秒,让t2带着锁睡眠,t1不带锁睡眠
TimeUnit.SECONDS.sleep(4);
// 此时t1不带锁睡眠
System.out.println(t1.getName() + ":" + t1.getState());
// 主线程睡眠3秒,t1和t2都终止了
TimeUnit.SECONDS.sleep(2);
// 此时t1终止了
System.out.println(t1.getName() + ":" + t1.getState());
}
}
结果:
t1:NEW
t1:RUNNABLE
t1:TIMED_WAITING
t2:BLOCKED
t1:WAITING
t1:TERMINATED
123456
解释:
一共展示了线程的六种状态,别纠结t1和t2,你主要看的是线程状态,无论t1还是t2,我都是为了把所有的状态都展示出来
1.4、wait和sleep的区别
答:
- wait睡眠的时候会放开手中的锁,而sleep睡眠的时候会带着手中的锁
- wait和sleep都可以设置睡眠时间,那他们的线程进入的是
TIMED_WAITING
状态,如果wait不设置睡眠时间,那需要使用其他方法使用共享对象.notify()
或者共享对象.notifyAll()
方法唤醒,并且进入的是WAITING
状态 - sleep是Thread的静态方法,wait是Object的方法任何对象都能调用
- sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁
- 他们都可以被interrupted方法打断
1.5、并发和并行
📗串行模式
- 串行表示所有任务都一一按先后顺序进行。
- 串行意味着必须先装完一车柴才能运送这车柴,只有运送到了,才能卸下这车柴,并且只有完成了这整个三个步骤,才能进行下一个步骤。
- 串行是一次只能取得一个任务,并执行这个任务。
📗并行模式
- 并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。
- 并行操作相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上则依赖于多核CPU。
📗并发
- 并发(concurrent) 指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。
- 并发的重点在于它是一种现象,并发描述的是多进程同时运行的现象。但实际上,对于单核心CPU来说,同一时刻只能运行一个线程。所以,这里的"同时运行"表示的不是真的同一时刻有多个线程运行的现象,这是并行的概念,而是提供一种功能让用户看来多个程序同时运行起来了,但实际上这些程序中的进程不是一直霸占CPU的,而是执行一会停一会。
- 要解决大并发问题,通常是将大任务分解成多个小任务。由于操作系统对进程的调度是随机的,所以切分成多个小任务后,可能会从任一小任务处执行。这可能会出现一些现象∶
📗并发和并行联系
-
并发:同一时刻多个线程在访问同一个资源,多个线程对一个点I
- 例子︰春运抢票电商秒杀.…
-
并行:多项工作一起执行,之后再汇总
- 例子︰泡方便面,电水壶烧水,一边撕调料倒入桶中
1.6、管程
- 管程叫做
Monitor
,在操作系统中称为监视器
,在Java中也就是所说的锁
。 - 它是一种同步机制,保证同一时间,只有一个线程访问被保护数据或者代码。
- jvm同步基于进入和退出,是使用管程对象实现的。
- 管程对象会随着Java对象一起创建一起销毁
1.7、用户线程和守护线程
- 用户线程:自定义的线程。
- 守护线程(Daemon):是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当程序中的用户线程全部执行结束之后,守护线程也会跟随结束。比如说垃圾回收线程
想要查看线程到底是用户线程还是守护线程,可以通过 Thread.isDaemon() 方法来判断,如果返回的结果是 true 则为守护线程,反之则为用户线程。
public static void main(String[] args) {
Thread aa = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " :" + Thread.currentThread().isDaemon());
while (true) {
}
}, "aa");
//设置守护线程
aa.setDaemon(true);
aa.start();
System.out.println(Thread.currentThread().getName()+ ": over");
}
注意:
- 主线程结束,用户线程还在运行,jvm存活
- 没有用户线程,都是守护线程,jvm结束
二、Lock接口
2.1、Synchronized
2.1.1、Synchronized作用范围
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
-
修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{ }括起来的代码,作用的对象是调用这个代码块的对象;
synchronized(this){ }
-
修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized 关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。·
-
修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
-
修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。
2.1.2、Synchronized实现卖票的例子
多线程的编程步骤:
第一步 创建资源类,在资源类创建属性和操作方法
第二步 创建多个线程,调用资源类的操作方法。
package com.example.demo;
/**
* @author Administrator
* 三个售票员卖30张票
*/
/**
* 第一步:创建资源类,定义属性和操作方法
*/
class Ticket {
private int number = 30;
public synchronized void sale() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出时票数:" + (number--) + " 剩下:" +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 < 40; i++) {
ticket.sale();
}
}
}, "AA").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
}, "BB").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
}, "CC").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
}, "DD").start();
}
}
2.1.3、线程调用start方法会立马创建线程么
答案是不一定,Thread线程类调用的start方法,但是底层调用的是start0()方法,是native修饰的,因此调用之后需要由操作系统的一系列指令后才可以创建
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
2.2、什么是Lock接口
2.2.1、Lock接口介绍
Lock锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock提供了比 synchronized更多的功能。
2.2.2、Lock与Synchronized的区别
- Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。
- Lock是一个接口,通过这个接口的实现类可以实现同步访问;
- 采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized 代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动通过unLock()去释放锁,就有可能导致出现死锁现象。因此使用Lock 时需要在finally块中释放锁;
- Lock可以让等待锁的线程响应中断,而synchronized 却不行,使用synchronized 时,等待的线程会一直等待下去,不能够响应中断;
- Lock可以知道有没有成功获取锁,而synchronized 却无法办到。
- Lock可以提高多个线程进行读操作的效率。
- 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。
2.2.3、Lock锁实现卖票的例子
package com.example.demo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class LTicket {
private int number = 30;
private final ReentrantLock lock = new ReentrantLock();
public void sale() {
lock.lock();
try{
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出时票数:" + (number--) + " 剩下:" +number);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class LSaleTicket {
//第二步:创建多个线程,调用资源类和操作方法
public static void main(String[] args) {
LTicket ticket = new LTicket();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "AA").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "BB").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "CC").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "DD").start();
}
}
2.2.4、可重入锁
维基百科的解释:
若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
通俗来说:当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。
再换句话说:可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
2.3、创建线程的多种方式
- 继承Thread类
- 实现Runable接口,没有返回值,不抛出异常
- 实现Callable接口,有返回值,会抛出异常。这个返回值可以被Future 拿到,也就是Future 可以拿到异步执行任务的返回值,可以声明异常,需要以实现的callable接口的类为参数创建FutureTask对象,将FutureTask作为参数创建Thread对象;
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "*******************come in Callable");
Thread.sleep(2000);
return 1024;
}
}
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
new Thread(futureTask, "AA").start();
new Thread(futureTask, "BB").start();
int result1 = 100;
while (!futureTask.isDone()) {
}
//如果没有计算完成就要去强求,会导致阻塞,值得计算完成
int result2 = futureTask.get();
System.out.println("*****result: " +(result1 + result2));
}
}
- 使用线程池的方法创建线程
ExecutorService threadPool = Executors.newFixedThreadPool(5); //一池5个处理线程
ExecutorService threadPool = Executors.newSingleThreadExecutor();//一池1个处理线程
ExecutorService threadPool = Executors.newCachedThreadPool(); //一池N个处理线程
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
}
}catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
三、线程间通信
一、线程通信概述和案例分析
案列:
有两个线程,实现对一个初始值是0的变量。一个线程对值+1,另外一个线程对值-1,达到交替执行的效果。
通过Object类的wait()和notify()或者notifyAll()方法实现
- wait():在其他线程调用此对象的notify()方法或notifyAll()方法前,导致当前线程等待。
- notify():唤醒在此对象监视器上等待的单个线程。
- notifyAll(): 唤醒在此对象监视器上等待的所有线程。
3.1、Synchronized实现案例
1. package com.example.demo;
//第一步 创建资源类,定义属性和操作方法
class Share {
private int number = 0;
/**
* 加一的方法
* @throws InterruptedException
*/
public synchronized void incr() throws InterruptedException {
//第二步 判断 干活 通知
if (number != 0) {
//判断number的值是否为0,如果不是0,线程等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "::" + number);
this.notifyAll();
}
/**
* 减一的方法
* @throws InterruptedException
*/
public synchronized void decr() throws InterruptedException {
//判断
if (number != 1) {
this.wait();
}
//干活
number--;
System.out.println(Thread.currentThread().getName() + "::" + number);
//通知其它线程
this.notifyAll();
}
}
public class ThreadDemo1 {
//第三步 创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
Share share = new Share();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"AA").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"BB").start();
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fkYTW63L-1677494734161)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230226212217226.png)]
3.2、虚假唤醒问题
当新增了两个线程进行加1减1操作后,代码如下所示:
package com.example.demo;
//第一步 创建资源类,定义属性和操作方法
class Share {
private int number = 0;
/**
* 加一的方法
* @throws InterruptedException
*/
public synchronized void incr() throws InterruptedException {
//第二步 判断 干活 通知
if (number != 0) {
//判断number的值是否为0,如果不是0,线程等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "::" + number);
this.notifyAll();
}
/**
* 减一的方法
* @throws InterruptedException
*/
public synchronized void decr() throws InterruptedException {
//判断
if (number != 1) {
this.wait();
}
//干活
number--;
System.out.println(Thread.currentThread().getName() + "::" + number);
//通知其它线程
this.notifyAll();
}
}
public class ThreadDemo1 {
//第三步 创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
Share share = new Share();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"AA").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"BB").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"CC").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {