Java并发编程基础——线程

1,497 阅读12分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

简介:

线程是操作系统调度的最小单元,在多核环境中,多个线程能同时执行,如果运用得当,能显著的提升程序的性能。

一、线程初步认识

1、什么是线程

操作系统运行一个程序会为其启动一个进程。例如,启动一个Java程序会创建一个Java进程。现代操作系统调度的最小单元是线程,线程也称为轻量级进程(Light Weight Process),一个进程中可以创建一个到多个线程,线程拥有自己的计数器、堆栈和局部变量等属性,并且能访问共享的内存变量。处理器会通过快速切换这些线程,来执行程序。

2、Java本身就是多线程

示例代码:

package com.lizba.p2;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.Arrays;

/**
 * <p>
 *
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/13 23:03
 */
public class MultiThread {

    public static void main(String[] args) {
        // 获取Java线程管理MXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 获取线程和线程堆栈信息;
        // boolean lockedMonitors = false  不需要获取同步的monitor信息;
        // boolean lockedSynchronizers = false  不需要获取同步的synchronizer信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        // 打印线程ID和线程name
        Arrays.stream(threadInfos).forEach(threadInfo -> {
            System.out.println("[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName());
        });
    }

}

输出结果(不一定一致):

[6]Monitor Ctrl-Break // idea中特有的线程(不用管)

[5]Attach Listener // JVM进程间的通信线程

[4]Signal Dispatcher // 分发处理发送给JVM信号的线程

[3]Finalizer // 调用对象的finalizer线程

[2]Reference Handler // 清楚Reference的线程

[1]main // main线程,用户程序入口


总结:

从输出结果不难看出,Java程序本身就是多线程的。它不仅仅只有一个main线程在运行,而是main线程和其他多个线程在同时运行。

3、为什么要使用多线程

使用多线程的好处如下:

  1. 更多处理器核心

计算机处理器核心数增多,由以前的高主频向多核心技术发展,现在的计算机更擅长于并行计算,因此如何充分利用多核心处理器是现在的主要问题。线程是操作系统调度的最小单元,一个程序作为一个进程来运行,它会创建多个线程,而一个线程在同一时刻只能运行在一个处理器上。因此一个进程如果能使用多线程计算,将其计算逻辑分配到多个处理器核心上,那么相比单线程运行将会有更显著的性能提升。

  1. 更快响应时间

在复杂业务场景中,我们可以将非强一致性关联的业务派发给其他线程处理(或者使用消息队列)。这样可以减少应用响应用户请求的时间

  1. 更好的编程模型

合理使用Java的提供的多线程编程模型,能使得程序员更好的解决问题,而不需要过于复杂的考虑如何将其多线程化。

4、线程的优先级

现代操作系统基本采用的是时间片分配的方式来调度线程,也就是操作系统将CPU的运行分为一个个时间片,线程会分配的若干时间片,当线程时间片用完了,就会发生线程调度等待下次时间片的分配。线程在一次CPU调度中能执行多久,取决于所分时间片的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。

在Java线程中,线程的优先级的可设置范围是1-10,默认优先级是5,理论上优先级高的线程分配时间片数量要优先于低的线程(部分操作系统这个设置是不生效的);


示例代码:

package com.lizba.p2;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 *      线程优先级设置
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/14 12:03
 */
public class Priority {

    /** 线程执行流程控制开关 */
    private static volatile boolean notStart = true;
    /** 线程执行流程控制开关 */
    private static volatile boolean notEnd = true;

    public static void main(String[] args) throws InterruptedException {
        List<Job> jobs = new ArrayList<>();
        // 设置5个优先级为1的线程,设置5个优先级为10的线程
        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;
        jobs.forEach(
                job -> System.out.println("Job priority : " + job.priority + ", Count : " + job.jobCount)
        );
    }


    /**
     * 通过Job来记录线程的执行次数和优先级
     */
    static class Job implements Runnable {

        private int priority;
        private long jobCount;

        public Job(int priority) {
            this.priority = priority;
        }

        @Override
        public void run() {
            while (notStart) {
                // 让出CPU时间片,等待下次调度
                Thread.yield();
            }
            while (notEnd) {
                // 让出CPU时间片,等待下次调度
                Thread.yield();
                jobCount++;
            }
        }
    }

}

执行结果:

从输出结果上来看,优先级为1的线程和优先级为10的线程执行的次数非常相近,因此这表明程序正确性是不能依赖线程的优先级高低的。

\

5、线程的状态

线程的生命周期如下:

状态名称说明
NEW初始状态,线程被构建,并未调用start()方法
RUNNABLE运行状态,Java线程将操作系统中的就绪和运行两种状态统称为“运行中”
BLOCKED阻塞状态,线程阻塞于锁
WAITING等待状态,线程进入等待状态,进入该状态表示当前线程需要等待其他线程作出一些特定动作(通知或中断)
TIME_WAITING超时等待,先比WAITING可以在指定的时间内自行返回
TERMINATED终止状态,表示当前线程已经执行完毕

通过代码来查看Java线程的状态

代码示例:

package com.lizba.p2;

import java.util.concurrent.TimeUnit;

/**
 * <p>
 *      睡眠指定时间工工具类
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/14 13:27
 */
public class SleepUtil {

