新的同步机制(新的锁类型)
一、ReentrantLock
ReentrantLock可以用于替代synchronized
1、用法
Lock lock = new ReentrantLock();
void m1() {
lock.lock(); //synchronized(this)
try {
// 业务代码
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//ReentrantLock必须在finally中进行主动释放锁。synchronized代码块执行结束后会自动释放,抛出异常时也会释放锁
lock.unlock();
}
}
2、特点
- 可以进行“尝试锁定”lock.tryLock(5, TimeUnit.SECONDS);这样无法锁定,或者在指定时间内无法锁定,线程可以决定是否继续等待。
- 可以调用lock.lockInterruptibly()方法,可以对线程interrupt方法做出响应, 在一个线程等待锁的过程中,可以被打断。
- 可以指定为公平锁。所谓公平锁是指其他线程进行锁竞争时,是否直接上来就竞争,还是进行排队(检查锁的线程等待队列,进入队列中排队)。如果直接竞争就是非公平锁,排队就是公平锁
public class ReentrantLockTest extends Thread {
private static ReentrantLock lock=new ReentrantLock(true); //参数为true表示为公平锁
public void run() {
for(int i=0; i<100; i++) {
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"获得锁");
}finally{
lock.unlock();
}
}
}
public static void main(String[] args) {
ReentrantLockTest rl=new ReentrantLockTest();
Thread th1=new Thread(rl);
Thread th2=new Thread(rl);
th1.start();
th2.start();
}
}
new ReentrantLock(true); //参数为true表示为公平锁
这时程序的输出结果为:线程1和线程2能够轮流执行,交替输出,并不是严格的1、2、1、2的结果。
new ReentrantLock();
这时程序要等到线程1执行完,线程2才会执行
二、CountDownLatch
1、概念
它是一个同步工具类,允许一个或多个线程一直等待,直到其他线程运行完成后再执行。
/**
* A synchronization aid that allows one or more threads to wait until
* a set of operations being performed in other threads completes.
*
* @since 1.5
* @author Doug Lea
*/
public class CountDownLatch {
}
2、使用场景:
场景1 让多个线程等待:模拟并发,让并发线程一起执行
为了模拟高并发,让一组线程在指定时刻(秒杀时间)执行抢购,这些线程在准备就绪后,进行等待(CountDownLatch.await()),直到秒杀时刻的到来,然后一拥而上;
这也是本地测试接口并发的一个简易实现。
在这个场景中,CountDownLatch充当的是一个发令枪的角色;
就像田径赛跑时,运动员会在起跑线做准备动作,等到发令枪一声响,运动员就会奋力奔跑。和上面的秒杀场景类似,代码实现如下:
CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
//准备完毕……运动员都阻塞在这,等待号令
countDownLatch.await();
String parter = "【" + Thread.currentThread().getName() + "】";
System.out.println(parter + "开始执行……");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Thread.sleep(2000);// 裁判准备发令
countDownLatch.countDown();// 发令枪:执行发令
运行结果:
【Thread-0】开始执行……
【Thread-1】开始执行……
【Thread-4】开始执行……
【Thread-3】开始执行……
【Thread-2】开始执行……
我们通过CountDownLatch.await(),让多个参与者线程启动后阻塞等待,然后在主线程 调用CountDownLatch.countdown(1) 将计数减为0,让所有线程一起往下执行;
以此实现了多个线程在同一时刻并发执行,来模拟并发请求的目的。
场景2 让单个线程等待:多个线程(任务)完成后,进行汇总合并
很多时候,我们的并发任务,存在前后依赖关系;比如数据详情页需要同时调用多个接口获取数据,并发请求获取到数据后、需要进行结果合并
这其实都是:在多个线程(任务)完成后,进行汇总合并的场景。
代码实现如下:
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
final int index = i;
new Thread(() -> {
try {
Thread.sleep(1000 + ThreadLocalRandom.current().nextInt(1000));
System.out.println("finish" + index + Thread.currentThread().getName());
countDownLatch.countDown();//将计数器减1。
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
countDownLatch.await();// 主线程在阻塞,Latch门闩/栅栏上;当计数器==0,就唤醒主线程往下执行。
System.out.println("主线程:在所有任务运行完成后,进行结果汇总");
运行结果:
finish4Thread-4
finish1Thread-1
finish2Thread-2
finish3Thread-3
finish0Thread-0
主线程:在所有任务运行完成后,进行结果汇总
在每个线程(任务) 完成的最后一行加上CountDownLatch.countDown(),让计数器-1;
当所有线程完成-1,计数器减到0后,主线程往下执行汇总任务。
3、CountDownLatch与Thread.join
CountDownLatch的作用就是允许一个或多个线程等待其他线程完成操作,看起来有点类似join() 方法,但其提供了比 join() 更加灵活的API。
CountDownLatch可以手动控制在n个线程里调用n次countDown()方法使计数器进行减一操作,也可以在一个线程里调用n次执行减一操作。
而 join() 的实现原理是不停检查join线程是否存活,如果 join 线程存活则让当前线程永远等待。所以两者之间相对来说还是CountDownLatch使用起来较为灵活。
三、CyclicBarrier
概念
一个同步工具类,是为了允许多个线程在一个障碍(Barrier)点上互相等待,当大家都达到这个障碍的时候,才继续往下执行。为什么叫重复障碍(Cyclic Barrier)呢?因为当所有的线程都到达这个点之后,还可以再重复使用它。
/**
* A synchronization aid that allows a set of threads to all wait for
* each other to reach a common barrier point. CyclicBarriers are
* useful in programs involving a fixed sized party of threads that
* must occasionally wait for each other. The barrier is called
* <em>cyclic</em> because it can be re-used after the waiting threads
* are released.
*/
public class CyclicBarrier {
}
在下面的代码中,我们首先创建了一个计数器初始值为 2 的 CyclicBarrier,你需要注意的是创建 CyclicBarrier 的时候,我们还传入了一个回调函数,当计数器减到 0 的时候,会调用这个回调函数。
线程 T1 负责查询订单,当查出一条时,调用 barrier.await() 来将计数器减 1,同时等待计数器变成 0;线程 T2 负责查询派送单,当查出一条时,也调用 barrier.await() 来将计数器减 1,同时等待计数器变成 0;当 T1 和 T2 都调用 barrier.await() 的时候,计数器会减到 0,此时 T1 和 T2 就可以执行下一条语句了,同时会调用 barrier 的回调函数来执行对账操作。
非常值得一提的是,CyclicBarrier 的计数器有自动重置的功能,当减到 0 的时候,会自动重置你设置的初始值。这个功能用起来实在是太方便了。
// 订单队列
Vector<P> pos;
// 派送单队列
Vector<D> dos;
// 执行回调的线程池
Executor executor =
Executors.newFixedThreadPool(1);
final CyclicBarrier barrier =
new CyclicBarrier(2, ()->{
executor.execute(()->check());
});
void check(){
P p = pos.remove(0);
D d = dos.remove(0);
// 执行对账操作
diff = check(p, d);
// 差异写入差异库
save(diff);
}
void checkAll(){
// 循环查询订单库
Thread T1 = new Thread(()->{
while(存在未对账订单){
// 查询订单库
pos.add(getPOrders());
// 等待
barrier.await();
}
});
T1.start();
// 循环查询运单库
Thread T2 = new Thread(()->{
while(存在未对账订单){
// 查询运单库
dos.add(getDOrders());
// 等待
barrier.await();
}
});
T2.start();
}
四、CountDownLatch与CyclicBarrier
CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
- CountDownLatch一般用于一个或多个线程,等待其他线程执行完任务后,才执行(CountDownLatch中侧重的是一个和一组线程之间的依赖关系。)
- CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程在同时执行(CyclicBarrier更强调一组线程之间的关系,这些线程间互相协调等待,强调同时。)
另外,CountDownLatch是减计数,计数减为0后不能重用;而CyclicBarrier是加计数,可置0后复用。
内容来自:
CountDownLatch的两种常用场景:https://mp.weixin.qq.com/s/RhK_BrYrooGGYbN5OvyXow
CountDownLatch和CyclicBarrier让多线程步调一致:
五、Phaser
可以看作是CyclicBarrier的升级版,可以将锁分为不同的阶段。每个阶段等一组线程集结完毕,调用onAdvance方法,并进入下一阶段。
线程到达某个阶段的时候,可以根据条件判断,该线程是否需要继续前进。
// 从Phaser继承,重写onAdvance方法
class TestPhaser extends Phaser {
// 该方法在某阶段的所有线程都到达后,自动调用(即phaser.arriveAndAwaitAdvance()阻塞结束之后)
@Override
protected boolean onAdvance(int phase, int registeredParties) {
switch (phase) {
case 0:
// TODO
return false;
case 1:
// TODO
return false;
case 2:
// TODO
return false;
case 3:
// TODO
return true;
default:
return true;
}
}
}
TestPhaser phaser = new TestPhaser ();
// 7代表进入下一阶段所需要的线程数量
phaser.bulkRegister(7);
// 定义某一个阶段
private void test() {
if(满足某些条件) {
//TODO
// 等待线程集结完毕后
phaser.arriveAndAwaitAdvance();
} else {
// 取消注册,取消注册后,之后的阶段该线程都不参与了,注册数-1
phaser.arriveAndDeregister();
// 增加注册线程
//phaser.register()
}
}
六、ReadWriteLock 读写锁
场景
有很多需求是读可以同时读,但是有人在写的时候,别人就不能读了(避免产生脏读)。而很多业务的大部分需求的是读多写少(经常查询,很少修改)。这时如果不分情况一律加排他锁,效率就会比较低,必须等别人读完才能读(串行)
概念
共享锁:又称为读锁。一个线程获取读锁之后,其他线程也能同时获取这个锁,即允许多个线程同时获取一个锁。
排他锁,又称为写锁。也称作独占锁或互斥锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。
实例
ReentrantLock就是一种排他锁。CountDownLatch是一种共享锁。这两类都是单纯的一类,即,要么是排他锁,要么是共享锁。
ReentrantReadWriteLock是同时包含排它锁和共享锁特性的一种锁,这里主要以ReentrantReadWriteLock为例来进行分析学习。
使用ReentrantReadWriteLock的读锁时,使用的便是共享锁的特性。
使用ReentrantReadWriteLock的写锁时,使用的便是排他锁的特性;
- 代码
static Lock lock = new ReentrantLock();
private static int value;
static CountDownLatch latch = new CountDownLatch(20);
static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
static Lock readLock = readWriteLock.readLock();
static Lock writeLock = readWriteLock.writeLock();
public static void read(Lock lock) {
lock.lock();
try {
Thread.sleep(1000);
System.out.println("read over!");
//模拟读取操作
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void write(Lock lock, int v) {
lock.lock();
try {
Thread.sleep(1000);
value = v;
System.out.println("write over!");
//模拟写操作
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
System.out.println(new Date());
Runnable readR = ()-> read(lock);
Runnable writeR = ()->write(lock, new Random().nextInt());
// Runnable readR = ()-> read(readLock);
// Runnable writeR = ()->write(writeLock, new Random().nextInt());
for(int i=0; i<18; i++) new Thread(readR).start();
for(int i=0; i<2; i++) new Thread(writeR).start();
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(new Date());
}
- 执行结果
当使用ReentrantLock的时候,因为加的是排他锁,所以read()方法需要等18个读线程逐个读取完毕,因此需要18s,write()方法同样了2s
而使用ReentrantReadWriteLock的readLock读锁时,因为时共享锁,因此18个线程调用read()大概在1s内即可执行完毕。write()方法同样用了2s
小结:
读写锁类似于 ReentrantLock,也支持公平模式和非公平模式。读锁和写锁都实现了 java.util.concurrent.locks.Lock 接口,所以除了支持 lock() 方法外,tryLock()、lockInterruptibly() 等方法也都是支持的。但是有一点需要注意,那就是只有写锁支持条件变量,读锁是不支持条件变量的,读锁调用 newCondition() 会抛出 UnsupportedOperationException 异常。
ReentrantReadWriteLock的实现能满足绝大多数的多线程环境,有如下特点:
- 支持两种优先级模式,以时间顺序获取锁和以读、写交替优先获取锁的模式;
- 当获得了读锁或写锁后,还可重复获取读锁或写锁,即ReentrantLock(可重入锁);
- 获得写锁后还可获得读锁,但获得读锁后不可获得写锁;
支持将写锁降级为读锁,但反之不行;
支持在等待锁的过程中中断;
对写锁支持Condition(用于取代wait,notify和notifyAll);
支持锁的状态检测,但仅仅用于监控系统状态而并非同步控制;
七、Semaphore信号量
场景
用于限流,例如收费站和买票口,同时行进的有4条车道,但是过收费站的时候只有两个收费通道,同时只能有两辆车通过。
- 代码
public static void main(String[] args) {
//允许两个线程同时执行
Semaphore s = new Semaphore(2);
// Semaphore s = new Semaphore(1);
//Semaphore s = new Semaphore(2, true);
new Thread(()->{
try {
// 获取锁
s.acquire();
System.out.println("T1 running...");
Thread.sleep(200);
System.out.println("T1 running...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
s.release();
}
}).start();
new Thread(()->{
try {
s.acquire();
System.out.println("T2 running...");
Thread.sleep(200);
System.out.println("T2 running...");
s.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
- 执行结果
当参数为2时,代表允许两个线程同时执行
Semaphore s = new Semaphore(2);
结果:T1、T2同时执行
当参数为1时,代表只允许已一个线程执行
Semaphore s = new Semaphore(1);
结果:T1执行完T2才执行
八、Exchanger
可以用于两个线程之间交换数据,两个线程都调用exchange()方法
static Exchanger<String> exchanger = new Exchanger<>();
public static void main(String[] args) {
new Thread(()->{
String s = "T1";
try {
s = exchanger.exchange(s);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + s);
}, "t1").start();
new Thread(()->{
String s = "T2";
try {
s = exchanger.exchange(s);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + s);
}, "t2").start();
}
相当于两个线程分别把要交换的东向放到一个要交换的位置,当两个位置都有东西的时候,进行交换。每个线程取走各自位置上的东西。