7.1 线程安全

- Thread中,可通过
setDefaultUncaughtExceptionHandler()的方式才能在主线程中捕捉到子线程异常 - 保证高并发的线程安全,四个维度可以考虑:
- 数据单线程内可见
- 只读对象
- 线程安全类
- 同步和锁机制
java并发包JUC(作者Doug Lea)
锁主要提供了两种特性:互斥性和不可见性
- Java中锁的实现
- Lock类-ReentranLock
- 核心是AbstractQueuedSynchronizer

- 核心是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 (){
}; ```@Overrideprotected 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数据
