美文网首页java学习之路
java高并发编程(三)简单并发面试题

java高并发编程(三)简单并发面试题

作者: 唯有努力不欺人丶 | 来源:发表于2020-02-17 22:44 被阅读0次

(ps:这道题是在马士兵老师的视频上看到的,觉得还不错,所以就单独写出来,据说是某宝曾经的面试题。)

实现一个容器,提供两个方法:add,size
写两个线程,线程1添加10个元素到容器中。线程2实现监控元素的个数。当数到5个时,线程2给出提示并结束。

是不是看这个题目觉得眼熟,好的吧,之前说volatile的时候用过这个demo,只不过这里打算单独好好讲讲。volatile只不过是一个做法,而且是不怎么好的一个做法。下面我一种一种实现方式说。
先列出基本代码:

package thread;

import java.util.ArrayList;
import java.util.List;

public class Test {
    List<Integer> list = new ArrayList<Integer>();
    
    public void add(Integer i) {
        list.add(i);
    }
    
    public int size() {
        return list.size();
    }
    
    public static void main(String[] args) {
        Test test = new Test();
        new Thread(()->{
            System.out.println("t1 开始");
            for(int i = 0;i<10;i++) {
                test.add(i);
                System.out.println("add +"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            System.out.println("t1 结束");
        }).start();
        new Thread(()->{
            while(true) {
                if(test.size() == 5) {
                    break;
                }               
            }
            System.out.println("t2 结束");
        }).start();
    }
}

上面列出的是最基本的代码,没有任何同步措施,所以是不对的,然后为什么不对说过了,咱们说怎么改对:

volatile解决

package thread;

import java.util.ArrayList;
import java.util.List;

public class Test {
    volatile List<Integer> list = new ArrayList<Integer>();
    
    public void add(Integer i) {
        list.add(i);
    }
    
    public int size() {
        return list.size();
    }
    
    public static void main(String[] args) {
        Test test = new Test();
        new Thread(()->{
            System.out.println("t1 开始");
            for(int i = 0;i<10;i++) {
                test.add(i);
                System.out.println("add +"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            System.out.println("t1 结束");
        }).start();
        new Thread(()->{
            while(true) {
                if(test.size() == 5) {
                    break;
                }               
            }
            System.out.println("t2 结束");
    
    }).start();
    }
}

如上代码,只不过list上添加了一个volatile就使得t2能够运行进if中,线程得已结束。


volatile运行结果

但是这有一个大问题,我这里之所以运行正常是因为是这每添加都睡了一秒。实际上不睡的话就会有问题,我直接上截图:


volatile不睡的运行结果
看出来了吧,这个问题就是因为时间问题,可能都加到10个元素了,t2这句话才打印出来。也就是说准确性不行。你就说图中这个情况,算是完成了这个需求还是每完成?标准意义上是每完成吧,但是volatile也就能保证这样啊,还想更精确就得换个思路。

wait/notify方法解决

wait方法是Object类的方法,其实就是让当前线程进入到等待阶段,同时释放锁和cpu。(这也是小知识点,sleep只会睡不会释放锁和cpu。不太了解的建议去补习这方面的基础)
notify是唤醒其中一条等待的线程,notifyAll是唤醒所有等待的线程。咱们这里用notify就行了。
然后其实这个复杂的地方是当size添加到五个,线程1就wait,然后线程2执行,执行完唤醒线程1是最直接的最简单的办法:

package thread;

import java.util.ArrayList;
import java.util.List;

public class Test {
    List<Integer> list = new ArrayList<Integer>();
    
    public void add(Integer i) {
        list.add(i);
    }
    
    public int size() {
        return list.size();
    }
    
    public static void main(String[] args) {
        Test test = new Test();
        final Object lock = new Object();
        new Thread(()->{
            System.out.println("t1 开始");
            synchronized (lock) {
                for(int i = 0;i<10;i++) {
                    test.add(i);
                    System.out.println("add +"+i);
                    if(i==4) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
//                  try {
//                      Thread.sleep(1000);
//                  } catch (InterruptedException e) {
//                      // TODO Auto-generated catch block
//                      e.printStackTrace();
//                  }
                }
            }
            System.out.println("t1 结束");
        }).start();
        new Thread(()->{
            synchronized (lock) {
                while(true) {
                    if(test.size() == 5) {
                        break;
                    }               
                }
                System.out.println("t2 结束");
                lock.notify();
            }
        }).start();
    }
}

但是其实这个代码也有两点小问题:

  1. 这个代码必须保证线程1先进入锁。如果线程2先拿到锁就会死锁了。
  2. 这个代码中while(true)是个不合理的消耗。

然后改进的办法其实说简单也简单,就是两边再加个wait/notify。如果先进入线程2,size还不是5则wait,让锁给线程1,而线程1在到了5个元素的时候唤醒线程2并且自身wait(因为notify不释放锁,只是唤醒线程2是不能让线程2正常启动的。),剩下的和上面的代码一样。我去改动下:

package thread;

import java.util.ArrayList;
import java.util.List;

public class Test {
    List<Integer> list = new ArrayList<Integer>();

    public void add(Integer i) {
        list.add(i);
    }

    public int size() {
        return list.size();
    }

    public static void main(String[] args) {
        Test test = new Test();
        final Object lock = new Object();
        new Thread(() -> {
            synchronized (lock) {
                if (test.size() != 5) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
                System.out.println("t2 结束");
                lock.notify();

            }
        }).start();
        new Thread(() -> {
            System.out.println("t1 开始");
            synchronized (lock) {
                for (int i = 0; i < 10; i++) {
                    test.add(i);
                    System.out.println("add +" + i);
                    if (i == 4) {
                        try {
                            lock.notify();
                            lock.wait();
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }
            }
            System.out.println("t1 结束");
        }).start();

    }
}

CountDownLatch方法解决

其实上面的wait/notify方法实现是能实现了,问题是性能啊。各种唤醒等待,还有互斥锁来回来去互斥。是一个解决办法, 但是还有更好了,这里就用到了出入锁了!
这个锁就是java中的一个类CountDownLatch。这个类通俗讲就是一个计数器。可以选择计数器减值。和等待,等到计数器值为0才能进入。我记得以前专门写过这个类的一些原理和方法,有兴趣的可以自己去搜索试试。我这里就是简单的说两句。然后我字节贴代码:

package thread;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

public class Test {
    List<Integer> list = new ArrayList<Integer>();

    public void add(Integer i) {
        list.add(i);
    }

    public int size() {
        return list.size();
    }

    public static void main(String[] args) {
        Test test = new Test();
        CountDownLatch countDownLatch = new CountDownLatch(1);
        new Thread(() -> {
            if (test.size() != 5) {
                try {
                    // 开始算这个计数器
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            System.out.println("元素个数到5,t2 结束");
        }).start();
        new Thread(() -> {
            System.out.println("t1 开始");
                for (int i = 0; i < 10; i++) {
                    test.add(i);
                    System.out.println("add +" + i);
                    if (i == 4) {
                        countDownLatch.countDown();
                    }
                }
        }).start();
    }
}

然后这个题从性能的角度来说是最好的解法,然后目前为止这个题我也就只有这三种解法。
今天的笔记就记到这里了,如果稍微帮到你了记得点个喜欢点个关注,另外这块内容大部分出自于马士兵老师的讲解,在此表示感谢。也祝大家工作顺顺利利,生活健健康康~!

相关文章

网友评论

    本文标题:java高并发编程(三)简单并发面试题

    本文链接:https://www.haomeiwen.com/subject/gbmifhtx.html