线程和进程都可以被操作系统直接调度,所不同的是多个线程之间会共享内存空间,但是多个进程之间的内存空间是相互隔离的。随着单核CPU的处理速度达到上限,多核时代正式到来,如何更好地利用多核资源成为了一个必须考虑的话题。在Java中倾向与使用多线程模型,Java针对多线程做了很多优化,包括提供了成熟的线程池、线程安全工具类以及对锁的优化等。Java Web服务器也都是多线程的。但多线程编程一直以来都很困难且容易出错,虽然有很好的工具支撑,但编写代码时也需要格外注意,稍不小心就会掉进并发的陷阱。另一方面有些语言如Ruby则倾向于多进程的方式,由操作系统来实现并发且保证程序正确性,借助Linux的copy on write属性来节省内存。两种方式哪种更好已经超出了本文的范畴,这也是仁者见仁智者见智。值得一提的是Ruby on Rails 4开始正式支持多线程,Rails 5 更是使用puma作为默认的开发服务器,这也从另一角度预示着多线程的到来。
哪些情况是线程安全的?
下面主要讨论下多线程环境下的线程安全问题,线程安全主要是由于并发导致的,1.操作系统可能在任何时刻挂起某个线程;2.多核情况下两个线程可能处于并向执行的情况。其中第一点可以看出在单核情况下也会有线程安全问题。下面介绍哪些情况是线程安全的:
- 单线程环境中,包括多进程单线程、基于事件的程序(服务器)中,不存在多个线程同时修改或访问同一个变量的情况,这种情况下肯定线程安全的。为什么把这个放在第一位呢,因为如果你可以选择一个多进程单线程的Web服务器或是基于事件的服务器你就从根本上解决了线程安全的问题(当然可能有其他问题,比如内存消耗大、状态信息如session不一致等);
- 局部变量:即方法中声明的局部变量,这些是线程安全的,它们会随着方法调用结束而被回收,且只会对本线程可见。
- ThreadLocal全局变量:这些变量是线程级的,线程安全的,java会保证每个线程只有一份,每个线程只能访问属于自己的ThreadLocal变量。
- 线程安全的类:java中很多类会明确声明是线程安全的,例如concurrent包下类。这些类在访问是自身会进行加锁、原子化等操作来保证线程安全。
- 线程安全的静态类方法:这些类是单例的,其内部会保证线程安全,spring中很多类都属于此类。
- 无状态的bean。如没有状态的控制器bean。spring中的所有pojo类都可以是bean,且是单例的,这种类如果包括实例变量,不管共有私有都不一定是线程安全的。
- 不可变对象。例如java中的string类,以及scala中很多不可变的类。不可变类有很多优点,对于并发,可重复执行很有用。
上面提到的都是多个线程可以同时访问的变量,比如全局变量、类的静态变量、单例或是bean中的类实例变量(包括共有、私有)。
线程安全的优化方法
如果程序中确实需要包括这些共享信息,如何保证线程安全呢?可以采用如下措施:
- 将公共信息保存在数据库中,或是其他公共的位置。这样除了保证线程安全还可以保证一致性,但会牺牲效率。
- 对变量进行ThreadLocal包装,作为线程局部变量,做到线程间独立。
- 使用线程安全的类保存和处理这些共享信息(变量),如使用java的concurrent包的常用工具类。
- 对共享信息的存取方法进行封装,并进行同步处理(加锁)。这里要注意不能只对set方法加锁,一方面是因为get方法不加锁不能保证取得的数据是最新的数据(参考java的happen before),另一方面是除非你认真考虑了源码的各个分支情况,包括其底层调用的各个细节,否则读取方法也可能很危险,因为读取方法并没有规定不能进行其他处理,只是很多情况下它只是简单返回而已。SimpleDateFormat这个类在java中就不是线程安全的,我们有次想当然的把它作为静态变量,结果在日期格式化的时候出了问题,返回日期出现了错乱。
[注]:Ruby中没有happen before的概念,取而代之的是GIL解释器锁,这个锁会保证所有的单个native c方法的原子性。这样就导致在同一个进程内同时只有1个线程在使用CPU,如果是单进程单线程模式下甚至都不能充分使用1个CPU。如果要真正使用多核只能采用多进程。GIL的存在并不是必须的,只是对多线程实现的一个妥协。
- 如果多个状态不是独立的,这时即使每个状态是线程安全的,但整个对象也不是线程安全的,仍然需要同步处理(这样来看的话其实内部状态是不是线程安全就没什么意义了)。这时也可以使用下面要提的不变对象来处理。
- 对变量进行不变处理,使用不变对象保存状态。当更新状态时新建一个对象。在赋值时需要注意进行同步处理或是使用java的volatile标识。不变对象特别适合与同时保存多个关联的状态的情况,这样可以把多个状态当做1个来处理。
- 除非一个第三方类声明为线程安全,或是你认真读过它的代码,否则都可能有线程问题。这时需要进行同步处理或是进行充分测试。
如何保证整个程序的线程安全?
- 框架自身是线程安全的
- 我们自己的应用代码是线程安全的
- 我们依赖的第三方代码是线程安全的
线程安全编程是很困难的,也是bug事故的多发区,除非性能要求否则尽量不要贸然使用多线程。现在很多web服务本身就是多线程的,进而我们的应用本身就是多线程的,在开发时也要小心,尽量不使用有状态的类。
参考资料: