进程、线程和多线程
在说进程之前,先回顾一下什么是程序。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的过程。
进程:执行程序的一次过程,是动态的概念。是系统资源分配的单位。
线程:一个进程中可以包括多个线程(一个进程中最少包含一个线程)。线程是 CPU 执行和调度的单位。
假设我们正在观看一部电影,电影的播放就可以看做是一个进程。电影播放包括声音、图像、字幕等,这些可以看做是线程,如图所示
多线程:多线程分两种情况,多核和单核。
-
多核:即有多个 CPU,此时的多线程是真正的多线程,不同的线程由不同的 CPU 执行,「并行」
-
单核:只有一个 CPU,此时的多线程是模拟出来的,CPU 在线程间快速切换,进而产生了同时进行的错觉。「并发」
创建线程的方式
创建线程主要有三种方式:
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口
继承 Thread 类
继承 Thread 类实现多线程包括以下三步:
- 自定义线程类继承 Thread 类
- 重写 run() 方法,run() 方法内是线程执行体
- 创建线程对象,调用对象的 start() 方法启动线程
public class TestThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("子线程第"+i+"次执行");
}
}
public static void main(String[] args) {
TestThread testThread = new TestThread(); // 创建一个线程对象
testThread.start(); // 调用 start() 方法
for (int i = 0; i < 20; i++) {
System.out.println("主线程第"+i+"次执行");
}
}
}
代码中,子线程和主线程分别打印 20 条输出语句。在 main 方法中,先执行子线程的 start() 方法,再执行主线程的输出语句。会发现主线程和子线程的输出语句是交替输出的,而不是顺序输出的。
注意:(1)线程开启不一定立即执行。(2)输出的结果取决于 CPU 的调度。
实例:多线程下载图片
首先,在 Commons IO – Download Apache Commons IO 中下载 commons-io jar包,该 jar 包可以用来帮忙开发 IO 功能。
解压后,复制文件加下的 commons-io-2.11.0.jar 包。
在 src 文件夹下新建一个 lib 文件夹,将刚刚复制的 jar 包粘贴进去。
在 jar 包上右键,选择 Add as Library,点击 ok 保存。
创建文件 DownLoadImage.class 文件,填入以下代码
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
// 多线程下载图片
public class DownLoadImage extends Thread {
private String url; // 网络图片地址
private String name; // 文件名
public DownLoadImage(String url, String path) {
this.url = url;
this.name = path;
}
@Override
public void run() {
// 下载执行体
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url, name);
System.out.println(name + "下载完成");
}
public static void main(String[] args) {
String url = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimgo.11773.com%2Fimg2022%2F5%2F18%2F16%2F2022051834530721.jpg&refer=http%3A%2F%2Fimgo.11773.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1655650763&t=09cbd0abf9db64d6c6bbba58587a596b";
DownLoadImage downLoadImage1 = new DownLoadImage(url, "大乔1.jpg");
DownLoadImage downLoadImage2 = new DownLoadImage(url, "大乔2.jpg");
DownLoadImage downLoadImage3 = new DownLoadImage(url, "大乔3.jpg");
downLoadImage1.start();
downLoadImage2.start();
downLoadImage3.start();
}
}
class WebDownloader {
public void downloader(String url, String path) {
try {
FileUtils.copyURLToFile(new URL(url), new File(path)); // 将 url 中内容拷贝到文件中
} catch (IOException e) {
e.printStackTrace();
System.out.println("downloader 方法 IO 异常");
}
}
}
代码中,定义了一个类 WebDownloader,其中的 downloader 方法用于将 url 中内容拷贝到指定文件中。在 main 方法中使用多线程,下载三张图片到不同文件下。
我们按顺序调用三个线程,三个线程分别下载图片到大乔1,大乔2,大乔3。但是最终打印的顺序,并不一定是
大乔1 下载成功,大乔2下载成功,大乔3 下载成功!
因为(1)线程开启不一定立即执行。(2)线程的调度顺序取决于 CPU
实现 Runnable 接口
实现 Runnable 接口方式包括以下三步:
- 在类中实现 Runnable 接口
- 实现 run 方法,编写线程执行体
- 创建线程对象,调用 start() 方法启动线程
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("第"+i+"次运行子线程");
}
}
public static void main(String[] args) {
// 创建 Runnable 接口的实现类
MyRunnable myRunnable = new MyRunnable();
// 通过 线程对象来开启线程 代理
new Thread(myRunnable).start();
for (int i = 0; i < 20; i++) {
System.out.println("第"+i+"次运行主线程");
}
}
}
建议使用 Runnable 方式,因为 Java 仅支持单继承
并发问题
并发的一个经典问题是购票系统,多个线程操作同一个对象。
public class BuyTickets implements Runnable{
private int ticketNumbers = 10;
@Override
public void run() {
while (true){
// 当票有存余时,即可购买
if(ticketNumbers<=0) break;
try {
Thread.sleep(200) ; // 线程休眠 200 ms ,避免操作过快,使得一个线程一次性买光所有票
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"购买了第"+ticketNumbers--+"张票"); // 买票后,执行自减操作
}
}
public static void main(String[] args) {
BuyTickets buyTickets = new BuyTickets();
new Thread(buyTickets,"购票人1").start(); // 参数 1 是线程对象,参数 2 是线程名
new Thread(buyTickets,"购票人2").start();
new Thread(buyTickets,"购票人3").start();
}
}
代码中,我们使用三个线程来模拟购票过程。代码执行结果如下:
我们发现,第一张票被同时被购票人 1 和购票人 3 购买,出现了数据紊乱,这个问题就是并发问题。
并发问题:当多个线程操作同一资源时,会出现线程不安全。
实现 Callable 接口
实现 Callable 接口包括以下步骤:
- 实现 Callable 接口,需要返回值类型
- 重写 call 方法
- 创建线程对象
- 创建执行服务:
ExecutorService service = Executors.newFixedThreadPool(size);
size 参数为需要创建线程池的大小。 - 提交执行:
Future<Boolean> submit = service.submit(thread);
submit() 中参数为需要执行的线程 - 获取结果:
boolean res = result1.get()
- 关闭服务:
service.shutdownNow;
关闭服务
import java.util.concurrent.*;
public class MyCallable implements Callable<Boolean> {
// 1.实现 Callable 方法,返回类型为 Boolean
@Override
public Boolean call() throws Exception {
// 2. 重写 call 方法
for (int i = 0; i < 20; i++) {
System.out.println("子线程执行第"+i+"次");
}
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable(); // 3. 创建线程对象
ExecutorService service = Executors.newFixedThreadPool(3); // 4. 创建执行服务,线程池大小为 3
Future<Boolean> submit = service.submit(myCallable); // 5. 提交执行 myCallable 线程
boolean res = submit.get(); // 6. 获取执行结果,需要抛出异常
service.shutdownNow(); // 关闭服务
}
}
lambda 表达式
Lambda 表达式用于简化代码,避免匿名内部类的过多定义。
函数式接口
定义:如果接口中只包含一个抽象方法,那么它就是一个函数式接口
优化:对于函数式接口,可以使用 lambda 表达式来创建该接口的对象。
当我们调用函数式接口中的方法时,可以用 lambda 表达式简化。
格式:lambda 表达式的格式很简单,object = (params)->{函数体}
,()
中填写所需要的参数,{}
内填写函数体。
public class TestLambda {
public static void main(String[] args) {
Animal animal = null; // 创建一个接口对象
animal = (String food) -> {
System.out.println("动物正在吃" + food);};
animal = (String food) -> System.out.println("动物正在吃" + food); // 因为执行体只有一行代码,可以省去花括号
animal = (food) -> System.out.println("动物正在吃" + food); // 省略数据类型
animal = food -> System.out.println("动物正在吃" + food); // 单个参数可省略括号
}
}
// 定义一个函数式接口
interface Animal{
void eating(String food);
}
静态代理
代理是指以他人的名义,在授权范围内执行权利的行为。简单来说就是以你的名义去做事。
例如,你想发表一篇专利,但你并不知道发表专利的流程。这时,你需要找专业的专利代理人以你的名义去发表专利,你只需告诉代理人你的创新点和实施例。
public class StaticProxy {
public static void main(String[] args) {
Agent agent = new Agent(new Subject());
agent.publish();
}
}
interface publishPatent{
void publish(); // 发表专利
}
class Subject implements publishPatent{
// 真实角色,发表专利的你
@Override
public void publish() {
System.out.println("发表专利");
}
}
class Agent implements publishPatent{
// 代理人
private publishPatent target;
public Agent(publishPatent target) {
this.target = target;
}
@Override
public void publish() {
System.out.println("proxy: 准备相关材料并润色");
this.target.publish();
System.out.println("proxy: 收取尾款");
}
}
代码中,代理类 Agent
和实体类 Subject
都实现了 PublishPatent
接口。Agent 类中的 target
指向实体对象。目标对象只实现了发专利这件事,其他细枝末节的东西由代理对象实现,这一点很像 Python 中的装饰器。
总结: (1) 真实对象和代理对象都要实现同一个接口 (2)代理对象要代理真实角色
代理的好处:(1)代理对象可以做很多真实对象做不了的事情 (2)真实对象可以专注需要自己做的事情
回顾一下,实现runnable
接口的方式启动线程的方法:new Thread(mythread).start();
可以发现其实这就是代理模式!
线程的状态
线程的状态一共有五种,创建、就绪、阻塞、运行和死亡。它们之间的关系如下图:
线程中的方法
方法 | 说明 |
---|---|
setPriority(int newPrioruty) |
更改线程的优先级 |
static void sleep(long mills) |
在指定的毫秒数内让当前正在运行的线程休眠 |
void join() |
等待该线程终止 |
static void yield() |
暂停当前正在执行的线程对象,并执行其他线程 |
boolean isAlive() |
判断线程是否处于活动状态 |
线程的停止
线程的停止建议使用标志位的方式来实现,例如,当 flag = false
时终止线程。主动停止线程并不一定安全,JDK 提供的 stop()
,destory()
方法已经被废弃。
public class TestStop implements Runnable {
private boolean flag = true; // 执行体使用该标志位
@Override
public void run() {
while (flag) {
System.out.println("线程正在运行");
}
}
public void stop() {
// 修改标志位
flag = false;
}
public static void main(String[] args) {
TestStop testStop = new TestStop();
new Thread(testStop).start();
for (int i = 0; i < 100000; i++) {
if (i == 52000) {
testStop.stop();// 通过修改标志位停止线程
}
}
}
}
线程的休眠
-
线程休眠使用
sleep(time)
方法,time 表示当前线程阻塞的毫秒数 -
sleep()
时间到达后,线程进入就绪状态 -
sleep()
可以模拟网络延时,倒计时 -
每个对象都有一把锁,
sleep()
不会释放锁
线程礼让
yield()
,让当前正在执行的线程暂停,但不阻塞- 将线程从运行状态转为就绪状态
- 让 CPU 重新调度,但礼让不一定成功!例如,A 礼让了,但 CPU 仍然可能调用 A 线程。
Join
join
合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞- 可以想象成,一群人在医院看病,突然某领导的亲戚来了,那么该亲戚直接插到最前面,等他看完了其他人才能看。
public class TestJoin implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("Vip 来咯,已看病" + i + "秒");
}
}
public static void main(String[] args) throws InterruptedException {
TestJoin testJoin = new TestJoin();
Thread vip = new Thread(testJoin);
vip.start();
for (int i = 0; i < 1000; i++) {
if (i == 100) vip.join();
System.out.println("普通人已看病" + i + "秒");
}
}
}
Thread.State
Java 提供了 Thread.State 来观测线程当前的状态
状态 | 说明 |
---|---|
NEW | 创建状态 |
RUNNABLE | 运行状态 |
BLOCKED | 阻塞状态 |
WAITING | 等待另一个线程执行特定动作时的状态 |
TIMED_WAITING | 等待另一个线程执行动作到达指定等待时间的状态 |
TERMINATED | 死亡状态 |
public class TestState {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
// 创建一个线程,使用 lambda 表达式
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程执行第"+i+"次");
}
});
Thread.State state = thread.getState();
System.out.println(state);
thread.start();
state = thread.getState();
System.out.println(state);
while (state != Thread.State.TERMINATED){
// 当线程没有死亡,持续运行
state = thread.getState();
System.out.println(state);
}
thread.start(); // 这里会报错,因为死亡的线程无法重新启动
}
}
需要注意的是:死亡后的线程无法重新启动。线程有点像一块干电池,用完了就无了。
线程优先级
Java 提供一个线程调度器来检测程序中所有已就绪线程的状态,线程调度器按照优先级来决定优先调度哪个线程
线程的优先级用数字 1~10
来表示,其中
-
Thread.MIN_PRIORITY = 1;
-
Thread.MAX_PRIORITY = 10;
-
Thread.NORM_PRIORITY = 5
-
thread.getPriority()
用于获取优先级,thread.setPriority(priority)
用于设置优先级
注意:并不是优先级高的就一定会执行,只是优先级更高的有更高的概率被执行。
// 测试线程优先级
public class TestPriority implements Runnable{
public static void printPriority(){
System.out.println(Thread.currentThread().getName()+"的优先级是"+Thread.currentThread().getPriority());
}
@Override
public void run() {
printPriority();
}
public static void main(String[] args) {
printPriority();
TestPriority testPriority = new TestPriority();
Thread t1 = new Thread(testPriority,"t1");
t1.start();
Thread t2 = new Thread(testPriority,"t2");
t2.setPriority(Thread.MIN_PRIORITY);
t2.start();
Thread t3 = new Thread(testPriority,"t3");
t3.setPriority(Thread.MAX_PRIORITY);
t3.start();
Thread t4 = new Thread(testPriority,"t4");
t4.setPriority(-1); // 这里会抛出异常,优先级范围是 1~10
t4.start();
Thread t5 = new Thread(testPriority,"t5");
t5.setPriority(11);
t5.start(); // 这里会抛出异常,优先级范围是 1~10
}
}
守护线程(daemon)
线程可以分为 用户线程和守护线程。
在 Java 虚拟机中,虚拟机必须确保用户线程执行完毕,但不用等待守护线程执行完毕。守护线程可用于记录操作日志,监控内存,垃圾回收等。
可以这样理解,守护线程是一个爱你的天使,他是永生的。他的职责就是保护你,当你(用户线程)不在了,他失去了存在的价值,也就消失了。
import org.junit.experimental.theories.Theories;
public class TestDaemon {
public static void main(String[] args) {
Thread angel = new Thread(new Angel());
angel.setDaemon(true); // 天使是守护线程
angel.start();
new Thread(new You()).start();
}
}
class Angel implements Runnable{
@Override
public void run() {
while (true) System.out.println("天使一直守护着你");
}
}
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
System.out.println("你在爱里开心的活着");
}
System.out.println("你松开了天使的手");
}
}
代码中,当 You 线程死亡后,Angel 线程还运行了一段时间才消失,因为 JVM 的停止需要一段时间。
线程同步
当多个线程操作同一个对象时,可能出现线程安全问题。
例子:假设,山上有一个厕所,有很多人想要方便。这个厕所可以看作需要操作的对象,多个人可以看作是多个线程。如果多个人去抢这个厕所,就会打架。为了避免打架,你们想了一个办法——排队。谁排在前谁先上厕所,这样最公平。但是还存在一个问题,上厕所的时候需要一个安静的环境,没有人希望方便到一半的时候,另一个人闯进来要方便。所以,在厕所上加了一把锁,方便的时候上锁,方便完解开锁。
从上面的例子中,我们可以看出解决线程安全问题需要队列和锁。
ArrayLIst<> 的线程不安全
我们都知道,ArrayList<> 是线程不安全的,但是具体为什么不安全呢?看下面的代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class TestArrayList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
System.out.println(list.size());
}
}
我们定义了一个 String
类型的 ArrayList
,创建了10000
个线程往里加入该线程的名字。执行完毕后,打印列表的长度。我们发现最终的结果不是我们期望的 10000
。因为出现了线程不安全问题,多个线程往同一个位置写填入了数据,导致了数据的覆盖。
synchronized 关键字
synchronized
关键字可以放在方法或代码块前,用于对声明的部分加上同步锁。在操作对象前加锁,操作结束后释放锁。用 synchronized
解决 ArrayList<>
中的线程不安全问题
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class TestArrayList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
System.out.println(list.size());
}
}
注意:synchronized
关键字一定要加到修改的变量上,才能生效。
死锁
死锁指的是多个线程各自占有一些共享资源,并且需要互相等待其他线程释放已占有的资源才能运行,从而导致两个或多个线程都在等待对方释放资源而停止执行的现象。
案例(黑货交易)
一个贴切的例子是黑货交易,卖家要先付钱在交货,买家要先给货后给钱,这样双方就陷入了僵持。代码如下
public class TestDeadLock {
public static void main(String[] args) {
Trade seller = new Trade("卖家", 0);
Trade buyer = new Trade("买家", 1);
seller.start();
buyer.start();
}
}
class Money {
// 金钱类
}
class Goods {
// 商品类
}
class Trade extends Thread {
private static Money money = new Money();
private static Goods goods = new Goods();
String name; // 交易方的名字
int method; // 当 method = 0 表示先钱后货,method = 1 表示先货后钱
public Trade(String name, int method) {
this.name = name;
this.method = method;
}
@Override
public void run() {
if (method == 0) {
synchronized (money) {
System.out.println(name + "需要先给钱");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (goods) {
System.out.println(name + "后给货");
}
}
} else {
synchronized (goods) {
System.out.println(name + "需要先给货");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (money) {
System.out.println(name + "后给钱");
}
}
}
}
}
死锁产生的必要条件
- 互斥:一个资源每次只能被一个线程使用
- 请求与保持条件:一个线程因为请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已经获取的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
只要破坏上面任意一个条件,死锁就能解除。
Lock()
从 JDK 5.0 开始,Java 提供了更强大的线程同步机制,即通过显示定义同步锁来实现同步。最常用的一种方式是 ReentrantLock 类实现的 Lock,ReentrantLock 译作可重入锁。
import java.util.concurrent.locks.ReentrantLock;
class BuyTickets implements Runnable {
private int ticketNumbers = 10;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 当票有存余时,即可购买
try {
lock.lock();
if (ticketNumbers > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNumbers-- + "张票");
}else{
break;
}
}finally {
lock.unlock();
}
}
}
}
public class Test{
public static void main(String[] args) {
BuyTickets buyTickets = new BuyTickets();
new Thread(buyTickets, "购票人1").start(); // 参数 1 是线程对象,参数 2 是线程名
new Thread(buyTickets, "购票人2").start();
new Thread(buyTickets, "购票人3").start();
}
}
注意:一般在finallY{}
代码块中释放锁。
synchronized 与 Lock 的对比
-
Lock 是显示锁,synchronized 是隐式锁。显示锁需要手动开启和关闭,隐式锁出了作用域自动释放
-
Lock 只有代码块锁,没有方法锁
-
Lock 锁的性能开销更小
线程协作
生产者消费者问题
假设我们有一个仓库,仓库中只能存放一件产品,生产者负责将生产的产品放入仓库,消费者负责将仓库中的产品拿走;如果仓库中没有产品,生产者就将产品放入仓库,否则停止生产并等待,直到消费者将产品拿走;如果仓库中有产品,消费者就将产品拿走并消费,否则等待,直到仓库中有产品。
为了解决这个问题,线程间需要相互通信。
Java 提供的线程间通信方法
方法名 | 作用 |
---|---|
wait() | 该线程一直等待,直到被通知;与 sleep()不同,它会释放锁 |
wait(long timeout) | 等待 timeout 毫秒 |
notify() | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒同一个对象上所有调用 wait() 方法的线程,优先级高的线程优先调度 |
注意:这些都是 Object 类中的方法,都只能在同步方法或者同步代码块中使用,否则会报错。
解决方式1 管程法
所谓管程法,就是使用缓冲区的方式来解决生产者消费者问题。生产者将产品放入缓冲区,消费者从缓冲区取出产品。如果缓冲区没满,生产者就生产产品放入缓冲区;如果缓冲区满,就等待。如果缓冲区为空,消费者就等待;否则消费者就消费缓冲区中的产品。
import java.util.List;
public class UseBuffer {
public static void main(String[] args) {
Buffer buffer = new Buffer();
Thread provider = new Thread(new Providers(buffer));
Thread consumer = new Thread(new Consumer(buffer));
provider.start();
consumer.start();
}
}
// 生产者和消费者保存同一个缓冲区
class Providers implements Runnable {
Buffer buffer;
public Providers(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
buffer.push(new Product(i));
System.out.println("生产了" + (i + 1) + "件产品");
}
}
}
class Consumer implements Runnable {
Buffer buffer;
public Consumer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
Product product = buffer.remove();
System.out.println("消费了" + (i + 1) + "件产品");
}
}
}
class Product {
int id; // 产品 id
public Product(int id) {
this.id = id;
}
}
class Buffer {
Product[] products = new Product[10]; // 此处为缓冲区,缓冲区大小为 10
int count = 0;// 记录容器中已放入产品的个数
// 生产者放入产品
public synchronized void push(Product product) {
// 如果缓冲区没有满放入产品,否则等待
if (count == products.length) {
try {
this.wait(); // 缓冲区满,生产者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
products[count] = product;
count++;
this.notifyAll(); // 通知消费者消费
}
public synchronized Product remove() {
if (count == 0) {
try {
this.wait(); // 缓冲区为空,消费者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
this.notifyAll(); // 通知生产者生产
return products[count];
}
}
解决方式2 信号灯法
信号灯法实际上就是通过标志位来通知生产或消费。
// 信号灯法
public class TestSignal {
public static void main(String[] args) {
Product product = new Product();
Thread provider = new Thread(new Provider(product));
Thread consumer = new Thread(new Consumer(product));
provider.start();
consumer.start();
}
}
class Provider implements Runnable {
Product product;
public Provider(Product product) {
this.product = product;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
product.produce(i+1);
}
}
}
class Consumer implements Runnable {
Product product;
public Consumer(Product product) {
this.product = product;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
product.consume(i+1);
}
}
}
class Product {
boolean flag; // flag 为 true 表示有产品,消费者消费,生产者等待; false 无产品 生产者生产,消费者等待
public synchronized void produce(int no) {
if (flag) {
// flag 为 true 阻塞生产者
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产者生产了产品"+no);
this.notifyAll();
this.flag = !this.flag;
}
public synchronized void consume(int no){
if (!flag) {
// flag 为 false 阻塞消费者
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费者消费了产品"+no);
this.notifyAll();
this.flag = !this.flag;
}
}
线程池
背景:前面提到过,死亡的线程无法重新运行。如果需要执行相同的任务,需要重新创建一个线程。频繁的创建和销毁线程,对性能的影响很大。
解决方案:提前创建好多个线程,放在一个池子里,需要使用的时候从池子里取,使用完了放回池子。这个池子就是线程池
好处
- 提高响应速度(减少了线程创建的时间)
- 降低资源消耗,复用线程池中的线程
- 便于线程管理
corePoolSize
核心池的大小maximunPoolSize
最大线程数keepAliveTime
线程没有任务时最多保持多长时间后会终止
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestPool {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);// 池子的大小
for (int i = 0; i <20 ; i++) {
service.execute(new Thread(new MyThread()));
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
service.shutdownNow();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println("正在执行子线程"+Thread.currentThread().getName());
}
}
注意:service.shutdownNow() 运行在主线程,service 关闭后,可能还有线程没有放入到 service 中执行。删除掉 第11~15
行代码就能观察到此现象。