day06 【线程、同步】

day06 【线程、同步】
主要内容

线程

同步

线程状态

教学目标

能够描述Java中多线程运行原理

能够使用继承类的方式创建多线程

能够使用实现接口的方式创建多线程

能够说出实现接口方式的好处

能够解释安全问题的出现的原因

能够使用同步代码块解决线程安全问题

能够使用同步方法解决线程安全问题

能够说出线程6个状态的名称

第一章 线程
1.1 多线程原理

目标

能够理解多线程的运行原理

昨天的时候我们已经写过一版多线程的代码,很多同学对原理不是很清楚,那么我们今天先画个多线程执行时序图来体现一下多线程程序的执行流程。

步骤

自定义MyThread类并继承Thread线程类。

MyThread类中重写run()方法,循环输出1-20

创建MyThread子类对象

调用start()启动线程任务

定义循环,输出 1-20数据

多次运行程序,查看程序的执行结果区别。

实现

自定义线程类:

// 1、定义类并声明为Thread类的子类
public class MyThread extends Thread {
// 2、 在类中重写run方法,即线程任务
public void run() {
// 循环打印 1- 20
for( int i = 1; i <= 20; i++ ) {
System.out.println( "小强 = " + i );
}
}
}
测试类:

// 演示线程的执行流程
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
// 创建线程子类对象
MyThread mt = new MyThread();
// 启动线程任务
mt.start();
// 主线程打印1-20
for( int i = 1; i <= 20; i++ ) {
System.out.println("旺财 = " + i );
}
}
}
流程图:

小结

程序启动运行main时候,java虚拟机启动一个进程,主线程在main()调用时候被创建。随着调用线程对象的start方法,另外一个新的线程也启动了,这样,程序就会出现两条线程同时运行。

多线程执行时,在栈内存中,每一个执行中的线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。

1、线程执行流程

当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

1.2 Thread类

目标

掌握Thread类中的构造方法和常用方法的作用。

在上一天内容中我们已经可以完成最基本的线程开启,那么在我们完成操作过程中用到了java.lang.Thread类,API中该类中定义了有关线程的一些方法,具体如下:

构造方法:

public Thread(): 分配一个新的线程对象。

public Thread(String name): 分配一个指定名字的新的线程对象。

线程在创建完成之后,JVM会给线程分配线程名称。格式为 Thread-x: x从0开始,逐1增加。例如:第一条线程的名字叫做 Thread - 0 ,那么第二条线程的名字就叫做 Thread - 1

常用方法:

public String getName():获取当前线程名称。

public void start():导致此线程开始执行; Java虚拟机调用此线程的run方法。

public void run():此线程要执行的任务在此处定义代码。

public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

public static Thread currentThread():返回对当前正在执行的线程对象的引用。

步骤

继续使用上一章节中的代码。

在main方法中再次创建一个线程任务

给每个线程命名

在线程任务中获取线程的名称

启动线程,查看线程的名称。

实现

自定义线程类:

// 1、定义类并声明为Thread类的子类
public class MyThread extends Thread {
// 2、 在类中重写run方法,即线程任务
public void run() {
// 循环打印 1- 20
for( int i = 1; i <= 20; i++ ) {
System.out.println( getName() + “…” + i );
}
}
}
测试类:

// 演示线程的执行流程
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
// 创建线程子类对象
MyThread mt1 = new MyThread();
// 启动线程任务
mt1.start();

// 创建线程子类对象
MyThread mt2 = new MyThread();
// 启动线程任务
mt2.start();

// 主线程打印1-20
for( int i = 1; i <= 20; i++ ) {
System.out.println(Thread.currentThread().getName() + “…” + i );
}
}
}
小结

Thread是描述线程的类,在类中提供了大量的可以操作线程的方法。每个线程都有一个线程名称。命名规则为:

thread - x。从0开始,逐1累加。也可以通过方法对线程的名称进行设置。线程的其他方法,可以自行查看API。

1.3 创建线程方式二

