Java高并發(fā)--線程安全策略
主要是學(xué)習(xí)慕課網(wǎng)實(shí)戰(zhàn)視頻《Java并發(fā)編程入門與高并發(fā)面試》的筆記
不可變對(duì)象
發(fā)布不可變對(duì)象可保證線程安全。
實(shí)現(xiàn)不可變對(duì)象有哪些要注意的地方坷牛?比如JDK中的String類腌逢。
- 不提供setter方法(包括修改字段、字段引用到的的對(duì)象等方法)
- 將所有字段設(shè)置為final怨绣、private
- 將類修飾為final迎罗,不允許子類繼承捧灰、重寫(xiě)方法钻注÷烨遥可以將構(gòu)造函數(shù)設(shè)為private,通過(guò)工廠方法創(chuàng)建幅恋。
- 如果類的字段是對(duì)可變對(duì)象的引用膘掰,不允許修改被引用對(duì)象。 1)不提供修改可變對(duì)象的方法佳遣;2)不共享對(duì)可變對(duì)象的引用识埋。對(duì)于外部傳入的可變對(duì)象,不保存該引用零渐。如要保存可以保存其復(fù)制后的副本窒舟;對(duì)于內(nèi)部可變對(duì)象,不要返回對(duì)象本身诵盼,而是返回其復(fù)制后的副本惠豺。
final關(guān)鍵字可以修飾在類、方法风宁、變量:
- 類:被修飾的類不能被繼承
- 方法:被修飾的方法不能被重寫(xiě)
- 變量:被修飾的是一個(gè)基本類型洁墙,其值不能被修改;被修飾的是一個(gè)對(duì)象引用戒财,這里的“不可變”指不允許其再指向其他對(duì)象热监,但是可以修改對(duì)象里面的值。
關(guān)于上一條中final修飾對(duì)象的引用饮寞。以ArrayList為例
final List<Integer> list= new ArrayList<>();
list.add(3);
list.add(4);
list = new ArrayList<>(); // 編譯時(shí)報(bào)錯(cuò)孝扛,不能再指向其他對(duì)象
list.set(0, 2); // 但是可以修改list里面的值
假如我們就是要求諸如List、Map一類的數(shù)據(jù)結(jié)構(gòu)也不能修改其中的元素呢幽崩?
JDK中Collections
的一些靜態(tài)方法提供了支持苦始,如下,舉一個(gè)List的例子
final List<Integer> list= new ArrayList<>();
list.add(3);
list.add(4);
List<Integer> unmodifiableList = Collections.unmodifiableList(list);
System.out.println(unmodifiableList);
unmodifiableList.add(5); // 運(yùn)行時(shí)異常
unmodifiableList.set(0, 2); // 運(yùn)行時(shí)異常
只需要將普通的list傳給Collections.unmodifiableList()
作為入?yún)⒓纯伞?/p>
其實(shí)現(xiàn)原理也很簡(jiǎn)單慌申,將普通list中的數(shù)據(jù)拷貝陌选,然后對(duì)于所有添加、修改的操作蹄溉,直接拋出異常即可咨油,這樣就保證了list不能修改其中的元素。
線程安全的問(wèn)題就是出在多個(gè)線程同時(shí)修改共享變量类缤,不可變對(duì)象的策略完全規(guī)避了對(duì)對(duì)象的修改臼勉,所以在多線程中使用也不會(huì)有任何問(wèn)題邻吭。
線程封閉
- 堆棧封閉:能使用局部變量的地方就不使用全局變量餐弱,多線程下訪問(wèn)同一個(gè)方法時(shí),方法中的局部變量都會(huì)拷貝一份到線程的棧中,也就是說(shuō)每一個(gè)線程中都有只屬于本線程的私有變量膏蚓,因此局部變量不會(huì)被多個(gè)線程共享瓢谢。
- ThreadLocal:特別好的線程封閉方法,其實(shí)現(xiàn)原理如下
對(duì)于共享變量驮瞧,一般采取同步的方式保證線程安全氓扛。而ThreadLocal是為每一個(gè)線程都提供了一個(gè)線程內(nèi)的局部變量,每個(gè)線程只能訪問(wèn)到屬于它的副本论笔。
下面是set和get的實(shí)現(xiàn)
// set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// 上面的getMap方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
從源碼中可以看出:每一個(gè)線程擁有一個(gè)ThreadLocalMap采郎,這個(gè)map存儲(chǔ)了該線程擁有的所有局部變量。
set時(shí)先通過(guò)Thread.currentThread()獲取當(dāng)前線程狂魔,進(jìn)而獲取到當(dāng)前線程的ThreadLocalMap蒜埋,然后以ThreadLocal自己為key,要存儲(chǔ)的對(duì)象為值最楷,存到當(dāng)前線程的ThreadLocalMap中整份。
get時(shí)也是先獲得當(dāng)前線程的ThreadLocalMap,以ThreadLocal自己為key籽孙,取出和該線程的局部變量烈评。
題話外,一個(gè)線程內(nèi)可以設(shè)置多個(gè)ThreadLocal犯建,這樣該線程就擁有了多個(gè)局部變量讲冠。比如當(dāng)前線程為t1,在t1內(nèi)創(chuàng)建了兩個(gè)ThreadLocal分別是tl1和tl2适瓦,那么t1的ThreadLocalMap就有兩個(gè)鍵值對(duì)沟启。
t1.threadLocals.set(tl1, obj1) // 等價(jià)于在t1線程中調(diào)用tl1.set(obj1)
t1.threadLocals.set(tl2, obj2) // 等價(jià)于在t1線程中調(diào)用tl2.set(obj1)
t1.threadLocals.getEntry(tl1) // 等價(jià)于在t1線程中調(diào)用tl1.get()獲得obj1
t1.threadLocals.getEntry(tl2) // 等價(jià)于在t1線程中調(diào)用tl2.get()獲得obj2
以一個(gè)角色驗(yàn)證的例子為例,為每一個(gè)請(qǐng)求(線程)保存了當(dāng)前訪問(wèn)人的角色犹菇。比如有g(shù)uest和admin德迹。
public class CurrentUserHolder {
private static final ThreadLocal<String> holder = new ThreadLocal<>();
public static void setUserHolder(String user) {
holder.set(user);
}
public static String getUserHolder() {
return holder.get();
}
}
在進(jìn)行某些敏感操作前,需要對(duì)當(dāng)前請(qǐng)求下的角色進(jìn)行驗(yàn)證揭芍。游客是沒(méi)有訪問(wèn)權(quán)限的胳搞,只有管理員可以。
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
@Component
public class AuthService {
public void checkAccess() {
// 通過(guò)ThreadLocal取得當(dāng)前線程(請(qǐng)求)中的角色
String user = CurrentUserHolder.getUserHolder();
if (!"admin".equals(user)) {
throw new RuntimeException("操作不被允許称杨!");
}
}
}
操作前的驗(yàn)證使用AOP過(guò)濾
import com.shy.aopdemo.security.AuthService;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AuthAspect {
@Autowired
private AuthService authService;
@Pointcut("execution(* com.shy.aopdemo.service.ProductService.*(..))")
public void adminOnly() {}
@Before("adminOnly()")
public void checkAccess() {
authService.checkAccess();
}
}
測(cè)試一下肌毅,如果當(dāng)前請(qǐng)求的角色是guest
import com.shy.aopdemo.security.CurrentUserHolder;
import com.shy.aopdemo.service.ProductService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class AopdemoApplicationTests {
@Autowired
private ProductService productService;
@Test
public void checkDeleteTest() {
// 當(dāng)前請(qǐng)求的角色是guest
CurrentUserHolder.setUserHolder("guest");
// AOP,在delete之前會(huì)先調(diào)用authService.checkAccess();結(jié)果驗(yàn)證不通過(guò)
productService.delete(1L);
}
}
總結(jié)
安全共享對(duì)象的策略
- 線程限制:一個(gè)被線程限制的對(duì)象姑原,由線程獨(dú)占悬而;只能由它的線程來(lái)修改,例如使用線程內(nèi)的局部變量锭汛、ThreadLocal等
- 共享只讀:只讀對(duì)象在沒(méi)有額外同步的情況下笨奠,可以被多個(gè)線程并發(fā)訪問(wèn)袭蝗,但是任何線程都無(wú)法修改它。例如般婆,使用不可變對(duì)象(final關(guān)鍵字修飾)
- 線程安全的對(duì)象:一個(gè)線程按安全的對(duì)象或容器到腥,通過(guò)內(nèi)部的同步機(jī)制來(lái)保證線程安全。比如StringBuffer蔚袍、ConcurrentHashMap乡范、AtomicInteger等線程安全對(duì)象。