对Java线程概念的理解

1、什么是线程

  现代操作系统在运行一个程序时, 会为其创建一个进程。 例如, 启动一个Java程序, 操作系统就会创建一个Java进程。 现代操作系统调度的最小单元是线程, 也叫轻量级进程(Light Weight Process) , 在一个进程里可以创建多个线程, 这些线程都拥有各自的计数器、 堆栈和局部变量等属性, 并且能够访问共享的内存变量。 处理器在这些线程上高速切换, 让使用者感觉到这些线程在同时执行

  一个Java程序从main()方法开始执行, 然后按照既定的代码逻辑执行, 看似没有其他线程参与, 但实际上Java程序天生就是多线程程序, 因为执行main()方法的是一个名 称为main的线程。 下面使用JMX来查看一个普通的Java程序包含哪些线程, 代码如下:

public class MultiThread{
    public static void main(String[ ] args) {
        // 获取Java线程管理MXBean
        ThreadMXBean threadMXBean = ManagementFactory. getThreadMXBean() ;
        // 不需要获取同步的monitor和synchronizer信息,仅获取线程和线程堆栈信息
        ThreadInfo[ ] threadInfos = 
            threadMXBean.dumpAllThreads(false, false) ;
        // 遍历线程信息,仅打印线程ID和线程名称信息
        for (ThreadInfo threadInfo : threadInfos) {
                System. out. println("[ " + threadInfo. getThreadId() + 
                    "] " + threadInfo.getThreadName()) ;
        }
    }
}

输出结果如下(多次运行,结果可能不同):

[4] Signal Dispatcher   //分发处理发送给JVM信号的线程
[3] Finalizer           //调用对象finalize方法的线程
[2] Reference Handler  //清除Reference的线程
[1] main               //main线程,用户程序入口

  可以看到, 一个Java程序的运行不仅仅是main()方法的运行, 而是main线程和多个其他线程的同时运行。

2、为什么要使用多线程

  执行一个简单的”Hello,World!”, 却启动了那么多的”无关”线程, 是不是把简单的问题复杂化了? 当然不是, 因为正确使用多线程, 总是能够给开发人员带来显著的好处, 而使用多线程的原因主要有以下几点:

  (1)更多的处理器核心

  随着处理器上的核心数量越来越多, 以及超线程技术的广泛运用, 现在大多数计算机都比以往更加擅长并行计算, 而处理器性能的提升方式, 也从更高的主频向更多的核心发展。 如何利用好处理器上的多个核心也成了现在的主要问题。线程是大多数操作系统调度的基本单元, 一个程序作为一个进程来运行, 程序运行过程中能够创建多个线程, 而一个线程在一个时刻只能运行在一个处理器核心上。 试想一下, 一个单线程程序在运行时只能使用一个处理器核心, 那么再多的处理器核心加入也无法显著提升该程序的执行效率。 相反, 如果该程序使用多线程技术, 将计算逻辑分配到多个处理器核心上, 就会显著减少程序的处理时间, 并且随着更多处理器核心的加入而变得更有效率。

  (2)更短的响应时间

  有时我们会编写一些较为复杂的代码(这里的复杂不是说复杂的算法, 而是复杂的业务逻辑), 例如, 一笔订单的创建, 它包括插入订单数据、 生成订单快照、 发送邮件通知卖家和记录货品销售数量等。 用户从单击“订购”按钮开始, 就要等待这些操作全部完成才能看到订购成功的结果。 但是这么多业务操作, 如何能够让其更快地完成呢?在上面的场景中, 可以使用多线程技术, 即将数据一致性不强的操作派发给其他线程处 理(也可以使用消息队列) , 如生成订单快照、 发送邮件等。 这样做的好处是响应用户请求的线程能够尽可能快地处理完成, 缩短了响应时间, 提升了用户体验。

  (3)更好的编程模型

  Java为多线程编程提供了良好、 考究并且一致的编程模型, 使开发人员能够更加专注于问题的解决, 即为所遇到的问题建立合适的模型, 而不是绞尽脑汁地考虑如何将其多线程化。 一旦开发人员建立好了模型, 稍做修改总是能够方便地映射到Java提供的多线程编程模型上。

3、线程优先级

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

  在Java线程中, 通过一个整型成员变量priority来控制优先级, 优先级的范围从1~10, 在线程构建的时候可以通过setPriority(int)方法来修改优先级, 默认优先级是5, 优先级高的线程分配时间片的数量要多于优先级低的线程。 设置线程优先级时, 针对频繁阻塞(休眠或者I/O操作) 的线程需要设置较高优先级, 而偏重计算(需要较多CPU时间或者偏运算) 的线程则设置较低的优先级, 确保处理器不会被独占。 在不同的JVM以及操作系统上, 线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定(因此,在程序中设置线程优先级的实践意义并不大,因为线程优先级的最终解释权在底层操作系统)。示例代码如下:

public class Priority {

    private static volatile boolean notStart = true;
    private static volatile boolean notEnd = true;

    public static void main(String[ ] args) throws Exception {
        List jobs = new ArrayList();

        for (int i = 0; i < 10; i++) {
            int priority = i < 5 ? Thread.MIN_PRIORITY : Thread.MAX_PRIORITY;
            Job job = new Job(priority);
            jobs.add(job);
            Thread thread = new Thread(job, "Thread: " + i);
            thread.setPriority(priority);
            thread.start();
        }

        notStart = false;
        TimeUnit. SECONDS.sleep(10);
        notEnd = false;

        for (Job job : jobs) {
            System.out.println("Job Priority : " + job.priority + ",Count : " + job.jobCount);
            }
    }

    static class Job implements Runnable {
        private int priority;
        private long jobCount;
        public Job(int priority) {
            this. priority = priority;
        }

        public void run() {
            while (notStart) {
                Thread.yield() ;
            }

            while (notEnd) {
                Thread.yield() ;
                jobCount++;
        }
    }

}

  运行该示例, 在博主笔记本上的输出如下:

Job Priority : 1, Count : 1259592
Job Priority : 1, Count : 1260717
Job Priority : 1, Count : 1264510
Job Priority : 1, Count : 1251897
Job Priority : 1, Count : 1264060
Job Priority : 10, Count : 1256938
Job Priority : 10, Count : 1267663
Job Priority : 10, Count : 1260637
Job Priority : 10, Count : 1261705
Job Priority : 10, Count : 1259967

  从输出可以看到线程优先级没有生效, 优先级1和优先级10的Job计数的结果非常相近,没有明显差距。 这表示程序正确性不能依赖线程的优先级高低。因为操作系统可以完全不用理会Java 线程对于优先级的设定。

4、并发与并行的区别

  如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统。如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。并发系统与并行系统这两个定义之间的关键差异在于“存在”这个词。

  在并发程序中可以同时拥有两个或者多个线程。这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入或者换出内存。这些线程是同时“存在”的——每个线程都处于执行过程中的某个状态。如果程序能够并行执行,那么就一定是运行在多核处理器上。此时,程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。

  “并行”概念是“并发”概念的一个子集。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。

  用一个极其简单的生活实例来解释如下:
  你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
  你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
  你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
  并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。

5、线程状态及其切换

  下面的这个图非常重要!你如果看懂了这个图,那么对于多线程的理解将会更加深刻。

