JAVA进阶 —— 多线程
创始人
2025-05-29 14:17:35
0

目录

一、什么是多线程 ?

1. 线程与进程

2. 并发和并行

 二、 多线程的实现方式

1. 多线程的第一种实现方式:Thread类

2. 多线程的第二种实现方式:Runnable接口

3. 多线程的第三种实现方式:Callable接口和Future接口

4. 多线程三种实现方式的对比

三、多线程常用的成员方法

四、线程的生命周期

五、线程安全问题

 1. 同步代码块

2. 同步方法

3. lock锁

六、死锁

七、生产者和消费者 ( 等待唤醒机制 )

 1.  消费者代码实现

2.  生产者代码实现

八、线程池

 1. 线程池方法实现

2.自定义线程池

3.最大并行数

 九、综合练习

1.  抢红包

2.  抽奖


一、什么是多线程 ?

  • 有了多线程,我们就可以让程序同时做多件事情。

1. 线程与进程

  • 线程是操作系统中能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
  • 简单理解:应用穿件中相互独立,可以同时运行的功能。

  • 进程是程序的基本执行实体。

2. 并发和并行

  • 并发:在同一时刻,有多个指令在单个CPU上交替执行。
  • 并行:在同一时刻,有多个指令在多个CPU上同时执行。

 二、 多线程的实现方式

  1. 继承Thread类的方式进行实现
  2. 实现Runnable接口的方式进行实现
  3. 利用Callable接口Future接口的方式进行实现

1. 多线程的第一种实现方式:Thread类

首先我们通过查找API帮助文档 了解 Thread类是什么?

多线程第一种实现方式:

  1.  自己手动定义一个类继承Thread类。
  2.  重写里面run方法
  3.  创建子类对象,并启动线程。
public class ThreadDemo01 {public static void main(String[] args) {// 多线程第一种实现方式:// 1.自己定义一个类继承Thread类// 2.重写里面run方法// 3.创建子类对象,并启动线程myThread t1 = new myThread();myThread t2 = new myThread();// t1.run() 只是单纯调用一个方法t1.setName("线程1");t1.setName("线程2");t1.start();t2.start();}
}myThread.java
public class myThread extends Thread {@Overridepublic void run() {//书写线程执行代码for (int i = 0; i < 100; i++) {System.out.println(getName() + "HelloWrold");}}
}

2. 多线程的第二种实现方式:Runnable接口

首先我们通过查找API帮助文档 了解 Runnable接口 是什么?

多线程第二种实现方式:

  1. 自己手动定义一个类去实现Runnable接口。
  2.  重写里面的run方法
  3.  创建自己的类的对象。
  4.  创建一个Thread类的对象,并开启线程。
public class ThreadDemo2 {public static void main(String[] args) {// 多线程第二种实现方式:// 1.自己定义一个类去实现Runnable接口// 2.重写里面的run方法// 3.创建自己的类的对象// 4.创建一个Thread类的对象,并开启线程// 创建MyRun的任务对象MyRun mr = new MyRun();// 创建线程对象// 将任务mr传递给线程Thread t1 = new Thread(mr);Thread t2 = new Thread(mr);t1.setName("线程1");t2.setName("线程2");t1.start();t2.start();}
}//MyRun.java
public class MyRun implements Runnable {@Overridepublic void run() {// 书写线程执行代码for (int i = 0; i < 100; i++) {//获取当前线程对象Thread thread = Thread .currentThread()System.out.println(thread.getName() + "HelloWrold");}}
}

3. 多线程的第三种实现方式:Callable接口和Future接口

多线程第三种实现方式:

  1. 创建一个类MyCallable实现Callable接口。
  2. 重写里面的call方法。( 返回值表示多线程运行结果 )
  3. 创建MyCallable的对象。( 表示多线程要执行的任务 )
  4. 创建FutureTask的对象。( 作用管理多线程运行的结果 )
  5. 创建Thread类的对象,并启动线程。( 表示线程 ) 

特点: 可以获取到多线程运行的结果。

public class ThreadDemo3 {public static void main(String[] args) throws InterruptedException, ExecutionException {// 多线程第三种实现方式:// 特点: 可以获取到多线程运行的结果// 1.创建一个类Mycallable实现Callable接口// 2.重写里面的call方法(返回值表示多线程运行结果)// 3.创建MyCallable的对象(表示多线程要执行的任务)// 4.创建FutureTask的对象(作用管理多线程运行的结果)// 5.创建Thread类的对象,并启动(表示线程)//创建MyCallable对象MyCallable mc = new MyCallable();//创建FuturaTask对象FutureTask ft = new FutureTask<>(mc);//创建线程对象Thread t1 = new Thread();t1.start();//获取线程运行结果Integer result = ft.get();System.out.println(result);}//MyCallable.java
public class MyCallable implements Callable {@Overridepublic Integer call() throws Exception {// 求1 ~ 100 和int sum = 0;for (int i = 0; i < 100; i++) {sum = sum + i;}return sum;}}

4. 多线程三种实现方式的对比

优点缺点
继承Thread类变成比较简单,可以直接使用Thread类中的方法可以扩展性较差,不能再继承其他的类
实现Runnable扩展性强,实现该接口的同时还可以继承其他的类编程相对复杂,不能直接使用Thread类中的方法
实现Callable接口

三、多线程常用的成员方法

方法名称说明
String getName ( )返回此线程的名称
void setName ( String name )设置线程的名字(构造方法也可以设置名字)
static Thread currentThread ( )获取当前线程的对象
static void sleep ( long time )让线程休眠指定的时间,单位为毫秒
setPriority (int newPriority )设置线程的优先级
final int getPriority ( )获取线程的优先级
final void setDaemon ( boolean on )设置为守护线程
public static void yield ( )出让线程 / 礼让线程
public static void join ( )插入线程 / 插队线程
public class ThreadDemo {public static void main(String[] args) throws InterruptedException {// 1.getName 返回此线程的名称myThread t1 = new myThread();// 如果我们没有给线程命名,线程默认名字// 格式:Thread-X(X序号,从0开始)t1.start();// 2.currentThread 获取当前线程的对象 (静态方法)Thread t = Thread.currentThread();// 哪条线程执行到这个方法,此时获取的就是哪条线程的对象System.out.println(t.getName());// 3.sleep 让线程休眠指定的时间// 方法参数:表示睡眠时间,单位好眠// 当时间到了之后,线程就会自动的醒来,继续执行下面的其他代码System.out.println("1111");Thread.sleep(5000);System.out.println("2222");}
}

线程的优先级:

  • 抢占式调度:CPU执行每一条的线程的时机和执行时间都是不确定的。
  • 非抢占式调度:所有的线程轮流进行,执行时间是差不多的。
public class ThreadDemo {public static void main(String[] args)  {//创建线程要执行的参数对象MyRunnable mr = new MyRunnable();//创建线程对象Thread t1 = new Thread(mr,"飞机");Thread t2 = new Thread(mr,"坦克");//优先级默认 : 5System.out.println(t1.getPriority());System.out.println(t2.getPriority());System.out.println(Thread.currentThread().getPriority());//细节:当其他的非守护线程执行完毕之后,守护线程将会陆续结束。// 把第二个线程设置为守护线程t2.setDaemon(true);}
}

四、线程的生命周期

 问: sleep方法会让线程睡眠,睡眠时间到了之后,立马就会执行下面的代码吗?

  • 答:不会。sleep方法结束后会进入就绪状态,抢到CPU执行权才会运行下面的代码。

线程的六大状态: 

新建状态(New)创建线程对象
就绪状态(RUNNABLE )start方法
阻塞状态(BLOCKED )无法获得锁对象
等待状态( WAITING )wait方法
计时等待(TIMED_WAITING )sleep方法
结束状态( TERMINATED )全部代码运行完毕

五、线程安全问题

通过小练习了解线程安全:

需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票

public class ThreadDemo {public static void main(String[] args) {//创建线程对象MyThread t1 = new MyThread();MyThread t2 = new MyThread();MyThread t3 = new MyThread();//线程命名t1.setName("窗口一");t2.setName("窗口二");t3.setName("窗口三");//开启线程t1.start();t2.start();t3.start();}
}//MyThread.java
public class MyThread extends Thread {// 表示这个类的对象都共享一个ticket对象static int ticket = 0;@Overridepublic void run() {// 书写线程执行代码while (true) {if (ticket < 100) {try {Thread.sleep(100);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}ticket++;System.out.println(getName() + "正在卖第" + ticket + "张票");} else {break;}}}
}

我们发现会出现三个窗口卖同样的票或者超出票数的问题:

那么我们该如何解决呢?

 1. 同步代码块

  • 把操作共享数据的代码锁起来

