Skip to main content

多线程

基本概念

程序概念

一些列代码指令的集合统称 ,应用,软件等等 都属于程序。

程序运行必须依托于进程而存在,进程负责分配资源,依赖线程来运行。

单核心配置

运行 - msconfig - 引导 - 高级选项 - 处理器个数为 1

进程定义

进行中应用程序中属于资源分配的基本单位 。

应用程序执行的实例,拥有独立的内存空间和CPU资源。

线程定义

线程是包含在进程之中的, 一个进程至少有一个线程,否则将无法运行,线程是CPU调度运算的基本单位。

线程是CPU调度和分派的基本单位,应用程序运算是最小单位。

多线程

  • 一个进程中同时运行了多个线程,用来完成不同的工作,则称之为"多线程"。

  • 单核CPU下,多个线程交替占用CPU资源,而非真正的并行执行。

线程开辟和线程执行

线程不是越多越好,要结合实际的硬件环境来决定。

在单核心CPU下,多个线程是轮流交替执行的,以windows操作系统为例,多个线程随机轮流交替执行,每个线程最多执行20ms,然后继续切换下一个线程,而非并行执行,因为切换的频率非常快,所以我们感知不到这个过程,宏观上是同时执行的,实际上,是轮流交替执行的。 

并发并行

并发:同时发生,轮流交替执行;宏观上同时执行,微观轮流交替执行。

并行:严格意义上的同时执行,相当于一个请求生成一个线程。

主线程

main方法为程序的入口,底层由main线程负责执行,由JVM自动调用执行。

Thread类

  • java.lang.Thread 线程类

  • 优先级越高执行的概率越高。

属性

最高的优先级是10
最低的优先级是1
默认的优先级是5

构造方法

// 无参
Thread()

// 传入一个Runnable接口的实现类
Thread(Runnable target)

// 传入一个Runnable接口的实现类,再传入线程的名称
Thread(Runnable target, String name)

// 传入一个String类型的作为线程的名字
Thread(String name)

方法

getName()

作用:获取线程名称

参数:无

返回值:String类,

示例:
// 获取当前Thread类对象(线程对象)
Thread thread = Thread.currentThread();
// 打印线程名称
System.out.println("当前线程对象 " + thread); // Thread[main,5,main]
// 获取当前线程对象名称
String name = thread.getName();
System.out.println("当前线程名称 " + name); // main

currentThread()

作用:(静态方法)获取当前线程对象

参数:无

返回值:Thread类型,线程对象

示例:
// 获取当前Thread类对象(线程对象)
Thread thread = Thread.currentThread();
// 打印线程名称
System.out.println("当前线程对象 " + thread); // Thread[main,5,main]

setName(String name)

作用:设置线程名称

参数:String类型

返回值:无

示例:
// 获取当前Thread类对象(线程对象)
Thread thread = Thread.currentThread();
// 打印线程名称
System.out.println("当前线程对象 " + thread); // Thread[main,5,main]
// 获取当前线程对象名称
String name = thread.getName();
System.out.println("当前线程名称 " + name); // main
// 设置当前线程名称
thread.setName("主线程");

start()

作用:开启线程,向CPU提示自己准备就绪,可以被执行。

参数:无

返回值:无

示例:
// 创建 自定义线程对象
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();

// 设置线程名称
myThread1.setName("线程A");
myThread2.setName("线程B");

// 启动线程
myThread1.start();
myThread2.start();

sleep()

作用:休眠进程,进入阻塞状态。

参数:long 类型的 毫秒数

返回值:无

示例:
public class MyThread extends Thread{
@Override
public void run() { // 运行
// 自定义线程执行的代码
// 遍历当前线程的名称
for (int i = 0;i<=20;i++){
try {
// 方法重写,因为父类run方法没有声明任何异常 所以子类也不能声明任何异常
// 不能通过 throws xxx 的形式在方法上声明异常,只能捕获处理异常。
// 线程睡眠,进入阻塞状态
Thread.sleep(3000); // 休眠3秒钟 到达时间 自动继续执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行, 第" + i + "次");
}
// 线程执行完毕
System.out.println("线程执行完毕");
// 线程挂了
}
}

setPriority()

作用:设置线程的优先级,默认为5,范围1-10。从1~10,1最低,10最高,默认为5,优先级高的线程只是获得CPU资源的概率较大,并不一定能够保证优先执行;

参数:int类型

返回值:无

示例:
// 创建 自定义线程对象
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();

// 设置线程的优先级 方式1
myThread1.setPriority(10);
// 设置线程优先级 方式2
// myThread1.setPriority(MAX_PRIORITY); //MAX_PRIORITY-10 NORM_PRIORITY-5 MIN_PRIORITY-1
// 获取线程的优先级
int priority1 = myThread1.getPriority();// 线程A 的优先级为 10
int priority2 = myThread2.getPriority(); // 线程B 的优先级为 5
package com.ThreadPart;

public class MyThread extends Thread{
@Override
public void run() { // 运行
// 自定义线程执行的代码
// 遍历当前线程的名称
for (int i = 0;i<=20;i++){
System.out.println(Thread.currentThread().getName() + "执行, 第" + i + "次");
}
// 线程执行完毕
System.out.println("线程执行完毕");
// 线程挂了
}
}

getPriority()

作用:打印线程的优先级,默认为5,范围1-10。

参数:无

返回值:Int类型

示例:
// 创建 自定义线程对象
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();

// 设置线程的优先级
myThread1.setPriority(10);
// 获取线程的优先级
int priority1 = myThread1.getPriority();// 线程A 的优先级为 10
int priority2 = myThread2.getPriority(); // 线程B 的优先级为 5

join()

作用:线程插队,直到插队线程执行完毕。

参数:无

返回值:无

示例:
public static void main(String[] args) throws InterruptedException {
// 创建 自定义线程对象
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();

// 设置线程名称
myThread1.setName("线程A");
myThread2.setName("线程B");

// 启动线程
myThread1.start();
myThread2.start();

// 主线程遍历打印
for (int i = 0;i<=20;i++){
// 当主线程执行到第11次遍历时,线程A加队,优先执行完毕,再执行主线程
if (i == 10){
// 线程A优先执行完毕后,主线程再执行
myThread1.join();
}
System.out.println("主线程main执行" + "第" +i + "次");
}
}

join(long millis)

作用:线程插队,指定插队时间,只允许在插队的时间优先执行,过了时间按照随机轮流执行。

参数:long 类型 500毫秒

返回值:无

示例:
public static void main(String[] args) throws InterruptedException {
// 创建 自定义线程对象
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();

// 设置线程名称
myThread1.setName("线程A");
myThread2.setName("线程B");

// 启动线程
myThread1.start();
myThread2.start();

// 主线程遍历打印
for (int i = 0;i<=20;i++){
// 当主线程执行到第11次遍历时,线程A加队,优先执行1000毫秒时间,然后主线程继续执行
if (i == 10){
// 线程A优先执行1000毫秒时间,然后主线程继续执行
myThread1.join(1000);
}
System.out.println("主线程main执行" + "第" +i + "次");
}
}

join(long millis,int nanos)

作用:线程插队,指定插队时间,只允许在插队的时间优先执行,过了时间按照随机轮流执行。

参数:long 类型 500毫秒,第二个参数是纳秒

返回值:无

示例:
public static void main(String[] args) throws InterruptedException {
// 创建 自定义线程对象
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();

// 设置线程名称
myThread1.setName("线程A");
myThread2.setName("线程B");

// 启动线程
myThread1.start();
myThread2.start();

// 主线程遍历打印
for (int i = 0;i<=20;i++){
// 当主线程执行到第11次遍历时,线程A加队,优先执行1000毫秒时间,然后主线程继续执行
if (i == 10){
// 线程A优先执行1000毫秒,2000纳秒时间,然后主线程继续执行
myThread1.join(1000,2000);
}
System.out.println("主线程main执行" + "第" +i + "次");
}
}

yield()

作用:线程礼让,当前线程向调度器发出信息,表示当前线程正在执行的线程愿意让步,但是调度器可以忽略这个信息。

参数:无

返回值:

注意:线程的礼让,可能会礼让不成功,但是插队是一定能够插队成功的。

示例:
package com.ThreadPart;

public class TestMyThread {
public static void main(String[] args) throws InterruptedException {
// 创建 自定义线程对象
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();

// 设置线程名称
myThread1.setName("线程A");
myThread2.setName("线程B");

// 设置线程的优先级
// myThread1.setPriority(10);

// 获取线程的优先级
int priority1 = myThread1.getPriority();// 线程A 的优先级为 10
int priority2 = myThread2.getPriority(); // 线程B 的优先级为 5
// 打印优先级
System.out.println(myThread1.getName() + " 的优先级为 " + priority1); // 线程A优先运行完毕
System.out.println(myThread2.getName() + " 的优先级为 " + priority2);

// 启动线程
myThread1.start();
myThread2.start();

// 主线程遍历打印
// for (int i = 0;i<=20;i++){
// // 当主线程执行到第11次遍历时,线程A加队,优先执行完毕,再执行主线程
// if (i == 10){
// myThread1.join(1000,2000);
// }
// System.out.println("主线程main执行" + "第" +i + "次");
// }
}
}

package com.ThreadPart;

public class MyThread extends Thread{
@Override
public void run() { // 运行
// 自定义线程执行的代码
// 遍历当前线程的名称
for (int i = 0;i<=20;i++){
try {
// 线程睡眠,阻塞状态
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 当i取值为10 则礼让 但不保证一定会礼让成功
if (i == 10){
System.out.println(Thread.currentThread().getName() + "线程礼让了");
Thread.yield(); // 线程礼让
}
System.out.println(Thread.currentThread().getName() + "执行, 第" + i + "次");
}
// 线程执行完毕
System.out.println(Thread.currentThread().getName() + "线程执行完毕");
// 线程挂了
}
}

创建线程

创建的子线程如果没有指定名称,将默认以 Thread-0 -1,这种方式来命名;

方式1:继承Thread类,重写run方法

package com.ThreadPart;

public class MyThread extends Thread{
@Override
public void run() {
// 自定义线程执行的代码
// 遍历当前线程的名称
for (int i = 0;i<=20;i++){
System.out.println(Thread.currentThread().getName() + "执行, 第" + i + "次");
}
}
}

创建线程对象,启用线程。

package com.ThreadPart;

public class TestMyThread {
public static void main(String[] args) {
// 创建 自定义线程对象
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();

// 设置线程名称
myThread1.setName("线程A");
myThread2.setName("线程B");

// 启动线程
myThread1.start();
myThread2.start();
}
}

方式2:实现Runnable接口,重写run方法

  • 实现Runnable接口 重写run方法 Runnable实现类可以作为参数构造Thread实例
package com.ThreadPart;

public class RunnableImpl implements Runnable{

@Override
public void run() {
for (int i = 0;i<=20;i++){
System.out.println(Thread.currentThread().getName() + "执行, 第" + i + "次");
}
}
}

创建线程对象,使用Runnable实现类作为参数的构造方法创建实例

package com.ThreadPart;

public class TestMyThread2 {
public static void main(String[] args) {
// 创建Runnable实现类对象
RunnableImpl runnable1 = new RunnableImpl();

// 使用Thread的构造函数创建线程对象
Thread thread1 = new Thread(runnable1,"线程A");
Thread thread2 = new Thread(runnable1,"线程B");

// 准备就绪
thread1.start();
thread2.start();
}
}

  • 两种创建方式的区别(重点掌握)
1、继承Thread类,编写简单,可直接操作线程,适用于单继承;
2、实现Runnable接口,避免单继承局限性,便于共享资源;
  • 调用start方法和run方法的区别(重点掌握)
1、调用start方法表示通知调度器(CPU)当前线程准备就绪,调度器会开启新的线程来执行任务;
2、调用run方法表示使用当前主线程来执行方法,不会开启新的线程;

线程状态

image-20250801103243194

线程的5种状态:创建、就绪、运行、阻塞、死亡;

线程优先级

