当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
如果一段代码根本不会与其他线程共享数据,那么从线程安全的角度看,程序是串行执行还是多线程执行对于它来说是完全没有区别的。
不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。
例如: 使用 final
关键字修饰为不可变
即在不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步。
需要付出极大的代价,甚至不可能。Java API 中绝大部分代码都不是绝对线程安全的。
它需要保证对这个对象的操作是线程安全的,我们在调用的实话不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
对象本身并不是线程安全的,但可以通过调用端正确的使用同步手段来保障对象在并发环境中的安全使用。
无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。
线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应尽量避免。
保障共享的数据在同一时刻只被一个(或一些,使用信号量的时候)线程使用。
互斥实现方式: 临界区、互斥量、信号量
对于同一个线程是可重入的,不会出现把自己锁死的问题。
synchronized
的每次阻塞与唤醒一个线程,都需要从用户态转化为核心态,而转化消耗的时间可能比用户执行代码时间还长,因此在必要时再使用。
位于 java.util.concurrent(J.U.C)
包下
高级特性:
非公平锁
,可通过构造函数改为公平锁
。ReentrantLock
可同时绑定多个 Condition 对象
来与多个条件关联,只需要多次调用 new Condition()
方法即可。基于冲突检测的乐观并发策略: 先进行操作,如果没有其他线程争用共享数据,那操作就成功;如果出现争用,产生冲突,采取补偿措施。
方法本就不涉及共享同步,自然无需任何同步措施。
这种代码也叫纯代码,可以在代码执行的任何时刻中断,转而去执行别的代码,返回控制权后原来的代码也不会有任何问题。
如果共享数据的代码能够在同一个线程中执行,我们就可以把共享数据的可见性范围限制在同一个线程内。
应用场景: "生产者-消费者" 模式
中,将产品消费的过程尽量在同一个线程中执行。
自旋锁: 如果一个及其有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面的请求锁的线程等待一会儿,但不放弃处理器的执行时间,看看持有锁的线程能否很快的释放锁。这个让线程等待的方式,只需让线程执行一个忙循环(自旋),这项技术就是自旋锁。
JDK 1.4.2: 引入,默认关闭,可通过 -XX:+UseSpining
参数开启。默认自旋 10 次,可通过参数 -XX:PreBlockSpin
控制。
JDK 1.6: 默认开启。默认自旋 10 次,可通过参数 -XX:PreBlockSpin
控制。
优点: 由于线程间的切换,需要在用户态与核心态之间进行切换,会浪费大量时间,因此如果上个线程能快速结束,就能够节省线程切换所需的时间,提升效率。
缺点:
自适应自旋: 自旋锁的加强版,即针对同一个锁对象,如果自旋等待刚刚成功,且持有锁的线程正在运行,那么虚拟机就会认为这个自旋的成功率很高,进而允许自旋锁持有更长的等待时间,反之可能忽略自旋等待,避免浪费资源。
锁消除: 虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
锁粗化: 如果虚拟机探测到有这样一串连续的操作:频繁的对同一个对象进行加锁和解锁,甚至加锁操作是出现在循环体中,这种频繁的互斥同步操作导致不必要的性能浪费。将会把加锁的同步范围扩大到整个操作序列外面。
轻量级锁: 在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
在没有竞争的情况下,轻量级锁使用 CAS 操作避免了使用互斥量的开销。
在存在竞争的情况下,除了存在互斥量的开销,还额外发生 CAS 操作,此时轻量级锁会比传统的重量级锁更慢。
偏向锁: 在无竞争的情况下把整个同步都消除掉,连 CAS 都不做。