java 并发包 编程_Java并发编程-JDK并发包

参考资料:《Java高并发程序设计》

1.同步控制

1.扩展了synchronized功能的:重入锁

1.简介

使用示例:

import java.util.concurrent.locks.ReentrantLock;

public class Test {

// 声明锁

private static ReentrantLock lock = new ReentrantLock();

private static int i = 0;

private static final Runnable runnable = () -> {

for (int j = 0; j < 100000; j++) {

// 加锁

lock.lock();

try {

i++;

} finally {

// 释放锁

lock.unlock();

}

}

};

public static void main(String[] args) throws InterruptedException {

Thread t1 = new Thread(runnable);

Thread t2 = new Thread(runnable);

t1.start();

t2.start();

t1.join();

t2.join();

System.out.println(i);

}

}

重入锁可以完全替代synchronized关键字。

与synchronized相比,重入锁有着明显的操作过程。开发人员必须手动指定何时加锁,何时释放锁。也正因为如此,重入锁对逻辑控制的灵活性要远好于synchronized。

需要注意的是,在退出临界区时,必须记得释放锁,否则,其他线程就没有机会再访问临界区了。

重入锁之所以有重入两字,是因为这种锁是可以被同一个线程反复进入的。示例中的核心代码可写成下面的形式:

lock.lock();

lock.lock();

try {

i++;

} finally {

lock.unlock();

lock.unlock();

}

可重入的目的是为了避免同一个线程在第2次获取锁的时候和自己产生死锁。

需要注意的是:如果一个线程多次获得锁,那么在释放锁的时候,也必须释放相同的次数。

如果释放锁的次数多,那么会得到一个IllegalMonitorStateException异常。

如果加锁的次数多,那么相当于线程还持有这个锁,其他线程仍无法进入临界区。

2.高级功能

1.中断

对于synchronized来说,如果一个线程在等待锁,那么结果只有两种情况:

获得这把锁继续执行

保持等待

而重入锁提供了另外一种可能:线程可以被中断。即在等待锁的过程中,程序可以根据需要取消对锁的请求。

下面的代码产生了一个死锁,但得益于锁中断,可以轻易地解决这个死锁:

import java.util.concurrent.locks.ReentrantLock;

import lombok.AllArgsConstructor;

public class Test {

private static final ReentrantLock lock1 = new ReentrantLock();

private static final ReentrantLock lock2 = new ReentrantLock();

@AllArgsConstructor

private static class IntLock implements Runnable {

private int lock;

@Override

public void run() {

try {

if (lock == 1) {

lock1.lockInterruptibly();

try {

Thread.sleep(500);

} catch (InterruptedException e) {

System.out.println("interrupted in sleep");

}

lock2.lockInterruptibly();

} else {

lock2.lockInterruptibly();

try {

Thread.sleep(500);

} catch (InterruptedException e) {

System.out.println("interrupted in sleep");

}

lock1.lockInterruptibly();

}

} catch (InterruptedException e) {

System.out.println("interrupted in business");

} finally {

if (lock1.isHeldByCurrentThread()) {

lock1.unlock();

}

if (lock2.isHeldByCurrentThread()) {

lock2.unlock();

}

System.out.println(Thread.currentThread().getId() + ":线程退出");

}

}

}

public static void main(String[] args) throws InterruptedException {

Thread t1 = new Thread(new IntLock(1));

Thread t2 = new Thread(new IntLock(2));

t1.start();

t2.start();

Thread.sleep(1000);

t2.interrupt();

}

}

// interrupted in business

// 12:线程退出

// 11:线程退出

上述代码中对锁的请求,统一使用lockInterruptibly()方法。这是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,可以响应线程中断。

2.锁申请等待限时

可以使用tryLock()方法进行一次限时的锁申请:

public class Test {

public static class TimeLock implements Runnable {

public static ReentrantLock lock = new ReentrantLock();

@Override

public void run() {

try {

if (lock.tryLock(5, TimeUnit.SECONDS)) {

Thread.sleep(6000);

} else {

System.out.println("get lock failed");

}

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

if (lock.isHeldByCurrentThread()) {

lock.unlock();

}

}

}

}

public static void main(String[] args) {

TimeLock lock = new TimeLock();

Thread t1 = new Thread(lock);

Thread t2 = new Thread(lock);

t1.start();

t2.start();

}

}

