Thread

Thread概要.PNG

构造函数

创建 Thread 对象的时间都会调用 init() 方法,取一个最常用的构造方法

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
        // 获取当前线程
        Thread parent = currentThread();
        if (g == null) {
            g = parent.getThreadGroup();
        }

        g.addUnstarted();
        this.group = g;

        this.target = target;
        this.priority = parent.getPriority();
        this.daemon = parent.isDaemon();
        setName(name);

        init2(parent);

        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;
        tid = nextThreadID();
    }    

init() 方法的第一个参数叫做线程组, Java 编程思想 书中提到,这个概念其实并没有什么叼用. 在构造方法中也可以看到传入的是 null.

init() 方法开始就通过 currentThread() 获取了创建 Thread 对象的当前线程, 一般就是 main thread.

后续用 traget 保存了提交的任务, 用 priority 保存了优先级, 用 daemon 保存了是否是守护(后台)线程. 其中必须要注意, prioritydaemon 的值取的是创建 Thread 对象的当前进程的相应的值, 也就是继承了当前线程的相应的属性.

同时,还可以注意到, 在创建 Thread 对象时, 会默认创建一个名字, 名字格式类似 Thread-threadId 这样的形式.

执行线程

在 Java 编程中, 一个 Thread 对象通常就是指的一个执行线程(thread of execution), 然而这个说法并不确切, 因为只有调用了 Thread 对象的 start() 方法后, 才会创建执行线程

    /**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     * It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     */
    public synchronized void start() {
        // 线程只有启动一次, 否则抛异常
        if (threadStatus != 0 || started)
            throw new IllegalThreadStateException();

        group.add(this);

        started = false;
        try {
            // 用当前的 Thread 对象创建执行线程
            nativeCreate(this, stackSize, daemon);
            started = true;
        } finally {
            // ...
        }
    }

可以看到, nativeCreate() 用当前 Thread 对象创建了一个执行线程. 这也就解释了为何 Thread 对象调用 start() 方法后, Thread 对象没有立即被回收, 而是等到执行完任务后才会回收, 因为执行线程使用了 Thread 对象.

为何要传入一个 Thread 对象? 因为执行线程要执行一个任务(Runnable), 而 Thread 类正好实现了 Runbale 接口. 所以接下来就是调用 Thread 类的 run() 方法

当执行线程创建后, 会调用 Thread 对象的 run() 方法

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

从这里可以清楚看到, 如果创建 Thread 对象的时候, 不传入 Runnable 对象, 这个线程其实不会做任何事. 另外一方面, 如果是继承 Thread 类, 可以通过复写 run() 方法来执行任务.

优先级

创建 Thread 对象的时候, 会把当前线程的优先级赋予给 Thread 对象, 注意, 我说的是 Thread 对象, 不是线程. 真正设置线程优先级的方法为 setPriority()

    public final void setPriority(int newPriority) {
        // ..
        if((g = getThreadGroup()) != null) {
            // ...
            synchronized(this) {
                // 赋值给 thread.priority
                this.priority = newPriority;
                if (isAlive()) {
                    // 给执行线程设置优先级
                    nativeSetPriority(newPriority);
                }
            }
        }
    }

nativeSetPriority() 才是真正的给执行线程设置优先级, 所以如果不调用 setPriority() 方法, 创建 Thread 对象的时候, 其实压根就没有把线程设置优先级, 只是给 Thread 对象设置变量.

再来看看 getPriority() 方法

    public final int getPriority() {
        return priority;
    }

我惊讶的发现, 这个获取的优先级居然不是执行线程的优先级, 而是 Thread 对象的优先级, 百撕不得骑姐 ~

优先级有三个, MIN_PRIORITY, NORM_PRIORITY, NORM_PRIORITY . 调度器会倾向于让优先级高的线程先执行, 但是这并不意味着优先级低的线程将得不到机会执行(不然不就造成死锁了). 优先级应该理解为执行频率. 所以试图通过优先级操作多线程的执行顺序, 通常就是错误的做法.

后台线程

所谓后台(daemon)线程, 也称为守护线程, 是指在程序运行的时候在后台提供一种通用服务的线程, 并且这种线程并不属于程序中不可或缺的部分. 因此当所有非后台线程执行结束时, 程序也就终止了, 同时会杀死进程中的所有后台线程.

