文章已经同步到GitHub开源项目:JVM底层原理分析线程安全当多个线程同时访问一个对象时,如果不需要考虑调度和在运行环境中交替执行这些线程,你可以在调用方法时不需要考虑额外的同步,或者一些其他的协调,调用这个对象的行为可以获得正确的结果。那么这个对象就说是线程安全的。这个定义是严谨的,可操作的。它要求线程安全的代码必须有一个共同的特性。代码本身封装了所有必要的正确性保证(例如互斥同步等)。使得调用者无需关心多线程下的调用问题。您不需要自己实施任何措施来确保安全。Java中的线程安全在Java语言中,从JVM底层的角度来看,线程安全并不是一个黑白分明的二元排他选项。按照安全程度来划分,我们可以将Java中各种操作共享的数据分为五类:不可变、绝对线程安全、相对线程安全、线程兼容、线程对立,接下来我们一一介绍。不可变在Java中,不可变对象必须是安全的。比如用final修饰的变量。只要一个不可见的对象被创建,它的外部可见状态就不会改变,永远不会在多个线程中看到它处于不一致的状态。在Java中,不变性带来的线程安全是最直接、最纯粹的。基本数据类型:定义时使用final修饰即可。参考数据类型:由于目前Java中还没有相应的支持,所以我们需要对shared进行封装数据到不可变对象中。具体来说,我们可以将对象中的属性封装为final类型。这样构造方法结束后,他就是一个无可比拟的值。比如String、Integer、Number、Long、Double等基本数据类型的包装类,将value部分修改为final。String的源码私有最终字符值[];Integer的源码privatefinalint值;Double的源代码privatefinaldouble值;绝对线程安全绝对线程安全完全可以满足线程安全的定义,但是在Java中将自己标记为线程安全的类不一定是绝对线程安全的类。比如Vector类,众所周知,是一个线程安全的类,常用的方法都是用synchronized修饰的。但是,它不是绝对线程安全的。如果要做到绝对线程安全,就必须在内部维护一套一致的快照访问。每次对元素进行更改时,都会生成一个新的快照。但付出的时间和空间成本是巨大的。相对线程安全相对线程安全就是我们通常所说的线程安全。它需要确保对该对象的单个操作是安全的。在Java中,大多数声明为线程安全的类都处于这一级别。比如Vector、HashTable、Collections中用synchronizedCollection()方法包装的集合。线程兼容性线程兼容性是指对象本身不是线程安全的,但是在调用端通过正确使用同步手段(锁)可以保证并发下是安全的。Java中的大多数类都处于这个级别。如ArrayList、HashMap等。线程对立线程对立是指无论调用者如何进行同步锁,都无法保证并发下的线程安全。Java中这样的类很少,我们应该避免使用它们。如System.setIn()、System.setOut()等线程安全的实现方案在Java中实现线程安全主要有三种方案,互斥同步、非阻塞同步、非同步方案互斥synchronization(悲观锁)synchronized的实现该关键字被javac编译后,会生成两条字节码指令.monitorenter和monitorexit。例如下面的代码}}反编译后0ldc#22dup3astore_14monitorenter5aload_16monitorexit7goto15(+8)10astore_211aload_112monitorexit13aload_214athrow15return可以看出在4的偏移地址,有一个字节码指令monitorenter,表示synchronized的开始,也就是从哪里开始同步。在12的偏移地址处,有一个monitor出口,表示同步结束。这两个指令都需要一个引用类型的参数来指示要锁定的对象。如果在代码中指定,则使用指定的对象锁。如果出现在方法声明的位置,那么虚拟机会判断如果是实例方法就锁定实例对象,如果是静态方法就锁定类对象。执行monitorenter时,虚拟机首先尝试获取对象锁。如果获得了对象锁,或者当前线程已经有对象锁,则对象锁中对象头位置的锁计数器+1。当执行monitorexit时,它会是-1。一旦当前锁对象的锁计数器为0,当前线程就会释放该对象的对象锁。如果不是,则当前线程进入阻塞状态。直到对象锁的值变为0。即持有对象锁的线程释放锁。特点:可重入,同一个线程多次进入同步块不会被锁定。在同步块中执行的线程无条件地阻塞其他线程的进入。这意味着无法强制获得锁的线程释放锁,等待锁的进程也无法退出。从执行成本的角度来看,synchronized是一个重量级的操作。在主流的Java虚拟机实现中,Java线程被映射为操作系统的内核线程。如果要唤醒或阻塞线程,需要从用户态切换到内核态。这种转换很耗时。所以synchronized是一个重量级的操作。如有必要,请再次使用它。锁的实现JDK1.5之后,Java类库中新提供了java.util.concurrent包,其中的locks.Lock接口成为了Java中另一种互斥同步的手段。接口定义如下publicinterfaceLock{//获取锁。如果锁已被另一个线程获取,请等待。空锁();//如果线程正在等待获取锁,线程可以响应中断,即打断线程的等待状态。voidlockInterruptibly()抛出InterruptedException;//尝试获取锁,如果获取成功,返回true,否则返回false,立即返回,不会像锁一样等待booleantryLock();//等待一定时间无法获取到锁,在超时未获取到锁的情况下,返回false。booleantryLock(longtime,TimeUnitunit)throwsInterruptedException;无效解锁();ConditionnewCondition();}使用Lock接口的实现类,用户可以实现非块结构的互斥同步,从而摆脱语言的束缚,而是在类库层面实现同步,这也为以后扩展不同的调度算法、不同的特性、不同性能的各种锁提供了空间。ReentrantLock是Lock接口最常见的实现。顾名思义,他和synchronized一样是可重入的。写法如下publicstaticvoidmain(String[]args){Locklock=newReentrantLock();锁.锁();try{//处理任务}catch(Exceptionex){}finally{lock.unlock();/如果在finally时没有释放释放锁,可能会出现死锁}}ReentrantLock相比synchronized增加了以下功能。等待可以被打断当持有锁的线程长时间没有释放锁时,等待的线程可以选择放弃等待。对处理执行时间长的同步块很有帮助。公平锁当多个线程在等待同一个锁时,必须按照申请的先后顺序获取锁。syn是不公平的,reentrantLock默认也是不公平的,需要在构造函数中传入true来指定使用公平锁。(使用公平锁会导致性能急剧下降)绑定多个条件的锁一个ReentrantLock对象可以同时绑定多个Condition对象。只需多次调用newCondition方法即可。这种互斥同步方案的主要问题是在线程被阻塞唤醒时会带来性能开销。从解决问题的方式来看,互斥同步(阻塞同步)是一种悲观的并发策略,认为只要有其他线程过来,数据就一定会被修改。不管会不会修改,都会加锁(这里讨论的是概念模型,实际虚拟机会优化一些不必要的加锁)。这样会造成用户态和内核态的频繁切换,需要维护锁计数器。比较麻烦。非阻塞同步(乐观锁)基于冲突检测的乐观并发策略。通俗地说,不顾风险,先操作。如果数据没有被修改,则修改成功。如果数据被修改,请继续重试。直到没有争用的共享数据。这种方案需要硬件的开发,因为检测是否修改和最后写入这两个操作必须保证原子性。如果用之前的互斥同步来解决这个,就没有意义了,所以需要硬件层面的支持。确保语义上看起来像多个操作的行为只需要一个处理器指令即可完成。常见的此类指令包括测试和设置TestAndSet获取和增加FetchAndIncrement交换Swap比较交换:CompareAndSwap使用比较交换CAS指令完成Java中的乐观锁。CAS指令需要三个操作数,旧期望值A,内存位置V,新值B。当旧期望值与内存中的真实值相同时,用新值替换旧值。否则不会更新。JDK1.5以后,Java类库中使用CAS操作,由sun.misc.Unsafe类中的方法封装提供。虚拟机对这些方法进行特殊处理,保证编译后是平台相关的处理器CAS指令。例如,AtomicInteger是一个包装了CAS指令的线程安全类。它的方法设置在无限循环中,它不断尝试为内存位置的值分配一个新值。如果失败了,说明已经被其他线程修改了,于是再次循环进行下一步操作,直到修改位置成功。CAS虽然看起来很漂亮,但它有一个逻辑漏洞。当其他线程把值从A改成B,再改回A时,当前线程是不会发现的。这个漏洞被称为CAS的ABA问题。为了解决这个问题,JUC提供了一个带标记的原子引用类AtomicStampedReference。通过控制变量值的版本来解决。文章已同步至GitHub开源项目:JVM底层原理解析