// get lock failed

上述代码中的tryLock()方法接收两个参数,一个表示等待时长,另一个表示计时单位。如果申请锁成功,则返回true,否则返回false。

tryLock()方法也可以不带参数直接运行。这种情况下,如果锁未被其他线程占用,则申请锁成功,返回true,否则不会做任何等待,立即返回false。下面演示了这种用法:

public class Test {

public static class TryLock implements Runnable {

public static ReentrantLock lock1 = new ReentrantLock();

public static ReentrantLock lock2 = new ReentrantLock();

int lock;

public TryLock(int lock) {

this.lock = lock;

}

public void tryDoubleLock(ReentrantLock firstLock,

ReentrantLock secondLock,

String param, Consumer consumer) {

while (true) {

if (firstLock.tryLock()) {

try {

if (secondLock.tryLock()) {

try {

consumer.accept(param);

return;

} finally {

secondLock.unlock();

}

}

} finally {

firstLock.unlock();

}

}

}

}

@Override

public void run() {

String param = ":My Job done";

Consumer consumer = str ->

System.out.println(Thread.currentThread().getId() + str);

if (lock == 1) {

tryDoubleLock(lock1, lock2, param, consumer);

} else {

tryDoubleLock(lock2, lock1, param, consumer);

}

}

}

public static void main(String[] args) {

Thread t1 = new Thread(new TryLock(1));

Thread t2 = new Thread(new TryLock(2));

t1.start();

t2.start();

}

}

// 11:My Job done

// 10:My Job done

先让t1获得lock1,再让t2获得lock2,接着让t1申请lock2,t2申请lock1。在一般情况下,这会导致t1和t2相互等待,从而引起死锁。但因为使用了tryLock(),线程不会傻傻等待,而是不断重试,直到某个线程同时获得lock1和lock2两把锁。执行上述代码,可以发现两个线程可以很快双双正常执行完毕。

3.公平锁

大多数情况下,锁的申请都是非公平的。举个例子:线程t1首先请求了锁A,接着线程t2也请求了锁A,那么锁A可用时,是t1获得锁还是t2获得锁是不确定的。系统会从这个锁的等待队列中随机挑选一个,因此不能保证公平性,有可能会产生饥饿。

synchronized关键字进行锁控制所产生的锁就是非公平的。

重入锁允许通过构造函数对锁的公平性进行设置。fair为true表示公平。

public ReentrantLock(boolean fair)

公平锁的实现需要维护一个有序队列,因此锁的实现成本高,性能也非常底下。因此默认情况下锁是非公平的。如果不是特殊的需求,也不需要公平锁。下面的代码演示了公平锁的使用:

public class Test {

public static class FairLock implements Runnable {

public static ReentrantLock fairLock = new ReentrantLock(true);

@Override

public void run() {

while (true) {

try {

fairLock.lock();

System.out.println(Thread.currentThread().getName() + "获得锁");

} finally {

fairLock.unlock();

}

}

}

}

public static void main(String[] args) {

FairLock r1 = new FairLock();

Thread t1 = new Thread(r1, "Thread_t1");

Thread t2 = new Thread(r1, "Thread_t2");

t1.start();

t2.start();

}

}

// 程序运行一段时间(稳定后)的部分输出:

// Thread_t1获得锁

// Thread_t2获得锁

// Thread_t1获得锁

// Thread_t2获得锁

// Thread_t1获得锁

// Thread_t2获得锁

// Thread_t1获得锁

// Thread_t2获得锁

而如果把fairLock初始化时的参数改为false,那么根据系统的调度,一个线程会倾向于再次获取已经持有的锁,这种分配方式是高效的,但无公平性可言。

3.总结

对ReentrantLock的几个重要方法整理如下:

lock(): 获得锁,如果锁已经被占用,则等待。

lockInterruptibly(): 获得锁,但优先响应中断。

tryLock(): 尝试获得锁,如果成功返回true,失败返回false。该方法不等待,立即返回。

tryLock(long time, TimeUnit unit): 在给定的时间内尝试获得锁。

unlock(): 释放锁。

从重入锁的实现来看,主要包含三个要素:

原子状态。原子状态使用CAS操作(CompareAndSwap)来存储当前锁的状态,判断锁是否已经被别的线程持有。

等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作。

阻塞原语park()和unpark()。这两个方法用来挂起和恢复线程。没有得到锁的线程将会被挂起。有关park()和unpark()可参考线程阻塞工具类LockSupport。

2.重入锁的好搭档:Condition条件

1.简介

Condition的作用和Object.wait()、Object.notify()的作用是大致相同的。但是Condition和重入锁相关联,而后者是和synchronized关键字合作使用的。

通过Lock接口(重入锁就实现了该接口)的newCondition()方法可以生成一个与当前重入锁绑定的Condition实例。利用Condition实例就可以让线程在合适的时间等待,或在特定的时刻得到通知继续执行。

2.方法

await()

调用该方法的前提是,当前线程已经成功获得与该条件对象绑定的重入锁,否则调用该方法时会抛出IllegalMonitorStateException。调用该方法外,当前线程会释放当前已经获得的锁(这一点与Object.wait方法一致),并且等待其它线程调用该条件对象的signal()或者signalAll()方法(这一点与Object.notify()或Object.notifyAll()很像)。或者在等待期间,当前线程被中断,则wait()方法会抛出InterruptedException并清除当前线程的中断状态。

await(long time, TimeUnit unit)

适用条件和行为与await()基本一致,唯一不同点在于,指定时间之内没有收到signal()或signalALL()信号或者线程中断时该方法会返回false;其它情况返回true。

awaitNanos(long nanosTimeout)

调用该方法的前提是,当前线程已经成功获得与该条件对象绑定的重入锁,否则调用该方法时会抛出IllegalMonitorStateException。nanosTimeout指定该方法等待信号的的最大时间(单位为纳秒)。若指定时间内收到signal()或signalALL()则返回nanosTimeout减去已经等待的时间;若指定时间内有其它线程中断该线程,则抛出InterruptedException并清除当前线程的中断状态;若指定时间内未收到通知,则返回0或负数。

awaitUntil(Date deadline)

适用条件与行为与awaitNanos(long nanosTimeout)完全一样,唯一不同点在于它不是等待指定时间,而是等待由参数指定的某一时刻。

awaitUninterruptibly()

调用该方法的前提是,当前线程已经成功获得与该条件对象绑定的重入锁,否则调用该方法时会抛出IllegalMonitorStateException。调用该方法后,结束等待的唯一方法是其它线程调用该条件对象的signal()或signalALL()方法。等待过程中如果当前线程被中断,该方法仍然会继续等待,同时保留该线程的中断状态。

signal()

用于唤醒一个等待中的线程。类似于Object.notify()

signalAll()

会唤醒所有在等待中的线程。类似于Object.notifyAll()

代码演示:

public class Test {

public static class ReenterLockCondition implements Runnable {

public static ReentrantLock lock = new ReentrantLock();

public static Condition condition = lock.newCondition();

@Override

public void run() {

try {

lock.lock(); // 申请并获得锁

condition.await(); // 释放锁

// 收到signal信号后重新申请锁、获得锁

System.out.println("thread is going on");

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

lock.unlock(); // 释放锁

}

}

}

public static void main(String[] args) throws Exception {

new Thread(new ReenterLockCondition()).start();

Thread.sleep(2000);

ReenterLockCondition.lock.lock(); // 获得锁

ReenterLockCondition.condition.signal(); // signal通知

ReenterLockCondition.lock.unlock(); // 释放锁,谦让给被唤醒的线程

}

}

// thread is going on

3.允许多个线程同时访问:信号量(Semaphore)

1.简介

从广义上说,信号量是对锁的扩展。无论是内部锁synchronized还是重入锁ReentrantLock,一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问某一个资源。

信号量主要提供了以下构造函数。在构造信号量对象时,必须指定信号量的准入数,即同时能申请多少个许可。当每个线程每次只申请一个许可时,这就相当于指定了同时有多少个线程可以访问某一个资源。

public Semaphore(int permits) // 参数为信号量的准入数

public Semaphore(int permits, boolean fair) // 第二个参数可以指定是否公平

2.方法

acquire()

尝试获得一个准入的许可。若无法获得,则线程会等待,直到有线程释放一个许可或者当前线程被中断。

acquireUninterruptibly()

和acquire()方法类似,但是不响应中断。

tryAcquire()

尝试获得一个许可,如果成功返回true,失败返回false,不会等待,立即返回。

release()

用于在线程访问资源结束后,释放一个许可,以使其他等待许可的线程可以进行资源访问。

代码演示:

public class Test {

public static class SemaphoreDemo implements Runnable {

final Semaphore semaphore = new Semaphore(5);

@Override

public void run() {

try {

semaphore.acquire();

Thread.sleep(2000);

System.out.println(System.currentTimeMillis() + "-" +

Thread.currentThread().getId() + ":done!");

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

semaphore.release();

}

}

}

public static void main(String[] args) throws Exception {

ExecutorService exec = Executors.newFixedThreadPool(20);

final SemaphoreDemo demo = new SemaphoreDemo();

for (int i = 0; i < 20; i++) {

exec.submit(demo);

}

}

}

// 从输出可以观察出系统以5个线程一组为单位,依次进行输出

4.读写分离锁:ReadWriteLock

读写分离锁(简称“读写锁”)可以有效地帮助 减少锁竞争,以提升系统性能。

读写锁允许多个线程同时读,但考虑到数据完整性,写写操作和读写操作间依然是需要相互等待和持有锁的。读写锁的访问约束如下表:

-

非阻塞

阻塞

阻塞

阻塞

在系统中,如果读操作次数远大于写操作,那么读写锁的功效就会非常明显。例如下面的代码:

public class Test {

private static Lock lock = new ReentrantLock();

private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

private static Lock readLock = readWriteLock.readLock();

private static Lock writeLock = readWriteLock.writeLock();

private static CountDownLatch latch = new CountDownLatch(20);

private int value;

public Object handleRead(Lock lock) throws InterruptedException {

try {

lock.lock(); // 模拟读操作

Thread.sleep(1000); // 读操作的耗时越多,读写锁的优势就越明显

return value;

} finally {

latch.countDown();

lock.unlock();

}

}

public void handleWrite(Lock lock, int index) throws InterruptedException {

try {

lock.lock(); // 模拟写操作

Thread.sleep(1000);

value = index;

} finally {

latch.countDown();

lock.unlock();

}

}

public static void main(String[] args) throws Exception {

final Test demo = new Test();

Runnable readRunnable = () -> {

try {

demo.handleRead(readLock);

//demo.handleRead(lock); // 使用重入锁而不是读写锁

} catch (InterruptedException e) {

e.printStackTrace();

}

};

Runnable writeRunnable = () -> {

try {

demo.handleWrite(writeLock, new Random().nextInt());

//demo.handleRead(lock); // 使用重入锁而不是读写锁

} catch (InterruptedException e) {

e.printStackTrace();

}

};

long startTime = System.currentTimeMillis();

for (int i = 0; i <= 18; i++) {

new Thread(readRunnable).start();

}

for (int i = 19; i < 20; i++) {

new Thread(writeRunnable).start();

}

latch.await();

System.out.println("used time: " + (System.currentTimeMillis() - startTime));

}

}

// 使用读写锁

// used time: 2022

// 使用重入锁

// used time: 20009

通过运行结果可以看出,使用读写锁程序大约2秒多就能结束(写线程直接是实际串行的)。而使用重入锁,所有的读和写线程之间都必须相互等待,整个程序的执行时间将长达20余秒。

5.倒计数器:CountDownLatch

倒计数器通常用来控制线程等待,它可以让某一个线程等待直到倒计数结束,再执行。

上一节介绍读写锁统计用时时,就是使用的倒计数器。此外倒计数器的一种典型场景就是火箭发射。在火箭发射前,为确保万无一失,往往会进行各项设备仪器的检查。只有等所有的检查完毕后,引擎才能点火。这种场景就非常适合使用CountDownLatch。它可以使点火线程等待所有检查线程全部完工后,再执行。示意图如下:

eb6b3a152e874f8f324b1d1cc73047a1.png

未命名文件(1).png-12.2kB

倒计时器的几个关键方法

// 构造函数接收一个整数作为参数,即当前这个倒计时器的计数个数

public CountDownLatch(int count)

// 计数减一

public void countDown()

// 使线程等待,直到倒计数完成

public void await()

6.循环栅栏:CyclicBarrier

CyclicBarrier与CountDownLatch类似,也可实现线程间的 计数等待 ,但功能更加强大:这个计数器可以 反复使用。

循环栅栏的一个使用场景:比如司令下达命令,要求10个士兵一起去完成一项任务。这时,就会要求10个士兵先集合报道,全部报道完毕后(第一次计数完成),再一起去执行任务,当10个士兵把自己的任务都完成了(第二次计数完成),那么司令再对外宣布:任务完成!

CyclicBarrier有两个主要的方法:

// parties:计数总数,也就是参与的线程总数

// barrierAction:当计数器完成一次计数后,系统要执行的动作

public CyclicBarrier(int parties, Runnable barrierAction)

// 等待一次计数完成

// 注意抛出的两种异常(见下一节的代码)

public int await() throws InterruptedException, BrokenBarrierException

上面的的场景用代码实现如下:

public class Test {

private static final int SOLDIER_NUM = 10;

private volatile static boolean allAssembledFlag = false;

private static final Runnable barrierRun = () -> {

if (allAssembledFlag) {

System.out.println("司令:[士兵" + SOLDIER_NUM + "个,任务完成!]");

} else {

System.out.println("司令:[士兵" + SOLDIER_NUM + "个,集合完毕!]");

}

};

public static class Soldier implements Runnable {

private final CyclicBarrier cyclic;

private String soldierName;

Soldier(CyclicBarrier cyclic, String soldierName) {

this.cyclic = cyclic;

this.soldierName = soldierName;

}

void doWork() {

try {

Thread.sleep(Math.abs(new Random().nextInt() % 10000));

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(soldierName + ":任务完成");

}

@Override

public void run() {

try {

// 等待所有士兵到齐

cyclic.await();

allAssembledFlag = true;

doWork();

// 等待所有士兵完成工作

cyclic.await();

} catch (InterruptedException e) {

System.out.println("线程等待时被中断——需要执行响应外部紧急事件的逻辑");

} catch (BrokenBarrierException e) {

System.out.println("当前的CyclicBarrier已经被损坏," +

"系统可能已经无法等待所有线程到齐——需要执行使等待线程就地解散的逻辑");

// 例如当前循环栅栏的计数总数为10,若有一个线程被中断,

// 那我们就会得到1个InterruptedException和9个BrokenBarrierException

}

}

}

public static void main(String[] args) throws Exception {

Thread[] allSoldier = new Thread[SOLDIER_NUM];

CyclicBarrier cyclic = new CyclicBarrier(SOLDIER_NUM, barrierRun);

// 设置屏障点,主要是为了执行这个方法

System.out.println("集合队伍!");

for (int i = 0; i < SOLDIER_NUM; ++i) {

System.out.println("士兵 " + i + " 报道!");

allSoldier[i] = new Thread(new Soldier(cyclic, "士兵 " + i));

allSoldier[i].start();

}

}

}

// 集合队伍!

// 士兵 0 报道!

// 士兵 1 报道!

// 士兵 2 报道!

// 士兵 3 报道!

// 士兵 4 报道!

// 士兵 5 报道!

// 士兵 6 报道!

// 士兵 7 报道!

// 士兵 8 报道!

// 士兵 9 报道!

// 司令:[士兵10个,集合完毕!]

// 士兵 3:任务完成

// 士兵 5:任务完成

// 士兵 9:任务完成

// 士兵 7:任务完成

// 士兵 4:任务完成

// 士兵 1:任务完成

// 士兵 8:任务完成

// 士兵 0:任务完成

// 士兵 6:任务完成

// 士兵 2:任务完成

// 司令:[士兵10个,任务完成!]

7.线程阻塞工具类:LockSupport

LockSupport是一个线程阻塞工具,可以在线程内任意位置让线程阻塞。

和Thread.suspend()相比,它弥补了由于resume()在前发生,导致线程无法继续执行的情况。

和Object.wait()相比,它不需要先获得某个对象的锁,也不会抛出InterruptedException异常。

LockSupport的 静态方法park() 可以阻塞当前线程,类似的还有parkNanos()、parkUntil()等方法,它们实现了一个限时的等待。

之前提到的有关 suspend()永久卡死线程 的例子,可用LockSupport重写如下:

public class Test {

private static final Object u = new Object();

public static class ChangeObjectThread extends Thread {

public ChangeObjectThread(String name) {

super.setName(name);

}

@Override

public void run() {

synchronized (u) {

try {

System.out.println("in " + getName());

Thread.sleep(1000);

LockSupport.park();

System.out.println("out " + getName());

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

public static void main(String[] args) throws Exception {

ChangeObjectThread t1 = new ChangeObjectThread("t1");

ChangeObjectThread t2 = new ChangeObjectThread("t2");

t1.start();

t2.start();

LockSupport.unpark(t1);

LockSupport.unpark(t2);

t1.join();

t2.join();

}

}

// in t1

// out t1

// in t2

// out t2

这里只是将原来的suspend()和resume()方法用park()和unpark()方法做了替换。并且为了确保unpark()在park()之前被调用,还让线程多sleep了1秒。但执行这段代码可以发现,它永远可以正常的结束,不会被永久性的挂起。

LockSupport不会因unpark()先于park()执行而被永久性挂起的原因,是因为它使用了类似 信号量 的机制。它为每一个线程准备了一个 许可,如果许可 可用,那么park()函数会立即返回,并且 消费掉 这个许可(也就是将许可变为 不可用),如果许可不可用,就会阻塞。而unpark()则可以使一个许可变为可用(但和信号量不同的是,许可不能累加,你不可能拥有超过一个许可,它永远只有一个)。

此外,处于park()挂起状态的线程不会像suspend()那样给出一个令人费解的Runnable的状态。它会非常明确地给出一个WAITING状态,而且还会标注是park()引起的:

35d2453b077f505f2025501f0f539a63.png

1.png-312.7kB

这种标注使得分析问题非常方便。此外,如果使用park(Object)函数,还可以为当前线程设置一个阻塞对象。这个阻塞对象会出现在线程Dump中,这样分析问题就更方便了。例如将上述代码中的park()改为park(this),那么在线程Dump中,可以看到如下信息:

fc9e1ed786ce9b51cd8e166c29bb77ff.png

1.png-379.1kB

除了有定时阻塞的功能外,LockSupport.park()还支持中断响应。但和其他接收中断的函数不同,park()函数不会抛出InterruptedException异常。park()函数只会默默的返回,但我们可以从Thread.interrupted()等方法获得中断标记。例如:

public class Test {

private static final Object u = new Object();

public static class ChangeObjectThread extends Thread {

public ChangeObjectThread(String name) {

super.setName(name);

}

@Override

public void run() {

synchronized (u) {

System.out.println("in " + getName());

LockSupport.park();

if (Thread.interrupted()) {

System.out.println(getName() + " 被中断了");

}

System.out.println("out " + getName());

}

}

}

public static void main(String[] args) throws Exception {

ChangeObjectThread t1 = new ChangeObjectThread("t1");

ChangeObjectThread t2 = new ChangeObjectThread("t2");

t1.start();

Thread.sleep(100);

t2.start();

t1.interrupt();

LockSupport.unpark(t2);

t1.join();

t2.join();

}

}

// in t1

// t1 被中断了

// out t1

// in t2

// out t2

end

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值