zoukankan      html  css  js  c++  java
  • Java并发编程(03):多线程并发访问,同步控制

    本文源码:GitHub·点这里 || GitEE·点这里

    一、并发问题

    多线程学习的时候,要面对的第一个复杂问题就是,并发模式下变量的访问,如果不理清楚内在流程和原因,经常会出现这样一个问题:线程处理后的变量值不是自己想要的,可能还会一脸懵的说:这不合逻辑吧?

    1、成员变量访问

    多个线程访问类的成员变量,可能会带来各种问题。

    public class AccessVar01 {
        public static void main(String[] args) {
            Var01Test var01Test = new Var01Test() ;
            VarThread01A varThread01A = new VarThread01A(var01Test) ;
            varThread01A.start();
            VarThread01B varThread01B = new VarThread01B(var01Test) ;
            varThread01B.start();
        }
    }
    class VarThread01A extends Thread {
        Var01Test var01Test = new Var01Test() ;
        public VarThread01A (Var01Test var01Test){
            this.var01Test = var01Test ;
        }
        @Override
        public void run() {
            var01Test.addNum(50);
        }
    }
    class VarThread01B extends Thread {
        Var01Test var01Test = new Var01Test() ;
        public VarThread01B (Var01Test var01Test){
            this.var01Test = var01Test ;
        }
        @Override
        public void run() {
            var01Test.addNum(10);
        }
    }
    class Var01Test {
        private Integer num = 0 ;
        public void addNum (Integer var){
            try {
                if (var == 50){
                    num = num + 50 ;
                    Thread.sleep(3000);
                } else {
                    num = num + var ;
                }
                System.out.println("var="+var+";num="+num);
            } catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    

    这里案例的流程就是并发下运算一个成员变量,程序的本意是:var=50,得到num=50,可输出的实际结果是:

    var=10;num=60
    var=50;num=60
    

    VarThread01A线程处理中进入休眠,休眠时num已经被线程VarThread01B进行一次加10的运算,这就是多线程并发访问导致的结果。

    2、方法私有变量

    修改上述的代码逻辑,把num变量置于方法内,作为私有的方法变量。

    class Var01Test {
        // private Integer num = 0 ;
        public void addNum (Integer var){
            Integer num = 0 ;
            try {
                if (var == 50){
                    num = num + 50 ;
                    Thread.sleep(3000);
                } else {
                    num = num + var ;
                }
                System.out.println("var="+var+";num="+num);
            } catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    

    方法内部的变量是私有的,且和当前执行方法的线程绑定,不会存在线程间干扰问题。

    二、同步控制

    1、Synchronized关键字

    使用方式:修饰方法,或者以控制同步块的形式,保证多个线程并发下,同一时刻只有一个线程进入方法中,或者同步代码块中,从而使线程安全的访问和处理变量。如果修饰的是静态方法,作用的是这个类的所有对象。

    独占锁属于悲观锁一类,synchronized就是一种独占锁,假设处于最坏的情况,只有一个线程执行,阻塞其他线程,如果并发高,处理耗时长,会导致多个线程挂起,等待持有锁的线程释放锁。

    2、修饰方法

    这个案例和第一个案例原理上是一样的,不过这里虽然在修改值的地方加入的同步控制,但是又挖了一个坑,在读取的时候没有限制,这个现象俗称脏读。

    public class AccessVar02 {
        public static void main(String[] args) {
            Var02Test var02Test = new Var02Test ();
            VarThread02A varThread02A = new VarThread02A(var02Test) ;
            varThread02A.start();
            VarThread02B varThread02B = new VarThread02B(var02Test) ;
            varThread02B.start();
            var02Test.readValue();
        }
    }
    class VarThread02A extends Thread {
        Var02Test var02Test = new Var02Test ();
        public VarThread02A (Var02Test var02Test){
            this.var02Test = var02Test ;
        }
        @Override
        public void run() {
            var02Test.change("my","name");
        }
    }
    class VarThread02B extends Thread {
        Var02Test var02Test = new Var02Test ();
        public VarThread02B (Var02Test var02Test){
            this.var02Test = var02Test ;
        }
        @Override
        public void run() {
            var02Test.change("you","age");
        }
    }
    class Var02Test {
        public String key = "cicada" ;
        public String value = "smile" ;
        public synchronized void change (String key,String value){
            try {
                this.key = key ;
                Thread.sleep(2000);
                this.value = value ;
                System.out.println("key="+key+";value="+value);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        public void readValue (){
            System.out.println("读取:key="+key+";value="+value);
        }
    }
    

    在线程中,逻辑上已经修改了,只是没执行到,但是在main线程中读取的value毫无意义,需要在读取方法上也加入同步的线程控制。

    3、同步控制逻辑

    同步控制实现是基于Object的监视器。

    • 线程对Object的访问,首先要先获得Object的监视器 ;
    • 如果获取成功,则会独占该对象 ;
    • 其他线程会掉进同步队列,线程状态变为阻塞 ;
    • 等Object的持有线程释放锁,会唤醒队列中等待的线程,尝试重启获取对象监视器;

    4、修饰代码块

    说明一点,代码块包含方法中的全部逻辑,锁定的粒度和修饰方法是一样的,就写在方法上吧。同步代码块一个很核心的目的,减小锁定资源的粒度,就如同表锁和行级锁。

    public class AccessVar03 {
        public static void main(String[] args) {
            Var03Test var03Test1 = new Var03Test() ;
            Thread thread1 = new Thread(var03Test1) ;
            thread1.start();
            Thread thread2 = new Thread(var03Test1) ;
            thread2.start();
            Thread thread3 = new Thread(var03Test1) ;
            thread3.start();
        }
    }
    class Var03Test implements Runnable {
        private Integer count = 0 ;
        public void countAdd() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized(this) {
                count++ ;
                System.out.println("count="+count);
            }
        }
        @Override
        public void run() {
            countAdd() ;
        }
    }
    

    这里就是锁定count处理这个动作的核心代码逻辑,不允许并发处理。

    5、修饰静态方法

    静态方法属于类层级的方法,对象是不可以直接调用的。但是synchronized修饰的静态方法锁定的是这个类的所有对象。

    public class AccessVar04 {
        public static void main(String[] args) {
            Var04Test var04Test1 = new Var04Test() ;
            Thread thread1 = new Thread(var04Test1) ;
            thread1.start();
            Var04Test var04Test2 = new Var04Test() ;
            Thread thread2 = new Thread(var04Test2) ;
            thread2.start();
        }
    }
    class Var04Test implements Runnable {
        private static Integer count ;
        public Var04Test (){
            count = 0 ;
        }
        public synchronized static void countAdd() {
            System.out.println(Thread.currentThread().getName()+";count="+(count++));
        }
        @Override
        public void run() {
            countAdd() ;
        }
    }
    

    如果不是使用同步控制,从逻辑和感觉上,输出的结果应该如下:

    Thread-0;count=0
    Thread-1;count=0
    

    加入同步控制之后,实际测试输出结果:

    Thread-0;count=0
    Thread-1;count=1
    

    6、注意事项

    • 继承中子类覆盖父类方法,synchronized关键字特性不能继承传递,必须显式声明;
    • 构造方法上不能使用synchronized关键字,构造方法中支持同步代码块;
    • 接口中方法,抽象方法也不支持synchronized关键字 ;

    三、Volatile关键字

    1、基本描述

    Java内存模型中,为了提升性能,线程会在自己的工作内存中拷贝要访问的变量的副本。这样就会出现同一个变量在某个时刻,在一个线程的环境中的值可能与另外一个线程环境中的值,出现不一致的情况。

    使用volatile修饰成员变量,不能修饰方法,即标识该线程在访问这个变量时需要从共享内存中获取,对该变量的修改,也需要同步刷新到共享内存中,保证了变量对所有线程的可见性。

    2、使用案例

    class Var05Test {
        private volatile boolean myFlag = true ;
        public void setFlag (boolean myFlag){
            this.myFlag = myFlag ;
        }
        public void method() {
            while (myFlag){
                try {
                    System.out.println(Thread.currentThread().getName()+myFlag);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    3、注意事项

    • 可见性只能确保每次读取的是最新的值,但不支持变量操作的原子性;
    • volatile并不会阻塞线程方法,但是同步控制会阻塞;
    • Java同步控制的根本:保证并发下资源的原子性和可见性;

    四、源代码地址

    GitHub·地址
    https://github.com/cicadasmile/java-base-parent
    GitEE·地址
    https://gitee.com/cicadasmile/java-base-parent
    

  • 相关阅读:
    浏览器嗅探
    (转)javascript中为何在匿名function函数后面还外加一个括号
    js十进制转换二进制
    css_毛玻璃
    css桌布样式
    ocr api 识别表格 图片
    Linux查看日志常用命令
    linux find 命令查找文件和文件夹
    docker上安装airflow
    文件权限中 chmod、u+x、u、r、w、x分别代表什么
  • 原文地址:https://www.cnblogs.com/cicada-smile/p/12594505.html
Copyright © 2011-2022 走看看