当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的
按照线程安全的“安全程度”由强至弱来排序,我们可以将Java语言中各种操作共享的数据分为
AtomicInteger
和AtomicLong
则并非不可变的)互斥是实现同步的一种手段 临界区、互斥量和信号量都是主要的互斥实现方式
Java中,最基本的互斥同步手段就是synchronized关键字。通过锁计数器+-1,实现对锁的加锁和释放。
synchronized是一个重量级的操作,因为:Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要消耗很多的处理器时间。对于代码简单的同步块(如synchronized修饰的getter()和setter方法),状态转换消耗的时间有可能比用户代码执行的时间还要长。而虚拟机本身也会进行一些优化,比如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态之中。
还可使用juc包中的ReentrantLock(重入锁)
来实现同步:JDK1.5多线程环境下synchronized的吞吐量下降的很严重,而ReentrantLock则基本保持在同一个比较稳定的水平上。JDK 1.6之后两者性能基本持平。
虚拟机在未来的性能改进中还会更偏向于原生的synchronize的,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此也称为阻塞同步 从处理问题的方式上说,互斥同步属于一种悲观的并发策略。随着硬件指令集的发展, 因为需要操作和冲突检测具有原子性,靠的就是硬件来完成这件事情,保证一个从语义看起来需要多次操作的行为只通过一条处理器指令就能完成
我们可以采用基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。
如果一个方法本来就不设计共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的。
引入的原因是互斥同步对性能最大的影响是阻塞,挂起线程和恢复线程都需要转入内核态完成,给并发性能带来很大压力。 自旋锁让物理机器有一个以上的处理器的时候,能让两个或以上的线程同时并行执行。我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
自旋锁的自旋次数默认值是10次,用户可以使用参数-XX:PreBlockSpin来更改。
JDK1.6引入了自适应的自旋锁。自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。(这个应该属于试探性的算法)。
JIT在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除 主要判定依据是逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据,认为它们是线程私有的,同步加锁自然就无需进行。
当多个彼此靠近的同步块可以合并到一起,形成一个同步块的时候,就会进行锁粗化。该方法还有一种变体,可以把多个同步方法合并为一个方法。如果所有方法都用一个锁对象,就可以尝试这种方法。
大多数锁,在它们的生命周期中,从来不会被多于一个线程所访问。即使在极少数情况下,多个线程真的共享数据了,锁也不会发生竞争。为了理解偏向锁的优势,我们首先需要回顾一下如何获取锁(监视器)。
获取锁的过程分为两部分。首先,你需要获得一份契约.一旦你获得了这份契约,就可以自由地拿到锁了。为了获得这份契约,线程必须执行一个代价昂贵的原子指令。释放锁同时就要释放契约。根据我们的观察,我们似乎需要对一些锁的访问进行优化,比如线程执行的同步块代码在一个循环体中。优化的方法之一就是将锁粗化,以包含整个循环。这样,线程只访问一次锁,而不必每次进入循环时都进行访问了。但是,这并非一个很好的解决方案,因为它可能会妨碍其他线程合法的访问。还有一个更合理的方案,即将锁偏向给执行循环的线程。
将锁偏向于一个线程,意味着该线程不需要释放锁的契约。因此,随后获取锁的时候可以不那么昂贵。如果另一个线程在尝试获取锁,那么循环线程只需要释放契约就可以了。Java 6的HotSpot/JIT默认情况下实现了偏向锁的优化。