1.概要
- 線程安全:多個線程同時訪問公共對象或者同一個對象時,採用了加鎖的機制,對公共數據進行保護,直到線程對該數據使用完。
- 非線程安全:多個線程同時訪問公共對象或者同一個對象時,發生數據不一致或者數據污染
- 髒讀:讀到的數據其實是被更改過的,數據不一致或者數據污染。
2.Synchronized方法與鎖對象
-
synchronized
1.監視器:每個對象都有一個監視器(monitor),它允許每個線程同時互斥和協作,就像每個對象都會有一塊受監控的區域
(數據結構),當線程執行需要取到監控區域的數據時,首先驗證是否有線程擁有監視器,已有線程擁有監視器則進入監視
器的monior entry list進行等待Thread.state:BLOCKED,直到釋放退出監控區域且釋放鎖。以這種FIFO的方式等待。
- 對象鎖:每個對象在堆內存中的頭部都會維持一塊鎖區域,任何線程要同步執行對象數據都會放入監視器且都必須獲鎖。
一個線程可以允許多次對同一對象上鎖.對於每一個對象來說,java虛擬機維護一個計數器,記錄對象被加了多少次鎖,沒被鎖的
對象的計數器是0,線程每加鎖一次,計數器就加1,每釋放一次,計數器就減1.當計數器跳到0的時候,鎖就被完全釋放了。
synchronized工作機制是這樣的:Java中每個對象都有一把鎖與之相關聯,
鎖控制着對象的synchronized代碼。一個要執行對象的synchronized代碼
的線程必須先獲得那個對象的鎖。
-
synchronized 同步方法
一個對象只有一把對象鎖
synchronized獲取的是對象鎖,保證線程順序進入對象方法。
public class test {
public void testSynchronized(){
try {
System.out.println("start:"+Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("end");
} catch (Exception e) {
// TODO: handle exception
}
}
public static void main(String[] args) {
test t = new test();
ThreadA threadA = new ThreadA(t);
threadA.setName("A");
threadA.start();
ThreadB threadB = new ThreadB(t);
threadB.setName("B");
threadB.start();
}
}
執行結果:可以看出線程是並行運行的。start:A
start:B
end
end
在方法加入同步synchronized關鍵字,執行結果:線程同步順序執行
start:A
end
start:B
end
鎖重入:關鍵字synchronized擁有鎖重入的功能,當一個線程得到對象鎖後在當前線程能夠再次獲得此對象鎖,這說明一個
線程在得到對象鎖後可以無限制的獲得對象鎖。
public class test {
public void methodA(){
System.out.println("非synchronized方法");
}
public synchronized void testSynchronized(){
try {
System.out.println("start:"+Thread.currentThread().getName());
methodB();
} catch (Exception e) {
// TODO: handle exception
}
}
public synchronized void methodB() throws InterruptedException{
Thread.sleep(5000);
System.out.println("end");
}
public static void main(String[] args) {
test t = new test();
ThreadA threadA = new ThreadA(t);
threadA.setName("A");
threadA.start();
ThreadB threadB = new ThreadB(t);
threadB.setName("B");
threadB.start();
}
}
執行結果:自己可以再次獲取自己的內部對象鎖,當線程獲取到對象鎖後在其內部還可以獲得對象鎖,如果鎖不可重入的話
就很容易造成死鎖,因爲外部對象鎖還未釋放,導致在內部永遠獲取不到對象鎖,線程永遠處於等待。
start:A
end
start:B
end
出現異常,鎖會自動釋放:當一個線程執行代碼是出現異常時,會自動釋放所持有的對象鎖。
同步不具有繼承性:當父類方法進行同步,子類重寫該方法,子類方法不具有同步性,需要添加synchronized關鍵字。
-
synchronized同步代碼塊
用synchronized同步方法是有弊端的,當方法某一條語句執行時間過長,就會導致其他線程需要等待較長時間。所以同步 代碼塊可以相對提高效率。
- 同步代碼塊的使用
synchronized需要依賴於對象鎖,同步代碼塊是需要一個鎖對象,可以是當前對象(this),一般系統併發量很高不採用當
前對象,而採用任意其他一個對象,不然造成大量線程等待在該對象。
public class test {
public void methodA(){
System.out.println("非synchronized方法");
}
public void testSynchronized(){
try {
System.out.println("start:"+Thread.currentThread().getName());
synchronized (this) {
methodB();
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
public synchronized void methodB() throws Exception{
Thread.sleep(5000);
System.out.println("end");
}
public static void main(String[] args) {
test t = new test();
ThreadA threadA = new ThreadA(t);
threadA.setName("A");
threadA.start();
ThreadB threadB = new ThreadB(t);
threadB.setName("B");
threadB.start();
}
}
執行結果:線程A、B併發進入方法,但有一個線程等待在同步代碼塊前。start:A
start:B
end
end
任意對象鎖:在同步代碼塊不取this(當前對象)鎖,而採用任意對象鎖,這樣的好處是當有很多synchronized同步方法時
,如果用this對象鎖,會造成阻塞,而採用任意鎖,其他同步塊則不會造成阻塞。
public class test {
private Object obj = new Object();
public void methodA(){
System.out.println("非synchronized方法");
}
public void testSynchronized(){
try {
System.out.println("start:"+Thread.currentThread().getName());
synchronized (this) {
methodB();
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
public synchronized void methodB() throws Exception{
Thread.sleep(5000);
System.out.println("end");
}
public void methodC(){
synchronized (obj) {
try {
System.out.println("start:"+Thread.currentThread().getName());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public static void main(String[] args) {
test t = new test();
ThreadA threadA = new ThreadA(t);
threadA.setName("A");
threadA.start();
ThreadB threadB = new ThreadB(t);
threadB.setName("B");
threadB.start();//執行methodC
}
}
執行結果:線程A使用當前this對象鎖同步代碼塊,線程B能夠同步執行OBJ對象鎖進行同步代碼塊
start:A
start:B
end
同步代碼塊解鎖無限等待問題:同步方法獲得的是當前對象的對象鎖,一單其中一個同步方法陷入死循環,該對象的其他同
步方法都無限等待,所以同步需要同步的部分代碼塊且使用任意對象鎖。
-
synchronized同步靜態方法
關鍵字synchronized還可以修飾static方法,如果這樣寫,那是對當前的.java文件的class對象進行加鎖。
- 與實例方法取得不同的對象鎖
Java程序在運行時,Java運行時系統一直對所有的對象進行所謂的運行時類型標識,即所謂的RTTI。這項信息紀錄了
每個對象所屬的類。虛擬機通常使用運行時類型信息選準正確方法去執行,用來保存這些類型信息的類是Class類。
Class類封裝一個對象和接口運行時的狀態,當裝載類時,Class類型的對象自動創建。
public class test {
private Object obj = new Object();
public synchronized void methodA(){
System.out.println("start not static :"+Thread.currentThread().getName());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("static end");
}
public synchronized static void testSynchronized(){
try {
System.out.println("start:"+Thread.currentThread().getName());
methodB();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
public static void methodB() throws Exception{
Thread.sleep(5000);
System.out.println("end");
}
public static void main(String[] args) {
test t = new test();
ThreadA threadA = new ThreadA(t);
threadA.setName("A");
threadA.start();
ThreadB threadB = new ThreadB(t);
threadB.setName("B");
threadB.start();
}
}
執行結果:兩個線程獲取的是不同的對象鎖,一個是test.classs對象鎖,一個是test對象鎖
start not static :A
start:B
static end
end
- String常量池類型鎖
在JVM中String有常量池緩存的特性
什麼事常量池?:
常量池(constant
pool)在編譯期間被指定,並被保存在已編譯的.class文件當中,用於存儲關於類、方法、接口中 的常量,也包括字符串直接量。
String與常量池
String str1 = new String("abc")
String str2 = "abc";
上面是兩種創建字符串的方式,看起來沒有什麼區別,但實則有很大區別
第一種是用new()來新建對象的,它會在存放於堆中。每調用一次就會創建一個新的對象。
而第二種是先在棧中創建一個對String類的對象引用變量str2,然後通過符號引用去字符串常量池裏找有沒有"abc",如果沒有
,則將"abc"存放進字符串常量池,並令str2指向”abc”,如果已經有”abc” 則直接令str2指向“abc”。
所以如果String str3 = “abc”;str2 和str3是同一對象,String str4 = “adcd”;str4和str2是不同的對象。
結論:如果使用String對象作爲對象鎖,必須要注意是否對象會改變;這就是String常量池帶來的問題,一般不會用String做
爲對象鎖,而改用其他,比如 new Object()。
-
死鎖
由於不同的線程都在等待永遠不能被釋放的鎖,從而導致任務不能繼續執行。在多線程中死鎖是必須避免的,會導致線程的
假死。
public class test {
public synchronized void methodA(){
System.out.println("start:"+Thread.currentThread().getName());
try {
Thread.sleep(5000);
while(true){
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("static end");
}
public static void main(String[] args) {
test t = new test();
ThreadA threadA = new ThreadA(t);
threadA.setName("A");
threadA.start();
ThreadB threadB = new ThreadB(t);
threadB.setName("B");
threadB.start();
}
}
執行結果:程序會一直等待。
我們可以通過jstack 命令來查看jvm內線程狀態,來找到死鎖的地方。
-
volatile關鍵字
volatile關鍵字的主要作用是使變量在多個線程中可見。
強制從公共的堆棧中取得變量的值,而不是在線程的私有棧中取變量的值。
解決同步死循環:
public class RunThread implements Runnable{
private boolean isRun = true;
public void setIsRun(boolean flag){
this.isRun = flag;
}
@Override
public void run() {
// TODO Auto-generated method stub
while(isRun == true){
try {
Thread.sleep(2000);
System.out.println("當前線程:"+Thread.currentThread().getName());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("end");
}
}
public class test {
public static void main(String[] args) {
RunThread runthread = new RunThread();
new Thread(runthread).start();
System.out.println("我要停止了");
runthread.setIsRun(false);
}
}
執行結果:上面代碼運行在64bitJVM,-server模式程序陷入死循環。解決辦法是使用volatile關鍵字,從公共堆棧中取得數
據,而不是線程私有棧中。
volatile非原子性
public class MyThread extends Thread{
volatile private static int count = 0;
public void run(){
addCount();
}
public void addCount(){
for(int i = 0;i<100;i++){
count++;
}
System.out.println(count);
}
}
主類:
public class test {
public static void main(String[] args) {
MyThread [] threadArr = new MyThread[100];
for(int i=0;i<100;i++){
threadArr[i] = new MyThread();
}
for(int i =0;i<100;i++){
threadArr[i].start();
}
}
}
執行結果
8500
8900
9100
9204
9298
9398
9498
9598
9698
9798
9898
9998
關鍵字volatile主要使用場合是在多線程中可以感知變量值更改了,並且可以獲得最新的值,每次取值都是從共享內存中
取的數據,而不是從線程的私有內存中取得數據。
但如果修改實例變量,比如i++,這樣的操作並不是一個原子操作,也是非線程安全的。表達式的步驟是:
1)從內存中取得i的值
2)計算i的值
3)將i的值寫到主存中
如果在第二步計算值時,其他線程也在修改i的值,就會出現髒讀。解決辦法是使用synchronized,volatile只能保證從
每次從主存中取得i的值。所以說volatile並不具有原子性。而是強制數據的讀寫影響到主存中去。