您当前的位置: 首页 > 

程序员一灯

暂无认证

  • 3浏览

    0关注

    152博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

synchronized底层原理分析

程序员一灯 发布时间:2021-10-29 12:10:50 ,浏览量:3

如果某一个资源被多个线程共享,为了避免因为资源抢占导致资源数据错乱,我们需要对线程进行同步,那么synchronized就是实现线程同步的关键字,可以说在并发控制中是必不可少的部分,今天就来看一下synchronized的使用和底层原理。

一、synchronized的特性 1.1 原子性

所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。但是像i++、i+=1等操作字符就不是原子性的,它们是分成读取、计算、赋值几步操作,原值在这些步骤还没完成时就可能已经被赋值了,那么最后赋值写入的数据就是脏数据,无法保证原子性。

被synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放,这中间的过程无法被中断(除了已经废弃的stop()方法),即保证了原子性。

注意!面试时经常会问比较synchronized和volatile,它们俩特性上最大的区别就在于原子性,volatile不具备原子性。

1.2 可见性

可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。

synchronized和volatile都具有可见性,其中synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到主存当中,保证资源变量的可见性,如果某个线程占用了该锁,其他线程就必须在锁池中等待锁的释放。

而volatile的实现类似,被volatile修饰的变量,每当值需要修改时都会立即更新主存,主存是共享的,所有线程可见,所以确保了其他线程读取到的变量永远是最新值,保证可见性。

1.3 有序性

有序性值程序执行的顺序按照代码先后执行。

synchronized和volatile都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。

1.4 可重入性

synchronized和ReentrantLock都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。

二、synchronized的用法

synchronized可以修饰静态方法、成员函数,同时还可以直接定义代码块,但是归根结底它上锁的资源只有两类:一个是对象,一个是类。

先看看下面的代码(初学者看到先不要晕,后面慢慢讲解):


public class TestSysn {
    private int i = 0;
    private static int j = 0;
    private final TestSysn instance = new TestSysn();
    
    // 对成员函数加锁,必须获得该类的实例对象的锁才能进去同步代码块
    public synchronized void addOne(){
        i++;
    }
    
    // 对静态方法加锁,必须获得类的锁才能进入同步块
    public static synchronized void addTwo(){
        j++;
    }
    
    public void method(){
        synchronized (TestSysn.class){
            //同步块,执行前必须获得Test1的锁
        }
        
        synchronized (instance){
            // 同步块,执行前必须先获得实例对象的锁
        }
        
    }
    
}

首先我们知道被static修饰的静态方法、静态属性都是归类所有,同时该类的所有实例对象都可以访问。但是普通成员属性、成员方法是归实例化的对象所有,必须实例化之后才能访问,这也是为什么静态方法不能访问非静态属性的原因。我们明确了这些属性、方法归哪些所有之后就可以理解上面几个synchronized的锁到底是加给谁的了。

首先看第一个synchronized所加的方法是addOne(),该方法没有被static修饰,也就是说该方法是归实例化的对象所有,那么这个锁就是加给TestSysn类所实例化的对象。

然后是addTwo()方法,该方法是静态方法,归TestSysn类所有,所以这个锁是加给TestSysn类的。

最后是method()方法中两个同步代码块,第一个代码块所锁定的是TestSysn.class,通过字面意思便知道该锁是加给TestSysn类的,而下面那个锁定的是instance,这个instance是TestSysn类的一个实例化对象,自然它所上的锁是给instance实例化对象的。

弄清楚这些锁是上给谁的就应该很容易懂synchronized的使用啦,只要记住要进入同步方法或同步块必须先获得相应的锁才行。那么我下面再列举出一个非常容易进入误区的代码,看看你是否真的理解了上面的解释。


public class TestThreadSysn implements Runnable{
    private static int i=0;
    
    private synchronized void add(){
        i++;
    }

    @Override
    public void run() {
        for(int i=0;i偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。

5.1.1 偏向锁

一句话总结它的作用:减少统一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁。

核心思想:

如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。

5.1.2 轻量级锁

轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。

5.1.3 重量级锁

重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。

重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。

5.2 锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。比如下面代码的method1和method2的执行效率是一样的,因为object锁是私有变量,不存在所得竞争关系。

 

5.3 锁粗化

锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。比如下面method3经过锁粗化优化之后就和method4执行效率一样了。

5.4 自旋锁与自适应自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。

自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。

自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。

结语

synchronized关键字是并发编程不可或缺的部分,个人认为能真实理解其内部运作原理能对平时的开发带来很大意义上的帮助,希望这篇文章能帮助你!

 

关注
打赏
1645367472
查看更多评论
0.0374s