1. Atomic类和线程同步新机制
这章我们来继续将Amotic的问题,然后将除了synchronized之外的锁。事实上,无锁化操作比synchronized效率更高。
下面写个程序分别说明synchronize 和longAdder,Amotic
package com.learn.thread.three;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
public class TestAtomicOrSynOrLongAdder {
static long count1 = 0L;
private static final AtomicLong count2 = new AtomicLong(0L);
private static LongAdder count3 = new LongAdder();
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[1000];
// AtomicLong
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() ->{
for (int k = 0; k < 10000; k++) {
count2.incrementAndGet();
}
});
}
long start = System.currentTimeMillis();
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
}
long end = System.currentTimeMillis();
System.out.println("Amotice " + count2.get() + "time " + (end - start));
// synchronized Long
Object lock = new Object();
for (int i = 0; i < threads.length; i++) {
threads[i] =
new Thread(() -> {
for (int k = 0; k < 10000; k++) {
synchronized (lock) {
count1 ++;
}
}
});
}
start = System.currentTimeMillis();
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
}
end = System.currentTimeMillis();
System.out.println("Sync: " + count1 + "time " + (end - start));
// LongAdder
for (int i = 0; i < threads.length; i++) {
threads[i] =
new Thread(() -> {
for (int k = 0; k < 10000; k++) {
count3.increment();
}
});
}
start = System.currentTimeMillis();
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
}
end = System.currentTimeMillis();
System.out.println("LongAdder: " + count1 + "time " + (end - start));
}
}
至于以上各种“锁”的效率,要分情况使用。先来看这三种的优势
Amotic和synchronized的对比下,synchronzied有可能要去操作系统申请重量级锁,所以synchronized的效率是偏低的
LongAdder和Amotic对比,LongAdder的内部做了一个分段锁,类似于分段锁的概念,在它的内部的时候,会把一个值放到一个数组里,比如说数组长度为4,最开始是0,1000个线程,250个线程就放在第一个数组元素里,以此类推,每一个都网上递增算出来的结果加在一起。
先来复习一下之前将的synchronized的细节
这是有锁分级的情况,在一定情况下,synchronized是pian是效率是好的,但是如果升级为重量级锁,那么效率是低的。
执行时间短,要同步的代码量少,线程数少用CAS
执行时间长,线程数多,用系统锁
当线程数为一万个的时候
2. 复习完了synchronized,下面看看基于CAS的一些新型锁,先来讲这些锁的用法,再来说这些锁的原理
2.1. ReentrantLock
第一种就是之前讲过的可重入锁ReentrantLock,其实synchronized也是一种可重入锁,之前讲述线程synchronized概论的时候就说过方法锁里面调用方法锁或者说子类和父类synchronized(this)就是同一把锁,是会使用到同一个锁的!这就是可重入锁。
ReentrantLock 是完全可以替代synchronized的,就是把原来写synchronized的地方换写成lock.lock(),加完锁之后需要注意记得lock.unlock解锁,因为synchronized是自动解锁的,大括号执行完就结束了,lock不行,lock必须手动解锁,建议手动解锁放在try…finally里面保证最好一定要解锁,不然的话,上锁之后中间执行的过程就有问题了,死在那里,别人就永远别想拿到锁了!!!
package com.learn.thread.three;
import com.learn.thread.second.T;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestReentranLock {
Lock lock = new ReentrantLock();
void m1() {
try {
lock.lock();
for (int i = 0; i < 10; i++) {
TimeUnit.SECONDS.sleep(2L);
System.out.println(i);
}
}catch (Exception ex) {
} finally {
lock.unlock();
}
}
void m2() {
try {
lock.lock();
System.out.println("m2");
}catch (Exception ex) {
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
TestReentranLock testReentranLock = new TestReentranLock();
new Thread(() -> {
testReentranLock.m1();
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
testReentranLock.m2();
}).start();
}
}
ReentrantLock比synchronized强大的地方就是tryLock进行尝试锁定,不管是否锁定,方法都将继续执行,synchronized如果搞不定的会,就会阻塞。但是用ReentrantLock你自己就可以决定你到底要不要wait
package com.learn.thread.three;
import java.sql.Time;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestReentranLockTryLock {
private static Lock lock = new ReentrantLock();
void m1() {
try {
lock.lock();
for (int i = 0; i < 10; i++) {
TimeUnit.SECONDS.sleep(1L);
System.out.println(i);
}
}catch (Exception ex) {
} finally {
lock.unlock();
}
}
/**
* 使用tryLock进行尝试锁定,不管锁定与否方法都将继续执行
* 可以使用返回值来判断是否锁定,true表示加锁成功,false表示枷锁失败
* 同样也可以指定trylock的时间
*/
void m2() {
// 这里不加时间参数,默认是锁一秒的
boolean locked = lock.tryLock();
System.out.println("m2 ... " + locked);
if (locked) {
System.out.println("我被锁住了,现在释放锁进入m2");
lock.unlock();
}
try {
// 这里只是加锁的时间,过了5秒以后,锁释放
System.out.println(locked);
locked = lock.tryLock(5, TimeUnit.SECONDS);
System.out.println("m2 ....." + locked);
}catch (Exception ex) {
System.out.printf(ex.toString());
} finally {
// 这里如果不去判断,会异常
if (locked) {
lock.unlock();
}
}
}
public static void main(String[] args) {
TestReentranLockTryLock testReentranLockTryLock = new TestReentranLockTryLock();
new Thread(() -> {
testReentranLockTryLock.m1();
}).start();
try {
TimeUnit.SECONDS.sleep(1);
}catch (Exception ex) {
}
new Thread(() -> {
testReentranLockTryLock.m2();
}).start();
}
}
当然除了tryLock,还可以用lock.lockInterruptibly对interrupt()做出相应,可以被打断的加锁,比如说我们可以调用一个t2.interrupt()打断它的等待,让它自己可以加锁。如果有线程1上来加锁,加锁以后开始没完没了的睡,如果线程1加了锁,那么线程2就永远无法得到锁了,这时候就可以使用interrupt强制打断线程2的等待,通过异常的形式让线程2去执行。
lockInterruptibly() 方法的作用:如果当前线程未被中断则获得锁,如果当前线程被中断则出现异常。
void m4() {
try {
// 强制打断锁
lock.lockInterruptibly();
System.out.println("线程2 打断线程1,开始执行");
try {
TimeUnit.SECONDS.sleep(5);
}catch (Exception ex) {
}
System.out.println("线程2 执行完成");
}catch (Exception ex) {
System.out.println("线程2被中断着,可以去完成其他事情");
} finally {
System.out.println(lock.tryLock());
lock.unlock();
}
}
public static void main(String[] args) {
TestReentranLockTryLock testReentranLockTryLock = new TestReentranLockTryLock();
new Thread(() -> {
testReentranLockTryLock.m3();
}).start();
Thread t2 = new Thread(() -> {
testReentranLockTryLock.m4();
});
t2.start();
try {
TimeUnit.SECONDS.sleep(1);
}catch (Exception ex) {
}
// t2 如果
t2.interrupt();
}
void m3() {
try {
lock.lock();
System.out.println("线程1 锁住,无休止的睡眠");
try {
TimeUnit.SECONDS.sleep(100000000);
}catch (Exception ex) {
}
}catch (Exception ex) {
} finally {
lock.unlock();
}
}
ReentrantLock还可以指定公平锁。公平锁的意思就是当我们new 一个ReentrantLock你可以传一个参数为true,这个表示公平锁,公平锁的意思是谁等在前面就让谁先执行,而不是后来了就执行。ReentrantLock默认是非公平锁
package com.learn.thread.three;
import com.learn.thread.second.T;
import java.util.concurrent.locks.ReentrantLock;
/**
* 测试公平锁
*/
public class TestReentrantLockTrue extends Thread{
private static ReentrantLock lock = new ReentrantLock(true);
@Override
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) {
TestReentrantLockTrue testReentrantLockTrue = new TestReentrantLockTrue();
Thread t1 = new Thread(testReentrantLockTrue);
Thread t2 = new Thread(testReentrantLockTrue);
t1.start();
t2.start();
}
}
2.2.回顾ReentrantLock
首先ReentrantLock是可以替代synchronized的,本身底层就是cas
tryLock:自己来控制,控制不住锁怎么办
lockInterruptibly: 用异常的形式,取消等待
公平锁与非公平锁的等待
以后聊AQS的时候,实际上它内部用的是park和unpark,也不是全部cas,也是一个锁升级的概念,只不过这个锁升级做的比较隐匿,在你等待这个队列的时候如果你拿不到还是会进入一个阻塞状态,前面至少有一个cas状态,它不像原先就直接进入阻塞状态了。
2.3.CountDownLatch
CountDownLatch 叫倒数,Latch是门栓的意思(倒数的一个门栓, 5 ,4,3,2,1 数到了,我这个门栓就打开了)
看一下下面这个程序unsingcountDownLatch,new了100个线程,接下来,又来个100个数量的CountDownLatch,这就是设置了门栓,记录个数为1000,每一个线程结束的时候就让latch.countDown(),然后启动所有的线程,在latch.await(),最后结束。
latch.countDown()是和latch.await()连用的,countDown是看住门栓,等每个线程执行到await()的时候就会按一下CountDown是,让其在原来的基础上减1,一直到这个数字变成0的时候就会被打开,这就是它们的概念,是用来等着线程结束的
用join实际上不太好控制,必须要你线程结束了才能控制,但是如果是一个门栓的话我在线程里不听得CountDown,在一个线程里就可以控制这个门栓什么时候可以往前走,用join我只能是当前线程结束了,你才能自动往前走,用join可以,但是用countDown更加灵活
package com.learn.thread.three;
import com.learn.thread.first.T;
import java.util.concurrent.CountDownLatch;
/**
*
*/
public class TestCountDownLatch {
/**
* 用CountDownLatch控制线程结束
*/
private static void usingCountDownLacth() {
Thread[] threads = new Thread[100];
CountDownLatch countDownLatch = new CountDownLatch(threads.length);
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
int result = 0;
for (int j = 0; j < 10000; j++) {
result += j;
}
System.out.println(result);
// 看住门栓,每调用一次就减1
countDownLatch.countDown();
System.out.println("我看住门栓了");
});
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
try {
// 当countDown减为0的时候,这里就会执行了
countDownLatch.await();
System.out.println("我来减门栓的数量了");
}catch (Exception ex) {
}
System.out.println("end Latch");
}
/**
* 模拟join 不好控制线程
*/
private static void usingJoin() {
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
int result = 0;
for (int j = 0; j < 10000; j++) {
result += j;
}
System.out.println(result);
});
}
for (int i = 0; i < threads.length; i++) {
System.out.println("线程start");
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
try {
System.out.println("执行join");
threads[i].join();
// 这里模拟线程结束结束之后的动作
System.out.println("equals countDown.latch");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("end usingJoin");
}
public static void main(String[] args) {
usingJoin();
// usingCountDownLacth();
}
}
2.4.CyclicBarrier
这个名字叫CyclicBarrier,也是一个同步工具,意思就是循环栅栏,就是什么时候人满了就把栅栏推到,全部放出去,之后栅栏又重新起来,再来人,满了,放出去再继续。
举例
CyclicBarrier的概念比如说一个复杂的操作,需要访问数据库,需要访问网络,需要方位文件。有一种方式是顺序执行,这事一种非常低的效率,还有一种方式就是并发的执行,用不同的线程去操作,并且是这三个步骤有结果了我再进行下一次操作,这时候就用到了CyclicBarrier
package com.learn.thread.three;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class TestCyclicBarrier {
private CyclicBarrier cyclicBarrier;
public static void main(String[] args) {
testSout();
}
private static void testSout() {
// 这里会起一个线程去执行第二个参数的内容可以为空
CyclicBarrier cyclicBarrier = new CyclicBarrier(20, () -> {
System.out.println("满人了");
});
for (int i = 0; i < 400; i++) {
new Thread(() -> {
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
2.5.Phaser
Phaser更像结合了CountDownLatch和CyclicBarrier,中文意思就是阶段。
Phaser是按照不同的阶段来对线程进行执行,就是它本身是维护着一个阶段这样的一个成员变量,比如说第0个阶段,第一个阶段,每个阶段不同的时候这个线程都可以往前走,有的线程走个某个阶段就停了,有的线程一直会走到结束。
你的程序中如果说用到分好几个阶段执行,而且有个阶段必须得几个人共同参与的一种情形就可能会用到Phaser
下面我们模拟一个场景结婚,分成4个阶段,分别是到达、吃饭、离开、洞房。首先吃饭之前必须要所有客人到达婚礼线程,离开也是要所用人吃完饭才离开,但是洞房是新郎和新娘的事情,客人们必须离开。
package com.learn.thread.three.marry;
import java.util.Random;
import java.util.concurrent.Phaser;
/**
* 人类,用来模拟每一个阶段人的操作
*/
public class Person implements Runnable {
public Person(String name) {
this.name = name;
}
private String name;
private void eat() {
System.out.println(this.name + "吃饭");
// 进入栅栏阶段
TestPhaser.phaser.arriveAndAwaitAdvance();
}
private void arrive() {
System.out.println(this.name + "到达婚礼现场");
TestPhaser.phaser.arriveAndAwaitAdvance();
}
private void leave() {
System.out.println(this.name + "离开");
TestPhaser.phaser.arriveAndAwaitAdvance();
}
private void hug() {
if (name.equals("新郎") || "新娘".equals(name)) {
System.out.println(this.name + "洞房了");
TestPhaser.phaser.arriveAndAwaitAdvance();
}else {
// 其他线程都不参与,控制栅栏的个数
TestPhaser.phaser.arriveAndDeregister();
// 还可以往栅栏上加线程
//TestPhaser.phaser.register();
}
}
@Override
public void run() {
arrive();
eat();
leave();
hug();
}
static class TestPhaser {
static Random random = new Random();
static MarriagePhaser phaser = new MarriagePhaser();
}
static class MarriagePhaser extends Phaser {
/**
* 线程抵达这个栅栏的时候,所有的线程都满足了这个第一个栅栏的条件了这个方法
* 会被自动调用
*
* @param phase 第几个阶段,从0开始
* @param registeredParties 这个阶段有多少线程参与
* @return
*/
@Override
protected boolean onAdvance(int phase, int registeredParties) {
switch (phase) {
case 0:
System.out.println(phase + "所有人都到齐了~" + registeredParties);
return false;
case 1:
System.out.println(phase + "所有人都吃完饭了~" + registeredParties);
return false;
case 2:
System.out.println(phase + "所有人都离开了~" + registeredParties);
return false;
case 3:
System.out.println(phase + "婚礼结束~ 新浪和新娘抱抱" + registeredParties);
return true;
default:
return true;
}
}
}
public static void main(String[] args) {
TestPhaser.phaser.bulkRegister(7);
for (int i = 0; i < 5; i++) {
new Thread(new Person("p" + i)).start();
}
new Thread(new Person("新郎")).start();
new Thread(new Person("新娘")).start();
}
}
分析上面的程序,我们可以看到phaser.arriveAndAwaitAdvance方法是在进入栅栏前停驻,等线程的数量达到了就会自动调用onAdvance方法,返回false说明不是最后的阶段,返回true就是说到达了最后的阶段。最后phaser.arriveAndDeregister方法是注销线程,让线程不再参与阶段的执行。
2.6.ReadWriteLock读写锁
读写锁的本质就是共享锁和排他锁,读锁就是共享锁,写锁就是排他锁,读写有很多种情况,比如说你的数据库里某条数据你放在内存里读的特别多,但是改的时候并不多。
我们先来自己定义一套读写锁
假设有两个方法,一个read方法,一个write方法,read的时候我需要往里头传一把锁,这个锁我们自己定,可以是排他锁,也可以读锁或者写锁,write的时候同样需要传这把锁,同时你传一个新值,在这里值里面传一个内容。我们模拟这个操作,读的是一个Int类型的值,读的时候上锁,设置一秒钟,完了之后read over 最后unlock,然后写锁,锁定之后睡1000秒,然后把新的值给value,write over之后解锁。
我们可以用之前的ReentrantLock进行加锁,分析一下这种情况,第一种方式就是直接new ReentrantLock传进去,主程序定义了一个Runnable对象,第一个是调用read方法,第二个是调用write方法同时往里边扔一个随机值,然后启18个读线程,启2个写线程,这个两个我要执行完的话,因为是用了ReentrantLock加锁,锁的一秒钟内不没有任何线程可以拿到锁,每一个线程执行完都要1秒钟,那么20个线程就需要20秒。
我们完全可以按功能加锁
上述无非两种功能,读和写,那么能不能读的时候,所有读操作都可以共享这把锁,写的时候不让读呢?ReentrantReadWirteLock是ReentrantLock的一种实现,可以实现上述的思想。它能分出两把锁,一把readLock,一把writeLock。这两把锁在我读的时候扔进去,因此,18个线程读是可以在一秒钟完成工作的,所以读写锁效率会大大提高
下面我们看看两种方法的效率
package com.learn.thread.three.ReentrantReadWriteLock;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class TestReentrantReadWriteLock {
static Lock lock = new ReentrantLock();
private static int value = 10;
static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
static Lock readLock = readWriteLock.readLock();
static Lock wirteLock = readWriteLock.writeLock();
/**
* 普通方式加锁
*
* @param lock 锁
*/
public static void read(Lock lock) {
try {
lock.lock();
Thread.sleep(1000);
System.out.println("取值" + value);
System.out.println("read over");
} catch (Exception exception) {
} finally {
lock.unlock();
}
}
/**
* 写锁
*
* @param lock 锁
* @param v 新值
*/
public static void write(Lock lock, int v) {
try {
lock.lock();
Thread.sleep(1);
value = v;
System.out.println("写了" + value);
} catch (Exception ex) {
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
testNoReadAndWriteLock();
//testReadAndWriteLock();
}
static void testNoReadAndWriteLock() {
Runnable runnable = () -> read(lock);
Runnable write = () -> write(lock, new Random().nextInt());
for (int i = 0; i < 18; i++) {
new Thread(runnable).start();
}
for (int i = 0; i < 2; i++) {
new Thread(write).start();
}
}
static void testReadAndWriteLock() {
Runnable runnable = () -> read(readLock);
Runnable write = () -> write(wirteLock, new Random().nextInt());
for (int i = 0; i < 18; i++) {
new Thread(runnable).start();
}
for (int i = 0; i < 2; i++) {
new Thread(write).start();
}
}
}
第一种效率明显上比第二种慢多了
以后还写不写synchronized?分布式锁怎么实现的?
以后一般都不会用这些新锁,多数用到synchronized,只有特别特别追求效率的时候才用到这些新的锁,现在的分布式锁很多,主要有redis和ZooKeeper都可以实现分布式锁,数据库也可以实现,但是数据库实现效率就低了。
给大家讲一个简单的例子,就说秒杀这个事情,在开始秒杀之前它会从数据库读取某一个数据,比如电视机500台,只能最多销售500台,完成这件事情是前面的线程访问同一个数,最开始是0一直涨到500就结束,需要加锁,从0递增。如果是单机的,LongAdder和AtomicIntegr就可以搞定。如果是分布式的,对一个数进行上锁,redis是单线程的,所以扔在一台机器上就ok。
2.7.Semaphore
词面意思就是信号灯,可以往里边传一个数,permits是允许的数量,你可以想着有几个信号灯,灯闪烁着数字表示到底允许几个来参考我这个信号灯。
s.acquire()这个方法叫做阻塞方法,阻塞方法的意思说我大概acquire不到的话我就停在这里。acquire的意思就是得到,如果我Semaphore s = new Semaphore(1)写的是1,我取一下,acquire一下他就变成0,当变成0之后,别人是acquired不到的,然后继续执行,线程结束之后注意要s.release(),执行完该执行的时候就把他release掉,release又把0变回去1,还原化。
Semaphore的含义也是限流,比如说你在买票,Semaphore写5,就是说只能5个人同时买票。acquire的意思叫获取这把锁,线程如果想继续往下执行,必须得从Semaphore里获取一个许可,他一共有5个许可用到了0你就得给我等着。
下面举例一个场景
例如,有一个八条车道的机动车道,这里只有两个收费站,到这里,谁acquire得到其中某一个谁执行。
默认Semaphore是非公平的,new Semaphore(2, true)第二个值传true才是设置公平,公平这个事情是有一堆队列在哪儿等,大家伙过来排队。用车道和收费站来举例子,就是我们有四辆车都在等着进一个车道,当后面再来一辆车的时候,它不会抄到前面去,这才叫公平,所以说内部是有队列的,不仅内部是有队列的,本章所讲的ReentrantLock,CountDownLatch,CyclicBarrier,Phaser,ReadWriteLock,Semaphore还有后边讲到Exchanger都是用同一个队列,同一个类实现的,这个类叫做AQS。
package com.learn.thread.three;
import java.util.concurrent.Semaphore;
public class TestSemaphore {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3, true);
new Thread(() -> {
try {
// 阻塞方法,如果线程acquire不到,就停在这里,等别的线程释放
semaphore.acquire();
System.out.println("t1 running");
Thread.sleep(1000);
System.out.println("t1 ending");
} catch (Exception ex) {
ex.printStackTrace();
} finally {
semaphore.release();
}
}).start();
new Thread(() -> {
try {
semaphore.acquire();
System.out.println("t2 running");
Thread.sleep(1000);
System.out.println("t2 ending");
} catch (Exception ex) {
ex.printStackTrace();
} finally {
semaphore.release();
}
}).start();
}
}
2.8.Exchanger
这个Exchanger叫做交换器,是两个线程互相交换数据用的。比如说第一个线程有一个成员变量s,然后exchanger.exchange(s),第二个也是这样,t1线程名字叫t1,第二个线程名字叫t2,到最后,打印出来你会发现他们两的数据交换了。线程间通信的方式非常多,这只是其中的一种,就是线程之间交换数据用的。
Exchanger 你可以想象成一个容器,这个容器有两个值,两个线程,两个格的位置,第一个线程执行到exchanger.exchange的时候,阻塞。但是要注意我这个exchange方法的时候是往里面扔了一个值,你可以认为把t1扔到第一个格子了,然后第二个线程开始执行,也执行到exchange方法了,把t2扔到第二个格子里,接下来两个线程交换了一下,t1扔给t2,t2扔给了t1,两个线程继续往前跑。Exchanger只能是两个线程之间,交换一个东西只能两两进行
下面举一个游戏中两个人状态交换
package com.learn.thread.three;
import java.util.concurrent.Exchanger;
public class TestExchanger {
static Exchanger exchanger = new Exchanger<>();
public static void main(String[] args) {
new Thread(() -> {
String s = "t1";
try {
s = exchanger.exchange(s);
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + s);
},"线程1").start();
new Thread(() -> {
String s = "t2";
try {
s = exchanger.exchange(s);
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + s);
},"线程2").start();
}
}