Java 线程基础

创建线程

在java编程中,我们可以继承Tread类并复写其run方法,然后new 一个该对象,并调用其start方法,这样就可以开启一个新线程了。

或者创建实现一个Runnable 接口的类。然后将该对象传递给一个Thread队象。这样也可以开启一个线程。

另外还一个使用线程池创建线程,或者使用带有返回值的Callable创建线程

其实这些方式的本质都是一样的,本质上只有一种创建线程的方式。

我们可以看一下JDK中Thread的定义

public class Thread implements Runnable {  
​  
 /\* What will be run. \*/  
 private Runnable target;  
}

发现Thread 类也实现了Runnable 接口。那么肯定也复写了run方法。

我们创建Thread对象后需要调用其start方法才能开启线程。看下start方法的定义。

/\*\*  
 \* 开启线程的执行,JVM 调用当前线程的run方法。  
 \* Causes this thread to begin execution; the Java Virtual Machine  
 \* calls the <code>run</code> method of this thread.  
 \* <p>  
 \* 结果会有两个线程并发执行。当前线程(调用start方法的线程)和另一个线程(执行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).  
 \* <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.  
 \*  
 \* @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.  
 \*  
 \* A zero status value corresponds to state "NEW".  
 \*/  
 // Android-changed: Replace unused threadStatus field with started field.  
 // The threadStatus field is unused on Android.  
 if (started)  
 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. \*/  
 group.add(this);  
​  
 // Android-changed: Use field instead of local variable.  
 // It is necessary to remember the state of this across calls to this method so that it  
 // can throw an IllegalThreadStateException if this method is called on an already  
 // started thread.  
 started = false;  
 try {  
 // Android-changed: Use Android specific nativeCreate() method to create/start thread.  
 // start0();  
 nativeCreate(this, stackSize, daemon);  
 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 \*/  
 }  
 }  
}

看下Thread类的run方法。

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

可以发现其run方法里面调用了target对象的run 方法,这个target对象就是我们传递进来的runnable对象。

如果我们采用继承Thread 并复写run方法的方式实现线程,这个时候虽然没有target 了,但是我们重写了run方法,所以这个时候jvm调用run的时候实际上执行的是我们复写的run 方法。

所以说这两种创建线程的方法本质上是一种。

实际编程中,受限于java的单继承模型。我们一般使用runnable方式创建线程。

使得Runnable作为一个单独的任务可以被多个线程共同执行,有利于实现并发。提高系统效率、

这样解耦了runnable和Thread。同时提高了程序的灵活性

Thread 类负责线程启动和属性设置等内容,权责分明。而runnable则专注于实现共享资源的操作。

但是如果需要获取到线程的返回结果,那就需要使用Callable 的Future的方式创建线程了。

如何正确的停止一个线程

通常情况下我们是不需要手动停止一个线程的,而是待其运行完毕后自动结束。但是依然会有许多特殊的情况需要我们提前停止线程,比如:用户突然关闭程序,或程序运行出错重启等

在这种情况下,即将停止的线程在很多业务场景下仍然很有价值。尤其是我们想写一个健壮性很好,能够安全应对各种场景的程序时,正确停止线程就显得格外重要。但是Java 并没有提供简单易用,能够直接安全停止线程的能力。

对接线程而言。最正确的方式是使用Interrupt的方式停止线程。它不会立刻停止线程,而是给需要停止的线程发送一个中断信号通知,而需要被中断的线程是否选择中断。由其自己决定。它可以立即停止,也可以将手头工作做完在停止,这也符合java多线程的初衷。直接停止该线程可能会造成数据丢失。任务执行失败。

实际上java希望线程直接可以相互协作。如果不了解对方的工作而冒然停止了对方。就可能会造成一些线程安全的问题,正确的方法是给对方留出收尾的时间让其对未完成的工作有个保存的机会。

如何用 interrupt 停止线程

while (!Thread.currentThread().islnterrupted() && more work to do) {  
 do more work  
}

一旦调用某个线程的 interrupt() 之后,这个线程的中断标记位就会被设置成 true。每个线程都有这样的标记位,当线程执行时,应该定期检查这个标记位,如果标记位被设置成 true,就说明有程序想终止该线程。回到源码,可以看到在 while 循环体判断语句中,首先通过 Thread.currentThread().isInterrupt() 判断线程是否被中断,随后检查是否还有工作要做。&& 逻辑表示只有当两个判断条件同时满足的情况下,才会去执行下面的工作。

sleep,wait这些让线程阻塞进入休眠状态是,线程依然是可以相应中断的,假如进入休眠状态的线程无法相应中断,只能休眠结束后才能相应中断,这样的话就会导致相应中断不及时,可能带来严重的后果。

线程的六种状态

线程整个生命周期可以分为6不,从Thread.State中可以看出

 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;
    }
  1. New 新建
  2. Runnable 运行态
  3. Blocked 阻塞装填
  4. Waiting 等待状态
  5. Timed_Waiting 等待超时
  6. Terminated 结束装填

new

当常见了Thread 对象后,该线程就进入了新建状态

Runnable

当调用了线程对象的start方法后,线程就进入了该状态,Runable 状态对应操作系统线程状态中的两种状态,分别是 Running 和 Ready

此时线程已经被创建,被添加到等待队列中,等待系统给分配CPU资源。此时线程出入Ready就绪态。

当线程获得CPU资源是,线程进入Running运行态。

Blocked

在 Java 中阻塞状态通常不仅仅是 Blocked,实际上它包括三种状态,分别是 Blocked(被阻塞)、Waiting(等待)、Timed Waiting(计时等待),这三 种状态统称为阻塞状态,

Waiting

线程进入 Waiting 状态有三种可能性。

  1. 没有设置 Timeout 参数的 Object.wait() 方法。
  2. 没有设置 Timeout 参数的 Thread.join() 方法。
  3. LockSupport.park() 方法。

刚才强调过,Blocked 仅仅针对 synchronized monitor 锁,可是在 Java 中还有很多其他的锁,比如 ReentrantLock,如果线程在获取这种锁时没有抢到该锁就会进入 Waiting 状态,因为本质上它执行了 LockSupport.park() 方法,所以会进入 Waiting 状态。同样,Object.wait() 和 Thread.join() 也会让线程进入 Waiting 状态。

Blocked 与 Waiting 的区别是 Blocked 在等待其他线程释放 monitor 锁,而 Waiting 则是在等待某个条件,比如 join 的线程执行完毕,或者是 notify()/notifyAll() 。

Timed Waiting 限期等待

以下情况会让线程进入 Timed Waiting 状态。

  1. 设置了时间参数的 Thread.sleep(long millis) 方法;
  2. 设置了时间参数的 Object.wait(long timeout) 方法;
  3. 设置了时间参数的 Thread.join(long millis) 方法;
  4. 设置了时间参数的 LockSupport.parkNanos(long nanos) 方法和 LockSupport.parkUntil(long deadline) 方法。

想要从 Blocked 状态进入 Runnable 状态,要求线程获取 monitor 锁,而从 Waiting 状态流转到其他状态则比较特殊,因为首先 Waiting 是不限时的,也就是说无论过了多长时间它都不会主动恢复。

只有当执行了 LockSupport.unpark(),或者 join 的线程运行结束,或者被中断时才可以进入 Runnable 状态。

如果其他线程调用 notify() 或 notifyAll()来唤醒它,它会直接进入 Blocked 状态,这是为什么呢?因为唤醒 Waiting 线程的线程如果调用 notify() 或 notifyAll(),要求必须首先持有该 monitor 锁,所以处于 Waiting 状态的线程被唤醒时拿不到该锁,就会进入 Blocked 状态,直到执行了 notify()/notifyAll() 的唤醒它的线程执行完毕并释放 monitor 锁,才可能轮到它去抢夺这把锁,如果它能抢到,就会从 Blocked 状态回到 Runnable 状态。

同样在 Timed Waiting 中执行 notify() 和 notifyAll() 也是一样的道理,它们会先进入 Blocked 状态,然后抢夺锁成功后,再回到 Runnable 状态。

当然对于 Timed Waiting 而言,如果它的超时时间到了且能直接获取到锁/join的线程运行结束/被中断/调用了LockSupport.unpark(),会直接恢复到 Runnable 状态,而无需经历 Blocked 状态。

Terminated

Terminated 终止状态,要想进入这个状态有两种可能。

  1. run() 方法执行完毕,线程正常退出。
  2. 出现一个没有捕获的异常,终止了 run() 方法,最终导致意外终止。

最后我们再看线程转换的两个注意点。

线程的状态是需要按照箭头方向来走的,比如线程从 New 状态是不可以直接进入 Blocked 状态的,它需要先经历 Runnable 状态。

线程生命周期不可逆:一旦进入 Runnable 状态就不能回到 New 状态;一旦被终止就不可能再有任何状态的变化。所以一个线程只能有一次 New 和 Terminated 状态,只有处于中间状态才可以相互转换。

也就是说线程可以再Runnable,Blocked,Waiting,TimedWaiting 状态之间转换

处于Runnable 状态的线程无法获取到锁会进入到Blocked状态,处于Waiting 状态的线程获取锁后会进入到Runnable状态。

wait/notify/notifyAll 方法的使用注意事项

有三个问题需要注意

  1. Q:为什么 wait 方法必须在 synchronized 保护的同步代码中使用?
  2. Q2:为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?
  3. Q3:wait/notify 和 sleep 方法的异同?

A1:

A2:

  1. 因为 Java 中每个对象都有一把称之为 monitor 监视器的锁,由于每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类。
  2. 因为如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。

A3:

相同点:

  1. wait/sleep都可以让线程进入到阻塞
  2. 它们都可以响应中断,在等待或者休眠时如果收到中断信号,都会响应中断并抛出InterruptedException异常

不同点:

  1. wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求
  2. wait方法属于对象。它会让线程让出当前持有的对象锁,而sleep属于Thread的方法。不会让线程让出锁。
  3. sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复

philadelphia
17 声望4 粉丝

雪山千古冷,独照峨眉峰