多线程下锁的应用


给女朋友上锁

有一天梦见女朋友跟一个陌生男人逛街,我很是着急,于是有很多志同道合的朋友开始为我出谋划策。有说,让那个男的指向null,让垃圾回收他。 也有的说给个死循环,让他们逛到累,累死他们。。。没错,你们说的都有道理,但是,如果换是我,我会给自己女朋友逛街这个行为上锁,并且只有我才能获取到锁,也不会把锁让给别人。好了,扯完,开始进入正题,没错,就是锁。

相信大家在开发过程中都会遇到这样一种场景:浏览器端发起一个请求,服务器接收到请求后要去数据库中把数据加载回来,对数据做处理,是不是很简单?但是当请求过于频繁或请求量比较大,并且数据库表数据量又大的情况,这时候我们就不得不去关心它的响应时间、性能怎么样等等这些问题了,因为它可能会影响到整个业务流程,甚至整个系统。为解决这个问题,相信很多人会想到用多线程来解决。即在程序中开多个线程并发去处理请求,也就是并发编程,从用户角度看还是一个串行过程,实际上是并发在处理,很显然这样做可以提升响应效率。但这又会引起另外一个问题,那就是线程安全,并发编程有三要素:原子性、有序性、可见性。在多线程编程中要遵守好这三大要素,如果程序没有处理好,可能会造成一些意想不到的后果……

针对这些问题,jdk提供了一些线程安全的接口和类,例如我们熟悉的Vector、HashTable。java还提供了synchronized关键字和修饰符valitate来保证线程安全,这两种都是利用锁来实现的。
  • valitate
    被valitate修饰的变量,当一个线程改变了它的值时,在内存中相对其他线程来说是可见的。还可以防止重排,即程序按代码的顺序来执行,防止顺序被打乱。但是它并不能保证原子性。

  • synchronized关键字
    synchronized是jdk定义实现的锁,确保程序能够对同步块或方法互斥访问,即当一个线程获取到锁后,别的线程只能等待,可以保证原子性。
    本文主要是对synchronized关键字来展开说明,以下为synchronized的一个例子:

案例一
大量请求同时读取数据库表load回数据写到一个文件(比如excel)中,这个表是分布在不同的库中,并且分表的。若单线程去处理这样的请求,势必会耗时比较久,甚至因为一些慢查询导致连接耗尽,造成严重后果。

思路:
使用ExecutorService接口来实现多线程,ExecutorService是在包java.util.concurrent下Java中对线程池定义的一个接口,实现异步执行的机制,让任务在后台执行。

ExcelUtils.java

// 使用poi的SXSSFWorkbook支持导出大数据
 private SXSSFWorkbook sxworkBook; 
 private OutputStream out;

 public ExcelBuilder(String path, String fileName) {
   sxworkBook = new SXSSFWorkbook(1000);
   try {
      File file = new File(path);
      if (!file.exists()) {
          file.mkdirs();
      }
      File savePath = new File(path + "/" + fileName);
      this.out = new FileOutputStream(savePath);
   } catch (FileNotFoundException e) {
  }
 }
 
public <T> void writeFile(String sheetname, List<T> dataList, Class<T> clazz) {
   …… 此处写文件,具体参考POI写文件API
}

public void create(){
 try {
        sxworkBook.write(out);
        out.flush();
    } catch (IOException e) {
         logger.error(e.getMessage(), e);
    } finally {
         // IOUtils.closeQuietly(out);
    }
}

ThreadTask.java

String dir = "C:/director";
String fileName = "xxxxx.xlsx";
ExcelUtils instance = new ExcelUtils(dir, fileName);
ExecutorService executor = Executors.newCachedThreadPool();

// 开两个线程处理
executor.execute(new InnerThread(queryParam1, instance));
executor.execute(new InnerThread(queryParam2, instance));

executor.shutdown();
while(!executor.awaitTermination(1, TimeUnit.SECONDS));

instance.create();




// 定义一个内部线程类 
class InnerThread implements Runnable{
  private Object queryParam;
  private ExcelUtils instance;
  Object obj = new Object();

  InnerThread(Object queryParam, ExcelUtils instance){
    this.queryParam = queryParam;
    this.instance = instance;
  }
  
  @Override
  public void run() {
     List<XXX> lists = service.query(queryParam); // service是查询接口类
     synchronized(obj){
        write(lists);
     }
  }
  
  public void write(List<Vo> lists){
     logger.info("当前线程:" + Thread.currentThread().getName());
     instance.writeFile(System.currentTimeMillis() + "", lists, XXX.class);
  }
}

到这里就可以实现多线程读DB加载数据,多线程写文件了,但是,事实并不是那样,报异常了!!!!!!


