并发设计模式和锁优化以及jdk8并发新特性
1 设计模式
(1) 单例模式
保证一个类只能一个对象实现。正常的单例模式分为懒汉式和饿汉式,饿汉式就是把单例声明称static a=new A(),系统第一次调用的时候生成(包括调用该类的其他静态资源也会生成),懒汉式就是系统调用get函数的时候,加个锁判断单例对象是否存在,存在就返回不存在就声明一个。好一点的懒汉式应该把单例加一个静态内部类,第一次访问的类的时候静态内部类不会初始化,当调用的get方法的时候再实例化,这样不用加锁效率高一些,
public class StaticSingleton {
private StaticSingleton(){
System.out.println("StaticSingleton is create");
}
private static class SingletonHolder {
private static StaticSingleton instance = new StaticSingleton();
}
public static StaticSingleton getInstance() {
return SingletonHolder.instance;
}
(2)不变模式 类和变量都声明为final,只要创建就不可变,常见的string Integer Double等都是不可变。
(3) future模式 客户端请求服务端数据,如果服务端处理时间较长,可以返回一个空值(类似代理),启动一个线程专门设值。客户端可以先干别的,当想要试用这个值时,可以从代理里拿,如果代理值已经设置好直接返回,如果没设置好则wait,等设置好了的时候notify。 可以向excutor里提交一个实现了Collable的对象,会返回一个Future,然后使用这个future.get()拿值。
(4) 生产者消费者模式 专门有生产者生产数据,消费者消费数据,中间靠线程安全的队列作为公共区域,各线程都从这个区域里写值和读值。各个线程无需了解对存在,只要负责自己的事情即可,也符合开闭原则。
2 锁优化
具体思路:减少锁持有时间,减小锁粒度,锁分离,锁粗化,锁消除。
(1)减少锁持有时间 尽量少的加锁代码,例如用具体代码段代替方法加锁。
(2)减小锁粒度 把大对象尽量改成小对象,增加并行度减少锁竞争。同时有利于偏向锁,轻量级锁。例如ConcurrentHashMap
(3)锁分离 读写分离,读读可重入,读写互斥,写写互斥。另一种分离,例如 LinkedBlockingQueue ,存数据和取数据从队列两端操作,两端各自加锁控制即可,两端的锁互不影响。
(4)锁粗化 如果一段程序要多次请求锁,锁之间的代码执行时间比较少,就应该整合成一个锁,前提是不用同步的部分执行时间短。例如for循环里面申请锁,如果for循环时间不长,可以在for外面加锁。
(5)锁消除 编译器级别的操作,如果jdk发现锁不可能被共享,会擦除这个锁。原理是逃逸分析,例如stringbuffer,本身操作是加锁的,如果只在局部使用不存在并发访问,那么会擦除锁,如果对象逃逸出去例如赋值给全局变量等,面临并发访问,就不会擦除锁。可以通过jvm参数来指定是否使用锁消除。
3 jdk的锁优化 sychronized的优化,由虚拟机完成
(1)偏向锁 在竞争比较少的情况下,会使用偏向锁来提高性能。
*对象头 markword,共32位,存hash,锁信息(指向锁的指针),垃圾回收标志(偏向锁id),年龄信息,偏向锁线程id,monitor信息等。
一个线程争取到对象资源时,对象会在对象头中标记为偏向,并且将线程id写入到对象头中,下次如果这个线程再来可以不通过锁竞争直接进入同步块。当其他线程访问的时候,偏向结束,升级为轻量级锁。所以在竞争激烈的场景下偏向锁会增加系统负担,jvm默认是开启偏向锁的,可以通过jvm参数设置取消偏向锁
*偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令,在只有一个线程执行同步块时进一步提高性能。
(2)轻量级锁 轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
轻量级锁的加锁过程 :
1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(偏向锁也是无锁),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。
2)拷贝对象头中的Mark Word复制到锁记录中。
3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。
4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为处于轻量级锁定状态。
5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
轻量级锁解锁时,把复制的对象头替换回去(cas)如果替换成功,锁结束,如果失败,说明有竞争,升级为重量级锁(先会自旋一下等等看),notify 唤醒其他等待线程。
* 轻量级锁是为了在线程交替执行同步块时提高性能。
(3)自旋锁
轻量级锁加锁失败以后,可能先自旋一段时间,尝试获得轻量级锁,不会着急升级为重量级锁挂起。如果自旋过多,会造成cpu资源浪费,JDK采用了适应性自旋,简单来说就是一开始设置固定自旋次数,线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
*自旋如果成功,可以省略线程挂起的时间。jdk7以后默认使用。
3 jdk8新特性
(1)LongAdder 类似automicLong, 但是提供了“热点分离”。过程如下:如果并发不激烈,则与automicLong 一样,cas赋值。如果出现并发操作,则使用数组,数组的各元素之和为真实value,让操作分散在数组各个元素上,把并发操作压力分散,一遇到并发就扩容数组,最后达到高效率。一般cas如果遇到高并发,可能一直赋值失败导致不断循环,热点分离可以解决这个问题。有点类似concurrenthashmap,分而治之。
(2)completableFuture 对Future进行增强,支持函数式编程的流式调用。提供更多功能,压缩编码量。
(3)stampedLock 改进读写锁,读不阻塞写。如果读的时候,发生了写,应该重新读,不是阻塞写。解决了一般读写锁读太多导致写一直阻塞的问题,读线程发现数据不一致时触发重新读操作。 原理是维护了一个stamp标记,在添加写锁的释放写锁的时候,stamp都会改变(比如++),代码在加读锁的时候,可以先得到stamp,读完数据释放读锁的时候,调用validate方法,检验刚才stamp和现在stamp是否相同,如果相同,说明读的过程中没有修改,读取成功,如果不相同,则说明读的时候发生了写,那么接下来两种策略,一个是继续用当前stamp为初试,继续读,读完比较stamp,是乐观的办法;另一种直接调用readlock(),升级为正常的读锁,是悲观办法。