关线程安全以及如何实现线程安全

有时候越是基础的概念,反而越是模糊。有时候先要对概念理解清楚。

学习Java多线程,经常跟线程安全打交道,那么什么是线程安全呢?

一、什么是线程安全

《Java并发编程实践》中对线程安全有这样的定义:

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

《深入理解Java虚拟机》的作者也认可这个观点。本人也认为这是一个恰当的定义,因为线程安全的主体是什么?是方法还是代码块?这里给出的主体是对象,这是非常恰当的,因为Java是纯面向对象的,Java中一切为对象。因此通过对象定义线程安全是恰当的。

但是,这里并不是说其他的方式定义不对(这里绝没有这个意思)。

我们可以看一下其他的定义方式,进行一下对比:

当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类时线程安全的。

如果一段代码可以保证多个线程访问的时候正确操作共享数据,那么它是线程安全的

其实这些定义都表达了线程安全是什么。不管如何表述,重要的是自己真正理解了就好。

二、线程安全的实现方法

1、互斥同步(阻塞同步)

同步是指多个线程在并发访问共享数据时,保证共享数据在同一时刻只能被一个线程使用。而互斥是实现同步的手段。

临界区、互斥量、信号量都是主要的互斥实现的方式。

这里可以通过加锁的方式,实现,比如使用synchronized关键字,ReentrantLock等。

2、非阻塞同步

使用互斥同步的最主要问题是进行线程阻塞和唤醒所带来的性能问题,这种同步也成为阻塞同步。

随着硬件指令集的发展,我们有了另一种选择:基于冲突检测的的乐观并发策略。

就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补救措施(最常见的补偿措施就是重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步被称为非阻塞同步。

为了使得操作和冲突检测都具备原子性,就需要硬件的支持。硬件保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成;常用指令有:

  • 测试并设置(Test-ans-Set)
  • 获取并增加(Fetch-and-Increment)
  • 交换(Swap)
  • 比较并交换(Comapre-and-Swap)
  • 加载连接/条件存储(Load-Linked/Store-Condition)

3、无同步方案

如果一些代码本身并不涉及共享数据,那么就无需使用任何的同步方式去保证它的正确性,因此有些代码天生就是线程安全的。

可重入代码(Reentrant Code): 这种代码也叫纯代码,可以在代码执行的任何时刻中断它,在控制权返回后,不会出现任何错误。因此所有可重入的代码都是线程安全的。

那么什么样的代码是可重入的呢?

可重入代码有一些共同特征:如不依赖堆上的数据和 公用的系统资源、用到的状态量由参数传入、不可调用非重入的代码等。

可以这样判断代码是否具有可重入性:如果一个方法,它的返回结果是可预测的,只要输入了相同的参数,就都能返回相同的结果,那它就满足可重入性的要求。

例如:Math.max(x,y);获取两个值中的最大者。

线程本地存储(Thread Local Storage): 如果一段代码中所需的数据必须与其他代码共享,那就看看这些共享数据的的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围控制在同一个线程中,这样,无需同步也不会出现数据争用的问题。

例如:生产者和消费者模式,可以将消费者的消费过程尽量控制在同一个线程中执行。

你可能感兴趣的:(java多线程)