温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

Java 多线程安全(一) 不共享与不可变

发布时间:2020-06-05 10:02:18 来源:网络 阅读:590 作者:wx5c78c8b1dbb1b 栏目:编程语言
  1. 线程不安全

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

线程安全

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

解决线程安全问题(一)

  • 不共享与不可变

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

  1. Ad-hoc 线程封闭

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

栈封闭(常用)

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

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

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. 某些时候不可变对象提供了一种弱类型的原子性,如下代码示例:

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关键字.)


向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI