并发编程------锁的分析

本文探讨了Java中的锁机制,包括悲观锁和乐观锁的概念及应用。悲观锁在写操作上加锁,可能导致阻塞,而乐观锁通过版本号比较避免冲突。此外,介绍了事务管理,讲解了Spring中的编程式和声明式事务,以及事务的提交与回滚与锁的关系。文章还提到了公平锁和非公平锁的区别,并讨论了锁的可重入性。
摘要由CSDN通过智能技术生成

项目地址
JUC_03

锁的意义

涉及到线程安全问题会使用锁,当多个线程在同时共享到同一个全局变量的时候,可能会受到其他线程干扰。一般只在写的操作上锁



悲观锁与乐观锁
1.悲观锁
每次在执行同步代码块的时候都会去获取锁,如果没有获取到锁的情况下,当前线程进入阻塞状态,效率比较低。如果获取到锁,就需要从阻塞–>就绪,会有阻塞超时和等待锁。

应用场景:

  1. synchronized
  2. mysql行锁
  3. Lock锁

在这里我们就利用mysql行锁来说明吧。在这之前我们需要了解一下事务。

事务: 主要保证数据的一致性,遵循acid原则
在spring中的事务分为两种:

  1. 编程事务
    手动提交事务
@Transactional
public String add(String name, String age) {
    boolean isTrue = userService.save(new User(name, age));
    int j = 1 / 0;
    return isTrue ? "success" : "fail";
}
  1. 声明事务
    自动事务,注解或者扫包的形式
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有两种事务形式:

  1. 自动提交模式(默认)
    查看命令:show session variables like 'autocommit'
    在这里插入图片描述
    可以看到自动commit()是开启的。那么我们把它关闭,再去直接运行sql会发现一直在running。
    关闭自动commit(),命令:SET AUTOCOMMIT = 0
    再来查询:
    在这里插入图片描述
    已关闭了。这么我们直接运行sql语句。
    UPDATE user SET name = 'Fred' , age = 21 WHERE id = 1

可以发现数据没有发生改变:
在这里插入图片描述
那么看看是不是锁了
在这里插入图片描述
获取的锁一直没有释放,这时候就需要我们手动去提交了。
运行commit;

数据也更新了,锁也释放了。
在这里插入图片描述

在这里插入图片描述

  1. 手动提交模式

那如果一直锁着,我们需要手动释放,需要怎么执行?
上面我们查询的锁中有一个参数,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,才能继续去获取锁。



公平锁与非公平锁

  1. 公平锁
    先来的线程先获取到锁,根据获取锁的顺序排列获取锁
  2. 非公平锁
    不会根据获取锁的顺序排列,谁能抢到锁就归谁

我们可以利用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锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙小虬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值