同步锁 vs 读写锁的性能表现

6

我试图证明,在有许多读者和仅有一些写者时,同步会变慢。但不知何故,我证明了相反的结论。

在 RW 示例中,执行时间为 313 毫秒:

package zad3readWriteLockPerformance;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Main {
    public static long start, end;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            end = System.currentTimeMillis();
            System.out.println("Time of execution " + (end - start) + " ms");
        }));
        start = System.currentTimeMillis();
        final int NUMBER_OF_THREADS = 1000;
        ThreadSafeArrayList<Integer> threadSafeArrayList = new ThreadSafeArrayList<>();
        ArrayList<Thread> consumerThreadList = new ArrayList<Thread>();
        for (int i = 0; i < NUMBER_OF_THREADS; i++) {
            Thread t = new Thread(new Consumer(threadSafeArrayList));
            consumerThreadList.add(t);
            t.start();
        }

        ArrayList<Thread> producerThreadList = new ArrayList<Thread>();
        for (int i = 0; i < NUMBER_OF_THREADS/10; i++) {
            Thread t = new Thread(new Producer(threadSafeArrayList));
            producerThreadList.add(t);
            t.start();

        }



        //  System.out.println("Printing the First Element : " + threadSafeArrayList.get(1));

    }

}
class Consumer implements Runnable {
    public final static int NUMBER_OF_OPERATIONS = 100;
    ThreadSafeArrayList<Integer> threadSafeArrayList;

    public Consumer(ThreadSafeArrayList<Integer> threadSafeArrayList) {
        this.threadSafeArrayList = threadSafeArrayList;
    }

    @Override
    public void run() {
        for (int j = 0; j < NUMBER_OF_OPERATIONS; j++) {
            Integer obtainedElement = threadSafeArrayList.getRandomElement();
        }
    }

}
class Producer implements Runnable {
    public final static int NUMBER_OF_OPERATIONS = 100;
    ThreadSafeArrayList<Integer> threadSafeArrayList;

    public Producer(ThreadSafeArrayList<Integer> threadSafeArrayList) {
        this.threadSafeArrayList = threadSafeArrayList;
    }

    @Override
    public void run() {
        for (int j = 0; j < NUMBER_OF_OPERATIONS; j++) {
            threadSafeArrayList.add((int) (Math.random() * 1000));
        }
    }

}

class ThreadSafeArrayList<E> {
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    private final Lock readLock = readWriteLock.readLock();

    private final Lock writeLock = readWriteLock.writeLock();

    private final List<E> list = new ArrayList<>();

    public void add(E o) {
        writeLock.lock();
        try {
            list.add(o);
            //System.out.println("Adding element by thread" + Thread.currentThread().getName());
        } finally {
            writeLock.unlock();
        }
    }

    public E getRandomElement() {
        readLock.lock();
        try {
            //System.out.println("Printing elements by thread" + Thread.currentThread().getName());
            if (size() == 0) {
                return null;
            }
            return list.get((int) (Math.random() * size()));
        } finally {
            readLock.unlock();
        }
    }

    public int size() {
        return list.size();
    }

}

同步示例,执行时间仅为241毫秒:

package zad3readWriteLockPerformanceZMIENONENENASYNCHRO;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Main {
    public static long start, end;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            end = System.currentTimeMillis();
            System.out.println("Time of execution " + (end - start) + " ms");
        }));
        start = System.currentTimeMillis();
        final int NUMBER_OF_THREADS = 1000;
        List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
        ArrayList<Thread> consumerThreadList = new ArrayList<Thread>();
        for (int i = 0; i < NUMBER_OF_THREADS; i++) {
            Thread t = new Thread(new Consumer(list));
            consumerThreadList.add(t);
            t.start();
        }

        ArrayList<Thread> producerThreadList = new ArrayList<Thread>();
        for (int i = 0; i < NUMBER_OF_THREADS / 10; i++) {
            Thread t = new Thread(new Producer(list));
            producerThreadList.add(t);
            t.start();
        }

        //  System.out.println("Printing the First Element : " + threadSafeArrayList.get(1));

    }

}

class Consumer implements Runnable {
    public final static int NUMBER_OF_OPERATIONS = 100;
    List<Integer> list;

    public Consumer(List<Integer> list) {
        this.list = list;
    }

    @Override
    public void run() {
        for (int j = 0; j < NUMBER_OF_OPERATIONS; j++) {
            if (list.size() > 0)
                list.get((int) (Math.random() * list.size()));
        }
    }

}

class Producer implements Runnable {
    public final static int NUMBER_OF_OPERATIONS = 100;
    List<Integer> threadSafeArrayList;

    public Producer(List<Integer> threadSafeArrayList) {
        this.threadSafeArrayList = threadSafeArrayList;
    }

    @Override
    public void run() {
        for (int j = 0; j < NUMBER_OF_OPERATIONS; j++) {
            threadSafeArrayList.add((int) (Math.random() * 1000));
        }
    }

}

当读者数量比写者多十倍时,为什么同步集合会更快。如何展示我在许多文章中阅读到的读写锁的优势?


4
必读:https://dev59.com/hHRB5IYBdhLWcg3wz6UK - assylias
1
值得注意的是,即使对于同步列表,list.get((int) (Math.random() * list.size()))通常也不是线程安全的,因为另一个线程可能会在调用sizeget之间调用remove,如果第一个线程尝试从以前的最后一个索引处获取,则会导致IndexOutOfBoundsException。但是,如果从未调用remove,那么这可能不是您使用情况的问题。 - MikeFHay
1个回答

14
获取读写锁的实际成本通常比获取简单互斥锁的成本要慢得多。 ReadWriteLock的javadoc 对此进行了说明:
无论读写锁是否能提高性能,取决于数据读取与修改的频率、读写操作的持续时间以及争用数据的数量 - 即尝试同时读取或写入数据的线程数。例如,初始填充数据并很少修改,但经常搜索(如某种目录)的集合是使用读写锁的理想选择。然而,如果更新变得频繁,则数据大部分时间都被排他性锁定,因此并发性几乎没有增加。此外,如果读取操作太短,读写锁实现的开销(本质上比互斥锁更复杂)可能会主导执行成本,特别是因为许多读写锁实现仍会将所有线程序列化通过一小段代码。最终,只有通过分析和测量才能确定使用读写锁是否适合您的应用程序。
因此,您的线程执行非常简单的操作可能意味着性能由实际获取锁所花费的时间所主导。
另外还有一个问题,在你的基准测试中,Math.random是同步的。根据javadoc:
该方法是正确同步的,以允许多个线程正确使用。但是,如果许多线程需要以大量的速度生成伪随机数,则每个线程都拥有自己的伪随机数生成器可能会减少竞争。

即使在获取ReadWriteLock后,您的并发读取程序不会相互阻塞,但它们仍可能争夺在Math.random中获取的锁,从而破坏使用ReadWriteLock的某些优势。您可以通过改用ThreadLocalRandom来改善这种情况。

此外,正如assylias所指出的那样,不考虑JIT编译和其他运行时特 quirks 的Java基准测试是不可靠的。对于这类基准测试,应使用Java微基准测试工具(JMH)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接