• 多线程 让程序更高效的运行


    Java Thread 的一些认识:

    • Java是抢占式线程,一个线程就是进程中单一的顺序控制流,单个进程可以拥有多个并发任务,其底层是切分CPU时间,多线程和多任务往往是使用多处理器系统的最合理方式
    • 进程可以看作一个程序或者一个应用;线程是进程中执行的一个任务,多个线程可以共享资源
    • 一个Java 应用从main 方法开始运行,main 运行在一个线程内,也被称为 “主线程”,Runnable也可以理解为Task (任务)
    • JVM启动后,会创建一些守护线程来进行自身的常规管理(垃圾回收,终结处理),以及一个运行main函数的主线程
    • 随着硬件水平的提高,多线程能使系统的运行效率得到大幅度的提高,同时异步操作也增加复杂度和各种并发问题

     ■ 线程 VS 进程

    • 在一个已有进程中创建一个新线程比创建一个新进程快的多
    • 终止一个线程比终止一个进程快的多
    • 同一个进程内线程间切换比进程间切换更快
    • 线程提供了不同的执行程序间通信的效率,同一个进程中的线程共享同一进程内存和文件,无序调用内核就可以互相通信,而进程间通信必须通过内核

     ■ 同步和异步

    • 同步方法一旦开始,调用者必须等到方法调用返回之后,才能继续后续行为
    • 无先后顺序,一旦开始,方法调用便立即返回,调用者就可以继续后续行为,一般为另一个线程执行

     ■ 阻塞和非阻塞

    • 当一个线程占用临界区资源,其他线程也想要使用该资源就必须等待,等待会导致线程的挂起,也就是阻塞(线程变成阻塞状态)。
      此时若占用资源的线程一直不愿意释放资源,那么其他所有阻塞在该临界区的线程都会被挂起,变成阻塞状态,不能正常工作,直到占用线程释放资源
    • 非阻塞强调没有一个线程可以妨碍其他线程执行,所有线程都会尝试去做下一步工作

     ■ 临界资源与临界区

    • 一般指的是公共共享资源,即可以被多个线程共享使用。但同一时间只能由一个线程去访问和操作临界区的资源,一旦临界区资源被一个线程占用,其他线程也想要使用该资源就必须等待,
      就好比好多人想上大号,但只有一个坑,一个人占了坑,其他人就得排队等待喽
    • 临界区可以认为是一段代码,线程会在该端代码中访问共享资源,因此临界区的界定标准就是是否访问共享(临界)资源(有点类似形成闭包的概念);一次只允许有一个程序(进程/线程)在该临界区中

     ■ 上下文切换

    • CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时可以重新加载这个任务的状态。所有任务从保存到再加载的过程就是一次上下文切换
    • 多线程性能问题:由于线程有创建和上下文切换的开销,在多线程环境下,这种开销对时间和资源的利用都是一个极大的负担,很可能导致并发任务执行速度还不如串行快
    • 减少上下文切换: 无锁并发编程、CAS算法、减少并发、使用最少线程、协程
        无锁并发编程:避免使用锁,比如数据分段执行(MapReduce)、尽可能使用无状态对象、避免竞争情况等
        CAS算法:java.util.concurrent包中大量使用CAS算法,比如Atomic、AQS等
        减少并发:JAVA8中新引入的LongAdder、DoubleAdder等新类,将CAS算法替换成value分担原则
        使用最少线程:避免创建不必要的线程,当任务很少但线程很多时,会导致大量线程为等待状态
        协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

    • 补充:需要注意的是,Java的线程是映射到操作系统的原生线程上,因此若要阻塞或唤醒一个线程都需要操作系统的协助,这就意味着要从用户态转换到核心态,因此状态转换是非常耗费处理器时间的

     ■ 竞合条件

    • 竞争条件指多个线程并发访问和操作同一数据且执行结果与线程访问的特定顺序有关
    • 竞争条件发生在当多个线程在读写数据时,其最终的的结果依赖于多个线程指令执行顺序
    • 由于竞争条件的指令顺序操作的不确定性甚至是错误的,可能会造成结果的混乱,例如臭名昭著的i++的原子性问题
    • 值得注意的是即使在单处理器环境下,也可能因为中断的可以在任何地方停止指令的执行的特性,导致类似并发情况下的数据不一致性,解决方案同竞争条件一致:保证指令的执行顺序

    ■ 并发级别

         A. 阻塞(Blocking)

    • 线程若是阻塞的,那么在其他线程释放资源之前,当前线程会被挂起,无法继续执行
    • 在Java中,若使用 synchronized 关键字或者重入锁时,得到的就是阻塞的线程
    • 无论是 synchronized 还是重入锁,都会试图在执行后续代码之前,竞争临界区的锁:
        如果竞争成功,当前线程会获得锁并占用资源,从而继续往后执行
        如果竞争失败,继续挂起阻塞,等待下次资源被释放后的再次竞争

     B.  无饥饿(Starvation-Free)

    • 若线程间区分优先级,那么线程调度通常会优先满足高优先级的线程(非公平原则),就可能产生饥饿
    • 对于非公平的锁来说,系统允许高优先级的线程插队,这样就可能导致低优先级线程产生饥饿 ,如 ReentrantLock 非公平构造 sync = new NonfairSync()
    • 对于公平的锁来说,不管新到的线程优先级多高都需要乖乖排队,所有线程都有机会执行,饥饿很难产生,ReentrantLock 公平构造 sync = boolean fair ? new FairSync() : new NonfairSync()
    • 当一个任务非常耗时导致某线程一直占据关键资源不放,其他线程难以获取,此时其他线程也可以说是饥饿的

    ■ 线程状态流程图:

     Java 线程转换状态 (重要)

    • 新建(new):新创建一个线程对象:在JAVA中的表现就是Thread thread = new Thread();
    • 就绪(runnable):线程创建后,其他线程调用该对象的start方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU时间分片使用权:在JAVA中的表现就是 thread.start()
    • 运行(running):就绪态线程获取到CPU时间分片之后,就可以执行任务:在JAVA中的表现就是thread.run(),但需要注意的是,此时线程不一定是立即执行,这跟系统调度有关
    • 阻塞(block):阻塞状态是指线程因为某种原因放弃CPU使用权,让出CPU时间片,暂时停止运行(注意此时线程在内存中是存在的,并没有被GC),直到线程进入就绪态,才有机会再次获取时间片转成运行态。阻塞分三种情况:
      •   常规阻塞:运行态线程在发出I/O请求、执行Thread.sleep方法、t.join方法时,JVM会将该线程设置为阻塞状态;当I/O处理完毕并发出响应、sleep方法超时、join等待线程终止或超时,线程重新转入就绪态
      •   同步阻塞:运行态线程在获取对象的同步锁时,当该同步锁已被其他线程占用,JVM会将该线程暂时放入同步队列中,当其他线程放弃同步锁同时该线程竞争到该同步锁时,该线程转为就绪态
      •   等待阻塞:运行态线程执行wait方法,JVM会将该线程放入等待队列中,直到被其他线程唤醒或其他线程中断或超时,再次获取同步锁标识,重新进入就绪态`
    • 死亡(dead):线程main方法、run方法执行完毕或出现异常,则该线程生命周期结束。处于死亡或终结状态的线程不可再次被调度,不可被分配到时间片;已死亡线程不可复生,当然可以使用线程池机制来提高线程的复用性,避免线程被直接杀死;此时其寄存器上下文和栈都将被释放

     Java 线程枚举状态

    /** 
      * JAVA对于线程状态的枚举类,使用jstack查看dump文件可以看到相对应的线程状态
      * 注意:状态的转换要以状态图为参照标准,枚举类只是用来统一记录某种状态以方便JAVA代码编写!
      * 对应JVM中对线程的监控的4种核心抽象状态:
      *     运行(running),休眠(Sleeping),等待(Wait),监视(Monitor)
      */
    public enum State {
        /**
          * Thread state for a thread which has not yet started.
          *     新建:线程创建但还不是就绪态(Thread还没有执行start方法)
          */
            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.
          *     运行状态:Java将就绪态和运行态统一设置为RUNNABLE
          *     笔者认为这可能与Thread执行start方法之后会立即执行run方法有关
          */
        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}.
          *     阻塞:线程正等待获取监视锁(同步锁),调用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.
          *     比如线程被wait方法执行等待,需要被notify或notifyAll唤醒
          *     再比如join方法会等待一个指定线程结束之后才会继续运行
          */
        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;
        }

    Thread 类定义

    public class Thread implements Runnable {
      /* Make sure registerNatives is the first thing <clinit> does. 
        初始化时调用 Java 本地方法,实现了Runnable接口
      */ private static native void registerNatives(); static { registerNatives(); }

    ■ 构造器

    /**
      * 默认构造器
      * 其中name规则为 "Thread-" + nextThreadNum()
      */
    public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }
    /**
      * 创建一个指定Runnable的线程
      * 其中name规则为 "Thread-" + nextThreadNum()
      */
    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    /**
      * 创建一个指定所属线程组和Runnable的线程
      * 其中name规则为 "Thread-" + nextThreadNum()
      */
    public Thread(ThreadGroup group, Runnable target) {
        init(group, target, "Thread-" + nextThreadNum(), 0);
    }
    /**
      * 创建一个指定name线程
      */
    public Thread(String name) {
        init(null, null, name, 0);
    }
    /**
      * 创建一个指定所属线程组和name的线程
      */
    public Thread(ThreadGroup group, String name) {
        init(group, null, name, 0);
    }
    /**
      * 创建一个指定Runnable和name的线程
      */
    public Thread(Runnable target, String name) {
        init(null, target, name, 0);
    }
    /**
      * Allocates a new {@code Thread} object so that it has {@code target}
      * as its run object, has the specified {@code name} as its name,
      * and belongs to the thread group referred to by {@code group}.
      *     创建一个新的Thread对象,同时满足以下条件:
      *         1.该线程拥有一个指定的Runnable对象用于方法执行
      *         2.该线程具有一个指定的名称
      *         3.该线程属于一个指定的线程组ThreadGroup
      * <p>If there is a security manager, its
      * {@link SecurityManager#checkAccess(ThreadGroup) checkAccess}
      * method is invoked with the ThreadGroup as its argument.
      *     若这里有个安全管理器,则ThreadGroup将调用checkAccess方法进而触发SecurityManager的checkAccess方法
      * <p>In addition, its {@code checkPermission} method is invoked with
      * the {@code RuntimePermission("enableContextClassLoaderOverride")}
      * permission when invoked directly or indirectly by the constructor of a subclass which
      * overrides the {@code getContextClassLoader} or {@code setContextClassLoader} methods.
      *     当enableContextClassLoaderOverride被开启时,checkPermission将被重写子类直接或间接地调用
      * <p>The priority of the newly created thread is set equal to the
      * priority of the thread creating it, that is, the currently running
      * thread. The method {@linkplain #setPriority setPriority} may be
      * used to change the priority to a new value.
      *     新创建的Thread的优先级等同于创建它的线程的优先级,调用setPriority会变更其优先级
      * <p>The newly created thread is initially marked as being a daemon
      * thread if and only if the thread creating it is currently marked
      * as a daemon thread. The method {@linkplain #setDaemon setDaemon}
      * may be used to change whether or not a thread is a daemon.
      *     当且仅当线程创建时被显示地标记为守护线程,新创建的线程才会被初始化为一个守护线程
      *     setDaemon方法可以设置当前线程是否为守护线程
      */
    public Thread(ThreadGroup group, Runnable target, String name) {
        init(group, target, name, 0);
    }
    /**
      * Allocates a new {@code Thread} object so that it has {@code target} as its run object,
      * has the specified {@code name} as its name, and belongs to the thread group referred to
      * by {@code group}, and has the specified <i>stack size</i>.
      *     创建一个新的Thread对象,同时满足以下条件:
      *         1.该线程拥有一个指定的Runnable对象用于方法执行
      *         2.该线程具有一个指定的名称
      *         3.该线程属于一个指定的线程组ThreadGroup
      *         4.该线程拥有一个指定的栈容量
      * <p>This constructor is identical to {@link #Thread(ThreadGroup,Runnable,String)}
      * with the exception of the fact that it allows the thread stack size to be specified. 
      * The stack size is the approximate number of bytes of address space that the virtual machine
      * is to allocate for this thread's stack. <b>The effect of the {@code stackSize} parameter,
      * if any, is highly platform dependent.</b> 
      *     栈容量指的是JVM分配给该线程的栈的地址(内存)空间大小,这个参数的效果高度依赖于JVM运行平台 
      * <p>On some platforms, specifying a higher value for the {@code stackSize} parameter may allow
      * a thread to achieve greater  recursion depth before throwing a {@link StackOverflowError}.
      * Similarly, specifying a lower value may allow a greater number of threads to exist
      * concurrently without throwing an {@link OutOfMemoryError} (or other internal error).
      * The details of the relationship between the value of the <tt>stackSize</tt> parameter
      * and the maximum recursion depth and concurrency level are platform-dependent.
      * <b>On some platforms, the value of the {@code stackSize} parameter
      * may have no effect whatsoever.</b>
      *     在一些平台上,栈容量越高,(会在栈溢出之前)允许线程完成更深的递归(换句话说就是栈空间更深) 
      *     同理,若栈容量越小,(在抛出内存溢出之前)允许同时存在更多的线程数
      *     对于栈容量、最大递归深度和并发水平之间的关系依赖于平台
      * <p>The virtual machine is free to treat the {@code stackSize} parameter as a suggestion. 
      * If the specified value is unreasonably low for the platform,the virtual machine may instead 
      * use some platform-specific minimum value; if the specified value is unreasonably  high, 
      * the virtual machine may instead use some platform-specific maximum. 
      * Likewise, the virtual machine is free to round the specified value up or down as it sees fit
      * (or to ignore it completely).
      *     JVM会将指定的栈容量作为一个参考依据,但当小于平台最小值时会直接使用最小值,最大值同理
      *     同样,JVM会动态调整栈空间的大小以适应程序的运行或者甚至直接就忽视该值的设置
      * <p><i>Due to the platform-dependent nature of the behavior of this constructor, extreme care
      * should be exercised in its use.The thread stack size necessary to perform a given computation 
      * will likely vary from one JRE implementation to another. In light of this variation, 
      * careful tuning of the stack size parameter may be required,and the tuning may need to
      * be repeated for each JRE implementation on which an application is to run.</i>
      *     简单总结一下就是:这个值严重依赖平台,所以要谨慎使用,多做测试验证
      * @param  group
      *         the thread group. If {@code null} and there is a security
      *         manager, the group is determined by {@linkplain
      *         SecurityManager#getThreadGroup SecurityManager.getThreadGroup()}.
      *         If there is not a security manager or {@code
      *         SecurityManager.getThreadGroup()} returns {@code null}, the group
      *         is set to the current thread's thread group.
      *             当线程组为null同时有个安全管理器,该线程组由SecurityManager.getThreadGroup()决定
      *             当没有安全管理器或getThreadGroup为空,该线程组即为创建该线程的线程所属的线程组
      * @param  target
      *         the object whose {@code run} method is invoked when this thread
      *         is started. If {@code null}, this thread's run method is invoked.
      *             若该值为null,将直接调用该线程的run方法(等同于一个空方法)
      * @param  name
      *         the name of the new thread
      * @param  stackSize
      *         the desired stack size for the new thread, or zero to indicate
      *         that this parameter is to be ignored.
      *             当栈容量被设置为0时,JVM就会忽略该值的设置
      * @throws  SecurityException
      *          if the current thread cannot create a thread in the specified thread group
      *             如果当前线程在一个指定的线程组中不能创建一个新的线程时将抛出 安全异常
      * @since 1.4
      */    
    public Thread(ThreadGroup group, Runnable target, String name,long stackSize) {
        init(group, target, name, stackSize);
    }

    ■ JVM栈异常分类

     根据栈异常的不同,主要有两种分类:
     1) 栈溢出:若线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常

     2) 内存溢出: 若虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

    ■ 重要变量

    //线程名,用char来保存(String底层实现就是char)
    private char        name[];
    //线程优先级
    private int         priority;
    //不明觉厉
    private Thread      threadQ;
    //不明觉厉
    private long        eetop;
    /* Whether or not to single_step this thread. 不明觉厉*/
    private boolean     single_step;
    /* Whether or not the thread is a daemon thread. 是否是守护线程,默认非守护线程*/
    private boolean     daemon = false;
    /* JVM state. 是否一出生就领便当,默认false*/
    private boolean     stillborn = false;
    /* What will be run. Thread的run方法最终会调用target的run方法*/
    private Runnable target;
    /* The group of this thread. 当前线程的所属线程组*/
    private ThreadGroup group;
    /* The context ClassLoader for this thread 当前线程的ClassLoader*/
    private ClassLoader contextClassLoader;
    /* The inherited AccessControlContext of this thread 当前线程继承的AccessControlContext*/
    private AccessControlContext inheritedAccessControlContext;
    /* For autonumbering anonymous threads. 给匿名线程自动编号,并按编号起名字*/
    private static int threadInitNumber;
    /* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. 
     * 当前线程附属的ThreadLocal,而ThreadLocalMap会被ThreadLocal维护(ThreadLocal会专门分析)
     */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     * 主要作用:为子线程提供从父线程那里继承的值
     * 在创建子线程时,子线程会接收所有可继承的线程局部变量的初始值,以获得父线程所具有的值
     * 创建一个线程时如果保存了所有 InheritableThreadLocal 对象的值,那么这些值也将自动传递给子线程
     * 如果一个子线程调用 InheritableThreadLocal 的 get() ,那么它将与它的父线程看到同一个对象
     */
     ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    /*
     * The requested stack size for this thread, or 0 if the creator did not specify a stack size. 
     * It is up to the VM to do whatever it likes with this number; some VMs will ignore it.
     * 栈容量:当设置为0时,JVM会忽略该值;该值严重依赖于JVM平台,有些VM甚至会直接忽视该值
     * 该值越大,线程栈空间变大,允许的并发线程数就越少;该值越小,线程栈空间变小,允许的并发线程数就越多
     */
    private long stackSize;
    /* JVM-private state that persists after native thread termination.*/
    private long nativeParkEventPointer;
    /* Thread ID. 每个线程都有专属ID,但名字可能重复*/
    private long tid;
    /* For generating thread ID 用于ID生成,每次+1*/
    private static long threadSeqNumber;
    /* 
     * Java thread status for tools,initialized to indicate thread 'not yet started'
     * 线程状态 0仅表示已创建
     */
    private volatile int threadStatus = 0;
    /**
      * The argument supplied to the current call to java.util.concurrent.locks.LockSupport.park.
      * Set by (private) java.util.concurrent.locks.LockSupport.setBlocker
      * Accessed using java.util.concurrent.locks.LockSupport.getBlocker
      * 主要是提供给 java.util.concurrent.locks.LockSupport该类使用
      */
    volatile Object parkBlocker;
    /* The object in which this thread is blocked in an interruptible I/O operation, if any. 
     * The blocker's interrupt method should be invoked after setting this thread's interrupt status.
     * 中断阻塞器:当线程发生IO中断时,需要在线程被设置为中断状态后调用该对象的interrupt方法
     */
    private volatile Interruptible blocker;
    //阻塞器锁,主要用于处理阻塞情况
    private final Object blockerLock = new Object();
     /* The minimum priority that a thread can have. 最小优先级*/
    public final static int MIN_PRIORITY = 1;
    /* The default priority that is assigned to a thread. 默认优先级*/
    public final static int NORM_PRIORITY = 5;
    /* For generating thread ID 最大优先级*/
    public final static int MAX_PRIORITY = 10;
    /* 用于存储堆栈信息 默认是个空的数组*/
    private static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0];
    private static final RuntimePermission SUBCLASS_IMPLEMENTATION_PERMISSION =
                new RuntimePermission("enableContextClassLoaderOverride");
    // null unless explicitly set 线程异常处理器,只对当前线程有效
    private volatile UncaughtExceptionHandler uncaughtExceptionHandler;
    // null unless explicitly set 默认线程异常处理器,对所有线程有效
    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler; 

    ■ 本地方法

    /* 
     * Make sure registerNatives is the first thing <clinit> does. 
     * 确保clinit最先调用该方法:所有该方法是类中的最靠前的一个静态方法
     * clinit:在JVM第一次加载class文件时调用,用于静态变量初始化语句和静态块的执行
     * 所有的类变量初始化语句和类型的静态初始化语句都被Java编译器收集到该方法中
     * 
     * registerNatives方法被native修饰,即是本地方法,将由C/C++去完成,并被编译成了.dll,供JAVA调用
     * 其主要作用是将C/C++中的方法映射到Java中的native方法,实现方法命名的解耦
     */
    private static native void registerNatives();
    static {
        registerNatives();
    }
    /** 主动让出CPU资源,当时可能又立即抢到资源 **/
    public static native void yield();
    /** 休眠一段时间,让出资源但是并不会释放对象锁 **/
    public static native void sleep(long millis) throws InterruptedException;
    /** 检查 线程是否存活 **/
    public final native boolean isAlive();
    /** 检查线程是否中断 isInterrupted() 内部使用 **/
    private native boolean isInterrupted(boolean ClearInterrupted);
    /** 返回当前执行线程 **/
    public static native Thread currentThread();
    public static native boolean holdsLock(Object obj);
    private native void start0();
    private native void setPriority0(int newPriority);
    private native void stop0(Object o);
    private native void suspend0();
    private native void resume0();
    private native void interrupt0();
    private native void setNativeName(String name);

    ■ 线程初始化

    /**
      * Initializes a Thread.
      *     初始化一个线程
      * @param g the Thread group
      * @param target the object whose run() method gets called
      * @param name the name of the new Thread
      * @param stackSize the desired stack size for the new thread, or
      *        zero to indicate that this parameter is to be ignored.
      */
    private void init(ThreadGroup g, Runnable target, String name,long stackSize) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
        //返回当前线程,即创建该hread的线程 currentThread是个本地方法
        Thread parent = currentThread();
        //安全管理器根据Java安全策略文件决定将哪组权限授予类
        //如果想让应用使用安全管理器和安全策略,可在启动JVM时设定-Djava.security.manager选项
        //还可以同时指定安全策略文件
        //如果在应用中启用了Java安全管理器,却没有指定安全策略文件,那么Java安全管理器将使用默认的安全策略
        //它们是由位于目录$JAVA_HOME/jre/lib/security中的java.policy定义的
        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();
            }
        }
        /* checkAccess regardless of whether or not threadgroup is explicitly passed in. */
        //判断当前运行线程是否有变更其线程组的权限
        g.checkAccess();
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }
        //新建线程数量计数+1 或者说未就绪线程数+1==> nUnstartedThreads++;
        g.addUnstarted();
        this.group = g;
        this.daemon = parent.isDaemon();//若当前运行线程是守护线程,新建线程也是守护线程
        this.priority = parent.getPriority();//默认使用当前运行线程的优先级
        this.name = name.toCharArray();
        //设置contextClassLoader
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext = AccessController.getContext();
        this.target = target;
        setPriority(priority);//若有指定的优先级,使用指定的优先级
        if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;
        /* Set thread ID 线程安全,序列号每次同步+1*/
        tid = nextThreadID();
    }
    private static synchronized long nextThreadID() { return ++threadSeqNumber; }

    ■ start 方法

    /**
      * Causes this thread to begin execution; the Java Virtual Machine
      * calls the <code>run</code> method of this thread.
      *     线程进入就绪态,随后JVM将会调用这个线程run方法
      *     当获取到CPU时间片时,会立即执行run方法,此时线程会直接变成运行态
      * <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.
      *     一个线程只能被start一次,特别是线程不会在执行完毕后重新start
      *     当线程已经start了,再次执行会抛出IllegalThreadStateException异常
      * @exception  IllegalThreadStateException  if the thread was already started.
      * @see        #run()
      * @see        #stop()
      */
    public synchronized void start() {
        /**
          * This method is not invoked for the main method thread or "system"
          * group threads created/set up by the VM. Any new functionality added
          * to this method in the future may have to also be added to the VM.
          * 该方法不会被主线程或系统线程组调用,若未来有新增功能,也会被添加到VM中
          * A zero status value corresponds to state "NEW".
          * 0对应"已创建"状态 -> 用常量或枚举标识多好
          */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        //通知所属线程组该线程已经是就绪状态,因而可以被添加到该线程组中
        //同时线程组的未就绪线程数需要-1,对应init中的+1
        group.add(this);
        boolean started = false;
        try {
            //调用本地方法,将内存中的线程状态变更为就绪态
            //同时JVM会立即调用run方法,获取到CPU之后,线程变成运行态并立即执行run方法
            start0();
            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 */
                //如果start0出错,会被调用栈直接通过
            }
        }
    }
    -------------
    //start之后会立即调用run方法
    Thread t = new Thread( new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    },"roman");
    t.start();  //roman

     ■ run 方法

     /**
       * If this thread was constructed using a separate <code>Runnable</code> run object,
       * then that <code>Runnable</code> object's <code>run</code> method is called;
       * otherwise, this method does nothing and returns.
       * Subclasses of <code>Thread</code> should override this method.
       *    若Thread初始化时有指定Runnable就执行其的run方法,否则doNothing
       *    该方法必须被子类实现
       */
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

     ■ isAlive 方法

    /**
      * Tests if this thread is alive. A thread is alive if it has
      * been started and has not yet died.
      *     测试线程是否处于活动状态
      *     活动状态:线程处于正在运行或者准备开始运行状态
      * @return  <code>true</code> if this thread is alive;
      *          <code>false</code> otherwise.
      */
    public final native boolean isAlive();

    ✺ 线程运行 : 模拟电梯运行类

    public class LiftOffTask implements Runnable {  //Runnable 可看作任务(Task)
        private int countDown = 10;   //电梯阶层
    
        public LiftOffTask(){
        }
    
        // syn countDown
        private synchronized int getCountDown(){
            --countDown;  // ++ -- 是非线程安全
            return countDown;
        }
    
        // getStatus
        public String status(){
            return Thread.currentThread().toString()+  "("+
                    (countDown > 0? countDown: "Liftoff!") + "),";
        }
    
        @Override
        public void run(){
            while (getCountDown() >0){
                System.out.println(status());
                Thread.yield();
            }
        }
    
        public static void main(String[] args){
            Thread thread = new Thread(new LiftOffTask());
            thread.start();  // 调用 run()
            System.out.println("================ Waiting for LiftOff... ===========================");
        }
    
    }

     ■ sleep 方法

    /**
     * Causes the currently executing thread to sleep (temporarily cease execution)
     * for the specified number of milliseconds plus the specified number of nanoseconds, 
     * subject to the precision and accuracy of system timers and schedulers.
     * The thread does not lose ownership of any monitors.
     *      使线程睡眠一段毫秒时间,但线程并不会丢失已有的任何监视器
     */
    public static void sleep(long millis, int nanos) throws InterruptedException {
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException("nanosecond timeout value out of range");
        }
        //换算用
        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }
        sleep(millis);
    }
    /** 我们一般会直接调用native方法,这或许是我们主动使用的最多次的native方法了 **/
    public static native void sleep(long millis) throws InterruptedException;

     ■ 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资源
      *  线程调度器会自由选择是否忽视此暗示
      * <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.
      *  该方法会放弃当前的CPU资源,将它让给其他的任务去占用CPU执行时间
      *  但放弃的时间不确定,可能刚刚放弃又获得CPU时间片
      * <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.
      *  该方法的适合使用场景比较少,主要用于Debug,比如Lock包设计
      */
    public static native void yield();

     ✺ 线程让步实例

    public class ThreadYieldTest {
        public static void main(String[] args) {
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    long begin = System.currentTimeMillis();
                    int count = 0;
                    for (int i=1; i<1000000; i++) {
    //                    Thread.yield();
                        count += i;
                    }
                    long end = System.currentTimeMillis();
                    System.out.println("耗时:" + (end - begin) + "毫秒!");
                }
            }, "yieldTask");
            t1.start();
        }
    }

     ■ 中断机制 

        1. Java 终止线程主要有三个手段

    • 自动终止: 使用退出标识,使线程正常退出,即当run()方法完成之后线程自动终止
    • 强行终止: 使用stop()方法强行终止,但该方法已被弃用,使用它们可能产生不可预料的后果:该线程会立即停止,并抛出特殊的ThreadDeath()异常,若此时任务仍未执行完毕,可能产生脏数据
    • 手动终止: 使用interrupt()方法中断线程,并在获取到线程中断时结束(退出)任务,当然 interrupt 并非真正中断线程,可由程序员自行处理

       2.  中断机制

      由于Java中无法立即停止一个线程,而停止操作很重要,因此Java提供了一种用于停止线程的机制,即中断机制:

    • 中断状态:在Java中每个线程会维护一个Boolean中断状态位,用来表明当前线程是否被中断,默认非中断为 false
    • 中断方法:中断仅仅只是一种协作方式(可以直接理解为开关标志),JDK仅提供设置中断状态和判断是否中断的方法,如 interrupted() 和i isInterrupted()
    • 中断过程:由于JDK只负责检测和更新中断状态,因此中断过程必须由程序猿自己实现,包括中断捕获、中断处理,因此如何优雅的处理中断变得尤为重要

      

       3.  中断方法

    • Thread.currentThread().isInterrupted(): 判断是否中断,对象方法,不会清除中断状态
    • Thread.interrupted(): 判断是否中断,静态方法,会清除中断状态(设置为false)
    • Thread.currentThread().interrupt(): 中断操作,对象方法,会将线程的中断状态设置为true,仅此而已(不会真正中断线程),捕获和处理中断由程序猿自行实现

      中断后:线程中断后的结果是死亡、等待新的任务或是继续运行至下一步,取决于程序本身
      /**
       * 中断一个线程(实质是设置中断标志位,标记中断状态)
       *   - 线程只能被自己中断,否则抛出SecurityException异常
       *   - 特殊中断处理如下:
       *     1.若中断线程被如下方法阻塞,会抛出InterruptedException同时清除中断状态:
       *     Object.wait()、Thread.join() or Thread.sleep()
       *
       *     2.若线程在InterruptibleChannel上发生IO阻塞,该通道要被关闭并将设置中断状态同时抛出ClosedByInterruptException异常
       *
       *     3.若线程被NIO多路复用器Selector阻塞,会设置中断状态且从select方法中立即返回一个非0值(当wakeup方法正好被调用时)
       *
       *   - 非上述情况都会将线程状态设置为中断
       *   - 中断一个非活线程不会有啥影响
       */
      public void interrupt() {
          if (this != Thread.currentThread())
              checkAccess();
          synchronized (blockerLock) {
              Interruptible b = blocker;
              if (b != null) {
                  // Just to set the interrupt flag
                  // 调用interrupt方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程!
                  interrupt0();           
                  b.interrupt(this);
                  return;
              }
          }
          interrupt0();
      }

      使用 isInterrupt() 方法可以测试一下: 

    /**
      * Tests whether this thread has been interrupted.  
      * The <i>interruptedstatus< /i> of the thread is unaffected by this method.
      *     测试线程Thread对象是否已经是中断状态,但不清除状态标志
      * @return  <code>true</code> if this thread has been interrupted;
      *          <code>false</code> otherwise.
      * @see     #interrupted()
      * @revised 6.0
      */
    public boolean isInterrupted() {
        //会调用本地isInterrupted方法,同时不清除状态标志
        return isInterrupted(false);
    }
    /**
      * @param ClearInterrupted 是否清除状态标注,false不清除,true清除
      */
    private native boolean isInterrupted(boolean ClearInterrupted);
    -------------
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0 ;i < 10000;i++){}
            System.out.println(Thread.currentThread().getName());
        }
    },"kira");
    t2.start();
    t2.interrupt();
    System.out.println("是否停止 1 ?= " + t2.isInterrupted());//是否停止 1 ?=true
    // 正常输出了10000次 线程名

      设置中断监听:

    Thread thread = new Thread(() -> {
        //循环监听中断状态
        while(!Thread.currentThread().isInterrupted()) {
            //正常执行任务
        }
        //处理中断
    }).start();

        4.  中断情况

      1. 中断非阻塞线程: volatile共享变量或使用interrupt(),前者需要自己实现,后者是JDK提供的
      2. 中断阻塞线程: 当处于阻塞状态的线程调用interrupt()时会抛出中断异常,并且会清除线程中断标志(设置为false);由于中断标志被清除,若想继续中断,需在捕获中断异常后需重新调用interrupt()重置中断标志位(true)
      3. 不可中断线程: synchronized 和 aquire()不可被中断,但AQS提供了acquireInterruptibly()方法相应中断

    • 中断阻塞线程测试用例 - 有可能抛出 InterruptedException
    Thread thread = new Thread(() -> {
        while(!Thread.currentThread().isInterrupted()) {
            System.out.println(Thread.currentThread().getName() + " while run ");
            try {
                System.out.println(Thread.currentThread().getName() + " sleep begin");
                Thread.sleep(500);
                System.out.println(Thread.currentThread().getName() + " sleep end");
            } catch (InterruptedException e) {
                //sleep方法会清空中断标志,若不重新中断,线程会继续执行
                Thread.currentThread().interrupt();
                e.printStackTrace();
            }
        }
        if (Thread.currentThread().isInterrupted()) {
            System.out.println(Thread.currentThread().getName() + "is interrupted");
        }
    });
    thread.start();
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    thread.interrupt();

     ■ Daemon

    • 分类:在JAVA中分成两种线程:用户线程守护线程
    • 特性:当进程中不存在非守护线程时,则全部的守护线程会自动化销毁
    • 应用: JVM在启动后会生成一系列守护线程,最有名的当属GC(垃圾回收器)
     1 Thread t2 = new Thread(new Runnable() {
     2     @Override
     3     public void run() {
     4         System.out.println("守护线程运行了");
     5         for (int i = 0; i < 500000;i++){
     6             System.out.println("守护线程计数:" + i);
     7         }
     8     }
     9 }, "kira");
    10 t2.setDaemon(true);
    11 t2.start();
    12 Thread.sleep(500);
    13 -------------
    14 //输出:
    15 ......
    16 守护线程计数:113755
    17 守护线程计数:113756
    18 守护线程计数:113757
    19 守护线程计数:113758
    20 //结束打印:会发现守护线程并没有打印500000次,因为主线程已经结束运行了

    线程间的通信(重要)

    线程与线程之间不是独立的个体,彼此之间可以互相通信和协作:
        Java提供多种线程间通信方案:轮询机制、等待/通知机制、join()、ThreadLocal、Synchronized、Volatile等

         Volitile: Java内存模型 &Volatile

         Synchronized: JAVA 锁之 Synchronied

         ThreadLocal: ThreadLocal 线程本地变量及源码分析

       - sleep + while(true) 轮询

    public class SleepWhileThread {
        public static void main(String[] args) {
            final List<Integer> list = new ArrayList<Integer>();
    
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run(){
                    try {
                        for (int i=0; i<6; i++){
                            list.add(i);
                            System.out.println("添加了" + (i+1) + "个元素");
                        }
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "listAdd");
    
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        while (true) {
                            if(list.size() == 3) {
                                System.out.println("已添加3个元素, sleepWhile线程需要退出");
                                throw new InterruptedException();
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();  //这里只是打印堆栈信息,不是真的停止执行
                    }
                }
            }, "sleepWhile");
            t1.start();
            t2.start();
        }
    }
    • 难以保证及时性:睡眠时,基本不消耗资源,但睡眠时间过长(轮询间隔时间较大),就不能及时发现条件变更
    • 难以降低开销:减少睡眠时间(轮训间隔时间较小),能更迅速发现变化,但会消耗更多资源,造成无端浪费
    • Java引入等待/通知 (wait/notify) 机制来减少CPU的资源浪费,同时还可以时间在多个线程间的通信

       - wait 和 notify 机制

        ■ Object.wait()

    • wait方法使当前线程进行等待,该方法是Object类的方法,用来将当前线程放入"等待队列"中,并在wait所在的代码处停止执行,直到收到通知被唤醒或被中断或超时
    • 调用wait方法之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait方法
    • 在执行wait方法后,当前线程释放锁,在从wait方法返回前,线程与其他线程竞争重新获得锁
    • 如果调用wait方法时没有持有适当的锁,则抛出运行期异常类IllegalMonitorStateException

        ■ Object.notify()

    • notify方法使线程被唤醒,该方法是Object类的方法,用来将当前线程从"等待队列中"移出到"同步队列中",线程状态重新变成阻塞状态,notify方法所在同步块释放锁后,从wait方法返回继续执行
    • 调用notify方法之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用notify方法
    • 该方法用来通知那么可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器从等待队列中随机选择一个WAITING状态线程,对其发出通知转入同步队列并使它等待获取该对象的对象锁
    • 在执行notify方法之后,当前线程不会马上释放对象锁,等待线程也并不能马上获取该对象锁,需要等到执行notify方法的线程将程序执行完,即退出同步代码块之后当前线程才能释放锁,而等待线程才可以有机会获取该对象锁
    • 如果调用notify方法时没有持有适当的锁,则抛出运行期异常类IllegalMonitorStateException

        ■ wait/notify 机制

      wait()使线程停止运行,notify()使停止的线程继续运行
            1). 使用wait()、notify()、notifyAll()需要先对调用对象加锁,即只能在同步方法或同步块中调用这些方法
            2). 调用wait()方法后,线程状态由RUNNING变成WAITING,并将当前线程放入对象的等待队列中
            3). 调用notify()或notifyAll()方法之后,等待线程不会从wait()返回,需要notify()方法所在同步块代码执行完毕而释放锁之后,等待线程才可以获取到该对象锁并从wait()返回
            4). notify()方法将随机选择一个等待线程从等待队列中移到同步队列中;notifyAll()方法会将等待队列中的所有等待线线程全部移到同步队列中,被移动线程状态由WAITING变成BLOCKED

    // wait/notify 实现线程调度
    public
    class NumberPrint implements Runnable { private int number; public byte[] res; public static int count = 5; public NumberPrint(int number, byte[] a ) { this.number = number; this.res = a; } @Override public void run() { synchronized (res) { while (count-- > 0){ try { res.notify(); System.out.println(" " + number); res.wait(); System.out.println("----------线程"+Thread.currentThread().getName() + "获得锁,wait()后的代码继续运行:"+ number); System.out.println("count:" + count); //count 第一次循环为3,因为两个线程都执行了 count-- } catch (InterruptedException e){ e.printStackTrace(); } } return; } //syn end } public static void main(String[] args) { final byte[] a = {0}; new Thread(new NumberPrint(1,a),"1").start(); new Thread(new NumberPrint(2,a),"2").start(); } }

       - join() : 等待线程对象销毁,可以使得一个线程在另一个线程结束后再执行,底层使用wait() 实现

    /**
      * Waits at most {@code millis} milliseconds for this thread to
      * die. A timeout of {@code 0} means to wait forever.
      *     后续线程需要等待当前线程至多运行millis毫秒(超过millis当前线程会自动死亡,结束等待)
      *     若millis表示0,表示后续线程需要永远等待(直到当前线程运行完毕)
      * <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.
      *     该方法的原理是循环调用wait方法阻塞后续线程直到当前线程已经不是存活状态了
      * @param  millis
      *         the time to wait in milliseconds
      * @throws  IllegalArgumentException
      *          if the value of {@code millis} is negative
      * @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.
      */
    //注意 join方法被synchronized修改,即是个同步方法,也是此处获取到同步锁,为wait做好前提准备
    //同时lock指的就是调用join方法的对象
    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");
        }
        //当millis为0时,说明后续线程需要被无限循环等待,直到当前线程结束运行
        if (millis == 0) {
            while (isAlive()) {
                wait(0);//wait的超时时间为0
            }
        } else {
            //当millis>0时,在millis毫秒内后续线程需要循环等待,直到超时当前线程自动死亡
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);//wait的超时时间为delay
                now = System.currentTimeMillis() - base;
            }
        }
    }
    /**
      * Thread类还提供一个等待时间为0的join方法
      * 用于将后续线程无限循环等待,直到当前线程结束运行
      */
    public final void join() throws InterruptedException {
        join(0);
    }

        join 使用

    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0;i<100;i++){
                System.out.println(Thread.currentThread().getName()+"线程值为:sally" + i);                    }
        }
    },"sally");
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0;i<2;i++){
                System.out.println(Thread.currentThread().getName()+"线程值为:kira" + i);
            }
        }
    },"kira");
    t1.start();
    t1.join();//让t2线程和后续线程无限等待直到sally线程执行完毕
    t2.start();
    -------------
    //输出:
    ......
    sally线程值为:sally97
    sally线程值为:sally98
    sally线程值为:sally99
    kira线程值为:kira0 //可以发现直到sally线程执行完毕,kira线程才开始执行
    kira线程值为:kira1

        ■ join  VS  sleep

    • join(long)方法 在内部使用 wait(long)方法 进行等待,因此 join(long)方法 能够释放锁
    • Thread.sleep(long)方法 却不会释放锁

        ■ join  VS  synchronized

    • join()方法 在内部使用 wait()方法 进行等待
    • `synchronized 使用的是对象监视器原理作为同步

    -------------------------------------------------------------------------------------------------------------------------------

    PS 致谢:

    ***** 各位观众,由于对线程的调度机制还理解比较浅,所以本文会持续迭代更新

    ***** 特别感谢我的好友kira 对本人的日常指导和源码分析提供,一万个Thank you! Kira *******

    更新版本:

    2018/2/8 : 添加线程状态、线程中断等论述

    2018/4/14:  修改了线程中断的一些说明;join() 说明

    2018/9/27:  修改了一些版式,添加了 join() 的例子

  • 相关阅读:
    前端UI框架
    Knowledge
    Microsoft SQL Server
    ASP.NET MVC
    将博客搬至CSDN
    python中的数据类型
    python基础知识
    接口和抽象类的区别
    面向对象的四大特征
    数据结构学习笔记
  • 原文地址:https://www.cnblogs.com/romanjoy/p/7280363.html
Copyright © 2020-2023  润新知