Java并发编程-锁
来源:互联网 发布:淘宝日本直邮可靠吗 编辑:程序博客网 时间:2024/05/18 16:38
Java并发编程-锁
最近接触了比较多关于锁方面的知识,就记下来,顺便巩固一下。
先定义一个比较简单的场景:
公司准备发一批优惠券,每个用户只能领取一张。
先不考虑多线程的话,可以用redis维护已经领取了优惠券的用户信息,比如手机号。
...private static final ConcurrentSkipListSet<String> mobileSet = new ConcurrentSkipListSet<String>();...//校验if (mobileSet.add(mobile)){ try{ //业务逻辑 do some business; }catch(Exception e){ mobileSet.remove(mobile) }}...
mobileSet.add(mobile) 这个操作其实实现了两个功能,一个是作为容器存储mobile,另一个是校验,如果mobile已经存在,add失败则返回false。以上代码并不能保证绝对安全,在处理业务逻辑的时候服务器异常,可能导致remove操作来不及执行。解决逻辑是拆分mobileSet.add(mobile)的校验和存储功能,拆分后又必须保证对于同一个手机号的并发请求,必须是线程安全的,所以需要引入锁
//利用String.intern()方法保证字面值一样的字符串是同一个对象synchronized (mobile.intern()){ //校验 mobileSet.contains(mobile); //业务逻辑 do some business; //业务逻辑处理成功 mobileSet.add(mobile)}//如果mobile特别多的情况下,mobile.intern()的效率会特别低。//TODO 关于更多关于String.intern()
以下使用google-guava工具包改进String.intern()
Interner<String> interner = Interners.newWeakInterner();synchronized (interner.intern(mobile)){ //校验 mobileSet.contains(mobile); //业务逻辑 do some business; //业务逻辑处理成功 mobileSet.add(mobile)}
以上仅仅在多线程下有效,多进程和分布式系统下就没有用了,简单点可以吧ConcurrentSkipListSet 换成 redis 来实现,局限性一样不能保证redis.SREM(key,mobile)肯定能执行。(因为业务逻辑执行时间比较长,所以风险高。反之,把add操作放在业务逻辑之后,redis高性能,高可用性的特性,能保证在很短时间内就能执行完)
//redis sadd 指令 添加失败返回0 if (redis.SADD(key,mobile) > 0){ try{ //业务逻辑 do some business; }catch(Exception e){ redis.SREM(key,mobile) }}
只能把sadd指令放在业务逻辑后面,然后把synchronized锁替换成分布式锁,分布式锁实现方式有很多种,这里仅简单实用redis实现
//分布式锁private boolean dl(String mobile,long timeout){ long b = System.currentTimeMillis(); while (true){ //redis.setnx 可以保证在已经存在key的时候返回false if (redisClient.setnx(mobile,“”)){ return true; } //超时解锁机制,根据需要处理业务逻辑的时间,设置超时解锁 if (System.currentTimeMillis() - b > timeout){ redisClient.del(mobile); break; } try { // Thread.sleep(50) 会主动释放cpu控制权 CountDownLatch 通过自旋阻塞,不会主动释放cpu控制权 CountDownLatch latch = new CountDownLatch(1); latch.await(50, TimeUnit.MILLISECONDS);// Thread.sleep(50); } catch (InterruptedException e) { logger.error(e); } } return false; }
if (dl(userId, 1000)){ try{ if (!redisClient.sismember(key,mobile)){ //业务逻辑 do some business; redisClient.sadd(key,mobile); } }finally{ redisClient.del(mobile); }}
超时解锁机制可以保证即便finally中没有成功del掉,还有个补救机制,不至于造成死锁。当然也可以用redis的expire来实现更简单。
引申-String.intern()
参考文献:http://java-performance.info/string-intern-in-java-6-7-8/
众做周知,常量池是java提供的系统级的缓存机制。8种基本类型的常量池都是系统协调的,String的常量池比较特殊:
- 用双引号声明String对象会放在常量池中
- 调用intern方法的String对象,会先检查常量池中是否有这个字符串,如果没有,就把这个对象放进去。
这里重点引申intern方法。
点开jdk 1.7(1.6的实现由差异)的源代码,发现是一个native方法:
/** * Returns a canonical representation for the string object. * <p> * A pool of strings, initially empty, is maintained privately by the * class <code>String</code>. * <p> * When the intern method is invoked, if the pool already contains a * string equal to this <code>String</code> object as determined by * the {@link #equals(Object)} method, then the string from the pool is * returned. Otherwise, this <code>String</code> object is added to the * pool and a reference to this <code>String</code> object is returned. * <p> * It follows that for any two strings <code>s</code> and <code>t</code>, * <code>s.intern() == t.intern()</code> is <code>true</code> * if and only if <code>s.equals(t)</code> is <code>true</code>. * <p> * All literal strings and string-valued constant expressions are * interned. String literals are defined in section 3.10.5 of the * <cite>The Java™ Language Specification</cite>. * * @return a string that has the same contents as this string, but is * guaranteed to be from a pool of unique strings. */public native String intern();
String的常量池,初始的时候是空的,仅有String类维护。当对象调用intern方法的时候,如果常量池中存在这个字符串(通过equal方法判断),就返回常量池中对象的引用。否则,调用intern的String对象会被加入到常量池中,返回自身对象的引用。
翻译成java代码就是:
String a;String b;当且仅当a.equal(b) 为 true;a.intern() == b.intern() 才为true;
JDK1.7+中使用String.intern:
好处
- 执行非常快,在多线程模式中(仍然使用全局字符串池)几乎没有性能损失
- 节省内存
坏处
- String pool 中的字符串引用是存储在哈希表StringTable中,且大小为1009。所以当字符串数量级达到一定临界值后,每一个链表(或二叉树)上的数量太多,遍历需要花费很多时间。(-XX:StringTableSize=N 可以设置这个值大小也算是这个问题的一个解决方法)
这段时间实践证明String.intern比较适用于重复率高的字符串场景(或有限集合)。比如人类定义的名词,包括名字,国家之类的。
如果还是没有概念,你可以动手试试,分十次intern 1000w个字符串。每次统计时间,可以明显的发现到后面花费的时间是很恐怖的。
引申-CyclicBarrier
栅栏,可以顾名思义的讲,它可以实现让一组线程互相等待至某个状态然后再一起执行。就像百米赛跑一样,运动员们都会先移动至起跑点并准备就绪,等待裁判的发令枪,这个过程有快有慢(你可以将运动员想象成线程),然后才同时开始跑。
在我看来,栅栏包含三个部分:
- 运动员进场到起跑点准备就绪
- 裁判开枪
- 运动员开始起跑
例子:
import java.util.concurrent.*;/** * Created by cxx on 2017/7/17. */public class CyclicBarrierTest { private static ExecutorService ES = Executors.newCachedThreadPool(); public static void main(String[] args) throws InterruptedException { //裁判 CyclicBarrier judge = new CyclicBarrier(5, new Runnable() { //发令枪 @Override public void run() { System.out.println("bang~~~~~~~~~~~~~~~~~~~~~"); } }); for (int i = 1 ; i < 6 ; i++){ ES.submit(new Runner(i,judge)); Thread.sleep(1000); } ES.shutdown(); }}//运动员class Runner implements Runnable{ private int i; private CyclicBarrier cyclicBarrier; public Runner(int i,CyclicBarrier cyclicBarrier){ this.i = i; this.cyclicBarrier = cyclicBarrier; } @Override public void run() { //准备 System.out.println("im no"+ i +", im be ready."); try { //等待发令枪 cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } //起跑->到达终点 System.out.println("im no"+ i +",im breast the yarn."); }}
结果:
im no1, im be ready.im no2, im be ready.im no3, im be ready.im no4, im be ready.im no5, im be ready.bang~~~~~~~~~~~~~~~~~~~~~im no5,im breast the yarn.im no1,im breast the yarn.im no2,im breast the yarn.im no3,im breast the yarn.im no4,im breast the yarn.
引申-CountDownLatch
介绍几个比较常用的方法:
//等待至countdown的计数器减到0countDown.await();//等待至countdown的计数器减到0 或 超时 (可在某些不希望cpu频繁切换的场景下替换Thread.sleep()使用)countDown.await(1,TimeUnit.MILLISECONDS);//计数器-1countDown.countDown();
例子
import java.util.concurrent.CountDownLatch;import java.util.concurrent.TimeUnit;/** * Created by cxx on 2017/7/17. */public class CountDownLatchTest { public static void main(String[] args) throws InterruptedException { final CountDownLatch countDown = new CountDownLatch(6); for (int i = 1; i < 6 ; i++){ final int j = i; new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("hi,"+j); countDown.countDown(); System.out.println("end,"+j); } }).start(); } countDown.await(1000, TimeUnit.MILLISECONDS); System.out.println("thank you"); }}
引申-Semaphore
一般用于限制流量保护一些有限的资源或服务器,免受高并发的流量导致瘫痪。
//获取一个许可,如果无法获取则阻塞public void acquire() throws InterruptedException//释放一个许可 public void release() //可选择创建一个公平或非公平的限流(公平是以性能为代价,不传的话默认非公平)public Semaphore(int permits, boolean fair) { sync = fair ? new FairSync(permits) : new NonfairSync(permits);}
例子
import java.util.concurrent.Semaphore;/** * Created by cxx on 2017/7/17. */public class SemaphoreTest { public static void main(String[] args) { final Semaphore semaphore = new Semaphore(5); //使用完释放 for (int i = 1 ; i < 11 ; i ++){ final int j = i; new Thread(new Runnable() { @Override public void run() { try { semaphore.acquire(); System.out.println(j); } catch (InterruptedException e) { e.printStackTrace(); }finally { semaphore.release(); } } }).start(); } //不释放,造成阻塞 for (int i = 1 ; i < 11 ; i ++){ final int j = i; new Thread(new Runnable() { @Override public void run() { try { semaphore.acquire(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(j); } }).start(); } }}
结果
2541638710912345
- Java并发编程-锁
- Java并发编程之锁
- JAVA并发编程-LOCK锁
- java并发编程---lock锁
- Java并发编程 并发容器
- JAVA并发编程--并发模式
- JAVA并发-并发编程概述
- java并发编程----并发模型
- 【Java并发编程】并发集合
- java编程思想笔记-并发之并发锁(一)
- java并发编程--锁和并发的小总结
- 【Java并发编程】并发编程大合集
- 【Java并发编程】并发编程大合集
- 【Java并发编程】并发编程大合集
- 【Java并发编程】并发编程大合集
- 【Java并发编程】并发编程大合集
- java并发编程-- 并发编程大合集
- Java并发编程-并发编程知识点总结
- 用python脚本获取CPU的大小端模式
- nyoj305
- NdkDemo开发从环境搭建到入门提高
- Java 利用google.zxing类生成的BitMatrix二维码添加logo图标
- MySQL基础入门—SQL介绍及MySQL的安装
- Java并发编程-锁
- JNI 数据类型与 Java 数据类型的映射关系
- C语言实验——圆周率
- 每日一发Python---python中的enumerate函数
- 网络征信技术接口(架构篇)
- JNI 字符串处理
- 《道德经》第四十六章
- Android常用工具类之Log的特别处理
- PHP 发邮件