JUC:通过PrintAB管中窥豹

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,但是对于这个问题来说不算最佳方案,在此就不展开了;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值