详解java中各类锁的机制

作者:码农研究僧 时间:2022-08-01 06:48:56 

前言

总结java常见的锁

区分各个锁机制以及如何使用

使用方法锁名
考察线程是否要锁住同步资源乐观锁和悲观锁
锁住同步资源后,要不要阻塞不阻塞可以使用自旋锁
一个线程多个流程获取同一把锁可重入锁
多个线程公用一把锁读写锁(写的共享锁)
多个线程竞争要不要排队公平锁与非公平锁

1. 乐观锁与悲观锁

悲观锁:不能同时进行多人,执行的时候先上锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁

乐观锁:通过版本号一致与否,即给数据加上版本,同步更新数据以及加上版本号。不会上锁,判断版本号,可以多人操作,类似生活中的抢票。每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的

(乐观锁可以使用版本号机制和CAS算法实现)

详解java中各类锁的机制

通过具体案例演示悲观锁和乐观锁

在redis框架中

执行multi之前,执行命令watch

具体格式如下


watch key1 [key2]

具体代码格式如下


127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> set add 100
OK
127.0.0.1:6379> watch add
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby add 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 120
127.0.0.1:6379>

flushdb是清空数据库

详解java中各类锁的机制

但如果在另一个服务器上,输入exec,会显示出错

因为用的是乐观锁,被修改了之后版本会发生改变

总的来说:

悲观锁:单独每个人完成事情的时候,执行上锁解锁。解决并发中的问题,不支持并发操作,只能一个一个操作,效率低

乐观锁:每执行一件事情,都会比较数据版本号,谁先提交,谁先提交版本号

2. 公平锁与非公平锁

公平锁:先来先到

非公平锁:不是按照顺序,可插队

  • 公平锁:效率相对低

  • 非公平锁:效率高,但是线程容易饿死

通过这个函数Lock lock = new ReentrantLock(true);。创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁

通过查看源码

带有参数的ReentrantLock(true)为公平锁

ReentrantLock(false)为非公平锁

主要是调用NonfairSync()与FairSync()


public ReentrantLock() {
       sync = new NonfairSync();
   }

/**
    * Creates an instance of {@code ReentrantLock} with the
    * given fairness policy.
    *
    * @param fair {@code true} if this lock should use a fair ordering policy
    */
   public ReentrantLock(boolean fair) {
       sync = fair ? new FairSync() : new NonfairSync();
   }

具体其非公平锁与公平锁的源码

查看公平锁的源码


