锁的分类是从不同角度出发去看的,这些分类并不是互斥得,比如ReentrantLock既是互斥锁,又是可重入锁。

脱离了使用场景就说哪种锁好或坏就是耍流氓
image.png

一、乐观锁和悲观锁

乐观锁—非互斥同步锁
悲观锁—互斥同步锁。

1.互斥同步锁-悲观锁

  1. 互斥同步锁的劣势:
  2. 1.阻塞和唤醒带来的性能劣势。
  3. 2.永久阻塞:如果持有锁的线程被永久阻塞,比如无线循环、死锁等问题,那么等待线程将永远无法获取到锁。
  4. 3.优先级反转:如果我们为线程设置了优先级,想要优先级高的先执行,但是由于优先级低的线程拿到锁以后,迟迟没有释放锁,就会导致优先级高的线程陷入等待。
  5. 悲观锁:
  6. 为了保证数据结果的正确性,每次获取并修改数据时,都将数据锁住,让别的线程无法访问,这样数据就万无一失。典型实现:LockSynchronized

数据库中的悲观锁:select for update,即查询到的结果先进行加锁以后再操作。
悲观锁使用场景:

  • 临界区有IO操作
  • 临界区代码复杂或者循环量大
  • 临界区竞争非常激烈

    2.非互斥同步锁-乐观锁

    乐观锁:
    1.乐观锁认为自己在操作数据的时候其他线程不会来干扰,所以并没有锁住对象。
    2.在更新的时候,对比在修改期间数据有没有被其他线程修改过,如果没有,就证明这段时间只有当前线程在操作数据,就进行数据更新。
    3.如果准备更新数据的时候跟我一开始拿到的数据不一致了,说明在此期间已经有其他线程对数据进行了修改,这个时候,当前线程就不能继续更新了,此时会执行放弃、报错或者重试策略。
    4.乐观锁的实现一般都是利用CAS算法来实现的
    
    乐观锁的典型实现:原子类和并发容器。git也是乐观锁。
    数据库中的乐观锁:
    image.png
    乐观锁的场景:
    适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅提高。

二、可重入锁和非可重入锁

ReentrantLock

可重入锁

synchronized 和 ReentrantLock 都是可重入锁。

概念:
    1.可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁
    2.重入锁又称递归锁,是指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提是锁对象得是同一个对象),不会因为之前已经获取过锁还没有释放而阻塞。

示例1:

public class GetHoldCount {
    private static ReentrantLock lock=new ReentrantLock();
    public static void main(String[] args) {
        //获取锁的持有数量
        int count1 = lock.getHoldCount();
        lock.tryLock();
        int count2 = lock.getHoldCount();
        lock.tryLock();
        int count3 = lock.getHoldCount();
        lock.tryLock();
        int count4 = lock.getHoldCount();
        lock.unlock();
        int uncount1 = lock.getHoldCount();
        lock.unlock();
        int uncount2 = lock.getHoldCount();
        lock.unlock();
        int uncount3 = lock.getHoldCount();
        System.out.println(count1);
        System.out.println(count2);
        System.out.println(count3);
        System.out.println(count4);
        System.out.println(uncount1);
        System.out.println(uncount2);
        System.out.println(uncount3);
    }
}
结果:
0
1
2
3
2
1
0
----
即主线程获得某个锁,可以再次获取锁而不会出现死锁,不需要对锁对象进行释放

示例2:演示可重入锁的递归

public class RecursionDemo {
    private static ReentrantLock lock=new ReentrantLock();

    public static void main(String[] args) {
        accessResource();
    }
    private static void accessResource(){
        lock.tryLock();
        try {
            int holdCount = lock.getHoldCount();
            System.out.println(holdCount);
            System.out.println("对资源进行了处理!");
            if (holdCount<5){
                accessResource();
            }

        }finally {
            lock.unlock();
        }
    }
}

源码分析:。。。。


三、公平锁和非公平锁

1.公平指的是按照线程请求的顺序,来分配锁;非公平指的是,不完全按照请求的顺序,在一定情况下,可以插队。

2.注意:非公平也同样不提倡“插队”行为,这里的非公平,指的是“在合适的时机”插队,而不是盲目插队。

