Android多线程编程__同步

版权声明:https://blog.csdn.net/petterp https://blog.csdn.net/petterp/article/details/88369372

目录

重入锁和条件对象

同步方法

同步代码块

volatile

 Java的内存模型

原子性

可见性

有序性

Volatile 关键字

volatile不保证原子性

volatile保证有序性

 正确使用volatile 关键字

volatile使用场景


 在多线程应用中,两个或两个以上的线程需要共享对同一个数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法,这种情况通常被称为竞争条件。而解决这种问题的办法通常是当线程A调用修改对象方法时,我们就交给它一把锁,等他处理完后在把锁给另一个要调用这个方法的线程。

重入锁和条件对象

synchronized 关键字提供了锁以及相关的条件。大多数需要显示锁的情况使用 synchronized 非常方便,但是等我们了解重入锁和条件对象时,能更好的理解 synchronized 关键字。重入锁 ReentrantLock 是Java se5.0引入的,就是支持重进入的锁,他表示锁能够支持一个线程对资源的重复加锁。

 Lock  lock = new ReentrantLock();
        try {
            ...
        }catch (Exception e){
            lock.unlock();
        }

 Demo如下

class Test implements Runnable {
    private Boolean on;

    public Test(Boolean on) {
        this.on = on;
    }

    @Override
    public void run() {
        new MyClass().getData(on);
    }
}

public class MyClass {
    private static Lock lock;
    private static Condition condition;

    public static void main(String[] args) {
        lock = new ReentrantLock();
        
        //我们这个线程已经获取了锁,具有排他性,别的线程无法获取锁,此时我们需要引入条件对象
        //一个锁对象拥有多个相关的条件对象。
        //得到条件对象
        condition = lock.newCondition();
        Thread a = new Thread(new Test(true));
        Thread b = new Thread(new Test(false));
        a.start();
        b.start();
    }

