文章目录
前言
岳不群是个江湖人,而笑傲江湖中的江湖,只和选择有关。岳不群,林平之,令狐冲,都是因其选择而成其人。令狐冲若是陷入林平之的境地,也不会成为林平之,正如他若在岳不群的境地,也不会成为岳不群。
今天我们不去评判岳不群的真伪,依旧把他当作那个立志发扬气宗的君子剑吧。
一、synchronized基本认识
令狐冲:师傅,徒儿前段时间去福报厂面试了,那个黑脸面试官问我知道synchronized是啥不?了解原理吗?徒儿一时没有回答出来。。。
令狐冲:师傅能不能给徒儿讲解一下。。。
岳不群:synchronized是Java中的一个关键字,在多线程共同操作共享资源的情况下,可以保证在同一时刻只有一个线程可以对共享资源进行操作,从而实现共享资源的线程安全。
令狐冲:那师傅能说说Synchronized的特性吗?是不是也满足JMM模型?
岳不群:是的,冲儿,依旧满足原子性、可见性、有序性。这也是使用synchronized的原因。
原子性 | synchronized可以确保多线程下对共享资源的互斥访问,可以实现原子性。 |
---|---|
可见性 | synchronized保证对共享资源的修改能够及时被看见 |
有序性 | synchronized可以有效解决重排序问题 |
二、synchronized的使用
岳不群:冲儿,在应用Sychronized关键字时,有几个注意点你一定要记牢了
一把锁只能同时被一个线程获取,没有获得锁的线程只能等待; |
---|
每个实例都对应有自己的一把锁(this),不同实例之间互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象公用同一把锁; |
synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁 |
令狐冲:师傅,徒儿都已经记下了,写代码的时候,绝对不会再犯这些错误!
岳不群:冲儿,你知道synchronized有几种用法吗?
令狐冲:这个我知道,**Synchronized一共有三种用法:修饰普通方法、修饰静态方法、修饰代码块。**这部分我可以讲讲。
2.1 修饰普通方法
令狐冲:师傅你看这个例子:
//同步非静态方法 ,当前线程的锁便是实例对象methodName
public synchronized void methodName() {
try {
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName()+" bbb");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
令狐冲:当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁, 所以无法访问该对象的其他synchronized实例方法。
岳不群:但是其他线程还是可以访问该实例对象的其他非synchronized方法,当然如果是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1),另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是obj2),这样是允许的,因为两个实例对象锁并不同相同。
岳不群:此时如果两个线程操作数据并非共享的,线程安全是有保障的,遗憾的是如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了。
令狐冲:哦哦,师傅刚刚点播完之后,我这些对synchronized修饰普通方法的使用更加熟悉了。
2.2 修饰静态方法
令狐冲:师傅我再说下synchronized如何修饰静态方法的吧。
//同步静态方法
public synchronized static void methodName() {
try {
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName()+" bbb");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(SynchronizedDemo::methodName).start();
}
}
令狐冲:当synchronized作用于静态方法时,其锁就是当前类的class对象锁。 由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发操作。
岳不群:不错,需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,知道为什么吗?
令狐冲:因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。师父我说的对吗?
岳不群:说的很对!
2.3 修饰代码块
令狐冲:师父,我再讲讲synchronized如何修饰代码块吧,其实修饰代码块的锁对象一共有两种,一种是当前对象this,一种是当前类class。
//修饰非静态方法
public void methodName() {
//修饰代码块,this=当前对象(谁调用就指待谁)
synchronized (this) {
try {
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + " bbb");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
岳不群:例子写的不错呀。这就是synchronized(this|object) {},在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。 类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。
令狐冲:师父,您再看下synchronized的锁对象为class类的情况:
//修饰非静态方法
public void methodName() {
//修饰代码块,使用Class类
//使用ClassLoader 加载字节码的时候会向堆里面存放Class类,所有的对象都对应唯一的Class类
//SynchronizedDemo.class 这里拿到的就是堆里面的Class类,也就是所有的Class的对象都共同使用这个synchronized
synchronized (SynchronizedDemo.class) {
try {
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + " bbb");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
岳不群:写的很好。synchronized(类.class) {}:在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。
令狐冲:师傅我晓得了。但是我对synchronized为什么会有这些特性还是挺好奇的。。
岳不群:那我就给你讲讲synchronized的原理吧,这个很重要,面试官就爱问!!
三、synchronized实现原理
3.1 同步代码块解析
令狐冲: 师父,我已经知道了synchronized咋用的了,面试的时候也问了,但是他问我背后的原理,我还是答不出来。。
岳不群:不要着急,我讲给你听。咱们先通过反编译下面的代码来看看Synchronized是如何实现对代码块进行同步的。
public class SynchronizedTest {
public void method(){
synchronized (this){
System.out.println(" method start");
}
}
}
岳不群:然后反编译下:
![](https://i-blog.csdnimg.cn/blog_migrate/bc28e385aede54bca0ecb7f00a30a7b3.png)
岳不群:你只关注红色方框里的monitorenter和monitorexit即可。你能看出来啥吗?
令狐冲:听说,synchronized的底层是监视器锁,监视器锁的底层是互斥锁,那monitorenter就是互斥入口,monitorexit就是互斥出口吧?
令狐冲:并且Monitorenter和Monitorexit指令,会让对象在执行的时候,使其锁计数器加1或者减1。
岳不群:说的很对,每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,一共有三种情况:
1)如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2)如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3)如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
令狐冲:那monitorexit怎么会有两个呢?
岳不群:monitorexit用于释放对于monitor的所有权,释放过程很简单,就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。
岳不群:monitorexit有两个,一个是正常出口,一个是异常出口
令狐冲:原来是这样啊,那个面试官问我我当时没有回答出来。
岳不群:通过这两段描述,你应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象, 这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
3.2 同步方法分析
令狐冲:师傅,你前面讲了synchronized在同步代码块中的原理,那在同步方法中,也是这个样子吗?
岳不群:稍微有点不一样,你看下这个例子:
public class SynchronizedTest {
public synchronized void method() {
System.out.println(" method start");
}
}
将代码反编译一下,得到以下结果:
![](https://i-blog.csdnimg.cn/blog_migrate/dc1de22f68a013e45cd4dc5bdc08cc6c.png)
令狐冲:从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成,不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。
岳不群:你观察的很仔细,JVM就是根据ACC_SYNCHRONIZED标示符来实现方法的同步的,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。
岳不群:在方法执行期间,其他任何线程都无法再获得同一个monitor对象。其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
令狐冲:这下我都明白了,下次面试的时候,我就这样回答。
岳不群:只回答到这还是不够的,你再看下对象、对象监视器、同步队列以及执行线程状态之间的关系:
岳不群:这个图非常重要,冲儿你看出什么了吗?
令狐冲:从这图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。
岳不群:看样你真的理解了!
3.3 synchronized的可重入原理
令狐冲:师傅,我记得之前读JVM书籍的时候,书上说synchronized是可重入锁,这个是咋回事呢?我没懂。。
岳不群:从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功。
岳不群:在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。
令狐冲:额,嗯,,,,,迷迷糊糊的
岳不群: 还是看个例子吧:
public class Accounting implements Runnable{
static Accounting instance=new Accounting();
static int i=0;
static int j=0;
@Override
public void run() {
for(int j=0;j<1000;j++){
//this,当前实例对象锁
synchronized(this){
i++;
increase();//synchronized的可重入性
}
}
}
public synchronized void increase(){
j++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
岳不群:在获取当前实例对象锁后进入synchronized代码块执行同步代码,并在代码块中调用了当前实例对象的另外一个synchronized方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现。
令狐冲:哦哦,原来是这样呀。
岳不群:需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。注意由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1.
岳不群:你要记住,Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
令狐冲:多谢师父,我记住了。synchronized原理和使用,都get了。
常考的面试题
1.Synchronized可以作用在哪里? 分别通过对象锁和类锁进行举例。
2. Synchronized本质上是通过什么保证线程安全的? 分三个方面回答:加锁和释放锁的原理,可重入原理,保证可见性原理。
3. Synchronized和Lock的对比,和选择?
4. Synchronized在使用时有何注意事项?
5. Synchronized修饰的方法在抛出异常时,会释放锁吗?
6. 多个线程等待同一个snchronized锁的时候,JVM如何选择下一个获取锁的线程?
7. 知道synchronized的监视器锁是咋回事吗?
===================================================
字节内推:
字节内推〉字节校招开启。简历砸过来!!!!!!!
200多个岗位,地点:北京 上海 广州 杭州 成都 深圳。。
字节内推码:B1RHWFK
官网校招简历投递通道:https://jobs.toutiao.com/campus/m/position?referral_code=B1RHWFK
===================================================
微信公众号:猿侠令狐冲