先看看 setDaemon() 方法

    /**
     * Marks this thread as either a {@linkplain #isDaemon daemon} thread
     * or a user thread. The Java Virtual Machine exits when the only
     * threads running are all daemon threads.
     *
     * This method must be invoked before the thread is started.
     */
    public final void setDaemon(boolean on) {
        checkAccess();
        if (isAlive()) {
            throw new IllegalThreadStateException();
        }
        daemon = on;
    }

从注释中可以明白两点问题:

  1. 参数 on 的值决定了线程是后台线程还是用户线程
  2. 如果只有后台线程在运行, JVM 将会退出, 也就是程序终止了.
  3. 这个方法必须在 Thread 对象调用 start() 方法之前调用. why? 可能是创建执行线程的时候, 要用到这个属性吧~

休眠

Thread 类的静态方法 sleep() 会让当前的执行线程在指定时间段内休眠, 例如

        new Thread(){
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();

首先 Thread 对象调用 start() 方法会创建一个执行线程, 然后执行 Thread 对象的 run() 方法, 这样 run() 方法就会新创建的执行线程中执行, 所以调用 Thread.Sleep(1000) 会让线程休眠 1s.

再看看源码

    public static void sleep(long millis) throws InterruptedException {
        Thread.sleep(millis, 0);
    }

    public static void sleep(long millis, int nanos)
    throws InterruptedException {
        // STEP1: 判断参数可用性
        if (millis < 0) {
            throw new IllegalArgumentException("millis < 0: " + millis);
        }
        if (nanos < 0) {
            throw new IllegalArgumentException("nanos < 0: " + nanos);
        }
        if (nanos > 999999) {
            throw new IllegalArgumentException("nanos > 999999: " + nanos);
        }

        // The JLS 3rd edition, section 17.9 says: "...sleep for zero
        // time...need not have observable effects."
        if (millis == 0 && nanos == 0) {
            // ...but we still have to handle being interrupted.
            if (Thread.interrupted()) {
              throw new InterruptedException();
            }
            return;
        }

        long start = System.nanoTime();
        long duration = (millis * NANOS_PER_MILLI) + nanos;

        // STEP2: 获取当前执行线程的锁
        Object lock = currentThread().lock;

        // Wait may return early, so loop until sleep duration passes.
        // STEP3: 同步锁, 并执行无限循环来休眠执行线程, 直到休眠时间完毕
        synchronized (lock) {
            while (true) {
                sleep(lock, millis, nanos);

                long now = System.nanoTime();
                long elapsed = now - start;

                if (elapsed >= duration) {
                    break;
                }

                duration -= elapsed;
                start = now;
                millis = duration / NANOS_PER_MILLI;
                nanos = (int) (duration % NANOS_PER_MILLI);
            }
        }
    }

    @FastNative
    private static native void sleep(Object lock, long millis, int nanos)
        throws InterruptedException;     

直接看两个参数的 sleep() 方法, 这个方法其实做了三件事:

  1. 判断参数可用性, 否则会抛出异常
  2. 获取当前线程的锁
  3. 同步锁, 执行循环来休眠线程,直到休眠时间完毕

第一步中会根据参数以及线程的中断标志, 会抛出参数异常(IllegalArgumentException)和中断异常(InterruptedException). 但是我们经常遇到的中断异常是在无限循环的时候, 调用的 native sleep() 方法抛出的, 它是响应线程中断而抛出的异常, 那么会有两个情况:

  1. 当线程正在休眠的时间, 调用了 thread.interrupt() 方法, 会出现中断异常
  2. 当调用了 thread.interrupt() 方法后, 再试图进入休眠, 也会出现中断异常

从第三步中还可以看出,休眠期间是不会释放当前 thread 的锁的, 这在并发资源竞争中很关键。

让步

    /**
     * A hint to the scheduler that the current thread is willing to yield
     * its current use of a processor. The scheduler is free to ignore this
     * hint.
     *
     * <p> Yield is a heuristic attempt to improve relative progression
     * between threads that would otherwise over-utilise a CPU. Its use
     * should be combined with detailed profiling and benchmarking to
     * ensure that it actually has the desired effect.
     *
     * <p> It is rarely appropriate to use this method. It may be useful
     * for debugging or testing purposes, where it may help to reproduce
     * bugs due to race conditions. It may also be useful when designing
     * concurrency control constructs such as the ones in the
     * {@link java.util.concurrent.locks} package.
     */
    public static native void yield();

注释说明了几点问题:

  1. yield() 方法只是暗示调度器,当前线程可以让出处理器,调度器也可以忽略这个暗示. 所以可以使用,并不能依赖它的效果
  2. 一般用于调度或者测试,可以用来复现问题。

加入线程

        final Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("execute t1");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1 awaken");
            }
        });
        Thread t2 = new Thread() {
            @Override
            public void run() {
                System.out.println("execute t2");
                try {
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2 awaken");
            }
        };
        t1.start();
        t2.start();

几乎同时启动了两个线程 t1 和 t2。 然而在 t2 执行的时候, 突然调用 t1.join() 让 t1 执行, t1 执行完毕后, t2 再继续。

执行结果如下

01-17 18:03:16.799  System.out: execute t1
01-17 18:03:16.799  System.out: execute t2
01-17 18:03:21.800  System.out: t1 awaken
01-17 18:03:21.800  System.out: t2 awaken

从 Log 可以看出, t2 确实等待 t1 休眠了5后,t2 才执行后面的代码。

看看源码

    public final void join() throws InterruptedException {
        join(0);
    }

    /**
     * Waits at most {@code millis} milliseconds for this thread to
     * die. A timeout of {@code 0} means to wait forever.
     *
     * <p> This implementation uses a loop of {@code this.wait} calls
     * conditioned on {@code this.isAlive}. As a thread terminates the
     * {@code this.notifyAll} method is invoked. It is recommended that
     * applications not use {@code wait}, {@code notify}, or
     * {@code notifyAll} on {@code Thread} instances.
     */
    public final void join(long millis) throws InterruptedException {
        synchronized (lock) {
            // ...
            if (millis == 0) {
                while (isAlive()) {
                    lock.wait(0);
                }
            } else {
                // ...
            }
        }
    }

代码我做了省略,主要逻辑就是这样。 这里理解起来有点难,我可以把例子中代码改编下

        Thread t2 = new Thread() {
            @Override
            public void run() {
                System.out.println("execute t2");
                try {
                    synchronized (t1.lock) {
                        // ...
                        if (millis == 0) {
                            while (t1.isAlive()) {
                                t1.lock.wait(0);
                            }
                        } else {
                            // ...
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2 awaken");
            }
        };

这样看起来就舒服了, 首先通过 synchronized 获取 t1.lock 的锁,防止 t1 的并发问题。 然后,调用 t1.isAlive() 判断 t1 是否还存活着, 如果还存活着,就调用 t1.lock.wait(0) 来挂起 t2 线程,注意,是挂起 t2 线程, 不是 t1。 当 t1 执行完毕后,会调用 t1.lock.notifyAll() 方法来唤醒 t2 线程,从而让 t2 线程继续执行。

中断

前面提到过,执行线程休眠或准备休眠的时候,如果调用 thread.interrupte() 方法会产生中断异常(InterruptedException).

看看 interrupte() 源码

    /**
     * Interrupts this thread.
     *
     * <p> Unless the current thread is interrupting itself, which is
     * always permitted, the {@link #checkAccess() checkAccess} method
     * of this thread is invoked, which may cause a {@link
     * SecurityException} to be thrown.
     *
     * <p> If this thread is blocked in an invocation of the {@link
     * Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link
     * Object#wait(long, int) wait(long, int)} methods of the {@link Object}
     * class, or of the {@link #join()}, {@link #join(long)}, {@link
     * #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)},
     * methods of this class, then its interrupt status will be cleared and it
     * will receive an {@link InterruptedException}.
     *
     * <p> If this thread is blocked in an I/O operation upon an {@link
     * java.nio.channels.InterruptibleChannel InterruptibleChannel}
     * then the channel will be closed, the thread's interrupt
     * status will be set, and the thread will receive a {@link
     * java.nio.channels.ClosedByInterruptException}.
     *
     * <p> If this thread is blocked in a {@link java.nio.channels.Selector}
     * then the thread's interrupt status will be set and it will return
     * immediately from the selection operation, possibly with a non-zero
     * value, just as if the selector's {@link
     * java.nio.channels.Selector#wakeup wakeup} method were invoked.
     *
     * <p> If none of the previous conditions hold then this thread's interrupt
     * status will be set. </p>
     *
     * <p> Interrupting a thread that is not alive need not have any effect.
     */
    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                nativeInterrupt();
                b.interrupt(this);
                return;
            }
        }
        nativeInterrupt();
    }

从注释中总结以下几点

  1. Objectwait() 方法,Threadsleep() 方法和 join() 方法,会导致线程阻塞。 如果调用了 interrupt() 方法,线程的中断标志(interrupt status)将会被清除,线程也会收到 InterruptedException
  2. InterruptibleChannel 也会阻塞线程,如果调用了 interrupt() 方法, 通道会被关闭,线程的中断标志将被设置,并且线程会收到 ClosedByInterruptException
  3. Selector 也会阻塞线程,当调用了 interrupt() 方法后,线程的中断标志将被设置,并且会立即从 selection operation 中返回,并且可以带有一个非零的值。
  4. 如果没有以上的任何一个条件,那么只会设置中断标志。

注释中已经列举了所有产生中断异常的情况,如果不是这种情况,肯定就不会产生中断异常,例如 I/O 造成的阻塞,synchronized 无法获取对象锁造成的阻塞, 都是不会产生中断异常的。 其实,有个小技巧,用 IDE 写代码的时候,如果提示要 catch 中断异常,那么相应的代码就会响应中断异常,如果没有提示,肯定就不响应了。

从上面的四点总结,还需要注意一个问题,就是中断标志

  1. 如果产生中断异常,线程退出,会清除中断标志
  2. 如果没有产生中断异常,就会设置中断标志

Thread 类还一个静态的 interrupted() 方法

    /**
     * Tests whether the current thread has been interrupted.  The
     * <i>interrupted status</i> of the thread is cleared by this method.  In
     * other words, if this method were to be called twice in succession, the
     * second call would return false (unless the current thread were
     * interrupted again, after the first call had cleared its interrupted
     * status and before the second call had examined it).
     *
     * <p>A thread interruption ignored because a thread was not alive
     * at the time of the interrupt will be reflected by this method
     * returning false.
     *
     * @return  <code>true</code> if the current thread has been interrupted;
     *          <code>false</code> otherwise.
     * @see #isInterrupted()
     * @revised 6.0
     */
    @FastNative
    public static native boolean interrupted();

Thread.interrupte() 方法比较有意思,它会检测当前线程是否已经被中断过,并且会清楚中断标志。 前面说过,当调用 thread.interrupte() 的时候, 如果没有碰到能产生中断的情况, 线程是不会抛出中断异常的, 那么我们可以用 Thread.interrupted() 方法检测中断标志,然后手动退出。

当然检测中断标志还有一个方法

    /**
     * Tests whether this thread has been interrupted.  The <i>interrupted
     * status</i> of the thread is unaffected by this method.
     *
     * <p>A thread interruption ignored because a thread was not alive
     * at the time of the interrupt will be reflected by this method
     * returning false.
     *
     * @return  <code>true</code> if this thread has been interrupted;
     *          <code>false</code> otherwise.
     * @see     #interrupted()
     * @revised 6.0
     */
    @FastNative
    public native boolean isInterrupted();

这个方法并不是静态方法,它与静态的 interrupted() 方法的唯一区别就是,不清除当前线程的中断标志。

互斥锁中断

interrupte() 方法的注释还有一点没提及, 虽然 synchronized 方法或者临界区的阻塞不可中断 ,但是 ReentrantLock 上阻塞具备可以中断的能力。

    ReentrantLock lock = new ReentrantLock();
    try {
        mLock.lockInterruptibly();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

从抛出的异常就可以看到,ReentrantLock 产生的阻塞具备可中断的能力。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,076评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,658评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,732评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,493评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,591评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,598评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,601评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,348评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,797评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,114评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,278评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,953评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,585评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,202评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,180评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,139评论 2 352