    public static final void sleepSecond(long seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
package com.lizba.p2;

/**
 * <p>
 *      线程状态示例代码
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/14 13:25
 */
public class ThreadStateDemo {

    public static void main(String[] args) {
        // TimeWaiting
        new Thread(new TimeWaiting(), "TimeWaitingThread").start();
        // Waiting
        new Thread(new Waiting(), "WaitingThread").start();
        // Blocked1和Blocked2一个获取锁成功,一个获取失败
        new Thread(new Blocked(), "Blocked1Thread").start();
        new Thread(new Blocked(), "Blocked2Thread").start();
    }

    // 线程不断的进行睡眠
    static class TimeWaiting implements Runnable {

        @Override
        public void run() {
            while (true) {
                SleepUtil.sleepSecond(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) {
                    SleepUtil.sleepSecond(100);
                }
            }
        }
    }

}

使用JPS查看Java进程:

查看示例代码ThreadStateDemo进程ID是2576,键入jstack 2576查看输出:

整理输出结果:

线程名称线程状态
Blocked2ThreadBLOCKED (on object monitor),阻塞在获取Blocked.class的锁上
Blocked1ThreadTIMED_WAITING (sleeping)
WaitingThreadWAITING (on object monitor)
TimeWaitingThreadTIMED_WAITING (sleeping)

总结:

线程在自身生命周期中不是规定处于某一个状态,而是随着代码的执行在不同的状态之间进行切换。

Java线程的状态变化图如下:

Java线程状态变迁图

总结:

  • 线程创建后,调用start()方法开始运行
  • 线程执行wait()方法后,线程进入等待状态,进入等待的线程需要依靠其他线程才能够返回到运行状态
  • 超时等待相当于在等待朱姑娘太的基础上增加了超时限制,达到设置的超时时间后返回到运行状态
  • 线程执行同步方法或代码块时,未获取到锁的线程,将会进入到阻塞状态。
  • 线程执行完Runnable的run()方法之后进入到终止状态
  • 阻塞在Java的concurrent包中Lock接口的线程是等待状态,因为Lock接口阻塞的实现使用的是Daemon线程

6、Daemon线程

简介:

Daemon线程是一种支持型线程,它的主要作用是程序中后台调度和支持性工作。当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。Daemon线程需要在启动之前设置,不能在启动之后设置。

设置方式:

Thread.setDaemon(true)

需要特别注意的点:

Daemon线程被用作支持性工作的完成,但是在Java虚拟机退出时Daemon线程的finally代码块不一定执行。

示例代码:

package com.lizba.p2;

/**
 * <p>
 *      DaemonRunner线程
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/14 19:50
 */
public class DaemonRunner implements Runnable{
    @Override
    public void run() {
        try {
            SleepUtil.sleepSecond(100);
        } finally {
            System.out.println("DaemonRunner finally run ...");
        }
    }
}

测试:

package com.lizba.p2;

/**
 * <p>
 *
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/14 19:59
 */
public class DaemonTest {

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

}

输出结果:

总结:

不难发现,DaemonRunner的run方法的finally代码块并没有执行,这是因为,当Java虚拟机种已经没有非Daemon线程时,虚拟机会立即退出,虚拟机中的所以daemon线程需要立即终止,所以线程DaemonRunner会被立即终止,finally并未执行。

二、线程启动和终止

1、构造线程

运行线程之前需要构造一个线程对象,线程对象在构造的时候需要设置一些线程的属性,这些属性包括线程组、线程的优先级、是否时daemon线程、线程名称等信息。

代码示例:

来自java.lang.Thread

  /**
     * Initializes a Thread.
     *
     * @param g the Thread group
     * @param target the object whose run() method gets called
     * @param name the name of the new Thread
     * @param stackSize the desired stack size for the new thread, or
     *        zero to indicate that this parameter is to be ignored.
     * @param acc the AccessControlContext to inherit, or
     *            AccessController.getContext() if null
     * @param inheritThreadLocals if {@code true}, inherit initial values for
     *            inheritable thread-locals from the constructing thread
     */
    private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
		// 设置线程名称
        this.name = name;
		// 当前线程设置为该线程的父线程
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            if (security != null) {
                g = security.getThreadGroup();
            }
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }
        g.checkAccess();
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }
        g.addUnstarted();
		// 设置线程组
        this.group = g;
        // 将daemon属性设置为父线程的对应的属性
        this.daemon = parent.isDaemon();
        // 将prority属性设置为父线程的对应的属性
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        // 复制父线程的InheritableThreadLocals属性
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

