简单认识synchronized和volatile关键字

1. synchronized关键字

目的:synchronized关键字是java提供的锁机制,主要解决线程的同步问题,那么它可以修饰方法和同步代码块,那么问题来了,我们什么时候用同步代码块和方法呢,我认为主要看锁对象的范围,一般情况是越小越好。

原理:synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的互斥锁(Mutex Lock)来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。这种依赖于操作系统互斥锁(Mutex Lock)所实现的锁我们称之为“重量级锁”。

锁优化:

优点 缺点 使用场景
偏向锁 加锁和解锁不需要额外的消耗 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU。

追求响应时间。

同步块执行速度非常快。

重量级锁 线程竞争不使用自旋,不会消耗CPU。 线程阻塞,响应时间缓慢

追求吞吐量。

同步块执行速度较长

锁的例子:

实验结果:发现在方法上的代码块所需要的时间比同步代码块的慢。

分析:我们可以看出把锁用在方法上和同步代码块上。锁的力度应该越小越好,因为synchronized关键字是重量级的锁。

synchronized应该注意的点(死锁):

虽然synchronized关键字可以避免线程问题,但是synchronized是比较重的锁,应该合理使用,很容易产生死锁问题。

死锁的产生看似是因为多线程问题产生的,其实是因为我们对锁的操作不当产生的。

分析:这是一个产生死锁的例子,虽然这种程序我们一般不会去写,其实多线程的问题都很少让我们这种初学者去写,但是我们应该对里面的原理有所了解。

产生死锁的条件:

1.形成互斥条件,一次只能够让一个线程进行访问

2.循环等待:线程之间相互进行等待

3.不可剥夺:线程一旦请求资源,如果在没有使用完毕之前,不会对资源进行释放

4.请求与保持:线程因为请求资源而阻塞,会对已经请求的资源进行阻塞。

2.volatile关键字的认识

1. 目的:volatile主要存在关键原因,就是线程与内存存在一定的关系,jvm为了提高代码的运行效率,进行指令排序,这样就会产生两个问题,一个是工作内存和主内存不同步,另一个是会产生对象没有初始化。

java本身通过几个原则性操作保证工作内存和主内存之间的交互

  1. lock:作用于主内存,把变量标识为线程独占状态。
  2. unlock:作用于主内存,解除独占状态。
  3. read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。
  4. load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。
  5. use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。
  6. assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。
  7. use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。
  8. assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。

volatile如何保持内存可见性

volatile的特殊规则就是:

  • read、load、use动作必须连续出现
  • assign、store、write动作必须连续出现

所以,使用volatile变量能够保证:

  • 每次读取前必须先从主内存刷新最新的值。
  • 每次写入后必须立即同步回主内存当中。
  • 指令重排导致单例模式失效

    判断单例模式:

    public class Singleton {

      private static Singleton instance = null;

      private Singleton() { }

      public static Singleton getInstance() {

         if(instance == null) {

            synchronzied(Singleton.class) {

               if(instance == null) {

                   instance = new Singleton();  //非原子操作

               }

            }

         }

         return instance;

       }

    }

    看似简单的一段赋值语句:instance= new Singleton(),但是很不幸它并不是一个原子操作,其实际上可以抽象为下面几条JVM指令:

    memory =allocate();    //1:分配对象的内存空间 

    ctorInstance(memory);  //2:初始化对象 

    instance =memory;     //3:设置instance指向刚分配的内存地址 

    上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:

    memory =allocate();    //1:分配对象的内存空间 

    instance =memory;     //3:instance指向刚分配的内存地址,此时对象还未初始化

    ctorInstance(memory);  //2:初始化对象

    可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。

    在线程A执行这段赋值语句,在初始化分配对象之前就已经将其赋值给instance引用,恰好另一个线程进入方法判断instance引用不为null,然后就将其返回使用,导致出错。

volatile如何处理指令重排序
volatile关键字通过提供“内存屏障”的方式来防止指令被重排序,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

volatile的总结

通过使用volatile发现,虽然volatile可以解决内存可见性问题和指令重排序问题,但是volatile可能产生jvm代码的优化问题,效率可能降低。

那么我们什么时候使用volatile,我个人建议可能存在多个线程访问成员变量时,对变量的写入操作不依赖变量的当前值,如count++就不能同volatile修饰。

synchronized和volatile之间的区别

1.volatile是比较弱的同步机制,在访问volatile变量时,不会进行加锁,也不会阻塞线程。

2.volatile无法保证原子性。

3.从同步机制看volatile关键字,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。

猜你喜欢

转载自blog.csdn.net/weixin_41629878/article/details/83998253
今日推荐