一、ThreadLocal
一般来说,变量在线程之间都是共享的,怎么让线程拥有自己的变量呢?ThreadLocal就可以做到,线程拥有自己的变量,不与其他线程共享。
ThreadLocal中有一个静态内部类ThreadLocalMap,ThreadLocalMap的key是ThreadLocal,value是Object对象。ThreadLocal的get()、set()、remove()方法本质上就是对ThreadLocalMap进行操作。
- set()设置ThreadLocalMap的值
- get()获取ThreadLocalMap的值
- remove()删除ThreadLocalMap对象
ThreadLocalMap是每个线程私有的。
内存泄漏问题
ThreadLocalMap中key是弱引用,value是强引用,如果ThreadLocal没有被外部强引用的情况下,gc垃圾回收的时候,key会被清理掉,value不会被清理掉,这样就会出现key值为null的Entry,ThreadLocalMap只有在线程销毁的时候才会跟着一起销毁,若线程寿命较长,就有可能造成内存泄漏。
为了防止内存泄漏,最好就是在使用完ThreadLocal的方法后手动调用remove()方法清除。
二、线程池
线程池的好处:
- 降低资源的消耗:可重复利用已经创建了的线程,减少线程创建和销毁的消耗。
- 提高响应速度:当任务到达时,可以不用等待线程的创建直接执行
- 提高线程的可管理性:无限制地创建线程,不仅会造成大量地消耗,还会降低线程的稳定性,用线程池来进行统一管理、调优和监控。
Runnable 接口和 Callable 接口
Java线程的实现方式有3种:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
Runnable 接口和 Callable 接口的区别:
Runnable接口是没有返回值的,也不会抛出异常;Callable接口是都可以的,如果执行的任务不需要返回值或者抛出异常的话,优先选择Runnable接口。
执行 execute()方法和 submit()方法的区别
execute()方法用于提交不需要返回值的任务,无法判断任务被线程池执行成功。
submit()方法用于提交需要返回值的任务,线程池会返回一个Future类型的对象,通过这个Futrue对象可以判断是否执行成功,并可通过Futrue的get()方法来获取返回值。
线程池的创建
现在创建线程池最好使用ThreadPoolExecutor,下面也主要讲ThreadPoolExecutor。
ThreadPoolExecutor的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数:
- corePoolSize:线程池的核心线程数,即线程池中允许同时存在的线程数,当线程池中的线程小于核心线程数时(刚创建是线程是0),有任务进来时,就会创建一个线程,直至线程数达到corePoolSize,此时再有任务进来就会放到workQueue中进行等待。
- workQueue:workQueue是一个阻塞队列,用来存储等待执行的任务,当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待。通过workQueue,线程池实现了阻塞功能
- maximumPoolSize:一般情况下,线程池中最多同时存在的线程数是corePoolSize,但如workQueue满了,此时再有任务进来,线程池中最多同时存在的线程数就会变成maximumPoolSize,创建新的线程执行任务。
- handler:饱和策略,当线程达到maximumPoolSize时,仍有任务进来,此时线程池已无法再添加任务,就要用饱和策略拒绝该任务。
- keepAliveTime:当线程池中的线程大于corePoolSize时,若有任务在keepAliveTime内没有任务,则会销毁该线程,直至线程数等于corePoolSize。
- unit:keepAliveTime的单位。
- threadFactory :线程工厂,用来创建线程。
饱和策略:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor
定义一些策略:
ThreadPoolExecutor.AbortPolicy
: 抛出RejectedExecutionException
来拒绝新任务的处理。ThreadPoolExecutor.CallerRunsPolicy
: 调用执行自己的线程运行任务,也就是直接在调用execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。ThreadPoolExecutor.DiscardPolicy
: 不处理新任务,直接丢弃掉。ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求。
举个例子: Spring 通过 ThreadPoolTaskExecutor
或者我们直接通过 ThreadPoolExecutor
的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler
饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy
。在默认情况下,ThreadPoolExecutor
将抛出 RejectedExecutionException
来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy
。当最大池被填满时,此策略为我们提供可伸缩队列。
ThreadPoolExecutor常用方法
shutdown() :使当前未执行的线程继续执行,而不再添加新的任务,池中状态立马变成SHUTDOWN状态,此时再往池中添加任务,会触发拒绝策略。此时池中不会立刻退出,直到池中的任务都已经完成,才会退出。。
shutdownNow() :池中状态立马变成STOP状态,并试图停止所有正在执行的线程(除非有if判断人为的抛出异常),不再处理还在池队列中等待的任务,会返还未执行的任务(用一个llist<Runnable>队列来存储未运行的任务,并返回。)。
1. 当在Runnable中使用 if(Thread.currentThread.isInterruptd() == true)来判断当前线程的中断状态,,中断所有的任务task,并且抛出InterruptedException异常,而未执行的线程不再执行,从任务队列中清除。
2. 如果没有if语句,则池中运行的线程直到执行完毕,而未执行的不再执行,从执行队列队列中删除。
isShutdown():
判断线程池是否已经关闭,只要调用的shutdown()方法,则isShutdown()方法的返回值就是true。
isTerminating() 和 isTerminated():
前者是否正在关闭,但尚未完全终止的过程,返回true。
后者是已经关闭了。
get方法:
getActiveCount() : 取得多少个线程正在执行的任务
getPoolSize() : 当前池中里面有多少个线程,包括正在执行任务的线程,也包括在休眠的线程
getCompletedTaskCount() :取得已经执行完成的任务数
getCorePoolSize() : 取的构造方法传入的corePoolSize参数值
getMaximumPoolSize() : 取的构造方法中MaximumPoolSize的参数值
getPoolSize() : 取的池中有多少个线程
getTaskCount() : 取得有多少个任务发送给了线程池,运行的+ 排队的
、
三、Atomic 原子类
Atomic原子类,就是指具有原子操作特性的类,任意时刻内,只能有一个线程进行操作,不能被中断。
JUC 包中的原子类是哪 4 类?
基本类型
使用原子的方式更新基本类型
AtomicInteger
:整形原子类AtomicLong
:长整型原子类AtomicBoolean
:布尔型原子类
数组类型
使用原子的方式更新数组里的某个元素
AtomicIntegerArray
:整形数组原子类AtomicLongArray
:长整形数组原子类AtomicReferenceArray
:引用类型数组原子类
引用类型
AtomicReference
:引用类型原子类AtomicStampedReference
:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。AtomicMarkableReference
:原子更新带有标记位的引用类型
对象的属性修改类型
AtomicIntegerFieldUpdater
:原子更新整形字段的更新器AtomicLongFieldUpdater
:原子更新长整形字段的更新器AtomicReferenceFieldUpdater
:原子更新引用类型字段的更新器
AtomicInteger 类常用方法
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
Atomic使用简易说明
不使用Atomic
class Test {
//用volatile修饰变量,确保变量可见性和有序性
private volatile int count = 0;
//若要线程安全执行执行count++,需要加锁,确保原子性
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
使用Atomic
class Test2 {
private AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
//使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
public int getCount() {
return count.get();
}
}
AtomicInteger原理
AtomicInteger 类部分源码
// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
四、AQS
AQS 的全称为(AbstractQueuedSynchronizer
),这个类在java.util.concurrent.locks
包下面。
AQS是一个构建锁和同步器的框架,用AQS可以构造出很多的同步器,如ReentrantLock,Semaphore等等。
AQS原理
AQS的核心思想是,如果被请求的共享资源是空闲的,那么请求该资源的线程就会设置为工作线程,并将该共享资源设置为锁定状态;如果被请求的共享资源被占用,则把该线程放入到队列中。
AQS(AbstractQueuedSynchronizer)原理图:
AQS用一个int变量来表示同步状态,用volatile修饰,确保可见性,同时用CAS来对同步状态进行原子操作,确保原子性。同时AQS还有一个内置的队列,来完成获取资源的线程的排队工作。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
//返回同步状态的当前值
protected final int getState() {
return state;
}
//设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
AQS 定义两种资源共享方式
- Exclusive(独占):只有一个线程能执行,如
ReentrantLock
。又可分为公平锁和非公平锁:- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
- Share(共享):多个线程可同时执行,如
CountDownLatch
、Semaphore
、CyclicBarrier
、ReadWriteLock
我们都会在后面讲到。
ReentrantReadWriteLock
可以看成是组合式,因为 ReentrantReadWriteLock
也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在顶层实现好了。
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
- 使用者继承
AbstractQueuedSynchronizer
并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放) - 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
默认情况下,每个方法都抛出 UnsupportedOperationException
。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。
以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。
再以 CountDownLatch
以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()
一次,state 会 CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()
函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease
、tryAcquireShared-tryReleaseShared
中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock
。
AQS 组件总结
Semaphore
(信号量)-允许多个线程同时访问:synchronized
和ReentrantLock
都是一次只允许一个线程访问某个资源,Semaphore
(信号量)可以指定多个线程同时访问某个资源。CountDownLatch
(倒计时器):CountDownLatch
是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。CyclicBarrier
(循环栅栏):CyclicBarrier
和CountDownLatch
非常类似,它也可以实现线程间的技术等待,但是它的功能比CountDownLatch
更加复杂和强大。主要应用场景和CountDownLatch
类似。CyclicBarrier
的字面意思是可循环使用(Cyclic
)的屏障(Barrier
)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier
默认的构造方法是CyclicBarrier(int parties)
,其参数表示屏障拦截的线程数量,每个线程调用await()
方法告诉CyclicBarrier
我已经到达了屏障,然后当前线程被阻塞。