第十九章 Java线程之Thread源码分析

前言

本章将会围绕下面几个问题展开分析:
线程是什么?
java中的线程状态有几种?
各个方法会对线程产生什么影响?

概念

线程是一种轻量级进程,而进程又被称为正在进行的程序。所以线程就是程序代码run起来的样子。就像一个田径赛场的运动员,程序的运行长度就是它的跑道长度,刚刚开始跑的时候,精力充沛,跑道中间的时候,体力下降,遇到障碍物了,摔倒一跤…等等。
所以,跑起来的程序,也就是线程它在不同阶段可能会有不同的状态。
线程是一种操作系统层面的定义,并非java创造,Java作为一种程序语言,只是实现了线程的概念和操作,所以要深刻理解线程还需要去啃计算机操作系统理论。

生命周期

在Thread源代码中,State是它的内部枚举,一共定义了六种状态:
NEW (就绪)
RUNABLE (运行)
BLOCKED (阻塞)
WAITING (等待)
TIMED_WAITING(限时等待)
TERMINATED**(结束)
这六种状态与计算机操作系统中定义的状态有些区别,一般来说,操作系统对一个线程的状态定义有六种:
起始
就绪
运行
等待
挂起
结束
为什么Java中定义的六种状态和操作系统的定义的状态不匹配呢?
因为它们的角度不一样。在操作系统中,状态的定义是围绕是否在CPU上运行,java中的定义是是否在JVM上运行。所以造成了两种状态定义的差异。
这种差异带来的不同主要是就绪、运行和等待三个主要状态。
因为,进入JVM后,线程状态是RUNABLE,但是在JVM中不代表在CPU上运行了,也可能JVM才把线程交到操作系统的排队队列中,也有可能运行了一段后被CPU切换了,重新进入了就绪等待下一次时间片。不论这个线程在操作系统中经过了多少次就绪、运行互相转换,这部分统统被JVM封装在RUNABLE状态中。
另外一点,操作系统中只有等待状态,当一个线程在CPU上运行到以某一刻,需要获取外部设备或其他事件时,为了提高计算机CPU的利用效率,CPU会让该线程先在一旁等待。这个动作称为运行状态到等待状态的转换。线程进入等待后,操作系统就没有再细化区分了,但是Java做了区分,它根据等待的不同时间,又分为了:
BLOCKED (阻塞)
WAITING (等待)
TIMED_WAITING(限时等待)
这三个等待对java来说有什么区别呢?
我的理解是主要针对java的设计产生了许多使用场景来区别的。如synchronized关键字,sleep方法、wait方法等等。

阻塞
它属于等待状态,它产生的场景一般在多线程环境中使用synchronized关键字,当一个线程没有抢到锁时,就会进入等待状态,此时,java叫它BLOCKED。
这种等待是不确定的,如果占有锁的线程一直不释放锁,那么阻塞的线程就会一直等待。直到锁被释放后,阻塞的状态才会结束,进入就绪状态重新竞争锁。

等待
它属于等待状态,它产生的场景一般在执行了wait()方法后,需要其他线程调用notify 或 notifyAll 方法才能解除它等待状态。这种状态Java叫他WAITING。

限时等待
它属于等待状态,它的等待是一种有限的等待,是有时间限制的。一般产生于调用sleep(long time)方法中。它状态的解除不需要其他线程来参与。这种等待,Java叫它TIMED_WAITING。

那么具体这六个状态怎么产生的,怎么相互装换的,具体的理解在下面的方法介绍中。

成员变量解析

	  private volatile String name;				// 线程名称
	  private int  priority;					// 线程优先级
	  private boolean     daemon = false; 		// 是否是守护线程
	  private Runnable target; 					// 执行的任务
	  private ThreadGroup group;				// 所在的线程组
	  private ClassLoader contextClassLoader;		// 上下文类加载器
	  private AccessControlContext inheritedAccessControlContext;
	  ThreadLocal.ThreadLocalMap threadLocals = null;			// 内部挂载的map容器
	  ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;   // 内部挂载的父子线程传递的map容器
	  private volatile int threadStatus = 0;		// 用于判断线程是否启动
	  private volatile Interruptible blocker;		
	  private long tid;
	  private volatile Interruptible blocker;
	  private final Object blockerLock = new Object();   // 线程内部的锁

