前言
关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能)。
synchronized的三种应用方式
synchronized关键字最主要有以下3种应用方式,下面分别介绍
- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
- 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
当你将synchronized加到非静态方法的实例方法时,在同一个实例对象的该方法调用,会对方法的调用进行加锁,但是,如果new了两个实例对象,则不会加锁,导致没有达到预期结果。
此时,需要在静态方法中加入synchronized关键字,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
当然,为了缩小同步的代码块,可以用synchronized关键字修饰代码块,减少加锁的范围,提升性能。
synchronized原理浅析
1.synchronized代码块底层原理
从javap反编译后得到字节码可以得到:
monitorenter //进入同步方法
//..........省略其他
monitorexit //退出同步方法
goto //return
//省略其他.......
monitorexit //退出同步方法
从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。
当执行到monitorenter时,线程会尝试获取对象锁所对应的monitor的持有权,获取成功,并将计数器设置为1,取锁成功,直到正在执行线程执行完毕,即执行monitorexit指令,计数器的值会被设置为0,并释放锁。当然当前线程可对该monitor进行重入,这里就涉及重入锁概念,但是无论如何,方法中调用过每条monitorenter都会对应一条monitorexit,都会进行一一配对。
从以上的字节码可以看到有两个monitorexit,因为为了保证在方法异常完成时,monitorenter和monitorexit都会正常配对执行,编译器会自动产生一个异常处理器,处理所有的异常,并执行monitorexit指令。
2.synchronized修饰方法底层原理
从javap反编译后得到字节码可以得到:
//..........省略其他
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
//省略其他.......
从字节码可知,当前无monitorenter 和 monitorexit 指令,只有ACC_SYNCHRONIZED标识,表示该方法是一个同步方法,具体锁方面的原理跟代码块的相似。
在理解原理之后,也要理解锁的状态、锁的竞争等概念。
锁状态
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。
1.偏向锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
在大多数情况下,锁总是由同一个线程多次获得,不存在多线程竞争,所以出现了偏向锁,从而达到提高性能的目的。
当偏向锁遇到其他线程尝试竞争偏向锁时,才会释放锁,会暂停拥有偏向锁的线程,判断是否处于被锁定状态,然后恢复到无锁状态或轻量级锁状态。
2.轻量级锁
当一个线程持有偏向锁是,呗另外的线程访问,偏向锁会升级为轻量级锁,其他线程通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
由此可知,当只有一个等待线程,则该线程通过自旋进行等待,但是当自旋超过一定的次数,或者一个线程持有锁,一个线程自旋,此时再来第三个线程,轻量级锁活升级为重量级锁
3.重量级锁
重量级锁,效率低下,因为monitor是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
synchronized注意点
1.synchronized可重入性
由synchronized底层原理可知,当前线程已经持有锁对象,再次申请锁时,属于重入锁。
for(int j=0;j<1000000;j++){
//this,当前实例对象锁
synchronized(this){
i++;
increase();//synchronized的可重入性
}
}
如上图伪代码可知,synchronized允许重入。
2.synchronized线程中断
对于synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,即使调用中断线程的方法,也不会生效。
3.synchronized等待唤醒机制
notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象
synchronized (obj) {
obj.wait();
obj.notify();
obj.notifyAll();
}
4.wait和sleep区别
wait方法调用之后,会释放持有的monitor,直到有线程调用notify/notifyAll后才能继续运行。
sleep方法只是让线程休眠,并不释放锁。
5.执行过程
线程进入synchronized代码块前后,执行过程如下:
- a.线程获得互斥锁
- b.清空工作内存
- c.从主内存拷贝共享变量最新的值到工作内存成为副本
- d.执行代码
- e.将修改后的副本的值刷新回主内存中
- f.线程释放锁