这部分内容是从高效并发的角度来谈论 Java 虚拟机如何处理多线程相关的问题。
计算机被要求并发执行的原因是:
1.处理器有这个能力 24 小时不间断高效的做任务。
2.由于从存储设备读写数据的速度远慢于处理器的运算速度,导致处理器会存在空闲等待的情况,为了避免等待,要求同时处理多个任务。
为了缩小这个速度差距,引入了一层缓存区,内存的数据会被先读到缓存,再读到处理器,同样写数据也是。由于内存是多线程(也就是多任务)共享的,这又导致另一个问题:缓存一致性。为了解决这个问题,处理器们就要遵循一套规范,协议来对缓存数据进行访问。
Java 内存模型
抽象的内存模型为的是屏蔽掉底层具体的硬件实现,达到跨平台的效果。这套模型的定义很大程度上也是为了解决并发内存访问的问题。模型始终围绕着并发过程中的原子性,可见性,有序性展开。
- 原子性
主要就是指基本数据类型的原子操作(long 和 double 类型的非原子协定),另外 synchronized 同步锁操作也具备原子性。 - 可见性
即某个线程修改了变量值,其他线程能立马知道这个修改。volatile 能在值修改后立即刷到主内存,在读取前保证工作内存的值是从主内存里刷新过来的。synchronized 和 final 也能保证可见性。 - 有序性
对单线程来说肯定是有序的。volatile 因为禁止了指令重排序优化,所以可以有序,synchronized 作为同步锁,自然也是有序的。 其余情况,java 将根据“先行发生”原则保证有序。
原子操作
相对于硬件上的内存和缓存,Java 内存模型定义了主内存与工作内存与之对应,并规定访问协议为以下 8 种操作,且每种操作都具有原子性。
- lock
- unlock
- read
- load
- use
- assign
- store
- write
volatile
是 Java 虚拟机提供的最轻量级的同步机制,当变量定义为 volatile 之后,它就具有对所有线程的可见性(即可以认为没有缓存不一致的问题,但不代表在多线程的情况下是安全的),如果要用 volatile 进行同步操作,首先要满足两个条件,否则还是建议用上同步锁,
- 运算结果不依赖变量的当前值,或能保证运行在单线程环境
- 变量不需要与其他的状态变量共同参与不变约束
另一个特性是禁止指令重排序优化。Java 代码在执行时会进行指令重排序优化,即过程可能不像我们代码写的那样的顺序,不过结果还是我们期望的结果,但这在多线程的时候,就有可能出问题,多线程需要必须保证顺序的正确性, volatile 可以避免重排序。
long 和 double 的特例
在谈论 long 和 double 类型时,前面说的 8 种原子操作中,load, store, read, write 可以不成立,即 long 和 double 的非原子性协定。
线程
线程的底层实现可以基于内核线程,用户线程或用户线程加轻量级进程混合,Java 线程的实现是基于操作系统原生线程模型来实现的。
Java 线程调度方式为抢占式调度,线程可以设置优先级来控制执行时间的多少,但优先级控制并不可靠;另外一种调度方式就是协同式调度。
线程安全
我们可以从 5 个方面来理解线程安全,
- 不可变性
拥有不可变特性的变量,对象等不管是单线程还是多线程都是安全的,例如 final 修饰的变量,String 类,枚举类,Long,Double 等数值包装类,BigInteger, BigDecimal 等。 - 相对线程安全
这是我们通常认为的线程安全,Java 类中说明是线程安全的主要就是指这种情况。它指的是在没有特殊需求的情况下(指的是单独操作),可以保证线程安全,其他情况可以借助同步手段来保证线程安全。例如 HashTable,Vector 这种线程安全的集合类 - 线程兼容
即本身不保证线程安全,但通过同步手段可以做到线程安全。例如 HashMap, ArrayList 等。 - 绝对线程安全
几乎不可能,因为要求的条件很严格。 - 线程对立
这种情况会造成线程的死锁。
互斥同步是确保线程安全的实现方式,通过互斥的手段达到线程同步访问某个代码段,从而保证线程安全。











网友评论