有关线程安全的探讨--final、static、单例、线程安全

个人代码中已经屡次使用了线程,而后还很是喜欢使用听说是线程不安全的静态方法,而后又看到不少地方最容易提的问题就是这个东西线程不安全
 
因而我难免产生了如下几个亟待解决的问题:
  1. 什么样的代码是天生线程安全的?而不用加锁
  2. 线程是否安全的本质是什么?
  3. 什么是快速把一段代码变成线程安全的通用方法
  4. final static 单例 线程安全 之间的关系
 
一、首先咱们知道,若是线程只是执行本身内部的代码(其实也是使用一些对象的方法,可是是局部变量,那么就线程安全),那必定是线程安全的
  1. 这句话严格一些说能够是这样:线程使用在run( )方法中实例化的局部变量的方法,是线程安全的
 
二、那下一个问题就是,一个线程能调用哪些代码,或者说能访问到哪些东西?访问这些东西的安全性如何?
一个线程能访问哪些东西,应该是跟它建立的环境有关,线程启动从这个意义上有两个方式
  1. 继承并重写一个Thread类,而后在使用的时候实例化这个类,最后调用这个对象实例的start方法启动
    1. 这种方式的run方法中,其实能调用的东西就不多了
      1. 你在继承时加的成员变量。(彻底不会有线程是否安全的问题,由于这个类就一个run()方法是多线程方法,就跟在run()中实例化的局部变量同样)
      2. 经过构造方法从外面传入的变量。(这种方式须要警戒!由于传递的是引用,若是你在线程中对这个引用指向的内容进行修改,那么会影响到原来的东西!)
      3. 使用其余的代码段(方法)
        1. 静态方法(相似单例模式)
        2. 实例方法——经过实例对象
      4. 使用其余的对象
        1. 静态对象
        2. 实例对象
      5. (两面两大点中,使用实例方法和实例对象都是线程安全的。而使用静态方法和静态对象时,是必定会冲突的)
    2. 因此总结一下,这种方式中
      1. 线程安全的有
        1. 在继承时加的成员变量
        2. 实例化其余对象,使用这个对象,或者使用这个对象的方法
      2. 不安全的有
        1. 经过构造方法从外面传入的变量
        2. 静态方法
        3. 静态对象
  2. 使用匿名内部类
    1. 这种方式,在上一种方式的继承上,只少了构造方法的方式,而后多了好几种危险的方式, 须要注意
      1. 所处方法中的局部变量
        1. 这个值得一提,原本这项是确定会线程不安全,并且很是经常使用,因此危险指数五颗星的,可是JAVA特意为此限定了一条规则,就是这样的局部变量必须是final的,不能修改,因而这个就变得很是安全了
        2. 但这条其实能够经过引用类型绕过,就是另外一回事了,其实也说明了它的不安全
      2. 所处类中的属性
      3. 所处类中的方法
 
另外,通过查阅资料,上面提到的全部跟方法有关的可能线程不安全的状况,其实都不是彻底不安全
方法是否线程安全取决于方法中是否使用了全局变量,方法自己是在JAVA中是线程安全的,每一个线程会有一个副本,可是在使用变量的时候就可能有问题
好比多线程中使用静态方法是否有线程安全问题?这要看静态方法是是引发线程安全问题要看在静态方法中是否使用了静态成员
 
总结一下,线程是否安全总的来讲状况比较复杂,可是有这些特色
  1. 方法自己不会有问题,问题的根源是(普通方法、静态方法)方法使用了变量(相对全局变量,或者说叫可共享变量)【好比静态成员、类属性等等】
  2. 匿名类中更加危险,要谨慎调用
 
三、线程是否安全的本质是什么?什么是快速把一段代码变成线程安全的通用方法?
而所谓的线程安全性具体又指的是什么
  1. 不能同时被多个线程调用
    1. 这个是最普通的,也是常规上咱们的线程安全的含义
    2. 这个问题能够经过加锁解决
  2. 不能被多个线程调用(不一样时也不行)
    1. 这个在第一类的程度上有所增长,不是经常使用的状况,可能你不只是要使用变量,你还须要记录变量的值
    2. 这个问题通常是把相关变量变成ThreadLocal的
  3. 不能被超过一次地调用
    1. 这个的状况更加特殊
    2. 通常使用单例模式解决
 
四、final static 单例 线程安全 之间的关系
  1. final
    1. 意思是,这个对象的值(基本类型就是值,引用类型是引用地址),不会再被改变
    2. 与线程安全的关系,如上文,必定程度上能使某些变量强制变得线程安全
  2. static
    1. 意思是,这个对象是一个全局变量了,你能够在多个地方,多个线程中调用到它,并且调用的是同一个它
    2. 与线程安全的关系,通常这种的变量很容易形成线程不安全的状况
  3. 单例
    1. 这首先是一种特殊的需求,就是某个类的实例在JVM中只能存在一个,跟前面的static,线程安全都不同
    2. 与线程安全的关系。实现单例须要考虑复杂的多线程的状况,这个东西须要线程安全
 
五、举个例子
常被说的SimpleDateFormat是非线程安全的,为何线程不安全,来分析一下
  1. 由于建立一个 SimpleDateFormat实例的开销比较昂贵,解析字符串时间时频繁建立生命周期短暂的实例致使性能低下
    1. 在程序中咱们应当尽可能少的建立SimpleDateFormat 实例,由于建立这么一个实例须要耗费很大的代价。在一个读取数据库数据导出到excel文件的例子当中,每次处理一个时间信息的时候,就须要建立一个SimpleDateFormat实例对象,而后再丢弃这个对象。大量的对象就这样被建立出来,占用大量的内存和 jvm空间。
  2. 因而,就很容易想到,将 SimpleDateFormat定义为静态类变量,貌似能解决这个问题
  3. 因而这就引出了,SimpleDateFormat是非线程安全的,这样的使用方式可能引起并发线程安全问题
那为何会有这个问题呢?来看看SimpleDateFormat自己
  1. SimpleDateFormat类内部有一个Calendar对象引用,它用来储存和这个SimpleDateFormat对象(叫sdf)相关的日期信息,例如sdf.parse(dateStr), sdf.format(date)
  2. 诸如此类的方法参数传入的日期相关String, Date等等, 都是交友Calendar引用来储存的
  3. 这样就会致使一个问题:若是你的sdf是个static的, 那么多个thread 之间就会共享这个sdf, 同时也是共享这个Calendar引用, 而且, 观察 sdf.parse() 方法,你会发现有以下的调用:
    1. Date parse() {
    2.   calendar.clear(); // 清理calendar
    3.   ... // 执行一些操做, 设置 calendar 的日期什么的
    4.   calendar.getTime(); // 获取calendar的时间
    5. }
  4. 这里会致使的问题就是:若是 线程A 调用了 sdf.parse(), 而且进行了 calendar.clear()后还未执行calendar.getTime()的时候,线程B又调用了sdf.parse(), 这时候线程B也执行了sdf.clear()方法, 这样就致使线程A的的calendar数据被清空了(实际上A,B的同时被清空了). 又或者当 A 执行了calendar.clear() 后被挂起, 这时候B 开始调用sdf.parse()并顺利i结束, 这样 A 的 calendar内存储的的date 变成了后来B设置的calendar的date
上边是复杂的具体的缘由,而这个缘由简单说就是,在线程中调用了一个static对象,这个对象存储值的变量被多个线程同时使用(修改),形成了混乱
 
六、OK,说了这么多,那知道了这些以后对我写代码有哪些指导做用呢?
  1. 你确定是喜欢使用匿名内部类的,以这个为基础
    1. 注意若是是调用所在方法中的局部变量,尽可能不要绕过final机制,若是须要绕过,并且会对这个局部变量进行修改的话,那必定是知道不会多个这样的线程同时运行(好比做为UI主线程外的一个子线程,这个子线程只会有一个)
    2. 不要尝试修改不是在本身内部实例化出的对象的值(只能改局部变量的值)(尽可能使用局部变量)
    3. 你还喜欢使用静态工具方法,全部的静态工具方法中使用变量尽可能使用局部变量(for循环中的i++ 是没有问题的),尽可能少地使用静态变量,更不要尝试对静态变量的值进行修改
 
后记:本文做者在并发领域只是新手,学习实践中偶有所得特此为记,可能出现错漏,还请多多指教,必定虚心学习
相关文章
相关标签/搜索