  • 特点1 : 锁默认打开,有一个线程进去了,锁自动关闭。
  • 特点2 :里面的代码全部执行完毕,线程出来,锁自动打开。 
public class MyThread extends Thread {// 表示这个类的对象都共享一个ticket对象static int ticket = 0;// 锁对象一定唯一static Object obj = new Object();@Overridepublic void run() {// 书写线程执行代码while (true) {//锁对象是任意的synchronized(obj) {if (ticket < 100) {try {Thread.sleep(100);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}ticket++;System.out.println(getName() + "正在卖第" + ticket + "张票");} else {break;}}}}
}

2. 同步方法

  • 就是把synchronized关键字加到方法上。

  • 特点1  : 同步方法是锁住方法里面的所有代码
  • 特点2 : 锁对象不能自己指定。 
public class MyRunnable implements Runnable {int ticket = 0;@Overridepublic void run() {// 1.循环while (true) {// 2.同步代码块(同步方法)if (method()) {break;}}}//thispublic synchronized boolean method() {// 3.判断共享数据是否到了末尾 如果到了末尾if (ticket == 100) {return true;// 4.判断共享数据是否到了末尾 如果没到末尾} else {try {Thread.sleep(100);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}ticket++;System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");}return false;}
}

3. lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,

但是我们并没有直接地看到在哪里加上了锁以及在哪里释放了锁,

为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。

Lock中提供了获得锁和释放锁的方法:

成员方法说明
void lock ( )获得锁
void unlock ( )释放锁

 Lock是接口不能直接实例化,这里采用它的实现类 ReentrantLock 实例化。

构造方法说明
ReentrantLock ( )创建一个  ReentrantLock 的实例
public class MyRunnable implements Runnable {int ticket = 0;// 多个对象共享同一个锁static Lock lock = new ReentrantLock();@Overridepublic void run() {// 1.循环while (true) {// 2.同步代码块(同步方法)// synchronized (MyThread.class) {lock.lock();try {// 3.判断共享数据是否到了末尾 如果到了末尾if (ticket == 100) {break;// 4.判断共享数据是否到了末尾 如果没到末尾} else {Thread.sleep(100);ticket++;System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");}} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}}
}

六、死锁

  • 所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 
  • 死锁是一个错误,在以后的编程过程中要避免锁的嵌套
//MyThread.java
public class MyThread extends Thread {static Object objA = new Object();static Object objB = new Object();@Overridepublic void run() {// 1.循环while (true) {if ("线程A".equals(getName())) {synchronized (objA) {System.out.println("线程A拿到了A锁,准备拿B锁");synchronized (objB) {System.out.println("线程A拿到了B锁,顺利执行完一轮");}}} else if ("线程B".equals(getName())) {if ("线程B".equals(getName())) {synchronized (objB) {System.out.println("线程B拿到了B锁,准备拿A锁");synchronized (objA) {System.out.println("线程B拿到了A锁,顺利执行完一轮");}}}}}}
}public class ThreadDemo {public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();t1.setName("线程A");t2.setName("线程B");t1.start();t2.start();}
}

运行结果:(卡死) 

七、生产者和消费者 ( 等待唤醒机制 )

生产者消费者模式是一种非常经典的多线程协作的模式。

常见方法:

成员方法说明
void wait ( ) 当前线程等待,直到被其他线程唤醒
void notify ( )所及唤醒单个线程
void notifyAll ( )唤醒所有线程

 举例:

 1.  消费者代码实现

//Desk.java
public class Desk {// 作用: 控制生产者和消费者的执行//判断桌子上是否有面条: 0:没有 ; 1:有public static int foodFlag = 0;//定义总个数public static int count = 10;//锁对象public static Object lock = new Object();
}//Foodie.java
public class Foodie extends Thread {@Overridepublic void run() {// 1.循环while (true) {// 同步代码块synchronized (Desk.lock) {if (Desk.count == 0) {break;} else {// 先判断桌子上是否有面条if (Desk.foodFlag == 0) {// 没有:等待try {Desk.lock.wait(); // 让当前线程与锁进行绑定} catch (InterruptedException e) {e.printStackTrace();}} else {// 把吃的总数- 1Desk.count--;// 有: 开吃System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗");// 吃完之后:唤醒厨师继续做Desk.lock.notifyAll();// 修改桌子的状态Desk.foodFlag = 0;}}}}}
}

2.  生产者代码实现

public class ThreadDemo {public static void main(String[] args) {// 创建线程对象Cook c = new Cook();Foodie f = new Foodie();// 线程命名c.setName("厨师");f.setName("吃货");//开启线程c.start();f.start();}
}//Cook.java
public class Cook extends Thread{@Overridepublic void run() {// 1.循环while (true) {// 同步代码块synchronized (Desk.lock) {if (Desk.count == 0) {break;} else {// 判断桌子上是否有食物if (Desk.foodFlag == 1) {// 如果有:就等待try {Desk.lock.wait();} catch (InterruptedException e) {e.printStackTrace();}} else {// 没有: 就制作食物System.out.println("厨师做了一碗面条");// 修改桌子上的食物状态Desk.foodFlag = 1;// 等待的消费者开吃Desk.lock.notifyAll();}}}}}
}

八、线程池

以前写多线程的弊端:

弊端一:用到线程的时候就要创建弊端二:用完之后线程消失

