美文网首页并发和多线程
2020-02-01 1. Java语言提供的基本线程安全保护

2020-02-01 1. Java语言提供的基本线程安全保护

作者: FredWorks | 来源:发表于2020-02-01 15:31 被阅读0次

    本文是Java线程安全和并发编程知识总结的一部分。

    1 java语言提供的基本线程安全保护

    本章节总结java语言本身自带的线程安全相关的知识点。

    1.1 什么时候需要考虑线程安全

    并不是所有情况都需要考虑线程安全,或者说,有些场景天生就是线程安全的,不需要我们做额外的工作。

    1.1.1 方法栈保护下的线程安全。

    前提:

    1. 类中的一个方法,没有返回对象(因此不会将对象发布出去),或返回不可变对象
    2. 方法参数不是对象,或是不可变对象
    3. 方法体中的代码没有访问类/对象的非常量成员变量(在讨论线程安全时,一般称为状态),或只访问常量成员变量,也没有访问其他类的成员变量。

    线程安全的原因:

    1. 由于每个线程调用一个方法时,都会拥有自己的方法栈;该方法内的局部变量只存在于线程自己的方法栈中,因此不会在多线程之间共享,从而受到方法栈的线程安全保护。
    2. 方法不访问类/对象成员变量,或只访问常量成员变量,因此不存在多线程之间的资源竟态,不会线程不安全。
    3. 方法不和外界通过对象(本质是引用)共享数据,或线程外界共享的对象都是不可变对象,因此不存在资源竟态,也是安全的。

    举例

    1. 最简单的方法栈提供的线程安全
      该方法输入参数没有对象,都是基本类型;返回值也是基本类型;且不使用类/对象成员变量,也不访问其他类的成员。它受到方法栈保护,天生是线程安全的。
    /**
     * @author xxx
     * 2020年1月31日 上午11:30:18
     */
    public class Sample1 {
        
        /**
         * 该方法由方法栈提供线程安全保护
         * 2020年1月31日 上午11:30:51 xxx 添加此方法
         * @param a 参与计算的基础类型参数
         * @param b 参与计算的基础类型参数
         * @return 返回的也是基础类型
         */
        public int  calc1(int a, int b) {
            int result = a * 10 + b;
            result++;
            
            return result;
        }
    }
    
    1. 输入参数或返回值使用不可变对象

    该方法输入参数接收不可变对象或基本类型,返回值也是一个不可变对象;且方法内代码不访问类/对象的成员变量,也不访问其他类的成员。它受到方法栈保护,天生线程安全。

    /**
     * @author xx
     * 2020年1月31日 下午2:39:31
     */
    public class Sample2 {
        
        /**
         * 接受不可变对象和基本类型作为输入参数,返回不可变对象的方法,收到方法栈的线程安全保护。
         * 2020年1月31日 下午2:50:17 xx添加此方法
         * @param laltitude
         * @param longitude
         * @return
         */
        public ImmutablePoint doSome2dCalc(ImmutablePoint point, double laltitude, double longitude) {
            // 对坐标做一系列业务处理,得到新坐标。
            double newLatitude = point.getLatitude();
            double newLongitude = point.getLongitude();
            
            // 返回一个不可变对象作为输出参数。
            return new ImmutablePoint(newLatitude, newLongitude);
        }
    }
    
    /**
     * 不可变的点对象
     * @author xx
     * 2020年1月31日 下午2:41:48
     */
    public class ImmutablePoint {
        
        /**
         * 构造函数
         */
        public ImmutablePoint(double latitude, double longitude) {
            this.latitude = latitude;
            this.longitude = longitude;
        }
        
        /**
         * 维度
         */
        private double latitude;
        
        /**
         * 经度
         */
        private double longitude;
        
        /**
         * 获取属性  latitude 的值
         * @return 属性 latitude 的值
         */
        public double getLatitude() {
            return this.latitude;
        }
        
        /**
         * 获取属性  longitude 的值
         * @return 属性 longitude 的值
         */
        public double getLongitude() {
            return this.longitude;
        }
    }
    
    1. 访问类的常量成员
      当一个方法只访问所在类的常量成员变量时,显然该常量成员变量不可能形成竟态条件,因此是天生线程安全的。
    /**
     * @author xx
     * 2020年1月31日 下午3:04:19
     */
    public class Sample3 {
        
        /**
         * 常量成员
         */
        private static final int Dummy_State = 1;
        
        /**
         * 只访问常量成员,受到方法栈的线程安全保护。
         * 2020年1月31日 下午2:50:17 xx添加此方法
         * @param a 基本类型参数
         * @param b 基本类型参数
         * @return
         */
        public int calc(int a, int b) {
            // 使用常量成员参与计算
            int result = this.doCalc(Sample3.Dummy_State, a, b);
            
            return result;
        }
        
        private int doCalc(int factor, int a, int b) {
            int result = 0;
            
            // 实际的计算逻辑
            
            return result;
        }
    }
    

    1.1.2 由调用者确保被调用方法不会被多线程访问

    任何线程安全代码,都会增加代码复杂度。因为编程语言本身,实际上是基于串行的。因此,当非常明确某个业务方法不会被用于多线程访问时,就无需进行线程安全保护处理。
    典型的场景包括:
    1. 供不支持并发执行的定时器调用的方法
    比如,定时器框架quartz允许将定时器配置为支持并行执行或不支持并行执行。
    当配置为不支持并行执行时,如果一个定时器的前一次执行尚未结束,下一次执行的时间又到了的话,下一次执行将被阻塞,直到前一次执行结束才启动执行。

    2. 只执行一次的初始化逻辑
    比如由spring的@PostConstruct注解的单例bean上的方法:

    /**
     * @author xx
     * 2020年1月31日 下午3:48:15
     */
    @Service
    public class Sample4 {
        
        /**
         * 只在一个线程中执行一次
         * 2020年1月31日 下午3:49:12 xx添加此方法
         */
        @PostConstruct
        public void init() {
            // 在独立线程中执行,避免影响spring容器的初始化。
            new Thread(() -> {
                this.loadCacheFromDb();
            }).start();
        }
    
        /**
         * 本方法不提供线程安全保护,由调用者确保该方法只被一个线程调用。
         * 2020年1月31日 下午3:55:48 xx添加此方法
         */
        private void loadCacheFromDb() {
            // 初始化逻辑,比如从数据库中加载需要缓存的数据等
        }
    }
    

    3. 类的静态初始化代码块。
    类的静态初始化块,是在类被jvm初始化时调用的,由虚拟机的内部同步机制确保其线程安全。

    /**
     * @author xx
     * 2020年1月31日 下午4:00:01
     */
    public class Sample5 {
        
        /**
         * 一个字符串键值对缓存容器。
         */
        private static Map<String, String> caches;
        
        /**
         * 静态初始化块,由jvm内部同步机制确保其线程安全。
         */
        static {
            caches = new HashMap<>(10);
            
            for (int i = 0; i < 10; i++) {
                caches.put(Integer.toString(i), "some value " + i);
            }
        }
    }
    

    1.1.3 由调用者提供线程安全保护

    即某个方法本身并未提供线程安全保护,且形成了竟态条件,是线程不安全的。但是在业务上可以确保该方法总是被另外一个方法调用,且调用该方法的代码块有线程安全保护。那么,这个方法实际上构成了事实线程安全。
    比如:

    /**
     * @author xx
     * 2020年1月31日 下午4:08:45
     */
    public class Sample6 {
        
        /**
         * 表示当前类状态的成员变量
         */
        private int state;
        
        /**
         * 只访问常量成员,受到方法栈的线程安全保护。
         * 2020年1月31日 下午2:50:17 xx添加此方法
         * @param a 基本类型参数
         * @param b 基本类型参数
         * @return
         */
        public int calc(int a, int b) {
            int result = 0;
            
            // 通过内部锁确保只有一个线程调用这段代码。
            // doCalc方法的调用者提供了线程安全保护,是线程安全的。
            synchronized (this) {
                // 先执行一些逻辑
                
                result = this.doCalc(a, b);
                
                // 再执行一些逻辑
            }
            
            return result;
        }
        
        /**
         * 该方法本身不提供线程安全保护,本身不具备线程安全,由其调用者提供保证。
         * 2020年1月31日 下午4:09:53 xx添加此方法
         * @param a 基础类型变量
         * @param b 基础类型变量
         * @return
         */
        private int doCalc(int a, int b) {
            int result = 0;
            
            // 内部成员变量加入计算,形成竟态条件,存在线程安全问题
            if (this.state < 0) {
                this.state++;
                
                // 执行计算逻辑1,并将结果赋值给 result
            } else if (this.state == 0) {
                // 执行计算逻辑2,并将结果赋值给 result
            } else {
                this.state--;
                
                // 执行计算逻辑3,并将结果赋值给 result
            }
            
            return result;
        }
    }
    

    这类场景往往重度依赖相关代码充分注释,或详细的文档说明,以及开发团队的管理。很容易因为人为因素导致线程安全被破坏。

    1.2 使用java语言的内置锁机制(synchronized语法)

    Java语法的synchronized关键词,提供了使用Java提供的内置可重入锁的机会。Jvm为每个对象默认提供了一个可重入锁;无论你是否使用,该锁都存在,因此称为内置锁。

    它有两种用法:
    1. 用于方法
    该关键词用于方法时,若该方法被调用,进入该方法时会自动尝试获得当前对象的内置锁,如果获取内置锁失败,则当前线程阻塞;退出该方法时会自动释放当前对象的内置锁。

    由于是使用方法所在对象的内置锁,因此任意时候,只有一个线程可以执行该方法,其他调用该方法的线程都会被阻塞。

    /**
     * @author xx
     * 2020年1月31日 下午4:08:45
     */
    public class Sample7 {
        
        /**
         * 表示当前类状态的成员变量
         */
        private int state;
        
        /**
         * 2020年1月31日 下午4:38:11 xx 添加此方法
         * @param args
         */
        public void startCalc() {
            // 虽然启动了5个线程,但由于内部锁的存在,实际上任意时刻都只有一个线程在执行,其他线程都被阻塞了
            for (int i = 0; i < 5; i++) {
                int times = i;
                int a = 5 + i;
                int b = 7 + i;
                new Thread(() -> {
                    int result = this.calc(a, b);
                    System.out.println(" i = " + times + ", result = " + result);
                }).start();
            }
        }
        
        /**
         * 由 synchronized 提供线程安全保护
         * 2020年1月31日 下午2:50:17 xx添加此方法
         * @param a 基本类型参数
         * @param b 基本类型参数
         * @return
         */
        private synchronized int calc(int a, int b) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            int result = 0;
            
            // 先执行一些逻辑
    
            // 内部成员变量加入计算,形成竟态条件,存在线程安全问题
            if (this.state < 0) {
                this.state++;
                
                // 执行计算逻辑1,并将结果赋值给 result
            } else if (this.state == 0) {
                this.state -= 2;
                // 执行计算逻辑2,并将结果赋值给 result
            } else {
                this.state--;
                
                // 执行计算逻辑3,并将结果赋值给 result
            }
            
            // 再执行一些逻辑
            
            return result;
        }
    }
    

    2. 用于代码块
    该关键词用于代码块时,必须用一个对象做锁。我们可以使用任意对象来作为 synchronized 关键词的锁;其实质,是使用给定对象的内部锁作为锁。
    比如:
    synchronized(this) ,实际上是使用 this 对象的内部锁做锁。比如 Sample6。
    synchronized(someObj),实际上是使用对象 someObj 的内部锁做锁。

    3. 注意

    • synchronized 语义实现的是不可中断锁。

    也就是说,当前进入同步代码块以后,及时代码跑出 InterruptException,代码也不会中断,因为不响应中断异常。具体参看2.2.1 可重入锁与不可重入锁,其中详细举例说明。

    • 当使用字符串做锁时,因为jvm字符串常量池的存在,要使用 string.intern() 做锁,不要直接用字符串。

    因为jvm字符串常量池的存在,为了避免两个字面值相同的字符串实际上指向不同的对象,要使用 string.intern() 做锁,不要直接用字符串。string.intern()方法返回相同字符串在字符串常量池中的对象引用,确保了对相同字符串内容,得到的是同一个对象。

    1.3 使用Java语言的volatile机制提供的弱同步保护

    这个关键词的作用有两个:

    1. 是告诉虚拟机,不要将该关键词修饰的类成员变量,存放在寄存器或其他处理器不可见的地方。这样,一旦该成员变量被修改,能够立即被所有线程看到。也就是能提供同步的可见性;但它不提供同步的原子性,因此是不完全的同步保护,是弱同步保护。
    2. 在方法中访问该关键词修饰的成员变量时,不要参与重排序。关于jvm重排序,不了解的自行百度。

    其优点在于:

    比同步效率高,当没有复合原子操作时,可以用该关键词提供可见性保护。

    其缺点在于:

    正如其优点所言,它只保证可见性保护,不确保原子性保护。

    显然,该关键词不能用于有多个相关状态属性的场景,此时必然出现竟态条件,有原子性保护需求。
    事实上,当需要该关键词的场景,都可以使用java.util.concurrent.atomic包提供的原子量来替代。

    相关文章

      网友评论

        本文标题:2020-02-01 1. Java语言提供的基本线程安全保护

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