Java与线程安全

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

Java语言中的线程安全

1. 不可变

不可变的对象一定是线程安全的,只要其被正确地创建出来。基础数据类型只要声明为final就是不可变的,对象可以通过将自己的所有带有状态的变量都声明为final使得在构造函数结束后不可变。

Integer、Long、Double这类数值包装类型,还有String,将包装的值变量声明为final,都是不可变的。而AtomicInteger、AtomicLong这类原子类中,包装的值声明为volatile而不是final,所以不是不可变的。

2. 绝对线程安全

这一安全等级要求完全符合线程安全的定义,大多数Java API中认为是“线程安全”的类并不在这一等级。

3. 相对线程安全

相对线程安全就是通常所说的线程安全,可以保证对对象的单独操作是线程安全的。比如说对于java.util.Vector,其中操作方法被synchronized修饰,所以如果有两个线程分别调用remove和get方法,并不会出现数据不一致的情况,但如果两个线程都是循环进行这类操作,则很有可能会出错,这也就是绝对线程安全难以保证的一个例子。

4. 线程兼容

对象本身不是线程安全,但是调用时可以使用同步手段保证在并发环境下的安全使用,比如Vector和HashTable这类线程安全类对应的集合类ArrayList和HashMap。

5. 线程对立

无论是否采取同步措施,都无法在多线程环境种并发使用。这种情况非常少见,例如Thread类的suspend和resume方法。


线程安全的实现方法

1. 互斥同步

同步是指多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个(或者一些)线程使用。互斥则是实现同步的一种手段,主要实现方式有临界区、互斥量和信号量。

Java中最基本的实现互斥的方式就是使用synchronized关键字,synchronized关键字在编译后会在同步块前后形成monitorenter和monitorexit指令。synchronized关键字对于同一条线程来说是可重入的,不会锁死自己。但是,synchronized关键字会阻塞其他的线程,这就回要求操作系统对线程进行阻塞和唤醒操作,会产生用户态和核心态之间的转换,耗费很多处理器时间,所以synchronized关键字是重量级的操作。

java.util.concurrent包中提供了重入锁(ReentrantLock)也可以实现同步。ReentrantLock增加了几个高级功能:

  • 等待可中断:当持有锁的线程长期不释放是,等待锁的线程可以放弃等待去处理其他事情。
  • 公平锁:可以通过带boolean参数的构造函数要求使用公平锁,即按照申请锁的时间顺序来依次获得锁。
  • 锁绑定多个条件:一个ReentrantLock对象可以绑定多个Condition对象,而使用synchronized关键字时,只能通过所对象的wait和notify实现一个隐含条件。

目前来看ReentrantLock有一定的性能优势,但考虑虚拟机会不断优化synchronized的具体实现,如果不是必须要用ReentrantLock实现需求,建议使用synchronized进行同步。

2. 非阻塞同步

互斥同步由于会阻塞线程,性能会比较差。而非阻塞同步则是一种基于冲突检测的乐观并发策略,即线程可以先尝试操作,如果存在冲突,则采取补偿措施(比如不断重试直到成功)。这一策略得益于硬件指令集的发展,可以保证语义上多次操作的行为一条处理器指令就能完成,比如:测试并设置、获取并添加、交换、比较并交换(Compare and Swap,CAS)、加载链接/条件存储。

CAS操作是拿当前线程操作的旧值A去和内存中的V比较,如果相等,则将V更新为新值B,如果不相等,则返回V的值。

AtomicInteger类中的compareAndSet()和getAndIncrement()等方法使用了sun.misc.Unsafe类中提供的CAS操作。

CAS操作有两个问题,第一个是这一操作使用场景有限,第二个是存在"ABA"问题,即比对时结果为A,也有可能是该变量已经被修改过为B,又修改回A,大部分情况这一问题也不会影响程序并发的正确性。

3. 无同步方案

可重入代码:可重入性是比线程安全更基本的特性,无论如何切换线程,可以理解为只要代码顺利执行完成,其产生的结果必然一致。

线程本地存储:java.lang.ThreadLocal类可以实现线程本地存储的功能,线程本地存储的变量放在Thread对象的ThreadLocalMap里面,通过ThreadLocal的threadLocalHashCode为键进行访问,而这个键具有唯一性,这样就保证了线程独享的变量。


参考文献:

《深入理解Java虚拟机:JVM高级特性与最佳时间》,第2版,周志明


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值