漲姿勢了!原來這纔是多線程正確實現方式

Java內存模型



線程同步

線程同步機制是一套適用於協調線程之間的數據訪問機制,該機制可以保障線程安全

java平臺提供的線程同步機制包括:鎖、volatile關鍵字、final關鍵字,static關鍵字、以及相關API如object.wait/object.notify

鎖概述

線程安全問題的產生前提是多個線程併發訪問共享數據,將多個數據對共享數據的併發訪問,轉化爲串行訪問,即共享數據只能被一個線程訪問,鎖就是這種思路。

線程訪問數據時必須先獲得鎖,獲得鎖的線程稱爲鎖的持有線程,一個鎖一次只能被一個線程持有,持有線程在獲得鎖之後和釋放鎖之前鎖執行的代碼稱之爲臨界區。

鎖具有排它性(Exclisive),即一個鎖只能被一個線程持有,這種鎖稱爲排它鎖或者互斥鎖。


JVM部分把鎖分爲內部鎖和顯示鎖,內部鎖通過Synchronized關鍵字實現,顯示鎖通過
java.concurrent.locks.Lock接口實現類實現的。

鎖的作用

鎖能夠實現對共享數據的安全,保障線程的原子性,可見性與有序性。

鎖是通過互斥保障原子性,一個鎖只能被一個線程持有,這就保證了臨界區的代碼一次只能被一個線程執行,使得臨界區代碼所執行的的操作自然而然的具有不可分割的特性,既具備了原子性。

好比一條路段所有車輛都在跑,併發執行,在經過某一個路段的時候,多車道變爲一車道,一次只能通過一輛車,由併發執行改爲串行執行。

可見性是通過寫線程沖刷處理器的緩存和讀線程刷新處理器緩存這兩個動作,鎖的獲得隱含着刷新處理器緩存的動作,鎖的釋放隱含着沖刷處理器緩存的動作。

鎖能夠保障有序性,寫線程在臨界區所執行的臨界區看來像是完全按照源碼順序執行的。

鎖的相關概念

可重入性:一個線程持有該鎖的時候能夠再次/多次申請該鎖

如果一個線程持有一個鎖的時候,還沒有釋放,但還能夠繼續成功申請該鎖,稱該鎖可重入,反之。

鎖的爭用與調度

java中內部鎖屬於非公平鎖,顯示鎖支持非公平鎖和公平鎖

鎖的粒度

一個所可以保護的共享數據的數量大小稱爲鎖的粒度。

鎖保護共享數據量大,稱爲鎖粒度粗,否則稱爲粒度細。

鎖的粒度過粗會導致線程在申請鎖時會進行不必要的等待,鎖粒度過細會增加鎖調度的開銷。

比如銀行有一個櫃檯一個員工可以辦理開卡、銷戶、取現、貸款那麼所有人都只能去這個櫃檯辦理業務,會需要很長的等待時間。但是如果把業務細分,一個業務一個櫃檯,這時候增加了銀行的開銷,需要三個員工。

內部鎖:Synchronized

Java中每一個對象都有一個與之關聯的內部鎖,這種鎖也叫監視器,是一種排它鎖,可以保障原子性、可見性、排它性。

Synchronized(對象鎖)
{
同步代碼塊,可以在同步代碼塊中訪問共享數據
}

修飾實例方法稱爲同步實例方法,修飾靜態方法稱爲同步靜態方法。

Synchronized同步代碼塊

public class SynchronizedLock {
public static void main(String[] args) {
SynchronizedLock synchronizedLock=new SynchronizedLock();
for (int i = 0; i <2 ; i++) {
new Thread(new RunnableThread())
{
@Override
public void run()
{
synchronizedLock.mm();
}
}.start();
}
}
public void mm()
{
for (int i = 0; i <100 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}

兩個線程的代碼都在併發執行


現在要打印的時候進行同步,同步的原理線程在執行的時候要先要獲得鎖

public class SynchronizedLock {
public static void main(String[] args) {
SynchronizedLock synchronizedLock=new SynchronizedLock();
for (int i = 0; i <2 ; i++) {
new Thread(new RunnableThread())
{
@Override
public void run() {
synchronizedLock.mm();//使用鎖的對象是synchronizedLock對象
}
}.start();
}
}
public void mm()
{
synchronized (this)//this作爲當前對象
{
for (int i = 0; i <100 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
}


因爲Synchronized內部鎖是排它鎖,一次只能被一個線程持有,現在是Thread-0先取得鎖對象,Thread-1在等待區等待Thread-0執行完畢釋放鎖,Thread-1獲得鎖再執行。

鎖對象不同不能實現同步


public class SynchronizedLock {
public static void main(String[] args) {
SynchronizedLock synchronizedLock=new SynchronizedLock();
SynchronizedLock synchronizedLock2=new SynchronizedLock();
new Thread(new RunnableThread())
{
@Override
public void run() {
synchronizedLock.mm();//使用鎖的對象是synchronizedLock對象
}
}.start();
new Thread(new RunnableThread())
{
@Override
public void run() {
synchronizedLock2.mm();//使用鎖的對象是synchronizedLock對象
}
}.start();
}
public void mm()
{
synchronized (this)//this作爲當前對象
{
for (int i = 0; i <100 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
}


因此想要同步必須使用同一個鎖對象

使用常量作爲鎖對象


public class SynchronizedLock {
public static void main(String[] args) {
SynchronizedLock synchronizedLock=new SynchronizedLock();
SynchronizedLock synchronizedLock2=new SynchronizedLock();
new Thread(new RunnableThread())
{
@Override
public void run() {
synchronizedLock.mm();//使用鎖的對象是synchronizedLock對象
}
}.start();
new Thread(new RunnableThread())
{
@Override
public void run() {
synchronizedLock.mm();//使用鎖的對象是synchronizedLock對象
}
}.start();
}
public static final Object obj=new Object();
public void mm()
{
synchronized (obj)//常量作爲當前對象
{
for (int i = 0; i <100 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
}

同步實例方法

使用synchronized修飾實例方法,同步實例方法,默認使用this作爲鎖對象

public class SynchronizedLock {
public static void main(String[] args) {
SynchronizedLock synchronizedLock=new SynchronizedLock();
new Thread(new RunnableThread())
{
@Override
public void run() {
synchronizedLock.mm();
}
}.start();
new Thread(new RunnableThread())
{
@Override
public void run() {
synchronizedLock.mm2();
}
}.start();
}
//同步實例方法
public synchronized void mm()
{
for (int i = 0; i <100 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
public void mm2()
{
synchronized (this)//常量作爲當前對象
{
for (int i = 0; i <100 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
}

同步靜態方法

使用synchronized修飾靜態方法,同步靜態方法,默認運行時使用SynchronizedLock class作爲鎖對象

public class SynchronizedLock {
public static void main(String[] args) {
SynchronizedLock synchronizedLock=new SynchronizedLock();
new Thread(new RunnableThread())
{
@Override
public void run() {
synchronizedLock.mm2();
}
}.start();
new Thread(new RunnableThread())
{
@Override
public void run() {
SynchronizedLock.mm();//使用鎖的對象是SynchronizedLock.class
}
}.start();
}
//同步靜態方法
public synchronized static void mm()
{
for (int i = 0; i <100 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
public void mm2()
{
synchronized (SynchronizedLock.class)//常量作爲當前對象
{
for (int i = 0; i <100 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
}

同步代碼塊和同步方法如何選擇

public class SynchronizedLock {
public static void main(String[] args) {
SynchronizedLock synchronizedLock=new SynchronizedLock();
new Thread(new RunnableThread())
{
@Override
public void run()
{
try {
synchronizedLock.mm2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
new Thread(new RunnableThread())
{
@Override
public void run()
{
try {
synchronizedLock.mm2();//使用鎖的對象是SynchronizedLock.class
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
//同步實例方法 鎖的粒度粗 執行效率低
public synchronized void mm() throws InterruptedException {
long starttime= System.currentTimeMillis();
System.out.println("start");
Thread.sleep(3000);
for (int i = 0; i <100 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
System.out.println("end");
long Endtime= System.currentTimeMillis();
System.out.println(Endtime-starttime);
}
//同步代碼塊 鎖的粒度細 併發效率高
public void mm2() throws InterruptedException {
System.out.println("start");
Thread.sleep(3000);
synchronized (this)//常量作爲當前對象
{
for (int i = 0; i <100 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
System.out.println("end");
}
}

在執行同步方法的時候,兩次線程調用每次都需要休眠三秒,而同步代碼塊同時啓動線程都先準備三秒,效率比較高

髒讀

public class Test06 {
public static void main(String[] args) throws InterruptedException {
User user=new User();
SubThread subThread=new SubThread(user);
subThread.start();
user.GetName();
}
static class SubThread extends Thread
{
public User user;

public SubThread(User user)
{
this.user=user;
}

@Override
public void run() {
user.SetValue("ww","456");
}
}
static class User
{
private String name="ylc";
private String pwd="123";
public void GetName()
{
System.out.println(Thread.currentThread().getName()+"==>"+name+"密碼"+pwd);
}
public void SetValue(String name,String pwd)
{
System.out.println("原來爲爲name="+this.name+",pwd="+this.pwd);
this.name=name;
this.pwd=pwd;
System.out.println("更新爲name="+name+",pwd="+pwd);
}

}
}


在修改數據還沒有完成的時候,就讀取到了原來的數據,而不是修改之後的

出現髒讀的原因是對共享數據的修改和讀取不同步引起的

解決辦法是對修改和讀取的方法進行同步方法上加上synchronized關鍵字


線程出現異常釋放鎖

假如在同步方法中,一個線程出現了異常,會不會沒有釋放鎖,其他在等待的線程就在一直等待,論證:

public class SynchronizedLock {
public static void main(String[] args) {
SynchronizedLock synchronizedLock=new SynchronizedLock();
new Thread(new RunnableThread())
{
@Override
public void run() {
synchronizedLock.mm();
}
}.start();
new Thread(new RunnableThread())
{
@Override
public void run() {
synchronizedLock.mm2();
}
}.start();
}
//同步實例方法
public synchronized void mm()
{
for (int i = 0; i <100 ; i++) {
if(i==50)
{
Integer.parseInt("abc");//異常設置
}
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
public void mm2()
{
synchronized (this)
{
for (int i = 0; i <100 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
}


同步過程中線程出現異常,會自動釋放鎖對象,以供下一個線程繼續執行

死鎖

多線程中可能需要使用多個鎖,如果獲取鎖的順序不一致,可能導致死鎖。

public class Text06_5 {
public static void main(String[] args) {
SubThread subThread=new SubThread();
SubThread subThread2=new SubThread();
subThread.setName("a"); subThread2.setName("b");
subThread.start();subThread2.start();
}
static class SubThread extends Thread
{
private static final Object lock1=new Object();
private static final Object lock2=new Object();

@Override
public void run() {
if("a".equals(Thread.currentThread().getName()))
{
synchronized (lock1)
{
System.out.println("a 線程 lock1獲得了鎖,再需要獲得lock2");
synchronized (lock2)
{
System.out.println("a 線程 lock2獲得了鎖");
}
}
}

if("b".equals(Thread.currentThread().getName()))
{
synchronized (lock2)
{
System.out.println("b 線程 lock2獲得了鎖,再需要獲得lock1");
synchronized (lock1)
{
System.out.println(" b 線程 lock1獲得了鎖");
}
}
}
}
}
}


程序還在運行,卻進入了卡死狀態,a線程得到了lock1,要想把該線程釋放的執行下面的代碼獲取lock2,而lock2被b線程獲取無法釋放,出現了鷸蚌相爭的情況。

避免死鎖:當需要獲得鎖時,所有線程獲得鎖的順序一致,a線程先鎖lock1,再鎖lock2,b線程同理,就不會出現死鎖了。

END

版權申明:內容來源網絡,版權歸原創者所有。除非無法確認,我們都會標明作者及出處,如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝。


如果你覺得文章不錯
記得給我「點贊」和「在看」哦~


本文分享自微信公衆號 - JAVA高級架構(gaojijiagou)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章