并发控制
一、锁
锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些所可以允许多个线程并发的访问共享资源,比如读写锁)。
1. Lock接口
1)Lock接口简介
在Lock接口之前,Java程序中依靠synchronized关键字来实现锁功能。在Java5以后,新增了Lock接口,用来实现锁的功能。相较于synchronized关键字,Lock接口提供的方法需要显式地获取和释放锁,虽然不如synchronized简便,但是使用起来更加灵活。
2)Lock接口的特性
特性 | 描述 |
---|---|
尝试非阻塞的获取锁 | 当前线程尝试获取锁,如果这一时刻所没有被其他线程获取到,则成功获取并持有锁 |
能中断的获取锁 | 与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放 |
超时获取锁 | 在指定的截止时间之前获取锁,如果截止时间到了仍然无法获取锁,则返回 |
3)Lock接口的API
方法名 | 描述 | 异常 |
---|---|---|
void Lock() | 获取锁,调用该方法的线程将会获得锁,当锁获得后,方法返回 | \ |
void lockInterruptibly() | 可中断的获得锁,和lock()方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程 | InterruptedException |
boolean tryLock() | 尝试非阻塞的获取锁,调用该方法后立即返回,如果能够获得锁则返回true,反之返回false | \ |
boolean tryLock(long time, TimeUnit unit) | 超时的获取锁,当前线程在以下三种情况下会返回: 1)当前线程在超时时间内获得了锁 2)当前线程在超时时间内被中断 3)超时时间结束,返回false | InterruptedException |
void unlock() | 释放锁 | \ |
Condition newCondition() | 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后当前线程释放锁 | \ |
2. 重入锁
重入锁ReentrantLock是Lock接口的常用实现之一,它表示该锁能够支持一个线程对资源的重复加锁。它支持获取锁时的公平和非公平选择。当一个线程再次执行到lock()方法获取锁时,可以顺利的获得锁而不会被阻塞。synchronized也是支持重入的锁,当synchronized修饰递归方法时,可以连续多次获得锁。
重入锁存在公平性问题。如果在时间上,一个先申请锁的请求一定先被满足,那么这个锁就是公平的,反之则是不公平的。公平的获得锁,也就是等待时间最长的线程最优先获得锁,也可以说是获取时顺序的。非公平锁的往往效率高于公平锁,但公平锁能够减少“饥饿”问题发生的概率,等待越久的请求越是能够优先得到满足。
在默认情况下ReetrantLock是非公平的。
1)重入锁的使用
重入锁的使用方式与synchronized类似,
public class ReentrantLockTest {
public static void main(final String[] args) throws Exception {
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
lock1.lock();
System.out.println("Thread A get Lock1");
TimeUnit.SECONDS.sleep(1);
lock2.lock();
System.out.println("Thread A get Lock 2");
} catch (InterruptedException e) {}
});
Thread t2 = new Thread(() -> {
try {
lock2.lock();
System.out.println("Thread A get Lock1");
TimeUnit.SECONDS.sleep(1);
lock1.lock();
System.out.println("Thread A get Lock 2");
} catch (InterruptedException e) {}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("end");
}
}
3. 读写锁
读写锁不是排它锁,即同一时刻允许多个线程进行访问。读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,只能允许一个写线程单独运行,其它线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性比一般的排他锁有了很大提升。
在没有读写锁的时候,写线程之间要通过synchronized来进行同步,而读线程则需要使用等待通知机制,当写线程进入时,读线程等待,写线程结束时通知读线程。大多数场景下,读多与写,读写锁能够比排它锁拥有更好的并发性和吞吐量。
1)ReentrantReadWriteLock的特性
特性 | 说明 |
---|---|
公平性 | 支持非公平(默认)和公平的获取锁方式 |
重进入 | 该锁支持重进入,读线程可以再次获取读锁,写线程也可以再次获取写锁 |
锁降级 | 遵循获取写锁、获取读锁、再释放写锁的次序,写锁能够降级成为读锁 |
2)读写锁的API
方法名称 | 说明 |
---|---|
ReentrantReadWriteLock.ReadLock readLock() | 返回一个支持多个线程获取的读锁 |
ReentrantReadWriteLock.WriteLock writeLock() | 返回一个写锁 |
int getReadLockCount() | 返回当前读锁被获取的次数,重入也会增加次数 |
int getReadHoldCount() | 返回当前线程获取锁的次数 |
boolean isWriteLocked() | 判断写锁是否被获取 |
int getWriteHoldCount() | 返回当前写锁被获取的次数 |
3)读写锁的使用
public class ReentrantReadWriteLockTest {
static Map<String, Object> map = new HashMap<>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock r = rwl.readLock();
static Lock w = rwl.writeLock();
public static final Object get(String key) {
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
public static final Object put(String key, Object value) {
w.lock();
try {
return map.put(key, value);
} finally {
w.unlock();
}
}
}
上述代码摘自《Java并发编程的艺术》。上述代码逻辑简单,即用读写锁实现了对于一个map的并发访问控制。在读操作时获取读锁,这使得读线程不会被阻塞。在写操作时获取写锁,当写锁获得后,其他读写线程都会阻塞。
4. LockSupport工具
在以前我们阻塞和唤醒某一个线程有很多限制:
1. 因为饿wait()方法需要释放锁,所以必须在synchronized代码块中使用,否则会抛出异常
2. notify()方法必须在synchronized中使用
3. synchronized、notify、wait对象必须一致,所以一个synchronized代码块中只能有一个线程调用wait()和notify()
在jdk1.6中引入了LockSupport这个同步工具,用来创建锁和其他同步工具类的基本线程阻塞原语。在jdk中很多地方的线程阻塞和唤醒都是用了LockSupport.park()和LockSupport.unpart()
1)LockSupport提供的阻塞和唤醒方法
方法名称 | 描述 |
---|---|
void park() | 阻塞当前线程,如果调用unpark(Thread thread)方法或者当前线程被中断,才能从park()方法中返回 |
void parkNanos(long nanos) | 阻塞当前线程最长不超过nanos纳秒,返回条件在part()基础上增加了超时 |
void parkUntil(long deadline) | 阻塞当前线程,直到deadline时间 |
void unpark(Thread thread) | 唤醒处于阻塞状态的线程thread |
2)LockSupport的使用
public class TestLockSupport {
static Thread t1 = null, t2 = null;
public static void main(String[] args) {
int[] a = new int[]{1,2,3,4};
char[] b = new char[]{'a', 'b', 'c', 'd'};
t1 = new Thread(() -> {
for (int i = 0; i < 4; i++) {
System.out.println(a[i]);
LockSupport.unpark(t2);
LockSupport.park();
}
});
t2 = new Thread(() -> {
for (int i = 0; i < 4; i++) {
LockSupport.park();
System.out.println(b[i]);
LockSupport.unpark(t1);
}
});
t1.start();
t2.start();
}
}
5. Condition接口
任意一个对象都拥有一组监视器方法,主要包括wait()、wait(long timeout)、notify()、notifyAll(),这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似的方法,与Lock配合可以实现等待/通知模式,但两者在使用方式和功能特性上有差别。
1)Object的监视器方法和Condition接口的对比
对比项 | Object | Condition |
---|---|---|
前置条件 | 获取对象的锁 | 调用Lock.lock()获得锁 调用Lock.newCodition()获取Condition对象 |
调用方式 | 直接调用 如:object.wait() | 直接调用 如:condition.await() |
等待队列个数 | 一个 | 多个 |
当前线程释放锁并进入等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态,在等待状态中不响应中断 | 不支持 | 支持 |
当前线程释放锁并进入超时等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态知道某个时间 | 不支持 | 支持 |
唤醒等待队列中的一个线程 | 支持 | 支持 |
唤醒等待队列中的所有线程 | 支持 | 支持 |
2)Condition的部分方法
方法名称 | 描述 |
---|---|
void await() | 当前线程进入等待状态知道被通知(signal)或中断,当前线程从await()方法中返回 |
void awaitUninterruptibly() | 当前线程进入等待状态,直到被通知,这个方法对中断不敏感 |
long awaitNanos(long nanosTimeout) | 当前线程进入等待状态,直到被通知、中断或超市。返回值表示剩余的时间。如果返回值是0或负数表示已经超时。 |
boolean awaitUntil(Date deadline) | 当前线程进入等待状态直到被通知、中断或到某个时间。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间返回false。 |
void signal() | 唤醒一个等待在Condition的线程 |
void signalAll() | 唤醒所有等待在Condition上的线程 |
3)Condition的使用
public class TestCondition {
public static void main(String[] args) {
char[] a = "1234".toCharArray();
char[] b = "abcd".toCharArray();
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Thread t1 = new Thread(() -> {
try {
lock.lock();
for (char c : a) {
System.out.println(c);
condition.signal();
condition.await();
}
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
Thread t2 = new Thread(() -> {
try {
lock.lock();
for (char c : b) {
System.out.println(c);
condition.signal();
condition.await();
}
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
t1.start();
t2.start();
}
}
二、并发工具
1. CountDownLatch
CountDownLatch按照字面意思叫做“倒数门闩”,允许一个或多个线程等待其他线程操作完成。
CountDownLatch的构造函数接收一个int类型的参数N作为计数器,当我们调用CountDownLatch的countDown()方法时N就会减1,知道N变成0.当N变成0以后,调用CountDownLatch.await()方法并等待的线程就会被唤醒继续执行。
public class TestCountDownLatch {
public static void main(String[] args) {
CountDownLatch cdl = new CountDownLatch(1);
Thread t = new Thread(() -> {
try {
Thread.sleep(1000);
cdl.countDown();
} catch(Exception e){}
});
cdl.await();
}
}
2. CyclicBarrier
同步屏障CyclicBarrier的字面意思是可循环使用的屏障。他要做的事情是让一组线程到达一个屏障(同步点)时被阻塞,直到所有线程到达屏障,屏障才会开门,所有被拦截的线程才会继续运行。
CyclicBarrier默认的构造方法传入一个int类型的参数,表示屏障拦截的线程数,每个线程调用await()方法告诉CyclicBarrier我已经到达屏障,然后当前线程被阻塞。
CyclicBarrier还有一种更高级的构造方法CyclicBarrier(int parties, Runnable barrierAction),用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。
1)CyclicBarrier应用场景
CyclicBarrier可以用于多线程计算,最后合并计算结果的场景。
2)CyclicBarrier和CountDownLatch的区别
- CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset方法重置。
- CyclicBarrier还提供了其他方法,如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量,isBroken方法来了解阻塞的线程是否被中断。
3)CyclicBarrier的使用
public class TestCyclicBarrier {
public static void main(String[] args) {
CyclicBarrier cb = new CyclicBarrier(2);
new Thread(() -> {
try {
cb.await();
} catch (InterruptedException | BrokenBarrierException e) {}
System.out.println(1);
}).start();
try {
cb.await();
} catch (Exception e) {}
System.out.println(2);
}
}
3. Semaphore
Semaphore(信号量)使用来控制同事访问特定资源的线程数量,他通过协调各个线程,以保证合理的使用公共资源。
1)Semphore的应用场景
Semaphore可以用于做流量控制(限流),特别是公用资源有限的应用场景,比如数据库连接。
2)常用方法
方法名 | 描述 |
---|---|
void acquire() | 获得许可证 |
void release() | 释放许可证 |
boolean tryAcquire() | 尝试获得许可 |
int availablePermits() | 返回可用许可证数目 |
boolean hasQueuedThreads() | 返回正在等待许可证的线程数 |
void reducePermits(int reduction) | 是否有线程正在等待许可证 |
Collection getQueueThreads() | 返回所有等待许可证的线程集合 |
3)Semphore的使用
public class TestSemaphore {
public static void main(String[] args) {
Semaphore s = new Semaphore(10);
ExecutorService tpe = Executors.newFixedThreadPool(30);
for (int i = 0; i < 30; i++) {
tpe.execute(() -> {
try {
s.acquire();
System.out.println("save data");
s.release();
} catch (Exception e) {
}
});
}
tpe.shutdown();
}
}
上述代码逻辑很简单,虽然有30个线程在执行,但是只允许10个并发执行。Semaphore(int permits)接收一个整形,表示可用的许可量。
4. Exchanger
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。两个线程可以通过exchange()方法来进行数据交换,如果其中一个线程执行了exchange()方法,她会一直等待另一个线程执行exchage()。当两个线程都到达同步点的时候,两个线程就可以进行数据交换。
public class TestExchange {
public static void main(String[] args) {
Exchanger<String> exgr = new Exchanger<>();
ExecutorService tpe = Executors.newFixedThreadPool(2);
tpe.execute(() -> {
String a = "a";
try {
exgr.exchange(a);
} catch (InterruptedException e){}
});
tpe.execute(() -> {
String b = "b";
try {
String a = exgr.exchange("b");
System.out.println("a=" + a + ", b=" + b);
} catch (InterruptedException e){}
});
tpe.shutdown();
}
}
三、原子操作
1. 原子更新基本类型类
AtomicBoolean:原子更新布尔类型
AtomicInteger:原子更新整形
AtomicLong:原子更新长整形
1.1 AtomicInteger的API
方法 | 含义 |
---|---|
int addAndGet(int delta) | 医院自方式将输入的数值与实例中的值相加,并返回结果 |
boolean compareAndSet(int expect, int update) | 如果输入的数值等于预期值,则以原子方式将该值设置为输入的值 |
int getAndIncrement() | 以原子方式将当前值加1 |
void lazySet(int newValue) | 最终会设置成为newValue |
上表中给出了AtomicInteger类的方法,其他三个类与其使用方法类似。
1.2 AtomicInteger类使用
public class TestAtomicInteger {
public static void main(String[] args) {
AtomicInteger ai = new AtomicInteger(1);
System.out.println(ai.getAndIncrement());
System.out.println(ai.get());
}
}
2. 原子更新数组
AtomicIntegerArray:原子更新整形数组里的元素
AtomicLongArray:原子更新长整形数组里的元素
AtomicReferenceArray:原子更新引用类型数组里的元素
2.1 AtomicIntegerArray的API
方法 | 含义 |
---|---|
int addAndGet(int i, int delta) | 以原子方式将输入值与数组中索引i的元素相加 |
boolean compareAndSet(int i, int expect, int update) | 如果当前值等于预期值,则以原子方式将数组位置i的元素设置为update值 |
上表给出了AtomicIntegerArray类的方法,其余两个类与其类似。
2.2 AtomicIntegerArray的使用
public class TestAtomicIntegerArray {
public static void main(String[] args) {
int[] value = new int[] {1,2};
AtomicIntegerArray ai = new AtomicIntegerArray(value);
ai.getAndSet(0, 3);
System.out.println(ai.get(0));
System.out.println(value[0]);
}
}
AtomicIntegerArray类在构造时,会拷贝一份传入的参数数组,所有的原子操作都在拷贝的数组中进行操作,而不会影响传入的原数组。
3. 原子更新引用类型
AtomicReference:原子更新引用类型
AtomicReferenceFieldUpdater:原子更新引用类型列的字段
AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef, boolean initialMark)
4. 原子更新字段类
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicStampedReference