Java Thread类

在多线程编程过程中,总会或多或少地接触到多线程这个概念。而 Java 的并发编程领域,想要使用线程技术,就不得不得接触到 java.lang.Thread 这个类。

很多程序员都使用过java.lang.Thread 这个类,但是大多数人可能只停留在了下边这种操作情况:

Thread t = new Thread(new Runnable(){....})
t.start();

然后就没有了~

其实呢,Thread 类的内部不单单只是有 run 方法,它还有蛮多特性的,那么这节课,就让我们一起去发现 Thread 背后隐藏的“特性”。

线程的构建

Thread 的含义是指线程,它实现了 java.lang.Runnable 接口,在 JDK8 里面,java.lang.Runnable 是一个函数式接口,其内部定义了 run 方法,而 run 方法就是线程内部具体逻辑的执行入口。

那么 在实际使用Thread的时候,我们会如何去操作它呢? 来看看下边这个案例

Thread thread = new Thread(() -> {
    while (true) {
        try {
            Thread.sleep(2000);
            System.out.println("i am running ....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
thread.start();

这段案例中,通过构建一个 Thread 对象之后,触发它的 start 方法,当 CPU 将时间片分配给到对应线程之后,线程内部的 run 逻辑便会触发执行,这也是大家使用线程的最基本方式。

但是随着系统的复杂性提升,一个进程中通常会运行着各种各样的线程,每个线程都有不同的业务含义,例如 #1001,#1002 线程是负责数据库连接,#1004,#1005 线程是负责第三方 http 请求,#1006,#1007 线程是负责计算任务,等等。

面对各式各样的线程,程序员们开始提出了线程分组的设计。

线程组

从名字上来看,线程组就是给不同的线程设计不同的分组,并且在命名上也做区分,在 JDK 中,它的具体表现是 ThreadGroup 这个类,如下边的这段案例:

public class ThreadGroupDemo {

    public static List<Thread> DbConnThread() {
        ThreadGroup dbConnThreadGroup = new ThreadGroup("数据库连接线程组");
        List<Thread> dbConnThreadList = new ArrayList<>();
        for (int i = 0; i < 2; i++) {
            Thread t = new Thread(dbConnThreadGroup, new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程名: " + Thread.currentThread().getName()
                            + ", 所在线程组: " + Thread.currentThread().getThreadGroup().getName());
                }
            }, "db-conn-thread-" + i);
            dbConnThreadList.add(t);
        }
        return dbConnThreadList;
    }

    public static List<Thread> httpReqThread() {
        ThreadGroup httpReqThreadGroup = new ThreadGroup("第三方http请求线程组");
        List<Thread> httpReqThreadList = new ArrayList<>();
        for (int i = 0; i < 2; i++) {
            Thread t = new Thread(httpReqThreadGroup, new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程名: " + Thread.currentThread().getName()
                            + ", 所在线程组: " + Thread.currentThread().getThreadGroup().getName());
                }
            }, "http-req-thread-" + i);
            httpReqThreadList.add(t);
        }
        return httpReqThreadList;
    }

    public static void startThread(List<Thread> threadList) {
        for (Thread thread : threadList) {
            thread.start();
        }
    }

    public static void main(String[] args) {
        List<Thread> dbConnThreadList = DbConnThread();
        List<Thread> httpReqThreadList = httpReqThread();
        startThread(dbConnThreadList);
        startThread(httpReqThreadList);
    }
}

运行这段程序,我们可以在控制台中看到每个线程都会有自己专属的名字和分组,这样可以方便我们后期对于线程的分类管理。

image.png 同样的,通过利用 VisualVM 相关工具,也可以看到对应的 Java 进程在执行过程中产生的线程信息,具体效果如下图所示:

image.png 不过我们一般不会选择在生产环境中使用 VisualVM 这类工具,因为它需要开放相关的端口,存在一定的危险性,所以,通常在生产环境中,我们会使用 Arthas 这款工具,并且通过 Arthas 的 thread 命令去查询相关线程的信息:

image.png

通过实战后,我们可以发现,采用了线程组技术之后,对于多线程的管理方面会降低一定的复杂度。

例如:我们可以通过线程组快速定位到具体是哪个业务模块的线程出现了异常,然后进行快速修复。

又或者是针对不同的线程组进行线程监控,了解各个业务模块对于CPU的使用率。

可能有些细心的同学会发现,使用 ThreadGroup 的时候,需要将它注入到 Thread 类中,这类硬编码的操作比较繁琐,是否有什么合理的方式可以简化相关代码呢?

其实是有的,JDK的开发者在设计的时候还留下了一个叫做 ThreadFacotry 的类。下边让我们一同来了解下这个类的作用。

线程工厂

了解过设计模式中工厂模式的朋友,应该对 ThreadFacotry 不会太陌生,ThreadFactory 是 一个JDK 包中提供的线程工厂类,它的职责就是专门用于生产 Thread 对象。使用了 ThreadFactory 之后,可以帮助我们缩减一些生产线程的代码量,例如下边这个 SimpleThreadFactory 类:

public class SimpleThreadFactory implements ThreadFactory {

    private final int maxThread;
    private final String threadGroupName;
    private final String threadNamePrefix;

    private final AtomicInteger count = new AtomicInteger(0);
    private final AtomicInteger threadSeq = new AtomicInteger(0);

    private final ThreadGroup threadGroup;


    public SimpleThreadFactory(int maxThread, String threadGroupName, String threadNamePrefix) {
        this.maxThread = maxThread;
        this.threadNamePrefix = threadNamePrefix;
        this.threadGroupName = threadGroupName;
        this.threadGroup = new ThreadGroup(threadGroupName);
    }


    @Override
    public Thread newThread(Runnable r) {
        int c = count.incrementAndGet();
        if (c > maxThread) {
            return null;
        }
        Thread t = new Thread(threadGroup, r, threadNamePrefix + threadSeq.getAndIncrement());
        t.setDaemon(false);
        //默认线程优先级
        t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        ThreadFactory threadFactory = new SimpleThreadFactory(10, "test-thread-group", "test-thread-");
        Thread t = threadFactory.newThread(new Runnable() {
            @Override
            public void run() {
                System.out.println("this is task");
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        countDownLatch.await();
    }
}

可以看到 ThreadFactory 内部提供了 newThread 方法,这个方法的具体实现中封装了关于线程产生的具体细节,例如线程的分组、命名、优先级,以及是否是守护线程类型。

如果你细心阅读过线程池底层的源代码,那么你应该会发现,线程池在生产线程的时候,其实也是使用了ThreadFactory这个工厂类。 在 Jdk1.8 中的线程池中,定义了两套工厂类,分别是 DefaultThreadFactory 和 PrivilegedThreadFactory,它们其实本质功能都差不多,只不过 PrivilegedThreadFactory 具备了 AccessControlContext 和上下文的类加载器权限。

/**
 * The default thread factory
 */
static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

/**
 * Thread factory capturing access control context and class loader
 */
static class PrivilegedThreadFactory extends DefaultThreadFactory {
    private final AccessControlContext acc;
    private final ClassLoader ccl;

    PrivilegedThreadFactory() {
        super();
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            // Calls to getContextClassLoader from this class
            // never trigger a security check, but we check
            // whether our callers have this permission anyways.
            sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);

            // Fail fast
            sm.checkPermission(new RuntimePermission("setContextClassLoader"));
        }
        this.acc = AccessController.getContext();
        this.ccl = Thread.currentThread().getContextClassLoader();
    }

    public Thread newThread(final Runnable r) {
        return super.newThread(new Runnable() {
            public void run() {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        Thread.currentThread().setContextClassLoader(ccl);
                        r.run();
                        return null;
                    }
                }, acc);
            }
        });
    }
}

好了,现在我们大概已经了解了该怎么去优雅地构建一个线程对象,以及如何去较好地管理多个线程,但是在实际工作中,线程还会有许多不同的应用场景,例如后台监控就是一类非常适合使用线程技术去完成的场景。

而 JDK 的开发者似乎也很早就预料到了这一点,所以他在设计 Thread 类的时候,还专门留下了一个叫做 daemon 的属性,这个属性主要是用于定义当前线程是否属于守护线程。

守护线程

守护线程其实是 JVM 中特殊定义的一类线程,这类线程通常都是以在后台单独运作的方式存在,常见的代表,例如 JVM 中的 Gc 回收线程,可以通过 Arthas 的 Thread 指令区查询这类线程: image.png 那么, 为什么需要守护线程呢 常规的线程也可以实现在后台执行的效果啊,下边我们来看一组实战代码案例:

public class DaemonThreadDemo {

    public static void main(String[] args) throws InterruptedException {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("jvm exit success!! ")));

        Thread testThread = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(2000);
                    System.out.println("thread still running ....");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        testThread.start();
    }
}

(ps:在上边的守护线程代码案例中,我使用了一个 ShutdownHook的钩子函数,用于监听当前JVM是否退出。)

执行的结果是:

image.png

可以看到,main 线程中构建了一个非守护线程 testThread,testThread 的内部一直在执行 while 循环,导致 main 线程迟迟都无法结束执行。而如果我们尝试将 testThread 设置为守护线程类型的话,结果就会发生变化:

public class DaemonThreadDemo {

    public static void main(String[] args) throws InterruptedException {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("jvm exit success!! ")));

        Thread testThread = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(2000);
                    System.out.println("thread still running ....");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        testThread.setDaemon(true);
        testThread.start();
    }
}

执行结果:

image.png

通过上边的这个实验可以发现,守护线程具有在JVM退出的时候也自我销毁的特点,而非守护线程不具备这个特点,这也是为什么GC回收线程被设置为守护线程类型的主要原因。

守护线程通常会在一些后台任务中所使用,例如分布式锁中在即将出现超时前,需要进行续命操作的时候,就可以采用守护线程去实现。 Thread 类其实还具有很多其他的特点,例如异常捕获器就是其中之一。

线程的异常捕获器

在线程的内部,有一个叫做异常捕获器的概念,当线程在执行过程中产生了异常,就会回调到该接口,来看看下边的这个案例代码:

public class ThreadExceptionCatchDemo {

    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("this is test");
                int i = 10/0;
            }
        });
        thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            //这里是对Throwable对象进行监控,所以无论是error或者exception都能识别到
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                System.err.println("thread is "+t.getName());
                e.printStackTrace();
            }
        });
        thread.start();
    }
}

执行结果:

image.png

可以看到,当线程出现异常的时候,会回调到 UncaughtExceptionHandler 中,而异常回调器其实本身也是一个函数接口,当线程出现异常的时候,JVM 会默认携带线程信息和异常内容回调到这个接口中:

@FunctionalInterface
public interface UncaughtExceptionHandler {
    /**
     * Method invoked when the given thread terminates due to the
     * given uncaught exception.
     * <p>Any exception thrown by this method will be ignored by the
     * Java Virtual Machine.
     * @param t the thread
     * @param e the exception
     */
    void uncaughtException(Thread t, Throwable e);
}

在 ThreadGroup 类中,其实就是对 UncaughtExceptionHandler 进行了单独的实现,所以每次当线程报错的时候才会有异常信息展示,这部分可以通过阅读 ThreadGroup 内部的源代码进行深入了解,下边我将这部分源代码粘出来给大家了解下:

//Jdk1.8中对于线程异常堆栈打印逻辑的源代码
public void uncaughtException(Thread t, Throwable e) {
    if (parent != null) {
        parent.uncaughtException(t, e);
    } else {
        Thread.UncaughtExceptionHandler ueh =
            Thread.getDefaultUncaughtExceptionHandler();
        if (ueh != null) {
            ueh.uncaughtException(t, e);
        } else if (!(e instanceof ThreadDeath)) {
            System.err.print("Exception in thread \""
                             + t.getName() + "\" ");
            e.printStackTrace(System.err);
        }
    }
}

如果我们希望当线程运行过程中出现异常后做些上报功能,可以通过采用异常捕获器的思路来实现。

上边我们所学习的各种属性,都是 Thread 类内部比较有用的属性,但是除开这些属性之外,Thread 中还有一个很容易误导开发者的属性,它就是 priority。

线程优先级

在 Thread 的内部还有一个叫做优先级的参数,具体设置可以通过 setPriority 方法去修改。例如下边这段代码:

public class ThreadPriorityDemo {

    static class InnerTask implements Runnable {

        private int i;

        public InnerTask(int i) {
            this.i = i;
        }

        public void run() {
            for(int j=0;j<10;j++){
                System.out.println("ThreadName is " + Thread.currentThread().getName()+" "+j);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new InnerTask(10),"task-1");
        t1.setPriority(1);
        Thread t2 = new Thread(new InnerTask(2),"task-2");
        //优先级只能作为一个参考数值,而且具体的线程优先级还和操作系统有关
        t2.setPriority(2);
        Thread t3 = new Thread(new InnerTask(3),"task-3");
        t3.setPriority(3);

        t1.start();
        t2.start();
        t3.start();
        Thread.sleep(2000);
    }
}

不过“优先级”这个参数通常并不是那么地“靠谱”,理论上说线程的优先级越高,分配到时间片的几率也就越高,但是在实际运行过程中却并非如此,优先级只能作为一个参考数值,而且具体的线程优先级还和操作系统有关, 所以大家在编码中如果使用到了“优先级”的设置,请不要强依赖于它。

终止线程的手段有哪些?

suspend 方法

suspend 翻译过来是暂停的意思,在 Thread 类的内部,也确实存在一个叫做 suspend 的方法,这个方法在执行的时候,可以将一个执行任务到一半的线程进行暂停,如果要恢复的话,调用 resume 方法即可。下边是一组使用 resume 方法和 suspend 方法代码案例:

package 并发编程02.线程终止的几种方式;

import java.util.concurrent.TimeUnit;

/**
*  @Author  linhao
*  @Date  created in 8:29 上午 2022/6/17
*/
public class ThreadSuspendDemo {

    /**
* 暂停线程
*/
static class SuspendThread implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                System.out.print(i+" ");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Thread suspendThread = new Thread(new SuspendThread());
        suspendThread.start();
        TimeUnit.SECONDS.sleep(5);
        System.out.print("线程暂停");
        //暂停线程
        suspendThread.suspend();
        TimeUnit.SECONDS.sleep(2);
        //从之前暂停的位置恢复继续执行
        suspendThread.resume();
        System.out.print(" 线程恢复");
    }
}

通过实践之后可以发现,调用了 suspend 方法和 resume 方法之后,线程会先进入暂停的状态,接着又会恢复到正常状态,控制台输出的结果如下图所示:

img

看起来 suspend 和 resume 方法使用起来还是比较简单易懂的,但是这两个方法却被 JDK 开发者声明了废弃标志。这是因为当线程调用了suspend操作之后,线程虽然暂停了,但是如果该线程曾经持有过锁并且也未曾主动释放过锁的话,那么这个处于暂停状态的线程就会一直持有锁,从而可能会导致其他希望获取锁的线程一直处于等待状态。

为了避免这种危险的情况发生,在后续的 JDK 版本中,开发者将 suspend 和 resume 方法声明了废弃标记。

从 suspend 方法的效果来看,它只是达到了一种暂停的效果,而要想实现线程的立即终止的话,可以使用 Thread 类中的 stop 函数。

stop方法

在 Thread 类中,提供了一个叫做 stop 的函数,这个方法有点类似于 Linux 操作系统中的 kill 指令,它的本质是直接终止线程,如果线程中持有某个锁对象,还会强制将该锁释放,从而可能导致该锁所保护的临界区缺少同步安全性。

下边是一个用 stop 函数去终止线程的代码案例:

package 并发编程02.线程终止的几种方式;

import java.util.concurrent.locks.ReentrantLock;

/**
*  @Author  linhao
*  @Date  created in 8:06 上午 2022/6/15
*/
public class StopThread {

    static class TestThread implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.print(i+" ");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class TestThreadWithSync implements Runnable {

        @Override
        public void run() {
            synchronized (this) {
                for (int i = 20; i < 30; i++) {
                    System.out.print(i+" ");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }


    static class TestThreadWithLock implements Runnable {

        ReentrantLock reentrantLock = new ReentrantLock();

        @Override
        public void run() {
            reentrantLock.lock();
            try {
                for (int i = 30; i < 40; i++) {
                    System.out.print(i+" ");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                reentrantLock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread testThread = new Thread(new TestThread());
        testThread.start();
        Thread testThreadWithSync = new Thread(new TestThreadWithSync());
        testThreadWithSync.start();
        Thread testThreadWithLock = new Thread(new TestThreadWithLock());
        testThreadWithLock.start();

        Thread.sleep(2000);
        testThread.stop();
        testThreadWithSync.stop();
        testThreadWithLock.stop();

    }
}

在这个用例中,通过调用 stop 函数可以发现,被 stop 的线程任务只执行了一半就中断了,而且不管线程的内部是采用了 synchronized 关键字也好,又或是采用了 ReentrantLock,统统都会被强制中断,并且释放该锁。对应的执行结果如下图所示:

img

看起来似乎 ****stop ****似乎却是可以实现线程的终止,但是为什么它还是被开发者加上了废弃的标记呢?

嘿嘿,让我们来看看下边这个模拟转账的案例:

package 并发编程02.线程终止的几种方式;

/**
*  @Author  linhao
*  @Date  created in 8:48 下午 2022/6/17
*/
public class StopTransferThreadDemo {

    static class User {
        int id;
        int balance;

        public User(int id, int balance) {
            this.id = id;
            this.balance = balance;
        }

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public int getBalance() {
            return balance;
        }

        public void setBalance(int balance) {
            this.balance = balance;
        }
    }

    /**
* 模拟A给B转账
*
*  @param  userA
*  @param  userB
*  @param  money
*/
public static void doTransfer(User userA, User userB, int money) throws InterruptedException {
        subtractUserBalance(userA,money);
        addUserBalance(userB,money);
    }

    private static void subtractUserBalance(User user,int money) throws InterruptedException {
        user.balance = user.balance - money;
        //模拟一些网络损耗
        Thread.sleep(1000);
    }

    private static void addUserBalance(User user,int money) throws InterruptedException {
        user.balance = user.balance + money;
        //模拟一些网络损耗
        Thread.sleep(1000);
    }

    static class TransferThread implements Runnable {

        private User userA;
        private User userB;

        public TransferThread(User userA, User userB) {
            this.userA = userA;
            this.userB = userB;
        }

        @Override
        public void run() {
            try {
                doTransfer(userA, userB, 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void showBalance(User userA, User userB) {
        System.out.println("用户A余额:" + userA.getBalance() + ",用户B余额:" + userB.getBalance());
    }

    /**
* 控制转账过程是否要中断
*/
public static void transferTest(boolean isNormal) throws InterruptedException {
        User userA = new User(1001, 100);
        User userB = new User(1001, 100);
        Thread t1 = new Thread(new TransferThread(userA, userB));
        t1.start();
        Thread.sleep(200);
        if (isNormal) {
            Thread.sleep(2000);
        } else {
            t1.stop();
        }
        showBalance(userA, userB);
    }

    public static void main(String[] args) throws InterruptedException {
        transferTest(true);
    }
}

在这个案例中,我们通过模拟转账操作,分别从 A 用户那里扣减了 10 元,然后再给 B 用户增加 10 元,这么一来一回的操作可以发现,如果没有 stop 的影响,账户的结果是正常的,如同下图所示:

img

但是如果有了 stop 的影响,那么执行转账的时候,就可能会出现类似下图中的中断问题:

img

当程序刚执行完 subtractUserBalance,准备执行 addUserBalance 时,线程被 stop 了,从而导致出现脏数据情况发生,最终产生的结果如下:

img

所以在 JDK 源代码中,stop 方法是被加入了 @Deprecated 注解进行修饰的,在实际工作中,我们也不推荐使用这个方法。

@Deprecated
public final void stop() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        checkAccess();
        if (this != Thread.currentThread()) {
            security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
        }
    }
    // A zero status value corresponds to "NEW", it can't change to
    // not-NEW because we hold the lock.
    if (threadStatus != 0) {
        resume(); // Wake up thread if it was suspended; no-op otherwise
    }

    // The VM can handle all thread states
    stop0(new ThreadDeath());
}

哦,看到这里,我们发现 suspend、stop,这两个方法都被 JDK 开发者声明为了废弃的方法,那么还有什么方式可以去关闭一个线程吗?

当然,这个方法是存在的,下边让我们来一起认识下 interrupt 方法。

interrupt 方法

在 Thread 类里面,提供了一个叫做 interrupt ****的函数,这个函数的名字翻译过来是中断的意思,但是它实际上并不是真正中断,只是将指定线程的状态调整为了中断类型。下边我们通过一个实际案例来理解 interrupt 函数的效果:

package 并发编程02.线程终止的几种方式;

import java.util.concurrent.locks.ReentrantLock;

/**
*  @Author  linhao
*  @Date  created in 8:41 上午 2022/6/15
*/
public class InterruptThread {


    static class TestThread implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.print(i+" ");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class TestThreadWithSync implements Runnable {

        @Override
        public void run() {
            synchronized (this) {
                for (int i = 20; i < 30; i++) {
                    System.out.print(i+" ");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }


    static class TestThreadWithLock implements Runnable {

        ReentrantLock reentrantLock = new ReentrantLock();

        @Override
        public void run() {
            reentrantLock.lock();
            try {
                for (int i = 30; i < 40; i++) {
                    System.out.print(i+" ");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                reentrantLock.unlock();
            }
        }
    }


    static class ForEverThread implements Runnable {

        @Override
        public void run() {
            System.out.println("开始执行");
            while (true){

            }
        }


    }

    public static void main(String[] args) throws InterruptedException {
        Thread testThread = new Thread(new TestThread());
        testThread.start();
        Thread testThreadWithSync = new Thread(new TestThreadWithSync());
        testThreadWithSync.start();
        Thread testThreadWithLock = new Thread(new TestThreadWithLock());
        testThreadWithLock.start();
        Thread forEverThread = new Thread(new ForEverThread());
        forEverThread.start();

        Thread.sleep(2000);

        forEverThread.interrupt();
        testThread.interrupt();
        testThreadWithSync.interrupt();
        testThreadWithLock.interrupt();

    }

}

通过实战演示之后,可以看到 interrupted 方法在进行中断的时候会抛出一个 java.lang.InterruptedException 的异常,但是被打断的线程在抛出异常之后依旧会正常执行任务,就如同下图所示:

img

所以说采用 interrupted 函数并不能真正地将线程中断,只能告知线程,目前需要进入中断状态,然后修改线程的状态为停止状态,但是接下来的处理流程得由线程自己去决定。

下边我将 interrupted 在不同场景下调用的执行效果做了一个归类:

当前线程状态 影响
阻塞(如线程调用了sleep,join,wait,condition.await) 如果线程已经标记为了 interrupt 状态,在线程调用了sleep,join,wait,condition.await 方法的时候会抛出 InterruptedException 异常。
正常活动 正常执行状态

所以通过实际验证后可以得出结论:interrupt() 并不能真正中断线程,需要被调用的线程自己进行配合才行。一个线程如果有被中断的需求,那么就可以这样做:在正常运行任务时,通过调用 isInterrupted ****方法去检查本线程的中断标志位,如果线程被设置了中断标志,就自行停止线程。例如下边这种方式:

static class TestInterruptedStop implements Runnable {

    @Override
    public void run() {
        synchronized (this) {
            //如果当前线程被中断,这里需要主动退出
            while (!Thread.currentThread().isInterrupted()) {
            }
            System.out.println("end");
        }
    }
}

\

interrupt 这个方法在设计层面上和 stop 以及 suspend 不同,它并没有强制性地去中断线程任务,只是发送了一个信号给到线程自身,然后让线程自身去决定如何执行

正因为 interrupt 的灵活性会比较高,所以在 JDK 的线程池中,关于关闭部分的实现也是采用了 interrupt 去实现。下边让我们一同来深入了解下线程池在关闭方面是如何使用 interrupt 函数的。

线程池的关闭

JDK 的线程池内部提供了两个关闭方法,shutdownNow 和 shuwdown 方法。

shutdownNow 方法的解释是:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行。

这两个方法又分别有什么区别呢,让我们一起来看看下边的这个实战案例:

package 并发编程02.线程池关闭;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
*  @Author  linhao
*  @Date  created in 10:21 下午 2022/6/16
*/
public class ExecutorShutDownDemo {

    public static void testShutDown() throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("[testShutDown] begin");
                    while (true) {
                    }
                }finally {
                    System.out.println("[testShutDown] end");
                }
            }
        });
        Thread.sleep(2000);
        executorService.shutdown();
        System.out.println("[testShutDown] shutdown");
    }

    public static void testShutDownNow() throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("[testShutDown] begin");
                    while (true) {
                    }
                }finally {
                    System.out.println("[testShutDown] end");
                }
            }
        });
        Thread.sleep(2000);
        executorService.shutdownNow();
        System.out.println("[testShutDown] shutdownNow");
    }

    public static void main(String[] args) throws InterruptedException {
        testShutDown();
        testShutDownNow();
    }
}

通过运行这段程序之后,你会发现,线程池虽然调用了 shutDown 函数和 shutDownNow 函数,然后线程池内部的任务依旧是在持续运行的。这一点主要和 shutDown 函数以及 shutDownNow 函数的底层源代码有关,下边这段内容是 shutdownNow 函数的底层实现,关于其中的核心代码实现我都在下方贴出了注释:

public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        //检测是否具有关闭线程池的权限
        checkShutdownAccess();
        //修改线程的状态为Stop状态
        advanceRunState(STOP);
        //中断worker线程
        interruptWorkers();
        //将队列中没有执行的任务放入到一个List集合中,并且返回给调用线程
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

private void interruptWorkers() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers)
        //注意这里的中断会对处于运行状态的线程也产生中断影响
            w.interruptIfStarted();
    } finally {
        mainLock.unlock();
    }
}