  1、新建状态(New):新创建了一个线程对象。
  2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程队列中,变得可运行,等待获取CPU的使用权
  3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
  4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会再次转到运行状态。阻塞的情况分三种:
  (一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中(wait会释放持有的锁)。
  (二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
  (三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态(注意,sleep不会释放线程所持有的锁)。
  5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

  Java线程在运行的生命周期中可能处于下图表中所示的6种不同的状态, 在给定的一个时刻,线程只能处于其中的一个状态。

对Java线程概念的理解_第1张图片

  下面使用jstack工具(可以选择打开终端, 键入jstack或者到JDK安装目录的bin目录下执行命令), 尝试查看示例代码运行时的线程信息, 更加深入地理解线程状态。测试代码如下:

public class ThreadState {
    // 该线程不断地进行睡眠
    static class TimeWaiting implements Runnable {
        @Override
        public void run() {
            while (true) {
                SleepUtils.second(100);
            }
        }
    }

    // 该线程在Waiting.class实例上等待
    static class Waiting implements Runnable {
        @Override
        public void run() {
            while (true) {
                synchronized (Waiting.class) {
                    try {
                        Waiting.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    // 该线程在Blocked.class实例上加锁后, 不会释放该锁
    static class Blocked implements Runnable {
        @Override
        public void run() {
            synchronized (Blocked.class) {
                while (true) {
                    SleepUtils.second(100);
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new TimeWaiting (), "TimeWaitingThread").start();
        new Thread(new Waiting(), "WaitingThread").start();
        // 使用两个Blocked线程, 一个获取锁成功, 另一个被阻塞
        new Thread(new Blocked(), "BlockedThread-1").start();
        new Thread(new Blocked(), "BlockedThread-2").start();
    }
}

  上述示例中使用的SleepUtils代码如下:

public class SleepUtils {

    public static final void second(long seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds) ;
        } catch (InterruptedException e) {
        }
    }
}

  运行该示例, 打开终端或者命令提示符, 键入“jps”, 输出如下:

16544 Jps
13700
10156 ThreadState

  可以看到运行示例对应的进程ID是10156,接着再输入“jstack 10156”,部分输出如下:

//BlockedThread-2线程阻塞在获取Blocked.class示例的锁上
"BlockedThread-2" #13 prio=5 os_prio=0 tid=0x00000000180ad800 nid=0x108c waiting for monitor entry [0x0000000018e8f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at ThreadState$Blocked.run(ThreadState.java:35)
        - waiting to lock <0x00000000e01e4db0> (a java.lang.Class for ThreadState$Blocked)
        at java.lang.Thread.run(Thread.java:745)
//BlockedThread-1线程获取到了Blocked.class的锁,处于睡眠状态
"BlockedThread-1" #12 prio=5 os_prio=0 tid=0x00000000180ad000 nid=0x2740 waiting on condition [0x0000000018d8e000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at java.lang.Thread.sleep(Thread.java:340)
        at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
        at SleepUtils.second(SleepUtils.java:6)
        at ThreadState$Blocked.run(ThreadState.java:35)
        - locked <0x00000000e01e4db0> (a java.lang.Class for ThreadState$Blocked)
        at java.lang.Thread.run(Thread.java:745)
//WaitingThread线程在Waitting实例上等待
"WaitingThread" #11 prio=5 os_prio=0 tid=0x00000000180a6000 nid=0x93c in Object.wait() [0x0000000018c8f000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000e01e37c0> (a java.lang.Class for ThreadState$Waiting)
        at java.lang.Object.wait(Object.java:502)
        at ThreadState$Waiting.run(ThreadState.java:21)
        - locked <0x00000000e01e37c0> (a java.lang.Class for ThreadState$Waiting)
        at java.lang.Thread.run(Thread.java:745)
//TimeWaitingThread线程处于超时等待
"TimeWaitingThread" #10 prio=5 os_prio=0 tid=0x00000000180a5000 nid=0x8c4 waiting on condition [0x0000000018b8f000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at java.lang.Thread.sleep(Thread.java:340)
        at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
        at SleepUtils.second(SleepUtils.java:6)
        at ThreadState$TimeWaiting.run(ThreadState.java:8)
        at java.lang.Thread.run(Thread.java:745)

  通过示例, 我们了解到Java程序运行中线程状态的具体含义。 线程在自身的生命周期中,并不是固定地处于某个状态, 而是随着代码的执行在不同的状态之间进行切换

  下面是一张更详细的线程状态迁移图:

对Java线程概念的理解_第2张图片

  从图中可以看到, 线程创建之后, 调用start()方法开始运行(这里的运行状态其实是就绪态和运行态的合集)。 当线程执行wait()方法之后, 线程进入等待状态。 进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态, 而超时等待状态相当于在等待状态的基础上增加了超时限制, 也就是超时时间到达时将会返回到运行状态。 当线程调用同步方法时, 在没有获取到锁的情况下, 线程将会进入到阻塞状态。 线程在执行Runnable的run()方法之后将会进入到终止状态。

  注意:阻塞状态是线程在进入synchronized关键字修饰的方法或代码块(尝试获取锁) 时没有拿到锁的状态,但是阻塞在java.concurrent包中Lock接口 的线程状态却是等待状态, 因为java.concurrent包中 Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法

6、Daemon线程

  Daemon线程是一种支持型线程, 因为它主要被用作程序中后台调度以及支持性工作。 这意味着, 当一个Java虚拟机中不存在非Daemon线程的时候, Java虚拟机将会退出。 可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。

  注意:Daemon属性需要在启动线程之前设置, 不能在启动线程之后设置。

  Daemon线程被用作完成支持性工作, 但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行, 示例代码如下:

public class Daemon {

    public static void main(String[] args) {
        Thread thread = new Thread(new DaemonRunner(), "DaemonRunner");
        thread.setDaemon(true) ;
        thread.start();
    }

    static class DaemonRunner implements Runnable {

        @Override
        public void run() {
            try {
                SleepUtils.second(10);
            } finally {
                System.out.println("DaemonThread finally run. ");
            }   
        }       
    }
}

  运行Daemon程序, 可以看到在终端或者命令提示符上没有任何输出。 main线程(非Daemon线程) 在启动了线程DaemonRunner之后随着main方法执行完毕而终止, 而此时Java虚拟机中已经没有非Daemon线程, 虚拟机需要退出。 Java虚拟机中的所有Daemon线程都需要立即终止, 因此DaemonRunner立即终止, 但是DaemonRunner中的finally块并没有执行。因此, 在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。

7、线程的启动、中断与终止

  线程对象在初始化完成之后, 调用start()方法就可以启动这个线程。 线程start()方法的含义是: 当前线程(即parent线程) 同步告知Java虚拟机, 只要线程规划器空闲, 应立即启动调用start()方法的线程。

  注意:启动一个线程前, 最好为这个线程设置线程名 称, 因为这样在使用jstack分析程序或者进行问题排查时, 就会给开发人员提供一些提示, 自定义的线程最好能够起个名字。

  中断可以理解为线程的一个标识位属性, 它表示一个运行中的线程是否被其他线程进行了中断操作。 中断好比其他线程对该线程打了个招呼, 其他线程通过调用该线程的interrupt()方法对其进行中断操作。

  线程通过检查自身是否被中断来进行响应, 线程通过方法isInterrupted()来进行判断是否被中断, 也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。 如果该线程已经处于终结状态, 即使该线程被中断过, 在调用该线程对象的isInterrupted()时依旧会返回false。

  调用某个线程的interrupt()方法,将会设置该线程为中断状态,即设置为true。线程中断后的结果是死亡、还是等待新的任务或是继续运行至下一步,取决于这个程序本身。线程会不时地检测这个中断标识位,以判断线程是否应该被中断(中断标志是否为true)。它并不像stop方法那样会真的会粗暴地打断一个正在运行的线程

  从Java的API中可以看到, 有许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法), 这些方法在抛出InterruptedException之前, Java虚拟机会先将该线程的中断标识位清除, 然后抛出InterruptedException, 此时调用isInterrupted()方法将会返回false。

  测试代码如下,首先创建了两个线程, SleepThread和BusyThread, 前者不停地睡眠, 后者一直运行, 然后对这两个线程分别进行中断操作, 观察二者的中断标识位。

public class Interrupted {

    public static void main(String[ ] args) throws Exception {
        // sleepThread不停的尝试睡眠
        Thread sleepThread = new Thread(new SleepRunner() , "SleepThread");
        sleepThread.setDaemon(true);
        // busyThread不停的运行
        Thread busyThread = new Thread(new BusyRunner() , "BusyThread");
        busyThread.setDaemon(true);
        sleepThread.start();
        busyThread.start();

        // 休眠5秒, 让sleepThread和busyThread充分运行
        TimeUnit.SECONDS.sleep(5);
        sleepThread.interrupt();
        busyThread.interrupt();
        System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
        System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());

        // 防止sleepThread和busyThread立刻退出
        SleepUtils. second(2);

    }

    static class SleepRunner implements Runnable {
        @Override
        public void run() {
            while (true) {
                SleepUtils. second(10) ;
            }
        }
    }

    static class BusyRunner implements Runnable {
        @Override
        public void run() {
            while (true) {

            }
        }   
    }
}

  输出如下:

SleepThread interrupted is false
BusyThread interrupted is true

  从结果可以看出, 抛出InterruptedException的线程SleepThread, 其中断标识位被清除了,而一直忙碌运作的线程BusyThread, 中断标识位没有被清除。

  中断状态是线程的一个标识位, 而中断操作是一种简便的线程间交互方式, 而这种交互方式最适合用来取消或停止任务。 除了中断以外, 还可以利用一个boolean共享变量来控制是否需要停止任务并终止该线程,这是最受推荐的终止一个线程(就是让一个线程彻底停止运行)的方式,使用共享变量(shared variable)来发出信号,告诉线程必须停止正在运行的任务。线程必须周期性的核查这一变量,然后有秩序地停止任务。测试代码如下:

public class Shutdown {

    public static void main(String[ ] args) throws Exception {
        Runner one = new Runner() ;
        Thread countThread = new Thread(one, "CountThread") ;
        countThread.start() ;

        // 睡眠1秒,main线程对Runner one进行中断, 使CountThread能够感知中断标识位的置位而结束
        TimeUnit.SECONDS.sleep(1) ;
        countThread.interrupt() ;
        Runner two = new Runner() ;
        countThread = new Thread(two, "CountThread") ;
        countThread.start() ;
        // 睡眠1秒,main线程对Runner two进行取消, 使CountThread能够感知on为false而结束
        TimeUnit.SECONDS.sleep(1) ;
        two.cancel() ;
    }

    private static class Runner implements Runnable {

        private long i;
        private volatile boolean on = true;

        @Override
        public void run() {
            while (on && ! Thread.currentThread().isInterrupted()) {
                i++;
            }
            System.out.println("Count i = " + i);
        }

        public void cancel() {
            on = false; 
        }
    }       
}

  输出结果如下(多次运行结果可能不同):

Count i = 543487324
Count i = 540898082

  示例在执行过程中, main线程通过中断操作和cancel()方法均可使CountThread得以终止。这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源, 而不是武断地将线程停止, 因此这种终止线程的做法显得更加安全和优雅。

8、join()方法、yield()方法和sleep()方法

  join()方法的作用:让“主线程”等待“子线程”结束之后再继续运行。这句话可能有点晦涩,我们还是通过例子去理解:

// 主线程
public class Father extends Thread {
    public void run() {
        Son s = new Son();
        s.start();
        s.join();
        ...
    }
}
// 子线程
public class Son extends Thread {
    public void run() {
        ...
    }
}

  上面的有两个类Father(主线程类)和Son(子线程类)。因为Son是在Father中创建并启动的,所以,Father是主线程类,Son是子线程类。在Father主线程中,通过new Son()新建一个“子线程s”。接着通过s.start()启动“子线程s”,并且调用s.join()。在调用s.join()之后,Father主线程会一直等待,直到“子线程s”运行完毕;在“子线程s”运行完毕之后,Father主线程才能接着运行。这也就是我们所说的join()的作用,让主线程等待,一直等到子线程结束之后,主线程才能继续运行。。

  sleep()、yield()、join()等是Thread类的方法(而wait()和notify()是Object类的方法)。yield()方法是停止当前线程,让同等优先权的线程运行。如果没有同等优先权的线程,那么yield()方法将不会起作用

   sleep()使当前线程进入超时等待状态(见上面的状态转移图),所以执行sleep()的线程在指定的时间内肯定不会被执行;sleep()方法只让出了CPU,而并不会释放同步资源锁

   sleep()方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield()方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程

   另外,sleep()方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep()方法,又没有受到 I\O 阻塞,那么,较低优先级的线程只能等待所有较高优先级的线程运行结束,才有机会运行。

   本文大部分内容出自《Java并发编程的艺术》第4章。

你可能感兴趣的:(Java学习笔记)