安全是多线程编程的核心主题,但并不是只要使用多线程就一定会引发安全问题。要了解哪些操作是安全的,哪些是不安全的,就必须先掌握如何使用多线程。不过在操作多线程之前,我们先了解一下多线程的几种状态。
线程的状态
在Thread的实现中,包含一个名为State的enum类,用来标识线程运行中的各种状态,其中定义了以下几个类型:
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.
*
* ...
*/
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 和 TERMINATED 是两种特殊的状态,前者表示线程还未开始运行,后者表示线程已经运行完毕。在这两种状态下,对线程进行一些操作是没有意义的,因为线程根本没有运行,也就不会去响应中断、睡眠等请求了。
当执行了start方法之后或者线程正在运行时,线程会进入RUNNABLE状态,这时线程或者正在运行,或者正在等待CPU调度。
BLOCKED表示线程正在等待锁,也就是说此时线程要执行的代码是同步的,有其他线程正在运行此代码,所以线程需要等待获取锁。
WAITING和TIMED_WAITING都表示线程要等待一段时间之后再运行,只是后者会有一个超时处理。
线程的执行就是在以上这些状态中不断切换,当然 NEW 和 TERMINATED 这两种表示线程起止的状态,在线程的生命周期中只会执行一次。
创建线程
在Java中创建一个线程有两种方式:继承Thread和实现Runnable。其实这两种方式并没有很大的差别,只是Java仅支持单继承,实现Runnable的方式更灵活一些,但是一个Runnable对象本身是无法执行的,需要用一个Thread对象来帮助它启动,就像这样:
new Thread(new MyRunnable()).start();
启动线程
线程的启动要使用start方法,而不是调用Runnable的run方法,不过如果多次调用start方法会抛出异常:
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".
*/
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. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
// ...
}
}
通过判断threadStatus的值来确保线程仅被启动一次,threadStatus对应一个枚举的线程状态,在前面已经分析过它。
调用start方法之后并不是说线程马上就开始运行了,因为CPU可能处于忙碌中,没有多余的时间片,因此start方法只是把Thread的状态变为RUNNABLE,等待CPU调度。
线程休眠
如果要让线程停止一段时间再继续运行,可以使用sleep(long millis)方法,sleep会让当前线程进入TIMED_WAITING状态,经过millis时间之后线程会自动苏醒,并重新进入RUNNABLE状态等待系统调度。
yield放弃时间片
当一个线程获得CPU时间片之后,可以通过调用yield方法放弃所获得的时间片,并重新进入RUNNABLE状态等待系统调度。和sleep不同之处在于,我们无法知道yield方法调用后线程会等待多久,因为它和其他所有的线程一样处于RUNNABLE状态,那么CPU就可能在任何时候调度它,也可能永远不会调度它。
中断停止线程
通过start方式可以启动线程,我们很容易就会想到使用stop方法来停止,然而stop方法已经过时了,如下:
@Deprecated
public final void stop() {
// ...
}
在说明如何正确的停止线程之前,我们先说明一下为什么stop方法会过时。stop会释放持有的锁,以使数据可以被其他线程访问,我们知道计算机解决任何问题都不是一蹴而就的,完成一个任务需要很多步骤,每一步都会对结果产生一定的影响,而如果让线程在某一步直接停止,就很可能得到一个不完整的数据。例如给一个用户依次设置姓名和昵称,如果stop正好发生在设置姓名和设置昵称之间,我们得到的用户信息就不再完整。
正确的停止线程方法是使用中断。中断不是说会立即打断线程的执行,而是给线程发送一个中断的信号,由线程来决定何时响应,这样我们就有了足够的时间对数据进行清理。
既然有发送信号,那就一定有办法判断是否接收到了中断信号,Thread中有两种方式来判断是否中断:
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
return isInterrupted(false);
}
这两种方式最终都是调用了一个native方法实现的:
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
可以看到,两种方式的区别在于,interrupted是判断当前的线程是否中断,并且会清除中断标记;而isInterrupted是判断调用此方法的Thread对象是否中断,并且不会清除中断标记。我们要区别线程对象和当前线程的区别,前者表示的就是某个线程,而当前线程表示的是执行的某段代码所处的线程,例如,在main线程中,执行以下两处代码时,当前线程currentThread值是不同的:
public class MyThread extends Thread {
public MyThread() {
System.out.println("MyThread constructor :" + Thread.currentThread().getName());
}
@Override
public void run() {
super.run();
System.out.println("MyThread run :" + Thread.currentThread().getName());
}
}
在主线程中,调用该线程的start方法,可以得到以下的输出结果:
MyThread constructor :main
MyThread run :Thread-0
也就是说,调用的代码是在哪个线程中执行的,Thread.currentThread的值就是哪个线程。明白了这个区别,我们就知道了 interrupted 和 isInterrupted 两个方法在何时有区别了。
前面说过,中断只是给线程发送了一个信号,至于如何响应还是由线程决定,可以不理会中断的信号,也可以根据中断的信号做一些数据的处理之后再结束掉当前的线程。
不同状态下的线程对中断的响应方式也有区别,NEW 和 TERMINATED 肯定是不会响应的,RUNNABLE 和 BLOCKED 则是会接收到中断信号,而 WAITING 和 TIMED_WAITING 则是会抛出InterruptedException,并且会清除中断标记,这从 sleep 和 wait 函数的定义中就可以看出:
// Thread.java
/**
* ...
* @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 static native void sleep(long millis) throws InterruptedException;
// Object.java
public final native void wait(long timeout) throws InterruptedException;
了解了中断的概念,中断一个线程就简单多了。例如在 RUNNABLE 状态下,只需要判断是否接收到了中断信号,就可以在合适的时间中断线程。
public class Worker extends Thread {
public void run() {
System.out.println("Worker started.");
while (!isInterrupted()) {
System.out.println("doing something.");
}
System.out.println("Worker stopped.");
}
}
然后,给线程发送一个中断信号:
Worker thread = new Worker();
thread.start();
// 让线程运行起来
Thread.sleep(10);
thread.interrupt();
就可以得到以下的输出:
Worker started.
doing something.
doing something.
doing something.
...
Worker stopped.
如果线程有睡眠等行为时,就可以利用中断异常来停止线程,修改Worker线程的run方法如下:
public class Worker extends Thread {
public void run() {
System.out.println("Worker started.");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("Worker Interrupted.");
}
System.out.println("Worker stopped.");
}
}
可以看到如下输出:
Worker started.
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at chapter01.section1.Interrupt$Worker.run(Interrupt.java:42)
Worker Interrupted.
Worker stopped.
线程从睡眠中被打断,并立即结束。
总结
了解线程的基本概念,是学习线程的第一步。但是至此我们只是学会了如何使用一个线程,接下来我们继续研究多个线程交互时,如何处理数据安全的问题。
网友评论