vlambda博客
学习文章列表

Java之线程并发的各种锁、锁、锁

因为两周没更新了...

也不是懒,这两周确实有些忙,赶项目进度赶的不亦乐乎Java之线程并发的各种锁、锁、锁...

终于赶在工期前,可以进入内测了,我也有了些时间,可以更新啦...


    线程并发锁是很常见的问题,而且在Java中锁的类型、概念、使用场景等等也是面试必问的,所以今天就来先简单的说一说线程并发中常用的一下锁。



0 1

  公平锁




    何为公平锁?就字面来理解,它就是一种有公平机制,并且不会因为你有任何的“背景”、“关系”就可以为所欲为的锁。在并发环境下,每个线程在获取锁时先会看该锁维护的等待队列,如果为空,或者当前线程时等待队列的第一个,就占有锁,否则就会加入到等待队列中,此后所有的线程都是以FIFO(先进先出)的规则来执行。

    在Java中提供了公平锁的实现类,先看个代码demo:

Java之线程并发的各种锁、锁、锁

打印值:

Java之线程并发的各种锁、锁、锁

可以看到线程获取到锁一定和启动保持一致的,另外ReentrantLock有参构造默认是true,也就是开启公平锁,

还有一点,代码中有虚线的问题,是因为我创建线程是显式的,alibaba检测编码规约是提示,必须使用线程池的方式创建线程,避免出现OOM。

Java之线程并发的各种锁、锁、锁


优点:

    那公平锁的有点也就一目了然了:所有的线程在CPU执行调度的时候,都能够得到资源,不至于有些线程因为抢占不到资源,而一直无法被执行。

缺点:

    有优点就必然有缺点:既然可以有序的执行,那肯定就会降低吞吐量,队列里面除了第一个线程,其它线程将会全部阻塞,CPU唤醒阻塞线程的开销会很大。



0 2

非公平锁



    何为非公平锁?同样字面意思理解,就是一种不公平机制的锁,随机抢占资源,不跟你整那一套先到先得的规则。线程加锁时先尝试获取锁,获取不到就自动到队尾等待。

上述同样的代码,把ReentrantLock的有参构造改成false,执行看下打印值:

Java之线程并发的各种锁、锁、锁

非公平锁时随机抢占的机制,所以也会出现公平锁的情况,所以测试时候不用纠结这个。

优点:

    非公平的性能肯定是高于公平锁性能的,非公平锁能更充分的利用CPU的时间片,尽量的减少CPU空闲的状态时间。

缺点:

    既然是随机抢占资源的,那就肯定会有队列中间的线程一直获取不到锁或长时间获取不到锁,导致“饿死”。



0 3

可重入锁




          何为可重入锁:可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,但前提是同一个对象或同一个类中,这样的锁就叫可重入锁。

    上面两种锁介绍的都是ReentrantLock实现的,而在真正的使用中保证线程安全synchronized也是不可缺少的,这两者都是可重入锁。

Java之线程并发的各种锁、锁、锁

父方法和子方法同时输出了线程的名称,表明即使递归使用synchronized也没有发生死锁,证明其是可重入的。


那这两者有什么区别呢?


0 4

ReentrantLock和synchronized区别




1,synchronized依赖于JVM,ReentrantLock依赖于API,需要lock()和unlock()方法配合try/finally语句块来实现。


2,synchronized使用比较简单方便,并且有编译器去保证锁的加锁和释放,而ReentrantLock需要手动声明加锁和释放锁,因此ReentrantLock锁的粒度和灵活度要高于synchronized


3,synchronized在优化以前,性能方面和ReentrantLock是差很多的,但是在JDK1.6之后synchronized引入了偏向锁和自旋锁,两者的性能就差不多了,官方甚至都建议使用synchronized。其实synchronized的优化感觉就是采用ReentrantLock的CAS技术(这里不要有歧义:ReentrantLock底层是基于AQS实现的,但AQS的本质其实就是volatile+CAS)。都是试图在用户态就把加锁的问题解决,避免进入内核态的线程阻塞。



0 5

