1.简介
1.1 作用
在并发场景中,保证同一时辰只要一个线程对有并发隐患的代码停止操作
1.2 错误案例
需求:两个线程对 count 变量停止200000次循环添加,预期结果是400000次
public class SynchronizedDemo implements Runnable { private static int count = 0; static SynchronizedDemo synchronizedInstance = new SynchronizedDemo(); public static void main(String[] args) { Thread t1 = new Thread(synchronizedInstance); Thread t2 = new Thread(synchronizedInstance); t1.start(); t2.start(); try { t1.join(); t2.join(); System.out.println("count 最终的值为: " + count); } catch (InterruptedException e) { e.printStackTrace(); } } @Override public void run() { synchronized (this) { for (int i = 0; i < 200000; i++) { count++; } } }}结果 :显然不等于400000次所以出现了运算错误
缘由:
count++;该语句包含三个操作:
- 线程t1、t2 从主内存中获取共享变量count的值,到本人的工作内存中将本人的工作内存中的count值停止+1操作
- 将修正完的count变量的值存入到主内存中
留意:他们是将本人工作内存中的值停止改变刷回主内存,假设当前count的值为8,t1、t2将count的值复制到本人的工作内存中停止修正,假如此时t1将count变成9、t2此时也将count的值变成9,当t1、t2两个线程都将值刷回主内存的时分count值为9,并不是10,这个时分就会形成最后的结果和预期的不分歧。
1.3 正确案例
@Overridepublic void run() { synchronized (this) { for (int i = 0; i < 200000; i++) { count++; } }}@Overridepublic synchronized void run() { for (int i = 0; i < 200000; i++) { count++; }}@Overridepublic void run() { for (int i = 0; i < 200000; i++) { synchronized (SynchronizedDemo.class) { count++; } }}输入结果:
后文详细讲解四种加 synchronized 的方式
2.用法
2.1 对象锁
2.1.1 方法锁
修饰普通方法默许锁对象为this当前实例对象
public synchronized void method() ;在普通方法下面加synchronized
public class SynchronizedDemo3 implements Runnable { static SynchronizedDemo3 synchronizedDemo3 = new SynchronizedDemo3(); public synchronized void method() { System.out.println("线程称号" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程称号" + Thread.currentThread().getName() + "运转完成"); } @Override public void run() { method(); } public static void main(String[] args) { Thread t1 = new Thread(synchronizedDemo3); t1.setName("我是线程 t1"); Thread t2 = new Thread(synchronizedDemo3); t2.setName("我是线程 t2"); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } }}输入结果: 线程 t1 和线程 t2 执行过程是顺序执行的
2.1.2 同步代码块
输入结果:线程 t1 和线程 t2 交叉执行构成了乱序
- 代码示例:加Synchronized 锁而定义的两个线程执行的状况,锁对象的是this(当前对象)
输入结果:线程 t1 和线程 t2 执行过程是顺序执行的
- 代码示例:加Synchronized 锁而定义的两个线程执行的状况,锁对象的是自定义对象
输入结果:线程 t1 和线程 t2 执行构成了顺序,这种状况下和this没有什么区别,但是假如是多个同步代码块的话就需求停止自定义对象锁了
代码示例:多个同步代码块运用自定义对象锁,(两个自定义对象锁对应两个同步代码块)
输入结果:输入顺序线程t1 和线程t2 代码停止了交叉执行,出现了乱序
代码示例:多个同步代码块运用自定义对象锁,(一个自定义对象锁对应两个同步代码块)
输入结果:线程 t1 和线程 t2 执行构成了顺序
2.2 类锁
特点:类锁只能在同一工夫被一个对象拥有(无论有多少个实例想访问也是一个对象持有它)
2.2.1 synchronized修饰静态的方法
代码示例: synchronized 加在普通方法下面
public class SynchronizedDemo4 implements Runnable { private static SynchronizedDemo4 synchronizedInstance1 = new SynchronizedDemo4(); private static SynchronizedDemo4 synchronizedInstance2 = new SynchronizedDemo4(); public synchronized void method() { System.out.println("线程称号" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程称号" + Thread.currentThread().getName() + "运转完成"); } @Override public void run() { method(); } public static void main(String[] args) { Thread t1 = new Thread(synchronizedInstance1); t1.setName("我是线程 t1"); Thread t2 = new Thread(synchronizedInstance2); t2.setName("我是线程 t2"); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } }}输入结果:输入顺序线程t1 和线程t2 代码停止了交叉执行,出现了乱序
代码示例: synchronized 加在静态方法下面
public static synchronized void method();运用方式
public class SynchronizedDemo4 implements Runnable { private static SynchronizedDemo4 synchronizedInstance1 = new SynchronizedDemo4(); private static SynchronizedDemo4 synchronizedInstance2 = new SynchronizedDemo4(); public static synchronized void method() { System.out.println("线程称号" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程称号" + Thread.currentThread().getName() + "运转完成"); } @Override public void run() { method(); } public static void main(String[] args) { Thread t1 = new Thread(synchronizedInstance1); t1.setName("我是线程 t1"); Thread t2 = new Thread(synchronizedInstance2); t2.setName("我是线程 t2"); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } }}输入结果:线程 t1 和线程 t2 执行构成了顺序
2.2.2 指定锁对象为Class对象
- 代码示例:synchronized 加.class锁
synchronized (SynchronizedDemo5.class)
public class SynchronizedDemo5 implements Runnable { private static SynchronizedDemo5 synchronizedInstance1 = new SynchronizedDemo5(); private static SynchronizedDemo5 synchronizedInstance2 = new SynchronizedDemo5(); void method() { synchronized (SynchronizedDemo5.class) { //类锁只要一把 System.out.println("线程称号" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程称号" + Thread.currentThread().getName() + "运转完成"); } } @Override public void run() { method(); } public static void main(String[] args) { Thread t1 = new Thread(synchronizedInstance1); t1.setName("我是线程 t1"); Thread t2 = new Thread(synchronizedInstance2); t2.setName("我是线程 t2"); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } }}输入结果: 线程 t1 和线程 t2 执行构成了顺序
3.性质
3.1 可重入性也叫递归锁
就是说你曾经获取了一把锁,等想要再次央求的时分不需求释放这把锁和其他线程一同竞争该锁,可以直接运用该锁
好处:避免死锁
粒度:线程而非调用
3.2案例证明可重入性
代码实例:
package synchronizedPage;public class SynchronizedDemo6 { int count = 0; public static void main(String[] args) { SynchronizedDemo6 synchronizedDemo6 = new SynchronizedDemo6(); synchronizedDemo6.method(); } private synchronized void method() { System.out.println(count); if (count == 0) { count++; method(); } }}输入结果:
代码实例:
package synchronizedPage;public class SynchronizedDemo7 { private synchronized void method1() { System.out.println("method1"); method2(); } private synchronized void method2() { System.out.println("method2"); } public static void main(String[] args) { SynchronizedDemo7 synchronizedDemo7 = new SynchronizedDemo7(); synchronizedDemo7.method1(); }}输入结果:
代码实例:
package synchronizedPage;public class SynchronizedDemo8 { public synchronized void doSomething() { System.out.println("我是父类方法"); }}class childrenClass extends SynchronizedDemo8{ public synchronized void doSomething() { System.out.println("我是子类方法"); super.doSomething(); } public static void main(String[] args) { childrenClass childrenClass = new childrenClass(); childrenClass.doSomething(); }}输入结果:
3.3 不可中缀
当A线程持有这把锁时,B线程假如也想要A线程持有的锁时只能等待,A永远不释放的话,那么B线程永远地等待下去。
4.底层原理完成
4.1 加锁和释放锁的原理
public void test() { synchronized(this){ count++; }}应用 javap -verbose 类的名字查看编译后的文件
monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定形状,线程执行monitorenter指令时尝试获取monitor的一切权,过程如下:
- 假如monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的一切者假如线程曾经占有该monitor,只是重新进入,则进入monitor的进入数加1【可重入性质】
- 假如其他线程曾经占用了monitor,则该线程进入阻塞形状,直到monitor的进入数为0,再重新尝试获取monitor的一切权
monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的一切者。指令执行时,monitor的进入数减1,假如减1后进入数为0,那线程加入monitor,不再是这个monitor的一切者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的一切权
monitorexit指令出现了两次,第1次为同步正常加入释放锁;第2次为发生异步加入释放锁
- synchronized加在方法上(无论是普通方法还是静态方法)
public synchronized void test() { count++;}应用 javap -verbose 类的名字查看编译后的文件
方法的同步并没有经过指令monitorenter和monitorexit来完成,不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来完成方法的同步的:
当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志能否被设置,假如设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行时期,其他任何线程都无法再获得同一个monitor对象,其实底层还是monitor对象锁。
5.Java虚拟机对synchronized的优化
从JDK6末尾,就对synchronized的完成机制停止了较大调整,包括运用JDK5引进的CAS自旋之外,还添加了自顺应的CAS自旋、锁消弭、锁粗化、倾向锁、轻量级锁这些优化策略。所以synchronized关键字的优化使得功能极大提高,同时语义明晰、操作简单、无需手动关闭,所以引荐在允许的状况下尽量运用此关键字,同时在功能上此关键字还有优化的空间。
5.1 锁次要存在的四种形状
无锁形状、倾向锁形状、轻量级锁形状、分量级锁形状
锁的收缩过程:
无锁形状 -> 倾向锁 -> 轻量级锁 -> 分量级锁
只能从低到高晋级,不会出现锁的降级
5.2 自旋锁
所谓自旋锁,就是指当一个线程尝试获取某个锁时,假如该锁已被其他线程占用,就不断循环检测锁能否被释放,而不是进入线程挂起或睡眠形状。(减少线程切换)
运用场景: 自旋锁适用于锁保护的临界区很小的状况,临界区很小的话,锁占用的工夫就很短。
缺陷:虽然它可以避免线程切换带来的开支,但是它占用了CPU处理器的工夫。假如持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白耗费掉处理的资源,它不会做任何有意义的工作,所以添加了顺应性自选锁
5.3 顺应性自旋锁
所谓自顺应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋工夫及锁的拥有者的形状来决议。
线程假如自旋成功了,那么下次自旋的次数会愈加多,由于上次成功了,那么此次自旋也很有能够会再次成功,那么它就会允许自旋等待持续的次数更多。反之,很少可以成功,那么当前自旋的次数会减少甚至省略掉自旋过程,以免糜费处理器资源。
5.4 锁消弭
为了保证数据的残缺性,在停止操作时需求对这部分操作停止同步控制,但是在有些状况下,JVM检测到不能够存在共享数据竞争,这是JVM会对这些同步锁停止锁消弭。作为写程序的人应该会知道哪里存在数据竞争,不能够随意的加锁。
5.5 锁粗化
将多个延续的加锁、解锁操作衔接在一同,扩展成一个范围更大的锁。虽然我们往常倡导把加锁的片段尽量小为了添加并发效率和功能。但是假如一系列的延续加锁解锁操作,能够会导致不必要的功能损耗,所以引入锁粗化。
5.6 倾向锁
在大多数状况下,锁不只不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了倾向锁。倾向锁是在单线程执行代码块时运用的机制,假如在多线程并发的环境下(即线程A尚未执行完同步代码块,线程B发起了央求锁的央求),则一定会转化为轻量级锁或者分量级锁。
引入倾向锁次要目的是:为了在没有多线程竞争的状况下尽量减少不必要的轻量级锁执行途径。由于轻量级锁的加锁解锁操作是需求依赖多次CAS原子指令的,而倾向锁只需求在置换ThreadID的时分依赖一次CAS原子指令。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁倾向的线程ID,当前该线程进入和加入同步块时不需求花费CAS操作来争夺锁资源,只需求检查能否为倾向锁、锁标识为以及ThreadID即可,处理流程如下:
- 暂停拥有倾向锁的线程
- 判别锁对象能否还处于被锁定形状,否,则恢复到无锁形状(01),以允许其他线程竞争。是,则挂起持有锁的当火线程,并将指向当火线程的锁记录地址的指针放入对象头,晋级为轻量级锁形状(00),然后恢复持有锁的当火线程,进入轻量级锁的竞争形式
倾向锁的获取和撤销流程:
5.7 轻量级锁
引入轻量级锁的次要目的是在没有多线程竞争的前提下,减少传统的分量级锁运用操作系统互斥量产生的功能耗费。当关闭倾向锁功能或者多个线程竞争倾向锁导致倾向锁晋级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:
- 在线程进入同步块时,假如同步对象锁形状为无锁形状(锁标志位为“01”形状,能否为倾向锁为“0”),虚拟机首先将在当火线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的复制。
- 拷贝对象头中的Mark Word复制到锁记录(Lock Record)中。拷贝成功后,虚拟机将运用CAS操作尝试将对象Mark Word中的Lock Word更新为指向当火线程Lock Record的指针,并将Lock record里的owner指针指向object mark word。假如更新成功,则执行步骤(4),否则执行步骤(5)。假如这个更新动作成功了,那么当火线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定形状。假如这个更新操作失败了,虚拟机首先会检查对象Mark Word中的Lock Word能否指向当火线程的栈帧,假如是,就阐明当火线程曾经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则阐明多个线程竞争锁,进入自旋执行(3),若自旋结束时仍未获得锁,轻量级锁就要收缩为分量级锁,锁标志的形状值变为“10”,Mark Word中存储的就是指向分量级锁(互斥量)的指针,当火线程以及后面等待锁的线程也要进入阻塞形状。
轻量级锁的释放也是经过CAS操作来停止的,次要步骤如下:
- 经过CAS操作尝试把线程中复制的Displaced Mark Word对象交换当前的Mark Word
- 假如交换成功,整个同步过程就完成了,恢复到无锁形状(01)
- 假如交换失败,阐明有其他线程尝试过获取该锁(此时锁已收缩),那就要在释放锁的同时,唤醒被挂起的线程
成绩:
为什么晋级为轻量锁时要把对象头里的Mark Word复制到线程栈的锁记录中呢?
由于在央求对象锁时需求以该值作为CAS的比较条件,同时在晋级到分量级锁时,能经过这个比较断定能否在持有锁的过程中此锁被其他线程央求过,假如被其他线程央求了,则在释放锁的时分要唤醒被挂起的线程。
为什么会尝试CAS不成功以及什么状况下会不成功?CAS本身是不带锁机制的,其是经过比较来操作的。假设如下场景:线程A和线程B都在对象头里的锁标识为无锁形状进入,那么如线程A先更新对象头为其锁记录指针成功之后,线程B再用CAS去更新,就会发现此时的对象头曾经不是其操作前的对象了,所以CAS会失败。也就是说,只要两个线程并发央求锁的时分会发生CAS失败。
- 此时线程B停止CAS自旋,等待对象头的锁标识重新变回无锁形状或对象头内容等于对象,这也就意味着线程A执行结束,此时线程B的CAS操作终于成功了,于是线程B获得了锁以及执行同步代码的权限。假如线程A的执行工夫较长,线程B经过若干次CAS时钟没有成功,则锁收缩为分量级锁,即线程B被挂起阻塞、等待重新调度。
5.8 分量级锁
Synchronized是经过对象外部的一个叫做监视器锁(Monitor)来完成的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来完成的。而操作系统完成线程之间的切换这就需求从用户态转换到核心态,这个成本非常高,功能耗费特别严重。因此,这种依赖于操作系统Mutex Lock所完成的锁我们称之为 “分量级锁”。
6. 缺陷
- 效率低锁的释放状况少试图获取锁时不能设定超时不能中缀一个正在试图获得锁的线程
- 不够灵敏加锁和释放锁的时分单一,每个锁仅有一个单一条件
- 不知道能否成功获取锁
|