一 java线程同步原理
java中的同步使用到了 Monitor(管程)机制
java会为每个object对象分配一个monitor,当某个对象的同步方法(synchronized methods)被多个线程调用时,该对象的monitor将负责处理这些访问的并发独占要求。
当一个线程调用一个对象的同步方法时,JVM会检查该对象的monitor。如果monitor没有被占用,那么这个线程就得到了monitor的占有权,可以继续执行该对象的同步方法;如果monitor被其他线程所占用,那么该线程将被挂起,直到monitor被释放。
当线程退出同步方法调用时,该线程会释放monitor,这将允许其他等待的线程获得monitor以使对同步方法的调用执行下去。
注意:Java对象的monitor机制和传统的临界检查代码区技术不一样。java的一个同步方法并不意味着同时只有一个线程独占执行,但临界检查代码区技术确实会保证同步方法在一个时刻只被一个线程独占执行。Java的monitor机制的准确含义是:任何时刻,对一个指定object对象的某同步方法只能由一个线程来调用。
java对象的monitor是跟随object实例来使用的,而不是跟随程序代码。两个线程可以同时执行相同的同步方法,比如:一个类的同步方法是xMethod(),有a,b两个对象实例,一个线程执行a.xMethod(),另一个线程执行b.xMethod().互不冲突。
二 synchronized 和 volatile 、ReentrantLock 的区别
Synchronized和volatile的比较
1)Synchronized保证内存可见性和操作的原子性,Volatile只能保证内存可见性。
2)volatile不需要加锁,比Synchronized更轻量级,并不会阻塞线程(volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。)
4)volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化(如编译器重排序的优化).
5)volatile是变量修饰符,仅能用于变量,而synchronized是一个方法或块的修饰符。
ReentrantLock 和 Synchronized
1、可重入性:
2、锁的实现:
3、性能的区别:
4、功能区别:
5、锁的细粒度和灵活度
1、可重入性:
从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。两者都是同一个线程没进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
2、锁的实现:
Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。
3、性能的区别:
在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
4、功能区别:
便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
5、锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized
ReenTrantLock独有的能力:
1、ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
2、ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
3、ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
ReenTrantLock实现的原理:
在网上看到相关的源码分析,本来这块应该是本文的核心,但是感觉比较复杂就不一一详解了,简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。
什么情况下使用ReenTrantLock:
答案是,如果你需要实现ReenTrantLock的三个独有功能时。
三 悲观锁乐观锁
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改, 每次都加锁
共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁
假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量
两种锁的使用场景
乐观锁适用于多读少写,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
悲观锁适用于多写少读的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能
乐观锁常见的两种实现方式
CAS 和 版本号管理,
AtomicReference类计语CAS实现, 但会有 ABA 的问题, AtomicStampedReference 同时有cas 和版本号管理的实现
乐观锁(CAS)的思想是不加锁,那不加锁如何确保某一变量的操作没有被其他线程修改过?
这里就需要CAS操作(CompareAndSwap)来实现。
CAS有三个操作参数:内存地址,期望值,要修改的新值,当期望值和内存当中的值进行比较不相等的时候,表示内存中的值已经被别线程改动过,这时候失败返回,只有相等时,才会将内存中的值改为新的值,并返回成功。
public final long getAndIncrement() {
return unsafe.getAndAddLong(this, valueOffset, 1L);
}
public final long getAndAddLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
可以看到AtomicLong.getAndIncrement()的实现就是通过CAS循环操作的实现,只有期望值与真实值相同情况下,CAS操作才会成功执行,退出循环,如果失败则继续自旋,直到成功。
ABA问题
ABA问题是指在CAS操作时,其他线程将变量值A改为了B,但是又被改回了A,等到本线程使用期望值A与当前变量进行比较时,发现变量A没有变,于是CAS就将A值进行了交换操作,但是实际上该值已经被其他线程改变过,这与乐观锁的设计思想不符合。ABA问题的解决思路是,每次变量更新的时候把变量的版本号加1,那么A-B-A就会变成A1-B2-A3,只要变量被某一线程修改过,改变量对应的版本号就会发生递增变化,从而解决了ABA问题。