Java锁详解:“独享锁/共享锁+公平锁/非公平锁+乐观锁/悲观锁+线程锁”

在Java并发场景中,会涉及到各类各样的锁如公平锁,乐观锁,悲观锁等等,这篇文章介绍各类锁的分类:java

公平锁/非公平锁mysql

可重入锁web

独享锁/共享锁算法

乐观锁/悲观锁sql

分段锁数据库

自旋锁编程

线程锁数组

乐观锁 VS 悲观锁

乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不一样角度,在Java和数据库中都有此概念对应的实际应用。缓存

1.乐观锁安全

顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可使用版本号等机制。

乐观锁适用于多读的应用类型,乐观锁在Java中是经过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操做就经过CAS自旋实现的。

CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的状况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是经过CAS来实现了乐观锁。

简单来讲,CAS算法有3个三个操做数:

  • 须要读写的内存值 V。
  • 进行比较的值 A。
  • 要写入的新值 B。

当且仅当预期值A和内存值V相同时,将内存值V修改成B,不然返回V。这是一种乐观锁的思路,它相信在它修改以前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改以前,必定会有其它线程去修改它,悲观锁效率很低

2.悲观锁

老是假设最坏的状况,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

传统的MySQL关系型数据库里边就用到了不少这种锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁。

相对其余数据库而言,MySQL的锁机制比较简单,其最显著的特色是不一样的存储引擎支持不一样的锁机制。

好比:

  1. MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking);
  2. InnoDB存储引擎既支持行级锁( row-level locking),也支持表级锁,但默认状况下是采用行级锁。

MySQL主要的两种锁的特性可大体概括以下:

阿里P8架构师谈:MySQL行锁、表锁、悲观锁、乐观锁的特色与应用

  • 表级锁: 开销小,加锁快;不会出现死锁(由于MyISAM会一次性得到SQL所需的所有锁);锁定粒度大,发生锁冲突的几率最高,并发度最低。
  • 行级锁: 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的几率最低,并发度也最高。
  • 页锁:开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度通常

行锁 和 表锁

1.主要是针对锁粒度划分的,通常分为:行锁、表锁、库锁

(1)行锁:访问数据库的时候,锁定整个行数据,防止并发错误。

(2)表锁:访问数据库的时候,锁定整个表数据,防止并发错误。

2.行锁 和 表锁 的区别:

  • 表锁: 开销小,加锁快,不会出现死锁;锁定力度大,发生锁冲突几率高,并发度最低
  • 行锁: 开销大,加锁慢,会出现死锁;锁定粒度小,发生锁冲突的几率低,并发度高

悲观锁 和 乐观锁

(1)悲观锁:顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。

传统的关系型数据库里边就用到了不少这种锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁。

(2)乐观锁: 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可使用版本号等机制。

乐观锁适用于多读的应用类型,这样能够提升吞吐量,像数据库若是提供相似于write_condition机制的其实都是提供的乐观锁。

(3)悲观锁 和 乐观锁的区别:

两种锁各有优缺点,不可认为一种好于另外一种,像乐观锁适用于写比较少的状况下,即冲突真的不多发生的时候,这样能够省去了锁的开销,加大了系统的整个吞吐量。但若是常常产生冲突,上层应用会不断的进行retry,这样反却是下降了性能,因此这种状况下用悲观锁就比较合适。

共享锁

共享锁指的就是对于多个不一样的事务,对同一个资源共享同一个锁。至关于对于同一把门,它拥有多个钥匙同样。就像这样,你家有一个大门,大门的钥匙有好几把,你有一把,你女友有一把,大家均可能经过这把钥匙进入大家家,这个就是所谓的共享锁。

刚刚说了,对于悲观锁,通常数据库已经实现了,共享锁也属于悲观锁的一种,那么共享锁在mysql中是经过什么命令来调用呢。经过查询资料,了解到经过在执行语句后面加上lock in share mode就表明对某些资源加上共享锁了。

何时使用表锁

对于InnoDB表,在绝大部分状况下都应该使用行级锁,由于事务和行锁每每是咱们之因此选择InnoDB表的理由。但在个别特殊事务中,也能够考虑使用表级锁。

  • 第一种状况是:事务须要更新大部分或所有数据,表又比较大,若是使用默认的行锁,不只这个事务执行效率低,并且可能形成其余事务长时间锁等待和锁冲突,这种状况下能够考虑使用表锁来提升该事务的执行速度。
  • 第二种状况是:事务涉及多个表,比较复杂,极可能引发死锁,形成大量事务回滚。这种状况也能够考虑一次性锁定事务涉及的表,从而避免死锁、减小数据库因事务回滚带来的开销。

固然,应用中这两种事务不能太多,不然,就应该考虑使用MyISAM表了。

表锁和行锁应用场景:

  • 表级锁使用与并发性不高,以查询为主,少许更新的应用,好比小型的web应用;
  • 而行级锁适用于高并发环境下,对事务完整性要求较高的系统,如在线事务处理系统。

