一、重入锁
1.锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized(重量级) 和 ReentrantLock(轻量级)等等 ) 。这些已经写好提供的锁为我们开发提供了便利。重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
2.在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁。
3.代码
//重入锁 轻量级(Lock)与重量级锁(synchronized)---可重入性(递归锁)
public class Test001 implements Runnable {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
set();
}
//synchronized代码块执行完毕的时候释放锁
private synchronized void set() {
System.out.println("set方法");
get();
}
private synchronized void get() {
System.out.println("synchronized 可具备可重入性-get方法");
}
public static void main(String[] args) {
Test001 test001 = new Test001();
Thread thread = new Thread(test001);
thread.start();
System.out.println(Thread.currentThread().getName()+"主线程结束");
}
}
4.结果
main主线程结束
set方法
synchronized 可具备可重入性-get方法
5.代码
//演示lock锁是否具备 可重入性(特征:锁可以传递(方法递归传递)),下面的方法为啥会调两次,因为最后一次调用他已经知道第一次已经上锁了(不会在重新获取锁)
public class Test002 implements Runnable {
Lock lock = new ReentrantLock();
@Override
public void run() {
set();
}
private void set() {
try {
//上锁
lock.lock();
System.out.println("set方法");
get();
} catch (Exception e) {
//重入锁的目的就是避免死锁
} finally {
lock.unlock();//释放锁
}
}
private void get() {
try {
lock.lock();
System.out.println("lock 可具备可重入性-get方法");
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Test002 test002 = new Test002();
Thread thread = new Thread(test002);
thread.start();
}
}
6.结果
set方法
lock 可具备可重入性-get方法
二、读写锁
1.相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些。假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(译者注:也就是说:读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。Java5在java.util.concurrent包中已经包含了读写锁。尽管如此,我们还是应该了解其实现背后的原理。
2.代码
//读写锁 jvm内置缓存
public class Test003 {
private volatile Map<String,String> caChe = new HashMap<>();
//读写锁
private ReentrantReadWriteLock rw1 = new ReentrantReadWriteLock();
//写入锁
private WriteLock writeLock = rw1.writeLock();
//读出锁
private ReadLock readLock = rw1.readLock();
//写入元素
public void put(String key,String value){
try {
writeLock.lock();
System.out.println("正在做写的操作,key:" + key + ",value:" + value + "开始.");
Thread.sleep(100);
caChe.put(key,value);
System.out.println("正在做写的操作,key:" + key + ",value:" + value + "结束.");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
writeLock.unlock();
}
}
//读取元素
public String get(String key){
try {
readLock.lock();
System.out.println("正在做读的操作,key:" + key + ",开始.");
Thread.sleep(100);
String value = caChe.get(key);
System.out.println("正在做读的操作,key:" + key + ",结束.");
return value;
} catch (InterruptedException e) {
e.printStackTrace();
return null;
}finally {
readLock.unlock();
}
}
public static void main(String[] args) {
Test003 test003 = new Test003();
//写线程
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0;i<10;i++){
test003.put("i",i+"");
}
}
});
//读线程
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0;i<10;i++){
test003.get(i+"");
}
}
});
t1.start();
t2.start();
}
}
3.结果
正在做写的操作,key:i,value:0开始.
正在做写的操作,key:i,value:0结束.
正在做写的操作,key:i,value:1开始.
正在做写的操作,key:i,value:1结束.
正在做读的操作,key:0,开始.
正在做读的操作,key:0,结束.
正在做写的操作,key:i,value:2开始.
正在做写的操作,key:i,value:2结束.
正在做写的操作,key:i,value:3开始.
正在做写的操作,key:i,value:3结束.
正在做写的操作,key:i,value:4开始.
正在做写的操作,key:i,value:4结束.
正在做读的操作,key:1,开始.
正在做读的操作,key:1,结束.
正在做写的操作,key:i,value:5开始.
正在做写的操作,key:i,value:5结束.
正在做写的操作,key:i,value:6开始.
正在做写的操作,key:i,value:6结束.
正在做写的操作,key:i,value:7开始.
正在做写的操作,key:i,value:7结束.
正在做读的操作,key:2,开始.
正在做读的操作,key:2,结束.
正在做写的操作,key:i,value:8开始.
正在做写的操作,key:i,value:8结束.
正在做写的操作,key:i,value:9开始.
正在做写的操作,key:i,value:9结束.
正在做读的操作,key:3,开始.
正在做读的操作,key:3,结束.
正在做读的操作,key:4,开始.
正在做读的操作,key:4,结束.
正在做读的操作,key:5,开始.
正在做读的操作,key:5,结束.
正在做读的操作,key:6,开始.
正在做读的操作,key:6,结束.
正在做读的操作,key:7,开始.
正在做读的操作,key:7,结束.
正在做读的操作,key:8,开始.
正在做读的操作,key:8,结束.
正在做读的操作,key:9,开始.
正在做读的操作,key:9,结束.
三、悲观锁与乐观锁
1.悲观锁
总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized的思想也是悲观锁。
2.乐观锁
2.1.总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。
version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
核心SQL语句
update table set x=x+1, version=version+1 where id=#{id} and version=#{version};
CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。
2.2.举例
举一个简单的例子: 假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。
- 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
- 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
- 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
- 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。
四、结束
Always keep the faith!!!