线程安全简介

线程和进程都可以被操作系统直接调度,所不同的是多个线程之间会共享内存空间,但是多个进程之间的内存空间是相互隔离的。随着单核CPU的处理速度达到上限,多核时代正式到来,如何更好地利用多核资源成为了一个必须考虑的话题。在Java中倾向与使用多线程模型,Java针对多线程做了很多优化,包括提供了成熟的线程池1、线程安全工具类以及对锁的优化等。Java Web服务器也都是多线程的。但多线程编程一直以来都很困难且容易出错,虽然有很好的工具支撑,但编写代码时也需要格外注意,稍不小心就会掉进并发的陷阱。另一方面有些语言如Ruby则倾向于多进程的方式,由操作系统来实现并发且保证程序正确性,借助Linux的copy on write属性来节省内存。两种方式哪种更好已经超出了本文的范畴,这也是仁者见仁智者见智。值得一提的是Ruby on Rails 4开始正式支持多线程,Rails 5 更是使用puma作为默认的开发服务器,这也从另一角度预示着多线程的到来。

哪些情况是线程安全的?

下面主要讨论下多线程环境下的线程安全问题,线程安全主要是由于并发导致的,1.操作系统可能在任何时刻挂起某个线程;2.多核情况下两个线程可能处于并向执行的情况。其中第一点可以看出在单核情况下也会有线程安全问题。下面介绍哪些情况是线程安全的:

  1. 单线程环境中,包括多进程单线程、基于事件的程序(服务器)中,不存在多个线程同时修改或访问同一个变量的情况,这种情况下肯定线程安全的。为什么把这个放在第一位呢,因为如果你可以选择一个多进程单线程的Web服务器或是基于事件的服务器你就从根本上解决了线程安全的问题(当然可能有其他问题,比如内存消耗大、状态信息如session不一致等);
  2. 局部变量:即方法中声明的局部变量,这些是线程安全的,它们会随着方法调用结束而被回收,且只会对本线程可见。
  3. ThreadLocal全局变量:这些变量是线程级的,线程安全的,java会保证每个线程只有一份,每个线程只能访问属于自己的ThreadLocal变量。
  4. 线程安全的类:java中很多类会明确声明是线程安全的,例如concurrent包下类。这些类在访问是自身会进行加锁、原子化等操作来保证线程安全。
  5. 线程安全的静态类方法:这些类是单例的,其内部会保证线程安全,spring中很多类都属于此类。
  6. 无状态的bean。如没有状态的控制器bean。spring中的所有pojo类都可以是bean,且是单例的,这种类如果包括实例变量,不管共有私有都不一定是线程安全的。
  7. 不可变对象。例如java中的string类,以及scala中很多不可变的类。不可变类有很多优点,对于并发,可重复执行很有用。 上面提到的都是多个线程可以同时访问的变量,比如全局变量、类的静态变量、单例或是bean中的类实例变量(包括共有、私有)。

线程安全的优化方法

如果程序中确实需要包括这些共享信息,如何保证线程安全呢?可以采用如下措施:

  1. 将公共信息保存在数据库中,或是其他公共的位置。这样除了保证线程安全还可以保证一致性,但会牺牲效率。
  2. 对变量进行ThreadLocal包装,作为线程局部变量,做到线程间独立。
  3. 使用线程安全的类保存和处理这些共享信息(变量),如使用java的concurrent包的常用工具类
  4. 对共享信息的存取方法进行封装,并进行同步处理(加锁)。这里要注意不能只对set方法加锁,一方面是因为get方法不加锁不能保证取得的数据是最新的数据(参考java的happen before2),另一方面是除非你认真考虑了源码的各个分支情况,包括其底层调用的各个细节,否则读取方法也可能很危险,因为读取方法并没有规定不能进行其他处理,只是很多情况下它只是简单返回而已。SimpleDateFormat这个类在java中就不是线程安全的,我们有次想当然的把它作为静态变量,结果在日期格式化的时候出了问题,返回日期出现了错乱。
    [注]:Ruby中没有happen before的概念,取而代之的是GIL解释器锁3,这个锁会保证所有的单个native c方法的原子性。这样就导致在同一个进程内同时只有1个线程在使用CPU,如果是单进程单线程模式下甚至都不能充分使用1个CPU。如果要真正使用多核只能采用多进程。GIL的存在并不是必须的,只是对多线程实现的一个妥协。
  5. 如果多个状态不是独立的,这时即使每个状态是线程安全的,但整个对象也不是线程安全的,仍然需要同步处理(这样来看的话其实内部状态是不是线程安全就没什么意义了)。这时也可以使用下面要提的不变对象来处理。
  6. 对变量进行不变处理,使用不变对象保存状态。当更新状态时新建一个对象。在赋值时需要注意进行同步处理或是使用java的volatile标识。不变对象特别适合与同时保存多个关联的状态的情况,这样可以把多个状态当做1个来处理。
  7. 除非一个第三方类声明为线程安全,或是你认真读过它的代码,否则都可能有线程问题。这时需要进行同步处理或是进行充分测试。

如何保证整个程序的线程安全?4

  1. 框架自身是线程安全的
  2. 我们自己的应用代码是线程安全的
  3. 我们依赖的第三方代码是线程安全的

线程安全编程是很困难的,也是bug事故的多发区,除非性能要求否则尽量不要贸然使用多线程。现在很多web服务本身就是多线程的,进而我们的应用本身就是多线程的,在开发时也要小心,尽量不使用有状态的类。

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

留言:

(lesstile enabled - surround code blocks with ---)