java內存模型(jmm)
java內存模型分爲兩大類型即,主內存和本地內存
1.主內存
也就是主進程所佔用的內存
2.本地內存
線程中開闢的屬於線程自己的內存,其中存放着全局變量的副本數據
也就是全局共享的數據在主內存中的,線程中複製一份放入自己的本地內存,線程執行結束後將其變動刷新至主內存。
這也就是爲什麼多線程會有線程安全的問題所在。
多個線程同時做了修改,都去刷新主內存,會造成結果和實際不一致。
volatile關鍵字
其功能爲多線程可見。
怎麼說呢?實際上就是當線程在本地內存中修改了被volatile修改的變量後,會立刻將其值刷新至主內存中,其他線程
也會馬上得到這個修改的結果
實際看上去就像線程在直接修改主線程中的這個變量一樣。
還有就是防止指令重新排序
代碼自上而下執行,這是常識,但是,計算機執行時,可能會將,沒有依賴關係的代碼指令的執行順序打亂,但一般不會
影響結果。
如:int a=1;int b=1;這時無論怎麼變化這兩條指令,都沒有影響。
如果再來一條int c=a+b;就會相互之間有了依賴關係,計算機就不會將其重新排序
指令並不是代碼行,指令是原子的,通過javap命令可以看到一行代碼編譯出來的指令,當然,像int i=1;這樣的代碼行也
是原子操作。
而對於那些比較隱晦的,指令重拍可能會引發問題得數據,volatile關鍵字可以指定其對應的代碼不會參與重新排序
案例
package live.yanxiaohui;
/**
* @Description 仿真消息的生產和消費(多線程之間的通訊),即來一個消息,消費一條消息
* @CSDN https://blog.csdn.net/yxh13521338301
* @Author: yanxh<br>
* @Date 2020-05-15 11:08<br>
* @Version 1.0<br>
*/
public class Test2 {
public static void main(String[] args) {
Money money = new Money();
new Task1(money).start();
new Task2(money).start();
}
}
class Task1 extends Thread{
private Money money;
private int count;
public Task1(Money money) {
this.money = money;
}
@Override
public void run() {
while (true){
if(count % 2 ==0){
money.name = "馬雲";
money.componet = "阿里";
}else {
money.name = "馬化騰";
money.componet = "騰訊";
}
count++;
}
}
}
class Task2 extends Thread{
private Money money;
public Task2(Money money) {
this.money = money;
}
@Override
public void run() {
while (true){
System.out.println(money.name + "," + money.componet);
}
}
}
class Money{
public String name;
public String componet;
}
首先我們運行程序,會發現數據錯亂的現象
這是由於多個線程訪問全局共享數據造成的線程安全問題。
說人話:
寫入的線程做了修改,將本地內存改動的數據要同步至主內存,剛好同步一半,這時被掛起,
讀線程從主內存讀取數據就是一個錯誤的數據。
那麼加鎖如何?
對共享的money對象加鎖。關鍵代碼如下
寫入操作:
synchronized (money){
if(count % 2 ==0){
money.name = "馬雲";
money.componet = "阿里";
}else {
money.name = "馬化騰";
money.componet = "騰訊";
}
}
讀操作:
synchronized (money){
System.out.println(money.name + "," + money.componet);
}
一定會有讀者有疑問:讀爲什麼還要加鎖?
此處對數據的修改和讀取是兩個變量,這兩個變量需要保證其原子性,否則依舊是可能出現線程安全的
比如:如果讀不加鎖,那麼其讀到第一個屬性值後,被掛起,寫入的線程做了修改,讀線程繼續執行後
就會出現第二個屬性和第一個屬性不同步的現象
運行程序
數據是保證一致性了,但依然不滿足我們消費的機制,所以需要再優化下
我們可以使用wait和notify配合進行多個線程之間對同一共享的全局數據進行通訊
package live.yanxiaohui;
/**
* @Description 仿真消息的生產和消費(多線程之間的通訊),即來一個消息,消費一條消息
* @CSDN https://blog.csdn.net/yxh13521338301
* @Author: yanxh<br>
* @Date 2020-05-15 11:08<br>
* @Version 1.0<br>
*/
public class Test2 {
public static void main(String[] args) {
Money money = new Money();
new Task1(money).start();
new Task2(money).start();
}
}
class Task1 extends Thread{
private Money money;
private int count;
public Task1(Money money) {
this.money = money;
}
@Override
public void run() {
try{
while (true){
synchronized (money){
// 方便查看效果,設置阻塞時間
Thread.sleep(100);
if(!money.success){
// 交出對象的使用權,等待消息消費
money.wait();
}
if(count % 2 ==0){
money.name = "馬雲";
money.componet = "阿里";
}else {
money.name = "馬化騰";
money.componet = "騰訊";
}
money.success = false;
// 喚醒此對象所有等待的線程
money.notify();
}
count++;
}
} catch (Exception e){
e.printStackTrace();
}
}
}
class Task2 extends Thread{
private Money money;
public Task2(Money money) {
this.money = money;
}
@Override
public void run() {
try {
while (true){
synchronized (money){
if(money.success){
// 交出對象的使用權,等待消息生產
money.wait();
}
System.out.println(money.name + "," + money.componet);
money.success = true;
// 喚醒此對象所有等待的線程
money.notify();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Money{
public String name;
public String componet;
// true表示可以寫,false表示可以讀
public boolean success;
}
由此可以看出,線程執行是搶奪CPU分配權的,和主線程代碼的執行順序沒太大關係
我們可以使用lock鎖,去替代sync
package live.yanxiaohui;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Description 仿真消息的生產和消費(多線程之間的通訊),即來一個消息,消費一條消息
* @CSDN https://blog.csdn.net/yxh13521338301
* @Author: yanxh<br>
* @Date 2020-05-15 11:08<br>
* @Version 1.0<br>
*/
public class Test3 {
public static void main(String[] args) {
Money money = new Money();
new Task1(money).start();
new Task2(money).start();
}
}
class Task1 extends Thread {
private Money money;
private int count;
public Task1(Money money) {
this.money = money;
}
@Override
public void run() {
while (true) {
try {
// 對象上鎖
money.lock.lock();
// 方便查看效果,設置阻塞時間
Thread.sleep(100);
if (!money.success) {
// 交出對象的使用權,等待消息消費
money.condition.await();
}
if (count % 2 == 0) {
money.name = "馬雲";
money.componet = "阿里";
} else {
money.name = "馬化騰";
money.componet = "騰訊";
}
money.success = false;
// 喚醒此對象所有等待的線程
money.condition.signal();
count++;
} catch (Exception e) {
e.printStackTrace();
} finally {
// 釋放鎖
money.lock.unlock();
}
}
}
}
class Task2 extends Thread {
private Money money;
public Task2(Money money) {
this.money = money;
}
@Override
public void run() {
try {
while (true) {
// 對象上鎖
money.lock.lock();
if (money.success) {
// 交出對象的使用權,等待消息生產
money.condition.await();
}
System.out.println(money.name + "," + money.componet);
money.success = true;
// 喚醒此對象所有等待的線程
money.condition.signal();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 釋放鎖
money.lock.unlock();
}
}
}
class Money {
public String name;
public String componet;
// true表示可以寫,false表示可以讀
public boolean success;
public Lock lock = new ReentrantLock();
public Condition condition = lock.newCondition();
}
運行結果也是相同
總結
1.多線程之間實現通訊基於對全局共享數據的鎖定,即多個線程都必須去爭奪鎖,獲取鎖之後依照業務進行對應的等待或運行。
2.wait和notify必須要配合synchronized使用, 且必須要在同步代碼塊中執行
3.lock鎖因爲是手動添加的,所以它可控,實際開發中可用到的最多
4.wait底層是將對象的鎖釋放掉,自身線程進入阻塞等待狀態,直到被對象的notify喚醒
5.sleep只是線程的定時阻塞,不會釋放鎖
6.volatile指定全局共享變量在內存模型中線程間數據的可見性
7.volatile還可以防止指令重新排序