文章目录
- Java多线程快速入门
- 1、认识多线程
- 2、多线程的实现
- 2.1 继承Thread类
- 2.2 实现Runnable接口
- 2.3 利用Callable和Futrue接口
- 2.4 三种方式的比较
- 3、Thread类常用API
- 3.1 守护线程
- 3.2 礼让线程
- 3.3 插入线程
- 3.4 线程的生命周期
- 5、线程安全问题
- 5.1 synchronized
- 5.2 Lock
- 6、等待唤醒机制
- 7、综合案例
- 7.1 售票
- 7.2 赠送礼物
- 7.3 打印数字
- 7.4 抢红包
- 7.5 抽奖箱
- 7.6 多线程统计并求最大值
- 7.7 多线程之间的比较
- 8、线程池
- 8.1 自定义线程
- 8.2 线程池最大并行数
Java多线程快速入门
趁着最近可少,复习一下Java多线程相关知识,顺便发一下以前的笔记
1、认识多线程
-
什么是线程?
线程是指在一个进程中,执行的一个相对独立的、可调度的、可执行的代码片段。线程是操作系统能够运算调度的最小单位,它包含在进程之中,是进程中的实际运作单位,它独立地运行于进程中,并与同一进程内的其他线程共享进程的资源,如内存、文件描述符等。每个线程都有自己的栈、程序计数器和局部变量等,但它们共享进程的静态数据、堆内存和全局变量等。
PS:可以简单理解线程是线程中的一条执行路径(可以参考流程图)
-
线程的优缺点
- 优点:
-
线程可以提高程序的并行性,增加程序的处理能力;
-
线程创建和切换的开销比进程小,因此更加高效;
-
线程可以与同一进程内的其他线程共享数据和资源,这样可以避免进程间的数据复制和通信开销。
-
- 缺点
-
同一进程内的线程都共享进程的资源,因此需要进行线程间的同步和互斥,否则容易出现竞争条件和死锁等问题;
-
线程之间的通信和同步需要额外的开销和复杂度,因此需要仔细规划和设计线程间的通信和同步机制;
-
由于线程共享进程的地址空间,因此需要避免线程间的访问冲突,否则容易出现数据不一致的问题。
-
- 优点:
-
什么是进程?
进程是指在计算机中运行的程序和其相关执行状态的总和。更具体地说,进程包括程序代码、数据、内存中的栈、堆和共享库等资源。每个进程在执行时都有自己的地址空间、内存、堆和栈,以及相应的文件描述符、信号处理程序等。进程是操作系统中最基本的、最重要的资源之一。
PS:可以简单理解进程就是正在运行的程序
-
进程的特点:
- 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
- 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
- 并发性:任何进程都可以同其他进程一起并发执行
-
什么是单线程和多线程?
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序
- 多线程:一个进程如果有多条执行路径,则称为多线程程序
-
多线程的应用场景:
- 拷贝、迁移大文件,可以单独使用一个线程去拷贝迁移大文件,从而可以空出时间去干其它事情
- 聊天软件中使用多线程,服务器为每一个客户创建一个线程,处理聊天任务;客户端使用多线程进行界面更新,单独使用一个线程更新界面,一个线程用来接收消息
- 在购物网站中,为了提高系统性能,单独使用一个线程去获取阻塞队列中的订单消息
……
多线程的主要作用是为了提高系统的性能,充分利用CPU
-
什么是并行与并发?
- 并行:在同一时刻,有多个指令在多个CPU上同时执行
- 并发:在同一时刻,有多个指令在同一个CPU上交替执行
-
什么是生产者和消费者?
- 生产者:负责向共享缓冲区中生产数据
- 消费者:负责从共享缓冲区中消费数据
2、多线程的实现
2.1 继承Thread类
通过继承Thread类实现多线程
-
写法一:传统编程方式
-
Step1:编写一个类,继承Thread类,重写Thread类的
run
方法package com.hhxy.thread; public class MyThread extends Thread { /** * 在线程开启后,此方法将被自动调用执行 */ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(getName() + "被执行了" + i + "次"); } } }
-
Step2:编写测试类,创建多线程,并运行多线程
package com.hhxy.test; import com.hhxy.thread.MyThread; public class ThreadTest { public static void main(String[] args) { // 创建线程对象 MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); // 为线程命名 t1.setName("线程1"); t2.setName("线程2"); // 启动线程 t1.start(); t2.start(); // 主线程输出 for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次"); } } }
可以从下图中看出,程序被启动,有三个线程在运行,需要注意的是线程输出是随机的,并不是说谁先调用start方法就会先输出
-
-
写法二:匿名内部类方式
使用匿名内部类方式就不需要去单独创建一个类类继承Thread类了,而是直接实现Thread
package com.hhxy.test; import com.hhxy.thread.MyThread; public class ThreadTest { public static void main(String[] args) { // 创建线程对象 Thread t1 = new Thread(){ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(getName() + "被执行了" + i + "次"); } } }; Thread t2 = new Thread(()->{ for (int i = 0; i < 10; i++) { // 注意这里由于使用了匿名内部类的写法,导致这里无法使用this,所以得调用currentThread后去当前线程名 System.out.println(Thread.currentThread().getName()+"被执行了" + i + "次"); } }); // 启动线程 t1.start(); t2.start(); for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次"); } } }
总结
通过Thread类实现多线程主要有以下几步:
- 创建一个类,继承Thread类重写run方法(或者直接使用匿名内部类的方式直接在实现run方法)
- 调用无参构造器,创建Thread对象
- 调用Thread对象的start方法启动线程
2.2 实现Runnable接口
-
方式一:传统写法
-
Step1:编写一个类实现Runnable接口,然后重写run方法
package com.hhxy.runnable; public class MyRunnable implements Runnable{ /** * 线程任务,当Runnable对饮的Thread对象调用start方法,就立刻执行 */ @Override public void run() { for (int i = 0; i < 10; i++) { // 由于没有继承Thread类,所以不能调用getName获取线程名 System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次"); } } }
-
Step2:编写一个测试类,创建Step1编写的Runnable实现类对象,创建Thread对象,调用Thread的有参构造,将Runnable对象放入Thread构造器中
package com.hhxy.test; import com.hhxy.runnable.MyRunnable; public class RunnableTest { public static void main(String[] args) { // 创建Runnable对象,表示线程任务 MyRunnable myRunnable = new MyRunnable(); // 创建Thread对象 Thread t1 = new Thread(myRunnable); Thread t2 = new Thread(myRunnable); // 启动线程 t1.start(); t2.start(); for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次"); } } }
-
-
方式二:匿名内部类写法
package com.hhxy.test; import com.hhxy.runnable.MyRunnable; public class RunnableTest { public static void main(String[] args) { // 创建Thread对象(直接使用匿名内部类实现Runnable接口) Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次"); } } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次"); } }); // 启动线程 t1.start(); t2.start(); for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次"); } } }
总结
- 创建一个类,实现Runnable接口并重写run方法(或者使用匿名内部类的方式直接实现run方法)
- 调用有参构造器,创建Thread对象
- 调用Thread对象的start方法启动线程
2.3 利用Callable和Futrue接口
-
Step1:创建一个类,实现Callable接口,重写call方法
package com.hhxy.callable; import java.util.concurrent.Callable; public class MyCallable implements Callable<String> { /** * 线程任务 * @return 返回线程任务执行后的线程结果 */ @Override public String call() throws Exception { for (int i = 0; i < 10; i++) { // 由于没有继承Thread类,所以不能调用getName获取线程名 System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次"); } return Thread.currentThread().getName() + "线程执行完毕!"; } }
注意:类的泛型要与call方法的返回值类型保持一致
-
Step2:编写测试类,创建Callable对象,调用有参构造器(参数为Callable对象)创建FutureTask对象,调用有参构造器(参数为FutureTask对象)创建Thread对象,调用Thread对象的start方法
package com.hhxy.test; import com.hhxy.callable.MyCallable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class CallableTest { public static void main(String[] args) throws ExecutionException, InterruptedException { // 创建Callable对象 MyCallable myCallable = new MyCallable(); // 创建FutureTask对象 FutureTask ft1 = new FutureTask<>(myCallable); FutureTask ft2 = new FutureTask<>(myCallable); // 创建Thread对象 Thread t1 = new Thread(ft1); Thread t2 = new Thread(ft2); // 启动线程 t1.start(); t2.start(); // 获取线程任务执行后的结果 String result1 = ft1.get().toString(); String result2 = ft2.get().toString(); System.out.println(result1); System.out.println(result2); for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次"); } } }
注意:一个Thread对象要对应一个FutureTask对象,如果两个Thread对象共用一个FutureTask对象,获取线程任务的结果会是一致的,并且结果以第一个线程任务的结果为准
总结
- 创建一个类,实现Callable接口并重写call方法
- 创建Callable对象,调用有参构造器(参数为Callable对象)创建FutureTask对象,调用有参构造器(参数为FutureTask对象)创建Thread对象
- 调用Thread对象的start方法启动线程
- 调用FutureTask对象的get方法,获取线程任务执行后的结果
2.4 三种方式的比较
- 如果我们想要获取线程任务的执行结果,请使用方式三
- 如果我们不需要获取线程任务的执行结果,同时对扩展性要求不要,请使用方式一
- 如果我们不需要获取线程任务的执行结果,同时对扩展性要求较高,请使用方式二
3、Thread类常用API
API介绍:
方法名 | 说明 |
---|---|
public void run() | 在线程开启后,run()方法将被自动调用执行 |
public synchronized void start() | 开启线程 |
public final synchronized void setName(String name) | 为线程命名 |
public final String getName() | 获取线程名 |
public final void setPriority(int newPriority) | 设置线程的优先级 |
public final intgetPriority() | 获取线程的优先级 |
public final void setDaemon(boolean on) | 设置为守护线程 |
public final void join() | 插入线程/插队线程 |
public static native void yield(); | 出让线程/礼让线程 |
public static native Thread currentThread(); | 获取当前线程 |
public static native void sleep(long millis); | 让线程休眠指定时间(单位ms) |
public final void wait() | 当前线程等待,直到被其他线程唤醒 |
public final native void notify(); | 随机唤醒单个线程 |
public final native void notifyAll(); | 唤醒所有线程 |
public State getState() | 获取线程状态 |
备注:
- 以
;
结尾的是成员变量,而()
结尾的是方法 - 优先级值越大,越优先执行。默认是5,最小值是1,最大值是10
3.1 守护线程
- 守护线程,就是“备胎线程”,当主线程结束后,守护线程会结束(但不是立即结束,而是执行一段时间后结束)
守护线程的应用场景:当我们在进行QQ聊天时,主线程就是QQ程序,而当我们发送文件时,就可以开启一个守护线程,这个守护线程单独用于发送文件,当我们关闭QQ时,QQ这个主线程就结束了,而此时守护线程也没有存在的必要了,所以此时也会随着主线程的结束而结束
public class DaemonTest {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
}
});
// 将t2线程设置为t1的守护线程,t1结束后,t2也会跟着结束(但不是立即结束)
t2.setDaemon(true);
t1.start();
t2.start();
// for (int i = 0; i < 100; i++) {
// System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
// }
}
}
可以看到,当t1执行完,此时整个程序中只有守护线程,此时JVM就会关闭守护线程(注意,如果我们开启主线程的打印,则t1执行完后,t2守护线程不会结束,因为此时系统中除了守护线程,还存在主线程,并不是只剩守护线程)
3.2 礼让线程
- 礼让线程,让出当前CPU
在需要多个线程协作、顺序执行的场景中,礼让线程是一种比较常用的线程协作方法,它可以让线程执行的顺序更加合理,提高系统的并发性能。
public class YieldTest {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
// 让出当前线程的CPU
Thread.yield();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
}
});
t1.start();
t2.start();
}
}
可以发现当当前系统中同时存在其它线程时,Thread-0只会被执行一次,这就是礼让线程的一个特性:
3.3 插入线程
-
插入线程,让当前线程等待线程t执行完成后再继续执行
它的应用场景较少,使用起来也要十分小心,因为很容易发生死锁
public class JoinTest { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次"); } }); t.start(); // 将t设置为插入线程,会阻塞当前线程,直到t执行完才重新执行当前线程 t.join(); for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次"); } } }
3.4 线程的生命周期
Java里没有定义运行态,因为当线程运行后直接将当前线程交给了CPU,此时JVM就不需要管这个线程了,所以Java中线程实际的状态只有6个
5、线程安全问题
5.1 synchronized
synchronized是Java中用来实现线程同步的关键字,它可以让多个线程在访问共享资源时,保证同一时刻只有一个线程访问,从而避免线程间的数据竞争和不一致性,实现线程安全。
示例:
多线程买票
package com.hhxy.test;
/**
* @author ghp
* @date 2023/6/8
* @title
* @description
*/
public class ThreadSafeTest {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
class MyThread extends Thread {
int ticket = 0;
@Override
public void run() {
while (true) {
if (ticket < 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println("正在卖第" + ticket + "张票");
} else {
break;
}
}
}
}
当前代码存在,一下问题,每一个线程对于票数量的计算都是独立的,命名只有100张票,但是让三个线程来买,却卖了300张:
同时还会出现超卖问题:
1)代码优化:将ticket使用static修饰,这样多个线程就可以共享一个变量了
static int ticket = 0;
但是仍然会出现这种情况,只是比例大幅度下降了:
同样仍然会出现超卖问题!
2)代码优化:使用synchronized
对同步代码块进行上锁
注意:synchronized
锁住的对象必须是唯一的
package com.hhxy.test;
public class ThreadSafeTest {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
class MyThread extends Thread {
// int ticket = 0;
static int ticket = 0;
@Override
public void run() {
while (true) {
synchronized (ThreadSafeTest.class) {
if (ticket < 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
} else {
break;
}
}
}
}
}
温馨提示:synchronized
不仅可以锁代码块,还可以锁方法。锁方法,不能自己指定,非静态的锁住的是this,静态的是当前类的字节码文件对象
5.2 Lock
Lock
是JDK5提供的一种全新的锁对象,位于java.util.concurrent.locks
包下,Lock提供了比使用synchronized
方法和语句更为广泛的锁操作,通过lock()
获取锁,通过unlock()
释放锁。Lock是一个接口,不能够直接实例化,一般我们是使用它的实现类ReentrantLock
来实例化。
package com.hhxy.test;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author ghp
* @date 2023/6/8
* @title
* @description
*/
public class ThreadSafeTest2 {
public static void main(String[] args) {
MyThread2 t1 = new MyThread2();
MyThread2 t2 = new MyThread2();
MyThread2 t3 = new MyThread2();
t1.start();
t2.start();
t3.start();
}
}
class MyThread2 extends Thread {
// int ticket = 0;
static int ticket = 0;
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (ticket < 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
} else {
break;
}
} finally {
lock.unlock();
}
}
}
}
可以看到又出现了多个窗口卖同一张票的情况:
出现这个问题的原因很简单,因为Lock对象没有加static,导致每创建一个MyThread2对象,都会新建一个Lock对象,所以我们需要使用static修饰Lock对象
static Lock lock = new ReentrantLock();
6、等待唤醒机制
等待唤醒机制是Java中常见的线程同步机制之一,它通过Object类的wait()和notify()/notifyAll()方法实现线程间的通信,实现“生产者-消费者”模型等多线程编程场景。
示例
示例一:
这里将利用
wait()
和notify()/notifyAll()
方法实现等待唤醒机制
-
桌子:用来放面条,同时记录食客消费面条的数量,以及桌子上面条的数量
public class Desk { // 消费者最大能消费的食物数量 public static int count = 10; // 桌子上食物的数量 0-桌子上没有食物 1-桌子上有食物 public static int foodFlag = 0; // 锁对象,用于上锁 public static final Object lock = new Object(); }
-
生产者:生产面条
public class Cook extends Thread { @Override public void run() { while (true) { synchronized (Desk.lock) { // 判断美食家是否还能吃下 if (Desk.count == 0) { // 美食家已经吃饱了 break; } else { // 美食家还能吃,判断桌子上是否有食物 if (Desk.foodFlag == 0) { // 桌子上没有食物,厨师做面条,然后唤醒正在等待的美食家 Desk.foodFlag++; System.out.println("厨师做了" + Desk.foodFlag + "碗面条"); Desk.lock.notifyAll(); } else { // 桌子上有食物,厨师等待 try { System.out.println("厨师等待……"); Desk.lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } } } }
-
消费者:消费面条
public class Foodie extends Thread { @Override public void run() { while (true) { synchronized (Desk.lock) { // 判断美食家是否吃饱 if (Desk.count == 0) { // 美食家已经吃饱了 break; } else { // 美食家还没有吃饱,判断桌子上是否有食物 if (Desk.foodFlag == 1) { // 桌子上有食物 Desk.count--; Desk.foodFlag--; System.out.println("美食家还能吃" + Desk.count + "碗面"); // 唤醒美食家,让他继续做面 Desk.lock.notifyAll(); } else { // 桌子上没有食物 try { System.out.println("美食家等待……"); Desk.lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } } } }
-
测试类:
public class Main { public static void main(String[] args) { Cook cook = new Cook(); Foodie foodie = new Foodie(); cook.start(); foodie.start(); } }
示例二:
这里将使用阻塞队列来实现等待唤醒机制。
备注:阻塞队列(Blocking Queue)是Java中的一种线程安全的队列,它支持在队列为空时阻塞获取元素,或者在队列已满时阻塞插入元素,可以很好地用于实现生产者-消费者模型等多线程编程场景。
-
桌子:
public class Desk { // 消费者最大能消费的食物数量 public static int count = 10; }
-
生产者:
public class Cook extends Thread { ArrayBlockingQueue<String> queue; public Cook(ArrayBlockingQueue queue) { this.queue = queue; } @Override public void run() { while (true) { // 判断美食家是否还能吃下 if (Desk.count == 0) { // 美食家已经吃饱了 break; } else { // 美食家还能吃,判断桌子上是否有食物 if (queue.isEmpty()) { // 桌子上没有食物,厨师做面条 try { queue.put("面条"); System.out.println("厨师做了1碗面条"); } catch (InterruptedException e) { throw new RuntimeException(e); } } else { System.out.println("厨师等待……"); } } } } }
-
消费者:
public class Foodie extends Thread { ArrayBlockingQueue<String> queue; public Foodie(ArrayBlockingQueue queue) { this.queue = queue; } @Override public void run() { while (true) { // 判断美食家是否吃饱 if (Desk.count == 0) { // 美食家已经吃饱了 break; } else { // 美食家还没有吃饱,判断桌子上是否有食物 if (queue.isEmpty()) { // 桌子上没有食物了,美食家等待 System.out.println("美食家等待……"); } else { // 桌子上有食物 try{ Desk.count--; String food = queue.take(); System.out.println("美食家吃了1碗面条,美食家还能吃" + Desk.count + "碗" + food); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } } }
-
测试类:
public class Main { public static void main(String[] args) { ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1); Cook cook = new Cook(queue); Foodie foodie = new Foodie(queue); cook.start(); foodie.start(); } }
打印出现重复,是由于打印语句在锁的外面,阻塞队列内部是使用了Lock锁,最终的实际效果是和示例一一致的,只是打印语句会发生错乱,并不影响最终效果
7、综合案例
7.1 售票
需求:一共有100张电影票,可以在两个窗口领取,假设每次领取的时间为100毫秒,请用多线程模拟卖票过程并打印剩余电影票的数量
-
测试类:
public class Main { public static void main(String[] args) { // synchronized实现 Thread t1 = new MyThread(); Thread t2 = new MyThread(); // lock实现 // Thread t1 = new MyThread2(); // Thread t2 = new MyThread2(); t1.start(); t2.start(); } }
-
线程类:
1)synchronized实现:
public class MyThread extends Thread { public static int ticket = 100; @Override public void run() { while (true) { if (ticket==0){ break; }else { synchronized (MyThread.class) { try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } if (ticket > 0) { ticket--; System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩" + ticket); } } } } } }
2)lock实现:
public class MyThread2 extends Thread { public static int ticket = 100; public static final Lock lock = new ReentrantLock(); @Override public void run() { while (true) { if (ticket == 0) { break; } else { lock.lock(); try { Thread.sleep(100); ticket--; System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩" + ticket); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } } } } }
7.2 赠送礼物
需求:有100份礼品,两人同时发送,当剩下的礼品小于10份的时候则不再送出。利用多线程模拟该过程并将线程的名字和礼物的剩余数量打印出来。
-
测试类:和7.1一样,略
-
线程类:
public class MyThread extends Thread { public static int count = 100; @Override public void run() { while (true) { try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (MyThread.class) { if (count < 10) { break; } else { count--; System.out.println(Thread.currentThread().getName() + "送出一个礼物,当前礼物还剩" + count); } } } } }
备注:这里有一个小疑惑,synchronized必须要把if-else全部锁住才能成功,如果和7.1一样,只锁else代码,会导致多多送一个礼物,线程1送出第90个礼物后,线程2还会送出第91个礼物,全部锁住就不会发生这样的事情。
自我解惑:其实出现这个问题的原因,是由于当线程1进入else中,还没有执行count–操作,此时线程2也进入了else,但此时锁被线程1拿到了,线程2在else中等待,这就导致线程1执行完count–后释放锁,线程2接着又拿到锁执行count–,这就导致,线程1送出第90个礼物后,线程2还会送出第91个礼物,全部锁住就不会发生这样的事情。
7.3 打印数字
需求:同时开启两个线程,共同获取1-100之间的所有数字,输出所有的奇数。
-
测试类:和7.1一样,略
-
线程类:
package com.hhxy.demo05; /** * @author ghp * @date 2023/6/9 * @title * @description */ public class MyThread extends Thread { public static int n = 0; @Override public void run() { while (true) { try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (MyThread.class) { if (n == 100) { break; } else { n++; if (n % 2 != 0) { System.out.println(Thread.currentThread().getName() + "找到一个奇数" + n); } } } } } }
7.4 抢红包
需求:抢红包也用到了多线程。
假设:100块,分成了3个包,现在有5个人去抢。
其中,红包是共享数据。
5个人是5条线程。
-
测试类:略
-
线程类:
package com.hhxy.demo06; import java.util.Random; public class MyThread extends Thread { // 红包的金额 public static double money = 100; // 红包的个数 public static int count = 3; // 红包的最小值 public static final double MIN = 0.01; @Override public void run() { synchronized (MyThread.class) { double price = 0; if (count == 1) { // 只剩一个红包了,剩下的钱都是这个红包 count--; price = money; money -= price; } else { if (count > 1) { count--; Random random = new Random(); // 红包的金额范围是 0.01~(money-(count-1)*0.01) double t = random.nextInt(1001 - count); price = t / 100; if (price == 0) { price = 0.01; } money -= price; } } System.out.println(this.getName() + "抢一个" + price + "元的红包,红包金额还剩" + money + ",红包数量还剩" + count); } } }
7.5 抽奖箱
需求:有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为 {10,5,20,50,100,200,500,800,2,80,300,700};
创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”
-
测试类:
public static void main(String[] args) { Thread t1 = new MyThread(); Thread t2 = new MyThread(); t1.setName("抽奖箱一"); t2.setName("抽奖箱二"); t1.start(); t2.start(); }
-
线程类:
package com.hhxy.demo07; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * @author ghp * @date 2023/6/9 * @title * @description */ public class MyThread extends Thread { public static List<Integer> list = new ArrayList<Integer>() {{ add(10); add(5); add(20); add(50); add(100); add(200); add(500); add(800); add(2); add(80); add(300); add(700); }}; @Override public void run() { while (true) { try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (MyThread.class) { if (list.size() == 0) { break; } else { try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } Collections.shuffle(list); Integer res = list.remove(0); System.out.println(this.getName() + "抽到了" + res + "元,抽奖箱中剩余" + list.size()); } } } } }
7.6 多线程统计并求最大值
需求:
在上一题基础上继续完成如下需求:
每次抽的过程中,不打印,抽完时一次性打印(随机)
在此次抽奖过程中,抽奖箱1总共产生了6个奖项。
分别为:10,20,100,500,2,300最高奖项为300元,总计额为932元
在此次抽奖过程中,抽奖箱2总共产生了6个奖项。
分别为:5,50,200,800,80,700最高奖项为800元,总计额为1835元
通过创建共享变量实现:
public class MyThread extends Thread {
public static List<Integer> list = new ArrayList<Integer>() {{
add(10);
add(5);
add(20);
add(50);
add(100);
add(200);
add(500);
add(800);
add(2);
add(80);
add(300);
add(700);
}};
public static List<Integer> list1 = new ArrayList<>();
public static List<Integer> list2 = new ArrayList<>();
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (MyThread.class) {
if (list.size() == 0) {
if ("抽奖箱一".equals(this.getName())){
System.out.println(list1);
}else {
System.out.println(list2);
}
break;
} else {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Collections.shuffle(list);
Integer res = list.remove(0);
if ("抽奖箱一".equals(this.getName())){
list1.add(res);
}else{
list2.add(res);
}
System.out.println(this.getName() + "抽到了" + res + "元,抽奖箱中剩余" + list.size());
}
}
}
}
}
通过创建局部变量实现:
public class MyThread2 extends Thread {
public static List<Integer> list = new ArrayList<Integer>() {{
add(10);
add(5);
add(20);
add(50);
add(100);
add(200);
add(500);
add(800);
add(2);
add(80);
add(300);
add(700);
}};
@Override
public void run() {
List<Integer> currentList = new ArrayList<>();
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (MyThread.class) {
if (list.size() == 0) {
System.out.println(this.getName() + currentList);
break;
} else {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Collections.shuffle(list);
Integer res = list.remove(0);
currentList.add(res);
System.out.println(this.getName() + "抽到了" + res + "元,抽奖箱中剩余" + list.size());
}
}
}
}
}
7.7 多线程之间的比较
需求:在上一题基础上继续完成如下需求,比较两个线程的最大值
线程类:
package com.hhxy.demo08;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
/**
* @author ghp
* @date 2023/6/9
* @title
* @description
*/
public class MyCallable implements Callable<Integer> {
public static List<Integer> list = new ArrayList<Integer>() {{
add(10);
add(5);
add(20);
add(50);
add(100);
add(200);
add(500);
add(800);
add(2);
add(80);
add(300);
add(700);
}};
@Override
public Integer call() throws Exception {
List<Integer> currentList = new ArrayList<>();
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (MyThread.class) {
if (list.size() == 0) {
System.out.println(Thread.currentThread().getName() + currentList);
break;
} else {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Collections.shuffle(list);
Integer res = list.remove(0);
currentList.add(res);
System.out.println(Thread.currentThread().getName() + "抽到了" + res + "元,抽奖箱中剩余" + list.size());
}
}
}
// 获取当前线程抽取到的最大值
int max = 0;
if (currentList.size()!=0){
max = Collections.max(currentList);
}
return max;
}
}
测试类:
package com.hhxy.demo08;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// Callable
MyCallable myCallable = new MyCallable();
FutureTask<Integer> f1 = new FutureTask<>(myCallable);
FutureTask<Integer> f2 = new FutureTask<>(myCallable);
Thread t1 = new Thread(f1);
Thread t2 = new Thread(f2);
t1.setName("抽奖箱一");
t2.setName("抽奖箱二");
t1.start();
t2.start();
System.out.println("抽奖箱一的最大值" + f1.get());
System.out.println("抽奖箱二的最大值" + f2.get());
}
}
8、线程池
-
什么是线程池?
线程池是一种多线程处理方式,它可以有效地管理和调度多个线程的执行。在使用线程池的情况下,可以避免因为创建大量线程而导致系统性能下降、内存消耗过大等问题。线程池中的线程都是已经创建好的线程对象,并保存在线程池中,每个线程可以执行多个任务,任务执行完毕后并不会立刻销毁线程,而是会保留在池中等待下次执行。
-
为什么需要线程池?
在多线程编程中,往往需要创建大量的线程来执行任务。但是,直接创建线程会导致以下问题:
-
系统资源浪费:对于一些线程生命周期很短的任务(比如执行完一段代码后就会结束的任务),频繁地创建、销毁线程会消耗大量的系统资源,并且增加了系统开销。
-
系统性能下降:当同时需要执行大量的任务时,不加限制地创建线程可能会导致系统性能下降、运行速度变慢,因为线程的创建和销毁开销非常大。
-
系统不稳定:在高并发情况下,线程过多时会导致系统崩溃、运行不稳定。
线程池的作用就是解决以上问题。它可以避免频繁地创建、销毁线程,可以提前准备好一定数量的线程,让线程复用,从而降低创建和销毁线程的开销,同时还可以严格地限制线程的数量和执行时间,实现对线程的调度和管理。
使用线程池的好处:
-
提高系统效率:通过线程的复用和调度,可以充分利用系统资源,提高系统效率。
-
提高程序响应速度:线程池中的线程可以随时响应任务,从而提高程序的响应速度。
-
避免系统由于线程过多而不稳定:由于可以控制线程的数量,线程池可以避免系统出现由于线程过多而导致的不稳定状态,提高系统的可靠性。
总而言之,线程池在多线程编程中是一种非常重要的工具,可以避免系统性能问题和不稳定问题,提高系统效率和可靠性。
-
8.1 自定义线程
-
如何创建线程池?
package com.hhxy.demo09; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Demo01 { public static void main(String[] args) throws InterruptedException { // 创建线程池 ExecutorService pool = Executors.newCachedThreadPool(); // 提交任务 pool.submit(new MyRunnable()); // main线程休眠1s,这样的目的是为了让Thread-0尽快执行完任务,之后就都会是Thread-0执行 // Thread.sleep(1000); pool.submit(new MyRunnable()); // 不光可以 // Thread.sleep(1000); pool.submit(new MyRunnable()); // Thread.sleep(1000); // 销毁线程池(线程池一般不销毁) pool.shutdown(); } }
-
线程池相关概念
-
先提交的任务不一定限制性
-
当核心线程真在忙,且线程池等待队列中的任务已满时,会创建临时线程
-
线程池能最大处理的任务数:核心线程数量+等待队列的长度+临时线程的数量,超过这个长度的任务会拒绝服务
拒绝策略:
AbortPolicy
:丢弃并抛出异常RejectedExecutionException
异常(默认策略)DiscardPolicy
:丢弃任务,但不抛出异常(不推荐)DiscardOldstPolicy
:抛弃队列中等待最久的任务,然后把当前任务加入到队列中CallerRunsPolicy
:调用任务的run()方法绕过线程池直接执行
-
package com.hhxy.demo10;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) {
/*
参数一:核心线程数量 >=0
参数二:最大线程数 >=核心线程数量
参数三:空闲线程最大存活时间 >=0
参数四:时间单位
参数五:任务队列 !=null
参数六:创建线程工厂 !=null
参数七:任务的拒绝策略 !=null
*/
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3, // 核心线程数量,不能小于0
6, // 最大线程数量,不能小于核心线程数量,临时线程数量=最大线程数量-核心线程数量
60, // 时间值
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(3), // 阻塞队列长度
Executors.defaultThreadFactory(), // 获取线程的方式
new ThreadPoolExecutor.AbortPolicy() // 任务的拒绝策略
);
}
}
8.2 线程池最大并行数
-
CPU密集型运算:最大并行数+1
第一种方式:
从这里可以看出,我笔记本的最大并行数是16
第二种方式:
int count = Runtime.getRuntime().availableProcessors(); System.out.println("当前电脑最大逻辑处理数:"+ count); // 16
-
I/O密集型运算: 最大并行数 ∗ 期望 C P U 利用率 ∗ 总时间 ( C P U 计算时间 + 等待时间 ) C P U 计算时间 最大并行数*期望CPU利用率*\frac{总时间(CPU计算时间+等待时间)}{CPU计算时间} 最大并行数∗期望CPU利用率∗CPU计算时间总时间(CPU计算时间+等待时间)
比如:从本地文件中,读取两个数据(耗时1秒速),并进行相加(耗时1秒钟)
则此时计算式为: 16 ∗ 100 % ∗ ( 2 s ) / 1 s = 16 16 *100\%*(2s)/1s = 16 16∗100%∗(2s)/1s=16,所以此时线程池的最大数量为16
CPU的等待时间可以使用
thread dump
进行计算