Java并发编程基础(一)--摘自《java并发编程的艺术》

  |   0 评论   |   0 浏览

什么是多线程

现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个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线程和多个其它线程同时运行==。

为什么要使用多线程

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

(1)更多的处理器核心
随着处理器上的核心数量越来越多,以及超线程技术的广泛使用,现在大多数的计算机都比以往==更擅长并行计算==,而==处理器性能的提升方式,也从更高的主频向更多的核心发展==。如何利用好处理器上的多个核心也成了现在的主要问题。

线程是大多数系统调度的基本单元,一个程序作为一个进程来运行,程序运行的过程中能够创建多个线程,而一个线程在同一时刻只能运行在一个处理器核心上。试想一下,==一个单线程程序在运行时只能使用一个处理器核心,那么再多的处理器核心也无法显著提高该程序的执行效率==。相反,如果该程序使用了多线程技术,==将计算逻辑分配到多个处理器核心上,就会显著减少程序处理的时间==,并且随着更多处理器核心的加入而变得更有效率

(2) 更快的响应时间
有时我们会编写一些较为复杂的代码(这里的复杂不是说复杂的算法,而是复杂的业务逻辑),例如,一笔订单的创建,它包括插入订单数据、生成订单快照、发送邮件通知卖家和纪录货品销售数量等。用户从单机“订购”按钮开始,就要等待这些操作全部完成才能看到订购成功的结果。但是这么多业务操作,如何能够更快地完成呢?

在上面的场景中,可以使用多线程技术,即将数据一致性不强的操作派发给其他线程处理(也可以使用消息队列),如生成订单快照、发送邮件等。这样做的好处是响应用户请求的线程能够尽可能快的处理完成,缩短了响应时间,提升了用户体验。

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

线程优先级

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

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

public class Priority {

    public static volatile boolean notStart = true;

    public static volatile boolean notEnd = true;

    public static void main(String[] args){

        List<Job> jobs = new ArrayList<Job>();
        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.setPriority(priority);
            thread.start();
        }
        notStart = false;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        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;
        }
        @Override
        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程序对于优先级的设定==

线程的状态

Java 线程在运行的声明周期中可能处于6中不同的状态,在给定的一个时刻,线程只能处于其中的一个状态。

|状态名称| 说明 |
|--|--|
|NEW |初始化状态,线程被创建,但是还没有调用 start()方法 |
|RUNNABLE| 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中” |
|BLOCKED|阻塞状态,表示线程阻塞于锁|
|WAITING |等待状态,表示线程进入等待状态,进入等待状态表示当前线程需要等待其他线程做出一些特定地动作(通知或中断) |
|TIME_WAITING | 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自动返回的 |
|TERMINATED| 终止状态,表示当前线程已经执行完毕 |

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

ThreadState.java

public class ThreadState {

    public static void main(String[] args){
        new Thread(new TimeWaiting(),"TimeWaitingThread").start();
        new Thread(new Waiting(),"WaitingThread").start();
        new Thread(new Blocked(),"BlockedThread-1").start();
        new Thread(new Blocked(),"BlockedThread-2").start();
    }
    //该线程不断地进行睡眠
    static class TimeWaiting implements Runnable{
        @Override
        public void run() {
           while(true){
               SleepUtils.second(1000);
           }
        }
    }
    //该线程在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);
                }
            }
        }
    }
}

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

611
935 Jps
929 ThreadState
270

可以看到运行示例对应的进程ID是929,接着再键入“jstack 929”(这里的进程ID需要和读者自己键入的jps得出的ID一致),部分输出如下所示:

// BlockedThread-2线程阻塞在获取Blocked.class示例的锁上
"BlockedThread-2" prio=5 tid=0x00007feacb05d000 nid=0x5d03 waiting for monitor
entry [0x000000010fd58000]
java.lang.Thread.State: BLOCKED (on object monitor)
// BlockedThread-1线程获取到了Blocked.class的锁
"BlockedThread-1" prio=5 tid=0x00007feacb05a000 nid=0x5b03 waiting on condition
[0x000000010fc55000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
// WaitingThread线程在Waiting实例上等待
"WaitingThread" prio=5 tid=0x00007feacb059800 nid=0x5903 in Object.wait()
[0x000000010fb52000]
java.lang.Thread.State: WAITING (on object monitor)
// TimeWaitingThread线程处于超时等待
"TimeWaitingThread" prio=5 tid=0x00007feacb058800 nid=0x5703 waiting on condition
[0x000000010fa4f000]
java.lang.Thread.State: TIMED_WAITING (sleeping)

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

在这里插入图片描述

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

注意:Java将操作系统中的运行和就绪两个状态合并成为运行状态。阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包的Lock接口的线程状态却是等待状态,因为Java.concurrent 包中的Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。

守护(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{
       public void run(){
          try{
          	SleepUtils.second(100);
          } finally{
             System.out.println("DaemonThread finally run.");
          }
       }
    }
}

运行Daemon程序,可以看到在终端或者命令提示符上没有任何输出。main线程(非Daemon线程)在启动了线程DaemonRunner之后随着main方法执行完毕而终止,而此时Java虚拟机中已经没有非Daemon线程,虚拟机需要退出。Java虚拟机中的所有Daemon线程都需要立即终止,因此DaemonRunner 立即终止,但是DaemonRunner中的finally 块并没有执行。

注意: 在构建Daemon线程时,不能依靠finally 块中的内容来确保执行关闭或清理资源的逻辑


标题:Java并发编程基础(一)--摘自《java并发编程的艺术》
作者:zh847707713
地址:http://lovehao.cn/articles/2019/06/21/1561082542492.html