文章目录
一、synchronized锁表现三种形势
Java中每个对象都可以作为锁。具体表现为以下3种方式:
- 对于普通方法,锁的是当前实例对象。
public class SynchronizedTest {
public synchronized void test(String name){
System.out.println(name+"开始执行");
try {
Thread.sleep(2000);
}catch (Exception e){
}
System.out.println(name+"执行完毕");
}
public static void main(String[] args) throws Exception {
SynchronizedTest t=new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
t.test("线程1");
}
}).start();
SynchronizedTest t1=new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
t1.test("线程2");
}
}).start();
}
}
上面这个代码我们new了两个不同的对象。打印结果如下。线程2并没有等线程1执行完成后才执行,说明对于普通方法,如果是不同的对象实例锁是不起作用的
线程1开始执行
线程2开始执行
线程2执行完毕
线程1执行完毕
我们把上面的代码修改一下,改为同一个实例
public class SynchronizedTest {
public synchronized void test(String name){
System.out.println(name+"开始执行");
try {
Thread.sleep(2000);
}catch (Exception e){
}
System.out.println(name+"执行完毕");
}
public static void main(String[] args) throws Exception {
SynchronizedTest t=new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
t.test("线程1");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
t.test("线程2");
}
}).start();
}
}
打印结果如下。从打印结果可以看出同一个对象实例的时候,第二个线程只有等到第一个线程执行完成后才开始执行。
线程1开始执行
线程1执行完毕
线程2开始执行
线程2执行完毕
- 对于静态同步方法,锁的是当前类的Class对象。
public class SynchronizedTest {
public synchronized static void test(String name){
System.out.println(name+"开始执行");
try {
Thread.sleep(2000);
}catch (Exception e){
}
System.out.println(name+"执行完毕");
}
public static void main(String[] args) throws Exception {
new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest.test("线程1");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest.test("线程2");
}
}).start();
}
}
打印结果如下。第一个线程执行完成后才开始执行第二个线程。
线程1开始执行
线程1执行完毕
线程2开始执行
线程2执行完毕
- 对于同步方法快,锁的是synchonized括号里配置的对象。
public class SynchronizedTest {
public void test(String name){
Object o=new Object();
synchronized(o.getClass()){
System.out.println(name+"开始执行");
try {
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(name+"执行完毕");
}
}
public static void main(String[] args) throws Exception {
SynchronizedTest t=new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
t.test("线程1");
}
}).start();
SynchronizedTest t1=new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
t1.test("线程2");
}
}).start();
}
}
打印结果如下,第一个线程执行完成后才开始执行第二个线程。
线程1开始执行
线程1执行完毕
线程2开始执行
线程2执行完毕
二、为什么说Synchronized是一个重量级锁
Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为 “重量级锁”。
三、Synchronized底层实现原理
同步方法通过ACC_SYNCHRONIZED 关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED时,需要先获得锁才能执行该方法。
同步代码块通过monitorenter和monitorexit执行来进行加锁。当线程执行到monitorenter的时候要先获得锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。每个对象自身维护着一个被加锁次数的计数器,当计数器不为0时,只有获得锁的线程才能再次获得锁。
四、Synchronized锁存储位置
Synchronized用的锁是存在java的对象头里面的。一个对象在new出来之后再内存中主要分为4个部分:
Mark Word:存储了对象的hashCode、GC信息、锁信息三部分。这部分占8字节。
Class Pointer:存储了指向类对象信息的指针。在64位JVM上有一个压缩指针选项-ClassPointer指针:-XX:+UseCompressedClassPointers 为4字节 不开启为8字节。默认是开启的。
实例数据(instance data):记录了对象里面的变量数据。引用类型:-XX:+UseCompressedOops 为4字节 不开启为8字节 Oops Ordinary Object Pointers
Padding:作为对齐使用,对象在64位服务版本中,规定对象内存必须要能被8字节整除,如果不能整除,那么久靠对齐来不。举个例子:new出了一个对象,内存只占用18字节,但是规定要能被8整除,所以padding=6
Mark Word存储结构如下:
32位虚拟机下:
64位虚拟机下:
五、Synchronized锁的升级过程
Java SE 1.6 为了减少获得锁和释放锁带来的性能消耗,引入了 “偏向锁” 和 “轻量级锁”:锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。
-
偏向锁:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中记录存储锁偏向的线程ID,以后该线程在进入同步块时先判断对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果存在就直接获取锁。
-
轻量级锁:当其他线程尝试竞争偏向锁时,锁升级为轻量级锁。线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,标识其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
-
重量级锁:锁在原地循环等待的时候,是会消耗CPU资源的。所以自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么等待锁的线程会不断的循环反而会消耗CPU资源。默认情况下锁自旋的次数是10 次,可以使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。10次后如果还没获取锁,则升级为重量级锁。