文档章节

java 多线程总结

侠客人生
 侠客人生
发布于 2017/07/20 12:56
字数 4075
阅读 97
收藏 2

1. 多线程

1.1进程

进程是一个实体。每一个进程都有它自己的地址空间。进程是一个“执行中的程序”。

1.2线程

一个进程中可以包含若干个线程 

1.3线程与进程的区别

1.3.1地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
1.3.2通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
1.3.3调度和切换:线程上下文切换比进程上下文切换要快得多。

1.3.4在多线程OS中,进程不是一个可执行的实体。

eg: 

QQ 进程

给范冰冰聊天 线程

给普京聊天 线程

1.4线程的状态

新建态,就绪态,运行态,阻塞态,消亡态

示意图

1.5创建线程的方式

1.5.3.1实现Runnable接口,重写run方法

1.5.3.2直接继承Thread类

1.5.3.3实现Runnable接口后使用线程池ThreadPool

    eg: 企业中推荐使用该方法去创建线程,想了解具体请查看【java线程池】

           https://my.oschina.net/u/2505908/blog/1476242

    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

1.5.3.4实现Callable接口

1.6多线程安全问题

1.6.1产生原因

多个线程可以利用所拥有的共享资源

1.6.2解决思想

就是将多个 [ 操作共享数据 ]的线程 代码封装起来,

当有线程在执行这些代码的时候,其他线程是不可以参与运算的,

必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。

比如synchronized代码块

2. java内存模型(JMM:java memory model)

2.1硬件的效率与一致性

2.1.1高速缓存产生原因:

由于计算机的存储设备与处理器的运算能力之间有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(cache)来作为内存与处理器之间的缓冲

2.1.2读写过程

将运算需要使用到的数据复制到缓存中,让运算能快速进行,

当运算结束后再从缓存同步回内存之中,这样处理器就无需等待缓慢的内存读写了。

2.1.3缓存一致性(Cache Coherence)

多个处理器运算任务都涉及同一块主存,需要一种协议可以保障数据的一致性,这类协议有MSI、MESI、MOSI及Dragon Protocol等

当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本, 会发出信号通知其他CPU将该变量的缓存行置为无效状态,

因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取

2.1.4Cpu读写示意图

 

2.1.5类比

Java虚拟机内存模型中定义的内存访问操作 与 硬件的缓存访问操作 是具有相似性

2.2 多线程类比cpu读写示意图

JMM

概念:描述了java程序中各种变量(共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节

2.2.2共享变量

概念:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量

即 一个变量 在 多个线程中 存在 多个副本,每个线程都是操作各自的变量副本,就可能发生副本不一致

2.2.2多线程操作共享变量规则

a. 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写

b. 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成

 

2.3内存间交互操作

注意:多线程讨论的安全问题都是针对共享变量而言,不包括局部变量

每个线程 定义的局部变量,不会被其他线程访问

2.3.1 线程读取变量过程

数据流向:从主内存拷贝到工作内存

3个操作:

lock(锁定)→ 变量标识为线程独占  →

→read(读取)→ 变量值从主内存传输到线程的工作内存中→

→load(载入)→ 把read操作从主内存中得到的变量值放入工作内存的变量副本中

2.3.2操作变量

两个操作

use(使用)→ 把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作→

→assign(赋值)→把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作

2.3.3从工作内存同步到主内存

三个操作

store(存储)→把工作内存中的变量的值传送到主内存中→

→write(写入)→把store传递过来的变量值写入主内存的变量中→

→unlock(解锁)→把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

2.3.4顺序执行规则

Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是可以插入其他指令的。

如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a

大家可以想一下,工作内存同步到主内存,主内存再同步到工作内存,需要花费时间的,花费的时间越多,发成不安全的因素也就越多,也就是我们的俗话:“夜长梦多”!!!

2.3.5重排序

 

2.3.5.1概念:代码书写的顺序与实际执行的顺序不同

2.3.5.2重排序的作用:在执行程序时为了提高性能,编译器或处理器经常会对指令进行重排序。

2.3.5.3三种类型:

1. 编译器优化的重排序(编译优化)

编译器在不改变单线程程序语义前提下,可以重新安排语句的执行顺序。

2. 指令级并行的重排序(处理器优化):

现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

3. 内存系统的重排序(处理器优化):

由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从Java源代码到最终实际执行的指令序列的过程:

 

2.3.5.4内存屏障

作用:为了保证内存的可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。

分类:LoadLoad、LoadStore、StoreLoad和StoreStore

2.3.5.5 as-if-serial

无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(java编译器、运行时和处理器都会保证java在单线程下遵循as-if-serial语义)

重排序不会给单线程带来内存可见性问题

多线程中程序交错执行时,重排序可能会造成内存可见性问题

3如何保证线程安全

3.1必须满足三个特性

原子性,可见性,有序性

3.2原子性

3.2.1概念:

原子性:

是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中 断操作,要不执行完成,要不就不执行。

原子操作:

指不可中断的一个或一系列操作

如:id=0是原子操作,id++不是原子操作,id=0,tmp=id+1,id=tmp

3.2.2线程安全:

原子操作(基本数据类型的访问和读写),

原子类(concurrent包下AtomicInteger、AtomicLong、AtomicReference)

3.2.3线程不安全:

非原子操作,非原子操作可转化为原子操作(通过synchronized,lock)

3.3可见性

3.3.1概念:

1. 可见性就是指

当一个线程修改了线程共享变量的值,

其它线程能够立即得知这个修改, 即变量的新值

2. 可见性,

是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。

也就是一个线程修改的结果。另一个线程马上就能看到

3.3.2导致共享变量在线程间不可见的原因:

1.线程的交叉执行——非原子性

2.重排序结合线程交叉执行——非原子性

3.共享变量更新后的值没有在工作内存和主内存间及时更新——非可见性

3.3.3可见性实现过程:

Java内存模型是通过

在变量修改后将新值同步回主内存,

在变量读取前从主内存刷新变量值 

这种依赖主内存作为传递媒介的方法来实现可见性的,

无论是普通变量还是volatile变量都是如此

3.3.4可见性的实现方法:

Volatile

规则:

工作内存中修改的新值能立即同步到主内存,

以及每使用前立即从内存刷新。

volatile只能让 [ 被它修饰的变量 ] 具有可见性,但不能保证它具有原子性。

比如 :  volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就这这个操作同样存在线程安全问题

底层原理:

1.通过加入内存屏障和禁止重排序优化来实现

2.对volatile变量执行写操作时,会在写操作后加入一条store屏障指令,即强迫线程将最新的值刷新到主内存中;

3.而在读操作时,会加入一条load屏障指令,即强迫从主内存中读入变量的值

volatile本质:是在告诉JVM当前变量在寄存器中的值是不确定的,

使用前,需要先从主存中读取,因此可以实现可见性。

线程写volatile变量的过程:

1. 改变线程工作内存中volatile变量副本的值

2. 将改变后的副本的值从工作内存中刷新到主内存中

线程读volatile变量的过程:

1. 从主内存中读取volatile变量的最新值到线程的工作内存中

2. 从工作内存中读取volatile变量的副本

Synchronized

JMM对synchronized的两条规定:

线程解锁前,必须把共享变量的最新值刷新到主内存中

线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(加锁与解锁是同一把锁)

线程执行互斥代码的过程:

1.获得互斥锁

2.先清空工作内存

3.在主内存中拷贝最新变量的副本到工作内存

4.执行代码

5.将更改后的共享变量的值刷新到主内存中

6.释放互斥锁。

 Final

被final修饰的字段是构造器一旦初始化完成,并且构造器没有把“this”引用传递出去,那么在其它线程中就能看见final字段的值。

 Synchronized和Volatile的比较

1)Synchronized保证内存可见性和操作的原子性
    2)Volatile只能保证内存可见性
    3)Volatile不需要加锁,比Synchronized更轻量级,并不会阻塞线程(volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。)
    4)volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化(如编译器重排序的优化).
    5)volatile是变量修饰符,仅能用于变量,而synchronized是一个方法或块的修饰符。

3.4有序性

3.4.1概念:

如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。

前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存中主内存同步延迟”现象。

 

3.4.2保证线程之间操作的有序性

a. volatile

volatile关键字包含了禁止指令重排序的语义

b. synchronized

由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则来获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入

 

3.4.3先行发生原则:

指Java内存模型中定义的两项操作之间的依序关系,如果说操作A先行发生于操作B,其实就是说发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包含了修改了内存中共享变量的值、发送了消息、调用了方法等

 

3.4.4先行原则的作用:

判断数据是否存在竞争,线程是否安全

3.4.5注意

一个操作”时间上的先发生“不代表这个操作会是”先行发生“,那如果一个操作”先行发生“是否就能推导出这个操作必定是”时间上的先发生“呢?也是不成立的,一个典型的例子就是指令重排序。所以时间上的先后顺序与先生发生原则之间基本没有什么关系,所以衡量并发安全问题一切必须以先行发生原则为准。

3.5总结

关键字\分类     原子性 可见性 有序性
volatile  
Synchronized
final    

 

应用场景

1. spring多线程访问共享数据处理

使用ThreadLocal方式,为每一个线程维护自己的一份变量副本。

案例分析

1) 案例我们结合懒汉式去讲线程安全(但对于我们企业级开发肯定不适用这种方式实现单例,具体请查看【单例详解】这篇文章)

import java.io.Serializable;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 
 * @Description:懒汉式   
 * @author:侠客人生    
 * @date:2017-4-18 上午7:18:56   
 * @version:V1.0  
 * @Copyright:2017 侠客人生 Inc. All rights reserved.
 */
public class Singleton implements Serializable {
   //3 创建变量来存储实例
   /**
    * 通过volatile 让instance具有可见性,但不能保证它具有原子性。
    */
   private static volatile Singleton instance = null;

   // 定义变量记录调用的次数
   /**
    * AtomicInteger 原子操作,线程安全
    */
   private static AtomicInteger count = new AtomicInteger(0);
   
   //私有构造方法,好在内部控制创建实例的数目
   private Singleton() {
      count.incrementAndGet();
   }
   
   public void show(){
      System.out.println("初始化实例次数:"+count);
   }

   /**
    * 通过这个单例问题大家一定要学会两个编程思想
    *  1) 延迟加载的思想
    *  2) 缓存思想
    *  我们再开发过程中,这两个思想会在我们的项目中经常使用,我们可以借鉴懒汉式去写自己的缓存来提高性能
    */
   //2.提供一个全局访问点
   //避免先生鸡还是先有蛋的问题,我们static 让其变成类级方法
   public static Singleton getInstance(){
      //4 判断我们instance 是否为空 B
      if(instance == null){
         /**
          * synchronized 保证其操作的原子性
          */
         synchronized (Singleton.class) {
            if(instance==null){
               //4.1 直到需要用我才去创建 A B
               instance = new Singleton();
            }
         }
        
      }
      //4.1 直接返回已经创建好的实例
      return instance;
   }

   /**
    * 从时间和空间角度分析:时间 换 空间
    * 从线程安全角度分析:线程不安全
    */
   
     private Object readResolve() {
           return instance;
    }
}

© 著作权归作者所有

共有 人打赏支持
侠客人生
粉丝 15
博文 43
码字总数 82954
作品 0
朝阳
私信 提问
JAVA基础再回首(三十)——JAVA基础再回首完美结束,感概万千!

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/m366917/article/details/52724939 JAVA基础再回首(三十)——JAVA基础再回首完美结束,感概万千! 经过了几...

Aduroidpc
2016/10/02
0
0
金九银十,史上最强 Java 面试题整理。

以下会重新整理所有 Java 系列面试题答案、及各大互联网公司的面试经验,会从以下几个方面汇总,本文会长期更新。 Java 面试篇 史上最全 Java 面试题,带全部答案 史上最全 69 道 Spring 面试...

Java技术栈
09/13
0
0
JVM内存结构 VS Java内存模型 VS Java对象模型

Java作为一种面向对象的,跨平台语言,其对象、内存等一直是比较难的知识点。而且很多概念的名称看起来又那么相似,很多人会傻傻分不清楚。比如本文我们要讨论的JVM内存结构、Java内存模型和...

Java架构
07/11
0
0
java.lang.ThreadLocal类研究

java.lang.ThreadLocal类研究 1、概述 ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是thread local variable(线程局部变量)。也许把它命名为...

SDK4
2011/09/17
0
2
InheritableThreadLocal详解

1、简介 在上一篇 ThreadLocal详解 中,我们详细介绍了ThreadLocal原理及设计,从源码层面上分析了ThreadLocal。但由于ThreadLocal设计之初就是为了绑定当前线程,如果希望当前线程的ThreadL...

沈渊
04/12
0
0

没有更多内容

加载失败,请刷新页面

加载更多

阿里千万级高性能、高并发架构的经验之谈

架构以及我理解中架构的本质 在开始谈我对架构本质的理解之前,先谈谈对今天技术沙龙主题的个人见解,千万级规模的网站感觉数量级是非常大的,对这个数量级我们战略上 要重 视 它 , 战术上又...

别打我会飞
22分钟前
3
0
Adnroid架构的详细说明

armeabi armeabi是一个非常老的基于ARM的架构。从Android 4.4开始,CDD(compatibility definition)严格要求ARMv7读取CDD文档。 CDD是Google向设备制造商提供的每个Android版本的规范,它包...

CrazyManDF
25分钟前
3
0
微信小程序内嵌网页web-view

web-view 组件是一个可以用来承载网页的容器,会自动铺满整个小程序页面。个人类型与海外类型的小程序暂不支持使用。 客户端 6.7.2 版本开始,navigationStyle: custom 对 <web-view> 组件无...

xiaogg
26分钟前
2
0
单例模式

第一种方式 public class SingletonA { public static final SingletonA INSTANCE = new SingletonA(); private SingletonA(){ //do something }} 第二种方式 public......

wuyiyi
26分钟前
2
0
git: Authentication failed for错误解决

如果push遇到在输入密码是输错后,就会报这个错误fatal: Authentication failed for 解决办法: git config --system --unset credential.helper 之后你在push就会提示输入名称和密码...

落雪飞声
27分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部