1. 进程和线程
-
程序:一段静态的代码,是应用程序执行的蓝本
-
进程:指一种正在运行的程序,有自己的地址空间
- 动态性(正在运行的程序)
- 并发性(同时运行)
- 独立性(QQ 和 微信互不干扰)
-
并发和并行的区别
- 并行(parallel):多个 CPU 同时执行多个任务,宏观和微观来看,都是同时执行
- 并发(concurrency):一个 CPU 同时执行多个任务(采用时间片轮转,A执行一段时间,B再执行一段时间)
-
线程
- 进程内部的一个执行单元,它是程序中一个单一的顺序控制流程,又被称为轻量级进程(lightweight process)
- 如果在一个进程中,同时运行了多个线程,用来完成不同的工作,则被称为多线程
- 线程特点:
- 轻量级进程
独立调度的基本单位
- 共享进程资源
- 可并发执行
-
线程和进程的区别:
区别 | 进程 | 线程 |
---|---|---|
根本区别 | 作为资源分配的单位(例:申请30M内存) | 调度和执行的单位(从30M中划分资源) |
开销 | 大:每个进程有自己独立的代码和数据空间 | 小:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器 |
所处环境 | 在操作系统中能同时运行多个任务 | 在同一应用程序中有多个顺序流同时执行 |
分配内存 | 每个进程分配不同的内存区域 | 除了CPU,不会为线程分配资源,使用的都是进程的,线程组只能共享资源 |
包含关系 | 没有线程的进程可以被看作单线程的,如果一个进程内拥有多个线程,那么就是多个线程同时完成 | 线程是进程的一部分 |
2. 线程的定义方式
2.1 继承 Thread 类
- Thread 类常用方法:
- run():线程要执行的任务
- start():启动线程
- getName():获取线程名称
- getPriority():获取优先级
- Thread.currentThread():得到当前线程
- 启动 main 方法,自动创建 main 线程
创建一个线程类:(extends Thread
)
public class TreadDemo extends Thread{
/**
* 线程体:线程要执行的任务
*/
@Override
public void run() {
while (true) {
System.out.println("[A] 线程执行了" + this.getName() + " " + this.getPriority());
}
}
}
创建线程对象、启动线程:
// 创建线程对象
Tread treadDemo = new TreadDemo();
treadDemo.setName("设置线程名称"); // 设置线程的名称
treadDemo.setPriority(Thread.MAX_PRIORITY); // 设置线程的优先级
// 启动线程
treadDemo.start();
// treadDemo.run(); 此处为普通的方法调用,直接调用 run() 方法
2.2 实现 Runnable 接口
- 两种方式的比较:
- 继承 Thread 类:编程简单,但是单继承,无法继承其他类
- 实现 Runnable 接口:编程稍繁琐,可以再继承其他类,实现其他接口,
操作的为同一个任务
,便于多个线程共享同一个资源
定义一个线程类:(implements Runnable
)
public class RunnableDemo implements Runnable{
@Override
public void run() {
while (true) {
System.out.println("[A] 线程执行了" + Thread.currentThread().getName() + " "
+ Thread.currentThread().getPriority());
}
}
}
创建线程对象、启动线程
// 创建线程对象
Runnable runnable = new RunnableDemo();
Thread thread = new Thread(runnable); // 使用 runnable
// 启动线程
thread.setName("[A] 线程");
thread.setPriority(Thread.NORM_PRIORITY);
thread.start();
// 创建另一个线程对象
Thread thread1 = new Thread(runnable); // 使用同一个 runnable,操作的为同一个任务对象
thread1.setName("[A] 线程 2");
=====》 还可以直接使用匿名内部类的方式
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("[A] 线程执行了" + Thread.currentThread().getName() + " "
+ Thread.currentThread().getPriority());
}
}
};
// 创建一个线程
Thread thread = new Thread(runnable); // 使用 runnable
thread.setName("[A] 线程");
thread.setPriority(Thread.NORM_PRIORITY);
thread.start();
// 创建另一个线程
Thread thread1 = new Thread(runnable); // 使用同一个 runnable,操作的为同一个对象
thread1.setName("[A] 线程 2");
2.3 实现 Callable 接口
JDK 1.5后推出
- 可以有返回值,支持泛型的返回值
- 可以抛出检查异常
- 需要借助 FutureTask,获取返回结果等
定义一个线程类:(implements Callable
)
public class CallableDemo implements Callable<Integer> {
/**
* 有返回值,并且可以抛出异常
* @return
* @throws Exception
*/
@Override
public Integer call() throws Exception {
if (false) {
throw new Exception();
}
return new Random().nextInt(10);
}
}
创建线程对象、启动线程
// 创建线程
Callable<Integer> callable = new CallableDemo();
// 需要使用 FutureTask 进行操作
FutureTask<Integer> task = new FutureTask(callable);
// 最终放入的是 FutureTask 对象
Thread thread = new Thread(task);
// 启动线程
thread.start();
// 获取返回值
Integer i = task.get(); // 得不到返回值,就一直等待!!!!!!!!!!
task.get(3, TimeUnit.SECONDS); // 就等待 3 秒,获取不到就报错
System.out.println(i);
3. 线程的生命周期
- 新生状态:
- 用 new 创建一个线程对象后,该对象就进入了新生状态
- 在此时的线程,拥有自己的内存空间,通过
start()
方法进入就绪状态
- 就绪状态:
- 此时线程具备了运行条件,但还没分配到CPU
- 当系统选定一个等待执行的线程后,它就进入了执行状态,称为“CPU调度”
- 运行状态
- 执行自己的 run 方法中的代码,直到等待某资源而阻塞,或完成任务而死亡
- 如果在给定时间片内,没执行结束,就会被系统给换下来回到阻塞状态
- 阻塞状态
- 处于运行状态的线程,在某些情况下(执行了sleep方法、等待I/O设备等)将让出CPU 并暂停自己的运行,进入阻塞状态
- 只有当睡眠时间结束,或资源已获取到,才能进入就绪状态中等待,被系统选中后,从停止的位置继续运行
- 死亡状态
- 正常运行的线程,完成了所有的工作
- 线程被强制性的终止,如通过 stop 方法来终止一个线程【不推荐使用】
- 线程抛出未捕获的异常
4. 线程控制
可以对线程的生命周期进行干预
基础线程类:
public class MyThread extends Thread {
@Override
public void run() {
this.setPriority(6);
for (int i = 0; i < 100; i++) {
System.out.println("线程开始执行了:" + this.getName() + " " + this.getPriority());
}
}
}
4.1 join()
阻塞其他线程,等待该线程执行完之后,再执行其他线程
for (int i = 0; i < 100; i++) {
if (i == 50) {
Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.start();
// 阻塞主线程,当 自定义线程 执行完,再执行主线程,【在 start() 方法之后】
thread.join();
}
System.out.println("主线程:" + Thread.currentThread().getName());
}
4.2 sleep()
让出 CPU,自身进入阻塞状态
Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.start();
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(1000); // 主线程休眠 1 秒,进入【阻塞状态】,时间到后再次进入【就绪状态】
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程:" + Thread.currentThread().getName());
}
4.3 yield()
礼让 CPU,执行一会儿,然后退回到就绪状态,继续争抢CPU(可能会立即抢到,继续执行)
Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.start();
for (int i = 0; i < 100; i++) {
Thread.yield(); // 主线程进行礼让,执行一会儿后,让出CPU,进入【就绪状态】
System.out.println("主线程:" + Thread.currentThread().getName());
}
4.4 setDaemon()
守护线程(寄生线程),启动它的线程执行完了,那么它也要停止执行
Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.setDaemon(true); // 启动它的线程执行结束之后,它也停止执行,【在 start() 方法之前】
thread.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程:" + Thread.currentThread().getName());
}
4.5 interrupt()
并不是结束了线程,而是修改了线程的状态,需要线程类进行判断isInterrupted()
Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程:" + Thread.currentThread().getName());
}
thread.interrupt(); // 修改线程的状态,告诉它该结束运行了
线程类修改为:
public class MyThread extends Thread {
@Override
public void run() {
while (this.isInterrupted()) { // 进行判断,是否该结束了
System.out.println("线程开始执行了:" + this.getName() + " " + this.getPriority());
}
}
}
5. 线程同步
5.1 问题的提出
-
场景:
-
多个用户同时操作一个银行账户。每次取款 400 元,取款前先检查余额是否足够,如果不够,放弃取款
例如 2 个人同时取 400,卡内还有 600 元
-
-
分析:
- 使用多线程解决
- 开发一个取款线程类,每个用户对应一个线程对象
- 多个线程共享同一个银行账户,使用 Runnable 方式
-
思路
- 创建银行账户类 Account
- 创建取款线程 AccountRunnable
- 创建测试类,进行测试
Account 类:
public class Account {
private int balance = 600;
/**
* 取款
* @param money
*/
public void withDraw(int money) {
this.balance -= money;
}
/**
* 查看余额
* @return
*/
public int getBalance() {
return balance;
}
}
AccountRunnable 类:
public class AccountRunnable implements Runnable {
private Account account = new Account();
/**
* 取款的步骤
*/
@Override
public void run() {
// 在这个流程中,需要保证【有一个人进入了,另一个人就无法进入】
// ======================== 取款开始 ===================================
if (account.getBalance() >= 400) {
try {
Thread.sleep(10); // 需要特别注意的地方
} catch (InterruptedException e) {
e.printStackTrace();
}
account.withDraw(400);
System.out.println(Thread.currentThread().getName() + " 取款成功,余额:" + account.getBalance());
} else {
System.out.println(Thread.currentThread().getName() + " 取款失败,余额:" + account.getBalance());
}
// ======================== 取款结束 ===================================
}
}
TestAccount 类:
public static void main(String[] args) {
// 创建 Runnable 对象
Runnable runnable = new AccountRunnable();
Thread user1 = new Thread(runnable, "张三");
user1.start();
Thread user2 = new Thread(runnable, "张三妻子");
user2.start();
}
可能出现的结果:
5.2 同步代码块
针对上面的问题,第一种解决方案:使用同步代码块
synchronized (account) { // 使用共享资源作为锁
// 在开始取款之前,先【加上一把锁】,保证【有一个人进入了,另一个人就无法进入】
// ======================== 取款开始 ===================================
if (account.getBalance() >= 400) {
try {
Thread.sleep(10); // 需要特别注意的地方
} catch (InterruptedException e) {
e.printStackTrace();
}
account.withDraw(400);
System.out.println(Thread.currentThread().getName() + " 取款成功,余额:" + account.getBalance());
} else {
System.out.println(Thread.currentThread().getName() + " 取款失败,余额:" + account.getBalance());
}
// ======================== 取款结束 ===================================
}
5.2.1 同步监视器(锁子)
synchronized (同步监视器) { 同步代码块 }
- 必须是引用数据类型,不能是基本数据类型
- 在同步代码块中,可以改变它的值,但是不能改变其引用,
建议使用 final 修饰同步监视器
- 一般使用共享资源作同步监视器即可
- 也可创建一个专门的同步监视器,没有任何业务意义
- 尽量不要使用 String 和包装类(Integer)作同步监视器
5.2.2 执行过程
- 第一个线程,来到同步代码块,发现同步监视器为 open 状态,需要改为 close,然后执行其中的代码
- 第一个线程,在执行过程中,发生了线程切换(阻塞),第一个线程就失去了 cpu,但是没有开锁 open
- 第二个线程,获取了cpu,来到同步代码块,发现同步监视器为 close 状态,无法执行其中的代码,也进入阻塞状态
- 第一个线程,再次获取 cpu,接着执行后续的代码;执行完成后,开锁 open
- 第二个线程,也再次获取到 cpu,来到同步代码块,发现为 open 状态,重复第一个线程的处理过程(加锁)
注意:同步代码块中,可以发生线程切换
,但是后续的线程,由于无法开锁,所以无法执行同步代码块
5.2.3 分析
-
加上锁之后,安全了,但是效率降低,而且可能出现死锁
-
多个代码块使用了同一个锁A,锁住一个代码块的同时,也把其他地方使用这把锁A的代码块锁住了
导致其他线程无法访问这些代码块
但是没有锁住使用其他锁B的代码块,其他线程可以执行这些代码块
5.3 同步方法
提取需要加锁的代码块,形成一个方法,给方法加锁(不能给 run() 方法加锁
)
public synchronized void withDraw() { // 【synchronized】 写在方法上
if (account.getBalance() >= 400) {
try {
Thread.sleep(10); // 需要特别注意的地方
} catch (InterruptedException e) {
e.printStackTrace();
}
account.withDraw(400);
System.out.println(Thread.currentThread().getName() + " 取款成功,余额:" + account.getBalance());
} else {
System.out.println(Thread.currentThread().getName() + " 取款失败,余额:" + account.getBalance());
}
}
- 非静态同步方法的锁:
this
(这个类)- 会将所有的非静态同步方法都锁住
- 静态同步方法的锁:
类名.class
- 会将所有的静态同步方法都锁住
- 同步代码块的效率高
5.4 Lock 锁
JDK 1.5推出的,API级别,可以调用方法接口
5.4.1 使用步骤
- 先购买一把锁
- 在需要同步的代码块之前,上锁
- 代码块执行结束的地方,解锁(
需要手动解锁
)
public class AccountRunnable implements Runnable {
private Account account = new Account();
// 购买锁
private Lock lock = new ReentrantLock(); // Re-entrant-Lock 【可重入锁】
/**
* 取款的步骤
*/
@Override
public void run() {
// ============== 上锁 ==============
lock.lock();
try {
if (account.getBalance() >= 400) {
try {
Thread.sleep(10); // 需要特别注意的地方
} catch (InterruptedException e) {
e.printStackTrace();
}
account.withDraw(400);
System.out.println(Thread.currentThread().getName() + " 取款成功,余额:" + account.getBalance());
} else {
System.out.println(Thread.currentThread().getName() + " 取款失败,余额:" + account.getBalance());
}
} finally {
// ============== 手动解锁 ==============
lock.unlock();
}
}
}
5.4.2 可重入锁
public void method1() {
lock.lock(); // 此处上锁
try {
method2(); // 进入 method2 方法
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock(); // 由于已经上过锁,直接进入即可,但是需要标识一下,比如是第 2 次进入
try {
method3();
} finally {
lock.unlock();
}
}
public void method3() {
lock.lock(); // 第 3 次进入
try {
// TODO
} finally {
lock.unlock(); // 解第 3 次的锁
}
}
5.4.3 Lock 和 synchronized 的区别
- Lock 是显式锁(手动开启和关闭锁),synchronized 是隐式锁,遇到异常自动解锁
- Lock 只有代码块锁,synchronized 有代码块锁和方法锁
- Lock 可以对读不加锁,对写加锁,synchronized 不可以
- Lock 锁可以有多种获取锁的方式,可以从 sleep 的线程中抢到锁,synchronized 不可以
- Lock 锁性能好,有更好的扩展性
优先使用顺序:Lock ----- 同步代码块 ------- 同步方法
5.5 线程同步练习 – 卖票
多个窗口卖票,一共100张,不允许重复,出现负数等(只是解决了线程同步问题,但可能会出现一个窗口全部卖出票的情况,后期可以引入线程通信)
Lock 方式:
public class TicketRunnable implements Runnable {
// 直接将票的数量写在这里
private int ticketNum = 200;
// 创建锁对象
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 对卖一次票的过程 【上锁】
lock.lock();
try {
if (ticketNum <= 0) {
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出了第T " + ticketNum + "张票");
ticketNum--;
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
Runnable runnable = new TicketRunnable();
// 创建 4 个线程,模拟四个窗口
Thread t1 = new Thread(runnable);
t1.setName("[1] 窗口");
Thread t2 = new Thread(runnable);
t2.setName("[2] 窗口");
Thread t3 = new Thread(runnable);
t3.setName("[3] 窗口");
Thread t4 = new Thread(runnable);
t4.setName("[4] 窗口");
// 开始售票
t1.start();
t2.start();
t3.start();
t4.start();
}
}
注意:对卖一次票的过程加锁,不要对循环加锁,否则就代表在循环开始之前某一线程先加锁,然后该线程进入循环,一直卖票,直到卖光
6. 线程通信
6.1 生产者和消费者
- 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中的产品取走
- 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到产品被消费者取走(告诉消费者)
- 如果仓库中有产品,则消费者可以取走消费,否则停止消费并等待,直到仓库中再次放入产品(告诉生产者)
分析:
- 线程同步问题,生产者和消费者共享同一个资源(但是进行的操作不一样,一个是
生产
,一个是消费
,账户的问题都是取钱),且生产者和消费者相互依赖,互为条件 - 生产者:没有
生产
产品之前,要告诉消费者等待;生产了产品后,要通知消费者进行消费
- 消费者:消费之后,告诉生产者继续生产
知识前提:(都是Object类的方法)
final void wait()
线程一直等待,直到其他线程通知 【让出 CPU,释放锁】 sleep方法只让出CPUvoid wait(long timeout)
线程等待一段时间,唤醒后继续往下执行
- final void wait(long timeout, int nanos) 线程等待一段时间
final void notify()
唤醒一个处于等待状态的线程final void notifyAll()
唤醒同一个对象上,所有调用 wait() 方法的线程,优先级别高的线程先执行
6.2 synchronized方式
必须调用同步监视器(锁子)的 wait()、notify()、notifyAll()
Produce 商品类
public class Produce {
private String name;
private String color;
boolean flag = false; // 商品有无的状态,默认没有
// getter / setter 方法
// 有参 / 无参 构造方法,toString()
}
ProduceRunnable 生产者类
public class ProduceRunnable implements Runnable {
// 此处不能直接 new,不然和消费者操作的不是同一件商品
private Produce produce;
// 让消费者获取的生产的对象,保证是同一个【set方式】
public void setProduce(Produce produce) {
this.produce = produce;
}
@Override
public void run() {
for (int i = 0; ; i++) {
synchronized (produce) {
// 1.如果已经有商品了,就等待
if (produce.flag) {
try {
produce.wait(); // 只释放CPU,同时释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 2. 生产商品并输出
if (i % 2 == 0) {
produce.setName("馒头");
try {
Thread.sleep(10); // 只释放CPU
} catch (InterruptedException e) {
e.printStackTrace();
}
produce.setColor("白色");
} else {
produce.setName("玉米饼");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
produce.setColor("黄色");
}
System.out.println("【生产】了一个 " + produce.getColor() + " 的 " + produce.getName());
// 3. 改变商品有无状态:有
produce.flag = true;
// 4. 通知消费
produce.notify(); //【随机唤醒一个线程】
}
}
}
}
ConsumeRunnable 消费者类
public class ConsumeRunnable implements Runnable {
// 此处不能直接 new,不然不是生产者生产的物品
private Produce produce;
// 让消费者获取生产的对象,保证是同一个【construct方式】
public ConsumeRunnable(Produce produce) {
this.produce = produce;
}
@Override
public void run() {
while (true) {
// 对生产者加锁后,也必须对消费者加锁,并且为同一把锁!!!!!!
synchronized (produce) {
// 1. 如果没有商品,就等待
if (!produce.flag) {
try {
produce.wait(); // 让出CPU,同时释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 2. 有商品,进行消费
System.out.println("【消费】了一个 " + produce.getColor() + " 的 " + produce.getName());
// 3. 改变商品有无状态:无
produce.flag = false;
// 4. 通知生产者生产
produce.notifyAll(); //【唤醒所有等待的线程】
}
}
}
}
Test 测试类
public class Test {
public static void main(String[] args) {
// 保证生产和消费的为同一个商品,所以在此处new一个,作为参数传递给生产者和消费者
Produce produce = new Produce();
// 生产者生产商品
ProduceRunnable runnable1 = new ProduceRunnable();
runnable1.setProduce(produce); // set方法
Thread t1 = new Thread(runnable1, "生产者");
t1.start();
// 消费者进行消费
Runnable runnable2 = new ConsumeRunnable(produce); // 直接构造方法
Thread t2 = new Thread(runnable2, "消费者");
t2.start();
}
}
问题:
- 需要定义一个商品类 ======》 生产者生产商品,消费者消费商品
- 需要保证生产者生产的,和消费者消费的,是同一件商品 (在测试类中new商品,分别传给生产者和消费者)
- 实现线程同步 =====》 避免出现白色的玉米饼,黄色的馒头
- 最终实现线程通信 =====》 使用从 Object 类继承的 wait(),notify(),必须用同步监视器的
6.3 线程通信的细节
-
进行线程通信,必须要使用
同一个同步监视器
,必须调用该同步监视器的 wait()、notify()、notifyAll() -
线程通信的三个方法(都是由同一个同步监视器操作的)
- wait() 等待:在其他线程未进行唤醒 notify() 【此对象】之前,一直等待,唤醒后继续执行代码
- wait(time) 一段时间等待:在其他线程未进行唤醒 notify() 或等待时间未到之前,等待
- notify() 随机唤醒一个:任意的唤醒一个
- notifyAll() 唤醒所有:唤醒所有的线程,优先级高的先执行
-
完整的线程生命周期
- 同步阻塞(锁池队列):没有获取同步监视器(没有拿到锁)的线程的队列
- 等待阻塞(等待队列):被调用了 wait() 后释放锁,然后进入该队列
-
sleep() 和 wait() 的区别
- sleep() :让出CPU,进入阻塞,但不会释放锁,
进入阻塞状态
- wait():让出CPU,进入阻塞,同时会释放锁,
进入等待队列
,只能在同步中使用
- sleep() :让出CPU,进入阻塞,但不会释放锁,
6.4 同步方法方式
需要将代码提炼成方法,但是又需要是同一把锁,所以就需要将业务放到 Produce 类
Produce 类:
public class Produce {
private String name;
private String color;
boolean flag = false; // 商品有无的状态,默认没有
// getter / setter 方法
// 有参 / 无参 构造方法,toString()
// 生产商品
public synchronized void produce(int i) { // 【锁是this】!!!!!
// 1.如果已经有商品了,就等待
if (this.flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 2. 生产商品并输出
if (i % 2 == 0) {
this.setName("馒头");
try {
Thread.sleep(10); // 只释放CPU
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setColor("白色");
} else {
this.setName("玉米饼");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setColor("黄色");
}
System.out.println("【生产】了一个 " + this.getColor() + " 的 " + this.getName());
// 3. 改变商品有无状态:有
this.flag = true;
// 4. 通知消费
this.notify(); //【随机唤醒一个线程】
}
public synchronized void consume() { // 【this 可以省略】
// 1. 如果没有商品,就等待
if (!flag) {
try {
wait(); // 让出CPU,同时释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 2. 有商品,进行消费
System.out.println("【消费】了一个 " + color + " 的 " + name);
// 3. 改变商品有无状态:无
flag = false;
// 4. 通知生产者生产
notifyAll(); //【唤醒所有等待的线程】
}
}
6.5 Lock锁
同步代码块和同步方法,生产者和消费者都在一个等待队列中,会存在本想唤醒一个生产者,却唤醒了一个消费者的问题
======》 使用 Lock 锁 + Condition类
解决
-
创建各自的等待队列(可以有多个等待队列,但只有一个锁池状态)
Condition produceCondition = lock.newCondition(); //【生产者的队列】
-
线程等待的方法
await()
produceCondition.await(); // 【进入生产者队列】
-
线程唤醒的方法
signal()
produceCondition.await(); // 【进入生产者队列】
Produce 类
public class Produce {
private String name;
private String color;
boolean flag = false; // 商品有无的状态,默认没有
private Lock lock = new ReentrantLock();
// 创建【生产者】的等待队列
private Condition produceCon = lock.newCondition();
// 创建【消费者】的等待队列
private Condition consumeCon = lock.newCondition();
public void produce(int i) {
lock.lock();
try {
if (this.flag) {
try {
produceCon.await(); //【进入生产者队列】!!! 使用的是 await()
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (i % 2 == 0) {
this.setName("馒头");
try {
Thread.sleep(10); // 只释放CPU
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setColor("白色");
} else {
this.setName("玉米饼");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setColor("黄色");
}
System.out.println("【生产】" + this.getColor() + this.getName());
this.flag = true;
consumeCon.signal(); // 从【消费者】队列中进行唤醒!!!让其消费
} finally {
lock.unlock();
}
}
public void consume() {
lock.lock();
try {
if (!flag) {
try {
consumeCon.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("【消费】" + color + name);
flag = false;
produceCon.signalAll(); // 唤醒【生产者】进行生产
} finally {
lock.unlock();
}
}
}
7. 线程池
7.1 线程池的引入
ThreadPoolExecutor
,对象的创建和销毁是非常消耗时间的,所以可以事先创建好多个线程,放入线程池,使用时直接获取引用,不使用时放回池中(还可以临时的多创建几个线程)
应用场景:需要大量的线程,并且完成任务的时间短;对性能要求苛刻;接受突发性的大量请求
7.2 创建线程池的方法
-
线程池中【只有一个】线程
newSingleThreadExecutor()
ExecutorService pool = Executors.newSingleThreadExecutor();
-
线程池中有【固定数量】的线程
newFixedThreadPool(10)
ExecutorService pool2 = Executors.newFixedThreadPool(10);
-
线程池中线程的数量【可以动态变化】 60s没有任务关闭线程
newCachedThreadPool()
ExecutorService pool3 = Executors.newCachedThreadPool();
-
用来执行大量的【定时任务】
newScheduledThreadPool(10)
ExecutorService pool4 = Executors.newScheduledThreadPool(10);
7.3 执行大量的 Runnable命令
public class ThreadPoolDemo {
public static void main(String[] args) {
// 1.创建一个线程池
// 串行执行,一个接一个 【耗时20s】
ExecutorService pool = Executors.newSingleThreadExecutor(); // 池中1个线程
// 10个线程随机选一个任务执行 【耗时2秒】
ExecutorService pool2 = Executors.newFixedThreadPool(10); // 池中10个线程
// 20个任务,创建20个线程 【耗时1秒】
ExecutorService pool3 = Executors.newCachedThreadPool(); // 根据任务动态变化
// ExecutorService pool4 = Executors.newScheduledThreadPool(10); // 执行定时任务
// 2.使用线程池,执行大量的 Runnable 命令
for (int i = 0; i < 20; i++) {
Runnable runnable = new MyRunnable(i);
pool.execute(runnable); // new Thread(runnable).start();
}
// 3.关闭线程池
pool.shutdown();
}
}
class MyRunnable implements Runnable {
private int i;
public MyRunnable(int i) {
this.i = i;
}
@Override
public void run() {
try {
Thread.sleep(1000); // 每个任务耗时 1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i + " 任务开始执行");
System.out.println(i + " 任务结束");
}
}
7.4 执行大量的 Callable任务
- 使用 Future 获取返回值
- Future 的 get() 方法,获取不到返回值时,就一直等待,可以先将 future 放到 list中,之后再获取
public class ThreadPoolCallable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建线程池
ExecutorService pool = Executors.newCachedThreadPool();
// 暂存结果
List<Future> list = new ArrayList<>();
// 使用线程池执行大量的 Callable任务
for (int i = 0; i < 20; i++) {
Callable task = new Callable(){ // 匿名内部类的方式
@Override
public Object call() throws Exception {
Thread.sleep(1000);
return new Random().nextInt(10);
}
};
// 利用 Future 获取返回值
Future<Integer> future = pool.submit(task); // 调用线程池的 submit方法!!!
// Integer res = future.get(); // 直接 get 需要一直等待获取到结果才返回
// 可以将结果先放入 List,之后再获取
list.add(future);
}
for (Future future : list) {
System.out.println(future.get());
}
// 关闭线程池
pool.shutdown();
}
}
7.5 线程池API
创建线程池时,最终调用的方法
- corePoolSize:核心池的大小(正式工的人数)
- 默认情况下创建了线程池,线程数为0,新任务来时,就会创建一个线程去执行任务
- 当池中线程数达到 corePollSize 后,就不再创建,把新到的任务放到队列中等待
- maximumPoolSize:最大线程数(正式工+临时工的人数)
- 池中可以存放的最大线程数量
- keepAliveTime + unit:线程存活时间(临时工可以存在的时间)
- BlockingQueue:阻塞队列
- 线程都在用时,有新任务进来了,排队等待的地方
线程池只有一个等待队列
- 线程都在用时,有新任务进来了,排队等待的地方
- ThreadFactory:线程工厂
- 池子中,创建线程的工厂
- RejectedExecutionHandler:拒绝策略
- 线程全部在用,等待队列也已经排满,现在来了新的任务,如何对待?
- CallerRunsPolicy:新创建一个线程来执行这个任务(不属于该线程池)
- DiscardOldestPolicy:把等待时间最长的那个舍弃,运行新的任务
- DiscardPolicy:什么也不做
- AbortPolicy:Java默认,抛出异常
- 线程全部在用,等待队列也已经排满,现在来了新的任务,如何对待?
8. ForkJoin 框架
JAVA7提供,将一个大的任务,分割成若干个小任务,然后执行求值等,最终汇总每个小任务结果,得到大任务结果
8.1 工作窃取算法(work-stealing)
一个大任务拆分成多个小任务,为减少线程间竞争,把这些子任务分别放到不同的队列(有多个等待队列
)中,每个队列都有单独的线程(线程都在线程池中)来执行队列中的任务,线程和队列一一对应
但是会出现一种情况:A处理完了自己队列中的任务,B的队列中还有很多任务要处理
==》 A可以选择闲着,不去帮忙
==》也可以去帮忙,但是B是从队列头部进行处理的,所以为了不产生竞争,A从队列尾部拿任务进行处理(双端队列
)
优点:利用了线程进行并行计算,减少了线程间的竞争
缺点:如果双端队列中只有一个任务,线程会存在竞争;消耗了更多的系统资源(会创建多个线程和双端队列)
8.2 主要类
拥有共同的父类:
-
ForkJoinWorkerThread
ForkJoinPool 线程池中的一个执行任务的线程
-
ForkJoinTask
用来创建一个 ForkJoin 任务,一般继承它的子类
- RecursiveAction:没有返回结果的任务
- RecursiveTask:有返回结果的任务
-
ForkJoinPool
ForkJoinPool 任务需要通过 ForkJoinPool 来执行
8.3 ForkJoin 实例
求1到100的和:
// 1.使用循环求和【相当于4核的cpu,只有1个核在工作】
int n = 100;
long sum = 0;
for (int i = 0; i <= n; i++) {
sum += i;
}
System.out.println(sum);
// 2.使用 ForkJoin 框架求和
public class SumTask extends RecursiveTask<Long> {
int start;
int end;
int step = 10; // 每10个数相加 为一个任务
public SumTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
// 1. 定义一个结果变量
long sum = 0;
// 2. 计算结果
if (end - start <= step) { // 10个数相加的小任务
for (int i = start; i <= end; i++) {
sum += i;
}
} else { // 分解大任务为小任务
// 分解任务
int mid = (start + end) / 2;
SumTask leftTask = new SumTask(start, mid);
SumTask rightTask = new SumTask(mid + 1, end);
// 继续分解
leftTask.fork(); // 【类似于递归】
rightTask.fork();
// 结果求和
long leftSum = leftTask.join();
long rightSum = rightTask.join();
sum = leftSum + rightSum;
}
// 3. 返回结果
return sum;
}
}
// ========================== 测试结果 ===============================
public static void main(String[] args) throws ExecutionException, InterruptedException {
int n = 100;
// 2.1 创建一个 ForkJoin 线程池
ForkJoinPool pool = new ForkJoinPool();
// 2.2 给出一个求和的任务
RecursiveTask task = new SumTask(1, n);
// 2.3 将任务交给线程池(线程池会分解任务,并合并结果)
Future<Long> future = pool.submit(task);
// 2.4 获取结果并输出
Long res = future.get();
System.out.println(res);
// 2.5 关闭线程池
pool.shutdown();
}