• 多线程编程学习二(对象及变量的并发访问).


    一、概念

    非线程安全:会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是"脏读",也就是取到的数据其实是被更改过的.
    线程安全:获得的实例变量的值是经过同步处理的,不会出现脏读的现象。

    二、synchronized 同步方法

    1、非线程安全的问题存在于实例变量中,如果变量是方法内部的私有变量,则不存在"非线程安全"的问题,永远是线程安全的,这是方法内部的变量是私有的特性造成的。

     

    2、如果访问的是类的实例变量,并且方法没有加synchronized,则会造成多个线程误修改了同一个变量值,导致线程不安全的问题,这个问题上一篇博文已经提到过了。

    3、调用关键字synchronized声明的方法一定是排队运行的。另外需要牢牢记住“共享”这两个字,只有共享资源的读写访问才需要同步化,如果不是共享资源,那么根本没有同步的需要。也就是说,如果不同的线程,访问的都不是同一个实例变量,那么连线程对资源的争抢都不存在,哪里来的线程不安全的问题呢?所以也没有必要进行同步了。

    4、synchronized 方法的锁 为这个类实例对象所持有,也就是说,一个Object对象中的不同synchronized方法 实际上持有的同一把锁,同属于Object的实例:

    (1) A线程先持有object对象的Lock的锁,B线程可以以异步的方式调用object对象中的非synchronized 类型的方法、
    (2) A线程先持有object对象的Lock的锁,B线程如果在这时调用object对象中的synchronized类型的方法则需要等待,也就是同步。
     
    5、脏读一定会出现操作实例变量的情况下,这就是不同线程“争抢”实例变量的结果。
     
    6、"可重入锁":自己可以再次获取自己的内部锁,也就是在synchronized时,当一个线程得到一个对象锁时,再次请求此对象锁时是可以再次得到该对象的锁的。另外,可重入锁也支持在父子类继承的环境中,同一个对象锁不同的synchronized方法执行的顺序按照调用的顺序执行。
     
    7、当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

    8、同步不具有继承性,也就是说当子类继承父类的synchronized方法时,子类的方法是不具有同步性、不是线程安全的,synchronized关键字不能继承,如果子类的方法需要同步性,则需要手动加上synchronized关键字。

    三、synchronized同步语句块

    1、synchronized 同步方法存在一定的弊端,synchronized 同步方法中 没有对实例变量操作的那部分代码也需要进行线程等待,也带有锁机制。可是对程序来说,那部分代码完成可以异步执行,减少等待时间,提高运行效率,这样就有了synchronized同步语句块。

    2、当两个并发线程访问同一个对象Object 中的synchronized(this)同步代码块时,一段时间只能有一个线程被执行,另一个线程必须等待当前线程执行完这个代码块后才执行该代码块。

    3、和synchronized方法一样,synchronized(this)代码块也是锁定当前对象的。当然,Java还支持对“任意对象” 作为锁对象 来实现同步的功能。这个“任意对象”大多数是实例变量 及方法的参数,使用格式为synchronized(非 this 对象)。

    • 多个线程的锁对象 为同一个 非this 对象时,同一时间只有一个线程可以执行synchronized(非 this 对象)同步块中的代码。
    • 多个线程的锁对象 不为同一个 非this 对象时,synchronized(非 this 对象)中的代码是可以异步执行的。

    锁非this对象的优点:如果在一个类中有很多个synchronized方法,这时虽然能实现同步,但会受到阻塞,所以影响运行效率;但如果使用同步代码块锁非this对象,则synchronized(非this)代码块中的程序和同步方法是异步的,不与其它锁this对象同步方法争抢this锁,则可以大大提高运行效率。

    4、判断多线程是同步还是异步执行synchronized 的依据就是:(只要对象没变,即使对象的属性被改变,运行的结果还是同步的。)

    • 多线程如果持有相同的锁对象,则这些线程之间就是同步的。
    • 多线程如果分别获得锁对象,则这些线程之间就是异步的。

    5、关键字synchronized 还可以应用在static静态方法上,这样就是对当前的*.java文件对应的Class类进行持锁。这个可以参考我的这篇博客

    6、要特别注意String常量池缓存的功能,因为可能两个String对象引用的是同一段内存空间。因此在大多数情况下,同步synchronized代码块都不使用String作为锁对象,而改用其他,比如new Object() 实例化一个Object对象,但他不放入缓存中。

    7、程序中应避免出现死锁,死锁出现的原因是因为存在锁之间的交叉引用,两个线程都在等待对方释放锁:

    四、volatile 关键字

    1、多线程中存在私有堆栈中的值 和 公共堆栈中的值不同步的问题。什么意思呢?可能线程在一个地方修改了内存中变量的值,而其它地方线程却从私有堆栈中去读取不一致的变量值。

    2、关键字 volatile 可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从主内存中获取,而对它的改变必须同步刷新回主内存,它能保证所有线程对变量访问的可见性。

    volatile private boolean running=false; //这样定义一个变量后,强调多线程running的读取是直接从内存读

    3、synchronized 和 volatile的区别?

    • 关键字volatile 是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能用于修饰变量,而synchronized 可以修饰方法以及代码块。
    • 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
    • volatile 能保证数据的可见性、有序性,但不能保证原子性;而 synchronized 可以保证原子性,也可以间接保证可见性,因为它会把私有内存和公有内存中的数据做同步。(原子性:原子操作是不可分割的整体,没有其他线程能够中断或检查正在原子操作中的变量,可以在没有锁的情况下保证安全)
    • 总之,关键字 volatile 解决的是变量在多个线程之间的可见性;而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

    4、关键字volatile 出现非线程安全的原因:

    • read 和 load 阶段:从主存复制变量到当前线程工作内存。
    • use 和 assign 阶段:执行代码,改变共享变量值。
    • store 和 write 阶段:用工作内存数据刷新主存对应变量的值。

        在多线程环境中,use和assign 是多次出现的,但这一操作并不是原子性,也就是说在read和load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,也就是私有内存和公有内存的变量不同步,所以计算出来的结果和预期不一样,也就出现了非线程安全的问题。

        对于用volatile修饰的变量,JVM虚拟机只是保证从主内存加载到线程工作内存的值是最新的,例如线程 1 和线程 2 在进行read 和load 的操作中,发现主内存中count的值都是5,name就会加载这个最新的值,也就是说,volatile关键字解决的是变量读时的可见性问题,但无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步。

    5、除了使用synchronized关键字外,还可以使用 AtomicInteget 原子类实现同步。但是在具有逻辑性的情况在,原子类也并不完全 安全,原因在于虽然原子类的方法是原子的,但是方法和方法之间的调用却不是原子的(这个时候仍然需要synchronized进行同步)。

    public class AddCountThread extends Thread {
        private AtomicInteger count =new AtomicInteger(0);
    
        @Override
        public void run() {
            super.run();
            for (int i=0;i<10000;i++){
                System.out.println(count.incrementAndGet());
            }
        }
    }

    6、关键字synchronized 不仅可以使多个线程访问同一个资源具有同步性,而且他还具有将线程工作内存中的私有变量和公共内存的变量进行同步的功能。它包含两个特征:互斥性和可见性。synchronized 本质上是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由 synchronized 所保护对象的监视器。(任何对象都有自己的监视器)

    7、根据 Java 内存模型的 happen before原则,对 volatile 字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值。

    8、学习多线程并发,要着重“外练互斥,内修可见”。

  • 相关阅读:
    Prometheus监控node-exporter常用指标含义
    Go 程序开发的注意事项
    kafka集群安装和使用
    storm集群的安装
    如何用zabbix监控mysql多实例
    企业环境下用脚本设置ubuntu防火墙
    使用教程:宝塔服务器管理助手Linux面版
    Zabbix是什么?
    小白都能看懂的Linux系统下安装配置Zabbix
    Linux:检查当前运行级别的五种方法
  • 原文地址:https://www.cnblogs.com/jmcui/p/7489805.html
Copyright © 2020-2023  润新知