前言
今天不谈设计模式,本来想写一篇从源码看hashmap,但是不传图片光靠文字要讲解清楚确实有点困难。
那我们来聊聊常见的锁的种类,锁的思想在多线程开发中基本上都是互通的,而且在数据库中我们也会用到。那就开始这篇文章吧。
正文
乐观锁与悲观锁
乐观锁的乐观在于使用者认为每次读取数据别人都不会修改,所以不进行上锁操作,但是更新的时候会判断是否其他人已经更新过了这个数据,如果别人已经修改了,则重复读,再进行更新。一般情况下这种是通过版本号来实现,即来数据库中添加一个列为更新版本号,如果版本号与上一次一致,则数据没有变动,如果不一致,则重复读然后比较再写。
java中的乐观锁通过CAS操作实现,比较当前值与传入值是否一样,如果一样更新,不一样则失败。而SQL的乐观锁则是使用版本号或者更新时间的列来实现。
乐观锁适合读多写少的情况。
悲观锁与乐观锁相反,认为每次读取数据时都是旧的,所以会在每次读取数据时都进行上锁操作。
java中常见的悲观锁就是Synchronized。
自旋锁
自旋锁就是在等待其他线程释放资源的同时,不阻塞,即没有让出cpu,不切换,而是通过一个自旋的操作来不断使用cpu进行使用,减少了切换线程所需要的开销。
自旋操作就是不断消耗cpu,直到超时都没有获取到锁时,才进行阻塞。
自旋的优点在于减少了线程的阻塞,如果锁的竞争不激烈,那自旋锁对性能的提升是巨大的,因为线程之间的切换是有消耗的。缺点也很明显,就是如果锁竞争激烈,那自旋锁就会在获取锁之前占用cpu,而等待时间长则自旋的消耗可能会小于线程阻塞再唤醒的消耗,导致cpu的浪费。
公平锁与非公平锁
这个概念比较好理解,就是根据锁提出获取请求的先后顺序来分配锁,如果按顺序,那就是公平锁,如果可以插队,那就是非公平锁。
synchronized是非公平锁,而ReentrantLock可以通过传入参数指定是否公平。
我们可以来看看ReentrantLock的构造函数。
1 | public ReentrantLock(boolean fair) { |
来看看公平锁与非公平锁的差别
1 | // 公平锁 |
可以发现公平锁多了一个!hasQueuedPredecessors()条件,跟进
1 | public final boolean hasQueuedPredecessors() { |
就是判断当前线程是否是同步队列中对的第一个。
可重入锁与非可重入锁
这个概念就是指一个线程在外层方法获得锁时,内层方法是否自动获得锁(前提是锁是同一个),在内层自动获得锁的就是可重入锁。
synchronized与ReentrantLock都是可重入锁。
可重入锁一定程度上可以避免死锁。即在循环调用时不会出现循环等待。
就拿synchronized举例,假设实例中有两个方法,方法都是synchronized修饰的,所以锁住的是实例对象,而方法A中调用了方法B,如果不是可重入锁,那么在A调用B时,B会出现等待A释放锁的情况,导致死锁。
可重入锁的原理是维护了一个status变量,来计数重入次数。当线程尝试获取锁时,先获取status值,如果status==0,表示没有其他线程在执行同步代码,就将status置为1,如果status!=0,就判断当前线程是否已经获取了这个锁,是的话status+1,并且可以继续获得锁。
而释放时,先获取status值,如果当前线程持有锁,则status-1,如果status这时等于0,则表示该线程重复获取锁的次数已经为0,这时就会释放锁。
不可重入锁则是status值要嘛为1要嘛为0,如果为1则其他获取锁的操作会失败,0的时候锁释放。
独享锁(排它锁)与共享锁
独享锁又称排它锁,指的是该锁只能被一个线程持有。
而共享锁指的是锁可以被多个线程持有。
Java中常见的共享锁有ReadWriteLock,即读写锁,允许多个读操作或者一个写操作。
而Mysql中则可以使用lock in share mode来添加共享锁,其他session可以查询数据,也可以再次添加共享锁。
使用for update来实现排它锁。
无锁 偏向锁 轻量级锁 重量级锁
这几个概念放在一起说,他们的区别在于在线程竞争同步资源时,获取锁的流程细节的差别。
无锁就是字面意思,不锁住资源,所有线程都能访问和修改同一个资源,但只有一个线程能成功。
偏向锁是指同一段同步代码一直被同一个线程访问,那么之后该线程在访问这段代码时,就会自动获取锁,减少获取锁的代价。
轻量级锁是指在偏向锁的情况下,有其他线程访问了同步代码,那偏向锁就会变成轻量级锁,当线程尝试获取锁时,不会阻塞,而是自旋。
重量级锁是指在轻量级锁时,如果等待的线程自旋次数过多,或者有第三个线程访问时,升级为重量级锁,所有等待的线程都会阻塞。
锁的状态只能升级不能降级。
这几种锁的实现在于Java对象头中的标记字段。
锁状态 | 前30位 | 后2位(锁标志位) |
---|---|---|
无锁 | 对象的hashCode、对象分代年龄、是否是偏向锁(0) | 01 |
偏向锁 | 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 |
重量级锁 | 指向互斥量(重量级锁)的指针 | 10 |
首先是偏向锁,当一个线程获取锁是,会在标记字段中存储当前线程id,在进入及退出同步代码时不通过cas操作来获取锁和解锁,而是检测标记字段中是否存储着与当前线程id一致的值,与是否是偏向锁的值。
当其他线程竞争时,持有偏向锁的线程才会释放偏向锁。
然后是轻量级锁,在线程进入同步块时,如果是无锁状态,就拷贝标记字段到一个名为锁记录的空间。拷贝完成后,将标记字段更新为指向锁记录的指针,然后把锁记录中的owner指针指向标记字段。
操作成功后,那么线程就获得该对象的锁,如果标记字段中的锁标志位为”00”,表示正处于轻量级锁状态。
如果操作失败,则检查标记字段是否指向当前线程,如果是,则该线程已经获得了锁,否则进行竞争。
重量级锁,在前30位中指向重量级锁monitor的指针,标记字段中锁标志位为”10”。
总结
以上讲解了常用的锁的分类,对其进行了简单介绍,有些地方从源码方式讲解实现原理,并给出了java用法,有些地方给出了mysql的用法。
其实从源码去分析是十分好的方法,从源码看问题能学到更多东西。