  • 优先级高的线程获取CPU资源的概率较大,并不保证优先执行,哪个线程执行是由调度器CPU来决定。

优先级示例

package com.ThreadPart;

public class MyThread extends Thread{
@Override
public void run() { // 运行
// 自定义线程执行的代码
// 遍历当前线程的名称
for (int i = 0;i<=20;i++){
try {
// 线程睡眠,阻塞状态
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 当i取值为10 则礼让 但不保证一定会礼让成功
// if (i == 10){
// System.out.println(Thread.currentThread().getName() + "线程礼让了");
// Thread.yield(); // 线程礼让
// }
System.out.println(Thread.currentThread().getName() + "执行, 第" + i + "次");
}
// 线程执行完毕
System.out.println(Thread.currentThread().getName() + "线程执行完毕");
// 线程挂了
}
}

package com.ThreadPart;

public class TestMyThread {
public static void main(String[] args) throws InterruptedException {
// 创建 自定义线程对象
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();

// 设置线程名称
myThread1.setName("线程A");
myThread2.setName("线程B");

// 设置线程的优先级
myThread1.setPriority(10);

// 获取线程的优先级
int priority1 = myThread1.getPriority();// 线程A 的优先级为 10
int priority2 = myThread2.getPriority(); // 线程B 的优先级为 5
// 打印优先级
System.out.println(myThread1.getName() + " 的优先级为 " + priority1); // 线程A优先运行完毕
System.out.println(myThread2.getName() + " 的优先级为 " + priority2);

// 启动线程
myThread1.start();
myThread2.start();

}
}

线程案例

爬山案例:模拟多人爬山。

需求:
每个线程代表一个人;
可设置每人爬山速度;
每爬完100米显示信息;
爬到终点时给出相应提示;
package com.ThreadPart;

/**
* 模拟多人爬山
* 分析:将run方法体的内容,作为两个角色共同执行的爬山过程,爬山的速度不一样,所以表示休眠的时间不同;
* 爬山的高度是相同的,角色名称不同,表示线程名不同,同时创建两个线程对象,分别 start方法,表示开始;
* 同时爬山,因为爬山速度不同,所以到达山顶时间是不同的.
*/

public class ClimbMountain extends Thread{
private String name;
private int length; // 长度 高度
private int time; // 每爬100米耗时
// 全参构造方法
public ClimbMountain(String name, int length, int time) {
// 通过父类的Thread构造方法设置线程名称
super(name);
this.name = name;
this.length = length;
}
// 重写run方法
@Override
public void run() {
// 循环遍历
while (length > 0){
// 不同通过 throws 的方式声明异常,因为是方法重写,不能超出父类的声明异常的范围
// 所以只能通过trycatch的方式捕获异常
try {
// 进程休眠,模拟爬山时间
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 每次爬山 高度降低100米
length -= 100;
// 提示信息
System.out.println(Thread.currentThread().getName() + "爬了100米,剩余" + length +"米");
}
// 循环结束
System.out.println(Thread.currentThread().getName() + "到达了山顶");
}
// 测试方法-同时也是主线程
public static void main(String[] args) {
// 创建爬山线程
ClimbMountain caixukun = new ClimbMountain("蔡徐坤",3000,200);
ClimbMountain mabaoguo = new ClimbMountain("马宝国",3000,400);

// 开启线程
caixukun.start();
mabaoguo.start();
}
}

模拟叫号看病案例

需求:
某科室一天需看普通号50个,特需号10个 (执行不同的次数)
特需号看病时间是普通号的2倍 (休眠时间)
开始时普通号和特需号并行叫号,叫到特需号的概率比普通号高(同时start开始执行 优先级不同)
当普通号叫完第10号时,要求先看完全部特需号,再看普通号 (插队)
使用多线程模拟这一过程

分析:子线程作为特需号类 主线程作为普通号类
package com.ThreadPart;

public class Special extends Thread{
@Override
public void run() {
// 循环10次,作为10次号
for (int i = 0;i < 10;i++){
// 看病时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 提示信息
System.out.println(Thread.currentThread().getName() + "第" + i + "号在看病");
}
System.out.println(Thread.currentThread().getName() + "看病完毕");
}
// main线程作为普通号
public static void main(String[] args) throws InterruptedException {
// 创建对象
Special special = new Special();
// 设置线程名称
special.setName("*****特需号*****");
// 设置线程优先级
special.setPriority(MAX_PRIORITY);
// 开启线程
special.start();
// 获取当前线程对象
Thread mainThread = Thread.currentThread();
// 设置当前线程对象的线程名称
mainThread.setName("普通号");
// 遍历普通号过程
for(int i = 1;i <= 50;i++){
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + "第" + i + "号在看病");

if(i == 10){
// 当遍历到第10号看病时,特殊号加队
special.join();
}
}
System.out.println(Thread.currentThread().getName() + "看病完毕");
}
}

同步关键字

synchronized(同步)关键字

可以用于修饰方法和代码块,分别表示同一时间只能有一个线程访问这个方法或者这个代码块。(需要排队,效率低)

基本使用

// 修饰方法(同步方法)
// 可以用于修饰方法 ,表示同一时间只能有一个线程访问这个方法
public synchronized void fangfaming(){

}


// 修饰代码块(同步代码块) / 同步代码块锁定的范围更加精确。
// 同步锁的是当前这个对象的访问权限,只有多个线程中访问的是同一个(Runnable)对象,才拥有锁定的效果。
// 当一个线程访问一个synchronized(this)同步代码块时,其他synchronized(this)同步代码块同样被锁定。
// 当一个线程访问一个synchronized(this)同步代码块时,其他线程可以访问该资源的非synchronized(this)同步代码
synchronized(this){ // this 表示当前实现类的对象

}

使用背景

当 ticketCount 为 2时,三个线程都进入了循环判断,然后进入休眠状态,休眠过后就会出现,三个线程继续往后执行代码,这个时候就出现了,不符合逻辑的情况,线程之间没有先后顺序,没有互相制约的效果,会导致票重复卖出,或者超卖的情况。

解决方案:多个线程必须排队买票,保证同一时间只能有一个线程(访问这段代码)买票,上一个线程执行完毕之后,下一个线程才能继续购买。

案例1:默认使用多线程出现的问题:

package com.ThreadPart;

// 未使用 sync线程同步关键字
public class BuyTicket1 implements Runnable{
// 定义属性
int ticketCount = 10;

@Override
public void run() {
while (ticketCount > 0){
// 线程休眠-保证每个线程都有机会抢到票
try {
Thread.sleep(500); // 三个线程都进入睡眠
} catch (InterruptedException e) {
e.printStackTrace();
}
// ticketCount--; 线程1执行完之后,线程2在循环体内,仍会执行
ticketCount--;
// 提示信息
System.out.println(Thread.currentThread().getName() + "抢到了第" + (10 - ticketCount) + "张票,还剩余" + ticketCount + "张票");
}
System.out.println("票卖完了");
}

// 测试类
public static void main(String[] args) {
// 创建Runnable对象
BuyTicket1 runnable = new BuyTicket1();
// 创建线程对象
Thread th1 = new Thread(runnable, "赵四");
Thread th2 = new Thread(runnable, "广坤");
Thread th3 = new Thread(runnable, "大拿");

th1.start();
th2.start();
th3.start();
}
}

案例2:使用方式2来创建线程,同时设置同步关键字,修饰代码块,保证线程安全。

