Java多线程进阶(锁策略)
常见的锁策略:
1)悲观锁和乐观锁
加锁的时候,预测当前锁冲突的概率是大还是小。
预测当前锁冲突概率大,后续要做的工作往往就会更多,加锁开销就更大(时间,系统资源)=》悲观锁
预测当前锁冲突概率不大,后续要做的工作往往就更少,加锁开销就更小(时间,系统资源)=》乐观锁
synchronzied既是乐观锁,也是悲观锁
synchronzied支持自适应,能够自动的统计出当前的锁冲突的次数,进行判定当前锁冲突概率低还是概率高。
当冲突概率低的时候,就是按照乐观锁的方式来执行的。(速度更快)
当锁冲突概率高的时候,就会升级悲观锁的方式来执行(做的工作更多)
2)重量级锁和轻量级锁
一般来说,悲观锁,往往就是重量级锁,乐观锁就是轻量级锁。
3)自旋锁和挂起等待锁
自旋锁,是轻量级锁的一种典型的实现方式
消耗了更多的cpu资源,但是一旦锁被释放,就能第一时间拿到锁。拿到锁的速度很快。消耗cpu
挂起等待锁,是重量级锁的一种典型实现方式。
借助系统中的线程,当尝试加锁,并且锁被占用了,出现锁冲突,就会让当前这个尝试加锁的线程,被挂起(阻塞状态)此时这个线程就不会参与调度了。
直到这个线程被释放了,然后系统才能唤醒这个线程,去尝试重新获取锁。
(拿到锁的速度更慢,节省cpu)
synchronzied 轻量级锁部分,基于自旋锁实现,重量级锁部分,基于挂起等待锁实现。(基于CAS机制来实现)
4)可重入锁和不可重入锁
synchronzied就是可重入锁,一个线程,针对这把锁,连续加锁两次,不会死锁。
比如c++de std::mutex就是不可重入锁,一个线程针对这把锁,连续加锁两次,会死锁。
5)公平锁和非公平锁
公平锁:严格按照先来后到的顺序来获取锁,哪个线程等待的时间长,哪个线程就拿到锁。
非公平锁:若干个线程,各凭本事,随机的获取到锁,和线程等待时间无关了。
synchronized属于非公平锁,多个线程,尝试获取这个锁,此时是按照概率均等的方式来进行获取的。
{系统本身线程调度的顺序就是随机的,如果需要实现公平锁,就需要引入额外的队列,按照加锁顺序把这些获取锁的线程的线程入队列,再一个一个的取}
6)互斥锁和读写锁
synchronized本事就是普通的互斥锁
读写锁,则是一个更特殊的锁(加读锁,加写锁,解锁)
Java的读写锁是这样设定的:
1)读锁和读锁之间,不会产生互斥
2)写锁和写锁之间,会产生互斥
3)读锁和写锁之间,会产生互斥
synchronized实现原理:
synchronized即使悲观锁也是乐观锁,既是轻量级锁也是重量级锁,轻量级锁是自旋转实现,重量级锁就是挂起等待锁实现,是可重入锁,不是读写锁,是非公平锁
synchronized的“自适应”
锁升级的过程:
1)未加锁的状态(无锁){代码中开始调用执行synchronized}
2)偏向锁
3)轻量级锁
4)重量级锁
偏向锁的过程:
首次使用synchronized对对象进行加锁的时候,不是真的加锁,而只是做一个“标记”(非常轻量,非常快,几乎没有开销)
如果没有别的线程尝试对这个对象加锁,就可以保持这个暧昧状态,一直到解锁。
但是,如果在偏向锁状态下,有某个线程也尝试来对这个对象加锁,立马把偏向锁升级成轻量级锁(真的加锁,真的有互斥了),相当于立即确认关系,也就可以保证锁能够正常生效。
本质上,偏向锁策略就是“懒”字具体体现,能不加锁,就不加锁,能晚加锁,就晚加锁。
针对一个锁对象来说,是不可逆的,只能升级,不能降级。
一旦升级到了重量级锁,不会回退到轻量级锁(当前jvm里面的做法)
7)锁消除,编译器优化策略
代码里面写了加锁操作,jvm会对当前的代码做出判定,看这个地方到底是不是真的需要加锁。
如果这个不需要加锁,就会自动的把加锁操作,给优化掉。
8)锁粗话,也是一种优化策略
有些逻辑中,需要频繁加锁频繁,编译器就会自动的把多次细粒度的锁,合并成一次粗粒度的锁。
三个简单的加锁操作,合并成一个加锁操作。
CAS compare and swap 比较和交换
这是一条cpu(原子的)指令,就可以完成比较和交换这样的一套操作下来。
CAS的流程==》想象成一个方法
boolean cas(address,reg1,reg2){if(address ==reg1){}//把address内存地址的值和reg2寄存器的值进行交换return true;}return false;
这里说的交换,实际上更多的是来“赋值”,一般更关心内存中,交换后的数据。而不关心reg2寄存器里交换后的数据。
由于cpu提供了上述指令,因此操作系统内核,也就能够完成上述操作,就会提供出这样的CAS的api,jvm又对于系统的CASapi进一步的封装了,在Java代码中也就可以使用CAS操作了。
在Java中也有一些类,对cas进行了进一步的封装,典型的就是原子类。
Atomiclnteger相当于针对int进行了封装。可以保证此处的++--操作是原子的
package Thread;import java.util.concurrent.atomic.AtomicInteger;public class Demo25 {private static AtomicInteger count=new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t=new Thread(() ->{for (int i = 0; i <10000 ; i++) {count.getAndIncrement();}});Thread t1=new Thread(() ->{for (int i = 0; i < 50000; i++) {count.getAndIncrement();}});t.start();t1.start();t.join();t1.join();System.out.println(count);}
}
CAS的ABA问题
CAS这里的核心是比较-发现相等-交换=》潜台词 发现相等=》数据中间没有发生过任何改变