Java高并发--线程安全策略

Java高并发--线程安全策略

主要是学习慕课网实战视频《Java并发编程入门与高并发面试》的笔记

不可变对象

发布不可变对象可保证线程安全。

实现不可变对象有哪些要注意的地方?比如JDK中的String类。

  • 不提供setter方法(包括修改字段、字段引用到的的对象等方法)
  • 将所有字段设置为final、private
  • 将类修饰为final,不允许子类继承、重写方法。可以将构造函数设为private,通过工厂方法创建。
  • 如果类的字段是对可变对象的引用,不允许修改被引用对象。 1)不提供修改可变对象的方法;2)不共享对可变对象的引用。对于外部传入的可变对象,不保存该引用。如要保存可以保存其复制后的副本;对于内部可变对象,不要返回对象本身,而是返回其复制后的副本。

final关键字可以修饰在类、方法、变量:

  • 类:被修饰的类不能被继承
  • 方法:被修饰的方法不能被重写
  • 变量:被修饰的是一个基本类型,其值不能被修改;被修饰的是一个对象引用,这里的“不可变”指不允许其再指向其他对象,但是可以修改对象里面的值。

关于上一条中final修饰对象的引用。以ArrayList为例

final List<Integer> list= new ArrayList<>();
list.add(3);
list.add(4);
list = new ArrayList<>(); // 编译时报错,不能再指向其他对象
list.set(0, 2); // 但是可以修改list里面的值

假如我们就是要求诸如List、Map一类的数据结构也不能修改其中的元素呢?

JDK中Collections的一些静态方法提供了支持,如下,举一个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()作为入参即可。

其实现原理也很简单,将普通list中的数据拷贝,然后对于所有添加、修改的操作,直接抛出异常即可,这样就保证了list不能修改其中的元素。

线程安全的问题就是出在多个线程同时修改共享变量,不可变对象的策略完全规避了对对象的修改,所以在多线程中使用也不会有任何问题。

线程封闭

  • 堆栈封闭:能使用局部变量的地方就不使用全局变量,多线程下访问同一个方法时,方法中的局部变量都会拷贝一份到线程的栈中,也就是说每一个线程中都有只属于本线程的私有变量,因此局部变量不会被多个线程共享。
  • ThreadLocal:特别好的线程封闭方法,其实现原理如下

对于共享变量,一般采取同步的方式保证线程安全。而ThreadLocal是为每一个线程都提供了一个线程内的局部变量,每个线程只能访问到属于它的副本。

下面是set和get的实现

// 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()获取当前线程,进而获取到当前线程的ThreadLocalMap,然后以ThreadLocal自己为key,要存储的对象为值,存到当前线程的ThreadLocalMap中。

get时也是先获得当前线程的ThreadLocalMap,以ThreadLocal自己为key,取出和该线程的局部变量。

题话外,一个线程内可以设置多个ThreadLocal,这样该线程就拥有了多个局部变量。比如当前线程为t1,在t1内创建了两个ThreadLocal分别是tl1和tl2,那么t1的ThreadLocalMap就有两个键值对。

t1.threadLocals.set(tl1, obj1) // 等价于在t1线程中调用tl1.set(obj1)
t1.threadLocals.set(tl2, obj2) // 等价于在t1线程中调用tl2.set(obj1)

t1.threadLocals.getEntry(tl1) // 等价于在t1线程中调用tl1.get()获得obj1
t1.threadLocals.getEntry(tl2) // 等价于在t1线程中调用tl2.get()获得obj2

以一个角色验证的例子为例,为每一个请求(线程)保存了当前访问人的角色。比如有guest和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();
    }
}

在进行某些敏感操作前,需要对当前请求下的角色进行验证。游客是没有访问权限的,只有管理员可以。

import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;


@Component
public class AuthService {
    public void checkAccess() {
        // 通过ThreadLocal取得当前线程(请求)中的角色
        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();
    }
}

测试一下,如果当前请求的角色是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() {
        // 当前请求的角色是guest
        CurrentUserHolder.setUserHolder("guest");
        // AOP,在delete之前会先调用authService.checkAccess();结果验证不通过
        productService.delete(1L);
    }
}

总结

安全共享对象的策略

  • 线程限制:一个被线程限制的对象,由线程独占;只能由它的线程来修改,例如使用线程内的局部变量、ThreadLocal等
  • 共享只读:只读对象在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都无法修改它。例如,使用不可变对象(final关键字修饰)
  • 线程安全的对象:一个线程按安全的对象或容器,通过内部的同步机制来保证线程安全。比如StringBuffer、ConcurrentHashMap、AtomicInteger等线程安全对象。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容