前言
几乎所有的程序员都知道,现代操作系统进行资源分配的最小单元是进程,而操作系统进行运算调度的最小单元是线程,其实,在Linux中线程也可以看作是一种轻量级的进程,那么线程是包含于进程之中的,是进程中实际的运作单位;同一进程中的多个线程共用同一块内存空间,而不同的线程又拥有独立的栈内存用以存放线程本地数据;
大家都知道,现在的计算机动辄就是多处理器核心的,而每一个线程同一时间只能运行在一个处理器上,那么如果程序采用单线程进行开发,那么就不能充分利用多核处理器带来的优势;所以为了充分利用多核处理器的资源来提高程序的执行性能,多线程编程变得越来越重要,比如对于计算密集型任务,使用一个线程可能需要100秒,但是,如果使用十个线程共同完成,那么需要的时间可能只有10秒左右;如果你是使用Java开发程序的,那么你很幸运,因为Java是内置多线程编程模型的;但是,想要使用好多线程这把利刃,还需要掌握好多线程编程的基础知识,从而做到得心应手地使用多线程进行高性能程序的开发!
多线程的应用场景
- 程序中出现需要等待的操作,比如网络操作、文件IO等,可以利用多线程充分使用处理器资源,而不会阻塞程序中其他任务的执行
- 程序中出现可分解的大任务,比如耗时较长的计算任务,可以利用多线程来共同完成任务,缩短运算时间
- 程序中出现需要后台运行的任务,比如一些监测任务、定时任务,可以利用多线程来完成
-
- *
自定义线程的实现
处于实用的角度出发,想要使用多线程,那么第一步就是需要知道如何实现自定义线程,因为实际开发中,需要线程完成的任务是不同的,所以我们需要根据线程任务来自定义线程,JDK为我们的开发人员提供了三种自定义线程的方式,供实际开发中使用,来开发出符合需求的多线程程序!
以下是线程的三种实现方式,以及对每种实现的优缺点进行分析,最后是对这三种实现方式进行总结;
方式一:继承Thread类
package com.thread;
//通过继承Thread类实现自定义线程类
public class MyThread extends Thread {
//线程体
@Override
public void run() {
System.out.println("Hello, I am the defined thread created by extends Thread");
}
public static void main(String[] args){
//实例化自定义线程类实例
Thread thread = new MyThread();
//调用start()实例方法启动线程
thread.start();
}
}
123456789101112131415
优点:实现简单,只需实例化继承类的实例,即可使用线程
缺点:扩展性不足,Java是单继承的语言,如果一个类已经继承了其他类,就无法通过这种方式实现自定义线程
方式二:实现Runnable接口
package com.thread;
public class MyRunnable implements Runnable {
//线程体
@Override
public void run() {
System.out.println("Hello, I am the defined thread created by implements Runnable");
}
public static void main(String[] args){
//线程的执行目标对象
MyRunnable myRunnable = new MyRunnable();
//实际的线程对象
Thread thread = new Thread(myRunnable);
//启动线程
thread.start();
}
}
优点:
- 扩展性好,可以在此基础上继承其他类,实现其他必需的功能
- 对于多线程共享资源的场景,具有天然的支持,适用于多线程处理一份资源的场景
缺点:构造线程实例的过程相对繁琐一点
方式三:实现Callable接口
package com.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable {
@Override
public String call() throws Exception {
return "Hello, I am the defined thread created by implements Callable";
}
public static void main(String[] args){
//线程执行目标
MyCallable myCallable = new MyCallable();
//包装线程执行目标,因为Thread的构造函数只能接受Runnable接口的实现类,而FutureTask类实现了Runnable接口
FutureTask futureTask = new FutureTask<>(myCallable);
//传入线程执行目标,实例化线程对象
Thread thread = new Thread(futureTask);
//启动线程
thread.start();
String result = null;
try {
//获取线程执行结果
result = futureTask.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(result);
}
}
优点:
- 扩展性好
- 支持多线程处理同一份资源
- 具备返回值以及可以抛出受检查异常
缺点:
- 相较于实现Runnable接口的方式,较为繁琐
小结
我们对这三种方式进行分析,可以发现:方式一和方式二本质上都是通过实现Runnable接口并重写run()方法,将接口实现类的实例传递给Thread线程类来执行线程体(run()方法中的实现),这里将Runnable接口实现类的实例作为线程执行目标,供线程Thread实例执行;对于方式三,其实也是这样的,由于Thread类只能执行Runnable接口实现类的执行目标,所以需要对Callable接口的实现类进行包装,包装成Runnable接口的实现类(通过实现了Runnable接口的FutureTask类进行包装),从而使得Thread类能够接收Callable接口实现类的实例,可见这里使用了适配器模式!
综上所述,三种实现方式都存在着一个使用范式,即首先实现线程执行目标对象(包含线程所要执行的任务),然后将目标对象作为构造参数以实例化Thread实例,来获得线程!本质上都是实现一个线程体,由Thread来执行线程体,达到开启线程执行任务的效果!但是,三种实现方式各有优缺点,使用时,应该结合具体需求来选用合适的实现方式进行开发!
线程的生命周期
经过上面的代码演示,我们知道了线程如何实现,但是如果我们想要更好地使用线程,还需要对程序运行中线程的状态以及状态之间的转换(即线程的生命周期)有所了解,这样才能在多线程程序运行出现问题时,分析问题产生的原因,从而快速准确地定位并解决问题!
首先,看一下Thread类中给出的关于线程状态的说明:
/**
* 线程生命周期中的的六种状态
* NEW:还没有调用start()的线程实例所处的状态
* RUNNABLE:正在虚拟机中执行的线程所处的状态
* BLOCKED:等待在监视器锁上的线程所处的状态
* WAITING:等待其它线程执行特定操作的线程所处的状态
* TIMED_WAITING:等待其它线程执行超时操作的线程所处的状态
* TERMINATED:退出的线程所处的状态
* 给定时间点,一个线程只会处于以下状态中的一个,这些状态仅仅是虚拟机层面的线程状态,并不能反映任何操作系统中线程的状态
*/
public enum State {
//还没有调用start()开启的线程实例所处的状态
NEW,
//正在虚拟机中执行或者等待被执行的线程所处的状态,但是这种状态也包含线程正在等待处理器资源这种情况
RUNNABLE,
// 等待在监视器锁上的线程所处的状态,比如进入synchronized同步代码块或同步方法失败
BLOCKED,
// 等待其它线程执行特定操作的线程所处的状态;比如线程执行了以下方法: Object.wait with no timeout、Thread.join with no timeout、 LockSupport.park
WAITING,
// 等待其它线程执行超时操作的线程所处的状态;比如线程执行了以下方法: Thread.sleep、Object.wait with timeout
//Thread.join with timeout、LockSupport.parkNanos、LockSupport.parkUntil
TIMED_WAITING,
//退出的线程所处的状态
TERMINATED;
}
- 新建(New):当线程实例被new出来之后,调用start()方法之前,线程实例处于新建状态
- 可运行(Runnable):当线程实例调用start()方法之后,线程调度器分配处理器资源之前,线程实例处于可运行状态或者线程调度器分配处理器资源给线程之后,线程实例处于运行中状态,这两种情况都属于可运行状态
- 等待(Waitting):当线程处于运行状态时,线程执行了obj.wait()或Thread.join()方法、Thread.join、LockSupport.park以及Thread.sleep()时,线程处于等待状态
- 超时等待(Timed Waitting):当线程处于运行状态时,线程执行了obj.wait(long)、Thread.join(long)、LockSupport.parkNanos、LockSupport.parkUntil以及Thread.sleep(long)方法时,线程处于超时等待状态
- 阻塞(Blocked):当线程处于运行状态时,获取锁失败,线程实例进入等待队列,同时状态变为阻塞
- 终止(Terminated):当线程执行完毕或出现异常提前结束时,线程进入终止状态
线程的状态转换
上面也提到了,某一时间点线程的状态只能是上述6个状态中的其中一个;但是,线程在程序运行过程中的状态是会发生变化的,由一个状态转变为另一个状态,那么下面给出线程状态转换图帮助我们清晰地理解线程的状态转变过程:
上面我们已经对线程的实现以及线程的状态有了较为清晰的认识,那么通过上述内容,我们也可以发现其实有很多方法,我们并没有详细地介绍,比如start()、yield()、wait()、notify()、notifyAll()、sleep()、join()等等,这些方法大多来源于JDK中Thread类这一关键的线程类中,下面结合Thread类的源码看一下,多线程编程中经常遇到的方法有哪些,以及这些方法的用途;
线程类Thread源码
实例同步方法:join()
/**
* 等待调用此方法的线程执行结束
* @throws InterruptedException 如果任何线程中断了当前线程,将会抛出此异常,同时将中断标志位清除
*/
public final void join() throws InterruptedException {
join(0);
}
/**
* 最多等待millis毫秒,时间一到无论是否执行完毕,都会返回
* 如果millis为0,那么意味着一直等到线程执行完毕才会返回
* 此方法的实现是基于循环检测当前线程是否存活来判断是否调用当前实例的wait方法来实现的
* @param millis 等待时间
* @throws IllegalArgumentException 非法参数异常
* @throws InterruptedException 如果任何线程中断了当前线程,将会抛出此异常,同时将中断标志位清除
*/
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
/**
* 线程执行结束之前最多等待millis毫秒nanos纳秒
* 此方法基于循环判断isAlive返回值来决定是否调用wait方法来实现
* 随着一个线程终止,将会调用notifyAll方法
* 所以建议不要在当前实例上调用 wait、 notify、 notifyAll
*/
public final synchronized void join(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
join(millis);
}
中断方法以及检测中断方法和判活方法:
/**
* 中断当前线程
* 如果当前线程阻塞在Object的wait()、wait(long)、wait(long, int),或者
* join()、join(long)、join(long, int)以及sleep(long)、sleep(long, int)等方法
* 那么将会清除中断标志位并受到一个中断异常
* 非静态方法
*/
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
/**
* 检测当前线程是否已经被中断,此方法会清除当前线程的中断标志
* 也就是说,如果这个方法被连续调用两次,并且第一次调用之前,线程被中断过,那么第一次调用返回true,第二次返回false
* @return true
如果当前线程已经被中断,返回true,否则返回false
* 静态方法
*/
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
/**
* 检测当前线程是否已经被中断,此方法不会清除当前线程的中断标志
* 非静态方法
*/
public boolean isInterrupted() {
return isInterrupted(false);
}
/**
* 根据参数值决定是否在判断中断标志位之后清除标志位
* 实例方法
*/
private native boolean isInterrupted(boolean ClearInterrupted);
/**
* 检测一个线程是否还存活,存活指的是已经启动但还没有终止
* 实例方法
*/
public final native boolean isAlive();
线程调度
Java线程的实现:Java线程模型是基于操作系统原生线程模型来实现的;
线程模型只对线程的并发规模和操作成本产生影响,对Java程序的编写和运行过程来说,并没有什么不同;
线程优先级
时分形式是现代操作系统采用的基本线程调度形式,操作系统将CPU资源分为一个个的时间片,并分配给线程,线程使用获取的时间片执行任务,时间片使用完之后,操作系统进行线程调度,其他获得时间片的线程开始执行;那么,一个线程能够分配得到的时间片的多少决定了线程使用多少的处理器资源,线程优先级则是决定线程可以获得多或少的处理器资源的线程属性;
可以通过设置线程的优先级,使得线程获得处理器执行时间的长短有所不同,但采用这种方式来实现线程获取处理器执行时间的长短并不可靠(因为系统的优先级和Java中的优先级不是一一对应的,有可能Java中多个线程优先级对应于系统中同一个优先级);Java中有10个线程优先级,从1(Thread.MIN_PRIORITY)到10(Thread.MAX_PRIORITY),默认优先级为5;
因此,程序的正确性不能够依赖线程优先级的高低来判断;
## 线程调度
线程调度是指系统为线程分配处理器使用权的过程;主要调度方式有:抢占式线程调度、协同式线程调度;
抢占式线程调度
每个线程由系统来分配执行时间,线程的切换不由线程本身决定;Java默认使用的线程调度方式是抢占式线程调度;我们可以通过Thread.yield()使当前正在执行的线程让出执行时间,但是,却没有办法使线程去获取执行时间;
### 协同式线程调度
每个线程的执行时间由线程本身来控制,线程执行完任务后主动通知系统,切换到另一个线程上;
两种线程调度方式的优缺点
协同式的优点:实现简单,可以通过对线程的切换控制避免线程安全问题;
协同式的缺点:一旦当前线程出现问题,将有可能影响到其他线程的执行,最终可能导致系统崩溃;
抢占式的优点:一个线程出现问题不会影响到其他线程的执行(线程的执行时间是由系统分配的,因此,系统可以将处理器执行时间分配给其他线程从而避免一个线程出现故障导致整个系统崩溃的现象发生);
结论
在Java中,线程的调度策略主要是抢占式调度策略,正是因为抢占式调度策略,导致多线程程序执行过程中,实际的运行过程与我们逻辑上理解的顺序存在较大的区别,也就是多线程程序的执行具有不确定性,从而会导致一些线程安全性问题的发生;那么,什么是线程安全呢?
线程安全
线程安全的定义
简单来说,线程安全就是对于多个线程并发执行的操作不需要进行任何外部的控制,也不需要进行任何的协调,就能够保证程序的执行结果与开发人员的预期结果保持一致,那么这个多线程程序就是线程安全的;
注意:
线程安全问题一定是基于多个线程之间存在访问共享数据这一前提下的;如果多个线程之间不会访问同一个变量,那么就不存在线程安全的问题;
线程安全的分类
线程安全这一概念并不仅仅分为线程安全和非线程安全,按照线程安全的强弱程度可以将各种共享变量的操作分为:不可变、绝对线程安全、相对线程安全、线程兼容以及线程对立这五种情况;
不可变:如果共享变量是不可变的对象,那么对该共享变量的多线程操作一定是线程安全的,因为对象是不可变的,所以任何线程都不可以改变共享变量的状态,也就不会出现脏读等现象;
- 如果共享变量是一个基本数据类型的变量,那么可以使用final关键字保证其是不可变的;
- 如果共享变量是一个对象,那么就需要保证对象的行为不会改变该对象的状态,可以将一个类的所有字段使用final关键字修饰,那么就可以保证该类的对象是不可变的,如java.lang.String类;
- 绝对线程安全:不需要在调用端进行任何同步处理,就能保证代码在多线程并发的场景下保证线程安全的,即多线程并发执行的结果符合预期的结果;Java API中标注为线程安全的类,大多数都不是绝对线程安全;
- 相对线程安全:Java API中标注为线程安全的类,大多数都是相对的线程安全,也就是通常意义上的线程安全,保证对共享变量单独操作时是线程安全的,调用时可以不用额外的保障措施;例如Vector、HashTable或通过Collections的synchronizedCollection()方法包装的集合等;
- 线程兼容:线程兼容指对象本身并不是线程安全的,但是可以通过在调用端正确采用同步手段来保证对象在并发环境中可以安全地使用,是通常意义上的非线程安全;Java API中的大部分类都是线程兼容的,例如ArrayList、HashMap等;
- 线程对立:无论调用端采用什么同步措施都不能保证多线程环境中的线程安全;线程对立很少出现;
-
- *
线程安全问题的解决方法
介绍了线程的调度原理之后,其实可以分析出线程安全问题的起因在于多线程的执行顺序具有不确定性,那么当多个线程同时操作一份资源就不出现意想不到的情况,而编译器和处理器会对执行的指令进行重排序,这些因素导致了线程安全问题;
那么,在实际开发中,我们一般需要解决的都是上述的相对线程安全以及线程兼容这两种线程安全性问题;那么,对于这两类问题,又可以细分为可见性、原子性以及有序性这三类问题;这里暂且先不进行细分,就线程安全问题,我们给出常用解决措施;
线程安全问题重现
下面结合具体的代码来看一下使用多线程编程时可能出现的线程安全问题:
package com.thread;
public class ThreadSafe implements Runnable{
//静态变量,所有对象共享
private static int count = 0;
@Override
public void run() {
for(int i = 0 ; i < 100 ; i++){
count();
}
}
public void count(){
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) throws InterruptedException {
ThreadSafe threadSafe1 = new ThreadSafe();
ThreadSafe threadSafe2 = new ThreadSafe();
Thread thread1 = new Thread(threadSafe1);
Thread thread2 = new Thread(threadSafe2);
thread1.start();
thread2.start();
Thread.currentThread().sleep(1000);
System.out.println(count);
}
}
这一段代码的目的是开启两个线程对同一个变量分别进行100次的累加,按照正常的逻辑(串行化执行),累加后的结果应该为200,但是实际输出的结果却是190,显然这和我们的预期结果不同,这就是线程安全问题;我们分析一下,为什么会出现这样的情况,之前提到过,多线程执行的时候代码执行的顺序具有不确定性,那么就可能出现,线程1(thread1)在获取到count的值之后,CPU执行权被分配给了线程2(thread2),线程2获取到的值与线程1获取到的相同,那么两个线程累加操作执行后,相当于只累加来一次,这样就会导致线程不安全问题产生;那么,如何解决这个问题,我们可以利用Java中的synchronized关键字对线程体进行同步,代码如下:
package com.thread;
public class ThreadSafeTwo implements Runnable{
//静态变量,所有对象共享
private static int count = 0;
@Override
public void run() {
//这里对线程体进行同步
synchronized(ThreadSafeTwo.class){
for(int i = 0 ; i < 100 ; i++){
count();
}
}
}
public void count(){
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) throws InterruptedException {
ThreadSafeTwo threadSafe = new ThreadSafeTwo();
Thread thread1 = new Thread(threadSafe);
Thread thread2 = new Thread(threadSafe);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
}
显然,经过同步后的代码,就可以保证多线程并发执行的情况下,结果依然符合预期结果;关于synchronized关键字的实现原理将会另起一文进行分析,下面我们看一下,synchronized关键字的使用方式有哪些?
synchronized关键字的使用方式
synchronized同步代码块
- 锁的对象为指定的对象
synchronized同步实例方法
- 锁的对象为当前实例
synchronized同步静态方法
- 锁的对象为Class对象
synchronized关键字的应用实例
线程安全的单例模式实现
package com.thread;
public class SingleTonThreadSafe {
//属性私有化,volatile实现内存可见性、禁止指令重排序
private volatile static SingleTonThreadSafe singleTonThreadSafe = null;
//无参构造函数私有化
private SingleTonThreadSafe(){}
//静态方法外部使用,获取对象实例
public static SingleTonThreadSafe getInstance(){
//第一次判断,避免不必要的加锁
if(singleTonThreadSafe == null){
//同步实例化代码块
synchronized(SingleTonThreadSafe.class){
//再次检测,避免其它线程已经实例化
if(singleTonThreadSafe == null){
//实例化,其他线程立即可见
singleTonThreadSafe = new SingleTonThreadSafe();
}
}
}
return singleTonThreadSafe;
}
}
synchronized同步锁的使用注意点
死锁
- 定义:多个线程互相等待已被对方占有的锁,同时都不释放自己已经占有的锁,导致线程之间陷入僵持,致使系统不可用
- 形成条件:互斥锁、锁只能主动释放、循环等待
- 避免策略:顺序加锁、超时获取自动放弃、死锁检测
活锁
- 定义:线程等待被其他线程唤醒,但是实际没有线程来唤醒,导致线程一直无法恢复到运行状态
- 避免策略:编程时有等待,就必须有对应的唤醒
-
- *
线程间通信
如果你的多线程程序仅仅是每个线程独立完成各自的任务,相互之间并没有交互和协作,那么,你的程序是无法发挥出多线程的优势的,只有有交互的多线程程序才是有意义的程序,否则,还不如使用单线程执行多个方法实现程序来的简单、易懂、有效!
那么,线程间进行交互通信的手段有哪些呢?下面,将给出常用的多线程通信的实现手段以及相应的代码示例,并结合具体的代码进行分析,对其中需要注意的地方进行突出提示;
等待通知机制
我们先看这样一个场景:线程A修改了对象O的值,线程B感知到对象O的变化,执行相应的操作,这样就是一个线程间交互的场景;可以看出,这种方式,相当于线程A是发送了消息,线程B接收到消息,进行后续操作,是不是很像生产者与消费者的关系?我们都知道,生产者与消费者模式可以实现解耦,使得程序结构上具备伸缩性;那么Java中如何实现这种功能呢?
一种简单的方式是,线程B每隔一段时间就轮询对象O是否发生变化,如果发生变化,就结束轮询,执行后续操作;
但是,这种方式不能保证对象O的变更及时被线程B感知,同时,不断地轮询也会造成较大的开销;分析这些问题的症结在哪?其实,可以发现状态的感知是拉取的,而不是推送的,因此才会导致这样的问题产生;
那么,我们就会思考,如何将拉取变为推送来实现这样的功能呢?
这就引出了Java内置的经典的等待/通知机制,通过查看Object类的源码发现,该类中有三个方法,我们一般不会使用,但是在多线程编程中,这三个方法却是能够大放异彩的!那就是wait()/notify()/notifyAll();
/**
* 调用此方法会导致当前线程进入等待状态直到其它线程调用同一对象的notify()或者notifyAll()方法
* 当前线程必须拥有对象O的监视器,调用了对象O的此方法会导致当前线程释放已占有的监视器,并且等待
* 其它线程对象O的notify()或者notifyAll()方法,当其它线程执行了这两个方法中的一个之后,并且
* 当前线程获取到处理器执行权,就可以尝试获取监视器,进而继续后续操作的执行
* 推荐使用方式:
* synchronized (obj) {
* while (<condition does not hold>)
* obj.wait();
* ... // Perform action appropriate to condition
* }
* @throws IllegalMonitorStateException 如果当前线程没有获取到对象O的监视器时,抛出异常
* @throws InterruptedException 如果在调用了此方法之后,其他线程调用notify()或者notifyAll()
* 方法之前,线程被中断,则会清除中断标志并抛出异常
*/
public final void wait() throws InterruptedException {
wait(0);
}
/**
* 唤醒等待在对象O的监视器上的一个线程,如果多个线程等待在对象O的监视器上,那么将会选择其中的一个进行唤醒
* 被唤醒的线程只有在当前线程释放锁之后才能够继续执行.
* 被唤醒的线程将会与其他线程一同竞争对象O的监视器锁
* 这个方法必须在拥有对象O的监视器的线程中进行调用
* 同一个时刻,只能有一个线程拥有该对象的监视器
* @throws IllegalMonitorStateException 如果当前线程没有获取到对象O的监视器时,抛出异常
*/
public final native void notify();
/**
* 唤醒等待在对象O的监视器上的所有线程
* 被唤醒的线程只有在当前线程释放锁之后才能够继续执行.
* 被唤醒的线程将会与其他线程一同竞争对象O的监视器锁
* 这个方法必须在拥有对象O的监视器的线程中进行调用
* 同一个时刻,只能有一个线程拥有该对象的监视器
* @throws IllegalMonitorStateException 如果当前线程没有获取到对象O的监视器时,抛出异常
*/
public final native void notifyAll();
下面看一下如何通过这三个方法实现经典的等待通知机制吧!
按照JDK中推荐的使用方式实现了等待通知样例代码如下:
package com.thread;
public class WaitAndNotify {
//轮询标志位
private static boolean stop = false;
//监视器对应的对象
private static Object monitor = new Object();
//等待线程
static class WaitThread implements Runnable{
@Override
public void run() {
synchronized(monitor){
//循环检测标志位是否变更
while(!stop){
try {
//标志位未变更,进行等待
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被唤醒后获取到对象的监视器之后执行的代码
System.out.println("Thread "+Thread.currentThread().getName()+" is awakened at first time");
stop = false;
}
//休眠1秒之后,线程角色转换为唤醒线程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//与上述代码相反的逻辑
synchronized(monitor){
while(stop){
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
monitor.notify();
stop = true;
System.out.println("Thread "+ Thread.currentThread().getName()+" notifies the waitted thread at first time");
}
}
}
//通知线程
static class NotifyThread implements Runnable{
@Override
public void run() {
synchronized (monitor){
while(stop){
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
stop = true;
monitor.notify();
System.out.println("Thread "+ Thread.currentThread().getName()+" notifies the waitted thread at first time");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (monitor){
while(!stop){
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread "+Thread.currentThread().getName()+" is awakened at first time");
}
}
}
public static void main(String[] args){
Thread waitThread = new Thread(new WaitThread());
waitThread.setName("waitThread");
Thread notifyThread = new Thread(new NotifyThread());
notifyThread.setName("notifyThread");
waitThread.start();
notifyThread.start();
}
}
通过上述代码,可以提炼出等待通知机制的经典模式:
等待方实现步骤:
- 加锁同步
- 条件不满足,进入等待,被唤醒之后,继续检查条件是否满足(循环检测)
- 条件满足,退出循环,继续执行后续代码
对应的伪代码:
synchronized(obj){
while(condition不满足){
obj.wait();
}
//后续操作
}
123456
通知方实现步骤:
- 加锁同步
- 条件不满足,跳过循环检测
- 设置条件并唤醒线程
对应的伪代码:
synchronized(obj){
while(condition不满足){
obj.wait();
}
更新condition
obj.notify();
//后续操作
}
生产者消费者模式
基于等待通知机制,我们可以很容易地写出生产者消费者模式的代码,下面给出一个实现样例代码:
package com.thread;
public class ProducerAndConsumer {
//商品库存
private static int storeMount = 0;
//监视器对应的对象
private static Object monitor = new Object();
//生产者线程
static class ProducerThread implements Runnable{
@Override
public void run() {
try {
produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void produce() throws InterruptedException {
while(true){
synchronized(monitor){
//循环检测库存是否大于0,大于0表示还有商品可以消费,线程等待消费者消费商品
while(storeMount > 0){
monitor.wait();
}
//被唤醒后获取到对象的监视器之后执行的代码
System.out.println("Thread "+Thread.currentThread().getName()+" begin produce goods");
//生产商品
storeMount = 1;
//唤醒消费者
monitor.notify();
Thread.sleep(1000);
}
}
}
}
//消费者线程
static class ConsumerThread implements Runnable{
@Override
public void run() {
try {
consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void consume() throws InterruptedException {
while(true){
synchronized (monitor){
//检测库存是否不为0,如果不为0,那么有商品可供消费,否则等待生产者生产商品
while(storeMount == 0){
monitor.wait();
}
//消费商品
storeMount = 0;
//唤醒生产者线程
monitor.notify();
System.out.println("Thread "+Thread.currentThread().getName()+" begin consume goods");
Thread.sleep(1000);
}
}
}
}
public static void main(String[] args){
Thread producerThread = new Thread(new ProducerThread());
producerThread.setName("producerThread");
Thread consumerThread = new Thread(new ConsumerThread());
consumerThread.setName("consumerThread");
producerThread.start();
consumerThread.start();
}
}
上述代码示例演示了一个生产者生产商品和一个消费者消费商品的场景,对于一个生产者多个消费者、多个生产者一个消费者、多个生产者多个消费者等场景,只需要将唤醒的方法换为notifyAll()即可,否则,会出现饥饿现象!
总结
以上就是本文叙述的所有内容,本文首先对于给出Java中线程调度形式,引出多线程编程中需要解决的线程安全问题,并分析线程安全问题,给出解决线程安全问题的常用手段(加锁同步),最后,结合Java内置的等待通知机制,进行了样例代码的展示以及分析,给出了经典的等待通知机制的编程范式,最后,基于等待通知机制给出了生产者消费者模式的实现样例,希望本文能给想要学习多线程编程的朋友一点帮助,如有不正确的地方,还望指出,十分感谢!
注意细节
线程分类
- 用户线程:大多数线程都是用户线程,用于完成业务功能
守护线程:支持型线程,主要用于后台调度以及支持性工作,比如GC线程,当JVM中不存在非守护线程时,JVM将会退出
- Thread.setDaemon(true)来设置线程属性为守护线程,该操作必须在线程调用start()方法之前执行
- 守护线程中的finally代码块不一定会执行,因此不要寄托于守护线程中的finally代码块来完成资源的释放
线程交互的方式
- join
- sleep/interrupt
- wait/notify
启动线程的方式
- 只能通过线程对象调用start()方法来启动线程
- start()方法的含义是,当前线程(父线程)同步告知虚拟机,只要线程规划期空闲,就应该立即启动调用了start()方法的线程
- 线程启动前,应该设置线程名,以便使用Jstack分析程序中线程运行状况时,起到提示性作用
终止线程的方式
中断检测机制
- 线程通过调用目标线程的interrupt()方法对目标线程进行中断标志,目标线程通过检测自身的中断标志位(interrupted()或isInterrupted())来响应中断,进行资源的释放以及最后的终止线程操作;
- 抛出InterruptedException异常的方法在抛出异常之前,都会将该线程的中断标志位清除,然后抛出异常
suspend()/resume()(弃用)
- 调用后,线程不会释放已经占有的资源,容易引发死锁问题
stop()(弃用)
- 调用之后不一定保证线程资源的释放
锁释放的情况:
- 同步方法或同步代码块的执行结束(正常、异常结束)
- 同步方法或同步代码块锁对象调用wait方法
锁不会释放的情况:
- 调用Thead类的静态方法yield()以及sleep()
- 调用线程对象的suspend()
结尾
到此为止,本文已经对线程的使用场景、实现方式以及生命周期、状态转换过程以及线程类常用的方法进行了介绍,实际开发中,我们使用多线程是为了解决实际问题,最后,希望读者在阅读之后,如果发现文中出现不正确的地方还请指出,同时,大家可以加入我的学习社区大家一同探讨和学习,点此进入一起共同成长^^!