多线程volatile和synchronized

2020-02-06  本文已影响0人  二狗不是狗

一. 内存模型

  • 寄存器是中央处理器内的组成部份。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和位址。在中央处理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序计数器(PC)。在中央处理器的算术及逻辑部件中,包含的寄存器有累加器(ACC)。
  • Cache即高速缓冲存储器,是位于CPU与主内存间的一种容量较小但速度很高的存储器。用于解决内存速率低于CPU的运算速率的冲突。一般分为L1/L2/L3三级缓存。
image.png

二.内存模型的三个特性

1.原子性
  • 在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
  • 只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
  • Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
// 原子性,直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中
x = 10;

// 它先要去读取x的值,再将x的值写入工作内存
// 虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
y = x;

// 包括3个操作:读取x的值,进行加1操作,写入新的值
x++;

// 包括3个操作:读取x的值,进行加1操作,写入新的值
x = x + 1;
2.可见性
  • 对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
  • 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
  • 另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
3.有序性
  • java内存模型规定如下的部分顺序,不符合以下规则的需要通过使用volitile 或者synchronized Lock进行操控。
  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

三.volatile关键字

  • 保证可见性:使用volatile修饰的变量,保证了不同线程之间对共享变量操作时的可见性,也就是说当一个线程修改后,另一个线程会立即看到最新的值。
  • 保证有序性:禁止对指令进行重新排序操作
  • 不能保证原子性:像num++这个操作上,因为num++不是个原子性的操作,而是个复合操作:1.读取;2.加一;3.赋值。所以,在多线程环境下,有可能线程A将num读取到本地内存中,此时其他线程可能已经将num增大了很多,线程A依然对过期的num进行自加,重新写到主存中,最终导致了num的结果不合预期。

四.synchronized关键字

  • synchroized可以保证原子性,可见性以及有序性
public class RespUtils {
    private static int sum = 0;

    private Integer objectSum() {
        for (int m = 1; m<=100000; m++) { sum++; }
        System.out.println("函数内返回=" + sum);
        return sum;
    }

    private synchronized Integer synObjectSum() {
        for (int m = 1; m<=100000; m++) { sum++; }
        System.out.println("函数内返回=" + sum);
        return sum;
    }

    private Integer synObjectBlockSum() {
        synchronized (this) {
            for (int m = 1; m<=100000; m++) { sum++; }
            System.out.println("函数内返回=" + sum);
            return sum;
        }
    }

    private static Integer classSum() {
        for (int m = 1; m<=100000; m++) { sum++; }
        System.out.println("函数内返回=" + sum);
        return sum;
    }

    private synchronized static Integer synClasssSum() {
        for (int m = 1; m<=100000; m++) { sum++; }
        System.out.println("函数内返回=" + sum);
        return sum;
    }

    private static Integer synClassBlockSum() {
        synchronized (RespUtils.class) {
            for (int m = 1; m<=100000; m++) { sum++; }
            System.out.println("函数内返回=" + sum);
            return sum;
        }
    }

    public static void main(String[] args) throws Exception {
        final RespUtils respUtils = new RespUtils();

        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return respUtils.objectSum();
            }
        };

        new Thread(new FutureTask<>(callable)).start();
        new Thread(new FutureTask<>(callable)).start();
    }
}
1.实例锁-普通方法
private Integer objectSum() {
    for (int m = 1; m<=100000; m++) { sum++; }
    System.out.println("函数内返回=" + sum);
    return sum;
}

上面代码输出打印(结果不确定):
函数内返回=116780
函数内返回=116780

2.实例锁-对象本身
final RespUtils respUtils = new RespUtils();

Callable<Integer> callable = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        synchronized (respUtils) {
            return respUtils.objectSum();
        }
    }
};

new Thread(new FutureTask<>(callable)).start();
new Thread(new FutureTask<>(callable)).start();

上面代码输出打印(结果确定):
函数内返回=100000
函数内返回=200000

3.实例锁-对象方法
private synchronized Integer synObjectSum() {
    for (int m = 1; m<=100000; m++) { sum++; }
    System.out.println("函数内返回=" + sum);
    return sum;
}

上面代码输出打印(结果确定):
函数内返回=100000
函数内返回=200000

4.实例锁-对象代码块
private Integer synObjectBlockSum() {
    synchronized (this) {
        for (int m = 1; m<=100000; m++) { sum++; }
        System.out.println("函数内返回=" + sum);
        return sum;
    }
}

上面代码输出打印(结果确定):
函数内返回=100000
函数内返回=200000

5.全局锁-普通方法
Callable<Integer> callable = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        return RespUtils.classSum();
    }
};

new Thread(new FutureTask<>(callable)).start();
new Thread(new FutureTask<>(callable)).start();

上面代码输出打印(结果不确定):
函数内返回=114195
函数内返回=114195

6.全局锁-类本身
Callable<Integer> callable = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        synchronized (RespUtils.class) {
            return RespUtils.classSum();
        }
    }
};

new Thread(new FutureTask<>(callable)).start();
new Thread(new FutureTask<>(callable)).start();

上面代码输出打印(结果确定):
函数内返回=100000
函数内返回=200000

7.全局锁-类方法
private synchronized static Integer synClasssSum() {
    for (int m = 1; m<=100000; m++) { sum++; }
    System.out.println("函数内返回=" + sum);
    return sum;
}

上面代码输出打印(结果确定):
函数内返回=100000
函数内返回=200000

8.全局锁-类代码块
private static Integer synClassBlockSum() {
    synchronized (RespUtils.class) {
        for (int m = 1; m<=100000; m++) { sum++; }
        System.out.println("函数内返回=" + sum);
        return sum;
    }
}

上面代码输出打印(结果确定):
函数内返回=100000
函数内返回=200000

五.使用注意事项

1.实例锁-注意事项
  • 实例锁的意思是要执行某段代码必须获得具体对象实例的锁。实例锁-对象本身是指要执行线程synchronized (object){ }内部代码必须获得对象实例的锁;实例锁-对象方法是指要执行对象方法内部代码必须获得对象实例的锁;实例锁-对象代码块是指要执行代码块synchronized (this) { }内部代码必须获得对象实例的锁。
  • 实例锁只针对需要获取对象实例锁才能执行的代码,而不针对不需要获取对象实例锁的代码。比如说调用了某对象的synObjectSum方法,还能同时访问该对象的objectSum方法。
  • 实例锁是针对对象的,对于两个不同的对象可以并发访问。比如说创建了两个对象respUtils1和respUtils2,多线程可以并发访问这两个对象的synObjectSum()方法。
2.全局锁-注意事项
  • 全局锁的意思是要执行某段代码必须获得Class全局的锁。全局锁-类本身是指要执行线程synchronized (RespUtils.class) { }内部代码必须获得Class全局的锁;全局锁-类方法是指要执行类方法内部代码必须获得Class全局的锁;全局锁-类代码块是指要执行代码块synchronized (RespUtils.class) { }内部代码必须获得Class全局的锁。
  • 全局锁只针对需要获取Class全局的锁才能执行的代码,而不针对不需要Class全局的锁的代码。比如说调用了某类的synClasssSum方法,还能同时访问该对象的classSum方法。
3.注意事项
  • 实例锁和全局锁是不同的锁,所以两个线程可以分别获取到实例锁和全局锁并发同时执行
new Thread(new Runnable() {
    @Override
    public void run() {
        synchronized (new RespUtils()) {
            new RespUtils().objectSum();
        }
    }
}, "线程1").start();

new Thread(new Runnable() {
    @Override
    public void run() {
        synchronized (RespUtils.class) {
            new RespUtils().objectSum();
        }
    }
}, "线程2").start();

上面代码输出打印(结果不确定):
函数内返回=114195
函数内返回=114195

上一篇下一篇

猜你喜欢

热点阅读