Educoder–Java高級特性(第六章)- 多線程基礎(3)線程同步【筆記+參考代碼】

Educoder–Java高級特性(第六章)- 多線程基礎(3)線程同步【筆記+參考代碼】


第一關


1.在併發編程中,我們需要以下哪幾個特性來保持多線程程序執行正確( ABD )

A、可見性
B、原子性
C、併發性
D、有序性

2.請分析以下語句哪些是原子操作( AB )
A、int a = 3;
B、boolean flag = false;
C、a–;
D、a =a *a

3.以下代碼的執行結果是( E )

public class Test {
    public  int inc = 0;
    public void increase() {
        inc++;
    }
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        while(Thread.activeCount()>1)  //保證前面的線程都執行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

A、10000
B、9870
C、大於10000
D、小於10000
E、不一定,大概率小於一萬



第二關


編程要求
請仔細閱讀右側代碼,根據方法內的提示,在Begin - End區域內進行代碼補充,具體任務如下:

使num變量在同一時刻只能有一個線程可以訪問。

測試說明
使程序的輸出結果如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18



參考代碼

package step2;

public class Task {
	public static void main(String[] args) {	
		final insertData insert = new insertData();
		
		for (int i = 0; i < 3; i++) {
			new Thread(new Runnable() {
				public void run() {
					insert.insert(Thread.currentThread());
				}
			}).start();
		}		
	}
}
class insertData{
	public static int num =0;
	/********* Begin *********/
	public synchronized void insert(Thread thread){
		for (int i = 0; i <= 5; i++) {
			num++;
			System.out.println(num);
		}
	}
	/********* End *********/
}



第三關


編程要求
請仔細閱讀右側代碼,根據方法內的提示,在Begin - End區域內進行代碼補充。

測試說明
使得程序輸出如下結果(因爲線程的執行順序是隨機的可能需要你評測多次):

Thread-0得到了鎖

1

2

3

4

5

Thread-0釋放了鎖

Thread-1得到了鎖

6

7

8

9

10

Thread-1釋放了鎖

Thread-2得到了鎖

11

12

13

14

15

Thread-2釋放了鎖



參考代碼

package step3;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Task {
	public static void main(String[] args) {
		final Insert insert = new Insert();
		Thread t1 = new Thread(new Runnable() {
			public void run() {
				insert.insert(Thread.currentThread());
			}
		});
		Thread t2 = new Thread(new Runnable() {
			public void run() {
				insert.insert(Thread.currentThread());
			}
		});
		Thread t3 = new Thread(new Runnable() {
			public void run() {
				insert.insert(Thread.currentThread());
			}
		});
		// 設置線程優先級
		t1.setPriority(Thread.MAX_PRIORITY);
		t2.setPriority(Thread.NORM_PRIORITY);
		t3.setPriority(Thread.MIN_PRIORITY);

		t1.start();
		t2.start();
		t3.start();

	}
}
class Insert {
	public static int num;
	// 在這裏定義Lock
	private Lock lock = new ReentrantLock(); 
	public void insert(Thread thread) {
		/********* Begin *********/
		if(lock.tryLock()){
			try{
				System.out.println(thread.getName()+"得到了鎖");
				for (int i = 0; i < 5; i++) {
					num++;
					System.out.println(num);
				}
			}finally{
				System.out.println(thread.getName()+"釋放了鎖");
				lock.unlock();
			}
		}else{
			System.out.println(thread.getName()+"獲取鎖失敗");
		}
	}
		/********* End *********/
}



第四關


編程要求
請仔細閱讀右側代碼,根據方法內的提示,在Begin - End區域內進行代碼補充。


測試說明
預期輸出:10000。

提示:可以使用兩種方式實現原子性,所以本關有多種方式都可以通關。

解題方法有兩個:
方法1: 使用synchronized加鎖,使i的數據同步,但是這個方式簡單是簡單,比較耗資源,在多線程競爭資源的時候,加鎖、釋放鎖,線程之間不停的切換會引起性能問題。一個線程持有鎖,其它線程需要等到的這把鎖之後才能執行,期間線程是被掛起,容易導致死鎖發生。

方法2: 使用原子變量AtomicInteger定義變量 i ,在i++原子性問題中,方案2對比起方案1更加優。


參考代碼

方法1:

package step4;

public class Task {
	public volatile int inc = 0;
	//請在此添加實現代碼
	/********** Begin **********/
	public synchronized void increase() {
		inc++;
	}
	/********** End **********/
	public static void main(String[] args) {
		final Task test = new Task();
		for (int i = 0; i < 10; i++) {
			new Thread() {
				public void run() {
					for (int j = 0; j < 1000; j++)
						test.increase();
				};
			}.start();
		}
		while (Thread.activeCount() > 1) // 保證前面的線程都執行完
			Thread.yield();
		System.out.println(test.inc);
	}
}

方法2:

package step4;
import java.util.concurrent.atomic.AtomicInteger;
public class Task {
	public static AtomicInteger inc = new AtomicInteger(0);
	//請在此添加實現代碼
	/********** Begin **********/
	public static void increase() {
		inc.incrementAndGet();
	}
	/********** End **********/
	public static void main(String[] args) {
		final Task test = new Task();
		for (int i = 0; i < 10; i++) {
			new Thread() {
				public void run() {
					for (int j = 0; j < 1000; j++)
						increase();
				};
			}.start();
		}
		while (Thread.activeCount() > 1) // 保證前面的線程都執行完
			Thread.yield();
		System.out.println(test.inc);
	}
}



【筆記】

在線程中比較重要的幾個概念:

1.原子性
原子性是指一個操作是不可中斷的,要麼全部執行成功要麼全部執行失敗。
在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程所幹擾。

	x = 10;        //語句1
	y = x;         //語句2
	x++;           //語句3
	x = x + 1;     //語句4

只有語句1是原子性操作。

語句1是直接將10的值賦值給x,所以也就是說執行這個語句會直接將數值10寫入到內存中,所以這是原子性的,語句2其實是兩個操作,先讀取x的值,然後賦值給y,這兩個步驟是原子性的,但是他們合起來就不是原子性操作了,後面兩個語句也是同樣的道理。

也就是說,只有簡單的讀取,賦值(必須是將數字賦值給某個變量,變量之間的賦值不是原子性操作),纔是原子性操作。

如果要實現大範圍的原子性,可以通過synchronized和lock來實現,synchronized和lock可以保證在任何時候只有一個線程執行該代碼塊,所以就保證了原子性。

2.可見性
可見性是當多個線程訪問一個變量時,一個線程改變了變量的值,其他線程立馬可以知道這個改變。

對於可見性,Java提供了Volatile關鍵字來保證,當一個變量被Volatile修飾時,它會保證修改的值會被立即重新寫入到主內存,當其他線程要調用該共享變量時,會去主內存中重新讀取。

但是普通的共享變量是不能保證可見性的,因爲普通變量會被讀入到線程自己的內存,當一個線程修改了之後,可能還沒來得及刷新到主內存,其他線程就從主存中讀取了該變量。所以其他線程讀取的時候可能還是原來的值,所以普通共享變量是無法保證可見性的。

關於保證可見性還可以通過Synchronized和lock的方式來實現。

3.有序性

有序性:即程序的執行是按照代碼編寫的先後順序執行的

 int a;              
 boolean flag;
 i = 10;                //語句1  
 flag = false;          //語句2

上述代碼定義了一個整形的變量a,布爾類型變量flag,使用語句1和語句2對變量i和flag進行賦值,看起來語句1是在語句2之前的,但是在程序運行的時候語句1一定會在語句2之前執行嗎?是不一定的,因爲這裏可能會發生指令重排序(Instruction Reorder)。

什麼是指令重排序呢?

一般來說,處理器爲了提升執行效率,會對輸入代碼進行優化,它不保證代碼執行的順序和代碼編寫的順序一致,但是它會保證程序的輸出結果和代碼的順序執行結果是一致的。

比如上面的代碼中,語句1和語句2誰先執行對最終的程序結果並沒有影響,那麼就有可能在執行過程中,語句2先執行而語句1後執行。

指令重排序不會影響單個線程的執行,但是會影響多線程的執行。

也就是說,要保證多線程程序執行的正確性,必須要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會導致程序運行不正確。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章