1.synchronized底层实现原理
互斥锁的特性:
互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同- -时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称为操作的原子性。
可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。
synchronized锁的不是代码,锁的都是对象
根据获取的锁的分类:获取对象锁和获取类锁
获取对象锁的两种用法
1.同步代码块 ( synchronized (this) , synchronized (类实例对象)) ,锁是小括号()中的实例对象。
2.同步非静态方法 ( synchronized method ) , 锁是当前对象的实例对象。
线程安全问题的主要诱因
➢存在共享数据(也称临界资源)
➢存在多条线程共同操作这些共享数据
解决问题的根本方法:
同一时刻有 且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作
对象锁和类锁的总结:
1.有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块;
2. 若锁住的是同一个对象, -个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞;
3.若锁住的是同- 一个对象, -个线程在访问对象的同步方法时,另一个访问对象同步方法的线程会被阻塞;
4.若锁住的是同-个对象, -个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然;5.同一个类 的不同对象的对象锁互不干扰 ;
6.类锁由于也是一种特殊的对象锁,因此表现和上述1,2,3,4一致,而由于一个类只有一 把对象锁,所以同-一个类的不同对象使 用类锁将会是同步的;
7.类锁和对象锁互不干扰。
再来说说实现synchronized的基础:
> Java对象头
> Monitor
再来说说对象在内存中的布局
➢对象头
➢实例数据
➢对齐填充
看看对象头结构:
几个小问题:
1.什么是锁的重入?
从互斥锁的设计上来说,当-个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入
2.为什么会对synchronized嗤之以鼻
➢早期版本中, synchronized属于重量级锁,依赖于Mutex Lock实现
➢线程之间的切换需要从用户态转换到核心态,开销较大
补充:在Java6以后, synchronized性能得到了很大的提升-------几种锁
◆Adaptive Spinning ◆ Lightweight Locking
◆Lock Eliminate ◆Biased Locking
◆Lock Coarsening 。。
讲讲自旋锁与自适应自旋锁
自旋锁 PreBlockSpin
➢许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得
➢通过让线程执行忙循环等待锁的释放,不让出CPU--------while循环
➢缺点:若锁被其他线程长时间占用,会带来许多性能上的开销
自适应自旋锁
➢自旋的次数不再固定
➢由前一-次在同一个锁上的自旋时间及锁的拥有者的状态来决定
锁消除:更彻底的优化
➢JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁
锁粗化:另-种极端
➢通过扩大加锁的范围,避免反复加锁和解锁
StringBuffer是线程安全的 其append方法会加锁 由于这是一个循环重复的操作 加锁的范围会扩大 只加一次锁
synchronized底层实现原理
synchronized的四种状态
➢无锁、偏向锁、轻量级锁、重量级锁
锁膨胀方向:无锁→偏向锁→轻量级锁→重量级锁
1.偏向锁:减少同一线程获取锁的代价 CAS ( Compare And Swap)
➢大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得
核心思想:
如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,只需要检查Mark Word的锁标记位为偏向锁以及当前线程Id等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。
*不适用于锁竞争比较激烈的多线程场合
2.轻量级锁
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
适应的场景:线程交替执行同步块
若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
**讲讲锁的内存语义:
当线程释放锁时,Java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中;而当线程获取锁时, Java内存模型会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
重点:synchronized和ReentrantLock的区别
先说说ReentrantLock (再入锁)
➢位于java.util.concurrent.locks包
➢和CountDownLatch、Future Task、Semaphore- 样基于AQS实现➢能够实现比synchronized更细粒度的控制,如控制 fairness
➢调用lock()之后,必须调用unlock()释放锁
➢性能未必比synchronized高,并且也是可重入的
ReentrantLock公平性的设置
➢ReentrantLock fairLock = new ReentrantL ock(true);
➢参数为true时 ,倾向于将锁赋予等待时间最久的线程
➢公平锁:获取锁的顺序按先后调用lock方法的顺序(慎用)
➢非公平锁:抢占的顺序不- -定,看运气
➢synchronized是非公平锁
ReentrantLock将锁对象化
➢判断是否有线程,或者某个特定线程,在排队等待获取锁
➢带超时的获取锁的尝试
➢感知有没有成功获取锁
总结:
➢ReentrantLock是类,synchronized是关键字
➢ReentrantLock可以对获取锁的等待时间进行设置,避免死锁
➢ReentrantLock可以获取各种锁的信息
➢ReentrantLock可以灵活地实现多路通知
➢机制区别:sync操作时Mark Word ,lock调用Unsafe类的park()方法
*什么是Java内存模型中的happens-before?
1Java内存模型JMM
Java内存模型(即Java Memory Model ,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
2.JMM中的主内存和工作内存
(1)JMM中的工作内存
➢存储当前方法的所有本地变量信息,本地变量对其他线程不可见
➢字节码行号指示器、Native方法信息
➢属于线程私有数据区域,不存在线程安全问题
(2)主内存与工作内存的数据存储类型以及操作方式归纳
➢方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构中
➢引用类型的本地变量:引用存储在工作内存中,实例存储在主内存中
➢成员变量、static变量、 类信息均会被存储在主内存中
➢主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新回主内存
3.JMM如何解决可见性问题
(1)指令重排序需要满足的条件
➢在单线程环境下不能改变程序运行的结果
➢存在数据依赖关系的不允许重排序
无法通过happens-before原则推导出来的,才能进行指令的重排序
(2)happens-before的八大原则
1程序次序规则: -个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
2.锁定规则: -个unLock操作先行发生于后面对同一个锁的lock操作;
3. volatile变量规则:对一一个变量的写操作先行发生于后面对这个变量的读操作;
4.传递规则:如果操作A先行发生于操作B ,而操作B又先行发生于操作C ,则可以得出操作A先行发生于操作C ;
5.线程启动规则: Thread对象的start()方法先行发生于此线程的每一个动作;6.线程中断规则:对线程interrupt()方法的调用先 行发生于被中断线程的代码检测到中断事件的发生;
7.线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Threadjoin0方法结束、Thread.isAlive()的 返回值手段检测到线程已经终止执行;
8.对象终结规则: 一个对象的初始化完成先行发生于他的finalize(方法的开始;
(3)happens-before的概念
如果两个操作不满足上述任意一个happens-before规则 , 那么这两个操作就没有顺序的保障, JVM可以对这两个操作进行重排序;
如果操作A happens-before操作B ,那么操作A在内存.上所做的操作对操作B都是可见的。
(4)volatile 关键字: JVM提供的轻量级同步机制
➢保证被volatile修饰的共享变量对所有线程总是可见的
➢禁止指令重排序优化
(5)volatile变量为何立即可见?
当写一个volatile变量时, JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中;
当读取一个volatile变量时 , JMM会把该线程对应的工作内存置为无效。
(6)volatile如何禁止重排优化?
内存屏障( Memory Barrier )概念
1.保证特定操作的执行顺序
2.保证某些变量的内存可见性
方法:通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化
强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本
volatile和synchronized的区别?
1. volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变 量, 只有当前线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止
2. volatile仅能使用在变量级别; synchronized则可以使用在变量、方法和类级别
3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量修改的可见性和原子性
4. volatile不会造成线程的阻塞; synchronized可能会造成线程的阻塞
5. volatile标记的变量不会被编译器优化; synchronized标记的变量可以被编译器优化
CAS ( Compare and Swap)
1.一种高效实现线程安全性的方法
➢支持原子更新操作,适用于计数器,序列发生器等场景
➢属于乐观锁机制,号称lock-free
➢CAS操作失败时由开发者决定是继续尝试,还是执行别的操作
2.CAS多数情况下对开发者来说是透明的
➢JUC的atomic包提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选
➢Unsafe类虽提供CAS服务,但因能够操纵任意内存地址读写而有隐患
➢Java9以后,可以使用Variable Handle API来替代Unsafe
3.缺点
➢若循环时间长,则开销很大
➢只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个方法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a合并一下ij=2a,然后用CAS来操作ij。从java1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
➢ABA问题
解决:
(1)给数据加版本号(每次变量更新的时候版本号加1,那么A->B->A就会变成1A->2B->3A)
(2)AtomicStampedReference:这个类中的compareAndSet方法的作用就是首先检查当前引用是否等于预期引用,并 且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值更新为指定的新值