Java线程基础

2015/03/05 JUC

Java线程基础

在Java编程语言将线程抽象为Thread类,用Thread类来描述线程的概念,并拟定了优先级(priority)、守护线程(daemon Thread)、等相关的属性。

Thread类

Thread类用于操作线程,是所以涉及到线程操作(如并发)的基础。

/**
 * A <i>thread</i> is a thread of execution in a program. The Java
 * Virtual Machine allows an application to have multiple threads of
 * execution running concurrently.
 * <p>
 *     
 * **线程是一个进程/程序(Program)的一条执行路径,JVM虚拟机允许一个应用同时执行多个线程。**
 * 
 * Every thread has a priority. Threads with higher priority are
 * executed in preference to threads with lower priority. Each thread
 * may or may not also be marked as a daemon. When code running in
 * some thread creates a new <code>Thread</code> object, the new
 * thread has its priority initially set equal to the priority of the
 * creating thread, and is a daemon thread if and only if the
 * creating thread is a daemon.
 * <p>
 *     
 * 线程拥有一个优先级别(priority),优先级高的优先于优先级低的线程执行。一个线程可能是守护线程。
 *  当线程A在执行过程中创建了一个新的线程a,则:线程a默认拥有与线程A同样的优先级。当且仅当线程A是一个守护线程时,线程a才会成为守护线程。
 *     
 * When a Java Virtual Machine starts up, there is usually a single
 * non-daemon thread (which typically calls the method named
 * <code>main</code> of some designated class). The Java Virtual
 * Machine continues to execute threads until either of the following
 * occurs:
 * 
 * Java虚拟机一经启动,就会有一个非守护线程——对应于main方法开始执行。 JVM虚拟机结束执行多线程状态的两种情况:
 * 
 * <ul>
 * <li>The <code>exit</code> method of class <code>Runtime</code> has been
 *     called and the security manager has permitted the exit operation
 *     to take place.
 *     
 * (1)Runtime类的exit()方法被调用,并且,security manager管理器也允许退出操作被执行。    
 * <li>All threads that are not daemon threads have died, either by
 *     returning from the call to the <code>run</code> method or by
 *     throwing an exception that propagates beyond the <code>run</code>
 *     method.
 * </ul>
 * 
 *  (2)所有的非守护线程都已死亡,可以是run()方法正常执行结束,也可以是run()方法执行过程中抛出了异常,都会导致该线程结束执行。
 * <p>
 * There are two ways to create a new thread of execution. One is to
 * declare a class to be a subclass of <code>Thread</code>. This
 * subclass should override the <code>run</code> method of class
 * <code>Thread</code>. An instance of the subclass can then be
 * allocated and started. For example, a thread that computes primes
 * larger than a stated value could be written as follows:
 * 
 * 创建线程的两种方式 方式1:定义一个Thread类的子类SubThread,然后重写Thread类的run()方法,然后创建一个SubThread子类的实例,将其启动即可
 * <hr><blockquote><pre>
 *     class PrimeThread extends Thread {
 *         long minPrime;
 *         PrimeThread(long minPrime) {
 *             this.minPrime = minPrime;
 *         }
 *
 *         public void run() {
 *             // compute primes larger than minPrime
 *             &nbsp;.&nbsp;.&nbsp;.
 *         }
 *     }
 * </pre></blockquote><hr>
 * <p>
 * The following code would then create a thread and start it running:
 * <blockquote><pre>
 *     PrimeThread p = new PrimeThread(143);
 *     p.start();
 * </pre></blockquote>
 * <p>
 * The other way to create a thread is to declare a class that
 * implements the <code>Runnable</code> interface. That class then
 * implements the <code>run</code> method. An instance of the class can
 * then be allocated, passed as an argument when creating
 * <code>Thread</code>, and started. The same example in this other
 * style looks like the following:
 * 
 * 第二种方式是:创建一个Runnable接口的子类SunRunnable,然后实现run()方法。接着创建SunRunnable子类对象,将其作为参数传递给Thread类,将其启动即可。
 * <hr><blockquote><pre>
 *     class PrimeRun implements Runnable {
 *         long minPrime;
 *         PrimeRun(long minPrime) {
 *             this.minPrime = minPrime;
 *         }
 *
 *         public void run() {
 *             // compute primes larger than minPrime
 *             &nbsp;.&nbsp;.&nbsp;.
 *         }
 *     }
 * </pre></blockquote><hr>
 * <p>
 * The following code would then create a thread and start it running:
 * <blockquote><pre>
 *     PrimeRun p = new PrimeRun(143);
 *     new Thread(p).start();
 * </pre></blockquote>
 * <p>
 * Every thread has a name for identification purposes. More than
 * one thread may have the same name. If a name is not specified when
 * a thread is created, a new name is generated for it.
 * <p>
 * Unless otherwise noted, passing a {@code null} argument to a constructor
 * or method in this class will cause a {@link NullPointerException} to be
 * thrown.
 */
