一. Synchronized 和lock /ReentrantLock 的区别
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,JVM会自动释放线程占有的锁,不会导致死锁现象发生;而Lock在发生异常时,如果不会主动通过unLock()释放锁,可能造成死锁现象,因此使用Lock时需要在finally块中释放锁,或者等待一定的时间响应中断。
3)Lock可以让等待锁的线程响应中断,而synchronized不行。
4)通过Lock相应方法的返回值,可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程读操作的效率。
从性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。
二. java.util.concurrent.locks包下常用的类
1.Lock是一个接口,其实现类有ReentrantLock、WriteLock,方法说明如下:
lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。
unLock()方法是用来释放锁的
void lock(); //如果锁已被其他线程获取,则进行等待。
void lockInterruptibly() throws InterruptedException; //该方法允许线程在等待锁时,调用 interrupt() 响应中断。 注意: 当一个线程获取了锁之后,是不能被interrupt()方法中断的
boolean tryLock(); //有返回值的,true:获得锁; false:获取锁失败。拿不到锁时不会一直在那等待,立即返回
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //拿不到锁时会等待一定的时间,如果还拿不到锁,则返回false;如果拿到锁,则返回true。
void unlock(); //释放锁
Condition newCondition();
三.ReentrantReadWriteLock的使用
ReadWriteLock也是一个接口,提供了readLock和writeLock两种锁的实现。
使用场合:
假设在程序中定义一个共享的数据结构用作缓存,它大部分时间提供读服务(例如:查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读服务可见。在没有读写锁支持的(Java 5 之前)时候,如果需要完成上述工作就要使用Java的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行 通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键字进行同步),这样做的目的是使读操作都能读取到正确的数据,而不会出现脏读。改用读写锁实现上述功能,只需要在读操作时获取读锁,而写操作时获取写锁即可,当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被 阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。
特性
ReentrantReadWriteLock的实现里面有以下几个特性
1、公平性:非公平锁(默认)。读线程之间没有锁操作,所以读操作没有公平性和非公平性。写操作时,由于写操作可能立即获取到锁,所以会推迟一个或多个读操作或者写操作。非公平锁的吞吐量要高于公平锁。(公平锁概念:公平锁利用AQS的CLH队列,释放当前保持的锁时,优先为等待时间最长的那个写操作分配写入锁)
2、重入性:读写锁允许读线程和写线程按照请求锁的顺序重新获取读取锁或者写入锁。只有写线程释放了锁,读线程才可以获取重入锁,写线程获取写入锁后可以再次获取读取锁,但是读线程获取读取锁后却不能获取写入锁。
3、锁降级:写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级特性,经典cache案例使用了锁降级
4、锁升级:读取锁是不能直接升级为写入锁的。因此获取一个写入锁需要先释放所有的读取锁,如果有两个读取锁试图获取写入锁,且都不释放读取锁时,就会发生死锁
5、锁获取中断:读取锁和写入锁都支持获取锁期间被中断
6、条件变量:写入锁提供了条件变量的支持,但是读取锁却不允许获取条件变量,否则会得到一个 nsupportedOperationExcetpion异常
7、重入锁:读取锁和写入锁的数量最大分别只能是65535
读写锁机制
读-读不互斥
读-写互斥
写-写互斥
ReentrantReadWriteLock实现缓存代码:
假设在程序中定义一个共享的数据结构用作缓存,它大部分时间提供读服务(例如:查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读服务可见。在没有读写锁支持的(Java 5 之前)时候,如果需要完成上述工作就要使用Java的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行 通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键字进行同步),这样做的目的是使读操作都能读取到正确的数据,而不会出现脏读。改用读写锁实现上述功能,只需要在读操作时获取读锁,而写操作时获取写锁即可,当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被 阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。
特性
ReentrantReadWriteLock的实现里面有以下几个特性
1、公平性:非公平锁(默认)。读线程之间没有锁操作,所以读操作没有公平性和非公平性。写操作时,由于写操作可能立即获取到锁,所以会推迟一个或多个读操作或者写操作。非公平锁的吞吐量要高于公平锁。(公平锁概念:公平锁利用AQS的CLH队列,释放当前保持的锁时,优先为等待时间最长的那个写操作分配写入锁)
2、重入性:读写锁允许读线程和写线程按照请求锁的顺序重新获取读取锁或者写入锁。只有写线程释放了锁,读线程才可以获取重入锁,写线程获取写入锁后可以再次获取读取锁,但是读线程获取读取锁后却不能获取写入锁。
3、锁降级:写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级特性,经典cache案例使用了锁降级
4、锁升级:读取锁是不能直接升级为写入锁的。因此获取一个写入锁需要先释放所有的读取锁,如果有两个读取锁试图获取写入锁,且都不释放读取锁时,就会发生死锁
5、锁获取中断:读取锁和写入锁都支持获取锁期间被中断
6、条件变量:写入锁提供了条件变量的支持,但是读取锁却不允许获取条件变量,否则会得到一个 nsupportedOperationExcetpion异常
7、重入锁:读取锁和写入锁的数量最大分别只能是65535
读写锁机制
读-读不互斥
读-写互斥
写-写互斥
ReentrantReadWriteLock实现缓存代码:
package com.bj.concrrent.locks;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.junit.Test;
public class LocksTest {
ReadWriteLock lock = new ReentrantReadWriteLock();
public static int num = 0;
private Cache cache = new Cache();
@Test
public void testLocks(){
//创建1000个线程同时对Cache做读写操作, 其中的key不同, 所以, 不会有读版本不一致的问题。
for(int i = 0; i< 1000; i++){
Thread thread = new Thread(new Runnable(){
@Override
public void run() {
int num = (int) (Math.random() * 1000 % 10);
cache.putIfNotExit("key"+num, "value"+num);
}
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(Cache.map);
}
}
/**
* 描述:使用HashMap实现缓存, 线程间可以同时读取, 不能同时写, 读和写相互隔离。类似于乐观锁
*
*/
class Cache {
//新建一个HashMap,线程间可见, 但不安全
public volatile static Map<String, Object> map = new HashMap<String, Object>(16);
private ReadWriteLock lock = new ReentrantReadWriteLock();
public Object get(String key){
lock.readLock().lock();
Object result = null;
try {
result = map.get(key);
} catch (Exception e) {
e.printStackTrace();
}finally{
lock.readLock().unlock();
}
return result;
}
public Object put(String key, Object value){
lock.writeLock().lock();
try {
map.put(key, value);
} catch (Exception e) {
e.printStackTrace();
}finally{
lock.writeLock().unlock();
}
return value;
}
//get功能描述: 如果map中有值则返回, 如果没有值设置为“default”返回
public Object putIfNotExit(String key,Object value){
lock.readLock().lock();//首先开启读锁,从缓存中去取
Object desValue = null;
try{
desValue = map.get(key); //如果不为空可以不用进入writeLock, 提高性能
System.out.println(Thread.currentThread().getName() + " : 读取key : " + key +" ,value : " + desValue);
if(desValue == null){ //如果缓存中没有对应key的值,释放读锁,上写锁
Thread.sleep(100); //模拟执行其他操作用时
lock.readLock().unlock(); // 有可能多个线程进入到这, 要修改map, 所以可能会产生线程1put, 紧接着线程2put
lock.writeLock().lock(); // 如果想保证读写的原子性, 不能使用读锁
System.out.println(Thread.currentThread().getName() + "进入写锁 " + key +" ,value : " + desValue);
try{
if(map.get(key) != null) return desValue; //① 在写之前重新读一次, 保证一个key只被写一次,不发生线程间覆盖, 类似事物的可重复读。
if(value != null){
System.out.println(Thread.currentThread().getName() + "真正写 " + key +" ,value : " + value);
map.put(key, value); // 能保证线程间写互斥, 但不能保证线程覆盖
desValue = value;
}
}finally{
lock.writeLock().unlock(); //释放写锁
}
lock.readLock().lock(); //然后再上读锁
}
}catch(Exception e){} finally{
lock.readLock().unlock(); //最后释放读锁
}
return desValue;
}
}
①处的代码注释掉结果:
①处的代码不注释掉,结果:
锁升级降级示例