并发编程synchronized实现原理

深入学习并发编程中的synchronized

第一章:并发编程中的三个问题

1.1 可见性

是什么?

一个线程对共享变量进行修改,另一个线程立即得到修改后的新值。

private static boolean flag = true;

@Test
public void testSynthonized() throws InterruptedException {
    new Thread(()->{
        while (flag){

        }
    }).start();

    Thread.sleep(2000);

    new Thread(() -> {
        flag = false;
        System.out.println("线程修改了共享变量flag的值为" + flag);
        log.info("线程修改了共享变量flag的值为:{}",flag);
    }).start();
}

结果: 
    可能会出现第一个线程一直while 循环
    但是第二个线程已经修改了flag的值,改为了 false
    所以第一个线程未读取到 第二个线程修改的变量的值

结果: 可能会出现第一个线程一直while 循环 但是第二个线程已经修改了flag的值,改为了 false 所以第一个线程未读取到 第二个线程修改的变量的值

总结:

在并发编程的时候,会出现可见性问题,当一个线程对共享变量进行了修改,另外的线程并没有立即看到修改后的最新值。

为什么?

怎么用?

1.2 原子性

是什么?

在一次或多次操作中,要么所有的操作都执行并且不受其他因素干扰而中断,要么所有的操作都不执行。

为什么?

例如:

num初始化 0,5个线程 同时循环1000次执行 num++ 操作,按理来说最终num的值应该为5000

结果:可能为5000,可能比5000小

为什么会比5000小? 因为num++ 里面是有4个步骤,读取值,修改值。5个线程之间有一些操作会交替执行,读取的不是最新的值,修改之后最终的值也就不符合了,这就是并发编程不能保证原子性问题。

怎么用?

1.3 有序性

是什么?

程序代码在执行过程中的先后顺序,由于Java在编译期以及运行期的优化,导致了代码的执行顺序未必就是开发着编写代码时的顺序。

num = 0;

flage = true;

上面的这 2 句代码,在实际的执行过程中,因为他们之间没有之间的关联关系,所以cpu可能会对其 重新排序 (cpu指令重排),顺序可能会变成

flage = true;

num = 0;

代码的顺便被打乱了,所有可能会出现不能的执行结果。这就是并发编程的时候不能保证程序的有序性。

为什么?

怎么用?

第二章:Java内存模型(JMM)

注意:

1.如果对一个变量执行lock操作,将会清空工作内存中此变量的值(先清空值,再lock)

2.对一个变量执行unlock操作,必须把此变量同步到主内存中(先把工作内存的变量值同步到主内存中,再执行unlock操作)

 

synchronized保证原子性的原理:

对num ++ 增加同步代码块后,保证同一时间只有一个线程操作number++;就不会出现线程安全的情况。

小结:synchronized保证原子性的原理,synthronized保证只有一个线程拿到锁,能够进入同步代码块。

synchronized保证可见性的原理:

volatile 修饰的属性可以解决可见性问题,

原理:volatile存在一个缓存一致性协议,当有线程修改共享变量的值的时候,会去将其他线程从主内存读取到的备份变量设置为失效,当其他线程需要使用改变量的时候,会重新到主内存读取最新的变量值、11

 

使用synchronized 来保证戏线程的可见性

原理:

当使用到synthronized的时候,底层实现是 lock 和 unlock操作,所以就能够保证可见性。

 

使用打印语句 sout 的时候也可以保证可见性:

原理:因为打印语句底层的实现也是使用了synchronized关键字,所以会主内存中读取最新的变量值

 

 

小结:

synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作,会刷新工作内存中共享变量的值。

synchronized保证有序性的原理:

为什么要重排序?

为了提高程序的执行效率,我们所写的代码顺序, 不一定是CPU执行代码的最佳顺序,所以CPU和编译器会对程序中代码重排序

重排序的规则?

as-if-serial 语义

定义:不管编译器和CPU如何对代码重排序,必须保证在单线程的情况下程序的结果是正确的。

synchronized后,虽然进行了重排序,保证只有一个线程进入同步代码块,也能保证有序性。

小结:

