Java高併發程式設計:使用JDK5中同步技術的3個面試題
第一題:
現有的程式程式碼模擬產生了16個日誌物件,並且需要執行16秒才能列印完這些日誌,請在程式中增加4個執行緒去呼叫parseLog()方法來分頭列印這16個日誌物件,程式只需要執行4秒即可列印完這些日誌物件。
public class Test {
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;
//BlockingQueue
public class Test {
public static void main(String[] args){
//建立一個空間大小為16的阻塞佇列,空間大小可以任意,因為每次列印都要1秒,在此期間,
//4個執行緒足以不斷去從佇列中取資料,然後列印,即在1秒內列印4條日誌資訊
final BlockingQueue<String> queue = new ArrayBlockingQueue<String>(16);
//開啟4個執行緒列印
for(int i=0;i<4;i++){
new Thread(new Runnable(){
@Override
public void run() {
while(true){
try {
String log = queue.take(); //開始沒有資料,阻塞,一旦有其中一個執行緒就去取
//資料,即不再阻塞,就開始列印
parseLog(log);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
//列印秒數
System.out.println("begin:"+(System.currentTimeMillis()/1000));
for(int i=0;i<16;i++){ //這行程式碼不能改動
final String log = ""+(i+1);//這行程式碼不能改動
{
try {
queue.put(log); //向佇列中儲存資料
} catch (InterruptedException e) {
e.printStackTrace();
}
//Test.parseLog(log);
}
}
}
//parseLog方法內部的程式碼不能改動
public static void parseLog(String log){
System.out.println(log+":"+(System.currentTimeMillis()/1000));
try {
Thread.sleep(1000); //模擬每條日誌列印需要1秒
} 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;
}
}
在實現之前先介紹一個阻塞佇列:SynchronousQuene,一種阻塞佇列,其中每個插入操作必須等待另一個執行緒的對應移除操作 ,反之亦然。同步佇列沒有任何內部容量,甚至連一個佇列的容量都沒有。除非另一個執行緒試圖移除某個元素,否則也不能(使用任何方法)插入元素;也不能迭代佇列,因為其中沒有元素可用於迭代。
應用:它非常適合於傳遞性設計,在這種設計中,在一個執行緒中執行的物件要將某些資訊、事件或任務傳遞給在另一個執行緒中執行的物件,它就必須與該物件同步。
import java.util.concurrent.SynchronousQueue;
/*Semaphore與SynchronousQueue的混合使用。
由於Semaphore只有1個許可權,所以誰先拿到誰執行,然後釋放,保證依次執行,
用鎖也行,只要保證一個執行緒執行即可
SynchronousQueue是必須有其他執行緒取的動作,這樣一一對應
*/
public class Test {
public static void main(String[] args) {
//定義一個許可權為1的訊號燈
final Semaphore semaphore = new Semaphore(1);
//產生的結果無序
final SynchronousQueue<String> queue = new SynchronousQueue<String>();
//產生10個執行緒
for(int i=0;i<10;i++){
new Thread(new Runnable(){
@Override
public void run() {
try {
semaphore.acquire(); //獲取許可
String input = queue.take(); //獲取並移除此佇列的頭
String output = TestDo.doSome(input);
System.out.println(Thread.currentThread().getName()+ ":" + output);
semaphore.release(); //釋放許可
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
}
System.out.println("begin:"+(System.currentTimeMillis()/1000));
for(int i=0;i<10;i++){ //這行不能改動
String input = i+""; //這行不能改動
try {
queue.put(input); //將指定元素新增到此佇列
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
//不能改動此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();
}
}
}
}
對於原始碼中關於實現值相同而物件不同的效果進行解釋:
對於:
a = “1”+”“;
b = “1”+””
編譯器自動優化,所以a和b是同一個物件
而對於:key = key+key2; 由於是變數,編譯器無法識別,這時a和b把“1”和“”賦值給key和key2時會得到兩個不同的物件
思想:將集合中的物件作為同步程式碼塊的鎖,即this鎖,每次將物件存入集合中的時候,就判斷是否原集合中已經存在一個與將要存入集合的物件值相同的物件,即用equals比較,如果有,那麼就獲取原來的這個物件,把這個物件作為將要存入物件的鎖,這樣它們持有的就是同一把鎖,即可實現互斥,這樣就可以實現值相同的物件在不同的時刻列印的效果
程式碼中出現的問題:在遍歷ArrayList集合查詢與要存入值相同元素的時候,進行了新增的動作,所以會出現併發修改異常,因此使用併發的CopyOnWriteArrayList
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
//不能改動此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; //這裡是變數,所以不會優化
/* a = "1"+"";
b = "1"+""
編譯器自動優化,所以a和b是同一個物件
*/
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;
}
//private ArrayList keys = new ArrayList();
//迭代的時候不能修改資料,所以使用同步的ArrayList
private CopyOnWriteArrayList keys = new CopyOnWriteArrayList();
public void doSome(Object key, String value) {
Object o = key;
if(!keys.contains(o)){ //比較是否已經存入了一個相同值的物件
keys.add(o);
}else{
//迭代,找出原集合裡和傳進來的值相同的物件
for(Iterator iter=keys.iterator();iter.hasNext();){
try {
Thread.sleep(20); //迭代的時候休息一會,在ArrayList下演示併發修改異常
} catch (InterruptedException e) {
e.printStackTrace();
}
Object oo = iter.next();
if(oo.equals(o)){ //如果兩個物件的值相同
o = oo; //就讓原集合中的那個相等值的物件作為鎖物件,由於原物件之前做的就是鎖
//這樣兩個鎖就相同了,就可以實現互斥
break;
}
}
}
synchronized(o)
// 以大括號內的是需要區域性同步的程式碼,不能改動!
{
try {
Thread.sleep(1000);
System.out.println(key+":"+value + ":"
+ (System.currentTimeMillis() / 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}