第一题:现有的程序代码模拟产生了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(){
@Override
public void run() {
while(true){
try {
String log = bq.take();
parseLog(log);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.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(){
@Override
public 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: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不同,
则并行执行(相互之间不互斥)。原始代码如下:
分析:此题目必须加锁,是肯定的。但是这里有个关键信息是若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 block
e.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了。