线程的状态和基本操作

在上一篇博客中 并发编程的优缺点 谈到了为什么花功夫去学习并发编程的技术。万事开头难,接下来就应该了解如何新建一个线程?线程状态是怎样转换的?关于线程状态的操作是怎样的?这篇博客就主要围绕这三个方面来聊一聊。 文章目录 创建线程的四种方式 线程的状态和生命周期 线程状态的基本操作 interrupted join sleep yield 进程和线程 线程优先级 守护线程和用户线程 守护线程详解

在上一篇博客中并发编程的优缺点谈到了为什么花功夫去学习并发编程的技术。万事开头难,接下来就应该了解如何新建一个线程?线程状态是怎样转换的?关于线程状态的操作是怎样的?这篇博客就主要围绕这三个方面来聊一聊。

创建线程的四种方式

创建线程的四种方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 使用Callable和Future创建线程
  4. 使用Executor框架创建线程池

创建线程的具体实现可以参考创建线程的四种方式

线程的状态和生命周期

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。

Java线程的状态

线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节):

在这里插入图片描述

由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。

操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。

在这里插入图片描述

当线程执行 wait()方法之后,线程进入 **WAITING(等待)**状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()方法之后将会进入到 TERMINATED(终止) 状态。

线程状态的基本操作

除了新建一个线程外,线程在生命周期内还有需要进行一些基本操作,而这些操作会成为线程间一种通信方式,比如使用中断(interrupted)方式通知实现线程间的交互等等,下面就将具体说说这些操作。

interrupted

中断可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了一个招呼。其他线程可以调用该线程的interrupt()方法对其进行中断操作,同时该线程可以调用
isInterrupted()来感知其他线程对其自身的中断操作,从而做出响应。另外,同样可以调用Thread的静态方法
interrupted()对当前线程进行中断操作,该方法会清除中断标志位。需要注意的是,当抛出InterruptedException时候,会清除中断标志位,也就是说在调用isInterrupted会返回false。

方法名 详细解释 备注
public void interrupt() 中断该线程对象 如果该线程被调用了Object wait/Object wait(long),或者被调用sleep(long),join()/join(long)方法时会抛出interruptedException并且中断标志位将会被清除
public boolean isinterrupted() 测试该线程对象是否被中断 中断标志位不会被清除
public static boolean interrupted() 测试当前线程是否被中断 中断标志位会被清除

下面结合具体的实例来看一看

public class InterruptDemo {    public static void main(String[] args) throws InterruptedException {        //sleepThread睡眠1000ms        final Thread sleepThread = new Thread() {            @Override            public void run() {                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    e.printStackTrace();                }                super.run();            }        };        //busyThread一直执行死循环        Thread busyThread = new Thread() {            @Override            public void run() {                while (true) ;            }        };        sleepThread.start();        busyThread.start();        sleepThread.interrupt();        busyThread.interrupt();        while (sleepThread.isInterrupted()) ;        System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted());        System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted());    }}

输出结果

sleepThread isInterrupted: falsebusyThread isInterrupted: true

java.lang.InterruptedException: sleep interrupted

at java.lang.Thread.sleep(Native Method)at com.jourwon.test.InterruptDemo$1.run(InterruptDemo.java:17)

开启了两个线程分别为sleepThread和BusyThread, sleepThread睡眠1s,BusyThread执行死循环。然后分别对着两个线程进行中断操作,可以看出sleepThread抛出InterruptedException后清除标志位,而busyThread就不会清除标志位。

另外,同样可以通过中断的方式实现线程间的简单交互, while (sleepThread.isInterrupted()) 表示在Main线程中会持续监测sleepThread线程,一旦sleepThread的中断标志位清零,即sleepThread.isInterrupted()返回为false时才会继续Main线程才会继续往下执行。因此,中断操作可以看做线程间一种简便的交互方式。一般在结束线程时通过中断标志位或者标志位的方式可以有机会去清理资源,相对于武断而直接的结束线程,这种方式要优雅和安全

join

join方法可以看做是线程间协作的一种方式,很多时候,一个线程的输入可能非常依赖于另一个线程的输出,这就像两个好基友,一个基友先走在前面突然看见另一个基友落在后面了,这个时候他就会在原处等一等这个基友,等基友赶上来后,就两人携手并进。其实线程间的这种协作方式也符合现实生活。在软件开发的过程中,从客户那里获取需求后,需要经过需求分析师进行需求分解后,这个时候产品,开发才会继续跟进。如果一个线程实例A执行了threadB.join(),其含义是:当前线程A会等待threadB线程终止后threadA才会继续执行。关于join方法一共提供如下这些方法:

方法名 详细注释 备注
public final void join() throws InterruptedException 等待这个线程死亡。 如果任何线程中断当前线程,如果抛出InterruptedException异常时,当前线程的中断状态将被清除
public final void join(long millis) throws InterruptedException 等待这个线程死亡的时间最多为millis毫秒。 0的超时意味着永远等待。 如果millis为负数,抛出IllegalArgumentException异常
public final void join(long millis, int nanos) throws InterruptedException 等待最多millis毫秒加上这个线程死亡的nanos纳秒。 如果millis为负数或者nanos不在0-999999范围抛出IllegalArgumentException异常

Thread类除了提供join()方法外,另外还提供了超时等待的方法,如果线程threadB在等待的时间内还没有结束的话,threadA会在超时之后继续执行。join方法源码关键是:

