Java性能优化-2

目录

并发容器

协程


synchronized锁实现
当多个线程访问同一段代码时,会被先放到EntryList集合中,处于block的线程也会被加入该列表
接下来当线程获取到对象的Monitor时,是依靠底层操作系统的Mutex Lock来实现互斥的,也就是说会发生用户态->内核态的切换,线程申请Mutex成功,持有Mutex,其他线程无法获取到Mutex
如果调用wait(),就会释放持有的Mutext,该线程会进入WaitSet集合中

JDK1.6之后对锁进行优化,引入了 偏向锁,轻量级锁,重量级锁
下图是Java对象头的构成,锁升级功能主要依赖Mark Word中锁标志位和释放偏向所标志位


synchronized同步锁是从偏向锁开始,随着竞争激烈升级到轻量级锁,再到重量级锁
偏向锁,当一个线程反复获取锁时,为避免频繁出现用户态到内核态的切换,还需要判断对象头是否持有锁标记,也就是抢到的锁持有的线程ID是否是自己的,如果是则进入偏向锁状态

如果出现多线程竞争,偏向锁会撤销,进入轻量级锁
这种就是CAS自旋,如果超过一定次数次数后,就变成重量级锁
在竞争不激烈,锁占用时间非常短的情况下,自旋锁可以提高系统性能,如果竞争非常激烈CAS反而会占用资源
可以通过JVM启动参数,取消偏向锁,自旋锁

还可以通过减小锁的粒度,还优化性能,比较典型的是集合类中的  ConcurrentHashMap

synchronized锁 vs Lock锁

Lock实现原理

读写锁,利用state标志位的低16位,如果不为空则表示为写锁

读写锁应用在读大于写的并发场景中,在读很多,写很少的情况下,RRW会使写线程遭遇到饥饿问题,也就是说写入线程会因迟迟无法竞争到锁而一直处于等待状态
JDK8提供了 StampedLock 解决了这个问题
StampedLock提供三种模式, 写,悲观读,乐观读
获取写锁之前先要获得一个票据 stamp,用来表示该锁的版本,当释放该锁的时候哦,需要unlockWrite并传递参数stamp

读线程获取锁的时候,首先会通过乐观锁 tryOptimisticRead操作获取票据stamp,如果当前没有线程持有写锁,则返回一个非0的stamp版本信息,线程获取该stamp后,会将拷贝一份共享资源到方法栈
之后方法还需要调用validate,验证之前调用 tryOptimisticRead 返回的stamp在当前是否有其他线程持有了写锁,如果是那么validate会返回0,升级为悲观锁,否则就可以使用该stamp版本的锁对数据进行操作
相比RRW,StampedLock获取锁只是使用与或操作进行检验,不涉及CAS操作,即使第一次乐观锁获取失败,也会马上升级到悲观锁,这样可以避免一直CAS操作带来的CPU占用性能的问题,因此StampedLock的效率更高

StampedLock不支持重入,不支持条件变量,线程被中断时可能导致CPU暴涨

对于多线程并发引起的CPU缓存不一致问题,处理器提供了 总线锁定,缓存锁定 两个机制来复杂内存操作的原子性
当处理器要操作一个共享变量的时候,其在总线上会发出一个Lock信号,这时其他处理器就不能操作共享变量了,该处理器会独享此共享内存中的变量,但总线锁定在阻塞其他处理器获取该共性变量的操作请求时,也可能会导致大量阻塞,从而增加系统的性能开销
后来的处理器提供了缓存锁定机制,当某讴歌处理器对缓存中共享变量进行了操作,就会通知其他处理器放弃存储该共享资源或者重新读取该共享资源,目前最新的处理器都支持缓存锁定机制

 

锁竞争会导致大量的上下文切换,在多线程编程中,锁其实不是性能开销的根源,竞争锁才是
优化方式
1.减少锁的持有时间,重构代码,将锁的范围减小
2.降低锁的粒度,锁分离,锁分段
3.非阻塞乐观锁替代竞争锁
合理设置线程池大小,避免创建过多线程
使用协程实现非阻塞等待
减少Java虚拟机的垃圾回收
 

并发容器

Map,List的各种特性,以及适用场景总结

 

Executors 实现了以下四种类型的 ThreadPoolExecutor

线程池中各个参数的相互关系

线程池中的线程分配流程

CPU密集型任务,线程分配数量是 N+1
I/O密集型任务,线程分配数量是 2N

 

协程

1:1 线程模型,通过系统调用 clone()和mutex()来实现
N:1 线程模型,可以在用户空间完成线程的创建,同步,销毁,调度,缺点是系统不能感知用户态的线程,容易造成某个线程进行系统调用内核线程时被阻塞,从而导致整个进程被阻塞
N:M 线程模型,基于上述两种实现的混合线程模型,Go的协程就是这种模式
Java可以用 Kilim 协程框架

用户态的协程调度关系如下图

 

一致性问题
Java模型会对代码进行重排序
还有缓存导致的可见性问题
比如经典的懒加载双重检测问题

Happend-before规则
程序次序规则,在单线程中,代码的执行是有序的,虽然可能会存在运行指令的重排序,但最终执行的结果和顺序执行的结果是一致的
锁定规则,一个锁处于被一个线程锁定占用状态,那么只有当这个线程释放锁之后,其他线程才能再次获取锁操作
volatile变量规则,如果一个线程正在写volatile变量,其他线程读取该变量会发生在写入之后
线程启动规则,Thread对象的start()方法先行发生于此线程的其他每一个动作
线程终结规则,线程中的所有操作都先行发生于此线程的终止检测
对象终结规则,一个对象的初始化完成先行发生于它的finalize()方法的开始
传递性,如果操作A happens-before操作B,操作B happens-before操作C,那么操作A happens-before操作C
线程中断规则,对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

严格一致性,,所有读写操作都按照全局时钟顺序执行,任何时刻线程读取到的缓存数据都是一致的,如HashTable操作结果如下

顺序一致性,多个线程的整体执行可能是无序的,但对于单个线程而且是有序的,要保证任何一次读都能读到最近一次写入的数据,volatile可以阻止指令重排序,所以修改的变量的程序属于顺序一致性

弱一致性,不能保证任何一次读都能读到最近一次写入的数据,但能保证最终可以读到写入的数据,单个写锁+无锁读,就是弱一致性的一种实现
ConcurrentHashMap的 Node<k,v>是volatile修饰的,所以一个县城对齐修改后,另一个线程马上就可以看到
但如果是node(也就是table)里面新加一个元素,这个元素Node不是volatile修饰的,其他线程就不会马上看到,所以是弱一致性的

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值