欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

synchronized内置锁原理

程序员文章站 2022-12-20 18:37:35
前言 并发编程是java中不可或缺的模块。与串行程序相比,它们能使复杂的异步代码变得简单,从而极大地简化了复杂系统的开发。此外,想要充分发挥多处理器系统的强大计算能力,最简单的方式就是使用线程。随着处理器数量的持续增长,如何高效地使用蝙蝠正变得越来越重要。同时在当今互联网的时代,大量的互联网应用都面 ......

前言

并发编程是java中不可或缺的模块。与串行程序相比,它们能使复杂的异步代码变得简单,从而极大地简化了复杂系统的开发。此外,想要充分发挥多处理器系统的强大计算能力,最简单的方式就是使用线程。随着处理器数量的持续增长,如何高效地使用蝙蝠正变得越来越重要。同时在当今互联网的时代,大量的互联网应用都面对着海量的访问请求,因此,并发编程在我们的应用中占的比重越来越大。

为什么会有并发安全

在讲并发编程前,我们先来看段代码:

public class unsafedemo {

    integer icount = 0;

    /**声明不变对象,用于加锁*/
    final object obj = new object();
    
    /**
     * 加1操作
     */
    public void addcount() {
        icount++;
    }
    
    /**
     * 使用内置锁 --加1操作
     */
    public void addcount1() {
        synchronized (obj){
            icount++;
        }
    }

    /**
     * 获取count的值
     *
     * @return int
     */
    public int getcount() {
        return icount;
    }

    /**
     * 启动方法
     *
     * @param args 参数
     */
    public static void main(string[] args) throws interruptedexception {
        unsafedemo unsafedemo = new unsafedemo();
        //新建2个线程执行+1的方法
        thread thread1 = new countaddthread(unsafedemo);
        thread thread2 = new countaddthread(unsafedemo);
        thread1.start();
        thread2.start();

        //阻塞等待程序执行
        thread.sleep(1000);
        system.out.println("count进行多线程相加后的结果:"+unsafedemo.getcount());
        /**
         * count进行多线程相加后的结果:17296
         * count进行多线程相加后的结果:11913
         * count进行多线程相加后的结果:10375
         */
    }

    /**
     * 定义一个私有的线程类
     */
    private static class countaddthread extends thread {

        private unsafedemo unsafedemo;

        public countaddthread(unsafedemo unsafedemo) {
            this.unsafedemo = unsafedemo;
        }

        @override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                unsafedemo.addcount();
            }
        }
    }

}

按照我们程序的思路,输出的结果应该是20000,但实际上,输出的结果并不确定。因为线程的运行是靠cpu来随机决定的;i++是非线程安全的,非原子性的操作(一共分3步,获取i的值,将i的值加1,将结果写入到i中);

线程1首先将元空间的icount值读取到线程1的本地缓存中,然后线程1将icount的值进行改变,然后再将改变后的icount的值写入到元空间中。在线程1执行的时候,可能线程2也在进行同样的操作,就出现了值覆盖的情况。由此可以引出并发安全的本质:

  1. 原子性:在一个操作中,cpu不可以在中途暂停然后再执行,即不可以被中断操作,要么执行完成,要么不执行
  2. 可见性:必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 。(volatile只保证可见性不保证原子性)
  3. 有序性:线程之间必须是有序的访问共享变量

对于这种情况,就需要对程序加锁操作。今天我们就来聊聊synchronized内置锁。

synchronized使用

synchronized是java提供的一个并发控制的关键字,可以修饰一个类、修饰一个方法、修饰代码块、修饰静态代码。保证了代码的原子性和可见性以及有序性,但是不会处理重排序以及代码优化的过程,但是在一个线程中执行肯定是有序的,因此是有序的。

synchronized原理

/**
* 对加1操作加上synchronized内置锁,这样在执行的时候,就能达到我们理想的效果,输出结果为20000
*/
public void addcount() {
    synchronized (this){
   		 icount++;
    }
}

那为什么synchronized能解决原子性,可见性,有序性这几个问题呢?

先来了解一下几个名词:

无锁状态:没有加锁

偏向锁:同步代码块第一次进入的时候,会生成偏向锁

轻量级锁:偏向锁的线程没有结束,此时又有其他的线程进入,进行锁升级,其他线程自旋

重量级锁:轻量级锁没有释放,其他线程一直休眠没有获取锁。重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的mutexlock(互斥锁)来实现的,所以重量级锁也称为互斥锁。

自旋:cpu空跑,让等待的线程不放弃cpu执行时间,而是执行一个自旋(一般是空循环);即do{ }while(自旋的次数) 循环。自旋次数可以通过参数 -xx:preblockspin 来修改。

