重量级锁

如果你学过多线程,那么你肯定知道锁这个东西,至于为什么需要锁,我就不给你普及了,就当做你是已经懂的了。

我们知道,我们要进入一个同步、线程安全的方法时,是需要先获得这个方法的锁的,退出这个方法时,则会释放锁。如果获取不到这个锁的话,意味着有别的线程在执行这个方法,这时我们就会马上进入阻塞的状态,等待那个持有锁的线程释放锁,然后再把我们从阻塞的状态唤醒,我们再去获取这个方法的锁。

这种获取不到锁就马上进入阻塞状态的锁,我们称之为重量级锁。

公平锁和非公平锁。

公平锁:是指按照申请锁的顺序来获取锁,
非公平所:线程获取锁的顺序不一定按照申请锁的顺序来的。

//默认是不公平锁,传入true为公平锁,否则为非公平锁
ReentrantLock reentrantLock =  new ReetrantLock();

共享锁和独享锁

独享锁:一次只能被一个线程所访问
共享锁:线程可以被多个线程所持有。
ReadWriteLock 读锁是共享锁,写锁是独享锁。

乐观锁和悲观锁。

乐观锁:对于一个数据的操作并发,是不会发生修改的。在更新数据的时候,会尝试采用更新,不断重入的方式,更新数据。
悲观锁:对于同一个数据的并发操作,是一定会发生修改的。因此对于同一个数据的并发操作,悲观锁采用加锁的形式。悲观锁认为,不加锁的操作一定会出问题,

分段锁

1.7及之前的concurrenthashmap。并发操作就是分段锁,其思想就是让锁的粒度变小。

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价

自旋锁

我们知道,线程从运行态进入阻塞态这个过程,是非常耗时的,因为不仅需要保存线程此时的执行状态,上下文等数据,还涉及到用户态到内核态的转换。当然,把线程从阻塞态唤醒也是一样,也是非常消耗时间的。

刚才我说线程拿不到锁,就会马上进入阻塞状态,然而现实是,它虽然这一刻拿不到锁,可能在下 0.0001 秒,就有其他线程把这个锁释放了。如果它慢0.0001秒来拿这个锁的话,可能就可以顺利拿到了,不需要经历阻塞/唤醒这个花时间的过程了。

然而重量级锁就是这么坑,它就是不肯等待一下,一拿不到就是要马上进入阻塞状态。为了解决这个问题,我们引入了另外一种愿意等待一段时间的锁 --- 自旋锁。

自旋锁就是,如果此时拿不到锁,它不马上进入阻塞状态,而是等待一段时间,看看这段时间有没其他人把这锁给释放了。怎么等呢?这个就类似于线程在那里做空循环,如果循环一定的次数还拿不到锁,那么它才会进入阻塞的状态。

至于是循环等待几次,这个是可以人为指定一个数字的。

自适应自旋锁

上面我们说的自旋锁,每个线程循环等待的次数都是一样的,例如我设置为 100次的话,那么线程在空循环 100 次之后还没拿到锁,就会进入阻塞状态了。

而自适应自旋锁就牛逼了,它不需要我们人为指定循环几次,它自己本身会进行判断要循环几次,而且每个线程可能循环的次数也是不一样的。而之所以这样做,主要是我们觉得,如果一个线程在不久前拿到过这个锁,或者它之前经常拿到过这个锁,那么我们认为它再次拿到锁的几率非常大,所以循环的次数会多一些。

而如果有些线程从来就没有拿到过这个锁,或者说,平时很少拿到,那么我们认为,它再次拿到的概率是比较小的,所以我们就让它循环的次数少一些。因为你在那里做空循环是很消耗 CPU 的。

所以这种能够根据线程最近获得锁的状态来调整循环次数的自旋锁,我们称之为自适应自旋锁。

轻量级锁

上面我们介绍的三种锁:重量级、自旋锁和自适应自旋锁,他们都有一个特点,就是进入一个方法的时候,就会加上锁,退出一个方法的时候,也就释放对应的锁。

之所以要加锁,是因为他们害怕自己在这个方法执行的时候,被别人偷偷进来了,所以只能加锁,防止其他线程进来。这就相当于,每次离开自己的房间,都要锁上门,人回来了再把锁解开。

这实在是太麻烦了,如果根本就没有线程来和他们竞争锁,那他们不是白白上锁了?要知道,加锁这个过程是需要操作系统这个大佬来帮忙的,是很消耗时间的,。为了解决这种动不动就加锁带来的开销,轻量级锁出现了。

轻量级锁认为,当你在方法里面执行的时候,其实是很少刚好有人也来执行这个方法的,所以,当我们进入一个方法的时候根本就不用加锁,我们只需要做一个标记就可以了,也就是说,我们可以用一个变量来记录此时该方法是否有人在执行。也就是说,如果这个方法没人在执行,当我们进入这个方法的时候,采用CAS机制,把这个方法的状态标记为已经有人在执行,退出这个方法时,在把这个状态改为了没有人在执行了。

显然,比起加锁操作,这个采用CAS来改变状态的操作,花销就小多了。

然而可能会说,没人来竞争的这种想法,那是你说的而已,那如果万一有人来竞争说呢?也就是说,当一个线程来执行一个方法的时候,方法里面已经有人在执行了。

如果真的遇到了竞争,我们就会认为轻量级锁已经不适合了,我们就会把轻量级锁升级为重量级锁了。

所以轻量级锁适合用在那种,很少出现多个线程竞争一个锁的情况,也就是说,适合那种多个线程总是错开时间来获取锁的情况。

偏向锁

偏向锁就更加牛逼了,我们已经觉得轻量级锁已经够轻,然而偏向锁更加省事,偏向锁认为,你轻量级锁每次进入一个方法都需要用CAS来改变状态,退出也需要改变,多麻烦。

偏向锁认为,其实对于一个方法,是很少有两个线程来执行的,搞来搞去,其实也就一个线程在执行这个方法而已,相当于单线程的情况,居然是单线程,那就没必要加锁了。

不过毕竟实际情况的多线程,单线程只是自己认为的而已了,所以呢,偏向锁进入一个方法的时候是这样处理的:如果这个方法没有人进来过,那么一个线程首次进入这个方法的时候,会采用CAS机制,把这个方法标记为有人在执行了,和轻量级锁加锁有点类似,并且也会把该线程的 ID 也记录进去,相当于记录了哪个线程在执行。

然后,但这个线程退出这个方法的时候,它不会改变这个方法的状态,而是直接退出来,懒的去改,因为它认为除了自己这个线程之外,其他线程并不会来执行这个方法。

然后当这个线程想要再次进入这个方法的时候,会判断一下这个方法的状态,如果这个方法已经被标记为有人在执行了,并且线程的ID是自己,那么它就直接进入这个方法执行,啥也不用做

你看,多方便,第一次进入需要CAS机制来设置,以后进出就啥也不用干了,直接进入退出。

然而,现实总是残酷的,毕竟实际情况还是多线程,所以万一有其他线程来进入这个方法呢?如果真的出现这种情况,其他线程一看这个方法的ID不是自己,这个时候说明,至少有两个线程要来执行这个方法论,这意味着偏向锁已经不适用了,这个时候就会从偏向锁升级为轻量级锁。

所以呢,偏向锁适用于那种,始终只有一个线程在执行一个方法的情况哦