Java锁架构

java提供了很多锁的实现,有的是在jvm层面实现的,有的是在rt.jar中实现的。今天我们来大致讲解下有哪些锁,以及简单的思想。

乐观锁 & 悲观锁

对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。

而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

悲观锁代码:

static class IntOfPessimistic{
	volatile int val ;
	
	public synchronized int get() {
		return val;
	}
	public synchronized void add(int a) {
		val = val + a;
	}
}

乐观锁代码:

static class IntOfOptimistic{
	volatile int val ;
	
	public int get() {
		return val;
	}
	public void add(int a) {
		//cmpxchgl为汇编指令,能实现原子性比较并且交换,当前cpu寄存器的值和预期值val是否相等,相等代表没有被其他线程修改过,就进行修改成a,否则失败
		while(!cmpxchgl(val , a)) {
			//不相等,代表有其他线程修改过,我们继续等待(总会刷新当前线程的val为最新值 JMM理论)
		}
		val = val + a;
	}
}

这个cmpxchgl 就是我们java的CAS操作,通过CAS我们实现了不用加锁代码。

CAS详解见这篇文章:

罗政:CAS原理 LongAdder思想

自旋锁 & 适应性自旋锁

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

自旋锁实现:

public class SpinLock implements Lock {
    /**
     *  相当于 锁监视器
     */
    private AtomicReference<Thread> owner = new AtomicReference<>();
    /**
     * reentrant count of a thread, no need to be volatile
     */
    private int count = 0;

    @Override
    public void lock() {
        Thread t = Thread.currentThread();
        // 如果拿锁的就是自己
        if (t == owner.get()) {
            ++count;
            return;
        }
        //别的线程进来,需要自旋等待,直到拿锁的线程释放owner 
        while (owner.compareAndSet(null, t)) {
        }
    }
    @Override
    public void unlock() {
        Thread t = Thread.currentThread();
        if (t == owner.get()) {
            if (count > 0) {
                // reentrant count not zero, just decrease the counter.
                --count;
            } else {
                // compareAndSet is not need here, already checked
                owner.set(null);
            }
        }
    }
}

owner 相当于锁监视器,当线程A拿锁成功就会通过CAS把owner 置为线程A,拿锁成功。线程B拿锁时,owner 不等于B,就会进行 自旋等待 ,直到线程A释放锁,将owner 置为null。

适应性自旋锁

就是自旋锁,通过智能计算出自选的次数。自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

无锁 &偏向锁 &轻量级锁 & 重量级锁

这些都是synchronized的实现,都是在JVM层面实现的,看这篇文章就知道:

罗政:synchronized 原理 锁升级 详解

公平锁 & 非公平锁

锁等待队列

当线程A首先进入临界区拿锁成功正常执行,但是在A没有退出临界区解锁时,线程B,C依次进入临界区,由于拿不到锁就要等待,此时就要有个等待队列来管理线程B,C 。

公平锁: 线程A释放锁,会拿最先进入等待队列的线程B,去执行临界区,然后再是C 。

非公平: 线程A释放锁,线程B会去进行竞争拿锁,如果此时有线程D进来,线程D是有可能竞争拿锁成功的,也就是说,我等了这么久,当到我时,可能会有一个人突然插过来。

公平锁和非公平锁在 ReentrantLock 中有实现,请看这篇文章:

罗政:AQS (3) ReentrantLock 公平锁与非公平锁

可重入锁 & 非可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

请看代码:

public class Widget {
    public synchronized void doSomething() {
        System.out.println("方法1执行...");
        doOthers();
    }

    public synchronized void doOthers() {
        System.out.println("方法2执行...");
    }
}

在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。

如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。

ReentrantLock 怎么实现可重入的:

罗政:AQS (1) ReentrantLock 的上锁 与 解锁 源码

独享锁 & 共享锁

独占锁 被某个线程持有时,其他线程只能等待当前线程释放后才能去竞争锁,而且只有一个线程能竞争锁成功。就是我们的 悲观锁。

共享锁 是可以被共享的,它可以被多个线程同时持有。如果一个线程获取共享锁成功,那么其他等待的线程也会去获取共享锁,而且获取大概率会成功。共享锁典型的有ReadWriteLock、CountdownLatch。

CountdownLatch共享锁解析请见:

罗政:AQS (2) 共享锁 CountDownLatch 源码分析


强烈推荐一个 进阶 JAVA架构师 的博客

JAVA架构师修炼

支付宝打赏 微信打赏

如果文章对您有帮助,您可以鼓励一下作者