java并发编程 ||深入理解synchronized,锁的升级机制

上一章我们说了多线程编程所带来的好处( java并发编程 ||Thread生命周期详解),但是既然有那么多好处,肯定也会带来一些问题,这一章我们就来看看它带来的问题以及解决的办法。

多线程所带来的问题?

线程不安全

1.首先我们举一个例子来证明线程的不安全

我们对一个数自增1000次,并且用多线程来实现。

/**
 * @Author Dark traveler
 * @Note 我心净处,何处不是西天。
 * @Descrption
 * @E-Mail : 1029149772@qq.com
 * @Date : Created in 9:02 2020-3-18
 */
public class NoSafeThread {
    private static int sum = 0;

    public static void incr(){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        sum++;
    }

    public static void main(String[] args) {
        for(int i=0,len=1000;i<len;i++){
            new Thread(()->{
                incr();
            }).start();
        }
        //确保所有程序都运行完成了
        TimeUnit.SECONDS.sleep(2);
        System.out.println(sum);
    }
}

我们会发现,我们一直得不到我们想要的答案1000,始终是一个小于1000的随机值,但是从逻辑上来说,分明应该是1000才对啊? 

这是为什么呢,对,这就代表了线程是不安全的,我们现在new了1000个线程来调用这个方法,很多线程可能会同时进入这个方法,也就意味着可能会有两个线程同时拿到sum = 0的初始值,然后同时进入方法incr(),并且线程1自增以后把sum赋值为1,但是线程2此时并没有拿到这个1,因为它们两个都进入了这个incr()方法,所以线程2又把0自增成为1,并且把sum再次赋值为1,这样就成为了1次重复操作,当很多次这种情况出现的时候,就出现了上面那种情况。

2.怎么解决线程不安全

既然出现了线程不安全,会让n个线程同时进入一个方法,那我们只要想办法同时只有一个线程进入这个方法,那就没问题了吧。所以java就引入了锁的概念,我们来看看怎么用锁把上面那个答案变回1000。

/**
 * @Author Dark traveler
 * @Note 我心净处,何处不是西天。
 * @Descrption
 * @E-Mail : 1029149772@qq.com
 * @Date : Created in 9:02 2020-3-18
 */
public class NoSafeThread {
    private static  int sum = 0;

    private static Object lock = new Object();

    public synchronized static void incr(){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //锁定代码块
       /* synchronized (lock){
            sum++;
        }*/
       sum++;
    }

    public static void main(String[] args) throws InterruptedException {
        for(int i=0,len=1000;i<len;i++){
            new Thread(()->{
                incr();
            }).start();
        }
        //确保上面线程都运行完毕了
        TimeUnit.SECONDS.sleep(2);
        System.out.println(sum);
    }
}

我们使用了关键字synchronized来完成对方法的上锁,使得每一时刻只有一个线程进入这个方法,这样就能确保线程安全了,那我们来看看synchronized关键字的用法。

3.synchronized关键字(重量级锁)

synchronized的三种使用方法,代码如下

 *修饰实例方法:demo()

 *修饰静态方法:demo3()

 *修饰代码块:demo2()

/**
 * @Author Dark traveler
 * @Note 我心净处,何处不是西天。
 * @Descrption
 * @E-Mail : 1029149772@qq.com
 * @Date : Created in 11:03 2020-3-18
 */
public class LockThread {
    //2种表现形式,放在方法层面和代码块层面来控制
    //2种作用范围,对象锁还是类锁  区别:是否跨对象跨线程被保护

    //修饰方法层面,对象锁
    public synchronized void demo(){

    }
    //也是对象锁,比较灵活
    public void demo2(){
        //todo
        synchronized (this){
            //保护存在线程安全的变量
        }
    }

    //加了static,全局锁
    public synchronized static void demo3(){

    }
    //也是全局锁
    public void demo4(){
        synchronized (LockThread.class){

        }
    }

    public static void main(String[] args) {
        LockThread lockThread1 = new LockThread();
        LockThread lockThread2 = new LockThread();

        //不是同一个对象,不存在互斥特性,不排队
        new Thread(()->lockThread1.demo()).start();
        new Thread(()->lockThread2.demo()).start();

        //存在互斥性,要排队
        new Thread(()->lockThread1.demo3()).start();
        new Thread(()->lockThread2.demo3()).start();
    }
}

总结:

1.锁的共享性和互斥性,只有共享项相同的锁才会有互斥作用。

2.锁的两种表现形式,锁整个方法和锁相应的代码块。

3.锁的两种作用范围,锁单个对象实例和锁整个类。

synchronized为什么能起到锁的作用?锁在内存中是怎么存储的。

我们启用synchronized关键字的时候,会去取得这个锁的对象,我们先来看看一个对象在内存中的内存布局。

查看hotspot源码,我们可以看到对象头中具体放了哪些东西。

 

所以我们发现,锁其实都是放到对象头中的,如果我们使用 synchronized关键字的时候,它会去取得这个对象的头中的锁信息,通过这个锁中的信息来判断是否加锁。

4. 1.6jdk以后锁的升级

虽然使用synchronized关键字可以保证线程的安全,但是降低了效率,那既想保证线程安全,又想保证性能怎么办呢?所以jdk1.6以后,想到了一种锁升级的情况,只有到重量级锁的时候才阻塞,之前的锁都不阻塞。

无锁 -》 偏向锁 -》轻量级锁 -》重量级锁

首先来说下锁的升级流程,假设有两个线程ThreadA/ThreadB访问同步代码块

1.只有ThreadA去访问(大部分情况属于这种)-》引入偏向锁标记(ThreadA的ThreadId,偏向锁的标志)

2.ThreadA和ThreadB交替访问-》轻量级锁(自旋锁)

3.多个线程同时访问-》申请重量级锁,阻塞

无锁-》偏向锁:

当线程A去访问同步代码块的时候,先通过CAS来比较,实现原子性,检查对象头中是否存储了线程1,如果没有存储就通过cas来替换,把线程A的id加入锁对象头中,并且加上偏向锁的标志,获得偏向锁布局就变成下图。

注:

CAS 乐观锁,compare and swap(value,expect,update),就相当于判断下当前的值是不是最新的。我们通过下面的图来简单说明下cas的作用原理,假设我们当前内存中有个值为 int i =0,当下面的CAS开始时,先读取当前的值E = 0,然后计算这个值(比如计算过程为++),那么计算值  V = 1,接下来并不是直接赋值,而是再比较一次当前值E和现在内存中这个 i的值,现在叫N,是不是相等,如果是相等,就把计算值更新为V,CAS结束。

当然上面是最完美的状态,在这个比较当前值E和内存中的最新值N的时候,也会出现不相等的状态,那么当前CAS就会失败,不会去更新新值。

除了失败以外,还有一种可能,就是有其它线程对当前值进行了操作,比如另一个线程把这个值从0改成1,又从1改成了0,其它线程是操作过这个值的,但是在CAS中的比较却是能成立的,这就是CAS中的ABA问题,那么这种问题怎么解决呢,或者说怎么感知到呢,那就是加一个标志,可以是版本号,也可以是一个布尔值,当读取这个内存中的i的时候,把它的版本号一起取过来,只有有线程对它进行过操作,就升级一下版本号,这样就能解决ABA问题了。

 

偏向锁-》轻量级锁(自旋锁):

此时当线程B来访问同步代码块,它也会通过CAS来比较对象头中的锁,线程A和B的id当然不相同,所以这个比较一定会失败,然后它就会去把线程A暂停,并且把它的偏向锁给撤销,把这个锁对象的锁标志和线程id给情况,这里又会分为两种情况,如果线程A已经做完了同步代码块中的指令,A就直接把锁对象释放,由线程B来把线程id放进锁对象,访问同步代码块;如果线程A并没有做完同步代码块中的指令,那么就会升级成轻量级锁,然后两者以轻量级锁的方式来竞争这个锁对象。

轻量级锁-》重量级锁

当升级成轻量级锁以后,线程B会启动自旋,就是在一个循环中反复通过cas来获取锁。(因为绝大部分线程在获得锁以后会在非常短的时间内释放锁,所以这种情况阻塞损耗性能不值得)因为自旋也会占用cpu资源,所以在自旋到指定次数以后,还没有获得轻量级锁,锁就会膨胀成重量级锁,然后直接阻塞。

所以自旋就有两种形式:

1.设置自旋次数 preBliockSpin,jvm设置

2.自适应自旋,推荐,根据线程获得锁的自旋次数来调整,虚拟机知道你上一次自旋锁是成功的,那么它会觉得你下一次也很可能成功,所以它会自动调整自旋锁的次数。

注:

自旋锁,一个循环

for(;;){

    if(cas){

         return ;//获得锁成功

     }

}

重量级锁

升级到重量级锁以后,没有获取到锁的线程会被阻塞,blocked状态,我们来看看升级到重量级锁做了什么。

使用重量级锁后,我们发现对象里面有一个配对监视器,3和5,表示拿到锁和释放锁,11是异常也释放锁,这是一个对象监视器,ObjectMonitor。

 看hotspot,我们会发现每个对象都存在一个ObjectMonitor,这也是重量级锁的核心。

monitor -》MutexLock,把用户态改变成内核态,基于操作系统底层来实现互斥,所以这也是比较耗费性能的原因。

也就是说重量级锁后,一个线程拿到对象监视器,成功后调用指令monitorenter,然后操作同步代码块,其它线程就被放到一个同步队列中,直到完成后调用指令monitorexit来唤醒同步队列中的一个。

总结:

偏向锁-》轻量级锁:一个是通过cas,原子替换;一个是自旋来不断尝试,不会阻塞线程,所以性能高。

重量级锁:通过阻塞来加锁,性能较低。

synchroized关键字把这些都封装到了jvm中,所以越简单的使用,底层的实现其实越复杂。

5、wait、notify、notifyall

线程的通信机制

我们用个代码演示一下:

/**
 * @Author Dark traveler
 * @Note 我心净处,何处不是西天。
 * @Descrption
 * @E-Mail : 1029149772@qq.com
 * @Date : Created in 15:22 2020-3-18
 */
public class ThreadA extends Thread{

    private Object lock;

    public ThreadA(Object lock){
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            System.out.println("start ThreadA");
            try {
                lock.wait();//实现线程的阻塞
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end ThreadA");
        }
    }
}

/**
 * @Author Dark traveler
 * @Note 我心净处,何处不是西天。
 * @Descrption
 * @E-Mail : 1029149772@qq.com
 * @Date : Created in 15:22 2020-3-18
 */
public class ThreadB extends Thread{

    private Object lock;

    public ThreadB(Object lock){
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            System.out.println("start ThreadB");
            lock.notify();//实现线程的唤醒
            System.out.println("end ThreadB");
        }
    }
}

/**
 * @Author Dark traveler
 * @Note 我心净处,何处不是西天。
 * @Descrption
 * @E-Mail : 1029149772@qq.com
 * @Date : Created in 15:26 2020-3-18
 */
public class waitNotifyDemo {
    public static void main(String[] args) {
        Object lock = new Object();
        ThreadA threadA = new ThreadA(lock);
        threadA.start();
        ThreadB threadB = new ThreadB(lock);
        threadB.start();
    }
}

wait:实现线程的阻塞,并且释放当前的同步锁

motify/notifyall:唤醒被阻塞的单个线程(全部线程)

用图来表示一下刚刚的流程

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值