十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
线程不安全
成都创新互联致力于成都网站设计、网站制作,成都网站设计,集团网站建设等服务标准化,推过标准化降低中小企业的建站的成本,并持续提升建站的定制化服务水平进行质量交付,让企业网站从市场竞争中脱颖而出。 选择成都创新互联,就选择了安全、稳定、美观的网站建设服务!
当一个类的状态(指的存储在状态变量里面的数据)是共享的和可变时,那么这个类就是线程不安全的."共享"意味着变量可以由多个线程同时访问,而"可变"意味着变量的值在生命周期发生变化.
线程安全
在线程安全的定义中,最核心的概念就是正确性,正确性的含义是指:某个类的行为与其规范完全一致.线程安全定义如下:当多个线程访问某个类时,不管运行时采用何种调度方式或者这些线程将如何交替运行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么那么我们就称这个类是线程安全的.
解决线程安全问题(一)
不共享与不可变
线程封闭(不共享数据):当访问的共享的可变数据时,通常需要使用同步.一种避免使用同步的方式就是不共享.如果仅在单线程里面访问内部数据,就不需要同步.这种技术被称为线程封闭技术,它是实现线程安全性的最简单方式之一.
Ad-hoc 线程封闭
Ad-hoc 线程封闭是指,维护线程的封闭性的职责完全由程序来承担.例如volatile变量上存在一种特殊的线程封闭,只要你能确保只有单个线程对共享的volatile的变量执行写操作,那么就可以安全地在这些共享的volatile变量上执行"读取-修改-写入"的操作.在这种情况下,相当于变量封闭在单个线程中防止发生竞态条件,并且volatile的变量的可见性保证还确保了其它线程能看见最新的值.由于Ad-hoc 线程封闭技术的脆弱性,没有任何一种语言的特性是能将对象封闭到目标线程上,因此尽量少用,在可能的情况下,使用更强的封闭技术(栈封闭和ThreadLocal).
栈封闭(常用)
栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象.正如封装能使得代码更容易维护不变性条件那样,同步变量也能使对象更易于封闭在线程中.局部变量的固有属性之一就是封闭在执行程序中.他们位于执行线程的栈中,其它线程无法访问这个栈.栈封闭(也被称为线程内部使用或者线程局部使用)比 Ad-hoc 线程更易于维护,也更加健壮.
如下代码实例:
/** * 获取user总数 * @param userList * @return */ public int getTotalUser (ListuserList) { List 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 ThreadLocalconnectionThreadLocal = new ThreadLocal (){ 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).如果某个对象在创建后其状态就不能被修改,那么这个对象就是不可变对象.线程安全性是不可变对象的固有属性之一.不可变对象一定是线程安全的.
当满足一下条件时,对象才是不可变的:
对象创建以后其状态就不能修改.
对象的所有域都是final类型.
对象是正确创建的(在对象的创建期间,this引用没有逸出).
Final 域
final 类型的域是不能修改的(但如果final引用的对象是可变的,那么这些被引用的对象是可以修改的).在Java内存模型中,final域能够确保初始化过程的安全性.即使对象是可变的,通过将对象的某些域声明为final类型,仍然可以简化对状态的判断.通过将域声明为final类型,也相当于告诉维护人员这些域是不会变化的.
某些时候不可变对象提供了一种弱类型的原子性,如下代码示例:
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关键字.)