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的计数器减到0
countDown.await();
//等待至countdown的计数器减到0 或 超时 (可在某些不希望cpu频繁切换的场景下替换Thread.sleep()使用)
countDown.await(1,TimeUnit.MILLISECONDS);
//计数器-1
countDown.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();
}
}
}
结果
2
5
4
1
6
3
8
7
10
9
1
2
3
4
5