首页 > 编程语言 >【并发编程】锁机制

【并发编程】锁机制

时间:2023-01-19 11:34:12浏览次数:39  
标签:Thread lock void 编程 并发 线程 new 机制 public


文章目录

  • ​​1.锁的分类​​
  • ​​2.深入理解Lock接口​​
  • ​​3.自定义实现可重入锁​​

1.锁的分类

  • 自旋锁:线程状态及上下文切换消耗系统资源,当访问共享资源的时间短,频繁上下文切换不值得。jvm实现,使线程在没有获得锁的时候,不被挂起,转而执行空循环,循环几次之后,如果还没能获得锁,则被挂起。
  • 阻塞锁:阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒或者时间)时,才可以进入线程的准备就绪状态,转为就绪状态的所有线程,通过竞争,进入运行状态。
  • 重入锁:支持线程再次进入的锁,就像我们有房间的钥匙,可以多次进入房间类似。
  • 读写锁:两把锁,读锁跟写锁,写写互斥、读写互斥、读读共享。
  • 互斥锁:当线程抢占到资源,其他线程进不来。
  • 悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
  • 乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
  • 公平锁:所有线程老老实实排队,对大家而言都很公平。
  • 非公平锁:一部分线程排着队,但是新来的线程可能插队。
  • 偏向锁:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
  • 独占锁:独占锁模式下,每次只能有一个线程能持有锁。
  • 共享锁:允许多个线程同时获取锁,并发访问共享资源。

2.深入理解Lock接口

(1)lock于synchronized的区别

  • synchronized是java内置的关键字,在jvm层面,Lock是个java类。
  • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁。
  • synchronized会自动释放锁(线程在执行代码块的过程中发生异常会自动释放锁),Lock需要在finally中手动释放锁(unlock方法),否则会造成死锁。
  • 用synchronized关键字的两个线程1和线程2,如果当线程1获取锁,线程2等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不会一直等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了。
  • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可中断、可公平(两者皆可)
  • Lock锁适合大量同步的代码的同步问题,而synchronized锁适合少量代码同步问题。

(2)Lock继承体系图

【并发编程】锁机制_重入锁

(3)Lock常用的API

  • lock()、unlock()加锁、解锁
public class LockTest {

private Lock lock = new ReentrantLock();

/**
* 当前线程释放锁后,其他线程才可以获取到锁
*/
public void lock(){
lock.lock();
try {
System.out.println("线程"+Thread.currentThread().getName()+":获取到锁资源");
Thread.sleep(2000L);
}catch (Exception e){
System.out.println("线程"+Thread.currentThread().getName()+":释放锁发生异常");
}finally {
lock.unlock();
System.out.println("线程"+Thread.currentThread().getName()+":释放锁完毕");
}
}

public static void main(String[] args) {

LockTest lockTest = new LockTest();

new Thread(()->{
lockTest.lock();
}).start();

new Thread(()->{
lockTest.lock();
}).start();
}
}

【并发编程】锁机制_java_02

  • 尝试获取锁tryLock(),表示用来尝试获取锁,如果获取成功返回true,失败返回false。
public class LockTest {

private Lock lock = new ReentrantLock();

/**
* 线程尝试获取线程锁,如果有其他线程占用,就返回false,无法拿到锁
*/
public void lock(){
if(lock.tryLock()){
try {
System.out.println("线程"+Thread.currentThread().getName()+":获取到锁资源");
Thread.sleep(2000L);
}catch (Exception e){
System.out.println("线程"+Thread.currentThread().getName()+":释放锁发生异常");
}finally {
lock.unlock();
System.out.println("线程"+Thread.currentThread().getName()+":释放锁完毕");
}
}else{
System.out.println("线程"+Thread.currentThread().getName()+":尝试获取锁失败,其他线程持有锁");
}
}

public static void main(String[] args) {

LockTest lockTest = new LockTest();

new Thread(()->{
lockTest.lock();
}).start();

new Thread(()->{
lockTest.lock();
}).start();
}
}

【并发编程】锁机制_System_03

  • tryLock(3000,TimeUtil.MILLISECONDS)尝试获取锁,获取不到就等待3s,如果3s后还是获取不到就返回false
