第四章 多线程编程
线程基础
- 进程与线程
- 进程:可以被看做是程序的实体, 是系统进行资源分配和调度的基本单位.
- 线程:是操作系统调度的最小单元, 也叫轻量级进程
- 使用多线程的优点
- 可以减少程序的响应时间。如果某个操作很耗时, 能够避免陷入长时间的等待, 从而有着更好的交互性.
- 线程较之进程, 创建和切换的开销更小, 在共享数据方面的效率非常高.
- 更高利用多CPU或多核设备的性能
- 简化程序结构, 便于理解维护.
- 线程的状态
- New
- Runnable(可运行状态, 不是立即运行, 取决于系统的决定)
- Blocked(当调用同步方法而未获取锁时会进入阻塞状态)
- Waiting(暂时不活动, 不运行任何代码, 消耗最少的资源)
- Timed waiting(与waiting相比, 能在指定的时间自行返回)
- Terminated
- 创建线程
创建线程的方法一般有三种:
继承
Thread类, 重写run方法(本质为实现Runnable接口的一个实例)
1.创建继承Thread类的子类, 并重写run方法(执行体).
2.创建子类的实例
3.调用实例对象的start
方法启动线程实现
Runnable接口, 并实现接口的run方法
1.自定义一个实现Runnable接口的类, 实现run方法
2.创建上面的类的对象, 并将其作为参数去创建一个Thread子类的实例.Thread mThread = new Thread(参数)
3.调用Thread.start().实现
Callable接口, 重写call方法
是Executor框架中的功能类, 与Runnable接口的功能类似, 但提供了比Runnable更强大的功能, 主要表现在以下3点:
1.任务结束后提供一个返回值,
2.call方法可以抛出异常
3.运行了Callback后可以得到一个Future对象, 该对象利用Future.get
方法监视目标线程调用call
方法的情况, 但会一直阻塞直到call方法返回结果
public class TestCallable{
//创建线程类
public String class MyTestCallable implements Callable{
public String call() throws Exception{
return "hello world";
}
}
public static void main(String[] args){
MyTestCallable mMyTestCallable = new MyTestCallable();
ExecutorService mExecutorService = Executors.newSingleThreadPool();
Future mfuture = mExecutorService.submit(mMyTestCallable);
try{
//等待线程结束, 返回结果
System.out.println(mfuture.get());
}catch(Exception e){
e.printStackTrace();
}
}
}
- 理解中断
中断作为一个终止的请求, 被中断的线程不一定会终止. 中断是为了引起线程的注意.
- 线程调用
interrupt
方法对中断标志位进行置位(true).- 线程会不断检测这个标志位, 判断线程是否应该被中断(比较重要的线程不会理会中断)
- 想要知道线程是否被置位可以调用
Thread.currentThread().isInterrupted()
- 若一个线程被阻塞, 则无法检测中断状态. (阻塞状态时, 线程如果发现中断标志位为true, 则会在阻断方法调用处抛出
InterruptedException
异常, 并将中断标志位复位(false))InterruptedException
异常时要进行处理
不能生吞中断
: 既不抛出异常, 也不进行处理(仅仅做记录也是不行的). 处理 InterruptedException
处理的方式有:
1.使用try-catch
来捕获异常. 在catch子句中调用Thread.currentThread.interrupt()
来设置中断状态(保留中断发生的证据), 让外界通过判断Thread.currentThread().isInterrupted()
来决定是否终止线程还是继续下去.
2.可以先在try-catch
进行一些处理, 然后再将异常抛出.
3.方法直接抛出异常, 让调用者捕获
- 安全的终止线程(利用中断的相关方法)
- 利用
thread.interrupt()
方法对中断置位来控制- 利用中断判断方法
Thread.currentThread().isInterrupted()
在run
方法中进行while
判断- 或者用volatile修饰的
boolean
型变量来控制
同步
- 同步
Java中的同步指的是通过人为的控制和调度,保证共享资源
的多线程访问
成为线程安全,来保证结果的准确。
- 关于
sleep
与wait
的不同点: 不同点参考的这里
sleep
方法没有释放锁,而wait
方法释放了锁,使得其他线程可以使用同步控制块或者方法。wait,notify和notifyAll
只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用sleep
必须捕获异常,而wait,notify和notifyAll
不需要捕获异常sleep
是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
- wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
- 重入锁(ReentrantLock)与条件对象
支持重进入的锁, 表示该锁支持一个
线程对锁本身
的重复加锁(其他的线程此时不能对该锁加锁)
- 加锁的次数要和解锁的次数要相同, 否则其他线程对此锁无法加锁
- 条件对象
condition
中提供了condition.await()
方法(进入阻塞状态并放弃锁)和condition.signalAll()
方法(将该条件对象上等待的线程唤醒, 解除阻塞, 这里说明一下并不是立即激活)- 重入锁看这里
1.可以实现公平锁(默认为非公平锁)
2.可响应中断
或者 更好的获取锁 限时等待的方法tryLock()
去解决死锁
- 上面链接中关于
condition
的使用
import java.util.concurrent.locks.*;
//对重入锁和条件对象的测试
public class HelloWorld {
static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
lock.lock();
new Thread(new SignalThread()).start();
System.out.println("主线程等待通知");
try {
System.out.println("主线程的前try");
condition.await(); //释放锁, 等唤醒后需要重新获取锁
System.out.println("主线程的后try");
} finally {
System.out.println("主线程的final");
lock.unlock();
}
System.out.println("主线程恢复运行");
}
static class SignalThread implements Runnable {
@Override
public void run() {
lock.lock();
try {
System.out.println("子线程的前try");
condition.signal();
System.out.println("子线程通知");
} finally {
System.out.println("子线程的final");
lock.unlock();
}
}
}
}
- 上面链接中关于
tryLock
的使用
public class ReentrantLockTest {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadDemo(lock1, lock2));//该线程先获取锁1,再获取锁2
Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//该线程先获取锁2,再获取锁1
thread.start();
thread1.start();
}
static class ThreadDemo implements Runnable {
Lock firstLock;
Lock secondLock;
public ThreadDemo(Lock firstLock, Lock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
while(!firstLock .tryLock()){
TimeUnit.MILLISECONDS.sleep(10);
}
while(!secondLock .tryLock()){
firstLock .unlock();
TimeUnit.MILLISECONDS.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName()+"正常结束!");
}
}
}
}
- 同步方法
- 使用
synchronized
关键字来编写, 与重入锁相似, synchronized关键字直接修饰方法
, 将整个方法进行保护.- 每一个对象是有内部锁的
- 直接使用
wait()
和notifyAll()
方法, 与condition作用相似.
- 同步代码块
主要是使用对象的锁. 同步代码块非常脆弱, 通常不建议使用.
private Object lock = new Object();
synchronized(lock){
//执行相关操作
}
- volatile
下面1和2是基本概念
- java内存模型
1.[内存模型的图]
2.堆内存是被所有线程共享的运行时内存区域. java内存模型定义了线程和主存之间的抽象关系: 线程之间的共享变量存储在主存中, 每个线程都有一个私有的本地内存(实际不存在), 本地内存中存储了该线程共享变量的副本.- 原子性, 可见性, 有序性
1.原子性: 原子性操作是不可被中断的, 要么执行完毕, 要么就不执行. 对基本数据类型的读取和赋值操作就是原子性操作. 而如果一个语句有多个操作时, 就不是原子性操作.
2.可见性: 一个线程修改的结果, 另一个线程能够马上看到. 普通的共享变量不能保证可见性, 因为普通的共享变量被修改后不会被立即写入主存. 而当一个共享变量被volatile修饰时, 他会保证修改的值立即被更新到主存.
3.有序性: java内存模型中允许编译器和处理器对指令进行重排序(分为编译期重排序和运行时重排序, 不会影响单线程执行的正确性, 优化程序性能), 但会影响到多线程并发执行的正确性.- volatile关键字
1.保证可见性,禁止重排序.
当一个共享变量被volatile修饰后, 具有两种含义: 一, 该共享变量具有可见性(强制一个线程中被修饰的值发生修改后立即
写入主存, 并使其他
线程工作缓存中该值的缓存无效
. 防止线程因做其他的事情没把值写入主存), 二, 禁止使用重排序.
2.不保证原子性
3.保证有序性
禁止重排序有两重含义: 一, 程序执行到volatile时, 在其前面的操作已经全部执行完毕, 并且结果会对后面的操作可见, 后面的操作都还没有执行. 二, 在进行指令优化时, volatile前面的语句不能在它后面zhixing, 后面的语句也不能在它前面执行- 正确使用volatile
volatile在某些情况下性能优于synchronized, 但是无法替代, 因为volatile无法保证操作的原子性.
1.使用volatile需要严格遵循两个使用条件, 一, 即变量真正独立于(不能使用)其他变量和自己以前的值, 即不能自增, 自减, 因为不能保证原子性. 二, 没有包含在其他变量的不变式中(两个线程都会通过用于保护不变式的检查导致错误), 比如 [0, 10], 一个线程修改最大值为5, 同时另一个线程修改最小值为6, 此时区间就变成了[6, 5]. 很明显就错了
2.使用场景
1.状态标志, 使用volatile修饰Boolean类型的变量, 不依赖于程序内的任何其他状态
2.双重检查锁定模式(DCL) 参考链接
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
阻塞队列
- 阻塞队列简介
- 阻塞队列就是生产者存放元素的容器, 常见的阻塞场景:
1.队列没有数据, 消费者端的所有线程都会自动挂起(阻塞), 直到有数据放入队列,线程被唤醒
2.队列中数据填满了, 生产者端的所有线程都会自动挂起, 直到队列中有空的位置, 线程被唤醒
- BlockingQueue的核心方法:
放入数据
1.offer(anObject): 如果阻塞队列可以容纳, 返回true, 否则返回false, 并且将anObject放入阻塞队列. (本方法不会阻塞当前执行方法的线程)
2.offer(E, long, TimeUnit): 在指定时间内如果不能往队列中添加, 则返回false
3.put(anObject): 如果阻塞队列没有空间, 则调用此方法的线程被阻断, 直到有空间再继续.
获取数据
1.poll(time): 在等待的时间内都不能取到排在队列首位的对象就返回null.
2.poll(long, TimeUnit): 在指定时间内不能取出数据, 返回false
3.take(): 为空则阻断
4.drainTo(): 取出所有的可用数据对象(还可以指定个数), 可以提高数据获取效率, 无需多次分批加锁. 释放锁
- java中的阻塞队列(P205)
- ArrayBlockingQueue: 由数据结构组成的有界阻塞队列
- LinkedBlockingQueue: 由链表结构组成的有界阻塞队列
- PriorityBlockingQueue: 支持优先级排序的无界阻塞队列
- DelayQueue: 使用优先级队列实现的无界阻塞队列
- SynchronousQueue: 不储存元素的阻塞队列
- LinkedTransferQueue: 由链表结构组成的无界阻塞队列
- LinkedBlockingQueue: 由链表结构组成的双向阻塞队列
- 实现原理
其实就是利用了可重入锁和条件对象, 相关可以看上面condition
的使用示例, 然后挑一个阻塞队列源码看看就可以.
- 使用场景
- 生产者-消费者模式
生产者-消费者模式可以使用阻塞队列实现(更为便捷), 或者也可以使用同步方法(Synchronized)实现, 自己处理好同步和线程通信问题,- 线程池
线程池
每个线程的创建和销毁都需要一定的开销, 如果每次执行一个任务都要开一个新线程去执行, 会消耗大量的资源. 同时线程又需要进行管理. 所以此时线程池就有存在性了.
一般任务的提交是Runnable或者Callable, 而用Executor框架来处理任务. 线程池的核心实现类就是Executor框架中的ThreadPoolExecutor
.
- ThreadPoolExcutor
拥有四个构造方法, 最长的构造方法有以下参数
corePoolSize
: 核心线程数. 默认情况下线程池是空的, 只有任务提交时才会创建线程. 如果调用线程池的prestartAllcoreThread方法, 线程池会提前创建并启动所有的核心线程来等待任务.maximumPoolSize
: 线程池允许创建的最大线程数.keepAliveTime
: 非核心线程闲置的超时时间, 超过这个时间则回收. 如果任务很多, 并且每个任务执行的时间很短, 则可以调大这个来提高线程的利用率.TimeUnit
: 上一个参数的时间单位, 有DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS等.workQueue
: 任务队列. 类型为BlockingQueue.ThreadFactory
: 线程工厂. 可以给每个创建出来的线程命名, 一般无需设置该参数.RejectedExecutionHandler
: 饱和后策略.
1.默认为AbordPolicy
: 表示无法处理新任务, 并抛出RejectedExecutionException异常
2.CallerRunsPolicy
: 调用者所在的线程来处理任务, 能够减缓新任务的提交速度.
3.DiscardPolicy
: 不能执行的任务, 会将该任务删除
4.DiscardOldestPolicy
: 丢弃队列最近的任务, 并执行当前的任务
- 线程池的处理流程
- 执行流程: 执行execute后, 未达到核心线程数, 就创建核心线程数处理任务. 达到就将任务放入任务队列, 如果任务队列也满了, 就创建非核心线程去处理任务, 当超过了最大线程数时, 此时执行饱和策略.
- 线程池的种类
通过直接或者间接的配置ThreadPoolExecutor的参数可以创建不同类型的线程池.比较常用的有四种
- FixedThreadPool: 是可重用固定线程数的线程池。只有核心线程, 核心线程数量固定并且不会被回收. 内部采用了无界的阻塞队列, LinkedBlockingQueue. 函数为
ExecutorService newFixedThreadPool(int 核心线程数)
- CachedThreadPool: 是一个根据需要创建线程的线程池。没有核心线程, 非核心线程无界, 提交的任务如果大于线程池中线程处理现成的速度, 就会不断地创建新线程, 等待任务最长时长为60s, 采用不储存元素的SynchronousQueue. 比较适合大量需要立即处理并且耗时比较少的任务. 函数为
ExecutorService newCacheThreadPool()
- SingleThreadExecutor: 是使用单个工作线程的线程池,只有一个核心线程, 采用了无界阻塞队列LinkedBlockingQueue, 能够确保所有的任务在一个线程中按顺序逐一执行. 函数为
ExecutorService newSingleThreadExecutor():
- ScheduledThreadPool: 是一个能实现定时和周期性任务的线程池. 有核心线程数, 队列为无界的DelayWorkQueue, 该队列会对任务进行排序, 未达到核心线程时会启动核心线程, 该线程会去取该队列中的
ScheduledFutureTask
然后执行, 执行完任务后, 会将ScheduledFutureTask
中time变量改为下次要执行的时间并放回到队列中.函数为ScheduledExecutorService newScheduledThreadPool(int 核心线程数)
AsyncTask的原理
- 基础使用在之前写的里面提到过 AsyncTask的使用
- 源码分析
- Android3.0之前的AsyncTask内部使用了ThreadPoolExecutor, 核心线程5个, 线程池允许创建的最大线程数是128, 队列大小为10, 所以最多支持138个任务. 等待新任务的最长时间是1s, 可并行.
- Android7.0版本的AsyncTask内部有一个串行的线程池
SerialExecutor
(保证一个时间段只有一个任务执行, 不会出现超过任务数而执行饱和策略), 后面会通过postResult
方法创建message, 最后处理后执行onPostExecute
方法, 得到异步任务执行后的结果. 其核心线程和线程池允许创建的最大线程数都是由CPU的核数来计算出来. 采用的阻塞队列还是LinkedBlockingQueue
, 容量设定为128.- 如果想在3.0 及以上使用并行的线程处理, 可以使用以下代码
asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, " ");
//也可以传入自定义的线程池
Executor exec = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
asyncTask.executeOnExecutor(exec, " ");