Java开发中的各种锁概念
前言:
本文不会深入不会深入!科普文,就是归纳一下平时我们遇到的各种锁,这样听到也不会太懵逼。真正深入的还是要看书的~
在Java开发中,特别是并发编程的时候我们会和很多的锁打交道。写下一篇笔记记录一下各种锁的概念。
1.死锁
在学习操作系统,或者并发编程中的时候我们经常会遇到死锁的概念。什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
举个简单的例子:
一个线程需要抢占A锁去执行某段代码,抢占到了A锁后,要抢占B锁去继续执行代码。另外一个线程则需要抢占B锁,然后抢占A锁去执行代码。这样就会造成彼此之间阻塞。两个线程一起卡死。
如何解决?
- 避免写嵌套锁(废话);
- 规范嵌套锁的顺序,多个线程用户同一个顺序并且放在同一个方法里;
- 引入超时机制(需要用到显示锁Lock)
2.显示锁,公平锁,可重入锁
标题中的说的三种锁说的就是Lock这个接口下的锁,Lock具有这三种概念。
显示锁:非Java给我们提供的关键字去操作。而是我们自己定义锁,然后显示的获取,显示的释放。
公平锁:一定程度上保证线程获得这个锁的机会是公平的。但是这样会大大消耗性能。
可重入锁:看下面的代码。这两个是同步代码块。他们获取的是同一把锁。因此获取锁的是线程,不是对象。因此在线程没有结束之前,这把锁可以一直使用。
PS:Lock下的ReentrantLock也具有可重入性。
public class Widget {
// 获得了锁
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
// 获得了锁
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}
3.Lock的介绍
其实synchronize从jdk一直发展到现在,性能已经非常好。如果不是很有必要还是建议synchronize;因为简单易用。不过这里还是要说一下Lock。下面是这个接口的方法。
![](https://img.haomeiwen.com/i2909474/0fb11b3f4ccdf0b9.png)
-
lock:获得锁
-
lockInterruptibly:可打断锁
-
tryLock:尝试获取锁。
-
tryLock带时间:引入超时机制。很大程度上可以避免死锁
-
unlock():释放锁
看上去是非常爽的。也非常的优雅,毕竟啥时候加锁上锁的控制权在我们这里了。但是如果我们忽略了释放锁,或者说程序出了点问题,这个锁没有释放。那就麻烦大了~因此还是要酌情使用!
怎么用呢?这里给一个demo
class LockDemo {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
4.ReentrantReadWriteLock
ReentrantReadWriteLock是一个读写锁:
- 在读取数据的时候,可以多个线程同时进入到到临界区(被锁定的区域)
- 在写数据的时候,无论是读线程还是写线程都是互斥的
当我们读的操作比较多。写的操作比较少的时候可以用ReentrantReadWriteLock。
ReentrantReadWriteLock就两个方法: - readLock():得到一个可以被多个读操作共用的毒锁。但是用会排斥所有写操作
- writeLock():得到一个写锁。排斥其他所有读操作和写操作
举个例子
读锁:
package com.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class LockTest2 {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void main(String[] args) {
final LockTest2 lockTest2 = new LockTest2();
new Thread(new Runnable() {
@Override
public void run() {
lockTest2.get(Thread.currentThread());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
lockTest2.get(Thread.currentThread());
}
}).start();
}
public void get(Thread thread) {
try {
lock.readLock().lock();
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName() + "正在进行读操作");
}
System.out.println(thread.getName() + "操作完毕");
}catch (Exception e) {
}
finally {
lock.readLock().unlock();
}
}
}
![](https://img.haomeiwen.com/i2909474/8aeb692c2a25aa94.png)
我们可以看到是交替进行的~
写锁:
写锁只有将readLock改成writeLock就好了。这里不贴代码了。直接观察结果可以看出是互斥的。必须一个个来
![](https://img.haomeiwen.com/i2909474/5aed509b582917a8.png)
5.悲观锁,乐观锁
悲观锁,乐观锁也是我们并发编程中常遇到的问题。在数据库层面有为体现。
下面举例子说明:
![](https://img.haomeiwen.com/i2909474/64e83348cb996b2e.png)
假设现在有两个线程:
A线程来了,读到了这个数据,status为0;B线程改为了1了。由于数据库的事务隔离机制。并发修改了这个数据。那数据就混乱了。
这个时候就要保持互斥性,同一个时间只能有一个线程来操作这行数据。
解决方案:
乐观锁:
加一个字段版本字段:
A线程读出来的时候版本号位1,然后更新的时候加个条件update xxx where version =1;但是B线程已经改为了2,所以会更新失败;这样就可以控制并发修改。一般是给用户提示,有人已经操作了数据。但是还是可以读取这一行数据
悲观锁
SQL关键字FOR_UPDATE
select * from XXXX FOR_UPDATE
把查询出来的数据锁住了,其他人读这条数据会阻塞。在用户界面上会转圈会等待。
举例:
比如外卖。用户下了一个单,这个时候订单的数据异步到配送端了。此时,用户突然申请退单了。此时数据库订单状态应该是退单状态。然而由于时差,订单的数据才到配送端。此时配送人员又抢单成功将订单状态重新改为了配送中(实际上这张订单应该是退单状态的。)
6.分布式锁
我们知道乐观锁,悲观锁是数据库层面(单个)。如果我们分库分表的话我们怎么处理呢?这个时候我们就要用分布式锁。
我们可以用zookeeper,redis实现~具体实现大家可以baidu一下!