与 ReentrantLock 相比,ReentrantReadWriteLock 的性能非常差

Posted

技术标签:

【中文标题】与 ReentrantLock 相比,ReentrantReadWriteLock 的性能非常差【英文标题】:ReentrantReadWriteLock performing very bad compared to ReentrantLock 【发布时间】:2019-10-20 09:03:52 【问题描述】:

我创建了 1000 个线程来递增,1000 个线程来递减,1000 个线程来读取值。

每增加一个线程,值增加25000倍。

每个递减线程,减值25000倍。

每个读取线程,读取值50000次。

所以所有的操作都是读取主导的。

读取值时放置ReadLock

WriteLock 用于递增和递减值的方法。

观察到:ReentrantReadWriteLock 大约需要 13000 毫秒 锁定大约需要 3000 毫秒。 预期:ReentrantReadWriteLock 提供比 ReentrantLock 更快的性能。

顺便说一句:我个人认为使用 getCounter 方法时不需要锁定/同步(只是读取值)

import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Main 
    public static void main(String[] args) throws InterruptedException 

        ArrayList<Thread> reads = new ArrayList<>();
        ArrayList<Thread> increments = new ArrayList<>();
        ArrayList<Thread> decrements = new ArrayList<>();
        Resources resources = new Resources();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000; i++) 
            Thread read = new Read(resources);
            Thread increment = new Increment(resources);
            Thread decrement = new Decrement(resources);
            reads.add(read);
            increments.add(increment);
            decrements.add(decrement);
            read.start();
            increment.start();
            decrement.start();
        
        for (int i = 0; i < 1000; i++) 
            reads.get(i).join();
            increments.get(i).join();
            decrements.get(i).join();
        
        System.out.println(resources.getCounter());
        System.out.println(System.currentTimeMillis() - start);
    

    private static abstract class UserThread extends Thread 
        protected Resources resources;

        public UserThread(Resources resources) 
            this.resources = resources;
        

    

    private static class Read extends UserThread 

        public Read(Resources resources) 
            super(resources);
        

        public void run() 
            for (int i = 0; i < 50000; i++) 
                resources.getCounter();

            

        
    

    private static class Increment extends UserThread 

        public Increment(Resources resources) 
            super(resources);
        

        public void run() 
            for (int i = 0; i < 25000; i++) 
                resources.increment();

            

        
    

    private static class Decrement extends UserThread 

        public Decrement(Resources resources) 
            super(resources);
        

        public void run() 
            for (int i = 0; i < 25000; i++) 
                resources.decrement();

            

        
    

    private static class Resources 

        private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

        private ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
        private ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        private ReentrantLock lock = new ReentrantLock();

        public int getCounter() 
            readLock.lock();
            try 
                return counter;
             finally 
                readLock.unlock();
            

        

        private int counter = 0;

        public void increment() 
            writeLock.lock();
            try 
                counter++;
             finally 
                writeLock.unlock();
            
        

        public void decrement() 
            writeLock.lock();
            try 
                counter--;
             finally 
                writeLock.unlock();
            
        

    


【问题讨论】:

您说代码是“读取主导”,但您的递增和递减线程加起来是 50,000 次操作,这与您的读取线程执行的操作数相同。另外,你的写作线程数是阅读线程数的两倍。 ReadWriteLock 在读多写少的情况下效果最好。 你的线程都是 100% CPU-bound,即没有 I/O。使用这样的线程,创建比虚拟核心(核心 + 超线程)更多的线程实际上会运行更慢,因为你现在强制进行过多的上下文切换。您的机器上有 3000 个虚拟内核吗? 【参考方案1】:

这些类型的锁 - 读写 - 通常经过优化,适用于许多读取器和单个或几个写入器。他们经常在 ops 上旋转,期望读取速度快而写入量很少。此外,它们针对公平性或请求的 FIFO 处理进行了优化,以避免线程停滞。

你做的恰恰相反。您编写了许多写入器,这会导致过度旋转和其他复杂的方法,适用于多读少写场景。

简单的锁很简单。他们只是在准备好时阻塞所有线程,并且不会发生旋转。它们的缺点是当它们唤醒多个线程让它们再次休眠时会引起雪崩效应。

【讨论】:

【参考方案2】:

感谢 Nick 和 Slaw 指出,它不是阅读主导。 我确保我有 100 个增量、100 个减量和 1000 个读取线程。

结果如期而至。 ReentrantReadWriteLock 的输出为 300 ms withLock 为 5000 毫秒。

这是修改后的代码

import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Main 
    public static void main(String[] args) throws InterruptedException 

        ArrayList<Thread> reads = new ArrayList<>();
        ArrayList<Thread> increments = new ArrayList<>();
        ArrayList<Thread> decrements = new ArrayList<>();
        Resources resources = new Resources();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) 
            Thread increment = new Increment(resources);
            Thread decrement = new Decrement(resources);
            increments.add(increment);
            decrements.add(decrement);
            increment.start();
            decrement.start();
        

        for (int i = 0; i < 1000; i++) 
            Thread read = new Read(resources);
            reads.add(read);
            read.start();
        

        for (int i = 0; i < 100; i++) 
            increments.get(i).join();
            decrements.get(i).join();
        

        for (int i = 0; i < 1000; i++) 
            reads.get(i).join();
        
        System.out.println(System.currentTimeMillis() - start);
    

    private static abstract class UserThread extends Thread 
        protected Resources resources;

        public UserThread(Resources resources) 
            this.resources = resources;
        

    

    private static class Read extends UserThread 

        public Read(Resources resources) 
            super(resources);
        

        public void run() 
                resources.getCounter();


        
    

    private static class Increment extends UserThread 

        public Increment(Resources resources) 
            super(resources);
        

        public void run() 
                resources.increment();



        
    

    private static class Decrement extends UserThread 

        public Decrement(Resources resources) 
            super(resources);
        

        public void run() 
                resources.decrement();



        
    

    private static class Resources 

        private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

        private ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
        private ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        private ReentrantLock lock = new ReentrantLock();

        public int getCounter() 
            readLock.lock();
            try 
                try 
                    Thread.sleep(5);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
                return counter;
             finally 
                readLock.unlock();
            

        

        private int counter = 0;

        public void increment() 
            writeLock.lock();
            try 
                counter++;
             finally 
                writeLock.unlock();
            
        

        public void decrement() 
            writeLock.lock();
            try 
                counter--;
             finally 
                writeLock.unlock();
            
        

    





【讨论】:

以上是关于与 ReentrantLock 相比,ReentrantReadWriteLock 的性能非常差的主要内容,如果未能解决你的问题,请参考以下文章

与 ReentrantLock 相比,ReentrantReadWriteLock 的性能非常差

AQS ReentrantLock

ReentrantLock及Condition原理解析

JUC长征篇之ReentrantLock源码解析

深入理解ReentrantLock Condition

Java并发程序设计(12)并发锁之可重入锁ReentrantLock