读写锁




    读写锁实际上就一种特殊的自旋锁,读写锁的规则可以共享读,但只能一个写,读读不互斥、读写互斥、写写互斥,而一般的独占锁是啥都互斥,而我们实际场景中,读一定是大于写的,一般情况下独占锁的效率低来源于高并发下对临界区的资源抢占导致线程上下文切换。因此当并发不是很高的情况下,读写锁需要额外的维护读锁的状态,可能还不如独占锁的效率高,所以要根据实际场景而定。(本篇不深入将方法的实现和源码)



0 6

synchronized JDK1.6优化之后的锁




6.1  偏向锁

        当线程执行到临界区时,会利用CAS操作,将线程ID插入到Mark Word(对象头中的标记子),同时修改偏向锁的标志位。

    所谓临界区,就是只允许一个线程进去执行操作的区域,即同步代码块,cas是一个原子性的操作。

    这个锁会偏向于第一个获得它的线程,在接下来的执行过程中,假如该锁没有被其它线程所获取,没有其它线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。也就是说,在此线程之后的执行过程中,如果再次进入或者退出同一段同步块代码,并不再需要去进行加锁或者解锁操作。


6.2 锁膨胀

    综合偏向锁,当出现有两个线程来竞争的话,那么偏向锁就失效了,此时锁就会膨胀,升级为轻量锁,这就是我们经常说的锁膨胀。


6.3 锁撤销

    当偏向锁失效,就得把该锁撤销,锁撤销的开销花费是很大的,大概的过程:

  1. 在一个安全点停止拥有锁的线程。

  2. 遍历线程栈,如果存在锁记录的话,需要修复锁记录和Markword,使其变成无锁状态。

  3. 唤醒当前线程,将当前锁升级成轻量级锁。
    所以,如果某些同步代码块大多数情况下都是有两个及以上的线程竞争的话,那么偏向锁就会是一种累赘,对于这种情况,我们可以一开始就把偏向锁这个默认功能给关闭

6.4 自旋锁(jdk1.4.2引入的)

    自旋锁属于轻量级锁。所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程阻塞,直到那个获得锁线程释放锁之后,这个线程就可以马上获取到锁。

    注意:锁在原地循环的时候,是会消耗CPU的,就相当于在循环一个啥也没有的for循环。所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短很短的时间就能够获得锁了。

经验表明,大部分同步代码块执行的时间都是很短很短的,也正是基于这个原因,才有了轻量级锁这么个东西。


自旋锁存在的问题:

1,如果同步代码块执行的很慢,需要消耗大量的时间,这个时候,其它线程在原地等待空消耗的CPU,这点就会有问题。

2,本来一个线程把锁释放之后,当前线程是能够获得锁的,但是假如这个时候好几个线程都在竞争这个锁的话,那么可能当前线程会获取不到锁,还在原地”转圈圈“,转到怀疑人生。

那基于这个问题,我们就需要给线程空循环设置一个值,当线程超过了这个次数,我们就认为继续使用自旋锁就不合适了,一直原地空循环,这谁受得了。此时锁就会再次膨胀,升级为重量级锁。


默认情况下,自旋的次数为10次,当然可以通过-XX:PreBlockSpin来进行更改。


6.5 自适应自旋锁

    所谓自适应自旋锁就是线程空循环等待的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数。
其大概原理是这样的:
    假如一个线程1刚刚成功获得一个锁,当它把锁释放了之后,线程2获得该锁,并且线程2在运行的过程中,此时线程1又想来获得该锁了,但线程2还没有释放该锁,所以线程1只能自旋等待,但是虚拟机认为,由于线程1刚刚获得过该锁,那么虚拟机觉得线程1这次自旋也是很有可能能够再次成功获得该锁的,所以会延长线程1自旋的次数。
另外,如果对于某一个锁,一个线程自旋之后,很少成功获得该锁,那么以后这个线程要获取该锁时,是有可能直接忽略掉自旋过程,直接升级为重量级锁的,以免空循环等待浪费资源。


轻量级锁也被称为非阻塞同步、乐观锁,因为这个过程并没有把线程阻塞挂起,而是让线程空循环等待,串行执行。


本篇着重讲的是一下名词的概念,之后会对于ReentrantLock的源码解析来真正的理解这些锁的底层实现原理,因为知其然先知其所以然啦。


- End -