1、基础梳理
- 进程和线程。1). 进程是操作系统正在执行的不同应用程序的一个实例,线程是操作系统分配处理器时间的基本单元。2). 每个进程运行在自己的地址空间,而线程共享数据内存和 IO 这些资源。
- 线程的优缺点。优点:1). 程序的运行效率可能会更高;2). 可以使用线程把占用时间较长的任务放在后台去执行;3). 在一些等待耗时任务和交互事件的时候同时可以执行其他任务。缺点:1).如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换;2).更多的线程需要更多的内存空间;3).通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生。
2、使用线程
2.1 使用 Thread 创建单线程
Java 中提供了 Thread 类用来创建线程。当我们调用 Thread 的 start()
方法的之后,Thread 的 run()
方法将会被调用。默认的,Thread 的 run()
方法中会调用传入的 Runnable 的 run()
方法执行任务,我们也可以覆写 Thread 的 run()
方法来让它直接执行我们的业务逻辑。所以,可以使用下面的两种方式使用线程,
/**
* 方式 1:覆写 run() 方法使用线程
* 按照下面的方式来启动线程即可,
* MyThread myThread = new MyThread();
* myThread.start();
*/
private class MyThread extends Thread {
@Override
public void run() {
// 业务逻辑
}
}
/**
* 方式 2:使用 Runnable 使用线程
*/
new Thread(new Runnable() {
public void run() {
// 业务逻辑
}
}).start();
对第二种方式,在创建多个线程的情况下,如果传入的 Runnable 实例是同一个实例的话,那么这几个线程是共享这个实例的数据的;如果不是同一个实例,则每个线程有一份自己的数据。当共享实例的时候,有时需要引入同步机制。
2.2 使用 Executor 创建线程池
2.2.1 线程池的基本使用
使用线程池的好处在于:因为线程的创建和销毁会占有一定的资源开销,尤其是当线程需要执行的逻辑耗时比较短,而创建和销毁的时间占用比较长的时候,对每个任务都创建和销毁线程就不太划算了。而线程池为我们提供了一种复用线程的机制,我们可以只创建执行数量的线程,然后将任务不断地提交到线程池中执行。
Executors 类有许多静态工程方法可以用来构建线程池:
-
newCachedThreadPool()
:对每个任务,如果有空闲线程,立即让它执行任务,若无,则创建新线程; -
newFixedThreadPool()
:构建一个具有固定大小的线程池,若提交的任务数目大于空闲线程,得不到服务的任务放在队列中,执行完其他任务再执行这些任务; -
newSingleThreadPool()
:大小为1的线程池,提交的任务会按照提交的顺序依次地被执行(执行完毕一个,才去执行另一个); -
newWorkStealingPool()
:具有Work-Stealing (工作窃取) 的能力的线程池,Java8 中引入的,基于分治思想,Java8 中的并行流就是基于它来实现的。
使用线程池的时候,除了像线程那样启动任何,还可以获取到线程执行的结果。ExecutorService 的 submit()
方法提供了多个重载的版本,我们可以将 Runnable 或者 Callable 作为任务来执行。对于 Callable 类型的任务,该方法定义如下,
<T> Future<T> submit(Callable<T> task)
也就是说,我们可以通过方法的返回结果得到一个 Future 实例。我们可以使用这个实例的方法来获取任务的执行结果,只是它的方法都是阻塞的。因此,我们又有了第三种使用线程的方式,
// 方式 3:使用线程池并获取线程的执行结果
// 定义固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 定义一个列表用来存储 submit 的返回实例
List<Future<Integer>> results = new ArrayList<Future<Integer>>();
for (int i=0; i<5; i++) {
results.add(executor.submit(new CallableTask(i, i)));
}
// 输出每个任务的执行结果
for (Future<Integer> result : results) {
try {
// 线程将会在 get() 方法上面阻塞
System.out.println(result.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
此外,也可以使用 Future 的 isDone()
方法来判断任务是否完成,并决定是否要调用 get()
方法。
另外,还有一个类叫做 FutureTask。它同时实现了 Future 和 Runnable,所以,它设计的目的在于让我们可以同时从 FutureTask 实例中设置任务并获取结果。FutureTask 提供了两个构造方法,分别接受一个 Callable 和 Runnable 类型的实例。所以,我们可以把自己的任务放进 FutureTask 中包装了之后再传递给 ExectorService 或者 Thread 来执行。
2.2.2 线程池的参数选择
上面我们使用的是线程池的默认实现,也就是使用 Executors 的静态方法提供的线程池。这种方式是没有问题的,但是当我们使用阿里的插件的时候,会发现阿里并不推荐我们这样使用,而是使用手动创建线程池的方式。所以,在创建线程池的时候,我们有几个参数需要决定,
以 Android 为例,它并没有明确规定可以创建的线程的数量,但是每个进程的资源是有限的,线程本身会占有一定的资源,所以受内存大小的限制,会有数量的上限。通常,我们在使用线程或者线程池的时候,不会创建太多的线程。线程池的大小经验值应该这样设置:(其中 N 为 CPU 的核数)
- 如果是 CPU 密集型应用,则线程池大小设置为
N + 1
;(大部分时间在计算) - 如果是 IO 密集型应用,则线程池大小设置为
2N + 1
;(大部分时间在读写,Android)
下面是 Android 中的 AysncTask 中创建线程池的代码(一些参数的意义已经添加到了注释种),
// CPU 的数量
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// 核心线程的数量:只有提交任务的时候才会创建线程,当当前线程数量小于核心线程数量,新添加任务的时候,会创建新线程来执行任务
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
// 线程池允许创建的最大线程数量:当任务队列满了,并且当前线程数量小于最大线程数量,则会创建新线程来执行任务
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
// 非核心线程的闲置的超市时间:超过这个时间,线程将被回收,如果任务多且执行时间短,应设置一个较大的值
private static final int KEEP_ALIVE_SECONDS = 30;
// 线程工厂:自定义创建线程的策略,比如定义一个名字
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
// 任务队列:如果当前线程的数量大于核心线程数量,就将任务添加到这个队列中
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
public static final Executor THREAD_POOL_EXECUTOR;
static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
/*corePoolSize=*/ CORE_POOL_SIZE,
/*maximumPoolSize=*/ MAXIMUM_POOL_SIZE,
/*keepAliveTime=*/ KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
/*workQueue=*/ sPoolWorkQueue,
/*threadFactory=*/ sThreadFactory
/*handler*/ defaultHandler); // 饱和策略:AysncTask 没有这个参数
threadPoolExecutor.allowCoreThreadTimeOut(true);
THREAD_POOL_EXECUTOR = threadPoolExecutor;
}
饱和策略:任务队列和线程池都满了的时候执行的逻辑,Java 提供了 4 种实现;
其他:
- 当调用了线程池的
prestartAllcoreThread()
方法的时候,线程池会提前启动并创建所有核心线程来等待任务; - 当调用了线程池的
allowCoreThreadTimeOut()
方法的时候,超时时间到了之后,闲置的核心线程也会被移除。
3、线程状态和生命周期
3.1 线程的状态
[图片上传失败...(image-fa054e-1554730026011)]
Java中的线程的生命周期大体可分为 5 种状态:
新建 (NEW):新创建了一个线程对象。
可运行 (RUNNABLE):线程对象创建后,其他线程(比如 main 线程)调用了该对象的
start()
方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 CPU 的使用权 。运行 (RUNNING):RUNNABLE 状态的线程获得了 CPU 时间片(timeslice) ,执行程序代码。
-
阻塞 (BLOCKED):阻塞状态是指线程因为某种原因放弃了 CPU 使用权,也即让出了 CPU timeslice,暂时停止运行。直到线程进入 RUNNABLE 状态,才有机会再次获得 CPU timeslice 转到 RUNNING 状态。阻塞的情况分三种:
等待阻塞:RUNNING 的线程执行
o.wait()
方法,JVM 会把该线程放入等待队列 (waitting queue) 中。同步阻塞:RUNNING 的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池 (lock pool) 中。
其他阻塞:RUNNING 的线程执行
Thread.sleep(long)
或t.join()
方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当sleep()
状态超时、join()
等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入 RUNNABLE 状态。
死亡 (DEAD):线程
run()
、main()
方法执行结束,或者因异常退出了run()
方法,则该线程结束生命周期。死亡的线程不可再次复生。
线程的 wait()
和 notify()
调度原理:
[图片上传失败...(image-7951c6-1554730026011)]
3.2 线程的启动 start()、停止 stop()、挂起 suspend() 和唤醒 resume()
通过对象的 start(), stop(), suspend(), resume()
方法可以分别用来启动/停止/挂起/继续线程,但是后面三种方法都已经过时,调用可能发生不可预料的结果。如果线程被停止或者挂起的时候,它仍然占有共享的资源,那么有可能会导致线程死锁。
说明:调用 start()
方法,线程处于 runnable,线程可运行,但是无法确定线程是否正在运行,这取决于操作系统提供的运行时间。
run()
方法执行结束之后,线程自动终止;如果 run()
无限循环,可以考虑加加标识,在一定情况下退出,不推荐使用 stop()
方法。另外,如果线程终止了,将无法再次启动。
一个线程会结束的原因可能是下面两者之一:1).run()
方法正常退出而线程自然地死亡;2).一个没有被捕获的异常终止了 run()
方法而意外地死亡。
3.3 线程休眠 Thread.sleep()
静态方法 Thread.sleep(long millis)
和 Thread.sleep(long millis, int nanos)
强行将当前线程休眠(暂停执行)指定时间,睡眠结束,即返回可运行状态。
此外,也可以使用 TimeUnit.MILLISECONDS.sleep(1000);
方法来实现线程的休眠。
注意:
- 一个线程不能针对另一个线程调用
Thread.sleep()
,即一个线程只能让自己睡眠; -
sleep()
方法会抛出一个 InterruptedException 异常。 - 如果当前线程获得了锁,sleep() 方法并不会使其失去锁。
另外,对于 sleep()
和 wait()
方法之间的区别,总结如下,
- 所属类不同:
sleep()
方法是 Thread 的静态方法,而wait()
是 Object 实例方法。 - 作用域不同:
wait()
方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()
方法没有这个限制可以在任何地方种使用。 - 锁占用不同:
wait()
方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()
方法只是会让出 CPU 并不会释放掉对象锁; - 锁释放不同:
sleep()
方法在休眠时间达到后如果再次获得 CPU 时间片就会继续执行,而wait()
方法必须等待Object.notift()/Object.notifyAll()
通知后,才会离开等待池,并且再次获得 CPU 时间片才会继续执行。
3.4 线程优先级 setPriority()
优先级使用正整数试着,通常为 0~10,默认为 5. Thread类中也定义了 3 个静态最终常量:Thread.MIN_PRIORITY (对应整数值为 1)、Thread.NORM_PRIORITY (对应整数值为 5) 和 Thread.MAX_PRIORITY (对应整数值为 10).
线程是根据优先级调度执行的,尽管 CPU 处理现有的线程集的顺序是不确定的,但是调度器倾向于让优先权最高的线程先执行。这不意味着优先权低的程序得不到执行,只是执行的频率较低。
说明:
- 默认情况下,一个线程继承它父线程的优先级,可以使用
setPriority()
方法提高或降低一个线程的优先级; - 高优先级线程没有进入非活动状态,低优先级线程永远不可能执行。每当调用一个新线程时,首先会在具有高优先级的线程中选择。尽管这样可能会使低优先级线程完全饿死;
- 在绝大多数的时间里,线程都应该以默认的优先级运行,试图操纵线程优先级通常是一种错误。
- 在不同 JVM 以及操作系统上,线程规划存在差异,有些操作系统甚至会忽略线程优先级的设定。
3.5 让步 Thread.yield()
即暂停当前正在执行的线程对象,并执行其他线程。并非永久暂停,只是让步一次执行时间片。需要注意的是,让出的CPU并不是代表当前线程不再运行了,如果在下一次竞争中,又获得了 CPU 时间片当前线程依然会继续运行。另外,让出的时间片只会分配给当前线程相同优先级的线程。
yield()
是 Thread 的一个静态方法,它给线程调度机制一个暗示:当前线程(在 run()
方法中调用 yield()
方法的线程)的工作已经差不多了,可以让别的线程使用 CPU 了。
但是,大体上,对任何重要的控制或在调整应用时,都不能依赖于 yield()
.
另外需要注意的是,sleep()
和 yield()
方法,同样都是当前线程会交出处理器资源。它们不同的是,sleep()
交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片。而 yield()
方法只允许与当前线程具有相同优先级的线程能够获得释放出来的 CPU 时间片。
3.6 加入一个线程 join()
join()
是 Thread 的实例方法,它用来等待,直到指定线程结束。如果我们在线程 A 中调用了 B 的 join()
方法,就表示我们将 A 添加到了 B 的尾部,如果 B 不执行完 A 不继续执行。join()
重载版本,
void join() // 加入线程,等待该线程终止后运行
void join(long millis) // 加入线程,等待该线程 millis 后运行,0 为无限等待
void join(long millis, int nanos) // 加入线程,等待该线程 millis+nanos 后运行
线程的 join()
方法允许传入 long 型的时间,表示我们可以为线程设置等待的时间上限。当超过了指定的时间另一个线程仍然没有执行完毕任务,当前线程就继续执行自己的任务。否则,当前线程会一直阻塞。
注意,因为线程的 join()
方法的本意是等待另一份线程直到结束,所以,如果我们没有对指定的线程调用 start()
方法,那么 join()
是没有效果的(因为线程本来就没启动,所以也不用等待了)。
3.7 线程中断 interrupt()
线程中断需要注意两种情形,一个是未处于阻塞时期的中断,另一个是处于阻塞时期的中断
interrupt()
方法用来中断线程,而不是立即终止线程。对线程调用 interrupt()
方法时,线程的中断状态将被置位(设置为 true),这是每个线程都具有的 boolean 状态。如果想要知道一个线程是否被置位,可以使用 Thread.currentThread().isInterrupted()
来判断。
当线程由于调用了 sleep(), wait(), join()
等方法而进入阻塞状态;若此时调用线程的 interrupt()
将线程的中断标记设为 true。由于处于阻塞状态,中断标记会被清除,同时产生一个 InterruptedException 异常。
所以,当你希望让一个线程从阻塞状态中结束的时候,你可以按照下面这样去写,
@Override
public void run() {
try {
while (true) {
// do something
}
} catch (InterruptedException ie) {
// 由于 InterruptedException 异常,退出 while 循环,线程终止!
}
}
通常,我们把对 InterruptedException 的捕获务一般放在 while 循环体的外面,这样,在产生异常时就退出了 while 循环。
让线程结束,你还可以通过判断中断标志位来进行。此外,你还可以通过使用一个额外的布尔类型的变量来让线程退出。
// 通过判断中断标志位来退出
private static class MyRunnable implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
// do something
}
}
}
// 通过一个布尔类型的变量来退出
private static class MyRunnable2 implements Runnable {
// 注意使用 volatile 修饰
private volatile boolean canceled = false;
@Override
public void run() {
while (!canceled) {
// do something
}
}
public void cancel() {
canceled = true;
}
}
上面的第一种方式还没有考虑线程被阻塞的情况,所以,我们需要综合线程是否处于阻塞来给出一个更完美的版本,
@Override
public void run() {
try {
while (!isInterrupted()) {
// do something
}
} catch (InterruptedException ie) {
// 线程因为阻塞时被中断而结束了循环
}
}
最后,注意 interrupted() 和 isInterrupted() 的区别
interrupted()
是属于 Thread 的静态方法,isInterrupted()
是属于 Thread 的实例方法。interrupted()
和 isInterrupted()
都能够用于检测对象的 “中断标记”。区别是,interrupted()
除了返回中断标记之外,它还会清除中断标记 (即将中断标记设为false);而 isInterrupted()
仅仅返回中断标记。
public static boolean interrupted() {
return currentThread().isInterrupted(/* ClearInterrupted= */ true);
}
public boolean isInterrupted() {
return isInterrupted(/* ClearInterrupted= */ false);
}
3.8 后台线程
Java 线程分为两类:用户线程和 Daemon 线程。
- 用户线程是通常意义的线程,Java 应用程序运行时,通过
main()
方法进入. 在主线程中可以创建和启动新线程,默认为用户线程. 只有所有用户线程结束后,应用程序才终止。 - 通过
setDaemon()
方法,可以设置线程为 Daemon 线程,在 Daemon 线程中创建的线程默认为 Daemon 线程. 通过方法isDaemon()
可以判断一个线程是否为 Daemon 线程。 - Daemon 线程(守护线程)是一个服务线程,其 优先级最低,一般为其他线程提供服务. 通常 Daemon 线程体是一个无限循环,如果所有的非 Daemon 线程都结束了,则 Daemon 线程自动终止。
- Daemon 线程应该永远不访问固有资源,如文件、数据等,因为它会在任何时候,甚至任一个操作中间发生中断。
- Deamon 线程通常是系统服务类线程,比如垃圾回收线程,JIT线程就可以理解守护线程。
3.9 线程组
最好把线程组看作一次不好的尝试,忽略就好。
3.10 线程控制 wait()、notify() 和 notifyAll()
wait()、notify() 和 notifyAll()
方法是 Object 的本地 final 方法,无法被重写。wait()
使当前线程阻塞,直到接到通知或被中断为止。前提是必须先获得锁,一般配合 synchronized 关键字使用,在 synchronized 同步代码块里使用wait()、notify() 和 notifyAll()
方法。如果调用wait()
或者notify()
方法时,线程并未获取到锁的话,则会抛出 IllegalMonitorStateException 异常。再次获取到锁,当前线程才能从wait()
方法处成功返回。由于
wait()、notify() 和 notifyAll()
在 synchronized 代码块执行,说明当前线程一定是获取了锁的。当线程执行wait()
方法时候,会释放当前的锁,然后让出 CPU,进入等待状态。只有当notify()/notifyAll()
被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完 synchronized 代码块或是中途遇到wait()
,再次释放锁。
也就是说,notify()/notifyAll()
的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify()/notifyAll()
后立即退出临界区,以唤醒其他线程。wait()
需要被try catch
包围,中断也可以使wait
等待的线程唤醒。notify()
和wait()
的顺序不能错,如果 A 线程先执行notify()
方法,B 线程再执行wait()
方法,那么 B 线程是无法被唤醒的。notify()
和notifyAll()
的区别:
notify()
方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。
notifyAll()
会唤醒所有等待 (对象的) 线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll()
方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。