公平锁与非公平锁、可重入锁(递归锁)、自旋锁、独占锁(写锁)、共享锁(读锁)、互斥锁

文章目录

  • 一、公平锁与非公平锁
    • 1.优先级反转
    • 2.饥饿现象
    • 3.两者特点
  • 二、可重入锁(递归锁)
    • 1.理论
    • 2.代码验证:ReentrantLock,Synchronized是可重入锁
    • 3.结论
    • 4.那么Synchronized与ReentrantLock有什么区别?
    • 5.问题:关于ReentrantLock的小问题
      • ①get()中,次数:lock()=unlock(),结果:程序正常执行完成
      • ②get()中,次数:lock()大于unlock(),结果:程序通过编译且可以运行,无法结束:由于缺少一次unlock()导致其他线程阻塞
      • ③get()中,次数:lock() 小于unlock(),结果:程序运行完成但抛出异常illegalMonitorStateException
    • 6.结论:lock次数需与unlock次数一致
  • 三、自旋锁
    • 1.理论
    • 2.代码验证
      • ①AtomicInteger实现原理CAS
      • ②自己编写一个自旋锁
    • 3.结论:自旋锁会使线程通过循环比较方式获取锁直到成功为止。
  • 四、读写锁
    • 1.理论
    • 2.代码验证:模拟一个分布式缓存进行多线程读写操作
      • 问题:发现不符合写操作的原子+独占
    • 3.结论:读写锁ReentrantReadWriteLock可以保证缓存中的读共享,写独占操作
  • 学习三板斧:理论、代码、小总结,缓冲的三大基本操作:读、写,清空


一、公平锁与非公平锁

二、可重入锁(递归锁)

三、自旋锁

四、独占锁(写锁)/共享锁(读锁)/互斥锁

一、公平锁与非公平锁

公平锁: 多个线程按照申请锁的顺序来获取锁,类似排队打饭,队列,FIFO先来后到。

非公平锁: 多个线程获取锁顺序并不是按照申请锁的顺序来获取锁,有可能后申请的线程比先申请的线程优先获取锁。在高并发情况下可能造成优先级反转或饥饿现象。抢占式,抢不到才排队。

1.优先级反转

一个线程执行完毕,本该为优先级高的2号线程执行,但由于非公平锁,使得8号线程获取到锁

2.饥饿现象

优先级低的反而频繁获取锁,造成其他线程因为没有获取到锁,而产生“饥饿”现象

3.两者特点

非公平锁吞吐量比公平锁大

ReentrantLock默认非公平锁通过构造函数参数确定它为公平true or 非公平false,Synchronized为非公平锁

二、可重入锁(递归锁)

1.理论

可重入锁(递归锁): 在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

即: 线程可以进入任何一个它已经拥有的锁所同步着的代码块

2.代码验证:ReentrantLock,Synchronized是可重入锁

/*目的:验证synchronized与ReentrantLock为可重入锁*/
//声明资源类Phone,
class Phone implements Runnable{
	@Override
    public void run() {
        get();
    }
    // sendMess()与receiveMess()验证synchronized,其中sendMess()调用了receiveMess()
    public synchronized void sendMess(){
        System.out.println(Thread.currentThread().getName()+"\tsend message!");
        receiveMess();
    }
    public synchronized void receiveMess() {
        System.out.println(Thread.currentThread().getName()+"\treceive message!");
    }
    
    // get()与set()验证ReentrantLock,其中get()调用了set()
    ReentrantLock lock = new ReentrantLock();
    public void get(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"\tget!");
            set();
        }
        finally {
            lock.unlock();
        }
    }
    public void set(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"\tset!");
        }
        finally {
            lock.unlock();
        }
    }
}
public class ReentrantLockDemo{
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        //建立两个线程调用sendMess()
        for (int i = 0; i < 2; i++) {
            new Thread(()->{
                phone.sendMess();
            },"t"+String.valueOf(i)).start();
        }
        //建立两个线程调用get()
        TimeUnit.SECONDS.sleep(2);
        System.out.println();
        System.out.println();
        System.out.println();

        Phone p1 = new Phone();
        Phone p2 = new Phone();
        Thread thread1 = new Thread(p1,"t3");
        Thread thread2 = new Thread(p2,"t4");

        thread1.start();
        thread2.start();
    }
}

结果:

公平锁与非公平锁、可重入锁(递归锁)、自旋锁、独占锁(写锁)、共享锁(读锁)、互斥锁_第1张图片
多执行几次:

