启动线程
自动线程的方式只有两种:
- 继承自Thread,然后调用start()方法
- 实现Runable接口,然后交给Thread运行
线程的状态
Java中的线程分为6种状态:
- 初始状态(NEW):新创建了一个线程,但还没有调用start方法
- 运行状态(Runable):Java线程中将就绪(ready)和运行中(running)两种状态统称为“运行”;
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
- 阻塞(BLOCKED):表示线程阻塞于锁;
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断);
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回;
- 终止(TERMINATED):表示该线程已经执行完毕。
状态之间的变迁如下图所示:

死锁
概念:
死锁的发生必须具备以下四个必要条件。
-
互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
-
请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
-
不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
-
环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
由于死锁需要上述四个必要条件,所以只要打破以上四种条件中的任何一个,就可以避免死锁。
- 打破互斥条件:改造独占性资源为虚拟资源。
- 打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。
- 打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。
- 打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。
解决办法:
- 内部通过顺序比较,确定拿锁的顺序;
- 采用尝试拿锁的机制。
其他线程安全问题:
活锁:
两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。
解决办法:每个线程休眠随机数,错开拿锁的时间。
线程饥饿:
低优先级的线程,总是拿不到执行时间。
ThreadLocal 解析
1.与Synchonized关键字的比较:
ThreadLocal和Synchonized都用于解决多线程并发訪问。可是ThreadLocal与synchronized有本质的差别。synchronized是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程訪问。而ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间訪问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。
2.ThreadLocal的使用
-
void set(Object value)
设置当前线程局部变量的值
-
Object get()
返回当前线程所对应的线程局部变量
-
void remove()
将当前线程局部变量的值删除,在JDK5.0新增的方法。但显式调用该方法清除线程的局部变量并不是必须的操作,但会加快内存回收的速度。所以推荐手动调用。
-
protected Object initialValue()
返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
3. ThreadLocal的实现原理
如下图:

