线程之三个小面试题 并发集合数据的访问 阻塞队列

来源:互联网 发布:传奇霸业网络不稳定 编辑:程序博客网 时间:2024/06/08 14:59

第一题:现有的程序代码模拟产生了16个日志对象,并且需要运行16秒才能打印完这些日志,请在程序中增加4个线程去调用parseLog()方法来分头打印这16个日志对象,程序只需要运行4秒即可打印完这些日志对象。

    原代码是:

public class Test1 {public static void main(String[] args){        System.out.println("begin:"+(System.currentTimeMillis()/1000));/*模拟处理16行日志,下面的代码产生了16个日志对象,当前代码需要运行16秒才能打印完这些日志。修改程序代码,开四个线程让这16个对象在4秒钟打完。*/for(int i=0;i<16;i++){  //这行代码不能改动final String log = ""+(i+1);//这行代码不能改动{     Test.parseLog(log);}}}//parseLog方法内部的代码不能改动public static void parseLog(String log){System.out.println(log+":"+(System.currentTimeMillis()/1000));try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}

解题:

 

import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.BlockingQueue;/** *   第一题:现有的程序代码模拟产生了16个日志对象,并且需要运行16秒才能打印完这些日志, *   请在程序中增加4个线程去调用parseLog()方法来分头打印这16个日志对象, *   程序只需要运行4秒即可打印完这些日志对象。原始代码如下: *    *   解题思路: *     生产者在循环16次,生产16个日志后。可以让消费者打印的线程,去阻塞式的取值。等put有值了,再立即take. *      * @author chen * */public class Test1 {public static void main(String[] args){        //这里为什么要定义一个阻塞队列呢,因为它有个很重要的特征,就是调用put时,会根据队列长度及已存在个数来判断//是否立即将数据put进去,若队列已经满了则阻塞等在那,那就要靠另外的线程去take。take后队列不满,就立即put进去了。//而且当队列是空empty的时候,take没有数据也是阻塞在那。此时就要等另外的线程去put,队列有元素了,就立即take走。//用阻塞队列BlockingQueue去解这题,很适合了。当然若用普通集合去解题时,//那在消费时,remove时及parseLog时,必须四个线程,每个线程取4次,每1次一秒,但这个方式很危险.因为一量取走的//remove先执行,而add进去的被全部取走了,此时还滑那 remove就是false 会浪费掉一次机会,最终可能会造成部分未取走打印。//当然,若先循环16次add进集合的先执行,就可以避免,但这个是不可取的因为要是扩展 循环的次数是未定的怎么办,//不可能等集合中全部满员了再取走吧,且若普通集合的remove/poll等方法都是不安全的要不就报异常,要不就返回个null(集合无数据时)。final BlockingQueue<String> bq = new ArrayBlockingQueue<String>(1);  System.out.println("begin:"+(System.currentTimeMillis()/1000));for(int i=0;i<4;i++){new Thread(new Runnable(){@Overridepublic void run() {while(true){try {String log  = bq.take();parseLog(log);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}).start();}/*模拟处理16行日志,下面的代码产生了16个日志对象,当前代码需要运行16秒才能打印完这些日志。修改程序代码,开四个线程让这16个对象在4秒钟打完。*/for(int i=0;i<16;i++){  //这行代码不能改动final String log = ""+(i+1);//这行代码不能改动{     // Test.parseLog(log);  //将此原来的代码注释掉,先把产生的日志放入集合中。try {bq.put(log);} catch (InterruptedException e) {e.printStackTrace();}}}}//parseLog方法内部的代码不能改动public static void parseLog(String log){System.out.println(log+":"+(System.currentTimeMillis()/1000));try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}


第二题:现成程序中的Test类中的代码在不断地产生数据,然后交给TestDo.doSome()方法去处理,就好像生产者在不断地产生数据,消费者在不断消费数据。请将程序改造成有10个线程来消费生成者产生的数据,这些消费者都调用TestDo.doSome()方法去进行处理,故每个消费者都需要一秒才能处理完,程序应保证这些消费者线程依次有序地消费数据,只有上一个消费者消费完后,下一个消费者才能消费数据,下一个消费者是谁都可以,但要保证这些消费者线程拿到的数据是有顺序的。
原代码如下:

public class Test {public static void main(String[] args) {System.out.println("begin:"+(System.currentTimeMillis()/1000));for(int i=0;i<10;i++){  //这行不能改动String input = i+"";  //这行不能改动String output = TestDo.doSome(input);System.out.println(Thread.currentThread().getName()+ ":" + output);}}}//不能改动此TestDo类class TestDo {public static String doSome(String input){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}String output = input + ":"+ (System.currentTimeMillis() / 1000);return output;}}

 

解题:

/** *  第二题:现成程序中的Test类中的代码在不断地产生数据,然后交给TestDo.doSome()方法去处理,就好像生产者在不断地产生数据,消费者在不断消费数据。 *  请将程序改造成有10个线程来消费生成者产生的数据,这些消费者都调用TestDo.doSome()方法去进行处理,故每个消费者都需要一秒才能处理完, *  程序应保证这些消费者线程依次有序地消费数据,只有上一个消费者消费完后,下一个消费者才能消费数据,下一个消费者是谁都可以, *  但要保证这些消费者线程拿到的数据是有顺序的。原始代码如下: * @author chen * */public class Test2 {public static void main(String[] args) {final Semaphore sap = new Semaphore(1); //加一个灯,坑相当于一个Lock了,一个线程进去了,另一个线程则进不去。//这里最好是用并发库中的队列,因为普通的队列在操作时。不管是ArrayList的remove还是LinkedList的poll或是其它的什么的//都会存在一个问题,即remove或poll元素不存在时,要不返回一个null,要不就报异常(打印null或异常都是错误的处理)。//这样普通集合达不到,集合中没有元素时就等着,等另外线程put进去。final SynchronousQueue<String> sq = new SynchronousQueue<String>();for(int i=0;i<10;i++){new Thread(new Runnable(){@Overridepublic void run() {try {sap.acquire();  //使用一个坑,代表这个坑当前有线程,其它线程进不去。String input = sq.take();String retVal = TestDo.doSome(input);System.out.println(retVal);} catch (InterruptedException e) {e.printStackTrace();}sap.release(); //释放这个坑,另外线程可以竞争进来了。}}).start();}System.out.println("begin:"+(System.currentTimeMillis()/1000));for(int i=0;i<10;i++){  //这行不能改动String input = i+"";  //这行不能改动try {sq.put(input);  //将产生的元素增加进入这里} catch (InterruptedException e) {e.printStackTrace();}/*String output = TestDo.doSome(input);System.out.println(Thread.currentThread().getName()+ ":" + output);*/}}}//不能改动此TestDo类class TestDo {public static String doSome(String input){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}String output = input + ":"+ (System.currentTimeMillis() / 1000);return output;}}


第三题:现有程序同时启动了4个线程去调用TestDo.doSome(key, value)方法,由于TestDo.doSome(key, value)方法内的代码是先暂停1秒,然后再输出以秒为单位的当前时间值,所以,会打印出4个相同的时间值,如下所示:
  4:4:1258199615
  1:1:1258199615
  3:3:1258199615
  1:2:1258199615
        请修改代码,如果有几个线程调用TestDo.doSome(key, value)方法时,传递进去的key相等(equals比较为true),则这几个线程应互斥排队输出结果,即当有两个线程的key都是"1"时,它们中的一个要比另外其他线程晚1秒输出结果,如下所示:
  4:4:1258199615
  1:1:1258199615
  3:3:1258199615
  1:2:1258199616
   总之,当每个线程中指定的key相等时,这些相等key的线程应每隔一秒依次输出时间值(要用互斥),如果key不同,则并行执行(相互之间不互斥)。原始代码如下:

//不能改动此Test类public class Test extends Thread{private TestDo testDo;private String key;private String value;public Test(String key,String key2,String value){this.testDo = TestDo.getInstance();/*常量"1"和"1"是同一个对象,下面这行代码就是要用"1"+""的方式产生新的对象,以实现内容没有改变,仍然相等(都还为"1"),但对象却不再是同一个的效果*/this.key = key+key2; this.value = value;}public static void main(String[] args) throws InterruptedException{Test a = new Test("1","","1");Test b = new Test("1","","2");Test c = new Test("3","","3");Test d = new Test("4","","4");System.out.println("begin:"+(System.currentTimeMillis()/1000));a.start();b.start();c.start();d.start();}public void run(){testDo.doSome(key, value);}}class TestDo {private TestDo() {}private static TestDo _instance = new TestDo();public static TestDo getInstance() {return _instance;}public void doSome(Object key, String value) {// 以大括号内的是需要局部同步的代码,不能改动!{try {Thread.sleep(1000);System.out.println(key+":"+value + ":"+ (System.currentTimeMillis() / 1000));} catch (InterruptedException e) {e.printStackTrace();}}}}


 

解题如下:

import java.util.ConcurrentModificationException;import java.util.Iterator;import java.util.concurrent.CopyOnWriteArrayList;/** * 第三题:现有程序同时启动了4个线程去调用TestDo.doSome(key, value)方法,由于TestDo.doSome(key, value)方法内的代码是先暂停1秒, * 然后再输出以秒为单位的当前时间值,所以,会打印出4个相同的时间值,如下所示:4:4:12581996151:1:12581996153:3:12581996151:2:1258199615        请修改代码,如果有几个线程调用TestDo.doSome(key, value)方法时,传递进去的key相等(equals比较为true),        则这几个线程应互斥排队输出结果,即当有两个线程的key都是"1"时,它们中的一个要比另外其他线程晚1秒输出结果,如下所示:4:4:12581996151:1:12581996153:3:12581996151:2:1258199616  总之,当每个线程中指定的key相等时,这些相等key的线程应每隔一秒依次输出时间值(要用互斥),如果key不同,  则并行执行(相互之间不互斥)。原始代码如下:    分析:此题目必须加锁,是肯定的。但是这里有个关键信息是若key是不同的,则可以并行的执行打印。若Key是相当的则可以直接执行。  显然,这用可以Synchronized,而且用Synchronized比Lock要好,因为Lock时不知道new出来几个对象,哪些相同的对象使用一个Lock去lock();  而用Synchronized就可以很轻松了,因为可以动态的根据Key不同而加Key不同的锁,这样不同的Key可以并行访问。而相同的Key则会互斥。    而这里唯一要解决的问题是,Key的比较不能使用==同一对象的比较,而是通过equals来比较,想到不管怎么比较,涉及比较肯定要将进来的Key  不管什么情况,先存入到集合中。然后通过此集合去比较,是否已经存在,存在则加相同的锁。不存在直接加Key不相同的锁。 * @author chen * *///不能改动此Test类public class Test3 extends Thread{private TestDo2 testDo;private String key;private String value;public Test3(String key,String key2,String value){this.testDo = TestDo2.getInstance();/*常量"1"和"1"是同一个对象,下面这行代码就是要用"1"+""的方式产生新的对象,以实现内容没有改变,仍然相等(都还为"1"),但对象却不再是同一个的效果*/this.key = key+key2; this.value = value;/** * 这里有个现象就是 key变量的值是1,key2变量值为空,a对象与b对象最终的key相加出来的是不同对象的。 * 因为在编译阶段时 this.key = key+key2,没法优先到运行阶段时,key是通过字符串相加出来的, * 不会直接从缓存池中直接取字符串。 *  * 而下面的代码,两者Strin 是相同的字符串,因为是常量,常量String的相加在编译阶段已经被编译器优化过了。 * 如 : a = "1"+"" ; 编译器看到这语句后,会认为是废话直接编译成a ="1",运行时会在缓存池在找对象。 *  b = "1"+"" ; 编译器看到这语句后,会认为是废话直接编译成a ="1",运行时会在缓存池在找对象。 *  所以a与b,就绝对是相同对象了。这与上面的两个变量相加是有区别的。 */}public static void main(String[] args) throws InterruptedException{Test3 a = new Test3("1","","1");Test3 b = new Test3("1","","2");Test3 c = new Test3("3","","3");Test3 d = new Test3("4","","4"); //new 出来 4个线程System.out.println("begin:"+(System.currentTimeMillis()/1000));a.start();b.start();c.start();d.start();//AbstractList}public void run(){ //每个线程都是去执行doSome,可能会存在并发访问,根据题目要限制相同Key的并发访问,不同Key可以并发。testDo.doSome(key, value);}}class TestDo2 {private TestDo2() {}private static TestDo2 _instance = new TestDo2();public static TestDo2 getInstance() {return _instance;}//这里为什么要使用CopyOnWriteArrayList集合,而不使用ArrayList是有原因的。因为若使用普通的ArrayList共享集合,//而多线程在访问时,可能会存在在迭代的同时,有个线程add了,说会报出并发修改异常ConcurrentModificationException了。private CopyOnWriteArrayList<Object> cowal = new CopyOnWriteArrayList<Object>();public void doSome(Object key, String value) {Object lock = key ; //一开始,默认上Key锁,若并发进来的线程中Key已经有相同的,则使用其中的一个Key/*if(!cowal.contains(key)){ // 注意,这里不能直接用contains来判断后,再add进去。除非下面搞同步锁,再双重判断。try {                   // 否则会有线程安全问题,如这里睡上20毫秒问题就来了。Thread.sleep(20);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}cowal.add(key);  //这里的contain与 add方法并不是线程安全的,因为contain时,不存在线程执行这里//可能去睡了会,走了。所以对它加锁,但似乎有个更安全的方法。可以用addIfAbsent方法//此方法可e不存在,则添加进去,存在了就不添加。是一个动作方法就不存在多条路么不安全了。}else{Iterator<Object> it = cowal.iterator();while(it.hasNext()){Object oldKey = it.next();if(key.equals(oldKey)){lock = oldKey;break;}}}*/// 以大括号内的是需要局部同步的代码,不能改动!if(!cowal.addIfAbsent(key)){ //若没有添加进去代表集合中已存在,addIfAbsent方法将返回false。Iterator<Object> it = cowal.iterator();while(it.hasNext()){Object oldKey = it.next();if(key.equals(oldKey)){lock = oldKey;break;}}}synchronized(lock){try {Thread.sleep(1000);System.out.println(key+":"+value + ":"+ (System.currentTimeMillis() / 1000));} catch (InterruptedException e) {e.printStackTrace();}}}}


 

总结

  1. java.util.concurrent 提供了在并发线程中很常用的实用的工具类呢。有阻塞式的队列ArrayBlockingQueue<E>SynchronousQueue<E>,有相当的坑对象Semaphore,交接对象Exchanger<V>,等到齐对象CyclicBarrier 等等。

 2. 关于同步锁Lock与Synchronized本不区别哪个好,哪个不好。而是看应用场景,比如上面的动态的锁,用Lock则非常的麻烦,而用Synchronized则很easy 了。而当用多个线程应用的场景是:要求一个线程干完活了,指定另一个线程干活,另一线程干完了,再指定另外一个线程干活。此时情况下用Synchronized则非常麻烦了,用Lock,new出多个Condition 分别的await再分别的singal就很easy了。

原创粉丝点击