电子说
介绍
JDK提供的锁分两种,一种是JVM实现的synchronized,是java的关键字,因此在这个关键字作用对象的范围内都是可以保证原子性的,主要是依赖特殊的CPU指令。另一种是JDK提供的代码层面的锁Lock。
一、synchronized的四种用法
1. 修饰代码块
大括号括起来的代码,称同步语句块,作用范围是大括号,作用对象是调用代码块的对象。
public void test1(int j) {
synchronized (this) {
for (int i = 0; i < 10; i++) {
log.info("test1 {} - {}", j, i);
}
}
}
测试代码:
public static void main(String[] args) {
SynchronizedExample1 example1 = new SynchronizedExample1();
SynchronizedExample1 example2 = new SynchronizedExample1();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() - > {
example1.test1(1);
});
executorService.execute(() - > {
example2.test1(2);
});
}
测试结果:
2. 修饰方法
被修饰的方法称为同步方法,作用范围是整个方法,作用于调用对象。
public synchronized void test2(int j) {
for (int i = 0; i < 10; i++) {
log.info("test2 {} - {}", j, i);
}
}
测试代码:
public static void main(String[] args) {
SynchronizedExample1 example1 = new SynchronizedExample1();
SynchronizedExample1 example2 = new SynchronizedExample1();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() - > {
example1.test2(1);
});
executorService.execute(() - > {
example2.test2(2);
});
}
测试结果:
3. 修饰静态方法
作用范围是整个方法,作用于所有对象。
public static synchronized void test3(int j) {
for (int i = 0; i < 10; i++) {
log.info("test3 {} - {}", j, i);
}
}
测试代码:
public static void main(String[] args) {
SynchronizedExample1 example1 = new SynchronizedExample1();
SynchronizedExample1 example2 = new SynchronizedExample1();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() - > {
example1.test3(1);
});
executorService.execute(() - > {
example2.test3(2);
});
}
测试结果:
4. 修饰类
作用范围是synchronized后面括号括起来的部分,作用于所有对象。
public static void test4(int j) {
synchronized (SynchronizedExample2.class) {
for (int i = 0; i < 10; i++) {
log.info("test4 {} - {}", j, i);
}
}
}
测试代码:
public static void main(String[] args) {
SynchronizedExample1 example1 = new SynchronizedExample1();
SynchronizedExample1 example2 = new SynchronizedExample1();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() - > {
example1.test4(1);
});
executorService.execute(() - > {
example2.test4(2);
});
}
测试结果:
二、synchronized的原理
在Java语言中存在两种内建的synchronized语法:synchronized语句、synchronized方法:
synchronized语句
如上示例,test1和test4使用的就是synchronized语句。使用Javap -c命令反编译test1代码,如下:
在Java虚拟机的specification中,有关于monitorenter和monitorexit字节码指令的详细描述:
monitorenter
每个对象都有一个锁,也就是监视器(monitor)。 Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。 每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。
当monitor被占有时就表示它被锁定。线程执行monitorenter指令时尝试获取对象所对应的monitor的所有权,过程如下:
monitorexit
执行monitorexit的线程必须是相应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
synchronized方法
如上示例,test2和test3使用的就是synchronized方法。synchronized方法加锁的方式是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1。如下:
三、synchronized的优化
synchronized在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里,而Java对象头又是什么呢?
Java对象头
以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。
Mark Word
默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
Klass Point
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
在JDK1.6及其之前的版本中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。然而在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境)。如果每次都调用Mutex Lock将严重的影响程序的性能。因此在JDK6中为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和 轻量级锁 。所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。如下:
无锁
偏向锁
轻量级锁
重量级锁
综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。
四、synchronized存在的问题
1.性能损耗
2. 阻塞
全部0条评论
快来发表一下你的评论吧 !