ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Java并发实战:二

2021-07-23 20:00:50  阅读:154  来源: 互联网

标签:实战 Java Thread 关键字 并发 线程 内存 JMM volatile


线程安全

线程安全的重要性不言而喻,两个并发的线程如果有一个共享数据,如果没有采用任何的安全措施,那这个数据几乎一定会被破坏,这里看个例子。

public class App {
    public static void main(String[] args) throws Exception {
        count c = new count();
        Thread manager = new Thread(new manager(c));
        Thread manager2 = new Thread(new manager(c));
        manager.start();
        manager2.start();
        manager.join();
        manager2.join();
        System.out.println(count.i);
    }
}
class count {
    static int i = 0;
    public void increase() {++i;}
}
class manager implements Runnable { 
   count temp;
    manager(count c) {
        temp = c;
    }
    @Override
    public void run() {
         for(int i = 0 ; i < 100; ++ i) {
             temp.increase();
         }         
    }
}

上面这个代码,两个经理负责对一个 count类的 实例 i 进行加法,最后输出结果看似是 200 但其实总是低于200,而且每次运行的结果都有变化,为什么呢?

这是因为对共享数据的操作不是原子性的,什么是原子性?一个操作在执行过程中不会被中断。 ++ i 看似好像一步完成,但熟悉CPU指令执行结果的人都知道

这其实是 i = i + 1,先读取i的值,然后对 i 进行加法,再赋值。

假设两个线程同时读取,同时计算,先后写入结果,那么前一个结果就会被后一个结果覆盖,而前一个线程毫无所知,就会丢失一些值。

synchronized关键字

synchronized 保证同一时间只能有一个线程进入包裹的代码块,看代码。

 @Override
    public void run() {
         for(int i = 0 ; i < 100; ++ i) {
             synchronized(temp) { temp.increase(); }
         }         
    }

这里指定了synchronized 加锁对象,当一个线程要进入代码块时,要获得给定对象的锁,如果其他线程有这个锁,则该线程就会等待直到其他线程释放这把锁。

这样保证一次只有一个线程执行 ++i 操作。

synchronized其实是把一个不是原子性的操作变为原子性了,在线程结束执行之前都不会有其他线程来干扰操作。

JMM内存模型

 

让我们从内存的角度来理解线程并发的安全问题,首先介绍一下JMM内存模型,建议读者先去其他博客看看Java内存区域的概念再回来看。

为什么有JMM内存模型

首先不存在什么JMM,堆栈。硬件上只有内存,CPU多级高速缓存,寄存器。这些缓存提升了CPU的执行效率,却也引入了内存一致性问题。

数据同时存在与缓存和主内存中,如果只是单线程的话没什么问题,但多线程同时访问数据的话就会出现上文例子的灾难性的后果。

Java内存区域(不是模型!) 是用虚拟机用来管理运行时内存的,并不能解决上述问题。所以Java语言在符合Java内存区域的基础上,推出了Java内存模型(JMM),

来解决缓冲内存数据不一致,处理器对代码乱序执行,编译器对代码指令重排等问题。

JMM的主内存和工作内存

JMM规定除方法参数,本地方法外的变量都保存在主内存,由所有线程共享。但线程对变量的操作都必须在工作内存中进行,首先把主内存的变量复制到工作内存中,操作完毕后再刷新到主内存中。

工作内存是线程的私有区域,不同线程不能访问其他线程的工作内存。

原子性,可见性,有序性

上面提到的是JMM对不同线程对共享变量访问过程的抽象。可以看到还有多级缓存的影子,正因为硬件的多级缓存才导致JMM也必须抽象出容易导致并发安全问题的主内存与工作内存之分,现在看看JMM是如何通过原子性,可见性,有序性,基于这个抽象解决问题的。

原子性:原子性指一个操作一开始就不可以中断。回看上面那个线程安全的例子,把CPU的寄存器换成JMM的工作内存抽象,当一个操作数被读入工作内存修改,其他线程正好写回主内存,这时该线程把修改完毕的数据写回主内存,就会导致问题。JMM使用synchronized关键字或重入锁来解决(如果还未学习Java并发编程锁的概念,请停止阅读,去学习一篇关于锁的优质博客)。

有人可能会有疑问:synchronized关键字不是我们自己编程的时候使用的吗,怎么和JMM内存模型扯上关系了?这是个好问题,我的理解是 JMM内存模型关键就是从CPU多级缓存中抽象出了主内存和工作内存,因为不同操作系统,硬件这些缓存都不同,如果没有JMM的抽象,那并发程序只能在一个平台上运行,换一个平台就会出现问题。正是因为这个普适的抽象,我们才能用锁来管理工作内存和主内存的变量访问,所以锁也算JMM内存模型的一部分。(仅个人理解,如果错误请在评论指正).

 可见性和有序性:可见性是指一个线程修改过共享变量的值后,其他线程能否马上知道这个值,有序性是指多线程环境中代码语义与原来不一致。如果没有达到可见性的概念,其他线程可能读取到的是一个过期的值。就像上面的例子,从而导致结果错误。而指令重排机制可能导致失去有序性。指令重排是指编译器或处理器因为各种原因,例如优化,提前了一些指令的执行,在单线程中没有任何问题,即使重排也会保证单线程的逻辑正确,但是在多线程中语义就会出现改变。

如何保证可见性和有序性呢,通过上文我们可以看到,指令重排在单线程中不会有任何问题。所以我们可以使用synchronized关键字或volatile关键字。前者通过排他锁保证同一时间内,被修饰的代码是单线程执行的,保证了同一段代码中的变量对其他所有线程的可见性。而volatile关键字可以强行同步不同线程的共享变量,还防止局部指令重排,从而防止了不同代码段指令重排后语义改变。

 volatile关键字

使用volatile关键字可以保证被修饰变量的可见性和有序性,但不保证原子性。

volatile保证可见性的原理是,volatile能够强行刷新更新后的数据到主内存中,同时通知其他线程的缓存数据已经过期。如果其他线程之前把volatile关键字加修饰的变量载进工作内存,就会重新从主内存加载最新的数据值。

volatile关键字保证有序性的原理是,volatile关键字能禁止指令重排,保证代码会严格按先后顺序执行。

但volatile关键字不能保证原子性,所以不能用它代替synchronized关键字,不然某些情况会有问题。比如 ++ i 操作,假设AB两个线程都读取 i 值到自己的缓存中,并进行加法运算,注意假设A线程先进行加法,可能不会立刻更新到主内存中,因为更新操作实际上是跟在加法指令后的一条指令完成的。如果在加法指令完成后切换到B线程执行,那就不会立刻刷新到主内存中。等A线程写入后B缓存失效,B缓存失效后重新读取,但是B线程加法指令已经执行过了,所以这时执行的指令为最后一步的写入操作,那这样就丢失了B线程的计算结果。所以结果出错,不能保证原子性。

 其他Java并发程序基础

介绍完基础的synchronied,volatile 关键字和JMM内存模型,现在来看看其他并发程序基础。

线程组

如果线程的数量很多且分工明确,可以把相同功能的线程归类到一个线程组中管理。

public class App {
    public static void main(String[] args) throws Exception {
        ThreadGroup tg = new ThreadGroup("aGroup");
        Thread t = new Thread(tg, new manager(),"aThread");  
        t.start();        
    }
}
class manager implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getThreadGroup());   
        System.out.println(Thread.currentThread().getName());    
    }
}

上述代码中,初始化了一个线程组,并把它作为初始化线程的参数传入。线程构造函数的第一个参数是所属线程组对象,第二个参数是Runnable对象,第三个参数是线程的名称。

有人可能有疑惑,线程的名称不是变量的名称吗?其实不是,如果你没有给第三个参数,默认线程名称为 Thread-0,Thread-1 等,在查看信息的时候十分不方便,所以最好给每个线程都指定一个名称。

守护线程

守护线程是一种特殊的线程,为其他线程提供服务,像垃圾回收线程等。我们平常用的线程是工作线程,如果工作线程都退出了只剩下守护线程,那Java虚拟机就会自然退出。

Thread t = new Thread(new manager() );
t.setDaemon(true);
t.start();

上述代码用 setDaemon 方法设置一个线程为守护线程,注意设置必须要在start 方法前,不然会报错。

到这里Java并发程序基础基本上就结束了,我们在下一章介绍一下JDK并发包。

标签:实战,Java,Thread,关键字,并发,线程,内存,JMM,volatile
来源: https://www.cnblogs.com/hitsz-yc/p/15012810.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有