前言

  • 本篇文章主要介绍Java基础多线程之间通信的一些玩法,并结合一些面试题来实践的

1.基于synchronized和java类锁的wait()和notify()

这是一道面试题,让字母和数字交叉打印(第一种)

/**
 * @author wanghp
 * @version 1.0
 * @date 2020/6/25 23:03
 * Thread.sleep与Object.wait区别
 * Thread.sleep需要指定休眠时间,时间一到可继续运行;和锁机制无关,没有加锁也不用释放锁
 * Object.wait需要在synchronized中调用,否则报IllegalMonitorStateException错误。wait方法会释放锁,需要调用相同锁对象Object.notify来唤醒线程
 */

public class JavaNotifyWait {
    static char[] charsNumber = "1234567".toCharArray();
    static char[] charsC = "ABCDEFG".toCharArray();
    static Thread t1 = null, t2 = null;

    static Object obj = new JavaNotifyWait();


    public static void charsNumber() {
        try {
            synchronized (obj) {
                for (char c : charsNumber) {
                    obj.notify();
                    System.out.println("A----------notify");
                    System.out.println(c);
                    obj.wait();
                    System.out.println("A----------wait");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        }
    }

    public static void charsC() {
        try {
            synchronized (obj) {
                for (char c : charsC) {
                    obj.notify();
                    System.out.println("B----------notify");
                    System.out.println(c);
                    obj.wait();
                    System.out.println("B----------wait");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        }
    }

    public static void main(String[] args) {
        t1 = new Thread(() -> charsNumber(), "t1");
        t2 = new Thread(() -> charsC(), "t2");
        t1.start();
        t2.start();
    }
}

2.基于LockSupport的park()和unpark()

面试题:让字母和数字交叉打印(第二种)

/**
 * @author wanghp
 * @version 1.0
 * @date 2020/6/25 23:03
 */

public class LockSupportDemo {
    static char[] charsNumber = "1234567".toCharArray();
    static char[] charsC = "ABCDEFG".toCharArray();
    static Thread t1 = null, t2 = null;

    //t1 exec method
    public static void charsNumber() {
        for (char c : charsNumber) {
            System.out.println(c);
            //唤醒当前打印字母的线程t2
            LockSupport.unpark(t2);
            //阻塞当前打印数字的线程t1
            LockSupport.park(t1);
        }
    }

    //t2 exec method
    public static void charsC() {
        for (char c : charsC) {
            //阻塞当前打印字母的线程t2
            LockSupport.park(t2);
            System.out.println(c);
            //唤醒打印数字的线程t1
            LockSupport.unpark(t1);
        }
    }

    public static void main(String[] args) {
        //打印数字的线程
        t1 = new Thread(() -> charsNumber(), "t1");
        //打印字母的线程
        t2 = new Thread(() -> charsC(), "t2");
        t1.start();
        t2.start();
    }
}

3.基于Lock和Condition的await()和signal()

面试题:手写一个阻塞式队列

/**
 * @author wanghp
 * @version 1.0
 * @date 2020/7/28 10:23
 * 结论:就是用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,
 * 直接继续运行if代码块之后的代码,而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,
 * 如果不成立再执行while代码块之后的代码块,成立的话继续wait。
 */

public class BlockedList {
    //传入true表示使用公平锁,false为非公平锁,默认为非公平锁
    static Lock lock = new ReentrantLock(false);
    //设置一个添加一个移除
    static Condition add = lock.newCondition();
    static Condition remove = lock.newCondition();

    //定义一个集合,存储数据
    static ArrayList<Integer> list = new ArrayList<>();

    public static void add(int val) throws InterruptedException {
        try {
            lock.lock();
            //  System.out.println("进入add方法,当前线程为:" + Thread.currentThread().getName());
            //为什么用while请看上面注释,当调用await方法,当前线程释放了锁,也就是说进来的线程都将阻塞在这里,
            //这块他就会进入到阻塞队列,一旦被唤醒,线程继续执行,这里使用非公平锁,可以从打印信息看出线程不存在先后顺序之分
            while (list.size() >= 2) {
                System.out.println("等待添加..." + Thread.currentThread().getName());
                add.await();
                System.out.println("开始添加" + Thread.currentThread().getName() + "  size:" + list.size());
            }
            list.add(val);
            System.out.println("add data:" + val);
        } finally {
            //节点会从condition队列移动到AQS等待队列,则进入正常锁的获取流程
            remove.signal();
            lock.unlock();
        }
    }

    public static void remove() throws InterruptedException {
        try {
            lock.lock();
            if (list.size() == 2) {
                //每2个满了拿前一个
                System.out.println("remove data:" + list.get(0));
                list.remove(0);
                System.out.println("移除完毕");
                remove.await();
                System.out.println("等待移除...");
            }
        } finally {
            //节点会从condition队列移动到AQS等待队列,则进入正常锁的获取流程
            add.signal();
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    add(finalI);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "t" + finalI).start();
        }
        //本地测试,避免电脑hold不住
        for (int i = 1; i <= 1000; i++) {
            //进行查询移除
            new Thread(() -> {
                try {
                    remove();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }

    }
}

4.多线程之间join()玩法

面试题:让线程顺序执行

/**
 * @author wanghp
 * @version 1.0
 * @date 2020/6/18 22:41
 */

public class JoinDemo {

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            System.out.println("t1");
        });
        Thread t2 = new Thread(() -> {
            System.out.println("t2");
        });
        Thread t3 = new Thread(() -> {
            System.out.println("t3");
        });
        t1.start();
        t1.join();  //他会阻塞主线程,什么时候释放先不管
        //等待释放,具体什么时候释放看join()方法
        //获取到t1线程的执行结果,应用场景,我们假如想要看到t1线程的执行结果就可以这样使用,或者建立一个happens-before规则,
        //TODO  或者让主线程可见,用join
        t2.start();
        t2.join();  //建立一个 happens-before原则
        t3.start();
    }
}

最后总结一下,一般Lock是和Condition的await()和signal()搭配,synchronized和类锁的wait()和notify()使用的。
Q:具体为什么?
1.每一个对象都有一个与之对应的监视器
2.每一个监视器里面都有一个该对象的锁和一个等待队列和一个同步队列

你可以简单理解为synchronized锁对象,进来的线程获得了该对象锁,同时拥有了该对象锁的监视器,有了监视器就可以调用wait()、notify()以及notifyAll()方法,然后其他线程竞争的锁须和当前线程的对象锁是一个,竞争失败的线程会放到阻塞队列,这时候当前线程释放锁之后,调用唤醒方法会将处于阻塞队列的线程放到同步队列,这里注意notify()和notifyAll(),前者只取一个,后者为全部,具体先后看CPU抢占率的。提到这个先后,Lock其实有公平锁和非公平锁,他们其实很像,主要一点就是公平锁是FIFO这样的,而非公平锁可能根据放入的先后没关系,还得根据竞争cpu来进行抢占锁的。