1. 线程不安全

    • 当一个类的状态(指的存储在状态变量里面的数据)是共享的和可变时,那么这个类就是线程不安全的."共享"意味着变量可以由多个线程同时访问,而"可变"意味着变量的值在生命周期发生变化.

  2. 线程安全

    • 在线程安全的定义中,最核心的概念就是正确性,正确性的含义是指:某个类的行为与其规范完全一致.线程安全定义如下:当多个线程访问某个类时,不管运行时采用何种调度方式或者这些线程将如何交替运行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么那么我们就称这个类是线程安全的.

  3. 解决线程安全问题(一)

    • 不共享与不可变

      • 线程封闭(不共享数据):当访问的共享的可变数据时,通常需要使用同步.一种避免使用同步的方式就是不共享.如果仅在单线程里面访问内部数据,就不需要同步.这种技术被称为线程封闭技术,它是实现线程安全性的最简单方式之一.

        1. Ad-hoc 线程封闭

          • Ad-hoc 线程封闭是指,维护线程的封闭性的职责完全由程序来承担.例如volatile变量上存在一种特殊的线程封闭,只要你能确保只有单个线程对共享的volatile的变量执行写操作,那么就可以安全地在这些共享的volatile变量上执行"读取-修改-写入"的操作.在这种情况下,相当于变量封闭在单个线程中防止发生竞态条件,并且volatile的变量的可见性保证还确保了其它线程能看见最新的值.由于Ad-hoc 线程封闭技术的脆弱性,没有任何一种语言的特性是能将对象封闭到目标线程上,因此尽量少用,在可能的情况下,使用更强的封闭技术(栈封闭和ThreadLocal).

        2. 栈封闭(常用)

          • 栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象.正如封装能使得代码更容易维护不变性条件那样,同步变量也能使对象更易于封闭在线程中.局部变量的固有属性之一就是封闭在执行程序中.他们位于执行线程的栈中,其它线程无法访问这个栈.栈封闭(也被称为线程内部使用或者线程局部使用)比 Ad-hoc 线程更易于维护,也更加健壮.

          • 如下代码实例:

          • /**
             * 获取user总数
             * @param userList
             * @return
             */
            public int getTotalUser (List<User> userList) {
                List<User> userLists = null;
                int  totalUser = 0;
                userLists = userList;
                for (User user : userList) {
                    totalUser ++;
                }
                return totalUser;
            }

            该方法userLists是一个局部变量,存在于每个线程的栈中,是每一个线程私有的,别的线程获取不到,只要不把这个对象的发布出去,也就是返回,这样这个userLists 闭在了这个线程栈中,就是线程安全的.而对于totalUser 这个基本类型来说,发布出去也没有关系,因为由于任何线程都无法获取对基本类型的引用,因此Java语言

            的这种机制就确保了基本类型的局部变量始终封闭在线程内,也是线程安全的.

        3. ThreadLocal类

          • 维持线程封闭的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来.ThreadLocal类为每一个线程都维护了自己独有的变量拷贝,竞争条件被彻底消除了,那就没有任何必要对这些线程同步,他们也能最大限度的cpu调度,并发执行,并且有每个线程在访问变量时,读取和修改,都是自己独有的那一份变量拷贝,变量就彻底封闭在了每个线程中,也就是线程安全的了,此方案是空间(内存)来换取线程安全的策略.

          • 代码示例:多线程获取数据库连接.

          • public class ConnectionUtils {
            
                private static ThreadLocal<Connection> connectionThreadLocal
                        = new ThreadLocal<Connection>(){
                    protected  Connection initialValue () {
                        Connection connection = null;
                        try {
                            Class.forName("org.postgresql.Driver").newInstance();
                            connection = DriverManager.getConnection
                            ("jdbc:postgresql://localhost:5432/postgres",
                                    "postgres", "test");
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        return connection;
                    }
            
                };
            
                public static Connection getConnection () {
                    return connectionThreadLocal.get();
                }
            
                public static void main (String[] args) throws Exception {
                    for (int i = 0; i < 2; i++) {
                        Thread thread = new Thread(() -> {
                            Connection connection = ConnectionUtils.getConnection();
                            System.out.println(Thread.currentThread().getName() + 
                            "--------" + connection.toString());
                        }, "thread" + i);
                        thread.start();
                    }
                }
            }

            thread0--------org.postgresql.jdbc4.Jdbc4Connection@4fce58ae

            thread1--------org.postgresql.jdbc4.Jdbc4Connection@257f7c5b

            通过代码可以看见两个线程获取了各自的连接对象,都是绑定在当前线程上的,第一次获取是调用initialValue这个方法的返回值来设定值的,如果调用set方法也会和当前

            线程绑定.ThreadLocal源码实现分析参考:敬请期待Smile

      • 不可变的对象

        • 满足同步需求的另一种方法是使用不可变对象 (Immutable Object).如果某个对象在创建后其状态就不能被修改,那么这个对象就是不可变对象.线程安全性是不可变对象的固有属性之一.不可变对象一定是线程安全的.

        • 当满足一下条件时,对象才是不可变的:

          1. 对象创建以后其状态就不能修改.

          2. 对象的所有域都是final类型.

          3. 对象是正确创建的(在对象的创建期间,this引用没有逸出).

        • Final 域

          1. final 类型的域是不能修改的(但如果final引用的对象是可变的,那么这些被引用的对象是可以修改的).在Java内存模型中,final域能够确保初始化过程的安全性.即使对象是可变的,通过将对象的某些域声明为final类型,仍然可以简化对状态的判断.通过将域声明为final类型,也相当于告诉维护人员这些域是不会变化的.

          2. 某些时候不可变对象提供了一种弱类型的原子性,如下代码示例:

          3. public class OneValueCache {
            
                private final BigInteger lastNumber;
            
                private final BigInteger[] lastFactors;
            
                public OneValueCache (BigInteger i , BigInteger[] fastFactors) {
                    lastNumber = i;
                    lastFactors = Arrays.copyOf(fastFactors,fastFactors.length);
                }
            
                public BigInteger[] getFactors (BigInteger i) {
                    if (lastNumber == null || !lastNumber.equals(i)) {
                        return null;
                    } else {
                        return Arrays.copyOf(lastFactors,lastFactors.length);
                    }
                }
            
             
            }

            代码分析:OneValueCache 有两个final 域的变量,并在构造函数时初始化它们(没有提供其它初始化数据方案,因为要保证初始化后状态的不可变),在getFactors 方法里面没有返回原数组引用,如果这样那就不安全了因为lastFactors数组的域是不可变的,但是引用对应的内容是可以修改的,所以要是有copyOf方法,返回一个新数组(也可以使用clone方法).如果我们要修改lastNumber和lastFactors只有调用构造方法重新构造一个不可变对象,而构造对象需要这两个变量一起传入,要么成功要么失败,所以说不可变对象是一种弱类型的原子性.


            对于访问和更新多个相关变量时出现的竞争问题,可以通过将这些变量全部保存在一个不可变对象中来消除.如果是一个可变对象,那么就必须使用锁来确保原子性.如果是一个不可变对象,那么当前获得了带对象的引用后,就不必担心另一个线程会修改对象的状态.如果要更新这些变量,那么只有重新建一个新的容器对象,但其他使用原有对象的线程仍然看到对象处于一致状态(其它线程看见的还是原来的对象,如果要保证可见性,可以使用volatile关键字.)