Java 基础语法:多线程

12731 字
64 分钟
Java 基础语法:多线程

学习目标:

  • 除了标记了解的, 其余的都掌握

引例:单线程不能满足”同时”的需求#

假如我要实现如下功能 程序不停地在屏幕上输出一句问候的语句(比如“你好”) “同时”,当我通过键盘输入固定输入的时候,程序停止向屏幕输出问候的语句(比如说输入gun)

单线程

无法做到”同时”发生

package _19thread01.com.cskaoyan._01introduction;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;
/**
* @description:
* @author: 景天
* @date: 2022/10/19 9:11
**/
/*
程序不停地在屏幕上输出一句问候的语句(比如“你好”) ----> sayHello()
“同时”,当我通过键盘输入固定输入的时候, --->waitToStop()
程序停止向屏幕输出问候的语句(比如说输入gun)
*/
public class Demo {
public static boolean flag = true;
public static void main(String[] args) {
System.out.println("main before");
System.out.println("hello before");
sayHello();
System.out.println("hello after");
System.out.println("wait before");
waitToStop();
System.out.println("wait after");
System.out.println("main after");
}
private static void waitToStop() {
Scanner scanner = new Scanner(System.in);
// 多次输入
while (flag) {
String s = scanner.nextLine();
if ("gun".equals(s)) {
flag = false;
break;
}
}
}
private static void sayHello() {
while (flag) {
System.out.println("你好");
try {
// 让程序暂停执行3秒
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

多线程

可以做到同时发生

package _19thread01.com.cskaoyan._01introduction;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;
/**
* @description:
* @author: 景天
* @date: 2022/10/19 9:11
**/
/*
程序不停地在屏幕上输出一句问候的语句(比如“你好”) ----> sayHello()
“同时”,当我通过键盘输入固定输入的时候, --->waitToStop()
程序停止向屏幕输出问候的语句(比如说输入gun)
多线程改进
*/
public class Demo2 {
public static boolean flag = true;
public static void main(String[] args) {
System.out.println("main before");
System.out.println("hello before");
sayHello();
System.out.println("hello after");
System.out.println("wait before");
waitToStop();
System.out.println("wait after");
System.out.println("main after");
}
private static void waitToStop() {
new Thread(new Runnable() {
@Override
public void run() {
Scanner scanner = new Scanner(System.in);
// 多次输入
while (flag) {
String s = scanner.nextLine();
if ("gun".equals(s)) {
flag = false;
break;
}
}
}
}).start();
}
private static void sayHello() {
new Thread(new Runnable() {
@Override
public void run() {
while (flag) {
System.out.println("你好");
try {
// 让程序暂停执行3秒
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}

image-20221019100623466
image-20221019100623466

操作系统基本概念#

进程(process)与线程(thread)#

进程

  • 计算机程序在某个数据集合上的运行活动.进程是操作系统进行资源调度与分配的基本单位
  • 正在运行的程序或者软件

线程

  • 进程中有多个子任务,每个子任务就是一个线程. 从执行路径的角度看, 一条执行路径就是一个线程
  • 线程是CPU进行资源调度与分配的基本单位

进程与线程的关系

  • 线程依赖于进程而存在
  • 一个进程中可以有多个线程(最少1个)
  • 线程共享进程资源
  • 举例: 迅雷, Word拼写

串行(serial),并行(parallel)与并发(concurrency)#

串行

  • 一个任务接一个任务按顺序执行

并行

  • 在同一个时间点(时刻)上, 多个任务同时运行

并发

  • 在同一时间段内,多个任务同时运行

image-20221019102440447
image-20221019102440447

同步(synchronization)与异步(asynchronization)#

有2个任务(业务) A B

同步:

  • A任务执行的时候B不能执行,按顺序执行

  • 你走我不走

异步:

  • A任务执行的时候,B任务可以执行
  • 你走你的,我走我的,互相不干扰
  • 多线程是天生异步的

举例: 去书店买java书, 给老板打了个电话, 问有没有java书

同步: 老板说找一下,然后告诉我, 电话没有挂断, 我在电话另一端等待.

异步:老板说找一下,然后告诉我, 电话挂断. 等老板找到后, 再通知我, 我在电话另一端不用等待

单道批处理: 内存中只能运行一个进程

多道批处理: 内存中可以运行多个进程, “同时”发生 (进程的上下文切换)

现代操作系统: 引入了线程

java程序运行原理#

java命令+主类类名运行原理#

  • java命令会启动jvm进程, jvm进程会创建一个线程(main线程)
  • 执行main线程里面的main方法

jvm是单线程还是多线程的#

结论: jvm是多线程的

除了main线程外,还有其他线程,起码还有一个垃圾回收线程

多线程的实现方式一:继承Thread类#

线程 是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。

文档示例#

image-20221019111047094
image-20221019111047094

步骤#

  1. 定义一个类继承Thread类
  2. 重写run方法
  3. 创建子类对象
  4. 通过start方法启动线程

Demo

package _19thread01.com.cskaoyan._02implone;
import java.util.concurrent.TimeUnit;
/**
* @description:
* @author: 景天
* @date: 2022/10/19 11:11
**/
/*
多线程的实现方式一:
1. 定义一个类继承Thread类
2. 重写run方法
3. 创建子类对象
4. 通过start方法启动线程
*/
public class Demo {
public static void main(String[] args) {
System.out.println("main start");
//3. 创建子类对象
MyThread t = new MyThread();
// 4. 通过start方法启动线程
t.start();
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main end");
}
}
//1. 定义一个类继承Thread类
class MyThread extends Thread{
//2. 重写run方法
@Override
public void run() {
System.out.println("子线程执行了!");
}
}
没加等待的结果:
main start
main end
子线程执行了!
加上等待的结果:
main start
子线程执行了!
main end

注意事项#

多线程的执行特点是什么?

  • 执行特点是随机的
package _19thread01.com.cskaoyan._02implone;
/**
* @description:
* @author: 景天
* @date: 2022/10/19 11:19
**/
/*
多线程的执行特点
*/
public class Demo2 {
public static void main(String[] args) {
// 创建并启动2个线程
MyThread2 t1 = new MyThread2();
MyThread2 t2 = new MyThread2();
// start()
t1.start();
t2.start();
}
}
class MyThread2 extends Thread{
// run
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// getName() 获取线程名称
System.out.println(getName()+"----"+i);
}
}
}
执行结果:
Thread-1----0
Thread-1----1
Thread-0----0
Thread-0----1
Thread-0----2
Thread-0----3
Thread-0----4
Thread-0----5
Thread-1----2
Thread-0----6
Thread-0----7
Thread-0----8
Thread-0----9
Thread-1----3
Thread-1----4
Thread-1----5
Thread-1----6
Thread-1----7
Thread-1----8
Thread-1----9

start方法跟run方法有什么区别?

  • start方法才是开辟新的执行路径, run方法只是普通方法调用, 并没有开辟新的执行路径, 还是一条执行路径, 仍然是单线程的.
package _19thread01.com.cskaoyan._02implone;
/**
* @description:
* @author: 景天
* @date: 2022/10/19 11:19
**/
/*
start() vs run()
*/
public class Demo3 {
public static void main(String[] args) {
System.out.println("main start");
// 创建并启动2个线程
MyThread3 t1 = new MyThread3();
// start()
// t1.start();
// run()方法只是一个普通方法调用
t1.run();
System.out.println("main end");
}
}
class MyThread3 extends Thread{
// run
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
调用start()方法的结果:
main start
main end
0
1
2
3
4
5
6
7
8
9
调用run()方法的结果:
main start
0
1
2
3
4
5
6
7
8
9
main end

同一个线程能否启动多次?

  • 不能启动多次, java.lang.IllegalThreadStateException

  • package _19thread01.com.cskaoyan._02implone;
    /**
    * @description:
    * @author: 景天
    * @date: 2022/10/19 11:19
    **/
    /*
    同一个线程对象能否启动多次?
    */
    public class Demo4 {
    public static void main(String[] args) {
    System.out.println("main start");
    // 创建并启动2个线程
    MyThread4 t1 = new MyThread4();
    // start()
    t1.start();
    t1.start();
    System.out.println("main end");
    }
    }
    class MyThread4 extends Thread{
    // run
    @Override
    public void run() {
    for (int i = 0; i < 10; i++) {
    System.out.println(i);
    }
    }
    }

谁才代表一个线程?

  • Thread及其子类对象才代表线程, 就是t1,t2

设置获取线程名称#

获取名字

StringgetName() 返回该线程的名称。
默认名字Thread-编号 从0开始
static ThreadcurrentThread() 返回对当前正在执行的线程对象的引用。

设置名字

voidsetName(String name) 改变线程名称,使之与参数 name 相同。

还可以通过**Thread**(String name) 分配新的 Thread 对象。设置名称

package _19thread01.com.cskaoyan._03api;
/**
* @description:
* @author: 景天
* @date: 2022/10/19 11:38
**/
/*
设置获取线程名称
*/
public class GetSetNameDemo {
public static void main(String[] args) {
// 获取主线程的名称
// currentThread()
// 返回对当前正在执行的线程对象的引用。
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
// 创建并启动2个线程
MyThread t1 = new MyThread("王道吴彦祖");
MyThread t2 = new MyThread("王道彭于晏");
// setName(String name)
// 改变线程名称,使之与参数 name 相同。
//t1.setName("王道吴彦祖");
//t2.setName("王道彭于晏");
t1.start();
t2.start();
}
}
class MyThread extends Thread{
public MyThread(String name) {
super(name);
}
// run
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"----"+i);
}
}
}

线程的调度方式#

什么是线程调度#

概念: 给线程分配CPU处理权的过程

调度方式的分类#

  • 协同式线程调度
    • 线程的执行时间由线程本身决定, 当这个线程执行完后报告操作系统,切换到下一个线程
  • 抢占式的线程调度方式
    • 线程的执行时间由系统决定, 哪个线程抢到了CPU的执行,哪个线程执行

java中采用哪种调度方式#

Java中采用的是抢占式的调度方式

线程的优先级(了解)#

操作系统优先级#

动态优先级

  • 正在执行的线程会随着执行时间的延长, 优先级降低
  • 正在等待的线程会随着等待的时间的延长,优先级会升高

静态优先级

  • 固定数值

动态优先级+静态优先级

java中优先级#

静态优先级 1-10

static intMAX_PRIORITY 线程可以具有的最高优先级。10
static intMIN_PRIORITY 线程可以具有的最低优先级。 1
static intNORM_PRIORITY 分配给线程的默认优先级。 5

设置获取优先级

intgetPriority() 返回线程的优先级。
voidsetpriority(int n) 设置线程优先级

Demo

package _19thread01.com.cskaoyan._03api;
/**
* @description:
* @author: 景天
* @date: 2022/10/19 14:38
**/
/*
获取设置线程优先级
*/
public class PriorityDemo {
public static void main(String[] args) {
// 创建子类对象
MyThread2 t = new MyThread2();
// setPriority(int n)
//t.setPriority(Thread.MAX_PRIORITY);
t.setPriority(Thread.MIN_PRIORITY);
// getPriority()
int priority = t.getPriority();
System.out.println("priority = " + priority);
// start
t.start();
}
}
class MyThread2 extends Thread{
//run
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}

优先级案例

创建并启动2个线程 A B

A线程设置最大优先级10

B线程设置最小优先级 1

每个线程打印10个数

结论: 做不到A先打印完 B打印完

优先级没有卵用.

然而,我们在java语言中设置的线程优先级,它仅仅只能被看做是一种”建议”(对操作系统的建议), 实际上,操作系统本身,有它自己的一套线程优先级 (静态优先级 + 动态优先级)

java官方: 线程优先级并非完全没有用,我们Thread的优先级,它具有统计意义,总的来说,高优先级的线程 占用的cpu执行时间多一点,低优先级线程,占用cpu执行时间,短一点

package _19thread01.com.cskaoyan._03api;
/**
* @description:
* @author: 景天
* @date: 2022/10/19 14:42
**/
/*
优先级案例
创建并启动2个线程 A B
A线程设置最大优先级10
B线程设置最小优先级 1
每个线程打印10个数
*/
public class Ex {
public static void main(String[] args) {
// 创建2个线程
MyThread3 t1 = new MyThread3("A");
MyThread3 t2 = new MyThread3("B");
// 设置优先级
t1.setPriority(10);
t2.setPriority(1);
// 启动
t1.start();
t2.start();
}
}
class MyThread3 extends Thread{
//run
public MyThread3(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+"-----"+i);
}
}
}

线程控制API#

线程休眠sleep#

static voidsleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
package _19thread01.com.cskaoyan._03api;
import java.util.concurrent.TimeUnit;
/**
* @description:
* @author: 景天
* @date: 2022/10/19 14:50
**/
/*
线程休眠
*/
public class SleepDemo {
public static void main(String[] args) {
System.out.println("main start");
//ThreadSleep t = new ThreadSleep();
//t.start();
new ThreadSleep().start();
//try {
// Thread.sleep(3000);
//} catch (InterruptedException e) {
// e.printStackTrace();
//}
System.out.println("main end");
}
}
class ThreadSleep extends Thread{
//run
@Override
public void run() {
System.out.println("子线程启动");
for (int i = 0; i < 10; i++) {
System.out.println(i);
// 休眠1s
// TimeUnit.SECONDS.sleep(1) 跟 Thread.sleep(1000)等价
try {
Thread.sleep(1000);
// TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

线程合并join#

voidjoin() 等待该线程终止。

谁等待?

执行结果上看main线程在等待, join这行代码在哪个线程上运行, 哪个线程等待

等待谁?

执行结果上看等待的是子线程, 哪个线程调用了join, 等待的就是这个线程

image-20221019150652986
image-20221019150652986

package _19thread01.com.cskaoyan._03api;
/**
* @description:
* @author: 景天
* @date: 2022/10/19 14:57
**/
/*
线程加入(合并)
*/
public class JoinDemo {
public static void main(String[] args) {
System.out.println("main start");
// 创建线程对象
ThreadJoin t = new ThreadJoin();
// 启动线程
t.start();
// 调用join
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main end");
}
}
class ThreadJoin extends Thread{
// run
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
没有使用join的输出结果:
main start
main end
0
1
2
3
4
5
6
7
8
9
使用join后输出结果:
main start
0
1
2
3
4
5
6
7
8
9
main end

线程礼让yield#

static voidyield() 暂停当前正在执行的线程对象,并执行其他线程。

创建并启动2个线程 A B

都是打印10个数

要求A打印0, B打印0, A打印1, B打印1…

结论: 通过yield方法做不到

package _19thread01.com.cskaoyan._03api;
/**
* @description:
* @author: 景天
* @date: 2022/10/19 15:10
**/
/*
线程礼让
创建并启动2个线程 A B
都是打印10个数
要求A打印0, B打印0, A打印1, B打印1.....
*/
public class YieldDemo {
public static void main(String[] args) {
// 创建并启动2个线程
new ThreadYield("A").start();
new ThreadYield("B").start();
}
}
class ThreadYield extends Thread{
public ThreadYield(String name) {
super(name);
}
// run
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+"----"+i);
// 立刻执行yield方法
// 暂停当前正在执行的线程对象,并执行其他线程。
Thread.yield();
// 虽然yield方法使当前线程放弃了CPU的执行权 但是仍然可以参与下轮的CPU的竞争
}
}
}

守护线程setDaemon#

线程分类

  • 用户线程(默认)
    • 系统的工作线程
  • 守护线程
    • 为用户线程服务的线程(GC垃圾回收线程), 系统的后台线程, 可以把它当做用户线程的奴仆
voidsetDaemon(boolean on) 将该线程标记为守护线程或用户线程
on - 如果为 true,则将该线程标记为守护线程。

注意:

  • 当正在运行的线程都是守护线程时,Java 虚拟机退出。
  • 该方法必须在启动线程前调用。 (start之前) java.lang.IllegalThreadStateException
package _19thread01.com.cskaoyan._03api;
/**
* @description:
* @author: 景天
* @date: 2022/10/19 15:45
**/
/*
守护线程
*/
public class DaemonDemo {
public static void main(String[] args) {
System.out.println("main start");
// 创建线程对象
ThreadDaemon t = new ThreadDaemon();
// 把t这个线程设置为守护线程
t.setDaemon(true);
// start
t.start();
// main 打印3个数
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+
"----"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("main end");
}
}
class ThreadDaemon extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+"----"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

线程中断stop(已过时,了解)#

voidstop() 已过时。 该方法具有固有的不安全性

安全中断线程#

案例:

定义一个flag标记, true 是正常状态 false中断

主线程打印3个数 打印1个 休眠1秒 中断子线程

创建子线程 打印10个数 休眠1秒

打印之前判断一下是否中断 如果正常----> 打印数据

如果发生了中断-------> 不在打印, 并且把中断信息保存到log.txt文件中

格式 年月日 时分秒 哪个线程发生了中断

package _19thread01.com.cskaoyan._03api;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @description:
* @author: 景天
* @date: 2022/10/19 16:11
**/
/*
安全的中断线程
案例:
定义一个flag标记, true 是正常状态 false中断
主线程打印3个数 打印1个 休眠1秒 中断子线程
创建子线程 打印10个数 休眠1秒
打印之前判断一下是否中断 如果正常----> 打印数据
如果发生了中断-------> 不在打印, 并且把中断信息保存到log.txt文件中
格式 年月日 时分秒 哪个线程发生了中断
*/
public class SecurityStopDemo {
public static void main(String[] args) {
// 创建线程对象
ThreadStop2 t = new ThreadStop2();
// start启动
t.start();
for (int i = 0; i < 3; i++) {
System.out.println("main----"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 中断子线程
t.flag = false;
}
}
class ThreadStop2 extends Thread{
// 定义一个标记
boolean flag = true;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 判断是否发生了中断
// 如果正常 正常打印
if (flag) {
System.out.println(getName()+"----"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
// 发生了中断
// 把中断信息保存log.txt文件中
// 创建输出流对象
FileWriter fileWriter = null;
try {
fileWriter = new FileWriter("log.txt");
// 创建SimpleDataFormat对象 指定格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = sdf.format(new Date());
// write(String s)
fileWriter.write(date+getName()+"发生了中断!");
// 格式 年月日 时分秒 哪个线程发生了中断
// flush
fileWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (fileWriter != null) {
try {
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
}

线程的生命周期#

线程的几种状态#

理论层面的状态#

新建

  • 刚new出来的线程对象

就绪

  • 线程执行了start()方法后

执行

  • 拥有CPU的执行权

阻塞

  • 线程会处于阻塞状态

死亡

  • run方法执行完

代码层面的状态#

  • NEW 至今尚未启动的线程处于这种状态。
  • RUNNABLE 正在 Java 虚拟机中执行的线程处于这种状态。
  • BLOCKED 受阻塞并等待某个监视器锁的线程处于这种状态。
  • WAITING 无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。
  • TIMED_WAITING 等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。
  • TERMINATED 已退出的线程处于这种状态。

线程状态的转换#

image-20221019163422301
image-20221019163422301

多线程实现方式二:实现Runnable接口#

文档示例#

image-20221019163830087
image-20221019163830087

步骤#

  1. 定义一个类实现Runnable接口
  2. 重写run方法
  3. 创建子类对象
  4. 创建线程对象, 把实现了Runnable接口的子类对象作为参数传递
  5. start方法启动线程

Demo

package _19thread01.com.cskaoyan._04impltwo;
/**
* @description:
* @author: 景天
* @date: 2022/10/19 16:40
**/
/*
多线程的实现方式二:
1. 定义一个类实现Runnable接口
2. 重写run方法
3. 创建子类对象
4. 创建线程对象, 把实现了Runnable接口的子类对象作为参数传递
5. start方法启动线程
*/
public class Demo {
public static void main(String[] args) {
//3. 创建子类对象
MyRunnable myRunnable = new MyRunnable();
// 4. 创建线程对象, 把实现了Runnable接口的子类对象作为参数传递
Thread t = new Thread(myRunnable);
// 5. start方法启动线程
t.start();
}
}
//1. 定义一个类实现Runnable接口
class MyRunnable implements Runnable{
@Override
public void run() {
// 2. 重写run方法
System.out.println("子线程执行了!");
}
}

其他写法 匿名内部类 lambda

package _20thread02.com.cskaoyan._01impltwo;
/**
* @description:
* @author: 景天
* @date: 2022/10/20 9:28
**/
/*
其他写法
*/
public class Demo {
public static void main(String[] args) {
// 匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("1111");
}
}).start();
// lambda
new Thread(()->{
System.out.println("222222");
}).start();
}
}

为什么Runnable中的run方法会运行在子线程中#

class Thread implements Runnable{
// 成员变量
private Runnable target;
// 构造方法
Thread(Runnable target){
init(target);
}
void init(Runnable target){
// 左边是成员变量 右边是传过来的参数 给成员变量赋值
this.target = target;
}
void run(){
if(target != null){
target.run()
}
}
}

方式一VS方式二#

  • 步骤上, 方式一4步, 方式二是5步
  • 方式一通过继承的方式(单继承的局限性),方式二通过实现接口的方式
  • 方式二把线程跟线程上要做的事情区分开来(执行路径,跟执行路径上的任务区分开来) 解耦
  • 方式二便于数据共享

多线程仿真如下场景: 假设A电影院正在上映某电影,该电影有100张电影票可供出售,现在假设有3个窗口售票。请设计程序模拟窗口售票的场景。

分析: 3个窗口售票,互不影响,同时进行。 -----> 多线程 3个线程 3个窗口共同出售这100张电影票 -------> 多线程共享数据

package _20thread02.com.cskaoyan._02datasecurity;
/**
* @description:
* @author: 景天
* @date: 2022/10/20 9:55
**/
/*
多线程仿真如下场景:
假设A电影院正在上映某电影,该电影有100张电影票可供出售,现在假设有3个窗口售票。请设计程序模拟窗口售票的场景。
分析:
3个窗口售票,互不影响,同时进行。
3个窗口共同出售这100张电影票
*/
public class Demo2 {
public static void main(String[] args) {
SellWindow2 myRunnable = new SellWindow2();
// 创建3个线程 并启动
Thread t1 = new Thread(myRunnable);
Thread t2 = new Thread(myRunnable);
Thread t3 = new Thread(myRunnable);
t1.setName("A窗口");
t2.setName("B窗口");
t3.setName("C窗口");
// start
t1.start();
t2.start();
t3.start();
}
}
// 方式二模拟
class SellWindow2 implements Runnable{
// 定义成员变量
int tickets = 100;
//run
@Override
public void run() {
// 卖票
while (true) {
// 分析 重复的票
// 假设ABC3个线程
// 假设A抢到了CPU的执行权 tickets = 100
// 假设B抢到了CPU的执行权 tickets = 100
// C抢到了CPU的执行权 tickets = 100
// 分析不存在的票
// 假设ABC3个线程
// 假设A抢到了CPU的执行权 tickets = 1
// 假设B抢到了CPU的执行权 tickets = 1
// 假设C抢到了CPU的执行权 tickets = 1
// 判断一下 >0 允许卖
if (tickets > 0) {
// A进来 睡觉
// B进来 睡觉
// C进来 睡觉
// 模拟网络延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖了第"+
(tickets -- ) + "票");
// tickets -- 分为几步?
// 1.取值 2.-1 3.重新赋值
// 假设A睡醒 取值100 没来及-1 被抢走了执行权
// B抢到了 取值100 没来及-1 被抢走了执行权
// C抢到了 取值100
// 分析不存在的情况
// 假设A睡醒 A打印 A窗口卖了第1张票 还剩0张
// 假设B睡醒 B窗口卖了第0张票 还剩-1张
// 假设C睡醒 C窗口卖了第-1张票 还剩-2张
}
}
}
}

多线程数据安全问题#

造成的现象#

  • 出现重复的票

image-20221020100918897
image-20221020100918897

  • 出现不存在的票

image-20221020101103507
image-20221020101103507

产生原因#

  • 多线程的运行环境(需求)
  • 多线程共享数据(需求)
  • 存在非原子操作
    • 原子操作: 一个不可分割的操作(一个操作要么一次执行完, 要么不执行)

解决多线程数据安全问题#

思路: 由产生的原因入手

需求无法更改

只能改非原子操作

引入锁的概念

image-20221020110451908
image-20221020110451908

synchronized#

同步代码块#

同步代码块的锁对象(对象 , 用来充当锁的角色)

可以是任意的对象, 但是要保证是同一个

synchronized(锁对象){
// 对共享数据的访问操作
}
package _20thread02.com.cskaoyan._03sync;
/**
* @description:
* @author: 景天
* @date: 2022/10/20 11:07
**/
/*
同步代码块
*/
public class Demo {
public static void main(String[] args) {
SellWindow myRunnable = new SellWindow();
// 创建3个线程 并启动
Thread t1 = new Thread(myRunnable);
Thread t2 = new Thread(myRunnable);
Thread t3 = new Thread(myRunnable);
t1.setName("A窗口");
t2.setName("B窗口");
t3.setName("C窗口");
// start
t1.start();
t2.start();
t3.start();
}
}
class SellWindow implements Runnable{
// 定义成员变量
int tickets = 100;
// 定义一把锁
//A obj = new A();
Object obj = new Object();
//run
@Override
public void run() {
// 卖票
while (true) {
// 锁对象是谁?
synchronized (obj) {
// 对共享数据的访问操作
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖了第"+
(tickets -- ) + "票");
}
}
}
}
}
class A{}

同步方法#

同步方法的锁对象是this

package _20thread02.com.cskaoyan._03sync;
/**
* @description:
* @author: 景天
* @date: 2022/10/20 11:07
**/
/*
同步方法
*/
public class Demo2 {
public static void main(String[] args) {
SellWindow2 myRunnable = new SellWindow2();
// 创建3个线程 并启动
Thread t1 = new Thread(myRunnable);
Thread t2 = new Thread(myRunnable);
Thread t3 = new Thread(myRunnable);
t1.setName("A窗口");
t2.setName("B窗口");
t3.setName("C窗口");
// start
t1.start();
t2.start();
t3.start();
}
}
class SellWindow2 implements Runnable {
// 定义成员变量
int tickets = 100;
// 定义一把锁
//Object obj = new Object();
//B obj = new B();
int i = 0;
//run
@Override
public void run() {
// 卖票
while (true) {
if (i % 2 == 0) {
synchronized (this) {
// 对共享数据的访问操作
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了第" +
(tickets--) + "票");
}
}
} else {
sell();
}
i++;
}
}
private synchronized void sell() {
// 对共享数据的访问操作
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了第" +
(tickets--) + "票");
}
}
}
class B{}

静态同步方法#

静态同步方法的锁对象是字节码文件对象(Class对象)

package _20thread02.com.cskaoyan._03sync;
/**
* @description:
* @author: 景天
* @date: 2022/10/20 11:07
**/
/*
静态同步方法
*/
public class Demo3 {
public static void main(String[] args) {
SellWindow3 myRunnable = new SellWindow3();
// 创建3个线程 并启动
Thread t1 = new Thread(myRunnable);
Thread t2 = new Thread(myRunnable);
Thread t3 = new Thread(myRunnable);
t1.setName("A窗口");
t2.setName("B窗口");
t3.setName("C窗口");
// start
t1.start();
t2.start();
t3.start();
}
}
class SellWindow3 implements Runnable {
// 定义成员变量
static int tickets = 100;
// 定义一把锁
Object obj = new Object();
//B obj = new B();
int i = 0;
//run
@Override
public void run() {
// 卖票
while (true) {
if (i % 2 == 0) {
synchronized (SellWindow3.class) {
// 静态的同步方法的锁对象是字节码文件对象 Class对象
// 对象.getClass()
// 类名.class属性
// Class.forName(String 全类名)
// 对共享数据的访问操作
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了第" +
(tickets--) + "票");
}
}
} else {
sell();
}
i++;
}
}
private static synchronized void sell() {
// 对共享数据的访问操作
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了第" +
(tickets--) + "票");
}
}
}

synchronized的细节#

执行流程#

  • AB2个线程访问sync代码块中的内容
  • 假设A线程抢到了CPU的执行权, 看一下锁对象是否可用, 可用, A线程就持有了锁对象, A线程访问同步代码块的内容
  • A还没有访问结束,发生了线程切换,B抢到了执行权,B也想访问同步代码块中的内容, 看一下锁是否可用, 不可用, 对于B线程来说, 只能在sync外面等待, B就处于同步阻塞状态
  • A再次抢到执行权. A接着执行,访问结束, 退出sync代码块, A释放锁
  • B线程就可以获取锁, 访问sync代码块中的内容.
package _20thread02.com.cskaoyan._03sync;
import java.util.concurrent.TimeUnit;
/**
* @description:
* @author: 景天
* @date: 2022/10/20 11:38
**/
/*
synchronized执行流程
*/
public class Demo4 {
// 定义一把锁
public static final Object OBJECT = new Object();
public static void main(String[] args) {
// 创建并启动一个线程
new Thread(()->{
// sync
synchronized (OBJECT) {
System.out.println("A进入sync");
// 休眠
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A退出sync");
}
},"A").start();
// main睡眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 创建并启动一个线程
new Thread(()->{
System.out.println("B线程执行了!");
// sync
synchronized (OBJECT) {
System.out.println("B进入sync!");
}
},"B").start();
}
}

出现异常会释放锁#

package _20thread02.com.cskaoyan._03sync;
import java.util.concurrent.TimeUnit;
/**
* @description:
* @author: 景天
* @date: 2022/10/20 14:29
**/
/*
验证异常会释放锁
*/
public class Demo5 {
// 定义一把锁
public static final Object OBJECT = new Object();
public static int count = 0;
public static void main(String[] args) {
// 创建并启动一个线程
new Thread(()->{
// sync
synchronized (OBJECT) {
System.out.println("A线程进入sync");
while (true) {
count++;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5) {
System.out.println(count);
// 人为制造异常
System.out.println(10/0);
}
}
}
},"A").start();
// sleep
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 创建并启动一个线程
new Thread(()->{
System.out.println("B线程已经执行了");
// sync
synchronized (OBJECT) {
System.out.println("B线程进入sync");
}
},"B").start();
}
}

1个对象的内存布局#

image-20221020144401249
image-20221020144401249

2条字节码指令(monitorenter/monitorexit)#

image-20221020145322378
image-20221020145322378

image-20221020145617634
image-20221020145617634

Lock#

基本使用

image-20221020145930843
image-20221020145930843

voidlock() 获取锁。
voidunlock() 释放锁

ReentrantLock可重入锁#

一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。

package _20thread02.com.cskaoyan._04lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @description:
* @author: 景天
* @date: 2022/10/20 15:02
**/
/*
Lock的使用
*/
public class Demo {
public static void main(String[] args) {
SellWindow myRunnable = new SellWindow();
// 创建3个线程 并启动
Thread t1 = new Thread(myRunnable);
Thread t2 = new Thread(myRunnable);
Thread t3 = new Thread(myRunnable);
t1.setName("A窗口");
t2.setName("B窗口");
t3.setName("C窗口");
// start
t1.start();
t2.start();
t3.start();
}
}
class SellWindow implements Runnable{
// 定义成员变量
int tickets = 100;
// 定义一把锁
Lock lock = new ReentrantLock();
//run
@Override
public void run() {
// 卖票
while (true) {
// 加锁, 获取锁
lock.lock();
try {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖了第"+
(tickets -- ) + "票");
}
}finally {
// 释放锁
lock.unlock();
}
}
}
}

synchronized VS Lock

  • synchronized是关键字, Lock是个接口
  • synchronized是一把隐式的锁, 加锁和释放锁是由jvm自动完成的. Lock它是一把真正的(显式的)锁, 我们能看到加锁跟释放锁的过程(lock() , unlock())

死锁#

什么是死锁#

2个或以上线程因为争抢资源而造成的互相等待的现象

image-20221020151428459
image-20221020151428459

死锁产生的场景#

一般出现在同步代码块嵌套

synchronized(objA){
synchronized(objB){
}
}
package _20thread02.com.cskaoyan._05dielock;
/**
* @description:
* @author: 景天
* @date: 2022/10/20 15:17
**/
public class Demo {
public static void main(String[] args) {
// 创建并启动2个线程
new Thread(new DieLock(true)).start();
new Thread(new DieLock(false)).start();
}
}
//定义一个锁类
class MyLock{
public static final Object objA = new Object();
public static final Object objB = new Object();
}
// 死锁类
class DieLock implements Runnable{
boolean flag;
public DieLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
// 同步代码块嵌套
if (flag) {
synchronized (MyLock.objA) {
// 假设A线程先执行 A线程获取A锁
System.out.println("if A");
synchronized (MyLock.objB) {
System.out.println("if B");
}
}
}else{
synchronized (MyLock.objB) {
// B线程进来 获取了B锁
System.out.println("else B");
synchronized (MyLock.objA) {
System.out.println("else A");
}
}
}
}
}

怎么解决死锁#

更改加锁的顺序

package _20thread02.com.cskaoyan._05dielock;
/**
* @description:
* @author: 景天
* @date: 2022/10/20 15:17
**/
public class Demo2 {
public static void main(String[] args) {
// 创建并启动2个线程
new Thread(new DieLock2(true)).start();
new Thread(new DieLock2(false)).start();
}
}
//定义一个锁类
class MyLock2{
public static final Object objA = new Object();
public static final Object objB = new Object();
}
// 死锁类
class DieLock2 implements Runnable{
boolean flag;
public DieLock2(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
// 同步代码块嵌套
if (flag) {
synchronized (MyLock2.objA) {
// 假设A线程先执行 A线程获取A锁
System.out.println("if A");
synchronized (MyLock2.objB) {
System.out.println("if B");
}
}
}else{
synchronized (MyLock2.objA) {
// B线程进来 获取了B锁
System.out.println("else B");
synchronized (MyLock2.objB) {
System.out.println("else A");
}
}
}
}
}

再加一把锁, 变成原子操作

package _20thread02.com.cskaoyan._05dielock;
/**
* @description:
* @author: 景天
* @date: 2022/10/20 15:17
**/
public class Demo3 {
public static void main(String[] args) {
// 创建并启动2个线程
new Thread(new DieLock3(true)).start();
new Thread(new DieLock3(false)).start();
}
}
//定义一个锁类
class MyLock3{
public static final Object objA = new Object();
public static final Object objB = new Object();
// 新加的锁
public static final Object objC = new Object();
}
// 死锁类
class DieLock3 implements Runnable{
boolean flag;
public DieLock3(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
// 同步代码块嵌套
if (flag) {
synchronized (MyLock3.objC) {
synchronized (MyLock3.objA) {
// 假设A线程先执行 A线程获取A锁
System.out.println("if A");
synchronized (MyLock3.objB) {
System.out.println("if B");
}
}
}
}else{
synchronized (MyLock3.objC) {
synchronized (MyLock3.objB) {
// B线程进来 获取了B锁
System.out.println("else B");
synchronized (MyLock3.objA) {
System.out.println("else A");
}
}
}
}
}
}

生产者消费者模型#

image-20221020155255443
image-20221020155255443

V1 使用同步代码块

