Java中synchronized和Lock的区别

前言

最近有一个需求是需要将数据库的一些数据抽取出来放到文件文件命名方式为“FILENAME_yyyyMMddHHmmss”,例如FILENAME_20200625120011。计划使用多线程去实现,这样可能生成的文件名会有重复导致内容被覆盖,因此考虑加锁实现生成文件方式。这时候考虑到是使用synchronized还是Lock?

synchronized

synchronized是Java提供的一个并发控制的关键字。主要有两种用法,分别是同步方法同步代码块。也就是说,synchronized既可以修饰方法也可以修饰代码块。

代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待获取锁的线程释放锁,这里获取锁的线程释放锁只会有两种情况:

  • 获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
  • 线程执行发生异常,此时JVM会让线程自动释放锁。

那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。

使用synchronized实现创建文件的synchronizedDemoThread线程代码如下:

public class synchronizedDemoThread implements Runnable {
    public static ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<String, String>();
    @Override
    public void run() {
        getThreadLog(getFileName());
    }
    public synchronized String getFileName() {
        try {
            String path = "FILENAME_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
            if (!concurrentHashMap.containsKey(path)) {
                synchronized (synchronizedDemoThread.class) {
                    if (!concurrentHashMap.containsKey(path)) {
                        getThreadLog("不存在此路径,正在创建此路径");
                        concurrentHashMap.put(path, path);
                        return path;
                    } else {
                        getThreadLog("此路径已经存在了,需要等待创建");
                        return getFileName();
                    }
                }
            } else {
                getThreadLog("此文件路径已经存在了,请等待创建。。。");
                this.wait(2000);
                return getFileName();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }
}

测试类代码:

public class LockTest {
    public static void main(String[] args) throws InstantiationException, IllegalAccessException {
        synchronizedDemoThreadTest();
    }
    //创建线程池对象模拟多线程调用
    public static void synchronizedDemoThreadTest(){
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            executor.execute(new synchronizedDemoThread());
        }
        executor.shutdown();
    }
    //获取线程名和时间
    public static void getThreadLog(String logContent) {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("[");
        stringBuffer.append(Thread.currentThread().getName());
        stringBuffer.append(" ");
        stringBuffer.append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()));
        stringBuffer.append("]");
        stringBuffer.append(logContent);
        System.out.println(stringBuffer.toString());
    }
}

测试日志:

[pool-1-thread-4 2020-06-25 12:08:47.004]不存在此路径,正在创建此路径
[pool-1-thread-4 2020-06-25 12:08:47.005]FILENAME_20200625120847
[pool-1-thread-1 2020-06-25 12:08:47.007]此文件路径已经存在了,请等待创建。。。
[pool-1-thread-2 2020-06-25 12:08:47.009]此文件路径已经存在了,请等待创建。。。
[pool-1-thread-3 2020-06-25 12:08:47.008]此文件路径已经存在了,请等待创建。。。
[pool-1-thread-5 2020-06-25 12:08:47.010]此文件路径已经存在了,请等待创建。。。
[pool-1-thread-1 2020-06-25 12:08:49.034]不存在此路径,正在创建此路径
[pool-1-thread-1 2020-06-25 12:08:49.037]FILENAME_20200625120849
[pool-1-thread-3 2020-06-25 12:08:49.038]此文件路径已经存在了,请等待创建。。。
[pool-1-thread-2 2020-06-25 12:08:49.038]此文件路径已经存在了,请等待创建。。。
[pool-1-thread-5 2020-06-25 12:08:49.039]此文件路径已经存在了,请等待创建。。。
[pool-1-thread-3 2020-06-25 12:08:51.057]不存在此路径,正在创建此路径
[pool-1-thread-3 2020-06-25 12:08:51.060]FILENAME_20200625120851
[pool-1-thread-2 2020-06-25 12:08:51.066]此文件路径已经存在了,请等待创建。。。
[pool-1-thread-5 2020-06-25 12:08:51.066]此文件路径已经存在了,请等待创建。。。
[pool-1-thread-2 2020-06-25 12:08:53.066]不存在此路径,正在创建此路径
[pool-1-thread-2 2020-06-25 12:08:53.066]FILENAME_20200625120853
[pool-1-thread-5 2020-06-25 12:08:53.067]此文件路径已经存在了,请等待创建。。。
[pool-1-thread-5 2020-06-25 12:08:55.069]不存在此路径,正在创建此路径
[pool-1-thread-5 2020-06-25 12:08:55.069]FILENAME_20200625120855

Lock

由于Lock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,Lock类提供了一些高级功能,主要有以下3项:

  • 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。
  • 公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好
  • 锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

使用Lock实现创建文件的LockDemoThread线程代码如下:

public class LockDemoThread implements Runnable {
    //定义一个concurrentHashMap用来存放文件名
    public static ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<String, String>();
    //定义一个Lock对象
    public static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        getThreadLog(getFileName());
    }

    //获取文件名
    public synchronized String getFileName() {
        try {
            lock.lock();
            String path = "FILENAME_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
            if (!concurrentHashMap.containsKey(path)) {
                getThreadLog("不存在此路径,重新创建");
                concurrentHashMap.put(path, path);
                return path;
            } else {
                getThreadLog("此文件路径已经存在了,请等待创建。。。");
                this.wait(2000);
                return getFileName();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return "";
    }
}

测试类代码:

public class LockTest {
    public static void main(String[] args) throws InstantiationException, IllegalAccessException {
        LockDemoThreadTest();
        //synchronizedDemoThreadTest();
    }
    //创建线程池对象模拟多线程调用
    public static void LockDemoThreadTest(){
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            executor.execute(new LockDemoThread());
        }
        executor.shutdown();
    }
    //获取线程名和时间
    public static void getThreadLog(String logContent) {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("[");
        stringBuffer.append(Thread.currentThread().getName());
        stringBuffer.append(" ");
        stringBuffer.append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()));
        stringBuffer.append("]");
        stringBuffer.append(logContent);
        System.out.println(stringBuffer.toString());
    }
}

打印日志:

[pool-1-thread-1 2020-06-25 13:43:37.009]不存在此路径,重新创建
[pool-1-thread-2 2020-06-25 13:43:37.013]此文件路径已经存在了,请等待创建。。。
[pool-1-thread-1 2020-06-25 13:43:37.011]FILENAME_20200625134337
[pool-1-thread-2 2020-06-25 13:43:39.054]不存在此路径,重新创建
[pool-1-thread-3 2020-06-25 13:43:39.055]此文件路径已经存在了,请等待创建。。。
[pool-1-thread-2 2020-06-25 13:43:39.056]FILENAME_20200625134339
[pool-1-thread-3 2020-06-25 13:43:41.056]不存在此路径,重新创建
[pool-1-thread-3 2020-06-25 13:43:41.057]FILENAME_20200625134341
[pool-1-thread-4 2020-06-25 13:43:41.057]此文件路径已经存在了,请等待创建。。。
[pool-1-thread-4 2020-06-25 13:43:43.058]不存在此路径,重新创建
[pool-1-thread-5 2020-06-25 13:43:43.060]此文件路径已经存在了,请等待创建。。。
[pool-1-thread-4 2020-06-25 13:43:43.060]FILENAME_20200625134343
[pool-1-thread-5 2020-06-25 13:43:45.062]不存在此路径,重新创建
[pool-1-thread-5 2020-06-25 13:43:45.062]FILENAME_20200625134345

总结一下

我们知道了synchronized和Lock都能达到锁的目的,那有哪些不同的地方呢?

  1. 如果获取线程需要等待IO或者其他(比如调用sleep方法)被阻塞了,但是没有释放锁,如果使用synchronized则其他线程一直无限期等待下去。这种场景适合使用Lock作为锁。
  2. 当有多个线程读写文件时,读操作和写操作会发生冲突现象,写写操作会发生冲突现象,但是读读操作不会发生冲突现象。但是采用synchronized关键字来实现同步的话,如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。这种场景适合使用Lock作为锁。
  3. 通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
  4. Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问。
  5. Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

猜你喜欢

转载自blog.csdn.net/u011047968/article/details/106956712