我理解的Java并发基础(四):并发锁和原子类

1,338 阅读8分钟
原文链接: my.oschina.net

java.util.concurrent是Java为开发者提供的一些高效的工具类,其实现大多基于前文分析的volatile+CAS来实现锁操作。

java.util.concurrent包含两个子包,java.util.concurrent.atomicjava.util.concurrent.locks

java.util.concurrent.atomic是一些原子操作类,比如AtomicBoolean、AtomicInteger、AtomicLong、AtomicIntegerArray等基本数据类型包装类的原子操作 以及 AtomicReference、AtomicReferenceArray等的普通java对象的原子操作类。JDK1.8新增了一些用于并行计算的DobleAccumulator、DoubleAdder、LongAccumulator、LongAdder等类。

java.util.concurrent.locks是一些Lock接口及其实现类。可用于开发的实现类包括之前介绍过的 ReentrantLock以及ReentrantReadWriteLock等类,以及Lock相关的工具类LockSupport。

除了这两个包之外,java.util.concurrent包下还包括许多的并发类。比如用于Queue的实现类、用于并发流程控制信号类的CountDownLatch、CyclicBarrier、Semaphore类等、用于并发安全集合类的ConcurrentHashMap、CopyOnWriteArrayList类等、用于线程池的Excutors、ThreadPollExecutor类等、用于执行并行任务类的ForkJoinPool、ForkJoinTask等。

本文首先介绍java.util.concurrent.atomic包提供的原子类。

  单线程执行代码i = i+1是不会出现意外情况的。但是在多线程语境下,可能得到期望之外的值,比如变 量i=1,A线程更新i+1,B线程也更新i+1,经过两个线程操作之后可能i不等于3,而是等于2。 因为A和B线程在更新变量i的时候拿到的i都是1,这就是线程不安全的更新操作。可以在方法上增加synchronized来解决,由synchronized来保证多线程不会同时更新变量i。但是,单单为了这一个操作就将整个方法或者代码块增加synchronized同步,效率会比较低。于是,java为开发者推出了原子类包java.util.concurrent.atomic,旨在解决这一问题,其底层使用的就是前文提到过的volatile+CAS操作。

1,AtomicBoolean、AtomicInteger、AtomicLong,这三个基本类型原子类的方法几乎一模一样。AtomicInteger的常用方法有:

void set(int newValue); // 等同于 赋值 i = newValue;
int getAndIncrement(); // 等同于 j = i++;
int getAndDecrement(); // 等同于 j = i--;
int getAndAdd(int delta); // 等同于 j = i; i = i + delta;
int incrementAndGet(); // 等同于 j = ++i;
int decrementAndGet(); // 等同于 j = --i;
int addAndGet(int delta); // 等同于 i = i + delta; j = i;
boolean compareAndSet(int expect, int update); // 直接通过CAS操作来比较
int getAndSet(int newValue); // 等同于 j = i; i = newValue;

2,AtomicReference<V>、AtomicStampedReference<V>,这两个是引用类型原子类。后者是前者的补充,是带有版本号的CAS操作,用于解决只带有值的CAS操作的ABA问题。AtomicReference<V>的常用方法有:

V get();
void set(V newValue);
boolean compareAndSet(V expect, V update);
V getAndSet(V newValue);

  AtomicStampedReference<V>的常用方法有:

V getReference();
int getStamp();
boolean compareAndSet(V   expectedReference,  V   newReference, int expectedStamp, int newStamp);
void set(V newReference, int newStamp);

3,**AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray<E>**是数组类的原子类。AtomicIntegerArray的常用方法有:

int get(int i); // 获取数组索引 i 处的值
void set(int i, int newValue);
int getAndSet(int i, int newValue);
boolean compareAndSet(int i, int expect, int update);
int getAndIncrement(int i);
int getAndDecrement(int i);
int getAndAdd(int i, int delta);
int incrementAndGet(int i);
int decrementAndGet(int i);
int addAndGet(int i, int delta);

4,**AtomicIntegerFieldUpdater<T>、AtomicLongFieldUpdater<T>、AtomicReferenceFieldUpdater<T,V>**这三个是字段级别的原子类。可以对<T>类型的class的指定字段进行原子更新操作。不过这三个类都是抽象类,需要使用静态方法构指明class和field构建后才能使用。AtomicIntegerFieldUpdater<T>的常用方法有:

static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName); // 静态方法构指明class和需要原子更新的field
boolean compareAndSet(T obj, int expect, int update);
void set(T obj, int newValue);
int get(T obj);
int getAndSet(T obj, int newValue);
int getAndAdd(T obj, int delta);
int getAndIncrement(T obj);
int getAndDecrement(T obj);
int incrementAndGet(T obj);
int decrementAndGet(T obj);
int addAndGet(T obj, int delta);

  原子操作类是synchronized同步锁的替代者之一,同时java.util.concurrent.locks提供了Lock接口,实现更为丰富的关于同步锁的操作,提供更为高级的synchronized的替代选项。

1,Lock接口最常用的实现类是 ReentrantLock 和 ReentrantReadWriteLock。前者表示重入锁,后者表示可重入的读写锁。Lock接口的API如下:

void lock(); // 阻塞获取锁,直到获得锁。
void lockInterruptibly() throws InterruptedException; // 阻塞获取锁,直到获得锁。但在等在获取锁的时候可以响应线程的中断标识。
boolean tryLock(); // 尝试获取锁。如果成功获得锁则返回true。如果没有获得锁则返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 一段时间内尝试获取锁。如果时间段内获得锁则返回true。超时则返回false。
void unlock(); // 释放锁
Condition newCondition(); // 创建一个绑定在该lock对象上的condition对象。

  Condition接口是Lock接口的一个组件,是对Lock功能各位丰富的补充。一个线程只有在获取到lock对象之后,才可以使用绑定在该lock接口上的condition对象的方法。Condition接口的常用API如下:

void await() throws InterruptedException; // 当前线程进入等待状态,直到被唤醒或中断
void awaitUninterruptibly(); // 当前线程进入等待状态,直到被唤醒,不响应线程中断标识
long awaitNanos(long nanosTimeout) throws InterruptedException; // 一段时间内处于等待状态。超时自动唤醒,允许被唤醒。返回值 = nanosTimeout - 唤醒时的时间
boolean await(long time, TimeUnit unit) throws InterruptedException; // 一段时间内处于等待状态。超时自动唤醒则返回false,被其他线程唤醒则返回true
boolean awaitUntil(Date deadline) throws InterruptedException; // 当前线程进入等待状态,直到指定时间点。如果到达指定时间点自动唤醒,则返回false,被其他线程唤醒则返回true
void signal(); // 唤醒等待该condition对象的其中一个线程
void signalAll(); // 唤醒等待该condition对象的所有线程

  获取一个Condition只能通过Lock的newCondition()方法。同一lock对象上可以绑定多个condition对象,但同一时间最多只有一个condition对象可以执行程序。

2,ReentrantLock表示可重入锁。持有该对象锁的线程可以对资源进行重复加锁。在该类的构造方法中可以指定公平锁和非公平锁。
2.1,可重入性
  线程执行方法A,使用lock.lock()方法成功获取到锁,然后调用方法B,当方法B内执行到lock.lock()的时候(这两个lock是通过一个lock对象),如果能顺利执行,则说明该lock对象具有可重入性。否则不具有可重入性。可类比synchronized关键字进行理解,因为synchronized关键字属于JVM层面的隐性的具有可重入性。tips:千万不要忘记在finally中使用lock.unlock()进行锁资源的释放。
2.2,公平锁和非公平锁
  ReentrantLock内部是通过AbstractQueuedSynchronizer同步器(AQS)来实现的。AQS同步器内部维护了一个FIFO(先入先出)的阻塞队列 和 一个int类型的状态码。所谓的公平性,就是当队列不为空的时候,如果有新的线程争夺锁资源,则该新的线程必须加入到队列的尾部。当有锁释放的时候,只能由队列的头部对应的线程来进行锁资源的获取。所谓的非公平性,就是当有锁释放的时候,队列的头部所对应的线程 和 新的线程 都可以争夺锁资源。如果新的线程成功获得锁对象则执行后续操作,如果获取锁对象失败则加入到队列尾部。
  由于AQS同步器获取锁资源的操作是通过CAS操作来完成的,刚释放锁的线程再次获取锁的几率会非常大,使得其他线程只能在同步队列中等待。所以非公平锁的效率要优于公平锁,吞吐量更大。

3,ReentrantReadWriteLock表示可重入的读写锁。具有可重入性,构造方法中可以指定公平锁和非公平锁。ReentrantReadWriteLock内部维护了一对锁,一个写锁和一个读锁。允许多个线程对资源进行读的访问,只有一个线程对资源进行写操作。
3.1,可重入性。 同ReentrantLock。
3.2,公平锁和非公平锁。同ReentrantLock。
3.3,锁降级
  当一个线程在持有了写锁并对资源进行写操作之后,再去获取读锁,然后再释放写锁。这个过程称为是锁降级。锁降级中读锁的获取是否必要呢?答案是必要的,主要是为了保证数据的可见性。当线程A获取写锁并进行写操作之后释放锁,但是获取读锁的时候失败,因为线程B获取到了写锁。这种情况下,线程B对资源进行写之后,线程A是并不知道的,线程A会使用之前的“脏数据”,会造成并发安全问题。

4,LockSupport是操作线程状态的并发工具类。常用的API如下:

static void unpark(Thread thread); // 唤醒处于阻塞状态的线程thread
static void park(); // 阻塞当前线程,直到被唤醒或中断
static void parkNanos(long nanos); // 阻塞当前线程,最长不超过nanos纳秒
static void parkUntil(long deadline); // 阻塞当前线程,最长不超过deadline时间点

// 以下为JDK1.6新增API,参数blocker是用来标识当前线程在等待的对象,主要用于问题排查和系统监控。
static void park(Object blocker); // 同park()
static void parkNanos(Object blocker, long nanos); // 同parkNanos(long nanos)
static void parkUntil(Object blocker, long deadline); // 同parkUntil(long deadline)

  LockSupport线程阻塞采用的是Unsafe类的native操作,与JVM层面的object.wait()使线程阻塞不同。即,使用LockSupport.park(Object blocker)后阻塞的线程,不能通过blocker.notify()唤醒,反之亦然。

参考资料:

  • 《Java并发编程的艺术》
  • 《深入理解Java虚拟机:JVM高级特性与最佳实践》
  • 以上内容为笔者日常琐屑积累,已无从考究引用。如果有,请站内信提示。