Java多线程用法(附20道练习题)

目录

  • 一、多线程的实现方式
    • 1. 继承Thread类
    • 2. 实现Runnable接口
    • 3. 实现Callable接口
    • 4. 三种方式的对比
  • 二、多线程的常用的实现方法
  • 三、守护线程、礼让线程和插队线程
    • 1. 守护线程 thread.setDaemon(true)
    • 2. 礼让线程 Thread.yield()
    • 3. 插队线程 thread.join();
  • 四、Java中线程的生命周期
    • 1. 新建(New)
    • 2. 就绪(Runnable)
    • 3. 运行(Running)
    • 4. 阻塞(Blocked)
    • 5. 等待(Waiting)
    • 6. 超时等待(Timed Waiting)
    • 7. 终止(Terminated)
  • 五、线程的安全问题
    • 1. 案例——多窗口卖票
    • 2. 同步代码块
    • 3. 同步方法
    • 4. 手动上锁和解锁
  • 六、死锁
    • 1. 死锁的条件
    • 2. 解决死锁的办法
    • 3. 案例——相互锁定资源
  • 七、生产者与消费者模型(唤醒等待机制)
    • 1. 常用方法
      • (1)wait()
      • (2)notify()
      • (3)notifyAll()
    • 2. 案例
  • 八、线程池
    • 1. 线程池的原理
    • 2. ThreadPoolExecutor用法和案例
    • 3. 功能线程池
      • (1)定长线程池(FixedThreadPool)
      • (2)定时线程池(ScheduledThreadPool)
      • (3)可缓存线程池(CachedThreadPool)
      • (4)单线程化线程池(SingleThreadExecutor)
  • 九、练习
    • 1. 继承Thread类,创建两个线程,分别打印1到100的奇数和偶数。
    • 2. 实现Runnable接口,创建两个线程,共享一个整数变量,每个线程对该变量进行100次增加1的操作。
    • 3. 实现Callable接口,该接口的call方法计算并返回1到100的和。
    • 4. 实现Callable接口,该接口的call方法设计一个10秒倒计时,倒计时结束后返回"开始!"字符串并打印。
    • 5. 创建一个守护线程,该线程每3秒打印一次系统时间。同时创建一个非守护线程,该线程打印从1到10的数字,并在每个数字后休眠1秒。确保主线程结束时,守护线程也停止运行。
    • 6. 创建一个守护线程,用于监控程序运行时的内存使用情况。当内存使用率达到15%时,守护线程将打印警告信息。
    • 7. 创建两个分别打印100次数字和字母的线程,其中打印数字的线程在每次循环时调用Thread.yield(),另一个线程不调用。观察两个线程的执行顺序。
    • 8. 两个售票窗口同时售票,直到售完200张票为止。用线程模拟售票窗口,使用synchronized关键字实现一个简单的线程同步。
    • 9. 编写线程安全的计数器类SafeCounter,包含一个静态变量count和一个增加计数的同步方法increment()。
    • 10. 编写一个BankAccount类,包含一个整数balance表示账户余额。实现一个同步方法deposit()用于存款,一个同步方法withdraw()用于取款。
    • 11. 编写一个TaskQueue类,包含一个队列用于存储任务,并实现一个方法addTask()用于添加任务,一个方法getTask()用于获取任务。使用手动上锁和解锁的方式确保添加和获取任务的线程安全。
    • 12. 创建两个线程,线程A打印数字1到10,线程B打印字母A到J。要求线程B在打印到字母F时,等待线程A打印完毕后再继续。
    • 13. 实现Runnable接口,创建两个线程,一个线程打印字母A-Z,另一个线程打印数字1-26,要求交替打印。
    • 14. 实现Runnable接口,创建两个线程,一个线程打印字母A,另一个线程打印字母B,要求交替打印出ABABAB...,各打印10次。
    • 15. 实现一个简单的生产者-消费者问题。生产者向队列中添加元素,消费者从队列中取出元素。
    • 16. 创建一个定长线程池,包含3个线程。提交5个任务到线程池,每个任务打印当前线程的名称。
    • 17. 创建一个定时线程池,安排一个任务在3秒后执行,任务打印当前时间。
    • 18. 创建一个可缓存线程池,提交10个任务,每个任务打印当前线程的名称,并等待所有任务执行完毕。
    • 19. 创建一个单线程化线程池,提交5个任务,每个任务打印当前线程的名称。
    • 20. 创建一个自定义线程池,核心线程数为2,最大线程数为5,线程空闲时间为1分钟,工作队列容量为5。提交10个任务到线程池,每个任务打印当前线程的名称。

一、多线程的实现方式

在Java中,多线程的实现主要有以下三种方式

  • 继承Thread类:通过继承Thread类并重写其run()方法来创建线程。
  • 实现Runnable接口:通过实现Runnable接口的run()方法来创建线程。
  • 实现Callable接口:通过实现Callable接口的call()方法来创建线程,这种方式可以获取线程的执行结果。

1. 继承Thread类

class MyThread extends Thread {@Overridepublic void run() {for (int i = 0; i < 3000; i++) {System.out.println("MyThread - " + i + ": " + Thread.currentThread().getName());}}
}public class Main {public static void main(String[] args) {MyThread thread1 = new MyThread();MyThread thread2 = new MyThread();thread1.start();thread2.start();}
}
输出结果片段展示:
...
MyThread - 966: Thread-0
MyThread - 1025: Thread-1
MyThread - 1026: Thread-1
MyThread - 1027: Thread-1
MyThread - 967: Thread-0
MyThread - 968: Thread-0
...

2. 实现Runnable接口

class MyRunnable implements Runnable {@Overridepublic void run() {for (int i = 0; i < 3000; i++) {System.out.println("MyRunnable - " + i + ": " + Thread.currentThread().getName());}}
}public class Main {public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();Thread thread1 = new Thread(myRunnable);Thread thread2 = new Thread(myRunnable);thread1.start();thread2.start();}
}
输出结果片段展示:
...
MyRunnable - 2261: Thread-0
MyRunnable - 2262: Thread-0
MyRunnable - 2723: Thread-1
MyRunnable - 2263: Thread-0
MyRunnable - 2724: Thread-1
MyRunnable - 2725: Thread-1
...