//这个函数实际上是在Worker线程类内部定义的,其本质也是调用Thread#interrupt方法
void interruptIfStarted() {
    Thread t;
    if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
        try {
            t.interrupt();
        } catch (SecurityException ignore) {
        }
    }
}

其实我们如果细心比对下 shutDown 和 shutDownNow 函数的话,就会发现,其实它们的主要差别体现在返回参数类型上,具体如下所示:

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
         //修改线程的状态为Shutdown状态
        advanceRunState(SHUTDOWN);
        //中断处于空闲状态的worker线程
        interruptIdleWorkers();
        //shutDownNow会将等待队列的任务放入到一个list集合返回给调用方,但是shutDown不会
        //专门留给ScheduledThreadPoolExecutor的扩展
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

//中断worker线程的执行
private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            //注意这里如果是非空闲的线程,w.tryLock会失败,具体可以看runWorker方法
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    //这里只是简单的修改了线程的状态为被中断状态    
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

好了,现在我们大概对线程池的 shutdown 函数以及 shutdownNow 方法有了个大概的了解了,最后我们也来对线程池的两种关闭方式做个总结,下边是我对两种关闭方式的一些思考。

线程池关闭方式 worker ****线程的中断 正在执行任务如何处理 等待队列的任务如何处理
shutdown 会对所有处于空闲状态的 worker 线程触发 interrupt 操作。 继续执行 等待队列的任务会继续执行。
shutdownNow 会对所有忙碌或非忙碌状态的 worker 线程触发 interrupt 操作。 继续执行 将队列中没有执行的任务放入到一个 List 集合中,并且返回给调用线程。
posted @ 2023-03-17 23:39  Dazzling!  阅读(55)  评论(0编辑  收藏  举报