zoukankan      html  css  js  c++  java
  • Volatile和Synchronized

    线程安全可以概括为三个方面:原子性可见性有序性

    原子性:对于涉及共享变量的操作看做一个整体,在同一时间内,只能由一个线程执行,在其它线程看来,这部分操作要么尚未开始,要么已经完成。Java中,基本类型除了long和double,其它类型变量的写操作都是原子性的。

    可见性:一个线程修改了共享变量后,其它线程能够立即看见改变后的值。

    有序性:即程序按照代码的先后顺序执行。我们写好的代码在执行的时候不一定是按照顺序的,因为虚拟机编译的时候,在保证输出结果不变的情况下可能对代码进行优化,也就是常说的指令重排。在单线程的情况下不会有什么影响,但是多线程的环境下则会有隐患。

    Volatile

    先来看Java内存模型

     线程先从主内存读取到变量,对变量进行修改之后再刷新回主内存。当使用了volatile之后:线程直接读写主内存。

    这就保证了共享变量在线程间的可见性。一个常见的例子就是使用volatile修饰的变量,线程A通过改变变量的值去停止另一个正在运行的线程B。

    class MyTask implements Runnable{
    
        private volatile boolean flag = true;
    
        public void stop(){
            flag = false;
        }
    
        @Override
        public void run() {
            System.out.println("====进入循环====" + flag);
            while (flag){
            }
            System.out.println("====停止循环====" + flag);
        }
    }

    测试

    import java.util.concurrent.*;
    
    public class Main {
        
        public static void main(String[] args) throws Exception {
            ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 15, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5));
    
            MyTask task = new MyTask();
            executor.execute(task);
            Thread.sleep(2000);
            System.out.println("外部调用stop");
            task.stop();
        }
    
    }

    如果不加volatile关键字,则不能停止线程。因为其它线程不能立即看见改变。另外volatile也保证了有序性

    对于volatile变量的写操作,JVM会在该操作之前加入一个释放屏障,操作之后加入一个存储屏障

     释放屏障禁止了volatile写操作和该操作之前的任何读写操作进行重排序。这就保障了实际执行顺序和源代码顺序一样,即保障了有序性。

    volatile虽然能够保障有序性,但是不具有锁那样的排他性,只能够保证所修饰变量写操作的原子性,不能保证其他操作的原子性。

    对于volatile的读操作,JVM会在该操作之前加入一个加载屏障,操作之后加入一个获取屏障

     volatile总结:

    volatile变量的写操作与该操作之的任何读写操作不会被重排序。

    volatile变量的读操作与该操作之的任何读写操作不会被重排序。

    volatile只能保证可见性和有序性,对于包含多个操作的共享区域,不能保证线程安全。

    Synchronized

    先来个简单代码

    package com.demo.tools;
    
    
    public class Demo {
    
        public void hello(){
            synchronized(this) {
                System.out.println("Hello");
            }
        }
    
    }

    编译之后进入classes目录,查看编译后的结构。

    可以看到在代码中用synchronized包起来的代码块前后有两个东西:monitorentermonitorexit,这就涉及了一个叫做Monitor的东西:我们知道对象在内存中分为三个区域(对象头、实例变量,填充数据),而这个Monitor则存储在对象头里。

    当执行到monitorenter,线程尝试获取锁(也就是抢占Monitor);也因为Monitor存在对象头里,所以解释了为什么Java中任意对象都可以作为锁。其中还有个计数器,当为0的时候代表可以获取,当线程获取到了,计数器+1,当执行到monitorexit的时候,线程不占有这个锁,计数器-1。由于Synchronized是可重入锁,也就是在持有当前锁的基础上继续获取当前锁,是可以的,这个时候,计数器继续+1,退出同步区域则-1,直到为0。

    同时Synchronized还维护了一个入口集(Entry Set),这个集合存放等待的线程。

    引申

    JDK1.6的时候,JVM团队做了一系列的锁优化,所以现在的synchronized在一些情况中性能不比ReentrantLock差:

    我们都知道线程的开销主要是上下文切换,挂起线程和恢复线程的操作都需要转入内核态中完成。而很多情况是:共享数据的锁定情况只会持续很短的一段时间,根本不值得挂起恢复。自旋锁由此而来:

    1. 自旋锁:线程不放弃处理器的执行时间,为了让线程等待,而是去执行一个忙循环(自旋)。

    当然,虽然避免了上下文切换的开销,但是它也是占用处理器时间的:如果锁占用时间很短,那么就达到了自旋的目的;反之占用时间很长,那么自旋线程只会白白浪费处理器资源。所以自旋通常都有个限制,自旋次数默认是10次。

    2. 在JDK1.6中引入了适应性自旋:

    自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
    如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。
    如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。

    说白了就是前事不忘后事之师,看到前面那个自旋成功,则本次也认为能够自旋成功;如果前面自旋失败,则本次也八九不离十失败,干脆省略自旋,直接挂起。

    3. 锁消除:指虚拟机即时编译器在运行时,对一些代码上被检测到不可能存在共享数据竞争的锁进行消除。

    4. 偏向锁:这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束,进而转变为轻量级锁。偏向锁可以提高带有同步但无竞争的程序性能。

    5. 轻量级锁:“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为“重量级”锁。

    对于synchronized的升级过程:

    ①:第一次执行到synchronized代码块的时候,锁对象是偏向锁。当线程执行完同步代码块,不释放锁,如果下一次又执行到了同步代码块,先判断持有锁的线程是不是自己(当前持有锁的线程ID存储在对象头里),如果是自己就不用重新加锁;否则就是有人来抢了,此时偏向锁升级为轻量级锁。

    ②:在轻量级锁上继续竞争,没有抢到锁的线程自旋,抢到锁的线程把锁对象的对象头里的线程ID更改为自己。自旋的线程在白白的消耗CPU,这种状态叫busy-waiting,超过了最大自旋次数的限制后,会将轻量级锁升级为重量级锁。后面再来线程尝试获取锁,发现是个重量级锁,就把自己挂起,等待被唤醒恢复。

    总结:之所以说synchronized不好,因为在1.6之前的synchronized直接是重量级锁,然后锁的是整个对象,不如ReentrantLock零活粒度细。而现在的synchronized经过优化之后性能以及很好了,ConcurrentHashMap都在用。还有就是synchronized只能按照偏向锁->轻量级锁->重量级锁的顺序逐渐升级(锁膨胀),不允许降级。

    另外,synchronized和ReentrantLock都是可重入锁(允许同一个线程多次获取同一把锁);synchronized是不可中断锁,Lock接口的实现类比如ReentrantLock都是可中断锁。

    Volatile和Synchronized区别

    volatile只能修饰变量,synchronized可以修饰方法和代码块。
    多线程访问volatile不会阻塞,synchronized会阻塞。
    volatile只保证了变量在多个线程之间的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将工作内存和主内存中的数据做同步处理。

  • 相关阅读:
    Node.js核心模块-net
    ie8兼容rgba写法
    Node.js核心模块-http
    Node.js核心模块-fs文件系统
    js监听滚动结束
    mac本地安装全局包报错npm WARN checkPermissions
    安全测试回顾(一)
    python学习笔记(二):python数据类型
    python学习笔记(一):python简介和入门
    Centos下安装Redis
  • 原文地址:https://www.cnblogs.com/LUA123/p/12023697.html
Copyright © 2011-2022 走看看