线程的6种状态
- New
创建后尚未启动的线程处于这种状态。
- Runnable
包括了操作系统线程状态中的 Running 和 Ready ,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。
- Blocked
阻塞状态是指线程因为某种原因放弃了cpu 使用权,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。
- 同步阻塞:运行(running)的线程进入了一个 synchronized 方法,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
- 其他阻塞:运行(running)的线程发出了I/O请求时,JVM会把该线程置为阻塞状态。当I/O处理完毕时,线程重新转入可运行(runnable)状态。
- Waiting
处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。
- Object.wait() 方法,并且没有使用 timeout 参数;
- Thread.join() 方法,没有使用 timeout 参数;
- LockSupport.park() 方法;
- Conditon.await() 方法。
- Timed Waiting
处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由操作系统自动唤醒。
- Thread.sleep() 方法;
- Object.wait() 方法,带有时间;
- Thread.join() 方法,带有时间;
- LockSupport.parkNanos() 方法,带有时间。
- Terminated
已终止的线程状态,线程已经结束执行。
线程池
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
public ThreadPoolExecutor(
int corePoolSize, //线程池核心线程数最大值
int maximumPoolSize, //线程池最大线程数大小
long keepAliveTime, // 线程池中非核心线程空闲存活时间大小
TimeUnit unit, //线程空闲存活时间单位
BlockingQueue<Runnable> workQueue, //存放任务的阻塞队列
ThreadFactory threadFactory, //用于设置创建线程的工厂
RejectedExecutionHandler handler) // 线城池的饱和策略事件,主要有四种类型。
newCachedThreadPool(可缓存线程的线程池)
- 核心线程数为0。
- 最大线程数为 Integer.MAX_VALUE。
- 阻塞队列是 SynchronousQueue。
- 非核心线程空闲存活时间为60秒。
newFixedThreadPool(固定数目线程的线程池)
- 核心线程数和最大线程数大小一样。
- 没有所谓的非空闲时间,即 keepAliveTime 为0。
- 阻塞队列为无界队列 LinkedBlockingQueue。
newScheduledThreadPool(定时及周期执行的线程池)
- 最大线程数为 Integer.MAX_VALUE。
- 阻塞队列是 DelayedWorkQueue。
- keepAliveTime 为0。
newSingleThreadExecutor(单线程的线程池)
- 核心线程数为1。
- 最大线程数也为1。
- 阻塞队列是 LinkedBlockingQueue。
- keepAliveTime 为0。
多线程常见问题
上下文切换
多线程并不一定是要在多核处理器才支持的,就算是单核也是可以支持多线程的。 CPU 通过给每个线程分配一定的时间片,由于时间非常短通常是几十毫秒,所以 CPU 可以不停的切换线程执行任务从而达到了多线程的效果。但是由于在线程切换的时候需要保存本次执行的信息,在该线程被 CPU 剥夺时间片后又再次运行恢复上次所保存的信息的过程就称为上下文切换。上下文切换是非常耗效率的。
通常有以下解决方案:
- 采用无锁编程,比如将数据按照 Hash(id) 进行取模分段,每个线程处理各自分段的数据,从而避免使用锁。
- 采用 CAS(compare and swap) 算法,如 Atomic 包就是采用 CAS 算法。
- 合理的创建线程,避免创建了一些线程但其中大部分都是处于 waiting 状态,因为每当从 waiting 状态切换到 running 状态都是一次上下文切换。
死锁
死锁的场景一般是:线程 A 和线程 B 都在互相等待对方释放锁,或者是其中某个线程在释放锁的时候出现异常如死循环之类的。这时就会导致系统不可用。
常用的解决方案如下:
- 尽量一个线程只获取一个锁。
- 一个线程只占用一个资源。
- 尝试使用定时锁,至少能保证锁最终会被释放。
资源限制
当在带宽有限的情况下一个线程下载某个资源需要 1M/S,当开 10 个线程时速度并不会乘 10 倍,反而还会增加时间,毕竟上下文切换比较耗时。如果是受限于资源的话可以采用集群来处理任务,不同的机器来处理不同的数据,就类似于开始提到的无锁编程。
多线程三大核心
- 原子性
Java 的原子性就和数据库事务的原子性差不多,一个操作中要么全部执行成功或者失败。
- 可见性
现代计算机中,由于 CPU 直接从主内存中读取数据的效率不高,所以都会对应的 CPU 高速缓存,先将主内存中的数据读取到缓存中,线程修改数据之后首先更新到缓存,之后才会更新到主内存。如果此时还没有将数据更新到主内存其他的线程此时来读取就是修改之前的数据。
volatile 关键字就是用于保证内存可见性,当线程A更新了 volatile 修饰的变量时,它会立即刷新到主线程,并且将其余缓存中该变量的值清空,导致其余线程只能去主内存读取最新值。使用 volatile 关键词修饰的变量每次读取都会得到最新的数据,不管哪个线程对这个变量的修改都会立即刷新到主内存。
synchronized 和加锁也能能保证可见性,实现原理就是在释放锁之前其余线程是访问不到这个共享变量的。但是和 volatile 相比开销较大。
- 顺序性
有时 JVM 为了提高整体的效率在保证最终结果和代码顺序执行结果一致的情况下会进行指令重排。重排在单线程中不会出现问题,但在多线程中会出现数据不一致的问题。
Java 中可以使用 volatile 来保证顺序性,synchronized 和 lock 也可以来保证有序性,和保证原子性的方式一样,通过同一段时间只能一个线程访问来实现的。
synchronized 关键字原理
synchronized 关键字是解决并发问题常用解决方案,有以下三种使用方式:
- 同步普通方法,锁的是当前对象。
- 同步静态方法,锁的是当前 Class 对象。
- 同步块,锁的是 () 中的对象。
实现原理: JVM 是通过进入、退出对象监视器(Monitor)来实现对方法、同步块的同步的。
具体实现是在编译之后在同步方法调用前加入一个 monitor.enter 指令,在退出方法和异常处插入 monitor.exit 的指令。其本质就是对一个对象监视器(Monitor)进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit 之后才能尝试继续获取锁。
private Object lock = new Object();
synchronized (lock) {
if / while () {
lock.wait();
}
lock.notifyAll();
}
锁优化
synchronized 很多都称之为重量锁,JDK1.6 中对 synchronized 进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁和轻量锁。
ReentrantLock
ReentrantLock 是一个普通的类,是基于 AQS(Java 并发包里实现锁、同步的一个重要的基础框架)来实现的。
synchronized 和 ReentrantLock 都是可重入锁(一个线程获得了锁之后仍然可以反复的加锁,不会出现自己阻塞自己的情况)。
ReentrantLock 分为公平锁和非公平锁:
- 公平锁就相当于买票,后来的人需要排到队尾依次买票,不能插队。
- 而非公平锁则没有这些规则,是抢占模式,每来一个人不会去管队列如何,直接尝试获取锁。
可以通过构造方法来指定具体类型:
//默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
由于公平锁需要关心队列的情况,得按照队列里的先后顺序来获取锁(会造成大量的线程上下文切换),而非公平锁则没有这个限制,因此非公平锁的效率和吞吐量都比公平锁高的多,默认一般使用非公平锁。
通常的使用方式如下:
private ReentrantLock lock = new ReentrantLock();
public void run() {
lock.lock();
try {
} catch (InterruptedException e) {
} finally {
lock.unlock();
}
}
锁的种类
- 可重入锁
如果锁具备可重入性(表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配),则称作为可重入锁。
其表现为:
当前线程可以反复加锁,但也需要释放同样加锁次数的锁。
同一个线程再次进入同步代码时,可以使用自己已经获取到的锁
Synchronized 和 ReentrantLock 都是可重入锁。
- 读写锁
读写锁将对一个资源的访问分成了2个锁:一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock 就是读写锁,它是一个接口,ReentrantReadWriteLock 实现了这个接口,可以通过 readLock() 获取读锁,通过 writeLock() 获取写锁。
- 可中断锁
即可以中断的锁。在 Java 中,Synchronized 不是可中断锁,而 Lock 是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
Lock 接口中的 lockInterruptibly() 方法就体现了 Lock 的可中断性。
- 公平锁
公平锁即尽量以请求锁的顺序来获取锁。同时有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的,这样就可能导致某个或者一些线程永远获取不到锁。
Synchronized 是非公平锁,它无法保证等待的线程获取锁的顺序。对于 ReentrantLock 和 ReentrantReadWriteLock,默认情况下是非公平锁,但是可以设置为公平锁。
Synchronized 和 Lock 的区别
- Lock 是一个接口,而 Synchronized 是 Java 中的关键字,Synchronized 是内置的语言实现;
- Synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock() 去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
- Lock 可以让等待锁的线程响应中断,而 Synchronized 却不行,使用 Synchronized 时,等待的线程会一直等待下去,不能够响应中断;
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- Lock可以提高多个线程进行读操作的效率。
sleep 和 wait 的区别
- 使用上:sleep 是 Thread 线程类的方法,而 wait 是 Object 顶级类的方法。sleep 可以在任何地方使用,而 wait 只能在同步方法或者同步块中使用。
- CPU及资源锁释放:sleep 和 wait 调用后都会暂停当前线程并让出cpu的执行时间,但不同的是 sleep 不会释放当前持有的对象的锁资源,到时间后会继续执行,而 wait 会放弃所有锁并需要 notify/notifyAll 后重新获取到对象锁资源后才能继续执行。
- 异常捕获:sleep 需要捕获或者抛出异常,而 wait/notify/notifyAll 不需要。
ThreadLocal
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被 private static 修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
Android中的应用:Looper类就是利用了ThreadLocal的特性,保证每个线程只存在一个Looper对象。