这篇文章分为六个部分,不一样特性的锁分类,并发锁的不一样设计,Synchronized中的锁升级,ReentrantLock和ReadWriteLock的应用,帮助你梳理 Java 并发锁及相关的操做。java
通常咱们提到的锁有如下这些:算法
上面是不少锁的名词,这些分类并非全是指锁的状态,有的指锁的特性,有的指锁的设计,下面分别说明。数据库
乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不一样角度,在Java和数据库中都有此概念对应的实际应用。编程
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可使用版本号等机制。数组
乐观锁适用于多读的应用类型,乐观锁在Java中是经过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操做就经过CAS自旋实现的。缓存
CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的状况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是经过CAS来实现了乐观锁。安全
简单来讲,CAS算法有3个三个操做数:数据结构
当且仅当预期值A和内存值V相同时,将内存值V修改成B,不然返回V。这是一种乐观锁的思路,它相信在它修改以前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改以前,必定会有其它线程去修改它,悲观锁效率很低。多线程
老是假设最坏的状况,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。架构
传统的MySQL关系型数据库里边就用到了不少这种锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁。
就是很公平,在并发环境中,每一个线程在获取锁时会先查看此锁维护的等待队列,若是为空,或者当前线程是等待队列的第一个,就占有锁,不然就会加入到等待队列中,之后会按照FIFO的规则从队列中取到本身。
公平锁的优势是等待锁的线程不会饿死。缺点是总体吞吐效率相对非公平锁要低,等待队列中除第一个线程之外的全部线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
上来就直接尝试占有锁,若是尝试失败,就再采用相似公平锁那种方式。
非公平锁的优势是能够减小唤起线程的开销,总体的吞吐效率高,由于线程有概率不阻塞直接得到锁,CPU没必要唤醒全部线程。缺点是处于等待队列中的线程可能会饿死,或者等好久才会得到锁。
java jdk并发包中的ReentrantLock能够指定构造函数的boolean类型来建立公平锁和非公平锁(默认),好比:公平锁可使用new ReentrantLock(true)实现。
是指该锁一次只能被一个线程所持有。
是指该锁可被多个线程所持有。
对于Java ReentrantLock而言,其是独享锁。可是对于Lock的另外一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
抽象队列同步器(AbstractQueuedSynchronizer,简称AQS)是用来构建锁或者其余同步组件的基础框架,它使用一个整型的volatile变量(命名为state)来维护同步状态,经过内置的FIFO队列来完成资源获取线程的排队工做。
concurrent包的实现结构如上图所示,AQS、非阻塞数据结构和原子变量类等基础类都是基于volatile变量的读/写和CAS实现,而像Lock、同步器、阻塞队列、Executor和并发容器等高层类又是基于基础类实现。
相交进程之间的关系主要有两种,同步与互斥。所谓互斥,是指散布在不一样进程之间的若干程序片段,当某个进程运行其中一个程序片断时,其它进程就不能运行它们之中的任一程序片断,只能等到该进程运行完这个程序片断后才能够运行。所谓同步,是指散布在不一样进程之间的若干程序片段,它们的运行必须严格按照规定的某种前后次序来运行,这种前后次序依赖于要完成的特定的任务。
显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。
也就是说互斥是两个线程之间不能够同时运行,他们会相互排斥,必须等待一个线程运行完毕,另外一个才能运行,而同步也是不能同时运行,但他是必需要安照某种次序来运行相应的线程(也是一种互斥)!
总结:互斥:是指某一资源同时只容许一个访问者对其进行访问,具备惟一性和排它性。但互斥没法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数状况),经过其它机制实现访问者对资源的有序访问。在大多数状况下,同步已经实现了互斥,特别是全部写入资源的状况一定是互斥的。少数状况是指能够容许多个访问者同时访问资源。
在访问共享资源以前对进行加锁操做,在访问完成以后进行解锁操做。 加锁后,任何其余试图再次加锁的线程会被阻塞,直到当前进程解锁。
若是解锁时有一个以上的线程阻塞,那么全部该锁上的线程都被编程就绪状态, 第一个变为就绪状态的线程又执行加锁操做,那么其余的线程又会进入等待。 在这种方式下,只有一个线程可以访问被互斥锁保护的资源
这个时候读写锁就应运而生了,读写锁是一种通用技术,并非Java特有的。
读写锁特色:
互斥锁特色:
Linux内核也支持读写锁。
互斥锁 pthread_mutex_init() pthread_mutex_lock() pthread_mutex_unlock() 读写锁 pthread_rwlock_init() pthread_rwlock_rdlock() pthread_rwlock_wrlock() pthread_rwlock_unlock() 条件变量 pthread_cond_init() pthread_cond_wait() pthread_cond_signal()
自旋锁(spinlock):是指当一个线程在获取锁的时候,若是锁已经被其它线程获取,那么该线程将循环等待,而后不断的判断锁是否可以被成功获取,直到获取到锁才会退出循环。
在Java中,自旋锁是指尝试获取锁的线程不会当即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减小线程上下文切换的消耗,缺点是循环会消耗CPU。
典型的自旋锁实现的例子,能够参考自旋锁的实现
它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较相似,它们都是为了解决对某项资源的互斥使用。不管是互斥锁,仍是自旋锁,在任什么时候刻,最多只能有一个保持者,也就说,在任什么时候刻最多只能有一个执行单元得到锁。可是二者在调度机制上略有不一样。对于互斥锁,若是资源已经被占用,资源申请者只能进入睡眠状态。
可是自旋锁不会引发调用者睡眠,若是自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是所以而得名。
下面是个简单的例子:
public class SpinLock { private AtomicReference<Thread> cas = new AtomicReference<Thread>(); public void lock() { Thread current = Thread.currentThread(); // 利用CAS while (!cas.compareAndSet(null, current)) { // DO nothing } } public void unlock() { Thread current = Thread.currentThread(); cas.compareAndSet(current, null); } }
lock()方法利用的CAS,当第一个线程A获取锁的时候,可以成功获取到,不会进入while循环,若是此时线程A没有释放锁,另外一个线程B又来获取锁,此时因为不知足CAS,因此就会进入while循环,不断判断是否知足CAS,直到A线程调用unlock方法释放了该锁。
根据所锁的设计方式和应用,有分段锁,读写锁等。
分段锁实际上是一种锁的设计,并非具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是经过分段锁的形式来实现高效的并发操做。
以ConcurrentHashMap来讲一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即相似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每一个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当须要put元素的时候,并非对整个hashmap进行加锁,而是先经过hashcode来知道他要放在那一个分段中,而后对这个分段进行加锁,因此当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
可是,在统计size的时候,可就是获取hashmap全局信息的时候,就须要获取全部的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操做不须要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操做。
锁消除,如无必要,不要使用锁。Java 虚拟机也能够根据逃逸分析判断出加锁的代码是否线程安全,若是确认线程安全虚拟机会进行锁消除提升效率。
锁粗化。若是一段代码须要使用多个锁,建议使用一把范围更大的锁来提升执行效率。Java 虚拟机也会进行优化,若是发现同一个对象锁有一系列的加锁解锁操做,虚拟机会进行锁粗化来下降锁的耗时。
轮询锁是经过线程不断尝试获取锁来实现的,能够避免发生死锁,能够更好地处理错误场景。Java 中能够经过调用锁的 tryLock 方法来进行轮询。tryLock 方法还提供了一种支持定时的实现,能够经过参数指定获取锁的等待时间。若是能够当即获取锁那就当即返回,不然等待一段时间后返回。
读写锁 ReadWriteLock 能够优雅地实现对资源的访问控制,具体实现为 ReentrantReadWriteLock。读写锁提供了读锁和写锁两把锁,在读数据时使用读锁,在写数据时使用写锁。
读写锁容许有多个读操做同时进行,但只容许有一个写操做执行。若是写锁没有加锁,则读锁不会阻塞,不然须要等待写入完成。
ReadWriteLock lock = new ReentrantReadWriteLock(); Lock readLock = lock.readLock(); Lock writeLock = lock.writeLock();
synchronized 代码块是由一对儿 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现单元。
在 Java 6 以前,Monitor 的实现彻底是依靠操做系统内部的互斥锁,由于须要进行用户态到内核态的切换,因此同步操做是一个无差异的重量级操做。
现代的(Oracle)JDK 中,JVM 对此进行了大刀阔斧地改进,提供了三种不一样的 Monitor 实现,也就是常说的三种不一样的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
锁的状态是经过对象监视器在对象头中的字段来代表的。
四种状态会随着竞争的状况逐渐升级,并且是不可逆的过程,即不可降级。
这四种状态都不是Java语言中的锁,而是Jvm为了提升锁的获取与释放效率而作的优化(使用synchronized时)。
这三种锁是指锁的状态,而且是针对Synchronized。在Java 5经过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是经过对象监视器在对象头中的字段来代表的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。下降获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另外一个线程所访问,偏向锁就会升级为轻量级锁,其余线程会经过自旋的形式尝试获取锁,不会阻塞,提升性能。
重量级锁是指当锁为轻量级锁的时候,另外一个线程虽然是自旋,但自旋不会一直持续下去,当自旋必定次数的时候,尚未获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其余申请的线程进入阻塞,性能下降。
所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不一样的竞争情况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操做(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,因此并不涉及真正的互斥锁。这样作的假设是基于在不少应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁能够下降无竞争开销。
若是有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就须要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操做 Mark Word 来试图获取锁,若是重试成功,就使用普通的轻量级锁;不然,进一步升级为重量级锁。
ReentrantLock,一个可重入的互斥锁,它具备与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
public class LockTest { private Lock lock = new ReentrantLock(); public void testMethod() { lock.lock(); for (int i = 0; i < 5; i++) { System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1))); } lock.unlock(); } }
synchronized与wait()和nitofy()/notifyAll()方法相结合能够实现等待/通知模型,ReentrantLock一样能够,可是须要借助Condition,且Condition有更好的灵活性,具体体如今:
在并发场景中用于解决线程安全的问题,咱们几乎会高频率的使用到独占式锁,一般使用java提供的关键字synchronized(关于synchronized能够看这篇文章)或者concurrents包中实现了Lock接口的ReentrantLock。
它们都是独占式获取锁,也就是在同一时刻只有一个线程可以获取锁。而在一些业务场景中,大部分只是读数据,写数据不多,若是仅仅是读数据的话并不会影响数据正确性(出现脏读),而若是在这种业务场景下,依然使用独占锁的话,很显然这将是出现性能瓶颈的地方。
针对这种读多写少的状况,java还提供了另一个实现Lock接口的ReentrantReadWriteLock(读写锁)。读写所容许同一时刻被多个读线程访问,可是在写线程访问时,全部的读线程和其余的写线程都会被阻塞。
ReadWriteLock,顾明思义,读写锁在读的时候,上读锁,在写的时候,上写锁,这样就很巧妙的解决synchronized的一个性能问题:读与读之间互斥。
ReadWriteLock也是一个接口,原型以下:
public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }
该接口只有两个方法,读锁和写锁。
也就是说,咱们在写文件的时候,能够将读和写分开,分红2个锁来分配给线程,从而能够作到读和读互不影响,读和写互斥,写和写互斥,提升读写文件的效率。
下面的实例参考《Java并发编程的艺术》,使用读写锁实现一个缓存。
import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class Cache { static Map<String,Object> map = new HashMap<String, Object>(); static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); static Lock readLock = readWriteLock.readLock(); static Lock writeLock = readWriteLock.writeLock(); public static final Object getByKey(String key){ readLock.lock(); try{ return map.get(key); }finally{ readLock.unlock(); } } public static final Object getMap(){ readLock.lock(); try{ return map; }finally{ readLock.unlock(); } } public static final Object put(String key,Object value){ writeLock.lock(); try{ return map.put(key, value); }finally{ writeLock.unlock(); } } public static final Object remove(String key){ writeLock.lock(); try{ return map.remove(key); }finally{ writeLock.unlock(); } } public static final void clear(){ writeLock.lock(); try{ map.clear(); }finally{ writeLock.unlock(); } } public static void main(String[] args) { List<Thread> threadList = new ArrayList<Thread>(); for(int i =0;i<6;i++){ Thread thread = new PutThread(); threadList.add(thread); } for(Thread thread : threadList){ thread.start(); } put("ji","ji"); System.out.println(getMap()); } private static class PutThread extends Thread{ public void run(){ put(Thread.currentThread().getName(),Thread.currentThread().getName()); } } }
读写锁支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁可以降级成为读锁,不支持锁升级,关于锁降级下面的示例代码摘自ReentrantWriteReadLock源码中:
void processCachedData() { rwl.readLock().lock(); if (!cacheValid) { // Must release read lock before acquiring write lock rwl.readLock().unlock(); rwl.writeLock().lock(); try { // Recheck state because another thread might have // acquired write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } // Downgrade by acquiring read lock before releasing write lock rwl.readLock().lock(); } finally { rwl.writeLock().unlock(); // Unlock write, still hold read } } try { use(data); } finally { rwl.readLock().unlock(); } } }
关注公众号:架构进化论,得到第一手的技术资讯和原创文章