java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > java多线程

java多线程:基础详解

作者:zuiziyoudexiao

这篇文章主要介绍了java多线程编程实例,分享了几则多线程的实例代码,具有一定参考价值,加深多线程编程的理解还是很有帮助的,需要的朋友可以参考下。

Java内存模型

在这里插入图片描述

多线程环境下原子性,可见性,有序性分别指的是

主内存和工作内存的交互命令

内存模型的原子性

Java内存模型只保证store和write两个命令按顺序执行,但不保证连续执行,因此多个线程同时写入共享变量可能出现线程安全问题。

诸如i++的操作,首先将主存中的变量i的值拷贝一份拿到线程的本地内存,在本地内存进行自增操作,然后将新的i值写回主存。
但是涉及到多线程环境下的线程上下文切换就会出现问题,可能线程1将i值拿来进行自增操作,然后还来不及写回主存,时间片用完,轮到线程2执行,线程2对i进行自减操作,然后轮到线程1时,线程1将上一次的值写回内存,就会将线程2上一步的计算结果覆盖,就会产生错误的结果。

通过多线程的学习我们知道,对共享数据加锁可以保证操作的原子性,相当于i++操作对应底层命令是原子化绑定的,这样就不会出现线程安全问题,但是会导致程序性能降低。

内存模型的可见性

public class Test {
    static volatile int a = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            for(int i=0;i<100000;i++){
                a++;
            }
        }).start();
        new Thread(()->{
            for(int i=0;i<100000;i++){
                a--;
            }
        }).start();
        Thread.sleep(1000);
        System.out.println(a); //不能保证a为0
    }
}

内存模型的有序性

有序性是指在单线程环境中, 程序是按序依次执行的。而在多线程环境中, 程序的执行可能因为指令重排而出现乱序。

指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序。这种排序(比如两个变量的定义顺序)不会影响单线程的结果,但是会对多线程程序产生影响。

比如 a=1 b=2两条语句就可能发生指令重排。而 a=1,b=a+1 不会发生指令重排。

示例:线程1执行f1方法,线程2执行f2方法。两个线程同时执行,可能发生如下结果: f1中发生指令重排 flag=true先执行,a=1后执行。线程1先执行flag=true,然后轮到线程2执行,此时flag为true,执行if语句,i=1。这就是指令重排造成的程序错乱。

class Test{
    int a = 0;
    boolean flag = false;
    public void f1() {
        a = 1;                   
        flag = true;           
    }
    public void f2() {
        if (flag) {                
            int i =  a +1;      
        }
    }
}

可以用volatile修饰flag来禁用指令重排达到有序性。

加锁也可以避免指令重排带来的混乱,但是本身并没有禁止指令重排,因为保证了原子性,所以即使指令重排在同步代码块中依然相当于单线程执行,也不会有逻辑上的错误。

指令重排优化的底层原理

一个指令的执行被分成:取指、译码、访存、执行、写回 5个阶段。然后,多条指令可以同时存在于流水线中,同时被执行。
指令流水线并不是串行的,并不会因为一个耗时很长的指令在“执行”阶段呆很长时间,而导致后续的指令阻塞。相反,流水线是并行的,多个指令可以同时处于同一个阶段,只要CPU内部相应的处理部件未被占满即可。

比如,依次有两条指令a和b需要执行,如果是串行执行,它们的执行过程如下

指令a                          指令b
阶段1 阶段2 阶段3 阶段4 阶段5    阶段1 阶段2 阶段3 阶段4 阶段5

但是,假如阶段2耗时很长,使用串行的方式就无法在一个阶段阻塞的时候去执行其他阶段。

如下就是流水线的方式来执行,当指令a的阶段2阻塞时,完全可以去执行指令b的阶段1,这样就提高了程序执行效率,最大程度利用CPU各个部件。

指令a                         
阶段1 阶段2 阶段3 阶段4 阶段5   
指令b
      阶段1 阶段2 阶段3 阶段4 阶段5

因此指令重排就是对于一个线程中的多个指令,可以在不影响单线程执行结果的前提下,将某些指令的各个阶段进行重排序和组合,实现指令级并行。

valatile原理

如下,假设对变量a用valatile关键字修饰。

valatile int a = 0;

那么,对变量a的写指令之后都会插入写屏障,对变量a的读指令之前都会插入读屏障。

a++;
//写屏障
//读屏障
int b = a;

写屏障会保证写屏障之前的所有对共享数据的改动都会同步到主存中。读屏障会保证读屏障之后对共享数据的读取操作都会到主存去读取。这样就保证了,每次对valatile变量的修改对其他线程始终是可见的,从而保证了可见性。

另外,写屏障会保证写屏障之前的指令不会被排到写屏障后面。读屏障会保证读屏障之后的代码不会排到读屏障前面。这样就保证了有序性。

如下,由于写屏障的存在,int b=1;语句只能排在 a++前面,不能颠倒顺序。

int b=1;
a++;
//写屏障

volatile与加锁的区别

volatile只能保证可见性和有序性,不能保证原子性,加锁既可以保证可见性 原子性 有序性都可以保证。

volatile只适用于一个线程写,多个线程读的情况,对于多个线程写的情况,必须要加锁。

加锁相对于volatile是更加重量级的操作,所以一般能用volatile解决的问题就不要加锁。

先行发生原则

先行发生是Java内存模型中定义的两项操作之间的偏序关系。如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响被操作B察觉。

先行发生原则–是判断是否存在数据竞争、线程是否安全的主要依据。先行发生原则主要用来解决可见性问题的。

如下代码

//以下操作在线程A中执行
i = 1;
//以下操作在线程B中执行
j = i;
//以下操作在线程C中执行
i = 2

如果A先行发生于B,B先行发生于C,那么必然j的值为1。如果A先行发生于B,B和C没有先行发生关系,那么j的值可能为1也可能为2。

Java内存模型存在一些天然的先行发生关系,这些先行发生关系不需要任何的同步操作,就可以保证其线程安全。

1、程序次序规则。在一个线程内,书写在前面的代码先行发生于后面的。确切地说应该是,按照程序的控制流顺序,因为存在一些分支结构。

2、Volatile变量规则。对一个volatile修饰的变量,对他的写操作先行发生于读操作。

3、线程启动规则。Thread对象的start()方法先行发生于此线程的每一个动作。

4、线程终止规则。线程的所有操作都先行发生于对此线程的终止检测。

5、线程中断规则。对线程interrupt()方法的调用先行发生于被中断线程的代码所检测到的中断事件。

6、对象终止规则。一个对象的初始化完成(构造函数之行结束)先行发生于发的finilize()方法的开始。

7、传递性。A先行发生B,B先行发生C,那么,A先行发生C。

8、管程锁定规则。一个unlock操作先行发生于后面对同一个锁的lock操作。

线程的三种实现方式

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

总结

本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注脚本之家的更多内容!

您可能感兴趣的文章:
阅读全文