前言
上篇文章講解了多線程的運行狀態。本篇文章就來講講線程之間的共享。
一、爲什麼要線程共享
因爲線程都是獨立的,相互之間是不可見的,所以當兩個線程對一個數據進行操作時,就很容易出現問題。
/**
* @version 1.0
* @Description 不同步線程demo
* @Author wb.yang
*/
public class NoSyncDemo {
static Integer count = 0;
public static class CountThread extends Thread {
@Override
public void run() {
count++;
}
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new CountThread().start();
}
System.out.println(count);
}
}
從代碼中看出,我們循環了1000次相加count關鍵字,結果應該是1000,我們看下結果
可見,結果才輸出了994,明顯跟預期的結果不同。導致這個問題的就是線程的共享,因爲線程之間是不不見的,可能線程A拿了count = 100,這時候還沒有進行count++的操作,這時候線程B搶到了cpu,也取出了count=100這個時候,就已經數據不同步了。造成了線程安全問題。
針對線程安全問題,我們就要進行線程之間的數據共享。
二、線程共享方式
1.volatile
volatile關鍵字,最輕量的同步機制。使用volatile關鍵字,可以使變量成爲可見性。保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。volatile 只能保證對單次讀/寫的原子性。i++ 這種操作不能保證原子性。
/**
* @version 1.0
* @Description volatile線程demo
* @Author wb.yang
*/
public class VolatileSyncDemo {
private volatile static boolean ready;
private static int number;
private static class CountThread extends Thread {
@Override
public void run() {
System.out.println("計數線程啓動.......");
//無限循環
while (!ready) ;
System.out.println("number = " + number);
}
}
public static void main(String[] args) {
new CountThread().start();
SleepTools.second(1);
number = 51;
ready = true;
SleepTools.second(5);
System.out.println("main is ended!");
}
}
從代碼中可以看出,我們先啓動了線程,之間打印了number,然後在主線程中賦值number=51,如果按照線程之間數據不可見的話,那麼應該打印null。我們看下結果把。
我們可以看出,輸出的賦值後的數據,這事爲什麼呢?這個就涉及到了volatile關鍵字,通過volatile關鍵字,使變量在線程之間可見,所以可以知道number的變化。
2.synchronized
synchronized內置鎖,有以下幾種鎖法
- 普通同步方法,鎖是當前對象實例
- 靜態同步方法,鎖是當前Class對象
- 同步代碼塊,鎖是括號中的參數對象
當一個線程訪問同步代碼塊時,必須先獲得鎖才能執行同步代碼塊中的代碼,當退出或者拋出異常時,必須要釋放鎖。
/**
* @version 1.0
* @Description TODO
* @Date 2020/3/25 17:41
*/
public class SynchronizedSyncDemo {
static Integer count = 0;
private static Object object = new Object();
public static class CountThread extends Thread {
@Override
public void run() {
synchronized (object){
count++;
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new CountThread().start();
}
SleepTools.second(2);
System.out.println(count);
}
}
從代碼可以看出,我們在run方法中添加了synchronized,並且鎖了方法,我們看下結果
我們看到結果是正常的。之所以正常,是因爲用了synchronized,來將count++給加鎖,進行鎖住了,當一個線程加鎖鎖住這些代碼的時候,其他的線程過來必須等待上一個線程執行完代碼釋放鎖,否則不會持有這把鎖,只能在這裏等待,保證了線程的共享。
3.ThreadLocal
ThreadLocal是在每個線程都使用一個副本,線程之間隔離,操作的是副本中變量的值,只在這個線程中生效,其他線程中的這個變量,又是新的本地副本,所以不會變化
/**
* @version 1.0
* @Description ThreadLocalDemo
* @Author wb.yang
*/
public class ThreadLocalSyncDemo {
static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
public static class CountThread extends Thread {
int id;
public CountThread(int id) {
this.id = id;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":start");
Integer s = threadLocal.get();
s = s + id;
threadLocal.set(s);
System.out.println(Thread.currentThread().getName() + ":"
+ threadLocal.get());
}
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new CountThread(i).start();
}
}
}
這個是一個ThreadLocal變量,默認值是1,然後啓動了三個線程,進行操作。
從結果中看出,每個線程都是單獨的處理,縣城之間並沒有干擾,這個就是ThreadLocal的作用,來保證線程的共享。
總結
線程之間難免要線程共享來保證線程安全,所以就產生了以上幾種方法來保證線程同步安全。