JAVA中主要有两种锁,显式锁和内置锁。下面就是一点我个人的理解:
内置锁:synchronized就是内置锁,它存在可选择性差、固话的锁。控制起来并不灵活。
显示锁:指的就是可以显式声明的锁。和synchronized这种内置锁不同的是,显式锁可以手动的声明和释放,应用起来会更加灵活。提供了超时、中断等利于开发的功能。
至于这两种锁应当如何选择的问题,首先JAVA对于内置锁的优化一直在进行,在现在jdk8中很多显式锁的代码也都改用了内置锁synchronized。而且synchronized要比显式锁占用的资源要小一点(最起码少一个要声明的对象)。所以当我们对锁的操作逻辑并不复杂,没有用到尝试取锁、中断等机制,尽量使用synchronized。
所有的显式锁,都是实现了Lock接口的对象。
lock() 拿锁
unlock()释放锁
lockInterruptibly()中断机制
tryLock()尝试去拿锁,超时机制。
PS :unlock()方法一定要包在finally中,业务代码也定要放在try-catch中,用来保证unlock一定会释放。
先申请锁的线程一定先拿到,这个锁是公平锁。如果资源是无序发放的,则这个锁是非公平的。
非公平锁的性能要比公平锁更好:因为唤醒线程,需要上下文切换。 上下文切换需要消耗5000~10000CPU周期。如果纯粹的公平锁,每个线程都需要一个这个消耗。而非公平锁则可以充分的利用上下文切换的时间做点事情。
ReentrantLock是非常典型的显式锁之一,而它的功能基本可以替代synchronized关键字(synchronized也是重入锁)。而所谓的重入锁,就是这个锁对于当前线程是可以反复进入的(比如递归调用。如果是不可重入,则线程会把自己锁死)。
ReentrantLock使用方法也和synchronized很相似,不过就是在需要加锁的地方 lock.lock(),在释放的地方lock.unlock()。
具体的实例我们放到Condition中,一起发一个完整的实例。
Condition条件主要用来控制线程等待和释放当前锁,作用与Object.wait()和Object.notify()方法类似。
如果想要获得一个Condition对象,我们可以通过通过Lock的newCondition()方法来获取这个lock对应的一个Condition。(多次调用这个方法,最后返回的是不同的对象,同时等待和释放的效果也是各自独立的)。
public class ReentrantLockDemo {
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition1 = lock.newCondition();
private static Condition condition2 = lock.newCondition();
public static void main(String[] args) {
System.out.println(condition1);
System.out.println(condition2);
new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
System.out.println("线程1启动");
System.out.println("线程1开始等待");
condition1.await();
System.out.println("线程1执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}).start();
SleepTools.second(2);
new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
System.out.println("线程2启动");
condition1.signal();
//创建的 condition2并不能唤醒线程1,导致线程1永远等待
// condition2.signal();
System.out.println("signal");
SleepTools.second(2);
System.out.println("释放锁");
}finally {
lock.unlock();
}
}
}).start();
}
}
执行结果:
在当前这个互联网时代,我们数据的读取规模远大于写入的规模。而这个时候,而每一次读操作对数据的完整性并没有任何的影响。所以如果使用传统的锁,每一次读都需要等待上一个线程释放锁,那样会大大的降低系统的效率。
而ReadWriteLock读写锁就是在JDK5中提供的读写分离锁,这个锁并不是一味的把线程锁死,而是会根据情况来进行线程的释放。具体的释放规则如下:
1、读操作不阻塞读操作
2、读操作阻塞写操作
3、写操作也阻塞读操作
如果在系统中读操作远大于写操作的话,这个读写锁就能很好的发挥它的作用。
使用读写锁,我们需要先创建一个ReadWriteLock对象(注意:这里的ReadWriteLock只是一个接口,具体的实现类是ReentrantReadWriteLock),然后通过这个对象的getReadLock()和getWriterLock()分别获取读锁和写锁。具体的操作是使用getReadLock()和getWriterLock()获得的对象来进行的。
接下来是一个读写锁的例子,并且和使用普通的重入锁的对比:
public class ReadWriteLockDemo {
private static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();
private static Lock writeLock = readWriteLock.writeLock();
private static Lock lock = new ReentrantLock();
private static CountDownLatch latch = new CountDownLatch(100);
static class ReadLockHandle implements Runnable{
private Lock lock;
private int id;
public ReadLockHandle(Lock lock, int id) {
this.lock = lock;
this.id = id;
}
@Override
public void run() {
try {
lock.lock();
SleepTools.ms(10);
System.out.println(id + ": 读操作完成!");
latch.countDown();
}finally {
lock.unlock();
}
}
}
static class WriteLockHandle implements Runnable{
private Lock lock;
private int id;
public WriteLockHandle(Lock lock, int id) {
this.lock = lock;
this.id = id;
}
@Override
public void run() {
try {
lock.lock();
SleepTools.ms(50);
System.out.println(id+": 写操作完成!");
latch.countDown();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newFixedThreadPool(10);
long begin = System.currentTimeMillis();
for (int i=0 ; i<100; i++){
if(i % 5 == 0){
exec.submit(new WriteLockHandle(writeLock,i));
// exec.submit(new ReadLockHandle(lock,i));
}else{
exec.submit(new ReadLockHandle(readLock,i));
// exec.submit(new WriteLockHandle(lock,i));
}
}
latch.await();
System.out.println(System.currentTimeMillis() - begin);
exec.shutdown();
}
}
(这个代码中提前用到了线程池的概念,我们这里就不详细介绍了,就可以忽视掉,认为就是不停的new出来的线程就可以了。)
在使用读写锁的时候,这段代码运行花费了1172毫秒。
而使用普通的重入锁的时候,使用了4219毫秒。
这两者在这种情况下的效率差距还是非常明显的。