秋风渐起
前言
前段时间同事去面了校招生,吐槽说候选人简历里写了熟悉多线程,但是连一道不限制任何实现方式的线程交替打印字符串都不会。
当时听到这个题的时候就想到了最常见的两种实现,觉得面试者一种都写不出来确实不太应该。后来没事的时候又思考这个问题,感觉对于校招生来说是个蛮不错的问题。
既能够考验候选人是否有实际写过创建线程的代码,又自然的把问题引导向了线程间的通信方式。
虽然听起来很有孔乙己问茴香豆的茴有几种写法的感觉。
下面简单的介绍一下我自己想到的几种写法,欢迎补充。
正片
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框架和同步器的问题,总的来说可以深入问下去的地方太多了,很考验一个人的知识深度。我也很久没看过这一块了,当年了解的东西忘了个七七八八,所以接下来的这个坑等过段时间抽空复习之后再来填。