對象變量的並發訪問
前言:本系列將從零開始講解java多線程相關的技術,內容參考於《java多線程核心技術》與《java並發編程實戰》等相關資料,希望站在巨人的肩膀上,再通過我的理解能讓知識更加簡單易懂。
目錄
- 認識cpu、核心與線程
- java多線程系列(一)之java多線程技能
- java多線程系列(二)之對象變量的並發訪問
- java多線程系列(三)之等待通知機制
- java多線程系列(四)之ReentrantLock的使用
- java多線程系列(五)之synchronized ReentrantLock volatile Atomic 原理分析
- java多線程系列(六)之線程池原理及其使用
線程安全
- 線程安全就是多線程訪問時,采用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其他線程不能進行訪問直到該線程讀取完,其他線程才可使用。不會出現數據不一致或者數據污染。 線程不安全就是不提供數據訪問保護,有可能出現多個線程先后更改數據造成所得到的數據是臟數據
局部變量並不會數據共享
public class T1 {
public static void main(String[] args) {
PrivateNum p=new PrivateNum();
MyThread threadA=new MyThread('A',p);
MyThread threadB=new MyThread('B',p);
threadA.start();
threadB.start();
}}
class MyThread extends Thread
{
char i;
PrivateNum p;
public MyThread(char i,PrivateNum p)
{
this.i=i;
this.p=p;
}
public void run()
{
p.test(i);
}
}
class PrivateNum
{
public void test( char i)
{
try {
int num=0;
if(i=='A')
{
num=100;
System.out.println("線程A已經設置完畢");
Thread.sleep(1000);
}
else
{
num=200;
System.out.println("線程B已經設置完畢");
}
System.out.println("線程"+i+"的值:"+num);
}
catch (InterruptedException e) {
// TODO 自動生成的 catch 塊
e.printStackTrace();
}}
}
線程A已經設置完畢
線程B已經設置完畢
線程B的值:200
線程A的值:100
- 在這段代碼中,線程A和B先后對num進行賦值,當兩個線程都賦值后再分別打印,但是輸出的結果並不相同,后賦值的線程並沒有對num的值進行覆蓋,因為這里的num是在方法里面的,也就是局部變量,不同線程的num並不共享,所以並不會發生覆蓋。
實例成員變量數據共享
public void test( char i)
{
int num=0;
try {
if(i=='A')
{
num=100;
System.out.println("線程A已經設置完畢");
Thread.sleep(1000);
}
else
{
num=200;
System.out.println("線程B已經設置完畢");
}
System.out.println("線程"+i+"的值:"+num);
}
catch (InterruptedException e) {
// TODO 自動生成的 catch 塊
e.printStackTrace();
}}
線程B已經設置完畢
線程A已經設置完畢
線程B的值:200
線程A的值:200
- 這里的代碼只是將
int num=0
放到了方法的外面,但是輸出的結果卻不同,因為這時候線程AB訪問的是同一個變量(指向同一個地址),所以這個時候會發生覆蓋,同時這里出現了線程安全問題。
synchronized關鍵字可以避免線程安全問題
public synchronized void test( char i)
{
int num=0;
try {
if(i=='A')
{
num=100;
System.out.println("線程A已經設置完畢");
Thread.sleep(1000);
}
else
{
num=200;
System.out.println("線程B已經設置完畢");
}
System.out.println("線程"+i+"的值:"+num);
}
catch (InterruptedException e) {
// TODO 自動生成的 catch 塊
e.printStackTrace();
}}
線程A已經設置完畢
線程A的值:100
線程B已經設置完畢
線程B的值:200
- 這里只是上面代碼的基礎上增加了一個
synchronized
,避免了線程安全問題
總結
- 如果多個線程訪問的是同一個對象方法中的局部變量,那么這個變量並不共享,線程AB對此變量的操作將互不影響
- 如果多個線程訪問的是同一個對象方法中的成員變量,那么這個變量共享,如果不處理好線程問題,可能會出現線程安全問題
- 通過synchronized關鍵字可以使方法同步
多個線程訪問的是兩個不同實例的同一個同步方法
public class T1 {
public static void main(String[] args) {
PrivateNum p1=new PrivateNum();
PrivateNum p2=new PrivateNum();
MyThread threadA=new MyThread('A',p1);
MyThread threadB=new MyThread('B',p2);
threadA.start();
threadB.start();
}}
線程A已經設置完畢
線程B已經設置完畢
線程B的值:200
線程A的值:100
- 這里的代碼又是在上面的代碼進行修改,這里我們添加了synchronized關鍵字,對方法上鎖,但是卻是異步執行的(同步的話,應該是這樣輸出
線程A已經設置完畢 線程A的值:100
),這是因為這里是兩個鎖,創建了p1和p2對象,創建的是兩個鎖,鎖對象不同不造成互斥作用。
多線程調用同一個實例的兩個不同(一個同步,一個非同步)方法
public class T1 {
public static void main(String[] args) {
PrivateNum p1=new PrivateNum();
MyThread threadA=new MyThread('A',p1);
MyThread2 threadB=new MyThread2('B',p1);
threadA.start();
threadB.start();
}}
class MyThread extends Thread
{
char i;
PrivateNum p;
public MyThread(char i,PrivateNum p)
{
this.i=i;
this.p=p;
}
public void run()
{
p.test(i);
}
}
class MyThread2 extends Thread
{
char i;
PrivateNum p;
public MyThread2(char i,PrivateNum p)
{
this.i=i;
this.p=p;
}
public void run()
{
p.test2(i);
}
}
class PrivateNum
{
int num=0;
public void test2(char i)
{
System.out.println("線程"+i+"執行,線程A並沒有同步執行");
}
public synchronized void test( char i)
{
try {
if(i=='A')
{
num=100;
System.out.println("線程A已經設置完畢");
Thread.sleep(100);
}
else
{
num=200;
System.out.println("線程B已經設置完畢");
}
System.out.println("線程"+i+"的值:"+num);
}
catch (InterruptedException e) {
// TODO 自動生成的 catch 塊
e.printStackTrace();
}}
}
線程A已經設置完畢
線程B執行,線程A並沒有同步執行
線程A的值:100
線程B的值:200
- 這里的代碼我們給PrivateNum添加了一個非同步的test2方法,MyThreadrun中調用的同步的test方法,MyThread2中調用的是非同步的test2方法,實驗表明線程B可以異步調用非同步的方法。
多線程調用同一個實例的兩個不同的同步方法
public synchronized void test2(char i)
{
System.out.println("線程"+i+"執行,線程A同步執行");
}
線程A已經設置完畢
線程A的值:100
線程B執行,線程A同步執行
線程B的值:200
- 這里的代碼我們只是給test2方法添加一個synchronized關鍵字,這個時候兩個線程調用的方法同步執行。
總結
- 多個線程調用的不同實例的同步方法,線程不互斥。
- 如果兩個線程的鎖對象一樣(都是p1),兩個線程分別調用同步方法和非同步方法,線程不會同步執行。
- 但是如果調用兩個不同的同步方法,因為鎖對象一致,兩個線程同步執行。
- 設想一個情況,有一個實例有兩個方法,一個修改值(synchronized),一個讀值(非synchronized),此時兩個線程一個修改值,一個讀取值,這個時候因為這兩個線程並不會掙搶鎖,兩個線程互不影響,那么此時可能就會出現一種情況,線程A還沒修改完,線程B就讀取到沒有修改的值。這就是所謂的臟讀。
重入鎖
public class T1 {
public static void main(String[] args) {
MyThread3 thread=new MyThread3();
thread.start();
}}
class MyThread3 extends Thread
{
Service s=new Service();
public void run()
{
s.service1();
}
}
class Service
{
public synchronized void service1()
{
System.out.println("服務1並沒有被鎖住");
service2();
}
public synchronized void service2()
{
System.out.println("服務2並沒有被鎖住");
service3();
}
public synchronized void service3()
{
System.out.println("服務3並沒有被鎖住");
}
}
服務1並沒有被鎖住
服務2並沒有被鎖住
服務3並沒有被鎖住
- 我們可能會這么認為,thread線程執行了同步的service1方法,這個時候把鎖占住,如果這個時候要執行另一個同步方法service2方法,必須先執行完service1方法,然后把鎖讓出去才行,但是實驗證明鎖是可以重入的,一個線程獲得鎖后,還沒釋放后可以再次獲取鎖。
出現異常會釋放鎖
- 如果同步方法里面出現異常,會自動將鎖釋放
同步方法不會繼承
public class T1 {
public static void main(String[] args) {
Service3 s=new Service3();
MyThread4 t1=new MyThread4(s,'1');
MyThread4 t2=new MyThread4(s,'2');
t1.start();
t2.start();
}}
class MyThread4 extends Thread
{
Service3 s;
char name;
public MyThread4(Service3 s,char name)
{
this.s=s;
this.name=name;
}
public void run()
{
s.service(name);
}
}
class Service2
{
public synchronized void service(char name)
{
for (int i = 3; i >0; i--) {
System.out.println(i);
}
}
}
class Service3 extends Service2
{
public void service(char name)
{
for (int i = 5; i >0; i--) {
System.out.println("線程"+name+":"+i);
}
}
}
線程1:5 線程2:5
- 如果父類的方法是同步的,如果子類重載同步方法,但是沒有synchronized關鍵字,那么是沒有同步作用的。
總結
- 重入鎖,一個獲得的鎖的線程沒執行完可以繼續獲得鎖。
- 線程占用鎖的時候,如果執行的同步出現異常,會將鎖讓出。
- 父類方法同步,子類重寫該方法(沒有synchronized關鍵字修飾),是沒有同步作用的。
同步代碼塊
public class T1 {
public static void main(String[] args) {
Service2 s=new Service2();
MyThread t1=new MyThread(s,'A');
MyThread t2=new MyThread(s,'B');
t1.start();
t2.start();
}
}
class Service2
{
public void service(char name)
{
synchronized(this)
{
for (int i = 3; i >0; i--) {
System.out.println(name+":"+i);
}
}
}
}
class MyThread extends Thread
{
Service2 s=new Service2();
char name;
public MyThread(Service2 s,char name)
{
this.s=s;
this.name=name;
}
public void run()
{
s.service(name);
}
}
A:3
A:2
A:1
B:3
B:2
B:1
- 當多個線程訪問同一個對象的synchronized(this)代碼塊時,一段時間內只有一個線程能執行
同步代碼塊的鎖對象
class Service2
{
String s=new String("鎖");
public void service(char name)
{
synchronized(s)
{
for (int i = 3; i >0; i--) {
System.out.println(name+":"+i);
}
}
}
}
- 將this換成自己創建的鎖(一個對象),同樣可以實現同步功能
部分同步,部分異步
public void service(char name)
{
for (int i = 6; i >3; i--) {
System.out.println(name+":"+i);
}
synchronized(this)
{
for (int i = 3; i >0; i--) {
System.out.println(name+":"+i);
}
}
}
A:6
B:6
A:5
B:5
A:4
B:4
A:3
A:2
A:1
B:3
B:2
B:1
- 不在同步代碼塊中的代碼可以異步執行,在同步代碼塊中的代碼同步執行
不同方法里面的synchronized代碼塊同步執行
public class T1 {
public static void main(String[] args) {
Service2 s=new Service2();
MyThread t1=new MyThread(s,'A');
MyThread2 t2=new MyThread2(s,'B');
t1.start();
t2.start();
}
}
class Service2
{
public void service(char name)
{
synchronized(this)
{
for (int i = 3; i >0; i--) {
System.out.println(name+":"+i);
}
}
}
public void service2(char name)
{
synchronized(this) {
for (int i = 6; i >3; i--) {
System.out.println(name+":"+i);
}
}
}
}
class MyThread extends Thread
{
Service2 s=new Service2();
char name;
public MyThread(Service2 s,char name)
{
this.s=s;
this.name=name;
}
public void run()
{
s.service(name);
}
}
class MyThread2 extends Thread
{
Service2 s=new Service2();
char name;
public MyThread2(Service2 s,char name)
{
this.s=s;
this.name=name;
}
public void run()
{
s.service2(name);
}
}
A:3
A:2
A:1
B:6
B:5
B:4
- 兩個線程訪問同一個對象的兩個同步代碼塊,這兩個代碼塊是同步執行的
鎖不同沒有互斥作用
class Service2
{
Strign s=new String();
public void service(char name)
{
synchronized(s)
{
for (int i = 3; i >0; i--) {
System.out.println(name+":"+i);
}
}
}
public void service2(char name)
{
synchronized(this) {
for (int i = 6; i >3; i--) {
System.out.println(name+":"+i);
}
}
}
}
- 將this改成s,也就是改變鎖對象,發現兩個方法並不是同步執行的
synchronized方法和synchronized(this)代碼塊是鎖定當前對象的
public void service(char name)
{
synchronized(this)
{
for (int i = 3; i >0; i--) {
System.out.println(name+":"+i);
}
}
}
public synchronized void service2(char name)
{
for (int i = 6; i >3; i--) {
System.out.println(name+":"+i);
}
}
- 將service2的代碼塊改成synchronized 方法,發現輸出結果是同步的的,說明鎖定的都是當前對象
總結
- 同步代碼塊的鎖對象可以是本對象,也可以是其他對象。同一個鎖對象可以產生互斥作用,不同鎖對象不能產生互斥作用
- 一個方法中有同步代碼塊和非同步代碼塊,同步代碼塊的代碼是同步執行的(塊里的代碼一次執行完),而非同步代碼塊的代碼可以異步執行
- 一個對象中的不同同步代碼塊是互斥的,執行完一個代碼塊再執行另一個代碼塊
- 同步代碼塊(this)和synchronized方法的鎖定的都是當前對象 this
syncronized static 同步靜態方法
class Service2
{
public synchronized static void service()
{
for (int i = 3; i >0; i--) {
System.out.println(name+":"+i);
}
}
}
- 在這里鎖對象就不是service對象,而是Class(Class(和String Integer一樣)是一個類)
Class鎖
class Service2
{
public static void service()
{
synchronized(Service.class)
{
for (int i = 3; i >0; i--) {
System.out.println(name+":"+i);
}
}
}
}
- 這里的效果和上面靜態的synchronized一樣
靜態類中非靜態同步方法
public class T1 {
public static void main(String[] args) {
Service.Service2 s=new Service.Service2();
Thread t1=new Thread(new Runnable()
{public void run(){s.service();}});
Thread t2=new Thread(new Runnable()
{public void run(){Service.Service2.service2();}});
t1.start();
t2.start();
}
}
class Service{
static class Service2
{
public synchronized void service()
{
for (int i = 20; i >10; i--) {
System.out.println(i);
}
}
public static synchronized void service2()
{
for (int i = 9; i >3; i--) {
System.out.println(i);
}
}
}}
//不同步執行
- 這里service方法的鎖還是service對象,
總結
- Class類也可以是鎖,Class鎖的實現可以通過靜態的synchronizd方法,也可以通過靜態方法里面的同步代碼塊(鎖對象為Class)
- 靜態類的同步方法鎖對象還是該類的一個實例
死鎖
public class DealThread implements Runnable {
public String username;
public Object lock1 = new Object();
public Object lock2 = new Object();
public void setFlag(String username) {
this.username = username;
}
@Override
public void run() {
if (username.equals("a")) {
synchronized (lock1) {
try {
System.out.println("username = " + username);
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("按lock1->lock2代碼順序執行了");
}
}
}
if (username.equals("b")) {
synchronized (lock2) {
try {
System.out.println("username = " + username);
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("按lock2->lock1代碼順序執行了");
}
}
}
}
}
- 線程AB開始執行時,因為鎖不同,所以不互斥,A當執行到另一個同步代碼塊(鎖2)的時候,由於這個時候鎖給線程B占有了,所以只能等待,同樣B線程也是如此,AB互相搶對方的鎖,但是所以造成了死鎖。
鎖對象發生改變
- 修改鎖對象的屬性不印象結果,比如此時鎖對象為user對象,我把user的name設為jiajun,此時不影響結果
volatile
public class Tee {
public static void main(String[] args) {
try {
RunThread thread = new RunThread();
thread.start();
Thread.sleep(1000);
thread.setRunning(false);
System.out.println("已經賦值為false");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class RunThread extends Thread {
private boolean isRunning = true;
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
@Override
public void run() {
System.out.println("進入run了");
while (isRunning == true) {
}
System.out.println("線程被停止了!");
}
}
- 在這里,把vm運行參數設置為-server(右鍵運行配置,自變量那里可以設置,server參數可以提高運行性能),結果發現雖然我們將值設置為false,但是卻仍然進入死循環。
- isRunning變量存放在公共堆棧和線程的私有堆棧中,我們對他賦值為false時,只對公共堆棧進行更新,而但我們設置為-server后,讀取的是線程私有棧的內容,所以也就造成了死循環。我們可以在isRunning變量前加上volatite關鍵字,這個時候訪問的是公共堆棧,就不會造成死循環了。
- 以前我們使用單線程的時候,這種情況情況不會發生,但是當多個線程進行讀寫操作的時候就可能爆發出問題,這是因為我們沒有用同步機制來保證他,這是我們需要注意的一點。
public class Tee {
public static void main(String[] args) {
MyThread[] mythreadArray = new MyThread[100];
for (int i = 0; i < 100; i++) {
mythreadArray[i] = new MyThread();
}
for (int i = 0; i < 100; i++) {
mythreadArray[i].start();
}
}
class MyThread extends Thread {
volatile public static int count;
private static void addCount() {
for (int i = 0; i < 100; i++) {
count++;
}
System.out.println("count=" + count);
}
@Override
public void run() {
addCount();
}
}
- 在這里我們只count前添加volatile,但是最終結果輸出的並不是10000,說明並沒有同步的作用,volatile不處理數據原子性(i++不是原子操作)
- 我們將volatile去掉,將addcount方法用synchronized修飾,發現輸出了10000,說明了synchronized的同步作用不僅保證了對同一個鎖線程的互斥,還保證了數據的同步。
總結
- volitate增加了實例變量在對個線程之間的可見性,保證我們獲得的是變量的最新值。
- volatile在讀上面保持了同步作用,但是在寫上面不保持同步
- synchronized的同步作用不僅保證了對同一個鎖線程的互斥,還保證了數據的同步
volatile對比synchronized
- 兩者修飾的不同,volatile修飾的是變量,synchronized修飾的是方法和代碼塊
- 兩者的功能不同。volatile保證數據的可見性,synchronized是線程同步執行(間接保證數據可見性,讓線程工作內存的變量和公共內存的同步)
- volatile性能比synchronized性能高
用原子類實現i++同步
class MyThread extends Thread {
static AtomicInteger count=new AtomicInteger(0);
private static void addCount() {
for (int i = 0; i < 100; i++) {
count.incrementAndGet();
}
System.out.println(count.get());
}
@Override
public void run() {
addCount();
}
}
- 將上面的count++進行用原子類AtomicInteger改變,最后輸出了1000
我覺得分享是一種精神,分享是我的樂趣所在,不是說我覺得我講得一定是對的,我講得可能很多是不對的,但是我希望我講的東西是我人生的體驗和思考,是給很多人反思,也許給你一秒鍾、半秒鍾,哪怕說一句話有點道理,引發自己內心的感觸,這就是我最大的價值。(這是我喜歡的一句話,也是我寫博客的初衷)
作者:jiajun 出處: http://www.cnblogs.com/-new/ 本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。如果覺得還有幫助的話,可以點一下右下角的【推薦】,希望能夠持續的為大家帶來好的技術文章!想跟我一起進步么?那就【關注】我吧。