Google Guava Striped 实现细粒度锁

Stella981
• 阅读 808

首先不谈Striped能做什么,我们来看下如下的代码

/**
  * 购买产品
  * @param user 用户
  * @param buyAmount 购买金额
  * @param productId 产品编号
  */
 public static void buy(String user,Integer buyAmount,String productId){
  System.out.println(user+":开始购买【"+productId+"】的产品");
  Product product = DB.getProduct(productId);
  if(product.getTotalAmount() > 0 && product.getTotalAmount() >= buyAmount){
   int residual = product.getTotalAmount() - buyAmount;
   product.setTotalAmount(residual);//更新数据库
   System.out.println(user+":成功购买【"+productId+"】产品,产品剩余价值为【"+residual+"】");
  }else{
   System.out.println(user+":购买【"+productId+"】产品失败,产品剩余价值为【"+product.getTotalAmount()+"】");
  }
 } 
 
public static void main(String[] args) {
  String user1 = "张三";
  buy(user1, 10000, "1");
}
/**
 * 销售产品
 * @author lis
 */
public class Product {
    /** ID */
    private String id;
    /** 总价值 ,每个产品的价值为1W */
    private Integer totalAmount = 10000;
    //省略getter..setter
}

运行结果
张三:开始购买【1】的产品
张三:成功购买【1】产品,产品剩余价值为【0】
我想大家能够立即看出来,这段代码是有问题的,非线程安全的。
假如同时有两个用户发起购买,一定会出现线程安全问题。
这里我就不在验证了,那么修改buy方法代码结构如下

//在buy方法上加了synchronized,同时使当前线程睡眠5秒

/**
  * 购买产品
  * @param user 用户
  * @param buyAmount 购买金额
  * @param productId 产品编号
  */
public synchronized static void buy(String user,Integer buyAmount,String productId)throws Exception{
  System.out.println(user+":开始购买【"+productId+"】的产品");
  Thread.sleep(5000);//睡眠5秒
  Product product = DB.getProduct(productId);
  if(product.getTotalAmount() > 0 && product.getTotalAmount() >= buyAmount){
   int residual = product.getTotalAmount() - buyAmount;
   product.setTotalAmount(residual);//更新数据库
   System.out.println(user+":成功购买【"+productId+"】产品,产品剩余价值为【"+residual+"】");
  }else{
   System.out.println(user+":购买【"+productId+"】产品失败,产品剩余价值为【"+product.getTotalAmount()+"】");
  }
 }

main方法修改如下

public static void main(String[] args) {
  //运行开始时间
  long startTime = System.currentTimeMillis();
  //这个类主要是,使多个线程同时进行工作,如果不了解建议网上搜索相关的文章进行学习
  final CyclicBarrier barrier = new CyclicBarrier(2);
  //不限制大小的线程池
  ExecutorService pool = Executors.newCachedThreadPool();
  final String user1 = "张三";
  final String user2 = "李四";
  pool.execute(new Runnable() {
   @Override
   public void run() {
    try {
     barrier.await();
     buy(user1, 10000, "1");
    } catch (Exception e) {
     e.printStackTrace();
    }
   }
  });
  pool.execute(new Runnable() {
   @Override
   public void run() {
    try {
     barrier.await();
     buy(user2, 10000, "2");
    } catch (Exception e) {
     e.printStackTrace();
    }
   }
  });
  pool.shutdown();
  while (!pool.isTerminated()) {  
  }
  System.out.println("运行时间为:【"+TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis() - startTime))+"】秒");
 }

运行结果
李四:开始购买【2】的产品
李四:成功购买【2】产品,产品剩余价值为【0】
张三:开始购买【1】的产品
张三:成功购买【1】产品,产品剩余价值为【0】
运行时间为:【10】秒

从运行结果不难看出,线程是安全了,但是运行效率降低了,众所周知,在一个方法上加锁,那么锁的粒度太大了。我们能不能对

销售产品的ID进行加锁呢?

比如这样修改buy方法?

/**
  * 购买产品
  * @param user 用户
  * @param buyAmount 购买金额
  * @param productId 产品编号
  */
 public static void buy(String user,Integer buyAmount,String productId)throws Exception{
  synchronized(productId){
   System.out.println(user+":开始购买【"+productId+"】的产品");
   TimeUnit.SECONDS.sleep(5);//使当前线程睡眠5秒
   Product product = DB.getProduct(productId);
   if(product.getTotalAmount() > 0 && product.getTotalAmount() >= buyAmount){
    int residual = product.getTotalAmount() - buyAmount;
    product.setTotalAmount(residual);//更新数据库
    System.out.println(user+":成功购买【"+productId+"】产品,产品剩余价值为【"+residual+"】");
   }else{
    System.out.println(user+":购买【"+productId+"】产品失败,产品剩余价值为【"+product.getTotalAmount()+"】");
   }
  }
 }

运行结果李四:开始购买【2】的产品
张三:开始购买【1】的产品
李四:成功购买【2】产品,产品剩余价值为【0】
张三:成功购买【1】产品,产品剩余价值为【0】
运行时间为:【5】秒

时间立即缩短了,想想如果2个用户购买的是1个产品,这样能够锁定么?运行时间是5秒还是10秒?

那么我们修改main方法中的两个线程,产品ID相同buy(user2, 10000, "1");,这里就不在贴代码了

运行结果
李四:开始购买【1】的产品
李四:成功购买【1】产品,产品剩余价值为【0】
张三:开始购买【1】的产品
张三:购买【1】产品失败,产品剩余价值为【0】
运行时间为:【10】秒

居然同步成功了,那么这个方法是不是就解决了不同产品之间,非同一条数据,就能够降低锁的粒度,同时提高程序的性能问题呢?

那么我们在对main方法中的buy方法调度进行修改:buy(user2, 10000, new String("1"));

运行结果
李四:开始购买【1】的产品
张三:开始购买【1】的产品
李四:成功购买【1】产品,产品剩余价值为【0】
张三:成功购买【1】产品,产品剩余价值为【0】
运行时间为:【5】秒

 看到这个结果...很明显失败了,这不是我们想要的结果。。

那么为什么在没有使用new之前是可以进行数据同步的呢?众所周知,synchronized是对象锁,它锁定的堆内存地址在JVM中一定是唯一的。之前之所以没有问题,是因为String的常量池机制,这个如果不清楚...建议搜索相关文章自学补脑

既然上述的形式不行,那么我们怎么降低锁的粒度,达到ID不一样则锁不会冲突呢?

-------------------------------------------------------

那么下面隆重介绍google guava的Striped这个类了

它的底层实现是ConcurrentHashMap,它的原理参照:http://blog.csdn.net/liuzhengkang/article/details/2916620

Striped主要是保证,传递对象的hashCode一致,返回相同对象的锁,或者信号量

但是它不能保证对象的hashCode不一致,则返回的Lock未必不是同一个。

如果想降低这种概率发生,可以调整stripes的数值,数值越高发生的概率越低。

不难理解,之所以会出现这种问题完全取决于缓存锁的大小,我个人是这么理解的,如有错误请批评指正,相互学习!

它可以获取如下两种类型:

  1. java.util.concurrent.locks.Lock

  2. java.util.concurrent.Semaphore

这里我介绍下Lock,而不说Semaphore。

创建一个强引用的Striped

com.google.common.util.concurrent.Striped.lock(int)

创建一个弱引用的Striped

com.google.common.util.concurrent.Striped.lazyWeakLock(int)

上面的两个方法等同于它的构造方法

那么如何理解它所谓的强和弱呢?

我个人是这么理解的:它的强和弱等同于Java中的强引用和弱引用,强则为不回收,弱则为在JVM执行垃圾回收时立即回收。

这里我使用的是弱引用,当JVM内存不够时,回收这些锁。