再好比上面提到的Java的同步synchronized关键字的实现就是典型的悲观锁。

最全Java锁详解:独享锁/共享锁+公平锁/非公平锁+乐观锁/悲观锁

3.总之:

  • 悲观锁适合写操做多的场景,先加锁能够保证写操做时数据正确。
  • 乐观锁适合读操做多的场景,不加锁的特色可以使其读操做的性能大幅提高。

公平锁 VS 非公平锁

1.公平锁

就是很公平,在并发环境中,每一个线程在获取锁时会先查看此锁维护的等待队列,若是为空,或者当前线程是等待队列的第一个,就占有锁,不然就会加入到等待队列中,之后会按照FIFO的规则从队列中取到本身。

公平锁的优势是等待锁的线程不会饿死。缺点是总体吞吐效率相对非公平锁要低,等待队列中除第一个线程之外的全部线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

2.非公平锁

上来就直接尝试占有锁,若是尝试失败,就再采用相似公平锁那种方式。

非公平锁的优势是能够减小唤起线程的开销,总体的吞吐效率高,由于线程有概率不阻塞直接得到锁,CPU没必要唤醒全部线程。缺点是处于等待队列中的线程可能会饿死,或者等好久才会得到锁。

最全Java锁详解:独享锁/共享锁+公平锁/非公平锁+乐观锁/悲观锁

3.典型应用:

java jdk并发包中的ReentrantLock能够指定构造函数的boolean类型来建立公平锁和非公平锁(默认),好比:公平锁可使用new ReentrantLock(true)实现。

独享锁 VS 共享锁

1.独享锁

是指该锁一次只能被一个线程所持有。

2.共享锁

是指该锁可被多个线程所持有。

3.比较

对于Java ReentrantLock而言,其是独享锁。可是对于Lock的另外一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。

读锁的共享锁可保证并发读是很是高效的,读写,写读 ,写写的过程是互斥的。

独享锁与共享锁也是经过AQS来实现的,经过实现不一样的方法,来实现独享或者共享。

4.AQS

抽象队列同步器(AbstractQueuedSynchronizer,简称AQS)是用来构建锁或者其余同步组件的基础框架,它使用一个整型的volatile变量(命名为state)来维护同步状态,经过内置的FIFO队列来完成资源获取线程的排队工做。

最全Java锁详解:独享锁/共享锁+公平锁/非公平锁+乐观锁/悲观锁

concurrent包的实现结构如上图所示,AQS、非阻塞数据结构和原子变量类等基础类都是基于volatile变量的读/写和CAS实现,而像Lock、同步器、阻塞队列、Executor和并发容器等高层类又是基于基础类实现。

分段锁

分段锁实际上是一种锁的设计,并非具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是经过分段锁的形式来实现高效的并发操做。

咱们以ConcurrentHashMap来讲一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即相似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每一个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。

当须要put元素的时候,并非对整个hashmap进行加锁,而是先经过hashcode来知道他要放在那一个分段中,而后对这个分段进行加锁,因此当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。

可是,在统计size的时候,可就是获取hashmap全局信息的时候,就须要获取全部的分段锁才能统计。

分段锁的设计目的是细化锁的粒度,当操做不须要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操做。


Java线程锁

多线程资源空闲

线程死锁

锁的选择

因为多个线程是共同占有所属进程的资源和地址空间的,那么就会存在一个问题:

若是多个线程要同时访问某个资源,怎么处理?

在Java并发编程中,常常遇到多个线程访问同一个 共享资源 ,这时候做为开发者必须考虑如何维护数据一致性,这就是Java锁机制(同步问题)的来源。

Java提供了多种多线程锁机制的实现方式,常见的有:

  1. synchronized
  2. ReentrantLock
  3. Semaphore
  4. AtomicInteger等

每种机制都有优缺点与各自的适用场景,必须熟练掌握他们的特色才能在Java多线程应用开发时驾轻就熟。

4种Java线程锁(线程同步)

1.synchronized

在Java中synchronized关键字被经常使用于维护数据一致性。

synchronized机制是给共享资源上锁,只有拿到锁的线程才能够访问共享资源,这样就能够强制使得对共享资源的访问都是顺序的。

Java开发人员都认识synchronized,使用它来实现多线程的同步操做是很是简单的,只要在须要同步的对方的方法、类或代码块中加入该关键字,它可以保证在同一个时刻最多只有一个线程执行同一个对象的同步代码,可保证修饰的代码在执行过程当中不会被其余线程干扰。使用synchronized修饰的代码具备原子性和可见性,在须要进程同步的程序中使用的频率很是高,能够知足通常的进程同步要求。

synchronized (obj) {

//方法

…….

}

synchronized实现的机理依赖于软件层面上的JVM,所以其性能会随着Java版本的不断升级而提升。

