看完后,你再也不用怕面试问并发编程啦( 五 )

例如:这就像我们同时读两本书,当我们在读一本英文的技术书时,发现某个单词不认识,于是便打开中英文字典,但是在放下英文技术书之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后 , 能够继续读这本书 。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度 。
如何减少上下文切换
减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程 。

  • 无锁并发编程: 多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash 算法取模分段,不同的线程处理不同段的数据 。
  • CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁 。
  • 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理 , 这样会造成大量线程都处于等待状态 。
  • 协程:在单线程里实现多任务的调度 , 并在单线程里维持多个任务间的切换 。
小结
强烈建议多使用JDK并发包提供的并发原子类和工具类来解决并发问题 , 因为这些类都已经通过了充分的测试和优化 , 均可解决了上面提到的几个挑战 。8、Java内存模型(Java Memory Model)什么是JMM内存模型?
Java内存模型(即Java Memory Model,简称JMM) 。Java内存模型跟CPU缓存模型类似,是基于CPU缓存模型来建立的,Java内存模型是标准化的,屏蔽了底层不同计算机的区别 。
JMM本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式 。由于JVM运行程序的实体是线程 , 而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据 。而Java内存模型中规定所有共享变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问 。
线程对共享变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存 , 不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过 , 工作内存是每个线程的私有数据区域 , 因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
看完后,你再也不用怕面试问并发编程啦

文章插图
关于JMM中的主内存和工作内存说明如下:
  • 主内存
主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量 。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题 。
  • 工作内存
主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝) , 每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的 , 就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量 , 当然也包括了字节码行号指示器、相关Native方法的信息 。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题 。
启动2个线程,线程A读取主内存的共享变量数据,之后线程B修改共享变量数据,线程A无法感知:package cn.itcast.thread;public class Test5 {// 定义flag属性private static boolean flag = false;public static void main(String[] args) throws InterruptedException {// 创建线程1new Thread(() -> {long num = 0;while (!flag){num++;}// 如果没有打?。?说明当前线程无法感知flag的修改System.out.println("num = " + num);}).start();// 休眠1000毫秒Thread.sleep(1000);// 创建线程2new Thread(() -> {// 修改flagflag = true;System.out.println("flag = " + flag);}).start();}} 运行效果:没有任务打印输出,上面的线程无法感知flag的修改 。9、Java并发编程三大特性Java并发编程三个特性: 可见性、原子性、有序性 。


推荐阅读