需要了解的概念
并发和并行
并发侧重于任务的交替执行,同一时间只能执行一个任务;而并行是任务的同时执行,统一时间可以有多个任务被执行。
单核CPU与多核CPU下任务表现分别为并发与并行。
临界区
临界区用于表示一种公共资源或是共享数据,可以被多个线程使用,但是同一时间内,只能有一个线程在使用它。一旦临界区资源被占用,其他线程要想使用这个资源,则必须等待。
死锁、饥饿和活锁
死锁
指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
饥饿
饥饿是指线程因为种种原因无法获取所需要的资源,导致一直无法执行。比如它的线程优先级可能太低,而高优先级的线程不断抢占他需要的资源。
活锁
当其他线程要使用临界资源时,如果线程主动放弃资源供其他线程使用,而其它线程也主动放弃来使其他线程使用。这样你让我,我让你,最后无论哪个线程都无法使用资源。
线程的状态
Java 线程有6种状态,其定义在Thread.State
中:
public class Thread implements Runnable {
/**
* A thread can be in only one state at a given point in time.
* These states are virtual machine states which do not reflect any operating system thread states.
*/
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;
}
}
NEW:初始状态,线程被构建,但是还没有调用start方法;
RUNNABLED:运行状态,JAVA 线程把操作系统中的就绪和运行两种状态统一称为RUNNABLED;
BLOCKED:阻塞状态,例如进入同步锁中;
WAITING:等待状态,例如调用Object.wait
、Thread.join
(with no timeout)、LockSupport.park
等;
TIME_WAITING:超时等待状态,例如调用Thread.sleep
以及带超时间的Object.wait
、Thread.join
等,超时以后自动返回;
TERMINATED:终止状态,表示当前线程执行完毕。
示意图如下:
线程的基本使用
java中多线程使用方式主要为继承Thread
类、实现Runnable
接口、实现Callable
接口。如果使用线程池,可以使用ExecutorService
等。
线程的新建
继承Thread
示例:
class ThreadDemo extends Thread {
public void run() {
System.out.println("run");
}
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.start();
}
}
Thread类本质上是实现了Runnable接口的一个实例。启动线程的唯一方法就是通过Thread类的 start()实例方法:
public class Thread implements Runnable {
...
/**
* 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() {
...
try {
start0();
started = true;
} finally {...}
}
}
start()中最终调用了start0(),而start0()是一个native方法,它会启动一个新线程,并在该线程中执行run方法。
start()执行涉及两个线程:当前线程(threadDemo
在该线程中调用自己的start())和新的线程(jvm会在该线程中调用run方法,并使其在在该线程中执行)。
值得注意的是,threadDemo
直接调用run()不会开启新的线程,因为没有最终调用start0(),此时仅是在当前线程中普通的方法调用;threadDemo
直接调用start()则会开启新线程,并让jvm调用run方法在开启的线程中执行。
另外,多次启动线程是不合法的,尤其是线程执行完成后不能重新启动。
实现Runnable接口
如果自己的类已经继承了其它类,无法直接继承Thread
,此时可以实现Runnable接口:
class ThreadDemo implements Runnable {
public void run() {
System.out.println("run");
}
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
new Thread(threadDemo).start();
}
}
实现方式虽然变了,但是最终还是要调用start()。
实现Callable接口
当需要新线程提供返回值给主线程时,例如主线程需要依赖该返回值进行后续的处理,此时可以使用Callable方式:
class ThreadDemo implements Callable {
public String call() {
return "run";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new ThreadDemo());
new Thread(futureTask).start();
String result = futureTask.get();
System.out.println(result);
}
}
Callable和Runnable
Callable方式属于Executor框架的功能类,相对于runable体系:
1. callable在任务结束时可以提供返回值,runnable无法提供;
2. run()不能抛出任何检查型异常,但是,非检查型异常会导致线程终止;
3. 运行callable可以拿到future,future能监视目标线程调用call方法的情况,当调用future的get方法以获取结果时,当前线程就会被阻塞,直到call方法结束返回结果。
对于第二条:
检查型异常(Checked Exception):指编译器要检查这类异常,这类异常的发生通常是难以避免的,编译器强制让开发者去解决掉这类异常(通过throws或try-catch),所以称为检查型异常。如果不处理这类异常则不会通过编译。
例如
FileNotFoundException
,编译器认为文件找不到是不可避免的,如果不处理这些异常,程序将来肯定会出错,此时编译器会提示捕获并处理这种可能发生的异常,不处理就不能通过编译。非检查型异常(Unchecked Exception):指编译器不会检查这类异常,编译器认为此类异常不是必须处理的,因此即使不处理这类异常,编译器也不会给出错误提示。
例如数组越界、空指针异常等。
run()不能抛出检查型异常,因为接口定义中就没有throws异常:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
并且throws是向上级调用者抛出异常,主线程调用start()后,是由jvm去调用run()方法的,因此run()的最终调用者是jvm。
但是线程依然有可能抛出unchecked exception
,此类异常抛出时会导致线程终止,但是对于主线程和其他线程则完全感知不到该异常的抛出(当然也无法法catch到该异常),且该异常的抛出对主线程和其他线程完全没有影响。
因此,对于Runnable体系的线程,我们不能捕获在线程中出现的异常,因此无论是checked exception
还是unchecked exception
,run方法内进行try-catch
并处理掉,即:
class ThreadDemo0 implements Runnable {
public void run() throws Exception { // throws Exception会导致编译不通过
System.out.println(1 / 0);
}
public static void main(String[] args) {
try {
ThreadDemo0 threadDemo0 = new ThreadDemo0();
new Thread(threadDemo0).start();
} catch (Exception e) {
e.printStackTrace(); // 无法catch到除0异常
}
}
}
java5之后,我们可以通过Executor来解决run()抛出的unchecked exception
问题。Thread.UncaughtExceptionHandler
是java5的新接口,它允许在每一个Thread对象上添加一个异常处理器:
public class Thread implements Runnable {
/**
* Interface for handlers invoked when a <tt>Thread</tt> abruptly
* terminates due to an uncaught exception.
* <p>When a thread is about to terminate due to an uncaught exception
* the Java Virtual Machine will query the thread for its
* <tt>UncaughtExceptionHandler</tt> using
* {@link #getUncaughtExceptionHandler} and will invoke the handler's
* <tt>uncaughtException</tt> method, passing the thread and the
* exception as arguments.
* If a thread has not had its <tt>UncaughtExceptionHandler</tt>
* explicitly set, then its <tt>ThreadGroup</tt> object acts as its
* <tt>UncaughtExceptionHandler</tt>. If the <tt>ThreadGroup</tt> object
* has no
* special requirements for dealing with the exception, it can forward
* the invocation to the {@linkplain #getDefaultUncaughtExceptionHandler
* default uncaught exception handler}.
*
* @see #setDefaultUncaughtExceptionHandler
* @see #setUncaughtExceptionHandler
* @see ThreadGroup#uncaughtException
* @since 1.5
*/
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* <p>Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
*/
void uncaughtException(Thread t, Throwable e);
}
}
当线程因未捕获的异常而将要被终止时,则jvm首先查询当前线程是否有UncaughtExceptionHandler
处理器,如果有则使用该处理器的uncaughtException()
来处理,并将当前线程及其异常作为参数传递过去;
如果没有该处理器,则查看当前线程所在线程组是否设置了UncaughtExceptionHandler
,如果已经设置则使用该线程组的UncaughtExceptionHandler
来处理;
否则,通过getUncaughtExceptionHandler
获取默认处理器:
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
如果上述处理器都不存在,那么jvm将直接在console中打印Exception的StackTrace信息。
线程的停止
线程停止不能使用Thread.stop()
,因为该方法是暴力停止,如果业务执行到一半被stop()终止,则可能会导致数据的不一致。
正确的停止方式应该由实际业务决定,例如:
class ThreadDemo0 implements Runnable {
private static volatile boolean stop = false;
private static volatile int i = 0;
public void run() {
while(!stop) {
System.out.println(i++);
}
}
public static void main(String[] args) {
new Thread(new ThreadDemo0()).start();
while(true) {
if (i == 5) {
stop = true;
}
}
}
}
实际上Thread提供了线程中断相关的API:
public void Thread.interrupt();
public boolean Thread.isInterrupted();
public static boolean Thread.interrupted();
interrupt()用于通知线程中断,也就是设置中断标志位,中断标志位表示当前线程已经被中断了。isInterrupted()用于判断当前线程是否被中断。静态方法interrupted()也是用于判断当前线程的中断状态,但同时会清除当前线程的中断标志位。
class ThreadDemo0 implements Runnable {
private static volatile int i = 0;
public void run() {
while(!Thread.currentThread().isInterrupted()) {
System.out.println(i++);
}
}
public static void main(String[] args) {
Thread thread = new Thread(new ThreadDemo0());
thread.start();
while(true) {
if (i == 5) {
thread.interrupt();
break;
}
}
}
}
当发生异常时,interrupt标志位会被复位。
线程等待和通知
wait()和notify()、notifyAll()用于多线程之间的协作,这两个方法都属于Object类,这意味着任何对象都能调用这两个方法。
当在一个对象A调用wait()方法后,当前线程就会在这个对象上等待,直到其他线程调用了对象A的notify()或notifyAll()为止。
如果一个线程调用了object.wait(),那么该线程则进入object对象的等待队列。这个队列中可能会存在多个线程,当object.notify()被调用时,它就会从这个等待队列中随机选择一个线程并将其唤醒。如果执行object.notifyAll()则唤醒队列中所有线程。
需要注意的是,wait()、notify()、notifyAll()必须在synchronized代码块中使用,方法执行前都必须获取目标对象的监视器,当wait()执行时会释放当前的监视器并使得当前线程进入阻塞队列,当notify()被调用时,首先要获取到object的监视器,然后才能去唤醒其他线程。
示例:
class ThreadDemo {
final static Object OBJECT = new Object();
public static class T1 extends Thread {
public void run() {
synchronized (OBJECT) {
try {
System.out.println("t1 start");
OBJECT.wait();
System.out.println("t1 end");
} catch (InterruptedException e) {}
}
}
}
public static class T2 extends Thread {
public void run() {
synchronized (OBJECT) {
System.out.println("t2 start");
OBJECT.notify();
System.out.println("t2 end");
try {
Thread.sleep(2000);
} catch (Exception e) {}
}
}
}
public static void main(String[] args) {
new T1().start();
new T2().start();;
}
}
上述代码执行时,t1被唤醒后并不能立即执行,因为t2线程sleep了2秒,在这2s内t2并未释放object的监视器,所以在t2线程sleep了2s后,才会输出t1 end
。
这三个方法必须在synchronized代码块中执行,因为这些操作都和监视器相关,wait必须要知道获取谁的监视器,而notify需要知道去唤醒等待在哪里的线程,而synchronized可以提供这个监视器。
监视器(monitor)和锁(lock)的关系:
在jvm中,锁的实现方式就是monitor;
entermonitor就是获得某个对象的lock(owner是当前线程);
leavemonitor就是释放某个对象的lock。
简单的认为,在object中,monitor就是lock。
线程挂起和继续
线程的挂起和继续执行的方法分别是suspend()
和resume()
,被挂起的线程,必须要等到resume()操作后才能继续执行。但这两个方法已经被废弃了。
因为suspend()在导致线程暂停的同时,并不会去释放任何锁资源。此时,其他任何线程想要访问被它占用的锁时,都会被阻塞。直到对应的线程上进行了resume()操作,被挂起的线程才能继续。但是,如果resume()操作意外地在suspend()前就执行了,那么被挂起的线程可能很难有机会被继续执行。并且它所占用的锁也不会被释放。
而且,对于被挂起的线程,从它的线程状态上看,居然还是Runnable,这也会严重影响我们对系统当前的判断。
线程join和yield
join和yield的部分源码:
public class Thread implements Runnable {
/**
* Waits for this thread to die.
*/
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.
*/
public final synchronized void join(long millis)
throws InterruptedException {
...
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
...
wait(delay);
...
}
}
}
/**
* 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.
*/
public static native void yield();
}
join
当前线程调用其他线程的join方法,会阻塞当前线程,直到其他线程执行完毕,才会继续执行。
join()表示无限期的等待,而join(long millis)则指定等待的最大时间,如果超过最大时间则不再等待该线程,继续执行。
从源码中可以发现,join()方法是通过wait()实现的,而join(long millis)则是通过wait(long timeout)实现的。当millis为0 时,会进入while(isAlive())循环,并且只要子线程是活跃的,宿主线程就不停的等待。
join方法会让宿主线程交出CPU执行权,并放弃占有的锁。
yield
调用yield()会让当前线程交出CPU资源,但是交出CPU资源后,该线程仍然会参与争夺下一轮CPU的使用权。但是,yield()不能控制具体的交出CPU的时间。
调用yield()方法并不会让线程进入阻塞状态,也不会释放锁,而是让线程重回就绪状态,它只需要等待重新得到CPU的执行权就又能继续执行了。