多线程
- 12.1 进程与进程
- 12.1.1 进程
- 12.1.2 线程
- 12.2 线程的创建
- 12.2.1 继承Thread类创建多线程
- 12.2.2 实现Runnable接口创建多线程
- 12.2.3 实现Callable接口创建多线程
- 12.2.4 Thread类与Runnable接口实现多线程的对比
- 12.2.5 后台线程
- 12.3 线程的生命周期及状态转换
- 12.4 线程操作的相关方法
- 12.4.1 线程的优先级
- 12.4.2 线程休眠
- 12.4.3 线程插队
- 12.4.4 线程让步
- 12.4.5 线程中断
- 12.5 线程同步
- 12.5.1 线程安全
- 12.5.2 同步代码块
- 12.5.3 同步方法
- 12.5.4 死锁问题
- 12.5.5 重入锁
12.1 进程与进程
12.1.1 进程
进程(process)是计算机中程序的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
虽然进程在程序执行时产生,但进程并不是程序。程序是“死”的,进程是“活”的。程序是指编译好的二进制文件,它存放在磁盘上,不占用系统资源,是具体的;而进程存在于内存中,占用系统资源,是抽象的。当一次程序执行结束时,进程随之消失,进程所用的资源被系统回收。
对计算机用户而言,计算机似乎能够同时执行多个进程,如听音乐、玩游戏、语音聊天等,都能在同一台计算机上同时进行。但实际上,一个单核的CPU同一时刻只能处理一个进程,用户之所以认为同时会有多个进程在运行,是因为计算机系统采用了多道程序设计技术。
所谓多道程序设计,是指计算机允许多个相互独立的程序同时进入内存,在内存的管理控制之下,相互之间穿插运行。多道程序设计必须有硬件基础作为保障。
采用多道程序设计的系统,会将CPU的周期划分为长度相同的时间片,在每个CPU时间片内只处理一个进程,也就是说,在多个时间片内,系统会让多个进程分时使用CPU。假如现在内存中只有3个进程——A、B、C,那么CPU时间片的分配情况大致如图12-1所示。
虽然在同一个时间片中,CPU只能处理一个进程,但CPU划分的时间片是非常微小的,且CPU运行速度极快(1秒可执行约10亿条指令),因此,在宏观上,可以认为计算机能并发执行多个程序、处理多个进程。
进程对CPU的使用权是由操作系统内核分配的,操作系统内核必须知道内存中有多少个进程,并且知道此时正在使用CPU的进程,这就要求内核必须能够区分进程,并可获取进程的相关属性。
12.1.2 线程
通过12.1.1节的介绍可以知道,每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看作程序执行的线程(thread)。每一个进程中都至少存在一个线程。例如,当一个Java程序启动时,就会产生一个进程,该进程默认创建一个线程,这个线程会运行main()方法中的代码。
在前面章节的程序中,代码都是按照调用顺序依次往下执行的,没有出现两段程序
代码交替运行的效果,这样的程序称作单线程程序。如果希望程序中实现多段程序代码交替运行的效果,则需要创建多个线程,即多线程程序。所谓多线程是指一个进程在执行过程中可以产生多个线程,这些线程在运行时是相互独立的,它们可以并发执行。多线程程序的执行过程如图12-2所示
12.2 线程的创建
Java提供了3种多线程的创建方式:
(1)继承java.lang包中的Thread类,重写Thread类的run()方法,在run()方法中实现多线程代码。
(2)实现java.lang.Runnable接口,在run()方法中实现多线程代码。
(3)实现java.util.concurrent.Callable接口,重写call()方法,并使用Future接口获取call()方法返回的结果
12.2.1 继承Thread类创建多线程
public class Example01 {
public static void main(String[] args) {
MyThread01 myThread = new MyThread01(); //创建MyThread01实例对象
myThread.run();
while(true){
System.out.println("Main方法在运行");
}
}
}
class MyThread01{
public void run(){
while(true){
System.out.println("MyThread类的run()方法在运行");
}
}
}
从图可以看出,程序一直打印“MyThread类的run()方法在运行”,这是因为该程序是一个单线程程序。在文件第4行代码调用MyThread01类的run()方法时,执行第12~14行代码定义的死循环,因此,MyThread类的println语句将一直执行,而main()方法中的println语句无法得到执行。如果希望两个while循环中的println语句能够并发执行,就需要实现多线程。为此Java提供了线程类Thread。通过继承Thread类,并重写Thread类中的run()方法,便可实现多线程。在Thread类中提供了start()方法用于启动新线程。新线程启动后,Java虚拟机会自动调用run()方法;如果子类重写了run()方法,便会执行子类中的run()方法。
修改后,通过继承Thread类的方法实现多线程
public class Example01 {
public static void main(String[] args) {
MyThread02 myThread = new MyThread02(); // 创建MyThread02的线程对象
myThread.start(); // 开启线程
while (true) { // 通过死循环语句打印输出
System.out.println("Main()方法在运行");
}
}
}
class MyThread02 extends Thread {
public void run() {
while (true) { // 通过死循环语句打印输出
System.out.println("MyThread类的run()方法在运行");
}
}
}
利用两个while循环模拟多线程环境。第3、4行代码在main()方法中创建了MyThread02类的线程对象myThread,并通过myThread对象调用start()方法启动新线程。第5~7行代码在main()方法中定义了一个while死循环,并在while死循环中输出“main()方法在运行”。第10~16行代码定义了MyThread02类,该类继承Thread类,并重写了run()方法,在run()方法中定义了一个while死循环,并在while死循环中输出“MyThread类的run()方法在运行”。
12.2.2 实现Runnable接口创建多线程
上述文件通过继承Thread类实现了多线程,但是这种方式有一定的局限性。因为Java只支持单继承,一个类一旦继承了某个父类,就无法再继承Thread类。例如,Student类继承了Person类,那么Student类就无法再通过继承Thread类创建线程。
为了克服这种端,Thread类提供了另一个构造方法一Thread(Runnabletarget),其中参数类型Runnable是一个接口,它只有一个run()方法。当通过Thread(Runnabletarget)构造方法创建线程对象时,只需为该方法传递一个实现了Runnable接口的对象,这样,创建的线程将实现Runnable接口中的run()方法作为运行代码,而不需要调用Thread类中的run()方法。
public class Example01{
public static void main(String[] args) {
MyThread03 myThread = new MyThread03(); //创建MyThread03类的实例对象
Thread thread =new Thread(myThread); //创建线程对象
thread.start();
while(true){
System.out.println("Main()方法正在运行");
}
}
}
class MyThread03 implements Runnable {
public void run() { // 线程的代码段,当调用start()方法时,线程从此处开始执行
while(true){
System.out.println("MyThread03类的run()方法在运行");
}
}
}
main()方法和MyThread03类中的run()方法都被执行了,说明文件实现了多线程。
12.2.3 实现Callable接口创建多线程
通过Thread类和Runnable接口实现多线程时,需要重写run()方法,但是由于run()方法没有返回值,无法从新线程中获取返回结果。为了解决这个问题,Java提供了Callable接口来满足这种既能创建新线程又有返回值的需求。通过实现Callable接口的方式创建并启动线程的主要步骤如下:
(1)创建Callable接口的实现类,同时重写Callable接口的call()方法。
(2)创建Callable接口的实现类对象。
(3)通过线程结果处理类FutureTask的有参构造方法封装Callable接口的实现类对象。
(4)调用参数为FutureTask类对象的有参构造方法Thread()创建Thread线程实例。
(5)调用线程实例的start()方法启动线程。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//定义一个实现Callable接口的实现类
class MyThread04 implements Callable<Object> {
//重写Callable接口的call()方法
public Object call() throws Exception {
int i = 0;
while (i++ < 5) {
System.out.println(Thread.currentThread().getName() + "的call()方法在进行");
}
return i;
}
}
public class Example01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread04 myThread = new MyThread04(); //创建Callable接口的实例对象
//使用FutureTask封装MyThread04类
FutureTask<Object> ft1 = new FutureTask<>(myThread);
//使用Thread(Runnable target,String name)构造方法创建线程对象
Thread thread1 = new Thread(ft1, "thread");
//调用线程对象的start()方法启动线程
thread1.start();
//通过FutureTask对象的方法管理返回值
System.out.println(Thread.currentThread().getName() + "的返回结果:" + ft1.get());
int a = 0;
while (a++ < 5) {
System.out.println("main()方法正在运行");
}
}
}
从上图可以看出,案例通过实现Callable接口的方式实现了多线程并且有返回结果。Callable接口方式实现的多线程是通过FutureTask类来封装和管理返回结果的,FutureTask类的直接父接口是RunnableFuture,从名称上可以看出RunnableFuture是Runnable和Future的结合体。FutureTask类的继承关系如图12-8所示。
FutureTask类的继承关系
方法声明 | 功能描述 |
---|---|
boolean cancel(boolean mayInterruptIfRunning) | 用于取消任务。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务。如果该参数设置为true,则表示可以取消正在执行的任务 |
boolean isCancelled() | 判断任务是否被取消成功。如果任务在正常完成前被取消成功,则返回true |
boolean isDone() | 判断任务是否已经完成。若任务完成,则返回true |
V get() | 用于获取执行结果。这个方法会发生阻塞,一直等到任务执行完毕才返回执行结果 |
get(longtime out,TimeUnit unit) | 用于在指定时间内获取执行结果。如果在指定时间内没获取结果,就直接返回null |
12.2.4 Thread类与Runnable接口实现多线程的对比
多线程的实现方式有3种,其中Runnable接口和Callable接口实现多线程的方式基本相同,主要区别就是Callable接口中的方法有返回值而Runnable接口中的方法没有返回值。通过继承Thread类和实现Runnable接口实现多线程方式会有一定的区别,下面通过一个应用场景来分析说明。
假设售票厅有4个窗口可发售某日某次列车的100张车票。这时,100张车票可以看作共享资源;4个售票窗口同时售票,可以看作4个线程同时运行。为了更直观地显示窗口的售票情况,可以调用Thread类的currentThread()方法获取当前线程的实例对象,然后调用getName()方法以获取线程的名称。
public class Example01 {
public static void main(String[] args) {
new TicketWindow().start();// 创建并开启第一个线程对象TicketWindow
new TicketWindow().start();// 创建并开启第二个线程对象TicketWindow
new TicketWindow().start();
new TicketWindow().start();
}
}
class TicketWindow extends Thread {
private int tickets=100;
public void run() {
while(tickets>0){ //通过while循环判断票数并打印语句
Thread th=Thread.currentThread(); //获取当前线程
String th_name=th.getName(); //获取当前线程的名字
System.out.println(th_name+"正在发售第 "+tickets-- +" 张票");
}
}
}
每张票都被打印了4次。出现这个现象的原因是4个线程没有共享100张票,而是各自出售了100张票。在程序中创建了4个TicketWindow对象,就等于创建了4个售票线程,每个线程中都有100张票,每个线程在独立地处理各自的资源。需要注意的是,每个线程都有自己的名字,主线程默认的名字是main,用户创建的第一个线程的名字默认为Thread-0,第二个线程的名字默认为Thread-1,以此类推,由于现实中铁路系统的车票资源是共享的,因此上面的运行结果显然不合理。为了保证资源共享,在程序中只能创建一个售票对象,然后开启多个线程运行同一个售票对象的售票方法,简单来说,就是4个线程运行同一个售票程序。用Thread类创建多线程,无法保证多个线程对共享资源的正确操作;而Runnable接口可以保证多个线程对共享资源的正确访问。接下来,通过实现Runnable接口的方式实现多线程的创建。修改文件,使用构造方法Thread(Runnabletarget,Stringname)在创建线程对象时指定线程的名称
public class Example01 {
public static void main(String[] args) {
TicketWindow tw = new TicketWindow(); // 创建TicketWindow实例对象tw
new Thread(tw, "窗口1").start(); // 创建线程对象并命名为窗口1,开启线程
new Thread(tw, "窗口2").start(); // 创建线程对象并命名为窗口2,开启线程
new Thread(tw, "窗口3").start(); // 创建线程对象并命名为窗口3,开启线程
new Thread(tw, "窗口4").start(); // 创建线程对象并命名为窗口4,开启线程
}
}
class TicketWindow implements Runnable {
private int tickets = 100;
public void run() {
while (tickets > 0) {
Thread th = Thread.currentThread(); // 获取当前线程
String th_name = th.getName(); // 获取当前线程的名字
System.out.println(th_name + " 正在发售第 " + tickets-- + " 张票 ");
}
}
}
12.2.5 后台线程
在多线程中,主线程如果不等待子线程的返回结果,那么主线程与子线程没有先后顺序,有可能主线程先结束了,子线程还没结束。在这样的情况下,虽然主线程结束了,但整个程序进程是不会结束的,因为子线程还在执行。有人可能会认为,当main()方法中创建并启动的4个新线程的代码执行完毕后,主线程也就随之结束了。然而,通过程序的运行结果可以看出,虽然主线程结束了,但整个Java程序却没有随之结束,仍然在执行售票的代码。对Java程序来说,只要还有一个前台线程在运行,这个进程就不会结束;如果一个进程中只有后台线程运行,这个进程就会结束。这里提到的前台线程和后台线程是一种相对的概念:新创建的线程默认是前台线程;如果某个线程对象在启动之前执行了setDaemon(true)语句,这个线程就变成了后台线程。下面通过一个案例演示当程序只有后台线程时就会结束的情况。
public class Example02 {
public static void main(String[] args) {
System.out.println("main线程是后台线程吗?" + Thread.currentThread().isDaemon());
DamonThread dt = new DamonThread();
Thread thread = new Thread(dt, "后台线程");
System.out.println("thread线程默认是后台线程吗?" + thread.isDaemon());
//将线程thread线程对象设置为后台线程
thread.setDaemon(true);
thread.start();
//模拟主线程main的执行任务
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
}
}
class DamonThread implements Runnable {
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "----在运行");
}
}
}
12.3 线程的生命周期及状态转换
在Java中,任何对象都有生命周期,线程也不例外,它也有自已的生命周期。当Thread对象创建完成时,线程的生命周期便开始了。当run()方法中的代码正常执行完毕或者线程抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束。在线程的整个生命周期中,线程可能处于不同的状态,例如,线程在刚刚创建完成时处于新建状态,线程在执行任务时处于运行状态。在线程的整个生命周期中,其基本状态一共有6种,分别是新建(New)状态、可运行(Runnable)状态、锁阻塞(Blocked)状态、无限等待(Waiting)状态、计时等待(Timed_Waiting)状态和被终止(Teminated)状态,线程的不同状态表明了线程当前正在进行的活动。接下来针对线程生命周期中的6种基本状态分别进行详细讲解。
1.新建状态
创建一个线程对象后,该线程对象就处于新建状态。此时还没调用start()方法启动线程,和其他Java对象一样,仅仅由JVM为其分配了内存,没有表现出任何线程的动态特征。
2.可运行状态
可运行状态也称为就绪状态。当线程对象调用了start()方法后就进人就绪状态。处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,要获得CPU的使用权并开始运行,还需要等待系统的调度。
3.锁阻塞状态
如果处于可运行状态的线程获得了CPU的使用权,并开始执行run()方法中的线程执行体,则线程处于运行状态。一个线程启动后,它可能不会一直处于运行状态,当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有时,则该线程进人锁阻塞状态;当该线程持有锁时,该线程将变成可运行状态。
4.无限等待状态
一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进人无限等待状态。线程进人这个状态后是不能自动唤醒的,必须等待另一个线程调用notify()或者notifyAll()方法才能够唤醒。
5.计时等待状态
计时等待状态是具有指定等待时间的线程状态。线程由于调用了计时等待的方法(包括Thread.sleep()、Object.wait()、Thread.join()、LockSupport.parkNanos()、LockSupport.parkUntil()),并且指定了等待时间,就处于计时等待状态。这一状态将一直保持到超时或者接收到唤醒通知。
6.被终止状态
被终止状态是终止运行的线程的状态。线程因为run()方法正常退出而死亡,或者因为没有捕获的异常终止了run()方法而结束执行。另外,在程序中,通过一些操作,可以使线程在不同状态之间转换。线程状态的转换如图12-12所示。
箭头表示线程可转换的方向
12.4 线程操作的相关方法
程序中的多个线程是并发执行的,某个线程若想执行,就必须获得CPU的使用权。Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称作线程的调度。
在计算机中,线程调度有两种模型,分别是分时调度模型和抢占式调度模型。分时调度模型是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用CPU的时间片。抢占式调度模型是指让线程池中优先级高的线程优先占用CPU;而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其他线程获取CPU的使用权。Java虚拟机默认采用抢占式调度模型。通常情况下程序员不需要关心计算机使用的是哪种调度模型,但在某些特定的需求下需要改变这种模式,由程序自身控制CPU的调度。本节将围绕线程调度的相关知识进行详细讲解。
12.4.1 线程的优先级
在应用程序中,如果要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得CPU使用权的机会越大,而优先级越低的线程获得CPU使用权的机会越小。线程的优先级用1~10的整数表示,数字越大,优先级越高。除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的3个静态常量表示线程的优先级,如表所示。
优先级常量 | 功能描述 |
---|---|
static int MAX_PRIORITY | 表示线程的最高优先级,值为10 |
static int MIN_PRIORITY | 表示线程的最低优先级,值为1 |
static int NORM_PRIORITY | 表示线程的默认优先级,值为5 |
程序在运行期间,处于就绪状态的每个线程都有自己的优先级,例如,主线程具有普通优先级。然而线程的优先级不是固定不变的,可以通过调用Thread类的setPriority(intnewPriority)方法进行设置,该方法中的参数newPriority接收的是1~10的整数或者Thread类的3个静态常量。下面通过一个案例演示不同优先级的两个线程在程序中的运行情况。 |
// 定义类MaxPriority实现Runnable接口
class MaxPriority implements Runnable {
public void run() {
for(int i = 0; i <5;i++){
System.out.println(Thread.currentThread().getName()+"正在输出:"+i);
}
}
}
class MinPriority implements Runnable {
public void run() {
for(int i = 0; i <5;i++){
System.out.println(Thread.currentThread().getName()+"正在输出:"+i);
}
}
}
public class Example03 {
public static void main(String[] args) {
Thread minPriority =new Thread(new MinPriority(),"优先级较低的线程");
Thread maxPriority =new Thread(new MaxPriority(),"优先级较高的线程");
minPriority.setPriority(Thread.MIN_PRIORITY); //设置线程的优先级1
maxPriority.setPriority(Thread.MAX_PRIORITY); //设置线程的优先级10
//开启两个线程
maxPriority.start();
minPriority.start();
}
}
12.4.2 线程休眠
线程休眠指让当前线程暂停执行,从运行状态进人阻塞状态,将CPU资源让给其他线程的一种调度方式,可以调用线程的操作方法sleep()实现线程休眠,sleep()方法是java.lang.Thread类中定义的静态方法。使用sleep()方法时需要指定当前线程休眠的时间,传人一个long类型的数据作为休眠时间,单位为毫秒,并且任意一个线程的实例化对象都可以调用该方法。下面通过一个案例演示sleep()方法在程序中的使用。
// 定义SleepThread类实现Runnable接口
class SleepThread implements Runnable {
public void run() {
for (int i = 1; i <= 8; i++) {
if (i == 3) {
try {
Thread.sleep(2000); // 当前线程休眠2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("SleepThread线程正在输出:" + i);
try {
Thread.sleep(500); // 当前线程休眠500毫秒
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public class Example03 {
public static void main(String[] args) throws Exception {
// 创建一个线程
new Thread(new SleepThread()).start();
for (int i = 1; i <= 8; i++) {
if (i == 5) {
Thread.sleep(2000); // 当前线程休眠2000毫秒
}
System.out.println("主线程正在输出:" + i);
Thread.sleep(500); // 当前线程休眠500毫秒
}
}
}
12.4.3 线程插队
线程插队指将某个线程插人当前线程中,由两个线程交替执行变成两个线程顺序执行,即一个线程执行完毕之后再执行第二个线程,可以通过调用线程对象的join()方法实现线程插队。
假设有两个线程一线程甲和线程乙。线程甲在执行到某个时间点的时候调用线程乙的join()方法,则表示从当前时间点开始CPU资源被线程乙独占,线程甲进人阻塞状态;直到线程乙执行完毕,线程甲才进人就绪状态,等待获取CPU资源后进人运行状态继续执行。
public class Example04 {
public static void main(String[] args) throws InterruptedException {
// 创建线程
Thread thread = new Thread(new JoinRunnable(), "thread");
thread.start(); // 开启thread线程
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "输出:" + i);
if (i == 2) {
thread.join(); // 调用join()方法
}
}
}
}
class JoinRunnable implements Runnable {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "输出:" + i);
}
}
}
在文件中,第4、5行代码开启了thread线程,main()方法的main线程会和thread线程相互争夺CPU使用权以输出语句。在第8~10行代码中,当main线程中的循环变量为2时,调用thread线程的join()方法,这时,thread线程就会“插队”优先执行,并且thread线程执行完毕后才会执行其他线程。文件12-10的运行结果如图12-15所示。从图12-15可以看出,当main线程输出2以后,thread线程就开始执行;直到thread执行完毕,main线程才继续执行。
Thread类不仅提供了无参数的线程插队方法join(),还提供了带有时间参数的线程插队方法join(longmillis)。当执行带有时间参数的join(longmillis)方法进行线程插队时,必须等待插人的线程指定时间过后才会继续执行其他线程。
同样是完成线程合并的操作,join()和join(longmillis)还是有区别的。join()表示在被调用线程执行完成之后才能执行其他线程。join(longmillis)则表示被调用线程执行millis毫秒之后,无论是否执行完毕,其他线程都可以和它争夺CPU资源。下面通过一个案例演示join(longmillis)方法在程序中的使用。
public class Example04 {
public static void main(String[] args) throws InterruptedException {
// 创建线程
Thread thread = new Thread(new JoinRunnable(), "thread");
thread.start(); // 开启线程
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "输出:" + i);
if (i == 2) {
thread.join(3000); // 调用join()方法并将参数设置为3000
}
}
}
}
class JoinRunnable implements Runnable {
public void run() {
for (int i = 1; i <= 5; i++) {
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "输出:" + i);
}
}
}
在图可以看到,当main线程执行到i=2时,thread线程插队,优先于main线程执行。thread线程插队是通过调用join(3000)方法实现的。从插队开始thread线程独占CPU资源,执行3000ms之后,main线程继续与thread线程抢占资源。因为thread线程每次执行会休眠1000ms,所以看到的结果是在执行了两次thread线程之后,main线程再次进人就绪状态,抢占CPU资源。
12.4.4 线程让步
线程让步是指在某个特定的时间点,让线程暂停抢占CPU资源的行为,即从运行状态或就绪状态转到阻塞状态,从而将CPU资源让给其他线程使用。这相当于现实生活中地铁排队进站,轮到你进站时,你让其他人先进了,把这次进站的机会让给其他人。但是这并不意味着你放弃排队,你只是在某个时间点做了一次让步,过了这个时间点,你依然要进行排队。线程的让步也是如此:假如线程甲和线程乙在交替执行,在某个时间点线程甲做出让步,让线程乙占用了CPU资源,执行其业务逻辑;线程乙执行完毕之后,线程甲会再次进人就绪状态,争夺CPU资源。
线程让步可以使用yield()方法来实现。下面通过一个案例演示yield()方法在程序中的使用。
public class Example05 {
public static void main(String[] args) {
// 创建两个线程
Thread thread1 = new YieldThread("thread1");
Thread thread2 = new YieldThread("thread2");
// 开启两个线程
thread1.start();
thread2.start();
}
}
// 定义YieldThread类继承Thread类
class YieldThread extends Thread {
// 定义一个有参的构造方法
public YieldThread(String name) {
super(name); // 调用父类的构造方法
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
if (i == 2) {
System.out.print("线程让步:");
Thread.yield(); // 线程运行到此,作出让步
}
}
}
}
yield()方法的弊端
通过yield()方法可以实现线程让步,让当前正在运行的线程失去CPU使用权,让系统的调度器重新调度一次,由于Java虚拟机默认采用抢占式调度模型,所有线程都会再次抢占CPU资源使用权,所以在执行线程让步后并不能保证立即执行其他线程,CPU可能会有一段空闲时间。
12.4.5 线程中断
这里介绍的线程中断是指在线程执行过程中通过手动操作停止该线程。例如,当用户在执行一次操作时,因为网络问题导致延迟,则对应的线程对象就一直处于运行状态。如果用户希望结束这个操作,即终止该线程,就要使用线程中断机制了。在Java中执行线程中断有如下两个常用方法:
- public void interrupt()。
- publicboolean isInterrupted()。
当一个线程对象调用interrupt()方法时,表示中断当前线程对象。每个线程对象都通过一个标志位来判断当前是否为中断状态。
isInterrupted()方法就是用来获取当前线程对象的标志位的。该方法有true和false两个返回值。true表示清除了标志位,当前线程对象已经中断;false表示没有清除标志位,当前对象没有中断。当一个线程对象处于不同的状态时,中断机制也是不同的。
public class Example06 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable(){
public void run() {
for (int i=0;i<10;i++){
if (i==5){
Thread.currentThread().interrupt();
//向控制台打印线程是否中断
System.out.println("thread线程是否已中断----"
+Thread.currentThread().isInterrupted());
}
}
}
}); // 创建MyThread的实例对象
thread.start(); //启动thread对象
}
}
12.5 线程同步
12.5.1 线程安全
在文件12-6的售票案例中,极有可能碰到意外情况,例如一张票被打印多次,或者输出的车的票编号为0甚至负数。这些意外都是由多线程操作共享资源ticket导致的线程安全问题。接下来针对文件12-6进行修改,模拟上述意外情况。假设有4个窗口同时出售10张票,并在售票的代码中使用sleep()方法,令每次售票时线程休眠300ms。
public class Example07 {
public static void main(String[] args) {
SaleThread saleThread = new SaleThread(); // 创建SaleThread对象
// 创建并开启四个线程
new Thread(saleThread, "线程一").start();
new Thread(saleThread, "线程二").start();
new Thread(saleThread, "线程三").start();
new Thread(saleThread, "线程四").start();
}
}
// 定义SaleThread类实现Runnable接口
class SaleThread implements Runnable {
private int tickets = 10; // tickets表示总票数:10张票
public void run() {
while (tickets > 0) {
try {
Thread.sleep(300); //线程休眠300毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---卖出的票"
+ tickets--);
}
}
}
最后输出的车票编号出现了0和负数,这种现象是不应该出现的,因为售票程序中只有当车票编号大于0时才会进行售票。运行结果中之所以出现了0和负数的车票编号,原因是在售票程序的while循环中调用了sleep()方法,出现了线程延迟。假设当车票编号减为1时,线程1获取了CPU执行权,出售1号票,对车票编号进行判断后,进人while循环,在售票之前调用sleep()方法进入休眠;线程1休眠之后,线程2获取了CPU执行权,会进行售票,由于此时票号仍为1,所以线程2也会进人循环;同理,线程3和线程4也会进人while循环。休眠结束后,4个线程都会继续售票,这样就相当于将车票编号减了4次,因此结果会出现0和负数这样的车票编号。
12.5.2 同步代码块
通过12.5.1节的介绍,可以了解到线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想解决文件12-15中的线程安全问题,必须保证在任何时刻都只能有一个线程访问共享资源。为了实现多个线程处理同一个资源,在Java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字修饰的代码块中,这个代码块被称作同步代码块。使用synchronized关键字创建同步代码块的语法格式如下:
synchronized(lock){
处理共享资源得代码块
}
在上面的代码中,lock是一个锁对象,它是同步代码块的关键,相当于为同步代码加锁。当某个线程执行同步代码块时,其他线程将无法执行同步代码块,进人阻塞状态。当前线程执行完同步代码块后,再与其他线程重新抢夺CPU的执行权,抢到CPU执行权的线程将进人同步代码块,执行其中的代码。以此循环往复,直到共享资源被处理完为止。这个过程就像一个公用电话亭,只有前一个人打完电话出来后,后面的人才可以进去打电话。
public class Example08 {
public static void main(String[] args) {
Ticket1 ticket = new Ticket1(); // 创建Ticket1对象
// 创建并开启四个线程
new Thread(ticket, "线程一").start();
new Thread(ticket, "线程二").start();
new Thread(ticket, "线程三").start();
new Thread(ticket, "线程四").start();
}
}
//定义Ticket1类继承Runnable接口
class Ticket1 implements Runnable {
Object lock = new Object(); // 定义任意一个对象,用作同步代码块的锁
private int tickets = 20; // 定义变量tickets,并赋值20
public void run() {
while (true) {
synchronized (lock) { // 定义同步代码块
try {
Thread.sleep(300); // 经过的线程休眠300毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tickets > 0) {
System.out.println(Thread.currentThread().getName()
+ "---卖出的票" + tickets--);
} else { // 如果 tickets小于0,跳出循环
break;
}
}
}
}
}
运行结果中并没有出现线程二和线程四的售票信息,出现这样的现象是很正常的,因为线程在获得锁对象时有一定的随机性,在整个程序的运行期间,线程二和线程四始终未获得锁对象,所以未能显示它们的输出结果。
同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是同一个。“任意”说的是共享锁对象的类型。锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法时都会创建一个新对象,这样,每个线程都会有一个不同的锁,每个锁都有自己的标志位,线程之间便不能产生同步的效果。
12.5.3 同步方法
同步代码块可以有效解决线程安全问题,当把共享资源的操作放在同步代码块中时,便为这些操作加了同步锁。synchronized关键字除了修饰代码块,同样可以修饰方法,被synchronized关键字修饰的方法称为同步方法。同步方法和同步代码块一样,在同一时刻只允许一个线程调用同步方法。synchronized关键字修饰方法的语法格式如下:
synchronized 返回值类型 方法名([参数列表]){}
public class Example08 {
public static void main(String[] args) {
Ticket1 ticket = new Ticket1(); // 创建Ticket1对象
// 创建并开启四个线程
new Thread(ticket, "线程一").start();
new Thread(ticket, "线程二").start();
new Thread(ticket, "线程三").start();
new Thread(ticket, "线程四").start();
}
}
// 定义Ticket1类实现Runnable接口
class Ticket1 implements Runnable {
private int tickets = 20;
public void run() {
while (true) {
saleTicket(); // 调用售票方法
if (tickets <= 0) {
break;
}
}
}
// 定义一个同步方法saleTicket()
private synchronized void saleTicket() {
if (tickets > 0) {
try {
Thread.sleep(300); // 经过的线程休眠300毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---卖出的票"
+ tickets--);
}
}
}
12.5.4 死锁问题
有这样一个场景:一个中国人和一个美国人在一起吃饭,美国人拿了中国人的筷子,中国人拿了美国人的刀叉,两个人开始争执不休:
中国人:“你先给我筷子,我再给你刀叉!”
美国人:“你先给我刀叉,我再给你筷子!”
…
结果可想而知,两个人都吃不成饭。这个例子中的中国人和美国人相当于不同的线程,筷子和刀叉就相当于锁。两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁。下面通过中国人和美国人吃饭的案例模拟死锁问题。
public class Example09 {
public static void main(String[] args) {
// 创建两个DeadLockThread对象
DeadLockThread d1 = new DeadLockThread(true);
DeadLockThread d2 = new DeadLockThread(false);
// 创建并开启两个线程
new Thread(d1, "Chinese").start(); // 创建开启线程Chinese
new Thread(d2, "American").start(); // 创建开启线程American
}
}
class DeadLockThread implements Runnable {
static Object chopsticks = new Object(); // 定义Object类型的chopsticks锁对象
static Object knifeAndFork = new Object(); // 定义Object类型的knifeAndFork锁对象
private boolean flag; // 定义boolean类型的变量flag
DeadLockThread(boolean flag) { // 定义有参的构造方法
this.flag = flag;
}
public void run() {
if (flag) {
while (true) {
synchronized (chopsticks) { // chopsticks锁对象上的同步代码块
System.out.println(Thread.currentThread().getName()
+ "---if---chopsticks");
synchronized (knifeAndFork) { // knifeAndFork锁对象上的同步代码块
System.out.println(Thread.currentThread().getName()
+ "---if---knifeAndFork");
}
}
}
} else {
while (true) {
synchronized (knifeAndFork) { // knifeAndFork锁对象上的同步代码块
System.out.println(Thread.currentThread().getName()
+ "---else---knifeAndFork");
synchronized (chopsticks) { // chopsticks锁对象上的同步代码块
System.out.println(Thread.currentThread().getName()
+ "---else---chopsticks");
}
}
}
}
}
}
第21~30行代码中设置American线程拥有knifeAndFork锁,只有在American线程获得chopsticks锁后才能执行完毕。两个线程都需要对方占用的锁,但是都无法释放自已拥有的锁,于是这两个线程都处于挂起状态,从而造成了图所示的死锁。
12.5.5 重入锁
重入锁(ReentrantLock)的作用类似于synchronized关键字,synchronized是通过Java虚拟机实现的,而重入锁通过JDK实现。重入锁是指可以给同一个资源添加多个锁,并且释放锁的方式与synchronized也不同。synchronized的锁在线程执行完毕之后自动释放,而ReentrantLock的锁必须手动释放。
重入锁的使用格式如下:
private ReentrantLock reentrantLock=new ReentrantLock();
reentrantLock.lock();//加锁
//需要锁的数据
reentrantLock.unlock();//释放锁
import java.util.concurrent.locks.ReentrantLock;
public class Example10 {
public static void main(String[] args) {
// 创建ReentrantLockTest对象
ReentrantLockTest reentrantLockTest = new ReentrantLockTest();
// 创建并开启四个线程
new Thread(reentrantLockTest, "线程一").start();
new Thread(reentrantLockTest, "线程二").start();
new Thread(reentrantLockTest, "线程三").start();
new Thread(reentrantLockTest, "线程四").start();
}
}
// 定义ReentrantLockTest类实现Runnable接口
class ReentrantLockTest implements Runnable {
private int tickets = 10;
private ReentrantLock reentrantLock = new ReentrantLock();
public void run() {
while (true) {
saleTicket(); // 调用售票方法
if (tickets <= 0) {
break;
}
}
}
// 定义一个同步方法saleTicket()
private void saleTicket() {
//调用lock()方法为票数加锁
reentrantLock.lock();
if (tickets > 0) {
try {
Thread.sleep(300); // 经过的线程休眠300毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---卖出的票"
+ tickets--);
}
//调用lock()方法为票数释放锁
reentrantLock.unlock();
}
}
没什么每一次都是线程一??
本章主要内容如下:
进程与线程的概念;
创建线程的3种方式及各自的优缺点,后台线程;
线程的生命周期与状态转换;
从线程的优先级、休眠、插队、让步和中断的相关方法;
多线程的同步,包括线程安全、同步代码块、同步方法和如何解决死锁问题。