这么多锁你都知道吗?

Posted 赵jc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了这么多锁你都知道吗?相关的知识,希望对你有一定的参考价值。

公平锁和非公平锁

公平锁

  • 定义:多个线程按照申请锁的顺序去获得锁,线程会直接进入到队列去排队,永远都是队列的第一个才会得到锁。(不管大人还是小孩,都需排队,谁在队伍前面,谁先获取锁)
  • 优点:所有的线程都能得到资源,不会饿死在队列中在
  • 缺点:吞吐量会下降许多,队列里除了第一个线程,其他线程都会阻塞,cpu唤醒阻塞线程的开销会很大
  • 实现:使用ReenTrantLock(true)实现
 Lock lock = new ReentrantLock(true);

非公平锁

  • 定义:多个线程去获取锁的时候,会直接去尝试获取(争夺似的),获取到了就直接执行相应代码,获取不到,再去等待队列中排队。
  • 优点:争抢似的获取,效率更高,可以减少CPU唤醒线程的开销
  • 缺点:有可能会导致队列中的线程一直获取不到锁导致饿了(比如队列中有小孩,和大人去争强,小孩肯定抢不过大人啦)
  • 实现:在Java语言中为了高效率,所有的锁默认都是非公平锁(synchronized和ReentrantLock()默认都是非公平锁)

乐观锁和悲观锁

乐观锁

  • 定义:乐观锁假设认为数据一般情况下不会产生并发冲突,只有在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果未发生冲突,则修改数据,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
  • 缺点:并不总是能处理所有问题,所以会引入一定的系统复杂度
  • 经典示例:CAS(Compare and swap)比较和交换

悲观锁

  • 定义:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
  • **缺点:**总是需要竞争锁,进而发生线程切换,导致效率不高
  • 经典实现:synchronized、lock等java提供得默认锁

CAS

什么是CAS?

CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B

  1. 比较 A 与 V 是否相等。(比较)
  2. 如果比较相等,将 B 写入 V(交换),如果不相等则报错
  3. 返回操作是否成功。
    当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。
    下面结合图片来理解一下
    在这里插入图片描述

CAS的实现原理是什么?

CAS在Java中是通过UnSafe类中C/C++提供的原生方法实现的,而C/C++又是通过调用操作系统的Atomic::cmpxchg原子指令)来实现的,( Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。)
简而言之,是因为硬件予以了支持,软件层面才能做到。
在这里插入图片描述

CAS的应用

Java中的Atomic*下的所有方法都是基于CAS实现的,可以保证线程的安全,下面举一个多线程下保证i++操作线程安全的例子

private static AtomicInteger count =
            new AtomicInteger(0);
    private static final int MAXSIZE = 100000;

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < MAXSIZE; i++) {
                    count.getAndIncrement(); // i++
//                    count.incrementAndGet(); // ++i
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < MAXSIZE; i++) {
                    count.getAndDecrement(); // i--
                }
            }
        });
        t2.start();

        t1.join();
        t2.join();
        System.out.println("最终结果:" + count);
    }

在这里插入图片描述

CAS中的ABA问题和解决方案

  • ABA问题是什么?

ABA 的问题,就是一个值从A变成了B又变成了A,(但此时这两个A已经不一样了)这干巴巴的概念肯定不好理解了,我们来举一个例子吧
在这里插入图片描述

  • ABA问题该如何解决

使用版本号,每次修改的时候判断预期的旧值和版本号两方面,每次修改成功之后也要更改版本号,这样即使预期的值A和V值相等,但因为版本号的不同也不能进行修改,从而解决ABA问题,例如使用AtomicStampedReference(Stamped是带邮标签的意思,方便记忆)

 private static AtomicStampedReference money =
            new AtomicStampedReference(1000, 1);

    public static void main(String[] args) throws InterruptedException {

        // 转账 -1000
           /**
         * compareAndSet()参数 旧值 新值 旧版本号 新版本号
         */
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean result = money.compareAndSet(1000, 0,
                        1, 2);
                System.out.println("线程1执行转账:" + result);
            }
        });
        t1.start();
        t1.join();

        // 账户增加了 1000
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean result = money.compareAndSet(0,
                        1000,
                        2, 3);
                System.out.println("线程3转入1000元:" + result);
            }
        });
        t3.start();
        t3.join();

        // 转账 -100
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean result = money.compareAndSet(1000,
                        0,
                        1, 2);
                System.out.println("线程2执行转账:" + result);
            }
        });
        t2.start();


    }

注意事项:
AtomicStampedReference可以解决ABA问题,但里面旧值对比的是引用而不是值这是由于Integer高速缓存的原因,数据在[-128~127]中会直接使用缓存值而不会重新new对象,可以修改jvm的参数来修改范围
在这里插入图片描述
在这里插入图片描述

synchronized

synchronized如何实现的?

Java层面:将锁标识信息放在对象头中
在这里插入图片描述

JVM层面:是基于monitor监视器锁实现的
monitor是线程私有的数据结构,每一个线程都有一个可用monitor列表,同时还有一个全局的可用列表,先来看monitor的内部
在这里插入图片描述

  • Owner:初始时为NULL表示当前没有任何线程拥有该monitor,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
  • EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor失败的线程。
  • RcThis:表示blocked或waiting在该monitor上的所有线程的个数。
  • Nest:用来实现重入锁的计数。
  • HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
  • Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值:0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。

在这里插入图片描述

操作系统层面:是基于互斥锁mutex实现的

  • 当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中
  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量

synchronized在java1.6之后有什么优化?

1.6之前锁没有分类,都为重量级锁,1.6之后,线程A创建之后先是无锁状态,之后如果得到锁了话会变成偏向锁,之后再有别的线程B来访问的话,但此时占有着锁,线程B会自旋等待,等待一段时间后,偏向锁会升级为轻量级锁,如果自旋一段时间之后还没有获得锁资源,那么就会升级为重量级锁
JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。

  • 无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。
  • 偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁, 降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁 的,只有当其他线程尝试竞争偏向锁才会被释放。
    偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;如果线程处于活动状态,升级为轻量级锁的状态。
  • 轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会 升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁 也会升级为重量级锁。
  • 重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
    重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

独占锁和共享锁

独占锁

  • 定义:一把锁只能被一个线程拥有(见名知义)
  • 经典实现:synchronized

共享锁

  • 定义:一把锁可以被多个线程同时拥有
  • 经典实现:读写锁ReadWriteLock(),读读共享,但写写互斥,读写互斥,优点(将锁的粒度更加细化,从而提高锁的性能)
public static void main(String[] args) {

        // 创建一个读写锁
        ReentrantReadWriteLock readWriteLock =
                new ReentrantReadWriteLock();
        // 得到读锁
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        // 写锁
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        // 创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                10, 10, 0, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1000));

        // 任务一:读锁演示
        executor.execute(new Runnable() {
            @Override
            public void run() {
                // 加锁
                readLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() +
                            " 进入了读锁,时间:" + new Date());
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    readLock.unlock();
                }
            }
        });

        // 任务二:读锁演示
        executor.execute(new Runnable() {
            @Override
            public void run() {
                // 加锁
                readLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() +
                            " 进入了读锁,时间:" + new Date());
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    readLock.unlock();
                }
            }
        });

        // 任务三:写锁
        executor.execute(new Runnable() {
            @Override
            public void run() {
                writeLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() +
                            " 执行了写锁,时间:" + new Date());
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    writeLock.unlock();
                }
            }
        });

        // 任务四:写锁
        executor.execute(new Runnable() {
            @Override
            public void run() {
                writeLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() +
                            " 执行了写锁,时间:" + new Date());
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    writeLock.unlock();
                }
            }
        });

    }

在这里插入图片描述

可重入锁

  • 定义:可以重新进入的锁,即允许同一个线程多次获取同一把锁。
public static void main(String[] args) {
        Object object = new Object();
        synchronized (object) {
            System.out.println("进入了方法");
            synchronized (object) {
                System.out.println("重复进入了方法");
            }
        }
    }
  • 使用场景:synchronized和ReentractLock

自旋锁

  • 定义:按之前的方式处理下,线程在抢锁失败后进入阻塞状态,放弃
    CPU,需要过很久才能再次被调度。但经过测算,实际的生活中,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。基于这个事实,自旋锁诞生了。
while (抢锁(lock) == 失败) {
		if(抢锁(lock) == 成功) {
			break;
		}
}
  • 使用场景:synchronized的升级

以上是关于这么多锁你都知道吗?的主要内容,如果未能解决你的问题,请参考以下文章

Java8用了这么久了,Stream 流用法及语法你都知道吗?

C语言关键字及进制的转换你都知道吗?

MySQL最常见错误代码及代码说明你都知道吗?

这些代码托管工具你都知道吗?

Git学习总结_06_作为一名程序员这些代码托管工具你都知道吗?

iOS底层探索之多线程(十四)—关于@synchronized锁你了解多少?