公平锁与非公平锁、可重入锁(递归锁)、自旋锁、独占锁(写锁)、共享锁(读锁)、互斥锁_第2张图片
公平锁与非公平锁、可重入锁(递归锁)、自旋锁、独占锁(写锁)、共享锁(读锁)、互斥锁_第3张图片

3.结论

观察结果发现,在synchronized修饰的sendMess()中调用synchronized修饰的receiveMess()竟然是同一个线程,也就说明了synchronized是一个可重入锁;

在get()中调用set()中使用ReentrantLock加锁后竟然是同一个线程调用,也就说明了ReentrantLock也是一个可重入锁。

4.那么Synchronized与ReentrantLock有什么区别?

项目 synchronized lock
原始结构 jvm层面关键字 API层面java.util.concurrent.locks.Lock
使用方法 不需要用户手动释放锁,底层为monitorenter与monitorexit(1:2) 需要用户手动释放锁,lock()与unlock()在try catch finally中使用
等待是否可中断 不可中断,除非抛出异常或者正常运行完成 可中断,设置超时方法tryLock(Long timeout,TimeUnit unit);lockInterruptibly(),调用interrupt()可中断
加锁是否公平 非公平锁 默认为非公平锁可传入true指定为公平锁
有无绑定多个条件的condition 没有,要么随即唤醒一个线程notify(),要么唤醒全部线程notifyall() 有,可以精确唤醒需要唤醒的线程condition.await(),condition.singall()

5.问题:关于ReentrantLock的小问题

①get()中,次数:lock()=unlock(),结果:程序正常执行完成

公平锁与非公平锁、可重入锁(递归锁)、自旋锁、独占锁(写锁)、共享锁(读锁)、互斥锁_第4张图片
结果:程序正常执行

公平锁与非公平锁、可重入锁(递归锁)、自旋锁、独占锁(写锁)、共享锁(读锁)、互斥锁_第5张图片

②get()中,次数:lock()大于unlock(),结果:程序通过编译且可以运行,无法结束:由于缺少一次unlock()导致其他线程阻塞

公平锁与非公平锁、可重入锁(递归锁)、自旋锁、独占锁(写锁)、共享锁(读锁)、互斥锁_第6张图片
结果:程序通过编译且可以运行,无法结束:由于缺少一次unlock()导致其他线程无法进行

公平锁与非公平锁、可重入锁(递归锁)、自旋锁、独占锁(写锁)、共享锁(读锁)、互斥锁_第7张图片

③get()中,次数:lock() 小于unlock(),结果:程序运行完成但抛出异常illegalMonitorStateException

公平锁与非公平锁、可重入锁(递归锁)、自旋锁、独占锁(写锁)、共享锁(读锁)、互斥锁_第8张图片
结果:程序运行完成但抛出异常illegalMonitorStateException

公平锁与非公平锁、可重入锁(递归锁)、自旋锁、独占锁(写锁)、共享锁(读锁)、互斥锁_第9张图片

6.结论:lock次数需与unlock次数一致

1.次数unlock()=lock(),程序正常执行

2.次数unlock()

3.次数unlock()>lock(),程序可以通过编译,也可以正常运行,但抛出异常。

三、自旋锁

1.理论

是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。

目的: 减少线程上下文频繁的切换而引起的性能损耗,所以才自旋让当前线程一直占用资源。

好处: 循环比较获取直到成功为止,没有类似wait()的阻塞

缺点: 是循环会消耗CPU资源

2.代码验证

①AtomicInteger实现原理CAS

atomicInteger.getAndIncrement();

public final int getAndIncrement() {
        return U.getAndAddInt(this, VALUE, 1);
 }

public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        return v;
 }

②自己编写一个自旋锁

