线程安全-下
- 使用层面锁优化
- 减少锁的时间:
- 减少锁的粒度:
- 锁粗化:
- 使用读写锁:
- 使用CAS:
- 系统层面锁优化
- 自适应自旋锁
- 锁消除
- 锁升级
- 偏向锁
- 轻量级锁
- 重量级锁
- ThreadLocal原理
- ThreadLocal简介
- 原理
- ThreadLocal内存泄漏
- HashMap线程安全
- 死循环造成 CPU 100%
- String不可变原因
使用层面锁优化
减少锁的时间:
不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放。
减少锁的粒度:
它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;java中很多数据结构都是采用这种方法提高并发操作的效率,比如:ConcurrentHashMap。
java中的ConcurrentHashMap在jdk1.8之前的版本,使用一个Segment 数组:Segment< K,V >[] segments。
Segment 继承自ReenTrantLock,所以每个Segment是个可重入锁,每个Segment 有一个HashEntry< K,V >数组用来存放数据,put操作时,先确定往哪个Segment放数据,只需要锁定这个Segment,执行put,其它的Segment不会被锁定;所以数组中有多少个Segment就允许同一时刻多少个线程存放数据,这样增加了并发能力。
锁粗化:
大部分情况下我们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度;
假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的。
使用读写锁:
ReentrantReadWriteLock 是一个读写锁,读操作加读锁,可并发读,写操作使用写锁,只能单线程写。
使用CAS:
如果需要同步的操作执行速度非常快,并且线程竞争并不激烈,这时候使用cas效率会更高,因为加锁会导致线程的上下文切换,如果上下文切换的耗时比同步操作本身更耗时,且线程对资源的竞争不激烈,使用volatile + cas操作会是非常高效的选择。
系统层面锁优化
自适应自旋锁
自旋锁可以避免等待竞争锁进入阻塞挂起状态被唤醒造成的内核态和用户态之间的切换的损耗,它们只需要等一等(自旋),但是如果锁被其他线程长时间占用,一直不释放CPU,死等会带来更多的性能开销;自旋次数默认值是10。
对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。
锁消除
锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。Netty中无锁化设计pipeline中channelhandler会进行锁消除的优化。
锁升级
偏向锁
如果线程已经占有这个锁,当他在次试图去获取这个锁的时候,他会已最快的方式去拿到这个锁,而不需要在进行一些 monitor 操作,因为在大部分情况下是没有竞争的,所以使用偏向锁是可以提高性能的;
轻量级锁
在竞争不激烈的情况下,通过CAS避免线程上下文切换,可以显著的提高性能。
重量级锁
重量级锁的加锁、解锁过程造成的损耗是固定的,重量级锁适合于竞争激烈、高并发、同步块执行时间长的情况。
ThreadLocal原理
ThreadLocal简介
通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的专属本地变量该如何解决呢?JDK中提供的 ThreadLocal 类正是为了解决这样的问题。类似操作系统中的TLAB。
原理
首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。
因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。
最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是 ThreadLocalMap 的封装,传递了变量值。
我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。
例如:
1.存储用户会话
private static final ThreadLocal threadSession = new ThreadLocal();
2.解决线程安全问题
private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>();
ThreadLocal内存泄漏
实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,⽽ value 是强引⽤。
弱引用的特点是,如果这个对象持有弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。假如我们不做任何措施的话,value 永远⽆法被GC 回收,如果线程长时间不被销毁,可能会产⽣内存泄露。
ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。
如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。
因此使⽤完ThreadLocal ⽅法后,最好⼿动调⽤ remove() ⽅法。
HashMap线程安全
死循环造成 CPU 100%
HashMap 有可能会发生死循环并且造成 CPU 100% ,这种情况发生最主要的原因就是在扩容的时候,也就是内部新建新的 HashMap 的时候,扩容的逻辑会反转散列桶中的节点顺序,当有多个线程同时进行扩容的时候,由于 HashMap 并非线程安全的,所以如果两个线程同时反转的话,便可能形成一个循环,并且这种循环是链表的循环,相当于 A 节点指向 B 节点,B 节点又指回到 A 节点,这样一来,在下一次想要获取该 key 所对应的 value 的时候,便会在遍历链表的时候发生永远无法遍历结束的情况,也就发生 CPU 100% 的情况。
所以综上所述,HashMap 是线程不安全的,在多线程使用场景中推荐使用线程安全同时性能比较好的 ConcurrentHashMap。
String不可变原因
- 可以使用字符串常量池,多次创建同样的字符串会指向同一个内存地址;
- 可以很方便地用作 HashMap 的 key。通常建议把不可变对象作为 HashMap的 key;
- hashCode生成后就不会改变,使用时无需重新计算;
- 线程安全,因为具备不变性的对象一定是线程安全的。