Java并发编程-锁

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()&nbsp;==&nbsp;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&trade; 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

猜你喜欢

转载自blog.csdn.net/qq250782929/article/details/75270758