/*目的:验证自旋锁的处理机制*/
public class SpinLockDemo {
    //实现一个有关于线程的原子类引用
    AtomicReference atomicReference=new AtomicReference<Thread>();
    //线程获取锁
    public void mylock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"获得锁");
        while(!atomicReference.compareAndSet(null,thread)){
            
        }
    }
    //线程释放锁
    public void myUnlock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"释放锁");
        atomicReference.compareAndSet(thread,null);
    }

    public static void main(String[] args) throws InterruptedException {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(()->{
            spinLockDemo.mylock();
            try {
                TimeUnit.SECONDS.sleep(4);//占用锁4s
                spinLockDemo.myUnlock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"AAA").start();
        TimeUnit.SECONDS.sleep(1);
        new  Thread(()->{
            try {
                spinLockDemo.mylock();
                TimeUnit.SECONDS.sleep(4);//占用锁4s
                spinLockDemo.myUnlock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"BBB").start();
    }
}

结果

公平锁与非公平锁、可重入锁(递归锁)、自旋锁、独占锁(写锁)、共享锁(读锁)、互斥锁_第10张图片

3.结论:自旋锁会使线程通过循环比较方式获取锁直到成功为止。

四、读写锁

1.理论

独占锁(写锁):该锁一次只能被一个线程所持有

共享锁(读锁):该锁可被多个线程所持有,

互斥锁

ReentrantLock

ReentrantReadWriteLock读共享锁,写独占锁

2.代码验证:模拟一个分布式缓存进行多线程读写操作

分布式缓存模拟:map实现

缓存三大基本操作:读,写,清空

业务要求:写操作 原子+独占
整个过程必须是一个完整的统一体,不许被分割,被打断

初始代码

/*模拟一个分布式缓存读、写与清空操作*/
class MyCatch{
    //缓存模拟
    private volatile Map<String,Object> hashmap=new HashMap<>();

    public void write(String key,Object value) throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+"\t正在写入:"+key);//主要注重key的写入
        TimeUnit.MILLISECONDS.sleep(300);//模拟网络延迟拥堵等情况
        hashmap.put(key,value);
        System.out.println(Thread.currentThread().getName()+"\t写入完成");
    }

    public void get(String key) throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+"\t正在读取:"+key);//主要注重key的写入
        TimeUnit.MILLISECONDS.sleep(300);//模拟网络延迟拥堵等情况
        Object result = hashmap.get(key);
        System.out.println(Thread.currentThread().getName()+"\t读取完成,读取值为:"+result);
    }

}
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCatch myCatch = new MyCatch();
        for (int i = 1; i <= 4; i++) {
            final int tempInt=i;
            new Thread(()->{
                try {
                    myCatch.write(tempInt+"",Integer.valueOf(tempInt));
                    TimeUnit.MILLISECONDS.sleep(300);
                    myCatch.get(tempInt+"");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

结果:

公平锁与非公平锁、可重入锁(递归锁)、自旋锁、独占锁(写锁)、共享锁(读锁)、互斥锁_第11张图片
公平锁与非公平锁、可重入锁(递归锁)、自旋锁、独占锁(写锁)、共享锁(读锁)、互斥锁_第12张图片

问题:发现不符合写操作的原子+独占

我们可以用Lock,Synchronized与ReentrantReadWriteLock
但是,前两者对于高并发读写操作有很大的性能负荷,不推荐。
代码:

class MyCatch{
    //缓存模拟
    private volatile Map<String,Object> hashmap=new HashMap<>();
    ReentrantReadWriteLock rwLock=new ReentrantReadWriteLock();

    public void write(String key,Object value) throws InterruptedException {
        try {
            rwLock.writeLock().lock();
            System.out.println(Thread.currentThread().getName()+"\t正在写入:"+key);//主要注重key的写入
            TimeUnit.MILLISECONDS.sleep(300);//模拟网络延迟拥堵等情况
            hashmap.put(key,value);
            System.out.println(Thread.currentThread().getName()+"\t写入完成");
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            rwLock.writeLock().unlock();
        }

    }

    public void get(String key) throws InterruptedException {
        try {
            rwLock.readLock().lock();
            System.out.println(Thread.currentThread().getName()+"\t正在读取:"+key);//主要注重key的写入
            TimeUnit.MILLISECONDS.sleep(300);//模拟网络延迟拥堵等情况
            Object result = hashmap.get(key);
            System.out.println(Thread.currentThread().getName()+"\t读取完成,读取值为:"+result);

        } catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            rwLock.readLock().unlock();
        }
    }

}
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCatch myCatch = new MyCatch();
        for (int i = 1; i <= 4; i++) {
            final int tempInt=i;
            new Thread(()->{
                try {
                    myCatch.write(tempInt+"",Integer.valueOf(tempInt));
                    TimeUnit.MILLISECONDS.sleep(300);
                    myCatch.get(tempInt+"");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

结果:
公平锁与非公平锁、可重入锁(递归锁)、自旋锁、独占锁(写锁)、共享锁(读锁)、互斥锁_第13张图片

3.结论:读写锁ReentrantReadWriteLock可以保证缓存中的读共享,写独占操作

学习三板斧:理论、代码、小总结,缓冲的三大基本操作:读、写,清空

你可能感兴趣的:(分布式,多线程,java,并发编程)