文章目录
- 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 int getPriority() | 获取线程的优先级 | 
| 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进行计算









![[读论文]Referring Camouflaged Object Detection](https://img-blog.csdnimg.cn/77ad43e5bcf54278aac20875f214a926.png)









