7.1 线程安全
- Thread中,可通过
setDefaultUncaughtExceptionHandler()
的方式才能在主线程中捕捉到子线程异常 - 保证高并发的线程安全,四个维度可以考虑:
- 数据单线程内可见
- 只读对象
- 线程安全类
- 同步和锁机制
java并发包JUC(作者Doug Lea)
锁主要提供了两种特性:互斥性和不可见性
- Java中锁的实现
- Lock类-ReentranLock
- 核心是AbstractQueuedSynchronizer
- synchronized, 有三种锁实现(这个机制已经不再笨重)
- 偏向锁:在没有锁竞争情况下尽量减少加锁带来的性能开销
- 轻量级锁:出现锁竞争,升级为轻量级锁
- 重量级锁:出现激烈锁竞争那个,升级为重量级锁
- Lock类-ReentranLock
7.3 线程同步
- i++操作没有原子性
volatile关键字
class LazyinitDemo {
//这里没有加volatile关键字的话,有bug,有可能返回未被初始化的对象
//new TransactionService()包含分配内存空间,设置默认值,和执行构造方法两个步骤
//如果正好有个线程B在前一个线程A执行到第一步的时候进来了,那么就会一路进if判断,直接返回未被初始化的对象
//解决办法是,给这个单例对象添加volatile关键字
//避免指令重排序
private static TransactionService service = null;
public static TransactionService getTransactionService() {
if (service == null) {
synchronized (this) {
if (service == null) {
service = new TransactionService();
return service;
}
}
}
return service;
}
}
- 保证此变量对所有线程可见
- 禁止指令重排序优化
volatile有可见性,但是没有原子性
类中一个voliatile数值 ,A线程count++ 100次,B线程count—100次,最终结果大概率不是0 为什么呢?因为count++ 和count—不是原子性操作, volatile仅支持可见性,不支持原子性 解决:count用AtomicLong,或者LongAddr(后者更推荐,性能更好)
volatile适合一写多读场景,不适合多写场景
- 案例:CopyOnWriteArrayList
CountdownLatch
3个事都办完之后,再统一处理
new CountdownLatch(3)
Semaphore
3个窗口,6个人来办事,任何一个窗口有空,排队的人就过去
new Semaphore(3)
如果Semaphore(1) ,就是一个互斥锁
CylicBarrier
一个安检口,6个人排队,每次放三个人一批进去,三个人安检完了 ,再放下一批3人。
AbstractQueuedSynchronizer 介绍
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock
,Semaphore
,其他的诸如 ReentrantReadWriteLock
,SynchronousQueue
,FutureTask
等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS 定义两种资源共享方式
- Exclusive(独占):只有一个线程能执行,如
ReentrantLock
。又可分为公平锁和非公平锁:- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
- Share(共享):多个线程可同时执行,如
CountDownLatch
、Semaphore
、CountDownLatch
、CyclicBarrier
、ReadWriteLock
我们都会在后面讲到。
ReentrantReadWriteLock
可以看成是组合式,因为 ReentrantReadWriteLock
也就是读写锁允许多个线程同时对某一资源进行读。
如何自定义AQS?
实现方法 isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
以
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()
函数返回,继续后余动作。
7.4 线程池
- 核心类ThreadPoolExecutor
- 核心实现-源码
- 队列
- 拒绝策略
7.5 ThreadLocal
- 软引用在内存紧张情况下由更好的回收能力,可以用于在服务器上缓存中间结果。
- 但是不建议缓存高频数据,因为一旦服务器重启或者软引用触发大规模回收,那么所有的访问都将指向数据库。
- WeakReference弱引用,在新生代频繁的gc中会被回收掉
- WeakReference的应用:WeakHashMap
- ThreadLocal中也使用了WeakReference
ThreadLocal的价值
CopyValuelntoEveryThread
//使得每个线程都可以有自己的随机数生成器
//如果用Random,虽然多线程下它是线程安全的,但是会因为多线程竞争同一个seed而导致性能下降
ThreadLocalRandom RANDOM = ThreadLocalRandom.current();
ThreadLocal的弱引用设计
弱引用的设计
- 红虚线代表弱引用
- ThreadLocal对象是线程共享的
- 每个线程里面的有单独自己的
ThreadLocal.ThreadLocalMap
对象 - ThreadLocalMap存的是很多个Entry对象
- 每个Entry是一个K-V结果,K是ThreadLocal对象的弱引用,V是真实的Value
- 所以ThreadLocal对象不持有用户设置的值,这个值是存在每个线程里面的ThreadLocalMap中的
- ThreadLocal近似的理解为,就是Map的一个key
``` /**
- 建立ThreadLocal如下
- shareNum在栈上,持有堆上ThreadLocal对象的引用
- ThreadLocal
**/
private static final ThreadLocal
shareNum = new ThreadLocal (){
}; ```@Override
protected Integer initialValue(){
return 100;
}
弱引用和内存泄露
- 弱引用的引入,是为了一定程度上解决内存泄露问题。
我们两种情况都讨论一下:
- key 使用强引用:引用的
ThreadLocal
的对象被置为null,但是ThreadLocalMap
还持有ThreadLocal
的强引用,如果没有手动删除,ThreadLocal
不会被回收,导致Entry
内存泄漏。 - key 使用弱引用:引用的
ThreadLocal
的对象被置为null,由于ThreadLocalMap
持有ThreadLocal
的弱引用,即使没有手动删除,ThreadLocal
也会被回收。value
在下一次ThreadLocalMap
调用set
,get
,remove
的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap
的生命周期跟Thread
一样长,如果都没有手动删除对应key
,都会导致内存泄漏。
但是使用弱引用可以多一层保障:弱引用ThreadLocal这个key
不会内存泄漏,对应的value
在下一次ThreadLocalMap
调用set
,get
,remove
的时候会被清除。
因此,ThreadLocal
内存泄漏的根源是:由于ThreadLocalMap
的生命周期跟Thread
一样长,如果没有手动删除对应key
就会导致内存泄漏,而不是因为弱引用。
内存泄露-深入
弱引用的引入,在一定程度上解决内存泄露问题。
但是不合理的使用仍然有可能导致内存泄露的问题。
原因:在threadLocal设为null和线程结束这段时间不会value被回收的(用户也不使用get set remove)
易发场景:单个线程可能因为ThreadLocal的使用产生内存泄露,但是只要线程结束,资源就释放了。
更恐怖的情况是,在线程池里,线程结束是可能不会销毁的,会再次使用的就可能出现内存泄露 。
(在web应用中,每次http请求都是一个线程,tomcat容器配置使用线程池时会出现内存泄漏问题)
解决办法:每次用完ThreadLocal,都手动remove一下
ThreadLocal的其他坑
脏数据
- 线程池中不正确地使用ThreadLocal,会造成脏数据问题
- 因为线程池中的线程是复用的,前一个线程中用到了ThreadLocal
- 如果没有手动remove,那么这个线程再次被启用时,就会发现 还遗留着上一次的的ThreadLocal数据