# java中的各种锁
一个锁并不是只能属于一种分类,一个锁可以同时是悲观锁,可重入锁,公平锁,可中断锁;
## 原子操作
```java
public class demo{
private AtomicInteger atomicI = new AtomicInteger(0);
private int i = 0;
public static void main(String[] args) {
final Counter cas = new Counter();
List ts = new ArrayList(600);
long start = System.currentTimeMillis();
for (int j = 0; j < 100; j++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
cas.count();
cas.safeCount();
}
}
});
ts.add(t);
}
for (Thread t : ts) {
t.start();
}
// 等待所有线程执行完成
for (Thread t : ts) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(cas.i);
System.out.println(cas.atomicI.get());
System.out.println(System.currentTimeMillis() - start);
}
/**
* 使用CAS实现线程安全计数器
*/
private void safeCount() {
for (; ; ) {
int i = atomicI.get();
boolean suc = atomicI.compareAndSet(i, ++i);
if (suc) {
break;
}
}
}
/**
* 非线程安全计数器
*/
private void count() {
i++;
}
}
```
所谓原子操作是指不会被**线程调度机制**打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何切换到另一个线程 ,即不可中断的操作 。
比如赋值操作就是一个原子操作`int i = 5;` **原子性操作本身是线程安全的** 。
```
但是 i++ 这个行为,事实上是有3个原子性操作组成的。
1. 取 i 的值
2. i + 1
3. 把新的值赋予i
这三个步骤,每一步都是一个原子操作,但是合在一起,就不是原子操作。就不是线程安全的。
换句话说,一个线程在步骤1 取i 的值结束后,还没有来得及进行步骤2,另一个线程也可以取 i的值了。
启动两个线程,每个线程对临界资源count++1 100次,因为这段代码是线程不安全的所以大概率会小于200.加上synchronized就好了
```
JDK6 以后,新增加了一个包**java.util.concurrent.atomic**,里面有各种原子类,比如**AtomicInteger**。
而AtomicInteger提供了各种自增,自减等方法,这些方法都是原子性的。 换句话说,自增方法 **incrementAndGet** 是线程安全的,同一个时间,只有一个线程可以调用这个方法。
> 上面代码的执行结果,安全与不安全的对比
![同步测试](https://stepimagewm.how2j.cn/2626.png)
### synchronized与Lock
Lock lock = new ReentrantLock();
Java中有两种加锁的方式:一种是用**synchronized关键字**,另一种是用**Lock接口**的实现类。
形象地说,synchronized关键字是**自动档**,可以满足一切日常驾驶需求。但是如果你想要玩漂移或者各种骚操作,就需要**手动档**了——各种Lock的实现类。
> **两者的不同**
1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,Lock是代码层面的实现。
2. Lock可以选择性的获取锁,如果一段时间获取不到,可以放弃(trylock方法)。synchronized不行,会一根筋一直获取下去。 借助Lock的这个特性,就能够规避死锁,synchronized必须通过谨慎和良好的设计,才能减少死锁的发生。
3. synchronized在发生异常和同步块结束的时候,会自动释放锁。而Lock必须手动释放, 所以如果忘记了释放锁,一样会造成死锁。
***
ReentrantLock(轻量级锁)也可以叫对象锁,可重入锁,互斥锁。synchronized重量级锁,JDK前期的版本lock比synchronized更快,在JDK1.5之后synchronized引入了偏向锁,轻量级锁和重量级锁。以致两种锁性能旗鼓相当。
![img](https://img-blog.csdnimg.cn/20181122101753671.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2F4aWFvYm9nZQ==,size_16,color_FFFFFF,t_70)
## 乐观锁 VS 悲观锁
乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度或者说是在并发情况下的两种不同策略 。
***
> 悲观锁
悲观锁就是很悲观,对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。(干活之前二话不说先上锁)Java中,synchronized关键字和Lock的实现类都是悲观锁。
***
> 乐观锁
乐观锁就是很乐观,认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁 , 只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者滚回重试)。
![img](https://img-blog.csdnimg.cn/20181122101819836.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2F4aWFvYm9nZQ==,size_16,color_FFFFFF,t_70)
**乐观锁的实现基础**
乐观锁则直接去操作同步资源。那么,为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢?需要了解乐观锁的主要实现方式 “CAS” 的技术
CAS- Compare-and-Swap,即**比较并替换**, 是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。
**CAS算法涉及三个操作数:需要读改的内存值`V`,进行比较的预期值`flag`,要写入的新值`B`。**
1. 比较:读取到一个值V,在把他设置为B之前,检查flag是否等于V,也就是判断是否被其他线程改动过
2. 替换:如果是,讲V更新为B,结束;如果不是则什么都不做或者回滚重试。
简单解释就是如果线程1更新数据之前线程2抢先一步更新,V值已经变了,所以此时V值和预期值flag就不同了,就提交失败,不会污染数据,达成锁效果。
***
上面两步操作是`原子操作`,也就是一下子完成,中间不会有其他线程来参与或干扰,CAS通过CPU指令,从硬件层面保证了操作的原子性,以达到类似于锁的效果。
之前提到java.util.concurrent包中的原子类,就是通过CAS来实现了乐观锁。
> 示例伪代码
```java
data = 123; // 共享数据
/* 更新数据的线程会进行如下操作 */
flag = true;
while (flag) {
oldValue = data; // 保存原始数据
newValue = doSomething(oldValue);
// 下面的部分为CAS操作,尝试更新data的值
if (data == oldValue) { // 比较
data = newValue; // 设置
flag = false; // 结束
} else {
// 啥也不干,循环重试
}
}
/*
很明显,这样的代码根本不是原子性的,
因为真正的CAS利用了CPU指令,
这里只是为了展示执行流程,本意是一样的。
*/
```
换句话说,乐观锁其实不是“锁”,它仅仅是一个循环重试CAS的算法而已!
***
`CAS算法存在的ABA问题`
`这里存在一个问题,就是一个值从A变为B,又从B变回了A。这种情况下,CAS可能会认为值没有发生过变化,但实际上是有变化的。对此,并发包下有AtomicStampedReference提供根据版本号判断的实现。`
`解决办法就是再加上版本号判断是否更新过。`
## 自旋锁 VS 适应性自旋锁
> 自旋锁
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
如果电脑多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,也就是不wait他,看看持有锁的线程是否很快就会释放锁。这样就避免了他的wait和notify。
而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是`自旋锁`。
> 通俗来讲就是多线程时,后面的准备获取线程在前面带锁的线程干活的时候转会圈,转了一小会前面的锁就干完了,自己就可以很快的干活,不必再等待唤醒,大大节省资源。
> 一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做**忙等(busy-waiting)**,自旋锁就是 **短时间的忙等,换取线程在用户态和内核态之间切换的开销。**
![img](https://img-blog.csdnimg.cn/2018112210212894.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2F4aWFvYm9nZQ==,size_16,color_FFFFFF,t_70)
> 自旋可以比喻为租房的人公用一个厕所,A上厕所时,B在门外急的转圈
> 刚刚的乐观锁就有类似的等待操作,那么它是自旋锁吗? 不是。“自旋”这两个字,特指自旋锁的自旋。
我们可以看出,如果锁的占用时间很短,自旋锁的效果就会很好,反之,如果锁的占用时间很长,那么自旋只会白白浪费处理器资源。 所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以通过虚拟机参数来更改)没有成功获得锁,就应当挂起线程。
***
> 自适应自旋锁
自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(**适应性自旋锁**)。
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,**自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功**,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,**自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。**
## 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
**ReentrantLock**轻量级锁。**synchronized**重量级锁,JDK前期的版本**lock**比**synchronized**更快,在JDK1.5之后**synchronized**引入了偏向锁,轻量级锁和重量级锁。以致两种锁性能旗鼓相当。
如同我们在自旋锁中提到的“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 5之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 5中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
***
> 无锁
CAS算法原理及应用就是无锁的实现。
***
> 偏向锁
偏向锁是指一段同步代码一直被一个线程所访问,那么这个线程执行完同步代码块后,线程并**不会主动释放偏向锁**。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。**由于之前没有释放锁,这里也就不需要重新加锁。**如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。
***
> 轻量级锁
当锁是偏向锁的时候,被另外的线程所访问(加入锁竞争),偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁( 即不停地循环判断锁是否能够被成功获取 ),不会阻塞,从而提高性能。
*自旋锁就是轻量级锁*
***
> 重量级锁
显然,自旋是有限度的,当自旋超过最大次数时又或者一个线程持有锁,一个在自旋,又有第三个来访时,就会从轻量级锁升级为重量级锁。
当后续线程尝试获取锁时,发现被占用的锁是**重量级锁**,则直接将自己挂起wait(而不是忙等),等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁,现价为他加上了偏向锁和轻量级锁,很明显现在得到了很好的优化。
*一个锁只能按照 偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫**锁膨胀**的),不允许降级。*
```
综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。
```
## 公平锁 VS 非公平锁
如果多个线程申请一把**公平锁**,那么当锁释放的时候,先来后到,先申请的先得到,非常公平。显然如果是**非公平锁**,后申请的线程可能先获取到锁,是随机或者按照其他优先级排序的。
对Lock实现类来说, 通过构造函数传参**可以指定该锁是否是公平锁,默认是非公平锁**。一般情况下,非公平锁的吞吐量比公平锁大,如果没有特殊要求,优先使用非公平锁。
![img](https://pic2.zhimg.com/80/v2-7a4a72fe7ace46095cd3ca2e6c5212d9_hd.jpg)
对于synchronized而言,它也是一种**非公平锁**,但是并没有任何办法使其变成公平锁。
## 可重入锁 VS 非可重入锁
可重入锁也叫递归锁。可重入锁的字面意思是“可以重新进入的锁”,即**允许同一个线程多次获取同一把锁**,不会因为之前已经获取过还没释放而阻塞 。
Java里只要以Reentrant开头命名的锁都是可重入锁,而且**JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。**
![img](https://img-blog.csdnimg.cn/20181122104329631.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2F4aWFvYm9nZQ==,size_16,color_FFFFFF,t_70)
> 可重入锁执行以上代码可行,不可重入锁会死锁。
## 独享锁 VS 共享锁
独享锁也叫排他锁、互斥锁、写锁, 是指该锁一次只能被一个线程所持有。 获得排它锁的线程即能读数据又能修改数据。 JDK中的synchronized和JUC中Lock的实现类就是互斥锁。
共享锁也叫读锁,是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
> 独享锁可读可写,共享锁只能读
作用:
线程知道自己读取数据后,是否是为了更新它。那么何不在加锁的时候直接明确这一点呢?如果我读取值是为了更新它那么加锁的时候就直接加**写锁**,我持有写锁的时候别的线程无论读还是写都需要等待,确保数据不被污染。
如果我读取数据仅为了前端展示,那么加锁时就明确地加一个**读锁,**其他线程如果也要加读锁,不需要等待,可以直接获取。
JDK提供的唯一一个ReadWriteLock接口实现类ReentrantReadWriteLock。看名字就知道,它不仅提供了读写锁,而是都是可重入锁。
## 可中断锁
如果线程A持有锁,线程B等待获取该锁。由于线程A持有锁的时间过长,线程B不想继续等待了,我们可以让线程B中断自己或者在别的线程里中断它,这种就是**可中断锁**。
在Java中,synchronized就是**不可中断锁**,而Lock的实现类都是**可中断锁**。
一键复制
编辑
Web IDE
原始数据
按行查看
历史