Java“锁”的深度解析

线程安全的定义:保证多线程环境下共享修改数据的正确性。
线程安全的几个基本特性:
a. 原子性即相关操作的过程中不会被其他线程打断,通常通过同步机制实现;
b. 可见性,线程修改某个共享变量其变更能够立即被其他变量知晓,通常解释为讲共享变量反应到主存上,通常通过cpu指令保证;
c. 有序性,保证线程内串行语义即java happens-before语义,通常通过插入内存屏障防止指令重排序;

Java并发编程的本质是不同线程间通过共享内存进行通信和同步。


Java规范中对于数据竞争的定义如下:
a. 在一个线程中写一个变量;
b. 在另一个线程中读同一个变量;
c. 而且写和读之间没有通过同步来排序。

1.volatile、锁、final 与 happens-before

1.1volatile

volatile使变量具有可见性与原子性,即对任意读操作总能看到任意线程对该变量最后的写入;读\写操作具备原子性,注意a++这种复合操作不具备原子性。
volatile读的内存语义:读一个volatile变量时,线程对本地内存置为无效,从主内存中读取共享变量。
volatile写的内存语义:写一个volatile变量时,JMM会把本地线程中的值刷新到主内存。
JMM通过限制指令重排以实现以上内存语义,编译器在生成字节码时会插入内存屏障来禁止特定类型的处理器重排序。JRS-133特意增强了volatile的内存语义,确保volatile写-读与锁的释放获取具有相同的内存语义。
参照:《Java 理论与实践:正确使用 Volatile 变量》
IBM Developer

1.2 锁

当线程释放锁时JMM会把该线程的本地内存中的共享变量刷新到主内存中;当线程获取锁时JMM会把该线程对应的本地内存置为无效。从而使被监视器保护的临界区代码必须从主内存获取共享变量。
锁得获取与实现会在下文中重点介绍。

1.3 final

对于final遵循以下两个重排序规则:
1.在构造函数内对一个final域的写入与随后把这个被构造对象的应用赋值给一个引用变量的操作不能重新排序。
2.初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作不允许重排。
写final域的重排规则可以确保:对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具备这个保证

1.4 happens-before

理解happens-before是理解JMM的关键,JMM提供两方面保证

1.为程序员提供足够强的内存可见性。

2.对编译器和处理器的限制尽可能的放松。

重点理解一下volatile变量规则:这是一条比较重要的规则,它标志着volatile保证了线程可见性。通俗点讲就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的。即写操作的结果对读操作可见。

2.java对象头与monitor模式

2.1 java对象头(这段是复制的别人的)

Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
四种锁状态对应的的Mark Word内容:

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

2.2 Monitor

Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。Monitor对象是同步的基本实现单元。
Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
要深入理解java的锁机制必须先理解Monitor Object 设计模式。否则很难看懂上面这段话。
我们在开发并发的应用时,经常需要设计这样的对象,该对象的方法会在多线程的环境下被调用,而这些方法的执行都会改变该对象本身的状态。为了防止竞争条件 (race condition) 的出现,对于这类对象的设计,需要考虑解决以下问题:
a. 在任一时间内,只有唯一的公共的成员方法,被唯一的线程所执行。
b. 对于对象的调用者来说,如果总是需要在调用方法之前进行拿锁,而在调用方法之后进行放锁,这将会使并发应用编程变得更加困难。合理的设计是,该对象本身确保任何针对它的方法请求的同步被透明的进行,而不需要调用者的介入。
c. 如果一个对象的方法执行过程中,由于某些条件不能满足而阻塞,应该允许其它的客户端线程的方法调用可以访问该对象。
我们使用 Monitor Object 设计模式来解决这类问题:将被客户线程并发访问的对象定义为一个 monitor 对象。客户线程仅仅通过 monitor 对象的同步方法才能使用 monitor 对象定义的服务。为了防止陷入竞争条件,在任一时刻只能有一个同步方法被执行。每一个 monitor 对象包含一个 monitor 锁,被同步方法用于串行访问对象的行为和状态。此外,同步方法可以根据一个或多个与 monitor 对象相关的 monitor conditions 来决定在何种情况下挂起或恢复他们的执行。

3.Synchronized 与 ReentrantLock

Synchronized是java内建的同步机制,他提供了互斥语义和可见性保证机制。Synchronized代码块是有一对monitorenter/monitorexit指令实现的。ReentrantLock通常翻译为重入锁,语义上与Synchrogazernized基本相同。
ReentrantLock可以设置公平性, 期带参构造函数可以指定公平性。而Synchronized只能采用不公平的策略。

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

在通常的场景中,公平性并没有想象的那么重要,java默认的调度策略中“饥饿”很少发生。与此同时引入公平性会引入额外的开销,自然也会导致吞吐量下降。
ReentrantLock相比Synchronized提供了更多精细的锁操作,如带超时的锁请求、判断是否有线程或特定线程在排队、相应中断。
从性能角度上,Synchronized的早起实现没有引入偏向锁和自选锁的概念因而比较低效,java6中做了非常多的改性,参考性能对比在高竞争情况下,ReentrantLock仍然具有性能的优势。
java6以后JVM提供了三种不同的Monitor实现,也就是通常说的三种锁即:偏向锁、轻量级锁、重量级锁。没有竞争出现时JVM默认会使用偏向锁,JVM利用CAS操作在对象头中的MarkWord部分设置线程ID以表示该对象偏向于该线程,所以并不涉及真正的互斥锁。当另外一个线程试图竞争已经被偏向的锁时,JVM就会升级为轻量级锁,轻量级锁依赖CAS操作MarkWord来试图获取锁,当重试一定次数或者跟多的竞争来到,JVM会进一步升级为重量级锁。JVM进入安全点(SafePoint)时,会检查是否有限制的Monitor,然后试图进行锁降级。
ReentrantLock的实现机制与Synchronized是不一样的:

public class ReentrantLock implements Lock, java.io.Serializable {
    /**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer{......}
    /**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync{......}
    /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync{......}
    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
}

通过以上源码我们可以理解重入锁的机制,其中包含了一个同步器和两种实现方式。

/**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

这段代码是非公平锁获取锁的过程:根据线程状态使用CAS操作获取锁,如果获取失败会将线程放入等待队列。
AQS中维护了一个同步状态status来计数重入次数,status初始值为0。这个值时volatile的经典实用方式。
当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。
这里我特别想强调条件变量(java.util.concurrent.Condition),如果说ReentrantLock是synchronized的替代选择,Condition则是将wait、notify、notifyAll等操作转化为相应的对象,将复杂而晦涩的同步操作转变为直观可控的对象行为。条件变量最为典型的应用场景就是标准类库中的ArrayBlockingQueue等。

顺便在这里写一下java其他类型的锁:

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    /** Performs all synchronization mechanics */
    final Sync sync;
    /**
     * Creates a new {@code ReentrantReadWriteLock} with
     * default (nonfair) ordering properties.
     */
    public ReentrantReadWriteLock() {
        this(false);
    }
    /**
     * Creates a new {@code ReentrantReadWriteLock} with
     * the given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }
    /**
     * Synchronization implementation for ReentrantReadWriteLock.
     * Subclassed into fair and nonfair versions.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {......}
    /**
     * Nonfair version of Sync
     */
    static final class NonfairSync extends Sync{......}
    /**
     * Fair version of Sync
     */
    static final class FairSync extends Sync {......}
     /**
     * The lock returned by method {@link ReentrantReadWriteLock#readLock}.
     */
    public static class ReadLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -5992448646407690164L;
        private final Sync sync;
        /**
         * Constructor for use by subclasses
         *
         * @param lock the outer lock object
         * @throws NullPointerException if the lock is null
         */
        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
    }
     /**
     * The lock returned by method {@link ReentrantReadWriteLock#writeLock}.
     */
    public static class WriteLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -4992448646407690164L;
        private final Sync sync;

        /**
         * Constructor for use by subclasses
         *
         * @param lock the outer lock object
         * @throws NullPointerException if the lock is null
         */
        protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
    }
}

读锁和写锁的锁主体都是Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。
这里需要特别注意一下AQS里的state(int 32位)在独享锁中这个值通常是0或者1(如果是重入锁的话state值就是重入的次数),在共享锁中state就是持有锁的数量。但是在ReentrantReadWriteLock中有读、写两把锁,所以需要在一个整型变量state上分别描述读锁和写锁的数量(或者也可以叫状态)。于是将state变量“按位切割”切分成了两个部分,高16位表示读锁状态(读锁个数),低16位表示写锁状态(写锁个数)。

获取读写锁:

protected final boolean tryAcquire(int acquires) {
            /*
             * Walkthrough:
             * 1. If read count nonzero or write count nonzero
             *    and owner is a different thread, fail. (如果读数不是0或者写线程不是当前线程,失败)
             * 2. If count would saturate, fail. (This can only
             *    happen if count is already nonzero.) 写锁数量超限 及>2^16-1 返回失败
             * 3. Otherwise, this thread is eligible for lock if
             *    it is either a reentrant acquire or
             *    queue policy allows it. If so, update state
             *    and set owner.
             */
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

每次着这种大神写的代码我都想跪下来看,仔细阅读上面的代码的时候你会好奇setExclusiveOwnerThread为什么没有采用任何同步机制?很快你就会发现:

/**
     * Sets the thread that currently owns exclusive access.
     * A {@code null} argument indicates that no thread owns access.
     * This method does not otherwise impose any synchronization or
     * {@code volatile} field accesses.
     * @param thread the owner thread
     */
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

线程在release会设置为null
获取读锁的代码就不再这里解释了,想聊的同学可以留言。

4.java.util.concurrent

首先来仔细参拜一下这张经典的图:

  1. java.util.concurrent提供了比synchronized更高级的同步结构,CountDownLatch、CyclicBarrier、Semaphore(可以作为资源管理器限制同步工作的线程数)

Semaphore,Java版的信号量实现,接下来看一下Semaphore的源码实现:

/**
     * Creates a {@code Semaphore} with the given number of
     * permits and nonfair fairness setting.
     *
     * @param permits the initial number of permits available.
     *        This value may be negative, in which case releases
     *        must occur before any acquires will be granted.
     */
    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }
abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 1192457210091910933L;

        Sync(int permits) {
            setState(permits);
        }

        final int getPermits() {
            return getState();
        }

        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

        protected final boolean tryReleaseShared(int releases) {
            for (;;) {
                int current = getState();
                int next = current + releases;
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next))
                    return true;
            }
        }

CountDownLatch允许一个或多个线程等待某些操作完成,CountDownLatch的操作组合是countDown/await。调用await的线程阻塞等待countDown足够的次数,不管在一个线程还是多个线程里面countDown只要次数达到即可。CountDownLatch操作的是事件。

CyclicBarrier是一种辅助的同步结构,允许多个线程等待到达某个屏障,CyclicBarrier的基本操作组合是await,当所有parties都调用了await时,才回继续执行任务并自动重置。注意,正常情况下,CyclicBarrier的重置都是自动发生的,如果我们调用了reset方法,但是线程在等待,就会导致线程被打扰抛出BrokenBarrierException。CyclicBarrier侧重的是线程而不是调用事件,他的典型应用场景是用来等待并发线程结束。

  1. java.util.concurrent提供了各种线程安全的容器,ConcurrentHashMap、有序的ConcurrentSkipListMap,通过类快照机制实现线程安全的动态数组CopyOnWriteArrayList、CopyOnWriteArraySet
  2. 各种并发队列实现BlockedQueue实现ArrayBlockingQueue、SynchorousQueue或针对特定场景的PriorityBlockingQueue
    ArrayBlockingQueue:有边界队列
    下面这段代码是ArrayBlockingQueue的构造方法:
/**
     * Creates an {@code ArrayBlockingQueue} with the given (fixed)
     * capacity and the specified access policy.
     *
     * @param capacity the capacity of this queue
     * @param fair if {@code true} then queue accesses for threads blocked
     *        on insertion or removal, are processed in FIFO order;
     *        if {@code false} the access order is unspecified.
     * @throws IllegalArgumentException if {@code capacity < 1}
     */
    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

获取方法:

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

插入:

/**
     * Inserts element at current put position, advances, and signals.
     * Call only when holding lock.
     */
    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal();
    }

当队列为空时,试图take的线程的正确行为应该是等待入队发生,而不是直接返回,这是BlockingQueue的语义,使用条件notEmpty就可以优雅地实现这一逻辑。通过signal/await的组合,完成了条件判断和通知等待线程,非常顺畅就完成了状态流转。注意signal和await成对调用非常重要,不然假设只有await动作,线程会一直等待直到被打断(interrupt)。

LinkedBlockingQueue 默认边界是Integer.MAX_VALUE
类似ConcurrentLinkedQueue等,则是基于CAS的无锁技术,不需要在每个操作时使用锁,所以拓展性更加优异。

SynchronousQueue是一个奇葩的队列,其内部容量为1每个删除操作都在等待插入,每个插入操作也要等待删除动作。java6中做了非常大的改动,利用CAS替换掉了原有的锁操作,它是Executors.newCachedThreadPool()的默认队列。
PriorityBlockingQueue是个无边界优先级队列,严格意义上讲大小收到操作系统资源影响
考虑应用场景中对队列边界的要求。ArrayBlockingQueue是有明确的容量限制的,而LinkedBlockingQueue则取决于我们是否在创建时指定,SynchronousQueue则干脆不能缓存任何元素。
从空间利用角度,数组结构的ArrayBlockingQueue要比LinkedBlockingQueue紧凑,因为其不需要创建所谓节点,但是其初始分配阶段就需要一段连续的空间,所以初始内存需求更大。
通用场景中,LinkedBlockingQueue的吞吐量一般优于ArrayBlockingQueue,因为它实现了更加细粒度的锁操作。
ArrayBlockingQueue实现比较简单,性能更好预测,属于表现稳定的“选手”。
如果我们需要实现的是两个线程之间接力性(handoff)的场景,可能会选择CountDownLatch,但是SynchronousQueue也是完美符合这种场景的,而且线程间协调和数据传输统一起来,代码更加规范。可能令人意外的是,很多时候SynchronousQueue的性能表现,往往大大超过其他实现,尤其是在队列元素较小的场景。

  1. 强大的Executor框架,可以创建不同类型的线程池,调度任务运行,绝大多数情况下,我们不需要自己实现线程池或任务调度器(推荐使用ThreadPoolExecutor来实现线程池)

  2. 精解AQS
    这里做一个源码底层的精解

总结

volatile使用的关键在于理解“变量真正独立于其他变量和自己以前的值”。
好东西往往写在最后讲,AQS是java并发包的基础,@author Doug Lea也是大师的呕心之作

这篇文章的写作经历了几个艰苦难熬的晚上,如果哪位仁兄能在这个浮躁的世界花费很长的时间读完我的这篇枯燥乏味难懂的文章,请收下我的膝盖。

欢迎大家关注&留言提问。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值