Java-多线程中的锁


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

乐观锁

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

是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创建对象来获取和释放锁。

猜你喜欢

转载自blog.csdn.net/tuziud233/article/details/109039173