java并发编程之内存模型&多线程三大特性

本文代码示例已放入github:请点击我

快速导航------>src.main.java.yq.Thread.TrainTickets

多线程的三大特性有哪些?

     1. 原子性:原子性,就跟我们的事务意思大概一致,就是表示一个或者多个线程进行操作,要么全部失败,要么全部成功,很经典的例子就是银行转行问题,要么转账成功,要么转账失败,不允许单方面成功,或者单方面失败,如果单方面成功或者失败,那么我想银行也做不下去了。

     2. 可见性:就是指得是,当一个线程修改了这个变量的值,那么其他的线程能够立即看到被修改的值。

     3. 有序性:即是多线程运行期间的代码执行顺序。程序一般的执行顺序就从上到下进行执行的,但是多线程情况下,机器为了提高运行效率,会对代码进行优化,他不会保证代码执行顺序,但是会保证最终执行结果是一致的,这听上去很矛盾,因为这里指的不保证执行顺序是对没有依赖关系的代码进行优化,不保证执行顺序,但是对有依赖关系的代码还是按照顺序执行,所以说会保证最终执行的结果。

什么是java内存模型?

     如果想详细了解java内存模型请点击我:java内存模型深入理解 

     答:java中的线程之间通讯就是使用java内存模型进行控制,java内存模型也指的是JMM,线程之间通讯?

     问:那么线程之间怎么通讯?

     答:线程之间通讯有两种,分别为 -- 共享内存 <-- 和 --> 消息传递 -- 我们这里只讲解共享内存,因为共享内存跟我们的JMM息息相关。

我们之前在 -- java并发编程之线程安全问题 -- 中并未提到为什么会产生线程安全问题,只提到了什么是线程安全问题,那么现在哦我们就来说说为什么会产生线程安全问题。

首先我们看一个流程图:

看了流程图,我们可以很字面的理解线程安全问题产生的原因就是:当使用多线程操作共享变量的时候就会发生线程安全问题。

什么是共享变量?

     答:所有实例域、静态域 和 数组元素存储在堆内存中,堆内存在线程之间共享,说简单点,就是全局变量,静态变量,以及数组,对象等,在多线程中可以称之为共享变量。

局部变量、方法定义参数 和 异常处理器参数 不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。 

那么接下来我们深入分析为什么会发生线程安全问题。

     首先我们可以清晰的看到,我们的共享变量是放在主内存的,而我们使用多线程进行操作共享变量不是直接到主内存进行操作,而是通过JMM作为媒介进行操作的,本地内存会给每一个线程分配一个属于它的本地内存,而我们对共享变量操作的时候实际上是先操作的本地内存的共享变量的副本,再通过JMM把本地内存的副本刷新到主内存中,进行更新共享变量,那么当线程A已经在把本地共享变量刷新到了本地内存,然后线程B也同时把共享变量副本刷新到本地内存,但是都没有刷新到主内存,因为完成这些操作是需要时间的,因为线程是独立的,这个时候线程A不知道线程B已经对数据进行修改了,所以就会发生线程安全问题,这个东西很抽象,需要多加理解。就相当于,线程A先把这个共享变量复制了一份拿去使用了,但是还没有结果,但是这个时候线程B又来了,因为线程A拿的是共享变量的Copy版本,但是还没有同步过来,也就是这个时候的共享变量还是那么多,并没有发生改变,但是线程B又复制了一份共享变量拿去使用了。所以这就是线程安全问题。

 总结:java内存模型简称为JMM,JMM定义了一个线程对另外一个线程的可见性,共享变量是保存在主内存中的,每个线程拥有独立的本地内存,当多个线程操作共享变量的时候,由于本地共享变量副本没有刷新到主内存之前,其他线程又进行读取了主内存中的共享变量,所以就发生了线程安全问题。

     那么我们需要解决这个问题,也就是当别的线程修改了主内存中的值,其他线程不可见(也就是不知道被修改),那他不知道,那我们就想办法通知其他线程,说:嘿,伙伴们,这个数据我修改了噢,或者马上就更新主内存的值,那么其他线程就直接拿到了被更新之后的共享变量

     通知其它线程我们做不到,但是我们可以使用Volatile关键字修饰变量从而达到被线程修改之后马上更新主内存中

//火车票
@Data
public class TrainTickets {

    //表示我们有100个火车票
    private volatile static Integer number = 100;


    //出票操作
    public static Integer ticketIssue(){
        return number--;
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    while (TrainTickets.number > 0) {
                        Thread.sleep(50);
                        System.out.println(Thread.currentThread().getName() + "拿到第了:" + TrainTickets.ticketIssue() + " 张火车票");
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    while (TrainTickets.number > 0) {
                        Thread.sleep(50);
                        System.out.println(Thread.currentThread().getName() + "拿到第了:" + TrainTickets.ticketIssue() + " 张火车票");
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}

     我们还是使用火车票的例子,使用两个窗口同时出票,我们使用了Volatile关键字进行把修改后的数据刷新到主内存中,既然我们的线程安全问题,就是因为其他线程修改了值,但是当前线程不知道而导致的吗?那按道理是不是使用Volatile关键字就可以解决线程安全问题?

答案是:还是会发生线程安全问题。

     从运行结果,我们还是看到了重复消费的清理,所以线程安全问题还是发生了。

     为什么使用Volatile关键字无法保证线程安全问题:简而言之就是用关键字修饰的就是修改后及时写回主存,对其他线程可见可理解为其他线程探嗅到自己缓存中的变量是过期的(不同线程在同一核心道理相同) 如果想详细了解可以点击我:为什么Volatile也不能解决线程安全问题

什么是重排序?

     答:重排序就是我们之前有序性中提到的,在多线程中,编译器和处理器为了提高并行度提高程序执行效率,就会进行重新排序,正常情况下(单线程)代码是一行一行进行执行的,所以不会发生重排序的问题,多线程情况下会对没有依赖关系的代码进行重新排序,甚至并行执行,这就是我们所说的重排序,多线程很有可能影响到程序的执行结果,为了不让我们的变量发生重排序,我们可以在多线程情况下使用Volatile关键字修饰变量,达到解决重排序问题。

     总结:Volatile可以解决从排序问题,可以把修改的变量马上刷新到主内存保证可见性。发生线程安全问题的根本就是每个线程会有一个本地的内存,里面保存的就是主内存的共享变量的副本,导致在读取之前其他线程可能进行了修改,但是还没有刷新到主内存。另外线程的三大特性就是:原子性,可见性,有序性

     到了这里又结束了,谢谢大家的观看,本文适合小白,我也是一名小白,希望和大家共同进步,如果有写的不对的地方请指出,谢谢阅读~~

本文代码示例已放入github:请点击我

快速导航------>src.main.java.yq.Thread.TrainTickets

发布了25 篇原创文章 · 获赞 9 · 访问量 3056

猜你喜欢

转载自blog.csdn.net/qq_40053836/article/details/99857904