回顾
线程不安全解决方案
- CPU抢占式执行(不可解决)
- 加锁(synchronizd、Lock)
- 私有变量(ThreadLocal 线程本地变量)
ThreadLocal:线程级别的私有变量
与任务级别的私有变量完全不同
eg:线程池:10,任务数:1000
- 任务级别的私有变量:会创建1000个私有变量
- 线程级别的私有变量:会创建10个私有变量
public void method(){
MyObject obj = new MyObject();
//......
//任务级别:会创建1000个私有变量
//线程级别:会创建10个私有变量
}
需求1:使用最高效的方式实现2个Date(时间类型)的格式化
需求2:使用最高效的方式实现10个Date(时间类型)的格式化
需求3:使用最高效的方式实现1000个Date(时间类型)的格式化
出现线程不安全问题
正常情况:
异常情况:
解决
①加锁,带来的新的问题:排队执行的性能消耗问题
解决线程不安全,也不排队执行:
②ThreadLocal :线程级别的私有变量
选择①或②的条件:看线程级变量的复用率,如果复用率较高,使用ThreadLocal,复用率较低,就使用加锁
ThreadLocal
使用方法
- set(T):将私有变量存放到线程中
- get():从线程中取得私有变量
- remove():从线程中移除私有变量(脏读、内存溢出)
remove 方法在任何场景都有意义,且不可省略 - initialValue():初始化方法1
注意:initialValue返回的数据类型一定要和 ThreadLocal 定义的泛型类型保持一致。
initialValue + get
- 正常存取操作
initialValue + set + get ?
- 先执行set方法,再执行get方法(不执行initialValue方法)
什么情况下不会执行initialValue?为什么?
- 调用set之后就不会执行了
- threadLocal是懒加载的,当调用了get方法之后,才会尝试执行initialValue方法,尝试获取一下ThreadLocal set 的值,如果获取到了值,那么初始化方法永远不会执行
ThreadLocal ->Map(ThreadLocalMap)->Entry[] -> key(ThreadLocal),value(实际存储值)
get
set
- withInitial():初始化方法2
static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(()->"java");
初始化没值就执行initial方法
每次remove之后会执行一次
使用场景
- 解决线程安全的问题
- 实现线程级别的数据传递
多个线程 ->每个线程拥有自己的变量(ThreadLocal)
ThreadLocal只能存储一个值
缺点
不可继承
子线程中不能读取到父线程的值
static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
解决:InheritableThreadLocal
InheritableThreadLocal不能实现不同线程之间的数据共享
脏读(脏数据)
在一个线程中读取到了不属于自己的数据
线程使用ThreadLocal 不会出现脏读,因为每个线程都使用的是自己的变量值和ThreadLocal
线程池使用ThreadLocal 会出现脏读,因为线程池会复用线程,复用线程后,也会复用线程中的静态属性,从而导致某些方法没有被执行,于是就出现了脏数据的问题
解决方案:
避免使用静态属性(静态属性在线程池中会被复用)
使用remove解决
内存溢出问题
(最常出现的问题)
打开数据连接但未关闭
内存溢出:当一个线程执行完后,不会释放这个线程所占内存,或者释放内存不及时的情况就叫做内存溢出(线程不用了,但是相关内存得不到及时释放)
内存溢出:ThreadLocal+线程池
线程池是长生命周期
线程是执行完任务线程就结束了,线程相关的资源都会释放掉
ThreadPool ->Thread -> ThreadLocal ->内存不会关闭 -> OOM
Thread -> ThreadLocalMap—>开放寻址法
升级:(链表–>红黑树)
链表长度大于8
数组长度大于64
降级:(红黑树–>链表)
链表长度小于6
HashMap和ThreadLocalMap处理哈希冲突的区别?
- HashMap使用链表法
- ThreadLocalMap使用开放寻址法
为什么要这么实现?
- 开放寻址法的特点和使用场景是数据量比较少的情况下性能更好
- HashMap里边存储的数据通常情况比较多,这个时候使用开放寻址法效率就比较低了
为什么将ThreadLocal中的key设置为弱引用
- 为了最大程度的避免OOM
为什么会发生OOM?
- ThreadLocal(长生命周期)–>Thread -> ThreadLocal ->Entry[] -> Entry -> key,value(强引用)
解决ThreadLocal的内存溢出
使用remove()
提升程序的性能
多线程
单例模式
设计模式
- 单例模式(手写)
- 工厂模式(简单工厂、抽象工厂)
- 模板模式
单例模式
在整个程序的运行中,只存在一个对象
饿汉方式:直接先创建一个对象
- 优点:不用加锁也是线程安全的
- 缺点:程序启动之后就会创建,但是创建完了之后可能不会使用,浪费了系统资源
懒汉方式:当程序启动之后,并不会进行初始化,而是在什么时候调用再进行初始化
非安全的单例模式 -----> 懒汉
解决方案:加锁
①先在内存中开辟空间
②初始化
③将变量singleton指向内存区域
指令优化(指令重排序)
指令重排序前:①->②->③
指令重排序后:①->③->②
执行1、3–>暂停执行了–>未执行初始化–>返回一个空对象–>线程不安全
自定义阻塞队列
生产者消费者模型
生产者生产数据,消费者消费生产者生产的数据
生产者:当数据满了之后,不要尝试给队列添加数据,而是阻塞等待
sleep(time)
wait()/notify()/notifyAll()
LookSupport park()/unpark()
消费者:当队列为空的时候,阻塞等待
自定义阻塞队列
实现队列:链表、数组(栈)