这是我参与8月更文挑战的第13天,活动详情查看: 8月更文挑战
锁在多线程编程中用的比较多,基本上多线程环境下内存资源被多线程共享,都会考虑加锁,不然就会造成数据混乱,但是在很多时候我们都不知道加怎样的锁,既能够满足我们的需求,又能够最大程度的介绍锁带来的性能开销,在下面从锁的基本概念入手,先来了解下锁的相关知识点,这样可以方便我们去选择正确的锁。实现锁的方式有两种:synchronized和Lock这两种方式。
乐观锁/悲观锁
乐观锁就和人的态度一样,对待一件事比较乐观,每次操作数据的时候就认为读取到的数据,在操作过程中没有被人动过,所以在读取到数据后不进行加锁,也允许其他线程访问,在最后将数据写回去的时候,在去判断数据有没有被人修改过,这种比较适合读多写少的场景。在MySQL中,我们经常会在表中加上version这个字段作为乐观锁,在更新数据的是时候使用这样的SQL语句:
UPDATE table_xxx SET c1 = xx WHERE id = xx AND version = xxxx;
然后在对返回的结果判断是否等于1,如果等于1则代表数据在操作过程中没有被修改过,不等于1则代表数据被修改了,需要我们重新来处理,CAS就属于乐观锁的实现。 悲观锁就是生活就比较悲观,认为生活中什么都不是好的,所以在每次操作数据的时候,就认为数据在操作过程中会被修改,就在读取的时候就开始加锁,让被人在操作被加锁的数据后,就阻塞到不允许操作,只有等自己操作数据完成后别人才可以操作,这种就比较适合写多读少的场景。在数据库中我们有时候为了让在读取的时候不能够修改数据,就会写下面的SQL:
select xxx for update;
synchronized就属于悲观锁
独享锁/共享锁
独享锁就是一个线程独自拥有这一把锁,当锁被线程拥有时,其他线程就不能够拥有该锁,ReentrantLock就属于独享锁;**共享锁 **就是多个线程可以共同拥有这把锁,并不是只能一个线程拥有锁,ReadWriteLock是个典型的例子,它的read锁共享,write锁独享 在很多情况下读锁是共享锁,这样做的目的是保证并发下读锁的效率比较高,因为读的情况下不涉及数据的修改不会造成数据不安全问题,写锁属于独享锁,因为在写的过程中,多个线程操作数据会造成数据不安全的问题哦,还有读写锁,写写锁是互斥的;独享锁和共享锁通过AQS来实现,不同的类采用的是不同的实现方式。
分段锁
分段锁就是将数据分成多个段,然后对不同的段操作时,只锁住需要操作的那段数据即可,无需对整个数据锁住,这样可以增加并发,不同的段由不同的锁来锁住。典型的案例就是:ConcurrentHashMap ConcurrentHashMap使用用Segment(分段锁)技术,将数据分成一段一段的存储,Segment数组将一 个大的table分割成多个小的table来进行加锁,Segment数组中每一个元素一把锁,每一个Segment元素存储的是 HashEntry数组+链表,这个和HashMap的数据存储结构一样。当访问其中一个段数据被某个线程加锁的时候,其 他段的数据也能被其他线程访问,这就使得ConcurrentHashMap不仅保证了线程安全,而且提高了性能。 但是这也引来一个负面影响:ConcurrentHashMap 定位一个元素的过程需要进行两次Hash操作,第一次 Hash 定位到 Segment,第二次 Hash 定位到元素所在的链表,所以 Hash 的过程比普通的 HashMap 要长。
可重入锁
可重入锁 在当前线程获取到锁后,在线程内部方法体内又需要对同一把锁进行获取,这个时候就不需要等待获取锁,而是直接放行,这样做的意义就是防止死锁发生,synchronized 和ReentrantLock 都是可重入锁。
Object obj = new Object();
synchronized (obj){
synchronized (obj){
synchronized (obj){
//TODO 业务逻辑
}
}
}
公平锁/非公平锁
像我们平时打饭的时候,就需要排队,先来的排在前面,后来的排在后面,这样打饭的话就比较公平。公平锁也类似这样的道理,每个线程在获取锁的时候先看出此锁的等待队列是否为空或者是队列中的第一个,就获取锁,如果不是就把自己加入到等待队列的最后一个派对,按照FIFO的规则从队列中取到自己,公平锁不会造成线程饥饿,无论如何都可以取到锁来执行任务。 非公平锁就好比打饭不排队一样,只要窗口开放,就可以去抢占打饭,非公平锁的性能要高于公平锁,因为线程有几率不阻塞获取到锁资源,但是会造成有些线程一直抢不到锁。 ReentrantLock支持创建公平锁和非公平锁(默认),想要实现公平锁,使用new ReentrantLock(true)
锁升级
锁可以分为四类锁:无锁、偏向锁、轻量级锁、重量级锁
- 无锁 顾名思义就是没有锁
- 偏向锁 就是一个窗口打饭,但是外面排了两队人打饭,每次轮到你和另外一队人打饭时,食堂阿姨比较喜欢你就先给你打饭
- 轻量级锁当你在打饭的时候,另一队人一直在旁边问你打完饭没有?
- 重量级锁就相当于VIP,当你打饭的时候你就清场食堂,其他人全部都在食堂外面等着,等你打放饭后在通知其他人进来打饭
锁可以升级,但是不能够反向升级,锁升级的顺序:偏向锁→轻量级锁→重量级锁,这几种锁的优缺点如下
| 锁 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 偏向锁 | 加锁和解锁不需要CAS操作,没 有额外的性能消耗,和执行非同 步方法相比仅存在纳秒级的差距 | 若线程间存在锁争,会带来额外的锁撤销的消耗 | 只有一个线程访问同步块或者同 步方法 |
| 轻量级锁 | 竞争的线程不会阻塞,提高了程 序的响应速度 | 若线程长时间竞争不到锁, 自旋会消耗 CPU 性能 | 线程交替执行同步块或者同步方 法,追求响应时间,锁占用时间 很短,阻塞还不如自旋的场景 |
| 重量级锁 | 线程竞争不使用自旋,不会消耗 CPU | 线程阻塞,响应时间缓慢, 在多线程下,频繁的获取释 放锁,会带来巨大的性能消 耗 | 追求吞吐量,锁占用时间较长 |
互斥锁/读写锁
在独享锁、共享锁的内容中已经提到过相关的概念。典型的互斥锁:synchronized,ReentrantLock,读写锁:ReadWriteLock,互斥锁属于独享锁,读写锁里的写锁属于独享锁,而读锁属于共享锁