显式锁
按照Java虚拟机对锁的实现方式划分,Java平台中的锁包括内部锁和显式锁。内部锁是通过synchronized关键字实现的;显式锁是通过java.util.concurrent.locks.Lock
接口的实现类(如java.util.concurrent.locks.ReentrantLock类)实现的。
锁能够保护共享数据以实现线程安全,其作用包括保障原子性、保障可见性和保障有序性。
内部锁synchronized关键字前面有介绍:JAVA并发编程——线程协作通信(一)
一个线程执行到同步代码块的时候必须先申请该同步块的引导锁,只有申请成功该锁的线程才能够执行相应的临界区。一个线程执行完临界区代码后引导该临界区的锁就会被自动释放。在这个过程中,线程对内部锁的申请与释放的动作由Java虚拟机负责代为实施,这也正是synchronized实现的锁被称为内部锁的原因。
Lock接口
显式锁是java.util.concurrent.locks.Lock接口的实例该接口对显式锁进行了抽象,类java.util.concurrent.locks.ReentrantLock是Lock接口的默认实现类。Lock接口定义的方法如下所示:
void lock()
获取锁。如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态void lockInterruptibly()
如果当前线程未被中断,则获取锁。Condition newCondition()
返回绑定到此 Lock 实例的新 Condition 实例boolean tryLock()
仅在调用时锁为空闲状态才获取该锁。如果锁可用,则获取锁,并立即返回值 true。如果锁不可用,则此方法将立即返回值 false。boolean tryLock(long time, TimeUnit unit)
如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。void unlock()
释放锁。在等待条件前,锁必须由当前线程保持。调用Condition.await()
将在等待前以原子方式释放锁,并在等待返回前重新获取锁。
一个Lock接口实例就是一个显式锁对象,Lock接口定义的lock方法和unlock方法分别用于申请和释放相应的Lock实例表示的锁。显式锁的使用方法如下所示:
//创建一个Lock接口实例
Lock lock = new ReentrantLock();
//申请lock
lock.lock();
try {
//do business......
} finally {
//总是在finally块中释放锁,以避免锁泄漏
lock.unlock();
}
代码示例:
public class LockCase {
private Lock lock = new ReentrantLock();
private int age = 100000;//初始100000
private static class TestThread extends Thread{
private LockCase lockCase;
public TestThread(LockCase lockCase,String name) {
super(name);
this.lockCase = lockCase;
}
@Override
public void run() {
for(int i=0;i<100000;i++) {//递增100000
lockCase.test();
}
System.out.println(Thread.currentThread().getName()
+" age = "+lockCase.getAge());
}
}
public void test() {
lock.lock();
try {
age++;
} finally {
lock.unlock();
}
}
public void test2() {
lock.lock();
try {
age--;
} finally {
lock.unlock();
}
}
public int getAge() {
return age;
}
public static void main(String[] args) throws InterruptedException {
LockCase lockCase = new LockCase();
Thread endThread = new TestThread(lockCase,"endThread");
endThread.start();
for(int i=0;i<100000;i++) {//递减100000
lockCase.test2();
}
System.out.println(Thread.currentThread().getName()
+" age = "+lockCase.getAge());
}
}
显式锁(Lock)与内部锁(synchronized)的比较
内部锁不够灵活,锁的申请和释放只能在一份方法内(方法块无法跨方法)进行。
显示锁支持在一个方法中申请锁,在另一个方法中释放锁。
内部锁不会产生锁泄漏。显示锁会可能会产生锁泄漏,写代码的时候要注意。
内部锁是非公平锁。显示锁可以是公平锁,也可是非公平锁。
内部锁当线程申请不到内部锁的时候会一直阻塞。
当线程使用显示锁的tryLock()方法去申请锁时,如果申请不到,不会一直阻塞,可以去干其他的事情。显示锁可以:
- 1、 尝试非阻塞获取锁
- 2、 可以被中断的获取锁
- 3、 超时获取锁
读写锁(ReadWriteLock)
略
CAS乐观锁
乐观锁与悲观锁
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
悲观锁机制存在以下问题:
在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
一个线程持有锁会导致其它所有需要此锁的线程挂起。
如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
适用场景
悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。
乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
总结:两种所各有优缺点,读取频繁使用乐观锁,写入频繁使用悲观锁。
CAS(Compare and Swap)
CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS 操作中包含三个操作数:需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了”我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。“这其实和乐观锁的冲突检查+数据更新的原理是一样的。
这里再强调一下,乐观锁是一种思想。CAS是这种思想的一种实现方式。
JAVA对CAS的支持
CAS在Java中的应用,即并发包中的原子操作类(Atomic系列),从JDK 1.5开始提供了java.util.concurrent.atomic包,在该包中提供了许多基于CAS实现的原子操作类,用法方便,性能高效,主要分以下4种类型。
原子更新基本类型主要包括3个类:
AtomicBoolean:原子更新布尔类型
AtomicInteger:原子更新整型
AtomicLong:原子更新长整型
原子更新引用数据类型主要包括:
AtomicReference 原子操作引用对象类型
CAS缺点
ABA问题
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但可能存在潜藏的问题。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
循环时间长开销大
自旋CAS(不成功,就一直循环执行,直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。