java中的锁有15种之多,但是有很多都是重复的,大的方面分为读锁和写锁,那今天我们就聊聊独占锁(写锁),共享锁(读锁)和互斥锁,独占锁呢,指该锁一次只能被一个线程所持有,对synchronized和ReentrantLock而言都是独占锁,那共享锁呢,指该锁可被多个线程所持有,对ReentrantReadWriteLock而言,其读锁是共享锁,其写锁是独占锁,读锁的共享可保证并发读是非常高效的,读写,写读,写写的过程是互斥的,多个线程同时读一个资源类没有任何问题,但是在这个高并发,大数据的互联网时代,为了满足海量用户并发访问,读取共享资源应该同时进行,那就必须保证对资源的读写分离,减少等待时间,但是,如果有一个线程想去写共享资源时,就不应该再有其他线程可以对该资源进行读或写。
说完概念,我们上一段代码,创建10个线程,分别来操作资源类,5个线程对资源类中的map进行设值,然后5个线程从map中取值,在没有加任何锁的情况下看看是什么效果。
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;public class ReentrantReadWriteLockDemo {
public static void main(String[] args) throws Exception {
MyResouce myResouce = new MyResouce();
for (int i = 1; i < 6; i++) {
final int tmp = i;
new Thread(() -> {
try {
myResouce.put(tmp + "", tmp + "");
} catch (Exception e) {
e.printStackTrace();
}
}, "写入线程" + i).start();
}for (int i = 1; i < 6; i++) {
final int tmp = i;
new Thread(() -> {
try {
myResouce.get(String.valueOf(tmp));
} catch (Exception e) {
e.printStackTrace();
}
}, "读取线程" + i).start();
}
}}
class MyResouce {
private volatile Map<String, Object> map = new HashMap<>();
public void put(String key, Object value) {
System.err.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.err.println(Thread.currentThread().getName() + "\t 写入完成");
}public void get(String key) {
System.err.println(Thread.currentThread().getName() + "\t 正在读取");
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object value = map.get(key);
System.err.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
}
}
从执行结果来看,不是我们期待的效果,没有保证数据的原子性,写线程还没有结束,其他线程就进来了,那怎样解决这个问题呢,我们继续往下看。
写入线程1 正在写入:1
写入线程5 正在写入:5
写入线程3 正在写入:3
写入线程2 正在写入:2
读取线程3 正在读取
写入线程4 正在写入:4
读取线程1 正在读取
读取线程4 正在读取
读取线程5 正在读取
读取线程2 正在读取
写入线程5 写入完成
写入线程1 写入完成
写入线程3 写入完成
读取线程2 读取完成:2
读取线程3 读取完成:null
读取线程5 读取完成:null
写入线程4 写入完成
写入线程2 写入完成
读取线程1 读取完成:1
读取线程4 读取完成:null
看到这里我们今天要讲的重点来了,那怎样读写分离呢,JUC包下也同样提供了一个类ReentrantReadWriteLock,从类名来看就是可重入的读写锁,它即包含了读锁,又包含了写锁,写操作时,这个过程必须是一个完整的统一体,中间不允许被分割,被打断,那怎么用呢,我们在上一段代码,主要看下粗体部分。
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReentrantReadWriteLockDemo {
public static void main(String[] args) throws Exception {
MyResouce myResouce = new MyResouce();
for (int i = 1; i < 6; i++) {
final int tmp = i;
new Thread(() -> {
try {
myResouce.put(tmp + "", tmp + "");
} catch (Exception e) {
e.printStackTrace();
}
}, "写入线程" + i).start();
}for (int i = 1; i < 6; i++) {
final int tmp = i;
new Thread(() -> {
try {
myResouce.get(String.valueOf(tmp));
} catch (Exception e) {
e.printStackTrace();
}
}, "读取线程" + i).start();
}
}}
class MyResouce {
private volatile Map<String, Object> map = new HashMap<>();
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void put(String key, Object value) {
rwLock.writeLock().lock();
try {
System.err.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.err.println(Thread.currentThread().getName() + "\t 写入完成");
} finally {
rwLock.writeLock().unlock();
}
}public void get(String key) {
rwLock.readLock().lock();
try {
System.err.println(Thread.currentThread().getName() + "\t 正在读取");
TimeUnit.MILLISECONDS.sleep(300);Object value = map.get(key);
System.err.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
}
从执行结果来看,即保证了原子性,还保证了数据一致性,完整性以及数据的并发性,读写分离,轻松搞定。
写入线程2 正在写入:2
写入线程2 写入完成
写入线程1 正在写入:1
写入线程1 写入完成
读取线程2 正在读取
读取线程2 读取完成:2
写入线程3 正在写入:3
写入线程3 写入完成
写入线程5 正在写入:5
写入线程5 写入完成
读取线程3 正在读取
读取线程3 读取完成:3
写入线程4 正在写入:4
写入线程4 写入完成
读取线程4 正在读取
读取线程1 正在读取
读取线程5 正在读取
读取线程4 读取完成:4
读取线程1 读取完成:1
读取线程5 读取完成:5
最后,题外话,个人认为一个初级程序员和资深程序员的分界点之一就是能否很好使用JUC包下的类开始的,让我们一起加油吧,下篇见。