目标

能够使用Runnable接口完成线程的创建和启动。

翻阅API后得知创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式,方式一我们上一天已经完成,接下来讲解方式二实现的方式。

采用java.lang.Runnable的方式创建线程,在开发中比较常用,我们只需要重写run方法即可。

如果可以,一般都是使用Runnable接口,完成线程的创建。

public Thread( Runnable target ): 分配一个带有指定目标新的线程对象。

public Thread( Runnable target,String name ):分配一个带有指定目标新的线程对象并指定名字

步骤

定义Runnable接口的实现类,并重写该接口的run()方法,也就是线程任务。

创建Runnable实现类的对象。

创建Thread线程对象,并将实现类对象当做参数传递到线程的构造方法中。

使用线程对象的start()方法来启动线程。

实现

public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println( Thread.currentThread().getName() + " "+i);
}
}
}

public class RunnableDemo {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread( mr, “小强” );
// 启动线程任务
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("旺财 " + i);
}
}
}
小结

Runnable接口中的run()方法就是多线程的线程任务。将线程任务完成之后,交由Thread线程对象进行启动。需要注意的是不管使用哪种线程的创建方式,线程任务的启动都需要通过线程对象调用start()方法来完成。

tips:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。
1.4 Thread和Runnable的区别

目标

能够知道使用thread和Runnable接口创建线程的区别。

实现Runnable接口比继承Thread类所具有的优势:

可以避免java中的单继承的局限性。

增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

适合多个相同的程序代码的线程去共享同一个资源。

步骤

演示Thread和Runnable资源共享的区别:程序中经常会有多个线程操作同一个资源。而使用Thread的继承方式不利于资源的共享。

创建Thread子类

在子类中定义共享变量 number = 10。

创建线程任务,在任务中打印输出共享的变量。

创建多个线程对象,每个线程对象分别调用共享变量number并赋不同的值。

启动线程任务,查看输出结果。

使用Runnable接口的方式将上述过程在完成一次,然后进行对比。

实现

通过Thread类实现

public class MyThread extends Thread {
// 定义共享变量
int number = 10;
// 2、 在类中重写run方法,即线程任务
public void run() {
System.out.println( getName() + “…” + number );
}
}
通过Runnable接口实现

public class MyRunnable implements Runnable {
// 定义共享变量
int number = 10;
@Override
public void run() {
System.out.println( Thread.currentThread().getName() + “…” + number );
}
}
测试类

class ThreadDemo {

public static void main(String[] args) {
// 使用Thread实现。
/* 创建线程子类对象
MyThread mt1 = new MyThread();
mt1.number = 100;
// 启动线程任务
mt1.start();

// 创建线程子类对象
MyThread mt2 = new MyThread();
mt2.number = 1000;
// 启动线程任务
mt2.start();*/

    // 使用Runnable接口实现。
    // 创建实现类对象
    MyRunnable my = new MyRunnable();
    my.number = 1000;
    // 创建线程对象
    Thread t1 = new Thread(my);
    Thread t2 = new Thread(my);
    // 启动线程
    t1.start();
    t2.start();
}

}
小结

因为使用Runnable接口的方式实现多线程的方式更优,因此在开发中。如果有的多线程的需求,应该首先考虑使用Runnable接口进行实现。

扩充:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程。
1.5 匿名内部类方式实现线程的创建

目标

能够使用匿名内部类的方式启动线程任务。

语义分析:当我们需要使用多线程的方式执行程序时,有两种方式:

使用Thread子类完成: 我们真的想定义一个子类吗 ? 不,只是语法上的要求。

使用Runnable接口完成:我们真的想定义一个实现类吗 ? 不,也是因为语法上的要求。

那么,能不能在不创建子类和实现类的前提下,就能够实现多线程的任务创建并启动呢?

多线程创建语法优化 之 匿名内部类 : 使用匿名内部类优化代码,省略了创建子类和实现类的过程。

匿名内部类的语法格式:

new 子类 或 实现类() {
重写子类或实现类的方法;
};
产生的结果是当前子类或实现类的对象。
步骤

创建测试类,并定义main方法。

创建Thread匿名内部类对象,重写run()方法,并启动线程任务。

创建Runnable接口匿名实现类对象,并多态赋值给Runnable接口类型。

创建Thread匿名对象,并将Runnable接口对象传递给构造方法,并启动线程。

创建Thread匿名对象,并在构造方法中创建Runnable接口内名内部类对象,重写run方法,并启动线程。

实现

// 演示匿名方式创建并启动线程任务
public class ThreadDemo {
public static void main(String[] args) {

  // 创建Thread匿名内部类对象,重写run()方法,并启动线程任务。
    new Thread(){
        public void run() {
            for(int i = 1; i <= 20; i++ ) {
                System.out.println( Thread.currentThread().getName() + "..." + i );
            }
        }
    }.start();
  
  
    // 创建Runnable接口匿名实现类对象,并多态赋值给Runnable接口类型。
    Runnable r = new Runnable(){
        @Override // 线程任务
        public void run() {
            for(int i = 1; i <= 20; i++ ) {
                System.out.println( Thread.currentThread().getName() + "..." + i );
            }
        }
    };
    // 创建Thread匿名对象,并将Runnable接口对象传递给构造方法,并启动线程。
    new Thread(r).start();



// 创建Thread匿名对象,并在构造方法中创建Runnable接口内名内部类对象,重写run方法,并启动线程。
new Thread(new Runnable() {
@Override // 线程任务
public void run() {
for(int i = 1; i <= 20; i++ ) {
System.out.println( Thread.currentThread().getName() + “…” + i );
}
}
}).start();


/*
面试题:
使用Runnable接口和Thread类结合匿名的方式创建线程任务并启动。
运行的结果执行的是重写类中的run方法,父类高于接口的优先级。
*/
new Thread( new Runnable() {
@Override // 重写接口中的线程任务
public void run() {
for(int i = 1; i <= 20; i++ ) {
System.out.println( “接口中的…” + i );
}
}
}){
// 重写Thread类中的线程任务
public void run() {
for(int i = 1; i <= 20; i++ ) {
System.out.println( “类中的…” + i );
}
}
}.start();
}
}
小结

如果只需要将线程任务启动一次,使用匿名内部类的方式更能使代码更加简单,省略了创建实现类和子类的过程。

第二章 线程安全
2.1 线程安全

目标

能够理解什么是线程安全问题,以及造成线程安全的原因。

场景:在线程的操作中,经常会遇到多个线程要操作同一个共享的资源。因此会产生各种安全问题:

比如:某个线程正要获取操作当前共享资源,另外一条线程将此共享资源删除了。

步骤

需求:模拟火车站售票。某辆动车的所有车票共100张。由多个售票窗口共享这些车票并出售。

100张车票为共享资源,有售票窗口共享。

每一个售票窗口可以理解为一条线程。

创建Runnable实现类Ticket。

创建共享资源车票: ticket= 100;

重写run方法(线程任务:就是售票)

在线程任务中获取线程名称,并打印ticket表示车票已经售出。

将售出的车票从所有的车票中扣除 ticket–

创建4个线程对象,并启动线程任务,查看数据。

实现

// 创建售票任务
public class Ticket implements Runnable {
// 定义变量,模拟电影票
private int ticket = 100;
// 2、 创建线程任务,模拟窗口售票
public void run() {
// 死循环模拟窗口一直在售票
while( true ) {
// 判断是否还有余票
if( ticket > 0 ) {
// 获取线程(窗口)的名字
String name = Thread.currentThread().getName();
// 输出语句模拟车票售出
System.out.println( name + “正在售出:” + ticket );

// 使用线程睡眠模拟出票的时间
try {Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}

// 电影票售出后需要将当前售出的票从总票中减除
ticket–;
}
}
}
}
测试类:

// 演示线程安全
public class TicketDemo {
public static void main(String[] args) {
// 创建实现类对象 线程任务
Ticket ticket = new Ticket();

// 创建4个售票窗口线程
Thread t1 = new Thread(ticket,“窗口1”);
Thread t2 = new Thread(ticket,“窗口2”);
Thread t3 = new Thread(ticket,“窗口3”);
Thread t4 = new Thread(ticket,“窗口4”);

// 启动线程任务,开始售票
t1.start();
t2.start();
t3.start();
t4.start();
}
}
小结

结果中有一部分这样现象:

有卖出相同的票数

有售出不存在的票,比如0票与-1票,是不存在的。

是多个线程在执行售票的任务的时候,由于在售票的代码中访问了同一个成员变量ticket。可是在操作ticket的这些语句中,一个线程操作到其中的一部分代码的时候,CPU切换到其他的线程上开始执行代码。这样就会导致ticket变量中的值被修改的不一致。

这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。

总结出现线程安全问题的原因

首先必须有多线程。

多个线程在操作(写操作)共享的数据。

操共享数据的语句不止一条,并且对共享数据有修改。

本质原因是CPU在处理多个线程的时候,在操作共享数据的多条代码之间进行切换导致的。

当程序中出现了线程的安全问题之后,我们首要的工作就是解决线程的安全隐患。

2.2 线程同步

目标

能够同步代解决线程安全问题的原理。

当我们使用多个线程访问同一资源的时候,且多个线程中对共享资源有写操作,就容易出现线程安全问题。

要解决上述多线程并发访问一个资源的安全性问题:Java中提供了同步机制( synchronized )来解决。

synchronized:同步机制的原理

人为的控制CPU在执行某个线程操作共享数据的时候,不让其他线程进入到操作共享数据的代码中去,这样就可以保证安全。

可以理解为在操作共享语句的代码位置上了一道门,在门上装了一把锁。当有一个线程去操作代码时,会关上并通过锁锁上门。这样其他线程就无法进入到同步代码中。而当线程操作完所有的同步代码之后,在将锁打开。然后其他线程再次抢夺锁。

上述的这个解决方案:称为线程的同步。在java中,有三种方式完成同步操作:

同步代码块。

同步方法。

锁机制。

小结

当多线程中出现安全问题时,可以通过代码同步的方式来解决。原理就是在操作共享语句的代码上上一把锁。只允许拿到锁的线程去执行同步代码块。执行结束之后释放锁,然后所有线程再次抢夺锁。

2.3 同步代码块

目标

能够使用同步代码块解决售票案例的安全问题。

同步代码块:synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

同步代码块格式:

synchronized( 同步锁 ){
代码块中放操作共享数据的代码。
}
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。

同步锁: 可以是任意类型的一个对象(锁对象)。

锁对象必须唯一 : 多个线程对象 要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
步骤

在售票的代码中创建一个锁对象

使用synchronized同步锁将操作共享语句的代码同步。

运行程序,查看结果。

实现

// 创建售票任务 添加同步代码块
public class Ticket implements Runnable {
// 定义变量,模拟电影票
private int ticket = 100;
// 创建锁对象
private Object lock = new Object();

// 2、 创建线程任务,模拟窗口售票
public void run() {
// 死循环模拟窗口一直在售票
while( true ) {
// 对操作共享语句的代码,添加同步代码块
synchronized ( lock ) {
// 判断是否还有余票
if( ticket > 0 ) {
// 获取线程(窗口)的名字
String name = Thread.currentThread().getName();
// 输出语句模拟车票售出
System.out.println( name + “正在售出:” + ticket );

// 使用线程睡眠模拟出票的时间
try {Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}

// 电影票售出后需要将当前售出的票从总票中减除
ticket–;
}
}
}
}
}
小结

当使用了同步代码块后,线程的安全问题解决了。当一个线程对象拿到锁之后,会进入到同步代码块中执行,只有同步代码执行结束将锁释放之后,其他线程才能在次抢夺锁对象。

特点注意: 锁对象可以是任意类型对象,但是必须要唯一。

2.4 同步方法

目标

能够使用同步方法解决售票案例的安全问题。

解决多线程的安全问题,除了使用同步代码块之外,还可以使用同步方法,以及静态同步方法。

run也是方法,因此在run方法内部可以调用其他方法。被调用的方法也变为了线程任务。

同步方法:当某个方法或静态方法中的代码都在操作共享数据时,我们可以把同步加在当前这个方法上,这时整个方法都会被同步。使用synchronized修饰的方法,就叫做同步方法,保证某个线程执行该方法的时候,其他线程只能在方法外等着。

注意:不可以在run方法上加同步,否则无法实现多线程操作。因为run方法需要多个线程同时执行。

同步方法原理:

任何线程需要进入到同步方法时,都需要获取方法同步方法上的锁,没有锁,则无法进入同步方法中。

当某个方法被同步之后,方法上会有默认的锁对象。静态方法和非静态方法的默认锁对象也会不相同。

非静态同步方法的默认锁对象:this ,知道即可,不需要手动指定。

静态同步方法的默认锁对象: 实现类名.class , 知道即可,不需要手动指定。

同步方法语法格式

public synchronized void 方法名( 参数 ){
可能会产生线程安全问题的代码;
}
步骤

在原来的案例中进行代码的修改,使用同步方法对线程安全进行处理。

创建payTicket()方法。

将售票的线程任务代码写到payTicket()方法中。

在run方法中调用payTicket()方法。(当某个方法在run方法中被调用之后,当前方法也变为了线程任务。)

运行程序,查看结果。

使用synchronized关键字对方法进行修饰。

再次运行程序查看结果。

定义一个静态方法,并将ticket变量也静态。然后重复上述过程。

实现

// 创建售票任务 同步方法
public class Ticket implements Runnable {
// 定义变量,模拟电影票
private static int ticket = 100;

// 2、 创建线程任务,模拟窗口售票
public void run() {
// 死循环模拟窗口一直在售票
while (true) {
// 调用同步方法
payTicketStatic();
}
}

/*
同步方法 , 将操作共享语句的所有代码抽取到同步方法中。
同步方法 :
1、将操作共享语句的所有代码抽取到同步方法中。
2、在方法上使用synchronize关键字修饰
修饰符 synchronize 返回值类型 方法名(参数列表) {
操作共享语句的代码;
}

    非静态同步方法上的锁对象就是实现类的对象:
        new Ticket(); 也就是this


静态同步方法上的锁对象不能是this,因为静态方法优先于对象。
静态同步方法上的锁对象是本类的class属性------> Ticket.class(反射)
Ticket.class;
*/
// 非静态同步方法
public /synchronized/ void payTicket(){
synchronized ( this ) {
// 判断是否还有余票
if (ticket > 0) {
// 获取线程(窗口)的名字
String name = Thread.currentThread().getName();
// 输出语句模拟车票售出
System.out.println(name + “正在售出:” + ticket);

// 使用线程睡眠模拟出票的时间
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 电影票售出后需要将当前售出的票从总票中减除
ticket–;
}
}
}

// 静态同步方法
public static /synchronized/ void payTicketStatic(){
synchronized ( Ticket.class ) {
// 判断是否还有余票
if (ticket > 0) {
// 获取线程(窗口)的名字
String name = Thread.currentThread().getName();
// 输出语句模拟车票售出
System.out.println(name + “正在售出:” + ticket);

// 使用线程睡眠模拟出票的时间
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 电影票售出后需要将当前售出的票从总票中减除
ticket–;
}
}
}
}
小结

使用同步方法也可以解决线程的安全问题。需要注意的一旦某个方法使用synchronized进行修饰之后,当前方法就会变为同步方法,同步方法都有默认的锁对象。

​ 非静态方法的锁对象为: this。 静态方法的锁对象为:实现类名.class

特别注意:同步关键字不可以使用在run方法上,否则无法使用多线程的方式运行run方法。

2.5 Lock锁

目标

能够使用Lock锁解决售票案例的线程安全问题。

在JDK1.5的时候,java增加了Lock锁来解决线程的安全问题。

java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更灵活强大,更体现面向对象。

Lock锁也称同步锁,加锁与释放锁被方法化了

public void lock() :加同步锁。

public void unlock() :释放同步锁。

Lock锁对象的获取: 因为Lock为接口类型,无法直接创建对象。可以多态创建其实现类对象。

Lock lock = new RenntrantLock();
步骤

在原来的案例中进行代码的修改,使用同步方法对线程安全进行处理。

通过多态的方式创建Lock锁对象。

在操作共享语句的代码上方,通过锁对象调用获取锁的方法lock()获取锁。

在操作共享语句的代码下方,通过锁对象调用释放锁的方法unlock()释放锁。

实现

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*
java.util.concurrent.locks.Lock;
接口不能直接创建对象,可以使用多态new实现类对象。ReentrantLock
接口中提供了两个方法:
lock(): 获取锁
unlock(): 释放锁

为了防止程序在执行的过程中遇到异常,导致锁无法释放,我们可以使用
try{}fianlly{} 代码块来解决这个问题
try代码块中书写正常执行的代码
finally代码块中书写释放锁的方法,这样在任何情况下释放锁的代码都会执行

*/
// 创建售票任务 添加Lock锁对象
public class Ticket implements Runnable {
// 定义变量,模拟电影票
private int ticket = 100;
// 创建Lock锁对象
Lock lock = new ReentrantLock();

// 2、 创建线程任务,模拟窗口售票
public void run() {
// 死循环模拟窗口一直在售票
while (true) {
// 使用lock() 方法上锁
lock.lock();
try{
// 判断是否还有余票
if (ticket > 0) {
// 获取线程(窗口)的名字
String name = Thread.currentThread().getName();
// 输出语句模拟车票售出
System.out.println(name + “正在售出:” + ticket);

// 使用线程睡眠模拟出票的时间
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 电影票售出后需要将当前售出的票从总票中减除
ticket–;
}
} finally { // 不管程序是否出现异常,最终都需要释放锁
// 使用unlock()方法释放锁
lock.unlock();
}
}
}
}
小结

JDK1.5增加的Lock锁,解决线程安全问题的操作更加强大和灵活。因为Lock为接口,在获取锁对象时只能创建其子类对象ReentrantLock。

特别注意:不管程序的执行结果如何,最终我们都需要释放锁对象。因此我们需要将释放锁对象的方法定义在finally代码块中。这样即使在程序运行的过程中出现了异常,也能够正常的释放锁对象。

第三章 线程状态
3.1 线程状态概述

目标

能够理解现象的6种状态。

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State这个枚举中给出了六种线程状态:

线程状态 导致状态发生条件
NEW(新建) 线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
Timed Waiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
我们不需要去研究这几种状态的实现原理,我们只需知道在线程操作中存在这样的状态。

小结

线程状态

3.2 Timed Waiting(计时等待)

目标

能够使用sleep()方法让线程进入睡眠状态。

public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等待),而在调用sleep()方法时,需要传递一个long类型的整数,这个整数单位为毫秒,意思就是当到达指定的毫秒时,线程就会自动苏醒。但线程苏醒后不一定直接进入运行状态,有可能会处在临时阻塞状态。

实现一个闹钟,计数到10,在每个数字之间暂停1秒,10秒钟的时候结束程序。

步骤

使用Runnable接口创建线程任务

线程任务中定义循环,共计循环10次。

每循环一次打印一次循环变量

每打印一次循环遍历,调用一次sleep()方法,让线程休息1000毫秒

循环结束输出“叮铃铃…”

创建测试类,运行线程任务。

实现

