多线程编程基础
1.进程与线程
- 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
- 线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程]之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
2.线程的状态
- New:创建状态。线程被创建,但未调用 .start() 方法。
- Runnable:可运行状态。新创建的线程调用 .start() 方法后可运行状态,但是否运行,还取决于操作系统分配给该线程的时间片。
- Blocked:阻塞状态。线程调用同步方法时,未获得锁时,进入阻塞状态。
- Waiting:等待状态。从等待状态恢复,需要线程调度器的激活。
- Timed waiting:超时等待状态。可在指定时间后恢复。
- Terminated:终止状态。进入终止状态的两种情况:1.run方法执行完毕正常退出;2.未捕获的异常导致线程终止。
3.多线程实现的三种方法
3.1 继承Thread类
- TestThread.class
package thread;
public class TestThread extends Thread {
@Override
public void run() {
System.out.println("TestThread!");
}
}
- Main.class
import thread.*;
public class Main {
public static void main(String[] args) {
Thread thread = new TestThread();
thread.start();
}
}
3.2 实现Runnable接口
- TestRunnable.class
package thread;
public class TestRunnable implements Runnable {
@Override
public void run() {
System.out.println("TestRunnable!");
}
}
- Main.class
import thread.*;
public class Main {
public static void main(String[] args) {
TestRunnable testRunnable = new TestRunnable();
Thread thread = new Thread(testRunnable);
thread.start();
}
}
3.3 实现Callable接口
- Callable接口属于Executor框架中的功能类,比Runnable更强大。
- 实现Callable接口,重写**call()**方法。
- Callable可以在任务接受后提供一个返回值。
- Callable中的call()方法可以抛出异常。
- 运行Callable可以获得一个Future对象。Future对象表示异步计算的结果,提供检查计算是否完成的方法。
- TestCallable.class
package thread;
import java.util.concurrent.Callable;
public class TestCallable implements Callable {
@Override
public String call() throws Exception {
System.out.println("TestCallable!");
return "success";
}
}
- Main.class
import thread.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) {
TestCallable testCallable = new TestCallable();
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(testCallable);
try {
//调用.get()方法时会阻塞当前线程,直到call()方法返回结果。
System.out.println(future.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.中断
4.1 interrupt
- 中断线程并非终止线程。中断线程目的是引起线程注意,由被中断的线程决定如何响应中断。重要的线程一般不理会中断,大多情况下普通线程会将中断作为一个终止的请求。
- 使用 interrupt 方法来请求中断线程。
- 当一个线程调用 interrupt 方法时,线程的中断标识位被置为 true 。线程会周期性检查这个中断标识位,可以使用 Thread.currentThread().isInterrupt() 判断线程是否被中断。
while(!Thread.currentThread().isInterrupt()){
//中断后的响应
}
4.2 被阻塞线程下的中断
- 如果一个线程被阻塞,线程在检查中断标识位时如果发现为 true ,会在阻塞方法调用处抛出 InterruptedException 异常,且在异常抛出前,将线程中的中断标识位复位为 false 。
- 在底层捕获 InterruptedException 异常后一般两种处理方式:
/**
* 方式一:
* 将标识位再次置为true。
* 外界通过Thread.currentThread().isInterrupt()判断后继续操作。
*/
void task(){
...
try{
sleep(1000);
}catch(InterruptedException e){
Thread.currentThread().interrupt();
}
...
}
/**
* 方式二:
* 直接抛出让调用者处理这个异常。
*/
void task() throw InterruptedException{
...
sleep(1000);
...
}
4.3 安全地终止线程
- 通过中断方式控制线程终止。
- TestRunnable.class
package thread;
public class TestRunnable implements Runnable {
private long i;
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
i++;
System.out.println("i=" + i);
}
System.out.println("STOP!");
}
}
- Main.class
import thread.*;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) throws InterruptedException {
TestRunnable testRunnable=new TestRunnable();
Thread thread = new Thread(testRunnable,"testRunnable");
thread.start();
TimeUnit.MILLISECONDS.sleep(10);
thread.interrupt();
}
}
- 通过boolean方式控制线程终止。
- TestRunnable.class
package thread;
public class TestRunnable implements Runnable {
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on) {
i++;
System.out.println("i=" + i);
}
System.out.println("STOP!");
}
public void cancel() {
on = false;
}
}
- Main.class
import thread.*;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) throws InterruptedException {
TestRunnable testRunnable=new TestRunnable();
Thread thread = new Thread(testRunnable,"testRunnable");
thread.start();
TimeUnit.MILLISECONDS.sleep(10);
testRunnable.cancel();
}
}
5.同步
5.1 重入锁
- 重入锁 ReentrantLock 支持一个线程对资源的重复加锁。
Lock mLock = new ReentrantLock();
mLock.lock();
try{
...
}
finally{
mLock.unlock();
}
- 条件对象又叫条件变量。
- 可以使用条件对象来管理那些获得了锁,但是因为条件限制不能完整执行完流程的线程。
private Lock mLock;
private Condition mCondition;
...
//实例化
mLock = new ReentrantLock();
mCondition = mLock.newCondition();
...
//在阻塞地方
while(条件){
...
mCondition.await();
}
...
//在激活的地方
mCondition.signalAll();
...
- .signalAll 方法会唤醒因这一条件对象被阻塞的所有线程,然后这些线程再通过竞争实现对对象的访问。
- 一般都是使用 while( ) 条件语句对流程控制。
5.2 同步方法
- Java中每一个对象都有一个内部锁,当一个方法使用 synchronized 关键字声明,那么该对象的锁将保护整个方法。
- synchronized 关键字声明的方法含有1个相关条件。wait() 方法等价于 condition.await()、notifyAll() 方法等价于 condition.notifyAll() 等。
public synchronized void method(){
...
}
//等价于
Lock mLock = new ReentrantLock();
public void method(){
mLock.lock();
try{
...
}finally{
mLock.unlock();
}
}
5.3 同步代码块
- 不推荐。同步代码块比较脆弱。
- 一般实现同步最好使用 java.util.concurrent 包下提供的类,比如阻塞队列。
synchronized(obj){
...
}
5.4 一些概念
Java 内存模型:
- Java中的堆内存用来存储对象实例,堆内存是被所有线程共享的运行时内存区域,存在内存可见性问题。
- 局部变量、方法定义的参数不会再线程间共享,则不会受到内存模型的影响。
- 线程之间的共享变量存储在主存之中,每个线程都有一个私有的本地内存,本地内存中存储了该线程共享变量的副本。(本地内存是抽象概念,并不存在,实际涵盖缓存、写缓冲区、寄存器等区域)
- Java内存模型控制线程之间的通信,决定一个线程对主存共享变量的写入何时对另一个线程可见。
A线程与B线程通信:
- 线程A把线程A本地内存中更新过的共享变量刷新到主存中去。
- 线程B到主存中读取线程A更新过的共享变量。
- 原子性:对基本数据类型变量的读取和赋值操作是原子性操作,是不可中断的。
- java.util.concurrent.atomic 包中存在很多高效的机器级指令,其操作具备原子性。
- 可见性:一个线程的修改的状态对另一个线程是可见的。volatile 修饰的共享变量会被立即更新到主存中,具备可见性。普通共享变量变量被修改,并不会被立即写入内存,写入时间不确定,无法保证可见性。
- 有序性:Java内存模型允许编译器和处理器对指令进行重排序。重排序不会影响单线程程序执行,但会影响多线程并发执行的正确性。可以通过 volatile 、synchronized、Lock 来保证有序性。
5.5 volatile
为读写一个或者两个实例域使用同步方法开销过大;volatile 关键字为实例域的同步访问提供了免锁机置。
禁止使用指令重排序。即线程修改共享变量的值之后,立即更新到主存。
volatile 保证有序性,但是不保证原子性。
使用 volatile 关键字需要具备的两个条件:
- 对变量的操作不会依赖于当前值。
- 该变量没有包含在具有其他变量的的不变式中。
volatile 使用场景举例。
//状态标志
volatile boolean stop;
...
public void shutdown(){
stop = true;
}
public void doWork(){
while(!stop){
...
}
}
//双重检查模式(DLC)
public class Singleton{
private volatile static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
6. 阻塞队列
6.1 阻塞队列简介
当队列中没有数据,消费者端的所有线程都会被阻塞,直到有数据放入队列。
当队列中填满数据,生产者端的所有线程都会被阻塞,直到队列中存在空位。
阻塞队列中的核心方法:
- 存数据:
- offer(E e):如果可以,将e添加到阻塞队列中。返回值boolean。不阻塞当前执行方法的线程。
- offer(E e, long timeout, TimeUnit unit):在指定时间内不能加入阻塞队列返回false。
- put(E e):如果阻塞队列满了,则阻塞调用方法的线程,直到阻塞队列有空间再唤醒。
- 取数据:
- poll(long timeout, TimeUnit unit):在指定时间内取队首元素。
- take():取队首元素,取不到则阻塞调用方法的线程,直到新元素添加到队列中。
- drainTo(Collection<? super E> c):一次性取出队列中存在的所有元素。
6.2 Java中的阻塞队列
ArrayBlockingQueue:
- 数组实现的有界阻塞队列。
- FIFO
- 默认不保证线程公平地访问队列。
LinkedBlockingQueue:
- 链表实现的有界阻塞队列。
- FIFO
- 对于生产者端和消费者端分别采用了独立的锁来控制数据同步。(高效)
- 默认大小(Integer.MAX_VALUE),注意可能会占满内存。
PriorityBlockingQueue:
- 支持优先级排序的无界阻塞队列。
- 可自定义实现 **compareTo()**方法排序。
- 可初始化时指定构造参数 Comparator 排序。
- 不能保证同优先级元素的顺序。
DelayQueue:
- 支持延时获取元素的无界阻塞队列。
- 通过PriorityQueue实现。
- 创建元素时可指定到期时间,到期后才可以取走该元素。
SynchronousQueue:
- 不存储元素的阻塞队列。
- 每个插入操作必须等待另一个线程的移除操作。
- 每个移除操作必须等待另一个线程的插入操作。
LinkedTransferQueue:
链表结构的无界阻塞TransferQueue队列。
实现了TransferQueue接口。
部分重要方法:
- transfer(E e):若当前存在一个正在等待的消费者线程,则立刻将元素传递给消费者;如果没有消费者等待,将元素插到队尾,并进入阻塞状态,直到有消费者线程取走该元素。
- tryTransfer(E e):若当前存在一个正在等待的消费者线程,则立刻将元素传递给消费者;如果没有消费者等待,返回false,不进入队列。
- tryTransfer(E e, long timeout, TimeUnit unit):在 tryTransfer(E e) 方法基础上增加了等待时间,超时后返回false。
LinkedBlockingDeque:
- 链表实现的双向阻塞队列。
- 支持双端插入和移除操作,减少了一半的竞争。
7.线程池
7.1 Java线程池
- 多线程任务提交给 Runnable / Callable 。
- Executor 框架用来处理多线程任务,其中核心部分 ThreadPollExecutor 是线程池的核心实现类。
7.2 ThreadPollExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...
}
corePoolSize:核心线程数。默认情况下当任务提交时才创建前程;调用 prestartAllCoreThreads 方法,会提前创建并启动所有的核心线程来等待任务。
maximumPoolSize:线程池允许创建的最大线程数。
keepAliveTime:非核心线程闲置超时时间。任务多且短时适度增大keepAliveTime会提高线程利用率。**allowCoreThreadTimeOut **属性值为 true 时,keepAliveTime 也会应用到核心线程上。
unit:时间单位。
workQueue:任务队列。
threadFactory:线程工厂。可为线程设置名字。
handler:饱和策略。
- 默认 AbordPolicy。
- AbordPolicy:抛弃任务,并抛出 RejectedExecutionException 异常。
- DiscardPolicy:抛弃任务,不做任何动作。
- CallerRunsPolicy:用调用者线程来处理任务。该策略提供简单的反馈控制,能够减缓新任务的提交速度。
- DiscardOldestPolicy:丢弃队列第一个任务。
7.3 线程池的处理流程和原理
- 线程池的处理流程图:
- 线程池执行示意图:
7.4 线程池种类
- 4种常见的线程池:newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、newScheduledThreadPool。
7.4.1 newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- 可重用固定线程数的线程池。
- 只有固定的核心线程数,没有非核心线程。
- LinkedBlockingQueue:默认 Integer.MAX_VALUE。
7.4.2 newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- 根据需要创建线程的线程池。
- 没有核心线程,非核心线程是无界的。
- SynchronousQueue。
- 适用于大量的需要立即处理且耗时较短的任务。
7.4.3 newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- 使用单个工作线程的线程池。
- LinkedBlockingQueue。
7.3.4 newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
能够定时和周期性执行任务的线程池。
当执行newScheduledThreadPool 的 scheduleAtFixedRate或scheduleWithFixedDelay方法时,会向DelayedWorkQueue添加一个实现 RunnableScheduledFuture 接口的ScheduledFutureTask(任务包装类),并检查运行的线程是否达到 corePoolSize。
- 没有达到核心线程数:新建线程并启动线程,然后去DelayedWorkQueue中取ScheduledFutureTask,然后再去执行任务。(而非直接启动线程后执行任务)
- 达到核心线程数:将任务添加到DelayedWorkQueue中。
DelayedWorkQueue会将任务进项排序,先要执行的任务放在队列前。
当任务执行完,会将ScheduledFutureTask中的time变量改为下次要执行的时间并放回到DelayedWorkQueue中。
8.参考资料
- Android 进阶之光