synchronized保证有序性的原理,我们加synchronized后,依然会发生重排序,只不过,我们有同步代码块,可以保证只有一个线程执行同步代码块中的代码,保证有序性。

synchronized保证可重入的原理:

可重入是什么?

原理:

synthronized的锁对象中有一个计数器(recurSion变量)会记录线程获得几次锁。

可重入锁的好处?

1.避免死锁

2.方便我们更好的的封装代码

 

流程:

1.线程1 先取到MyThread.class的锁,对应的程序计数器会 +1 ,变为1

2.线程1 进入到加锁的同步代码块里面(它已经拿到MyThread.class的锁,所以能访问,)此时程序计数器+1,变为2。

3.当执行完,退出对应的代码块的时候 程序计数器会 -1,-1。程序计数器 = 0 的时候代表没有线程获取到MyThread.class的锁,然后其他线程就可以竞争获取到这个锁

4.线程2 获取到MyThread.class的锁,然后走线程1的逻辑。

不管加锁的是同步代码块是在,在不能的方法中,都可以取到锁

总结:

synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁,在执行完同步代码块时,计数器的数量会 -1 ,直到计数器的值为0,就会释放该锁。

synchronized的不可中断特性:

学习synchronized的不可中断特性

学习lock的可中断特性

是什么(什么是不可中断)?

一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。

为什么?

怎么用?

synchronized是不可中断,处于阻塞状态的线程会一直等待锁。

lock锁是不可中断的锁

private static Lock lock = new ReentrantLock();

/**
 * lock的不可中断和可中断
 */
@Test
public void testLockInte() throws InterruptedException {
    Runnable runnable = ()->{
        try {
            lock.lock();
            String name = Thread.currentThread().getName();
            System.out.println(name + "获得锁,进入锁执行");
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
            System.out.println("释放锁");
        }
    };

    Thread t1 = new Thread(runnable);
    t1.start();
    Thread.sleep(1000);
    Thread t2 = new Thread(runnable);
    t2.start();

    System.out.println("停止线程t2前");
    t2.interrupt();
    System.out.println("停止线程t2后");

    Thread.sleep(1000);
    System.out.println("t1" + t1.getState());
    System.out.println("t2" + t2.getState());
}

结果:

第一个线程拿到锁

第二个线程拿不到锁,然后就会进入waitting 状态,就会一直等待或者阻塞

Thread-1获得锁,进入锁执行
停止线程t2前
停止线程t2后
t1 TIMED_WAITING
t2 WAITING

小结:

不可中断是指,当一个线程获得锁后,另一个线程一直处于阻塞或等待状态,前一个线程不释放锁,后一个线程会一直阻塞或等待,不可被中断。

synchronized属于不可被中断

Lock的lock方法是不可中断的,

Lock的tryLock方法是可中断的

Java反汇编

每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。当jvm执行某个线程的方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。过程如下:

1.当monitor的进入数为0,线程可以进入monitor,并将monitor的进入数设置为1。当前线程成为monitor的ower(所有者)

2.若线程已拥有monitor的所有权,允许它重入monitor,则monitor的进入数加1

3.若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新获取monitor的所有权。

 

monitor小结:

synchronized的锁对象会关联monitor,这个monitor不是我们主动创建的对象,是jvm的线程执行到这个同步代码块,发现锁对象没有monitor就会创建monitor(c++对象)

monitor的内部有2个重要的成员变量 1.owner;拥有这把锁的线程,2.recursions:会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程就只能等待

monitorexit

 

面试题:synchronized出现异常会释放锁?

 

同步方法

 

同步方法在反汇编之后,会增加ACC_SYNCHRONIZED修饰,会隐式调用monitorenter,和monitorexit,

在执行同步方法前会调用monitorenter,

在执行完同步方法之后会调用monitorexit。

小结

过javap反汇编我们可以看到synchronized使用编程了monitorenter和monitorexit两个指令,每个锁对象都关联一个monitor(监视器,它就是真正的锁对象),

它内部有两个重要的成员变量owner会保存获得锁的线程,

recursions会保存线程获取锁的次数。

当执行到monitorexit的时候,recursions会-1,当计数器减到0 的时候,线程会释放锁。

面试题:synchronized 和 lock的区别

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值