多线程写文件以及分布式锁实现

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

在开发过程中往往会遇到大量数据请求,并且需要快速响应的问题,为解决这个问题,相信很多人会想到用多线程来实现。即在程序中开多个线程并发去处理请求,从用户角度看是还是一个串行过程,实际上是并发在处理,很好的提高了响应速度。

案例一
有大量数据需要同时请求数据库表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来解决我们的问题,也可以在业务上实现锁的机制,比如说在数据库变层面来处理。 但在也正是因为业务量越来越大,需求更复杂的前提下,系统分布式部署就越来越重要,负载均衡,多节点,多机器部署势必带来更多的问题,因此分布式锁就广泛被使用,多种实现也随之出现,如前面提到的基于缓存redis,还有基于Zookeeper实现分布式锁等等。

猜你喜欢

转载自blog.csdn.net/huangdi1309/article/details/79047085