锁机制
1、概述
加锁是为了解决并发场景下,多个线程对同一资源同时进行操作,而导致同一线程多次操作出现结果不唯一的情况(一次操作包含多条指令)。结果不唯一发生的原因在于指令的错乱,前提条件是多线程环境及多个线程的执行顺序是无序的。对于资源A来说,有一次操作[A++] (内包含了多条指令:指令1,读取A的值;指令2,将A的值加1;指令3,将结果值重新赋予A),某一时刻,线程1执行了指令1,2,将要执行指令3时,线程2开始执行,并执行完了指令1,这样最终结果A的值只加了1;另一时刻,线程1执行完了指令1,2,3,线程2再执行指令1,2,3,这样最终结果A的值就加了2。同一操作,不同时刻,最终的结果却不唯一的现象发生了。
此时要引出原子性这一概念了,指某一操作已达到最小级别不可继续划分,对于上述操作[A++]来说就,该操作就不是原子性操作,而对于操作[读A的值](内只包含了一条指令:读取A的值)就是原子性操作。
而加锁的本质就是通过保证了线程执行操作的有序性,进而保证了操作的原子性。加锁通过对一次操作做出限制,多个线程同时执行该操作时,如若已有线程进行操作,则其他线程必须等待该线程完成后才可进行操作,且一次只允许一个线程进行操作。如此,对于线程来说,该操作是原子性的。
1. 对于 Java 来说,锁机制总体可分为两种,synchronized 关键字和 Lock 接口。
-
synchronized 关键字,可用于修饰普通方法、代码块、静态方法。
-
Lock 接口,定义了lock、lockInterruptibly、tryLock、unlock、newCondition等方法。
2. 对于 MySQL 来说,锁机制总体可分为两种,读锁和写锁,也可称为共享锁和排他锁。
- 读锁,lock in share mode,允许多个事务对同一行数据加读锁。
- 写锁,for update,只允许一个事务对同一行数据加写锁,且加写锁之后也不能再加读锁。
3. 对于 Redis 来说,虽然没有直接提供加锁的方式,但提供了 setnx 这一命令可供我们实现加锁。
- setnx,设置一个 key-value 键值对,如果 key 不存在,则 value 设置成功,并返回 1;如果 key 已存在,则 value 设置失败,并返回 0。
2、Java 中的锁
案例:读写操作下的并发问题
public class LockTest {
private Integer count = 0;
public static void main(String[] args) throws InterruptedException {
LockTest lockTest = new LockTest();
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executorService.execute(lockTest::operate);
}
executorService.shutdown();
TimeUnit.HOURS.sleep(1);
}
private void operate() {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 读操作
if (count == 0) {
// 写操作
count = 1;
System.out.println(Thread.currentThread().getName() + " 加锁成功!");
}
}
}
2.1、使用 synchronized
private void operate() {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 加锁
synchronized (this) {
// 读操作
if (count == 0) {
// 写操作
count = 1;
System.out.println(Thread.currentThread().getName() + " 加锁成功!");
}
}
}
2.2、使用 ReentrantLock 类
private final ReentrantLock lock = new ReentrantLock();
private void operate() {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 加锁
lock.lock();
try {
// 读操作
if (count == 0) {
// 写操作
count = 1;
System.out.println(Thread.currentThread().getName() + " 加锁成功!");
}
} finally {
lock.unlock();
}
}
2.3、使用 ReentrantReadWriteLock 类
private final ReentrantReadWriteLock.WriteLock lock = new ReentrantReadWriteLock().writeLock();
private void operate() {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 加锁
lock.lock();
try {
// 读操作
if (count == 0) {
// 写操作
count = 1;
System.out.println(Thread.currentThread().getName() + " 加锁成功!");
}
} finally {
lock.unlock();
}
}
2.4、使用 StampedLock 类
private final StampedLock stampedLock = new StampedLock();
private void operate() {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 加锁
long stamp = stampedLock.writeLock();
try {
// 读操作
if (count == 0) {
// 写操作
count = 1;
System.out.println(Thread.currentThread().getName() + " 加锁成功!");
}
} finally {
stampedLock.unlockWrite(stamp);
}
}
2.5、使用 Semaphore 类
private final Semaphore semaphore = new Semaphore(1);
private void operate() {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
// 加锁
semaphore.acquire();
// 读操作
if (count == 0) {
// 写操作
count = 1;
System.out.println(Thread.currentThread().getName() + " 加锁成功!");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
3、MySQL 中的锁
# 查看事务自动提交是否开启
SHOW VARIABLES LIKE 'autocommit';
# 开启事务自动提交
SET autocommit = 1;
# 关闭事务自动提交
SET autocommit = 0;
# 查看未提交的事务
SELECT * FROM performance_schema.events_transactions_current WHERE STATE = 'ACTIVE';
# 开启事务
BEGIN;
或者
START TRANSACTION;
# 提交事务
COMMIT;
# 回滚事务
ROLLBACK;
注:MySQL的行锁是在事务结束时自动释放的。
3.1、读锁,lock in share mode
# 事务A
select scene_id, scene_name from scene
where scene_id = 13
lock in share mode;
# 事务B
select scene_name, scene_logo from scene
where scene_id = 13
lock in share mode;
3.2、写锁,for update
# 事务A
select scene_id, scene_name from scene
where scene_id = 13
for update ;
# 事务B
select scene_name, scene_logo from scene
where scene_id = 13
for update ;
select scene_name, scene_logo from scene
where scene_id = 13
lock in share mode;
4、Redis 中的锁
4.1、setnx
# 第一次操作
setnx lock-flag 1
# 第二次操作
setnx lock-flag 0