PS:该文章是借鉴掘金的 石衫的架构笔记
附上借鉴的所有链接:
大白话聊聊Java并发面试问题之Java 8如何优化CAS性能?【石杉的架构笔记】
大白话聊聊Java并发面试问题之谈谈你对AQS的理解?【石杉的架构笔记】
大白话聊聊Java并发面试问题之公平锁与非公平锁是啥?【石杉的架构笔记】
谈到公平锁和非公平锁,首先要引入2个概念。一个是CAS,一个是AQS。
CAS:全名叫做CompareAndSet,顾名思义就是先比较再往里面塞值。
AQS:全名是AbstractQueuedSynchronizer,中文叫抽象队列同步器。他是ReentrantLock中的一个基类。借鉴一下别人的图
这两个关键字都是用于java中的并发处理。
CAS其实对于volatile关键字是经常提及的,volatile的可见性其实就是CAS的实现。
线程1去修改变量以后,会强制刷新主内存,线程2再去取data变量的时候,会先拿自己的虚拟内存和主内存进行比较,如果不一样的话自己的虚拟内存会自动失效,并且主动去主内存中重新读取,然后再对data进行修改。更多关于volatile的相关原理需要涉及到JMM的指令重排,内存屏障。
众所周知,vloatile修饰的变量是原子性的,但是操作并不是原子操作。当我们需要对一个Integer的类进行多线程叠加时,如果仅仅是用volatile命名变量时,在多线程里面对线程进行 ++ 操作的话,实际的值会小于预期值。
private volatile int a = 0;
// 多线程里面的代码
@Override
public void run(){
//实现逻辑
a++;
}
如图所示,a最后的输出结果会小于预期值,就是因为a++的操作不是原子性所导致的。所以解决的方式有两种。
1:引入synchronized关键字,然后锁住代码块,但是只是++操作的话用synchronized会显示代码太重。
2:多线程concurrent包下的Atomic类。比如 AtomicInteger 的操作。
实际上,Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性
但是Atomic类也有自己的缺陷。因为Atomic类的实现原理是CAS,就是说线程1操作了a数据,这时候线程B和线程C同时去拿数据,线程B拿完数据并进行了++操作,然后通过CAS改变了a的值,这时候线程C也通过++操作,当执行CAS时候发现compare的值和自己当时拿的值不同,所以线程C在CAS操作时失败了,就需要重新获取a的值再进行如上的操作,这是3个线程的情况,如果在线程多的情况下,线程C如果人品很差,就有可能一直在进行CAS操作,所以就陷入了无限的循环之中,直到没有其他线程和他进行争抢了,他最后一个执行了CAS的操作改变了a的值。
根据上述的陈述,由此可见,Atomic类会造成大量的空循环线程,一定意义上会影响性能。于是java8中推出了一个新的类用于解决这个问题 LongAdder 我个人理解为他的实现原理和 ForkJoin 是一样的,就是把一个任务拆分为多个同时进行,最后再将所有结果进行统一的计算。一定程度上解决了空循环的问题。
下面说一说AQS:
当考虑到并发问题时,我们会用到 ReentrantLock 来加锁和释放锁。那么 ReentrantLock是如何实现加锁和解锁的呢。
ReentrantLock lock = new ReentrantLock();
lock.lock();
//逻辑代码
lock.unlock();
AQS内部有3个对象,一个是state(用于计数器,类似gc的回收计数器),一个是线程标记(当前线程是谁加锁的),一个是阻塞队列(用于存放其他未拿到锁的线程);
首先,比如线程A操作了lock()方法,会先通过CAS将state赋值为1,然后将该锁标记为 线程A加锁。
当线程A还未释放锁时,线程B来请求,会查询锁标记的状态,因为当前的锁标记为 线程A,线程B未能匹配上,所以线程B会加入阻塞队列,直到线程A触发了 unlock() 方法,这时候线程B才有机会去拿到锁,但是不一定肯定是线程B先取到
如果线程A触发了unlock()方法,会先将state减1变为0,当state为0的时候会将锁的状态清楚,并且state变成null,此时线程B可以拿到锁。
state的赋值可能大于1,是因为 ReentrantLock 为可重入锁,所以他的计数机制类似于gc的计数回收算法。
以上就是AQS的机制
————————————————————————————————————————————————————————
现在说到公平锁和非公平锁。
在问到synchronized和ReentrantLock的区别时,都会提及到 公平锁和非公平锁 。这个可以自行百度下
ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获锁。
我们刚才提及到AQS中,如果线程A正处于lock状态,线程B进来时发现线程A处于lock状态,会自动进入阻塞队列,等待取锁。
当线程C也进来了也发现线程A处于lock状态,也会自动进入阻塞队列。那么下次加锁到底是线程B先拿到还是线程C先拿到呢。
ReentrantLock有个构造方法用于设置锁的公平性,如果我们仅仅是new了一个ReentrantLock的话,那么就是非公平性的,就是靠自己去争取,完全的随机性。如果我们在new ReentrantLock(true) 加入 true参数时,就会遵循先入先出的原则,保证了锁的公平性。
以上就是从上述的文章中总结的内容以及一些自己的小见解了。
如果错误之处欢迎指出。