多线程
1 线程、进程、多线程概述
- 线程:是操作系统中能够进行运算调度的最小单位,包含在进程中,是进程中的实际运作单位。
- 进程:是程序执行一次的过程。
- 多线程:一个进程可以并发出多个线程,这就是多线程。
2 创建线程 (重点)
2.1 继承Thread类(Thread类也实现了Runnable接口)
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("开启线程:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
new MyThread().start();
}
}
第一次输出:
开启线程:Thread-1
开启线程:Thread-2
开启线程:Thread-0
第二次输出:
开启线程:Thread-1
开启线程:Thread-0
开启线程:Thread-2
可以发现 每次输出,每次输出,顺序不同,说明他们是同时在执行(如果是单核单cpu,实际上不是同时)。
2.2 实现Runnable接口(无消息返回)
public class MyThread implements Runnable {
@Override
public void run() {
System.out.println("开启线程:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
new Thread(new MyThread()).start();
new Thread(new MyThread()).start();
new Thread(new MyThread()).start();
}
}
第一次输出:
开启线程:Thread-0
开启线程:Thread-1
开启线程:Thread-2
第二次输出:
开启线程:Thread-1
开启线程:Thread-2
开启线程:Thread-0
结论同上,开启线程基本上是同时进行的(如果是单核单cpu,实际上不是同时)。
2.3 实现callable接口(有消息返回)
实现callable接口创建线程要用到FutureTask类。
public class MyThread implements Callable<String> {
@Override
public String call() {
System.out.println("开启线程:" + Thread.currentThread().getName());
return Thread.currentThread().getName();
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> f1 = new FutureTask<>(new MyThread());
FutureTask<String> f2 = new FutureTask<>(new MyThread());
FutureTask<String> f3 = new FutureTask<>(new MyThread());
new Thread(f1).start();
new Thread(f2).start();
new Thread(f3).start();
System.out.println("f1的返回值是:" + f1.get());
System.out.println("f2的返回值是:" + f2.get());
System.out.println("f3的返回值是:" + f3.get());
}
}
第一次输出:
开启线程:Thread-1
开启线程:Thread-0
开启线程:Thread-2
f1的返回值是:Thread-0
f2的返回值是:Thread-1
f3的返回值是:Thread-2
第二次输出:
开启线程:Thread-0
开启线程:Thread-2
f1的返回值是:Thread-0
开启线程:Thread-1
f2的返回值是:Thread-1
f3的返回值是:Thread-2
结论同上,但实现callable接口可以返回消息。
2.4 线程如何停止?
虽然jdk提供了stop方法和destroy方法,但是更推荐的是,用外部标志位来告诉程序是否继续运行。
public class Test{
public static void main(String[] args) throws InterruptedException {
NeedStop ns = new NeedStop();
new Thread(ns).start();
Thread.sleep(10);
ns.flag = false;
}
}
class NeedStop implements Runnable {
boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag) {
System.out.println("运行了 " + i++ + " 次");
}
}
}
当 flag 变为 false时,线程便终止了,这是很安全的做法。
3 线程的一些方法
3.1 线程休眠__sleep
例:如买票的系统,假如没有处理并发问题,就可能会存在多个人买到同一张票,或者余票为负等情况。
3.1.1 利用线程休眠来模拟网络延时,放大问题
public class Account {
String cardId;
int RMB;
public Account(String cardId, int RMB) {
this.cardId = cardId;
this.RMB = RMB;
}
public static void main(String[] args) throws InterruptedException {
Account ac = new Account("123456", 100000);
new Thread(new DrawMoney(ac, 100000), "小明").start();
new Thread(new DrawMoney(ac, 100000),"小红").start();
Thread.sleep(1000);
System.out.println("剩余 " + ac.RMB + " 元");
}
}
class DrawMoney implements Runnable {
Account ac;
int money;
public DrawMoney(Account ac, int money) {
this.ac = ac;
this.money = money;
}
public void drawMoney(Account ac) throws InterruptedException {
if (money <= ac.RMB) {
System.out.println(Thread.currentThread().getName() + "取走了 " + money + " 元");
Thread.sleep(10);
ac.RMB -= money;
}
else {
System.out.println("余额不足!");
}
}
@Override
public void run() {
try {
drawMoney(ac);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
多次输出发现:
可能会出现小明小红同时取到钱,余额为负数的情况。
当小红进入if判断时,ac.RMB还没来得及扣除。
3.1.2 利用sleep方法来模拟倒计时
package com.stop;
public class SleepTest {
public static void main(String[] args) throws InterruptedException {
System.out.println("倒计时:");
for (int i = 10; i > 0; i--) {
Thread.sleep(1000);
System.out.println(i);
}
}
}
一秒输出一个数。
3.2 线程礼让__yield
- 让当前线程暂停,但不阻塞
- 将线程从运行=>就绪
- 让cpu重新调度,礼让不一定成功
相当于大家重回同一起跑线,重新争夺资源
public class YieldTest implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行");
Thread.yield();
System.out.println(Thread.currentThread().getName() + "=>结束");
}
public static void main(String[] args) {
new Thread(new YieldTest(), "a").start();
new Thread(new YieldTest(), "b").start();
}
}
如果礼让成功,则a(或b)开始和结束不会连续出现。
3.3 线程强制执行__join
执行join会让其他线程阻塞,待当前线程结束后,其他线程才能执行,如同霸道的插队。
public class JoinTest implements Runnable{
@Override
public void run() {
for (int i = 10; i > 0; i--) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("vip来了" + i);
}
}
public static void main(String[] args) throws InterruptedException {
Thread test = new Thread(new JoinTest(), "vip");
test.start();
for (int i = 0; i < 500; i++) {
if (i == 250) {
test.join();
}
System.out.println("main" + i);
}
}
}
当main中i == 250时,就会被阻塞然后让咱们的vip线程执行完毕,再继续执行main线程。
3.4 观测线程状态
3.4.1 线程的几种状态
NEW(新建)
线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
RUNNABLE(就绪)
线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
- 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
BLOCKED(阻塞于锁)
同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
WAITING(等待)
进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。
TIMED WAITING(超时等待)
其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
TERMINATED(终止)
终止线程的线程状态。 线程已完成执行。
public class StatusTest implements Runnable{
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("//");
}
public static void main(String[] args) throws InterruptedException {
Thread test = new Thread(new StatusTest());
System.out.println(test.getState());
test.start();
System.out.println(test.getState());
Thread.State state = test.getState();
while (state != Thread.State.TERMINATED) {
Thread.sleep(1000);
state = test.getState();
System.out.println(state);
}
test.start(); // 线程死亡后不可以再次启动
}
}
线程从 NEW= >RUNNABLE=> TIME_WAITTING阻塞了10秒=>TERMINNATED
最后再次启动线程时 将报错。
3.5 线程优先级(priority)
线程调度按照优先级决定应该先调谁,所有就绪状态的线程都会被监控。具体调谁具体还得看CPU心情。
改变、获取优先级
改变:setPriority(int xxx)
获取:getPriority()
优先级最高为10
最低为1
默认优先级是5
public class PriorityTest implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " " + Thread.currentThread().getPriority());
}
public static void main(String[] args) {
Thread t1 = new Thread(new PriorityTest());
Thread t2 = new Thread(new PriorityTest());
Thread t3 = new Thread(new PriorityTest());
Thread t4 = new Thread(new PriorityTest());
Thread t5 = new Thread(new PriorityTest());
t1.setPriority(10);
t2.setPriority(8);
t4.setPriority(3);
t5.setPriority(1);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
3.6 线程守护
线程分为用户线程和守护(daemon)线程,JVM只确保用户线程执行完毕而不用等待守护线程执行完毕
守护线程如:监控内存、垃圾回收等
Thread种的**setDaemon(boolean on)**方法可以设置线程是否为守护线程,默认为false,用户线程。
public class DaemonTest extends Thread{
@Override
public void run() {
while (true) {
System.out.println("====守护====");
}
}
public static void main(String[] args) {
Thread daemon = new DaemonTest();
daemon.setDaemon(true);
daemon.start();
new NormalThread().start();
}
}
class NormalThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("====用户====");
}
}
}
这段代码将会在用户主线程结束时,守护线程结束,不会死循环。
4 线程同步 (重点)
4.1 概述
在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。
同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。
“同”字从字面上容易理解为一起动作
其实不是,“同”字应是指协同、协助、互相配合。
如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作。
在多线程编程里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。
4.2 并发
在同一时刻,有多个线程同时访问 某一个(一些)资源,带来数据的不安全性 、不稳定性、不确定性。
同步就是为了解决并发问题。
下面这段代码就存在着并发问题:
public class Account {
String cardId;
int RMB;
public Account(String cardId, int RMB) {
this.cardId = cardId;
this.RMB = RMB;
}
public static void main(String[] args) throws InterruptedException {
Account ac = new Account("123456", 100000);
new Thread(new DrawMoney(ac, 100000), "小明").start();
new Thread(new DrawMoney(ac, 100000),"小红").start();
Thread.sleep(1000);
System.out.println("剩余 " + ac.RMB + " 元");
}
}
class DrawMoney implements Runnable {
Account ac;
int money;
public DrawMoney(Account ac, int money) {
this.ac = ac;
this.money = money;
}
public void drawMoney(Account ac) throws InterruptedException {
if (money <= ac.RMB) {
System.out.println(Thread.currentThread().getName() + "取走了 " + money + " 元");
Thread.sleep(10);
ac.RMB -= money;
}
else {
System.out.println("余额不足!");
}
}
@Override
public void run() {
try {
drawMoney(ac);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
多次输出发现:
可能会出现小明小红同时取到钱,余额为负数的情况。
当小红进入if判断时,ac.RMB还没来得及扣除。
4.3 同步方法和同步代码块
解决并发问题的 synchronized
synchronized 可以给代码加锁,同一时刻只有一个线程可以执行被锁定的代码
synchronized又分为同步方法和同步代码块
4.3.1 同步方法
若给方法加上锁
public synchronized void methodName() {
}
每次只能有一个线程执行这个方法
4.3.2 同步方法解决买票问题
现有以下代码:
public class Tickets implements Runnable {
private int ticketNum = 10;
boolean flag =true;
public void buy() throws InterruptedException {
if (ticketNum <= 0) {
flag = false;
return;
}
System.out.println(Thread.currentThread().getName() + " 拿到第 " + ticketNum + "张 票");
Thread.sleep(100);
ticketNum--;
}
@Override
public void run() {
while (flag) {
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Tickets station = new Tickets();
new Thread(station, "小明").start();
new Thread(station, "小红").start();
new Thread(station, "小张").start();
}
}
会出现多人拿到同一张票或者拿到不存在的票的安全问题,此时,同步方法可以解决这个问题。
只需要给buy()方法加上锁,那么同一时间,只能有一个线程执行buy()。因为小明、小红、小张,都是同一个Tickets对象,所以this是相同的,同一时间只能有一个线程进入这个方法。
4.3.3 同步代码块
synchronized(obj) {
code……
}
obj填锁对象。谁先拿到obj谁就先执行,其余线程只能排队。
4.3.4 同步代码块解决取款问题
之前取款的例子
public class Account {
String cardId;
int RMB;
public Account(String cardId, int RMB) {
this.cardId = cardId;
this.RMB = RMB;
}
public static void main(String[] args) throws InterruptedException {
Account ac = new Account("123456", 100000);
new Thread(new DrawMoney(ac, 100000), "小明").start();
new Thread(new DrawMoney(ac, 100000),"小红").start();
Thread.sleep(1000);
System.out.println("剩余 " + ac.RMB + " 元");
}
}
class DrawMoney implements Runnable {
Account ac;
int money;
public DrawMoney(Account ac, int money) {
this.ac = ac;
this.money = money;
}
public void drawMoney(Account ac) throws InterruptedException {
if (money <= ac.RMB) {
System.out.println(Thread.currentThread().getName() + "取走了 " + money + " 元");
Thread.sleep(10);
ac.RMB -= money;
}
else {
System.out.println("余额不足!");
}
}
@Override
public void run() {
try {
drawMoney(ac);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
此时再用同步方法并不能解决并发问题,因为小明和小红不是同一个this(不同的DrawMoney实例),所以可以同时执行
。
同步代码块 可以解决这个问题,用synchronized把关键的代码加上锁(很显然是29~36行)
public void drawMoney(Account ac) throws InterruptedException {
synchronized (ac) {
if (money <= ac.RMB) {
System.out.println(Thread.currentThread().getName() + "取走了 " + money + " 元");
Thread.sleep(10);
ac.RMB -= money;
}
else {
System.out.println("余额不足!");
}
}
}
那么在同一时间内,只能有一个线程能够对ac账户进行操作。
4.4 死锁
当两个线程拿着对方需要的锁而不释放时,因为双方都拿不到锁,所以就成了死锁,线程就阻塞在那里。
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。产生死锁的原因,主要包括:
- 系统资源不足;
- 程序执行的顺序有问题;
- 资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,那么死锁出现的可能性就很低;否则,
就会因争夺有限的资源而陷入死锁。其次,程序执行的顺序与速度不同,也可能产生死锁。产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
4.4.1 死锁代码
public class DeadLock implements Runnable {
public static final String lock1 = "Lock_1";
public static final String lock2 = "Lock_2";
boolean flag;
public DeadLock(boolean flag) {
this.flag = flag;
}
public void dead() throws InterruptedException {
if (flag) {
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " 拿到了lock1");
Thread.sleep(1000);
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " 拿到了lock2");
}
}
} else {
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " 拿到了lock2");
Thread.sleep(1000);
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " 拿到了lock1");
}
}
}
}
@Override
public void run() {
try {
dead();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new DeadLock(true)).start();
new Thread(new DeadLock(false)).start();
}
}
一个线程在 第 12 行拿到了lock1,在他休眠时另一个线程在 20 行拿到了lock2,拿到lock1的线程休眠结束后,需要拿lock2的锁,可是lock2在另一个线程手里,所以lock1就等待lock2释放,而lock2那边也是同理,在等待lock1释放,互相阻塞在那里,这就是死锁。
解决方法也很简单,lock1 用完 就释放掉,lock2同理
4.4.2 解决死锁
于是我们不再继续嵌套书写synchronized
public class Solute implements Runnable{
public static final String lock1 = "Lock_1";
public static final String lock2 = "Lock_2";
boolean flag;
public Solute(boolean flag) {
this.flag = flag;
}
public void dead() throws InterruptedException {
if (flag) {
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " 拿到了lock1");
Thread.sleep(1000);
}
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " 拿到了lock2");
}
} else {
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " 拿到了lock2");
Thread.sleep(1000);
}
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " 拿到了lock1");
}
}
}
@Override
public void run() {
try {
dead();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new Solute(true)).start();
new Thread(new Solute(false)).start();
}
}
lock1唤醒后,释放lock1,lock2同理。
4.5 Lock(锁)
作用和synchronized类似,都是用来解决并发问题。Lock是一个接口,而synchronized是一个关键字。Lock是接口意味着它有许多方法,在复杂的情况下比synchronized要方便。
4.5.1 并发案例
继续用经典的买票案例:
package com.locklock;
public class TestLock implements Runnable {
private int ticketsNum;
private boolean flag;
public TestLock(int ticketsNum, boolean flag) {
this.ticketsNum = ticketsNum;
this.flag = flag;
}
public void buyTickets() throws InterruptedException {
if (ticketsNum > 0) {
Thread.sleep(100);
ticketsNum--;
System.out.println(Thread.currentThread().getName() + " 来买了第 " + ticketsNum + " 张票");
} else {
flag = false;
System.out.println("已售罄!");
}
}
@Override
public void run() {
while (flag) {
try {
buyTickets();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
TestLock testLock = new TestLock(10,true);
new Thread(testLock, "xm").start();
new Thread(testLock, "xh").start();
new Thread(testLock, "xz").start();
}
}
4.5.2 Lock解决问题
已经知道,刚刚这段代码有问题,会出现多个人拿到同一张票,或者拿到无效票(<=0)的情况。
按照之前的方法,只需把buyTichets()方法变成同步方法即可。
现在我们不用synchronized关键字,用Lock里面的方法。
package com.locklock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestLock implements Runnable {
private int ticketsNum;
private boolean flag;
Lock lock = new ReentrantLock();
public TestLock(int ticketsNum, boolean flag) {
this.ticketsNum = ticketsNum;
this.flag = flag;
}
public void buyTickets() throws InterruptedException {
try {
lock.lock();
if (ticketsNum > 0) {
Thread.sleep(100);
ticketsNum--;
System.out.println(Thread.currentThread().getName() + " 来买了第 " + ticketsNum + " 张票");
} else {
flag = false;
System.out.println("已售罄!");
}
} finally {
lock.unlock();
}
}
@Override
public void run() {
while (flag) {
try {
buyTickets();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
TestLock testLock = new TestLock(10,true);
new Thread(testLock, "xm").start();
new Thread(testLock, "xh").start();
new Thread(testLock, "xz").start();
}
}
Lock接口创建ReentrantLock实例(多态),把关键的代码块用lock()方法和unlock()方法包裹起来,和synchronized (this) {}类似。
最好用try环绕代码、finally环绕unlock(),这样即使上面代码有异常,也会释放锁。
4.6 synchronized 和 Lock 比较
synchronized | Lock | |
---|---|---|
类型 | 关键字 | 接口 |
范围 | 锁方法和代码块 | 只能锁代码块 |
形式 | 隐式锁,作用于外自动释放 | 显示锁,手动释放 |
性能比较 | 底层指令来控制锁,少量同步 | 性能更好,大量同步 |
使用优先级:
- Lock锁 > 同步代码块 > 同步方法
5 线程通信
线程通信就是当多个线程共同操作共享的资源时,互相告知自己的状态以避免资源争夺。
5.1 生产者消费者模式
生产者消费者模式并不是 GOF 提出的 23 种设计模式之一,23 种设计模式都是建立在面向对象的基础之上的,但其实面向过程的编程中也有很多高效的编程模式,生产者消费者模式便是其中之一。
在实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。单单抽象出生产者和消费者,还够不上是生产者-消费者模式。该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。
为了不至于太抽象,我们举一个寄信的例子(虽说这年头寄信已经不时兴,但这个例子还是比较贴切的)。假设你要寄一封平信,大致过程如下:
1、你把信写好——相当于生产者制造数据
2、你把信放入邮筒——相当于生产者把数据放入缓冲区
3、邮递员把信从邮筒取出——相当于消费者把数据取出缓冲区
4、邮递员把信拿去邮局做相应的处理——相当于消费者处理数据
生产者:负责生产数据的模块。
消费者:负责处理数据的模块。
缓冲区:消费者要通过缓冲区才能使用生产者生产的数据。
5.2 管程法
通过变量的值控制
public class ProducerCustorm {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Producer(container).start();
new Consumer(container).start();
}
}
// 生产者
class Producer extends Thread {
final SynContainer container;
public Producer(SynContainer container) {
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
container.push(new Chicken(container.id));
}
}
}
// 消费者
class Consumer extends Thread {
SynContainer container;
public Consumer(SynContainer container) {
this.container = container;
}
@Override
public void run() {
for (int i = 0; i <50; i++) {
container.pop();
}
}
}
// 产品
class Chicken {
int id; //产品编号
public Chicken(int id) {
this.id = id;
}
}
// 缓冲区
class SynContainer {
int id = 1; // 产品编号
// 需要一个容器
Chicken[] chickens = new Chicken[10];
// 计数器
int count = 0;
// 生产者放入产品
public synchronized void push(Chicken chicken) {
// 如果容器 满了 则消费者消费
if (count == chickens.length) {
// 生产者等待消费者消费
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 被消费者通知生产
if (count < chickens.length) {
chickens[count++] = chicken;
id++;
System.out.println(Thread.currentThread().getName() + ": 生产了" + chicken.id + "只鸡");
// 通知消费者消费
this.notifyAll();
}
}
// 消费者消费产品
public synchronized void pop() {
if (count == 0) {
// 等待生产者生产
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果可以消费
if ( count != 0 ) {
Chicken chicken = chickens[--count];
System.out.println("消费了第" + chicken.id + "只鸡");
// 通知生产者生产
this.notifyAll();
}
}
}
用一个数值和别的线程传递信息,告诉他们是否可以继续就绪。
5.3 信号灯法
通过标志位控制
public class ProducerAndConsumer {
public static void main(String[] args) {
Buffered buffered = new Buffered();
new Producers(buffered, "厨师").start();
new Consumer(buffered, "顾客").start();
}
}
// 生产者
class Producers extends Thread {
Buffered buffered;
public Producers(Buffered buffered, String name) {
super(name);
this.buffered = buffered;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
buffered.push(new Bread(i));
}
System.out.println(Thread.currentThread().getName() + "工作了一天,该休息了!");
}
}
// 消费者
class Consumer extends Thread {
Buffered buffered;
public Consumer(Buffered buffered, String name) {
super(name);
this.buffered = buffered;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
buffered.pop();
}
}
}
// 缓冲区
class Buffered {
// 产品
Bread bread;
// 信号灯
boolean flag = false;
//生产者生产
public synchronized void push(Bread bread) {
if (flag) {
System.out.println(Thread.currentThread().getName() + ": 已经有面包了,休息会儿!");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (!flag) {
System.out.println(Thread.currentThread().getName() + ": 没东西吃了,赶快做……");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.bread = bread;
System.out.println("厨师做了:" + (bread.getId()+1) + " 个面包");
System.out.println("现在有东西吃了!");
this.flag = !this.flag;
this.notifyAll();
}
}
//消费者消费
public synchronized void pop() {
if (!flag) {
System.out.println(Thread.currentThread().getName() + ": 面包呢?");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (flag) {
System.out.println("销售了:" + (bread.getId()+1) + " 个面包");
this.flag = !this.flag;
System.out.println(Thread.currentThread().getName() + ": 面包吃完了!");
this.notifyAll();
}
}
}
// 产品
class Bread {
private int id;
public Bread(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
就是设置一个标志位,然后告诉其他线程是否可以工作,协同处理一个数据。
6 线程池
如果有请求就新建一个线程,那么会创建很多线程,严重影响性能,线程池里面的线程可以复用,提高了性能。
6.1 线程池创建
ExecutorService 接口代表线程池
创建方式一般有两种:
- 使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
- 使用Executors调用方法返回不同特点的线程池对象
6.2 ThreadPoolExecutor
6.2.1 ThreadPoolExecutor七个参数
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
创建一个新
ThreadPoolExecutor
给定的初始参数。
一共七个参数
- corePoolSize:指定线程池的数量(核心线程)。不能小于0。
- maximumPoolSize:指定线程池可支持的最大线程数。最大数量>=核心线程数量。
- keepAliveTime指定临时线程的最大存活时间。不能小于0。
- unit:指定存活时间单位(秒、分、时、天)。
- workQueue:指定任务队列。不能为null。
- threadFactor:指定用哪线程工厂创建线程。不能为nul。
- handler:指定线程忙、任务满的时候,新任务来了怎么办。不能为null。
6.2.2 临时线程什么时候创建?什么时候会开始拒绝任务?
- 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
- 核心线程和临时线程都在忙,任务队列也满了,新的任务来的时候才会开始拒绝。
6.2.3 创建线程池对象实例
知道了一些原理过后,我们开始尝试创建对象
ExecutorService pool = new ThreadPoolExecutor(
3,
10,
5,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
拒绝策略:
策略 | |
---|---|
ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出异常(默认) |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,不抛出异常 |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务,然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy | 不进入线程池、由主线程调用任务的run方法 |
使用线程池:
//对于实现了Runnable接口的类
pool.execute(Runnable target);
//与下面这个效果相同
new Thread(Runnable target).start();
//对于实现了Callable接口的类
Future<T> r1 = pool.submit(Callable target);
//获取Call方法的值
r1.get();
6.3 Executors
一个工具类,提供了简单创建线程池的方法。
查看帮助文档,有很多静态方法可以调用。
最常用的是
public static ExecutorService newFixedThreadPool(int nThreads)
6.4 阿里巴巴Java开发手册建议
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
- 说明:Executors 返回的线程池对象的弊端如下: 1) FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2) CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());