Java——乐观锁和悲观锁

目录

  • 乐观锁
    • CAS
  • 悲观锁
    • synchronized
    • synchronized的局限性
    • Lock
    • ReadWriteLock
    • 锁升级和锁降级

当一个数据被多个线程所共同使用,且线程并发执行时,我们需要保证保证该数据的准确性,既一个线程对数据的操作不会对另一个线程产生不合理的影响。
实现的手段基本上是对数据加锁,当线程要对数据进行操作时必须获得锁后再进行操作。锁可分为乐观锁和悲观锁。

乐观锁

乐观锁,总是乐观地假设最好的情况,每次去拿数据的时候都认为别人不会修改这个数据,所以不会上锁,只会要对数据进行更新时判断一下在此期间(拿到数据到更新的期间)别人有没有去更改这个数据,可以使用版本号机制和CAS算法实现。

CAS

  • CAS(Compare And Swap)是一种常见的“乐观锁”,大部分的CPU都有对应的汇编指令,它有三个操作数:内存地址V,旧值A,新值B。只有当前内存地址V上的值是A,B才会被写到V上,否则操作失败。
  • Java从5.0开始引入了对CAS的支持,与之对应的是 java.util.concurrent.atomic 包下的AtomicInteger、AtomicReference等类,它们提供了基于CAS的读写操作和并发环境下的内存可见性。

以AtomicInteger为例看看底层是怎么进行操作的

AtomicInteger integer=new AtomicInteger(123);
int a=integer.addAndGet(321);//+321
System.out.println(a);//结果为444

上面编写了一个示例,创建一个AtomicInteger对象,调用它的addAndGet方法,此方法是加上一个数并返回相加后的结果。然后我们来看看这个方法的源码。

public final int addAndGet(int delta) {
    return U.getAndAddInt(this, VALUE, delta) + delta;
}

可以看到这个方法的返回值调用了U(Unsafe对象)的getAndInt方法来获取当前对象在内存中的值
下面是getAndInt方法的源码

@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);//获取对象中offset偏移地址对应的整型field的值
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}

可以看到逻辑就是若weakCompareAndSetInt的返回值为false则不断的获取整形值field
下面是weakCompareAndSetInt的源码

@HotSpotIntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset,
                                          int expected,
                                          int x) {
    return compareAndSetInt(o, offset, expected, x);//比较当前内存中的值和期望值x是否相等
}

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

synchronized

Java中的关键字,是由JVM来维护的。是JVM层面的锁。
是非公平锁。

  • 同步代码块
    sychronized可用于修饰一个代码块,当线程想要执行代码块中的代码时必须先取得锁对象
    格式:synchronized(锁对象){…}
  • 同步方法
    在方法的返回值前加上synchronized关键字,线程想要执行改方法必须获得锁。
    修饰实例方法:获得的锁默认是this(当前对象)。
    修饰静态方法:获得的锁默认是当前类。

synchronized的局限性

如果获取锁的线程由于要等待一些原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待。
当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。采用synchronized则会导致一个线程在进行读操作,其他线程会等待此线程读完。
综上所述,下synchronized十分的影响效率,上述的这些问题通过使用Lock可以解决。
另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

Lock

是JDK5以后才出现的接口。使用Lock是调用对应的API。是API层面的锁
在创建对象时从构造方法传入true可创建公平锁,不传入默认是不公平锁。
相较synchronized的自动获得和释放锁,Lock需要手动获得和释放锁。

  • 获取锁
    • Lock()方法
      就是用来获取锁。如果锁已被其他线程获取,则进行等待。
    • tryLock()方法
      此表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,这个方法会立即返回true或false。在拿不到锁时不会一直在那等待。
    • tryLock(long time, TimeUnit unit)方法
      与tryLock()方法类似,区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
    • lockInterruptibly()方法
      此方法优先考虑响应中断,而不是响应锁的获取。也就是说如果线程获取不到锁则可以通过调用interrupt()方法中断线程。
  • 释放锁
    • unLock()方法

Lock是一个接口,一般使用它的实现类ReentrantLock创建对象来获取和释放锁。

ReadWriteLock

ReadWriteLock是一个接口,用来获取只读的锁和写锁。

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

实现类有ReentrantReadWriteLock。
通过ReentrantReadWriteLock的readLock()和WriteLock()方法获取读锁。

/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;

public static class ReadLock implements Lock, java.io.Serializable {......}
public static class WriteLock implements Lock, java.io.Serializable {......}

public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

可通过以下的方式获得锁实例

Lock lock=reentrantReadWriteLock.readLock();
Lock lock=reentrantReadWriteLock.writeLock();

获得锁的方法和释放锁的方法与Lock接口中使用方式相同。

写锁获取锁:
如果读取锁定和写入锁定都不被另一个线程保持并立即返回,则将获取写入锁定。
如果当前线程已经保持此锁定,则该方法立即返回。
如果锁由另一个线程持有,那么当前线程将被禁用以进行线程调度,并且在发生以下两种情况之一之前处于休眠状态:

  • 写锁定由当前线程获取;
  • 一些其他线程interrupts当前线程。

读锁获取锁:
如果写锁定未被另一个线程保持并立即返回,则获取读锁定。
如果写锁定由另一个线程保持,则当前线程将被禁用以进行线程调度,并且在获取读取锁定之前处于休眠状态。

锁升级和锁降级

锁降级:从写锁变成读锁;锁升级:从读锁变成写锁。
ReentrantReadWriteLock支持锁升级,不支持锁降级
下面是一段测试锁升级,代码在还没释放写锁的情况下去申请读锁。

public static void main(String[] args) {
    ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock();
    System.out.println(3);
    reentrantReadWriteLock.writeLock().lock();
    System.out.println(2);
    reentrantReadWriteLock.readLock().lock();
    System.out.println(1);
}

结果如下:

Java——乐观锁和悲观锁_第1张图片
上面代码不会产生死锁。会发生锁降级,从写锁降级成读锁。但没有正确的释放写锁,不会自动释放当前线程获取的写锁,仍然需要显示的释放,否则别的线程永远也获取不到写锁。

下面是测试锁升级的代码,在读锁还没释放的情况下去申请写锁

public static void main(String[] args) {
    ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock();
    System.out.println(3);
    reentrantReadWriteLock.readLock().lock();
    System.out.println(2);
    reentrantReadWriteLock.writeLock().lock();
    System.out.println(1);
}

结果如下:
Java——乐观锁和悲观锁_第2张图片
上面的测试代码会产生死锁,因为ReentrantReadWriteLock是不支持锁升级的。

你可能感兴趣的:(Java,java,并发编程)