到了Java1.6,synchronized进行了不少的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提升。在以后推出的Java1.7与1.8中,均对该关键字的实现机理作了优化。

须要说明的是,当线程经过synchronized等待锁时是不能被Thread.interrupt()中断的,所以程序设计时必须检查确保合理,不然可能会形成线程死锁的尴尬境地。

最后,尽管Java实现的锁机制有不少种,而且有些锁机制性能也比synchronized高,但仍是强烈推荐在多线程应用程序中使用该关键字,由于实现方便,后续工做由JVM来完成,可靠性高。只有在肯定锁机制是当前多线程程序的性能瓶颈时,才考虑使用其余机制,如ReentrantLock等。

2.ReentrantLock

可重入锁,顾名思义,这个锁能够被线程屡次重复进入进行获取操做。

ReentantLock继承接口Lock并实现了接口中定义的方法,除了能完成synchronized所能完成的全部工做外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

Lock实现的机理依赖于特殊的CPU指定,能够认为不受JVM的约束,并能够经过其余语言平台来完成底层的实现。在并发量较小的多线程应用程序中,ReentrantLock与synchronized性能相差无几,但在高并发量的条件下,synchronized性能会迅速降低几十倍,而ReentrantLock的性能却能依然维持一个水准。

所以咱们建议在高并发量状况下使用ReentrantLock。

ReentrantLock引入两个概念:公平锁与非公平锁

公平锁指的是锁的分配机制是公平的,一般先对锁提出获取请求的线程会先被分配到锁。反之,JVM按随机、就近原则分配锁的机制则称为不公平锁。

ReentrantLock在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。这是由于,非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊须要,不然最经常使用非公平锁的分配机制。

ReentrantLock经过方法lock()与unlock()来进行加锁与解锁操做,与synchronized会被JVM自动解锁机制不一样,ReentrantLock加锁后须要手动进行解锁。为了不程序出现异常而没法正常解锁的状况,使用ReentrantLock必须在finally控制块中进行解锁操做。一般使用方式以下所示:

Lock lock = new ReentrantLock();

try {

lock.lock();

//…进行任务操做5 }

finally {

lock.unlock();

}

3.Semaphore

上述两种锁机制类型都是“互斥锁”,学过操做系统的都知道,互斥是进程同步关系的一种特殊状况,至关于只存在一个临界资源,所以同时最多只能给一个线程提供服务。可是,在实际复杂的多线程应用程序中,可能存在多个临界资源,这时候咱们能够借助Semaphore信号量来完成多个临界资源的访问。

Semaphore基本能完成ReentrantLock的全部工做,使用方法也与之相似,经过acquire()与release()方法来得到和释放临界资源。

经实测,Semaphone.acquire()方法默认为可响应中断锁,与ReentrantLock.lockInterruptibly()做用效果一致,也就是说在等待临界资源的过程当中能够被Thread.interrupt()方法中断。

此外,Semaphore也实现了可轮询的锁请求与定时锁的功能,除了方法名tryAcquire与tryLock不一样,其使用方法与ReentrantLock几乎一致。Semaphore也提供了公平与非公平锁的机制,也可在构造函数中进行设定。

Semaphore的锁释放操做也由手动进行,所以与ReentrantLock同样,为避免线程因抛出异常而没法正常释放锁的状况发生,释放锁的操做也必须在finally代码块中完成

4.AtomicInteger

首先说明,此处AtomicInteger是一系列相同类的表明之一,常见的还有AtomicLong、AtomicLong等,他们的实现原理相同,区别在与运算对象类型的不一样。

咱们知道,在多线程程序中,诸如++i

i++等运算不具备原子性,是不安全的线程操做之一。一般咱们会使用synchronized将该操做变成一个原子操做,但JVM为此类操做特地提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。经过相关资料显示,一般AtomicInteger的性能是ReentantLock的好几倍。

Java线程锁总结

1.synchronized:

在资源竞争不是很激烈的状况下,偶尔会有同步的情形下,synchronized是很合适的。缘由在于,编译程序一般会尽量的进行优化synchronize,另外可读性很是好。

2.ReentrantLock:

在资源竞争不激烈的情形下,性能稍微比synchronized差点点。可是当同步很是激烈的时候,synchronized的性能一会儿能降低好几十倍,而ReentrantLock确还能维持常态。

高并发量状况下使用ReentrantLock。

3.Atomic:

和上面的相似,不激烈状况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。可是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。由于他不能在多个Atomic之间同步。

因此,咱们写同步的时候,优先考虑synchronized,若是有特殊须要,再进一步优化。ReentrantLock和Atomic若是用的很差,不只不能提升性能,还可能带来灾难。

以上就是Java线程锁的详解,除了从编程的角度应对高并发,更多还须要从架构设计的层面来应对高并发场景,例如:Redis缓存、CDN、异步消息等,详细的内容以下。

相关文章
相关标签/搜索