从锁的思想到Java主流锁分析

乐观锁和悲观锁

乐观锁和悲观锁是一种概念,他们的区别主要是在对待线程同步时的态度。java

  • 悲观锁认为本身在使用数据时必定存在其余线程在修改数据,因此它在使用数据前会先加上锁,待到使用完毕释放锁资源。Java中,synchronized关键字和Lock的实现类都属于悲观锁。
  • 反之乐观锁则认为在使用数据时不会有线程修改数据,因此它不会添加锁,只是在更新数据时判断是否有线程修改了数据。若是数据没有被更新,则当前线程成功将数据写入。若是数据被更新了,则会根据实现方式不一样执行不一样的处理(报错 or 重试)。Java中,最多见的乐观锁实现就是CAS原子类。

正是由于乐观锁和悲观锁的不一样,他们所适用的场景天然不同,算法

  1. 乐观锁适合于读多写少的场景,无锁的设计能大幅提升并发效率。
  2. 悲观锁则适合写多读少场景,使用前先加锁能保证数据安全。

使用

//=============== 悲观锁 ===============
//synchronized
public synchronized void test() {
    //须要同步的资源
}

/** * ReentrantLock * 须要保证多线程操做的是同一个锁 */
ReentrantLock lock = new ReentrantLock();
public void test1() {
    lock.lock();
    try {
        //须要同步的资源
    } finally {
        lock.unlock();
    }
}

//=============== 乐观锁 ===============
/** * 须要保证多线程操做的是同一个AtomicInteger */
AtomicInteger atomicInteger = new AtomicInteger(0);
public void test2() {
    atomicInteger.getAndIncrement();
}
复制代码

经过上述使用方式咱们能够总结出,悲观锁都是经过显式调用去获取锁从而同步数据,可是为何乐观锁不须要显示的获取锁也一样能同步数据呢。这里就要谈谈什么是CAS。安全

CAS (compare and swap)

从字面意思上看,即比较和交换,是一种无锁的算法。便可以在不须要加锁的状况下,实现多线程变量同步。在Java中,atomic包下的原子类们是CAS的一系列实现多线程

其中,咱们就以最多见的AtomicInteger分析,源码以下,并发

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            //反射获取AtomicInteger类中value值的偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    //经过volatile关键字防止cpu指令重排序
    //使value对全部线程可见
    private volatile int value;
    
    ...
    
    public final int getAndIncrement() {
        //实际调用的是Unsafe.getAndAddInt
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
}

//Unsafe类
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    //经过循环重试比较新值与旧值,直到二者相等说明此时数据未被其余线程修改,以后更新内存中的变量值
    do {
        var5 = this.getIntVolatile(var1, var2);
    //compareAndSwapInt这个方法是native方法具体分析见底下
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

//native方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
复制代码

可见实际的CAS操做的实现是在native层的compareAndSwapInt()中,JNI里是借助于CPU指令cmpxchg完成的,该指令是一个原子操做。显然,能够保证变量的可见性。高并发

具体CPU的cmpxchg指令作的事情是,比较寄存器中的A和内存中的值V。ui

  1. 若是相等,把要写入的新值 B 存入内存中。
  2. 若是不相等,将内存值 V 赋值给寄存器中的值 A。

以后经过上述的do-while循环再次调用cmpxchg指令进行重试,一直到更新成功为止。this

CAS带来的问题

CAS这种算法虽然很是高效,但也存在问题。atom

  1. ABA问题,由于CAS在更新变量前须要先检查变量是否能够更新,此时若是将变量A更新成B随后立马又更新成A。那么显然存在一种状况致使CAS认为变量没有变化,但实际是有变化的(线程安全策略变得不可靠)。解决办法能够将变量每次的更新记录一个版本号,即1A-2B-3A,这样CAS作compare的时候就不会出现变量已更新却被误判为未更新的状况了。
  2. 循环策略致使CPU开销高。

公平锁和非公平锁

  • 公平锁,线程按照申请锁的顺序来持有锁。优势是等待的线程不会饥饿,但缺点是吞吐效率比非公平锁降低。除了获取锁的线程,其他线程处于阻塞状态,并且CPU作线程唤醒的开销很大。
  • 非公平锁,线程获取锁是无序的,存在线程插队获取到锁的状况。优势是吞吐效率高,由于线程有概率不被阻塞就获取到了锁,但缺点可能会致使线程一直等待,处于饥饿状态。

ReentrantLock中的公平锁与非公平锁

public class ReentrantLock implements Lock, java.io.Serializable {
    ...
    public ReentrantLock() {
        //可见ReentrantLock默认使用的是非公平锁
        sync = new NonfairSync();
    }
    ...
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    
    //非公平锁的实现
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
    
        final void lock() {
            ...
        }
    
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    
    //公平锁的实现
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            ...
        }
        
        protected final boolean tryAcquire(int acquires) {
            ...
        }
    }
}
复制代码

咱们观察到实际获取锁的逻辑在tryAcquire方法中,咱们对NonfairSyncFairSync中(左为FairSync)该方法作横向比较来看看他们的区别是什么,spa

除了增长了hasQueuedPredecessors之外没有什么不一样,

public final boolean hasQueuedPredecessors() {
    ...
    Node t = tail;
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
复制代码

该方法主要是判断当前线程是否位于同步队列中的第一个。若是是则返回true,不然返回false。

可重入锁和不可重入锁

  • 可重入锁,在外层方法获取了锁后,若是内部调用的方法也须要获取锁,那么会自动获取(必须为同一个锁)。不会由于外层方法获取到的锁没有被释放掉而被阻塞。可重入锁的特色就是能够在必定程度上避免产生死锁。
  • 同理不可重入锁则不容许出现上述状况,好比不能使用它作递归操做。

独享锁和共享锁

  • 独享锁又名互斥锁。该锁一次只能被一个线程所持有,得到锁的线程能同时进行读写操做。Java中synchronizedLock的实现类都属于互斥锁。
  • 共享锁,该锁能够被多个线程持有,若是一个变量A被线程加了共享锁,则以后的线程也只能加共享锁。而且得到共享锁的线程只能写,不能读。

Java中ReentrantReadWriteLock类实现了互斥锁与共享锁,以下

ReentrantReadWriteLock有两把锁,ReadLock读锁,是共享锁,WriteLock写锁,是互斥锁。

相关文章
相关标签/搜索