- 当synchronized修饰的是实例方法时,线程获取的锁是该对象的锁。当synchronized修饰的是静态方法时,线程获取的锁是该对象对应的Class对象的锁。
- 当一个对象拥有多个由synchronized修饰的是实例方法时,那么只有一个线程能够获取该对象的锁,其他的线程就会等待,注意我们实例化出来的对象一定是同一个对象。
- 案例:
public class MyThreadTest2 { public static void main(String[] args) { MyClass myClass = new MyClass(); Thread t1 = new Thread1(myClass); Thread t2 = new Thread2(myClass); t1.start(); try { Thread.sleep(700); } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); } } class MyClass { public synchronized void hello () { try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("hello"); } public synchronized void world() { System.out.println("world"); } public static synchronized void say() { System.out.println("say"); } } class Thread1 extends Thread { private MyClass myClass; public Thread1(MyClass myClass) { this.myClass = myClass; } @Override public void run() { myClass.hello(); } } class Thread2 extends Thread { private MyClass myClass; public Thread2(MyClass myClass) { this.myClass = myClass; } @Override public void run() { myClass.say(); }
运行结果:
当我们把代码修改一下:
class Thread2 extends Thread { private MyClass myClass; public Thread2(MyClass myClass) { this.myClass = myClass; } @Override public void run() { myClass.world(); } }
运行结果:
二、synchronized字节码分析使用javap 对以下代码的Class文件进行反编译后:
1.synchronized关键字对应的字节码指令是monitorenter和monitorexit,monitor翻译过来的意思是监视器,也就是我们经常说的锁。其实我们会发现Thread类中的方法很多都是由native修饰的,这些方法的底层代码是由c++编写的,说白了学到多线程的底层还是java虚拟机,线程是由我们的 *** 作系统创建和执行的。
2.为什么会出现两次monitorexit,这个是我们后面要讨论的问题?
为了防止异常情况的出现,编译器会生成一条monitorexit指令来保证线程会释放该对象的锁。下面的程序有可能正常退出,就是会执行17行的指令直接return了,还有一种情况是打印的时候出现了异常,此时就需要一个monitorexit来释放对象的锁。
public class MyTest1 { private Object object = new Object(); public void method() { synchronized (object) { System.out.println("hello world"); } } }
当我们将代码修改后然后再反编译:
此时的synchronized对应一个monitorexit关键字,因为无论是打印时出现异常还是抛出异常,此时只有一个monitorexit,所以synchronized关键字和monitorenter和monitorexit并不是一对一的关系。
public class MyTest1 { private Object object = new Object(); public void method() { synchronized (object) { System.out.println("hello world"); throw new RuntimeException("出错了"); } } }
3.JVM使用了ACC_SYNCHRONIZED访问标志来区分一个方法是否为同步方法;当方法被调用时,调用指令会检查该方法是否拥有ACC_SYNCHRONIZED标志。所以synchronized修饰方法和使用代码块的底层实现是不同的。
三、自旋对synchronized的意义synchronized底层是基于Monitor对象实现的,Monitor是依赖于底层 *** 作系统的mutex lock来实现互斥的。假如a线程和b线程同时去竞争某一对象的Monitor,当a线程拿到了Monitor时,b线程不会立即进行等待状态,它首先会进行自旋,因为进入到等待状态会涉及到用户态和内核态之间的切换。而用户态和内核态之间的切换会极大的影响锁的性能。如果a线程在很短的时间内执行完并释放了对象的锁后,b线程很可能会立即获取到该对象的锁,这样就避免了线程在用户态和内核态之间的切换。如果a线程执行的时间比较长,那么自旋就失去了意义,因为自旋它会一直占用CPU的资源。所以总体的思想是:先自旋,不成功再进行阻塞。最后解释一下用户态和内核态,
内核态:运行 *** 作系统程序, *** 作硬件。
用户态:运行用户程序。
四、synchronized锁升级的过程synchronized是基于底层 *** 作系统的Mutex Lock来实现的,每次对锁的获取与释放动作都会带来用户态与内核态之间的切换,这个才是synchronized锁升级的目的。这种锁的优化实际上是通过Java对象头中的一些标志位来去实现的;对于锁的访问与改变,实际上都与Java对象头息息相关。所以想学会多线程还是得先学会jvm。
对象头主要也是由3块内容来构成:
1. Mark Word
2. 指向类的指针
3. 数组长度
其中Mark Word(它记录了对象、锁及垃圾回收相关的信息,在64位的JVM中,其长度也是64bit)的位信息包括了如下组成部分:
1. 无锁标记
2. 偏向锁标记
3. 轻量级锁标记
4. 重量级锁标记
5. GC标记
偏向锁:针对于一个线程来说的,它的主要作用就是优化同一个线程多次获取一个锁的情况;如果一个synchronized方法被一个线程访问,那么这个方法所在的对象就会在其Mark Word中将偏向锁进行标记,同时还会有一个字段来存储该线程的ID;当这个线程再次访问同一个synchronized方法时,它会检查这个对象的Mark Word的偏向锁标记以及是否指向了其线程ID,如果是的话,那么该线程就无需再去进入管程(Monitor)了,而是直接进入到该方法体中。
轻量级锁:若第一个线程已经获取到了当前对象的锁,这时第二个线程又开始尝试争抢该对象的锁,由于该对象的锁已经被第一个线程获取到,因此它是偏向锁,而第二个线程在争抢时,会发现该对象头中的Mark Word已经是偏向锁,但里面存储的线程ID并不是自己(是第一个线程),那么它会进行CAS(Compare and Swap),从而获取到锁,这里面存在两种情况:
1. 获取锁成功:那么它会直接将Mark Word中的线程ID由第一个线程变成自己(偏向锁标记位保持不变),这样该对象依然会保持偏向锁的状态。这种情况说明竞争不是很激烈,所以还是偏向锁。
2. 获取锁失败:则表示这时可能会有多个线程同时在尝试争抢该对象的锁,那么这时偏向锁就会进行升级,升级为轻量级锁。这种情况说明锁竞争有点激烈,所以锁就由偏向锁升级为轻量级锁。
自旋锁:上面我们提到了自旋的意义,总体的思想就是:先自旋,如果失败,线程就进入等待状态。若自旋失败(依然无法获取到锁),那么锁就会转化为重量级锁,在这种情况下,无法获取到锁的线程都会进入到Monitor(即内核态)。自旋最大的一个特点就是避免了线程从用户态进入到内核态。
重量级锁:线程最终从用户态进入到了内核态。
以上就是我对synchronized的总结,欢迎交流。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)