项目地址
JUC_03
锁的意义
涉及到线程安全问题会使用锁,当多个线程在同时共享到同一个全局变量的时候,可能会受到其他线程干扰。一般只在写的操作上锁
悲观锁与乐观锁
1.悲观锁
每次在执行同步代码块的时候都会去获取锁,如果没有获取到锁的情况下,当前线程进入阻塞状态,效率比较低。如果获取到锁,就需要从阻塞–>就绪,会有阻塞超时和等待锁。
应用场景:
- synchronized
- mysql行锁
- Lock锁
在这里我们就利用mysql行锁来说明吧。在这之前我们需要了解一下事务。
事务: 主要保证数据的一致性,遵循acid原则
在spring中的事务分为两种:
- 编程事务
手动提交事务
@Transactional
public String add(String name, String age) {
boolean isTrue = userService.save(new User(name, age));
int j = 1 / 0;
return isTrue ? "success" : "fail";
}
- 声明事务
自动事务,注解或者扫包的形式
public String add(String name, String age) {
TransactionStatus begin = transactionUtil.begin();
boolean isTrue = false;
try {
isTrue = userService.save(new User(name, age));
int j = 1 / 0;
transactionUtil.commit(begin);
} catch (Exception e) {
if (begin != null)
transactionUtil.rollback(begin);
}
return isTrue ? "success" : "fail";
}
从上面的示例可以知道。
编程事务也就是手动使用事务的begin()、commit()、rollback()
声明事务也就是使用@Transaction。
那么,这些都是完整的事务形式,如果我只是用begin(),不使用commit()和rollback(),会怎样?
定义一个update接口:
@GetMapping("/update")
public String update(int id, String name, String age) {
TransactionStatus begin = transactionUtil.begin();
boolean isTrue = userService.updateById(new User(id,name,age));
return isTrue ? "success" : "fail";
}
访问:http://127.0.0.1:8080/update?id=1&name=lxq&age=19
我们可以发现数据没有更改成功。
为什么会这样?其实是事务没有提交,我们在添加一个updateUser接口再来试试。
@GetMapping("/updateUser")
public String updateUser(int id, String name, String age) {
TransactionStatus begin = transactionUtil.begin();
boolean isTrue = userService.updateById(new User(id,name,age));
transactionUtil.commit(begin);
return isTrue ? "success" : "fail";
}
访问:http://127.0.0.1:8080/updateUser?id=1&name=lxq&age=19
发现数据更改成功了
这样我们应该就可以发现了,锁的释放是在commit()和rollback()
那么锁的获取是在哪一部分?我们直接debug到TransactionStatus begin = transactionUtil.begin();
这一行
继续访问http://127.0.0.1:8080/updateUser?id=1&name=lxq&age=20
程序运行到这一行了。让这一行运行
我们去查询一下是否有进程获取了行锁。
查询命令:
select * from information_schema.innodb_trx t
运行结果:
这里可以看到并没有任何进程获取了锁。
那就看下一行boolean isTrue = userService.updateById(new User(id,name,age));
,并让其运行
查询锁:
可以看到有线程获取到了锁。所以说获取锁是在执行mysql语句时开启,并与begin()的事务id绑定。
那就有疑问了,为什么我们平时直接使用SQL语句没有commit(),数据更改成功了,而且还能正常增加,其实是因为mysql有两种事务形式:
- 自动提交模式(默认)
查看命令:show session variables like 'autocommit'
可以看到自动commit()是开启的。那么我们把它关闭,再去直接运行sql会发现一直在running。
关闭自动commit(),命令:SET AUTOCOMMIT = 0
再来查询:
已关闭了。这么我们直接运行sql语句。
UPDATE user SET name = 'Fred' , age = 21 WHERE id = 1
可以发现数据没有发生改变:
那么看看是不是锁了
获取的锁一直没有释放,这时候就需要我们手动去提交了。
运行commit;
数据也更新了,锁也释放了。
- 手动提交模式
那如果一直锁着,我们需要手动释放,需要怎么执行?
上面我们查询的锁中有一个参数,t.trx_mysql_thread_id
,直接用命令kill t.trx_mysql_thread_id
即可释放锁。
乐观锁
其实乐观锁中拥有悲观锁,但是在不同的角度来说,他就是乐观的,因为我们在前面可以知道,悲观锁一旦没有释放锁,线程会一直卡在那里,而乐观锁是一旦没有获取到锁,会一直去获取锁。这也代表着乐观锁不会释放CPU执行权,而悲观锁会。
在不同的角度方面来考虑,它和悲观锁还是有区别的。
定义:
做些的操作没有锁的概念,也没有阻塞概念,通过阈值或者版本号比较,如果不一致性的情况则通过循环控制修改,当前线程不会被阻塞,是乐观,效率比较高。 非常消耗内存
应用场景:
CAS、自旋
示例:
@GetMapping("/updateData")
public String updateData(int id, String name, String age) {
boolean isTrue = false;
double version = userService.getById(id).version;
int count = 0;
while (true) {
isTrue = userService.updateById(id, name, age, version);
if (!isTrue) {
version = userService.getById(id).version;
count++;
}
if (count >= 5) {
break;
}
}
return isTrue ? "success" : "fail";
}
可以看到乐观锁利用的是一个死循环,但是为了效率,添加了一个循环次数,以免一直死循环,这样的话,此程序在多线程并不会因为获取到锁而阻塞。因为多线程中,一旦一个线程修改成功,那么其他线程就需要重新去查询version,才能继续去获取锁。
公平锁与非公平锁
- 公平锁
先来的线程先获取到锁,根据获取锁的顺序排列获取锁 - 非公平锁
不会根据获取锁的顺序排列,谁能抢到锁就归谁
我们可以利用Lock可以设置公平锁和非公平锁
New ReentramtLock()(true)---公平锁
New ReentramtLock()(false)---非公平锁
示例:
package com.lxq.test;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 龙小虬
* @date 2021/4/23 9:16
*/
public class MyLock implements Runnable {
private static int count = 0;
private static Lock lock = new ReentrantLock(true);
@Override
public void run() {
while (count < 200) {
createCount();
}
}
public synchronized void createCount() {
System.out.println(Thread.currentThread().getName() + ",count:" + count);
count++;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new MyLock()).start();
}
}
}
我们可以比较一下公平锁和非公平锁的差距:
公平锁:
非公平锁:
可以看到基本上就是非公平锁一旦拿到锁,就有可能一直拿着。
锁的可重入性
定义:
在同一个线程中锁可以不断传递的,可以直接获取。
可重入性实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配
package com.lxq.test;
/**
* @author 龙小虬
* @date 2021/4/23 9:22
*/
public class Main {
public synchronized void get(){
set();
}
public synchronized void set(){
}
}
现在set()、get()均使用了synchronized 锁,如果一旦获取到了get()的锁,那么执行set()方法就可以直接执行,不需要再次去获取锁。因为get()已经获取到了this锁。