【java】java 并发编程 StampedLock 锁 【不重要】

408 篇文章 482 订阅 ¥19.90 ¥99.00


在这里插入图片描述

1.概述

转载并且补充链接:https://www.pdai.tech/md/java/java8/java8-stampedlock.html

2.synchronized

synchronized 在java5之前,实现同步主要是使用synchronized。它是Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。 有四种不同的同步块:

实例方法
静态方法
实例方法中的同步块
静态方法中的同步块

大家对此应该不陌生,所以不多讲了,以下是代码示例

synchronized(this)
// do operation
}

小结: 在多线程并发编程中Synchronized一直是元老级角色,很多人都会称呼它为重量级锁,但是随着Java SE1.6对Synchronized进行了各种优化之后,性能上也有所提升。

3.读写锁

rwlock.writeLock().lock();
try {
	// do operation
} finally {
	rwlock.writeLock().unlock();
}

它是Java 5在java.util.concurrent.locks新增的一个API。

Lock是一个接口,核心方法是lock(),unlock(),tryLock(),实现类有ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock

ReentrantReadWriteLock, ReentrantLock 和synchronized锁都有相同的内存语义。

与synchronized不同的是,Lock完全用Java写成,在java这个层面是无关JVM实现的。Lock提供更灵活的锁机制,很多synchronized 没有提供的许多特性,比如锁投票,定时锁等候和中断锁等候,但因为lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中

下面是Lock的一个代码示例

著作权归https://www.pdai.tech所有。
链接:https://www.pdai.tech/md/java/java8/java8-stampedlock.html

class Point {
   private double x, y;
   private final StampedLock sl = new StampedLock();
   void move(double deltaX, double deltaY) { // an exclusively locked method
     long stamp = sl.writeLock();
     try {
       x += deltaX;
       y += deltaY;
     } finally {
       sl.unlockWrite(stamp);
     }
   }
  	//下面看看乐观读锁案例
   double distanceFromOrigin() { // A read-only method
     long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
     double currentX = x, currentY = y; //将两个字段读入本地局部变量
     if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生? 
        stamp = sl.readLock(); //如果没有,我们再次获得一个读悲观锁
        try {
          currentX = x; // 将两个字段读入本地局部变量
          currentY = y; // 将两个字段读入本地局部变量
        } finally {
           sl.unlockRead(stamp);
        }
     }
     return Math.sqrt(currentX * currentX + currentY * currentY);
   }
	//下面是悲观读锁案例
   void moveIfAtOrigin(double newX, double newY) { // upgrade
     // Could instead start with optimistic, not read mode
     long stamp = sl.readLock();
     try {
       while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
         long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
         if (ws != 0L) { //这是确认转为写锁是否成功
           stamp = ws; //如果成功 替换票据
           x = newX; //进行状态改变
           y = newY; //进行状态改变
           break;
         }
         else { //如果不能成功转换为写锁
           sl.unlockRead(stamp); //我们显式释放读锁
           stamp = sl.writeLock(); //显式直接进行写锁 然后再通过循环再试
         }
       }
     } finally {
       sl.unlock(stamp); //释放读锁或写锁
     }
   }
 }
  

小结: 比synchronized更灵活、更具可伸缩性的锁定机制,但不管怎么说还是synchronized代码要更容易书写些

3.1 读写锁缺点

在RenentrantReadWriteLock里面,存在的问题:

  1. 写线程 “饥饿”问题
  2. 如果有线程在读,那么写线程是无法获取写锁的,是一种悲观锁的策略

4.StampedLock

它是java8在java.util.concurrent.locks新增的一个API。

ReentrantReadWriteLock 在沒有任何读写锁时,才可以取得写入锁,这可用于实现了悲观读取(Pessimistic Reading),即如果执行中进行读取时,经常可能有另一执行要写入的需求,为了保持同步,ReentrantReadWriteLock 的读取锁定就可派上用场。

然而,如果读取执行情况很多,写入很少的情况下,使用 ReentrantReadWriteLock 可能会使写入线程遭遇饥饿(Starvation)问题,也就是写入线程吃吃无法竞争到锁定而一直处于等待状态。

StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁

所谓的乐观读模式,也就是若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定,程序可以查看读取资料之后,是否遭到写入执行的变更,再采取后续的措施(重新读取变更信息,或者抛出异常) ,这一个小小改进,可大幅度提高程序的吞吐量!!

java8引入StampedLock 相当于对ReentrantReadWriteLock进行增强,优化了读锁、写锁的访问,使读写锁之间可以相互转换,
因此可以更细粒度的控制并发。

4.1 缺点

  1. 设计初衷: 是作为一个内部工具类,用于辅助开发其它的线程安全组件。用不好:容易产生死锁,甚至莫名其妙的问题
    2 . 不支持重入,在实用过程中,场景非常受限。

5.案例

5.1 案例1

下面是java doc提供的StampedLock一个例子


class Point {
   private double x, y;
   private final StampedLock sl = new StampedLock();
   void move(double deltaX, double deltaY) { // an exclusively locked method
     long stamp = sl.writeLock();
     try {
       x += deltaX;
       y += deltaY;
     } finally {
       sl.unlockWrite(stamp);
     }
   }
  //下面看看乐观读锁案例
   double distanceFromOrigin() { // A read-only method
     long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
     double currentX = x, currentY = y; //将两个字段读入本地局部变量
     if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生? 
        stamp = sl.readLock(); //如果没有,我们再次获得一个读悲观锁
        try {
          currentX = x; // 将两个字段读入本地局部变量
          currentY = y; // 将两个字段读入本地局部变量
        } finally {
           sl.unlockRead(stamp);
        }
     }
     return Math.sqrt(currentX * currentX + currentY * currentY);
   }
	//下面是悲观读锁案例
   void moveIfAtOrigin(double newX, double newY) { // upgrade
     // Could instead start with optimistic, not read mode
     long stamp = sl.readLock();
     try {
       while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
         long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
         if (ws != 0L) { //这是确认转为写锁是否成功
           stamp = ws; //如果成功 替换票据
           x = newX; //进行状态改变
           y = newY; //进行状态改变
           break;
         }
         else { //如果不能成功转换为写锁
           sl.unlockRead(stamp); //我们显式释放读锁
           stamp = sl.writeLock(); //显式直接进行写锁 然后再通过循环再试
         }
       }
     } finally {
       sl.unlock(stamp); //释放读锁或写锁
     }
   }
 }
  

5.1 案例2

package com.java.lock.stampedlock;

/**
 * @author: chuanchuan.lcc
 * @date: 2021-01-03 12:16
 * @modifiedBy: chuanchuan.lcc
 * @version: 1.0
 * @description:
 */

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;

public class JUCDemo {

    public static void main(String[] args) {

        // 实例化对象-存放数据
        Account account = new Account("张三", 0.0);

        double moneyData[] = new double[]{100.00, 200.00, 300.00, 400.00, 500.00};

        // 5个写入线程
        for(int i = 0; i < 5; i++){
            new Thread(
                    ()->{
                        for(int j = 0; j < moneyData.length; j++){
                            // 存放数据
                            account.saveMoney(moneyData[j]);
                        }
                    }, "写线程:"
            ).start();
        }

        // 30个读取线程
        for(int j = 0; j < 30; j++){
            new Thread(
                    ()->{
                        while (true){
                            // 获取数据
                            System.out.println(account.toString());
                        }
                    }, "读线程:"
            ).start();
        }
    }
}

// 银行账户
class Account{
    private String name;        // 账户名称
    private double asset;       // 账户资产

    // 读/写锁
    private StampedLock stampedLock = new StampedLock();

    /**
     * 双参构造函数---设置账户信息
     * @param name
     * @param asset
     */
    public Account(String name, double asset) {
        this.name = name;
        this.asset = asset;
    }

