JVM有主內存(Main Memory)和工作內存(Working Memory),主內存就是平時所說的java堆內存,存放程序中所有的類實例、靜態數據等變量,是線程共享的,而工作內存中存放的是從主內存中拷貝過來的變量以及訪問方法所取得的局部變量,是每個線程獨立所有的,其他線程不能訪問。
每個線程都有自己的執行空間(即工作內存),線程執行的時候用到某變量,首先要將變量從主內存拷貝的自己的工作內存空間,然後對變量進行操作:讀取,修改,賦值等,這些均在工作內存完成,操作完成後再將變量寫回主內存;
各個線程都從主內存中獲取數據,線程之間數據是不可見的;打個比方:主內存變量A原始值爲1,線程1從主內存取出變量A,修改A的值爲2,在線程1未將變量A寫回主內存的時候,線程2拿到變量A的值仍然爲1;
這便引出“可見性”的概念:當一個共享變量在多個線程的工作內存中都有副本時,如果一個線程修改了這個共享變量的副本值,那麼其他線程應該能夠看到這個被修改後的值,這就是多線程的可見性問題。
普通變量情況:如線程A修改了一個普通變量的值,然後向主內存進行寫回,另外一條線程B在線程A回寫完成了之後再從主內存進行讀取操作,新變量的值纔會對線程B可見;
如下圖:
Java內存模型定義了8種操作來完成關於主內存和工作內存之間具體的交互,這些操作都是原子的,不可分割(long double類型除外)。這8種操作如下所示:
1) lock(鎖定) 作用於主內存的變量,它把一個變量標誌爲一條線程獨佔的狀態
2) unlock(解鎖) 作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其它線程鎖定
3) read(讀取) 作用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用
4) load(載入) 作用於工作內存的變量,它把read操作從主內存得到的變量值放入工作內存的變量副本中
5) use(使用) 作用於工作內存的變量,它把變量副本的值傳遞給執行引擎,每當虛擬機遇到一個需要使用的變量的值的字節碼指令時,將會執行這個操作。
6) assign(賦值) 作用於工作內存的變量,它把一個從執行引擎接收到的值賦值給工作副本變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作
7) store(存儲) 作用於工作內存的變量,將工作副本變量的值傳輸給主內存,以便隨後的write操作使用
8) write(寫入) 作用於主內存的變量, 它把store操作從工作內存得到的變量的值放入主內存的變量
如果要把一個變量從主內存複製到工作內存,那就要按順序地執行read和load操作,如果要把變量從工作內存同步回主內存,那就要順序地執行store和write操作。注意,Java內存模型只要求上述兩個操作必須按順序地執行,而沒有保證必須是連續執行,也就是說read和load之間,store和write之間是可以插入其它指令的,如對內存中的變量a,b進行訪問時,一種可能出現的順序是read a, read b, load b, load a。
當有多條線程同時訪問共享數據時,如果不進行同步,就會發生錯誤,java提供了多種機制保證線程同步,這裏主要說下synchronized和Lock;
一.synchronized關鍵字:
最簡單的方式是加入synchronized關鍵字,只要將操作共享數據的語句加入synchronized關鍵字,在某一時段
只會讓一個線程執行完,在執行過程中,其他線程不能進來執行:
import static java.lang.System.out;
public class Counting {
public static void main(String[] args)
throws InterruptedException {
class Count{
private int count=0;
public <span style="color:#ff6666;">synchronized</span> void increment(){count++;}
public int getCount(){return count;}
}
final Count counter=new Count();
class CountingThread extends Thread {
public void run(){
for (int i=0; i<1000;i++ ) {
counter.increment();
}
}
}
CountingThread t1=new CountingThread();
CountingThread t2=new CountingThread();
t1.start();
t2.start();
t1.join();
t2.join();
out.println(counter.getCount());
}
}
方法聲明中同步(synchronized )關鍵字。當它用來修飾一個方法或者一個代碼塊的時候,能夠保證在同一時刻最多隻有一個線程執行該段代碼。遵循以下五條原則:
一、當兩個併發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以後才能執行該代碼塊。
二、然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。
三、尤其關鍵的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。
四、第三個例子同樣適用其它同步代碼塊。也就是說,當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就獲得了這個object的對象鎖。結果,其它線程對該object對象所有同步代碼部分的訪問都被暫時阻塞。
五、以上規則對其它對象鎖同樣適用。
還有,synchronized 鎖機制存在重入的特性,就是可以重複獲取同一個對象的鎖。如下:
public synchronized void methodA(int a, int b);
public synchronized void methodB(int a){
methodA(a, 0);
}
B方法可以執行,就是說B方法獲得鎖之後,調用的A方法也可以獲得該鎖。
二.使用鎖:
import static java.lang.System.out;
import java.util.concurrent.locks.ReentrantLock;
public class thread02 {
public static int count=0;
public static void main(String[] args) throws InterruptedException{
ReentrantLock lock=new ReentrantLock();
class My_thread01 extends Thread{
public void run(){
lock.lock();
try{
for(int i=0; i<10000;i++) {
count++;
}
}
finally{
lock.unlock();
}
}
}
class My_thread02 extends Thread{
public void run(){
lock.lock();
try{
for(int i=0; i<10000;i++) {
count++;
}
}
finally{
lock.unlock();
}
}
}
My_thread01 t1=new My_thread01();
My_thread02 t2=new My_thread02();
t1.start();
t2.start();
t1.join();
t2.join();
out.println("count is "+count);
}
}
區別:
a.Lock使用起來比較靈活,但需要手動釋放和開啓;採用synchronized不需要用戶去手動釋放鎖,
當synchronized方法或者synchronized代碼塊執行完之後,系統會自動讓線程釋放對鎖的佔用;
b.Lock不是Java語言內置的,synchronized是Java語言的關鍵字,因此是內置特性。Lock是一個類,通過這個類可以實現同步訪問;
c.在併發量比較小的情況下,使用synchronized是個不錯的選擇,但是在併發量比較高的情況下,其性能下降很嚴重,此時Lock是個不錯的方案。
d.使用Lock的時候,等待/通知 是使用的Condition對象的await()/signal()/signalAll() ,而使用synchronized的時候,則是對象的wait()/notify()/notifyAll();由此可以看出,使用Lock的時候,粒度更細了,一個Lock可以對應多個Condition。
e.雖然Lock缺少了synchronized隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與是釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized所不具備的同步特性;