Java并发——线程安全

一、线程安全的定义

如果一个对象可以安全地被多个线程同时使用,那么它就是线程安全的。(这个定义没有可操作性)

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

——Brian Goetz《Java并发编程实战》

二、共享数据线程安全程度分类

按照线程安全的”安全程度“由强到弱,可以把Java语言中各种操作共享的数据分为五类

  • 不可变
  • 绝对线程安全
  • 相对线程安全
  • 线程兼容
  • 线程对立

不可变对象:一定是线程安全的,不再需要任何线程安全的保障。比如final修饰的基本数据类型,String对象实例。Java类库API中不可变类包括String、枚举类、Number部分子类如Long、Double、BigInteger、BigDecimal等。

绝对线程安全:一个类要达到“不管运行时环境如何、调用者都不需要任何额外的同步措施”可能要付出非常高昂、甚至不切实际的代价。

相对线程安全:就是通常意义上的线程安全,它需要保证对这个对象的单次调用是线程安全的,在调用时不需要进行额外的保障措施。但对于一些特定顺序的连续调用,可能需要使用额外的同步手段来保证调用的正确性。Java API中标注的线程安全的类,大多数都属于这种类型。

线程兼容:指对象本身不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。通常说一个类不是线程安全的,即指这种情况。Java类库API中大部分类都是线程兼容的。

线程对立:不管调用端是否采取同步措施,都无法在多线程环境中并发使用代码。由于Java语言天生支持多线程,所以线程对立这种排斥多线程的代码很少出现,而且通常都有害,应当尽量避免。

三、线程安全的实现方法

1.互斥同步

互斥同步是最常见也是最主要的并发正确性保障手段,因为会阻塞其他线程,所以也称为阻塞同步。同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只能被一条(当使用信号量时,是一些)线程使用。互斥是实现同步的一种手段,互斥是方法,同步是目的。常见互斥实现方式有临界区、互斥量、信号量。

实现互斥同步的手段

(1)synchornized关键字

最基本的互斥同步手段是synchronized关键字,这是一种块结构的同步语法,基于语言特性。synchronized关键字经过Javac编译后,会在同步块前后分别形成monitorenter和monitorexit这两个字节码指令。这两个指令都需要一个Reference类型的参数来指明要锁定和解锁的对象。

  • synchronized指定了对象参数,引用参数就是这个对象的引用
  • synchronized没有指定且修饰类方法或static代码块,参数是类的class对象的引用
  • synchronized没有指定且修饰成员方法或其中代码块,参数是当前对象的引用

根据《Java虚拟机规范》的要求,执行monitorenter指令时,尝试获取对象锁。如果对象没有被锁定或当前线程已经持有对象锁,那么锁计数器的值加一,执行monitorexit时,计数器减一。计数器值为零,锁随即被释放。如果获取锁失败,那么当前线程被阻塞,直到持有对象锁的线程释放。据此会有两个推论

  • synchronized代码块或方法可重入
  • 被synchronized修饰的代码块或方法在持有锁的线程执行完毕并释放前,会无条件阻止其他线程进入,即不能强制等待锁的线程中断等待或超时退出。

(2)Lock接口

使用Lock接口能够以非块结构来实现互斥同步,这是类库层面的实现。Lock接口的一个常见实现是重入锁(ReentrantLock),也是可重入的。与synchronized相比,增加了高级功能,主要是

  • 等待可中断:当持有锁的线程长期不释放锁时,正在等待的线程可以放弃等待,处理其他事情。
  • 公平锁:多个线程等待同一锁时,必须按照申请锁的时间顺序获取锁。synchronized的锁是非公平的,ReentrantLock默认也是非公平的,可以通过带布尔值的构造方法要去使用公平锁。不过使用公平锁会导致吞吐量明显下降。
  • 锁绑定多个条件:ReentrantLock可以绑定多个Condition对象。synchronized的锁对象可以实现一个隐含的条件,如果要关联多个条件,需要添加多个锁;而ReentrantLock多次调用newCondition()方法即可。

在jdk5中,当线程数增加时,synchronized的吞吐量下降非常严重,而Lock接口的吞吐量基本不变,所以Lock接口优于synchronized。而从jdk6起,synchronized锁经过优化,两者性能基本持平。其他区别

  • synchronized是java语法层面的同步,清晰简单
  • Lock需要显式加锁,并在finally中释放锁。而synchronized由虚拟机保证即使出现异常,锁也会被释放

2.非阻塞同步

互斥同步(即阻塞同步)是一种悲观的并发策略,由于存在用户态到核心态转换、维护锁计数器、检查是否有被阻塞的线程需要被讹唤醒等开销,所以代价较大。而随着硬件指令集的发展,出现了另一个选择:基于冲突检测的乐观并发策略,它不再需要把线程阻塞挂起,因此被称为非阻塞同步,使用这种措施的代码被称为无锁编程。

硬件指令集的发展,使的操作和冲突检测这两个步骤具备原子性,即硬件保证出某些看起来需要多次操作的行为只需要一条处理器指令就能完成,常见的这类指令有

  • 测试并设置(test-and-set)
  • 获取并增加(fetch-and-increment)
  • 交换(swap)
  • 比较并交换(compare-and-swap,简称CAS)
  • 加载链接/条件储存(load-liinked/store-conditional,简称LL/SC)

CAS有“ABA问题“的漏洞。JUC包提供了带有标记的原子引用类AtomicStampedReference,它通过控制变量值的版本来保证CAS的正确性。不过大部分ABA问题不影响程序并发的正确性,如果需要解决ABA问题,使用传统的互斥同步可能比原子累更加高效。

jdk5之后Java类库才开始使用CAS操作,由sun.misc.Unsafe类的compareAndSwapInt()、compareAndSwapLong()等几个方法包装提供。由于Unsafe类限制了只有启动类加载器加载的Class才能访问,用户程序不能调用(不过可以使用反射手段突破Unsafe的访问限制),所以jdk9之前只有Java类库可以使用CAS,比如JUC包里的整数原子类,其中的compareAndSet()、getAndIncrement()方法都使用了Unsafe类的CAS操作实现。jdk9后,Java类库在VarHandle类里开放了面向用户程序使用的CAS操作。

3.无同步方案

一些代码天生就是安全的,比如可重入代码和线程本地存储。

可重入代码(纯代码)是指可以在代码执行的任何时刻中断它,转去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误,也不会对结果有影响。可重入代码是线程安全代码的一个真子集。

可重入代码的共同特征:

  • 不依赖全局变量、存储在堆上的数据和公用的系统资源
  • 用到的状态量都由参数传入
  • 不调用非可重入的方法

判断方法:如果一个方法的返回结果可以预测,只要输入了相同的数据,都能返回相同的结果,它就满足可重入特性,也就是线程安全的。

线程本地存储,如果一段代码中所需数据必须与其他代码共享,那就看看这些共享数据的代码能否保证在同一线程中执行。如果能保证,就把共享数据的可见范围限制在同一个线程内,这样不需要同步也能保证线程之间不出现数据争用的问题。比如经典web交互模型中,一个请求对应一个服务器线程。Java中可以使用ThreadLocal类来实现线程本地存储的功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值