一、java中的锁
- 在JDK5中,新增了Lock锁接口,有ReentrantLock实现类,ReentrantLock锁称为可重入锁,它的功能比synchronize多,更加强大。
二、锁的可重入性
- 锁的可重入性,是指当一个线程已经获得一个对象锁时,再次请求获得该对象锁时,是可以获得的。
private synchronized void ff1(){
System.out.println("方法1");
ff2();
}
private synchronized void ff2() {
System.out.println("方法2");
ff3();
}
private synchronized void ff3() {
System.out.println("方法3");
}
public static void main(String[] args) {
Demo6 demo = new Demo6();
new Thread(new Runnable() {
@Override
public void run() {
demo.ff1();
}
}).start();
}
- 开启线程,线程调用ff1方法,默认的this就是锁对象,在ff1方法中,调用了ff2方法,当前线程已经持有了锁对象。在ff2方法中,默认的锁对象也是this对象,要执行ff2,必须要获得该锁对象。虽然当前线程已经有了一个锁对象,但是还可以再次获得这个锁对象,这就是锁的可重入性。
- 如果锁没有可重入性,那可能会造成死锁。因为线程要执行ff2方法,必须等待锁释放,但是它自己拿着这个锁,如果ff2没有执行完毕,它就不可能释放这个锁,就死锁了。
三、ReentrantLock
static Lock lock = new ReentrantLock();
public static void ff(){
lock.lock();
for(int i = 0; i <10;i++){
System.out.println(Thread.currentThread().getName()+"--"+i);
}
lock.unlock();
}
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
ff();
}
};
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
}
运行结果:
- 先定义一个ReentrantLock对象,在方法中,我们需要同步的位置调用lock方法,当不需要同步的时候,调用unlock方法释放锁,然后创建三个线程,分别去调用ff方法,发现,所有的线程都完成了0~9的打印,互不干扰。
- 一般我们会在try代码块中获得Lock锁(也就是调用lock方法),在finally代码块中释放锁(unlock方法)。
- 对于synchronize内部锁来说,如果一个线程在等待锁,只会有两个结果,要么该线程获得锁,开始执行,要么就一直等着。
对于ReentrantLock可重入锁来说,提供了第三种可能,在等待锁的途中,程序可以根据需求,取消对锁的请求。(使用lockInterruptibly()方法,可以解决死锁问题 )
四、ReentrantReadWriteLock 读写锁
1. ReentrantReadWriteLock 读写锁基本介绍
- synchronize内部锁和ReentrantLock锁都是独占锁(排它锁),同一时间只能有一个线程执行同步代码块,可以保证线程安全,但是效率比较低。
- ReentrantReadWriteLock 读写锁可以理解为一种改进的排它锁,也可以称作共享排它锁。它允许多个线程同时读取共享数据,但是只允许一个线程对共享数据更新。
- 读写锁通过读锁+写锁来完成读写操作。线程在读取共享数据前,必须持有读锁,读锁可以被多个线程持有。写锁就是排他的,线程更新共享数据前,必须先拿到写锁。当一个线程持有写锁时,其他线程不仅拿不到写锁,连读锁也拿不到。读锁只是在要读共享数据的线程之间共享,只要有一个线程有读锁,别的线程是无法获得写锁的。这保证了线程在读数据期间,没有其他线程对数据进行修改,使得读线程能够读到数据的最新值。
获得条件 | 排他性 | 作用 | |
---|---|---|---|
读锁 | 写锁未被任意线程持有 | 对要读的线程是共享的,对要写的线程是排他的 | 允许多个要读的线程可以同时读取共享数据,并且保证了读的时候没有其他线程对共享数据修改 |
写锁 | 该写锁未被其他线程持有,并且相应的读锁也未被其他线程持有 | 对要读,要写的线程都排他 | 保证要写的线程以独享的方式修改数据 |
- 读写锁允许读读共享,读写互斥,写写互斥。
2. ReentrantReadWriteLock 读写锁的基本使用
- 在java.util.concurrent.locks包中定义了ReentrantWriteLock接口,该接口中定义了readLock()返回读锁,定义writeLock()返回写锁,该接口的实现类是ReentrantReadWriteLock类。注意:readLock()和writeLock()返回的锁,是同一个锁的两个部分(角色),并不是两个不同的锁!
//定义读写锁
ReadWriteLock rwLock = new ReentrantReadWriteLock();
//获得读锁
Lock readLock = rwLock.readLock();
//获得写锁
Lock writeLock = rwLock.writeLock();
//读数据
readLock.lock();//申请读锁
try {
//读取共享数据
}finally {
readLock.unlock();//我们总是在finally里释放锁
}
//写数据
writeLock.lock();
try {
//修改共享数据
}finally {
readLock.unlock();//释放锁
}
3. 读读共享
- ReadWriteLock读写锁可以实现多个线程同时读取共享数据,即读读共享,可以提高程序的读取数据的效率。
演示代码:
static class Service{
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss SSS");
//定义读写锁
ReadWriteLock rwLock = new ReentrantReadWriteLock();
//定义方法读数据
public void read() {
rwLock.readLock().lock();//获取读锁
System.out.println(Thread.currentThread().getName()+"获得读锁,开始读数据"+sdf.format(new Date()));
try {
TimeUnit.SECONDS.sleep(3);//模拟读数据
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();//释放锁
}
}
}
public static void main(String[] args) {
Service service = new Service();
//创建几个线程,调用read方法
for(int i = 0; i < 5; i++){
new Thread(new Runnable() {
@Override
public void run() {
service.read();
}
}).start();
}
}
运行结果:
从运行结果可以看出,所有的线程都是同时获得读锁,执行lock后面的代码
4. 写写互斥
- 通过ReadWriteLock读写锁中的写锁,只允许有一个线程执行Lock()后面的代码。
演示代码:
static class Service{
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss SSS");
//定义读写锁
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//定义方法写数据
public void write(){
readWriteLock.writeLock().lock();//申请获得写锁
System.out.println(Thread.currentThread().getName()+"获得写锁,开始写数据"+sdf.format(new Date()));
try {
TimeUnit.SECONDS.sleep(3);//模拟修改数据数据
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+"数据写完的时间="+sdf.format(new Date()));
readWriteLock.writeLock().unlock();//释放锁
}
}
}
public static void main(String[] args) {
Service service = new Service();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
service.write();
}
}).start();
}
}
运行结果:
在这里插入图片描述从运行结果可以看出,通过写锁,每个线程必须要“排队”写数据,实现了写写互斥。
5. 读写互斥
- 写锁是排它锁,那么读与写也是互斥的。
演示代码:
static class Service {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss SSS");
//定义读写锁
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//定义方法读数据
public void read() {
readWriteLock.readLock().lock();//申请获得读锁
System.out.println(Thread.currentThread().getName() + "获得读锁,开始读数据" + sdf.format(new Date()));
try {
TimeUnit.SECONDS.sleep(3);//模拟读数据
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "数据读完的时间=" + sdf.format(new Date()));
readWriteLock.readLock().unlock();//释放锁
}
}
//定义方法写数据
public void write() {
readWriteLock.writeLock().lock();//申请获得写锁
System.out.println(Thread.currentThread().getName() + "获得写锁,开始写数据" + sdf.format(new Date()));
try {
TimeUnit.SECONDS.sleep(3);//模拟修改数据数据
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "数据写完的时间=" + sdf.format(new Date()));
readWriteLock.writeLock().unlock();//释放锁
}
}
}
public static void main(String[] args) {
Service service = new Service();
new Thread(new Runnable() {
@Override
public void run() {
service.read();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
service.write();
}
}).start();
}
运行结果:
程序创建了两个线程,第一个线程先获得读锁,开始读数据,这个时候第二个线程就进不来了,必须等第一个线程把读锁释放了,才能获得写锁,实现了读写互斥。