多线程
文章目录
1 概述
1.1 线程与进程
1.1.1 进程
- 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间;
- 进程之间互无影响,不共享内存空间,除非通过技术手段使不同进程之间进行通信。
1.1.2 线程
- 是进程中的一个执行路径,同一个进程中的所有线程共享一个内存空间,线程之间可以自由切换,并发执行,一个进程最少有一个线程,没有任何线程则进程关闭;
- 线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。
对于一个程序来说,是一个进程,其只能控制内部的线程的调度,不能去干涉其它的进程,所以说程序是多线程的而非多进程(仅就一般意义上进行特定功能实现的程序而言,可在运行时调用多条进程的大型软件不在此范围内)。
1.2 线程调度
1.2.1 分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
1.2.2 抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。可以调整线程的优先级,优先级越高的线程抢到CPU时间片的概率越高(如 55% 比 45%)。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核芯而言,某个时刻,只能执行一个线程,而CPU的在多个线程间切换速度极高,人类无法有效感知,看上去就像是在同一时刻运行。
其实,对固定的核芯数量CPU来说,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率(运行效率)更高。
如面试问题:(网络编程和数据库) 假设有千人访问数据库服务器,即有千条线程,而数据库的服务器有八个CPU各一个核芯,是线程排队执行(同步)还是由线程抢占CPU时间片(异步)执行快?排队执行速度更快,因线程切换仍会有时间损失。
1.3 同步与异步
- 同步[Synchronous]:排队执行 , 效率低但是数据安全(线程安全)。
- 异步[Asynchronous]:同时执行 , 效率高但是数据不安全(线程不安全)。可能引发数据错乱。如两个线程同时操作一个变量,线程1做完if判断,线程2修改了变量,线程1再使用该变量时将不能保证其可靠性,线程1将数据错乱。
参见:同步(Synchronous)和异步(Asynchronous)
1.4 并发与并行
- 并发:指两个或多个事件在同一个时间段内发生。
一般来说程序可以高并发,指的是在某个时间段内执行多个线程(一天、一小时、一秒内的并发量)。 - 并行:指两个或多个事件在同一时刻发生(同时发生)。
一般来说一个程序很难实现严格的并行,因CPU的处理核心一次只能处理一个线程。使用多个核心可能可以实现并行,但并行的线程数量仅可低于核芯数量。
2 在Java中启用多线程
2.1 继承Tread类
Java提供了Thread类,通过继承Thread类即可调用多线程:
代码实例:
- 创建Thread类的继承类:
public class MyThread extends Thread{
/**
* run方法就是线程要执行的任务方法
*/
@Override
public void run() {
// 这里的代码 就是一条新的执行路径
// 这个执行路径的触发方式不是调用run()方法,
// 而是通过调用Thread对象的start()方法启动任务
// run()方法不需要显式调用
for (int i=0; i<10; i++){
System.out.println("床前明月光" + i);
}
}
}
- 在具有执行入口的类中调用该线程:
public class Main {
// main 方法为主线程
public static void main(String[] args) {
// 开启MyThread线程,
// Thread线程为main线程的分支线程
MyThread thread = new MyThread();
// 使用start()方法触发myTread线程
thread.start();
// 主线程的代码语句:
for (int i=0; i<10; i++){
System.out.println("疑是地上霜" + i);
}
// 主线程和分支线程为并发执行
}
}
输出:
床前明月光0
疑是地上霜0
床前明月光1
疑是地上霜1
床前明月光2
疑是地上霜2
床前明月光3
床前明月光4
床前明月光5
床前明月光6
床前明月光7
床前明月光8
床前明月光9
疑是地上霜3
疑是地上霜4
疑是地上霜5
疑是地上霜6
疑是地上霜7
疑是地上霜8
疑是地上霜9
多次运行的结果均不同。
可见Java中给线程分配CPU时间片是随机且无序的,即CPU时间片是由线程随机抢占的。
线程执行时序图如下:
存在多条线程时,当且仅当所有线程全部执行完毕,没有在执行的线程时,进程结束,即程序结束。
注意:子线程任务中调用的方法,均在子线程中执行,与主线程不冲突:
- 每个线程都拥有自己独立的栈空间,但所有线程共用一份堆内存;
- 各个线程中调用的方法将会在执行时入栈,在执行完毕后弹栈。
2.2 实现Runnable接口
Java中创建并启用线程的更常用方式。
Java提供了Runnable接口,通过实现该接口,可以进行线程的创建和启用:
- 实现Runnable接口
public class MyRunnable implements Runnable {
/**
* 用于给线程执行的任务
*/
@Override
public void run() {
for (int i=0; i<5; i++){
System.out.println("锄禾日当午" + i);
}
}
}
- 在执行入口的main方法中启用:
public class Main {
public static void main(String[] args) {
// 0. 实现Runnable接口并写入执行代码
// (此例中:MyRunnable.java)
// 1. 创建任务对象:
// (创建Runnable实现类的对象)
Runnable runnable = new MyRunnable();
// 2. 创建一个线程,
// 传入上一步创建的任务对象
// (为其分配一个任务)
Thread thread = new Thread(runnable);
// 3. 执行该线程
thread.start();
// 主线程main执行代码
for (int i=0; i<5; i++){
System.out.println("汗滴禾下土" + i);
}
}
}
输出:(每次执行的输出结果大概率并不相同)
锄禾日当午0
汗滴禾下土0
锄禾日当午1
汗滴禾下土1
汗滴禾下土2
汗滴禾下土3
汗滴禾下土4
锄禾日当午2
锄禾日当午3
锄禾日当午4
2.3 继承Thread与实现Runnable对比
实现Runnable与继承Thread相比,有如下优势:
- 通过创建任务,然后给线程分配的方式实现多线程,更适合多个线程同时执行相同任务的情况;
- 可以避免Java不允许单继承所带来的局限性,而多实现是允许的,同时还可以继承其它的类;
- 任务与线程本身是分离的,提高了程序的健壮性;
- 线程池技术(参见 本文档线程池部分),可接受Runnable类型的任务,而不接收Tread类型的线程。
而继承Thread除以上劣势外,与实现Runnable相比有一个很简单的使用匿名内部类创建线程的方式,例如以下代码:
public class SimpleWayForThread {
public static void main(String[] args) {
// 通过声明Thread匿名内部类
// 并直接填写run()方法的方式
// 快速创建线程类
// 匿名内部类只能使用一次
// 故该线程只能执行一次
new Thread(){
@Override
public void run() {
for (int i=0; i<5; i++){
System.out.println("一二三四五" + i);
}
}
}.start();
for (int i=0; i<5; i++){
System.out.println("上山打老虎" + i);
}
}
}
运行输出:(每次运行结果都不同)
一二三四五0
上山打老虎0
一二三四五1
上山打老虎1
一二三四五2
上山打老虎2
上山打老虎3
上山打老虎4
一二三四五3
一二三四五4
使用匿名内部类创建线程的代码也可以简写为lambda表达式:
public class SimpleWayForThread {
public static void main(String[] args) {
new Thread(() -> {
for (int i=0; i<5; i++){
System.out.println("一二三四五" + i);
}
}).start();
for (int i=0; i<5; i++){
System.out.println("上山打老虎" + i);
}
}
}
2.4 实现Callable接口
上面的两种多线程启动方式所创建的线程均为独立的执行路径,与其它的线程(包括主线程和父线程)互不干涉。
而Callable接口实现的线程具备返回值,启动Callable的父线程或主线程可以在Callable线程执行完毕后获得其返回值。Callable接口实现的线程也可以不提供返回值,如其它线程一般使用。
2.4.1 使用方式
- 编写
Callable
接口实现类(注意需要指定一个泛型作为返回值的类型); - 创建第一步编写的
Callable
实现类的对象; - 创建
FurtureTask
对象,并传入第二步创建的Callable
实现类对象; - 创建
Thread
对象,传入第三步创建的FurtureTask
对象,并启动线程。
代码实例:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class TryCallable {
public static void main(String[] args) throws InterruptedException {
Callable<Integer> c = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(c);
new Thread(task).start();
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
System.out.println(i);
}
}
static class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
System.out.println(i);
}
return 100;
}
}
}
输出:
0
0
1
1
2
2
3
3
4
4
可见是并发执行。
2.4.2 Callable获取返回值
Callalble
接口支持返回执行结果,需要调用FutureTask.get()
得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
将上面的代码实例修改为如下代码:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class TryCallable {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Callable<Integer> c = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(c);
new Thread(task).start();
Integer integer = task.get();
System.out.println("返回值为:" + integer);
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
System.out.println(i + " (" + Thread.currentThread().getName() + ")");
}
}
static class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
System.out.println(i + " (" + Thread.currentThread().getName() + ")");
}
return 100;
}
}
则main线程将在task.get()
处等待运行Callable的Task的子线程执行完毕(阻塞),输出:
0 (Thread-0)
1 (Thread-0)
2 (Thread-0)
3 (Thread-0)
4 (Thread-0)
返回值为:100
0 (main)
1 (main)
2 (main)
3 (main)
4 (main)
2.4.3 Future<V>类的其它方法
使用Callable创建线程需要使用的FutureTask<T>
类实现了Future<V>
接口,以下FutureTask<V>
对象可以调用的方法实际上是Future<V>
接口的常用方法:
boolean isDone()
判断线程任务是否执行完毕,如果执行完毕返回true
,否则返回false
;主线程可以通过该方法判断子线程的任务是否执行完毕,从而判断使用get()
方法的时机,避免阻塞耗费太多时间而提高效率,或者如果等待时间太长可以使用下面的方法取消线程任务;boolean cancel(boolean mayInterruptIfRunning)
取消线程任务,传入true则表示线程任务在运行时可以被取消,将强制取消线程任务;返回true则取消成功,返回false取消失败,绝大多数情况下返回false是因为线程任务已经执行完毕,无法取消。
2.4.4 比较Callable与Runnable
2.4.2.1 接口定义
-
Callable接口:
public interface Callable<V> { V call() throws Exception; }
-
Runnable接口:
public interface Runnable { public abstract void run(); }
2.4.2.2 相同点
- 都是接口;
- 都可以编写多线程程序;
- 都使用
Thread.start()
启动线程。
2.4.2.3 不同点
Runnable
没有返回值;Callable
可以返回执行结果;Callable
接口的call()
允许抛出异常;Runnable
的run()
不能抛出。
3 Tread类解析
无论是继承Tread类也好,还是实现Runnable接口也好,都需要使用到Thread类。Thread类即为Java提供的线程。
3.1 (常用)构造方法
Thread()
创建线程,此时任务未指定;Thread(Runnable target)
创建线程并传入Runnable的实现类对象(可以为Runnable类型也可以是实现类型)以对该线程指定任务;Thread(Runnable target, String name)
创建线程并传入Runnable和String对象,以给该线程指定任务并为该线程命名,可通过getName()
方法获取该线程的名称;Thread(String name)
创建线程,不指定任务,但为其命名,可通过getName()
方法获取该线程的名称;
3.2 (常用)方法和静态方法
-
void setName()
指定线程的名称; -
String getName()
返回该线程的名称; -
long getID()
返回线程的标识符; -
void setPriority(int new Priority)
设置线程优先级 (另参见 本文档线程优先级部分); -
int getPriority()
获取线程优先级 (另参见 本文档线程优先级部分); -
void start()
启动该线程; -
static void sleep(long millis)
使线程休眠(暂时停止执行),传入毫秒值 (1 s = 1000 millisecond),比如使程序每隔1s输出1个数字,则需要调用sleep(1000)
使线程休眠1s,然后再输出; -
static void sleep(long millis, int nanos)
使线程休眠(暂时停止执行),传入毫秒值和纳秒值 (1 millisecond = 1000000 nanosecond); -
void setDaemon(boolen on)
标记是否启用为守护线程,若为是(true
)则启用为守护线性,若为否(false
)则启用为用户线程(详见 守护线程 ); -
static Thread currentThread
返回调用此方法的(当前)线程实例对象的引用; -
void interrupt()
给该线程进行中断标记(详见 线程的中断);
方法实例
- 设置以及获取线程名称:(
currentThread()
与setName()
以及getName()
方法的使用)
public class SetGetThreadName {
public static void main(String[] args) {
// 获取线程的名称
System.out.println(Thread.currentThread().getName());
new Thread(new MyRunnable(), "锄禾日当午").start();
}
static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
}
运行输出:
main
锄禾日当午
若不指定名称:
...
new Thread(new MyRunnable()).start();
...
输出将改为:
main
Thread-0
可见子线程默认名称为Thread-0
,若有多个线程:
...
new Thread(new MyRunnable()).start();
new Thread(new MyRunnable()).start();
new Thread(new MyRunnable()).start();
...
则输出为:
main
Thread-0
Thread-2
Thread-1
线程的执行顺序是随机的。
上述代码是使用匿名对象的方式创建了Thread对象,当然也可以将其声明出来,再调用各种方法:
...
System.out.println(Thread.currentThread().getName());
Thread thread = new Thread(new MyRunnable());
thread.setName("我的线程");
thread.start();
new Thread(new MyRunnable()).start();
new Thread(new MyRunnable()).start();
...
输出:
main
我的线程
Thread-1
Thread-2
- 线程休眠:(
sleep()
方法)
public class GoToBed {
public static void main(String[] args) throws InterruptedException {
for(int i=0; i<10; i++){
System.out.println(i);
Thread.sleep(1000);
}
}
}
注意需要处理InterruptedException
。
3.3 线程优先级
为Thread类中的静态常量成员参数,该参数可控制线程抢到CPU时间片的几率,源码如下:
/**
* The minimum priority that a thread can have.
*/
public static final int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public static final int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public static final int MAX_PRIORITY = 10;
即默认优先级NORM_PRIORITY
数值为整型5、最大优先级MAX_PRIORITY
为整型10、最小优先级MIN_PRIORITY
为整型1,优先级取值为[1,10],如在上面的任意代码中的thread.start()
方法前加入以下代码行:
...
// 输入任意[1,10]内正整数
tread.setPriority(2);
...
或:
...
// 调用Thread类中的静态常量
tread.setPriority(Thread.MIN_PRIORITY);
...
则分支线程的优先级将被设置为极低,从程序运行输出来看造成的结果: 除极少数概率较低的情况外,main方法中的代码行大概率将先于分支线程的代码行执行,main方法的大部分代码将于分支线程代码之前执行完毕。
注意:
-
main方法的默认优先级为5,可以通过以下代码查看main线程的(或者任意当前线程的)优先级:
System.out.println(Thread.currentThread().getPriority());
-
也可以通过以下代码修改main线程的(或者任意当前线程的)优先级(此例中设置为10):
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
-
线程实例对象的(默认)优先级继承于其父线程(创建该线程的线程)的优先级;并且,若要使优先级继承在父线程修改优先级后仍然生效,父线程的优先级更改必须在声明该子线程实例对象之前,否则子线程实例对象将保持父线程修改前的优先级,如:
(
MyRunnable
实现类代码见本文档 实现Runnable
接口 部分)public static void main(String[] args) { System.out.println(Thread.currentThread().getPriority()); // 设置main线程优先级为10 Thread.currentThread().setPriority(Thread.MAX_PRIORITY); Runnable runnable = new MyRunnable(); // 声明子线程(分支线程) Thread tread = new Thread(runnable); tread.start(); System.out.println(Thread.currentThread().getPriority()); System.out.println(tread.getPriority()); for (int i=0; i<5; i++){ System.out.println("汗滴禾下土" + i); } }
输出为:
5 10 10 汗滴禾下土0 锄禾日当午0 汗滴禾下土1 锄禾日当午1 汗滴禾下土2 锄禾日当午2 汗滴禾下土3 锄禾日当午3 汗滴禾下土4 锄禾日当午4
若将代码更改为:
public static void main(String[] args) { System.out.println(Thread.currentThread().getPriority()); Runnable runnable = new MyRunnable(); // 声明子线程(分支线程) Thread tread = new Thread(runnable); // 设置main线程优先级为10 Thread.currentThread().setPriority(Thread.MAX_PRIORITY); tread.start(); System.out.println(Thread.currentThread().getPriority()); System.out.println(tread.getPriority()); for (int i=0; i<5; i++){ System.out.println("汗滴禾下土" + i); } }
则输出为:
5 10 5 汗滴禾下土0 汗滴禾下土1 汗滴禾下土2 汗滴禾下土3 汗滴禾下土4 锄禾日当午0 锄禾日当午1 锄禾日当午2 锄禾日当午3 锄禾日当午4
相关的Thread类源码:
private Thread(...){ ... Thread parent = currentThread(); ... this.priority = parent.getPriority(); }
(参见:Thread之五:线程的优先级)
4 线程阻塞与线程的中断
4.1 线程阻塞(耗时操作)
线程的休眠只是线程阻塞的一种情况。
比如一个线程有100行执行代码,中间有10行用以读取某个文件,耗时1s,这1秒钟实际上也是阻塞的,因为线程代码执行暂时停止在读取文件的这个操作上了。
因此可以把线程阻塞简单理解为所有(比较)消耗时间的操作:比如文件读取,会导致代码不会继续执行,而是等待,直到文件读取完成,获得结果;再比如接收用户输入,线程会等待用户输入内容,直到用户输入内容并(回车)确认,线程获得用户输入的内容,才会继续执行代码,如果用户一直不输入,则该线程将一直等待。
4.2 线程的中断
一个线程是一个独立的执行路径,它是否应该结束,应该由其自身决定。
因线程在执行过程中会使用系统资源,如果外部强制将其中断,则很可能造成线程被中断时其使用的数据资源未被释放,从而产生内存垃圾,以这种方式产生的垃圾是不能被回收的;也有可能线程占用了某些硬件资源(如蓝牙等),强制中断将导致这些硬件资源一直处于被占用的状态得不到释放,即使占用该硬件资源的线程已经被强制中断了,其它线程也将无法使用这些硬件资源。
面试问题:如何正确关闭线程?
早期版本的JDK的void stop()
方法已被弃用,因该方法不安全,有可能导致该线程使用的某些资源未来得及释放即被stop,不能使用该方法关闭线程。
关闭线程正确的方式为:通知线程其需要结束 => 线程接到通知后进行自我”销毁“ (使run()
方法强制return
),如以下流程:
- 定义整型变量初赋值为1(进行“标记”);
- 线程执行过程中使其不断观察该变量值是否改变;
- 若该变量值改变,比如重新赋值为-1,则进行各种资源的释放等操作后,使
run()
方法强制return
。
也可以使用void interrupt()
方法对线程进行中断标记:
代码实例:
public class StopThread {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyRunnable());
t1.start();
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
for(int i=1; i<=3; i++){
System.out.println(Thread.currentThread().getName() + ":" + i);
Thread.sleep(1000);
}
// 假设有如下需求:
// 如果主线程代码执行完毕,
// 不管子线程是否执行完毕,
// 子线程都需要被中断
// 此时需要 给线程t1添加中断标记
// 添加了中断标记的异常不一定会被中断
// 若线程中存在sleep()方法的try-catch块
// 能够触发该标记的还有其它方法,具体见API Docs
// 程序员需要在catch块中决定该线程是否确实被中断
t1.interrupt();
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=1; i<=5; i++){
System.out.println(Thread.currentThread().getName() + ":" + i);
// 注意:
// run()方法无法将异常抛出
// 因Runnable接口没有声明异常抛出
// 实现类也就不能抛出异常
// (实现类异常范围不能扩大)
// 故在run()方法内的异常只能进行处理
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// InterruptedException: 线程中断的异常
// 若添加了中断标记,则该线程在执行
// Thread.sleep(1000)时将生成并抛出
// InterruptedException
// 并进入catch块
// 由程序员决定是否在catch块中将该线程中断
// 若不进行中断:
System.out.println("发现中断标记,但我不想中断hhh");
// 则线程正常执行
// 若进行中断操作,则进行return即可:
// return;
}
}
}
}
}
interrupt()
方法的作用仅仅是给该线程做中断标记,告知该线程其有可能被终止,具体中止的操作在catch块中给出:可以不中止线程;也可以进行释放资源等操作然后中止线程。
5 守护线程(daemon thread)和用户线程(user thread)
从入口线程为主线程,主线程开启的线程为分支线程的角度,线程区分为父子线程;
从线程的生命周期的角度来看,线程区分为守护线程(daemon thread)与用户线程(user thread):
- 守护线程(daemon thread):顾名思义为“守护”用户线程的线程(英语中daemon意为“守护神”),是“依附”在用户线程上的线程,当在当前进程内的所有的用户线程各自的代码完全执行完毕了,最后一个用户线程结束时,所有守护线程便自行“消亡”,守护线程何时“死亡”,不由其自身掌控;
- 用户线程(user thread):如上述代码实例中的线程,main线程和其开启的子线程(分支线程),当且仅当两者各自的全部代码完全执行完毕后,该进程才会结束,有任何一个线程仍在执行,则进程继续执行,这种线程称为用户线程,用户线程是否“死亡”,由线程本身掌控。
设置守护线程需要调用setDaemon(boolean on)
方法,必须在线程启动start()
之前调用该方法进行设置。
public class DaemonThread {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyRunnable());
// 设置为守护线程
t1.setDaemon(true);
t1.start();
for(int i=1; i<=5; i++){
System.out.println(Thread.currentThread().getName() + ":" + i);
Thread.sleep(1000);
}
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=1; i<=10; i++){
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
输出:
Thread-0:1
main:1
Thread-0:2
main:2
Thread-0:3
main:3
Thread-0:4
main:4
Thread-0:5
main:5
Thread-0:6
即main线程输出完成后子线程运行一次(此时main线程尚未结束)后也结束,尽管尚有输出操作未完成。
6 线程安全与线程不安全
6.1 线程不安全
多个线程在同时运行,很容发生线程不安全的问题。
如经典案例:卖票,代码如下:
public class ThreadUnsafe {
public static void main(String[] args) {
// 线程不安全演示
// 经典案例:卖票
// 卖票操作交予3个线程执行
Runnable run = new TicketSell();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class TicketSell implements Runnable{
// 票数
private int count = 5;
@Override
public void run() {
// 调用该任务时,在此进行卖票操作
while (count>0){
// 卖票
System.out.println("正在准备卖票("
+ Thread.currentThread().getName()
+ ")");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("出票成功,余票" + count
+ "张(" + Thread.currentThread().getName()
+ ")");
}
}
}
}
输出:(重复运行,每次输出的结果很大概率不相同)
正在准备卖票(Thread-0)
正在准备卖票(Thread-1)
正在准备卖票(Thread-2)
出票成功,余票4张(Thread-0)
正在准备卖票(Thread-0)
出票成功,余票3张(Thread-1)
正在准备卖票(Thread-1)
出票成功,余票2张(Thread-2)
正在准备卖票(Thread-2)
出票成功,余票1张(Thread-0)
正在准备卖票(Thread-0)
出票成功,余票0张(Thread-1)
出票成功,余票-1张(Thread-2)
出票成功,余票-2张(Thread-0)
可以看到,出现了余票数为-1和-2张的情况,解释如下:
该例很好地体现出线程不安全的问题。假设卖票流程中,进行到余票仅剩余1张时,即上述输出结果的出票成功,余票1张(Thread-0)
时间点,此时:
- Thread-1"正在准备卖票(sleep)"且已经等待了一段时间,sleep内Thread-1丢失了CPU时间片,此后Thread-2和Thread-0分别抢得CPU时间片完成了两次卖票操作(分别余票2张和1张);
- Thread-2"正在准备卖票(sleep)"且也已经等待一段时间,但等待时间比Thread-1短,sleep内Thread-2丢失了CPU时间片,此后Thread-0抢得CPU时间片完成了一次卖票操作(余票1张);
- Thread-0刚刚完成上一个出票成功操作,即将开始"正在准备卖票(sleep)";
即此时三个线程均处于while循环体内的Thread.sleep(1000)
语句位置,count == 1,对于这三个线程来说,此时while
循环的判断(count>0
)均已经通过,故该判断已经失效,此后:
- Thread-1首先抢得CPU时间片完成了
出票成功,余票0张(Thread-1)
,count == 0; - Thread-2在Thread-1之后抢得CPU时间片,虽然此时余票已经是0张,因已经不存在必须票数大于0的判断,Thread-2仍然将count减1,完成了
出票成功,余票-1张(Thread-2)
; - Thread-0最后抢得CPU时间片,虽然此时余票已经是-1张,因已经不存在必须票数大于0的判断,Thread-0仍然将count减1,完成了
出票成功,余票-2张(Thread-0)
。
一个进程内的多个线程同时操作同一个变量完成同一段代码逻辑,则极易出现上述的问题。
就上述实例来说,出现问题的根本在于,一个线程变量的判断和变量的值的修改之间,由其它的线程插足更改了变量的值,导致其不再可靠。解决该问题的方法是将该变量在该线程机型变量的判断和变量的值的修改之间进行锁定,不允许其它线程操作。即排队执行。
6.2 线程安全
当前有三种不同的加锁机制以实现针对上述问题的线程安全的实现。其中的两种使用了同步(synchronized)。
6.2.1 (隐式锁)同步代码块
可以简单理解为,由同步代码块包裹的内容(代码)将同步(排队)执行,格式:
synchronized(锁对象){
...
}
需要传入一个锁对象。任何对象均可以是锁对象,即任何对象均可以打上"锁"的标记,该对象仅为具有"锁"标记的对象,象征为一把锁,不需要具有其它的代码逻辑,如何打上"锁"的标记为内存底层的操作。任何要执行被synchronized代码块包裹的代码的线程都将观察该对象,若该对象为上锁的,则表示该代码块由其它线程执行中,该后至的线程将等待;当执行该代码块的线程执行结束,将把锁对象的状态更改为解锁,则等待的线程将争抢该锁,抢得锁的线程将其加锁并执行代码块,其它线程继续等待,如此循环。
注意:多个线程执行同一个同步代码块时,必须使用同一个锁对象,否则依然线程不安全。
上面代码应用该机制可改写为:
public class ThreadSafeSyncBlock {
public static void main(String[] args) {
// 同步代码块
// 格式:synchronized(锁对象){}
Runnable run = new TicketSell();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class TicketSell implements Runnable{
private int count = 5;
// 声明锁对象
private final Object o = new Object();
@Override
public void run() {
// 注意锁对象的声明不能出现在这个位置,
// 否则将相当于每个线程都有一个锁对象
// 同步代码块将因此而失效
while (true){
synchronized (o){
if (count>0){
System.out.println("正在准备卖票("
+ Thread.currentThread().getName()
+ ")");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("出票成功,余票" + count
+ "张(" + Thread.currentThread().getName()
+ ")");
} else {
break;
}
}
}
}
}
}
输出:
正在准备卖票(Thread-0)
出票成功,余票4张(Thread-0)
正在准备卖票(Thread-1)
出票成功,余票3张(Thread-1)
正在准备卖票(Thread-1)
出票成功,余票2张(Thread-1)
正在准备卖票(Thread-2)
出票成功,余票1张(Thread-2)
正在准备卖票(Thread-2)
出票成功,余票0张(Thread-2)
可以看到不再出现负值的余票数。但程序运行时间显著增长了。
6.2.2 (隐式锁)同步方法
在Runnable的实现类中添加方法,并且在方法的返回值前添加synchronized关键字,即可使方法同步(排队执行):
public class ThreadSafeSyncFunc {
public static void main(String[] args) {
// 同步方法
// 格式:在方法声明的返回值之前添加synchronized关键字
Runnable run = new TicketSell();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class TicketSell implements Runnable{
// 票数
private int count = 5;
private final Object o = new Object();
@Override
public void run() {
while (true){
boolean flag = sale();
if (!flag) {
break;
}
}
}
private synchronized boolean sale(){
if (count>0){
System.out.println("正在准备卖票("
+ Thread.currentThread().getName()
+ ")");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("出票成功,余票" + count
+ "张(" + Thread.currentThread().getName()
+ ")");
return true;
}
return false;
}
}
}
输出:
正在准备卖票(Thread-0)
出票成功,余票4张(Thread-0)
正在准备卖票(Thread-0)
出票成功,余票3张(Thread-0)
正在准备卖票(Thread-2)
出票成功,余票2张(Thread-2)
正在准备卖票(Thread-1)
出票成功,余票1张(Thread-1)
正在准备卖票(Thread-2)
出票成功,余票0张(Thread-2)
注意:同步方法也是锁的应用,非静态同步方法的锁为调用该方法的对象this,静态同步方法的锁为类名.Class
(字节码文件)。上述代码实例中,同步方法为非静态同步方法,该同步方法的锁为通过下面语句声明的run
实例对象:
Runnable run = new TicketSell();
若上述代码实例中为静态同步方法,则锁为TicketSell.class
。
若将上述代码main方法更改为:
...
// Runnable run = new TicketSell();
new Thread(new TicketSell()).start();
new Thread(new TicketSell()).start();
new Thread(new TicketSell()).start();
...
则相当于每个线程有自己的锁,同步失效。
注意:若同步代码块锁了一部分代码,同步方法锁了一部分代码,两者使用同一个锁对象this,则占用该锁的线程将同时占用同步代码块和同步方法,其它线程将无法访问这两部分的代码;若有其它同步方法,因其锁也是this,同样将被占用。
6.2.3 (显式锁) Lock 子类 ReentrantLock
在Runnable
实现类的run()
方法之前声明私有不可变Lock
的子类ReentrantLock
的实例化对象:
private final Lock l = new ReentrantLock();
在需要加锁的代码块之前使用lock()
方法加锁:
l.lock();
在被加锁的代码块之后使用unlock()
方法解锁:
l.unlock();
代码实例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadSafeReentrantLock {
public static void main(String[] args) {
// 显式锁
// Lock 子类 ReentrantLock
Runnable run = new TicketSell();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class TicketSell implements Runnable{
private int count = 5;
// 声明显式锁对象
private final Lock l = new ReentrantLock();
@Override
public void run() {
while (true){
l.lock();
if (count>0){
System.out.println("正在准备卖票("
+ Thread.currentThread().getName()
+ ")");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("出票成功,余票" + count
+ "张(" + Thread.currentThread().getName()
+ ")");
} else {
break;
}
l.unlock();
}
}
}
}
输出:
正在准备卖票(Thread-0)
出票成功,余票4张(Thread-0)
正在准备卖票(Thread-0)
出票成功,余票3张(Thread-0)
正在准备卖票(Thread-1)
出票成功,余票2张(Thread-1)
正在准备卖票(Thread-1)
出票成功,余票1张(Thread-1)
正在准备卖票(Thread-1)
出票成功,余票0张(Thread-1)
(程序不能正确退出,卡在此处)
问题:程序执行上述代码不能正常退出。为解决该问题,对代码做以下修改:
- 在lock语句前设置输出语句:
System.err.println("locked!");
- 在unlock语句后设置输出语句:
System.err.println("unlocked!");
- 在break语句前设置输出语句:
System.out.println("break!");
- 在while代码块后设置输出语句:
System.out.println("while loop broken!");
观察多次输出:
locked!
locked!
locked!
正在准备卖票(Thread-0)
出票成功,余票4张(Thread-0)
正在准备卖票(Thread-2)
unlocked!
locked!
出票成功,余票3张(Thread-2)
正在准备卖票(Thread-1)
unlocked!
locked!
出票成功,余票2张(Thread-1)
正在准备卖票(Thread-0)
unlocked!
locked!
出票成功,余票1张(Thread-0)
正在准备卖票(Thread-2)
unlocked!
locked!
出票成功,余票0张(Thread-2)
break!
while loop broken!
unlocked!
locked!
(程序不能正确退出,卡在此处)
locked!
locked!
locked!
正在准备卖票(Thread-2)
出票成功,余票4张(Thread-2)
正在准备卖票(Thread-2)
unlocked!
locked!
出票成功,余票3张(Thread-2)
正在准备卖票(Thread-1)
unlocked!
locked!
出票成功,余票2张(Thread-1)
正在准备卖票(Thread-0)
unlocked!
locked!
出票成功,余票1张(Thread-0)
正在准备卖票(Thread-2)
unlocked!
locked!
出票成功,余票0张(Thread-2)
break!
while loop broken!
unlocked!
locked!
(程序不能正确退出,卡在此处)
locked!
locked!
locked!
正在准备卖票(Thread-1)
出票成功,余票4张(Thread-1)
正在准备卖票(Thread-2)
unlocked!
locked!
出票成功,余票3张(Thread-2)
正在准备卖票(Thread-0)
unlocked!
locked!
出票成功,余票2张(Thread-0)
正在准备卖票(Thread-1)
unlocked!
locked!
出票成功,余票1张(Thread-1)
正在准备卖票(Thread-2)
unlocked!
locked!
出票成功,余票0张(Thread-2)
break!
while loop broken!
unlocked!
locked!
(程序不能正确退出,卡在此处)
可知问题在于输出的最后locked!
,此时代码块加锁,但没有线程给它解锁(break跳出了整个while,导致unlock语句也无法执行),于是线程均处于等待锁的状态,程序无法退出。
解决方案:
在l.lock();
语句后添加if判断:
...
if (count == 0){
l.unlock();
System.err.println("unlocked!");
}
...
运行程序,程序已经可以正常退出:
locked!
locked!
locked!
正在准备卖票(Thread-0)
出票成功,余票4张(Thread-0)
正在准备卖票(Thread-2)
unlocked!
locked!
出票成功,余票3张(Thread-2)
正在准备卖票(Thread-1)
unlocked!
locked!
出票成功,余票2张(Thread-1)
正在准备卖票(Thread-0)
unlocked!
locked!
出票成功,余票1张(Thread-0)
正在准备卖票(Thread-2)
unlocked!
locked!
出票成功,余票0张(Thread-2)
unlocked!
unlocked!
break!
while loop broken!
break!
while loop broken!
unlocked!
locked!
unlocked!
break!
while loop broken!
更改后可正常运行的代码:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadSafeReentrantLock {
public static void main(String[] args) {
// 显式锁
// Lock 子类 ReentrantLock
Runnable run = new TicketSell();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class TicketSell implements Runnable{
private int count = 5;
// 声明显式锁对象
private final Lock l = new ReentrantLock();
@Override
public void run() {
while (true){
System.err.println("locked!");
l.lock();
if (count == 0){
l.unlock();
System.err.println("unlocked!");
}
if (count > 0) {
System.out.println("正在准备卖票("
+ Thread.currentThread().getName()
+ ")");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("出票成功,余票" + count
+ "张(" + Thread.currentThread().getName()
+ ")");
} else {
System.err.println("break!");
break;
}
l.unlock();
System.err.println("unlocked!");
}
System.err.println("while loop broken!");
}
}
}
使用显式锁更契合于java的面向对象性。
6.3 公平锁与非公平锁
-
公平锁:先来先到,正常排队;
-
非公平锁:抢锁,抢得的线程优先执行,不管先来后到;
Java中的隐式锁均为非公平锁,有更好的性能。
显式锁ReentrantLock
可通过传入布尔型参数为true
的方式转变为公平锁(默认为非公平锁):
// 不传参则默认为false
private Lock l = new ReentrantLock(true);
6.4 线程死锁
两个线程互相等待对方释放资源,造成线程死锁。
如以下代码实例:
public class DeadLock {
public static void main(String[] args) {
// 线程死锁
Culprit c = new Culprit();
Police p = new Police();
new MyThread(c,p).start();
c.say(p);
}
static class MyThread extends Thread{
private Culprit c;
private Police p;
public MyThread(Culprit c, Police p){
this.c = c;
this.p = p;
}
@Override
public void run() {
p.say(c);
}
}
static class Culprit{
public synchronized void say(Police p){
System.out.println("罪犯:你放了我,我放人质");
p.reply();
}
public synchronized void reply(){
System.out.println("罪犯被放走了,罪犯也放了人质");
}
}
static class Police{
public synchronized void say(Culprit c){
System.out.println("警察:你放了人质,我放了你");
c.reply();
}
public synchronized void reply(){
System.out.println("警察救了人质,但是罪犯跑了");
}
}
}
有较大几率程序卡死在如下输出之后:
罪犯:你放了我,我放人质
警察:你放了人质,我放了你
(程序卡住不能退出)
因c.say(p)
需要调用p.reply()
,p.say(c)
需要调用c.reply()
,所有的方法都是synchronized
,很容易造成线程Thread-0
的p
对象等待正被main
线程使用的c
对象的reply()
,而同时main
线程的c
对象等待线程Thread-0
的p
对象的reply()
,陷入DeadLock
。
若程序正常输出:
罪犯:你放了我,我放人质
警察救了人质,但是罪犯跑了
警察:你放了人质,我放了你
罪犯被放走了,罪犯也放了人质
则表明有一条线程完全执行完毕之后,另一条线程才开始执行,如上面的输出,是main线程先执行完毕了,Thread-0才开始执行。一般CPU主频比较低核芯数和线程数比较少的电脑上会比较容易出现这种结果。
如何避免线程死锁:
在任何有可能导致锁产生的方法里,不要再调用另外一个可能产生锁的方法。
7 线程通信问题
如一个进程中,A线程负责下载音乐,B线程负责播放,则需要A线程下载完毕后(无缓存和分段下载机制)通知B线程进行播放,此时需要A线程与B线程之间进行通信。需要使用Object
类的方法:
wait()
特定对象在当前线程调用该方法,则当前线程等待,直到被通知;wait(long timeoutMillis)
特定对象在当前线程调用该方法,则当前线程等待,直到被通知,经过传入参数的时间长度后自动解除等待状态;wait(long timeoutMillis, int nanos)
特定对象在当前线程调用该方法,则当前线程等待,直到被通知,经过传入参数的时间长度后自动解除等待状态;notify()
随机通知一个因特定对象在线程内调用wait()
方法而处于等待状态的线程解除等待状态;notifyAll()
通知所有因特定对象在线程内调用wait()
方法而处于等待状态的线程解除等待状态;
如在线程A中对象a调用wait()
方法,该线程立即等待;当到达时间长度(若等待时wait()
传入参数),或者在A以外的其它线程中,相同的对象a调用notify()
,则A线程将被通知(假设当前只有A线程因对象a在其中调用了wait()
而等待),A线程解除等待状态。若对象a在多个线程中调用了wait()
,则a在未处于的等待状态的线程中调用notifyAll()
方法将通知所有因对象a在线程内调用了wait()
方法而等待的线程。
经典案例:生产者与消费者问题
如上述例子中,A为产生数据方,即生产者;B为使用数据方,即消费者。程序开始时,消费者先处于等待状态,生产者执行完毕生产后通知消费者,消费者使用数据(此时生产者处于等待状态),消费者使用数据完毕通知生产者解除等待状态,以此循环。该机制能确保生产者在产生数据(生产)时,消费者不会在使用数据(消费);且消费者在消费时,生产者没有在生产数据。严格确保数据安全,不考虑效率问题。
代码实例
场景:
- “厨师 (Cook)” - 生产者;
- “服务员 (Waiter)” - 消费者;
- “食物 (Food)” - 数据。
厨师烹饪完毕食物,唤醒服务员取出食物上菜并准备新的食物容器(厨师此时wait),服务员完成操作后唤醒厨师继续烹饪食物(服务员wait),以此循环。
演示无任何线程安全和生产者消费者机制设置情况下的案例:(两个线程在协同工作时不协调的问题)
public class CookAndWaiter {
/**
* 生产者与消费者问题案例演示
* 无机制设置和线程安全设置
*/
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
// 厨师
// 循环生产10份菜,两种,各5份
static class Cook extends Thread{
private Food f;
public Cook(Food f){
this.f = f;
}
@Override
public void run() {
for (int i=0; i<10; i++){
if (i%2==0){
f.setNameAndTaste("鱼香肉丝", "鲜咸");
} else {
f.setNameAndTaste("拔丝地瓜", "甜");
}
}
}
}
// 服务员
static class Waiter extends Thread{
private Food f;
public Waiter(Food f){
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.consume();
}
}
}
// 食物
// (实际上更像是食物的容器)
// 厨师将修改该类对象(只有一个)
// 的name和taste属性
// 服务员接收并处理
static class Food{
private String name;
private String taste;
public void setNameAndTaste(String name, String taste) {
this.name = name;
// 演示若不使用生产者与消费者机制
// 线程通信会产生的问题
// 以下代码使该线程更容易丢失CPU时间片
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
}
public void consume() {
// 数据会发生错乱
System.out.println("服务员端走了菜:" + name + ", 味道:" + taste);;
}
}
}
输出:
服务员端走了菜:鱼香肉丝, 味道:null
服务员端走了菜:鱼香肉丝, 味道:甜
服务员端走了菜:拔丝地瓜, 味道:鲜咸
服务员端走了菜:鱼香肉丝, 味道:甜
服务员端走了菜:拔丝地瓜, 味道:鲜咸
服务员端走了菜:鱼香肉丝, 味道:甜
服务员端走了菜:拔丝地瓜, 味道:鲜咸
服务员端走了菜:鱼香肉丝, 味道:甜
服务员端走了菜:拔丝地瓜, 味道:鲜咸
服务员端走了菜:拔丝地瓜, 味道:甜
可以看到出现了数据的严重混乱,味道:null
表明此时厨师线程完成了对Food
对象name
属性的初赋值但尚未完成对taste
属性的初赋值,服务员线程就将其拿走consume了,此后也出现了拔丝地瓜, 味道:鲜咸
和鱼香肉丝, 味道:甜
这种不符合设定的情形,表明此时厨师线程尚未完成对taste
属性的再赋值,服务员线程就将其拿走consume了,此时Food
对象的taste
属性仍为上一次厨师对象对Food
对象taste
属性的赋值。
在这种情况下,因Cook线程和Waiter线程处理的是同一个Food对象,如果将对象的两个方法设置为synchronized,不但不能解决上面的数据混乱问题,反而会带来更大的问题:数据丢失和数据不合理重复。
Java中的锁均默认为非公平锁以保证性能,在此前提下:
假设Cook线程调用setNameAndTaste(name, taste)
方法完成了赋值,将该方法解锁后,又立即获得了CPU时间片,再度给该方法加锁,加锁该方法同时将导致consume()
方法被锁定,Waiter线程继续等待,而Cook线程在Food对象的上一组属性数据仍未被consume掉的情况下仍会修改该对象的属性进行“烹饪”,从而导致Food对象的的属性数据上一组属性数据未曾被使用就被擦除替换,造成数据丢失。
假设Waiter线程调用consume()
方法完成了上菜(控制台打印),将该方法解锁后,又立即获得了CPU时间片,再度给该方法加锁,加锁该方法同时将导致setNameAndTaste(name, taste)
方法被锁定,Cook线程继续等待,而Waiter线程继续调用consume()
方法,此时Food对象的已经“上菜”的属性数据还未经过Cook线程的“烹饪”(再次赋值),Waiter线程此时consume的将是本应不存在的Food对象的属性数据,造成数据不合理重复。
如以上代码中Food的两个方法更改为synchronized:
...
public synchronized void setNameAndTaste(String name, String taste) {...}
...
public synchronized void consume() {...}
...
并给每个Food对象的属性对进行计数:
...
public void run() {
int countFood01 = 0;
int countFood02 = 0;
// 两种菜,各五份
for (int i=0; i<10; i++){
if (i%2==0){
countFood01++;
f.setNameAndTaste("鱼香肉丝" + countFood01, "鲜咸");
} else {
countFood02++;
f.setNameAndTaste("拔丝地瓜" + countFood02, "甜");
}
}
}
...
则输出为:(每次输出大概率不相同)
服务员端走了菜:鱼香肉丝1, 味道:鲜咸
服务员端走了菜:拔丝地瓜1, 味道:甜
服务员端走了菜:鱼香肉丝2, 味道:鲜咸
服务员端走了菜:拔丝地瓜2, 味道:甜
服务员端走了菜:鱼香肉丝3, 味道:鲜咸
服务员端走了菜:拔丝地瓜4, 味道:甜
服务员端走了菜:鱼香肉丝5, 味道:鲜咸
服务员端走了菜:拔丝地瓜5, 味道:甜
服务员端走了菜:拔丝地瓜5, 味道:甜
服务员端走了菜:拔丝地瓜5, 味道:甜
可以看到除编号为1和2的两轮正常外,拔丝地瓜3
、鱼香肉丝4
从未被consume,拔丝地瓜5
被重复consume3次。
从以上的表述可以得出结论,在这种情况下只使用线程安全无法保证不出现问题。
为解决上述问题,只能严格确保在Cook线程进行生产时,Waiter线程必须等待,反之当Waiter线程在消费时,Cook线程必须等待,即使用Object
类的wait()
和notifyAll()
方法:
public class CookAndWaiterModified {
/**
* 生产者与消费者问题案例演示
* 使用wait()和notifyAll()方法
*/
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
static class Cook extends Thread{
private Food f;
public Cook(Food f){
this.f = f;
}
@Override
public void run() {
int countFood01 = 0;
int countFood02 = 0;
for (int i=0; i<10; i++){
if (i%2==0){
countFood01++;
f.setNameAndTaste("鱼香肉丝" + countFood01, "鲜咸");
} else {
countFood02++;
f.setNameAndTaste("拔丝地瓜" + countFood02, "甜");
}
}
}
}
static class Waiter extends Thread{
private Food f;
public Waiter(Food f){
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.consume();
}
}
}
static class Food{
private String name;
private String taste;
// true表示可以生产
// 交给消费者时更改为false
private boolean flag = true;
public synchronized void setNameAndTaste(String name, String taste) {
if (flag){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void consume() {
if (!flag){
System.out.println("服务员端走了菜:" + name + ", 味道:" + taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
输出:
服务员端走了菜:鱼香肉丝1, 味道:鲜咸
服务员端走了菜:拔丝地瓜1, 味道:甜
服务员端走了菜:鱼香肉丝2, 味道:鲜咸
服务员端走了菜:拔丝地瓜2, 味道:甜
服务员端走了菜:鱼香肉丝3, 味道:鲜咸
服务员端走了菜:拔丝地瓜3, 味道:甜
服务员端走了菜:鱼香肉丝4, 味道:鲜咸
服务员端走了菜:拔丝地瓜4, 味道:甜
服务员端走了菜:鱼香肉丝5, 味道:鲜咸
服务员端走了菜:拔丝地瓜5, 味道:甜
(程序不能正确退出,卡在此处,原因未知)
问题:程序执行完上述代码不能正常退出,解释:
程序只有只剩下守护线程的时候才会关闭,程序不能退出说明仍有用户线程在运行,而以上的代码中的两个方法中都有wait()
方法,调用方法的线程在完成输出后进入休眠等待状态,当所有输出都完成了,仍然会有没被唤醒的线程在等待,但此时已经没有其它线程来唤醒休眠线程,导致用户线程不能结束,则程序不能停止。
(临时)解决方法:
将wait()方法传入自动唤醒时间:
// 设定等待3秒自动唤醒
wait(3000);
因代码中的任务没有耗时任务,3秒内如各线程都全无动作,则必定各线程都完成了任务,但全部处于wait状态而无其它线程将其唤醒,此时只需要唤醒线程,通过程序内的正常判断即可退出程序。修改后程序正常运行并正常关闭。
但实际工程环境中,有时无法判断自动唤醒时间究竟该给多少,此时需要使用其它的方法,在此不做进一步论述,可能的解决方案参阅以下链接:
多线程生产者、消费者模式中,如何停止消费者?多生产者情况下对“毒丸”策略的应用
上述案例可以正常运行并退出的代码:
public class CookAndWaiterModified {
/**
* 生产者与消费者问题案例演示
* 使用生产者与消费者模式
*/
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
static class Cook extends Thread{
private Food f;
public Cook(Food f){
this.f = f;
}
@Override
public void run() {
int countFood01 = 0;
int countFood02 = 0;
for (int i=0; i<10; i++){
if (i%2==0){
countFood01++;
f.setNameAndTaste("鱼香肉丝" + countFood01, "鲜咸");
} else {
countFood02++;
f.setNameAndTaste("拔丝地瓜" + countFood02, "甜");
}
}
}
}
static class Waiter extends Thread{
private Food f;
public Waiter(Food f){
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.consume();
}
}
}
static class Food{
private String name;
private String taste;
// true表示可以生产
// 交给消费者时更改为false
private boolean flag = true;
public synchronized void setNameAndTaste(String name, String taste) {
if (flag){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;
this.notifyAll();
try {
this.wait(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void consume() {
if (!flag){
System.out.println("服务员端走了菜:" + name + ", 味道:" + taste);
flag = true;
this.notifyAll();
try {
this.wait(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
8 线程的六种状态
Thread类中的枚举内部类Enum Thread.State
NEW
创建且还未启动状态;RUNNABLE
正在执行状态;BLOCKED
使用锁使多线程同步执行(线程安全),线程正在排队(或未抢得CPU时间片)时处于阻塞状态,解除后变为RUNNABLE
状态;WAITING
调用wait()
使线程休眠,线程处于无限等待状态,由其它线程唤醒解除后变为RUNNABLE
状态;TIMED_WAITING
调用有传参的wait(long millis [,int nanosec])
使线程休眠,线程处于等待状态,由其它线程唤醒或计时结束解除后变为RUNNABLE
状态;TERMINATED
线程结束运行状态。
线程的六种状态的转变可以如下图形表述:
9 线程池 Executors
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要大量额外时间(甚至有可能大于真正的功能实现的时间)。 线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。线程池的内部结构:线程数组(定长/变长/单值)[忙/闲]、任务列表。
9.1 线程池的好处
- 降低资源消耗。
- 提高响应速度。
- 提高线程的可管理性。
9.2 Java中的四种线程池 ExecutorService
9.2.1 缓存线程池
并发执行的任务适合使用该线程池。长度无限制。任务加入后的执行流程:
- 判断线程池是否存在空闲线程;
- 存在则使用;
- 不存在,则创建线程,并放入线程池,然后使用。
代码实例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPool {
public static void main(String[] args) {
// 创建缓存线程池
ExecutorService service = Executors.newCachedThreadPool();
// 向线程池添加任务并执行
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
// 向线程池添加任务并执行
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
// 向线程池添加任务并执行
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
// 使主线程休眠1秒钟
// 演示线程的重复利用
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 向线程池添加任务并执行
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
// 关闭线程池(若不关闭则程序不能正常退出)
service.shutdown();
}
}
输出:
pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
pool-1-thread-3
向线程池连续添加三个任务创建了三个线程,主线程休眠1s后再添加的任务由已经空闲的thread-3接收并执行,不再创建新线程。
9.2.2 定长线程池
并发执行的任务适合使用该线程池。线程数组长度是指定的数值(创建时传入的参数)。
任务加入后的执行流程:
- 判断线程池是否存在空闲线程;
- 存在则使用;
- 不存在空闲线程,且线程池未满的情况下,则创建线程线程并放入线程池,然后使用该线程;
- 不存在空闲线程,且线程池已满的情况下,则等待,直到线程池中出现空闲线程,使用该空闲线程。
代码实例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPool {
public static void main(String[] args) {
// 创建一个容量为2(即线程数组长度为2)的定长线程池
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
service.shutdown();
}
}
输出演示:
最后一个输出等待了3s才出现,证明线程池的容量为2,即线程数组长度为2,只能提供两个线程,所以第三个任务在前面一个任务的线程休眠时处于排队等待的状态,休眠3秒结束后才由thread-2执行。
9.2.3 单线程线程池
任务要求必须排队执行时,可使用单线程线程池。与固定长度线程池传入参数1创建实际上结果相同。
任务加入后的执行流程:
- 判断线程池的单线程是否空闲;
- 空闲则使用;
- 不空闲则等待,池中的单个线程空闲后再使用。
public class SingleThreadExecutor {
public static void main(String[] args) {
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
service.shutdown();
}
}
输出:
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
可见只有一个线程在工作。
9.2.4 周期性任务定长线程池
定时执行或特定间隔周期重复执行。任务执行流程:
- 判断线程池是否存在空闲线程;
- 存在则使用;
- 不存在空闲线程,且线程池未满的情况下,则创建线程,并放入线程池,然后使用该线程;
- 不存在空闲线程,且线程池已满的情况下,则等待,直到线程池出现空闲线程,使用此空闲线程。
周期性任务执行:定时执行,当某个时机触发时,将自动执行某任务。
代码实例:
-
创建周期性任务定长线程池对象:(此例中设置容量为2)
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
注意:类型不再是
ExecutorService
,虽然使用ExecutorService
声明不报错,但一旦声明为ExecutorService
,将无法使用周期执行的功能,即:将无法调用schedule()
方法。 -
使用场景1,定时执行一次任务:
需要传入三个参数:
参数1:定时执行的任务(Runnable类型的对象);
参数2:时长数字;
参数3:时长数字的长度单位(枚举类TimeUnit
的常量)service.schedule(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }, 3, TimeUnit.SECONDS);
设置了一个延时三秒执行的任务,内容为输出线程名称。
-
使用场景2,周期性执行任务:
需要传入的参数:
参数1:任务(Runnable类型的对象);
参数2:延迟时长数字(第一次执行在何时);
参数3:周期时长数字(每隔多久运行一次);
参数4:时长数字的单位(枚举类TimeUnit
的常量);service.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("我说hello你说hi"); } },3,1,TimeUnit.SECONDS);
设置了一个延时3秒执行,之后每隔1秒执行一次的周期性任务,内容为控制台打印
我说hello你说hi
。
10 Lambda表达式
JDK 1.8 版本引入,函数式编程思想。与面向对象的思想不同,面向对象:创建对象=>调用方法=>解决问题;函数编程:编写方法=>解决问题,不关注过程,只关注结果。
冗余的Runnable
代码:
- 创建
Runnable
实现类; - 填充
run()
任务代码; - 创建
Runnable
实现类实例对象; - 创建
Thread
线程对象,将Runnable
对象传入; - 线程对象调用
start()
方法执行任务。
其实上述流程中,有效的逻辑代码仅在第二步的run()
方法中。代码实例:
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("嘿嘿嘿");
}
}
以上代码冗余过多,观察可知对象runnable实际只使用一次,可以进一步将其简写为匿名对象的方式,省略实现类的编写:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("嘿嘿嘿");
}
});
thread.start();
而函数式编程将不再关注对象(new Runnable()
),而仅仅关注方法(逻辑代码):
System.out.println("嘿嘿嘿")
所以使用Lambda表达式来改写上述代码,可以将所有代码简写为:
Thread thread = new Thread(() -> {
System.out.println("嘿嘿嘿");
});
thread.start();
并可以进一步简写为:
Thread thread = new Thread(() -> System.out.println("嘿嘿嘿"));
thread.start();
若有传参,则在上面箭头左侧的括号中写入参数即可;若有返回值,在上面箭头的右侧使用大括号括起来,写return语句即可。
使用lambda注意:接口必须只有一个(抽象)方法。
保留参数和方法体,即构成lambda表达式:
代码实例:(自定义接口的lambda表达式调用)
public class MyLambda {
public static void main(String[] args) {
// 1. 方法引用(method reference)
// lambda表达式的简写形式
print(Integer::sum, 100, 200);
// 2. lambda表达式写法
print((x, y) -> x + y, 100, 200);
// 3. 匿名对象写法
print(new MyMath() {
@Override
public int sum(int x, int y) {
return x + y;
}
}, 100, 200);
}
public static void print(MyMath myMath, int x, int y){
int num = myMath.sum(x,y);
System.out.println(num);
}
interface MyMath{
int sum(int x, int y);
}
}
代码内的三种写法,效果完全一致。
有关Java的方法引用,参阅下面的文章:
Java的方法引用(method reference)的使用例。