那么下面直接上代码:究竟如何用这个玩意,修改之前的buy方法

//创建一个弱引用的Striped<Lock>
 private static final Striped<Lock> striped = Striped.lazyWeakLock(127);
 /**
  * 购买产品
  * @param user 用户
  * @param buyAmount 购买金额
  * @param productId 产品编号
  */
 public static void buy(String user,Integer buyAmount,String productId)throws Exception{
  Lock lock = striped.get(productId);//获取锁
  try{
   lock.lock();//锁定
   System.out.println(user+":开始购买【"+productId+"】的产品");
   TimeUnit.SECONDS.sleep(5);//使当前线程睡眠5秒
   Product product = DB.getProduct(productId);
   if(product.getTotalAmount() > 0 && product.getTotalAmount() >= buyAmount){
    int residual = product.getTotalAmount() - buyAmount;
    product.setTotalAmount(residual);//更新数据库
    System.out.println(user+":成功购买【"+productId+"】产品,产品剩余价值为【"+residual+"】");
   }else{
    System.out.println(user+":购买【"+productId+"】产品失败,产品剩余价值为【"+product.getTotalAmount()+"】");
   }
  }finally{
   lock.unlock();//释放锁
  }
 }

运行结果:相同ID的销售产品
张三:开始购买【1】的产品
张三:成功购买【1】产品,产品剩余价值为【0】
李四:开始购买【1】的产品
李四:购买【1】产品失败,产品剩余价值为【0】
运行时间为:【10】秒 
---------------------------------------------------------------------
运行结果:不同ID的销售产品
李四:开始购买【2】的产品
张三:开始购买【1】的产品
张三:成功购买【1】产品,产品剩余价值为【0】
李四:成功购买【2】产品,产品剩余价值为【0】
运行时间为:【5】秒

那么我们之前想要的效果达到了。。。

---------------------------------------------------完整代码------------------------------------------------------

package com.lis.guava.study;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import com.google.common.util.concurrent.Striped;
public class Operator {
 
 public static void main(String[] args) {
  //运行开始时间
  long startTime = System.currentTimeMillis();
  //这个类主要是,使多个线程同时进行工作,如果不了解建议网上搜索相关的文章进行学习
  final CyclicBarrier barrier = new CyclicBarrier(2);
  //不限制大小的线程池
  ExecutorService pool = Executors.newCachedThreadPool();
  final String user1 = "张三";
  final String user2 = "李四";
  pool.execute(new Runnable() {
   @Override
   public void run() {
    try {
     barrier.await();
     buy(user1, 10000, new String("1"));
    } catch (Exception e) {
     e.printStackTrace();
    }
   }
  });
  pool.execute(new Runnable() {
   @Override
   public void run() {
    try {
     barrier.await();
     //buy(user2, 10000, new String("2"));
     buy(user2, 10000, new String("1"));
    } catch (Exception e) {
     e.printStackTrace();
    }
   }
  });
  pool.shutdown();
  while (!pool.isTerminated()) {  
  }
  System.out.println("运行时间为:【"+TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis() - startTime))+"】秒");
 }
 
 //创建一个弱引用的Striped<Lock>
 private static final Striped<Lock> striped = Striped.lazyWeakLock(100);
 /**
  * 购买产品
  * @param user 用户
  * @param buyAmount 购买金额
  * @param productId 产品编号
  */
 public static void buy(String user,Integer buyAmount,String productId)throws Exception{
  Lock lock = striped.get(productId);//获取锁
  try{
   lock.lock();//锁定
   System.out.println(user+":开始购买【"+productId+"】的产品");
   TimeUnit.SECONDS.sleep(5);//使当前线程睡眠5秒
   Product product = DB.getProduct(productId);
   if(product.getTotalAmount() > 0 && product.getTotalAmount() >= buyAmount){
    int residual = product.getTotalAmount() - buyAmount;
    product.setTotalAmount(residual);//更新数据库
    System.out.println(user+":成功购买【"+productId+"】产品,产品剩余价值为【"+residual+"】");
   }else{
    System.out.println(user+":购买【"+productId+"】产品失败,产品剩余价值为【"+product.getTotalAmount()+"】");
   }
  }finally{
   lock.unlock();//释放锁
  }
 }
 
} 
package com.lis.guava.study;
import java.util.HashMap;
import java.util.Map;
/**
 * 模拟DataBase
 * @author lis
 *
 */
