目录
1.悲观锁和乐观锁
1.1 什么是悲观锁和乐观锁?
(1)悲观锁
(2)乐观锁
1.2 两种锁的使用场景
1.3 乐观锁的两种实现方式
(1)版本号机制
(2)CAS
1.4 乐观锁的优缺点
(1)优点
(2)缺点
2.公平锁和非公平锁
2.1 是什么?
2.2 两者区别
3.可重入锁(递归锁)
3.1 是什么?
3.2 代码演示理解
3.3 自己手写一个可重入锁
4.自旋锁
5.读写锁
- 乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,
- 悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。
- 这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人。
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,实际上,当多个线程同时操作一个共享资源时,只有一个线程会成功,那么失败的线程呢?它们不会像悲观锁一样在操作系统中挂起,而仅仅是返回,并且系统允许失败的线程重试,也允许自动放弃退出操作。乐观锁适用于多读的应用类型,这样可以提高吞吐量,
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,
乐观锁一般会使用版本号机制或CAS算法实现。
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
举一个简单的例子:
假设我们要使用sql语句执行更新余额的任务
而解决上述问题就可以使用版本号机制
CAS算法涉及到三个操作数
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
关于CAS更详细的讲解,看我的另一篇博客:https://blog.csdn.net/qq_34805255/article/details/99232549
ABA 问题是乐观锁一个常见的问题
compareAndSet 方法
就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志(这个预期标志就是相当于上述的版本号),如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。2.循环时间长开销大
3.只能保证一个共享变量的原子操作
AtomicReference类
来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类
把多个共享变量合并成一个共享变量来操作。ReetranLock调用默认构造方法是非公平锁,只有在构造方法中传入true才是公平锁
注意:
其实关于加锁的内外层方法的调用有如下几种情况:
情况1:内外层方法都加锁
public synchronized void method01(){
//...
method02();
//...
}
public synchronized void method02(){
//...
}
method01在执行method02的时候自动获得它所得到的锁
情况2:内层方法加锁,外层方法不加锁,但是外层方法只调用了加锁了的方法,没有其他代码
public void method01(){
method02();
}
public synchronized void method02(){
//...
}
这种情况,method01即使不加锁,也因为内层它执行的代码加了锁,所以它在执行的时候还是保证了线程同步,相当于加了锁
情况3:内层方法加锁,外层方法不加锁,而且外层方法还执行了其他非同步代码
public void method01(){
//...
method02();
//...
}
public synchronized void method02(){
//...
}
这种情况下,method01线程不安全
情况4:内层方法不加锁,外层方法加锁
public synchronized void method01(){
//...
method02();
//...
}
public void method02(){
//...
method03();
//...
}
public void method03(){
//...
}
这种情况下,由于外层加了锁,所以在它当中执行的代码都是单线程的,所以在它调用method02执行的时候,也是单线程的,加了锁的,保证了线程安全,即便method02再去调用一个非同步方法method03,对method01方法的执行,仍然是线程安全的,因为synchronized对method01加了锁,它就保证了在执行method01方法中任何代码都是加锁的,单线程执行,必须执行完整个方法,才能切换CPU执行权
而可重入锁描述的就是上述的情况1
示例代码1:
public class Demo {
/**
* 验证synchronized是重入锁
*
* 如果synchronized是非重入锁的话,当线程调用a()方法时,a方法就获取到this锁了,
* 而这时在a方法中调用b方法,b方法也要获取this锁,这时候因为锁被a方法占着,它就会等待,这时就会产生死锁
*
* 而实际是下述代码能正确执行,所以synchronized是可重入锁,内层代码块可以直接再次获得外层synchronized
* 的锁,即由于a方法获得到了this锁,b方法的调用作为它的内部代码块,可以再次获得该this锁
* 对于a方法中的同步代码块,同理,它也可以自动获取到this锁,即外层获取到,内层就可以使用该锁
*/
public synchronized void a(){
System.out.println("a");
b();
synchronized (this){
System.out.println("a方法中的this锁同步代码块");
}
}
public synchronized void b(){
System.out.println("b");
}
public static void main(String[] args) {
Demo d = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
d.a();
}
}).start();
}
}
示例代码2:
public class Demo {
/**
* 当两个线程分别调用一个同步方法时,它们使用同一个锁对象,
* 会不会在一个线程的同步方法还没执行完就去执行另外一个方法?
*
*
* 不能
*/
public synchronized void a(){
System.out.println("a");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void b(){
System.out.println("b");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Demo d = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
d.a();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
d.b();
}
}).start();
}
}
示例代码3:
public class Demo {
/**
* 当两个线程分别调用一个同步方法时,它们使用不同锁对象,
* 会不会在一个线程的同步方法还没执行完就去执行另外一个方法?
*
* 可以
*/
public synchronized void a(){
System.out.println("a");
System.out.println(System.currentTimeMillis());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void b(){
System.out.println("b");
System.out.println(System.currentTimeMillis());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Demo d1 = new Demo();
Demo d2 = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
d1.a();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
d2.b();
}
}).start();
}
}
总结:
示例4:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
/**
* 验证ReetranLock是可重入锁
*
* 正常输出,所以是可重入锁
*/
Lock lock = new ReentrantLock();
public void a(){
lock.lock(); //线程可以进入任何一个它已经拥有的锁
System.out.println("a");
b(); //所同步着的代码块
lock.unlock();
}
public void b(){
lock.lock();
System.out.println("b");
lock.unlock();
}
public static void main(String[] args) {
Demo d = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
d.a();
}
}).start();
}
}
示例5:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
/**
* 验证ReetranLock是可重入锁
*
* 正常输出,所以是可重入锁
*/
Lock lock = new ReentrantLock();
public void a(){
lock.lock(); //线程可以进入任何一个它已经拥有的锁
lock.lock(); //所同步着的代码块
System.out.println("a");
lock.unlock();
lock.unlock();
}
public static void main(String[] args) {
Demo d = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
d.a();
}
}).start();
}
}
版本1:
public interface Lock {
//加锁
void lock();
//释放锁
void unlock();
}
public class MyLock implements Lock {
/**
* isLocked相当于一个锁标志,
* 为true的时候,表示有线程拥有锁,
* 为false的时候,表示没有线程拥有该锁
*/
private boolean isLocked = false;
/**
* 在lock和unlock加锁有两个作用:
* 1.为了保证这两个过程的原子性
* 2.wait和notify只能在synchronized方法或代码块中执行
*
*/
@Override
public synchronized void lock() {
/**
* 此处用while不用if的原因:
* 因为线程在wait完(即被notify唤醒以后)以后,会从wait()的下一句执行,
* if的话,整个判断直接结束,当前线程直接去修改了isLocked,
* 当有多个线程同时wait时,使用if的话,多个线程都会执行isLocked=true,这样就多个线程同时拿到了锁
* while的话,每个线程要再次判断是否锁已经被其他线程又再次持有
* 锁被释放后,第一个获得CPU执行权的线程,遇到isLocked=false,直接执行isLocked=true
*/
while(isLocked){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
isLocked = true;
}
@Override
public synchronized void unlock() {
isLocked = false;
this.notify();
}
}
public class Test {
private MyLock lock = new MyLock();
/**
* 如果加锁成功,这个方法中3条打印语句会一起输出
* 如果没有加锁,它们会交替执行
*/
public void print(){
lock.lock();
System.out.println("------------------------");
String threadName = Thread.currentThread().getName();
System.out.println(threadName+" has acquired lock!");
System.out.println(threadName+" is running!");
System.out.println(threadName+" is realsing lock!");
lock.unlock();
}
public static void main(String[] args) {
Test t = new Test();
new Thread(new Runnable() {
@Override
public void run() {
t.print();
}
},"thread-1").start();
new Thread(new Runnable() {
@Override
public void run() {
t.print();
}
},"thread-2").start();
new Thread(new Runnable() {
@Override
public void run() {
t.print();
}
},"thread-3").start();
}
}
public class Test02 {
private MyLock lock = new MyLock();
public void a(){
lock.lock();
System.out.println("a方法开始执行了");
b();
lock.unlock();
}
public void b(){
lock.lock();
System.out.println("b方法开始执行了");
lock.unlock();
}
public static void main(String[] args) {
Test02 t2 = new Test02();
new Thread(new Runnable() {
@Override
public void run() {
t2.a();
}
}).start();
}
}
下面对MyLock进行改造实现可重入
版本2:
public class MyLock implements Lock {
/**
* isLocked相当于一个锁标志,
* 为true的时候,表示有线程拥有锁,
* 为false的时候,表示没有线程拥有该锁
*/
private boolean isLocked = false;
//用于记录当前的线程
private Thread lockByThread = null;
//递归(重入)的锁的数量
private int lockCount = 0;
@Override
public synchronized void lock() {
//获取到当前的线程
Thread currentThread = Thread.currentThread();
//当锁已被其他线程占有并且当前线程不是占有锁的线程时,当前线程就需要等待
while(isLocked && currentThread != lockByThread){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lockByThread = Thread.currentThread();
isLocked = true;
lockCount++;
}
@Override
public synchronized void unlock() {
//在当前线程是占有锁的线程的时候,需要进行处理
if(lockByThread == Thread.currentThread()){
//将占有锁的数量-1,表示递归的锁的个数
lockCount --;
//只有当递归的锁的数量为0的时候,表示当前线程应该释放锁
if(lockCount == 0){
isLocked = false;
this.notify();
}
}
//在当前线程是占有锁的线程的时候,不需要进行任何处理
}
}
public class Test03 {
private MyLock lock = new MyLock();
public void a(){
lock.lock();
System.out.println("a方法开始执行了");
b();
lock.unlock();
}
public void b(){
lock.lock();
System.out.println("b方法开始执行了");
c();
lock.unlock();
}
public void c(){
lock.lock();
System.out.println("c方法开始执行了");
lock.unlock();
}
public static void main(String[] args) {
Test03 t2 = new Test03();
new Thread(new Runnable() {
@Override
public void run() {
t2.a();
}
}).start();
}
}
自己手写一个自旋锁:
import java.util.concurrent.atomic.AtomicReference;
public class SpinLockDemo {
//定义为原子引用保证了操作的原子性
private AtomicReference atomicReference = new AtomicReference<>();
public void mylock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"\t lock");
long start = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+"mylock start当前毫秒数:"+start);
while (!atomicReference.compareAndSet(null,thread)){
}
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+"mylock end当前毫秒数:"+end);
System.out.println(Thread.currentThread().getName()+"自旋时间为:"+(end-start)+"ms");
}
public void myunlock(){
System.out.println(Thread.currentThread().getName()+"myunlock start当前毫秒数:"+System.currentTimeMillis());
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName()+"\t unLock");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(()->{
spinLockDemo.mylock();
try {
Thread.sleep(5*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myunlock();
},"t1").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
spinLockDemo.mylock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myunlock();
},"t2").start();
}
}
读锁,又叫共享锁
写锁,又叫独占锁
独占锁:指该锁一次只能被一个线程所持有,
共享锁:指该锁可被多个线程所持有
即多个线程同时读一个资源没有问题,所以为了满足并发量,读取共享资源应该可以同时进行,但是如果有一个线程想去写共享资源,就不应该再有其他线程可以对该资源进行读或写
总结:
代码演示:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//模拟Redis
public class MyCathe {
private volatile Map map = new HashMap<>();
//写
public void put(String key,Object value){
System.out.println(Thread.currentThread().getName()+"\t正在写入:"+key);
//暂停一会线程
try {
TimeUnit.MICROSECONDS.sleep(300);
}catch (Exception e){
e.printStackTrace();
}
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"\t写入完成");
}
//读
public void get(String key){
System.out.println(Thread.currentThread().getName()+"\t正在读取:");
//暂停一会线程
try {
TimeUnit.MICROSECONDS.sleep(300);
}catch (Exception e){
e.printStackTrace();
}
Object result = map.get(key);
System.out.println(Thread.currentThread().getName()+"\t读取完成:"+result);
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCathe myCathe = new MyCathe();
for (int i = 0; i < 5; i++) {
int tempInt = i;
new Thread(()->{
myCathe.put(tempInt+"",tempInt+"");
},String.valueOf(i)).start();
}
for (int i = 0; i < 5; i++) {
int tempInt = i;
new Thread(()->{
myCathe.get(tempInt+"");
},String.valueOf(i)).start();
}
}
}
使用读写锁来改造程序
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
//模拟Redis
public class MyCathe {
private volatile Map map = new HashMap<>();
private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//写
public void put(String key,Object value){
reentrantReadWriteLock.writeLock().lock();
//暂停一会线程
try {
System.out.println(Thread.currentThread().getName()+"\t正在写入:"+key);
TimeUnit.MICROSECONDS.sleep(300);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"\t写入完成");
}catch (Exception e){
e.printStackTrace();
}finally {
reentrantReadWriteLock.writeLock().unlock();
}
}
//读
public void get(String key){
reentrantReadWriteLock.readLock().lock();
//暂停一会线程
try {
System.out.println(Thread.currentThread().getName()+"\t正在读取:");
TimeUnit.MICROSECONDS.sleep(300);
Object result = map.get(key);
System.out.println(Thread.currentThread().getName()+"\t读取完成:"+result);
}catch (Exception e){
e.printStackTrace();
}finally {
reentrantReadWriteLock.readLock().unlock();
}
}
}
测试程序同上