public
class Thread implements Runnable {
    
}

Runnable接口

Runnable是一个提供线程的接口,有一种抽象的publicabstract void run()方法。

/**
 * The <code>Runnable</code> interface should be implemented by any
 * class whose instances are intended to be executed by a thread. The
 * class must define a method of no arguments called <code>run</code>.
 * 
 * 如果类的实例需要在一个单独的线程中执行,那么这些类应该实现 Runnable 接口。这些类必须定义一个无参的 run 方法。
 * <p>
 * This interface is designed to provide a common protocol for objects that
 * wish to execute code while they are active. For example,
 * <code>Runnable</code> is implemented by class <code>Thread</code>.
 * Being active simply means that a thread has been started and has not
 * yet been stopped.
 * 
 * 本接口是为了给那些在活跃时希望执行代码的对象们提供一个通用的协议而设计的。例如,Thread 类便实现了 Runnable 接口。活跃一般是指一个线程已经启动且还没有停止。
 * <p>
 * In addition, <code>Runnable</code> provides the means for a class to be
 * active while not subclassing <code>Thread</code>. A class that implements
 * <code>Runnable</code> can run without subclassing <code>Thread</code>
 * by instantiating a <code>Thread</code> instance and passing itself in
 * as the target.  In most cases, the <code>Runnable</code> interface should
 * be used if you are only planning to override the <code>run()</code>
 * method and no other <code>Thread</code> methods.
 * This is important because classes should not be subclassed
 * unless the programmer intends on modifying or enhancing the fundamental
 * behavior of the class.
 * 
 * 另外,Runnable 接口让类可以不需要继承 Thread 类也可以变成活跃的。实现了Runnable 接口的类可以不继承 Thread 类,而是通过实例化一个 Thread 实例并把自己作为目标传给该实例来获得运行。如果你只是打算覆写 Thread 类 的 run 方法,而没有覆写其他方法,那么应该使用 Runnable 接口。这很重要,因为除非程序员打算修改或加强类的基础行为,否则不应该继承类。
 */
@FunctionalInterface
public interface Runnable {

}

Callable接口

Callable: 返回结果并且可能抛出异常的任务。

/**
 * A task that returns a result and may throw an exception.
 * Implementors define a single method with no arguments called
 * {@code call}.
 *
 *  Callable接口用于创建一个返回结果的线程(可能抛出异常)。该接口的实现者需要重写一个无参方法call().
 * <p>The {@code Callable} interface is similar to {@link
 * java.lang.Runnable}, in that both are designed for classes whose
 * instances are potentially executed by another thread.  A
 * {@code Runnable}, however, does not return a result and cannot
 * throw a checked exception.
 *
 * Runnable接口的run()方法既没有返回值,也不会抛出异常,而Callable接口的call()方法就可以。当无法计算返回值的时候,Callable接口的call()方法就抛出异常;如果可计算出结果,就将其返回,并正常结束子线程的执行。
 * <p>The {@link Executors} class contains utility methods to
 * convert from other common forms to {@code Callable} classes.
 */
@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

如何创建多线程

  • 继承 Thread 类。
  • 实现 Runnable 接口
  • 使用匿名内部类创建线程
  • 使用Callable和Future接口创建线程

实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。

继承 Thread 类

同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。

package com.concurrency.thread;

/**
 * 步骤:1、定义一个线程类 A 继承于 java.lang.Thread 类
 *    2、在 A 类中覆盖 Thread 类的 run() 方法
 *    3、在 run() 方法中编写需要执行的操作
 *    4、在 main 方法(线程)中,创建线程对象,并启动线程
 *      创建线程类:A类 a = new A()类;
 *      调用 start() 方法启动线程:a.start();
 */
public class ThreadExtends {

    public static void main(String[] args) {
        SubThread subThread1 = new SubThread();
        subThread1.start();
        SubThread subThread2 = new SubThread();
        subThread2.start();
    }
}

