复习悲观锁和乐观锁
悲观锁进行数据操作时只能有一个线程,只要这个线程一执行就会给账户上锁,等到此线程完毕才会解锁,其他线程才可以对此账户继续操作
悲观锁可以解决并发中的问题,但是恰恰因为其不支持并发操作,所以使用悲观锁后代码效率低
乐观锁会有一个版本号属性,当一个线程对数据对数据进行操作之后会更新版本号version。其他线程也可以对数据进行操作,但是在提交之前会比较版本号,版本号低于version则提交失败
复习表锁和行锁
- 表锁:只操作一条记录,但是整张表都上锁,整个表都锁住
- 行锁:只操作一条记录,就只对这条记录所在行进行上锁,其他线程仍可操作其他行数据
-
读写锁
关于死锁
读锁发生死锁解释:线程1,2都要对数据进行读取和修改,但是线程1必须等待线程2读取完才可以对数据进行修改,不然会发生脏读。所以线程1 就要等待2读完。同理线程2也要修改,也要等待线程1读完,这样两个线程都不会修改数据,发生死锁。
写锁发生死锁解释:线程1先对第一条记录操作接着要对第二条记录操作;线程2先对第二条记录操作,在对第一条记录操作;这样就出现了线程1必须等线程2执行完毕才会对第二条记录操作,但线程2要执行完毕必须要修改第一条记录,但此记录正在线程1的控制中,所以就出现了相互等待;这样的相互等待出现了死锁读写锁概述
现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那 么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以 应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源, 就不应该允许其他线程对该资源进行读和写的操作了。 简而言之:可以共同读,只可以单一写
针对这种场景,JAVA 的并发包提供了读写锁 ReentrantReadWriteLock, 它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称 为排他锁 线程进入读锁的前提条件:
- 没有其他线程的写锁
- 没有写请求, 或者==有写请求,但调用线程和持有锁的线程是同一个(可重入 锁)。==
线程进入写锁的前提条件:
- 没有其他线程的读锁
-
读写锁案例
模拟多线程在map中取数据和读数据 ```java //资源类 class MyCache { //创建map集合,volatile表示数据不断发生变化 private volatile Map
map = new HashMap<>(); //创建读写锁对象 private ReadWriteLock rwLock = new ReentrantReadWriteLock();
//写数据 public void put(String key,Object value) { //添加写锁 rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+" 正在写操作"+key);
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
//放数据
map.put(key,value);
System.out.println(Thread.currentThread().getName()+" 写完了"+key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放写锁 rwLock.writeLock().unlock();
} }
//读数据 public Object get(String key) { //添加读锁 rwLock.readLock().lock(); Object result = null; try {
System.out.println(Thread.currentThread().getName()+" 正在读取操作"+key); //暂停一会 TimeUnit.MICROSECONDS.sleep(300); result = map.get(key); System.out.println(Thread.currentThread().getName()+" 取完了"+key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放读锁 rwLock.readLock().unlock();
} return result; } }
public class ReadWriteLockDemo { public static void main(String[] args) throws InterruptedException { MyCache myCache = new MyCache(); //创建线程放数据 for (int i = 1; i <=5; i++) { final int num = i; new Thread(()->{ myCache.put(num+””,num+””); },String.valueOf(i)).start(); }
TimeUnit.MICROSECONDS.sleep(300);
//创建线程取数据
for (int i = 1; i <=5; i++) {
final int num = i;
new Thread(()->{
myCache.get(num+"");
},String.valueOf(i)).start();
}
}
}
========================================================= 1 正在写操作1 1 写完了1 2 正在写操作2 2 写完了2 4 正在写操作4 4 写完了4 3 正在写操作3 3 写完了3 5 正在写操作5 5 写完了5 1 正在读取操作1 3 正在读取操作3 5 正在读取操作5 2 正在读取操作2 4 正在读取操作4 3 取完了3 5 取完了5 2 取完了2 1 取完了1 4 取完了4
Process finished with exit code 0
可见读可以一起读,写只能单独写
<a name="zVmL5"></a>
## 读写锁深入
<a name="pabwB"></a>
### 演变和优缺点

<a name="Cg554"></a>
### 锁降级

```java
//演示读写锁降级
public class ReentrantReadWriteLockDemo{
public static void main(String[] args) {
//可重入读写锁对象
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();//读锁
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();//写锁
//锁降级
//1 获取写锁
writeLock.lock();
System.out.println("---write");
//2 获取读锁
readLock.lock();
System.out.println("---read"); //输出read说明获取到了读锁
//3 释放写锁
writeLock.unlock();
//4 释放读锁
readLock.unlock();
}
}
//若1,2互换位置,则获取不到写锁,因为还没读完没释放读锁。说明读锁不能升级为写锁
//读的时候不能写,写的时候可以读(锁降级)
原因
当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把 获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写 锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释 放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。