并发编程系列(二):线程锁使用场景和原理

问题: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对象包含两个成员_markmetadata
_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文件信息

加了同步的代码会看到monitorentermonitorexit.
每个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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值