 因此,我们我们引入线程池

线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。

  1. 创建一个池子,池子中是空的。
  2. 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子;下次再次提交任务时,不需要创建新的的线程,直接复用已有的线程即可。
  3. 但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待。

 1. 线程池方法实现

Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。

方法名称说明
public static ExecutorService newCachedThreadPool ( )创建一个没有上限的线程池
public static ExecutorService newFixedThreadPool ( int nThreads )创建有上限的线程池
public class MyThreadPoolDemo {public static void main(String[] args) {// 1.获取线程池对象ExecutorService pool1 = Executors.newCachedThreadPool();ExecutorService pool2 = Executors.newFixedThreadPool(3);// 2.提交任务pool1.submit(new MyRunnable());pool1.submit(new MyRunnable());pool1.submit(new MyRunnable());pool1.submit(new MyRunnable());// pool2只能看到3个线程pool2.submit(new MyRunnable());pool2.submit(new MyRunnable());pool2.submit(new MyRunnable());pool2.submit(new MyRunnable());// 3.销毁任务pool1.shutdown();}
}//MyRunnable.java
public class MyRunnable implements Runnable {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + " - - " + i);}}
}

2.自定义线程池

任务拒绝策略说明
ThreadPoolExecutor.AbortPolicy默认策略:丢弃任务并抛出RejectedExecutionException异常
ThreadPoolExecutor.DiscardPolicy丢弃任务,但是不抛出异常这是不推荐的做法
ThreadPoolExecutor.DiscardoldestPolicy抛弃队列中等待最久的任务然后把当前任务加入队列中
ThreadPoolExecutor.callerRunsPolicy调用任务的run()方法绕过线程池直接执行
  • 核心元素一:核心线程的数量(不能小于0)
  • 核心元素二:线程池中最大线程的数量(最大数量>=核心线程数量)
  • 核心元素三:空闲时间(值)(不能小于0)
  • 核心元素四:空闲时间(单位)(用TimeUnit指定)
  • 核心元素五:堵塞队列(不能为null)
  • 核心元素六:创建线程的方式(不能为null)
  • 核心元素七:要执行的任务过多时的解决方案(不能为null)
public class MyThreadPoolDemo {public static void main(String[] args) {ThreadPoolExecutor pool1 = new ThreadPoolExecutor(3, // 核心线程数量,不能小于06, // 最大线程数,不能小于0,最大数量 >= 核心线程数量60, // 空间线程最大存活时间TimeUnit.SECONDS, // 时间单位new LinkedBlockingQueue<>(3), // 任务队列Executors.defaultThreadFactory(), // 创建线程工厂new ThreadPoolExecutor.AbortPolicy() // 任务拒绝策略);}
}

不断的提交任务,会有以下三个临界点:

  1. 当核心线程满时,再提交任务就会排队。
  2. 当核心线程满,队伍满时,会创建临时线程。
  3. 当核心线程满,队伍满,临时线程满时,会触发任务拒绝策略。

3.最大并行数

CPU密集型运算

(读取文件操作比较少)

I/O密集型运算

(读取文件操作比较多)

public class MyThreadPoolDemo {public static void main(String[] args) {//向Java虚拟机返回可用处理器的数目int count = Runtime.getRuntime().availableProcessors();System.out.println(count); //12}
}

所以线程池多大合适呢?

示例:(4线8核通过计算公式)

 九、综合练习

1.  抢红包

抢红包也用到了多线程。
假设:100块,分成了3个包,现在有5个人去抢。

其中,红包是共享数据。
5个人是5条线程。
打印结果如下:
                 XXX抢到了XXX元

