线程交替打印的几种实现方式

秋风渐起

前言

前段时间同事去面了校招生,吐槽说候选人简历里写了熟悉多线程,但是连一道不限制任何实现方式的线程交替打印字符串都不会。

当时听到这个题的时候就想到了最常见的两种实现,觉得面试者一种都写不出来确实不太应该。后来没事的时候又思考这个问题,感觉对于校招生来说是个蛮不错的问题。
既能够考验候选人是否有实际写过创建线程的代码,又自然的把问题引导向了线程间的通信方式。

虽然听起来很有孔乙己问茴香豆的茴有几种写法的感觉。

下面简单的介绍一下我自己想到的几种写法,欢迎补充。

正片

1. volatile

volatile能够保证可见性和有序性。

可见性,被volatile修饰的变量的修改能立刻同步到主存,由于处理器缓存一致性机制,会使其他线程缓存中的数据失效,从而能够保证线程读到的都是最新数据。

有序性,防止指令重排序,指令重排序在单线程下是没有问题的,因为不会影响最终的结果,只是处理顺序上有不同,但是在多线程情况下,一旦指令进行重排序,会出现一些意料之外的结果。例如单例模式下,单例对象实例化完成之前引用提前暴露。当变量被声明为volatile之后,会在读写间建立happens-before关系,通过添加内存屏障,来禁止指令重排序。

代码

class A implements Runnable{

    @Override
    public void run() {
        while (true) {
            if (symbol){
                System.out.println("A");
                symbol = false;
            }
        }
    }
}

class B extends Thread{

    @Override
    public void run() {
        while (true){
            if (!symbol){
                System.out.println("B");
                symbol = true;
            }
        }
    }
}

这种实现的缺点就是不保证if语句里代码块的原子性,如果我们将print和修改symbol值的操作交换一下,就会发现输出乱掉了。这是因为在symbol值被修改后,其他线程立刻得到了修改后的值,并开始执行自己的逻辑,所以也会出现不可预料的输出结果。而把对symbol值得修改放在代码块最后一行的写法,得益于volatile关键字保证不进行指令重排序,最终输出了正确的打印结果。

并发编程艺术这本书里对volatile的语义做了总结,写变量线程实际上是对接下来读变量的线程发出消息,读变量线程会因为缓存失效去接收其他线程修改的消息,其实就是线程间通过共享内存进行了通信。

2. object wait/notify

wait/notify是Object实现的方法,wait是线程释放锁,进入等待队列,直到其他线程notify或者notifyAll才重新进入阻塞队列竞争锁;notify/notifyAll是唤醒等待队列中的线程进入阻塞队列中。

实现

static class C extends Thread {
    @Override
    public void run() {
        while (true) {
            synchronized (C.class) {
                System.out.println("C");
                C.class.notifyAll();
                try {
                    C.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

static class D extends Thread {
    @Override
    public void run() {
        while (true) {
            synchronized (C.class) {
                System.out.println("D");
                C.class.notifyAll();
                try {
                    C.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
3. Reentrantlock Condition await/single

当调用condition.await()方法后会使得当前获取lock的线程进入到等待队列,如果该线程能够从await()方法返回的话一定是该线程获取了与condition相关联的lock。

调用condition的signal或者signalAll方法可以将等待队列中等待时间最长的节点移动到同步队列中。

代码

static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();

static class E implements Runnable {

    @Override
    public void run() {
        while (true) {
            lock.lock();
            condition.signal();
            System.out.println("E");
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

static class F implements Runnable {

    @Override
    public void run() {
        while (true) {
            lock.lock();
            condition.signal();
            System.out.println("F");
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

在这段代码中,会有一个线程先获取到锁,而另一个线程在同步列队中等待。先获取到锁的代码会执行到await,由于await的效果,当前线程会释放锁并加入等待队列;这时另一个线程会获得锁,执行singal将等待队列里的线程移至同步队列,这时就又变成了一个线程得到锁在执行,另一个线程在同步队列中等待所。之后往复。

4. 信号量Semaphore

Semaphore和Reentrantlock一样,都是基于AQS框架实现的。

jdk官方文档对信号量的描述是:
A counting semaphore. Conceptually, a semaphore maintains a set of permits. Each acquire() blocks if necessary until a permit is available, and then takes it. Each release() adds a permit, potentially releasing a blocking acquirer. However, no actual permit objects are used; the Semaphore just keeps a count of the number available and acts accordingly.

Semaphores are often used to restrict the number of threads than can access some (physical or logical) resource.

通常用于限制同时访问资源的线程数。例如,某段代码只允许最多三个线程进入执行,那么就可以初始化Semaphore为3,每次进入时使用acquire进行减一,退出这段代码时使用release加1,就可以达到控制的目的。

下面的这段代码里的用法,通过对另一个线程中信号量的控制,来完成交替打印的逻辑.

private static Semaphore s1 = new Semaphore(1);
private static Semaphore s2 = new Semaphore(0);

static class G extends Thread{
    @Override
    public void run() {
        while (true){
            try {
                s1.acquire();
                System.out.println("G");
                s2.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

static class H extends Thread{
    @Override
    public void run() {
        while (true){
            try {
                s2.acquire();
                System.out.println("H");
                s1.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

semaphore针对的是资源的可进入次数,如果可进入次数为1,那么此时可以类比lock的实现,遍又想到了使用reentrantlock的condition实现的另一个变种,一个lock的两个condition,类似一个资源的两个semaphore。代码的写法几乎一样,就不再在这贴出来了。

由此又可以引出AQS框架和同步器的问题,总的来说可以深入问下去的地方太多了,很考验一个人的知识深度。我也很久没看过这一块了,当年了解的东西忘了个七七八八,所以接下来的这个坑等过段时间抽空复习之后再来填。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值