面试题目:两个线程交替输出字符-线程间通信

最近学习多线程和锁方面的知识,偶然看到马士兵老师对于题目这道面试题的解析,觉得对自己学习多线程很有帮助,所以把其中个人觉得比较优雅和常用的方式代码写下来以备记录。

题目大概是这样的:要求新建两个线程,使得这两个线程依次输出:1A2B3C4D5E6F....,这里给出三种不同的解决方法。


import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.LockSupport;

/**
 * 两个线程之间通讯
 * 交替输出每个线程变量的值
 * eg:1A2B3C4D5E6F...
 * @author admin
 * @DATE 2020/6/20
 */
public class TwoThreadCommunication {
	// 定义两个线程
	static Thread t1,t2;
	static char[] iArray = "1234567".toCharArray();
	static char[] cArray = "ABCDEFG".toCharArray();
	enum MONITOR{MONITOR1,MONITOR2};
	static volatile MONITOR m = MONITOR.MONITOR1; // 这里m变量必须为volatile,不然自旋可能会出问题
	final static Object obj = new Object();

	public static void main(String[] args) {
//		parkAndUnpark();
//		compareAndSet();
		standardComm();
	}
}

第一 种:利用locksupport线程工具类的park和unpark方法,使得线程之间互相阻塞等待和启动唤醒。park方法表示当前线程停止执行,直到其他线程显示调用unpark方法唤醒自己,这个unpark 方法可以指定唤醒具体哪个参数,这是不同于notify的,notify是随机唤醒队列里的某个线程,不能确定具体唤醒哪个。

    /*
		使用LockSupport unpark/park 启动、暂停线程
	 */
	static void parkAndUnpark(){
		t1 = new Thread( () -> {
			for (char i : iArray){
				System.out.print(i);
				LockSupport.unpark(t2);
				LockSupport.park();
			}
		},"t1");
		t2 = new Thread(() -> {
			for( char c : cArray){
				LockSupport.park(); // 可以在线程没有获取到cpu的情况下调用park,不同于notify方法必须当前线程正在运行,此处代码能保证优先输出t1里面的char。
				System.out.print(c);
				LockSupport.unpark(t1);
			}
		},"t2");
		t1.start();
		t2.start();
	}

第二种:利用代码实现cas自旋锁,在获取到cpu之前,线程一直自旋无任何操作等待,直到满足条件,退出自旋执行具体业务代码。cas方式仅停留在jvm层面,也就是在用户态进行自旋,并不会进入内核态,提高了运行速度,但是由于一直在自旋,会占用cpu,所以cas适用于线程数较少并且业务逻辑代码耗时较少的环境。

cas需要二要素:1、自旋(不进行任何操作),2、结束自旋的条件,这个条件可以是各种判断,但必须是线程安全的判断,因为如果线程不安全,则可能会永远跳不出自旋条件。

         /*
		使用自旋锁,不断自旋直到本线程得到cpu调度
	 */
	static void compareAndSet(){
		new Thread(() -> {
			for(char i : iArray){
				while ( m != MONITOR.MONITOR1){}; // while循环空实现,进行自旋
				System.out.print(i);
				m = MONITOR.MONITOR2;
			}
		},"t1").start();
		new Thread(() -> {
			for(char i : cArray){
				while ( m != MONITOR.MONITOR2){};
				System.out.print(i);
				m = MONITOR.MONITOR1;
			}
		},"t2").start();
	}

其中,m为自旋结束条件,可以在定义处看到,m为线程安全的 volatile。

第三种:利用标准线程间 通信的方法:notify、notifyAll、wait等方法。

    /*
		使用标准线程间通讯方法 notify、wait
	 */
	public static void standardComm(){
        CountDownLatch cdl = new CountDownLatch(1); // 利用countdownlatch定义线程间通讯并且保证线程t1先执行
		new Thread( () -> {
            cdl.countDown();
			synchronized (obj){
				for(char c : iArray){
					System.out.print(c);
						try {
							obj.notify();
							obj.wait();
						}catch (Exception e){
							e.printStackTrace();
						}
				}
				obj.notify();
			}
		},"t1").start();
        new Thread( () -> {
            try{

                cdl.await();
            }catch(Exception e){

            }
            synchronized (obj){
                for(char i : cArray){
                    System.out.print(i);
                    try {
                        obj.notify();
                        obj.wait();
                    }catch (Exception e){

                    }
                }
                obj.notify();
            }
        },"t2").start();

	}

notify方法与LockSupport的unpark方法相比,在当前线程持有锁的时候调用,能够唤醒等待队列的随机一个线程,不能保证唤醒指定的线程,并且是重量级锁定义,由于在等待队列之间切换也会比较消耗性能。我们可以看到,在代码中,为了保证t1线程的输出优先于t2的输出,定义了一个CountDownLatch类,这个类的构造器传入一个int值,一旦调用此类实例的countdown方法,这个int值就减1,直到为0,则调用了此实例awake方法的所有线程会同时被唤醒,此类主要用于线程间先后顺序使用,一般不推荐线程的setPriority方法。

由于初学多线程方面的,代码或者文字阐述有任何问题欢迎大神评论指教,不胜感激。

你可能感兴趣的:(多线程和锁)