public class LockTest {

private Lock lock = new ReentrantLock();

/**
* 线程尝试获取线程锁,如果有其他线程占用,就返回false,无法拿到锁
*/
public void lock() throws InterruptedException {
if(lock.tryLock(3000, TimeUnit.MILLISECONDS)){
try {
System.out.println("线程"+Thread.currentThread().getName()+":获取到锁资源");
Thread.sleep(5000L);
}catch (Exception e){
System.out.println("线程"+Thread.currentThread().getName()+":释放锁发生异常");
}finally {
lock.unlock();
System.out.println("线程"+Thread.currentThread().getName()+":释放锁完毕");
}
}else{
System.out.println("线程"+Thread.currentThread().getName()+":尝试获取锁失败,其他线程持有锁");
}
}

public static void main(String[] args) {

LockTest lockTest = new LockTest();

new Thread(()->{
try {
lockTest.lock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();

new Thread(()->{
try {
lockTest.lock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}

【并发编程】锁机制_java_04

3.自定义实现可重入锁

(1)可重入锁简介

  • 可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他线程是不可以的。
  • synchronized和ReetrantLock都是可重入锁。
  • 可重入锁的意义就在于防止死锁。
  • 实现原理是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的,线程请求一个未被占用的锁时,JVM将记录锁的占有者,并且将请求计数器置为1。
  • 如果同一个线程再次请求这个锁,计数器将递增。
  • 每次占用线程退出同步块,计数器将递减,直到计数器为0,锁被释放。
  • 关于父类和子类的锁的重入:子类覆盖了父类的synchonized方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码将会产生死锁。

(2)伪代码案例

class A
public synchronized methodA(){
methodB();
}

public synchronized methodB(){
}

#当线程调用A类的对象methodA同步方法,如果其他线程没有获取A类的对象锁,那么当前线程就获得点钱A类对象的锁,然后执行methodB同步方法,当前线程能够在次获取A类对象的锁,其他线程是不可以的,这就是可重入锁。

(3)自定义锁发生死锁问题

  • 自定义锁继承ReentrantLock类重写lock和unlock方法
public class MyLock extends ReentrantLock {

//定义锁的标志位
private boolean isHoldLock = false;

/**
* 同一时刻,能且仅能有一个线程获取到锁
* 其他线程只能等待该线程释放锁之后才能获取到锁
*/
@Override
public synchronized void lock() {
//判断是否有当前线程持有锁,如果有就进入等待
if (isHoldLock) {
try {
wait();
} catch (Exception e) {
e.printStackTrace();
}
}
//将锁的标志置成true,表示当前线程持有锁
isHoldLock = true;
}

@Override
public synchronized void unlock() {
//唤醒等待的线程
notify();
//释放锁资源
isHoldLock = false;
}
}
  • 定义ReentryDemo创建两个方法A、B其中在A方法中调用B方法
public class ReentryDemo {

private Lock lock = new MyLock();

public void methodA(){
lock.lock();
System.out.println("进入方法A");
methodB();
lock.unlock();
}

public void methodB(){
lock.lock();
System.out.println("进入方法B");
lock.unlock();
}

public static void main(String[] args) {
ReentryDemo reentryDemo = new ReentryDemo();
reentryDemo.methodA();
}

}
  • 上面这段代码看似没有问题,但其实已经造成了死锁。
  • 运行结果:

【并发编程】锁机制_java_05

  • jconsole调出java管控台,查看当前运行的线程,发现并没有出现死锁的现象。

【并发编程】锁机制_自定义_06

  • 回到代码分析原因

【并发编程】锁机制_System_07

  • 解决办法:将自定义的锁改成可重入锁,当同一线程再次访问的时候,允许再次获取锁。

(4)自定义可重入锁

  • 自定义锁继承ReentrantLock类重写lock和unlock方法
public class MyLock extends ReentrantLock {

private boolean isHoldLock = false;

//当前线程实例
private Thread holdLockThread = null;

//重入的次数
private int reentryCount = 0;

/**
* 同一时刻,能且仅能有一个线程获取到锁
* 其他线程只能等待该线程释放锁之后才能获取到锁
*/
@Override
public synchronized void lock() {
//判断当前线程是否持有锁,并且是不是同一个线程重复进入
if(isHoldLock && Thread.currentThread() != holdLockThread){
try{
wait();
}catch (Exception e){
e.printStackTrace();
}
}
holdLockThread = Thread.currentThread();
isHoldLock = true;
//记录加锁的次数
reentryCount++;
}

@Override
public synchronized void unlock() {
//判断当前线程是否是持有锁的线程,是,重入次数减1,不是就不做处理
if(Thread.currentThread() == holdLockThread){
reentryCount--;
if(reentryCount == 0){
notify();
isHoldLock = false;
}
}
}
}
  • 测试代码和上面相同,此处省略
  • 执行结果:

【并发编程】锁机制_System_08

  • 分析原因:

【并发编程】锁机制_System_09


【并发编程】锁机制_自定义_10

(5) 一个线程执行同步代码时,再次重入该锁过程中,如果抛出异常,会释放锁吗?

  • 注意:如果有涉及锁的操作的业务方法的时候,都要加上try-catch-finally,在finally中释放锁资源。
  • 锁不释放的案例

【并发编程】锁机制_自定义_11

public class ReentryDemo {

private Lock lock = new MyLock();

private int num = 0;

public void methodA() throws InterruptedException {

lock.lock();
System.out.println("进入方法A");
methodB();
Thread.sleep(3000L);
if (num == 0) {
num++;
int a = 1 / 0;
}
lock.unlock();
}

public void methodB() {
lock.lock();
System.out.println("进入方法B");
lock.unlock();
}

public static void main(String[] args) {
ReentryDemo reentryDemo = new ReentryDemo();
new Thread(() -> {
try {
reentryDemo.methodA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();

new Thread(() -> {
try {
reentryDemo.methodA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}

【并发编程】锁机制_自定义_12

  • 修改代码优化
public class ReentryDemo {

private Lock lock = new MyLock();

private int num = 0;

public void methodA() {
try {
lock.lock();
System.out.println("进入方法A");
methodB();
Thread.sleep(3000L);
if (num == 0) {
num++;
int a = 1 / 0;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

public void methodB() {
try {
lock.lock();
System.out.println("进入方法B");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

public static void main(String[] args) {
ReentryDemo reentryDemo = new ReentryDemo();
new Thread(() -> {
reentryDemo.methodA();
}).start();

new Thread(() -> {
reentryDemo.methodA();
}).start();
}

}

【并发编程】锁机制_System_13


标签:Thread,lock,void,编程,并发,线程,new,机制,public
From: https://blog.51cto.com/u_15646271/6019972

相关文章

  • 浅谈InnoDB存储引擎的MVCC机制
    文章目录​​一、数据库事务隔离级别​​​​1、事务原则​​​​2、4种隔离级别​​​​3、会出现的3种问题​​​​二、MVCC概念​​​​1、基本结构​​​​2、字段介绍......
  • 【并发编程】ThreadLocal详解
    文章目录​​1.ThreadLocal简介​​​​2.ThreadLocal的简单使用​​​​3.ThreadLocal的实现原理​​​​4.ThreadLocal不支持继承性​​​​5.InheritableThreadLocal支持......
  • 【并发编程】线程间的通信
    文章目录​​1.wait、notify、notifyAll​​​​2.生产者消费者模型​​​​3.管道流进行线程间的通信​​​​4.Thread.join()方法​​​​5.Condition详解​​1.wait、not......
  • Spring-webflux 响应式编程
    热爱可抵漫长岁月文章目录​​1.前言​​​​2.Spring-webflux简介​​​​3.什么是“响应式”​​​​4.Spring-webflux的响应式API​​​​5.SpringMVC还是WebFlu......
  • 读编程与类型系统笔记11_高级类型及其他
    1. 范畴论1.1. 范畴论是数学的一个分支,研究的是由对象及这些对象之间的箭头组成的结构1.2. 函子和单子的概念来自范畴论1.3. Haskell是一种编程语言,从范畴论中汲取......
  • 17种编程语言实现排序算法-冒泡排序
    开源地址https://gitee.com/lblbc/simple-works/tree/master/sort/bubbleSort1.安卓Java版privatevoidsort(int[]array){for(inti=0;i<array.length......
  • python基础: 垃圾回收机制、字符编码、文件操作
    目录垃圾回收机制引用计数标记清除分代回收字符编码简介发展史字符编码的实操文件操作垃圾回收机制说明:我们在编写代码的时候涉及到存储空间的申请和存储空间的释放的操......
  • 【Dubbo3终极特性】「流量治理体系」一文教你如何通过Dubbo-Admin实现动态进行流量隔
    背景信息如果一个应用有多个版本在线上同时运行,部署在不同环境中,如日常环境和特殊环境,则可以使用标签路由对不同环境中的不同版本进行流量隔离,将秒杀订单流量或不同渠道......
  • 高职学生如何成为编程高手
    高职学生如何成为编程高手不知不觉在高职教学7年了,这7年的教学经验使我感受颇深!高职高专学习软件的学生,很多在入学的时候都有比较大理想:我一定要成为一名编程高手......
  • 提高C#编程水平的50个要点(二)
    26.对需要排序的对象实现IComparable和IComparer接口27.避免使用 ICloneable接口28.避免使用类型转换操作符29.只有当基类加入了与派生类中现有的函数名称相同的函数时,才需......