zgq105 / blog

2 stars 0 forks source link

java并发-并发关键字 #91

Open zgq105 opened 4 years ago

zgq105 commented 4 years ago

1.并发三大特性

1.1 原子性

原子性是指一个或者多个操作,要么全部执行成功(且中间不被中断),要么全部不执行;是一个不可再分割的工作单元。比如,数据库事务操作就是原子性操作。

java中原子性分析 看以下代码:

        int a = 1;//语句1
        int b = 2;//语句2
        a = b;//语句3
        a = b + 1;////语句4
        a++;//语句5

以上代码片段只有语句1和语句2是原子性操作,只有一个赋值操作;其中语句3包括读取b的值然后再赋值给a两个操作;语句4包括3个操作,先读取b的值,再计算操作,然后再赋值给变量a;语句5实质是等同于a=a+1也是有3个操作。

自增原子性问题分析

public class Increment {
    private int count = 1;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}
public static void main(String[] args) {
        Increment increment = new Increment();
        ExecutorService executorService= Executors.newFixedThreadPool(1000);
        for (int i = 0; i < 50000; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    increment.increment();
                }
            });
        }

        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(increment.getCount());
    }

实际输出结果为49997,本来按照代码预想的逻辑应该是50001,因为多线程执行非原子性操作导致的多线程问题。

解决原子性问题主要包括以下解决方案:

  1. 通过synchronized或者Lock。
  2. 通过Atomic原子类型。(java.util.concurrent.atomic包下)

1.2 可见性

可见性是指在多线程环境下,由于线程之间是不可见的,每个线程都有自己的工作内存;因此,默认情况下,对于共享变量的更改,是不能及时通知到每个工作线程。可见性就是为了解决共享变量对于每个线程都是可见的问题。当一个线程修改了共享变量的值,其他线程能够看到修改的值。保证线程可见性有以下几种机制:

  1. 通过volatile关键字标记内存屏障保证可见性。
  2. 通过synchronized关键字定义同步代码块或者同步方法保障可见性。
  3. 通过Lock接口保障可见性。
  4. 通过Atomic类型保障可见性。(java.util.concurrent.atomic包下)

普通情况下,多线程不能保证可见性

public class App {
    private  static boolean stop;//共享变量
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(" A is running...");
            while (!stop){

            }
            System.out.println(" A is terminated.");
        },"threadA").start();

        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            System.out.println(" B is running...");
            stop = true;
            System.out.println(" B is terminated.");
        },"threadB").start();

    }
}

输出结果: A is running... B is running... B is terminated.

结论:从程序输出的结果可知,threadA并没有结束,就是因为线程可见性问题,threadA还是使用的自己工作内存中共享变量stop的副本,导致线程没有终止。

volatile保证线程可见性

public class App {
    private volatile static boolean stop;//共享变量
    public static void main(String[] args) {
        //普通情况下,多线程不能保证可见性
        new Thread(() -> {
            System.out.println(" A is running...");
            while (!stop){

            }
            System.out.println(" A is terminated.");
        },"threadA").start();

        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            System.out.println(" B is running...");
            stop = true;
            System.out.println(" B is terminated.");
        },"threadB").start();

    }
}

输出结果: A is running... B is running... B is terminated. A is terminated.

结论:通过使用关键字volatile修饰共享变量stop保证了线程的可见性,因此,线程B修改了共享变量stop ,同时,线程A也能够及时获取最新的修改,保证线程之间的可见性,进而线程A也终止了。

Atomic保证原子性

public class App {

    private static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
    public static void main(String[] args) {
        //普通情况下,多线程不能保证可见性
        new Thread(() -> {
            System.out.println(" A is running...");
            while (!atomicBoolean.get()) {

            }
            System.out.println(" A is terminated.");
        }, "threadA").start();

        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            System.out.println(" B is running...");
            atomicBoolean.set(true);
            System.out.println(" B is terminated.");
        }, "threadB").start();

    }
}

输出结果: A is running... B is running... B is terminated. A is terminated.

结论:通过使用原子类型AtomicBoolean保证了线程的可见性,因此,线程B修改了共享变量atomicBoolean,同时,线程A也能够及时获取最新的修改,保证线程之间的可见性,进而线程A也终止了。

通过关键字synchronized保证线程可见性

public class App {

private  static boolean stop;
private static Object object=new Object();
public static void main(String[] args) {
    //普通情况下,多线程不能保证可见性
    new Thread(() -> {

        synchronized (object){
            System.out.println(" A is running...");
            while (!stop) {
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        System.out.println(" A is terminated.");
    }, "threadA").start();

    try {
        Thread.sleep(10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    new Thread(() -> {
        System.out.println(" B is running...");
        synchronized (object){
            stop=true;
            object.notifyAll();
        }
        System.out.println(" B is terminated.");
    }, "threadB").start();

}

}

输出结果: A is running... B is running... B is terminated. A is terminated.

结论:通过使用synchronized对同一个对象object加锁和线程的休眠唤醒机制保证了线程的可见性,因此,线程B修改了共享变量stop,同时,线程A也能够及时获取最新的修改,保证线程之间的可见性,进而线程A也终止了。

使用Lock接口保证线程可见性

public class App {

    private static boolean stop;
    private static ReentrantLock reentrantLock = new ReentrantLock(true);
    private static Condition condition = reentrantLock.newCondition();

    public static void main(String[] args) {
        //普通情况下,多线程不能保证可见性
        new Thread(() -> {

            System.out.println(" A is running...");
            reentrantLock.lock();
            while (!stop) {
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            reentrantLock.unlock();

            System.out.println(" A is terminated.");
        }, "threadA").start();

        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            System.out.println(" B is running...");

            reentrantLock.lock();
            stop = true;
            condition.signalAll();
            reentrantLock.unlock();

            System.out.println(" B is terminated.");
        }, "threadB").start();

    }
}

输出结果: A is running... B is running... B is terminated. A is terminated.

结论:通过使用ReentrantLock和Condition机制保证了线程的可见性,因此,线程B修改了共享变量stop,同时,线程A也能够及时获取最新的修改,保证线程之间的可见性,进而线程A也终止了。ReentrantLock和synchronized使用的机制都是锁机制

1.3 有序性

有序性指的是代码的执行顺序按照代码编写的顺序执行。但事实是因为编译器和处理器为了提高执行效率往往会发生指令重排,从而导致程序执行顺序的改变,进而可能导致意想不到的Bug。接下来以单例为例来谈谈有序性问题:

public class Singleton {

    private static Singleton mInstance;
    private Singleton(){}

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

以上代码看起来是几乎完美的,但是在多线程环境下还是可能出问题的,原因就是new操作是非原子性的操作,主要包括以下几个过程:

  1. 在堆内存中分配一块内存块A。
  2. 在内存A中初始化Singleton对象。
  3. 将内存地址的引用赋值给变量mInstance。 如果指令重排导致执行顺序为 1 -> 3 -> 2的化,就有可能出现以下的情形: image

假设线程A先执行getInstance()方法,当执行完指令2时恰好发生了线程切换,切换到了线程B上;如果此时线程B也执行getInstance()方法,那么线程B在执行第一个判断时会发现 instance != null,所以直接返回instance,而此时的instance是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。

结论:以上过程由于指令重排破坏了程序的有序性,导致代码逻辑在多线程执行环境中可能发生bug,解决办法就是使用volatile关键字修饰mInstance变量;因为volatile通过内存屏障机制可以防止指令重排和内存的可见性。

2. volatile

volatile的作用主要有两个:

  1. 保证线程之间对于共享变量的可见性。
  2. 防止指令重排。

如何保证可见性? 在JVM中jmm规定,凡是被volatile修饰的变量,线程每次对于共享变量的修改都必须立即写回主内存;同时,线程对于共享变量的读取每次都需要从主内存中读取最新的值。这种机制就保证了共享变量在线程中的可见性。

如何阻止指令重排? 禁止指令重排是通过一种特殊的一种指令实现的,那就是内存屏障。内存屏障主要有两个作用:

  1. 有内存屏障标识的代码,编译器和处理器将不会进行指令重排。
  2. 它可以强制把缓存中的共享变量写回主内存,让缓存中相应的数据失效。

3. synchronized

synchronized是java中实现线程同步的关键字,主要分为同步方法和同步代码块。

3.1 同步方法

修饰实例方法 当synchronized修饰实例方法时,本质上是对象锁,举例如下:

public synchronized void f2() {
        System.out.println();
    }

通过javap -v SynchronizedTest.class查看结果如下: image

通过编译的结果可以看到通过ACC_SYNCHRONIZED标识该实例方法为同步方法。

修饰静态方法 当synchronized修饰静态方法时,本质上是类锁,举例如下:

public synchronized static void f1() {
        System.out.println();
    }

通过javap -v SynchronizedTest.class查看结果如下: image

通过编译的结果可以看到通过ACC_SYNCHRONIZED标识该静态方法为同步方法。

3.2 同步代码块

当sync修饰代码块时,看锁的对象是类还是类的实例,也分对象锁和实例锁,以对象锁为例,举例如下:

public void f3() {
        synchronized (this) {
            System.out.println();
        }
    }

通过javap -v SynchronizedTest.class查看结果如下: image

通过编译的结果可以看到通过monitorentermonitoexit标识该代码块为同步代码块。

结论:从以上的同步代码块和同步方法可知,当synchronized修饰方法时,采用的是ACC_SYNCHRONIZED进行标识加锁操作;当synchronized修饰代码块时,采用的是monitorenter和monitoexit进行标识加锁操作。无论哪种方式,本质上就是对象监视器,而且具有排他性,同一个时刻只能有一个线程获得同步块对象的监视器。

3.3 synchronized底层原理

什么是Java的对象头? 在jvm中,java对象主要有对象头、实例数据、对其填充组成,如下所示: image

其中对象头包括Mark Word、指向类的指针等数据;实例数据主要就是对象中的属性(包括父类的属性);对其填充主要作用使字节对齐。

这里和synchronized有关联的主要是对象头,它是synchronized实现锁的基础。Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息。synchronized锁的状态主要分为无锁状态、偏向锁、轻量级锁、重量级锁。锁的类型和状态在对象头Mark Word中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。

什么是对象监视器? 对象监视器是每个对象都具有的,同时每一个锁都对应一个monitor对象,如下图所示: image

对象监视器代码,如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;  //锁计数器
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }