上一篇讲了线程的实现和调度,这一篇谈线程的安全。

什么是线程安全

这是《Java Concurrencu in Practice》中作者的定义。

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外同步,或者在调用方进行任何其他的协调操作,调用对象的行为都可以获得正确的结果,那这个对象是线程安全的。

Java 中的线程安全

种类

1.不可变

不可变对象或者数据带来的安全性是最简单和最纯粹的。比如String对象。

2.绝对线程安全

Java API中标注自己是线程安全的类大多数不是绝对线程安全,而是相对线程安全。绝对线程安全实现的代价太高。

3.相对线程安全

就是我们通常意义上讲的线程安全,他需要保证对这个对象单独操作是线程安全的,我们在调用的时候不需要做额外的保护措施。

4.线程兼容

对象本身并不是线程安全的,但是可以通过在调用端正确使用同步手段来保证对象在并发条件下是可以安全使用的。

5.线程对立

这种情况很少见,不提也罢

实现方法

1.互斥同步

最常见的一种并发正确性的保障手段。最基本的互斥手段是 synchronized 关键字。

此关键字在经过编译之后,会在同步块前后形成monitorenter和monitorexit这两个字节码的指令,这两个字节码都需要一个reference来指定对象参数,来指明要锁定和解锁的对象。

如果没有明确指定,那就根据 synchronized 修饰的实例方法还是类方法来取对应的对象实例或class对象来作为锁对象。

需要注意的是:Java线程是映射到操作系统的原生线程之上的,如果要阻塞或者唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换成核心态中,因此状态转换需要耗费很多的时间,对于代码简单的同步块,状态转换消耗的时间有可能比用户代码执行的时间还要长,所以synchronized是Java语言中一个重量级操作,只有在确实必要的情况下才使用这种操作。

除 synchronized 之外,在 java.util.concurrent.*包中还存在一种实现叫 ReentrantLock。它们基本用法上大体相同,只是还存在一些别的区别。参见:https://github.com/leexuehan/leexuehan.github.com/issues/64

2.非阻塞同步

互斥同步是一种悲观同步,即认为如果不加锁,就会出现同步问题。但是,这种会产生资源浪费,如果数据在很多地方不需要加锁也不会产生同步问题,则加锁就是一种浪费。

乐观同步就是非阻塞同步,就是先进行操作,如果没有其他线程争用共享数据,那么操作就成功了,如果产生了冲突,就采取其他的补偿措施。

这也叫一种 CAS 的方式。

关于 CAS 专门有一篇文章会介绍之。请参见:https://github.com/leexuehan/leexuehan.github.com/issues/92。

3.无同步方案

要保证线程安全不一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时正确性的一种手段,如果方法本来就不涉及共享数据,它就无需同步。

以下两类代码天生就是安全的:

可重入代码:

可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不出任何错误。

线程本地存储:

如果一段代码中所必要的数据必须与其他代码共享,那就看看这些共享数据代码是否能保证在同一个线程之内?如果可以,我们就可以吧共享数据的可见范围限制在同一个线程之内。

最经典的应用就是:Web交互模型中的一个请求对应一个服务器线程的处理方式。可以使用 ThreadLocalMap 对象来实现。