 while (isAlive()) {

wait(0);

}

可以看出来当前等待对象threadA会一直阻塞,直到被等待对象threadB结束后即isAlive()返回false的时候才会结束while循环,当threadB退出时会调用notifyAll()方法通知所有的等待线程。下面用一个具体的例子来说说join方法的使用:

public class JoinDemo {    public static void main(String[] args) {        Thread previousThread = Thread.currentThread();        for (int i = 1; i <= 10; i++) {            Thread curThread = new JoinThread(previousThread);            curThread.start();            previousThread = curThread;        }    }    static class JoinThread extends Thread {        private Thread thread;        public JoinThread(Thread thread) {            this.thread = thread;        }        @Override        public void run() {            try {                thread.join();                System.out.println(thread.getName() + " terminated.");            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}

输出结果为:

main terminated.Thread-0 terminated.Thread-1 terminated.Thread-2 terminated.Thread-3 terminated.Thread-4 terminated.Thread-5 terminated.Thread-6 terminated.Thread-7 terminated.Thread-8 terminated.

在上面的例子中一个创建了10个线程,每个线程都会等待前一个线程结束才会继续运行。可以通俗的理解成接力,前一个线程将接力棒传给下一个线程,然后又传给下一个线程…

sleep

public static native void sleep(long millis)方法显然是Thread的静态方法,很显然它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。需要注意的是如果当前线程获得了锁,sleep方法并不会失去锁。sleep方法经常拿来与Object.wait()方法进行比价,这也是面试经常被问的地方。

sleep() VS wait()

两者主要的区别:

  1. sleep()方法是Thread的静态方法,而wait是Object实例方法
  2. wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁;
  3. sleep()方法在休眠时间达到后,如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。

yield

public static native void yield()这是一个静态方法,一旦执行,它会是当前线程让出CPU,但是,需要注意的是,让出的CPU并不是代表当前线程不再运行了,如果在下一次竞争中,又获得了CPU时间片当前线程依然会继续运行。另外,让出的时间片只会分配给当前线程相同优先级的线程。什么是线程优先级了?下面就来具体聊一聊。

现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当前时间片用完后就会发生线程调度,并等待下次分配。线程分配到的时间多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要或多或少分配一些处理器资源的线程属性。

在Java程序中,通过一个整型成员变量Priority来控制优先级,优先级的范围从1~10.在构建线程的时候可以通过**setPriority(int)**方法进行设置,默认优先级为5,优先级高的线程相较于优先级低的线程优先获得处理器时间片。需要注意的是在不同JVM以及操作系统上,线程规划存在差异,有些操作系统甚至会忽略线程优先级的设定。

另外需要注意的是,sleep()和yield()方法,同样都是当前线程会交出处理器资源,而它们不同的是,sleep()交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片。而yield()方法只允许与当前线程具有相同优先级的线程能够获得释放出来的CPU时间片。

进程和线程

进程和线程的详细区别请参考进程和线程的区别(超详细)

线程优先级

理论上来说系统会根据优先级来决定首先使哪个线程进入运行状态。当 CPU 比较闲的时候,设置线程优先级几乎不会有任何作用,而且很多操作系统压根不会理会你设置的线程优先级,所以不要让业务过度依赖于线程的优先级。

另外,线程优先级具有继承特性比如 A 线程启动 B 线程,则 B 线程的优先级和 A 是一样的。线程优先级还具有随机性 也就是说线程优先级高的不一定每一次都先执行完。

Thread 类中包含的成员变量代表了线程的某些优先级。如Thread.MIN_PRIORITY(常数 1)Thread.NORM_PRIORITY(常数 5),Thread.MAX_PRIORITY(常数 10)。其中每个线程的优先级都在110 之间,1的优先级为最低,10的优先级为最高,在默认情况下优先级都是Thread.NORM_PRIORITY(常数 5)

一般情况下,不会对线程设定优先级别,更不会让某些业务严重地依赖线程的优先级别,比如权重,借助优先级设定某个任务的权重,这种方式是不可取的,一般定义线程的时候使用默认的优先级就好了。

相关方法:

public final void setPriority(int newPriority) //为线程设定优先级public final int getPriority() //获取线程的优先级

设置线程优先级方法源码:

public final void setPriority(int newPriority) {    ThreadGroup g;    checkAccess();    //线程游戏优先级不能小于 1 也不能大于 10,否则会抛出异常    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {        throw new IllegalArgumentException();    }    //如果指定的线程优先级大于该线程所在线程组的最大优先级,那么该线程的优先级将设为线程组的最大优先级    if((g = getThreadGroup()) != null) {        if (newPriority > g.getMaxPriority()) {            newPriority = g.getMaxPriority();        }        setPriority0(priority = newPriority);    }}

守护线程和用户线程

守护线程和用户线程简介:

  • 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
  • 守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作

main 函数所在的线程就是一个用户线程啊,main 函数启动的同时在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。

那么守护线程和用户线程有什么区别呢?

比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线程运行。而守护线程不会影响 JVM 的退出。

注意事项:

  1. setDaemon(true)必须在start()方法前执行,否则会抛出 IllegalThreadStateException 异常
  2. 在守护线程中产生的新线程也是守护线程
  3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
  4. 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语句块可能无法被执行。

守护线程详解

守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地守护一些系统服务,比如垃圾回收线程,JIT线程就可以理解为守护线程。与之对应的就是用户线程,用户线程就可以认为是系统的工作线程,它会完成整个系统的业务操作。用户线程完全结束后就意味着整个系统的业务任务全部结束了,因此系统就没有对象需要守护的了,守护线程自然而然就会退。当一个Java应用,只有守护线程的时候,虚拟机就会自然退出。下面以一个简单的例子来表述Daemon线程的使用。

public class DaemonDemo {    public static void main(String[] args) {        Thread daemonThread = new Thread(new Runnable() {            @Override            public void run() {                while (true) {                    try {                        System.out.println("i am alive");                        Thread.sleep(500);                    } catch (InterruptedException e) {                        e.printStackTrace();                    } finally {                        System.out.println("finally block");                    }                }            }        });        daemonThread.setDaemon(true);        daemonThread.start();        //确保main线程结束前能给daemonThread能够分到时间片        try {            Thread.sleep(800);        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

输出结果为:

i am alive

finally block

i am alive

上面的例子中daemodThread run()方法中是一个while死循环,会一直打印,但是当main线程结束后daemonThread就会退出所以不会出现死循环的情况。main线程先睡眠800ms保证daemonThread能够拥有一次时间片的机会,也就是说可以正常执行一次打印“i am alive”操作和一次finally块中"finally block"操作。紧接着main 线程结束后,daemonThread退出,这个时候只打印了"i am alive"并没有打印finnal块中的。因此,这里需要注意的是守护线程在退出的时候并不会执行finnaly块中的代码,所以守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑

线程可以通过setDaemon(true)的方法将线程设置为守护线程。并且需要注意的是设置守护线程要先于start()方法,否则会报

Exception in thread "main" java.lang.IllegalThreadStateException

但是该线程还是会执行,只不过会当做正常的用户线程执行。

线程死锁

认识线程死锁

百度百科:死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

线程死锁

下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》):

public class DeadLockDemo {    private static Object resource1 = new Object();//资源 1    private static Object resource2 = new Object();//资源 2    public static void main(String[] args) {        new Thread(() -> {            synchronized (resource1) {                System.out.println(Thread.currentThread() + "get resource1");                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    e.printStackTrace();                }                System.out.println(Thread.currentThread() + "waiting get resource2");                synchronized (resource2) {                    System.out.println(Thread.currentThread() + "get resource2");                }            }        }, "线程 1").start();        new Thread(() -> {            synchronized (resource2) {                System.out.println(Thread.currentThread() + "get resource2");                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    e.printStackTrace();                }                System.out.println(Thread.currentThread() + "waiting get resource1");                synchronized (resource1) {                    System.out.println(Thread.currentThread() + "get resource1");                }            }        }, "线程 2").start();    }}

输出结果

Thread[线程 1,5,main]get resource1

Thread[线程 2,5,main]get resource2

Thread[线程 1,5,main]waiting get resource2

Thread[线程 2,5,main]waiting get resource1

线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到CPU执行权,然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。

形成死锁的四个必要条件:

  1. 互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
  2. 请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞

如何避免线程死锁

我们只要破坏产生死锁的四个条件中的其中一个就可以了。

破坏互斥条件

这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

破坏请求与保持条件

一次性申请所有的资源。

破坏不剥夺条件

占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

破坏循环等待条件

靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

我们对线程 2 的代码修改成下面这样就不会产生死锁了。

new Thread(() -> {    synchronized (resource1) {        System.out.println(Thread.currentThread() + "get resource1");        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println(Thread.currentThread() + "waiting get resource2");        synchronized (resource2) {            System.out.println(Thread.currentThread() + "get resource2");        }    }}, "线程 2").start();

输出结果

Thread[线程 1,5,main]get resource1

Thread[线程 1,5,main]waiting get resource2

Thread[线程 1,5,main]get resource2

Thread[线程 2,5,main]get resource1

Thread[线程 2,5,main]waiting get resource2

Thread[线程 2,5,main]get resource2

我们分析一下上面的代码为什么避免了死锁的发生?

线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。

知秋君
上一篇 2024-07-03 15:30
下一篇 2024-07-03 15:30

相关推荐