通过一个详细例子引入重入锁ReentrantLock、ReadWriteLock读写锁
1.概述
ReentrantLock是可重入的独占锁。比起synchronized功能更加丰富,支持公平锁实现,支持中断响应以及限时等待等等。可以配合一个或多个Condition条件方便的实现等待通知机制。
2.代码示例
Demo1:
并发安全会存在抢占问题
package com.company.lock;
public class MyLockDemo {
static int i = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(new Add()).start();
new Thread(new Add()).start();
//Thread.sleep(3000);
System.out.println(i);
}
}
class Add implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
LockDemo.i++;
}
}
}
上面程序的执行结果是0,没有加Thread.sleep(3000)时:
因为:
主函数所在的类默认是一个线程类,在执行过程中启动两个add线程,线程被start的时候并不是立即执行,而是会经历一系列的准备过程,这就意味着add线程还在启动中,主线程可以抢占执行权继续执行,从而输出0。如果上面的线程没有执行完,那么主线程即使抢到执行权,也应该阻塞,所以添加sleep3秒,先让线程计算完成,此时输出的结果如下,并不是20W说明出现了线程抢占,出现了线程并发安全问题。
Demo2:调整上面代码加入锁synchronized,确定锁对象就为Add.class,代码如下
package com.company.lock;
public class MyLockDemo {
static int i = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(new Add()).start();
new Thread(new Add()).start();
Thread.sleep(3000);
System.out.println(i);
}
}
class Add implements Runnable{
@Override
public void run() {
synchronized (Add.class) {
for (int i = 0; i < 100000; i++) {
MyLockDemo.i++;
}
}
}
}
此时执行的结果就是20W
Demo3:
上面的Demo2中将Add.class作为了锁对象,但是还是有问题,当有2个类--A类和B类,他们中都有2个同样的方法(代码如下)此时的A和B是2个线程的情况下此时启动A和B类中4个线程,那么他们之间构成了互斥,因为锁对象是一致的都是Add.class。如果想要A中的2个线程互斥,B中的2个线程互斥,各干各的,互不影响---这种情况下synchronized不好实现,由此可以发现synchronized锁的一个特点。
synchronized在使用过程中需要去确定锁对象。如果锁对象确定错误,容易导致死锁。
synchronized不够灵活
package com.company.lock;
public class MyLockDemo {
static int i = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(new Add()).start();
new Thread(new Add()).start();
Thread.sleep(3000);
System.out.println(i);
}
}
class Add implements Runnable{
@Override
public void run() {
synchronized (Add.class) {
for (int i = 0; i < 100000; i++) {
MyLockDemo.i++;
}
}
}
}
class A {
public static void main(String[] args) {
new Thread(new Add()).start();
new Thread(new Add()).start();
}
}
class B {
public static void main(String[] args) {
new Thread(new Add()).start();
new Thread(new Add()).start();
}
}
Demo4
上述Demo3中synchronized还需要去确定锁对象,且存在不灵活等问题,所以在jdk5中提供了Lock 接口,它的一个实现类是ReentrantLock--重入锁。所以先构建一个锁对象,并传入线程中,然后在通过有参构造接收锁对象。
此时因为锁对象是自己传递进来的,所以此时加锁,只需要调用lock()方法加锁即可(也就不用再去寻找锁对象),结束后调用方法解锁。
此时验证执行结果正确
package com.company.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyLockDemo {
static int i = 0;
public static void main(String[] args) throws InterruptedException {
//构建锁对象
Lock lock = new ReentrantLock();
//传入锁对象
new Thread(new Add(lock)).start();
new Thread(new Add(lock)).start();
Thread.sleep(3000);
System.out.println(i);
}
}
class Add implements Runnable {
//通过有参构造接收锁对象
private Lock lock;
public Add(Lock lock) {
this.lock = lock;
}
@Override
public void run() {
//加锁
lock.lock();
for (int i = 0; i < 100000; i++) {
MyLockDemo.i++;
}
//解锁
lock.unlock();
}
}
有了重入锁,此时A和B 类如果想要内部互斥,外部不影响,那么分别创建自己的锁就行。
class A {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
new Thread(new Add(lock)).start();
new Thread(new Add(lock)).start();
}
}
class B {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
new Thread(new Add(lock)).start();
new Thread(new Add(lock)).start();
}
}
ReentrantLock - 重入锁:当线程将锁资源释放之后,其他线程依然可以抢占这个锁资源来继续使用;
非重入锁 - 当线程将锁资源释放之后,这个锁就不能被再次使用(Lock中只提供了重入,没有提供非重入)
读写锁的添加,需要根据场景进行判断添加,如下面的例子,是要进行数据求和,所以添加写锁
package com.company.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class MyLockDemo {
static int i = 0;
public static void main(String[] args) throws InterruptedException {
//获取一个写锁
ReadWriteLock rwl = new ReentrantReadWriteLock();
Lock lock = rwl.writeLock();
//传入锁对象
new Thread(new Add(lock)).start();
new Thread(new Add(lock)).start();
Thread.sleep(3000);
System.out.println(i);
}
}
class Add implements Runnable {
//通过有参构造接收锁对象
private Lock lock;
public Add(Lock lock) {
this.lock = lock;
}
@Override
public void run() {
//加锁
lock.lock();
for (int i = 0; i < 100000; i++) {
MyLockDemo.i++;
}
//解锁
lock.unlock();
}
}
运行结果正常
查阅ReentrantReadWriteLock的源码,可以看到里面的描述提到了公平策略
读写锁和公平、非公平策略归纳如下:
ReadWriteLock - 读写锁
- 读锁:允许多个线程同时读但是不允许线程写
- 写锁:只允许1个线程写但是不允许线程读
公平和非公平策略
- 在资源有限的情况下,每一个线程实际抢占执行的次数并不均等,这种方式称之为非公平策略
- 在公平策略的前提下,线程并不是直接去抢占资源而是抢占入队顺序。在这种策略下,每一个线程的执行次数是大致相等
- 相对而言,非公平策略的效率更高一些
- 如果不指定,则默认使用非公平策略
- synchronized也是非公平的