3. 实现Callable接口

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;class MyCallable implements Callable<String> {@Overridepublic String call() {for (int i = 0; i < 3000; i++) {System.out.println("MyCallable - " + i + ": " + Thread.currentThread().getName());}// 返回一个字符串表示线程执行完成return "执行完成";}
}public class Main {public static void main(String[] args) {MyCallable myCallable = new MyCallable();// 使用FutureTask包装Callable对象,以便获取线程的执行结果FutureTask<String> futureTask1 = new FutureTask<>(myCallable);FutureTask<String> futureTask2 = new FutureTask<>(myCallable);// 创建两个线程,每个线程都执行同一个FutureTaskThread thread1 = new Thread(futureTask1);Thread thread2 = new Thread(futureTask2);thread1.start();thread2.start();try {// 获取线程的执行结果System.out.println("线程1返回的结果为: " + futureTask1.get());System.out.println("线程2返回的结果为: " + futureTask2.get());} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}}
}
输出结果片段展示:
...
MyCallable - 846: Thread-0
MyCallable - 847: Thread-0
MyCallable - 848: Thread-0
MyCallable - 849: Thread-0
MyCallable - 887: Thread-1
MyCallable - 850: Thread-0
...

4. 三种方式的对比

  • 继承Thread:适用于简单的场景,不需要与其他类继承关系冲突。
  • 实现Runnable:适用于需要共享资源或需要多线程执行相同任务的情况,是Java多线程编程的推荐方式。
  • 实现Callable:适用于需要线程返回值或者需要异常处理的情况,但代码相对复杂。

在选择实现方式时,应根据具体需求和场景来决定。
一般来说,实现Runnable接口是最常见的选择,因为它更灵活且易于维护。如果需要线程返回结果或处理异常,则应该使用Callable接口。而继承Thread类的方式在现代Java多线程编程中已经较少使用。

二、多线程的常用的实现方法

成员方法及原理

  • getName(): 返回线程的名称。
  • setName(String name): 设置线程的名称。
  • currentThread(): 返回当前正在执行的线程对象的引用。
  • sleep(long millis): 使当前正在执行的线程暂停执行指定的毫秒数。
  • setPriority(int newPriority): 更改线程的优先级。
  • getPriority(): 返回线程的优先级。
class MyRunnable implements Runnable {@Overridepublic void run() {// 获取当前线程的引用Thread currentThread = Thread.currentThread();// 输出当前线程的名称和优先级System.out.println("当前线程: " + currentThread.getName() + ",优先级: " + currentThread.getPriority());// 让线程休眠200mstry {Thread.sleep(200);} catch (InterruptedException e) {// 如果线程在休眠期间被中断,捕获中断异常e.printStackTrace();}// 输出线程结束信息System.out.println(currentThread.getName() + " 已经结束");}
}public class Main {public static void main(String[] args) {// 创建两个线程,可在Thread构造方法中创建线程名字Thread thread1 = new Thread(new MyRunnable(), "线程1");Thread thread2 = new Thread(new MyRunnable());// 设置第二个线程的名称thread2.setName("线程2");// 设置线程优先级thread1.setPriority(Thread.MIN_PRIORITY); // 设置thread1为最低优先级thread2.setPriority(Thread.MAX_PRIORITY); // 设置thread2为最高优先级// 打印"Hello World1",这是主线程的输出System.out.println("Hello World1");// 启动线程,这会导致新线程开始执行run()方法,但不会立即执行thread1.start();thread2.start();// 打印"Hello World2",这是主线程的输出System.out.println("Hello World2");// 这两条System.out.println()语句是在主线程中执行的,因此它们会立即执行// 而thread1和thread2的start()方法调用后,这两个线程会并行执行// 由于线程调度的不确定性,主线程可能会在两个新线程开始执行之前完成打印操作}
}
输出结果:
Hello World1
Hello World2
当前线程: 线程1,优先级: 1
当前线程: 线程2,优先级: 10
线程2 已经结束
线程1 已经结束

三、守护线程、礼让线程和插队线程

1. 守护线程 thread.setDaemon(true)

守护线程是一种特殊的线程,它的作用是为其他线程提供服务。当程序中所有的非守护线程都结束时,即使守护线程的代码没有执行完,JVM也会退出。守护线程通常用于执行后台任务,如垃圾回收、内存管理等。

// 实现Runnable接口的守护线程类
class DaemonRunnable implements Runnable {@Overridepublic void run() {int count = 0;// 守护线程无限循环打印信息while (true) {// 打印守护线程正在执行的信息System.out.println("守护线程执行中... (" + count + ")");count++;try {// 守护线程休眠2秒Thread.sleep(2000);} catch (InterruptedException e) {// 捕获并打印InterruptedException异常e.printStackTrace();}}}
}// 主类
public class Main {public static void main(String[] args) {// 创建一个守护线程实例,并指定线程名称为"Daemon-Thread"Thread daemonThread = new Thread(new DaemonRunnable(), "Daemon-Thread");// 设置该线程为守护线程daemonThread.setDaemon(true);// 启动守护线程daemonThread.start();// 主线程执行一些任务for (int i = 0; i < 5; i++) {// 打印主线程正在执行的信息System.out.println("主线程执行中... (" + i + ")");try {// 主线程休眠1秒Thread.sleep(1000);} catch (InterruptedException e) {// 捕获并打印InterruptedException异常e.printStackTrace();}}// 主线程任务完成System.out.println("主线程执行结束.");}
}
输出结果:
守护线程执行中... (0)
主线程执行中... (0)
主线程执行中... (1)
主线程执行中... (2)
守护线程执行中... (1)
主线程执行中... (3)
守护线程执行中... (2)
主线程执行中... (4)
主线程执行结束.

2. 礼让线程 Thread.yield()

礼让线程是指线程通过调用Thread.yield()方法,主动让出CPU的使用权,给其他线程执行的机会。这并不意味着线程会立即停止执行,它可能会在稍后重新获得CPU时间。礼让线程是一种启发式的方法,用于提高线程调度的公平性。

// 实现Runnable接口的礼让线程类
class YieldingRunnable implements Runnable {@Overridepublic void run() {for (int i = 0; i < 1000; i++) {System.out.println(Thread.currentThread().getName() + " 正在执行 (" + i + ")");// 线程A礼让CPUThread.yield();}System.out.println(Thread.currentThread().getName() + " 执行完毕.");}
}// 实现Runnable接口的普通线程类
class NormalRunnable implements Runnable {@Overridepublic void run() {for (int i = 0; i < 1000; i++) {System.out.println(Thread.currentThread().getName() + " 正在执行 (" + i + ")");// 线程B不礼让CPU,直接继续执行}System.out.println(Thread.currentThread().getName() + " 执行完毕.");}
}public class Main {public static void main(String[] args) {// 创建礼让线程AThread threadA = new Thread(new YieldingRunnable(), "礼让线程A");// 创建普通线程BThread threadB = new Thread(new NormalRunnable(), "普通线程B");// 启动线程A和线程BthreadA.start();threadB.start();}
}
后几条输出结果:
礼让线程A 正在执行 (995)
礼让线程A 正在执行 (996)
礼让线程A 正在执行 (997)
礼让线程A 正在执行 (998)
礼让线程A 正在执行 (999)
普通线程B 执行完毕.
礼让线程A 执行完毕.

YieldingRunnable类定义了一个礼让线程的行为,它在每次循环中都会调用Thread.yield()方法,表示它愿意让出CPU给其他线程。因此CPU往往先执行完普通线程B。

3. 插队线程 thread.join();

join()方法是Thread类的一个实例方法,它允许一个线程等待另一个线程执行完毕。当一个线程调用另一个线程的join() 方法时,它将阻塞直到被调用的线程结束。
例如,让线程B等待线程A结束,需要在线程B中调用threadA.join()

// 实现Runnable接口的普通线程类
class NormalRunnable implements Runnable {@Overridepublic void run() {for (int i = 0; i < 1000; i++) {System.out.println(Thread.currentThread().getName() + " 正在执行 (" + i + ")");}System.out.println(Thread.currentThread().getName() + " 执行结束.");}
}// 实现Runnable接口的插队线程类
class JoiningRunnable implements Runnable {private Thread threadToJoin;public JoiningRunnable(Thread threadToJoin) {this.threadToJoin = threadToJoin; //threadA赋值给它}@Overridepublic void run() {for (int i = 0; i < 1000; i++) {System.out.println(Thread.currentThread().getName() + " 正在执行 (" + i + ")");if (i == 500) {try {threadToJoin.join(); //threadA插入到B的线程,要等待threadA执行结束,才能继续执行B的线程} catch (InterruptedException e) {throw new RuntimeException(e);}}}System.out.println(Thread.currentThread().getName() + " 执行结束.");}
}public class Main {public static void main(String[] args) {Thread threadA = new Thread(new NormalRunnable());Thread threadB = new Thread(new JoiningRunnable(threadA));threadA.setName("threadA");threadB.setName("threadB");threadA.start();threadB.start();}
}
执行结果最后几条:
...
threadB 正在执行 (995)
threadB 正在执行 (996)
threadB 正在执行 (997)
threadB 正在执行 (998)
threadB 正在执行 (999)
threadB 执行结束.

四、Java中线程的生命周期

在这里插入图片描述
(图转自CSDN-Evankaka作者)

1. 新建(New)

当使用new关键字创建一个线程后,线程就处于新建状态。在这个状态下,线程并没有开始执行,只是实例化了一个线程对象。

2. 就绪(Runnable)

调用线程的start()方法后,线程进入就绪状态。此时,线程已经准备好被CPU调度执行,但具体什么时候开始执行,由线程调度器来决定。

3. 运行(Running)

当线程获得CPU时间片后,它就会进入运行状态,执行run()方法中的代码。注意,运行状态是就绪状态的一个子集,是线程实际执行代码的状态。

4. 阻塞(Blocked)

线程在执行过程中,可能会因为某些原因(如等待同步锁、等待IO、线程调用了sleep()方法等)而暂时停止运行,此时线程就进入了阻塞状态。在阻塞状态解除后,线程会重新进入就绪状态,等待再次被调度。

5. 等待(Waiting)

当线程调用了Object.wait()方法后,线程会进入等待状态。线程会等待其他线程的通知(通过Object.notify()或者Object.notifyAll()方法),在收到通知后,线程会从等待状态进入阻塞状态,然后再次进入就绪状态。

6. 超时等待(Timed Waiting)

超时等待状态与等待状态类似,但是它是带有指定等待时间的等待。线程在调用Thread.sleep(long millis)Object.wait(long timeout)Thread.join(long millis)等方法后,会进入超时等待状态。当指定时间到达后,线程会自动进入阻塞状态,然后再次进入就绪状态。

7. 终止(Terminated)

线程的run()方法执行完毕或者线程被强制终止(如调用stop()方法,不过这个方法已经不推荐使用)后,线程就进入了终止状态。此时,线程的生命周期结束,不能再被调度执行。

五、线程的安全问题

1. 案例——多窗口卖票

模拟场景
三个窗口一共卖5张票,一个线程模拟一个窗口卖票。

class Seller implements Runnable {private static int tickets = 5; // 总共的票数@Overridepublic void run() {while (true) {if (tickets > 0) {try {Thread.sleep(100); // 模拟售票操作需要的时间} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 卖出了一张票,剩余 " + --tickets + " 张票。");} else {break; // 票已售完,结束售票}}}
}public class Main {public static void main(String[] args) {// 创建三个线程模拟三个窗口Thread window1 = new Thread(new Seller());Thread window2 = new Thread(new Seller());Thread window3 = new Thread(new Seller());// 启动三个线程window1.start();window2.start();window3.start();}
}
输出结果:
Thread-0 卖出了一张票,剩余 4 张票。
Thread-1 卖出了一张票,剩余 4 张票。
Thread-2 卖出了一张票,剩余 3 张票。
Thread-1 卖出了一张票,剩余 2 张票。
Thread-0 卖出了一张票,剩余 2 张票。
Thread-2 卖出了一张票,剩余 2 张票。
Thread-0 卖出了一张票,剩余 1 张票。
Thread-2 卖出了一张票,剩余 1 张票。
Thread-1 卖出了一张票,剩余 1 张票。
Thread-0 卖出了一张票,剩余 0 张票。
Thread-2 卖出了一张票,剩余 -1 张票。
Thread-1 卖出了一张票,剩余 -1 张票。

实际上,三个窗口一共执行了12次run方法,也就是卖出了12张票,与实际一共只有5张票数不符合。这就引出了线程的安全问题。

2. 同步代码块

为了解决线程的安全问题,可以用synchronized(Seller.class)创建一个同步代码块。同步代码块的作用是可以使得同一时间只有一个线程能够进入这个块
同步代码块使用Seller.class作为锁,不需要创建额外的对象(如new Object())。这使得代码更简洁,并且避免了不必要的内存分配。所有线程必须竞争这个锁才能执行同步块中的代码。

同步代码块可以使用Seller.class作为锁,原因在于:

(1)唯一性:在JVM中,对于同一个类,无论创建多少个实例,.class引用总是指向同一个Class对象。
(2)不可变性:Class对象被加载到JVM中,它的状态就不会改变。因此它是一个很好的锁候选者。
(3)全局可见性:Class对象是全局可见的,这意味着它可以被JVM中的所有线程访问。因此,使用Seller.class作为锁可以确保所有访问Seller类的线程都能够看到这个锁。

class Seller implements Runnable {private static volatile int tickets = 5; // 总共的票数// private final Object lock = new Object(); // 使用了一个final修饰的Object作为锁,必须是final类型@Overridepublic void run() {while (true) {// 使用Seller.class作为锁对象,使用synchronized (lock) 也可以synchronized (Seller.class) { // // 同步代码块里面的代码,确保对tickets变量的访问是线程安全的if (tickets > 0) {try {Thread.sleep(100); // 模拟售票操作需要的时间} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 卖出了一张票,剩余 " + --tickets + " 张票。");} else {break; // 票已售完,结束售票}}}}
}public class Main {public static void main(String[] args) {Seller seller = new Seller(); // 创建一个Seller实例// 创建三个线程模拟三个窗口,共享同一个Seller实例Thread window1 = new Thread(seller);Thread window2 = new Thread(seller);Thread window3 = new Thread(seller);// 启动三个线程window1.start();window2.start();window3.start();}
}
输出结果:
Thread-0 卖出了一张票,剩余 4 张票。
Thread-0 卖出了一张票,剩余 3 张票。
Thread-0 卖出了一张票,剩余 2 张票。
Thread-0 卖出了一张票,剩余 1 张票。
Thread-0 卖出了一张票,剩余 0 张票。

3. 同步方法

当一个方法被声明为synchronized时,确保了在任一时刻只有一个线程可以执行该方法。

  • 对于实例方法,同步方法的锁是当前对象实例(即this)。
  • 对于静态方法,同步方法的锁是类的Class对象
class Seller implements Runnable {// 将tickets变量设为volatile,确保对变量的修改对其他线程立即可见private static volatile int tickets = 5;@Overridepublic void run() {while (true) {// 使用synchronized方法确保线程安全sellTicket();if (tickets <= 0) {break; // 票已售完,结束售票}}}// 同步方法,确保一次只有一个线程可以执行此方法// 使用synchronized关键字,同步方法的锁是当前对象实例(this)private synchronized void sellTicket() {if (tickets > 0) {try {Thread.sleep(100); // 模拟售票操作需要的时间} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 卖出了一张票,剩余 " + (--tickets) + " 张票。");}}
}public class Main {public static void main(String[] args) {Seller seller = new Seller(); // 创建一个Seller实例// 创建三个线程模拟三个窗口,共享同一个Seller实例Thread window1 = new Thread(seller);Thread window2 = new Thread(seller);Thread window3 = new Thread(seller);// 启动三个线程window1.start();window2.start();window3.start();}
}
输出结果:
Thread-0 卖出了一张票,剩余 4 张票。
Thread-0 卖出了一张票,剩余 3 张票。
Thread-0 卖出了一张票,剩余 2 张票。
Thread-0 卖出了一张票,剩余 1 张票。
Thread-0 卖出了一张票,剩余 0 张票。

4. 手动上锁和解锁

手动上锁和解锁是Java中用于控制多线程访问共享资源的一种机制,通常通过java.util.concurrent.locks.Lock接口的实现类来完成,如ReentrantLock

  • 上锁:当一个线程尝试获取锁时,它会调用lock()方法。如果锁当前没有被其他线程持有,那么这个线程将成功获取锁并继续执行。如果锁已经被其他线程持有,那么这个线程将被阻塞,直到锁被释放。
  • 解锁: 当线程完成对共享资源的操作后,它会调用unlock()方法来释放锁。一旦锁被释放,其他等待该锁的线程将有机会获取锁并继续执行。

模板示例:

Lock lock = new ReentrantLock();
public void accessResource() {lock.lock(); // 获取锁try {// 访问共享资源} finally {lock.unlock(); // 释放锁} 
} 
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;class Seller implements Runnable {private static volatile int tickets = 5; // 总共的票数// 创建一个可重入锁对象private final Lock lock = new ReentrantLock();@Overridepublic void run() {while (true) {// 手动上锁lock.lock();try {// 在此区域内的代码是同步的,确保对tickets变量的访问是线程安全的if (tickets > 0) {Thread.sleep(100); // 模拟售票操作需要的时间System.out.println(Thread.currentThread().getName() + " 卖出了一张票,剩余 " + --tickets + " 张票。");} else {break; // 票已售完,结束售票}} catch (InterruptedException e) {throw new RuntimeException(e);} finally {// 手动解锁,确保即使在发生异常时也能释放锁lock.unlock();}}}
}public class Main {public static void main(String[] args) {Seller seller = new Seller(); // 创建一个Seller实例// 创建三个线程模拟三个窗口,共享同一个Seller实例Thread window1 = new Thread(seller);Thread window2 = new Thread(seller);Thread window3 = new Thread(seller);// 启动三个线程window1.start();window2.start();window3.start();}
}
输出结果:
Thread-0 卖出了一张票,剩余 4 张票。
Thread-0 卖出了一张票,剩余 3 张票。
Thread-0 卖出了一张票,剩余 2 张票。
Thread-0 卖出了一张票,剩余 1 张票。
Thread-0 卖出了一张票,剩余 0 张票。

六、死锁

死锁是指两个或多个线程永久地等待对方释放资源而无法继续执行的状态。在死锁的情况下,每个线程都持有一些资源并且等待获取其他线程持有的资源,但由于每个线程都在等待其他线程释放资源,所以它们都无法继续执行,导致整个系统处于停滞状态。

1. 死锁的条件

死锁通常发生在以下四个条件同时成立时:

(1)互斥条件:资源不能被多个线程同时使用。
(2)持有和等待条件:线程至少持有一个资源,并且正在等待获取额外的资源,而该资源又被其他线程持有。
(3)非抢占条件:线程所获得的资源在未使用完毕前不能被其他线程强行抢占。
(4)循环等待条件:存在一个线程与资源的循环等待链,每个线程都在等待下一个线程所持有的资源。

2. 解决死锁的办法

解决死锁的方法可以分为预防死锁、避免死锁、检测死锁解除死锁

(1)预防死锁
通过破坏死锁的四个必要条件之一来预防死锁的发生:

  • 破坏互斥条件:有些资源可以被多个线程共享使用,例如读操作。
  • 破坏持有和等待条件:要求线程一次性获取所有需要的资源,但这可能导致资源利用率降低。
  • 破坏非抢占条件:允许线程从其他线程抢占资源,但这可能导致系统复杂性和不确定性增加。
  • 破坏循环等待条件:对资源进行排序,线程只能按照顺序请求资源。

(2)避免死锁
在运行时动态地避免死锁的发生,最著名的算法是银行家算法,它通过预测资源分配是否会导致死锁来避免死锁。
(3)检测死锁
允许死锁发生,但通过系统中的检测机制来识别死锁,并采取措施解除死锁。常用的检测方法是资源分配图。
(4)解除死锁
一旦检测到死锁,可以采取以下措施来解除死锁:

  • 剥夺资源:从某些线程中强行剥夺资源,分配给其他线程,以打破循环等待链。
  • 终止线程:终止(或回滚)一个或多个线程,释放它们持有的资源。
  • 资源抢占:如果系统支持抢占,可以从一个线程抢占资源分配给其他线程。

3. 案例——相互锁定资源

public class Main {public static void main(String[] args) {final Object resource1 = "资源1"; // 定义第一个资源对象final Object resource2 = "资源2"; // 定义第二个资源对象// 线程1尝试先锁定资源1然后锁定资源2Thread t1 = new Thread(() -> {synchronized (resource1) { // 线程1锁定资源1System.out.println("线程1:锁定了资源1");synchronized (resource2) { // 线程1锁定资源2System.out.println("线程1:锁定了资源2");} // 线程1释放资源2的锁} // 线程1释放资源1的锁});// 线程2尝试先锁定资源2然后锁定资源1Thread t2 = new Thread(() -> {synchronized (resource2) { // 线程2锁定资源2System.out.println("线程2:锁定了资源2");synchronized (resource1) { // 线程2锁定资源1System.out.println("线程2:锁定了资源1");} // 线程2释放资源1的锁} // 线程2释放资源2的锁});t1.start(); // 启动线程1t2.start(); // 启动线程2}
}
输出结果:
线程1:锁定了资源1
线程2:锁定了资源2
...
(由于死锁,该进程不结束)

七、生产者与消费者模型(唤醒等待机制)

1. 常用方法

(1)wait()

  • 用途:

    • 当一个线程执行到一个 synchronized 代码块内部时,如果某些条件尚未满足,它可以使用 wait() 方法让当前线程暂停执行,并释放当前持有的锁,进入等待状态(WAITINGTIMED_WAITING)。
  • 语法:

    • wait():无限期等待,直到另一个线程调用 notify()notifyAll() 方法。
    • wait(long timeout):等待指定的时间,如果时间到了还没有被唤醒,线程自动醒来。
    • wait(long timeout, int nanos):类似于 wait(long timeout),但增加了纳秒级的精度。
  • 注意事项:

    • wait() 必须在同步代码块中调用,否则会抛出 IllegalMonitorStateException
      当线程处于等待状态时,它不会占用CPU资源。
    • wait() 被唤醒后,线程需要重新获得锁才能继续执行。

(2)notify()

  • 用途:
    • 当一个线程完成了某个条件,希望通知正在等待这个条件的线程时,它可以在同步代码块内部调用 notify() 方法。
  • 语法:notify()
  • 注意事项:
    • notify() 也必须在同步代码块中调用。
    • notify() 随机唤醒一个在该对象上等待的线程。
    • 被唤醒的线程不会立即执行,它必须等待当前持有锁的线程释放锁。

(3)notifyAll()

  • 用途:notifyAll() 方法与 notify() 类似,但它会唤醒所有在该对象上等待的线程。
  • 语法:notifyAll()
  • 注意事项:
    • notifyAll() 也必须在同步代码块中调用。
    • 调用 notifyAll() 后,所有等待的线程都会被唤醒,但它们仍然需要竞争锁。

2. 案例

模拟场景
厨师上菜,顾客吃菜。厨师把上完的菜放桌上,桌子最多能放3个菜。厨师一天上100道菜,上完菜厨师下班(线程结束)。

两个线程:生产者线程(厨师)、消费者线程(顾客)
一个公共资源:容量为3的桌子

import java.util.LinkedList;// 桌子类,模拟生产者-消费者问题中的共享资源,一次只能被一个线程使用
class Desk {private int capacity; // 桌子的容量,即最多能放多少道菜private LinkedList<String> dishes; // 桌子上的菜// 构造方法,初始化桌子的容量和菜品列表public Desk(int capacity) {this.capacity = capacity;this.dishes = new LinkedList<>();}// 放置菜品的方法,由生产者(厨师)线程调用public synchronized void put(String dish) throws InterruptedException {// 只要桌子已满,则当前线程(生产者)一直等待while (dishes.size() == capacity) {wait(); // 当前线程(生产者)进入等待状态,释放对锁的占用}dishes.add(dish); // 添加菜品到桌子上System.out.println("厨师放置了" + dish + " \t\t桌子大小: " + dishes.size());notifyAll(); // 唤醒所有等待的线程(消费者),因为可能有菜可以取了}// 取走菜品的方法,由消费者线程调用public synchronized String take() throws InterruptedException {// 只要桌子上没有菜,则当前线程(消费者)一直等待while (dishes.isEmpty()) {wait(); // 当前线程(消费者)进入等待状态,释放对锁的占用}String dish = dishes.remove(); // 从桌子上移除一道菜System.out.println("顾客取走了" + dish + " \t\t桌子大小: " + dishes.size());notifyAll(); // 唤醒所有等待的线程(生产者),因为桌子上有空间了return dish; // 返回取走的菜品}
}// 厨师类,实现了Runnable接口,可以作为一个线程运行
class Chef implements Runnable {private Desk desk; // 厨师操作的桌子private int dishCount = 1; // 菜品编号// 构造方法,初始化厨师操作的桌子public Chef(Desk desk) {this.desk = desk;}@Overridepublic void run() {try {while (dishCount <= 10) { // 做最多10道菜String dish = "菜品 " + dishCount++; // 制作一道菜desk.put(dish); // 将菜放在桌子上Thread.sleep(100); // 模拟厨师准备下一道菜的时间}System.out.println("厨师上完菜下班");} catch (InterruptedException e) {Thread.currentThread().interrupt(); // 中断当前线程}}
}// 消费者类,实现了Runnable接口,可以作为一个线程运行
class Customer implements Runnable {private Desk desk; // 消费者取菜的桌子// 构造方法,初始化消费者操作的桌子public Customer(Desk desk) {this.desk = desk;}@Overridepublic void run() {try {while (true) { // 无限循环,模拟消费者不断取菜desk.take(); // 从桌子上取走一道菜Thread.sleep(200); // 模拟消费者吃菜的时间}} catch (InterruptedException e) {Thread.currentThread().interrupt(); // 中断当前线程}}
}// 主类,程序入口
public class Main {public static void main(String[] args) {Desk desk = new Desk(3); // 创建一个容量为3的桌子// 生产者和消费者共用deskThread producer = new Thread(new Chef(desk), "厨师"); // 创建生产者线程(厨师)Thread consumer = new Thread(new Customer(desk), "消费者"); // 创建消费者线程producer.start(); // 启动生产者线程consumer.start(); // 启动消费者线程}
}
输出结果:
厨师放置了菜品 1 		桌子大小: 1
顾客取走了菜品 1 		桌子大小: 0
厨师放置了菜品 2 		桌子大小: 1
顾客取走了菜品 2 		桌子大小: 0
厨师放置了菜品 3 		桌子大小: 1
厨师放置了菜品 4 		桌子大小: 2
顾客取走了菜品 3 		桌子大小: 1
厨师放置了菜品 5 		桌子大小: 2
厨师放置了菜品 6 		桌子大小: 3
顾客取走了菜品 4 		桌子大小: 2
厨师放置了菜品 7 		桌子大小: 3
顾客取走了菜品 5 		桌子大小: 2
厨师放置了菜品 8 		桌子大小: 3
顾客取走了菜品 6 		桌子大小: 2
厨师放置了菜品 9 		桌子大小: 3
顾客取走了菜品 7 		桌子大小: 2
厨师放置了菜品 10 		桌子大小: 3
厨师上完菜下班
顾客取走了菜品 8 		桌子大小: 2
顾客取走了菜品 9 		桌子大小: 1
顾客取走了菜品 10 		桌子大小: 0(由于桌子没有菜,顾客处于一直等待的状态,该进程不结束...)

八、线程池

线程池是一种用于管理线程的资源池,它可以重用一定数量的线程来执行多个任务,避免了频繁创建和销毁线程的开销。

1. 线程池的原理

在这里插入图片描述
(图转自CSDN-孙强 Jimmy作者)

2. ThreadPoolExecutor用法和案例

线程池的实现类是ThreadPoolExecutor,它有四种用法:

1. ThreadPoolExecutor(	int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)2. ThreadPoolExecutor(	int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)3. ThreadPoolExecutor(	int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)4. ThreadPoolExecutor(	int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
  1. int corePoolSize(必需)
    核心线程数,即在没有任务执行时仍然保持活跃的线程数。
  2. int maximumPoolSize(必需)
    线程池允许的最大线程数。当工作队列满了并且所有核心线程都在使用时,线程池可能会创建额外的线程,直到线程总数达到这个最大值。
  3. long keepAliveTime(必需)
    非核心线程的最大空闲时间。当线程池中的线程数量大于核心线程数时,这是非核心线程在终止前可以保持空闲状态的最长时间。
  4. TimeUnit unit(必需)
    时间单位。通常使用 TimeUnit.SECONDSTimeUnit.MILLISECONDS 等来指定 keepAliveTime 的单位。
  5. BlockingQueue workQueue(必需)
    用于存放等待执行的任务的队列。当核心线程都在忙碌时,新提交的任务将在这个队列中等待。可以选择不同类型的队列,如 LinkedBlockingQueueArrayBlockingQueueSynchronousQueue,具体取决于任务的特性和系统需求。
  6. ThreadFactory threadFactory(可选)
    线程工厂,用于创建新线程。通过线程工厂,可以设置线程的名称、优先级、守护进程状态等属性。如果没有设置该参数,默认值使用 Executors.defaultThreadFactory()
  7. RejectedExecutionHandler handler(可选)
    拒绝策略。当线程池无法执行新提交的任务时(例如,因为线程池关闭或达到其容量限制),这个处理器定义了如何处理这些任务。
    如果没有设置该参数,默认值使用 ThreadPoolExecutor.AbortPolicy,它将抛出RejectedExecutionException。可以提供自定义的处理器,比如 ThreadPoolExecutor.CallerRunsPolicyThreadPoolExecutor.DiscardPolicyThreadPoolExecutor.DiscardOldestPolicy,以适应不同的系统需求。

代码示例

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class Main {public static void main(String[] args) {// 创建一个ThreadPoolExecutor// 核心线程数为3,最大线程数为5,线程空闲时间为60秒,工作队列容量为10ThreadPoolExecutor executor = new ThreadPoolExecutor(3,      		   // 核心线程数5,                 // 最大线程数60,                // 线程空闲时间TimeUnit.SECONDS,  // 时间单位new ArrayBlockingQueue<>(5) // 工作队列);final int n = 8;// 提交任务到线程池for (int i = 1; i <= n; i++) {int taskId = i;executor.execute(() -> {try {// 模拟任务执行时间Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("任务 " + taskId + " 执行完毕,线程名称:" + Thread.currentThread().getName());});}// 关闭线程池executor.shutdown();try {// 等待所有任务完成或超时if (!executor.awaitTermination(1, TimeUnit.HOURS)) {// 超时后强制关闭executor.shutdownNow();}} catch (InterruptedException e) {// 如果等待被中断,也尝试立即关闭executor.shutdownNow();}System.out.println("所有任务完成,线程池关闭");}
}
任务数在1 ~ 3时,使用核心线程
任务数在4 ~ 8时,未执行的任务要在工作队列中排队
任务数在9 ~ 10时,启用额外的非核心线程
任务数 >=11 时,默认抛出异常

3. 功能线程池

(1)定长线程池(FixedThreadPool)

特点:

  • 只有核心线程。固定数量的线程来执行任务。
  • 如果某个线程因为执行异常而结束,那么线程池会补充一个新的线程。
  • 提交的任务队列没有大小限制,如果线程池满了,新提交的任务会在队列中等待。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;class Task implements Runnable {public void run() {System.out.println("执行的线程: " + Thread.currentThread().getName());}
}public class Main {public static void main(String[] args) {ExecutorService executor = Executors.newFixedThreadPool(3);for (int i = 0; i < 10; i++) {executor.execute(new Task());}executor.shutdown();}
}
输出结果:
执行的线程: pool-1-thread-3
执行的线程: pool-1-thread-2
执行的线程: pool-1-thread-1
执行的线程: pool-1-thread-3
执行的线程: pool-1-thread-1
执行的线程: pool-1-thread-3
执行的线程: pool-1-thread-1
执行的线程: pool-1-thread-3
执行的线程: pool-1-thread-1
执行的线程: pool-1-thread-3

(2)定时线程池(ScheduledThreadPool)

特点:

  • 可以延迟执行或者定期执行任务。
  • 核心线程数是固定的,非核心线程数没有限制
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;// 定义一个Task类,实现Runnable接口,表示一个可以被线程执行的任务
class Task implements Runnable {// 实现Runnable接口的run方法,该方法将在任务执行时被调用public void run() {// 打印当前线程的名字,表明任务正在执行System.out.println("Executing Scheduled Task: " + Thread.currentThread().getName());}
}public class Main {public static void main(String[] args) {// 创建一个ScheduledExecutorService实例,线程池中包含3个线程ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);System.out.println("hello");// 使用scheduler的schedule方法安排一个任务在5秒后执行// 参数分别是:任务实例,延迟时间,时间单位scheduler.schedule(new Task(), 5, TimeUnit.SECONDS);// 关闭ScheduledExecutorService,不再接受新任务,已提交的任务会继续执行scheduler.shutdown();}
}
输出结果:
hello
Executing Scheduled Task: pool-1-thread-1

(3)可缓存线程池(CachedThreadPool)

特点:

  • 没有核心线程,非核心线程数量无限。线程的数量是动态的,可以根据需求自动增长或收缩。
  • 当线程空闲超过60秒,就会被回收。
  • 适合执行大量短生命周期的异步任务。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;class Task implements Runnable {public void run() {System.out.println("执行缓存线程池任务: " + Thread.currentThread().getName());}
}public class Main {public static void main(String[] args) {// 使用Executors类的newCachedThreadPool方法创建一个可缓存线程池ExecutorService executor = Executors.newCachedThreadPool();for (int i = 0; i < 10; i++) {executor.execute(new Task());}executor.shutdown();}
}

(4)单线程化线程池(SingleThreadExecutor)

特点:

  • 只有一个核心线程来执行任务,没有非核心线程。
  • 任务队列无界,如果线程因为异常结束,线程池会创建一个新的线程来继续执行后续的任务。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;class Task implements Runnable {public void run() {System.out.println("Executing Single Task: " + Thread.currentThread().getName());}
}public class Main {public static void main(String[] args) {// 使用Executors类的newSingleThreadExecutor方法创建一个单线程化的线程池ExecutorService executor = Executors.newSingleThreadExecutor();for (int i = 0; i < 10; i++) {executor.execute(new Task());}executor.shutdown();}
}
输出结果:
Executing Single Task: pool-1-thread-1
Executing Single Task: pool-1-thread-1
Executing Single Task: pool-1-thread-1
Executing Single Task: pool-1-thread-1
Executing Single Task: pool-1-thread-1
Executing Single Task: pool-1-thread-1
Executing Single Task: pool-1-thread-1
Executing Single Task: pool-1-thread-1
Executing Single Task: pool-1-thread-1
Executing Single Task: pool-1-thread-1

九、练习

1. 继承Thread类,创建两个线程,分别打印1到100的奇数和偶数。

class Thread1 extends Thread {@Overridepublic void run() {//打印奇数的线程for (int i = 1; i <= 100; i += 2) {System.out.println(Thread.currentThread().getName() + "正在打印" + i);}}
}class Thread2 extends Thread {@Overridepublic void run() {//打印偶数的线程for (int i = 2; i <= 100; i += 2) {System.out.println(Thread.currentThread().getName() + "正在打印" + i);}}
}public class Main {public static void main(String[] args) {Thread1 thread1 = new Thread1();Thread2 thread2 = new Thread2();thread1.start();thread2.start();}
}

2. 实现Runnable接口,创建两个线程,共享一个整数变量,每个线程对该变量进行100次增加1的操作。

方法一:

class AddThread implements Runnable {// static保证是所有AddThread类共享的private static int num = 0;@Overridepublic void run() {for (int i = 1; i <= 100; i++) {synchronized (this) {System.out.println(Thread.currentThread().getName() + "正在执行第" + i + "次加法,num的值为" + (++this.num));}}}
}public class Main {public static void main(String[] args) {AddThread addThread1 = new AddThread();AddThread addThread2 = new AddThread();Thread thread1 = new Thread(addThread1);Thread thread2 = new Thread(addThread2);thread1.start();thread2.start();}
}

方法二:

class Counter implements Runnable {private int count = 0;@Overridepublic void run() {for (int i = 1; i <= 100; i++) {synchronized (Counter.class) {count++;System.out.println(Thread.currentThread().getName() + ": " + count);}}}
}public class Main {public static void main(String[] args) {Counter counter = new Counter();//多个线程执行相同的代码块,但可能需要访问共享资源时,可以将一个Runnable对象传递给两个线程Thread thread1 = new Thread(counter);Thread thread2 = new Thread(counter);thread1.start();thread2.start();}
}

3. 实现Callable接口,该接口的call方法计算并返回1到100的和。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;class SumCallable implements Callable {int sum = 0;@Overridepublic Integer call() {for (int i = 1; i <= 100; i++) {sum += i;}return sum;}
}public class Main {public static void main(String[] args) {SumCallable sumCallable = new SumCallable();FutureTask futureTask = new FutureTask<>(sumCallable);Thread thread = new Thread(futureTask);thread.start();try {System.out.println(futureTask.get());} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}}
}

4. 实现Callable接口,该接口的call方法设计一个10秒倒计时,倒计时结束后返回"开始!"字符串并打印。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;class ClockCallable implements Callable {@Overridepublic String call() {for (int i = 10; i >= 1; i--) {System.out.println(i);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}return "开始!";}
}public class Main {public static void main(String[] args) {ClockCallable clockCallable = new ClockCallable();FutureTask futureTask = new FutureTask<>(clockCallable);Thread thread = new Thread(futureTask);thread.start();try {Object o = futureTask.get();System.out.println(o);} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}}
}

5. 创建一个守护线程,该线程每3秒打印一次系统时间。同时创建一个非守护线程,该线程打印从1到10的数字,并在每个数字后休眠1秒。确保主线程结束时,守护线程也停止运行。

import java.time.LocalDateTime;class ClockRunnable implements Runnable {@Overridepublic void run() {while (true) {System.out.println("当前时间:" + LocalDateTime.now());try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}class NumberRunnable implements Runnable {@Overridepublic void run() {for(int i = 1 ;i<=10;i++){System.out.println(i);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}public class Main {public static void main(String[] args) {ClockRunnable clockRunnable = new ClockRunnable();NumberRunnable numberRunnable = new NumberRunnable();Thread thread1 = new Thread(clockRunnable);Thread thread2 = new Thread(numberRunnable);thread1.setDaemon(true); // 设置为守护线程thread1.start();thread2.start();}
}

6. 创建一个守护线程,用于监控程序运行时的内存使用情况。当内存使用率达到15%时,守护线程将打印警告信息。

class MemoryMonitor implements Runnable {@Overridepublic void run() {while (true) {Runtime runtime = Runtime.getRuntime();// 获取当前使用的内存long usedMemory = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024;// 获取最大内存long maxMemory = runtime.maxMemory() / 1024 / 1024;double memoryUsage = (double) usedMemory / maxMemory * 100;System.out.println(memoryUsage);if (memoryUsage > 10) {System.out.println("警告:内存使用率超过10%!");}try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}public class Main {public static void main(String[] args) {Thread memoryMonitor = new Thread(new MemoryMonitor());memoryMonitor.setDaemon(true);memoryMonitor.start();// 模拟内存使用while (true) {// 创建大量对象以增加内存使用Object[] memoryHog = new Object[10000];for (int i = 0; i < memoryHog.length; i++) {memoryHog[i] = new byte[1024]; // 1KB per object}try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}

7. 创建两个分别打印100次数字和字母的线程,其中打印数字的线程在每次循环时调用Thread.yield(),另一个线程不调用。观察两个线程的执行顺序。

class NumRunnable implements Runnable {@Overridepublic void run() {for (int i = 1; i <= 500; i++) {Thread.yield();System.out.println(1);}}
}class CharRunnable implements Runnable {@Overridepublic void run() {for (int i = 1; i <= 500; i++) {System.out.println("a");}}
}public class Main {public static void main(String[] args) {NumRunnable numRunnable = new NumRunnable();CharRunnable charRunnable = new CharRunnable();Thread thread1 = new Thread(numRunnable);Thread thread2 = new Thread(charRunnable);thread1.start();thread2.start();}
}

8. 两个售票窗口同时售票,直到售完200张票为止。用线程模拟售票窗口,使用synchronized关键字实现一个简单的线程同步。

class printNumber implements Runnable{static volatile int j = 0;@Overridepublic void run() {while (j<2000){synchronized (printNumber.this) {System.out.println(Thread.currentThread().getName() + "正在出售第" + (++j) +"张票");}}}
}public class Main {public static void main(String[] args) {printNumber printNumber1 = new printNumber();printNumber printNumber2 = new printNumber();Thread thread1 = new Thread(printNumber1);Thread thread2 = new Thread(printNumber2);thread1.start();thread2.start();}
}

9. 编写线程安全的计数器类SafeCounter,包含一个静态变量count和一个增加计数的同步方法increment()。

class SafeCounter {private static int conut = 0;public static synchronized void increment() {conut++;System.out.println(Thread.currentThread().getName() + ":" + conut);}
}class CountThread implements Runnable {@Overridepublic void run() {for (int i = 0; i < 1000; i++) {SafeCounter.increment();}}
}public class Main {public static void main(String[] args) {CountThread countThread1 = new CountThread();CountThread countThread2 = new CountThread();Thread thread1 = new Thread(countThread1);Thread thread2 = new Thread(countThread2);thread1.start();thread2.start();}
}

10. 编写一个BankAccount类,包含一个整数balance表示账户余额。实现一个同步方法deposit()用于存款,一个同步方法withdraw()用于取款。

class BankAccount {private int balance = 0;public synchronized void deposit(int money) {balance += money;System.out.println("存款成功,余额" + balance);}public synchronized void withdraw(int money) {if (balance - money >= 0) {balance -= money;System.out.println("取款成功,余额" + balance);} else {System.out.println("取款失败,余额不足");}}
}class Transaction implements Runnable {private BankAccount bankAccount;Transaction(BankAccount bankAccount) {this.bankAccount = bankAccount;}@Overridepublic void run() {bankAccount.deposit(100);bankAccount.withdraw(50);}
}public class Main {public static void main(String[] args) {BankAccount account = new BankAccount();// 两个线程共享一个accountThread t1 = new Thread(new Transaction(account));Thread t2 = new Thread(new Transaction(account));t1.start();t2.start();}
}

11. 编写一个TaskQueue类,包含一个队列用于存储任务,并实现一个方法addTask()用于添加任务,一个方法getTask()用于获取任务。使用手动上锁和解锁的方式确保添加和获取任务的线程安全。

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;class TaskQueue {private Queue<String> queue = new LinkedList<>();private final Lock lock = new ReentrantLock();public void addTask(String task) {lock.lock();try {queue.add(task);System.out.println("成功添加" + task);} finally {lock.unlock();}}public String pollTask() {lock.lock();try {if (!queue.isEmpty()) {String pollTask = queue.poll();System.out.println("成功删除: " + pollTask);return pollTask;}return null;} finally {lock.unlock();}}
}class TaskProducer implements Runnable {private TaskQueue taskQueue;TaskProducer(TaskQueue taskQueue) {this.taskQueue = taskQueue;}@Overridepublic void run() {for (int i = 1; i <= 100; i++) {taskQueue.addTask("Task" + i);}}
}class TaskConsumer implements Runnable {private TaskQueue taskQueue;TaskConsumer(TaskQueue taskQueue) {this.taskQueue = taskQueue;}@Overridepublic void run() {for (int i = 1; i <= 100; i++) {taskQueue.pollTask();}}
}public class Main {public static void main(String[] args) {TaskQueue queue = new TaskQueue();TaskProducer taskProducer = new TaskProducer(queue);TaskConsumer taskConsumer = new TaskConsumer(queue);Thread thread1 = new Thread(taskProducer);Thread thread2 = new Thread(taskConsumer);thread1.start();try {thread1.join();} catch (InterruptedException e) {throw new RuntimeException(e);}thread2.start();}
}

12. 创建两个线程,线程A打印数字1到10,线程B打印字母A到J。要求线程B在打印到字母F时,等待线程A打印完毕后再继续。

class A implements Runnable {@Overridepublic void run() {for (int i = 1; i <= 10; i++) {System.out.println(i);}}
}class B implements Runnable {private Thread A;B(Thread A) {this.A = A;}@Overridepublic void run() {for (char i = 'A'; i <= 'J'; i++) {System.out.println(i);if (i == 'F') {// F之前要把数字打印完try {A.join();} catch (InterruptedException e) {throw new RuntimeException(e);}}}}
}public class Main {public static void main(String[] args) {A a = new A();Thread threadA = new Thread(a);B b = new B(threadA);Thread threadB = new Thread(b);threadA.start();threadB.start();}
}

13. 实现Runnable接口,创建两个线程,一个线程打印字母A-Z,另一个线程打印数字1-26,要求交替打印。

class LetterRunnable implements Runnable {private Object lock;LetterRunnable(Object lock) {this.lock = lock;}@Overridepublic void run() {for (char i = 'A'; i <= 'Z'; i++) {synchronized (lock) {System.out.println(i);lock.notify();try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}}
}class DightRunnable implements Runnable {private Object lock;DightRunnable(Object lock) {this.lock = lock;}@Overridepublic void run() {for (int i = 1; i <= 26; i++) {synchronized (lock) {System.out.println(i);lock.notify();try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}}
}public class Main {public static void main(String[] args) {Object lock = new Object();LetterRunnable letterRunnable = new LetterRunnable(lock);DightRunnable dightRunnable = new DightRunnable(lock);Thread thread1 = new Thread(letterRunnable);Thread thread2 = new Thread(dightRunnable);thread1.start();thread2.start();}
}

14. 实现Runnable接口,创建两个线程,一个线程打印字母A,另一个线程打印字母B,要求交替打印出ABABAB…,各打印10次。

class PrintChar implements Runnable {private char charToPrint;private Object lock;public PrintChar(char charToPrint, Object lock) {this.charToPrint = charToPrint;this.lock = lock;}@Overridepublic void run() {for (int i = 0; i < 10; i++) {synchronized (lock) {// 执行 -> 唤醒 -> 等待 System.out.print(charToPrint);// 唤醒在锁对象上等待的线程(另一个线程)lock.notify();try {// 当前线程挂起,等待被其他线程唤醒,否则会继续执行循环lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}}}
}public class Main {public static void main(String[] args) {Object lock = new Object();Thread t1 = new Thread(new PrintChar('A', lock), "Thread-A");Thread t2 = new Thread(new PrintChar('B', lock), "Thread-B");t1.start();t2.start();}
}

15. 实现一个简单的生产者-消费者问题。生产者向队列中添加元素,消费者从队列中取出元素。

import java.util.LinkedList;
import java.util.Queue;class ProducerConsumer {private Queue<Integer> queue = new LinkedList<>();private int capacity = 10;public void produce() throws InterruptedException {int value = 0;while (true) {synchronized (this) {while (queue.size() == capacity) {wait();}queue.add(value++);System.out.println("Produced: " + value);notify();Thread.sleep(1000);}}}public void consume() throws InterruptedException {while (true) {synchronized (this) {while (queue.isEmpty()) {wait();}int value = queue.poll();System.out.println("Consumed: " + value);notify();Thread.sleep(1000);}}}}public class Main {public static void main(String[] args) {ProducerConsumer pc = new ProducerConsumer();Thread t1 = new Thread(() -> {try {pc.produce();} catch (InterruptedException e) {e.printStackTrace();}});Thread t2 = new Thread(() -> {try {pc.consume();} catch (InterruptedException e) {e.printStackTrace();}});t1.start();t2.start();}
}

16. 创建一个定长线程池,包含3个线程。提交5个任务到线程池,每个任务打印当前线程的名称。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;class MyTask extends Thread {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "正在执行");}
}public class Main {public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(3);for (int i = 1; i <= 300; i++) {executorService.execute(new MyTask());}executorService.shutdown();}
}

17. 创建一个定时线程池,安排一个任务在3秒后执行,任务打印当前时间。

import java.time.LocalDateTime;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;class MyTask extends Thread {@Overridepublic void run() {System.out.println(LocalDateTime.now());}
}public class Main {public static void main(String[] args) {ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);scheduledExecutorService.schedule(new MyTask(), 3, TimeUnit.SECONDS);scheduledExecutorService.shutdown();}
}

18. 创建一个可缓存线程池,提交10个任务,每个任务打印当前线程的名称,并等待所有任务执行完毕。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;class CacheTask implements Runnable {public void run() {System.out.println("执行缓存线程池任务:" + Thread.currentThread().getName());}
}public class Main {public static void main(String[] args) {ExecutorService executor = Executors.newCachedThreadPool();for (int i = 0; i < 10; i++) {executor.execute(new CacheTask());}executor.shutdown();}
}

19. 创建一个单线程化线程池,提交5个任务,每个任务打印当前线程的名称。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;class SingleTask implements Runnable {public void run() {System.out.println("执行单线程任务:" + Thread.currentThread().getName());}
}public class Main {public static void main(String[] args) {ExecutorService executor = Executors.newSingleThreadExecutor();for (int i = 0; i < 5; i++) {executor.execute(new SingleTask());}executor.shutdown();}
}

20. 创建一个自定义线程池,核心线程数为2,最大线程数为5,线程空闲时间为1分钟,工作队列容量为5。提交10个任务到线程池,每个任务打印当前线程的名称。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;class Task implements Runnable {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "正在执行");}
}public class Main {public static void main(String[] args) {ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,        					//核心线程数量5,                      	//最大线程数量1,                      	//线程空闲时间TimeUnit.MINUTES,    		// 时间单位new ArrayBlockingQueue<>(5) //队列);for (int i = 1; i <= 10; i++) {threadPoolExecutor.execute(new Task());}threadPoolExecutor.shutdown();}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/3248476.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

Go 语言 UUID 库 google/uuid 源码解析:UUID version7 的实现

google/uuid 库地址 建议阅读内容 在阅读此篇文章之前&#xff0c;建议先了解 UUIDv1 的构成、UUIDv4 的 API 以及掌握位运算。 了解 UUIDv1 的构成可以参考Go 语言 UUID 库 google/uuid 源码解析&#xff1a;UUID version1 的实现 或 RFC 9562。 了解 UUIDv4 的 API 可以看…

husky 和 lint-staged 构建代码项目规范

目录 前言 最简单的方法 过 scripts 来解决如果检测工具多&#xff0c;需要多次处理 通过 husky(哈士奇)来解决容易遗忘的问题 1. 安装 2. husky init 3. 试一试​ lint-stadge 只 lint 改动的 1. 安装 2. 修改 package.json 配置 3. 添加 npm 脚本: 4.使用 Husky…

成为git砖家(1): author 和 committer 的区别

大家好&#xff0c;我是白鱼。一直对 git author 和 committer 不太了解&#xff0c; 今天通过 cherry-pick 的例子搞清楚了区别。 原理 例如我克隆了著名开源项目 spdlog 的源码&#xff0c; 根据某个历史 commit A 创建了分支&#xff0c; 然后 cherry-pick 了这个 commit …

卡片式组件封装demo

效果视频&#xff1a; 卡片组件 样式还得细调~&#xff0c;时间有限&#xff0c;主要记录一下逻辑。 html结构&#xff1a; 目录 父组件数据处理数据格式 父组件的全部代码 子组件数据处理props参数 样式部分三个圆点点击三圆点在对应位置显示查看弹框点击非内容部分隐藏查看…

第四章 自定义序列类

目录 5.1 序列类型的分类 容器序列 扁平序列 可变序列 不可变序列 5.2 序列的abc继承关系 5.3 序列的、和extend的区别 操作符 操作符 extend方法 5.4 实现可切片的对象 5.5 bisect管理可排序序列 深入解释 5.6 什么时候我们不该用列表 深入解释 5.7 列表推导式…

第十章 多线程、多进程和线程池编程

目录 11.1 多线程编程 什么是多线程&#xff1f; 创建和启动线程 线程同步 11.2 多进程编程 什么是多进程&#xff1f; 创建和启动进程 进程间通信 11.3 线程池和进程池 什么是线程池和进程池&#xff1f; 使用线程池 使用进程池 11.4 选择多线程还是多进程 适用…

vue3 vxe-grid修改currentPage,查询数据的时候,从第一页开始查询

1、当我们设置好VxeGrid.Options进行数据查询的时候,下面是可能的设置&#xff1a; const gridOptions reactive<BasicTableProps>({id: UserTable,showHeaderOverflow: false,showOverflow: true,keepSource: true,columns: userColumns,size: small,pagerConfig: {cur…

【常见开源库的二次开发】基于openssl的加密与解密——单向散列函数(四)

目录&#xff1a; 目录&#xff1a; 一、什么是单项散列函数&#xff1f; 1.1 如何验证文件是否被修改过 1.2 单项散列函数&#xff1a; 二、单向hash抗碰撞 2.1 弱抗碰撞&#xff08;Weak Collision Resistance&#xff09; 2.2 强抗碰撞&#xff08;Strong Collision Resista…

[GXYCTF2019]Ping Ping Ping1

打开靶机 结合题目名称&#xff0c;考虑是命令注入&#xff0c;试试ls 结果应该就在flag.php。尝试构造命令注入载荷。 cat flag.php 可以看到过滤了空格,用 $IFS$1替换空格 还过滤了flag&#xff0c;我们用字符拼接的方式看能否绕过,ag;cat$IFS$1fla$a.php。注意这里用分号间隔…

【光伏发电功率预测】方法综述学习笔记2《光伏发电功率超短期预测方法综述》

本文参考《光伏发电功率超短期预测方法综述》&#xff1a;https://d.wanfangdata.com.cn/periodical/ChlQZXJpb2RpY2FsQ0hJTmV3UzIwMjQwNzA0Eg5nZHlqczIwMjMwNzAyNBoIeHp4NW40YmU%3D 文章目录 摘要&#xff1a;引言1. 光伏发电功率的影响因素分析1.1传递过程中的影响因素1.1.1…

DDei在线设计器-数据格式说明

数据格式说明 DDei的所有设计数据都以文件为单位保存在一个JSON对象中。JSON对象包含了全量的页签、舞台、图层、控件的位置以及属性信息。开发人员可以存储这个JSON到服务端数据库中&#xff0c;从而轻易的实现保存功能&#xff1b;也解析这个JSON&#xff0c;将其转换成自己业…

对红酒品质进行数据分析(python)

http://t.csdnimg.cn/UWg2S 数据来源于这篇博客&#xff0c;直接下载好csv文件。 这篇内容均在VScode的jupyter notebook上完成&#xff0c;操作可以看我的另一篇博客&#xff1a;http://t.csdnimg.cn/69sDJ 一、准备工作 1. 导入数据库 #功能是可以内嵌绘图&#xff0c;并…

用了6年git,不知道cherry-pick是啥意思

背景 可能是测试开发角色原因&#xff0c;平时很少有代码冲突或多人协同的编码场景。今天有个协同项目&#xff0c;需要提交自己的代码到其它业务的代码库中&#xff0c;这个代码库是分支开发分支上线模式&#xff0c;同时会有多个同事提交代码&#xff0c;然后模块负责的同学…

常用优秀内网穿透工具(实测详细版)

文章目录 1、前言2、安装Nginx3、配置Nginx4、启动Nginx服务4.1、配置登录页面 5、内网穿透5.1、cpolar5.1.1、cpolar软件安装5.1.2、cpolar穿透 5.2、Ngrok5.2.1、Ngrok安装5.2.2、随机域名5.2.3、固定域名5.2.4、前后端服务端口 5.3、NatApp5.4、Frp5.4.1、下载Frp5.4.2、暴露…

【数学建模】——【线性规划】及其在资源优化中的应用

目录 线性规划问题的两类主要应用&#xff1a; 线性规划的数学模型的三要素&#xff1a; 线性规划的一般步骤&#xff1a; 例1&#xff1a; 人数选择 例2 &#xff1a;任务分配问题 例3: 饮食问题 线性规划模型 线性规划的模型一般可表示为 线性规划的模型标准型&…

vue2.0结合使用 el-scrollbar 和 v-for实现一个横向滚动的元素列表,并且能够自动滚动到指定元素(开箱即用)

效果图&#xff1a; 代码&#xff1a; <div class"gas-mode-item-body"><el-scrollbar style"width: 300px;height: 100%;" wrap-style"overflow-y:hidden" ref"scrollbarRef"><div style"display: flex&quo…

Python Linux环境(Centos8)安装minicoda3+jupyterlab

文章目录 安装miniconda安装python环境启动 最近服务器检查&#xff0c;我下面的服务器有漏洞&#xff0c;不得已重装了&#xff0c;正好记录下怎么从零到python写代码。 安装miniconda miniconda是anconda的精简版&#xff0c;就是管理python环境的得力助手。 # 创建一个名…

7.18 学习笔记 解决分页越界问题 及分页查询

1.解决分页越界 1.1出现的问题 于是我索性把分页去掉想是不是就可以了&#xff0c;结果发现还不行 1.2解决方法 就当我找了一两个小时抓耳挠腮时&#xff0c;万幸在csdn上找到了相关的帖子&#xff0c;在此感谢一下那位大佬。 原因是我的实体类中没有构造方法&#xff0c;那…

软考系规百天备考攻略:基础阶段的三轮强化

早在今年4-5月份的时候&#xff0c;我就曾经讲过系统规划与管理师的备考建议&#xff0c;也就是先从教程学起&#xff0c;先读教程&#xff0c;而且我也说过&#xff0c;不要迷信任何培训班或者培训视频&#xff0c;任何培训班或者培训视频都不能取代你认真读至少一遍教程&…

BIOMOD2 物种分布模拟教程

原文链接&#xff1a;BIOMOD2 物种分布模拟教程https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247609373&idx5&sn492e7597314a5f9e358c35e4780b275f&chksmfa826dfacdf5e4ecf8ac06bdeba5469b31650bdbefbc8fb88b79c0f332714c453a4cc058d29f&token155…