Java并发编程笔记

来源:互联网 发布:windows 安全配置指南 编辑:程序博客网 时间:2024/05/21 18:33

在多线程环境下,必须考虑同步问题,当多个线程访问某个状态变量并且其中至少有一个线程执行写入操作时, 必须采用同步机制来协同这些线程对变量的访问。Java中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,其他的同步术语还包括volatile类型的变量, 显示锁(Explicit Lock)以及原子变量。

synchronized

synchronized用于控制线程同步,synchronized既可以加在一段代码上,也可以 加在方法上。`注意,synchronized是对象锁,锁住的是对象的同步代码块和方法。当退出同步代码块时自动释放锁(与lock不同,lock则需要显式地释放锁。从而lock可以在不同的地方进行加锁和释放锁)。synchronized在同步代码块前加入monitorenter字节码,在同步代码块结束前加入monitorexit字节码。synchronized锁是可重入的。
对类方法(static method)加上关键字synchronized则是对类加锁,相当于全局锁。

volatile(可见性,不会重排序)

Java语言提供了一种稍弱的同步机制, 即volatile变量,用来保证将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器和运行时都会注意到这个变量是共享的,因此不会对该变量上的操作和其他内存操作一起做重排序。在读取volaite类型的变量时总会返回最新写入的值。 结合java内存模型来看,在一个线程内对volatile变量的改变对其他线程是立即可见的。这就好像是每个线程对volatile变量的操作都是直接对主内存变量操作,而不是对其工作内存保存的拷贝进行操作。对从内存可见性的角度来看,写入volatile变量相当于退出同步代码块, 而读取volatie变量就相当于进入同步代码块(并不是说volatile变量的读取与同步代码块的同步方式相同,它们的底层原理并不相同)。**volatile的使用场景:
1. 对变量的修改不依赖变量的原值。正例:a = 1; 反例:a = a + 1;(由于java中的运算操作不是原子操作,所以volatile变量的运算在依赖原值的情况下在并发中一样时不安全的)
2. 变量不需要与其他状态变量共同参与不变约束。

Java内存模型(CHA12)

物理机中的并发问题:硬件的效率和一致性,处理器的运算速度与计算机的存储设备的传输速度不匹配,所以现代计算机系统不得不加入一层读写速度尽量接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能够快速进行,当运算结束后再从缓存同步回内存,这样处理器就无需等待缓慢的内存读写了。(显然这在并发的情况下可以满足提高吞吐量的要求。)

基于高速缓存的存储交互解决了处理器和内存的速度矛盾,但引入了一个新问题:缓存一致性。在多处理器系统中,每个处理器都有自己的高速缓存而它们又共享同一主内存。当多个处理器的运算任务都涉及同一主内存区域时,可能将导致各自缓存的数据不一致。
java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种软件和操作系统的内存访问差异,以实现让Java程序在各种平台上都能达到一致的内存访问效果。
java内存模型的主要目标是定义程序中各个变量的访问规则,即虚拟机将变量存储到内存中和从内存中取出变量这样的底层细节。此处的所指的变量不包括局部变量和方法参数,因为它们是线程私有的,不会被共享也就自然不存在竞争问题。

主内存和工作内存

java内存模型规定所有的变量存储在主内存中(与介绍物理硬件的主内存名字一样,可互相类比)。每条线程还有自己的工作内存(类比高速缓存)。线程的工作内存中保存了被该线程使用的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程也无法直接访问对方工作内存中的变量,线程间的变量值传递均需要通过主内存来完成。

内存间的交互操作

关于主内存和工作内存之间具体的交互协议,Java内存模型规定了一下8种操作来完成。
* lock
* unlock
* read
* load
* use
* assign
* store
* write
跟本科时做的课程设计五级流水线cpu的工作流程很像,可自行回忆(解决流水线的三大相关问题)。

先行发生原则(happens-before)

下面是java内存模型中天然的一些先行发生关系,这些先行发生关系无需任何同步器协助就已经存在。

  • 程序次序原则(Program Order Rule):在一个线程内,按照程序代码控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 管程锁定原则(Monitor Lock Rule):一个unlock发生于对同一个锁的lock操作之前。
  • volatile变量规则:对一个volatie的写操作先行发生于对它的读操作。
  • 传递性:若A先行发生于B,B先行发生于C,则A先行发生于C。

线程

说明操作系统这门课还是有用啊…。实现线程主要有三种方式:
* 使用内核线程实现
* 使用用户线程实现
* 使用用户线程+轻量级进程混合实现(Java方式)

线程的调度

线程的调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是:
* 协同式线程调度(线程的执行时间由线程本身控制)
* 抢占式线程调度(Java使用)
没有提及调度算法,省去好多时间,爽。不过还是记一下常用的吧。先来先服务(FCFS),短进程优先(SPF),高优先权优先调度(说明有优先级了),高响应比优先调度(解决长作业长期得不到服务问题),时间片轮转。

线程的状态转换

  • 新建
  • 运行
  • 无限期等待,需要被其他进程显式唤醒。
    • 没有设置Timeout参数的Thread.jion()方法。
    • 没有设置Timeout参数的Object.wait()方法。
    • LockSupport.park()方法
  • 限期等待:
    • Thread.sleep()方法。
    • 设置了Timeout参数的Thread.jion()方法。
    • 设置了Timeout参数的Object.wait()方法。
  • 阻塞:等待获取一个排他锁。
  • 结束。

线程安全和锁优化

线程安全的实现方法(仔细读一遍《深入理解java虚拟机啊》-CHA13.2.2)

  1. 互斥同步:互斥指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或是一些,当使用信号量时)线程使用。互斥是实现同步的一种手段。
    1. 在java中,最基本的互斥同步手段就是synchronized关键字。synchronized关键字在经过编译后,会在同步块前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型来指明要锁定和解锁的对象,如果Java程序中synchronized关键字指明了对象参数,那就是这个对象的reference;若没有明确指明,那就根据synchronized修饰的是实例方法还是类方法,去获取对应的实例对象或Class对象来作为锁对象。
      根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经获得了那个对象的锁(说明了是可重入的),则把锁的计数器加1。相应地,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。如果获取对象的锁失败,那当前线程就要阻塞等待。
    2. ReentrantLock(java.util.concurrent包中的重入锁),基本用法上与synchronized相似,也是线程可重入的,但在代码写法上有区别,一个表现为API层面的互斥锁(lock()和unlock()方法配合try/finally语句块来完成),另一个表现为原生语法层面的互斥锁,与synchronized相比,ReentrantLock增加了一些高级功能。主要有一下三项:
      1. 等待可中断:指当持有锁的线程长期不释放锁时,正在等待锁的线程可以选择放弃等待,改为处理其他的事情,可中断特性对处理执行时间非常长的同步块很有帮助。
      2. 公平锁:指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁,而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized锁是非公平的,ReentrantLock默认情况也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。
        1. 锁绑定多个条件,值ReentrantLock对象可以同时绑定多个Condition对象(??),而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联时,就不得不额外地添加一个锁。
  2. 非阻塞同步:互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也叫作阻塞同步(Blocking Synchronization),从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁等操作。随着硬件指令集的发展,我们有了另外一个选择,:基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就采取其他的补偿措施,这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)。 常见的有CAS。

CAS(Compare And Swap,比较并交换)

通过硬件保证CAS只通过一条处理器指令就可以完成。CAS指令需要三个操作数,分别时内存位置(在Java中可以简单理解为变量的内存地址,用V表示),旧的预期值(用A表示)和新值(用B表示)。当CAS指令执行时,当且仅当V符合旧预期值A时,处理器用新值B更新V的值,否则它就不执行更新,但是无论是否更新了V值,都会返回V的旧值,上述处理过程是一个原子操作。
CAS尽管看起来很美,但无法涵盖互斥同步的所有使用场景。例如存在ABA问题,变量V初次读取时为A,并且在准备赋值的时候检查到它仍然为A,那我们就能说它的值没有被其他线程更改过了吗?如果这段时间它的值曾被改为B,后来又被改回为A,CAS操作则会误认为它从来没有被改变过,大部分情况下ABA问题不会影响并发的正确性,如果需要解决ABA问题,改为使用互斥同步。

锁优化

自旋锁和自适应自旋

轻量级锁

轻量级锁能提升程序同步性能的依据是:“对于绝大部分的锁,在整个周期内是不存在竞争的。”如果没有竞争,轻量级锁使用CAS操作避免了互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。
虚拟机对象(对象头部分)的内存布局,对象头分为了两部分信息,第一部分用于存储对象自身的运行时数据,如HashCode,GC分代年龄等,称为“Mark Word”。它是实现轻量级锁和偏向锁的关键。另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。
在代码进入同步块的时候,如果此时同步对象没有被锁定,虚拟机将首先在当前线程的栈帧中创建一个名为锁记录的空间,用于存储锁对象当前Mark Word的拷贝(官方把这个拷贝加了一个Displaced名字前缀,即Displaced Mark Word)。
然后虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向锁记录(Lock Record)的指针。如果这个操作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位将转变。

偏向锁

目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争情况下把整个同步都消除掉,连CAS都不做了。
未完

0 0
原创粉丝点击