3。公平锁的作用是避免唤醒带来的空档期,节约资源。

image.png
由图可以看到,当线程1释放了锁以后,线程2此时在等待队列里,理应线程2获取到锁,但此时线程正常唤醒但没有完全准备好,线程5此时就可以插队获取到锁。
代码示例:以打印机一个线程需要打印两张纸举例

public class FairLock {
    public static void main(String[] args) {
        Thread[] threads=new Thread[10];
        PrintQueue printQueue = new PrintQueue();
        for (int i = 0; i < threads.length; i++) {
            threads[i]=new Thread(new Job(printQueue));

        }
        for (int i = 0; i < 10; i++) {
            threads[i].start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


}

class Job implements Runnable{
private PrintQueue printQueue;
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"开始打印!");
        printQueue.printJob(new Object());
        System.out.println(Thread.currentThread().getName()+"结束打印!");

    }

    public Job(PrintQueue printQueue) {
        this.printQueue = printQueue;
    }
}
class PrintQueue{
    //true为公平
    private Lock lock=new ReentrantLock(true);
    public void printJob(Object document){
        lock.lock();
        try{
            int duration = (int) (Math.random() * 10)+1;
            System.out.println(Thread.currentThread().getName()+"正在打印第一张材料,打印需要"+duration+"秒");
            Thread.sleep(duration*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

        lock.lock();
        try{
            int duration = (int) (Math.random() * 10)+1;
            System.out.println(Thread.currentThread().getName()+"正在打印第二张材料,打印需要"+duration+"秒");
            Thread.sleep(duration*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

}

执行过程解析:
1.线程0启动并且第一次拿到锁,开始打印第一张材料。
2.随后所有线程全部启动进入等待队列。

Thread-0开始打印!
Thread-0正在打印第一张材料,打印需要4秒
Thread-1开始打印!
Thread-2开始打印!
Thread-3开始打印!
Thread-4开始打印!
Thread-5开始打印!
Thread-6开始打印!
Thread-7开始打印!
Thread-8开始打印!
Thread-9开始打印!
Thread-1正在打印第一张材料,打印需要3秒
Thread-2正在打印第一张材料,打印需要4秒
Thread-3正在打印第一张材料,打印需要3秒
Thread-4正在打印第一张材料,打印需要2秒
Thread-5正在打印第一张材料,打印需要1秒
Thread-6正在打印第一张材料,打印需要2秒
Thread-7正在打印第一张材料,打印需要3秒
Thread-8正在打印第一张材料,打印需要5秒
Thread-9正在打印第一张材料,打印需要4秒
Thread-0正在打印第二张材料,打印需要4秒
Thread-0结束打印!
Thread-1正在打印第二张材料,打印需要1秒
Thread-2正在打印第二张材料,打印需要1秒
Thread-1结束打印!
Thread-3正在打印第二张材料,打印需要4秒
Thread-2结束打印!
Thread-3结束打印!
Thread-4正在打印第二张材料,打印需要3秒
Thread-4结束打印!
Thread-5正在打印第二张材料,打印需要3秒
Thread-5结束打印!
Thread-6正在打印第二张材料,打印需要5秒
Thread-6结束打印!
Thread-7正在打印第二张材料,打印需要5秒
Thread-8正在打印第二张材料,打印需要2秒
Thread-7结束打印!
Thread-8结束打印!
Thread-9正在打印第二张材料,打印需要2秒
Thread-9结束打印!
-----------------------------------------------------

执行过程解析:
1.线程0启动并且第一次拿到锁,开始打印第一张材料。
2.随后所有线程全部启动进入等待队列。
3.当线程0释放锁以后按等待队列里的顺序线程依次执行,此时队列里的顺序就是1—9—0
4.1-9线程的第一张材料打印完以后,线程0此时又排在了队列的最前面,开始打印第二张材料,依次执行。

非公平锁结果:

Thread-0开始打印!
Thread-0正在打印第一张材料,打印需要4秒
Thread-1开始打印!
Thread-2开始打印!
Thread-3开始打印!
Thread-4开始打印!
Thread-5开始打印!
Thread-6开始打印!
Thread-7开始打印!
Thread-8开始打印!
Thread-9开始打印!
Thread-1正在打印第一张材料,打印需要3秒
Thread-1正在打印第二张材料,打印需要5秒
Thread-1结束打印!
Thread-2正在打印第一张材料,打印需要4秒
Thread-3正在打印第一张材料,打印需要5秒
Thread-3正在打印第二张材料,打印需要1秒
Thread-3结束打印!
Thread-4正在打印第一张材料,打印需要3秒
Thread-5正在打印第一张材料,打印需要2秒
Thread-5正在打印第二张材料,打印需要5秒
Thread-5结束打印!
Thread-6正在打印第一张材料,打印需要1秒
Thread-7正在打印第一张材料,打印需要5秒
Thread-8正在打印第一张材料,打印需要4秒
Thread-9正在打印第一张材料,打印需要3秒
Thread-9正在打印第二张材料,打印需要1秒
Thread-0正在打印第二张材料,打印需要5秒
Thread-9结束打印!
Thread-0结束打印!
Thread-2正在打印第二张材料,打印需要5秒
Thread-4正在打印第二张材料,打印需要3秒
Thread-2结束打印!
Thread-4结束打印!
Thread-6正在打印第二张材料,打印需要4秒
Thread-6结束打印!
Thread-7正在打印第二张材料,打印需要2秒
Thread-8正在打印第二张材料,打印需要4秒
Thread-7结束打印!
Thread-8结束打印!

执行过程解析:
可以看到,执行的先后取决于线程唤醒的快慢。

特例:
针对tryLock()方法,它是很猛的,它不遵守设定的公平的规则
例如,当有线程执行tryLock()的时候,一旦有线程释放,那么这个正在tryLock的线程就能获取到锁,即使在它之前已经有其他现在在等待队列里了


四、共享锁和排它锁(读写锁)

image.png
策略2:避免饥饿
image.png

锁的升降级

可以降级不能升级。

升级容易造成死锁。因为读锁会有很多线程持有,如果A线程想升级,那么就要求B和C将锁释放,但此时B也想升级,B想A和C释放读锁进行升级。。。,那么就会造成死锁问题。

共享锁和排它锁总结

1.ReentrantReadWriteLock实现了ReadWriteLock接口,最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁

2.锁申请和释放策略:
a) 多个线程只申请读锁,都可以申请到
b) 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
c) 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
d) 要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现。

3.插队策略:为了防止饥饿,读锁不能插队

4.升降级策略∶只能降级,不能升级

5.适用场合:相比于ReentrantLock适用于一般场合,ReentrantReadWriteLock适用于读多写少的情况,合理使用可以进—步提高并发效率。

五、自旋锁和阻塞锁

产生背景:

