美文网首页
android 多线程 — 同步

android 多线程 — 同步

作者: 前行的乌龟 | 来源:发表于2018-06-03 01:25 被阅读335次

    通过上篇博文(android 多线程 — java 内存模型)我们知道了多个线程同时多同一个对象读写可能会造成数据混乱,结果错误。

    同步干啥了


    那么 java 如果解决的这个问题呢,就是同步机制 — synchronized。什么是同步呢,就是让Object 象同一时间只能被一个 Thread 读写。那么又是如何让 Object 同一时间只能被一个 Thread 读写呢,是给每个 Object 里面加一把锁,哪个 Thread 在使用这个 Object 就把这个对象上的锁给谁,直到这个 Thread 执行完对这个 Object 的操作,把 Object 上的锁还给这个 Object ,然后下一个 Thread 才能对这个 Object 进行操作

    synchronized 干的事就是这样,管理对象上锁,只给一个线程对象,保证同一时刻只有一个线程能操作这个对象

    多余我们来说茶不必直接操作对象上的锁,我们只要把对象传给 synchronized 就行,至于是哪个对象,根据实际来选择。

    synchronized


    synchronized 本身是一个关键字,用来修饰普通方法,静态方法和代码块

    修饰方法

    synchronized public static void staticMethod()
    

    修饰代码块

    synchronized (SynchronizedObject.class) {
        xxxxxxxxxx
    }
    

    synchronized 作为关键字,使用是很方便的,看这2个代码片段就能体会到,方法,代码块加了 synchronized 就能在多线程中保持内存同步,同一时间内只能有一个 Thread 进来操作 synchronized 标记的方法和代码块。

    synchronized 用的谁身上的锁

    然后我们进一步思考,synchronized 需要一把对象锁,在 synchronized 修饰方法时,不论是静态方法还是普通方法,都是在方法前面加上 synchronized 就行了,那么和这个 synchronized 对应的锁是用的谁的

    • synchronized 修饰普通方法
      用的是这个方法所在对象的锁

    • synchronized 修饰静态方法
      用的是这个方法所在对象的类的锁

    同步代码块在具体书写时,我们会碰到下面几种使用锁的方式

    // 使用 .class 锁
    synchronized (SynchronizedObject.class) {
        xxxxxxxxxx
    }
    
    // 使用 Object 对象锁
    Object c = new  Object ();
    synchronized (c) {
        xxxxxxxxxx
    }
    
    // 使用当前对象的锁
    synchronized (this) {
        xxxxxxxxxx
    }
    

    this 就是对象的锁一种写法,本质和普通同步方法相同,效果页相同

    多线程并发环境下会造成阻塞,会影响执行效率,所以对象锁的阻塞范围要有清晰的了解,这是 synchronized 的特征,因为 synchronized 本身不带锁,要用别人的。

    不同锁对应的阻塞范围

    synchronized 的锁本质上2种,写法3种:

    • Object.class 类.class锁
    • this 当前对象锁,等同于同步方法思路,这个对象就是容器对象。
    • object 成员变量锁

    我翻了好多书和资料,没看到有人仔细说不同的对象锁对应的阻塞范围,那咱们就自己东西来试试。

    其实我们要搞清楚的就是下面几个问题没,前提是多线程环境下对个线程同时操作同一个对象的方法和成员变量:

    • 调用同一个同步方法会不会阻塞
    • 在别的线程调用同步方法时,非同步方法能不能同时调用,成员变量等同于方法
    • 在别的线程调用同步方法时,该对象的成员变量中的同步方法能不能同时调用
    • Object.class 和 对象锁一样不一样

    测试用例:

    • 我们设计一个对象 Animal, 提供同步和非同步打印方法,连续打印5次,每次间隔1秒。
    • 这个对象有个成员变量 Book,Book 也能提供非同和非同步的打印方法
    • 我们在 UI 线程启动2个 thread 出来,分别调用 animal 对象的方法和 animal 的成员变量 Book 的方法

    Animal 对象

        public class Animal {
    
            public String name;
            public Book book;
    
            public Animal(String name) {
                this.name = name;
                book = new Book("《Android 开发艺术探索》");
            }
    
            public String getName() {
                return name;
            }
    
            public void setName(String name) {
                this.name = name;
            }
    
            public void speak() {
                for (int i = 0; i <= 5; i++) {
                    Log.d("AAA", name + "第 " + i + " 次" + "非同步叫唤," + "Thread: " + Thread.currentThread().getName() + " / Time: " + System.currentTimeMillis());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
    
            synchronized public void speakSynchronized() {
                for (int i = 0; i <= 5; i++) {
                    Log.d("AAA", name + "第 " + i + " 次" + "同步叫唤," + "Thread: " + Thread.currentThread().getName() + " / Time: " + System.currentTimeMillis());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
    
            synchronized public void OhterSynchronized() {
                for (int i = 0; i <= 5; i++) {
                    Log.d("AAA", name + "其他同步方法," + " 第 " + i + " 次" + ", Thread: " + Thread.currentThread().getName() + " / Time: " + System.currentTimeMillis());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    

    Book 对象

        public class Book {
    
            public String name;
    
            public Book(String name) {
                this.name = name;
            }
    
            public String getName() {
                return name;
            }
    
            public void setName(String name) {
                this.name = name;
            }
    
            public void speak() {
                for (int i = 0; i <= 5; i++) {
                    Log.d("AAA", name + "第 " + i + " 次" + "非同步阅读," + "Thread: " + Thread.currentThread().getName() + " / Time: " + System.currentTimeMillis());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
    
            synchronized public void speakSynchronized() {
                for (int i = 0; i <= 5; i++) {
                    Log.d("AAA", name + "第 " + i + " 次" + "同步阅读," + "Thread: " + Thread.currentThread().getName() + " / Time: " + System.currentTimeMillis());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
    
        }
    

    测试1

    2个线程同时调用 animal 的同步方法

    测试代码

            Animal dog = new Animal("汪酱");
    
            Thread t1 = new Thread() {
                @Override
                public void run() {
                    super.run();
                    Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                    dog.speakSynchronized();
                }
            };
    
            Thread t2 = new Thread() {
                @Override
                public void run() {
                    super.run();
                    Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                    dog.speakSynchronized();
                }
            };
    
            t1.start();
            t2.start();
    
    Snip20180602_9.png

    不出所料,同一个对象里同一个同步方法只能有一个 Thread 调用,其他想调用改方法的 Thread 都得在后面排队,也就是阻塞。这是 synchronized 最常见的使用,也是 synchronized 的初衷。

    测试2

    1个线程调用 animal 对象同步方法的同时,另一个线程调用 animal 对象的非同步的普通方法

    测试代码

            Animal dog = new Animal("汪酱");
    
            Thread t1 = new Thread() {
                @Override
                public void run() {
                    super.run();
                    Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                    dog.speakSynchronized();
                }
            };
    
            Thread t2 = new Thread() {
                @Override
                public void run() {
                    super.run();
                    Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                    dog.speak();
                }
            };
    
            t1.start();
            t2.start();
    
    Snip20180602_10.png

    可以看到,同步和非同步方法一起执行。着说明对象锁的阻塞范围不包括非同步方法。这下我们心里有跟了,没有同步标记的方法多线程中没有使用限制

    测试3

    1个线程调用 animal 对象同步方法的同时,另一个线程调用 animal 对象的另一个同步方法

    测试代码

            Animal dog = new Animal("汪酱");
    
            Thread t1 = new Thread() {
                @Override
                public void run() {
                    super.run();
                    Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                    dog.speakSynchronized();
                }
            };
    
            Thread t2 = new Thread() {
                @Override
                public void run() {
                    super.run();
                    Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                    dog.OtherSynchronized();
                }
            };
    
            t1.start();
            t2.start();
    
    Snip20180602_11.png

    可以看到,2个不同的同步方法还是阻塞执行的,同一时间只有一个同步方法能跑。说明对象锁的阻塞范围是这个对象内的所有同步方法的。

    测试4

    1个线程调用 animal 对象同步方法的同时,另一个线程调用 animal 对象成员变量 book 的同步方法

    测试代码

            Animal dog = new Animal("汪酱");
    
            Thread t1 = new Thread() {
                @Override
                public void run() {
                    super.run();
                    Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                    dog.speakSynchronized();
                }
            };
    
            Thread t2 = new Thread() {
                @Override
                public void run() {
                    super.run();
                    Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                    dog.book.speakSynchronized();
                }
            };
    
            t1.start();
            t2.start();
    
    Snip20180602_12.png

    结果可能出乎我们意料,但是想想又是非常合理的,2个同步方法同时执行饿了。着说明对象锁的阻塞范围仅限于自身直接的方法,而对于自身成员变量的同步方法是阻塞不了的,大家想想啊,我的成员变量是个对象,那么这个对象有自己的锁,肯定页不应该受外部容器对象锁的影响

    测试5

    对象锁我们基本摸清规律了,剩下的场景我们也能根据上面的阻塞规则分析出来了,现在我们还要解决 Object.class 的问题,和对象锁一样吗

    我们先来测下静态同步方法,静态方法是属于类的,而不是对象的,这个好理解我们先来测

    给 Animal 对象添加一个静态同步方法,我就不再上 Animal 的代码了,大家想象下,同时 new 2个 animal 对象出来,调用同一个静态同步方法

    测试代码

            Animal dog = new Animal("汪酱");
            Animal dog2 = new Animal("papi酱");
    
            Thread t1 = new Thread() {
                @Override
                public void run() {
                    super.run();
                    Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                    dog.staticSynchronized();
                }
            };
    
            Thread t2 = new Thread() {
                @Override
                public void run() {
                    super.run();
                    Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                    dog2.staticSynchronized();
                }
            };
    
            t1.start();
            t2.start();
    
    Snip20180602_13.png

    不愧为 static 属于类本身一说啊,不管 new 几个同一类型的对象出来,类本身的 static 的静态同步方法同一时刻只能有一个线程调用。

    测试6

    我们来试试在代码块内使用 Object.class ,在 Animal 中添加一个普通方法内有同步代码块,使用 Object.class 的类锁。new 2个 Animal 对象,2个线程同时调用 这个方法

    测试代码

            Animal dog = new Animal("汪酱");
            Animal dog2 = new Animal("papi酱");
    
            Thread t1 = new Thread() {
                @Override
                public void run() {
                    super.run();
                    Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                    dog.codeBlock();
                }
            };
    
            Thread t2 = new Thread() {
                @Override
                public void run() {
                    super.run();
                    Log.d("AAA", Thread.currentThread().getName() + "开始执行......" + " / Time: " + System.currentTimeMillis());
                    dog2.codeBlock();
                }
            };
    
            t1.start();
            t2.start();
    
    Snip20180602_14.png

    恩,可以看到,2个 Animal 对象的 Object.class 同步代码块方法同时只能有一个线程跑。这说明在代码块中使用 Object.class 相当于把这个方法标记为静态同步的。

    总结下对象锁的阻塞范围:

    • 对象锁的阻塞先于自身的同步方法,同步方法没有数量限制,一个线程正在调用对象的摸某一个同步方法,那么此时另一个线程调用这个对象的另一个同步方法也是会被阻塞的
    • 对象锁的不会阻塞非同步的阻塞方法,即使此时一个线程正在调用这个对象的同步方法,其他线程这个时候也是可以调用这个对象的非同步方法的
    • 对象锁的范围仅限自身,对象的成员变量不受外部对象锁的阻塞影响,这符合一个对象一把锁的设计思路
    • 静态同步方法属于类本身,不管这个类有多少个实例,同一时刻只能有一个线程操作这个类的这个静态的同步方法,和对象实例没关系,只和类有关系
    • 同步代码块使用 Object.class 等同于把方法标记为静态同步的
    • 同步代码块使用 this.class 等同于把方法标记为同步的

    synchronized 扯了半天,但是只要我们把 synchronized 搞清楚了,同步基本就没问题了,实际编码时,同步我们都是使用 synchronized 的,synchronized 玩好了就差不多成了。

    并发编程中的三原则


    在并发编程中,有三个关键的概念:可见性、原子性和有序性,只有保证了这三点才能使得程序在多线程情况下获得预期的运行结果。

    这2个原则都是 JVM 虚拟机层面的,我们绝对不了这些,在代码层面体现的也很少,但是我们不能了解啊,了解了这3个特性,的确有助于我们理解多线程中复杂的底层操作。

    1. 可见性

    是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。简单来说,线程的私有内存中对象副本和主内存对象的数据之间就是可见性问题,线程不把他私有内存中的对象副本协会到主内存中,那么对于其他想操作这个对象的线程来说就是"不可见的" = 最新数据不可见,所以此时其他线程可以获取该对象的旧数据。

    在 Java 中 volatile、synchronized 和 final 实现可见性。

    1. 原子性

    对基本类型变量的读取和赋值操作是原子性操作,即这些操作是不可中断的,要么执行完毕,要么就不执行。

    在多线程中,原子性是线程不安全的,其实 原子性 和 有序性是紧密相联的概念,着2个概念理解了,才能明白为啥 原子性 线程不安全

    //语句1
    x =3;    
    
    //语句2
    y =4;    
    
    //语句3
    z = x+y ;
    
    //语句4
    x++;    
    

    这里面的操作只有语句1和语句2是原子性的操作,语句3,4不是原子性的操作;因为再语句3中包括了三个操作,1是先读取x的值,2读取y的值,3将z的值写入内存中。语句4的解释是一样的。一般的一个语句含有多个操作该语句就不是原子性的操作,只有简单的读取和赋值才是原子性的操作。

    在 Java 中 synchronized 同步操作可以保证原子性

    1. 有序性

    即程序执行的顺序按照代码的先后顺序执行,上面讲了因为原子性问题,绝部分操作都是可以再分的,分成多个操作,这其中有对数据的读,改,写等,这些操作速度不一,JVM 为了提高效率,在保证结果相同的前提下,有计划的多这么操作分组,在执行需要等待的操作中,穿插执行其他执行速度块的操作,这叫指令重排序

    指令重排序指的是在 保证程序最终执行结果和代码顺序执行的结果一致的前提 下,改变语句执行的顺序来优化输入代码,提高程序运行效率。

    重排序在单线程中没啥问题,咱们等着执行结果呗,反正重排序保证结果正确。但是在多先撤我那个环境下是存在并发的,你这里对某一个对象的执行重排序了,但是不是瞬间完成的,这时另外的线程可以操作这个对象,那么你这个重排序后的执行可能造成此时对象数据的不正确,会对其他线程使用这个对象产生影响。

    以下面的举个例子:

    int i = 0;              
    boolean flag = false;
    i = 1; //语句1           
    flag = true; //语句2
    

    定义了一个整形和Boolean型变量,并通过语句1和语句2对这两个变量赋值,但是JVM在执行这段代码的时候并不保证语句1在语句2之前执行,也就是说可能会发生 指令重排序。

    再来个例子:

    //线程1:
    context = loadContext(); //语句1
    inited = true; //语句2
     
    //线程2:
    while (!inited) {
        sleep()
    }
    
    doSomethingWithConfig(context);
    

    对于线程1来说,语句1和语句2没有依赖关系,因此有可能会发生指令重排序的情况。但是对于线程2来说,语句2在语句1之前执行,那么就会导致进入doSomethingWithConfig函数的时候context没有初始化。

    Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile是因为其 本身包含禁止指令重排序 的语义,synchronized 是由 一个变量在同一个时刻只允许一条线程对其进行 lock 操作 这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。

    volatile


    Volatile 是面试时最容易问的多线程是知识点了,做 android 开发的朋友也有被问到的,这其实取决于面试官的出身了,要是后台开发出身的,绝对会问。

    volatile 也是一个关键字,修饰变量的,volatile 的特性有4个: 保证可见性 、 非同步、保证有序性、 不保证原子性

    1. 保证可见性

    volatile 修饰的变量,会直接忽略线程私有内存中的副本,CPU 寄存器,高速缓存的副本,直接从主内存中独读取数据,写数据也是直接往主内存中写,这就叫内存可见了,永远保持主内存中的 Volatile 修饰的数据是最新的,那么就能对其他所有线程保证数据永远是最新的

    1. 非同步

    volatile 修饰的变量不是 synchronized 的,不是同步的,同一时间是能被多个线程操作的,所以 volatile 的使用范围比较窄,多用于修饰 static 静态变量

    1. 不保证原子性

    volatile 语义并不能保证变量的原子性。对任意单个volatile变量的读/写具有原子性,但类似于i++、i–这种复合操作不具有原子性,因为自增运算包括读取i的值、i值增加1、重新赋值3步操作,并不具备原子性

    1. 保证有序性

    volatile 能够屏蔽指令重排序:

    • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
    • 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

    以下面的例子为例:

    //flag 为 volatile 变量
    
    x = 2; //语句1
    y = 0; //语句2
    flag = true;  //语句3
    x = 4; //语句4
    y = -1; //语句5
    

    由于flag为volatile变量,因此,可以保证语句1/2在语句3之前执行,语句4/5在其之后执行,但是并不保证语句1/2之间或者语句4/5之间的顺序。

    对于前面举的有关 Context 问题,我们就可以通过将 inited 变量声明为 volatile,这样就会保证 loadContext() 和 inited 赋值语句之间的顺序不被改变,避免出现inited=true但是 Context 没有初始化的情况出现。

    还有我们可以见到的使用 volatile 的经典例子:单例

    public class RxBus {
    
        public static volatile RxBus instance;
    
        public static RxBus getInstance() {
            if (instance == null) {
                synchronized (RxBus.class) {
                    if (instance == null) {
                        instance = new RxBus();
                    }
                }
            }
            return instance;
        }
    }
    

    我们对于静态单例使用了 volatile 就能保证整个方法的执行顺序是按照我们缩写的执行。

    若是我们不加 volatile ,在多线程时指令重排序,一个线程发现 instance 是 null 的就会 new 一个对象出来,此时因为指令冲排序,很可能先在内存 new 一块空间然后赋值给 instance ,然后再去执行实例化对象的操作,对象实例化的操作是比较重的。这是领一额个线程进来,发现 instance 不是 null ,然后就去执行代码,但是此时 instance 实际只是有了一块内存地址,但是对象本身还没初始化,就会产生空指针的问题

    所以 volatile 的使用很严格,我们很少看到 volatile 即使这个原因,用 volatile 实现同步机制很难,还要考虑代码执行顺序的问题,绝逼比 synchronized 难用多了。所以,只有我们需要确保变量可见性的时候,才会使用volatile关键字。

    volatile 露脸的机会不多,还都是经过时间考验的经典用法,约到记下来记好了。

    租后 volatile 的性能开销比锁低很多,这个了解即可,在上面的 synchronized 例子中,可以看到切换锁给不同的线程要好几毫秒,比 new 线程对象并执行都耗费时间多了。

    参考资料:


    相关文章

      网友评论

          本文标题:android 多线程 — 同步

          本文链接:https://www.haomeiwen.com/subject/hcpksftx.html