1. 为什么要有多线程?
线程:线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中实际运行单位。
进程:进程是程序的基本执行实体。
-
什么是多线程?
有了多线程,我们就可以让程序同时做多件事情。
- 多线程的作用?
提高效率
- 多线程的应用场景?
只要你想让多个事件同时运行就需要多线程
比如:软件中的耗时操作、所有的聊天软件、所有的服务器。
2. 多线程的两个概念?
并发:在同一时刻,有多个指令在单个 CPU 上交替执行
并行:在同一时刻,有多个指令在多个 CPU 上同时执行
3. 多线程的实现方式
1. 继承 Thread 类的方法进行实现
2. 实现 Runnable 接口的方式进行实现
3. 利用 Callable 接口和 Future 接口方式实现
多线程实现方式1-代码示例:
public class MyThread extends Thread{
@Override
public void run() {
// 线程要执行的代码
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "hello world");
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
/**
* 多线程的第一种启动方式
* 1. 自己定义一个类继承 Thread 类
* 2. 重写 run 方法
* 3. 创建子类对象,并启动线程
*/
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("线程1");
t2.setName("线程2");
// 开启线程
t1.start();
t2.start();
}
}
多线程实现方式2-代码示例:
public class MyRun implements Runnable{
@Override
public void run() {
// 线程要执行的代码
for (int i = 0; i < 100; i++) {
// 获取当前线程对象
System.out.println(Thread.currentThread().getName() + "hello world");
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
/**
* 多线程的第二种实现方式
* 1. 自己定义一个类实现 Runnable 接口
* 2. 重写里面的 run 方法
* 3. 创建自己的类的对象。
* 4. 创建一个 Thread 类的对象,并开启多线程
*/
// 创建 MyRun 对象
// 表示多线程要执行的任务
MyRun mr = new MyRun();
// 创建线程对象
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
// 给线程设置名字
t1.setName("线程一");
t2.setName("线程二");
// 开启线程
t1.start();
t2.start();
}
}
多线程实现方式3-代码示例:
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 求 1-100 之间的和
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum = sum + i;
}
return sum;
}
}
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/**
* 多线程第三种实现方式:
* 特点:可以获取到多线程运行的结果
*
* 1. 创建一个类 MyCallable 实现 Callable 接口
* 2. 重写 call(是有返回值的,表示多线程运行的结果)
* 3. 创建 MyCallable 的对象(表示多线程要执行的任务)
* 4. 创建 FutureTask 的对象(作用管理多线程运行的结果)
* 5. 创建 Thread 类的对象,并启动(表示线程)
*/
// 创建 MyCallable 的对象(表示多线程要执行的任务)
MyCallable mc = new MyCallable();
// 创建 FutureTask 的对象(作用管理多线程运行的结果)
FutureTask<Integer> ft = new FutureTask<>(mc);
// 创建线程的对象
Thread t1 = new Thread(ft);
// 开启线程
t1.start();
// 获取多线程运行的结果
Integer result = ft.get();
System.out.println(result);
}
}
4. 常见的成员方法
setName && currentThread && sleep
public class MyThread extends Thread{
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "@" + i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
/**
* void setName(String name) 设置线程的名字(构造方法也可以设置名字)
* 细节:
* 1、如果我们没有给线程名字,线程也有默认的名字的
* 格式:Thread-X(X 序号,从 0 开始的)
* 2、如果我们要给线程设置名字,可以用 set 方法进行设置,也可以用构造方法设置
*
* static Thread currentThread() 获取当前线程对象
* 细节:
* 当 JVM 虚拟机启动后,会自动启动多条线程
* 当其中有一条线程就叫做 main 线程
* 它的主要作用发就是调用 main 方法,并执行里面的代码
* 在以前,我们写的所有代码,其实就是运行在 main 线程当中。
* static void sleep(long time) 让线程休眠指定的时间,单位为毫秒
* 细节:
* 1、那条线程执行到这个方法,那么哪条线程就会停留对应的时间
* 2、方法的参数:就表示睡眠的时间,单位毫秒(1秒 = 1000毫秒)
* 3、当时间到了之后,线程就会自动醒来,继续执行下面的其他代码
*
*/
//setName
/*
// 1. 创建线程对象
MyThread t1 = new MyThread("飞机");
MyThread t2 = new MyThread("坦克");
// 2. 开启线程
t1.start();
t2.start();*/
// 哪条线程执行到这个方法,此时获取的就是哪条线程的镀锡
/*Thread t = Thread.currentThread();
String name = t.getName();
System.out.println(name); // main*/
// sleep
/*System.out.println("111111111111111");
Thread.sleep(5000);
System.out.println("222222222222222");*/
}
}
setPriority && getPriority
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" +i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
/**
* setPriority(int newPriority) 设置线程的优先级
* final int getPriority() 获取线程的优先级
*/
// 创建线程要执行的参数对象
MyRunnable mr = new MyRunnable();
// 创建线程对象
Thread t1 = new Thread(mr, "飞机");
Thread t2 = new Thread(mr, "坦克");
t1.setPriority(1);
t2.setPriority(10);
t1.start();
t2.start();
}
}
守护线程
public class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "@" + i);
}
}
}
public class MyThread2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "@" + i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
/**
* final void setDaemon(boolean on) 设置为守护线程
* 细节:
* 当其他的非守护线程执行完毕之后,守护线程就会陆续结束
* 通俗易懂:
* 当女神线程结束了,那么备胎也没有存在的必要了
*/
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("女神");
t2.setName("备胎");
// 把第二个线程设置为守护线程(备胎线程)
t2.setDaemon(true);
t1.start();
t2.start();
}
}
礼让线程
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "@" + i);
// 表示出让当前 CPU 的执行权,让出执行权的线程也会重新参与抢夺。
Thread.yield();
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
/**
* public static void yield() 出让线程/礼让线程
*/
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("飞机");
t2.setName("坦克");
t1.start();
t2.start();
}
}
插入线程
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "@" + i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
/**
* public final void join() 插入线程/插队线程
*/
MyThread t = new MyThread();
t.setName("土豆");
t.start();
// 把 t 线程插入到当前线程之前。
// t:土豆
// 当前线程:main
t.join();
// 执行在 main 线程中的方法
for (int i = 0; i < 10; i++) {
System.out.println("main 线程" + i);
}
}
}
线程的生命周期
5. 线程安全问题
public class MyThread extends Thread{
// static 表示这个类所有的对象都共享 ticket
int ticket = 0; // 0 ~ 99
@Override
public void run() {
while (true) {
if (ticket < 100) {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖" + ticket + "张票!!!");
} else {
break;
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
/**
* 需求:
* 某电影院目前正在上映国产大片,共 100 张票,而它有 3 个窗口卖票,请设计一个程序模拟该电影院卖票。
*/
// 创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
// 起名字
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
// 开启线程
t1.start();
t2.start();
t3.start();
}
}
上面的代码存在以下问题:
- 超卖:线程1、2、3都有可能在同时查看剩余票数时,都看到还有可卖的票,于是同时执行买票操作。
- 卖出相同的票:因为在线程1、2、3都有可能同一时间进行买票操作
同步代码块解决线程安全问题
格式:
synchronized (锁) {
操作共享数据的代码
}
特点1:锁默认打开,有一个线程进去了,锁会自动关闭
特点2:里面的代码全部执行完毕,线程出来,锁自动打开
修改之后的线程代码
public class MyThread extends Thread{
// static 表示这个类所有的对象都共享 ticket
static int ticket = 0; // 0 ~ 99
// 加 static 保证 obj 是唯一的。(锁对象要保证是唯一的)
static Object obj = new Object();
@Override
public void run() {
while (true) {
// 同步代码块
synchronized (obj) { // 这里的 obj 也可以替换成 MyThread.class(MyThread 的字节码文件),因为 MyThread 的字节码文件也是唯一的。
if (ticket < 100) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖" + ticket + "张票!!!");
} else {
break;
}
}
}
}
}
注意:锁要加在 while 循环的里面,如果加在循环的外面,某个线程抢到锁后,会一直执行循环内的代码,直到这个线程把所有的票买完。因为线程抢到票之后,就算其它线程也抢到票,也只能在循环锁外面等着。
同步方法
格式:修饰符 synchronized 返回值类型 方法名(方法参数) {…}
特点1:同步方法是锁住方法里面的代码
特点2:锁对象不能自己指定
- 非静态:this
- 静态:当前类的字节码文件对象
StringBuffer 与 StringBuilder 的线程安全区别:
-
StringBuffer 是线程安全的。因为在 StringBuffer 中有 synchronized 关键字。
-
而 StringBuilder 则不是线程安全的。
那对 StringBuffer 和 StringBuilder 我们如何选择?
- 代码是单线程的,不涉及多线程、线程安全的问题,那么选择 StringBuilder 就好了。
- 如果是多线程,设计线程安全问题,那么可以选择 StringBuffer 。
6. 死锁
6.1 锁 lock
-
示例代码
// 创建锁对象 Lock lock = new ReentrantLock(); // 设置锁 lock.lock(); // 释放锁 lock.unlock();
-
锁的应用
public class MyThread extends Thread{ static int ticket = 0; // 加 static 使每个线程都用统一把锁 static Lock lock = new ReentrantLock(); @Override public void run() { // 1. 循环 while (true) { // 2. 同步代码块 lock.lock(); try { // 3. 判断 if (ticket == 100) { break; } else { // 4. 判断 Thread.sleep(10); ticket++; System.out.println(getName() + "在卖第" + ticket + "张票"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } }
public class ThreadDemo { public static void main(String[] args) { /** * 需求: * 某电影院目前正在上映国产大片,共 100 张票,而它有 3 个窗口卖票,请设计一个程序模拟该电影院卖票。 * * 利用同步方法完成 * */ MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); MyThread t3 = new MyThread(); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }
6.2 死锁
- 死锁示例代码
public class MyThread extends Thread{
static Object objA = new Object();
static Object objB = new Object();
@Override
public 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();
}
}
执行这个代码就会出现死锁的情况。
7. 生产者和消费者
7.1 等待唤醒机制
等待唤醒机制思路(Desk、Cook、Foodie)
示例代码(写法一):
public class Desk {
/**
* 作用:控制生产者和消费者的执行
*/
// 是否有面条 0:没有面条 1:有面条
public static int foodFlog = 0;
// 总个数
public static int count = 10;
// 锁对象
public static Object lock = new Object();
}
public class Cook extends Thread{
@Override
public void run() {
/**
* 1. 循环
* 2. 同步代码块
* 3. 判断共享数据是否到了尾声(到了尾声)
* 4. 判断共享数据是否到了尾声(没有到尾声,执行核心逻辑)
*/
while (true) {
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
// 判断桌子上是否有食物
// 如果有,就等待
if (Desk.foodFlog == 1) {
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 如果没有,就制作食物
System.out.println("厨师做了一碗面条");
// 修改桌子上的食物状态
Desk.foodFlog = 1;
// 叫醒等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
public class Foodie extends Thread{
@Override
public void run() {
/**
* 1. 循环
* 2. 同步代码块
* 3. 判断共享数据是否到了末尾(到了末尾)
* 4. 判断共享数据是否到了末尾(没到末尾,执行核心逻辑)
*/
while (true) {
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
// 先判断桌子上是否有面条
if (Desk.foodFlog == 0) {
// 如果没有,就等待
try {
Desk.lock.wait(); // 让当前线程跟锁进行绑定
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 把吃的总数 -1
Desk.count--;
// 如果有,就开吃
System.out.println("吃货正在吃面条,还能再吃" + Desk.count + "碗!!!");
// 吃完之后,唤醒厨师继续做
Desk.lock.notifyAll();
// 修改桌子的状态
Desk.foodFlog = 0;
}
}
}
}
}
}
示例代码(写法二 — 阻塞队列方式实现):
阻塞队列的继承结构
public class Cook extends Thread{
ArrayBlockingQueue<String> queue;
public Cook(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
// 不断的把面条放到阻塞队列中
try {
queue.put("面条");
System.out.println("厨师放了一碗面条");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Foodie extends Thread{
ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
// 不断从阻塞队列中获取面条
try {
String food = queue.take();
System.out.println(food);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
/**
* 需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码
*
* 细节:
* 生产者和消费者必须使用同一个阻塞队列
*/
// 1. 创建阻塞队列的对象
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
// 2. 创建线程的对象,并把阻塞队列传递过去
Cook c = new Cook(queue);
Foodie f = new Foodie(queue);
// 3. 开启线程
c.start();
f.start();
}
}
7.2 线程的状态
但是再 JVM 里面是没有定义运行状态的
8. 练习
8.1 多线程实现抢红包
public class MyThread extends Thread{
// 共享数据
// 100 块,分成了3个红包
static double money = 100;
static int count = 3;
// 最小的中奖金额
static final double MIN = 0;
@Override
public void run() {
// 同步代码块
synchronized (MyThread.class) {
if (count == 0) {
// 判断,共享数据是否到了末尾(已经到末尾)
System.out.println(getName() + "没有抢到红包!");
} else {
// 判断,共享数据是否到了末尾(没有到末尾)
// 定义一个变量,表示中奖金额
double prize = 0;
if (count == 1) {
// 表示此时是最后一个红包
// 就无需随机,剩余所有的钱都是中奖金额
prize = money;
} else {
// 表示第一次,第二次(随机)
Random r = new Random();
double bounds = money - (count - 1) * MIN;
prize = r.nextDouble(bounds);
if (prize < MIN) {
prize = MIN;
}
}
// 从 money 中去掉当前中奖的金额
money = money - prize;
// 红包的个数 -1
count--;
// 本次红包的信息进行打印
System.out.println(getName() + "抢到了" + prize + "元");
}
}
}
}
public class Test {
public static void main(String[] args) {
/**
* 微信中的抢红包也用了多线程。
* 假设:100块,分成了3个包,现在有5个人去抢。
* 其中,红包是共享数据。
* 5个人是5条线程
* 打印结果如下:
* xxx 抢到了 xxx 元
* xxx 抢到了 xxx 元
* xxx 没抢到
* xxx 没抢到
*
*/
// 创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
MyThread t4 = new MyThread();
MyThread t5 = new MyThread();
// 给线程设置名字
t1.setName("小A");
t2.setName("小B");
t3.setName("小C");
t4.setName("小D");
t5.setName("小E");
// 启动线程
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
8.2 多线程实现抽奖
public class MyThread extends Thread{
ArrayList<Integer> list;
public MyThread(ArrayList<Integer> list) {
this.list = list;
}
@Override
public void run() { // 1 // 2
// 循环
// 同步代码块
// 判断
// 判断
while (true) {
synchronized (MyThread.class) {
if (list.size() == 0) {
break;
} else {
// 继续抽奖
Collections.shuffle(list);
int price = list.remove(0);
System.out.println(getName() + "又产生了一个 " + price + " 元大奖");
}
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test {
public static void main(String[] args) {
/**
* 有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为{18,5,20,50,100,200,500,800,2,80,300,700);
* 创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”
* 随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
* 每次抽出一个奖项就打印一个(随机)
* 抽奖箱1 又产生了一个 10 元大奖
* 抽奖箱1 又产生了一个 100 元大奖
* 抽奖箱1 又产生了一个 200 元大奖
* 抽奖箱1 又产生了一个 800 元大奖
* 元大奖抽奖箱2 又产生了一个 700
* ......
*/
// 创建奖池
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 18,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();
}
}
8.3 多线程统计并求最大值(解法1)
public class MyThread extends Thread{
ArrayList<Integer> list;
public MyThread(ArrayList<Integer> list) {
this.list = list;
}
// 线程1
static ArrayList<Integer> list1 = new ArrayList<>();
// 线程2
static ArrayList<Integer> list2 = new ArrayList<>();
@Override
public void run() { // 1 // 2
// 循环
// 同步代码块
// 判断
// 判断
while (true) {
synchronized (MyThread.class) {
if (list.size() == 0) {
if ("抽奖箱1".equals(getName())) {
System.out.println("抽奖箱1" + list1);
} else {
System.out.println("抽奖箱2" + list2);
}
break;
} else {
// 继续抽奖
Collections.shuffle(list);
int price = list.remove(0);
if ("抽奖箱1".equals(getName())) {
list1.add(price);
} else {
list2.add(price);
}
}
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test {
public static void main(String[] args) {
/**
* 有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为{10,5,20,50,100,200,500,800,2,80,300,700};
* 创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”
* 随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
* 每次抽的过程中,不打印,抽完时一次性打印(随机) 在此次抽奖过程中,抽奖箱1总共产生了6个奖项。
* 分别为: 10,20,100,500,2,300最高奖项为300元,总计额为932元
* 在此次抽奖过程中,抽奖箱2总共产生了6个奖项。
* 分别为: 5,50,200,800,80,700最高奖项为800元,总计额为1835元
*/
// 创建奖池
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 18,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();
}
}
8.4 多线程统计并求最大值(解法2)
public class MyThread extends Thread{
ArrayList<Integer> list;
public MyThread(ArrayList<Integer> list) {
this.list = list;
}
@Override
public void run() {
ArrayList<Integer> boxList = new ArrayList<>();
while (true) {
synchronized (MyThread.class) {
if (list.size() == 0) {
System.out.println(getName() + boxList);
break;
} else {
// 继续抽奖
Collections.shuffle(list);
int price = list.remove(0);
boxList.add(price);
}
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test {
public static void main(String[] args) {
/**
* 有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为{10,5,20,50,100,200,500,800,2,80,300,700};
* 创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”
* 随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
* 每次抽的过程中,不打印,抽完时一次性打印(随机) 在此次抽奖过程中,抽奖箱1总共产生了6个奖项。
* 分别为: 10,20,100,500,2,300最高奖项为300元,总计额为932元
* 在此次抽奖过程中,抽奖箱2总共产生了6个奖项。
* 分别为: 5,50,200,800,80,700最高奖项为800元,总计额为1835元
*/
// 创建奖池
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 18,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();
}
}
8.4 多线程之间的比较
public class MyCallable implements Callable<Integer> {
ArrayList<Integer> list;
public MyCallable(ArrayList<Integer> list) {
this.list = list;
}
@Override
public Integer call() throws Exception {
ArrayList<Integer> boxList = new ArrayList<>();
while (true) {
synchronized (MyCallable.class) {
if (list.size() == 0) {
System.out.println(Thread.currentThread().getName() + boxList);
break;
} else {
// 继续抽奖
Collections.shuffle(list);
int price = list.remove(0);
boxList.add(price);
}
}
Thread.sleep(10);
}
// 把集合中的最大值返回
if (boxList.size() == 0) {
return null;
} else {
return Collections.max(boxList);
}
}
}
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/**
* 有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为{10,5,20,50,100,200,500,800,2,80,300,700};
* 创建两个抽奖箱(线程)设置线程名称分别为 "抽奖箱1","抽奖箱2"
* 随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
* 在此次抽奖过程中,抽奖箱1总共产生了6个奖项,分别为: 10,20,100,500,2,300
* 最高奖项为300元,总计额为932元
*
* 在此次抽奖过程中,抽奖箱2总共产生了6个奖项,分别为: 5,50,200,800,80,700
* 最高奖项为800元,总计额为1835元
*
* 在此次抽奖过程中,抽奖箱2中产生了最大奖项,该奖项金额为800元
* 核心逻辑:获取线程抽奖的最大值(看成是线程运行的结果)
* 以上打印效果只是数据模拟,实际代码运行的效果会有差异
*/
// 创建奖池
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 10,5,20,50,100,200,500,800,2,80,300,700);
// 创建多线程要运行的参数对象
MyCallable mc = new MyCallable(list);
// 创建多线程运行结果的管理对象
FutureTask<Integer> ft1 = new FutureTask<>(mc);
FutureTask<Integer> ft2 = new FutureTask<>(mc);
// 创建线程对象
Thread t1 = new Thread(ft1);
Thread t2 = new Thread(ft2);
// 设置名字
t1.setName("抽奖箱1");
t2.setName("抽奖箱2");
// 开启线程
t1.start();
t2.start();
Integer max1 = ft1.get();
Integer max2 = ft2.get();
System.out.println(max1);
System.out.println(max2);
// 在此次抽奖过程中,抽奖箱2中产生了最大奖项,该奖项金额为800元
System.out.println("在此次抽奖过程中,抽奖箱"+(ft1.get()==800?t1.getName():t2.getName())+"中产生了最大奖项,该奖项金额为800元");
}
}
9. 线程池
9.1 以前写多线程的弊端
- 弊端1:用到线程就得创建
- 弊端2:用完之后线程就消失
9.3 线程池主要核心原理
- 创建一个池子,池子中是空的
- 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子,下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可。
- 但是如果提交任务的时候,池子中没有空闲的线程,也无法创建新的线程,任务就会排队等待。
9.3 线程池代码实现
- 创建线程池
- 提交任务
- 所有任务全部执行完毕,关闭线程池
newCachedThreadPool 演示
public class MyThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
/**
* public static ExecutorService newCachedThreadPool() 创建一个没有上限的线程池
* public static ExecutorService newFixedThreadPool(int nThreads) 创建有上线的线程池
*/
// newCachedThreadPool 演示
// 1. 获取线程池的对象
ExecutorService pool1 = Executors.newCachedThreadPool();
Thread.sleep(1000);
// 2. 提交任务
pool1.submit(new MyRunnable());
Thread.sleep(1000);
pool1.submit(new MyRunnable());
Thread.sleep(1000);
pool1.submit(new MyRunnable());
Thread.sleep(1000);
pool1.submit(new MyRunnable());
Thread.sleep(1000);
pool1.submit(new MyRunnable());
// 3. 销毁线程池
// pool1.shutdown();
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "---");
}
}
运行效果:可以看到会一直在服用线程池中的线程1。
newFixedThreadPool 演示
public class MyThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
/**
* public static ExecutorService newCachedThreadPool() 创建一个没有上限的线程池
* public static ExecutorService newFixedThreadPool(int nThreads) 创建有上线的线程池
*/
// newFixedThreadPool 演示
// 1. 获取线程池的对象
ExecutorService pool1 = Executors.newFixedThreadPool(3);
// 2. 提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
// 3. 销毁线程池
// pool1.shutdown();
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
在运行的结果可以看到虽然 new 的线程大于3个,但是实际生成的线程只有3个。
或者我们可以使用 DEBUG 的方式查看结果
在长度为3的线程池中,创建了4个线程之后,线程池的长度为3,在外面的等待的任务数为1。
9.4 自定义线程池详解
注意:Java 默认的任务拒绝策略是 AbortPolicy,默认策略:丢弃任务并抛出 PejectdExecutionException 异常
拒接策略
创建自定义线程的构造方法参数解析:
自定义线程池小结:
- 当核心线程满时,再提交任务就会排队
- 当核心线程满,队伍满时,会创建临时线程
- 当核心线程满时,队伍满,临时线程满时,会触发任务策略
学习视频:https://www.bilibili.com/video/BV1LG4y1T7n2?p=1&vd_source=6108736e361d963b64f872fefb8bc1e7