  • 阻塞或唤醒─个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。
  • 如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长
  • 在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。
  • 如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。
  • 而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
  • 阻塞锁和自旋锁相反,阻塞锁如果遇到没拿到锁的情况,会直接把线程阻塞,直到被唤醒阻塞锁和自旋锁相反,阻塞锁如果遇到没拿到锁的情况,会直接把线程阻塞,直到被唤醒

自旋锁的确定:

  • 如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。
  • 在自旋的过程中,一直消耗cpu,所以虽然自旋锁的起始开销低于悲观锁,但是随着自旋时间的增长,开销也是线性增长的

源码和原理分析:

- 在java1.5版本及以上的并发框架java.util.concurrent的atmoic包下的类基本都是自旋锁的实现。

- AtomicInteger的实现︰自旋锁的实现原理是CAS ,
AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改过程中遇到其他线程竞争导致没修改成功,就在while里死循环,直至修改成功

使用场景:

1. 自旋锁一般用于多核的服务器,在并发度不是特别高的情况,比阻塞锁的效率高。
2. 另外,自旋锁适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到锁,很久以后才会释放),那也是不合适的

可中断锁

在Java中,synchronized就不是可中断锁,而Lock是可中断锁因为tryLock(time)和lockInterruptibly都能响应中断。 如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以中断它,这种就是可中断锁

如何优化锁和提高并发性能

  • 缩小同步代码块
  • 尽量不要锁住方法
  • 减少请求锁的次数
  • 避免人为制造“热点”
  • 锁中尽量不要再包含锁
  • 选择合适的锁类型或合适的工具类