锁消除:虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。一般根据逃逸分析的数据支持来作为判定依据。

在了解锁的时候,首先需要了解java的对象头,所有的锁的实现都依赖于对象头。

synchronized内置锁原理

以32位的操作系统为例,对象头的运行时数据(mark word)的默认存储结构如下表所示:

synchronized内置锁原理

回到上面的例子,来查看synchronized的原理,理解锁膨胀的过程(即synchronized内置锁的原理):

1.无锁状态:默认偏向锁为0;

synchronized内置锁原理

2.偏向锁:当有一个线程进入同步代码块,将锁对象头的hashcode改成线程1的id,同时标志成偏向锁。同时线程2也访问到了同步代码块,发现锁对象头的id并不是自己的id;但是线程2还是要得到得到锁,于是就去尝试修改对象头的hashcode值。

synchronized内置锁原理

3.轻量级锁:如果线程1刚好释放锁,那线程2就能修改成功,获得锁。但如果线程1一直持有锁,线程2修改失败,那线程2就进行锁消除,同时虚拟机对线程1的锁升级为轻量级锁。

4.重量级锁:虚拟机会有线程1和线程2分配一块各自的内存空间,并且把锁复制到各自的空间中,通知将锁状态变成空的。同时把锁状态变成线程对应的id。此时线程2进入自旋(不释放cpu),当线程2自旋到一定的程度(上面的自旋次数),线程2进入睡眠状态,释放cpu。

线程1执行完毕,释放轻量级锁,这时候发现,锁对象的指针并不是指向自己,开始释放锁,并唤醒所有休眠的线程。

synchronized内置锁原理

cas(compare and swap)机制

要谈cas机制,还是先来段代码,跟前文的代码差别不大。就是将int换成了atomicinteger,将addcount和getcount方法改变了一下。可以看到执行出来的结果,跟使用synchronized的效果一样。并且在某些情况下,代码的性能会比synchronized更好。

public class volatiledemo {

    atomicinteger count =new atomicinteger(0);

    public void addcount(){
        count.incrementandget();//i++
    }

    public int getcount(){
        return count.get();
    }

    /**
     * 启动方法
     *
     * @param args 参数
     */
    public static void main(string[] args) throws interruptedexception {
        volatiledemo volatiledemo = new volatiledemo();
        //新建2个线程执行+1的方法
        thread thread1 = new volatiledemo.countaddthread(volatiledemo);
        thread thread2 = new volatiledemo.countaddthread(volatiledemo);
        thread1.start();
        thread2.start();

        //阻塞等待程序执行
        thread.sleep(100);
        system.out.println("count进行多线程相加后的结果:"+volatiledemo.getcount());
        /**
         * count进行多线程相加后的结果:20000
         * count进行多线程相加后的结果:20000
         * count进行多线程相加后的结果:20000
         */
    }

    /**
     * 定义一个私有的线程类
     */
    private static class countaddthread extends thread {

        private volatiledemo volatiledemo;

        public countaddthread( volatiledemo volatiledemo) {
            this.volatiledemo = volatiledemo;
        }

        @override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                volatiledemo.addcount();
            }
        }
    }
}

来看看atomicinteger#incrementandget()的源码:

/**
* atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementandget() {
	return unsafe.getandaddint(this, valueoffset, 1) + 1;
}

public final int getandaddint(object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getintvolatile(var1, var2);
    } while(!this.compareandswapint(var1, var2, var5, var5 + var4));

    return var5;
}

public final native boolean compareandswapint(object var1, long var2, int var4, int var5);

可以看到getandaddint()里面是使用了do{ }while()循环,也就是一个自旋。调用的compareandswapint()是使用native修饰的,去内存中查询新值和旧值并比较。

而atomic操作类的底层正是用到了“cas机制”。上文讲到的自旋也是使用了cas机制。cas的核心是利用unsafe对象实现的。unsafe包是用来帮助java访问操作系统底层资源的类。通过unsafe,java具有了操作底层的能力,可以提升运行效率。

那么什么是cas机制呢?

cas机制中使用了3个基本操作数:内存地址v,旧的预期值a,要修改的新值b。

更新一个变量的时候,只有当变量的预期值a和内存地址v当中的实际值相同时,才会将内存地址v对应的值修改为b。否则不进行任何操作。

从思想上来说,synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,cas属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。

synchronized内置锁原理

cas机制的优点:

  1. 直接操作底层,在并发量小的时候,可以提搞效率

cas机制的缺点:

  1. ​ 循环开销大
  2. 只能保证单独共享变量的原子操作
  3. aba问题,当一个值从a变成b,又更新回a,普通cas机制会误判通过检测。利用版本号比较可以有效解决aba问题。

引用: