synchronized的4种应用方式
synchronized关键字最主要有以下3种应用方式,都是作用在对象上
- 修饰类,作用范围:synchronized括号内, 作用对象:类的所有对象;synchronized(Service.class){ }
- 修改静态方法,作用范围:整个静态方法, 作用对象:类的所有对象;
- 修饰方法,被修饰的同步方法,作用范围:整个方法, 作用对象:调用这个方法的对象;
- 缺点:A线程执行一个长时间任务,B线程必须等待
- 修饰代码块,被修饰的代码块同步语句块,作用范围:大括号内的代码, 作用对象:调用这个代码块的对象;
- 优点:减少锁范围,耗时的代码放外面,可以异步调用
synchronized底层 每个对象有一个监视器锁(monitor),当monitor被占用时处于锁定状态
1.线程执行monitor enter指令时尝试获取monitor的所有权:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数+1.
3、如果其他线程已经占用monitor,该线程进入阻塞状态,直到monitor的进入数为0,再尝试获取monitor的所有权
2.线程执行monitor exit指令
monitor的进入数-1,如果-1后进入数为0,线程退出monitor;其他被monitor阻塞的线程尝试获取 monitor
每个线程获得锁后,要完成变量 copy到工作内存-> 修改-> 刷新主存 的过程,才会释放它得到的锁,达到线程安全。
- 锁住(lock)
- 主->从 将需要的数据从主内存拷贝到自己的工作内存(read and load)
- 修改 根据程序流程读取或者修改相应变量值(use and assign)
- 从->主 将自己工作内存中修改了值的变量拷贝回主内存(store and write)
- 释放对象锁(unlock)
线程安全:
- 当多个线程访问某个类,其始终能表现出正确的行为
- 采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,限制其他线程访问,直到锁释放
synchronized和volatile区别 锁的目标:互斥和可见性
1、锁提供了两种主要特性:互斥性(mutual exclusion) 和可见性(visibility)。
互斥即一次只允许一个线程持有某个锁,使用该共享数据。
可见性确保新值立即同步到主存,每次使用前立即从主内存刷新
2、在Java中,为了保证多线程读写数据时保证数据的一致性,可以采用两种方式:
synchronized同步:释放锁之前会将对变量的修改刷新到主存当中;阻塞
volatile关键字:确保新值立即同步到主存,每次使用前立即从主内存刷新;非阻塞
3.区别
1)volatile非阻塞,synchronized只有当前线程可以访问修饰的变量,其他线程阻塞
2)volatile仅能修饰变量,synchronized则可以使用在变量,方法.
3)volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性(操作不可分割)
volatile详解
1.实现对所有线程的可见性
volatile保证新值立即同步到主存,每次使用前立即从主内存刷新
编译器为了加快程序运行的速度,对一些变量的写操作会先在(工作内存)寄存器或者是CPU缓存上进行,最后才写入内存,这个过程中,变量的新值对其他线程是不可见的
volatile适用场景: 某线程修改某个状态变量,通知其他线程做别的事
确保只有单一的线程修改变量的值 或 运算结果不依赖当前变量值
变量不需要与其他的状态变量共同参与不变约束
2.实现禁止指令重排序优化
- java不能保证变量赋值操作的顺序与代码中一致
- volatile修饰的变量相当于生成内存屏障,重排序时不能把后面的指令排到屏障之前
3.无法实现 i++ 原子操作
A读取 i 后,B也读取 i ,此时A进行 +1,B的 i 就变了
原子类如何解决:CAS,B在进行+1时,检查此时的 i 跟主存的 i 是否一致,一致才+1
Java中的锁优化 编码、JDK
编码 锁优化
- 减少锁持有时间
- 使用同步代码块,而非同步方法;
- 减小锁粒度
- JDK1.6中 ConcurrentHashMap采取对segment加锁而不是整个map加锁,提高并发性;
- 锁分离 读锁之间不互斥
- 根据同步操作的性质,把锁划分为的读锁和写锁,读锁之间不互斥,提高了并发性
JDK1.6 锁优化 synchronized底层
1.引入偏向锁、轻量级锁
- 锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级
- 锁可以升级不可降级,提高 获得锁和释放锁 效率
- “轻量级锁”和“偏向锁”作用:减少 获得锁和释放锁 的性能消耗
锁 | 优点 | 缺点 | 适用场景 |
偏向锁 | 记录线程iD,加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 | 适用于只有一个线程访问同步块场景。 |
轻量级锁 | 自旋方式竞争,竞争的线程不会阻塞,提高了程序的响应速度CAS尝试将头部的二进制位修改00 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 | 追求响应时间。 同步块执行速度非常快。 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU。 | 线程阻塞,响应时间缓慢。 | 追求吞吐量。 同步块执行速度较长。 |
2.锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,如循环体内,很耗性能
加锁同步的范围扩展到整个操作序列的外部:第一个append到最后一个append
3.锁消除 逃逸分析的数据的支持
编译器判断到一段代码中,堆上的数据不会逃逸出当前线程,可以认为是线程安全的,不必加锁
4.自旋与自适应自旋:想要获取锁的线程做几个空循环 10
.为什么引入:
轻量级锁失败后,线程会在操作系统层面挂起,
操作系统实现线程之间的切换时,需要从用户态转换到核心态,状态转换耗时
.解决方法:
假设不久当前的线程可以获得锁,虚拟机会让当前想要获取锁的线程做几个空循环,可能是50个循环或100循环
.结果:
如果得到锁,就顺利进入临界区;如果不能,就将线程在操作系统层面挂起,升级为重量级锁
.JDK做的优化:自适应自旋
当线程在获取轻量级锁时CAS操作失败时,通过自旋让线程等待,避免线程切换的开销
自旋是需要消耗CPU的,如果一直获取不到锁,线程一直自旋,浪费CPU资源
线程如果自旋成功了,下次自旋的次数会更多,自旋失败了,自旋的次数就会减少。
CAS底层实现原理
CAS:Compare and Swap, 翻译成比较并交换
CAS需要在:操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,最终都会返回内存地址,且是原子操作
需要3个操作数:内存地址V,旧预期值A、新值B
当且仅当V符合预期值A时(即V存储的值无变化),用B更新A,否则不执行更新,最终都返回内存地址V
死锁
多个线程因竞争资源而造成僵局(互相等待),无法向前推进
产生的原因
1) 系统资源的竞争
系统不可剥夺资源,数量不足以满足多个进程运行,使得进程在运行过程中,因竞争资源而陷入僵局
2) 进程推进顺序非法
请求和释放资源的顺序不当,也同样会导致死锁。如,互相申请各占有的资源。
信号量使用不当也会造成死锁。进程间彼此相互等待消息,结果也会使得这 些进程间无法继续向前推进。
3) 死锁产生的必要条件
产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。
- 资源互斥条件:资源互斥,即某资源仅为一个进程占有
- 资源不可剥夺条件:进程所获得的资源在未使用完毕之前,只能是主动释放,不能被其他进程强行夺走
- 保持和请求条件:进程已经保持了一个资源,又提出了新的资源请求,而该资源已被其他进程占有
- 循环等待条件:进程资源循环等待
如何避免死锁
- 加锁顺序(线程按照一定的顺序加锁)
- 按照顺序加锁是一种死锁预防机制,需要事先知道所有会用到的锁
- 加锁时限(超时则放弃)
- 获取锁时加上时限,超过时限则放弃请求,并释放锁,等待一段随机的时间再重试
- 死锁检测与恢复
- 操作系统中:系统为进程分配资源,不采取任何限制性措施,提供检测和解脱死锁的手段
死锁检测:当一个线程请求锁失败时,遍历锁的关系图检测死锁。
死锁恢复
- 撤消进程,剥夺资源
- 线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁
- 死锁发生的时候设置随机的优先级;如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。
锁模式包括:
- 共享锁:(读取)用户可以并发读取数据,但不能获取写锁,直到释放所有读锁。
- 排他锁(写锁):加上写锁后,其他线程无法加任何锁;写锁可以读和写
- 更新锁: 防止死锁而设立,转换读锁为写锁之前的准备,仅一个线程可获得更新锁
乐观锁:认为数据一般情况下不会造成冲突,在数据提交更新时,才进行数据的冲突检测;
如果冲突,返回I信息让用户决定如何去做。实现方式:记录数据版本。
悲观锁:操作数据时上锁保护,限制其他线程访问,直到该锁释放。关系型数据库锁机制,行锁、页锁、表锁,都是在做操作之前先上锁。
锁的粒度: 都是悲观锁
- 行锁: 粒度最小,并发性最高
- 页锁:锁定一页。25个行锁可升级为一个页锁。
- 表锁:粒度大,并发性低
- 数据库锁:控制整个数据库操作
重入锁 Reentrantlock
- 无阻塞的同步机制,实现了独占功能
- 加锁和解锁都需要显式写出,实现了Lock接口,注意一定要在适当时候unlock
- 添加了轮询锁、定时锁等候和可中断锁特性;
- 提供了一个Condition(条件)类,用来实现分组唤醒线程
- 默认使用非公平锁,可插队跳过对线程队列的处理
- ReentrantLock的内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync。
- 公平锁:线程获取锁的顺序和调用lock的顺序一样,FIFO;
- 非公平锁:线程获取锁的顺序和调用lock的顺序无关
公平锁为了保证线程规规矩矩地排队,需要增加阻塞和唤醒的时间开销;直接插队获取非公平锁,跳过了对队列的处理,速度会更快
不要抛弃 synchronized
- 易忘记 finally 块释放锁,对程序有害
- synchronized 管理锁定和释放时,能标识死锁或者其他异常行为的来源,利于调试
- Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多
使用场景
对锁进行更精确的控制,分组唤醒
轮询锁、定时锁、可中断锁
介绍Condition Java多线程系列--“JUC锁”06之 Condition条件
对锁进行更精确的控制
Condition中的await()方法相当于Object的wait()方法
Condition中的signal()方法相当于Object的notify()方法
Condition中的signalAll()相当于Object的notifyAll()方法
Condition函数列表
造成当前线程在接到信号或被中断之前一直处于等待状态 void await() // 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态 boolean await(long time, TimeUnit unit) // 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态 long awaitNanos(long nanosTimeout) // 造成当前线程在接到信号之前一直处于等待状态 void awaitUninterruptibly() // 造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态 boolean awaitUntil(Date deadline) // 唤醒一个等待线程 void signal() // 唤醒所有等待线程 void signalAll()
介绍AbstractQueuedSynchronizer 一个用来构建锁和同步工具的框架
- 使用int类型的volatile变量维护同步状态(state),
- 使用Node实现FIFO队列存放阻塞的等待线程,来完成线程的排队执行
- 围绕state提供两种基本操作“获取”和“释放”
- 组合AQS对象的方式实现锁的语义
AQS与锁(如Lock)的对比:
- 锁是面向使用者的,锁定义了用户调用的接口,隐藏了实现细节;
- AQS是锁的实现者,通过用AQS简化了锁的实现屏蔽了同步状态管理,线程的排队,等待唤醒的底层操作。
- 简而言之,锁是面向使用者,AQS是锁的具体实现者。