ThreadLocal内部实现,主要关注其set()和get()方法
-
set()方法
public void set(T value){ Thread t = Thread.getCurrentThread(); ThreadLocalMap map = getMap(t); if(map != null) map.set(this,value); else createMap(t,value); } ThreadLocalMap getMap(Thread t){ return t.threadLocals; } void createMap(Thread t, T firstValue){ t.threadLocals = new ThreadLocalMap(this,firstValue); }ThreadLocal内部有一个实例变量:
ThreadLocal.ThreadLocalMap threadLocals = null -
get()方法
public T get() { //获取当前的Thread对象 Thread t = Thread.currentThread(); // getMap方法获取对应的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); // 判断map是否为空 if (map != null) { // 如果map不为空,则以当前的ThreadLocal为Key,获取到Entry对象,并从中取值返回 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 如果map为空,则调setInitialValue方法, return setInitialValue(); // } private T setInitialValue() { T value = initialValue(); //返回空 Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } -
ThreadLocalMap的源码

可以看到有个Entry内部静态类,它继承了WeakReference,总之它记录了两个信息,一个是ThreadLocal<?>类型,一个是Object类型的值。getEntry方法则是获取某个ThreadLocal对应的值,set方法就是更新或赋值相应的ThreadLocal对应的值。
private Entry getEntry(ThreadLocal<?> key){ int i = key.threadLocalHashcode & (table.length - 1); Entry e = table[i]; ..... } ..... private void set(ThreadLocal<?> key, Object value){ ........ }回顾我们的get方法,其实就是拿到每个线程独有的ThreadLocalMap
然后再用ThreadLocal的当前实例,拿到Map中的相应的Entry,然后就可以拿到相应的值返回出去。当然,如果Map为空,还会先进行map的创建,初始化等工作。
CAS原理
1. 什么是原子操作?
假定有两个操作A和B,A和B的操作可能都很复杂,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行,那么A和B对彼此来说是原子的。
2.如何实现原子操作?
实现原子操作可以使用锁机制,比如synchronized关键字,完全可以满足我们的需求。但sychronized关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其他线程都会被阻塞,直到该线程释放锁。
这样就会出现一些问题:
- 如果被阻塞的线程优先级很高,或者很重要,我们希望它很快就被执行,但拥有锁的线程一直不释放锁,就不满足我们的期望;
- 如果有大量线程都来竞争资源,CPU将会花费大量的时间和资源来处理这种竞争(上下文切换),同时还可能出现死锁等糟糕的现象。
- 锁机制是一种比较粗糙、粒度较大的机制,相对于少量资源的同步来说太过于笨重。
实现原子操作还可以使用当前的处理器基本都支持CAS()的指令,只不过每个厂家所实现的算法并不一样,每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。
CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环CAS就是在一个循环里不断的做cas操作,直到成功为止。

3. CAS实现原子操作的三大问题
1)ABA问题
因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。
2)循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
3)只能保证对一个变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
4. JDK中原子操作类的使用
1)基本类型
- AtomicBoolean:以原子更新的方式更新 boolean;
- AtomicInteger:以原子更新的方式更新 Integer;
- AtomicLong:以原子更新的方式更新 Long;
这几个类的用法基本一致,以 AtomicInteger 为例常用的方法
- addAndGet(int delta) :以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果;
- incrementAndGet() :以原子的方式将实例中的原值进行加 1 操作,并返回最终相加后的结果;
- getAndSet(int newValue):将实例中的值更新为新值,并返回旧值;
- getAndIncrement():以原子的方式将实例中的原值加 1,返回的是自增前的旧值;
2)数组类型
- AtomicIntegerArray:原子更新整型数组中的元素;
- AtomicLongArray:原子更新长整型数组中的元素;
- AtomicReferenceArray:原子更新引用类型数组中的元素
这几个类的用法一致,就以 AtomicIntegerArray 来总结下常用的方法:
- addAndGet(int i, int delta):以原子更新的方式将数组中索引为 i 的元素与输入值相加;
- getAndIncrement(int i):以原子更新的方式将数组中索引为 i 的元素自增加 1;
- compareAndSet(int i, int expect, int update):将数组中索引为 i 的位置的元素进行更新
可以看出,AtomicIntegerArray 与 AtomicInteger 的方法基本一致,只不过在 AtomicIntegerArray 的方法中会多一个指定数组索引位 i。
3)引用类型
- AtomicReference:原子更新引用类型;
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段;
- AtomicMarkableReference:原子更新带有标记位的引用类型;
4)字段类型
AtomicIntegeFieldUpdater:原子更新整型字段类;
AtomicLongFieldUpdater:原子更新长整型字段类;
AtomicStampedReference:原子更新引用类型,这种更新方式会带有版本号。而为什么在更新的时候会带有版本号,是为了解决 CAS 的 ABA 问题;
要想使用原子更新字段需要两步操作:
- 原子更新字段类都是抽象类,只能通过静态方法
newUpdater来创建一个更新器,并且需要设置想要更新的类和属性; - 更新类的属性必须使用
public volatile进行修饰;
线程池
在Java开发中,合理的利用线程池可以有以下好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。 如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。
- 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
1.ThreadPoolExecutor 的类关系
-
Executor是一个接口,它是Executor框架的基础,它将任务的提交与任务的执行分离开来。
-
ExecutorService接口继承了Executor,在其上做了一些shutdown()、submit()的扩展,可以说是真正的线程池接口;
-
AbstractExecutorService抽象类实现了ExecutorService接口中的大部分方法;
-
ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。
-
ScheduledExecutorService接口继承了ExecutorService接口,提供了带"周期执行"功能ExecutorService;
-
ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。ScheduledThreadPoolExecutor比Timer更灵活,功能更强大。
2.线程池创建各个参数的含义
public ThreadPoolExecutor(
int corePoolSize, //核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime, //线程存活时间
TimeUnit unit, //线程存活时间单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂
RejectedExecutionHandler handler //拒绝策略
)
1)corePoolSize
线程中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;
如果当前线程数为corePoolSize,继续提交的任务会被保存在阻塞队列中,等待被执行;
如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有的核心线程。
2)maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize。
3)keepAliveTime
线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间。默认情况下,该参数只在线程数大于corePoolSize时才有用。
4)TimeUnit
keepAliveTime的时间单位。
5)workQueue
workQueue必须是BlockingQueue阻塞队列。当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待。通过workQueue,线程池实现了阻塞功能。
一般来说,我们应该尽量使用有界队列,因为使用无界队列作为工作队列会对线程池带来如下影响。
1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize。
2)由于1,使用无界队列时maximumPoolSize将是一个无效参数。
3)由于1和2,使用无界队列时keepAliveTime将是一个无效参数。
4)更重要的,使用无界queue可能会耗尽系统资源,有界队列则有助于防止资源耗尽,同时即使使用有界队列,也要尽量控制队列的大小在一个合适的范围。
6)threadFactory
创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名,当然还可以更加自由的对线程做更多的设置,比如设置所有的线程为守护线程。
Executors静态工厂里默认的threadFactory,线程的命名规则是“pool-数字-thread-数字”。
7)RejectedExecutionHandler
线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
(1)AbortPolicy:直接抛出异常,默认策略;
(2)CallerRunsPolicy:用调用者所在的线程来执行任务;
(3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
(4)DiscardPolicy:直接丢弃任务;
当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
3 线程池的工作机制
- 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
- 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
- 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务。
- 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。
4 提交任务
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
5 关闭线程池
可以通过调用线程池的shutdown()或shutdownNow()方法来关闭线程池。原理是遍历线程池中的工作线程,然后逐个调用工作线程的interrupt()方法来中断线程,所以无法响应中断的任务可能永远无法停止。但两种方式存在一定的区别,shutdownNow()方法首先将线程池的状态置为STOP,然后尝试停止所有正在执行或者暂停任务的线程,并返回等待执行任务的列表,而shutdown()方法只是将线程池的状态置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法中的任意一个,isShutdown()方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed()方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown()方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。
6 合理分配线程池
要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。
-
任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
-
任务的优先级:高、中和低。
-
任务的执行时间:长、中和短。
-
任务的依赖性:是否依赖其他系统资源,如数据库连接。
性质不同的任务可以用不同规模的线程池分开处理。
CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。
混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。
建议使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点儿,比如几千。
如果当时我们设置成无界队列,那么线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。