首页 > 编程语言 >Java Thread类

Java Thread类

时间:2023-03-17 23:44:08浏览次数:28  
标签:Java Thread thread void 线程 new public

在多线程编程过程中,总会或多或少地接触到多线程这个概念。而 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);
    }
}

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

标签:Java,Thread,thread,void,线程,new,public
From: https://www.cnblogs.com/fxh0707/p/17228744.html

相关文章

  • java运算符
    一表达式表达式由运算符和操作数组成如:5num1,num1+num2,sum=num1+num2二运算符1.算术运算符算术运算符主要用于进行基本的算术运算,如加法,减法,乘......
  • 说一下线程池内部工作原理(ThreadPoolExecutor)
    ThreadPoolExecutor构造方法的参数corePoolSize:线程池的核心线程数,说白了就是,即便是线程池里没有任何任务,也会有corePoolSize个线程在候着等任务。maximumPoolSize:最大......
  • Java图形界面设计-切换按钮复选按钮及单选按钮
    Java程序设计语言(一)示例程序P164程序8.5与书上程序不完全一样,匿名类使用lambda使用jdk1.8.0_311packagetech.bugstar.practice.gui;importjavax.swing.*;im......
  • Java IO
    文件1.两种路径//1,绝对路径从盘符开始的路径 Filefile1=newFile("D:\\fil\\a.txt");//2,相对路径相对于工程的路径//../返回上一层目录 Filefile2=ne......
  • 使用Java开发贪吃蛇游戏一之静态界面
    一、设置窗口,包括但不限于窗口标题、可见、窗口可关闭,固定大小、设置大小 packagelearn_snake;/**@authorMK*@date2023年3月15日*/importjavax.swing.JFra......
  • Java基础字符串练习
    ​请定义一个方法用于判断一个字符串是否是对称的字符串,并在主方法中测试方法。例如:"abcba"、"上海自来水来自海上"均为对称字符串。训练提示:1、判断是否对称,方法的返回值......
  • Java基础字符串练习
    ​我国的居民身份证号码,由由十七位数字本体码和一位数字校验码组成。请定义方法判断用户输入的身份证号码是否合法,并在主方法中调用方法测试结果。规则为:号码为18位,不能以数......
  • JAVA代码查错
    JAVA代码查错1.abstractclassName{privateStringname;publicabstractbooleanisStupidName(Stringname){}}大侠们,这有何错误?答案: 错。abstract method必......
  • JAVA编程题
    JAVA编程题1.现在输入n个数字,以逗号,分开;然后可选择升或者降序排序;按提交键就在另一页面显示按什么排序,结果为,提供resetimportjava.util.*;publicclassbycomma{publicsta......
  • .NET Threadpool 饥渴,以及队列是如何使它更糟的
    .NETThreadpool饥渴,以及队列是如何使它更糟的.NETThreadpoolstarvation,andhowqueuingmakesitworse-CriteoEngineering已经有一些对threadpool饥渴的讨论......