static final class FairSync extends Sync {
  private static final long serialVersionUID = -3000897897090466540L;

/**
 * Acquires only if reentrant or queue is empty.
  */
 final boolean initialTryLock() {
  Thread current = Thread.currentThread();
  int c = getState();
  if (c == 0) {
  if (!hasQueuedThreads() && compareAndSetState(0, 1)) {
    setExclusiveOwnerThread(current);
     return true;
   }
   } else if (getExclusiveOwnerThread() == current) {
     if (++c < 0) // overflow
         throw new Error("Maximum lock count exceeded");
        setState(c);
        return true;
      }
   return false;
}

通过代码实例具体操作


//第一步  创建资源类,定义属性和和操作方法
class LTicket {
   //票数量
   private int number = 30;

//创建可重入锁
   private final ReentrantLock lock = new ReentrantLock(true);
   //卖票方法
   public void sale() {
       //上锁
       lock.lock();
       try {
           //判断是否有票
           if(number > 0) {
               System.out.println(Thread.currentThread().getName()+" :卖出"+(number--)+" 剩余:"+number);
           }
       } finally {
           //解锁
           lock.unlock();
       }
   }
}

public class LSaleTicket {
   //第二步 创建多个线程,调用资源类的操作方法
   //创建三个线程
   public static void main(String[] args) {

LTicket ticket = new LTicket();

new Thread(()-> {
   for (int i = 0; i < 40; i++) {
       ticket.sale();
   }
},"AA").start();

new Thread(()-> {
           for (int i = 0; i < 40; i++) {
               ticket.sale();
           }
       },"BB").start();

new Thread(()-> {
           for (int i = 0; i < 40; i++) {
               ticket.sale();
           }
       },"CC").start();
   }
}

结果截图如下

详解java中各类锁的机制

都是A线程执行,而BC线程都没执行到,出现了非公平锁

具体改变其设置可以通过可重入锁中的一个有参构造方法

修改代码为private final ReentrantLock lock = new ReentrantLock(true);

代码截图为

详解java中各类锁的机制

3. 可重入锁

可重入锁也叫递归锁

而且有了可重入锁之后,破解第一把之后就可以一直进入到内层结构


Object o = new Object();
new Thread(()->{
   synchronized(o) {
       System.out.println(Thread.currentThread().getName()+" 外层");

synchronized (o) {
           System.out.println(Thread.currentThread().getName()+" 中层");

synchronized (o) {
               System.out.println(Thread.currentThread().getName()+" 内层");
           }
       }
   }

},"t1").start();

synchronized (o)代表锁住当前{ }内的代码块

以上都是synchronized锁机制

下面讲解lock锁机制


public class SyncLockDemo {

public synchronized void add() {
       add();
   }

public static void main(String[] args) {
       //Lock演示可重入锁
       Lock lock = new ReentrantLock();
       //创建线程
       new Thread(()->{
           try {
               //上锁
               lock.lock();
               System.out.println(Thread.currentThread().getName()+" 外层");

try {
                   //上锁
                   lock.lock();
                   System.out.println(Thread.currentThread().getName()+" 内层");
               }finally {
                   //释放锁
                   lock.unlock();
               }
           }finally {
               //释放做
               lock.unlock();
           }
       },"t1").start();

//创建新线程
       new Thread(()->{
           lock.lock();
           System.out.println("aaaa");
           lock.unlock();
       },"aa").start();
       }
}

在同一把锁中的嵌套锁,内部嵌套锁没解锁还是可以输出,但是如果跳出该线程,执行另外一个线程就会造成死锁

要把握上锁与解锁的概念,都要写上

详解java中各类锁的机制

4. 读写锁(共享锁与独占锁)

读锁是共享锁,写锁是独占锁

  • 共享锁的一种具体实现

  • 读写锁管理一组锁,一个是只读的锁,一个是写锁。

读写锁:一个资源可以被多个读线程访问,也可以被一个写线程访问,但不能同时存在读写线程,读写互斥,读读共享(写锁独占,读锁共享,写锁优先级高于读锁)

读写锁ReentrantReadWriteLock

读锁为ReentrantReadWriteLock.ReadLock,readLock()方法

写锁为ReentrantReadWriteLock.WriteLock,writeLock()方法

创建读写锁对象private ReadWriteLock rwLock = new ReentrantReadWriteLock();

写锁 加锁 rwLock.writeLock().lock();,解锁为rwLock.writeLock().unlock();

读锁 加锁rwLock.readLock().lock();,解锁为rwLock.readLock().unlock();

案例分析:

模拟多线程在map中取数据和读数据

完整代码如下


//资源类
class MyCache {
   //创建map集合
   private volatile Map<String,Object> map = new HashMap<>();

//创建读写锁对象
   private ReadWriteLock rwLock = new ReentrantReadWriteLock();

//放数据
   public void put(String key,Object value) {
       //添加写锁
       rwLock.writeLock().lock();

try {
           System.out.println(Thread.currentThread().getName()+" 正在写操作"+key);
           //暂停一会
           TimeUnit.MICROSECONDS.sleep(300);
           //放数据
           map.put(key,value);
           System.out.println(Thread.currentThread().getName()+" 写完了"+key);
       } catch (InterruptedException e) {
           e.printStackTrace();
       } finally {
           //释放写锁
           rwLock.writeLock().unlock();
       }
   }

//取数据
   public Object get(String key) {
       //添加读锁
       rwLock.readLock().lock();
       Object result = null;
       try {
           System.out.println(Thread.currentThread().getName()+" 正在读取操作"+key);
           //暂停一会
           TimeUnit.MICROSECONDS.sleep(300);
           result = map.get(key);
           System.out.println(Thread.currentThread().getName()+" 取完了"+key);
       } catch (InterruptedException e) {
           e.printStackTrace();
       } finally {
           //释放读锁
           rwLock.readLock().unlock();
       }
       return result;
   }
}

public class ReadWriteLockDemo {
   public static void main(String[] args) throws InterruptedException {
       MyCache myCache = new MyCache();
       //创建线程放数据
       for (int i = 1; i <=5; i++) {
           final int num = i;
           new Thread(()->{
               myCache.put(num+"",num+"");
           },String.valueOf(i)).start();
       }

TimeUnit.MICROSECONDS.sleep(300);

//创建线程取数据
       for (int i = 1; i <=5; i++) {
           final int num = i;
           new Thread(()->{
               myCache.get(num+"");
           },String.valueOf(i)).start();
       }
   }
}

5. 互斥锁

互斥锁是独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性


pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;//创建互斥锁并初始化

pthread_mutex_lock(&mutex);//对线程上锁,此时其他线程阻塞等待该线程释放锁

//要执行的代码段

pthread_mutex_unlock(&mutex);//执行完后释放锁

6. 自旋锁

查看百度百科的解释,具体如下 :

它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名

通俗的来说就是一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务。

其特点:

  1. 持有锁时间等待过长,消耗CPU

  2. 无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题

  3. 自旋锁不会使线程状态发生切换,处于用户态(不会到内核态进行线程的状态转换),一直都是活跃,不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。

其模拟算法如下


do{
b=1;
while(b){
lock(bus);
b = test_and_set(&lock);
unlock(bus);
}
//临界区
//lock = 0;
//其余部分
}while(1)

7. 无锁 / 偏向锁 / 轻量级锁 / 重量级锁

  • 无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功

  • 偏向锁:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价

  • 轻量级锁:锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能

  • 重量级锁:线程并发加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,还有线程要访问

来源:https://blog.csdn.net/weixin_47872288/article/details/122093366

标签:java,锁,机制
0
投稿

猜你喜欢

  • 解决try-catch捕获异常信息后Spring事务失效的问题

    2022-11-15 03:17:33
  • MyBatis字段名和属性名不一致的解决方法

    2022-12-15 18:15:22
  • 基于Map的computeIfAbsent的使用场景和使用方式

    2023-04-30 03:04:06
  • Springboot jar主清单属性丢失解决方案

    2022-04-06 05:30:26
  • spring boot 集成 shiro 自定义密码验证 自定义freemarker标签根据权限渲染不同页面(推荐

    2023-07-28 17:39:16
  • java中this与super关键字的使用方法

    2022-05-04 22:03:29
  • Java Stopwatch类,性能与时间计时器案例详解

    2023-07-24 04:08:50
  • Java实战之在线租房系统的实现

    2022-09-29 04:44:18
  • Java遍历Properties所有元素的方法实例

    2022-09-08 14:58:24
  • 使用Maven搭建Hadoop开发环境

    2021-09-11 07:55:45
  • 浅谈java类和对象

    2021-10-01 06:01:59
  • SpringBoot集成Redis—使用RedisRepositories详解

    2023-09-04 08:55:59
  • java_object的简单使用详解

    2023-08-22 11:35:57
  • Java反射通过Getter方法获取对象VO的属性值过程解析

    2023-04-11 06:11:33
  • Android实现悬浮窗的简单方法实例

    2023-06-17 18:11:02
  • Java字节码中jvm实例用法

    2023-08-08 05:25:09
  • 深入学习C#网络编程之HTTP应用编程(下)

    2023-03-16 12:06:37
  • SrpingDruid数据源加密数据库密码的示例代码

    2021-06-21 03:26:26
  • SpringBoot Java后端实现okhttp3超时设置的方法实例

    2022-11-06 04:56:03
  • Java编程中的检查型异常与非检查型异常分析

    2023-11-04 13:08:38
  • asp之家 软件编程 m.aspxhome.com