  • 同步代码块锁定的范围更加精确
package com.ThreadPart;

public class BuyTicket2 implements Runnable{
// 定义属性
int ticketCount = 10;

@Override
public void run() {
while (true){
// 首先进入方法中,三个线程都先进行休眠,当有一个线程苏醒了,然后进入代码执行,他执行完之后,后面苏醒的线程才有机会进入访问。
// 线程休眠-保证每个线程都有机会抢到票
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 使用 synchronized 修饰代码块
// 三个线程对象 共享一个 Runnable实现类对象,保证唯一的一份,ticketCount为10;
synchronized (this){
if(ticketCount == 0){
break;
}
// 卖票
ticketCount--;
// 提示信息
System.out.println(Thread.currentThread().getName() + "抢到了第" + (10 - ticketCount) + "张票,还剩余" + ticketCount + "张票");
}
}
System.out.println("票卖完了");

}
// 测试类
public static void main(String[] args) {
// 创建Runnable对象
// 三个线程对象 共享一个 Runnable实现类对象,保证唯一的一份,ticketCount为10;
BuyTicket2 runnable = new BuyTicket2();
// 创建线程对象
Thread th1 = new Thread(runnable, "赵四");
Thread th2 = new Thread(runnable, "广坤");
Thread th3 = new Thread(runnable, "大拿");

th1.start();
th2.start();
th3.start();
}
}

案例3:使用方式2来创建线程,同时设置同步关键字,修饰方法,保证线程安全。

