線程安全問題
當多個線程同時共享同一個全局變量或靜態(tài)變量,做寫操作時卫玖,可能會發(fā)生數(shù)據(jù)沖突問題公你,也就是線程安全問題。但是做讀操作是不會發(fā)生數(shù)據(jù)沖突問題假瞬。
例子:現(xiàn)在有100張火車票陕靠,有兩個窗口同時搶火車票,請使用多線程擬搶票效果脱茉。
class Demo2 implements Runnable {
private int count = 100;
private static Object oj = new Object();
@Override
public void run() {
while (count > 0) {
try {
Thread.sleep(50);
} catch (Exception e) {
// TODO: handle exception
}
sale();
}
}
public void sale() {
// 前提 多線程進行使用剪芥、多個線程只能拿到一把鎖。
// 保證只能讓一個線程 在執(zhí)行 缺點效率降低
// synchronized (oj) {
// if (count > 0) {
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
count--;
// }
// }
}
public static void main(String[] args) {
Demo2 threadTrain1 = new Demo2();
Thread t1 = new Thread(threadTrain1, "①號窗口");
Thread t2 = new Thread(threadTrain1, "②號窗口");
t1.start();
t2.start();
}
}
運行結(jié)果:
結(jié)果發(fā)現(xiàn)琴许,多個線程共享同一個全局成員變量時税肪,做寫的操作可能會發(fā)生數(shù)據(jù)沖突問題。
線程安全解決辦法
1.如何解決多線程之間的線程安全問題榜田?
使用多線程之間同步synchronized或使用鎖(lock)益兄。
2.為什么使用線程同步或使用鎖能解決線程安全問題?
將可能會發(fā)生數(shù)據(jù)沖突問題(線程不安全問題)箭券,只能讓當前一個線程進行執(zhí)行净捅。代碼執(zhí)行完成后釋放鎖,然后才能讓其他線程進行執(zhí)行辩块。這樣的話就可以解決線程不安全問題蛔六。
3.什么是多線程之間同步荆永?
當多個線程共享一個資源,不會受到其他線程的干擾国章。
同步代碼塊
同步代碼塊就是將可能會發(fā)生線程安全問題的代碼具钥,給包括起來。
synchronized(同一個數(shù)據(jù)){
可能會發(fā)生線程沖突問題
}
synchronized(對象){//這個對象可以為任意對象
需要被同步的代碼
}
對象如同鎖液兽,持有鎖的線程可以在同步中執(zhí)行骂删,沒持有鎖的線程即使獲取CPU的執(zhí)行權(quán)也進不去。
同步的前提:
1.必須要有兩個或者兩個以上的線程抵碟。
2.必須是多個線程使用同一個鎖桃漾,保證同步中只能有一個線程在運行坏匪。
好處:解決了多線程的安全問題拟逮。
弊端:多個線程需要判斷所,比較小號資源适滓、搶鎖的資源敦迄。
public class Demo1 implements Runnable{
private int count =100;
private static Object obj = new Object();
@Override
public void run() {
while(count>0) {
try {
Thread.sleep(50);
} catch (Exception e) {
// TODO: handle exception
}
sale();
}
}
public void sale() {
synchronized (obj) {
if(count>0) {
System.out.println(Thread.currentThread().getName()+",出售第"+(100-count+1)+"票");
count--;
}
}
}
public static void main(String[] args) {
Demo1 threadTrain = new Demo1();
Thread t1 = new Thread(threadTrain,"1號窗口");
Thread t2 = new Thread(threadTrain,"2號窗口");
t1.start();
t2.start();
}
}
同步函數(shù)函數(shù)this鎖凭迹。
public synchronized void sale(){
if(count>0){
try{
Thread.sleep(40);
}catch(Exception e){
}
System.out.prinln(.....);
count--;
}
}
靜態(tài)同步函數(shù)
方法上加上static關(guān)鍵字罚屋,使用synchronized關(guān)鍵字修飾或者使用類.class文件。
靜態(tài)的同步函數(shù)使用的鎖是該函數(shù)所屬字節(jié)碼文件對象嗅绸。
可以用getClass方法獲取脾猛,也可以用當前類名.class表示。
synchronized(demo1.class){
System.....;
count--;
.....
}
總結(jié):synchronized修飾方法使用鎖是當前this鎖鱼鸠。
synchronized修飾靜態(tài)方法使用鎖是當前類的字節(jié)碼文件猛拴。
多線程死鎖
同步中嵌套同步,導(dǎo)致鎖無法釋放會導(dǎo)致死鎖
多線程有三大特性
原子性蚀狰、可見性愉昆、有序性
原子性
一個操作或多個操作 要么全部執(zhí)行并且執(zhí)行的過程中不會被任何因素打斷,要么就都不執(zhí)行麻蹋。
一個很經(jīng)典的例子就是銀行賬戶轉(zhuǎn)賬問題:
比如從賬戶A向賬戶B轉(zhuǎn)1000元跛溉,那么必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元扮授。這2個操作具備原子性才能保證不出現(xiàn)一些意外的問題芳室。
我們操作數(shù)據(jù)也是如此,比如i=i+1;其中就包括刹勃,讀取i的值堪侯,計算i,寫入i深夯。這行代碼在Java中是不具備原子性的抖格,多線程運行肯定會出問題诺苹,所以也需要我們使用同步和lock這些東西來確保這個特性了。
原子性其實就是保證數(shù)據(jù)一致雹拄、線程安全一部分收奔。
可見性
當多個線程訪問同一個變量時,一個線程修改了這個變量的值滓玖,其他線程能夠立即看到修改的值坪哄。
若兩個線程在不同的CPU,那么線程1改變了i的值還沒刷新到主存势篡,線程2又使用了i翩肌,那么這個i值肯定還是之前的,線程1對變量的修改線程沒看到這就是可見性禁悠。
從上圖來看念祭,線程A與線程B之間如果要通信的話,必須要經(jīng)歷下面2個步驟:
1.線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去碍侦。
2.線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量粱坤。
如上圖所示,本地內(nèi)存A和B有主內(nèi)存中共享變量x的副本瓷产。假設(shè)初始時站玄,這三個內(nèi)存中的x都為0,線程A在執(zhí)行時濒旦,把更新后的x(假設(shè)值為1)臨時存放在自己的本地內(nèi)存A中株旷。當線程A和線程B需要通信時,線程A首先會把自己本地內(nèi)存中修改后的x值刷新到主內(nèi)存中尔邓,此時主內(nèi)存中的x值變?yōu)榱?晾剖。隨后,線程B到主內(nèi)存中去讀取線程A更新后的x值铃拇,此時線程B的本地內(nèi)存的x值也變成1钞瀑。
從整體來看,這2個步驟是指上是線程A在向線程B發(fā)送消息慷荔,而且這個通信過程必須要經(jīng)過主內(nèi)存雕什。JMM通過控制主內(nèi)存與每個線程的本地內(nèi)存之間的交互,來為java程序要提供內(nèi)存可見性保證显晶。
總結(jié):jmm就是java內(nèi)存模型,定義了一個線程對另一個線程可見贷岸。共享變量存放在主內(nèi)存中,每個線程都有自己的本地內(nèi)存磷雇,當多個線程同時訪問一個數(shù)據(jù)的時候偿警,可能本地內(nèi)存沒有及時刷新到主內(nèi)存,所以就會發(fā)生線程安全問題唯笙。
有序性
程序執(zhí)行的順序按照代碼的先后順序執(zhí)行螟蒸。
一般來說處理器為了提高程序運行效率盒使,可能會對輸入代碼進行優(yōu)化,它不保證程序中各個語句的執(zhí)行先后順序同代碼中的順序一致七嫌,但是它會保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一直的少办。如下:
int a = 1;//語句1
int r = 2;//語句2
a = a+3; //語句3
r = a*a;//語句4
則因為重排序,它還可能執(zhí)行順序為2-1-3-4诵原,1-3-2-4
但絕不可能2-1-4-3英妓,因為這打破了依賴關(guān)系。
顯然重排序?qū)尉€程運行時不會有任何問題绍赛,而多線程就不一定了蔓纠,所以我們在多線程變成時就得考慮這個問題了。
Volatile
volatile關(guān)鍵字的作用是變量在多個線程之間可見
class Demo extends Thread{
public boolean flag = true;
@Override
public void run() {
System.out.println("開始執(zhí)行子線程");
while(flag) {
}
System.out.println("線程停止");
}
public void setRuning(boolean flag) {
this.flag = flag;
}
}
public class Demo1{
public static void main(String[] args) throws InterruptedException {
Demo demo = new Demo();
demo.start();
Thread.sleep(3000);
demo.setRuning(false);
System.out.println("flag已經(jīng)設(shè)置成false");
Thread.sleep(1000);
System.out.println(demo.flag);
}
}
在沒有設(shè)置延遲的時候flag改為false后線程能夠順利結(jié)束吗蚌,但是加入延遲后程序一直在運行沒有結(jié)束腿倚,原因是線程之間是不可見的,讀取的是副本褪测,沒有及時讀取到主內(nèi)存的結(jié)果猴誊。
解決辦法就是在變量flag之前添加關(guān)鍵字volatile解決線程之間的可見性,強制線程每次讀取該值的時候都去主內(nèi)存中取值侮措。
volatile非原子性
public class VolatileNoAtomic extends Thread{
public static volatile int count;
private static void addCount() {
for (int i = 0; i < 1000; i++) {
count++;
}
System.out.println(count);
}
public void run() {
addCount();
}
// public class Demo1{
public static void main(String[] args) {
VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
for (int i = 0; i < 10; i++) {
arr[i] = new VolatileNoAtomic();
}
for (int i = 0; i < 10; i++) {
arr[i].start();
}
}
}
多運行幾次會發(fā)現(xiàn)最大值有小概率不是10000,數(shù)據(jù)不同步乖杠,說明volatile不具備原子性分扎。
AtomicInteger是一個提供院子操作的Integer類,通過線程安全的方式操作加減胧洒。
public class VolatileNoAtomic extends Thread{
// public static volatile int count=0;
private static AtomicInteger atomicInteger = new AtomicInteger(0);
private static void addCount() {
for (int i = 0; i < 1000; i++) {
//等同于i++
atomicInteger.incrementAndGet();
}
System.out.println(atomicInteger.get());
}
public void run() {
addCount();
}
// public class Demo1{
public static void main(String[] args) {
VolatileNoAtomic[] arr = new VolatileNoAtomic[10];
for (int i = 0; i < 10; i++) {
arr[i] = new VolatileNoAtomic();
}
for (int i = 0; i < arr.length; i++) {
arr[i].start();
}
}
}
volatile與synchronized區(qū)別
僅僅靠volatile不能保證線程的安全性畏吓。
1.volatile輕量級,只能修飾變量卫漫。synchronized重量級菲饼,還可以修飾方法。
2.volatile只能保證數(shù)據(jù)的可見性列赎,不能用來同步宏悦,因為多個線程并發(fā)訪問volatile修飾的變量不會阻塞。
synchronized不僅保證可見性包吝,而且還保證原子性饼煞。因為,只有獲得了鎖的線程才能進入臨界區(qū)诗越,從而保證臨界區(qū)中的所有語句都全部執(zhí)行砖瞧。多個線程爭搶synchronized鎖對象時,會出現(xiàn)阻塞嚷狞。
線程安全性
線程安全性包括兩個方面1.可見性块促。2.原子性荣堰。
從上面自增的例子中可以看出,僅僅使用volatile不能保證線程安全性竭翠。而synchronized則可實現(xiàn)線程的安全性持隧。