多个读锁不互斥, 读锁与写锁互斥, 写锁与写锁互斥, 这是由JVM自行控制的,我们只要上好相应的锁即可。
Java中map实现缓存,主要有三种 HashTable, synchronizedMap, ConcurrentHashMap
package test;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 设计一个缓存系统
* 读写锁的应用。
* JDK1.5自带的读写锁特性,读与读不互斥,读与写互斥,写与写互斥。
* 为什么要使用读写锁?一句话概括那就是提高系统性能,如何提高呢?
* 试想,对于所有对读的操作是不需要线程互斥的,而如果方法内
* 使用了synchronized关键字同步以达到线程安全,对于所有的线程不管是读还是写的操作都要同步。
* 这时如果有大量的读操作时就会又性能瓶颈。
*
* 所以,当一个方法内有多个线程访问,并且方法内有读和写读操作时,
* 提升性能最好的线程安全办法时采用读写锁的机制对读写互斥、写写互斥。这样对于读读就没有性能问题了
* @author zhurudong
*
*/
public class CacheTest {
// 缓存的map
private Map map = new HashMap();
// 读写锁对象
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/**
* 从缓存中获取数据的方法
* @param key
* @return
*/
public Object getData(String key) {
readWriteLock.readLock().lock();//读锁,只对写的线程互斥
Object value = null;
try {
// 尝试从缓存中获取数据
value = map.get(key);
if (value == null) {
readWriteLock.readLock().unlock();//发现目标值为null,释放掉读锁
readWriteLock.writeLock().lock();//发现目标值为null,需要取值操作,上写锁
try {
value = map.get(key);// 很严谨这一步。再次取目标值
if (value == null) {//很严谨这一步。再次判断目标值,防止写锁释放后,后面获得写锁的线程再次进行取值操作
// 模拟DB操作
value = new Random().nextInt(10000) + "test";
map.put(key, value);
System.out.println("db completed!");
}
readWriteLock.readLock().lock();//再次对读进行锁住,以防止写的操作,造成数据错乱
} finally {
/*
* 先加读锁再释放写锁读作用:
* 防止在43行出多个线程获得写锁进行写的操作,所以在写锁还没有释放前要上读锁
*/
readWriteLock.writeLock().unlock();
}
}
} finally {
readWriteLock.readLock().unlock();
}
return value;
}
/**
* test main
* @param args
*/
public static void main(String[] args) {
final CacheTest cache = new CacheTest();
final String key = "user";
for (int i = 0; i < 1000; i++) {
new Thread(){
public void run() {
System.out.println(cache.getData(key));
};
}.start();
}
}
}
1. java并发中有一个map就是Collections.synchronizedMap(不是绝对的线程安全),它是将map的每个方法都加了同步(都加了虚拟锁机制),如:
private Map map=Collections.synchronizedMap(new HashMap());
2 虽然Collections.synchronizedMap是线程安全的map(不是绝对的)。也是将map中的每个方法都加了线程安全的锁来控制同步。但是如果把它的任意两个方法合在一起使用还是会出现线程不安全的事情。如:
private Map map=Collections.synchronizedMap(new HashMap());
public void removeKey(String key){
if(map.containsKey(key))
{//containsKey方法是线程安全的方法
map.remove(key);//remove方法也是线程安全的方法。
}
}
但是他们放在一起操作(多个该map(map是通过Collections.synchronizedMap来定义的)的方法)还是会出现线程不安全的事情。如当程序正在执行containsKey方法时,另一个程序已经将key删除了。会出现这种情况。就是会有两个线程同时操作map,导致不可预见性的结果。这时为了解决此问题,还是要加上同步机制。如:
private static final Object object=new Object();
public void removeKey(String key){
synchronized(object){
if(map.containsKey(key))
{//containsKey方法是线程安全的方法
map.remove(key);//remove方法也是线程安全的方法。
}
}
}
故Collections.synchronizedMap也只是有条件的线程安全的。而不是绝对的线程安全的。还是少用的好。
java并发问题,解决此类的问题。要么用绝对的线程安全类来处理。要么加同步机制锁。
ConcurrentHashMap融合了Hashtable和HashMap二者的优势。
Hashtable是做了线程同步,HashMap未考虑同步。所以HashMap在单线程下效率较高,Hashtable在多线程下同步操作能保证程序的正确性。 但是Hashtable每次执行同步操作都需要锁住整个结构。
ConcurrentHashMap的出现就是为了解决Hashtable同步lock整个数据结构的问题。ConcurrentHashMap锁的方式是细颗粒度。
ConcurrentHashMap将Hash表分为16个桶(默认值),诸如get/put/remove操作只需要锁着需要的单个桶即可。
ConcurrentHashMap只有在size等操作的时候才会锁住整个Hash表。
下面是自己实现的一个ConcurrentHashMap的本地缓存的例子:ConcurrentHashMap 和Guava cache相比,需要自己显示的删除缓存。
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapTest {
private static ConcurrentHashMap cacheMap = new ConcurrentHashMap<>();
/**
* 获取缓存的对象
*
* @param account
* @return
*/
public static String getCache(String account) {
account = getCacheKey(account);
// 如果缓冲中有该账号,则返回value
if (cacheMap.containsKey(account)) {
return cacheMap.get(account);
}
// 如果缓存中没有该账号,把该帐号对象缓存到concurrentHashMap中
initCache(account);
return cacheMap.get(account);
}
/**
* 初始化缓存
*
* @param account
*/
private static void initCache(String account) {
// 一般是进行数据库查询,将查询的结果进行缓存
cacheMap.put(account, "18013093863");
}
/**
* 拼接一个缓存key
*
* @param account
* @return
*/
private static String getCacheKey(String account) {
return Thread.currentThread().getId() + "-" + account;
}
/**
* 移除缓存信息
*
* @param account
*/
public static void removeCache(String account) {
cacheMap.remove(getCacheKey(account));
}
}
下面是使用ConcurrentHashMap封装的缓存工具类,并提供了常用的方法:存入缓存、查找缓存、删除缓存、获取缓存大小等等;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.concurrent.*;
/**
* @author xxkfz
* 自定义缓存
*/
@Slf4j
public class ConcurrentHashMapCache {
private final Map CACHE_MAP = new ConcurrentHashMap();
/**
* 每个缓存生效时间12小时
*/
public final long CACHE_HOLD_TIME_12H = 12 * 60 * 60 * 1000L;
/**
* 每个缓存生效时间24小时
*/
public final long CACHE_HOLD_TIME_24H = 24 * 60 * 60 * 1000L;
/**
* 存放一个缓存对象,默认保存时间12小时
*
* @param cacheName
* @param obj
*/
public void put(String cacheName, Object obj) {
put(cacheName, obj, CACHE_HOLD_TIME_12H);
}
/**
* 存放一个缓存对象,保存时间为holdTime
*
* @param cacheName
* @param obj
* @param holdTime
*/
public void put(String cacheName, Object obj, long holdTime) {
CACHE_MAP.put(cacheName, obj);
// 设置缓存失效时间
CACHE_MAP.put(cacheName + "_HoldTime", System.currentTimeMillis() + holdTime);
log.info("{}:成功存入缓存! 过期时间:{}", cacheName, holdTime);
}
/**
* 取出一个缓存对象
*
* @param cacheName
* @return
*/
public Object get(String cacheName) {
if (checkCacheName(cacheName)) {
return CACHE_MAP.get(cacheName);
}
return null;
}
/**
* 删除所有缓存
*/
public void removeAll() {
CACHE_MAP.clear();
}
/**
* 删除某个缓存
*
* @param cacheName
*/
public void remove(String cacheName) {
CACHE_MAP.remove(cacheName);
CACHE_MAP.remove(cacheName + "_HoldTime");
}
/**
* 检查缓存对象是否存在,
* 若不存在,则返回false
* 若存在,检查其是否已过有效期,如果已经过了则删除该缓存并返回false
*
* @param cacheName
* @return
*/
public boolean checkCacheName(String cacheName) {
Long cacheHoldTime = (Long) CACHE_MAP.get(cacheName + "_HoldTime");
if (cacheHoldTime == null || cacheHoldTime == 0L) {
return false;
}
if (cacheHoldTime < System.currentTimeMillis()) {
log.info("缓存:{}已失效!", cacheName);
remove(cacheName);
return false;
}
return true;
}
/**
* 获取缓存大小
*
* @return
*/
public Integer size() {
if (null != CACHE_MAP && CACHE_MAP.size() > 0) {
return CACHE_MAP.size();
}
return 0;
}
/**
* 判断缓存的key是否存在
*
* @param cacheName
* @return
*/
public boolean isExist(String cacheName) {
if (!checkCacheName(cacheName)) {
return Boolean.FALSE;
}
return CACHE_MAP.containsKey(cacheName);
}
}