趁着最近课少,复习一下Java多线程相关知识,顺便发一下以前的笔记
什么是线程?
线程是指在一个进程中,执行的一个相对独立的、可调度的、可执行的代码片段。线程是操作系统能够运算调度的最小单位,它包含在进程之中,是进程中的实际运作单位,它独立地运行于进程中,并与同一进程内的其他线程共享进程的资源,如内存、文件描述符等。每个线程都有自己的栈、程序计数器和局部变量等,但它们共享进程的静态数据、堆内存和全局变量等。
PS:可以简单理解线程是线程中的一条执行路径(可以参考流程图)
线程的优缺点
线程可以提高程序的并行性,增加程序的处理能力;
线程创建和切换的开销比进程小,因此更加高效;
线程可以与同一进程内的其他线程共享数据和资源,这样可以避免进程间的数据复制和通信开销。
同一进程内的线程都共享进程的资源,因此需要进行线程间的同步和互斥,否则容易出现竞争条件和死锁等问题;
线程之间的通信和同步需要额外的开销和复杂度,因此需要仔细规划和设计线程间的通信和同步机制;
由于线程共享进程的地址空间,因此需要避免线程间的访问冲突,否则容易出现数据不一致的问题。
什么是进程?
进程是指在计算机中运行的程序和其相关执行状态的总和。更具体地说,进程包括程序代码、数据、内存中的栈、堆和共享库等资源。每个进程在执行时都有自己的地址空间、内存、堆和栈,以及相应的文件描述符、信号处理程序等。进程是操作系统中最基本的、最重要的资源之一。
PS:可以简单理解进程就是正在运行的程序
进程的特点:
什么是单线程和多线程?
多线程的应用场景:
……
多线程的主要作用是为了提高系统的性能,充分利用CPU
什么是并行与并发?
什么是生产者和消费者?
通过继承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类实现多线程主要有以下几步:
方式一:传统写法
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 + "次");
}
}
}
总结
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对象,获取线程任务的结果会是一致的,并且结果以第一个线程任务的结果为准
总结
API介绍:
方法名 | 说明 |
---|---|
public void run() |
在线程开启后,run()方法将被自动调用执行 |
public synchronized void start() |
开启线程 |
public final synchronized void setName(String name) |
为线程命名 |
public final String getName() |
获取线程名 |
public final void setPriority(int newPriority) |
设置线程的优先级 |
public final intgetPriority() |
获取线程的优先级 |
public final void setDaemon(boolean on) |
设置为守护线程 |
public final void join() |
插入线程/插队线程 |
public static native void yield(); |
出让线程/礼让线程 |
public static native Thread currentThread(); |
获取当前线程 |
public static native void sleep(long millis); |
让线程休眠指定时间(单位ms) |
public final void wait() |
当前线程等待,直到被其他线程唤醒 |
public final native void notify(); |
随机唤醒单个线程 |
public final native void notifyAll(); |
唤醒所有线程 |
public State getState() |
获取线程状态 |
备注:
;
结尾的是成员变量,而()
结尾的是方法守护线程的应用场景:当我们在进行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守护线程不会结束,因为此时系统中除了守护线程,还存在主线程,并不是只剩守护线程)
在需要多个线程协作、顺序执行的场景中,礼让线程是一种比较常用的线程协作方法,它可以让线程执行的顺序更加合理,提高系统的并发性能。
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只会被执行一次,这就是礼让线程的一个特性:
插入线程,让当前线程等待线程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 + "次");
}
}
}
Java里没有定义运行态,因为当线程运行后直接将当前线程交给了CPU,此时JVM就不需要管这个线程了,所以Java中线程实际的状态只有6个
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,静态的是当前类的字节码文件对象
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();
等待唤醒机制是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锁,最终的实际效果是和示例一一致的,只是打印语句会发生错乱,并不影响最终效果
需求:一共有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();
}
}
}
}
}
需求:有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个礼物,全部锁住就不会发生这样的事情。
需求:同时开启两个线程,共同获取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);
}
}
}
}
}
}
需求:抢红包也用到了多线程。
假设: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);
}
}
}
需求:有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为 {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());
}
}
}
}
}
需求:
在上一题基础上继续完成如下需求:
每次抽的过程中,不打印,抽完时一次性打印(随机)
在此次抽奖过程中,抽奖箱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());
}
}
}
}
}
需求:在上一题基础上继续完成如下需求,比较两个线程的最大值
线程类:
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());
}
}
什么是线程池?
线程池是一种多线程处理方式,它可以有效地管理和调度多个线程的执行。在使用线程池的情况下,可以避免因为创建大量线程而导致系统性能下降、内存消耗过大等问题。线程池中的线程都是已经创建好的线程对象,并保存在线程池中,每个线程可以执行多个任务,任务执行完毕后并不会立刻销毁线程,而是会保留在池中等待下次执行。
为什么需要线程池?
在多线程编程中,往往需要创建大量的线程来执行任务。但是,直接创建线程会导致以下问题:
系统资源浪费:对于一些线程生命周期很短的任务(比如执行完一段代码后就会结束的任务),频繁地创建、销毁线程会消耗大量的系统资源,并且增加了系统开销。
系统性能下降:当同时需要执行大量的任务时,不加限制地创建线程可能会导致系统性能下降、运行速度变慢,因为线程的创建和销毁开销非常大。
系统不稳定:在高并发情况下,线程过多时会导致系统崩溃、运行不稳定。
线程池的作用就是解决以上问题。它可以避免频繁地创建、销毁线程,可以提前准备好一定数量的线程,让线程复用,从而降低创建和销毁线程的开销,同时还可以严格地限制线程的数量和执行时间,实现对线程的调度和管理。
使用线程池的好处:
提高系统效率:通过线程的复用和调度,可以充分利用系统资源,提高系统效率。
提高程序响应速度:线程池中的线程可以随时响应任务,从而提高程序的响应速度。
避免系统由于线程过多而不稳定:由于可以控制线程的数量,线程池可以避免系统出现由于线程过多而导致的不稳定状态,提高系统的可靠性。
总而言之,线程池在多线程编程中是一种非常重要的工具,可以避免系统性能问题和不稳定问题,提高系统效率和可靠性。
如何创建线程池?
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() // 任务的拒绝策略
);
}
}
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
进行计算