zoukankan      html  css  js  c++  java
  • Java多线程13:读写锁和两种同步方式的对比

    读写锁ReentrantReadWriteLock概述

    大型网站中很重要的一块内容就是数据的读写,ReentrantLock虽然具有完全互斥排他的效果(即同一时间只有一个线程正在执行lock后面的任务),但是效率非常低。所以在JDK中提供了一种读写锁ReentrantReadWriteLock,使用它可以加快运行效率。

    读写锁表示两个锁,一个是读操作相关的锁,称为共享锁;另一个是写操作相关的锁,称为排他锁。我把这两个操作理解为三句话:

    1、读和读之间不互斥,因为读操作不会有线程安全问题

    2、写和写之间互斥,避免一个写操作影响另外一个写操作,引发线程安全问题

    3、读和写之间互斥,避免读操作的时候写操作修改了内容,引发线程安全问题

    总结起来就是,多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作

    读和读共享

    先证明一下第一句话"读和读之间不互斥",举一个简单的例子:

    public class ThreadDomain48 extends ReentrantReadWriteLock
    {        
        public void read()
        {
            try
            {
                readLock().lock();
                System.out.println(Thread.currentThread().getName() + "获得了读锁, 时间为" + 
                        System.currentTimeMillis());
                Thread.sleep(10000);
            } 
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            finally
            {
                readLock().unlock();
            }
        }
    }
    public static void main(String[] args)
    {
        final ThreadDomain48 td = new ThreadDomain48();
        Runnable readRunnable = new Runnable()
        {
            public void run()
            {
                td.read();
            }
        };
        Thread t0 = new Thread(readRunnable);
        Thread t1 = new Thread(readRunnable);
        t0.start();
        t1.start();
    }

    看一下运行结果:

    Thread-0获得了读锁, 时间为1444019668424
    Thread-1获得了读锁, 时间为1444019668424

    尽管方法加了锁,还休眠了10秒,但是两个线程还是几乎同时执行lock()方法后面的代码,看时间就知道了。说明lock.readLock()读锁可以提高程序运行效率,允许多个线程同时执行lock()方法后面的代码

    写和写互斥

    再证明一下第二句话"写和写之间互斥",类似的证明方法:

    public class ThreadDomain48 extends ReentrantReadWriteLock
    {        
        public void write()
        {
            try
            {
                writeLock().lock();
                System.out.println(Thread.currentThread().getName() + "获得了写锁, 时间为" + 
                        System.currentTimeMillis());
                Thread.sleep(10000);
            } 
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            finally
            {
                writeLock().unlock();
            }
        }
    }
    public static void main(String[] args)
    {
        final ThreadDomain48 td = new ThreadDomain48();
        Runnable readRunnable = new Runnable()
        {
            public void run()
            {
                td.write();
            }
        };
        Thread t0 = new Thread(readRunnable);
        Thread t1 = new Thread(readRunnable);
        t0.start();
        t1.start();
    }

    看一下运行结果:

    Thread-0获得了写锁, 时间为1444021393325
    Thread-1获得了写锁, 时间为1444021403325

    从时间上就可以看出来,10000ms即10s,和代码里一致,证明了读和读之间是互斥的

    读和写互斥

    最后证明一下第三句话"读和写之间互斥",证明方法无非是把上面二者结合起来而已,看一下:

    public class ThreadDomain48 extends ReentrantReadWriteLock
    {        
        public void write()
        {
            try
            {
                writeLock().lock();
                System.out.println(Thread.currentThread().getName() + "获得了写锁, 时间为" + 
                        System.currentTimeMillis());
                Thread.sleep(10000);
            } 
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            finally
            {
                writeLock().unlock();
            }
        }
        
        public void read()
        {
            try
            {
                readLock().lock();
                System.out.println(Thread.currentThread().getName() + "获得了读锁, 时间为" + 
                        System.currentTimeMillis());
                Thread.sleep(10000);
            } 
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            finally
            {
                readLock().unlock();
            }
        }
    }
    public static void main(String[] args)
        {
            final ThreadDomain48 td = new ThreadDomain48();
            Runnable readRunnable = new Runnable()
            {
                public void run()
                {
                    td.read();
                }
            };
            Runnable writeRunnable = new Runnable()
            {
                public void run()
                {
                    td.write();
                }
            };
            Thread t0 = new Thread(readRunnable);
            Thread t1 = new Thread(writeRunnable);
            t0.start();
            t1.start();
        }

    看一下运行结果:

    Thread-0获得了读锁, 时间为1444021679203
    Thread-1获得了写锁, 时间为1444021689204

    从时间上看,也是10000ms即10s,和代码里面是一致的,证明了读和写之间是互斥的。注意一下,"读和写互斥"和"写和读互斥"是两种不同的场景,但是证明方式和结论是一致的,所以就不证明了。

    synchronized和ReentrantLock的对比

    到现在,看到多线程中,锁定的方式有2种:synchronized和ReentrantLock。两种锁定方式各有优劣,下面简单对比一下:

    1、synchronized是关键字,就和if...else...一样,是语法层面的实现,因此synchronized获取锁以及释放锁都是Java虚拟机帮助用户完成的;ReentrantLock是类层面的实现,因此锁的获取以及锁的释放都需要用户自己去操作。特别再次提醒,ReentrantLock在lock()完了,一定要手动unlock()

    2、synchronized简单,简单意味着不灵活,而ReentrantLock的锁机制给用户的使用提供了极大的灵活性。这点在Hashtable和ConcurrentHashMap中体现得淋漓尽致。synchronized一锁就锁整个Hash表,而ConcurrentHashMap则利用ReentrantLock实现了锁分离,锁的只是segment而不是整个Hash表

    3、synchronized是不公平锁,而ReentrantLock可以指定锁是公平的还是非公平的

    4、synchronized实现等待/通知机制通知的线程是随机的,ReentrantLock实现等待/通知机制可以有选择性地通知

    5、和synchronized相比,ReentrantLock提供给用户多种方法用于锁信息的获取,比如可以知道lock是否被当前线程获取、lock被同一个线程调用了几次、lock是否被任意线程获取等等

    总结起来,我认为如果只需要锁定简单的方法、简单的代码块,那么考虑使用synchronized,复杂的多线程处理场景下可以考虑使用ReentrantLock。当然这只是建议性地,还是要具体场景具体分析的。

    最后,查看了很多资料,JDK1.5版本只有由于对synchronized做了诸多优化,效率上synchronized和ReentrantLock应该是差不多。

  • 相关阅读:
    RecyclerView 数据刷新的几种方式 局部刷新 notify MD
    【图片】批量获取几万张图片
    RV BaseRecyclerViewAdapterHelper 总结 MD
    RecyclerView.ItemDecoration 间隔线
    Kotlin【简介】Android开发 配置 扩展
    Kotlin 特性 语法糖 优势 扩展 高阶 MD
    一个十分简洁实用的MD风格的UI主框架
    折叠伸缩工具栏 CollapsingToolbarLayout
    FloatingActionButton FAB 悬浮按钮
    Glide Picasso Fresco UIL 图片框架 缓存 MD
  • 原文地址:https://www.cnblogs.com/xrq730/p/4855631.html
Copyright © 2011-2022 走看看