Java并发知识概述
1.Java内存模型的抽象结构
Java中,所有的实例、静态域和数组元素都存储在堆内存中,堆内存是线程共享的。局部变量,形参,异常处理参数不会在线程之间共享,所以不存在内存可见性问题,也就不受内存模型的影响。
Java之间的通信由JMM控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。 JMM定义了线程和主内存之间的抽象关系: 线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该变量读、写共享变量的副本,本地内存只是JMM的一个抽象的概念,并不真实存在。JMM抽象示意图如下所示:
如果线程A和线程B之间要通信的话,需要经历下面两个步骤:
(1)线程A把本地内存A中更新过的共享变量刷新到主内存中去;
(2)线程B到主内存中去读取线程A之前已更新过的共享变量;
2.happens-before规则
JSR-133使用happens-before的概念来指定两个操作之间的执行顺序 。 由于这两个操作可以在一个线程之内, 也可以是在不同的线程之间 。 因此, JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证 (如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行, 但JMM向程序员保证a操作将对b操作可见)
- 程序顺序规则:一个线程中的每个操作,happens-before于随后该线程中的任意后续操作
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的获取
- volatile变量规则:对一个volatile域的写,happens-before于对这个变量的读
- 传递性:如果A happens-before B,B happens-before C,那么A happens-before C
- start规则:如果线程A执行线程B的start方法,那么线程A的ThreadB.start()happens-before于线程B的任意操作
- join规则:如果线程A执行线程B的join方法,那么线程B的任意操作happens-before于线程A从TreadB.join()方法成功返回。
3.volatile
Java支持多个线程同时访问一个对象或者对象的成员变量, 由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存实在共享内存中,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特征),所以程序在执行的过程中, 一个线程看到的变量并不一定是最新的 。
volatile写的内存语义 : 当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量值刷新到主内存 。
volatile读的内存语义 : 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效, 线程接下来将从主内存读取共享变量 。
4.CAS(Compare And Set)
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
5.Lock接口
lock接口提供了与synchronized关键字类似的同步功能,只是在使用时需要显示的获取和释放锁。虽然它缺少了隐式获取释放锁的便捷性,但是却拥有了锁获取和锁释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
使用synchronized关键字将会隐式地获取锁,也就是先获取再释放。当然,这种方式简化了同步的管理,可是扩展性没有显示的锁获取和释放好。例如,针对一个场景,手把手的进行锁的获取和释放,先获取锁A,然后再获取锁B,当获取锁B后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取D,以此类推。这种场景下,synchronized关键字就不那么容易实现了,而使用Lock却容易许多。
Lock接口提供的synchronized关键字不具备的主要特性如下:
(1)尝试非阻塞的获取锁。 没有获取到马上返回 ;
(2)能被中断地获取锁。 能响应中断;
(3)超时获取锁。 指定的时间内没有获取锁则返回 ;
Lock是一个接口,它定义了锁的获取和释放的基本操作,API如下图:
5.队列同步器
队列同步器AbstractQueuedSynchronizer,是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作 。
AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的三个方法:
getState(),setState(int newState),和compareAndSetState(int expect,int update)来进行操作,因为它们能够保证状态的改变是安全的。 子类推荐定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式的获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便的实现不同类型的同步组件 。
5.1 队列同步器的接口与示例
同步器的设计是基于模板方法模式,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法 。
重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或者修改同步状态 。
(1) getState() : 获取当前的同步状态 。
(2)setState(int newState) : 设置当前同步状态 ;
(3)compareAndSetState(int expect , int update) : 使用CAS设置当前状态,该方法能够保证状态设置的原子性 。
同步器可以重写的方法与描述如下:
(1)protected boolean tryAcquire(int arg) : 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态 ;
(2)protected boolean tryRelease(int arg) : 独占式的释放同步状态,等到获取同步状态的线程将有机会获取同步状态 ;
(3)protected int tryAcquierShared(int arg) : 共享式获取同步状态,返回大于0 的值表示获取成功,否则获取失败 ;
(4)protected boolean tryReleaseShared(int arg) : 共享式释放同步状态 ;
(5)protected boolean isHeldExclusively() : 当前同步器是否在独占模式下被线程占用, 一般该方法表示是否被当前线程独占 ;
· 实现自定义同步组件时,将会调用同步器提供的模板方法,这些模板方法与描述如下:
可以看出,同步器提供的模板方法基本上分为3类 : 独占式获取与释放同步状态 、 共享式获取与释放同步状态 、 查询同步队列中等待线程的情况 ; 自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义 。
下面看一个独占锁的示例,独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继线程才能够获取锁 :
public class Mutex implements Lock, java.io.Serializable { /** * 继承同步器的静态内部类,通过实现同步器提供的抽象模板方法管理同步状态 * * */ private static class Sync extends AbstractQueuedSynchronizer { // 是否处于占用状态 protected boolean isHeldExclusively() { return getState() == 1; } //当状态为0时获取锁 public boolean tryAcquire(int acquires) { if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } // 释放锁,将状态设置为0 protected boolean tryRelease(int releases) { if (getState() == 0) throw new IllegalMonitorStateException(); setExclusiveOwnerThread(null); setState(0); return true; } // 返回一个Condition , 每个Condition都包含了一个condition队列 Condition newCondition() { return new ConditionObject(); } // Deserialize properly private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); setState(0); // reset to unlocked state } } // 仅需将操作代理到Sync上即可 private final Sync sync = new Sync(); public void lock() { sync.acquire(1); } public boolean tryLock() { return sync.tryAcquire(1); } public void unlock() { sync.release(1); } public Condition newCondition() { return sync.newCondition(); } public boolean isLocked() { return sync.isHeldExclusively(); } public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); } public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } }
上述示例中,Mutex是一个简单的自定义同步组件,它在同一时刻只允许一个线程占有锁,Mutex中定义了一个静态内部类,继承了同步器,并实现了同步器的获取和释放同步状态的抽象方法,。 在 tryAcquire(int arg) 中, 如果经过CAS设置同步状态成功,则代表该线程获取了同步状态,也就是获得了锁,而在方法tryRelease(int release) 方法中,只需要将同步状态设置为0即表示释放同步状态,释放同步状态不需要CAS保证原子性,因为此刻只有获取了同步状态的线程在操作 。 在Mutex的实现中,以调用lock()为例,只需要在方法实现中调用同步器的模板方法acquire(int args) 即可, 当前线程如果获取同步状态失败后会被加入到同步队列中等待, 这样就大大降低了实现一个可靠自定义同步组件的门槛。
5.2同步器实现分析
1. 同步队列
同步器内部依靠一个先进先出的同步队列完成对同步状态的管理,该同步队列基于链表实现,当前线程获取同步状态失败的时候,同步器会将当前线程和等待信息构造成一个节点,添加到同步队列的尾部,, 同时会阻塞当前线程 ; 当同步状态释放时, 会把队列的首节点中的线程唤醒使其再次尝试获取同步状态 。
同步队列中的节点用来保存获取同步状态失败的线程的引用、等待状态以及前驱和后继节点,节点的属性类型与名称以及描述如下所示 :
static final class Node { static final Node SHARED = new Node(); static final Node EXCLUSIVE = null; static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3; /** * 等待状态 。 * 包含如下状态 。 * (1)CANCELLED , 值为1 , 由于在同步队列中等待的线程等待超时或者被中断, * 需要从同步队列中取消等待,节点进入该状态将不会发生变化 。 * (2)SIGNAL , 值为-1 , 后继节点的线程处于等待状态, 而当前节点的线程如 * 果释放了同步状态或者被取消,将会通知后继节点,是后继节点的线程得以运行 。 * (3)CONDITION , 值为-2 , 节点在等待队列中, 节点线程等待在Condition上, * 当其他线程对Conditiond调用了signal()方法后,该节点将会从等待队列中 * 转移到同步队列中,加入到对同步状态的获取 。 * (4)PROPAGATE , 值为-3 , 表示下一次共享式同步状态获取将会无条件的被传播下去 。 */ volatile int waitStatus; /** * 前驱节点,当前节点加入同步队列时被设置 */ volatile Node prev; /** * 后继节点 */ volatile Node next; /** * 获取同步状态的线程 */ volatile Thread thread; /** * 等待队列中的后继节点。 如果当前节点是共享的, 那么这个字段僵尸一个SHARED常量, 也就是说节点类型(独占和共享)和等待队列中的后继节点公用一个字段 */ Node nextWaiter; }
节点是构成同步队列(和等待队列)的基础, 同步器将拥有首节点和尾节点 ,没有成功获取同步状态的线程将会成为节点加入该队列的尾部,同步队列的基本结构可如下表示 :
同步器只包含了两个节点的引用, 一个指向队列的头节点, 一个指向尾节点, 。 当一个线程成功的获取了同步状态(或者锁), 其他线程将无法获取同步状态,转而被构造成节点加入到同步队列的尾部 , 而这个加入的过程必须要保证线程安全 , 因为同步器提供了一个基于CAS的设置尾节点的方法 : compareAndSetTail(Node except , Node update) , 它需要传递当前线程 “ 认为” 的尾节点 , 和当前节点, 只有设置成功后, 当前节点才正式与之前的尾节点建立关联 。
加入过程如下所示 :
同步队列遵循FIFO 原则, 首节点是获取同步状态成功的节点, 首节点的线程在释放同步状态时, 将会唤醒后继节点, 而后继节点将会在获取同步状态成功时将自己设置为首节点, 该过程如下图 :
设置首节点是通过获取同步状态成功的线程来完成的 , 由于只有一个线程获取到同步状态 , 因此设置头结点的方法并不需要CAS来保证, 它只需要将首节点设置成为原首节点并断开原首节点的next引用即可 。
2 .独占式同步状态获取与释放
通过调用同步器的acquire(int arg)方法可以获取同步状态 , 该方法对中断不敏感, 也就是由于线程获取同步状态失败后进入同步队列中 , 后续对线程进行中断操作时, 线程不会从同步队列中移除, 该方法代码如下 :
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
上述代码完成了同步状态的获取、节点构造 、 加入同步队列 中自旋等待的工作 。 主要过程为 :调用自定义组件 子类重写的方法tryAcquire(arg) , 该方法保证线程安全的获取同步状态(CAS保证) , 如果同步状态获取失败, 则构造节点,
Node.EXCLUSIVE标记为独占式,同一时刻只能有一个线程获取到同步状态, 并通过addWaiter(Node node)方法将该节点加入到队列的尾部, 然后调用acquireQueued(Node node , int arg)方法,使该节点以自旋获取同步状态(死循环),如果获取不到则阻塞节点中的线程,
而被阻塞的线程唤醒则是依靠前驱节点的出队或阻塞线程被中断来实现 。
下面分析源代码, 首先是节点的构造以及加入同步队列, 代码如下 :
private Node addWaiter(Node mode) { //把当前线程构造为节点 Node node = new Node(Thread.currentThread(), mode); //快速尝试在尾部添加 Node pred = tail; // 获取尾节点 if (pred != null) { node.prev = pred; //把新构造的节点的前驱引用指向尾节点 //CAS设置新构造的节点为尾节点 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //快速设置失败,自旋 enq(node); return node; }
private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
节点进入同步队列之后, 就进入了一个自旋的过程, 每个节点(或者说每个线程) 都在自我观察, 当条件满足, 获取到了同步状态, 就可以从这个自旋的过程退出, 否则依旧留在这个自旋过程中(会阻塞节点的线程) , 代码如下所示 :
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
释放同步状态 :
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
总结: 在获取同步状态时 , 同步器会维护一个同步队列, 获取状态失败的线程会被加入到队列的尾部 并在队列中自旋 ; 移出队列(或停止自旋)的条件是前驱节点为首节点并且获取了同步状态 。 在释放同步状态时, 同步器调用tryRelease(int arg)释放同步状态, 然后换新 头结点的后继节点 。
6 .ReentrantLock
(1)锁重入
(2)公平锁
(3)非公平锁
7.ReentrantReadWriteLock
(1)同步状态表示读写
(2)锁降级
8 ConcurrentHashMap
(1)分段锁