为了实现各种线程的操作,比如线程阻塞、阻塞恢复,JDK提供了大量模型来操纵线程运行。《Java多线程与高并发五(常用并发工具类)》给大家伙儿分享下JDK中常见的并发工具类。
实现一个容器,提供add,size方法,两个线程,线程一添加十个元素到容器中,线程二监控容器中元素的个数,当线程一添加到容器的元素个数到5时,线程二给出提示并结束。
解法一:wait/notify和synchronized的组合
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Semaphore;
/**
* 实现一个容器,提供add,size方法
* 两个线程,线程一添加十个元素到容器中,线程二监控元素的个数,当个数到5时,线程二给出提示并结束
*
* @author zab
* @date 2019-10-20 21:54
*/
public class ThreadCommunicationTest1 {
private volatile MyContainer1 myContainer1 = new MyContainer1();
public void f1() {
synchronized (this) {
for (int i = 1; i <= 10; i++) {
myContainer1.add("test");
System.out.println("add" + i);
if(i == 5){
this.notify();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public void f2() {
synchronized (this) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("size等于5啦!!!");
this.notify();
}
}
public static void main(String[] args) {
ThreadCommunicationTest1 t = new ThreadCommunicationTest1();
new Thread(t::f2).start();
new Thread(t::f1).start();
}
}
class MyContainer1 {
List list = new LinkedList<>();
public void add(String s) {
list.add(s);
}
public int size() {
return list.size();
}
}
多次运行结果如下:
wait/notify的解法比较常规,大概逻辑是,f2方法启动的线程先运行,由于f2方法一来就加锁等待,释放锁,f1方法的线程获得锁,循环输出1、2、3、4、5,当输出5时,叫醒f2,同时自己wait,释放锁,f2得以执行,输出"size等于5啦!!!",输出完毕过后叫醒正在wait的f1,f1得以执行下面的输出。
方法二:lock、condition组合
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 实现一个容器,提供add,size方法
* 两个线程,线程一添加十个元素到容器中,线程二监控元素的个数,当个数到5时,线程二给出提示并结束
*
* @author zab
* @date 2019-10-20 21:54
*/
public class ThreadCommunicationTest2 {
private volatile MyContainer2 myContainer2 = new MyContainer2();
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
public void f1() {
lock.lock();
for (int i = 1; i <= 10; i++) {
System.out.println("add" + i);
if (i == 5) {
c2.signal();
try {
c1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
lock.unlock();
}
public void f2() {
lock.lock();
try {
c2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("size等于5啦!!!");
c1.signal();
lock.unlock();
}
public static void main(String[] args) {
ThreadCommunicationTest2 t = new ThreadCommunicationTest2();
new Thread(t::f2).start();
new Thread(t::f1).start();
}
}
class MyContainer2 {
List list = new LinkedList<>();
public void add(String s) {
list.add(s);
}
public int size() {
return list.size();
}
}
道理同方法一,实现不同。
方法三:LockSupport的park和unpark组合,也是相对简单的用法,不需要锁
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
/**
* 实现一个容器,提供add,size方法
* 两个线程,线程一添加十个元素到容器中,线程二监控元素的个数,当个数到5时,线程二给出提示并结束
*
* @author zab
* @date 2019-10-20 21:54
*/
public class ThreadCommunicationTest3 {
private volatile MyContainer3 myContainer3 = new MyContainer3();
Thread t1 = null;
Thread t2 = null;
public void f1() {
for (int i = 1; i <= 10; i++) {
myContainer3.add("test");
System.out.println("add" + i);
if (i == 5) {
LockSupport.unpark(t2);
LockSupport.park();
}
}
}
public void f2() {
LockSupport.park();
System.out.println("size等于5啦!!!");
LockSupport.unpark(t1);
}
public static void main(String[] args) {
ThreadCommunicationTest3 t = new ThreadCommunicationTest3();
t.t2 = new Thread(t::f2);
t.t1 = new Thread(t::f1);
t.t2.start();
t.t1.start();
}
}
class MyContainer3 {
List list = new LinkedList<>();
public void add(String s) {
list.add(s);
}
public int size() {
return list.size();
}
}
方法四:CountDownLatch倒数门栓的await和countDown组合
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.LockSupport;
/**
* 实现一个容器,提供add,size方法
* 两个线程,线程一添加十个元素到容器中,线程二监控元素的个数,当个数到5时,线程二给出提示并结束
*
* @author zab
* @date 2019-10-20 21:54
*/
public class ThreadCommunicationTest4 {
private volatile MyContainer4 myContainer4 = new MyContainer4();
CountDownLatch latch = new CountDownLatch(5);
CountDownLatch latch1 = new CountDownLatch(1);
public void f1() {
for (int i = 1; i <= 10; i++) {
myContainer4.add("test");
System.out.println("add" + i);
latch.countDown();
if (i == 5) {
try {
latch1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void f2() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("size等于5啦!!!");
latch1.countDown();
}
public static void main(String[] args) {
ThreadCommunicationTest4 t = new ThreadCommunicationTest4();
new Thread(t::f2).start();
new Thread(t::f1).start();
}
}
class MyContainer4 {
List list = new LinkedList<>();
public void add(String s) {
list.add(s);
}
public int size() {
return list.size();
}
}
门栓定义的时候,传入一个值,每调用一次countDown方法,就减少一,当减少到0,门栓对应的await方法就由阻塞状态释放。
方法五:信号量Semaphore
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Semaphore;
/**
* 实现一个容器,提供add,size方法
* 两个线程,线程一添加十个元素到容器中,线程二监控元素的个数,当个数到5时,线程二给出提示并结束
*
* @author zab
* @date 2019-10-20 21:54
*/
public class ThreadCommunicationTest {
private volatile MyContainer myContainer = new MyContainer();
Semaphore semaphore1 = new Semaphore(0);
Semaphore semaphore2 = new Semaphore(0);
public void f1() {
for (int i = 1; i <= 10; i++) {
myContainer.add("test");
System.out.println("add" + i);
if (i == 5) {
semaphore1.release();
try {
semaphore2.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void f2() {
try {
semaphore1.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("size等于5啦!!!");
semaphore2.release();
}
public static void main(String[] args) {
ThreadCommunicationTest t = new ThreadCommunicationTest();
new Thread(t::f2).start();
new Thread(t::f1).start();
}
}
class MyContainer {
List list = new LinkedList<>();
public void add(String s) {
list.add(s);
}
public int size() {
return list.size();
}
}
信号量的用法也很简单,定义信号量时会初始化一个数量,可以理解为一块令牌,每当release的时候,就相当于给阻塞的acquire方法发一个令牌,acquire方法才由阻塞状态变为可通行状态。案例中,初始化为0,意味着只有别人release的时候,acquire才能获得一块令牌通行;当初始化为1的时候,acquire方法可以直接通行,但是初始化的值相应减少1,如果要再次调用acquire方法,就会阻塞,需要等到别的线程给令牌(调用release方法),acquire方法才能执行。
在上面的面试题中,我们了解了LockSupport、CountDownLatch、Semaphore等并发工具类的用法,下面介绍几种特殊用途的并发工具类。
说再多都不一个简单的demo来的直接:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
*
* @author zab
* @date 2019-11-09 18:44
*/
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(20,()->System.out.println("满二十人,发车"));
for (int i = 0; i < 100; i++) {
new Thread(new Runnable(){
@Override
public void run() {
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
模拟一个场景,我们去公司上班,会在某个地方等公司班车,班车司机会先后到达乘车点,装满一车人就会开往公司。这个类似于CyclicBarrier的作用,一个新起的线程(一个人)过来遇到await方法,就会阻塞(坐车上等着),但是阻塞到循环屏障定义的次数(理解为装满人了)后,就放行所有线程(发车了),并且调用定义的方法。输出的结果是:
还可以看到,这个await是循环使用的,一百次的调用可以发车五次。
读写锁就是读不加锁,写加锁,看示例:
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 读写锁
*
* @author zab
* @date 2019-11-10 11:46
*/
public class ReadWriteLockTest {
Lock readLock;
Lock writeLock;
Lock commonLock;
static Semaphore readSemaphore = new Semaphore(0);
static Semaphore writeSemaphore = new Semaphore(0);
static Semaphore writeSemaphore1 = new Semaphore(0);
static Semaphore readSemaphore1 = new Semaphore(0);
ThreadPoolExecutor threadPoolExecutor;
Map map = new HashMap<>();
public ReadWriteLockTest(Lock readLock, Lock writeLock) {
this.readLock = readLock;
this.writeLock = writeLock;
}
public static void main(String[] args) throws Exception {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReentrantLock commonLock = new ReentrantLock();
//ReadWriteLockTest test = new ReadWriteLockTest(lock.readLock(),lock.writeLock());
ReadWriteLockTest test = new ReadWriteLockTest(commonLock, commonLock);
long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
new Thread(test.new WriteOperation()).start();
}
writeSemaphore.release(100);
writeSemaphore1.acquire(100);
long end = System.currentTimeMillis();
System.out.println("lock锁写操作耗时:" + (end - start));
long start1 = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
new Thread(test.new ReadOperation()).start();
}
readSemaphore.release(100);
readSemaphore1.acquire(100);
long end1 = System.currentTimeMillis();
System.out.println("lock锁读操作耗时:" + (end1 - start1));
}
class WriteOperation implements Runnable {
@Override
public void run() {
writeLock.lock();
try {
writeSemaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
writeSemaphore1.release();
writeLock.unlock();
}
}
class ReadOperation implements Runnable {
@Override
public void run() {
readLock.lock();
try {
readSemaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
readSemaphore1.release();
readLock.unlock();
}
}
}
我们用四个信号量精准控制读写操作的进行,记录了读写操作的时间,假设一次写操作耗时10ms,读操作耗时100ms,分别用sleep模拟读写逻辑,同时对读写操作都加锁,锁的初始化可为普通ReentrantLock,可为ReentrantWriteReadLock,对比其效率:
可以看到ReentrantLock读操作耗时10秒左右,而ReentrantWriteReadLock的读操作耗时0.1秒左右。读性能可见一斑。
交换器用于两个线程之间交换数据,示例代码:
import java.util.concurrent.Exchanger;
/**
* 交换器
*
* @author zab
* @date 2019-11-10 12:00
*/
public class ExchangerTest {
String s1 = "s1";
String s2 = "s2";
Exchanger exchanger = new Exchanger<>();
public void f1() {
try {
s1 = exchanger.exchange(s1);
System.out.println("f1----" + s1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void f2() {
try {
Thread.sleep(1000);
s2 = exchanger.exchange(s2);
System.out.println("f2----" + s2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ExchangerTest et = new ExchangerTest();
new Thread(et::f2).start();
new Thread(et::f1).start();
}
}
这段代码表示f1和f2之间把字符串s1和s2交换一下,输出结果:
exchange方法工作的时候,必须两个线程同时到达才会交换数据,输出上图结果之前,其实有一秒的阻塞。