问题:java中如何解决线程并行导致的数据安全问题?
java SE 1.6后对synchronized做了优化,增加了偏向锁、轻量级锁概念,以减少获得/释放锁带来的性能消耗
一、synchronized的使用
锁的粒度,从上到下变小
1.修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
public synchronized static void demo2() {
// 代码块
}
2.修饰实例方法,作用于当前实例对象加锁,进入同步代码前要获得当前实例对象的锁
public synchronized void demo2() {
// 代码块
}
3.修饰代码块,指定加锁对象,进入同步代码前要获得给定对象的锁
public void demo(){
synchronized (this){
// 代码块
}
}
二、锁是如何存储的
- 问题:锁如何实现线程的互斥特性?
- 思路:synchronized(lock)为切入点,锁是否与对象生命周期有关,锁在对象中存在状态是怎样的
1.对象在内存中存储
对象头(header),实例数据(instance data),对齐填充(padding)
eg.
2.jvm源码实现
new 实例对象时,(Hotspot虚拟机)JVM会创建instanceOopDesc对象。
OOP-klass模型描述对象实例,OOP(Ordinary ObjectPoint)普通对象指针,klass描述对象具体类型。Hotspot采用instanceOopDesc和arrayOopDesc类型描述对象头
instanceOopDesc,继承自OopDesc类。OopDesc,定义在oop.hpp,OopDesc对象包含两个成员_mark和metadata。
_mark,表示对象标记,属于markOop类型,也即Mark Word,它记录了对象和锁有关的信息
_metadata,表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示普通指针,_compressed_klass表示压缩类指针
MarkWord
markOop,定义在markOop.hpp文件,代码如下
Mark Word记录了对象和锁有关的信息,里面存储的信息会随着锁标志位的变化而变化。
Mark Word可能存在5种变化情况,以下演示32位机的(也有64位机的,存储的大小不一样)
eg.记录5种状态图表
锁状态 | 25bit | 4bit | 1bit | 2bit | |
23bit | 2bit | 是否偏向锁 | 锁标志位 | ||
无锁 | 对象的HashCode | 分代年龄 | 0 | 01 | |
偏向锁 | 线程ID | epoch | 分代年龄 | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
重量级锁 | 指向重量级锁的指针 | 10 | |||
GC标记 | 空 | 11 |
3.为什么任何对象都可以实现锁
任何一个对象都继承Object对象,object在jvm中都有native的C++对象 oop/oopDesc对应。
线程在获取锁时,其实是获取监视器对象monitor,多线程访问同步代码块时,相当于去争抢对象监视器,以修改对象的锁标识。
三、锁升级
无锁,偏向锁,轻量级锁,重量级锁
1.偏向锁
当只有一个线程访问时,会将MarkWorld锁标志位设为“01”,并通过CAS操作把线程ID记录到MarkWord。当下次访问时,无需再次加锁和释放锁,会判断对象头里是否有当前线程ID,若有,则无需尝试获得锁(也即是锁会偏向于某个线程)
eg.偏向锁流程
2.轻量级锁
当一个线程Thread-1已经持有偏向锁,还没走完同步代码,此时若有竞争线程Thread-2尝试进入,则会将持有偏向锁线程先暂停,然后升级为轻量级锁。
竞争线程Thread-2通过自旋方式,有限次数内获得锁,如果自旋获取锁失败,则当前线程升级为重量级锁,竞争线程挂起阻塞
轻量级锁升级细节
在线程栈帧中创建锁记录,将MarkWord中标记的对象信息复制到锁记录中,将锁记录的Owner指向锁对象,将MarkWord替换为指向锁记录的指针
轻量级锁的解锁
轻量级锁的释放,获得锁的逆向逻辑。通过CAS操作将线程栈帧中的lockRecord替换回MarkWord中,若成功表示没有竞争;若失败,则存在竞争,轻量级锁会膨胀为重量级锁
eg.轻量级锁流程
3.重量级锁
若两个线程正常交替执行,那么轻量级锁基本能够满足锁的要求,若当两个线程同时进入临界区,则轻量级锁会膨胀为重量级锁,未获得锁的线程挂起阻塞
通过命令:javap -v 类.class,查看生成的class文件信息
加了同步的代码会看到monitorenter和monitorexit.
每个java对象都会与一个监视器monitor关联,要想取得synchronized修饰的同步方法或代码块的执行权限,首先需要取得修饰对象的monitor。
monitorenter表示去获得一个对象监视器,monitorexit表示释放监视器所有权,使其他被阻塞线程可以尝试获得监视器。
eg.重量级锁加锁流程
任意对Object访问的线程,首先要获得Object的监视器
四、wait,notify,notifyAll
- 问题:通过synchronized,阻塞线程只有等到获得锁线程执行完毕才唤醒,如何显示控制线程唤醒?
- 通过信号机制:Object提供的wait,notify,notifyAll控制线程状态。
基本概念
wait,持有对象锁线程A,准备释放对象锁权限,释放CPU资源进入等待状态。
notify,当持有对象锁线程A准备释放锁权限时,通知jvm唤醒某个竞争该锁的线程,当A执行完毕并释放锁,线程X直接获得对象锁权限,其他线程继续等待。
notifyAll,唤醒所有竞争统一对象锁的线程。
五、练习
1.请分析以下程序的执行结果,并详细说明原因
public class SynchronizedDemo implements Runnable{
int x = 100;
public synchronized void m1() {
x = 1000;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("x=" + x);
}
public synchronized void m2() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
x = 2000;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedDemo sd = new SynchronizedDemo();
new Thread(()->sd.m1()).start();
new Thread(()->sd.m2()).start();
sd.m2();
System.out.println("Main x=" + sd.x);
}
@Override
public void run() {
m1();
}
}
分析:
SynchronizedDemo sd = new SynchronizedDemo();
new Thread(()->sd.m1()).start(); // x=1000,休眠1秒
new Thread(()->sd.m2()).start(); // 休眠0.2秒,x=2000
sd.m2(); // 休眠0.2秒,x=2000
System.out.println("Main x=" + sd.x);
Main,t1(执行m1方法),t2(执行m2方法)
Main线程获得锁,x=2000
打印时,若t1获得锁,并先于打印给x=1000,则打印Main x=1000,休眠1秒打印x=1000
打印时,若t1获得锁,晚于打印或者t2获得锁,则输出Main x=2000,再输出x=1000
t1获得锁,打印x=1000,再打印Main x=2000(不管main还是t2获得锁,结果一样)
t2获得锁,x赋值2000
Main获得锁后,在打印时,若t1先于打印把x赋值1000,则输出Main=1000,x=1000
t1获得锁,x=1000,再打印Main x=2000
2.下面这个程序的最终结果是多少?为什么?
public class SynchronizedDemo {
static Integer count=0;
public static void incr(){
synchronized (count) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}
public static void main(String[] args) throws IOException, InterruptedException {
for(int i=0;i<1000;i++){
new Thread(()->SynchronizedDemo.incr()).start();
}
Thread.sleep(5000);
System.out.println("result:"+count);
}
}
分析:Integer可变,所以基本可以视为没有加锁,也就是存在并发问题,值会小于1000