跳到主要内容

11、Java JUC 源码分析 - 锁的概述

1.1公平锁与非公平锁

根据线程获取锁的机制,所可以分为公平锁和非公平锁。如果我们不看概念,光从字面上理解,其实也并不难理解。也就是一个锁对于线程来说是公平,另一个是不公平的。公平锁表示线程获取锁的顺序,是按照线程请求锁的时间来决定的。也就是先到先得。最早请求锁的就最早获取这把锁。但是非公平所就不一定了,非公平锁在运行时闯入,也就是先来不一定先得。

下面举一个很简单的例子:加入线程A已经持有了锁,这时候线程B请求该锁,那么线程B就会被挂起。当线程A释放所以后,假如这个时候线程C也需要这把锁。如果采用非公平锁,根据线程调度策略,线程B和线程C都有可能获得锁。但是如果采用公平锁,那么只有线程B会得到这把锁,线程C会挂起,因为线程B先请求的锁。

一般在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销

那么如何实现公平锁和非公平锁呢?ReentrantLock给我们提供了公平和非公平的实现。

ReentrantLock pairLock = new ReentrantLock(true);//公平锁
ReentrantLock pairLock = new ReentrantLock(false);//非公平锁
ReentrantLock pairLock = new ReentrantLock();//非公平锁

1.2独占锁与共享锁

根据锁只能被单个线程持有还是能被多个线程共同持有,所可以分为独占锁和共享锁。从字面上可以很快速的理解这两个锁。

独占锁保证了,不管在什么时候都只有一个线程能够得到锁,ReentrantLock就是以独占方式实现的。共享锁则可以由多个线程持有,例如ReadWriteLock读写锁,他允许一个资源可以被多个线程同时进行读操作。

独占锁其实是一种悲观锁,由于每次访问资源都要先加上互斥锁,这才限制了并发性,因为读操作并不会影响数据的一致性,而独占锁只允许在同一时间有一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取。

而共享锁是一种乐观锁,他放宽了加锁的条件,允许多个线程同时进行读操作。

1.3可重入锁

接下来我们要说可重入锁。那么什么是可重入锁呢?当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,那么当一个线程再次获取他自己已经获取到的锁的时候呢?他是否会被阻塞?如果不被阻塞,那么我们说这个锁是可重入的,也就是该线程获得了这把锁,那么他可以无限次数的进入被该锁锁住的代码。(Talk is cheap. Show me the code.

public class Hello {
   
     
    public synchronized void helloA(){
   
     
        System.out.println("helloA");
    }
    public synchronized void helloB(){
   
     
        System.out.println("helloB");
        helloA();
    }
}

如上代码,如果我们调用B方法,我们就要先获得Hello这个类的内置锁,然后打印输出helloB。之后调用helloA方法,在调用前会获取内置锁,如果内置锁不是可重入的,那么调用线程将会一直被阻塞。(这里肯定会有很多人疑惑,明明是两个synchronized,但是为什么是同一把锁。因为synchronized修饰实例方法,作用于当前实例,也就是锁住的是hello的实例对象,也就是说这两个方法用的同一把锁)

实际上,synchronized是可重入锁。可重入锁原理是在锁内部维护一个线程标示,用来标示当前锁被哪个线程占用,然后在关联一个计数器。一开始计数器值为0,说明该锁没有被任何线程占用。当一个锁获取了该锁时,计数器就会变为1,这时其他线程再来获取该锁时就会发现该锁的所有者不是自己,因而被阻塞挂起。

但是获取了该锁的线程再次获取所示发现拥有者是自己,就会把计数器的值+1,当释放锁之后计数器的值-1。当计数器值为0时,锁里面线程标识被重置为null,这时被阻塞的线程就会唤醒来竞争该锁。

1.4自旋锁

自旋锁就更加简单了。我们知道,当一个线程获取锁失败以后,会被切换到等待队列,而当这个线程获得到了这把锁的时候,才会进入就绪态,然后等待CPU调度选取,开始执行。但是这里就会产生一个问题,线程状态这样切换带来的开销是很大的,在一定程度上是会影响并发性能的。而这个时候自旋锁就派上了用场。自旋锁是:当线程自获取锁的时候,如果发现所已经被其他线程占有,他不会马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取(默认是10次,可以使用-XX:PreBlockSpinsh参数进行设置),很有可能在后面几次尝试中,其他线程已经释放了这把锁,如果其他线程没有释放锁,那么他还是会进入等待队列。

自旋锁其实是在用CPU时间去换线程阻塞与调度的开销,但是如果多次尝试后,锁没有释放那么CPU的时间就会被白白浪费。所以各有利弊。

1.5悲观锁和乐观锁

乐观锁和悲观锁其实是数据库中引入的名词,但是在并发包锁里面也引入类似的思想,所以在这里还是有必要提一下。

悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先对数据进行加锁,并在真个数据处理过程中,使数据处于锁定状态。悲观锁的实现往往依靠数据库提供的锁机制,即在数据库中,在数据记录操作前给数据记录加排它锁。

而乐观锁就不同了,他认为数据在一般情况下不会发生冲突,所以在访问记录前一般是不会加排它锁的,但是要对数据进行更新的时候,才会正式对数据冲突与否进行检测。

11篇博客,结束编程的基础篇,之后的博客会以JUC包的源码为主,喜欢的话点个赞喽。