Java的锁机制:synchronized和CAS详解

目录
  • 一 为什么要用锁
  • 二 synchronized怎么实现的
  • 三 CAS来者何人
  • 四synchronized和CAS孰优孰劣
    • 轻量级锁
    • 重量级锁
  • 总结

提到Java的知识点一定会有多线程,JDK版本不断的更迭很多新的概念和方法也都响应提出,但是多线程和线程安全一直是一个重要的关注点。比如说我们一入门就学习的synchronized怎么个实现和原理,还有总是被提到的CAS是啥,他和synchronized关系是啥?这里大概会让你对这些东西有一个认识。

一 为什么要用锁

我们使用多线程肯定是为了提高效率,压榨硬件的性能提高效率,假设多一个线程相当于多一个人干活,但是有时候人多了就不是很好管理,可能出现问题。

比如我现在搞一个多线程的demo,我的本意是每个线程都高呼“ZPNB!”,我写下了如下的代码。

public class ThreadDemo implements Runnable{
    @Test
    public void testThread() {
        System.out.println("大声告诉我:");
        ThreadDemo demo =  new ThreadDemo();
        Thread threadOne = new Thread(demo,"张三:ZPNB");
        Thread threadTwo = new Thread(demo,"李四:ZPNB");
        Thread threadThree = new Thread(demo,"王二麻子:ZPNB");
        Thread threadFour = new Thread(demo,"赵四:ZPNB");
        threadOne.start();
        threadTwo.start();
        threadThree.start();
        threadFour.start();
    }
    @Override
    public void run() {
//        synchronized (this){
            for( int i = 0; i < 10 ;i++  ){
                try {
                    System.out.println(Thread.currentThread().getName());
                    //这里设置0是因为Junit的限制你设置长了,他就执行一段时间就不执行了
                    Thread.sleep(0);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
//        }
    }

没有加锁的情况是这样的,看起来很乱我希望他们每个人都喊十遍然后下一个人,显然下面的结果不满意

大声告诉我:
李四:ZPNB
张三:ZPNB
李四:ZPNB
张三:ZPNB
李四:ZPNB
张三:ZPNB
张三:ZPNB
李四:ZPNB
张三:ZPNB
李四:ZPNB
张三:ZPNB
李四:ZPNB
张三:ZPNB
李四:ZPNB
张三:ZPNB
李四:ZPNB
张三:ZPNB
李四:ZPNB
张三:ZPNB
李四:ZPNB
王二麻子:ZPNB
赵四:ZPNB
王二麻子:ZPNB
赵四:ZPNB
王二麻子:ZPNB
赵四:ZPNB
赵四:ZPNB
赵四:ZPNB
王二麻子:ZPNB
王二麻子:ZPNB
赵四:ZPNB
王二麻子:ZPNB
赵四:ZPNB
王二麻子:ZPNB
赵四:ZPNB
王二麻子:ZPNB
赵四:ZPNB
王二麻子:ZPNB
赵四:ZPNB
王二麻子:ZPNB

但是如果我把synchronized的注释取消就变成了我想要的依次每人喊十遍

大声告诉我:
张三:ZPNB
张三:ZPNB
张三:ZPNB
张三:ZPNB
张三:ZPNB
张三:ZPNB
张三:ZPNB
张三:ZPNB
张三:ZPNB
张三:ZPNB
赵四:ZPNB
赵四:ZPNB
赵四:ZPNB
赵四:ZPNB
赵四:ZPNB
赵四:ZPNB
赵四:ZPNB
赵四:ZPNB
赵四:ZPNB
赵四:ZPNB
王二麻子:ZPNB
王二麻子:ZPNB
王二麻子:ZPNB
王二麻子:ZPNB
王二麻子:ZPNB
王二麻子:ZPNB
王二麻子:ZPNB
王二麻子:ZPNB
王二麻子:ZPNB
王二麻子:ZPNB
李四:ZPNB
李四:ZPNB
李四:ZPNB
李四:ZPNB
李四:ZPNB
李四:ZPNB
李四:ZPNB
李四:ZPNB
李四:ZPNB
李四:ZPNB

这就突出了锁的重要性,我们希望有些线程能按照我们希望的一个顺序依次来执行,而不是先到先得的。

二 synchronized怎么实现的

实际上每一个对象实际都拥有一个叫做监视器monitor的东西,线程只有获得了这个监视器才能才能进入同步块和同步方法,如果没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,具体过程如下图:

那如果没获取到监视器怎么办,有个同步队列的东西,你没得到监视器就等一等,等上一个获取监视器的exit推出监视器你再根据队列顺序去再获取,当然可能在这个再获取的过程碰到一个“新来的”没进队列直接跟你抢,你还没抢过,那你就还要重复之前的等待过程。

其实这里还涉及一个锁的“happen before”的概念(“ A hapen-bfore B,那么 A 的结果对 B 是可见的”),就是上一个线程如果对某些值有改写,后一个应该在这个基础上改写的原则,假设一个计算程序,值都改了,新的线程你还在拿原先的值再去计算是不对的,应该是在新的值上面再去做操作,这样多线程协作才有实际意义。

以下是关于synchronized作用范围(基本是实际对象或者是类对象,如果你是类对象的话,那你new多少个实例对象还是被锁的。)

三 CAS来者何人

CAS突然这个概念出来作为线程安全的一个实现方式出现,那它和synchronized是一个什么样的关系呢?

实际二者应该是同级的概念,大家都是锁,synchronized是悲观锁,基本就是来一个线程就是锁起来,阻塞同步的。认为任何操作都有可能是冲突,所以按照最坏的情况来处理,线程竞争阻塞了就阻塞,阻塞结束了就唤醒阻塞的进程。

CAS就是compare and swap ,不是直接锁起来,大概意思就是:
CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程

CAS对于线程竞争冲突的情况相对来说就温柔一些,他会有自己的重试机制,就是这次不行我等一会再去看看,而不是直接阻塞挂起再唤醒的状态,这样太耗费时间了。

在Java.util,ConCurrent包里面很多都是用CAS来处理同步的问题,而不是直接来个synchronized来修饰。

四synchronized和CAS孰优孰劣

实际上现在来看,还真不好说,因为在CAS的方案提出,实际上synchronized也是不断的进步的。不能说CAS一定比synchronized好。

比如说CAS也会有自己的问题,最主要的有:ABA,自旋时间过长和只能保证一个共享变量的原子操作,虽然说都要相关的解决方案:

(1)ABA就是两个线程第一个线程将最开始的A值改成B再改成A,第二个线程接手直接CAS,会得不到之前的转换的过程,解决方式跟数据库一样加一个版本号1A 2B 3C解决。

(2)自旋时间过长就是线程竞争冲突,不停地重试,实际是一个循环操作,这个循环可能要等好长时间,导致所谓的自旋时间过长。

(3)只能操作一个共享原子,就让这个原子变成一个对象,把要共享的都塞进去。

synchronized自身也在不断地优化自身,甚至也借鉴了CAS的思想在1.6里面。为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。

偏向锁(通过线程ID来看对象头和栈帧里面查找线程ID(记录的线程ID就是偏向的线程ID),有就获取没有就尝试CAS设置自己为偏向的线程)

具体如下:

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

轻量级锁

(替换锁的指针替换成就获得锁,替换不成就自旋循环去找机会替换)

具体如下:

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

重量级锁

(monitor监视器锁的实现,最重的一步,因为涉及到用户态和系统态切换。)

重量级锁是依赖对象内部的monitor锁来实现。当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,需要从用户态转换到内核态,而转换状态是需要消耗很多时间。

这么看来synchronized并不是那么不堪,未必你用CAS实现的就一定在某些环境比synchronized这个“元老”强。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • Java CAS基本实现原理代码实例解析

    一.前言 了解CAS,首先要清楚JUC,那么什么是JUC呢?JUC就是java.util.concurrent包的简称.它有核心就是CAS与AQS.CAS是java.util.concurrent.atomic包的基础,如AtomicInteger.AtomicBoolean.AtomicLong等等类都是基于CAS. 什么是CAS呢?全称Compare And Swap,比较并交换.CAS有三个操作数,内存值V,旧的预期值E,要修改的新值N.当且仅当预期值E和内存值V相同时,将内存值V修改为N

  • Java synchronized最细讲解

    目录 前言 Synchronization实现原理 先理解Java对象头与Monitor 1.对象头:锁的类型和状态和对象头的Mark Word息息相关: jdk6 之后做了改进,引入了偏向锁和轻量级锁: 1.无锁到偏向锁转化的过程 2.偏向锁升级轻量级 3.轻量级到重量级 总结 前言 线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据. 因此为了解决这个问题,我们可能需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在

  • Java CAS机制的一些理解

    多线程实践 public class test { private static int x; public static void main(String[] args) throws InterruptedException { Thread task1 = new Thread(){ @Override public void run() { super.run(); for (int i=0; i<1000; i++){ x=x+1; } } }; Thread task2 = new

  • 关于Java 并发的 CAS

    目录 一.为什么要无锁 二.什么是CAS? 三.Java 中的CAS 四.CAS存在的问题 1.自旋的劣势 2.ABA 问题 3.尝试应用 4.CAS 源码 一.为什么要无锁 我们一想到在多线程下保证安全的方式头一个要拎出来的肯定是锁,不管从硬件.操作系统层面都或多或少在使用锁.锁有什么缺点吗?当然有了,不然 JDK 里为什么出现那么多各式各样的锁,就是因为每一种锁都有其优劣势. 使用锁就需要获得锁.释放锁,CPU 需要通过上下文切换和调度管理来进行这个操作,对于一个 「独占锁」 而言一个线程在

  • java synchronized 锁机制原理详解

    目录 前言: 1.synchronized 的作用: 2.synchronized 底层语义原理: 3. synchronized 的显式同步与隐式同步: 3.1.synchronized 代码块底层原理: 3.2.synchronized 方法底层原理: 4.JVM 对 synchronized 锁的优化: 4.1.锁升级:偏向锁->轻量级锁->自旋锁->重量级锁 4.1.1.synchronized 的 Mark word 标志位: 4.1.2.锁升级过程: 4.2.锁消除: 4.3

  • java synchronized的用法及原理详解

    目录 为什么要用synchronized 使用方式 字节码语义 对象锁(monitor) 锁升级过程 为什么要用synchronized 相信大家对于这个问题一定都有自己的答案,这里我还是要啰嗦一下,我们来看下面这段车站售票的代码: /** * 车站开两个窗口同时售票 */ public class TicketDemo { public static void main(String[] args) { TrainStation station = new TrainStation(); //

  • Java的锁机制:synchronized和CAS详解

    目录 一 为什么要用锁 二 synchronized怎么实现的 三 CAS来者何人 四synchronized和CAS孰优孰劣 轻量级锁 重量级锁 总结 提到Java的知识点一定会有多线程,JDK版本不断的更迭很多新的概念和方法也都响应提出,但是多线程和线程安全一直是一个重要的关注点.比如说我们一入门就学习的synchronized怎么个实现和原理,还有总是被提到的CAS是啥,他和synchronized关系是啥?这里大概会让你对这些东西有一个认识. 一 为什么要用锁 我们使用多线程肯定是为了提

  • java中注解机制及其原理的详解

    java中注解机制及其原理的详解 什么是注解 注解也叫元数据,例如我们常见的@Override和@Deprecated,注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包.类.接口.字段.方法参数.局部变量等进行注解.它主要的作用有以下四方面: 生成文档,通过代码里标识的元数据生成javadoc文档. 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证. 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码. 运行时动态处理,运行时通过代码里标识

  • java ConcurrentHashMap锁分段技术及原理详解

    一.背景: 线程不安全的HashMap 因为多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap. 效率低下的HashTable容器 HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下.因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态.如线程1使用put进行添加元素,线程2不但不能

  • java无锁hashmap原理与实现详解

    java多线程环境中应用HashMap,主要有以下几种选择:使用线程安全的java.util.Hashtable作为替代​使用java.util.Collections.synchronizedMap方法,将已有的HashMap对象包装为线程安全的.使用java.util.concurrent.ConcurrentHashMap类作为替代,它具有非常好的性能.而以上几种方法在实现的具体细节上,都或多或少地用到了互斥锁.互斥锁会造成线程阻塞,降低运行效率,并有可能产生死锁.优先级翻转等一系列问题.

  • Java并发编程总结——慎用CAS详解

    一.CAS和synchronized适用场景 1.对于资源竞争较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源:而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能. 2.对于资源竞争严重的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized.以java.util.concurrent.atomic包中AtomicInteger类为例,其getAn

  • Java中synchronized实现原理详解

    记得刚刚开始学习Java的时候,一遇到多线程情况就是synchronized,相对于当时的我们来说synchronized是这么的神奇而又强大,那个时候我们赋予它一个名字"同步",也成为了我们解决多线程情况的百试不爽的良药.但是,随着我们学习的进行我们知道synchronized是一个重量级锁,相对于Lock,它会显得那么笨重,以至于我们认为它不是那么的高效而慢慢摒弃它. 诚然,随着Javs SE 1.6对synchronized进行的各种优化后,synchronized并不会显得那么

  • java并发编程之cas详解

    CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值.这听起来可能有一点复杂但是实际上你理解之后发现很简单,接下来,让我们跟深入的了解一下这项技术. CAS的使用场景 在程序和算法中一个经常出现的模式就是"check and act"模式.先检查后操作模式发生在代码中首先检查一个变量的值,然后再基于这个值做一些操作.下面是一个

  • Java线程之线程同步synchronized和volatile详解

    上篇通过一个简单的例子说明了线程安全与不安全,在例子中不安全的情况下输出的结果恰好是逐个递增的(其实是巧合,多运行几次,会产生不同的输出结果),为什么会产生这样的结果呢,因为建立的Count对象是线程共享的,一个线程改变了其成员变量num值,下一个线程正巧读到了修改后的num,所以会递增输出. 要说明线程同步问题首先要说明Java线程的两个特性,可见性和有序性.多个线程之间是不能直接传递数据交互的,它们之间的交互只能通过共享变量来实现.拿上篇博文中的例子来说明,在多个线程之间共享了Count类的

  • Java Synchronized的使用详解

    1.为什么要使用synchronized 在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据.关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile. 2.实现原理 synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性 3.synchronized的三种应

  • Java锁擦除与锁粗化概念和使用详解

    目录 一.什么是锁擦除 二.锁擦除的演示 三.什么是锁粗化 四.锁粗化的演示 一.什么是锁擦除 锁擦除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行擦除.锁擦除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行. 二.锁擦除的演示 public class LockErasureDemo { publi

随机推荐