synchronized关键字
1、当一个线程进入一个对象的一个synchronized方法后,其他线程可以进入此对象的非同步方法,不可进入此对象此同步方法,也不可进入此对象其他同步方法。同步监视器的意思是:线程开始执行同步代码块或者同步方法时,必须先要获得对同步监视器的锁定。任何时刻只能有一个线程获得对同步监视器的锁定,synchronized代码块的监视器是括号里的obj对象,synchronized方法的同步监视器是this。例如Test t = new Test(),t.f(),如果f是同步方法,则同步监视器是t(即调用该方法的对象),即线程获得的锁对象是t
2、可重入锁和synchronized的基本特性
1) 同步代码块使用monitorenter和monitorexit指令实现,monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,jvm需要保证每一个monitorenter都有一个monitorexit与之对应。任何对象都有一个monitor与之相关联,一个monitor被持有之后,对象将处于锁定状态。根据jvm规范,在执行monitorenter指令前,首先要获得对象的锁,获取成功后,把锁的计数器加1,相应的,在执行monitorexit指令时,会将锁计数器减1,当计数器为0时,锁就被释放。如果获取对象锁失败,当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。
2) 同步方法,虚拟机可以从方法表中的ACC_SYNCHRONIZED访问标志得知一个方法是否为同步方法,当方法调用时,调用指令会检查ACC_SYNCHRONIZED访问标志是否被设置了,若被设置了,执行线程就要先成功持有monitor,然后才能执行方法,最后当方法完成时释放monitor,在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获取到同一个monitor。
在java设计中,每一个对象自打娘胎里出来就带了把看不见的锁,即monitor锁。一个monitor只能被一个线程拥有。线程获取monitor成功,就成为该监视器对象的拥有者。monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局可用列表。每一个被锁住的对象都会和一个monitor关联。
在使用synchronized的时候,当一个线程请求一个由其他线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁就会成功,否则阻塞。一个例子:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的。为什么要引入可重入锁这种机制?这种锁的概念就是:自己可以获取自己的内部锁。假如一个线程T获得了对象A的锁,那么该线程如果在未释放前再次请求该对象的锁时,如果没有可重入锁的机制,是不会获取到锁的,这样就会出现死锁的情况。就如上述代码,线程T在执行到method1内部的时候,由于该线程已经获取了该对象syncDubbo的对象锁,当执行到调用method2时,会再次请求该对象的对象锁,如果没有可重入锁的机制,就会出现死锁。
synchronized其他特性:出现异常时,其持有的锁会自动释放。另外,可以将任意对象作为监视器。
synchronized实现原理:本质上是对一个对象监视器进行获取,执行方法的线程必须先获取到该对象的同步监视器才能进入同步块或者方法块,没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态,在同步队列中等待。
可以看出,Object由synchronized保护,任意线程对Object的访问首先要获得Object的监视器。如果获取失败,线程进入同步队列,进入BLOCKED状态。当之前获得了锁的线程释放了锁,该释放操作唤醒在同步队列中的线程,使其重新尝试对监视器的获取。
jvm对synchronized的优化:jdk1.6以后为了减少获得和释放锁带来的性能消耗引入了偏向锁和轻量级锁。此时synchronized的状态总共有4种:无锁,偏向锁,轻量级锁,重量级锁。锁的级别从前往后依次升级,称为锁膨胀。锁膨胀是单向的只能从低往高。先看下synchronized锁的存放位置,synchronized用的锁是存在java对象头里的,如果对象是数组类型的,则jvm用3个字宽(Word)存储对象头,如果是非数组类型的,则用2字宽存储对象头。32位虚拟机中,1字宽等于4字节即32比特。

java对象头里的Mark Word里默认存储对象的hashcode、分代年龄和锁标记位。32位jvm的Mark Word里默认存储结构如下图

偏向锁:在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得的,为了让线程获得锁的代价更低,于是就引进了偏向锁。偏向锁指的是,它会偏向于第一个访问锁的线程,如果在运行过程中同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下会给线程加一个偏向锁。偏向锁执行流程:当一个线程访问同步代码块并获取锁时,会在对象头的 Mark Word 里存储获取偏向锁的线程 ID,在线程进入和退出同步块时不用加锁和释放锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁,如果Mark Word中的线程ID和访问的线程ID一致,则可以直接进入同步块进行代码执行。如果线程 ID 不同,则使用CAS尝试获取锁,如果获取成功则进入同步块执行代码,否则会将锁的状态升级为轻量级锁。偏向锁优点:偏向锁是为了在无多线程竞争的情况下,尽量减少不必要的锁切换而设计的,因为锁的获取及释放要依赖多次 CAS 原子指令,而偏向锁只需要在置换线程ID的时候执行一次CAS原子指令即可。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的
轻量级锁:重量级锁会使用操作系统的互斥锁,成本较高,涉及操作系统用户态和内核态的切换,产生较多性能消耗。 引入轻量级锁的目的是减少传统的重量级锁使用互斥锁产生的性能消耗。多个线程竞争偏向锁时就会导致偏向锁升级为轻量级锁,是一种乐观锁,所以不使用互斥同步,而是使用cas操作更新mark word的锁标志位来获得锁,如果更新成功就拥有了该对象的锁,这样能减少互斥同步所使用的互斥量带来的性能开销。
偏向锁和轻量级锁的相同点和区别:相同点都是乐观锁,都认为同步期间不会有其他线程竞争锁。不同点:轻量级锁是在无竞争情况下使用cas操作来代替互斥量的使用,从而实现同步,偏向锁是在无竞争的情况下完全取消同步
重量级锁:如果上述轻量级锁cas更新mark word的锁标志位失败,说明其他线程已经获得锁,那么该线程会自旋操作获取锁,如果自旋多次后仍然获取失败,则锁膨胀为重量级锁,锁标志位变为10。重量级锁依赖监视器Monitor实现方法同步或代码块同步的,代码块同步使用的是 monitorenter 和 monitorexit 指令来实现的,monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处的,任何对象都有一个Monitor与之关联,当且一个 Monitor 被持有后,它将处于锁定状态。重量级锁会涉及CPU从用户态到内核态的切换。用户态:应用程序只能在用户态运行——运行用户程序, 用户可以操作和访问的空间,这个空间通常存放我们用户自己写的程序、数据等。内核态:操作系统在系统态运行——运行操作系统程序,是系统内核来操作的一块空间,这块空间里面存放系统内核的函数、接口等。
当一个线程访问Object的synchronized方法时,另一个线程能访问object的非Synchronized方法,但是不能访问object的其他Synchronized方法。
如果synchronized修饰普通方法,那么锁监视器是当前实例对象,如果类的两个不同实例就没有这种约束了。例如account类的实例account1和account2,对account1操作只能获取account1的对象锁。
如果synchronized修饰static方法或者synchronized(A.class),相当于在类上加锁,锁是当前类的class对象。只要是这个类产生的对象,在调用这个静态方法时都会产生互斥。该类的所有实例对象公用一个监视器
3、Synchronized有一个同步队列,一个等待队列
同步队列:获取锁失败的线程,都加入同步队列排队获取锁
等待队列:已经拿到锁的线程在等待其他资源时,主动释放锁,置入等待队列中,等待被唤醒。线程调用 Object.wait()方法后,线程会从同步队列转移到等待队列。notifyAll()方法将等待队列中的所有线程唤醒,并加入同步队列
4、synchronized关键字能否锁住字符串
public class ThreadTest implements Runnable {
@Override
public void run() {
synchronized (new String("字符串常量")) {
//线程进入
System.out.println(" thread start");
try {
//进入后睡眠
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程结束
System.out.println(" thread end");
}
}
如果用这种方式,锁不住字符串,因为每次会在堆上new出来新对象
public class ThreadTest implements Runnable {
String str = "字符串常量";
@Override
public void run() {
synchronized (str) {
//线程进入
System.out.println(" thread start");
try {
//进入后睡眠
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程结束
System.out.println(" thread end");
}
}
用这种方式,或者synchronized (new String("字符串常量").intern()) 可以锁住字符串,因为此时锁对象是常量池中的对象。
浙公网安备 33010602011771号