构造器

Thread线程的创建共有九种方式:

一、public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}
二、public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}
三、Thread(Runnable target, AccessControlContext acc) {
    init(null, target, "Thread-" + nextThreadNum(), 0, acc, false);
}
四、public Thread(ThreadGroup group, Runnable target) {
    init(group, target, "Thread-" + nextThreadNum(), 0);
}
五、public Thread(String name) {
    init(null, null, name, 0);
}
六、public Thread(ThreadGroup group, String name) {
    init(group, null, name, 0);
}
七、public Thread(Runnable target, String name) {
    init(null, target, name, 0);
}
八、public Thread(ThreadGroup group, Runnable target, String name) {
    init(group, target, name, 0);
}
九、public Thread(ThreadGroup group, Runnable target, String name,
              long stackSize) {
    init(group, target, name, stackSize);
}

从上可以看出,不论有多少种创建方式,这都基于其调用的init初始化方法。所以,了解了init初始化方法,对线程的创建就知道大概了。

init()线程初始化
/**
*  在注入参数中,分别是当前线程组、执行任务目标、线程名字、程序栈大小、类加载器、
*/
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) {
        /* Determine if it's an applet or not */
        /* If there is a security manager, ask the security manager
           what to do. */
        if (security != null) {
            g = security.getThreadGroup();
        }

        /* If the security doesn't have a strong opinion of the matter
           use the parent thread group. */
        if (g == null) {
            g = parent.getThreadGroup();		// 将新线程归入到父线程的组
        }
    }

    g.checkAccess();

    /*
     * Do we have the required permissions?
     */
    if (security != null) {
        if (isCCLOverridden(getClass())) {
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }

    g.addUnstarted();		// 线程组中未启动线程数

    this.group = g;
    this.daemon = parent.isDaemon();		// 继承父线程的守护状态
    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);
    
    // 指定栈大小
    this.stackSize = stackSize;

   // 获取线程唯一编号
    tid = nextThreadID();
}

如果对线程的初始化很懵,虽然我分析得也不是很深入,但是我还是得画个图出来大概解释一下,为什么会这样初始化。

  1. 首先是线程的成员变量定义了线程需要的参数有哪些,所以在初始化方法中才需要这些重要的参数注入。
  2. 线程的编组方法:
    在这里插入图片描述
    如上图所示,每一个线程肯定都在某一个线程组中,对于新创建的线程而言,比如S1xianc线程,其父线程是P,如果没有指定线程组,那么它也会在父线程P所在的组内。如果指定了线程组,如S3线程,虽然P也是它的父亲,但它可以在其他组内。

还有一点,在线程中没有明确的“父子线程”这个结构,这个描述只是基于线程初始化中的如下代码:

扫描二维码关注公众号,回复: 12505388 查看本文章
	Thread parent = currentThread();			// 指定创建线程为父线程
	...
	 this.daemon = parent.isDaemon();		    // 继承父线程的守护状态
    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();

它说明每个线程都是被赋予了创建者线程的一些结构和数据,但是他们并没有继承关系。
每一个线程组都与它的父组关联,是一个典型的树状结构。
基于这样的线程父子继承关系,每一个线程也与它的直接父线程关联,ThreadLocal的设计,实现了线程内的变量传递,但是无法实现线程之间的变量传递,因此基于父子线程关系,设计出了inheritableThreadLocals 这个变量用来满足父子线程间的变量传递,这个传递在线程初始化时进行。
但是具体是只传递了Map结构,还是结构及数据都传递了呢?

根据初始化代码继续向下:

  this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

这里调用了ThreadLocalMap的构造器

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

private ThreadLocalMap(ThreadLocalMap parentMap) {

        Entry[] parentTable = parentMap.table;			// 传递父对象的结构
        int len = parentTable.length;
        setThreshold(len);
        table = new Entry[len];

        for (int j = 0; j < len; j++) {					// 遍历父对象的每个结点
            Entry e = parentTable[j];
            if (e != null) {
                @SuppressWarnings("unchecked")
                ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                if (key != null) {
                    Object value = key.childValue(e.value);
                    Entry c = new Entry(key, value);		// 根据父对象的结点信息,为子对象创建新结点
                    int h = key.threadLocalHashCode & (len - 1);
                    while (table[h] != null)
                        h = nextIndex(h, len);			// 解决哈希冲突的开放地址法之线程查探
                    table[h] = c;
                    size++;
                }
            }
        }
    }

从上面可以看出,inheritableThreadLocals 这个变量实现了父子线程间的map拷贝,从而实现了线程之间的数据传递。

线程的常用方法

1. currentThread()

	/**
	 * 这是一个静态本地方法,返回当前线程对象。
	 *
	 public static native Thread currentThread();

2. yield

	 /**
	 * 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.   这个方法是告诉cpu的调度,当前线程愿意放弃对cpu的使用权。
 	*
 	* <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();

线程让步,最近也在看相关的书籍和资料,这个方法总的来说,是让当前线程放弃对CPU的使用权,重新处于一种对cpu的平等竞争状态,但是再次获得cpu的线程可能还是它,也可能不是。因为平时用这个方法的经验不是很多,个人理解这个方法能相对减缓线程之间的相对进度。

3. sleep(long millis)

	  * @throws  InterruptedException
	  *          if any thread has interrupted the current thread. The
 	  *          <i>interrupted status</i> of the current thread is
	  *          cleared when this exception is thrown.
	  * 
	public static native void sleep(long millis) throws InterruptedException;

这个方法是平时常用的方法,它会让当前线程进入TIMED_WAITING状态,而不是WAITING状态。TIMED_WAITING是限时等待,自己可以苏醒;WAITING 是一直等待,需要其他线程唤醒,对于调用sleep(long millis)方法的线程来说,线程的苏醒自己就可以完成,因此是进入TIMED_WAITING。
不过这个方法的有意思的地方不在这里,而是InterruptedException。通过官方的解释可以理解到:

如果当前线程在睡眠中被其他线程中止了,就会抛出InterruptedException异常,并且当前线程中断标志被清除。

意思就是,当前线程的初始中断标志位false,其他线程调用了interrupt方法后,当前线程的中断标志变为了true,但是如果正好当前线程是在睡眠中被中断的,那么抛出异常,并且中断标志为回到false。

4. sleep(long millis, int nanos)

这个方法和sleep(long millis) 作用一致,不同的是它多了一个纳秒单位。可以更精细的控制时间。

5. start

	/**
	 * Causes this thread to begin execution; the Java Virtual Machine
	 * calls the <code>run</code> method of this thread.
	 * <p> 这个线程一旦开始,JVM就会调用这个线程的run方法。
	 * 
	 * 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).
	 * 	意思就是这是两个线程在运行,一个是当前线程,一个是JVM的调用线程。
	 * <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() {
   		
   		/**
   		*  threadStatus  是线程初始的标志,默认为0,线程一旦启动,threadStatus  也会改变。因此线程不可以多次启动。
   		*/  	
    	if (threadStatus != 0)
        	throw new IllegalThreadStateException();
        	
    	group.add(this);		// 初始化的线程组中,添加这个启动的线程。这与初始化的g.addUnstarted()不同,g.addUnstarted()是增加线程组中未启动的线程个数,而这个是添加线程对象,添加时如果涉及扩容,threadGroup的数组按原容量的2倍扩容。

    boolean started = false;
    try {
        start0();	这是一个本地方法,只有它调用成功,才能代表这个线程确实启动成功了,所以有started前后判断控制。
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);		// 如果启动失败,线程组则处理当前线程。
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
	}

对于start方法而言,它能让线程从NEW状态进入RUNNABLE状态,因为这个方法可以启动线程。

7. run()

run方法是一个任务容器,由JVM调用执行。

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

在原生的Thread中,run方法是一个平台,是一辆空卡车,没有执行目标。因此在创建原生Thread线程时,都需要注入一个Runable任务。由Runable接口的run方法来具体描述目标任务。否则什么都不会执行。
如果自定义一个线程MyThread继承了Thread,通常可以重写Thread的run方法,直接在run里实现细节,这样JVM在调用run时,就能执行到具体细节。而不需要注入Runable任务。

run方法不会启动一个线程,它仅仅是一个方法而已。由JVM调用。

run方法是无法返回值的,这取决于返回描述为void。但是callable可以接受返回值。callable属于并发包里面的类,这个会在接下来专门的篇章中分析。这里不再赘述。

8. resume(过时)
这个方法已被弃用,所以这里只做简单的概括。它可以重启线程。
9. suspend(过时)
这个也是过时的方法,它可以暂停线程,但是只会释放cpu不会释放锁,因此容易造成死锁。
10. stop(过时)
这个也是过时的方法,它会强制停止线程,容易造成线程不安全的情况。

11. interrupt、interrupted、 isInterrupted
这里分析一下这三兄弟。

interrupt
interrupt 是一个成员方法,意味着谁拿到了这个线程对象,都可以调用这个方法,包括自己。因此会存在并发问题。调用此方法将会在这个线程对象上打上一个中断标志,而线程依然会继续运行。

public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {		// 这里处理并发
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();           // Just to set the interrupt flag,调用了这个方法的线程将会被打上中断标志。
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

对于中断行为,可以分为好几个场景:
(1). NEW状态下中断
通过测试,NEW状态下,线程未启动。中断不会生效。

(2). RUNNABLE状态下中断
RUNABLE下,中断操作会使线程打上中断标志位。

(3). 阻塞状态(WAITING、TIMED_WAITING 、BLOCKED)下中断
阻塞状态下,如果被中断则会退出阻塞状态,并抛出InterruptedException异常,抛出异常后,中断标志将被清除。这一点从常用的几个方法上就可以看出端倪:

		 public final void wait() throws InterruptedException{...};	
		 public static native void sleep(long millis) throws InterruptedException;
		 public final void join() throws InterruptedException{...};  

interrupted

 public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

这是一个静态方法,从方法体中可以看出,这个方法只能对自己执行。用于判断当前线程的中断标志位是ture还是false。

isInterrupted()

  public boolean isInterrupted() {
    return this.isInterrupted(false);
}

这是一个成员方法,与interrupted相同之处在于,其都是调用的isInterrupted(boolean ClearInterrupted)方法。然而不同之处在于一个为参数true,一个参数为false。

这里的玄机在于boolean ClearInterrupted这个参数的意思为,是否清除中断标志位。对于interrupted()静态方法来说,调用它会清除当前线程的中断标志位,重置为false。而isInterrupted()不会清除。所以考虑使用成员方法isInterrupted()还是静态方法interrupted(),主要取决于是否想清除中断标志位。

14. join

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.
译文:这个方法是用一个循环的wait来实现,线程结束时将会执行这个线程对象的notifyAll()方法,
释放这个对象监视器中的所有等待线程。所以在这个线程对象上,最好就不要再使用wait和notify、notifyAll等方法了。

public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {			// 如果join时间为0,则表示进入WAITING状态,。
        while (isAlive()) {		// 外面为什么需要加循环? 可以参考wait方法调用说明,防止虚假唤醒。
            wait(0);			// 调用了wait(0)方法来实现。
        }
    } else {
        while (isAlive()) {		// 这里是同理
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);		// 进入TIMED_WAITING状态。
            now = System.currentTimeMillis() - base;
        }
    }
}

join方法的底层其实就是使用wait方法来实现。对于执行任何线程t1.join方法的线程t2而言,该线程t2执行到t1.join处,将会调用wait()从而进入t1对象的监视器中,t2也因此由RUNNABLE状态转为WAITTING状态,直到等到t1线程对象结束时调用notifyAll方法,t2线程才会被释放,由WAITTING回到RUNNABLE状态。

所以,join方法可以实现线程间的执行排序,决定谁先执行谁后执行。

15. wait()
wait方法源于Object对象,线程作为一个对象也同样继承了这些方法。
对于wait方法,可以详细阅读官方注释:

/**
 * Causes the current thread to wait until another thread invokes the
 * {@link java.lang.Object#notify()} method or the
 * {@link java.lang.Object#notifyAll()} method for this object.
 * In other words, this method behaves exactly as if it simply
 * performs the call {@code wait(0)}.
 *  当前线程进入wait 直到其他线程调用notify或notifyAll唤醒,这个方法相当于调用了 wait(0),wait(0)是永久等待的意思,直到被其他线程唤醒;
 * <p>
 * The current thread must own this object's monitor. The thread
 * releases ownership of this monitor and waits until another thread
 * notifies threads waiting on this object's monitor to wake up
 * either through a call to the {@code notify} method or the
 * {@code notifyAll} method. The thread then waits until it can
 * re-obtain ownership of the monitor and resumes execution.
 *  当前线程必须拥有这个对象的监视器。调用后会一直等待直到其他线程通过notify或notifyAll唤醒。
 *  唤醒后,当前线程将会进入锁池,重新竞争锁以获取运行权利。
 * <p>
 * As in the one argument version, interrupts and spurious wakeups are
 * possible, and this method should always be used in a loop:
 * This method should only be called by a thread that is the owner
 * of this object's monitor. 
 * 单参数版本中,有可能会造成中断和虚假唤醒,所以调用这个方法必须在循环中进行。
 * 线程必须拥有这个对象的监视器才能调用这个对象的wait方法,
 *
 * @throws  IllegalMonitorStateException  if the current thread is not
 *               the owner of the object's monitor.   // 未拥有监视器非法调用异常
 * @throws  InterruptedException if any thread interrupted the		// 阻塞情况下,被其他线程中断异常。
 *             current thread before or while the current thread
 *             was waiting for a notification.  The <i>interrupted
 *             status</i> of the current thread is cleared when
 *             this exception is thrown.
 */
 
  public final void wait() throws InterruptedException {
    wait(0);
}

从方法体中可以看出实际上是调用的wait(long timeout)方法;
因此我们看一下wait(long timeout)方法。

16.wait(long timeout)、notify() 和 notifyAll()

