文章已同步至GitHub开源项目: JVM底层原理解析 从JVM角度解析Java是如何保证线程安全的
当多个线程同时访问一个对象,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要考虑额外的同步,或者在调用方法时进行一些其他的协作,调用这个对象的行为都可以获得正确的结果。那么就称这个对象是线程安全的。
这个定义是严谨并且有可操作性的,他要求线程安全的代码都必须具备一个共同的特性。代码本身封装了所有必要的正确性保障手段(如互斥同步等)。令调用者无需关心多线程下的调用问题。更无需自己实现任何措施来保证安全。
在Java语言中,从JVM底层来看的话,线程安全并不是一个非黑即白的二元排他选项,按照安全程度
来划分,我们可以将Java中各种操作共享的数据分为五类:不可变
、绝对线程安全
、相对线程安全
、线程兼容
、线程对立
接下来,我们一一介绍。
在Java中,实现线程安全,主要有三种方案, 互斥同步
、非阻塞同步
、无同步方案
synchronized的实现
此关键字经过javac编译之后,会生成两条字节码指令.monitorenter
和monitorexit
。
比如以下代码
public synchronized void dosomething(){
synchronized (SynchronizedTest.class){
System.out.println("do something");
}
}
反编译之后
0 ldc #2 <cn/shaoxiongdu/chapter6/SynchronizedTest>
2 dup
3 astore_1
4 monitorenter
5 aload_1
6 monitorexit
7 goto 15 (+8)
10 astore_2
11 aload_1
12 monitorexit
13 aload_2
14 athrow
15 return
可以看到,在偏移址为4的地方,有一条字节码指令monitorenter
,表示synchronized
开始的地方,也就是表示开启同步的位置,在偏移址为12的地方,有一条monitorexit
表示同步结束的地方。
这两个指令都需要一个引用类型的参数来指明需要锁住的对象。如果代码中指定了,则使用指定的对象锁,如果出现在方法声明位置,那么虚拟机会判断,如果是实例方法则锁实例对象,如果是静态方法则锁类对象。
在执行monitorenter
时,首先虚拟机会尝试获取对象锁
monitorexit
时,会将其-1。一旦当前锁对象的锁计数器为0,则当前线程就会释放对象的对象锁。特征:
从执行的成本来看,synchronized
是一个重量级的操作。主流的Java虚拟机实现中,Java的线程是映射到操作系统的内核线程中的,如果要唤醒或者阻塞一个线程,需要从用户态切换到内核态。这种转化是很耗时的。所以synchronized
是一个重量级的操作。在有必要的情况下,再去使用其。
lock的实现
在JDK1.5之后,Java类库中新提供了java.util.concurrent包,其中的locks.Lock接口便成为Java另外一种互斥同步的手段。
该接口的定义如下
public interface Lock {
//获取锁。如果锁已被其他线程获取,则进行等待。
void lock();
//如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。
void lockInterruptibly() throws InterruptedException;
//尝试获取锁,如果获取成功,则返回true 否则返回false 立即返回 不会和lock一样等待
boolean tryLock();
//拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
使用Lock接口的实现类,用户可以以非块结构
来实现互斥同步,从而摆脱了语言的束缚,改为在类库层面去实现同步,这也为日后扩展出不同的调度算法,不同的特性,不同性能的各种锁提供了空间。
重入锁(ReentrantLock)是Lock接口中最常见的一种实现方式。故名思意,他和synchronized
一样是可以重入的。写法如下
public static void main(String[] args) {
Lock lock = new ReentrantLock();
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁 不在finally处释放可能会发生死锁
}
}
相比synchronized
,ReentrantLock
增加了如下的功能。
syn
是非公平的,reentrantLock
默认也是非公平的,需要在构造函数中传入true指定使用公平锁。(使用公平锁会导致性能急剧下降)
ReentrantLock
对象可以同时绑定多个Condition对象。只需要多次调用newCondition方法即可。
这种互斥同步的放方案主要问题是在线程阻塞和唤醒的时候会带来性能开销问题。从解决问题的方式上看,互斥同步(阻塞同步)属于一种悲观的并发策略,认为只要是别的线程过来,就一定会修改数据。无论是否真的会修改,他都会进行加锁(此处讨论的是概念模型,实际虚拟机会优化一些不必要的加锁)。这会导致用户态和内核态频繁切换,并且需要维护锁的计数器。比较繁琐。
基于冲突检测的乐观并发策略。
通俗的说,就是不管风险,先进行操作。如果数据没有被修改,则修改成功。如果数据被修改,则不断重试。直到出现没有竞争的共享数据为止。
此种方案需要硬件的发展,因为进行检测是否修改
和最终写入
这两个操作必须保证原子性。如果这里用前边的互斥同步来解决,就没有什么意义了,所以需要硬件层面的支持。确保在语义上看起来有多个操作的行为只需要一条处理器指令就可以完成。常见的这种指令有
测试并设置 TestAndSet
获取并增加 FetchAndIncrement
交换 Swap
比较和交换: CompareAndSwap
在Java中完成乐观锁用的是比较和交换
CAS指令。
CAS指令需要有三个操作数,一个是旧的预期值A,一个是内存位置V,还有一个新值B。
当旧的预期值与内存中真正的值相同的时候,就将旧值替换为新值。否则就不更新。
在JDK1.5之后,Java类库中才开始使用CAS操作,该操作由 sun.misc.Unsafe
类中的方法包装提供。虚拟机会对这些方法进行特殊处理,保证编译之后是一条平台相关的处理器CAS指令。
比如AtomicInteger
就是包装了CAS指令之后的线程安全类,他的方法都设置在一个死循环中,不断尝试将一个新值赋给内存位置的值,如果失败,说明被其他线程改了,于是再次循环进行下一次操作,直到修改成功位置。
尽管CAS看起来很美好,但是它存在一个逻辑漏洞,当别的线程将值从A改为B,然后又改回A的时候,当前线程是不会发现的。这个漏洞叫做CAS的ABA问题
,JUC为了解决这个问题,提供了一个带有标记的原子引用类AtomicStampedReference
。它通过控制变量值的版本来解决。
文章已同步至GitHub开源项目: JVM底层原理解析