  • 可以用于修饰方法 ,表示同一时间只能有一个线程访问这个方法
package com.ThreadPart;

public class BuyTicket2 implements Runnable{
// 定义属性
int ticketCount = 10;

@Override
public synchronized void run() {
while (ticketCount > 0){
// 线程休眠-保证每个线程都有机会抢到票
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票
ticketCount--;
// 提示信息
System.out.println(Thread.currentThread().getName() + "抢到了第" + (10 - ticketCount) + "张票,还剩余" + ticketCount + "张票");
}
System.out.println("票卖完了");

}
// 测试类
public static void main(String[] args) {
// 创建Runnable对象
BuyTicket2 runnable = new BuyTicket2();
// 创建线程对象
Thread th1 = new Thread(runnable, "赵四");
Thread th2 = new Thread(runnable, "广坤");
Thread th3 = new Thread(runnable, "大拿");

th1.start();
th2.start();
th3.start();
}
}

案例4:使用方式2来创建线程,同时设置同步关键字,修饰代码块,同步代码块中的this。

// 同步锁的是当前这个对象的访问权限,this 指向多个线程中访问的是同一个(Runnable)对象,才拥有锁定的效果。只要是 多个线程访问的是同一个对象空间就可以

package com.ThreadPart;

public class BuyTicket2 implements Runnable{
// 定义属性
int ticketCount = 10;
// 创建一个对象
Object obj = new Object();
// 重写run方法
@Override
public void run() {
while (true){
// 线程休眠-保证每个线程都有机会抢到票
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 这里使用 Runnable实现类对象中的
synchronized (obj){
if (ticketCount == 0){
break;
}
// 卖票
ticketCount--;
// 提示信息
System.out.println(Thread.currentThread().getName() + "抢到了第" + (10 - ticketCount) + "张票,还剩余" + ticketCount + "张票");
}
}
System.out.println("票卖完了");
}
// 测试类
public static void main(String[] args) {
// 创建Runnable对象
BuyTicket2 runnable = new BuyTicket2();
// 创建线程对象
Thread th1 = new Thread(runnable, "赵四");
Thread th2 = new Thread(runnable, "广坤");
Thread th3 = new Thread(runnable, "大拿");

th1.start();
th2.start();
th3.start();
}
}

线程安全

之前接触到线程安全的类 StringBuffer Vector Hashtable 都是使用同步关键字synchronized修饰方法实现的线程安全。