Java多线程和高并发学习笔记2

Volatile

主要有两个作用:
1、线程可见性:用volatile修饰的变量,可以在不同线程之间互相看到,如果一旦发生修改,其他线程就会刷新当前的变量,保证变量的数据一致性
2、阻止指令重排序:cpu执行指令是乱序的,如果加了volatile,可以防止cpu对指令重排序

这个是网上看到的,记录一下
在这里插入图片描述

下面这个代码,无论加不加volatile,都会执行到end,暂时没搞懂

package thread;

import java.util.concurrent.TimeUnit;

public class TestVolatile {
    private static boolean isRunning = true;

    void m() {
        System.out.println("start");
        while (isRunning) {
            System.out.println("is Running");
        }

        System.out.println("end");
    }

    public static void main(String[] args) {
        TestVolatile testVolatile = new TestVolatile();
        new Thread(testVolatile::m, "test").start();

        try {
            // 系统睡眠1秒
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        TestVolatile.isRunning = false;
    }
}

补充:上面的代码找到为什么没有一直执行的原因了,是因为我在while循环内加了一个输出语句,具体的原因可以看下这篇博客:https://blog.csdn.net/C_AJing/article/details/103307797

cpu架构
在这里插入图片描述
ALU:cpu里面的计算单元

超线程:一个ALU对应多个PC

上下文切换:cpu从一个线程切换到另一个线程执行,简称 上下文切换

一般来说,1个cpu核心对应2个线程,两个线程之间互相切换,可以实现超线程

cache line 缓存行:数据存入到缓存里面,是按行存储的,而cpu读取数据的时候,是按每个缓存行去读取的,一个缓存行大小是8字节,cpu会把相邻的变量尽量的存入一个缓存行里面

下面的程序,会对x,y添加了volatile关键字,保证数组中第一个元素和第二个元素的线程可见性,如果x和y没有被存入同一个缓存行内,就会导致cpu读取很慢

package thread;

public class CachePadding {
    private static class T {
        public volatile long x = 0L;
    }

    public static T[] array = new T[2];

    static {
        array[0] = new T();
        array[1] = new T();
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (long i = 0; i < 1000_0000L; i++) {
                array[0].x = i;
            }
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < 1000_0000L; i++) {
                array[1].x = i;
            }
        });

        final long start = System.nanoTime();

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println((System.nanoTime() - start) / 100_0000);
    }
}

运行结果:
在这里插入图片描述
下面是一个新的程序,可以对比一下上面的代码,多了一个Padding类,用于补齐缓存行,这样的话,就不会把数组的第一个元素和第二个元素存入同一个缓存行内,这样就不会反复的读取和修改

package thread;

public class CachePaddingMore {
    private static class Padding {
        // 每个long类型的变量占用8个字节,下面的p1~p7一共占用56个字节
        public volatile long p1 = 0L, p2 = 0L, p3 = 0L, p4 = 0L, p5 = 0L, p6 = 0L, p7 = 0L;
    }

    private static class T extends Padding{
        // 这个地方占用8个字节
        public volatile long x = 0L;
    }

    public static T[] array = new T[2];

    // 这个时候,array[0]和array[1]必然不可能在同一个cache line内
    static {
        array[0] = new T();
        array[1] = new T();
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (long i = 0; i < 1000_0000L; i++) {
                array[0].x = i;
            }
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < 1000_0000L; i++) {
                array[1].x = i;
            }
        });

        final long start = System.nanoTime();

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println((System.nanoTime() - start) / 100_0000);
    }
}

运行结果:
在这里插入图片描述
消耗时间比上一个少了接近一半,这就是修改了缓存行的存储方式的结果

cpu的执行方式是乱序的,从下面的代码可以看出来:

package thread;

public class T04_Disorder {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (; ; ) {
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            Thread one = new Thread(() -> {
                //由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
                shortWait(100000);
                a = 1;
                x = b;
            });

            Thread other = new Thread(() -> {
                b = 1;
                y = a;
            });
            one.start();
            other.start();
            one.join();
            other.join();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if (x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }


    public static void shortWait(long interval) {
        long start = System.nanoTime();
        long end;
        do {
            end = System.nanoTime();
        } while (start + interval >= end);
    }
}

如果说,cpu没有乱序执行的话,最终的结果应该是(0,1),但是我们运行了10万次以后发现,各种情况都会出现:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这就说明,cpu执行代码的顺序,一定不是按照我们定义的顺序去执行的,而是哪一个线程抢占到资源以后,哪一个线程就会执行

指令乱序执行会出什么问题?
首先我们看个例子,单例模式的实现我们应该都知道,下面这种是实现的代码:

package thread;

/**
 * 单例模式
 * 缺点:还未使用就会完成实例化
 */
public class Mgr01 {
    private static final Mgr01 INSTANCE = new Mgr01();