       // 设置一个线程id
        tid = nextThreadID();
    }

总结:

在上述代码中,一个新构建的线程对象时由其parent线程来分配空间的,而child继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时会分配一个唯一的ID来标志线程。此时一个完整的能够运行的线程对象就初始化好了,在堆内存中等待运行。

2、什么是线程中断

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以通过调用静态方法Thread.interrupted()对当前线程的中断标志位进行复位。

如下情况不能准确判断线程是否被中断过:

  1. 线程已经终止运行,即使被中断过,isInterrupted()方法也会返回false
  2. 方法抛出InterruptedException异常,即使被中断过,调用isInterrupted()方法将会返回false,这是因为抛出InterruptedException之前会清除中断标志。

示例代码:

package com.lizba.p2;

/**
 * <p>
 *      线程中断示例代码
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/14 20:36
 */
public class Interrupted {

    public static void main(String[] args) {
        // 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运行充分
        SleepUtil.sleepSecond(5);

        // 中断两个线程
        sleepThread.interrupt();
        busyThread.interrupt();

        System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
        System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());

        // 睡眠主线程,防止daemon线程退出
        SleepUtil.sleepSecond(2);
    }

    static class SleepRunner implements Runnable {

        @Override
        public void run() {
            while (true) {
                SleepUtil.sleepSecond(10);
            }
        }
    }

    static class BusyRunner implements Runnable {

        @Override
        public void run() {
            while (true) {}
        }
    }

}

查看运行结果:

总结:

抛出InterruptedException的是sleepThread线程,虽然两者都被中断过,但是sleepThread线程的中断标志返回的是false,这是因为TimeUnit.SECONDS.sleep(seconds)会抛出InterruptedException异常,抛出异常之前,sleepThread线程的中断标志被清除了。但是,busyThread一直在运行没有抛出异常,中断位没有被清除。

3、suspend()、resume()和stop()

举例:

线程这三个方法,相当于QQ音乐播放音乐时的暂停、恢复和停止操作。(注意这些方法已经过期了,不建议使用。)

示例代码:

package com.lizba.p2;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 *      线程过期方法示例
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/14 20:57
 */
public class Deprecated {


    static DateFormat format = new SimpleDateFormat("HH:mm:ss");

    public static void main(String[] args) {
        Thread printThread = new Thread(new PrintThread(), "PrintThread");
        printThread.start();
        SleepUtil.sleepSecond(3);

        // 暂停printThread输出
        printThread.suspend();
        System.out.println("main suspend PrintThread at " + format.format(new Date()));
        SleepUtil.sleepSecond(3);

        // 恢复printThread输出
        printThread.resume();
        System.out.println("main resume PrintThread at " + format.format(new Date()));
        SleepUtil.sleepSecond(3);

        // 终止printThread输出
        printThread.stop();
        System.out.println("main stop PrintThread at " + format.format(new Date()));
        SleepUtil.sleepSecond(3);
    }


    static class PrintThread implements Runnable {

        @Override
        public void run() {
           while (true) {
               System.out.println(Thread.currentThread().getName() + "Run at "
               + format.format(new Date()));
               SleepUtil.sleepSecond(1);
           }
        }
    }

}

输出结果:

总结:

上述代码执行输出的结果,与API说明和我们的预期完成一致,但是看似正确的代码却隐藏这很多问题。

存在问题:

  • suspend()方法调用后不会释放已占有的资源(比如锁),可能会导致死锁
  • stop()方法在终结一个线程时不能保证资源的正常释放,可能会导致程序处于不确定的工作状态

4、正确的终止线程

  1. 调用线程的interrupt()方法
  2. 使用一个Boolean类型的变量来控制是否停止任务并终止线程

示例代码:

package com.lizba.p2;

/**
 * <p>
 *      标志位终止线程示例代码
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/14 21:17
 */
public class ShutDown {

    public static void main(String[] args) {
        Runner one = new Runner();
        Thread t = new Thread(one, "CountThread");
        t.start();
        SleepUtil.sleepSecond(1);
        t.interrupt();

        Runner two = new Runner();
        t = new Thread(two, "CountThread");
        t.start();
        SleepUtil.sleepSecond(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;
        }
    }

}

输出结果:

总结:

main线程通过中断操作和cancel()方法均可使CountThread得以终止。这两种方法终止线程的好处是能让线程在终止时有机会去清理资源。做法更加安全和优雅。

文章总结至《Java并发编程的艺术》,下文总结-线程间通信。

\