class SubThread extends Thread {
    //2.重写run方法,方法内实现此子线程要完成的功能
    @Override
    public void run() {
        for (int i = 0; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

注意:1、不能通过Thread实现类对象的run()去启动一个线程,此时只是主线程调用方法而已,并没有启动线程,要启动线程,必须通过Start()方法

2、一个线程只能够执行一次start(),start()中会判断threadStatus的状态是否为0,不为0则抛出异常,所以一个线程调用两次start()会报异常

Exception in thread "main" Disconnected from the target VM, address: '127.0.0.1:57072', transport: 'socket'
java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Thread.java:708)
	at com.concurrency.thread.ThreadExtends.main(ThreadExtends.java:16) 

实现 Runnable 接口

需要实现 run() 方法。

通过 Thread 调用 start() 方法来启动线程。

1、Runnable接口应由任何类实现,其实例将由线程执行。 该类必须定义一个无参数的方法,称为run 。 2、该接口旨在为希望在活动时执行代码的对象提供一个通用协议。此类整个只有一个 run() 抽象方法

package com.concurrency.thread;

/**
 * 步骤:1、定义一个线程类 A 实现于 java.lang.Runnable 接口(注意:A类不是线程类,没有 start()方法,不能直接 new A 的实例启动线程)
 *    2、在 A 类中覆盖 Runnable 接口的 run() 方法
 *    3、在 run() 方法中编写需要执行的操作
 *    4、在 main 方法(线程)中,创建线程对象,并启动线程
 *       创建线程类:Thread t = new Thread( new A类() ) ;
 *       调用 start() 方法启动线程:t.start();
 */
public class ThreadInterface {

    public static void main(String [] args){
        //此程序存在线程的安全问题,打印车票时,会出现重票、错票,后面线程同步会讲到
        Window window=new Window();
        Thread thread1=new Thread(window,"窗口一");
        Thread thread2=new Thread(window,"窗口二");
        Thread thread3=new Thread(window,"窗口三");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}
class Window implements  Runnable{
    int ticket=10;
    @Override
    public void run(){
        while (true){
            if(ticket > 0){
                try {
                    Thread.currentThread().sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket--);
            }else {
                break;
            }
        }
    }
}

**哪个方式好?实现的方式优于继承的方式 **

why? ①避免java单继承的局限性      ②如果多个线程要操作同一份资源,更适合使用实现的方式

注意:此程序存在线程的安全问题,打印车票时,会出现重票、错票,下一篇线程同步会讲到

使用匿名内部类创建线程

public static void main(String[] args) {
    for(int i = 0 ; i < 10 ; i++){
        System.out.println("玩游戏"+i);
        if(i==5){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i = 0 ; i < 10 ;i++){
                        System.out.println("播放音乐"+i);
                    }
                }
            }).start();
        }
    }
}

④使用Callable和Future接口创建线程

使用Callable和Future接口创建线程。

与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

public class ThreadTest {
    public static void main(String[] args) {
        Callable<Integer> myCallable = new MyCallable();    // 创建MyCallable对象
        FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装MyCallable对象
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Thread thread = new Thread(ft);   //FutureTask对象作为Thread对象的target创建新的线程
                thread.start();                      //线程进入到就绪状态
            }
        }
        System.out.println("主线程for循环执行完毕..");
        try {
            int sum = ft.get();            //取得新创建的新线程中的call()方法返回的结果
            System.out.println("sum = " + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class MyCallable implements Callable<Integer> {
    private int i = 0;

    // 与run()方法不同的是,call()方法具有返回值
    @Override
    public Integer call() {
        int sum = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            sum += i;
        }
        return sum;
    }
}

实现接口 VS 继承 Thread

实现接口会更好一些,因为:

  • Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
  • 类可能只要求可执行就行,继承整个 Thread 类开销过大。

线程的状态

在 Thread 类中,有一个枚举内部类:

    /**
     * A thread state.  A thread can be in one of the following states:
     * <ul>
     * <li>{@link #NEW}<br>
     *     A thread that has not yet started is in this state.
     *     </li>
     * <li>{@link #RUNNABLE}<br>
     *     A thread executing in the Java virtual machine is in this state.
     *     </li>
     * <li>{@link #BLOCKED}<br>
     *     A thread that is blocked waiting for a monitor lock
     *     is in this state.
     *     </li>
     * <li>{@link #WAITING}<br>
     *     A thread that is waiting indefinitely for another thread to
     *     perform a particular action is in this state.
     *     </li>
     * <li>{@link #TIMED_WAITING}<br>
     *     A thread that is waiting for another thread to perform an action
     *     for up to a specified waiting time is in this state.
     *     </li>
     * <li>{@link #TERMINATED}<br>
     *     A thread that has exited is in this state.
     *     </li>
     * </ul>
     *
     * <p>
     * A thread can be in only one state at a given point in time.
     * These states are virtual machine states which do not reflect
     * any operating system thread states.
     *
     * @since   1.5
     * @see #getState
     */
    public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }

新建状态(NEW

使用 new 创建一个线程,仅仅只是在堆中分配了内存空间

新建状态下,线程还没有调用 start()方法启动,只是存在一个线程对象而已

Thread t = new Thread();//这就是t线程的新建状态

可运行状态(Runnable)

新建状态调用 start() 方法,进入可运行状态。而这个又分成两种状态,ready 和 running,分别表示就绪状态和运行状态

  • 就绪状态(Ready) 线程对象调用了 start() 方法,等待 JVM 的调度,(此时该线程并没有运行)
  • 运行状态(Running) 线程对象获得 JVM 调度,如果存在多个 CPU,那么运行多个线程并行运行

注意:线程对象只能调用一次 start() 方法,否则报错:illegaThreadStateExecptiong

阻塞状态(Blocked)

正在运行的线程因为某种原因放弃 CPU,暂时停止运行,就会进入阻塞状态。此时 JVM 不会给线程分配 CPU,知道线程重新进入就绪状态,才有机会转到 运行状态。   注意:阻塞状态只能先进入就绪状态,不能直接进入运行状态

阻塞状态分为两种情况:

①、当线程 A 处于可运行状态中,试图获取同步锁时,却被 B 线程获取,此时 JVM 把当前 A 线程放入锁池中,A线程进入阻塞状态

②、当线程处于运行状态时,发出了 IO 请求,此时进入阻塞状态

1)等待阻塞:通过调用线程的wait()方法,让线程等待某工作的完成。

2)同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态。

3)其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或超时、或者I/O处理完毕时,线程重新转入就绪状态。

**无限期等待(Waiting)**

等待状态只能被其他线程唤醒,此时使用的是无参数的 wait() 方法

①、当线程处于运行状态时,调用了 wait() 方法,此时 JVM 把该线程放入等待池中

等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。

进入方法 退出方法
没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll()
没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕
LockSupport.park() 方法 -

**限期等待(Timed waiting)**

调用了带参数的 wait(long time)或 sleep(long time) 方法

无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。

①、当线程处于运行状态时,调用 Object.wait(long time) 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述,此时 JVM 把该线程放入等待池中。

②、当前线程调用了 sleep(long time) 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。

睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。

阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。

进入方法 退出方法
Thread.sleep() 方法 时间结束
设置了 Timeout 参数的 Object.wait() 方法 时间结束 / Object.notify() / Object.notifyAll()
设置了 Timeout 参数的 Thread.join() 方法 时间结束 / 被调用的线程执行完毕
LockSupport.parkNanos() 方法 -
LockSupport.parkUntil() 方法 -

终止状态(Terminated

通常称为死亡状态,表示线程终止

①、正常终止,执行完 run() 方法,正常结束

②、强制终止,如调用 stop() 方法或 destory() 方法

③、异常终止,执行过程中发生异常

线程组

从名字上来看,线程组就是给不同的线程设计不同的分组,并且在命名上也做区分,在 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);
    }
}

运行这段程序,我们可以在控制台中看到每个线程都会有自己专属的名字和分组,这样可以方便我们后期对于线程的分类管理。 采用了线程组技术之后,对于多线程的管理方面会降低一定的复杂度。 例如:我们可以通过线程组快速定位到具体是哪个业务模块的线程出现了异常,然后进行快速修复。

又或者是针对不同的线程组进行线程监控,了解各个业务模块对于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 指令区查询这类线程 那么, 为什么需要守护线程呢 ? 常规的线程也可以实现在后台执行的效果啊,下边我们来看一组实战代码案例:

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();
    }
}

在上边的守护线程代码案例中,我使用了一个 ShutdownHook的钩子函数,用于监听当前JVM是否退出。 可以看到,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();
    }
}

通过上边的这个实验可以发现,守护线程具有在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();
    }
}

可以看到,当线程出现异常的时候,会回调到 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);
    }
}

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

Search

    微信好友

    博士的沙漏

    Table of Contents