不得不说的 Java "锁" 事

2020-05-15  本文已影响0人  JackDaddy

相信每一个在Java 海洋里畅游的水手们都听过 "锁" 这个东西,今天我们就来解开它的神秘面纱。

首先我们要知道 "锁" 是什么?

锁是在多线程情况下为了保证数据的安全性而衍生出来的,保证数据在每个线程中是一致的。在这里我们要记住一点 "锁" 是和 多线程是联系在一起的,只有在多线程情况下才有意义。

互斥 与 同步

1)首先我们要记住,互斥与同步是两种特性,而锁是一种东西,两者是不同的,他们的联系是锁具有了互斥/同步的特性。
2)同样地,互斥与同步也是发生在多线程的情况下。
3)互斥:指的是多个线程抢占资源,其中某一个线程持有了资源(锁),不让其他资源继续持有,等待执行完之后再释放资源(锁)。
4)同步:同样也是多个线程使用资源,但多个线程是按照一定的顺序持有资源,按照规定好的顺序。
互斥与同步的最大区别就是是否按照顺序持有资源。(大多数情况下,同步是建立在互斥的基础上)

而在 Java 中存在着许多锁,下面就来一一解释他们的区别: 锁分类

对象锁 VS 类锁

private synchronized void testA() {
        synchronized (this) {

        }
    }
private synchronized void testB() {

    }
private static synchronized void testA() {

    }
---------------------------------------------------------------------------------------------------
 private void testA() {
        synchronized (MyClass.class) {

        }
    }

乐观锁 VS 悲观锁

1)悲观锁:指的是在操作数据时,认为会有其他线程来修改数据,因此在操作数据前都会加锁。
2)乐观锁:指的是在操作数据时,认为不会有其他线程来修改数据,因此不会进行加锁,整个过程是,先对数据进行操作,然后判断是否有其他线程修改了数据,如果其他线程没有修改数据则把操作完的数据成功写入;如果有其他线程修改了数据,则抛出错误或者进行重试(再次修改,然后进行判断)
因此:

public final int getAndAddInt(Object o, long offset, int delta) {
  int v;
  do {
      v = getIntVolatile(o, offset);
  } while (!compareAndSwapInt(o, offset, v, v + delta));
  return v;
}

而正是因为封装成了一个原子操作也才可以实现无锁,让多线程下变量同步。
在通过CAS实现多线程下变量同步的过程中会出现 3 个比较严重的问题:

自旋锁 VS 适应性自旋锁

在介绍自旋锁之前我们要知道一些知识:
在对多线程中进行变量同步时,我们通常会使用锁来进行使其他线程阻塞,当获得到锁的时候时唤醒线程,也就是我们经常听到的CPU上下文切换,阻塞与唤醒(上下文切换)其实是非常耗费性能的。
如果在某些同步线程中要执行的逻辑又是很简单,此时如果再进行上下文切换,就有可能让用户等待上下文切换的时间比真正执行任务的时间还长。
因此同步资源的锁定时间比较短,而为了这小段时间进行线程切换,线程挂起和恢复,对系统来说是非常得不偿失的。
在两个或多线程允许并行执行的情况下,当前线程不放弃CPU的执行时间,进行一个自旋等待,等待前面线程会释放锁(资源),从而实现不切换线程而获锁(资源),这就是所谓的自旋锁
自旋锁本身也是会耗费性能的,他虽然节省了线程切换的开销,但同时占用了CPU的时间。如果占用的时间较短,性能会非常好,但如果占用较长,仍然需要使用锁机制,这也就是为什么仍然需要使用存在"锁"的原因。

一般来说,自旋的默认次数是10,当超过了次数后,没有获取锁,就应当挂起线程。

适应性自旋锁

从 JDK6 中引入了适应性自旋锁,从名字可以看出,自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定

公平锁 VS 非公平锁

1)公平锁:在多线程中,每个线程按照顺序获取锁对象,其他线程阻塞线程,按照顺序进行阻塞与唤醒,新进的线程排在队列尾部。

ReentrantLock reentrantLock = new ReentrantLock(true);

--------------------------------------------------------------------------------------------
//以下是ReentrantLock 的构造函数,从这里可以看出,当传入为true时,ReentrantLock 是公平锁
public ReentrantLock(boolean var1) {
        this.sync = (ReentrantLock.Sync)(var1 ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync());
    }

可重入锁 VS 不可重入锁

所谓可重入锁,也可以理解为递归锁,就是持有相同锁对象的一系列流程可以反复调用同一把个锁对象。在底层维持有一个计数器,每获取一次锁对象,计数器+1。

private synchronized void testB() {
        System.out.println("test---B");
        testE();
    }

    private synchronized void testE() {
        System.out.println("test---E");
    }

可以看出,这两个方法都是持有的相同一把对象锁,同时又因为是可重入锁,因此,可以执行到 testE。如果是不可重入锁,因为锁对象一开始被testB 持有,此时再调用testE,则会进行等待,从而造成死锁。
同时前面说到的ReentrantLock 也是属于可重入锁。

以上就是对Java中常见锁的一些讲解,希望对你有所帮助

上一篇下一篇

猜你喜欢

热点阅读