Java并发编程之有序性、原子性、可见性

一、可见性

可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。

public class T01_Volatile {
    private boolean running = true;
    private void m(){
        System.out.println("m start");
        while (running){

        }
        System.out.println("m end");
    }
    public static void main(String[] args) throws InterruptedException, IOException {
        T01_Volatile t = new T01_Volatile();
        new Thread(t::m,"T01").start();
        Thread.sleep(1000);
        t.running = false;
    }
}

这段程序的逻辑很简单,开启一个子线程运行m方法,方法开始打印m start,之后是个死循环,主线程沉睡一秒后将running设置为false,死循环结束,接着打印m end,运行程序:m start;这是我们发现主线程将running设置成了false,但是只打印了m start而没有打印m end;说明子线程的循环并没有结束,这是因为主线程和子线程在不同的cpu内核上运行,程序运行的时候主线程和子线程会把runing拷贝一份放在自己的缓存区,程序运行后他们都各自操作的是自己缓存区的runing,主线程对running的修改对子线程来说是不可见的。

接下来我们对变量runing通过添加volatile关键字进行修饰

private volatile boolean runing = true;

再次运行程序,m end被打印了,通过volatile修饰的变量能保证线程之间数据的同步,主线程将runing设置为false之后,子线程是及时可见的,这就是volatile的主要作用。

volite本质是汇编指令lock和addl ,bytecode.cpp ,本质就是一把锁,锁缓存或锁总线,lock指令不能但是存在,她的本质意思是当我执行后面的这条指令的时候,锁缓存或者锁总线。这就要提一下缓存行的概念:MESI缓存一致性协议,时不时的会触发一下,来保持数据的一致性,把两个x拆开,他们位于不同的内存空间,一个缓存行,读的是一块数据;MESI协议,没有上锁是一种不及时的同步,不管加不加v,s底层永远存在,慢:同一个缓存行,数据同步有消耗,快:在不同的缓存行,不需要同步数据。

我们可以对代码再次修改,将runing的volatile关键字设置remove掉,在while循环中打印一句话,即:

private boolean running = true;
    private void m(){
        System.out.println("m start");
        while (running){
            System.out.println("ok");
        }
        System.out.println("m end");
    }

这时,我们运行程序会发现m end照样打印了,这是因为System.out.println方法是一个同步的方法,源码如下:

public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

在多线程编程模型中某些语句会触发数据同步机制,此时我们可以发现synchronized关键字不但保证了原子性,还保证的数据的可见性。 

二、有序性

有序性,即程序的执行顺序按照代码的先后顺序来执行。

public class T02_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(new Runnable() {
                public void run() {
                    //由于线程one先启动,下面这句话让它等一等线程two. 
                    //读者可根据自己电脑的实际性能适当调整等待时间.
                    //shortWait(100000);
                    a = 1;
                    x = b;
                }
            });

            Thread other = new Thread(new Runnable() {
                public void run() {
                    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);
            }
        }
    }
}

乱序执行,不是两个线程之间的乱序执行,而是同一个线程中代码的乱序执行,乱序执行是为了提供程序的运行效率,烧水泡茶的例子,线程的执行结果能保证最终的一致性,此时就可能发生乱序来提升效率。

 这时就需要体积另一个概念,指令重排序:一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。但是,并不是所有的代码都可以进行指令重排序的。例如下面的代码就是不能重排的:

x++;
x=x+1;

可以重排:

a = 1;
x = b;
public class T03_NoVisbllity {
    private static boolean ready = false;
    private static int number;
    private static class ReaderThread extends Thread{
        @Override
        public void run() {
            while (!ready){
                Thread.yield();
            }
            System.out.println(number);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = new ReaderThread();
        t.start();
        number = 42;
        ready = true;
        t.join();
    }
}

可能一直处于没有准备好,ready的是不可见的,虽然ready被主线程修改,另外可能打印的结果为0,因为:

number = 42;
ready = true;

可能指令重排序:

public class T04_ThisEscape {
    private int num = 8;

    public T05_ThisEscape() {
        new Thread(()->{
            System.out.println(this.num);
        }).start();
    }

    public static void main(String[] args) throws IOException {
        new T05_ThisEscape();
        System.in.read();
    }
}

 num的打印结果可能为0,this是一个半初始化状态

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

instance 可能由于指令重排而导致的处于半初始化状态,加上volatile关键字即可,禁止指令重排。

三、原子性

原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。

synchronized的实现过程:
1.Java代码:synchronized
2.字节码:monitorenter moniterexit
3.jvm:自动升级
4.汇编:lock comxchg
工作原理
synchronized互斥锁的本质是原子性,把下面的整体操作当成一个原子,一个线程执行完毕,另外一个线程才能执行,把原来并发执行的程序,变成序列化执行。

在Java SE 1.5之前,多线程并发中,synchronized一直都是一个元老级关键字,而且给人的一贯印象就是一个比较重的锁。为此,在Java SE 1.6之后,这个关键字被做了很多的优化,从而让以往的“重量级锁”变得不再那么重。

synchronized主要有两种使用方法,一种是代码块,一种关键字写在方法上。这两种用法底层究竟是怎么实现的呢?在1.6之前是怎么实现的呢?

字节码实现原理 在java语言中存在两种内建的synchronized语法:1、synchronized语句;2、synchronized方法。对于synchronized语句当Java源代码被javac编译成bytecode的时候,会在同步块的入口位置和退出位置分别插入monitorenter和monitorexit字节码指令。而synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。

那么monitorenter和monitorexit以及access_flags底层又是通过什么底层技术来实现的原子操作呢?

Mutex Lock

简单来说在JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。

四、锁升级

五、CAS工作原理 

CAS底层:lock cmpxchg 本质是锁定北桥芯片的一个电信号 

六、sychronized和volatile关键字的区别

1、volatile本质是在告诉jvm当前变量在寄存器(工作内存)中是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

2、volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的

3、volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性

4、volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

5、volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

七、volatile的原理和实现机制

前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。

下面这段话摘自《深入理解Java虚拟机》:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

2)它会强制将对缓存的修改操作立即写入主存;

3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

八、Synchronized原理和实现机制

(1)synchronized静态代码块

将一个synchronized静态代码块反编译会看到两个专有名词

monitorenter

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit:

执行monitorexit的线程必须是objectref所对应的monitor的所有者。

指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

总结:

通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

(2)synchronized同步方法

将一个synchronized同步方法反编译:

ACC_SYNCHRONIZED:

从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
参考博客链接:https://blog.csdn.net/geduo_83/article/details/111585862                                                                 https://blog.csdn.net/weixin_29015051/article/details/114547760

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值