* This method causes the current thread (call it <var>T</var>) to
 * place itself in the wait set for this object and then to relinquish
 * any and all synchronization claims on this object. Thread <var>T</var>
 * becomes disabled for thread scheduling purposes and lies dormant
 * until one of four things happens:
 *  调用线程将会进入这个对象监视器中的等待集区域,并且释放对象监视器。 
 * 	该线程将会处于禁用和休眠状态,除非以下四件事情之一发生:
 * 
 * <ul>
 * <li>Some other thread invokes the {@code notify} method for this
 * object and thread <var>T</var> happens to be arbitrarily chosen as
 * the thread to be awakened.
 *  其他线程调用这个对象监视器的notify方法来随机唤醒等待池中的某个线程。 
 * 
 * <li>Some other thread invokes the {@code notifyAll} method for this
 * object.
 * 	其他线程调用这个对象监视器的notify方法来随机唤醒等待池中的所有线程。
 *  
 * <li>Some other thread {@linkplain Thread#interrupt() interrupts}
 * thread <var>T</var>.
 *  其他线程调用这个线程的interrupt方法来中断线程。
 * 
 * <li>The specified amount of real time has elapsed, more or less.  If
 * {@code timeout} is zero, however, then real time is not taken into
 * consideration and the thread simply waits until notified.
 *  入参时间结束将会被唤醒,如果参数为0,则代表一直休眠。
 * </ul>
 * The thread <var>T</var> is then removed from the wait set for this
 * object and re-enabled for thread scheduling. It then competes in the
 * usual manner with other threads for the right to synchronize on the
 * object; once it has gained control of the object, all its
 * synchronization claims on the object are restored to the status quo
 * ante - that is, to the situation as of the time that the {@code wait}
 * method was invoked. Thread <var>T</var> then returns from the
 * invocation of the {@code wait} method. Thus, on return from the
 * {@code wait} method, the synchronization state of the object and of
 * thread {@code T} is exactly as it was when the {@code wait} method
 * was invoked.
 *  如果线程被唤醒,则从对象等待池中删掉这个线程,并进入锁池与其他线程重新竞争。
 *  如果重新竞争成功,这个线程将会从之前调用wait方法的位置继续向下执行,而不是代码最开始的地方。 
 * 
 * <p>
 * A thread can also wake up without being notified, interrupted, or
 * timing out, a so-called <i>spurious wakeup</i>.  While this will rarely
 * occur in practice, applications must guard against it by testing for
 * the condition that should have caused the thread to be awakened, and
 * continuing to wait if the condition is not satisfied.  In other words,
 * waits should always occur in loops, like this one:
 *  等待池的线程也可能不在上述四种情况下被唤醒,也就是所谓的“虚假唤醒”。
 *  所以调用wait时应该在一个循环代码块中去执行,像下方例子一样:
 * 
 * <pre>
 *     synchronized (obj) {
 *         while (&lt;condition does not hold&gt;)
 *             obj.wait(timeout);
 *         ... // Perform action appropriate to condition
 *     }
 * </pre>
 *
 * <p>If the current thread is {@linkplain java.lang.Thread#interrupt()
 * interrupted} by any thread before or while it is waiting, then an
 * {@code InterruptedException} is thrown.  This exception is not
 * thrown until the lock status of this object has been restored as
 * described above.
 * 调用interrupt方法会唤醒该线程并抛出异常。但是在恢复此线程锁定状态前,不会抛出异常。
 * 意思就是,如果采用intterrput唤醒线程,线程会进入到锁池里和其他线程重新竞争锁,不管这个过程有多久,
 * 也必须等到它竞争到锁,回到synchronized代码块后,才会抛出 InterruptedException异常。
 *
 * <p>
 * Note that the {@code wait} method, as it places the current thread
 * into the wait set for this object, unlocks only this object; any
 * other objects on which the current thread may be synchronized remain
 * locked while the thread waits.
 *  注意调用wait方法只会解除当前对象的锁,不会解除其他对象的锁。
 *  意思就是当有多重锁时,比如锁1和锁2为包含关系,锁2解除了,但还是在锁1内。
 * <p>

上述提到了几个点:

(1) 对象监视器
网上已经有很多关于wait方法的描述和解释。这里我借鉴一些在此描述:

每个对象都只有一个对象监视器,对象监视器相当于一个建筑,建筑中有一个茅坑。
线程相当于上厕所的人,在多线程的情况下,相当于许多人都要去使用这个对象的茅坑,存在竞争关系。
怎么办?
于是在建筑内有一个锁池的概念,设置了一个锁池,锁池区域长100米,宽无边际,所有人都在锁池区域的起点开始百米赛跑。
谁先跑到终点抢到茅坑,谁就先上厕所。
于是画面妙不可言...

在这里插入图片描述
最后的结果,当然是谁抢占了茅坑,谁就上厕所。其他没有抢到茅坑的回到锁池起点,等待茅坑空出来后再重新抢。
现在假如有一个人抢到进去了,开始使用茅坑。这个意思就如进入同步代码块中开始执行。
在这里插入图片描述
如果这个人上到一半,被调用了wait怎么办?
这个人就会被送入等待池中,等待池也是建筑中的一个概念,相当于休息区。
在这里插入图片描述
当这个人被送入等待池后,茅坑就空出来了。这也就是调用wait方法,会释放锁的原因。茅坑空出来后,其他锁池中的对象又会开始竞争茅坑了。
那等待池中的人怎么办?

wait的官方注释说了,只有四种情况会唤醒该线程:
(1)nofity (对于这个对象监视器中等待区的所有线程,随机唤醒,不一定能唤醒指定线程)
(2)notifyAll (对于这个对象监视器中等待区的所有线程,全部唤醒)
(3)interrupt (中断异常必须等线程回到同步代码块中才能抛出)
(4)超时时间到(如果超时时间为0,则代表永远睡眠,不会自动唤醒)

被唤醒后怎么办?

唤醒后的线程会重新进入锁池,继续竞争茅坑。锁竞争成功后,继续从之前的调用wait的位置向下执行代码,而不是从代码第一行。

(2) 虚假唤醒
官方还说了,除了上述四种情况外可能唤醒线程,还有一种虚假唤醒的可能。

什么情况下会产生虚假唤醒?

其他方法

  1. registerNatives()
  2. activeCount
  3. enumerate
  4. dumpStack
  5. getAllStackTraces
  6. isCCLOverridden
  7. auditSubclass
  8. getThreads

猜你喜欢

转载自blog.csdn.net/weixin_43901067/article/details/105748479