Synchronized的使用
一、特性
1、原子性:指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。但是像i++、i+=1等操作字符就不是原子性的,它们是分成读取、计算、赋值几步操作,原值在这些步骤还没完成时就可能已经被赋值了,那么最后赋值写入的数据就是脏数据,无法保证原子性。
2、可见性:即当读写两个线程同时访问同一个变量时,synchronized用于确保写线程更新变量后,会更新到主内存上,读线程再访问该变量时可以读取到该变量最新的值
3、有序性:程序的执行顺序会按照代码先后顺序进行执行。Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序
4、可重入:当一个线程得到一个对象的锁后,在该锁里执行代码的时候可以再次获得该对象的其他锁。当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞
可重入性举例说明
public class SynchroDemo3 {
public synchronized void method1() {
System.out.println("开始执行同步方法1");
method2();
}
public synchronized void method2() {
System.out.println("开始执行同步方法2");
}
public static void main(String[] args) {
SynchroDemo3 synchroDemo3 = new SynchroDemo3();
new Thread(new Runnable() {
@Override
public void run() {
synchroDemo3.method1();
}
}).start();
}
}
在方法1中调用方法2,两个方法都是同步方法,在获取方法1的锁之后,是可以直接调用方法2,而不需要重新获取锁,这就是可重入性;内部实现原理是一个锁的计数器,使用一次增加1,释放一次减少1,直至为0,即释放锁
二、用法
1、同步非静态方法;锁是当前实例对象
public synchronized void method(){
……
}
2、同步静态方法;锁是当前类的class对象
public synchronized static void method(){
……
}
3、同步代码块;锁是括号里面的对象
private final Object object = new Object();
public void method(){
//锁是当前类的class对象
synchronized(类.class){
……
}
}
// 锁是当前实例对象
public void method(){
synchronized(this|object){
……
}
}
被static修饰的静态方法、静态属性归类所有,同时该类的所有实例对象可以访问。
普通成员属性、成员方法是归实例化的对象所有,必须实例化之后才能访问
三、底层原理
在 Java 中,每个对象都会有一个 monitor (锁标记)对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。某一线程占有这个对象的时候,先monitor 的计数器是不是0,如果是0还没有线程占有,这个时候线程占有这个对象,并且对这个对象的monitor+1;如果不为0,表示这个线程已经被其他线程占有,这个线程等待。当线程释放占有权的时候,monitor-1;
原理分析
1、代码块加锁
public void accessResources1(){
synchronized(this){
try {
synchronized (this){
TimeUnit.MINUTES.sleep(2);
}
System.out.println(Thread.currentThread().getName()+" is runing");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
javap -v xxx.class
这边的第一个monitorexit代表正常退出,第二个monitorexit代表异常退出
2、对于方法的加锁
public synchronized static void accessResources0(){
try {
TimeUnit.MINUTES.sleep(2);
System.out.println(Thread.currentThread().getName()+" is runing");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
锁的信息 :ACC_SYNCHRONIZED
使用synchronized注意的问题
1、与moniter关联的对象不能为空
2、synchronized作用域太大
3、不同的monitor企图锁相同的方法
4、多个锁的交叉导致死锁
Java虚拟机对synchronized的优化
一个对象实例包含:对象头、实例变量、填充数据
对象头:加锁的基础
实例变量:存放类的属性数据信息
填充数据:不是必须部分,由于虚拟机要求对象起始地址必须是8字节的整数倍,对齐填充仅仅是为了使字节对齐
synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark Word 和 Class Metadata Address组成,其中Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例JDK6之前只有两个状态:无锁、有锁(重量级锁),
在JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁(等待时间长)
无锁状态:没有加锁
偏向锁:减少统一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁(竞争不激烈适用)
如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。
轻量级锁:轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块
重量级锁:重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景
自旋锁:轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段;许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销
自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点
锁消除:JIT在编译的时候吧不必要的锁