package _20thread02.com.cskaoyan._07producer_consumer.v1;
// 定义蒸笼类
public class Box {
// 定义成员变量
Food food;
// 定义方法
// 生产包子的方法 只有生产者才执行
public void setFood(Food newFood) {
// 表示放入包子
food = newFood;
System.out.println(Thread.currentThread().getName() +
"生产了" + food);
}
// 吃包子的方法 只有消费者执行
public void eatFood() {
System.out.println(Thread.currentThread().getName() +
"吃了" + food);
food = null;
}
// 判断蒸笼状态的方法
public boolean isEmpty() {
return food == null;
// true ---> 空
// false ---> 非空
}
}
// 定义包子类
class Food{
// 成员变量
String name;
int price;
public Food(String name, int price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "Food{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
package _20thread02.com.cskaoyan._07producer_consumer.v1;
public class ConsumerTask implements Runnable {
// 成员变量
Box box;
public ConsumerTask(Box box) {
this.box = box;
}
@Override
public void run() {
// 吃包子
while (true) {
synchronized (box) {
//判断蒸笼状态
if (box.isEmpty()) {
// 如果蒸笼为空 ,
// 没有包子阻止自己吃包子
//   wait
try {
box.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
// 如果蒸笼非空 , 有包子
// 消费吃包子通知生产者再生产
box.eatFood();
// notify
box.notify();
}
}
}
}
}
package _20thread02.com.cskaoyan._07producer_consumer.v1;
import java.util.Random;
public class ProducerTask implements Runnable{
// 成员变量
Box box;
Food[] foods = {new Food("韭菜鸡蛋", 1),
new Food("生蚝包", 10),
new Food("羊腰子包", 20)};
Random random = new Random();
public ProducerTask(Box box) {
this.box = box;
}
@Override
public void run() {
// 生产包子
while (true) {
synchronized (box) {
// 判断蒸笼的状态
if (box.isEmpty()) {
// 如果蒸笼为空, 没有包子生产包子 放进去
int index = random.nextInt(foods.length);
box.setFood(foods[index]);
// 通知消费者吃 notify
box.notify();
}else{
// 如果蒸笼非空, 有包子 生产者不能生产
// 阻止自己生产 wait
try {
box.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
package _20thread02.com.cskaoyan._07producer_consumer.v1;
public class Demo {
public static void main(String[] args) {
// 创建蒸笼对象
Box box = new Box();
// 创建生产者任务
ProducerTask producerTask = new ProducerTask(box);
// 创建消费者任务
ConsumerTask consumerTask = new ConsumerTask(box);
// 创建生产者线程
Thread t1 = new Thread(producerTask);
// 创建消费者线程
Thread t2 = new Thread(consumerTask);
t1.setName("生产者");
t2.setName("消费者");
// 启动
t1.start();
t2.start();
}
}

V2 使用同步方法

package _20thread02.com.cskaoyan._07producer_consumer.v2;
public class Box {
Food food;
// 生产包子的方法 生产者执行
public synchronized void setFood(Food newFood) {
// 判断蒸笼的状态
if (food == null) {
// 如果蒸笼为空,
// 没有包子生产包子 放进去通知消费者吃
food = newFood;
System.out.println(Thread.currentThread().getName()+
"生产了"+food);
//  notify
this.notify();
}else{
// 如果蒸笼非空, 有包子生产者不能生产 阻止自己生产
// wait
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 吃包子的方法 消费者执行
public synchronized void eatFood() {
// 判断蒸笼状态
if (food == null) {
// 如果蒸笼为空 , 没有包子阻止自己吃包子
//   wait
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
// 如果蒸笼非空 , 有包子
// 消费吃包子通知生产者再生产
System.out.println(Thread.currentThread().getName()+
"吃了"+food);
food = null;
// notify
this.notify();
}
}
}
class Food{
String name;
int price;
@Override
public String toString() {
return "Food{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
public Food(String name, int price) {
this.name = name;
this.price = price;
}
}
package _20thread02.com.cskaoyan._07producer_consumer.v2;
public class ConsumerTask implements Runnable {
Box box;
public ConsumerTask(Box box) {
this.box = box;
}
@Override
public void run() {
// 只吃包子
while (true) {
box.eatFood();
}
}
}
package _20thread02.com.cskaoyan._07producer_consumer.v2;
import java.util.Random;
public class ProducerTask implements Runnable {
Box box;
public ProducerTask(Box box) {
this.box = box;
}
Food[] foods = {new Food("豆沙包", 1),
new Food("小笼包", 6),
new Food("狗不理", 40)};
Random random = new Random();
@Override
public void run() {
// 只做一件事 生产包子
while (true) {
int index = random.nextInt(foods.length);
box.setFood(foods[index]);
}
}
}
package _20thread02.com.cskaoyan._07producer_consumer.v2;
public class Demo {
public static void main(String[] args) {
// 创建蒸笼对象
Box box = new Box();
// 创建生产者任务
ProducerTask producerTask = new ProducerTask(box);
// 创建消费者任务
ConsumerTask consumerTask = new ConsumerTask(box);
// 创建生产者线程
Thread t1 = new Thread(producerTask);
// 创建消费者线程
Thread t2 = new Thread(consumerTask);
t1.setName("生产者");
t2.setName("消费者");
// 启动
t1.start();
t2.start();
}
}

当有多个生产 多个消费者的时候, 出现”卡顿”的现象, 为什么?

c1,c2,p1,p2 都start
c1抢到 > 进入sync , 空的, wait ,释放锁
c2抢到 > 进入sync,空的, wait , 释放锁
p1抢到 > 进入sync, 空的可以生产, notify唤醒c1 , 退出sync,释放锁
p1又抢到 > 进入sync, 非空, wait, 释放锁
p2抢到> 进入sync, 非空, wait 释放锁
c1抢到 > 进入sync, 非空,吃, notify 唤醒c2 ,退出sync 释放锁
c2 > 进入 sync, 空, wait 释放锁
c1 > 进入 sync, 空 , wait
到此 所有线程都wait
怎么解决?
notifyAll

线程间通信#

wait与notify机制#

wait与notify机制

拥有相同锁的线程才可以实现wait/notify机 制,所以后面的描述中都是假定操作同一个锁。

  • wait()方法是Object类的方法,它的作用是使当前执行wait()方法的线程等待,在wait()所在的 代码行处暂停执行,并释放锁,直到接到通知被唤醒。在调用wait()之前,线程必须获得锁对象,即只能在同步方法或同步块中调用wait()方法。如果调用wait()时没有持有适当的锁,则抛出 IllegalMonitorStateException,它是 RuntimeException的一个子类,因此不需要try-catch语句捕捉异常。
  • notify()方法要在同步方法或同步块中调用, 即在调用前,线程必须获得锁对象,如果调用notify() 时没有持有适当的锁,则会抛IllegalMonitorStateException。该方法用来通知那 些可能等待该锁对象的其他线程,如果有多个线程等待,则唤醒其中随机一个线程,并使该线程重新获取锁。
  • 需要说明的是,执行notify()方法后,当 前线程不会马上释放该锁,因wait方法而阻塞的线程也 并不能马上获取该对象锁,要等到执行notify()方 法的线程将程序执行完,也就是退出synchronized 同步区域后,当前线程才会释放锁,而处于阻塞状 态的线程才可以获取该对象锁。当第一个获得了 该对象锁的wait线程运行完毕后,它会释放该对 象锁,此时如果没有再次使用notify语句,那么其 他呈阻塞状态的线程因为没有得到通知,会继续 处于阻塞状态。

总结:wait()方法使线程暂停运行,而notify() 方法通知暂停的线程继续运行

wait()#

1. 阻塞功能:
当在某线程中,对象上.wait(), 在哪个线程中调用wait(), 导致哪个线程处于阻塞状态
当某线程,因为调用执行某对象的wait(),而处于阻塞状态,我们说,该线程在该对象上阻塞。
2. 唤醒条件
当某线程,因为某对象A的wait(), 而处于阻塞状态时,如果要唤醒该线程,只能在其他线程中,
再同一个对象(即对象A)上调用其notify()或notifyAll()
即在线程的阻塞对象上,调用notify或notifyAll方法,才能唤醒,在该对象上阻塞的线程
3. 运行条件
当前线程必须拥有此对象监视器。
监视器:指synchronized代码块中的锁对象
即我们只能在,当前线程所持有的synchronized代码块中的,锁对象上调用wait方法,
才能正常执行
如果没有锁对象就会有这样一个异常 IllegalMonitorStateException
4. 执行特征
a.该线程发布(release)对此监视器的所有权
b.等待(阻塞)
注意:Thread的sleep方法,执行的时候:
该线程不丢失任何监视器的所属权

执行条件与特点#

执行条件

需要锁对象

package _20thread02.com.cskaoyan._06wait_notify;
/*
使用条件
*/
public class Demo {
public static void main(String[] args) {
// 对象.wait()
Object o = new Object();
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// java.lang.IllegalMonitorStateException
}
}

执行特点

package _20thread02.com.cskaoyan._06wait_notify;
/*
执行特点
*/
public class Demo2 {
public static void main(String[] args) {
// 对象.wait()
Object o = new Object();
synchronized (o) {
try {
System.out.println("wait before");
o.wait();
System.out.println("wait after");
} catch (InterruptedException e) {
e.printStackTrace();
}
/*
输出结果:
wait before
main线程处于阻塞状态
想要after打印出来, 必须在别的线程中, 同一个锁对象上调用notify方法唤醒
*/
}
}
}

验证wait释放锁#

package _20thread02.com.cskaoyan._06wait_notify;
/*
wait方法释放锁
*/
public class Demo3 {
// 定义一把锁
public static final Object OBJECT = new Object();
public static void main(String[] args) {
// 创建并启动一个线程
new Thread(()->{
// sync
synchronized (OBJECT) {
System.out.println("A线程进入sync");
try {
Thread.sleep(10000);
System.out.println("wait before");
OBJECT.wait();
System.out.println("wait after");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
// sleep
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 创建并启动一个线程
new Thread(()->{
System.out.println("B线程已经执行了");
// sync
synchronized (OBJECT) {
System.out.println("B线程进入sync!");
}
},"B").start();
}
}

wait与notify的基本使用#

package _20thread02.com.cskaoyan._06wait_notify;
/*
wait notify的基本使用
*/
public class Demo4 {
// 定义一把锁
public static final Object OBJECT = new Object();
public static void main(String[] args) {
// 创建并启动一个线程
new Thread(()->{
// sync
synchronized (OBJECT) {
System.out.println("A进入sync");
// 调用wait
try {
Thread.sleep(5000);
System.out.println("wait before");
OBJECT.wait();
System.out.println("wait after");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
// sleep
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 创建并启动一个线程
new Thread(()->{
System.out.println("B已经执行了");
// sync
synchronized (OBJECT) {
System.out.println("B进入sync");
// 调用notify
System.out.println("notify before");
OBJECT.notify();
System.out.println("notify after");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}

练习

创建2个线程A B

A线程打印1,B线程打印2, A线程打印3, B打印4… B线程打印100(使用wait notify)

package _20thread02.com.cskaoyan._06wait_notify;
/*
练习
创建2个线程A B
A线程打印1,B线程打印2, A线程打印3, B打印4.... B线程打印100(使用wait notify)
*/
public class Ex {
// 定义一把锁
public static final Object OBJECT = new Object();
public static void main(String[] args) {
// 创建A线程 1 3 5....99
new Thread(()->{
// sync
synchronized (OBJECT) {
for (int i = 1; i < 100; i+=2) {
// 唤醒另一个线程
OBJECT.notify();
// 打印
System.out.println(Thread.currentThread().getName()+
"----"+i);
// 阻止自己打印 wait
try {
OBJECT.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// notify 把对方唤醒
OBJECT.notify();
}
},"A").start();
// sleep
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 创建B线程 2 4 6.....100
new Thread(()->{
// sync
synchronized (OBJECT) {
for (int i = 2; i <= 100; i+=2) {
// 唤醒另一个线程
OBJECT.notify();
// 打印
System.out.println(Thread.currentThread().getName()+
"----"+i);
// 阻止自己打印 wait
try {
OBJECT.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// notify()唤醒对象
OBJECT.notify();
}
},"B").start();
}
}

sleep VS wait#

Thread.sleep VS Object.wait()
1. 所属不同:
a. sleep定义在Thread类,静态方法
b. wait定义在 Object类中,非静态方法
2. 唤醒条件不同
a. sleep: 休眠时间到
b. wait: 在其他线程中,在同一个锁对象上,调用了notify或notifyAll方法
3. 使用条件不同:
a. sleep 没有任何前提条件
b. wait(), 必须当前线程,持有锁对象,锁对象上调用wait()
4. 休眠时,对锁对象的持有,不同:(最最核心的区别)
a. 线程因为sleep方法而处于阻塞状态的时候,在阻塞的时候不会放弃对锁的持有
b. 但是wait()方法,会在阻塞的时候,放弃锁对象持有

notify()#

  • 唤醒在此对象监视器上等待的单个线程。
  • 如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。
  • 选择是任意性的

notifyAll()#

唤醒多个等待的线程

为什么wait,notify,notifyAll方法不定义在Thread类中?#

任意Java对象都能充当锁的角色

完整的线程状态转换图#

理论层面#

image-20221021100134163
image-20221021100134163

代码层面#

image-20221021100731019
image-20221021100731019

多线程工具#

线程池#

线程池(Thread Pool)是一种基于池化技术的并发执行模式,主要用来管理线程的创建、销毁和任务分配,以提高程序执行效率和资源利用率。线程池在处理大量短生命周期的任务时尤为有用,因为它减少了线程创建和销毁的开销。

Thread t = new Thread();
t.start();
Thread t1 = new Thread();
t1.start();

线程工具类中提供的3种线程池#

Executors: 线程工具类, 负责产生线程池

ExecutorServices: 代表线程池对象

//JDK5提供了一Executors来产生线程池,有如下方法:
ExecutorService newCachedThreadPool()
// 特点:
// 1.会根据需要创建新线程,也可以自动删除,60s处于空闲状态的线程
// 2.线程数量可变,立马执行提交的异步任务(异步任务:在子线程中执行的任务)
ExecutorService newFixedThreadPool(int nThreads)
// 特点:
// 1.线程数量固定
// 2.维护一个无界队列(暂存已提交的来不及执行的任务)
// 3.按照任务的提交顺序,将任务执行完毕
ExecutorService newSingleThreadExecutor()
// 特点:
// 1.单个线程
// 2.维护了一个无界队列(暂存已提交的来不及执行的任务)
// 3.按照任务的提交顺序,将任务执行完毕

线程池的使用

  • Future<?> submit(Runnable task)
  • Future submit(Callable task)

Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。

计算完成后只能使用 get 方法来获取结果,如有必要,计算完成前可以阻塞此方法

Future 用来存储返回值的结果(Callable是带返回值的)

// 如有必要,等待计算完成,然后获取其结果
V get()

Runnable类型的任务

package _21thread03.com.cskaoyan._01threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/*
向线程池中提交Runnable类型的任务
*/
public class Demo {
public static void main(String[] args) {
// 创建线程池
ExecutorService pool = Executors.newCachedThreadPool();
// 提交任务
// submit(Runnable task)
pool.submit(new RunnableTask());
pool.submit(new RunnableTask());
}
}
class RunnableTask implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+
"----"+i);
}
}
}

提交Callable类型的任务

package _21thread03.com.cskaoyan._01threadpool;
import java.util.concurrent.*;
/*
提交Callable类型的任务
*/
public class Demo2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
// 提交任务
// submit(Callable task)
Future<String> future = pool.submit(new CallableTask());
// 接收返回值
// get()
System.out.println("get before");
String s = future.get();
System.out.println("get after");
System.out.println(s);
}
}
class CallableTask implements Callable<String>{
@Override
public String call() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
TimeUnit.SECONDS.sleep(10);
return "Call执行结束";
}
//@Override
//public Object call() throws Exception {
// return null;
//}
}

关闭线程池

// 启动一次顺序关闭,执行以前提交的任务,但不接受新任务
shutdown()
// 试图停止所有正在执行的活动任务,暂停处理正在等待的任务。
shutdownNow()

多线程的实现方式三:实现Callable接口#

不借助线程池

需要借助FutureTask

FutureTask = Future + Task

构造方法

// 创建一个 FutureTask,一旦运行就执行给定的 Callable。
FutureTask(Callable<V> callable)

继承关系

image-20221021110859791
image-20221021110859791

基本使用

package _21thread03.com.cskaoyan._02call;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/*
使用Callable
*/
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建FutureTask对象
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
// 创建线程对象
Thread t = new Thread(futureTask);
// start
t.start();
// get() 获取结果
String s = futureTask.get();
System.out.println(s);
}
}
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("call执行了");
return "1234";
}
}

练习

创建2个线程,A线程计算1+2+3…+100的结果

B线程计算1+2+3+…200的结果

使用Callable

package _21thread03.com.cskaoyan._02call;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/*
练习
创建2个线程,A线程计算1+2+3...+100的结果
B线程计算1+2+3+...200的结果
使用Callable
*/
public class Ex {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建FutureTask对象
FutureTask<Integer> futureTask1 = new FutureTask<>(new SumTask(100));
FutureTask<Integer> futureTask2 = new FutureTask<>(new SumTask(200));
// 创建线程对象
Thread t1 = new Thread(futureTask1);
Thread t2 = new Thread(futureTask2);
// start启动
t1.start();
t2.start();
// get方法获取计算的结果
Integer result1 = futureTask1.get();
Integer result2 = futureTask2.get();
System.out.println("result1 = " + result1);
System.out.println("result2 = " + result2);
}
}
class SumTask implements Callable<Integer> {
// 成员变量
int num;
public SumTask(int num) {
this.num = num;
}
@Override
public Integer call() throws Exception {
// 计算
int sum =0;
for (int i = 1; i <= num ; i++) {
sum+=i;
}
return sum;
}
}

Runnable VS Callable

  • Runnable里是run方法, Callable里是call方法
  • Runnable没有返回值, Callable有返回值

线程池核心参数#

ThreadPoolExecutor 是 Java 中 java.util.concurrent 包提供的一个强大的线程池实现。

它实现了 ExecutorService 接口,提供了一种灵活的线程池管理机制,允许程序员自行设置线程池中的核心参数.

public class ThreadPoolExecutor extends AbstractExecutorService {
// 核心线程数
private volatile int corePoolSize;
// 最大线程数
private volatile int maximumPoolSize;
// 阻塞队列
private final BlockingQueue<Runnable> workQueue;
// 存活时间
private volatile long keepAliveTime;
// 拒绝策略
private volatile RejectedExecutionHandler handler;
// 默认拒绝策略
private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();
}

线程池参数配置

  • corePoolSize:核心线程数,即使它们处于空闲状态,线程池也会尽力维持该数量的线程。
  • maximumPoolSize:线程池允许创建的最大线程数。
  • keepAliveTime:当线程数超过核心线程数时,这是超出数量的空闲线程在终止前等待新任务的最长时间。
  • unitkeepAliveTime 的时间单位。
  • workQueue:任务队列,用于存放待执行任务的阻塞队列

任务拒绝策略

  • AbortPolicy:抛出 RejectedExecutionException(默认情况)。
  • CallerRunsPolicy:直接在调用者线程中运行任务。
  • DiscardPolicy:丢弃无法处理的任务。
  • DiscardOldestPolicy:丢弃队列中最旧的未处理任务,并尝试再次提交当前任务

线程池执行流程#

每次往线程池中提交任务的时候,有如下的处理流程:

step1:判断当前线程数是否大于或等于corePoolSize。如果小于,则新建线程执行;如果大于,则进入step2。

step2:判断队列是否已满。如未满,则放入;如已满,则进入step3。

step3:判断当前线程数是否大于或等于maxPoolSize。如果小于,则新建线程执行;如果大于,则进入step4。

step4:根据拒绝策略,拒绝任务。

总结一下:首先判断corePoolSize,其次判断blockingQueue是否已满,接着判断maxPoolSize,最后使用拒绝策略。

image-20240423170932132
image-20240423170932132

定时器与定时任务#

定时器Timer#

一种工具,线程用其安排以后在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行。

构造方法

// 创建一个新计时器。
Timer()

调度方法

schedule(TimerTask task, Date time)
schedule(TimerTask task, long delay, long period)
schedule(TimerTask task, Date firstTime, long period)
scheduleAtFixedRate(TimerTask task, long delay, long period)
schedule VS scheduleAtFixedRate 区别
追赶特性

取消计时器

// 终止此计时器,丢弃所有当前已安排的任务。
void cancel()

定时任务TimerTask#

public abstract class TimerTask
//由 Timer 安排为一次执行或重复执行的任务

如何使用:

  • 定义一个任务继承TimerTask
  • 重写run方法
package _21thread03.com.cskaoyan._03timer;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
public class Demo {
public static void main(String[] args) throws ParseException, InterruptedException {
// 创建定时器
Timer timer = new Timer();
// 调度
// schedule(TimerTask task, Date time)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time = "2022-10-21 11:48:00";
Date date = sdf.parse(time);
// timer.schedule(new MyTask(), date);
//schedule(TimerTask task, long delay, long period)
timer.schedule(new MyTask(),5000,3000);
//schedule(TimerTask task, Date firstTime, long period)
//timer.schedule(new MyTask(), date, 3000);
//scheduleAtFixedRate(TimerTask task, long delay, long period)
// 取消定时器
TimeUnit.SECONDS.sleep(15);
timer.cancel();
}
}
// 定义一个定时任务
// 继承TimerTask
// 重写run
class MyTask extends TimerTask{
@Override
public void run() {
System.out.println("炸弹爆炸了Boom!");
}
}

单例设计模式补充(线程安全)#

  1. 构造方法私有
  2. 提供一个全局的自身的成员变量
  3. 提供一个静态的方法获取实例

同步方法

package com.cskaoyan._04singleton;
public class Singleton {
//2. 提供一个全局的自身的成员变量
private static Singleton instance;
// 1. 构造方法私有
private Singleton() {
}
//3. 提供一个静态的方法获取实例
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

double check

package com.cskaoyan._04singleton;
public class Singleton2 {
//2. 提供一个全局的自身的成员变量
private static Singleton2 instance;
// 1. 构造方法私有
private Singleton2() {
}
//3. 提供一个静态的方法获取实例
public static Singleton2 getInstance() {
// double check
if (instance == null) {
// 假设A线程抢到了CPU执行权 A执行
// A持有锁对象
// 切换B线程 B执行
// B没有锁 进入不了sync
synchronized (Singleton2.class) {
// A线程进来
// B进来了
// 第二次校验
if (instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
}

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Java 基础语法:多线程
https://firefly-mu-weld.vercel.app/posts/17-多线程/
作者
Daisy
发布于
2026-06-10
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Daisy
Hello, I'm Daisy.
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签

文章目录