坚持学习第三天:Google Guava Striped 实现细粒度锁

  • 认真是一种态度,坚持是一种品格,优秀是一种习惯!

一、基础理论

二、为何需要加细粒度锁

我们先简单的看一下一下代码及执行效果:

2.1 基础类

package com.cooper.demp;

import lombok.Data;

/**
 * 商品信息
 */
@Data
public class Product {

    /**
     * ID
     */
    private String id;
    /**
     * 总价值 ,每个产品的价值为1W
     */
    private Integer totalAmount = 10000;

    public Product(String id){
        this.id=id;
    } 
} 
package com.cooper.demp;

import java.util.HashMap;
import java.util.Map;

/**
 * 获取商品信息工具类【模拟数据库操作】
 */
public class DbUitil {

    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);
    }
} 

2.2 不加任何锁机制的售卖商品

package com.cooper.demp;

public class SimpleBuy {

    /**
     * 购买产品
     * @param user 用户
     * @param buyAmount 购买金额
     * @param productId 产品编号
     */
    public static void buy(String user,Integer buyAmount,String productId){
        System.out.println(user+":开始购买【"+productId+"】的产品");
        Product product = DbUitil.getProduct(productId);
        if(product.getTotalAmount() > 0 && product.getTotalAmount() >= buyAmount){
            int residual = product.getTotalAmount() - buyAmount;
            product.setTotalAmount(residual);
            System.out.println(user+":simple buy成功购买【"+productId+"】产品,产品剩余价值为【"+residual+"】");
        }else{
            System.out.println(user+":simple buy购买【"+productId+"】产品失败,产品剩余价值为【"+product.getTotalAmount()+"】");
        }
    }
}

  • 测试:
package com.cooper.demp;

public class TestBuyDemo {

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        SimpleBuy.buy("张三", 10000, "1");
        System.out.println("执行时间【" + (System.currentTimeMillis() - startTime) + "】毫秒");
    }
}
  • 执行效果:
张三:开始购买【1】的产品
张三:simple buy 成功购买【1】产品,产品剩余价值为【0】
执行时间【1006】毫秒

从结果上看,好像挺正常也没什么问题,但很明显我们能看得出这个代码是非线程安全的。那么多个用户同时购买的情况下,必然会出现商品多卖的情况,我们通过多线程简单模拟一下。

 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();
                        SimpleBuy.buy(user1, 10000, "1");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        barrier.await();
                        SimpleBuy.buy(user2, 10000, "1");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            pool.shutdown();
            while (!pool.isTerminated()) {
            }
            System.out.println("运行时间为:【"+ TimeUnit.MILLISECONDS.toMillis((System.currentTimeMillis() - startTime))+"】毫秒");
    }
  • 执行结果:
李四:开始购买【1】的产品
张三:开始购买【1】的产品
李四:simple buy 成功购买【1】产品,产品剩余价值为【0】
张三:simple buy 购买【1】产品失败,产品剩余价值为【0】
运行时间为:【1010】毫秒

2.3 开始添加锁了解锁

很明显,库存仅10000的商品,被卖出去了两次。有的同学就会说给buy加上synchronized不就可以解决这个问题了吗,我们看一下效果,先修改buy方法。

  /**
     * 购买产品
     * @param user 用户
     * @param buyAmount 购买金额
     * @param productId 产品编号
     */
    public synchronized static void syncBuy(String user,Integer buyAmount,String productId){
        System.out.println("syncBuy :"+user+":开始购买【"+productId+"】的产品");
        try{
            //使当前线程睡眠1秒
            TimeUnit.SECONDS.sleep(1);
        }catch(Exception e){
            e.printStackTrace();
        }
        Product product = DbUitil.getProduct(productId);
        if(product.getTotalAmount() > 0 && product.getTotalAmount() >= buyAmount){
            int residual = product.getTotalAmount() - buyAmount;
            product.setTotalAmount(residual);
            System.out.println(user+":syncBuy  成功购买【"+productId+"】产品,产品剩余价值为【"+residual+"】");
        }else{
            System.out.println(user+":syncBuy  购买【"+productId+"】产品失败,产品剩余价值为【"+product.getTotalAmount()+"】");
        }
    }
  • 执行效果:
syncBuy :张三:开始购买【1】的产品
张三:syncBuy  成功购买【1】产品,产品剩余价值为【0】
syncBuy :李四:开始购买【1】的产品
李四:syncBuy  购买【1】产品失败,产品剩余价值为【0】
运行时间为:【2010】毫秒

从结果上可以看到,商品没有出现多卖的情况,但是执行时间却上去了。众所周知,在一个方法上加锁,那么锁的粒度太大了,后一个线程必须等待这个方法执行完释放锁才能开始执行。那么能否减小锁的粒度呢!比如只锁住我们操作的商品。
再次对buy方法做出改变。

 /**
     * 购买产品
     * @param user 用户
     * @param buyAmount 购买金额
     * @param productId 产品编号
     */
    public static void syncProductBuy(String user,Integer buyAmount,String productId){
        synchronized(productId) {
            System.out.println("syncProductBuy :" + user + ":开始购买【" + productId + "】的产品");
            try {
                //使当前线程睡眠1秒
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
            Product product = DbUitil.getProduct(productId);
            if (product.getTotalAmount() > 0 && product.getTotalAmount() >= buyAmount) {
                int residual = product.getTotalAmount() - buyAmount;
                product.setTotalAmount(residual);
                System.out.println(user + ":syncProductBuy  成功购买【" + productId + "】产品,产品剩余价值为【" + residual + "】");
            } else {
                System.out.println(user + ":syncProductBuy  购买【" + productId + "】产品失败,产品剩余价值为【" + product.getTotalAmount() + "】");
            }
        }
    }
  • 测试:
 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();
                        SimpleBuy.syncProductBuy(user1, 10000, "1");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        barrier.await();
                        SimpleBuy.syncProductBuy(user2, 10000, "2");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            pool.shutdown();
            while (!pool.isTerminated()) {
            }
            System.out.println("运行时间为:【"+ TimeUnit.MILLISECONDS.toMillis((System.currentTimeMillis() - startTime))+"】毫秒");
    }
  • 执行结果:
