在多线程环境下为了保证数据安全,需要用到互斥锁保证多线程对数据操作的安全性。Synchronized关键字可以修饰方法、代码块,修饰的地方不同锁的范围也不一样。主要有两种区别:
- 修饰静态方法(锁的是类,作用范围跨对象锁)
- 修饰实例方法(锁的是对象,作用范围不跨对象)
表现形式:
public class SynchronizedDemo {
private static Integer count = 0;
public Integer count2=0;
// 类锁 只锁当前方法,其他类方法没有加上synchronized,不锁
public static synchronized void incLockClazz() {
count++;
}
//类锁
public void incLockClazz2() {
synchronized (SynchronizedDemo.class) {
count++;
}
}
//对象锁
public synchronized void incLockObj2() {
count2++;
}
//对象锁
public void incLockObj() {
synchronized (this) {
count2++;
}
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch=new CountDownLatch(1);
SynchronizedDemo synchronizedDemo=new SynchronizedDemo();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
incLockClazz();}).start();
new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronizedDemo.incLockObj();
}).start();
}
countDownLatch.countDown();
TimeUnit.SECONDS.sleep(5);
System.out.println(SynchronizedDemo.count);
System.out.println(synchronizedDemo.count2);
}
}
synchronized原理
对象监视器Monitor
monitorenter->获取Objectmonitor->成功:monitor标记owner为当前线程->获取对象锁->monitorexit->唤醒同步队列争抢锁
monitorenter->获取Objectmonitor->失败->进入同步队列
当需要加锁时jvm执行指令monitorenter,监视器执行锁持有者标记,如果成功则获取对象锁,如果失败则进入同步队列,当释放锁执行monitorexit后,监视器唤醒同步队列再次执行monitorenter竞争锁。ObjectMonitor包含有等待队列用于保存执行了wait的线程、同步队列用于保存竞争失败的线程。
锁的原理
JVM中对象在内存中的结构包括(具体信息在markOop.hpp中定义)
- 对象头
- Mark Word(标记字段)
- Klass Pointer(类型指针)
- 实例数据
- 填充数据
Mark Word:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
Klass Pointer:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
MarkWord
结构说明
jdk1.6之后JVM对synchronized中锁进行了优化,锁的变化过程为
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
偏向锁
对象头MarkWord信息包含
ThreadID、epoch、偏向锁标记、锁标记
当线程进入同步方法块,此时如果markword是无锁状态,则通过CAS修改markword记录自己的线程ID。当再次进入时无需再进行CAS操作加锁、解锁,只需要判断偏向锁线程ID是否与自己相同。相同则获得锁。如果不同且偏向锁标记不为0(无锁状态),则通过CAS竞争,尝试把markword线程id设成自己。这时,当程序到达全局安全点的时候(无代码执行),则判断偏向锁中指向的线程是否还存活,如果不存活,则标记为无锁状态。如果还存活则进行锁升级,升级为轻量级锁。
轻量级锁
执行代码块时JVM会创建当前线程栈空间的锁记录空间(lock record:包含displaced hdr,owner),复制markword的信息到displaces hdr,并通过CAS尝试将markword指向线程锁记录空间,如果成功则获得锁,失败则存在竞争。这时通过自旋不断尝试,因为大部分的线程在获得锁之后很快就会释放锁。然而自旋会占用CPU资源,不能无限自旋影响性能,当自旋多次还获得锁失败时,则升级为重量级锁。
锁的升级流程
JVM优化
- 由于大多数情况下程序多线程都会出现竞争的情况,可以选择关闭偏向锁。jdk1.6以后默认开启偏向锁。通过参数-XX:-UseBiasedLocking,可以关闭偏向锁。
- 轻量级锁自旋次数设置。通过优化自旋次数来达到性能的优化。可以通过-XX:PreBlockSpin=10 设置自旋次数,默认10。jdk1.6前还需要先开启-XX:+UseSpinning。jdk1.6开始PreBlockSpin也去掉,由jvm控制,自适应。