美文网首页
Java基础-线程 (三)-锁

Java基础-线程 (三)-锁

作者: 森屿暖茶 | 来源:发表于2021-01-30 20:21 被阅读0次

    CAS是什么?

    了解CAS之前,我们先了解变量的2大特性。

    原子操作(原子性):对于操作A,要么执行完,要么完全不执行。即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

    可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

    CAS全名(Compare And Swap),字面意思就是比较并且交换。实际操作也是如此的。

    CAS是基于锁的操作,并且它是乐观锁。CAS操作。通过不断的自旋达到目的,能够在很短的时间持有和释放资源。自旋操作,可能会带来cpu的性能过分消耗(自选始终达不到目的的时候,这样就会导致无限接近于死循环)

    我们常定义的根据获取锁的方式有2种

    乐观锁:不加锁的方式通过记录比较的方式来进行操作。通俗点说就是,大家都可以操作,但是结果会根据冲突检测结果而不同。因为如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。(CAS就是乐观锁)(应用于多读场景)

    悲观锁: 总是让一个任务拿到锁,执行完毕释放锁过后其他任务才能接着拿锁执行。通俗点说就是,大家只能一个个操作,必须等前面一个拿到锁释放过后才能执行下一个。synchronized 就是悲观锁。(应用于多写场景)

    我们常定义的根据获取锁时是否先参与排队有2种

    公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。优点:所有的线程都能得到资源,不会饿死在队列中。缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。(如ReentrantLock,可自定义设置公平或非公平)

    非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。(如ReentrantLock,可自定义设置公平或非公平)

    我们常定义的根据锁的性质分类有3种:

    共享锁:线程可以同时获取锁。ReentrantReadWriteLock对于读锁是共享的。在读多写少的情况下使用共享锁会非常高效。

    重入锁:线程获取锁后可以重复执行锁区域。Java提供的锁都是可重入锁。不可重入锁非常容易导致死锁。

    排它锁:多线程不可同时获取的锁,与共享锁对立。与重入锁不矛盾可以是并存属性。

    我们常定义的根据状态划分有3种:

    偏向锁:一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。类似于乐观锁。

    轻量级锁:当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能

    重量级锁:当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

    为什么说CAS是乐观锁呢,CAS上面提到,进行的操作就是比较且进行交换。

    每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B。在进行一次CAS操作时,如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。这里看出CAS也是一个原子操作。

    由于CAS是一种乐观锁。那么在检查冲突的时候,过分简单的条件会引起ABA问题。

    什么是ABA问题?

    举个栗子。

    如果有2个线程:我们需要进行如下操作。A→B→A→C

    线程1:在执行的过程中把值从A→B→A。

    线程2:在执行的过程中,如果恰好线程1执行完,线程A进行检测的时候以为A值未发生变化,把值又变成了B。这样就造成了操作错误。

    因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际过程中操作或者成员上却发生了变化。

    如何解决ABA问题,增加检查冲突的条件。JDK5以上的atomic包里提供了解决办法。

    Atomic包提供了以下3个类。

    AtomicReference:原子更新引用类型。多个变量合并为一个变量。类似一个类内有多个变量。更改类内的多个变量。然后通过比较类这个单一变量的CAS操作。

    AtomicMarkableReference:原子更新带有标记位的引用类型。记录改没改过

    AtomicStampedReference:版本戳的形式记录了每次改变以后的版本号。记录该没改过,改过几次。

    CAS只能保证一个共享变量(因为一个地址对应一个变量)的原子操作。当代码块内需要进行多个变量更改操作时,CAS就不太适应了。就需要使用其他锁的机制操作,如synchronized。

    此外需要注意,CAS对cpu的开销可能会很大,因为他是一种自旋操作。自旋操作可以无限接近于死循环。

    Volatile:可以把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步.一般是共享变量在多线程中的使用,让每个线程能够及时了解共享变量的变化。(只能保证可见性,并不能保证原子性,详见注意),应用场景:单线程写,多线程读。

    注意:volatile对于变量读/写具有原子性具有原子性,但是类似于volatile++复合操作不具有原子性,可能会导致多线程拿到的共享变量不一致。

    Synchronized:可用于修饰类、方法、代码块的同步锁,只有获取锁的线程能够执行,其他尝试获取锁的线程均被阻塞。是重量级锁。(可重入,依赖JVM实现)(非公平锁)(保证了可见性和原子性),应用场景:多线性读写。

    sychronied修饰普通方法对象锁,同一对象的对象锁是干扰的,不同对象实例的对象锁是互不干扰的

    sychronied修饰静态方法或类类锁,由于类只有一个(一个class对象),类锁是互相干扰的。

    可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

    ReentrantLock:可重入锁,自己实现。(可自定义设置公平或非公平)。

    PS:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。

    上面有了解到根据锁的状态可以划分为3种,这里加上无锁状态的也可以称锁有四种状态:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。

    锁的状态是存放在对象头中的。对象头的状态可能随着对象在运行过重发生改变。即锁的状态可能会发生变化。所以引入了锁的这4种状态。

    锁状态: 无锁、偏向锁、轻量级锁、重量级锁。

    无锁:不锁住资源,多个线程只有一个能修改资源成功,其他线程会重试。

    偏向锁:它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。单线程场景。(应用场景:几乎无竞争的情况下。)ps:同一线程执行同步资源时自动获取。

    偏向锁→轻量级锁:在大多数情况下一个锁是同一线程使用的,在线程拿锁的过程中不进行CAS操作只简单检查上个拿锁线程是否是自己(因为偏向锁对象头有线程ID),直接给偏向锁线程。

    轻量级锁:轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。(应用场景:轻度竞争的情况下。比如临界访问。 ps:多线程争夺同步资源时,没拿到锁的线程自旋等待资源锁释放。

    轻量级→重量级:假如前一个拿锁的线程运行很快,后一个轻量级锁线程就使用CAS操作自旋操作几次,拿到锁即成功,否则锁撤销并升级为重量级锁。(可看出有锁撤销的额外操作,一般使用轻量级锁最多。)

    重量级锁:仅持有锁的对象能够运行,其他竞争对象均阻塞。(应用场景:多线程竞争激烈。)ps:多线程竞争同步资源时,没拿到锁的线程阻塞等待被唤醒。

    synchronized锁优化引用了这些状态变化。优化性能(因为上下文切换性能损耗远大于运算,锁优化的核心思想就是尽量避免上下文切换进行操作)。synchronized是JVM层次实现的,已做过大量优化,官方推荐的形式。

    相关文章

      网友评论

          本文标题:Java基础-线程 (三)-锁

          本文链接:https://www.haomeiwen.com/subject/yobtnktx.html