高并發(fā)編程系列(二)
High concurrency programming series
程序在執(zhí)行過程中,如果出現(xiàn)異常,默認狀況鎖會被釋放
所以,在并發(fā)處理過程中,有異常要多加小心,不然會發(fā)生不一致的情況,
比如,在一個web app處理過程中,多個servlet線程共同訪問同一個資源,這時如果異常處理不適合,
在第一個線程中拋出異常,其他線程進入同步代碼區(qū),有可能訪問到異常產(chǎn)生的數(shù)據(jù).
因此要非常小心的處理同步業(yè)務邏輯中的異常
此案例 t1 鎖被釋放 t2 方可開始
若你不想釋放鎖請你加入try{}catch{}
public class Tj {
int count = 0;
synchronized void m() {
System.out.println(Thread.currentThread().getName() + "start");
while (true) {
count ++;
System.out.println(Thread.currentThread().getName() + "count: " + count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5) {
//此處拋出異常,鎖將被釋放,要想不釋放就在此處進行catch,然后循環(huán)繼續(xù).
int i = 1/0;
}
}
}
public static void main(String[] args) {
Tj t = new Tj();
Runnable r = new Runnable() {
@Override
public void run() {
t.m();
}
};
new Thread(r,"t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r,"t2").start();
}
}
volatile 關(guān)鍵字,使一個變量在多個線程之間可見
A B 線程都用到一個變量,java默認是A線程中保留一份copy,這樣如果B線程修改了該變量,則A線程未必知道.
使用volatile 關(guān)鍵字,會讓所有線程都會讀到變量值的修改.
在下面代碼中,running是存在于堆內(nèi)存的t對象中,
當線程t1開始運行的時候,會把running的值從內(nèi)存中讀到t1線程的工作區(qū),在運行過程中,直接使用copy,
并不是每次都去使用colatile,將會強制所有線程去堆內(nèi)存中讀取running的值
olatile 并不能保證多個線程共同修改running變量時所帶來的一直問題,也就說volatile不能代替synchronized
public class Tk {
//對比一下有無volatitle ,整個程序運行結(jié)果的區(qū)別
/*volatile*/ boolean running = true;
void m(){
System.out.println("m statr");
while (running) {
}
System.out.println("m end");
}
public static void main(String[] args) {
Tk t = new Tk();
new Thread(t::m,"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.running = false;
}
}
volatile 并不能保證多個線程共同修改running 變量 時所帶來的不一致問題,也就是說volatile 不能代替synchronized
運行下面的程序,分析結(jié)果
創(chuàng)建10個線程
volatile 和 synchronized 的區(qū)別
volatile 保證可見性并不保證原子性;synchronized 既保證可見性又保證原子性;
synchronized的效率要比volatile低不少 面試必出
public class Tl {
volatile int count = 10;
void m() {
for (int i=0; i<10000;i++) count++;
}
public static void main(String[] args) {
Tl t = new Tl();
List<Thread> threads = new ArrayList<Thread>();
for (int i=0; i<10; i++) {
threads.add(new Thread(t::m, "thread" + i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
解決同樣的問題的更高效的方法,使用AtomXXX類.原子類
AtmXXX類本身方法都是原子性的,但是不能保證多個方法調(diào)用是原子性的.
public class Tm {
/*volatile int count = 0;*/
AtomicInteger count = new AtomicInteger(0);
synchronized void m(){
for (int i=0; i<10000; i++)
//if(count.get()<1000) 注意這里 如果未加鎖,之間還會有其他線程插進來
count.incrementAndGet(); //count++
}
public static void main(String[] args) {
Tm t = new Tm();
List<Thread> threads = new ArrayList<Thread>();
for (int i=0; i<10; i++) {
threads.add(new Thread(t::m,"thread" + i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
volatile 關(guān)鍵字,使一個變量在多個線程之間可見
A B 線程都用到一個變量,java默認是A線程中保留一份copy,這樣如果B線程修改了該變量,則A線程未必知道.
使用volatile 關(guān)鍵字,會讓所有線程都會讀到變量值的修改.
在下面代碼中,running是存在于堆內(nèi)存的t對象中,
當線程t1開始運行的時候,會把running的值從內(nèi)存中讀到t1線程的工作區(qū),在運行過程中,直接使用copy,
并不是每次都去使用colatile,將會強制所有線程去堆內(nèi)存中讀取running的值
volatile 并不能保證多個線程共同修改running變量時所帶來的一直問題,也就說volatile不能代替synchronized
public class Tk {
//對比一下有無volatitle ,整個程序運行結(jié)果的區(qū)別
/*volatile*/ boolean running = true;
void m(){
System.out.println("m statr");
while (running) {
}
System.out.println("m end");
}
public static void main(String[] args) {
Tk t = new Tk();
new Thread(t::m,"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.running = false;
}
}
不加volatile
添加volatile
volatile 并不能保證多個線程共同修改running 變量 時所帶來的不一致問題,也就是說volatile 不能代替synchronized
運行下面的程序,分析結(jié)果
創(chuàng)建10個線程
volatile 和 synchronized 的區(qū)別
volatile 保證可見性并不保證原子性;synchronized 既保證可見性又保證原子性;
synchronized的效率要比volatile低不少 面試必出
public class Tl {
volatile int count = 10;
void m() {
for (int i=0; i<10000;i++) count++;
}
public static void main(String[] args) {
Tl t = new Tl();
List<Thread> threads = new ArrayList<Thread>();
for (int i=0; i<10; i++) {
threads.add(new Thread(t::m, "thread" + i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
解決同樣的問題的更高效的方法,使用AtomXXX類.原子類
AtmXXX類本身方法都是原子性的,但是不能保證多個方法調(diào)用是原子性的.
public class Tm {
/*volatile int count = 0;*/
AtomicInteger count = new AtomicInteger(0);
synchronized void m(){
for (int i=0; i<10000; i++)
//if(count.get()<1000) 如果未加鎖,之間還會有其他線程插進來
count.incrementAndGet(); //count++
}
public static void main(String[] args) {
Tm t = new Tm();
List<Thread> threads = new ArrayList<Thread>();
for (int i=0; i<10; i++) {
threads.add(new Thread(t::m,"thread" + i));
}
threads.forEach((o) -> o.start());
threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
synchonized優(yōu)化.
同步代碼塊中的語句越少越好.
比較m1和m2
public class Tn {
int count = 0;
synchronized void m1() {
//do sth need sync
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//業(yè)務邏輯中只有下面這句需要synchronized,這時不應該給整個方法上都上鎖
count++;
//do sth need not sync
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
void m2() {
//do sth need not sync
try{
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//業(yè)務邏輯中只有下面這句需要sync 時不應該給整個方法上鎖
//采用細粒度的鎖,可以使線程爭用時間變短,從而提高效率 細粒度鎖要比粗粒度鎖效率要高
synchronized (this) {
count++;
}
//do sth need not sync
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//do sth not sync
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
鎖定某個對象o,如果o的屬性發(fā)生改變,不影響使用.
但是如果o變成另外一個對象,則鎖定的對象發(fā)生改變.
應該避免將鎖定對象的引用變成另外對象
public class To {
Object o = new Object();
void m() {
synchronized (o) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
public static void main(String[] args) {
To t = new To();
//啟動線程
new Thread(t::m, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//創(chuàng)建第二個線程
Thread t2 = new Thread(t::m, "t2");
//鎖定對象發(fā)生變化,所以t2線程得以進行,如注釋掉這句話,線程2將永遠得不到執(zhí)行機會
//鎖是鎖在堆內(nèi)存 不是鎖在棧內(nèi)存
t.o = new Object();
t2.start();
}
}
不要以字符串常量作為鎖定對象
在下面m1 m2 其實鎖定的是同一個對象
這種情況下還會發(fā)生比較詭異的現(xiàn)象,比如你用到了一個類庫,在該類庫中的代碼鎖定了"Hello",
但是你都不到源碼,所以你在自己的代碼中鎖定了"Hello",這時候有可能發(fā)生非常詭異的死鎖阻塞,
因為你的程序和你用到的類庫不經(jīng)意間使用了同一把鎖.
public class Tp {
String s1 = "Hello";
String s2 = "Hello";
void m1() {
synchronized (s1) {
}
}
void m2() {
synchronized (s2) {
}
}
}
曾經(jīng)的面試題
實現(xiàn)一個容器,提供兩個方案 add size
寫兩個線程,線程添加十個元素到容器中,線程2實現(xiàn)監(jiān)控元素個數(shù),當個數(shù)到5個時,線程2給出提示并結(jié)束.
但是,t2線程死循環(huán)很浪費cpu,如果不用死循環(huán),該怎么做呢?
public class Tq {
//添加volatile 使t2能夠得到通知
volatile List lists = new ArrayList();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
Tq t = new Tq();
new Thread(()-> {
for (int i=0; i<10; i++) {
t.add(new Object());
System.out.println("add" + i);
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()-> {
while (true) {
if (t.size() == 5) {
break;
}
}
System.out.println("t2結(jié)束");
},"t2").start();
}
}
這里使用wait 和notify做到,wait會釋放鎖,而notify不會釋放鎖
需要注意的是這種方法必須保證t2先執(zhí)行,也就是讓t2監(jiān)聽才可以.
閱讀下面的程序踏施,并分析輸出結(jié)果.
可以讀到輸出結(jié)果并不是size=5 t2退出,而是t1結(jié)束時t2才可以接收到通知推出
思考為什么
wait 是調(diào)用被鎖定對象的wait方法 notify
public class Ts {
volatile List lists = new ArrayList();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
Ts t = new Ts();
final Object lock = new Object();
new Thread(() -> {
synchronized(lock) {
System.out.println("t2啟動");
if (t.size() != 5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2結(jié)束");
}
},"t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
System.out.println("t1啟動");
synchronized(lock) {
for (int i=0; i<10; i++) {
t.add(new Object());
System.out.println("add" + i);
if (t.size() == 5) {
lock.notify();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"t1").start();
}
}
這里是當size=5時,t1線程等,釋放鎖同時叫醒t2,t2執(zhí)行,t2執(zhí)行結(jié)束調(diào)用notify叫醒t1。
public class Tt {
volatile List lists = new ArrayList();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
Ts t = new Ts();
final Object lock = new Object();
new Thread(() -> {
synchronized(lock) {
System.out.println("t2啟動");
if (t.size() != 5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2結(jié)束");
//通知t1繼續(xù)執(zhí)行·
lock.notify();
}
},"t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
System.out.println("t1啟動");
synchronized(lock) {
for (int i=0; i<10; i++) {
t.add(new Object());
System.out.println("add" + i);
if (t.size() == 5) {
lock.notify();
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"t1").start();
}
}