很明显,write方法使用关键字synchronized加锁无效, 多个线程同时进入同时写文件,从而导致出现异常。那么为什么加了synchronized还出现并发写呢? 先看看synchronized的定义,如下:(本段摘自百度百科)
Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
原来synchronized修饰的代码块,必须要实例相同才能锁住代码块,也就是只有一个线程可以执行该块内容。
于是做了以下修改:

class InnerThread implements Runnable{
  private Object queryParam;
  private ExcelUtils instance;
  private Lock lock;

  InnerThread(Object queryParam, ExcelUtils instance, Lock lock){
    this.queryParam = queryParam;
    this.instance = instance;
    this.lock = lock; // 传入同一个实例
  }
  
  @Override
  public void run() {
     List<XXX> lists = service.query(queryParam); // service是查询接口类
     synchronized(lock){
        write(lists);
     }
  }
  
  public void write(List<Vo> lists){
     logger.info("当前线程:" + Thread.currentThread().getName());
     instance.writeFile(System.currentTimeMillis() + "", lists, XXX.class);
  }
}
Lock lock = new ReentrantLock();
// 开两个线程处理
executor.execute(new InnerThread(queryParam1, instance, lock));
executor.execute(new InnerThread(queryParam2, instance, lock));

再次执行,生成文件成功。


注意到一点,这里只对写文件部分加了锁,对于读DB加载数据返回并没有加锁,load数据依然是多线程并行去请求DB,在响应效率上得到了较高提升。

在上面的单机场景中,我们可以运用ava中提供的很多并发处理相关的API,但是这些API在分布式场景中就无能为力了,由于分布式系统的分布性,即多线程和多进程并且分布在不同机器中,synchronized这种锁将失去原有锁的效果,这时候就需要我们自己实现分布式锁。

案例二
redis基于缓存,实现分布式锁,利用redis的锁机制来实现分布式锁。

思路:
利用redis接口API对对象就行上锁,并且设置过期时间,实现并发编程。

RedisClient.java

public class RedisClient{
    private Logger log = LoggerFactory.getLogger(RedisClient.class);
    private JedisPool jedisPool;
    private Jedis jedis;
    String lock;
    long expires = 5000;
    
    public RedisClient(String lock) {
       this.lock = lock;
       this.init();
    }
    private void init() {
        // 池基本配置
        JedisPoolConfig config = new JedisPoolConfig();
        // config.setMaxActive(20);
        config.setMaxIdle(5);
        // config.setMaxWait(1000l);
        config.setTestOnBorrow(false);

        jedisPool = new JedisPool(config, "XX.XXX.XXX.XX", 6400);
        jedis = jedisPool.getResource();
    }
    
    public boolean getLock() {
        while (true) {
            boolean lock = setlock();
            if (lock) {
                return lock;
            }
        }
    }
    
    public boolean setlock() {
        long currentTime = System.currentTimeMillis();
        String expire = String.valueOf(currentTime + expires);
        if (jedis.setnx(lock, expire) > 0) {
            log.info("当前线程:" + Thread.currentThread().getName() + "获取到锁");
            jedis.expire(lock, 5);
            return true;
        } else {
            String oldTime = jedis.get(lock);
            if (oldTime != null && (currentTime - Long.parseLong(oldTime)) > 0) {
                String oldValue = jedis.getSet(lock, expire);
                if (oldValue != null && oldValue.equals(oldTime)) {
                    jedis.expire(lock, 5);
                    log.info("过期了,让其它线程获取锁,当前线程:" + Thread.currentThread().getName() + "获取到锁");
                    return true;
                }
            }
        }

        return false;
    }
}

ThreadTest.java

public class ThreadTest extends Thread {
    private Logger log = LoggerFactory.getLogger(Main.class);
    String lock;

    ThreadTest(String lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        RedisClient client = new RedisClient(lock);
        boolean hasLock = client.getLock();
        if (hasLock) {
            log.info(Thread.currentThread().getName() + "开始执行……");
        }
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        String lock = "key";
        new ThreadTest(lock).start();
        new ThreadTest(lock).start();
        new ThreadTest(lock).start();
    }

}

运行效果:



总结:
随着日益增长的业务量,数据越来越大,处理请求响应速度也要随着提升,并发编程就必不可少,在单机多线程下,我们可以使用jdk提供的并发处理相关的API来解决我们的问题,例如上面提到的valitate,synchronized,还有CAS,也可以在业务上实现锁的机制,比如说在数据库变层面来处理。 但在也正是因为业务量越来越大,需求更复杂的前提下,系统分布式部署就越来越重要,负载均衡,多节点,多机器部署势必带来更多的问题,因此分布式锁就广泛被使用,多种实现也随之出现,如前面提到的基于缓存redis,还有基于Zookeeper实现分布式锁等等。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容