                 XXX抢到了XXX元

                 XXX抢到了XXX元

                 XXX没抢到
                 XXX没抢到

public class MyThread extends Thread {// 总金额static BigDecimal money = BigDecimal.valueOf(100.0);// 个数static int count = 6;// 最小抽奖金额static final BigDecimal MIN = BigDecimal.valueOf(0.01);@Overridepublic void run() {synchronized (MyThread.class) {if (count == 0) {System.out.println(getName() + "没有抢到红包");}else {//中奖金额BigDecimal prize;if (count == 1) {prize = count;}else {//获取抽奖范围double bounds = money.subtract(BigDecimal.valueOf(count -1).multiply(MIN).doubleValue());Random r = new Random(); //抽奖金额prize = BigDecimal.valueOf(r.nextDouble()bounds);}//设置抽中红包,小数点保留两位,四舍五入prize = prize.setScale(2,RoundingMode.HALF_UP);//在总金额中去掉对应的钱money = money.subtract(prize);//红包少了一个count--;//输出红包信息System.out.println(getName() + "抽中了" + prize + "元");}}}
}//Test.java
public class Test {public static void main(String[] args) {// 创建线程对象MyThread t1 = new MyThread();MyThread t2 = new MyThread();MyThread t3 = new MyThread();MyThread t4 = new MyThread();MyThread t5 = new MyThread();MyThread t6 = new MyThread();// 线程命名t1.setName("张三");t2.setName("李四");t3.setName("王五");t4.setName("赵六");t5.setName("钱七");t6.setName("孙八");// 线程启动t1.start();t2.start();t3.start();t4.start();t5.start();t6.start();}
}

2.  抽奖

有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为{10,5,20,50,100,200,500,800,2,80,300,700};
创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”

随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
                          每次抽出一个奖项就打印一个(随机)
                          抽奖箱1又产生了一个10元大奖

                          抽奖箱1又产生了一个100元大奖

                          抽奖箱1又产生了一个200元大奖

                          抽奖箱1又产生了一个800元大奖

                          抽奖箱2又产生了一个700元大奖
                          ...

public class MyThread extends Thread {ArrayList list;public MyThread(ArrayList list) {this.list = list;}@Overridepublic void run() {while (true) {synchronized (MyThread.class) {if (list.size() == 0) {break;} else {// 继续抽奖try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}Collections.shuffle(list);int prize = list.remove(0);System.out.println(getName() + "又产生了一个" + prize + "元大奖");}}}}
}//Test.java
public class Test {public static void main(String[] args) {//创建奖池ArrayList list = new ArrayList<>();Collections.addAll(list, 10,5,20,50,100,200,500,800,2,80,300,700);// 创建线程对象MyThread t1 = new MyThread(list);MyThread t2 = new MyThread(list);// 线程命名t1.setName("抽奖箱1");t2.setName("抽奖箱2");// 线程启动t1.start();t2.start();}
}

相关内容

热门资讯

中证A500ETF摩根(560... 8月22日,截止午间收盘,中证A500ETF摩根(560530)涨1.19%,报1.106元,成交额...
A500ETF易方达(1593... 8月22日,截止午间收盘,A500ETF易方达(159361)涨1.28%,报1.104元,成交额1...
何小鹏斥资约2.5亿港元增持小... 每经记者|孙磊    每经编辑|裴健如 8月21日晚间,小鹏汽车发布公告称,公司联...
中证500ETF基金(1593... 8月22日,截止午间收盘,中证500ETF基金(159337)涨0.94%,报1.509元,成交额2...
中证A500ETF华安(159... 8月22日,截止午间收盘,中证A500ETF华安(159359)涨1.15%,报1.139元,成交额...
科创AIETF(588790)... 8月22日,截止午间收盘,科创AIETF(588790)涨4.83%,报0.760元,成交额6.98...
创业板50ETF嘉实(1593... 8月22日,截止午间收盘,创业板50ETF嘉实(159373)涨2.61%,报1.296元,成交额1...
港股异动丨航空股大幅走低 中国... 港股航空股大幅下跌,其中,中国国航跌近7%表现最弱,中国东方航空跌近5%,中国南方航空跌超3%,美兰...
电网设备ETF(159326)... 8月22日,截止午间收盘,电网设备ETF(159326)跌0.25%,报1.198元,成交额409....
红利ETF国企(530880)... 8月22日,截止午间收盘,红利ETF国企(530880)跌0.67%,报1.034元,成交额29.0...