美文网首页
Java高并发--线程安全策略

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

作者: sunhaiyu | 来源:发表于2019-04-20 16:04 被阅读0次

    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等线程安全对象。

    相关文章

      网友评论

          本文标题:Java高并发--线程安全策略

          本文链接:https://www.haomeiwen.com/subject/pnllgqtx.html