public class DB {
 
 private static Map<String, Product> products = new HashMap<>();
 static {
  // 初始化数据
  products.put("1", new Product("1"));
  products.put("2", new Product("2"));
 }
 public static Product getProduct(String productId) {
  return products.get(productId);
 }
}
package com.lis.guava.study;
/**
 * 销售产品
 * @author lis
 */
public class Product {
 /** ID */
 private String id;
 /** 总价值 ,每个产品的价值为10W */
 private Integer totalAmount = 10000;
 public Product(String id) {
  this.id = id;
 }
 public String getId() {
  return id;
 }
 public void setId(String id) {
  this.id = id;
 }
 public Integer getTotalAmount() {
  return totalAmount;
 }
 public void setTotalAmount(Integer totalAmount) {
  this.totalAmount = totalAmount;
 }
}

-------------------------------------------------------------------------------------------------------------------

Striped我就介绍到这里,感兴趣的童鞋可以自己研究下它底层是如何实现的。

我的观点未必正确,如有错误,十分希望各位童鞋能够批评指正,相互学习、相互进步!!!

点赞
收藏
评论区
推荐文章
陈发良 陈发良
3年前
utils.js 文件 工具类 方法分享
javascript/时间解析工具@param{(Object|string|number)}time@param{string}cFormat@returns{string|null}/exportfunctionparseTime(time,cFormat){if(arguments.leng
Wesley13 Wesley13
2年前
java 处理emoji表情
public class EmojiUtil {/  将str中的emoji表情转为byte数组    @param str  @return /public static String resolveToByteFromEmoji(String str
Stella981 Stella981
2年前
C# winform判断窗体是否已打开
Form1form;///<summary///开始检测///</summary///<paramname"sender"</param///<paramname"e"</param
Stella981 Stella981
2年前
Parameter 'name' not found. Available parameters are [arg1, arg0, param1, param2]
!(https://oscimg.oschina.net/oscnet/0c930a24d551e0970e306d79a64f7148999.gif)!(https://oscimg.oschina.net/oscnet/3bca3884ca653a179e799d56df36d4622b3.png)解决方法:<selectid"sel
Stella981 Stella981
2年前
Scala中的match(模式匹配)
/\\模式匹配\/caseclassClass1(param1:String,param2:String)caseclassClass2(param1:String)objectCase{defmain(args:Array\String\){//通过模式匹配进行条件判断valtest1:
Wesley13 Wesley13
2年前
CURL请求
<?php    /        发起一个HTTP(S)请求,并返回json格式的响应数据        @param array 错误信息  array($errorCode, $errorMessage)        @param string 请求Url        @para
Stella981 Stella981
2年前
C#下载csv代码总结
一、C导出csv格式代码如下:1///<summary2///下载3///</summary4///<paramname"startTime"</param5///<
Wesley13 Wesley13
2年前
JAVA 线程基本知识汇总--ThreadLocal 和 InheritableThreadLoc
package org.test;public class ThreadLocalTest {public static void main(String args) {User user  new User(new ThreadLocal<String());Book book 
Stella981 Stella981
2年前
OneThink1.0正式版插件URL生成位置修复
/  插件显示内容里生成访问插件的url  @param string $url url  @param array $param 参数  @author 麦当苗儿 <zuojiazi@vip.qq.com /function addons_url($url, $param 
Stella981 Stella981
2年前
Monogdb使用 MapReduce进行分组统计查询
/      @param businessNo    @param beginTime 开始时间   @param endTime 结束时间   @param pageNo  页码   @param pageSiz