目录
一、synchronized的特性
1.1 原子性
1.2 可见性
1.3 有序性
1.4 可重入性
二、synchronized的使用
2.1 修饰普通方法
2.2 修饰静态方法
2.3 修饰代码块
三、synchronized的锁机制
3.1 偏向锁
3.2 轻量级锁
3.3 重量级锁
一、synchronized的特性
1.1 原子性
原子性是指一个操作或者多个操作,要么全部执行并且在执行的过程中不会被任何因素打断,要么就都不执行.
一条Java语句不一定是原子的,也不一定是一条指令,Java中对基本数据类型的变量读取和赋值操作都是原子性操作,也就是说这些操作是不可中断的,要么执行,要么不执行,像++,+=等操作符就不是原子的,他们是分成读取 计算 赋值几步操作的,有可能这些步骤还未完成就被赋值了,这就是脏读问题,无法保证原子性.
被synchronized
修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放。
1.2 可见性
可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。 synchronized和volatile都具有可见性,其中synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到共享内存当中,保证资源变量的可见性
1.3 有序性
有序性程序执行的顺序按照代码先后执行 ,synchronized和volatile都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。
1.4 可重入性
synchronized和ReentrantLock都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。
- 既是悲观锁,也是乐观锁,开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
- 既是轻量级锁,也是重量级锁(自适应),开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
- 轻量级锁部分基于自旋锁实现,重量级锁部分基于挂起等待锁来实现
- 不是读写锁
- 是非公平锁
- 是可重入锁
二、synchronized的使用
2.1 修饰普通方法
多线程环境下,每次只能有一个线程访问该方法。
public synchronized void func() {
//代码
}
2.2 修饰静态方法
就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
synchronized void staic Func() {
//代码
}
2.3 修饰代码块
this 代表当前实例对象,锁住的是当前实例:
synchronized(this) {
for (int i = 0; i < 10; i++) {
//代码
}
}
.class锁住的是当前类的 class 对象锁:
synchronized(Main.class) {
for (int i = 0; i < 10; i++) {
//代码
}
}
synchronized
关键字加到static
静态方法和synchronized(class)
代码块上都是是给 Class 类上锁。
synchronized
关键字加到实例方法上是给对象实例上锁。
三、synchronized的锁机制
从JDK5引入了现代操作系统新增加的CAS原子操作( JDK5中并没有对synchronized关键字做优化,而是体现在J.U.C中,所以在该版本concurrent包有更好的性能 ),从JDK6开始,就对synchronized的实现机制进行了较大调整,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。由于此关键字的优化使得性能极大提高,同时语义清晰、操作简单、无需手动关闭,所以推荐在允许的情况下尽量使用此关键字,同时在性能上此关键字还有优化的空间。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
3.1 偏向锁
第一个尝试加锁的线程, 优先进入偏向锁状态.偏向锁不是真的 “加锁”, 只是给对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程.如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)
如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别 当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.偏向锁本质上相当于 “延迟加锁” . 能不加锁就不加锁, 尽量来避免不必要的加锁开销.
3.2 轻量级锁
随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).此处的轻量级锁就是通过 CAS 来实现.
通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)如果更新成功, 则认为加锁成功如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源.因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了.也就是所谓的 “自适应”
3.3 重量级锁
如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁 执行加锁操作, 先进入内核态在内核态判定当前锁是否已经被占用 如果该锁没有占用, 则加锁成功, 并切换回用户态.如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒 这个线程, 尝试重新获取锁