# 购买不同的个产品
syncProductBuy :张三:开始购买【1】的产品
syncProductBuy :李四:开始购买【2】的产品
张三:syncProductBuy  成功购买【1】产品,产品剩余价值为【0】
李四:syncProductBuy  成功购买【2】产品,产品剩余价值为【0】
运行时间为:【1011】毫秒 

对比之前的锁方法发现,购买不同的产品时,时间瞬间得到提升;但是在购买同一个产品的情况下,是不是依然可以同步成功,并且缩短执行时间呢。

  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();
                        SimpleBuy.syncProductBuy(user1, 10000, "1");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        barrier.await();
                        SimpleBuy.syncProductBuy(user2, 10000, "1");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            pool.shutdown();
            while (!pool.isTerminated()) {
            }
            System.out.println("运行时间为:【"+ TimeUnit.MILLISECONDS.toMillis((System.currentTimeMillis() - startTime))+"】毫秒");
    }
  • 执行效果:
# 购买同一个产品
syncProductBuy :李四:开始购买【1】的产品
李四:syncProductBuy  成功购买【1】产品,产品剩余价值为【0】
syncProductBuy :张三:开始购买【1】的产品
张三:syncProductBuy  购买【1】产品失败,产品剩余价值为【0】
运行时间为:【2009】毫秒

居然同步成功了,那么这个方法是不是就解决了不同产品之间,非同一条数据,就能够降低锁的粒度,同时提高程序的性能问题呢?那么我们在对main方法中的buy方法调度进行修改:buy(user2, 10000, new String("1"));

 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();
                        SimpleBuy.syncProductBuy(user1, 10000, new String("1"));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        barrier.await();
                        SimpleBuy.syncProductBuy(user2, 10000, new String("1"));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            pool.shutdown();
            while (!pool.isTerminated()) {
            }
            System.out.println("运行时间为:【"+ TimeUnit.MILLISECONDS.toMillis((System.currentTimeMillis() - startTime))+"】毫秒");
    }
  • 执行效果:
syncProductBuy :李四:开始购买【1】的产品
syncProductBuy :张三:开始购买【1】的产品
李四:syncProductBuy  成功购买【1】产品,产品剩余价值为【0】
张三:syncProductBuy  成功购买【1】产品,产品剩余价值为【0】
运行时间为:【1010】毫秒

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

那么为什么在没有使用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<Lock>

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

创建一个弱引用的Striped<Lock>

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

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

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

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

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

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

 /**
     * 购买产品
     * @param user 用户
     * @param buyAmount 购买金额
     * @param productId 产品编号
     */
    public static void stripedBuy(String user,Integer buyAmount,String productId)throws Exception{
        //获取锁
        Lock lock = striped.get(productId);
        try{
            //锁定
            lock.lock();
            System.out.println(user+":stripedBuy 开始购买【"+productId+"】的产品");
            //使当前线程睡眠1秒
            TimeUnit.SECONDS.sleep(1);
            Product product = DbUitil.getProduct(productId);
            if(product.getTotalAmount() > 0 && product.getTotalAmount() >= buyAmount){
                int residual = product.getTotalAmount() - buyAmount;
                //更新数据库
                product.setTotalAmount(residual);
                System.out.println(user+":stripedBuy 成功购买【"+productId+"】产品,产品剩余价值为【"+residual+"】");
            }else{
                System.out.println(user+":stripedBuy 购买【"+productId+"】产品失败,产品剩余价值为【"+product.getTotalAmount()+"】");
            }
        }finally{
            lock.unlock();//释放锁
        }
    }
  • 执行效果:
# 购买相同商品
李四:stripedBuy 开始购买【1】的产品
李四:stripedBuy 成功购买【1】产品,产品剩余价值为【0】
张三:stripedBuy 开始购买【1】的产品
张三:stripedBuy 购买【1】产品失败,产品剩余价值为【0】
运行时间为:【2022】毫秒
---------------------------------------------------------------------
# 购买不同商品
李四:stripedBuy 开始购买【2】的产品
张三:stripedBuy 开始购买【1】的产品
李四:stripedBuy 成功购买【2】产品,产品剩余价值为【0】
张三:stripedBuy 成功购买【1】产品,产品剩余价值为【0】
运行时间为:【1022】毫秒

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

我也是看了这篇https://my.oschina.net/lis1314/blog/664142?fromerr=8CDQbye9之后,在根据自己的理解,梳理了一下,感觉自己终于清晰了,诸多不足或不正确的地方,望见谅,可以留言,相互学习,相互提升。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343