1. 線程安全性
1.1. 繼承方式VS實(shí)現(xiàn)方式(掌握)
當(dāng)多線程并發(fā)訪問同一個(gè)資源時(shí)纯衍,會(huì)導(dǎo)致線程出現(xiàn)安全性的原因蔗衡,看案例。
案例:現(xiàn)有50個(gè)蘋果蝙叛,現(xiàn)在有請(qǐng)三個(gè)童鞋(小A俺祠、小B、小C)上臺(tái)表演吃蘋果借帘。
因?yàn)锳蜘渣、B、C三個(gè)人可以同時(shí)吃蘋果肺然,此時(shí)得使用多線程技術(shù)來實(shí)現(xiàn)這個(gè)案例蔫缸。
分析: 可以定義三個(gè)線程對(duì)象,并啟動(dòng)線程.
第一步:每一個(gè)同學(xué)吃蘋果的時(shí)候:先展示自己拿到手上蘋果的編號(hào)际起,如1拾碌,2,3街望,36...
第二步:再吃掉蘋果(意味著蘋果的總數(shù)少一個(gè))
方式1:可以使用繼承Thread方式來實(shí)現(xiàn).
方式2:可以使用實(shí)現(xiàn)Runnable方式來實(shí)現(xiàn).
使用繼承方式
class Person extends Thread {
private int num = 50;//蘋果總數(shù)
public Person(String name) {
super(name);
}
public void run() {
for (int i = 0; i < 50; i++) {
if (num > 0) {
System.out.println(super.getName() + "吃了編號(hào)為:" + num-- + "的蘋果");
}
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
//創(chuàng)建三個(gè)線程倦沧,吃蘋果
new Person("小A").start();
new Person("小B").start();
new Person("小C").start();
}
}
使用繼承方式完成該案例的時(shí)候,會(huì)發(fā)現(xiàn)A它匕、B展融、C都各自吃了50個(gè)蘋果,為何?
使用實(shí)現(xiàn)方式
class Apple implements Runnable {
private int num = 50;//蘋果總數(shù)
public void run() {
for (int i = 0; i < 50; i++) {
if (num > 0) {
System.out.println(Thread.currentThread().getName()
+ "吃了編號(hào)為:" + num-- + "的蘋果");
}
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Apple a = new Apple();
//創(chuàng)建三個(gè)線程告希,吃蘋果
new Thread(a, "小A").start();
new Thread(a, "小B").start();
new Thread(a, "小C").start();
}
}
在使用實(shí)現(xiàn)方式的時(shí)候扑浸,我們發(fā)現(xiàn)A、B燕偶、C一共吃了50個(gè)蘋果喝噪,為何?
通過吃蘋果比賽指么,分析繼承方式和實(shí)現(xiàn)方式的區(qū)別:
繼承方式:
Java中類是單繼承的酝惧,如果繼承了Thread了,該類就不能再有其他的直接父類了伯诬。
從操作上分析晚唇,繼承方式更簡(jiǎn)單,獲取線程名字也簡(jiǎn)單盗似。
從多線程共享同一個(gè)資源上分析哩陕,繼承方式不能多個(gè)線程共享同一個(gè)資源。
實(shí)現(xiàn)方式:
Java中類可以多實(shí)現(xiàn)接口赫舒,此時(shí)該類還可以繼承其他類悍及,并且還可以實(shí)現(xiàn)其他接口(設(shè)計(jì)上,更優(yōu)雅)。
從操作上分析接癌,獲取線程名字也比較復(fù)雜心赶,得使用Thread.currentThread()來獲取當(dāng)前線程的引用。
從多線程共享同一個(gè)資源上分析缺猛,實(shí)現(xiàn)方式可以多線程共享同一個(gè)資源园担。
1.2. 線程同步(掌握)
當(dāng)多線程并發(fā)訪問同一個(gè)資源對(duì)象的時(shí)候,可能出現(xiàn)線程不安全的問題。
但是枯夜,分析打印的結(jié)果弯汰,有時(shí)候發(fā)現(xiàn)沒有問題:
意識(shí):看不到問題,不代表沒有問題湖雹,可能是我們經(jīng)驗(yàn)不夠咏闪,或者說問題出現(xiàn)的不夠明顯。
那么可以使用線程休眠來模擬網(wǎng)絡(luò)延遲摔吏,讓問題來得更明顯一些:
Thread.sleep(10);//當(dāng)前線程睡10毫秒,當(dāng)前線程休息著,讓其他線程去搶資源.
在程序中并不是使用Thread.sleep(10)之后程序才出現(xiàn)問題鸽嫂,而是使用之后,問題更明顯征讲,休眠的時(shí)間越久問題越明顯据某,一般用10或100即可,具體根據(jù)情況而定诗箍。
class Apple implements Runnable {
private int num = 50;//蘋果總數(shù)
public void run() {
try {
for (int i = 0; i < 50; i++) {
if (num > 0) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()
+ "吃了編號(hào)為:" + num-- + "的蘋果");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
分析運(yùn)行結(jié)果癣籽,為什么有錯(cuò)誤的結(jié)果。
在這里,打印蘋果的編號(hào)和蘋果數(shù)量的減少筷狼,應(yīng)該是一個(gè)原子操作瓶籽,也就說是一個(gè)不能分割的操作,兩個(gè)步驟之間不能被其他線程插一腳埂材。
第一步:System.out.println(Thread.currentThread().getName()
- "吃了編號(hào)為:" + num + "的蘋果");
第二步:num --;
解決方案:保證打印蘋果編號(hào)和蘋果總數(shù)減1操作塑顺,必須同步完成。
解決思路:A線程獲得同步鎖進(jìn)入操作的時(shí)候俏险,B和C線程只能在外等著严拒,A操作結(jié)束,釋放同步鎖竖独。A和B和C才有機(jī)會(huì)去搶同步鎖(誰獲得同步鎖裤唠,誰才能執(zhí)行代碼)。
通俗例子:A预鬓、B巧骚、C三個(gè)人去搶廁所的雅間赊颠,為了保證安全規(guī)定誰搶到了必須上鎖格二,把其他人排除外雅間外面。若A搶到了竣蹦,進(jìn)入后應(yīng)該立馬上鎖顶猜,B和C只能在外等著,當(dāng)A釋放鎖出來的時(shí)候痘括,A长窄、B、C又開始嘗試搶資源纲菌。
方式1:同步代碼塊
方式2:同步方法
1.2.1. 同步代碼塊(掌握)
同步代碼塊語法:
synchronized(同步鎖){
需要同步操作的代碼
}
同步鎖挠日,又稱之為同步監(jiān)聽對(duì)象/同步鎖/同步監(jiān)聽器/互斥鎖:
為了保證每個(gè)線程都能正常執(zhí)行原子操作,Java引入了線程同步機(jī)制翰舌。
對(duì)象的同步鎖只是一個(gè)概念嚣潜,可以想象為在對(duì)象上標(biāo)記了一個(gè)鎖。
Java程序允許使用任何對(duì)象作為同步監(jiān)聽對(duì)象椅贱,一般的懂算,我們把當(dāng)前并發(fā)訪問的共同資源作為同步監(jiān)聽對(duì)象,比如此時(shí)三個(gè)線程的共同資源Apple對(duì)象庇麦。
注意:在任何時(shí)候计技,最多允許一個(gè)線程擁有同步鎖,誰拿到鎖就執(zhí)行山橄,其他的線程只能在代碼塊外等著垮媒。
class Apple implements Runnable {
private int num = 50;//蘋果總數(shù)
public void run() {
try {
for (int i = 0; i < 50; i++) {
synchronized (this) {
if (num > 0) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()
+ "吃了編號(hào)為:" + num-- + "的蘋果");
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
此時(shí)的同步鎖this表示Apple對(duì)象,而程序中Apple對(duì)象只有一份,故可以作為同步鎖涣澡。
1.2.2. 同步方法(掌握)
使用synchronized修飾的方法贱呐,就叫做同步方法。保證A線程執(zhí)行該方法的時(shí)候入桂,其他線程只能在方法外等著奄薇。
synchronized public void doWork(){
///TODO
}
此時(shí)同步鎖是誰——其實(shí)就是,調(diào)用當(dāng)前同步方法的對(duì)象:
對(duì)于非static方法抗愁,同步鎖就是this馁蒂。
對(duì)于static方法,同步鎖就是當(dāng)前方法所在類的字節(jié)碼對(duì)象蜘腌。
class Apple implements Runnable {
private int num = 50;//蘋果總數(shù)
public void run() {
for (int i = 0; i < 50; i++) {
this.eat();
}
}
synchronized private void eat() {
try {
if (num > 0) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()
+ "吃了編號(hào)為:" + num-- + "的蘋果");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
1.2.3. synchronized的優(yōu)劣(掌握)
好處:保證了多線程并發(fā)訪問時(shí)的同步操作沫屡,避免線程的安全性問題。
缺點(diǎn):使用synchronized的方法/代碼塊的性能要低一些撮珠。
建議:盡量減小synchronized的作用域沮脖。
面試題:
1、StringBuilder和StringBuffer的區(qū)別
2芯急、說說ArrayList和Vector的區(qū)別
3勺届、HashMap和Hashtable的區(qū)別
通過源代碼會(huì)發(fā)現(xiàn),主要就是方法有沒有使用synchronized的區(qū)別娶耍,比如StringBuilder和StringBuffer免姿。
因此得出結(jié)論:使用synchronized修飾的方法性能較高,但是安全性較低榕酒,反之則反胚膊。
若要獲得最好的學(xué)習(xí)效果,需要配合對(duì)應(yīng)教學(xué)視頻一起學(xué)習(xí)想鹰。需要完整教學(xué)視頻紊婉,請(qǐng)參看https://ke.qq.com/course/272077。