java锁机制介绍

java锁

乐观锁与悲观锁

乐观锁与悲观锁都是一种锁机制。用于实现线程同步机制。

  • 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。适用于数据频繁修改的场景。

  • 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。

java中的锁机制

  • synchronized

    在java5之前,使用关键字synchronized修饰一个方法或者代码块对临界资源的保护实现多线程并发同步。有四种不同的同步块:

    • 实例方法
    • 静态方法
    • 实例方法中的同步块
    • 静态方法中的同步块
    synchronized(lockObject) { 
        //需要访问的临界资源 
    }
    
  • ReentrantLock

    Java5 java.util.concurrent.locks 提供了Lock接口。Lock提供了更加灵活的锁实现。写法一般如下:

    lock.lock();
    try {
        //需要访问的临界资源 
    } finally {
        lock.unlock();
    }
    

    获取锁的方式:

    • lock() 阻塞持续等待直到获取锁
    • tryLock() 方式获得锁,默认不阻塞,不等待,如果能够获取锁返回true,可以设置等待时间。
    • lockInterruptibly() 中断锁, 使用中断锁在等待获取锁的时候,会响应线程中断。

    在Java1.5中,synchronized是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。到了Java1.6,synchronized进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的Java1.7与1.8中,均对该关键字的实现机理做了优化。

    两种锁在低并发的情况下性能都差不多,更推荐使用synchronized,毕竟不用怕忘记在finnaly中执行unlock()。

    但在高并发量的条件下,synchronized性能会迅速下降几十倍,因为多了很多自旋锁操作,而ReentrantLock的性能却能依然维持一个水准。

    当线程通过synchronized等待锁时是不能被Thread.interrupt()中断的

  • Semaphore

    • 使用方法与ReentrantLock相似。
    • acquire()方法默认为可响应中断锁,与ReentrantLock.lockInterruptibly()作用效果一致。
    • 支持多个临界资源,而ReentrantLock只支持一个临界资源
  • Atom系列原子类
    public class AtomicInteger extends Number implements java.io.Serializable {
        private static final long serialVersionUID = 6214790243416807050L;
        /**
         * Atomically adds the given value to the current value.
         *
         * @param delta the value to add
         * @return the updated value
         */
        public final int addAndGet(int delta) {
            return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
        }
    }
    

    通过CAS实现,是乐观锁的一种。所以性能比上面的高。

  • StampedLock
    java8 java.util.concurrent.locks新增的一个API。

    ReentrantReadWriteLock有很多缺点:

    • 有可能导致线程饥饿,在读多写少的模型,锁获取的时候是随机分配的,很可能导致写线程一直获取不到write lock。
    • 无法将读锁升级为写锁
    • 不支持乐观读

    StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。

    method tryConvertToWriteLock(long)attempts to “upgrade” a mode, returning a valid write stamp if

    • (1) already in writing mode
    • (2) in reading mode and there are no other readers or
    • (3) in optimistic mode and the lock is available.
    //读锁升级为写锁
    void moveIfAtOrigin(double newX, double newY) { // upgrade
         // Could instead start with optimistic, not read mode
         long stamp = sl.readLock();
         try {
           while (x == 0.0 && y == 0.0) {
             long ws = sl.tryConvertToWriteLock(stamp);
             if (ws != 0L) {
               stamp = ws;
               x = newX;
               y = newY;
               break;
             }
             else {
               sl.unlockRead(stamp);
               stamp = sl.writeLock();
             }
           }
         } finally {
           sl.unlock(stamp);
         }
    }
    

非阻塞算法

在轻度到中度的争用情况下,非阻塞算法的性能会超越阻塞算法,因为 CAS 的多数时间都在第一次尝试时就成功,而发生争用时的开销也不涉及线程挂起和上下文切换,只多了几个循环迭代。没有争用的 CAS 要比没有争用的锁便宜得多(这句话肯定是真的,因为没有争用的锁涉及 CAS 加上额外的处理),而争用的 CAS 比争用的锁获取涉及更短的延迟。

在高度争用的情况下(即有多个线程不断争用一个内存位置的时候),基于锁的算法开始提供比非阻塞算法更好的吞吐率,因为当线程阻塞时,它就会停止争用,耐心地等候轮到自己,从而避免了进一步争用。

  • 优势:以硬件代替JVM的锁,锁的粒度更加细,带来了良好的性能。
  • 劣势:需要冒险,用循坏来实现失败重试,甚至有可能因为内存复用机制,带来ABA问题。随着临界资源的增加,编程的复杂度急剧上升。
    //非阻塞算法堆栈
    public class ConcurrentStack<E> {
        AtomicReference<Node<E>> head = new AtomicReference<Node<E>>();
        public void push(E item) {
            Node<E> newHead = new Node<E>(item);
            Node<E> oldHead;
            do {
                oldHead = head.get();
                newHead.next = oldHead;
            } while (!head.compareAndSet(oldHead, newHead));
        }
        public E pop() {
            Node<E> oldHead;
            Node<E> newHead;
            do {
                oldHead = head.get();
                if (oldHead == null) 
                    return null;
                newHead = oldHead.next;
            } while (!head.compareAndSet(oldHead,newHead));
            return oldHead.item;
        }
        static class Node<E> {
            final E item;
            Node<E> next;
            public Node(E item) { this.item = item; }
        }
    }
    

##乐观锁ABA问题

比较并交换(compare and swap, CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。

ABA问题是无锁结构实现中常见的一种问题,可基本表述为:

1. 进程P1读取了一个数值A
2. P1被挂起(时间片耗尽、中断等),进程P2开始执行
3. P2修改数值A为数值B,然后又修改回A
4. P1被唤醒,比较后发现数值A没有变化,程序继续执行。

对于P1来说,数值A未发生过改变,但实际上A已经被变化过了,继续使用可能会出现问题。

参考链接: