一、常见的锁策略
锁策略是是指在多线程编程中用于同步资源访问的机制,常见的锁策略有:
1.乐观锁 和 悲观锁
乐观锁:假设在一个程序中锁的竞争不大,认为数据一般情况下不会产生并发冲突,因此在操作数据时不对数据加锁,在修改数据的时候才会判定是否发生并发冲突。
悲观锁:假设一个程序中的锁竞争很大,认为修改数据的时候一般会发生并发冲突,因此每次在拿到数据的时候都会加上锁,其他线程只能阻塞等待锁。
2.轻量级锁 和 重量级锁
轻量级锁:加锁的开销比较小
重量级锁:加锁的开销比较大
3.自旋锁 和 挂起等待锁
自旋锁:指在锁竞争失败后不会释放CPU资源,而是立刻尝试在获取锁,一直无限循环直到得到锁。
挂起等待锁:指的的在锁竞争失败后会阻塞等待,释放CPU资源,等到没有线程使用锁的时候再来获取锁。
4.公平锁 和 非公平锁
公平锁:指的是在锁竞争中遵守 '先来后到' ,哪个线程先加的锁,当锁释放的时候就那个线程先加锁。
非公平锁:指的是锁竞争是随机的,不遵守 '先来后到',当一个线程释放锁之后,哪个线程先获得锁是随机的。
5.可重入锁 和 不可重入锁
可重入锁:是指允许一个线程对自己加锁多次,当一个线程连续对自己加锁多次,而不会构成死锁。
不可重入锁:是指不允许一个线程对自己加锁多次,当一个线程对自己加锁多次,就会导致 '自己把自己锁死' 。
6.读写锁
读写锁:是指把锁拆分成 读锁 和 写锁,在多线程中2个线程同时读取同一个数据的时候不会构成线程安全问题,而2个线程同时写入同一个数据或者1个线程读数据,1个线程写数据的时候则会构成线程安全问题。把锁拆分成读写锁,当2个锁都是读锁的时候,线程之间不互斥;当2个线程都是写锁或者1个线程是读锁1个线程是写锁的时候,线程之间互斥,由于互斥会引起线程等待,所以减少互斥就能提高程序效率,读写锁使用与于"频繁读,不频繁写"的场景中。
二、CAS
CAS: 全称Compareandswap,字面意思: '比较并交换',CAS涉及到到操作:假设内存中存储的数据位V,预期的原数值为A,要修改的数值为B。当要修改数值的时候,会先比较 V 与 A 的值,当 V 与 A 的值相等的时候(说明没有别的线程修改过数据)才会让V 与 B 交换 (相当于赋值),否则不做任何操作。当多个线程同时对某个资源进行CAS操作,只能有⼀个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。CAS相当于通过一个原子的操作,同时完成"读取内存,比较是否相等,修改内存"这三个步骤。
CAS的实现
在Java标准库中提供了 java.util.concurrent.atomic 包,里面的类都是基于这种方式来实现的. 典型的就是AtomicInteger类。
AtomicInteger类事例:
代码结果:
CAS的ABA问题
ABA问题:假设有两个线程t1 和 t2,有一个变量 num 值为A,当线程t1使用CAS对num进行修改的时候会判定sum里的值是否为A当为A的时候才会进行交换,但是有可能当 t1 在执行的时候,t2把num的值从 A改成B再改成A,这时当 t1 进行比较的时候还是num的值还是A,就会进行交换导致了线程安全问题。
ABA问题可以通过使用引入版本号来解决,每次修改完数据就让版本号自增,在比较的时候也比较一下版本号,如果版本号一样才进行修改操作。
三、关于锁的优化操作
1.synchronized的优化过程:
JVM将synchronized锁分为⽆锁、偏向锁、轻量级锁、重量级锁状态。会根据情况,进行依次升级。
偏向锁:用synchronized加锁的时候不会马上加锁,而是会先加上偏向锁,偏向锁不是真的加锁,而是做个标记,记录锁是属于哪个线程,等到出现锁竞争的时候才会真正加上锁。
2.锁消除
在Java中编译器和JVM会判断锁是否有作用,如果没作用会把锁优化掉,进行锁消除。
例如:Java中有些方法或类中有自带的synchronized(例如StringBuffer)当在单线程模式下使用的时候不存在线程安全问题,这时就不需要synchronized加锁,这时JVM就会进行锁消除。
3.锁粗化
⼀段代码中如果出现多次加锁解锁,编译器 和 JVM会自动动进行锁的粗化。
锁的粒度分为粗和细:
锁的粒度细说明对一个线程要频繁的加锁和解锁,这时JVM会进行锁粗化的优化。