// 演示线程的休眠状态
class ThreadSleep implements Runnable {
// 创建线程任务,模拟闹钟
public void run() {
// 10秒钟闹钟自动报警
for( int i = 1; i <= 10; i++ ) {
// 模拟输出秒
System.out.println(“第”+ i +“秒!”);

// 让线程休息1000毫秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(“叮铃铃…”);
}
}
public class ThreadSleepDemo {
public static void main(String[] args) {
// 创建线程任务
ThreadSleep ts = new ThreadSleep();
// 创建线程对象
Thread t = new Thread( ts );
// 启动线程任务
t.start();
}
}
小结

通过案例可以发现,sleep方法的使用还是很简单的。我们需要记住下面几点:

进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。

为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠

sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。

小提示:sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始立刻执行。
Timed Waiting 线程状态图:

3.3 BLOCKED(锁阻塞)

目标

理解什么是锁阻塞状态。

Blocked状态在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。

我们已经学完同步机制,那么这个状态是非常好理解的了。比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。

这是由Runnable状态进入Blocked状态。除此Waiting以及Time Waiting状态也会在某种情况下进入阻塞状态,而这部分内容作为扩充知识点带领大家了解一下。

Blocked 线程状态图

小结

简单来说,就是启动之后没有获取到锁的线程。处于此状态。

3.4 Waiting(无限等待)

目标

理解什么是无限等待状态,以及能够让线程进入和退出此状态。

Wating状态在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

当一个线程对象调用了wait()方法之后,当前线程就会处在等待状态,如果没有另外一个线程对象唤醒它,则会一直等待。只有某一个线程对象调用了notify()方法 或 notifyAll()方法之后,等待中的线程才会被唤醒。

当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒而不是执行,如果获取到锁对象,那么A线程唤醒后就进入Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。

步骤

演示线程之间的通信协助,等待状态的进入和退出。

使用匿名内部类的方式开启买包子线程。

线程任务是买包子,告诉老板需求后,调用wait()进入等待状态,等待老板做包子

使用匿名内部类方式开启卖包子线程。

线程任务就是卖包子,在收到买包子的请求时开始做包子

让线程休息3秒,表示正在做包子(模拟做包子的过程)

3秒过后,表示包子制作完成,调用notify()方法唤醒等待中的线程

启动程序,查看结果

实现

// 演示线程的等待状态
public class ThreadWaitDemo {
// 创建锁对象
private static Object lock = new Object();

public static void main(String[] args) {

// 创建等待线程 模拟顾客买包子
new Thread(){
@Override // 线程任务 让线程等待
public void run() {
// 循环让线程一致执行
while( true ) {
synchronized ( lock ) {
System.out.println(“老板,来10个肥肉包子…”);
// 让线程等待
try {
lock.wait(); // 调用让线程等待的方法
} catch (InterruptedException e) {
e.printStackTrace();
}

// 唤醒1秒后开始吃包子
try {
lock.wait(1000);
System.out.println(“吃完就减肥…”);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}.start();

    // 创建等待线程
    new Thread() {
        @Override // 线程任务,唤醒等待的线程
        public void run() {
            while( true ) {
                synchronized ( lock ) {
                    try {
                        System.out.println("开始做包子...");
                        // 经过3秒,包子准备好了
                        lock.wait(3000);
                        lock.notify(); // 唤醒等包子的顾客线程...
                        System.out.println("您的包子做好了...");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }.start();
}

}
小结

Waiting 线程状态图

3.5 补充知识点

到此为止我们已经对线程状态有了基本的认识,想要有更多的了解,详情可以见下图:

一条有意思的tips:

我们在翻阅API的时候会发现Timed Waiting(计时等待) 与 Waiting(无限等待) 状态联系还是很紧密的,比如Waiting(无限等待) 状态中wait方法是空参的,而timed waiting(计时等待) 中wait方法是带参的。这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通知,可是如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实是一举两得。如果没有得到(唤醒)通知,那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来;如果在倒计时期间得到(唤醒)通知,那么线程从Timed Waiting状态立刻唤醒。

猜你喜欢

转载自blog.csdn.net/u014452148/article/details/85866604