    /**
     * 资产追加
     * @param money
     */
    public void saveMoney(double money){
        // 获取读锁,检查状态
        long stamp = this.stampedLock.readLock();
        boolean flag = true;    // 设置标志位

        try {
            // 转为写锁
            long writeStamp = this.stampedLock.tryConvertToWriteLock(stamp);
            while(flag){
                // 当前为写锁
                if(writeStamp != 0){
                    stamp = writeStamp;                     // 修改为写锁的标记
                    this.asset += money;                    // 进行资产修改
                    TimeUnit.SECONDS.sleep(1);    // 模拟延迟
                    System.out.println("【" + Thread.currentThread().getName() +
                            "】修改银行资产数据,修改金额:" + money +
                            ",当前资产:" + this.asset);

                    // 结束循环
                    flag = false;
                }else{
                    // 释放读锁
                    this.stampedLock.unlock(stamp);
                    // 获取写锁
                    writeStamp = this.stampedLock.writeLock();
                    stamp = writeStamp;
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            // 解锁
            this.stampedLock.unlock(stamp);
        }
    }

    /**
     * 数据读取
     * @return
     */
    public String toString(){
        // 获取乐观锁
        long stamp = this.stampedLock.tryOptimisticRead();

        try {

            double current = this.asset;          // 获取当前的资产
            TimeUnit.SECONDS.sleep(1);  // 模拟延迟

            /**
             * validate()方法虽然可以检测,但是依然有可能出现异常,
             * 所以本出依据StampledLock类的源代码多追加了一个验证机制
             */
            if(!this.stampedLock.validate(stamp) || (stamp & (long)(Math.pow(2,7)-1)) == 0){
                long readStamp = this.stampedLock.readLock();
                current = this.asset;
                stamp = readStamp;
            }

            return "【账户信息:(" + Thread.currentThread().getName() +
                    ")】账户名称:" + this.name +
                    "、银行资产:" + this.asset;

        }catch (Exception e){
            return null;
        }finally {
            try {
                this.stampedLock.unlock(stamp);
            }catch (Exception e){}
        }
    }
}

运行结果

【写线程:】修改银行资产数据,修改金额:100.0,当前资产:100.0
【写线程:】修改银行资产数据,修改金额:200.0,当前资产:300.0
【写线程:】修改银行资产数据,修改金额:300.0,当前资产:600.0
【写线程:】修改银行资产数据,修改金额:400.0,当前资产:1000.0
【写线程:】修改银行资产数据,修改金额:500.0,当前资产:1500.0
【账户信息:(读线程:)】账户名称:张三、银行资产:1500.0
【账户信息:(读线程:)】账户名称:张三、银行资产:1500.0
【账户信息:(读线程:)】账户名称:张三、银行资产:1500.0
【账户信息:(读线程:)】账户名称:张三、银行资产:1500.0
【账户信息:(读线程:)】账户名称:张三、银行资产:1500.0
【账户信息:(读线程:)】账户名称:张三、银行资产:1500.0
【账户信息:(读线程:)】账户名称:张三、银行资产:1500.0
【账户信息:(读线程:)】账户名称:张三、银行资产:1500.0
【账户信息:(读线程:)】账户名称:张三、银行资产:1500.0
【账户信息:(读线程:)】账户名称:张三、银行资产:1500.0
【写线程:】修改银行资产数据,修改金额:100.0,当前资产:1600.0
【账户信息:(读线程:)】账户名称:张三、银行资产:1600.0
【账户信息:(读线程:)】账户名称:张三、银行资产:1600.0
【账户信息:(读线程:)】账户名称:张三、银行资产:1600.0
【账户信息:(读线程:)】账户名称:张三、银行资产:1600.0
【写线程:】修改银行资产数据,修改金额:100.0,当前资产:1700.0

小结:

StampedLock要比ReentrantReadWriteLock更加廉价,也就是消耗比较小。

6.性能对比

是和ReadWritLock相比,在一个线程情况下,是读速度其4倍左右,写是1倍。

下图是六个线程情况下,读性能是其几十倍,写性能也是近10倍左右:
在这里插入图片描述

7.总结

  1. synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定;
  2. ReentrantLock、ReentrantReadWriteLock,、StampedLock都是对象层面的锁定,要保证锁定一定会被释放,就必须将unLock()放到finally{}中;
  3. StampedLock 对吞吐量有巨大的改进,特别是在读线程越来越多的场景下;
  4. StampedLock有一个复杂的API,对于加锁操作,很容易误用其他方法;
  5. 当只有少量竞争者的时候,synchronized是一个很好的通用的锁实现;
  6. 当线程增长能够预估,ReentrantLock是一个很好的通用的锁实现;

StampedLock 可以说是Lock的一个很好的补充,吞吐量以及性能上的提升足以打动很多人了,但并不是说要替代之前Lock的东西,毕竟他还是有些应用场景的,起码API比StampedLock容易入手。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值