目录
- 一、多线程的实现方式
- 二、多线程的常用的实现方法
- 三、守护线程、礼让线程和插队线程
- 四、Java中线程的生命周期
- 五、线程的安全问题
- 六、死锁
- 七、生产者与消费者模型(唤醒等待机制)
- 八、线程池
- 九、练习
- 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 {
@Override
public 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 {
@Override
public 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> {
@Override
public 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);
// 创建两个线程,每个线程都执行同一个FutureTask
Thread 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 {
@Override
public void run() {
// 获取当前线程的引用
Thread currentThread = Thread.currentThread();
// 输出当前线程的名称和优先级
System.out.println("当前线程: " + currentThread.getName() + ",优先级: " + currentThread.getPriority());
// 让线程休眠200ms
try {
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 {
@Override
public 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 {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName() + " 正在执行 (" + i + ")");
// 线程A礼让CPU
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " 执行完毕.");
}
}
// 实现Runnable接口的普通线程类
class NormalRunnable implements Runnable {
@Override
public 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) {
// 创建礼让线程A
Thread threadA = new Thread(new YieldingRunnable(), "礼让线程A");
// 创建普通线程B
Thread threadB = new Thread(new NormalRunnable(), "普通线程B");
// 启动线程A和线程B
threadA.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 {
@Override
public 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赋值给它
}
@Override
public 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; // 总共的票数
@Override
public 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类型
@Override
public 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;
@Override
public 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();
@Override
public 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然后锁定资源2
Thread t1 = new Thread(() -> {
synchronized (resource1) { // 线程1锁定资源1
System.out.println("线程1:锁定了资源1");
synchronized (resource2) { // 线程1锁定资源2
System.out.println("线程1:锁定了资源2");
} // 线程1释放资源2的锁
} // 线程1释放资源1的锁
});
// 线程2尝试先锁定资源2然后锁定资源1
Thread t2 = new Thread(() -> {
synchronized (resource2) { // 线程2锁定资源2
System.out.println("线程2:锁定了资源2");
synchronized (resource1) { // 线程2锁定资源1
System.out.println("线程2:锁定了资源1");
} // 线程2释放资源1的锁
} // 线程2释放资源2的锁
});
t1.start(); // 启动线程1
t2.start(); // 启动线程2
}
}
输出结果:
线程1:锁定了资源1
线程2:锁定了资源2
...
(由于死锁,该进程不结束)
七、生产者与消费者模型(唤醒等待机制)
1. 常用方法
(1)wait()
-
用途:
- 当一个线程执行到一个
synchronized
代码块内部时,如果某些条件尚未满足,它可以使用wait()
方法让当前线程暂停执行,并释放当前持有的锁,进入等待状态(WAITING
或TIMED_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;
}
@Override
public 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;
}
@Override
public 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的桌子
// 生产者和消费者共用desk
Thread 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)
- int corePoolSize(必需)
核心线程数,即在没有任务执行时仍然保持活跃的线程数。- int maximumPoolSize(必需)
线程池允许的最大线程数。当工作队列满了并且所有核心线程都在使用时,线程池可能会创建额外的线程,直到线程总数达到这个最大值。- long keepAliveTime(必需)
非核心线程的最大空闲时间。当线程池中的线程数量大于核心线程数时,这是非核心线程在终止前可以保持空闲状态的最长时间。- TimeUnit unit(必需)
时间单位。通常使用TimeUnit.SECONDS
、TimeUnit.MILLISECONDS
等来指定keepAliveTime
的单位。- BlockingQueue workQueue(必需)
用于存放等待执行的任务的队列。当核心线程都在忙碌时,新提交的任务将在这个队列中等待。可以选择不同类型的队列,如LinkedBlockingQueue
、ArrayBlockingQueue
或SynchronousQueue
,具体取决于任务的特性和系统需求。- ThreadFactory threadFactory(可选)
线程工厂,用于创建新线程。通过线程工厂,可以设置线程的名称、优先级、守护进程状态等属性。如果没有设置该参数,默认值使用Executors.defaultThreadFactory()
。- RejectedExecutionHandler handler(可选)
拒绝策略。当线程池无法执行新提交的任务时(例如,因为线程池关闭或达到其容量限制),这个处理器定义了如何处理这些任务。
如果没有设置该参数,默认值使用ThreadPoolExecutor.AbortPolicy
,它将抛出RejectedExecutionException
。可以提供自定义的处理器,比如ThreadPoolExecutor.CallerRunsPolicy
、ThreadPoolExecutor.DiscardPolicy
或ThreadPoolExecutor.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秒,工作队列容量为10
ThreadPoolExecutor 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 {
@Override
public void run() {
//打印奇数的线程
for (int i = 1; i <= 100; i += 2) {
System.out.println(Thread.currentThread().getName() + "正在打印" + i);
}
}
}
class Thread2 extends Thread {
@Override
public 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;
@Override
public 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;
@Override
public 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;
@Override
public 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 {
@Override
public 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 {
@Override
public void run() {
while (true) {
System.out.println("当前时间:" + LocalDateTime.now());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class NumberRunnable implements Runnable {
@Override
public 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 {
@Override
public 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 {
@Override
public void run() {
for (int i = 1; i <= 500; i++) {
Thread.yield();
System.out.println(1);
}
}
}
class CharRunnable implements Runnable {
@Override
public 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;
@Override
public 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 {
@Override
public 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;
}
@Override
public void run() {
bankAccount.deposit(100);
bankAccount.withdraw(50);
}
}
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount();
// 两个线程共享一个account
Thread 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;
}
@Override
public 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;
}
@Override
public 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 {
@Override
public 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;
}
@Override
public 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;
}
@Override
public 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;
}
@Override
public 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;
}
@Override
public 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 {
@Override
public 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 {
@Override
public 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 {
@Override
public 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();
}
}