Java高并發(fā)--線程安全策略
主要是學(xué)習(xí)慕課網(wǎng)實戰(zhàn)視頻《Java并發(fā)編程入門與高并發(fā)面試》的筆記
不可變對象
發(fā)布不可變對象可保證線程安全。
實現(xiàn)不可變對象有哪些要注意的地方?比如JDK中的String類。
- 不提供setter方法(包括修改字段、字段引用到的的對象等方法)
- 將所有字段設(shè)置為final、private
- 將類修飾為final,不允許子類繼承、重寫方法。可以將構(gòu)造函數(shù)設(shè)為private,通過工廠方法創(chuàng)建。
- 如果類的字段是對可變對象的引用,不允許修改被引用對象。 1)不提供修改可變對象的方法;2)不共享對可變對象的引用。對于外部傳入的可變對象,不保存該引用。如要保存可以保存其復(fù)制后的副本;對于內(nèi)部可變對象,不要返回對象本身,而是返回其復(fù)制后的副本。
final關(guān)鍵字可以修飾在類、方法、變量:
- 類:被修飾的類不能被繼承
- 方法:被修飾的方法不能被重寫
- 變量:被修飾的是一個基本類型,其值不能被修改;被修飾的是一個對象引用,這里的“不可變”指不允許其再指向其他對象,但是可以修改對象里面的值。
關(guān)于上一條中final修飾對象的引用。以ArrayList為例
final List<Integer> list= new ArrayList<>();
list.add(3);
list.add(4);
list = new ArrayList<>(); // 編譯時報錯,不能再指向其他對象
list.set(0, 2); // 但是可以修改list里面的值
假如我們就是要求諸如List、Map一類的數(shù)據(jù)結(jié)構(gòu)也不能修改其中的元素呢?
JDK中Collections的一些靜態(tài)方法提供了支持,如下,舉一個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); // 運行時異常
unmodifiableList.set(0, 2); // 運行時異常
只需要將普通的list傳給Collections.unmodifiableList()作為入?yún)⒓纯伞?/p>
其實現(xiàn)原理也很簡單,將普通list中的數(shù)據(jù)拷貝,然后對于所有添加、修改的操作,直接拋出異常即可,這樣就保證了list不能修改其中的元素。
線程安全的問題就是出在多個線程同時修改共享變量,不可變對象的策略完全規(guī)避了對對象的修改,所以在多線程中使用也不會有任何問題。
線程封閉
- 堆棧封閉:能使用局部變量的地方就不使用全局變量,多線程下訪問同一個方法時,方法中的局部變量都會拷貝一份到線程的棧中,也就是說每一個線程中都有只屬于本線程的私有變量,因此局部變量不會被多個線程共享。
- ThreadLocal:特別好的線程封閉方法,其實現(xiàn)原理如下
對于共享變量,一般采取同步的方式保證線程安全。而ThreadLocal是為每一個線程都提供了一個線程內(nèi)的局部變量,每個線程只能訪問到屬于它的副本。
下面是set和get的實現(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();
}
從源碼中可以看出:每一個線程擁有一個ThreadLocalMap,這個map存儲了該線程擁有的所有局部變量。
set時先通過Thread.currentThread()獲取當(dāng)前線程,進而獲取到當(dāng)前線程的ThreadLocalMap,然后以ThreadLocal自己為key,要存儲的對象為值,存到當(dāng)前線程的ThreadLocalMap中。
get時也是先獲得當(dāng)前線程的ThreadLocalMap,以ThreadLocal自己為key,取出和該線程的局部變量。
題話外,一個線程內(nèi)可以設(shè)置多個ThreadLocal,這樣該線程就擁有了多個局部變量。比如當(dāng)前線程為t1,在t1內(nèi)創(chuàng)建了兩個ThreadLocal分別是tl1和tl2,那么t1的ThreadLocalMap就有兩個鍵值對。
t1.threadLocals.set(tl1, obj1) // 等價于在t1線程中調(diào)用tl1.set(obj1)
t1.threadLocals.set(tl2, obj2) // 等價于在t1線程中調(diào)用tl2.set(obj1)
t1.threadLocals.getEntry(tl1) // 等價于在t1線程中調(diào)用tl1.get()獲得obj1
t1.threadLocals.getEntry(tl2) // 等價于在t1線程中調(diào)用tl2.get()獲得obj2
以一個角色驗證的例子為例,為每一個請求(線程)保存了當(dāng)前訪問人的角色。比如有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();
}
}
在進行某些敏感操作前,需要對當(dāng)前請求下的角色進行驗證。游客是沒有訪問權(quán)限的,只有管理員可以。
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
@Component
public class AuthService {
public void checkAccess() {
// 通過ThreadLocal取得當(dāng)前線程(請求)中的角色
String user = CurrentUserHolder.getUserHolder();
if (!"admin".equals(user)) {
throw new RuntimeException("操作不被允許!");
}
}
}
操作前的驗證使用AOP過濾
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();
}
}
測試一下,如果當(dā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)前請求的角色是guest
CurrentUserHolder.setUserHolder("guest");
// AOP,在delete之前會先調(diào)用authService.checkAccess();結(jié)果驗證不通過
productService.delete(1L);
}
}
總結(jié)
安全共享對象的策略
- 線程限制:一個被線程限制的對象,由線程獨占;只能由它的線程來修改,例如使用線程內(nèi)的局部變量、ThreadLocal等
- 共享只讀:只讀對象在沒有額外同步的情況下,可以被多個線程并發(fā)訪問,但是任何線程都無法修改它。例如,使用不可變對象(final關(guān)鍵字修飾)
- 線程安全的對象:一個線程按安全的對象或容器,通過內(nèi)部的同步機制來保證線程安全。比如StringBuffer、ConcurrentHashMap、AtomicInteger等線程安全對象。