之前在做一个项目的时候,遇到了这样一个问题:
读操作可以同时进行,写操作不能同时进行,读写操作不能同时进行。
通俗来说,就是当执行读操作的时候,除了不能写入之外,其他都行。但是,当执行写操作的时候,除了当前操作之外,什么都做不了。
当时一怒之下暴力加锁,管你什么操作,全都给我一个一个执行,这样的话,当然是可以,但是也极大的削弱了项目的性能。
后来接触到了读写锁这个东西,粗略看了一下貌似正是应对这种场景的,但是,源码暂时有点不大想研究,于是决定用自己的想法实现一个简易版的读写锁。
在此过程中发现自己实际上对于一些线程方面的知识还不够熟悉,有的概念在运用的时候才察觉到并没有领会透彻,先记录一下这次学习的过程。
import lombok.SneakyThrows; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; public class ReadWriteLock { private static Set<Long> state = new CopyOnWriteArraySet<>(); private static volatile int reading = 0; private static volatile int writing = 0; private static volatile int readWait = 0; private static volatile int writeWait = 0; private static Object READ_LOCK = new Object(); private static Object WRITE_LOCK = new Object(); public static void read() throws InterruptedException { for (;;) { synchronized (READ_LOCK) { if (writing != 0) { if (!state.contains(getThreadId())) { readWait++; state.add(getThreadId()); } READ_LOCK.wait(); } else { reading++; if (state.contains(getThreadId())) { readWait--; state.remove(getThreadId()); } break; } } } } public static void readComplete() { synchronized (READ_LOCK) { reading--; } if (writeWait != 0) { synchronized (WRITE_LOCK) { WRITE_LOCK.notify(); } } } public static void write() throws InterruptedException { for (;;) { synchronized (WRITE_LOCK) { if (writing != 0 || reading != 0) { if (!state.contains(getThreadId())) { writeWait++; state.add(getThreadId()); } WRITE_LOCK.wait(); } else { writing++; if (state.contains(getThreadId())) { writeWait--; state.remove(getThreadId()); } break; } } } } public static void writeComplete() { synchronized (WRITE_LOCK) { writing--; } if (writeWait >= readWait) { synchronized (WRITE_LOCK) { WRITE_LOCK.notify(); } } else { synchronized (READ_LOCK) { READ_LOCK.notifyAll(); } } } public static Long getThreadId() { return Thread.currentThread().getId(); } public static void main(String[] args) throws InterruptedException { for (int j = 0; j < 500; j++) { final int[] read = {0}; final int[] write = {0}; Object readLock = new Object(); Object writeLock = new Object(); for (int i = 0; i < 5000; i++) { new Thread(new Runnable() { @SneakyThrows @Override public void run() { ReadWriteLock.read(); //System.out.println("read:" + Thread.currentThread().getName()); synchronized (readLock) { read[0]++; } ReadWriteLock.readComplete(); } }, String.valueOf(i)).start(); } for (int i = 0; i < 5000; i++) { new Thread(new Runnable() { @SneakyThrows @Override public void run() { ReadWriteLock.write(); //System.out.println("write:" + Thread.currentThread().getName()); synchronized (writeLock) { write[0]++; } ReadWriteLock.writeComplete(); } }, String.valueOf(i)).start(); } Thread.sleep(2000); if (read[0] == write[0]) { read[0] = 0; write[0] = 0; } else { throw new RuntimeException("Thread not safe"); } } } }
主方法中进行了500次测试,最终没有抛出异常,那么我暂时认为这个简易版的读写锁是可用的。
state容器用于存储线程id,线程只有在第一次进入等待池前,才会将id存入state,后续用于判断该线程是新创建的线程还是处于等待状态的线程,从而决定是否将等待数量减一。
写的时候感觉有点小饶,可能是因为没有在写之前把所有可能发生的线程安全等问题罗列出来,导致在写的过程中发现问题,修改代码,写到这里忽然想到另一个需要注意的问题,如果代码中可能报错,那么最好将释放锁的语句写在finally里,否则加锁后得不到释放,可能会死锁。