    private Mgr01() {

    }

    public static Mgr01 getInstance() {
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        Mgr01 mgr01 = Mgr01.getInstance();
        Mgr01 mgr02 = Mgr01.getInstance();

        System.out.println(mgr01.hashCode());
        System.out.println(mgr02.hashCode());
    }
}

输出如下:
在这里插入图片描述
我们会发现,两个对象的hashcode是一致的,那么就表明,这两个对象是一样的

但是这种实现方式有个缺点,就是如果还没调用getInstance的方法,对象就已经完成实例化了,那么有没有改进的方式?下面的这种就可以避免

package thread;

public class Mgr02 {
    private static Mgr02 INSTANCE;

    private Mgr02() {

    }

    public static Mgr02 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            INSTANCE = new Mgr02();
        }

        return INSTANCE;
    }

    public static void m() {
        Mgr02 mgr02 = getInstance();
        System.out.println(mgr02.hashCode());
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(Mgr02::m).start();
        }
    }
}

这种的话,会去判断当前的INSTANCE对象是否已经初始化过了,如果初始化过了,就会返回初始化以后的对象,否则就会初始化一遍以后返回

但是这种写法在多线程的访问下,会是安全的吗?能不能保证只创建了一个对象?我们看下运行结果:
在这里插入图片描述
每个对象的hashcode都是不一样的,或许你又会问,怎么证明呢?那么我们试试单线程调用,就可以看出结果了,首先代码做一点小小的修改

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            Mgr02.m();
        }
    }

然后运行结果如下:
在这里插入图片描述
我们会发现,对象的hashcode都是一样的,所以多线程下的单例模式,还是有问题的,不只会创建一个对象,而是会创建多个对象,那么怎么接近呢?很简单,直接加一个synchronized就行了

    public static synchronized Mgr02 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            INSTANCE = new Mgr02();
        }

        return INSTANCE;
    }

然后我们会发现,synchronized给整个方法上锁了,范围太大,那么能不能缩小一点?可以啊,我们可以这么改一下

package thread;

public class Mgr02 {
    private static volatile Mgr02 INSTANCE;

    private Mgr02() {

    }

    public static Mgr02 getInstance() {
        if (INSTANCE == null) {
            synchronized (Mgr02.class) {
                // 双重检查
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    INSTANCE = new Mgr02();
                }
            }
        }

        return INSTANCE;
    }

    public static void m() {
        Mgr02 mgr02 = getInstance();
        System.out.println(mgr02.hashCode());
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(Mgr02::m).start();
        }
    }
}

这么写的好处有什么呢?首先,我们在初始化之前,加了一把锁,如果其他线程来的时候,判断一下当前对象是否为空,如果为空,那么就会给这个对象加一把锁,然后再去判断一次,这个对象是否为空,那么,为什么要判断两次呢?因为如果第一个线程,进入方法以后,判断当前对象为空,然后加锁,初始化对象,然后解锁返回的时候,如果同时再来一个线程,进入方法以后,判断当前对象,因为还没有返回初始化完的对象,这个时候对象还是为空,就会有可能又重新的初始化一遍,就会有问题。所以要加一个二次检查,用来防止多重初始化。

这种单例模式也叫 DCL单例(double check lock),双重加锁单例

那么,我们的INSTANCE要不要加volatile关键字?答案是肯定要加

我们先看个东西:

package thread;

public class NewObject {
    public static void main(String[] args) {
        Object object = new Object();
    }
}

在这里插入图片描述
上面这个图是这段代码的字节码,我们可以看到,首先NEW是创建一个对象,INVOKESPECIAL是调用构造方法,ASTORE分配存储空间,

当我们创建一个对象的时候,他有一个中间态,属于一个半初始化状态,如果在创建对象的时候,INVOKESPECIALASTORE两个指令发生了指令重排序,那么就会出现问题,所以我们要加volatile关键字来防止指令重排序

那么,volatile是怎么防止指令重排序的?因为他给指令加了一个内存屏障。那么,什么是内存屏障?内存屏障两边的指令不可以重排,可以保证有序。

在这里插入图片描述
如上图所示,指令1和指令2之间,是不允许重排序的

而在java虚拟机里面的实现,其实就是使用了汇编语言里面的锁总线的方法,来防止指令重排序的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值