PrintAB ?
在我们面试Java并发的时候,面试官有一个经典问题叫做多线程循环打印"A"和"B",这题之所以经典,就是考察了Java的线程通信模型.学完JUC之后,咱们也能用多种方法解决这个问题,
show me the code!!
互斥锁+共享变量
互斥锁是Java并发模型最容易想到的解决方案,通过一个锁来保障只有一个线程成功打印,通过一个共享变量来决定哪个线程可以获得锁;
public class PrintAB{
private final volatile boolean isPrintA = true;
private final int loopTimes = 10;
class PrintA implements Runnable{
@Override
public void run(){
for(int i=0;i<loopTimes; i++){
synchronized(this){
while(!isprintA){
this.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.print("A");
isPrintA = false;
this.notifyAll();
}
}
}
}
class PrintB implements Runnable{
@Override
public void run(){
for(int i=0;i<loopTimes; i++){
synchronized(this){
while(isprintA){
this.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.print("B");
isPrintA = true;
this.notifyAll();
}
}
}
}
public void printAB(){
Thread a= new Thread(new PrintA()).start();
Thread b = new Thread(new PrintB()).start();
a.start();
b.start();
}
}
代码中使用了一个共享的对象作为锁,通过isPrintA变量来控制线程A和线程B的交替打印。在PrintA线程中,当isPrintA为false时,线程进入等待状态;当isPrintA为true时,线程打印"A"并将isPrintA设为false,然后唤醒其他等待的线程。在PrintB线程中,当isPrintA为true时,线程进入等待状态;当isPrintA为false时,线程打印"B"并将isPrintA设为true,然后唤醒其他等待的线程。通过循环10次,线程A和线程B交替打印"A"和"B"。
信号量Semaphore
对比共享变量和互斥锁的视线,信号量是一种性能更高的写法:
/**
* 使用信号量来控制打印AB <p>
* 信号量有两种方法 acquire和 release分别使 semaphore -1 和 +1<p>
* 当 semaphore >0 可以继续执行acquire之后的代码
*/
public class PrintAB {
public static void main(String[] args) {
final int loopTimes = 10;
Semaphore s1 = new Semaphore(1);
Semaphore s2 = new Semaphore(0);
new Thread(() -> {
for (int i = 0; i < loopTimes; i++) {
try {
s1.acquire();
System.out.print("A");
s2.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"threadA").start();
new Thread(() -> {
for (int i = 0; i < loopTimes; i++) {
try {
s2.acquire();
System.out.print("B");
s1.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"threadB").start();
}
}
在这个版本的代码中,我们使用了两个信号量,semaphoreA和semaphoreB。初始时,semaphoreA的许可数为1,semaphoreB的许可数为0。在PrintA线程中,先通过semaphoreA.acquire()获取semaphoreA的许可,然后打印"A",最后通过semaphoreB.release()释放semaphoreB的许可,使得PrintB线程可以获取semaphoreB的许可。在PrintB线程中,先通过semaphoreB.acquire()获取semaphoreB的许可,然后打印"B",最后通过semaphoreA.release()释放semaphoreA的许可,使得PrintA线程可以获取semaphoreA的许可。通过循环10次,线程A和线程B交替打印"A"和"B"。使用信号量可以更高效地实现线程的交替执行,避免了不必要的等待和唤醒操作,但是会有线程阻塞问题;
无锁的CAS
CAS(Compare And Swap) 通过不断的判断当前共享变量是否满足要求来实现,Java提供了一些Atomic的原子安全变量来完成安全的共享:
public class PrintAB {
private static volatile AtomicInteger turn = new AtomicInteger(0);
public static void main(String[] args) {
Thread threadA = new Thread(new PrintA());
Thread threadB = new Thread(new PrintB());
threadA.start();
threadB.start();
}
static class PrintA implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
while (turn.get() == 1) {
Thread.yield();
}
System.out.print("A");
turn.getAndIncrement();
}
}
}
static class PrintB implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
while (turn.get() == 0) {
Thread.yield();
}
System.out.print("B");
turn.decrementAndGet();
}
}
}
}
在这个版本的代码中,我们使用了AtomicInteger来保证对turn变量的原子性操作。turn变量表示当前轮到哪个线程执行打印操作,初始值为0。在PrintA线程中,通过循环判断turn的值是否为0,如果不是0,则调用Thread.yield()让出CPU时间片,让其他线程执行。当turn的值为0时,打印"A",然后通过turn.getAndIncrement()原子地将turn的值加1。在PrintB线程中,通过循环判断turn的值是否为1,如果不是1,则调用Thread.yield()让出CPU时间片,让其他线程执行。当turn的值为奇数时,打印"B",然后通过turn.decrementAndGet()原子地将turn的值-1。通过循环10次,线程A和线程B交替打印"A"和"B"。使用CAS操作可以实现对共享变量的原子性读取和修改,从而实现多线程的交替执行;
CAS 和 Semaphore哪个性能更好?
在性能方面,CAS(Compare and Swap)操作通常比信号量(Semaphore)更高效。这是因为CAS是一种轻量级的原子操作,它不需要进入内核态或阻塞线程,而是在用户态进行原子性的读取和修改。相比之下,信号量需要进行线程的阻塞和唤醒,涉及到内核态和用户态之间的切换,因此开销相对较大。
CAS操作适用于对单个变量进行原子性操作的场景,特别是在并发读取和修改的情况下。它可以避免线程竞争和锁的开销,因此在高并发的情况下通常具有较好的性能。
信号量适用于需要对资源进行控制和同步的场景,它可以限制并发访问的线程数量。信号量可以确保在达到一定条件之前,线程将被阻塞,从而协调线程的执行顺序。尽管信号量提供了更高级的同步机制,但由于涉及线程的阻塞和唤醒,因此相对于CAS而言,它的性能开销更大。
综上所述,CAS在性能方面通常更好,特别适用于对单个变量进行原子操作的场景。而信号量更适合于需要控制和同步多个线程访问共享资源的场景。然而,性能的优劣也取决于具体的使用情况和上下文环境,因此在选择使用CAS还是信号量时,需要综合考虑实际需求和性能特点。
LockSupport的park和unpark
public class PrintAB {
private static Thread threadA;
private static Thread threadB;
public static void main(String[] args) {
threadA = new Thread(new PrintA());
threadB = new Thread(new PrintB());
threadA.start();
threadB.start();
}
static class PrintA implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.print("A");
LockSupport.unpark(threadB);
LockSupport.park();
}
}
}
static class PrintB implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
LockSupport.park();
System.out.print("B");
LockSupport.unpark(threadA);
}
}
}
}
在这个版本的代码中,我们使用了LockSupport类来实现线程的阻塞和唤醒。在PrintA线程中,先打印"A",然后调用LockSupport.unpark(threadB)唤醒PrintB线程,接着调用LockSupport.park()将PrintA线程阻塞。在PrintB线程中,先调用LockSupport.park()将PrintB线程阻塞,等待被唤醒,然后打印"B",最后调用LockSupport.unpark(threadA)唤醒PrintA线程。通过循环10次,线程A和线程B交替打印"A"和"B"。使用park和unpark方法可以实现线程的阻塞和唤醒,从而实现多线程的交替执行。
除了上述方法还可以使用CyclicBarrier,但是对于这个问题来说不算最佳方案,在此就不展开了;