    void getData(Boolean on) {
        lock.lock();
        System.out.println("我被锁住了");
        try {
            if (on) {
                System.out.println("我阻塞了,放弃锁");
                condition.await();
            }
             condition.signalAll();
            System.out.println("解除阻塞");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

一旦一个线程调用await 方法,他就会进入该条件的等待集并处于阻塞状态,直到另一个线程调用了同一个条件的 signalAll 方法时为止。

当调用 singalAll 方法时并不是立即激活一个等待线程,他仅仅解除了等待线程的阻塞,以便这些线程能够在当前线程退出同步方法后,通过竞争实现对对象的访问。还有一个方法时 sinal ,它则是随机解除某个线程的阻塞,如果该线程任然不能运行,则再次被阻塞。 如果没有其他线程再次调用 singal ,那么系统就死锁了

同步方法

Java 的每一个对象都有一个内部锁,如果一个方法用 synchronized 关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。

 public synchronized  void method(){
        
    }
    
    等价于下面这个Demo
    
    Lock lock=new ReentrantLock();
    public void method(){
        try {
            ...
        }finally {
            lock.unlock();
        }
    }

对于上面的例子,我们可以用 synchronized 修饰getData ,而不是使用一个显示锁。内部对象锁只有一个相关条件, wait 方法将一个线程添加到等待集中, notifyAll 或者 notify 方法解除等待线程的阻塞状态。也就是说 wait相当于调用 await(), notifyAll 等价于 condition.signalAll(), 上面的Demo改为下面这样

class Test implements Runnable {
    private Boolean on;

    public Test(Boolean on) {
        this.on = on;
    }

    @Override
    public void run() {
        new MyClass().getData(on);
    }
}

public class MyClass {

    public static void main(String[] args) {
        Thread a = new Thread(new Test(true));
        Thread b = new Thread(new Test(false));
        a.start();
        b.start();
    }

   synchronized  void getData(Boolean on) {
        System.out.println("我被锁住了");
        try {
            while (on) {
                System.out.println("我阻塞了,放弃锁");
                wait();
            }
             notifyAll();
            System.out.println("解除阻塞");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


}

 

同步代码块

每一个java对象都有一个锁,线程可以调用同步方法来获得锁。还有一种机制可以获得锁,那就是使用一个同步代码块。

 synchronized (this){
        
 }

 同步代码块是非常脆弱的,通常不推荐使用。一般实现同步最好使用 java.util.concurrent包下提供的类,比如阻塞队列。如果同步方法适合你的程序,那么请尽量使用 同步方法,这样可以减少编写代码的数量,减少出错的概率。如果特别需要使用Lock/Condition结构提供的独有特性时,才使用Lock/Condition.

volatile

有时仅仅为了读写一个或两个实例域就使用同步的话,显得开销过大;而volatile 关键字为实例域的同步访问提供了免锁的机制。如果声明一个域 为 volatile ,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。

学习volatile之前,我们需要了解一下内存模型的相关概念以及并发编程中的3个特性:原子性,可见性,有序性

 Java的内存模型

Java中的堆内存用来存储对象实例,堆内存是被所有线程共享的运行时内存区域,因此,他存在内存可见性的问题。而局部变量,方法定义的参数则不会再线程之间共享,他们不会有内存可见性的问题,也不受内存模型的影响。

Java内存模型定义了线程和主存之间的抽象关系:线程之间的共享变量存储在主存中,每一个线程都有一个私有的本地内存,本地内存中存储了该线程共享变量的副本。需要注意的是本地内存是Java 内存模型的一个抽象概念,其实并不真实存在,它涵盖了缓存,写缓冲区,寄存器等区域。Java内存模型控制线程之间的通信,他决定一个线程对主存共享变量的写入核实对另一个线程可见。

线程A 和 线程B 之间若要通信的话,必须要经历下面两个步骤:

  1. 线程A把线程A本地内存中更新过的共享内存刷新到主存中去。

  2. 线程 B到主存中去读取线程A之前已更新过的共享变量。由此可见,如果我们执行下面的语句: i=3;

    执行线程必须先在自己的工作线程中对变量 i 所在的缓存进行赋值操作,然后再写入主存中,而不是直接将数值3写入到主存中

 

原子性

对基本数据类型的变量的读取和赋值时原子性操作,即这些操作是不可以被中断的,要么执行完毕,要么就不执行。看一下下面的代码,如下:

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

在上面3个语句中,只有语句1是原子性操作,其他两个语句都不是原子性操作。

语句2虽说很短,但他包含了两个操作,它先读取x的值,在将x的值写入工作内存。读取x的值以及将x的值写入工作内存这两个操作单拿出来都是原子性操作,但是合起来就不是原子性操作了。

语句3包含3个操作: 读取x的值,对x的值进行+1,向工作内存写入新值。

所以,当一个语句包含多个操作时,就不是原子性操作,只有简单的读取和赋值(将数字赋给某个变量)才是原子性操作。

可见性

可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果 ,另一个线程马上就可以看到。当一个共享变量被 volatie修饰时,它会保证修改的值立即被更新到主存,所以随其他线程是可见的。当有其他线程需要读取该值时,其他线程会去主存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,并不会立即写入主存,何时被写入主存也是不确定的。当其他线程去读取该值时,此时主存中可能还是原来的旧值,这样就无法保证可见性。

有序性

Java内存模型允许编译器和处理器对指令进行重排序,虽然重排过程不会影响到单线程执行的正确性,但是会影响到多线程并发执行的正确性。这时可以通过 valatie 来保证有序性,除了 voliatie ,也可以通过 synchronized 和 Lock 来保证有序性。syncheonized 和 Lock 保证每个时刻只有一个线程执行同步代码,这相当于让线程顺序执行同步代码,从而保证了有序性。

Volatile 关键字

当一个共享变量被 volatile 修饰之后,其就具备了两个含义,一个是线程修改了变量的值时,变量的新值对其他线程是立即可见的。换句话说,就是不同线程对这个变量进行操作时具有可见性。另一个含义是禁止使用指令重排序。

看下面这个Demo

假设线程1先执行,线程2后执行

	//线程1
        boolean stop=false;
        while (!stop){
            
        }
        
        //线程2
        stop=true;

上面这段代码可以用于中断线程,但是这段代码不一定会将线程中断。虽然概率很小。

为什么说有可能无法中断线程呢?

 每个线程在运行时都有私有的工作内存,因此线程1在运行时会将stop变量的值复制一份放在私有的工作内存中。当线程2更改了Stop变量的值后,线程2突然需要去做其他的操作,这时就无法将更改的Stop变量写入到主存中,这样线程1就不会知道线程2对Stop变量进行了更改,因此线程1就会一直循环下去。当Stop用volatile修饰之后,那么情况就变的不同了,当线程2进行修改时,会强制将修改的值立即写入主存,并且会导致线程1的工作内存中变量stop的缓存无效,这样线程1再次读取stop的值时就会去主存读取。

volatile不保证原子性

我们知道 volatile 保证了操作的可见性,但是是否能保证原子性呢?

class A{
    public volatile int a=0;
    public   void setA(){
        a++;
    }
}

public class  MyClass{
    public static void main(String[] args) {
        final A a=new A();
        for (int i=0;i<10;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i=0;i<1000;i++){
                        a.setA();
                    }
                }
            }).start();
        }
        //如果有子线程就让出资源,保证所有子线程都执行完
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(a.a);
    }
}

这段代码每次运行,结果都不一致。因为自增不具备原子性,它包括读取变量的原始值,进行+1,写入工作内存。也就是说,自增操作的3个子操作可能会分隔开执行。假如某个时刻变量inc的值为9,线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了。之后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,然后进行加1操作,并把10写入工作内存,最后写入主存。随后线程1接着进行+1操作,因为线程1在此前已经读取了 inc 的值为9,所以不会再去主存读取最新的数值,线程1对 inc 进行+1操作后 inc 的值为10,然后将10 写入工作内存,最后写入主存。两个线程分别对 inc 进行了一次自增操作后,inc 的值只增加了1,因此自增操作不是原子性操作, volatile也无法保证对变量的操作是原子性的。

volatile保证有序性

volatile关键字能禁止指令重排序,因此 volatile 能保证有序性。volatile 关键字禁止指令重排序有两个含义:一个是当程序执行 volatile 变量的操作时,在其前面的操作已经全部执行完毕,并且结果会对后面的操作可见,在其后面的操作还没有进行,在进行指令优化时,在 volatile 变量之前的语句不能在 volatile 变量后面执行;同样,在volatile 变量之后的语句也不能在 volatile变量前面执行

