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()&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的计数器减到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