java多線程基礎(chǔ)(二)

簡介

本次主要介紹java多線程中的同步,也就是如何在java語言中寫出線程安全的程序。如何在java語言中解決非線程安全的相關(guān)問題。本次著重介紹如下的技術(shù)點:

  • 1.synchronized對象監(jiān)視器的使用
  • 2.非線程安全問題是如何出現(xiàn)的
  • 3.關(guān)鍵字 volatile的主要作用
  • 4.volatile與synchroized的區(qū)別

synchronized關(guān)鍵字的使用

synchronized實例化的線程安全

如果多個線程共同訪問同一個對象中的變量,則有可能出現(xiàn)“非線程安全”的問題。用線程訪問的對象中如果有多個實例變量,則運行的結(jié)果可能出現(xiàn)交叉的情況。下面以一個例子來進行說明如何利用synchroized來避免這種情況的發(fā)生。
首先,我們通過一個例子來看線程之間的變量存在怎樣的安全問題:
safeThread代碼如下:

public class safeThread {
int num= 0;//共享變量
public void addI(String username){
try {
if(username.equals("a")){
num =100;
System.out.println("a set over!");
Thread.sleep(2000);
}else{
num = 200;
System.out.println("b set over!");
}
System.out.println("username= "+username+" num= "+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

這里值得注意的是,這里的num變量是被當(dāng)作共享變量的。下面是ThreadA的代碼:

public class ThreadA extends Thread{
private safeThread thread ;
public ThreadA(safeThread thread){
super();
this.thread= thread;
}
public void run(){
super.run();
thread.addI("a");
}
}

ThreadB的代碼基本上和ThreadA的相同,只是在run中,執(zhí)行的是thread.addI("b");,具體代碼就不詳細給出了。
運行類;

public class runTest {
public static void main(String[] args) {
safeThread num = new safeThread();
ThreadA threada = new ThreadA(num);
ThreadB threadb = new ThreadB(num);
threada.start();
threadb.start();
}
}

運行結(jié)果如圖:

Paste_Image.png

就本程序而言,我們希望得到的結(jié)果是a將num設(shè)置為100,b將num設(shè)置為200.而最后得到的結(jié)果卻不是我們所預(yù)想得到的結(jié)果。那么怎么修改才能達到我們想要的效果呢?
首先我們得需要先分析為什么會得到兩個200的結(jié)果,解釋就是threadb將前面threada修改過的num又重新修改成200了,所以出現(xiàn)了兩個200.針對出現(xiàn)的結(jié)果,我們有兩種解決方案。一種就是將num設(shè)置成線程的私有變量。另一種就是通過添加synchroized關(guān)鍵字來進行控制。
方法一:將num設(shè)置成私有變量
將safeThread代碼修改如下,其實就是換了以下num定義的位置:


public class safeThread {

public void addI(String username){
int num= 0;
try {
if(username.equals("a")){
num =100;
System.out.println("a set over!");
Thread.sleep(2000);
}else{
num = 200;
System.out.println("b set over!");
}
System.out.println("username= "+username+" num= "+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

方法二:添加synchroized來進行加鎖控制,將safeThread代碼修改如下,其實就是在addI前面加個關(guān)鍵字synchroized。

public class safeThread {
int num= 0;
synchronized public void addI(String username){

try {
if(username.equals("a")){
num =100;
System.out.println("a set over!");
Thread.sleep(2000);
}else{
num = 200;
System.out.println("b set over!");
}
System.out.println("username= "+username+" num= "+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

修改后兩個的結(jié)果如圖:

方法一的結(jié)果.png
方法二的結(jié)果.png

兩種修改都達到了我們預(yù)想的效果。synchroized關(guān)鍵字在這里的作用就是將AddI方法塊添加了一把鎖,線程a還沒有使用完,線程b需要訪問時,必須排隊等候,等待a使用完畢才能使用AddI的方法。這就是synchroized關(guān)鍵字的一種簡單的用法,為一個方法加鎖。同時,通過我們上面的例子,我們可以得出一個結(jié)論就是:在兩個線程訪問同一個對象中的同步方法一定是線程安全的。

如果針對上面方法二的代碼,即添加了synchroized關(guān)鍵字的addI方法,修改其運行類的代碼如下;

public class runTest {
public static void main(String[] args) {
safeThread nu1 = new safeThread();
safeThread num2 = new safeThread();
ThreadA threada = new ThreadA(nu1);
ThreadB threadb = new ThreadB(num2);
threada.start();
threadb.start();
}
}

運行結(jié)果如圖;

Paste_Image.png

那么這個寫,與前面的寫法究竟有什么區(qū)別?上面的示例中是兩個線程分別訪問同一個類的兩個不同實例的相同名稱的同步方法,效果卻是以異步的方式進行的。本示例由于創(chuàng)建了兩個業(yè)務(wù)對象,在系統(tǒng)中產(chǎn)生了兩個鎖,所以運行結(jié)果是異步的,打印的結(jié)果就是先打印b,然后打印a。
從上面程序運行的結(jié)果來看,雖然上例中使用了synchroized關(guān)鍵字,但打印的順序卻不是同步的,是交叉的,為什么會出現(xiàn)這樣的結(jié)果?
關(guān)鍵字synchroized取得的鎖都是對象鎖,而不是把一段代碼或者方法當(dāng)作鎖,所以在上面的實例中,那個線程先執(zhí)行代synchroized關(guān)鍵字的方法,那個線程就先占有了該方法所屬對象的鎖Lock,那么其他線程只能呈等待狀態(tài),前提是多個線程訪問的是同一個對象。
但如果多線線程訪問多個對象,則JVM會創(chuàng)建多個鎖,本示例就是創(chuàng)建了兩個類的對象,所以就會產(chǎn)生了兩個鎖。
調(diào)用用關(guān)鍵字synchroized聲明的方法一定是排隊運行的。另外需要牢牢記住“共享”這兩個字,有共享資源的讀寫訪問才需要同步化,如果不是共享資源,那么根本沒有同步的必要。
為了加深對synchroized關(guān)鍵鎖的是對象,創(chuàng)建下面實例進行說明;
MyObject.java文件代碼如下:

public class MyObject {
public void methord() {
try {
System.out.println("開始運行,線程名" + Thread.currentThread().getName());
Thread.sleep(2000);
System.out
.println("線程 " + Thread.currentThread().getName() + " 結(jié)束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

自定義線程類ThreadA.java和ThreadB.jave。

public class ThreadA extends Thread {
private MyObject myObject;
public ThreadA(MyObject object){
this.myObject =object;
}

public void run(){
myObject.methord();
}
}

public class ThreadB extends Thread {
private MyObject myObject;
public ThreadB(MyObject object){
this.myObject =object;
}

public void run(){
myObject.methord();
}
}

類Runtest代碼如下:

public class Runtest {

public static void main(String[] args) {
MyObject myObject = new MyObject();
ThreadA a = new ThreadA(myObject);
ThreadB b = new ThreadB(myObject);
a.setName("A");
b.setName("B");
a.start();
b.start();
}
}

程序運行效果如下圖,更改MyObject.java代碼如下(添加synchroized關(guān)鍵字):

public class MyObject {
synchronized    public void methord() {
try {
System.out.println("開始運行,線程名" + Thread.currentThread().getName());
Thread.sleep(2000);
System.out
.println("線程 " + Thread.currentThread().getName() + " 結(jié)束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

如上面代碼所示,在methord方法前加入關(guān)鍵字synchroized進行同步處理。程序運行效果如下圖:

沒有關(guān)鍵字synchroized的運行效果圖.png
有關(guān)鍵字synchroized的運行效果圖.png

由此,我們不難得出下面的結(jié)論:

  1. A線程先持有object對象的Lock鎖,B線程可以以異步的方式調(diào)用object對象中的非synchroized類型的方法。
  2. B線程先持有object對象的Lock鎖,B線程如果在這時調(diào)用object對象中的synchronized類型的方法則需要等待,也就是同步。

雖然在賦值時進行了同步,但在取值的時候可能學(xué)壞才能一些意想不到的意外,這種情況就是臟讀,發(fā)生臟讀的情況就是在讀取實例變量時,此值已經(jīng)被其他的線程更改過了。
下面以一個例子來說明臟讀是如何出現(xiàn),以及我們怎么去解決臟讀。創(chuàng)建項目,新建PublicVar.java文件代碼如下:

public class PublicVar {
private String username="a",password="aa";

public void setAccout(String username,String password) throws InterruptedException{
this.username=username;
Thread.sleep(1000);
this.password = password;
}
synchronized public void getAccout(){
System.out.println("username ="+username+"\n password: "+password);
}
}

同步方法setAccout的鎖屬于類publicVar的實例。創(chuàng)建線程類ThreadVar的代碼如下:

public class ThreadVar extends Thread{
PublicVar var = new PublicVar();

public ThreadVar(PublicVar var){
this.var = var;
}

public void run(){
try {
var.setAccout("b", "bb");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

運行類文件代碼如下:

public class runVarTest {
public static void main(String[] args) throws InterruptedException {
PublicVar var = new PublicVar();
ThreadVar thread = new ThreadVar(var);
thread.setName("a");
thread.start();
Thread.sleep(200);
var.getAccout();
}
}

程序運行結(jié)果如圖:

Paste_Image.png

出現(xiàn)臟讀是因為getAccout()方法并不是同步的,所以可以在任意的時候進行調(diào)用,解決方法當(dāng)然是加上同步synchronized的關(guān)鍵字,代碼如下:

public class PublicVar {
private String username="a",password="aa";
synchronized public void setAccout(String username,String password) throws InterruptedException{
this.username=username;
Thread.sleep(1000);
this.password = password;
}
synchronized public void getAccout(){
System.out.println("username ="+username+"\n password: "+password);
}
}

運行結(jié)果如圖:

Paste_Image.png

通過這個案例不僅知道臟讀是通過synchronized關(guān)鍵字解決的,要可以總結(jié)出以下的內(nèi)容

  • 當(dāng)A線程第啊用angObject對象加入synchronized關(guān)鍵字的X方法時,A線程就獲得了X方法鎖,更準確的講,是獲得了對象的鎖,所以其他線程必須等線程A執(zhí)行完畢之后才可以調(diào)用X方法,但線程B可以隨意調(diào)用其他的非synchronized同步方法。
  • 當(dāng)線程A調(diào)用anyObject對象加入synchronized關(guān)鍵字的X方法時,A線程就獲得了X方法所在的對象鎖,所以其他線程必須等待A線程執(zhí)行完畢才可以調(diào)用X方法,而B線程如果調(diào)用了聲明了synchronized關(guān)鍵字的非X方法時,必須等待A線程將X方法執(zhí)行完畢,也就是釋放對象鎖后才可以進行調(diào)用。這時A線程畢竟執(zhí)行完一個完整的任務(wù)。也就是說username和password這兩個實例變量已經(jīng)同時被賦值,不存在臟讀的基本環(huán)境。

synchronized鎖重入

“可重入鎖”的感念是:自己可以再次獲取自己的內(nèi)部鎖,比如有一條線程獲得了某個對象的鎖,此時這個對象的鎖還沒有釋放,當(dāng)其再次想要獲取這個對象的鎖的時候還是可以獲取的,如果不可鎖沖入的話,就會造成死鎖。
關(guān)鍵字synchronized擁有鎖沖入的功能,也就是在使用synchronized時,當(dāng)一個線程得到一個對象后,再次請求次對象鎖時是可以再次得到該對象的鎖的。這也證明了在一個synchronized方法/塊的內(nèi)部調(diào)用本類的其他synchronized方法/塊時,是永遠可以得到鎖的。
下面以一個例子來進行說明:創(chuàng)建Methord代碼文件如下:

public class Methord {
synchronized public void methord1(){
System.out.println("methord1 begin run");
methord2();
System.out.println("methord1 end -----");
}

synchronized public void methord2(){
System.out.println("methord2 begin run");
methord3();
System.out.println("methord2 end -----");
}

synchronized public void methord3(){
System.out.println("methord3 begin run");
System.out.println("methord3 end -----");
}
}

線程類的代碼如下:

public class ThreadTestA implements Runnable{

Methord metord = new Methord();

public ThreadTestA(Methord metord){
this.metord = metord;
}
@Override
public void run() {
this.metord.methord1();
}
}

運行類代碼:

public class RunTest {

public static void main(String[] args) {
Methord methord = new Methord();
Runnable runnable1 = new ThreadTestA(methord);

Thread thread = new Thread(runnable1);
thread.start();
}
}

運行結(jié)果如圖:

Paste_Image.png

通過這個例子,相信大家都能很清楚的知道可重入鎖的含義是什么了。

synchronized的同步與及繼承性

接下來要講的是,可重入鎖也支持在父子類繼承的環(huán)境中。當(dāng)存在父子類繼承關(guān)系時,子類是完全可以通過“可重入鎖”調(diào)用父類的同步方法的。下面我們以例子為證明:

//父類方法
public class Main {
public int i = 10;

synchronized public void domain() {
try {
i--;
System.out.println("main print i= " + i);

Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

//子類方法
public class Sub extends Main {
synchronized public void domain() {
try {
while(i>0){
i--;
System.out.println("sub print i= " + i);
Thread.sleep(100);
super.domain();
}

} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

//線程類

public class MyThread extends Thread{
public void run(){
Sub sub = new Sub();
sub.domain();
}
}

//測試調(diào)用類:
public class TestRun {

public static void main(String [] args){
MyThread thread = new MyThread();
thread.start();
}
}

該程序的運行結(jié)果如圖:

Paste_Image.png

值得注意的是,同步是不可以繼承的。如果需要子類和父類進行同步,需在子類的方法中添加synchronized關(guān)鍵字。
需要注意的一點是,當(dāng)出現(xiàn)異常時,鎖會自動釋放。這個比較好理解,就不做例子進行解釋。

synchronized同步語句塊

前面介紹的都是用關(guān)鍵字synchronized聲明方法,但是聲明方法也是有弊端的,比如A線程調(diào)用的異步方法執(zhí)行一個長時間的任務(wù),那么B線程就必須等待比較長時間,在這種情況下,可以考慮使用synchronized同步語句塊來解決。
當(dāng)兩個并發(fā)線程訪問同一個對象object中的synchronized同步代碼塊時,一段時間內(nèi)只有有一個線程被執(zhí)行,另一個線程必須等待當(dāng)前線程執(zhí)行完這個代碼塊之后才能執(zhí)行該代碼塊。
創(chuàng)建測試實例:

//文件Task代碼文件
public class Task {

private String getData1;
private String getData2;

public void doLongTimeTask() {
try {
System.out.println("begin task...");
Thread.sleep(3000);//模擬長時間的運行任務(wù)
getData1="a long time task had run and return 1 thread name ="+Thread.currentThread().getName();
getData2="a long time task had run and return 2 thread name ="+Thread.currentThread().getName();
synchronized(this){
System.out.println(getData1);
System.out.println(getData2);
}

System.out.println("task end...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

//文件Comment代碼,用于存儲時間

public class Common {
public static long beginTime1;
public static long beginTime2;
public static long endTime1;
public static long endTime2;
}

//線程A和線程B
public class MyThreadA extends Thread{

private Task task;
public MyThreadA(Task task){
this.task = task;
}

public void run(){
Common.beginTime1 = System.currentTimeMillis();
task.doLongTimeTask();
Common.endTime1 =System.currentTimeMillis();
}
}

public class MyThreadB extends Thread{

private Task task;
public MyThreadB(Task task){
this.task = task;
}

public void run(){
Common.beginTime2 = System.currentTimeMillis();
task.doLongTimeTask();
Common.endTime2 =System.currentTimeMillis();
}

}

//運行類
public class Runtest {
public static void main(String[] args) {
Task task = new Task();
MyThreadA a = new MyThreadA(task);
MyThreadA b = new MyThreadA(task);
a.start();
b.start();
try{
Thread.sleep(10000);
}catch(Exception e){
System.out.println(e);
}
long beginTime = Common.beginTime1>Common.beginTime2?Common.beginTime1:Common.beginTime2;
long endTime = Common.endTime1>Common.endTime2? Common.endTime1:Common.endTime2;
System.out.println("task use: "+(endTime-beginTime));
}
}

運行結(jié)果如圖:

Paste_Image.png

如果我們是synchronized修飾doLongTimeTask方法,則運行時間必定大于等于6s,通過使用同步代碼塊有效的提高了效率,將時間降低至3s。通過上面的實驗可以得知,當(dāng)一個線程訪問object時的一個synchronized同步代碼塊時,另一個線程仍然可以訪問該object對象中的非synchronized(this)同步代碼塊。
在使用同步synchronized(this)代碼塊時需注意的是,當(dāng)一個線程訪問object的一個synchronized同步代碼塊時,其他線程對同一個object中所有其他synchronized(this)同步代碼塊的訪問將被阻塞,這說明synchronized使用的“對象監(jiān)視器”是一個。和synchronized方法一樣,synchronized(this)代碼塊也是鎖定當(dāng)前對象。下面將介紹將任意對象作為對象監(jiān)視器。

synchronized對象監(jiān)視器

多個線程調(diào)用同一個對象中的不同名稱的synchronized同步方法或者synchronized(this)同步代碼塊時,調(diào)用的效果就是按順序執(zhí)行,也就是同步的,阻塞的。
這說明synchronized同步方法或synchronized(this)同步代碼塊分別有兩種作用。

  1. synchronized同步方法
  • 對其他synchronized同步方法或synchronized(this)同步代碼塊調(diào)用呈阻塞狀態(tài)。
  • 同一時間只有一個線程可以執(zhí)行synchronized同步方法中的代碼。
  1. synchronized(this)同步方法塊
  • 對其他synchronized同步方法或synchronized(this)同步代碼塊調(diào)用呈阻塞狀態(tài)。
  • 同一時間只有一個線程可以執(zhí)行synchronized(this)同步代碼塊中的代碼。

前面我們都是通過synchronized方法或者synchronized(this)來作為“對象監(jiān)視器”,其實java還支持“任意對象”作為“對象監(jiān)視器”。使用格式為:synchronized(angObject)。
根據(jù)前面的總結(jié):

  1. 在多線程持有“對象監(jiān)視器”為同一個對象的前提下,同一時間只有一個線程可以執(zhí)行synchronized(angObject)同步代碼塊中的代碼。
  2. 當(dāng)持有“對象監(jiān)視器”為同一個對象的前提下,同一時間只有一個線程可以執(zhí)行synchronized(angObject)不同代碼塊中的代碼。
    下面以示例來進行說明:
    Service代碼如下:
public class Service {
private String username;
private String password;
private String angString = new String();

public void setUserAndrPassword(String user, String pass) {

try {
synchronized (angString) {
System.out.println("the thread name is "
+ Thread.currentThread().getName() + " at "
+ System.currentTimeMillis() + " be come in");
this.username = user;
Thread.sleep(3000);
System.out.println("the thread name is "
+ Thread.currentThread().getName() + " at "
+ System.currentTimeMillis() + " be come out");

this.password = pass;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

其他的文件代碼如下:

//線程A代碼文件
public class ThreadA extends Thread{
private Service service;
public ThreadA(Service stervice){
this.service =stervice;
}
public void run(){
service.setUserAndrPassword("a", "aa");
}
}
//線程B的文件代碼
public class ThreadB extends Thread{
private Service service;
public ThreadB(Service stervice){
this.service =stervice;
}
public void run(){
service.setUserAndrPassword("b", "bb");
}
}
//運行類
public class RunTest {
public static void main(String[] args) {
Service service = new Service();
ThreadA a = new ThreadA(service);
ThreadB b = new ThreadB(service);
a.setName("A");
b.setName("B");
a.start();
b.start();
}
}

運行結(jié)果如圖:

Paste_Image.png

可見,使用“synchronized(anyObject)”格式進行同步操作時,對象監(jiān)視器必須是同一個對象,如果不是同一個對象監(jiān)視器,運行的結(jié)果就是異步調(diào)用了,就會交叉運行。
將Service.java文件代碼更改如下(將angString變成私有對象):

public class Service {
private String username;
private String password;

public void setUserAndrPassword(String user, String pass) {
String angString = new String();
try {
synchronized (angString) {
System.out.println("the thread name is "
+ Thread.currentThread().getName() + " at "
+ System.currentTimeMillis() + " be come in");
this.username = user;
Thread.sleep(3000);
System.out.println("the thread name is "
+ Thread.currentThread().getName() + " at "
+ System.currentTimeMillis() + " be come out");

this.password = pass;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

運行的效果如圖:

Paste_Image.png

“synchronized(angObject)”格式的寫法是將X對象本身作為“對象監(jiān)視器”,這樣就可以得出以下的三個結(jié)論:

  1. 當(dāng)多個線程同時執(zhí)行synchronized同步代碼塊時呈同步效果。
  2. 當(dāng)其他線程執(zhí)行x對象中synchronized同步方法時呈同步效果。
  3. 當(dāng)其他線程執(zhí)行x對象方法里面的synchronized(this)代碼塊時也呈現(xiàn)同步效果。但需要注意:如果其他線程調(diào)用不加synchronized關(guān)鍵字方法時,還是異步調(diào)用。

到了這里,還有幾點需要注意的:當(dāng)“對象監(jiān)視器”為String類型時,需要注意String的常量池特性。對于String常量池的特性,在次不多做解釋,有需要的可以自己查閱相關(guān)資料,簡單的的介紹如下:

String a="a";
String b ="a";
System.out.println(a==b);

此時a和b的地址是相同的,此時返回的值為true;
所以在對象監(jiān)視器為string類型時,要時刻注意其常量池的特性。所以通常不用synchronized代碼塊使用String作為所對象,而改用其他,比如new Object()實例化一個Object,但他并不放入緩存中。
對了,這里還需要提示一下,只要對象不變,即使對象的屬性被改變,運行的結(jié)果還是同步的。

靜態(tài)同步synchronized方法與synchronized(class)代碼塊

關(guān)鍵字synchronized還可以應(yīng)用在static靜態(tài)方法上,如果這樣寫,那就是對當(dāng)前的*.java文件對應(yīng)的class類進行持鎖,下面就次舉例說明:
類文件:

public class Service {
synchronized public static void printA(){
try{
System.out.println("the Thread name is "+Thread.currentThread().getName()+" at :"+System.currentTimeMillis()+" come in ");
Thread.sleep(3000);
System.out.println("the Thread name is "+Thread.currentThread().getName()+" at :"+System.currentTimeMillis()+" come out ");
}catch(InterruptedException e){
e.printStackTrace();
}
}
synchronized public static void printfB(){
System.out.println("the Thread name is "+Thread.currentThread().getName()+" at :"+System.currentTimeMillis()+" come in ");
System.out.println("the Thread name is "+Thread.currentThread().getName()+" at :"+System.currentTimeMillis()+" come out ");
}
}

線程類:

//線程A
public class ThreadA implements Runnable{
@Override
public void run() {
Service.printA();
}
}
//線程B
public class ThreadB implements Runnable{
@Override
public void run() {
Service.printfB();
}
}
//運行測試類
public class RunTest {
public static void main(String[] args) {
Runnable a = new ThreadA();
Runnable b = new ThreadB();
Thread A = new Thread(a);
Thread B = new Thread(b);
A.setName("A");
B.setName("B");
A.start();
B.start();
}
}

運行結(jié)果如圖:

Paste_Image.png

從運行效果來看,并沒有什么特別之處,都是同步的效果,和將synchronized關(guān)鍵字添加到非static方法上使用的效果是一樣的,而實際上還是有本質(zhì)的區(qū)別,synchronized關(guān)鍵字添加到static靜態(tài)方法上是給class類上鎖,相當(dāng)與synchronized(class),而synchronized關(guān)鍵字加到非static靜態(tài)方法是給對象上鎖。

死鎖避免

記得在操作系統(tǒng)中學(xué)的死鎖的四個必要條件:

  1. 互斥條件
  2. 占有等待
  3. 不可剝奪條件
  4. 循環(huán)等待條件
    這里就不做仔細的講解,反正在設(shè)計程序的時候要避免出現(xiàn)死鎖。查看是否出現(xiàn)死鎖的方法,這里稍微介紹一下:
    進入cmd,進入安裝JDK文件夾下的bin目錄,執(zhí)行jps命令,查看當(dāng)前運行的線程id,然后運行jstack -1 [id]此處的id就是前面查看的id。檢測出現(xiàn)死鎖,如圖所示:

內(nèi)置類與靜態(tài)內(nèi)置類

關(guān)于內(nèi)置類和靜態(tài)內(nèi)置類的知識點在這里就不展開介紹了。內(nèi)置類以形如PrivateClass privateClass = publicClass.new PrivateClass();的形式來實例化。我們這里以一個例子來說明synchronized與內(nèi)置類的使用。

public class OutClass {
static class Inner{
public void methord1(){
synchronized("suo"){
for(int i=0;i<10;i++){
System.out.println("i= "+i+" "+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public synchronized void methord2(){
for(int i=10;i<20;i++){
System.out.println("i= "+i+" "+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}

運行類代碼:

import innerClass.OutClass.Inner;

public class Run {
public static void main(String[] args) {
final Inner inner = new Inner();
Thread t1 = new Thread(new Runnable() {

@Override
public void run() {
inner.methord1();
}
}, "a");

Thread t2 = new Thread(new Runnable() {

@Override
public void run() {
inner.methord2();
}
}, "b");
t1.start();
t2.start();
}
}

運行結(jié)果:

Paste_Image.png

通過不同的鎖對象,實現(xiàn)異步內(nèi)置類方法異步執(zhí)行。

volatile關(guān)鍵字

關(guān)鍵字volatile的主要作用是使變量在多個線程間可見。
如果不是在多繼承的情況下,使用繼承Thread類和實現(xiàn)Runnable接口在程序運行的結(jié)果上并沒有多大的區(qū)別。如果一旦出現(xiàn)“多繼承”的情況,則用實現(xiàn)Runnable接口的方式來處理多線程的問題就很有必要了。
創(chuàng)建一個測試類:

public class PrintString implements Runnable {
private boolean isContinuePrint = true;
public void setContinuePrint(boolean isContinuePrint) {
this.isContinuePrint = isContinuePrint;
}
public void printMethord(){
try{
while(isContinuePrint){
System.out.println("run methord thread name is "+Thread.currentThread().getName()+" --");
Thread.sleep(1000);
}
System.out.println("it had stop");
}catch(Exception e){
e.printStackTrace();
}
}
@Override
public void run() {
printMethord();
}
}
//運行類代碼
public class RunTest {
public static void main(String[] args){
PrintString print = new PrintString();
new Thread(print).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("stop! stop!!!!!!!!!!!");
print.setContinuePrint(false);
}
}

運行結(jié)果如圖:

Paste_Image.png

看起來程序并沒有什么問題,但當(dāng)上面的代碼運行在-servcer服務(wù)器模式模式的64位的JVM中上時,會出現(xiàn)死循環(huán)。那么是什么樣的原因造成JVM設(shè)置成-service就出現(xiàn)死循環(huán)?在啟動線程時,變量private boolean isContinuePrint存在與公共堆棧及線程的私有堆棧中,在JVM被設(shè)置呈-service模式時,為了線程運行的效率,線程一直在私有堆棧中取得isContinuePrint的值是true.而代碼print.setContinuePrint(false);雖然被執(zhí)行,更新的卻是公共堆棧中的isContinuePrint變量值false,所以就一直是死循環(huán)狀態(tài)。
解決辦法就是使用volatile關(guān)鍵字。用volcatile修飾isContinuePrint變量就可以了。通過使用volatile關(guān)鍵字,強制的從公共內(nèi)存中讀取變量的值,內(nèi)存的結(jié)構(gòu)如圖:

Paste_Image.png

關(guān)鍵字volatile的作用就是強制從公共堆棧中取得變量的值。而不是從線程私有數(shù)據(jù)棧中取得變量的值。使用volatile關(guān)鍵字增加了實例變量在多個線程之間的可見性,但是volatile最致命的缺點就是不支持原子性。
volatile的使用比較簡單,關(guān)于原子性的討論,讀者有興趣可以自己去查閱相關(guān)資料。這里不多做介紹。

synchronized與volatile的比較

1)關(guān)鍵字volatile是縣城同步的輕量級實現(xiàn),所以volatiel性能肯定比synchronized要好,并且volatile只能修飾變量,而synchronized可以修飾方法,已經(jīng)代碼塊,在開發(fā)中使用synchronized關(guān)鍵字的性能消耗肯定還是可較大的。
2) 多線程訪問volatile不會發(fā)生阻塞,而synchronized會出現(xiàn)阻塞。
3) volatile能保證數(shù)據(jù)的可見性,但是不能保證原子性;synchronized可以保證原子性,也可以簡潔的可見性。因為他可以將私有內(nèi)存和公
共內(nèi)存中的數(shù)據(jù)做同步。
4) 關(guān)鍵字volatile結(jié)局的是變量在多線程之間的可見性,而synchronized關(guān)鍵字解決的是多線程之間訪問資源的同步性。

最后介紹如何通過使用synchronized實現(xiàn)線程編程的可見性。
Servicer.java代碼如下:

public class Service {
private boolean isContinueRun = true;
public void runMethord(){
while(isContinueRun){
}
System.out.println("it had stop!");
}
public void stopMethord(){
isContinueRun = false;
}
}

線程類的代碼如下:

//線程A
public class ThreadA implements Runnable{
private Service service;
public ThreadA(Service service){
this.service = service;
}

public void run(){
service.runMethord();
}
}
//線程B
public class ThreadB implements Runnable{
private Service service;
public ThreadB(Service service){
this.service = service;
}

public void run(){
service.stopMethord();
}
}
//運行類
public class RunTest {

public static void main(String [] args) throws InterruptedException{
Service servcer = new Service();
Runnable a = new ThreadA(servcer);
Runnable b = new ThreadB(servcer);

Thread threada = new Thread(a);
Thread threadb = new Thread(b);
threada.start();
Thread.sleep(1000);
threadb.start();
}
}

如果此時運行,會發(fā)現(xiàn)什么現(xiàn)象都沒有發(fā)生,并沒有我們預(yù)想中的線程停止的提示信息彈出,這個結(jié)果是個線程間數(shù)據(jù)值沒有可視性造成的,而關(guān)鍵字可以具有可視性,修改Service.java代碼如下:

public class Service {
private boolean isContinueRun = true;
public void runMethord(){
Object object = new Object();
while(isContinueRun){
synchronized( object ) {
}
}
System.out.println("it had stop!");
}
public void stopMethord(){
isContinueRun = false;
}
}

運行效果如圖:

Paste_Image.png

關(guān)鍵字synchronized可以保證在同一時刻,只有一個線程執(zhí)行某一個方法或者某個代碼塊。他包含兩個特征:互斥性和可見性。

???K??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Java-Review-Note——4.多線程 標簽: JavaStudy PS:本來是分開三篇的,后來想想還是整...
    coder_pig閱讀 1,677評論 2 17
  • 寫在前面的話: 這篇博客是我從這里“轉(zhuǎn)載”的,為什么轉(zhuǎn)載兩個字加“”呢?因為這絕不是簡單的復(fù)制粘貼,我花了五六個小...
    SmartSean閱讀 4,790評論 12 45
  • 從三月份找實習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍閱讀 42,366評論 11 349
  • Java8張圖 11、字符串不變性 12、equals()方法、hashCode()方法的區(qū)別 13、...
    Miley_MOJIE閱讀 3,731評論 0 11
  • “我第一饞是燒鴨,第二饞是茄子。” 這是風(fēng)第一次吃霜做的菜時說的話。10年過去了,這句話霜一直記得。此后的燒鴨,風(fēng)...
    _荷包蛋_閱讀 264評論 0 0