 正确使用volatile 关键字

synchronized关键字可防止多个线程同时执行一段代码,那么这就会很影响程序执行效率。而 volatile 关键字在某些情况下的性能要优于 synchronized 。但是要注意 volatile 关键字是无法替代 synchronized 关键字的,因为 volatile 关键字无法保证操作的原子性。通常来说,使用 volatile 必须具备以下两个条件:

  1. 对变量的写操作不会依赖于当前值

  2. 对变量没有包含在具有其他变量的不变式中。

第一个条件就是不能是自增,自减等操作,因为 volatile不保证原子性。

volatile使用场景

1.状态标识

 volatile  boolean on;
    public void setOn(){
        on=true;
    }
    public void Print(){
        while(!on){
            System.out.println("123");
        }
    }

2.双重检查模式 (DCL)  

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

在上面的单例模式中,为什么要用valoatile 修饰?

因为 instance=new Singleton(),并非是一个原子操作,事实上在 JVM中这句话大概做了3件事

  1. 给 instance 分配内存

  2. 调用 Singletion 的构造函数来初始化成员变量

  3. 将 instance 对象指向分配的内存空间。(执行完这步 instance 就为 非null 了)

但是JVM 的即时编译器中存在指令重排序的优化,也就是说上面的第二步和第三步顺序是不确定的,一旦2,3,顺序乱了,这时候有另一个线程调用了方法,结果虽然非null,但是未初始化,所以会直接报错。

猜你喜欢

转载自blog.csdn.net/petterp/article/details/88369372