- 认真是一种态度,坚持是一种品格,优秀是一种习惯!
一、基础理论
- 关于Striped此处不作深入说明,它的底层实现是ConcurrentHashMap,它的原理参照:http://blog.csdn.net/liuzhengkang/article/details/2916620
二、为何需要加细粒度锁
我们先简单的看一下一下代码及执行效果:
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之后,在根据自己的理解,梳理了一下,感觉自己终于清晰了,诸多不足或不正确的地方,望见谅,可以留言,相互学习,相互提升。。。