java并发编程之美读书笔记前两章
1--80面
java并发编程基础
什么是线程
进程:
是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位
线程:
是进程的一次执行路径,一个进程至少有一个线程,进程中的多个线程共享进程的资源.
线程是CPU分配的基本单位
栈:
每个线程都有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程无法访问,除此之外栈还可以用来存放线程的调用栈帧.
堆:
堆是一个进程中最大的一个内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用new操作创建的对象实例.
方法区:
存放JVM加载的类,常量及静态变量等信息,也是线程共享的.
线程的创建与运行
java一共三种创建线程的方式
- 实现Runnable接口的run方法
- 继承Thread类并重写run方法
- 使用FutureTask方式
线程的通知和等待
wait()函数
当一个线程调用共享线程的wait()方法的时候,该线程就会被组赛挂起,知道发生以下几种才会返回
- 其他线程调用了该共享线程的notify()或者notifyAll()
- 其他线程调用了该线程的interrupt(),该线程抛出了中断异常返回
注意:调用wait()方法的时候如果没有提前获取该对象的监视器锁,则调用该方法的时候会抛出IllegalMonitorException异常
获取共享变量的监视器锁的方法
执行synchronized同步代码块时候,使用该共享变量作为参数
调用该共享变量方法的时候,并且该方法使用了synchronized来进行修饰
虚假唤醒
某个线程没有被其他线程调用notify(),notifyAll()方法进行通知,并且没有被中断,等待超时,就从挂起态变成了运行状态
要避免出现这种虚假唤醒
方法: 不断的测试该线程被唤醒的条件是否满足,不满足则继续等待,满足唤醒条件了就退出循环
注意:当前线程调用共享变量的wait()方法后只会释放当前共享线程变量上的锁,如果当前线程还持有其他线程的锁是不会被释放的.
当一个线程调用共享对象的wait()方法被阻塞挂起的时候,如果其他线程中断了该线程,则该线程会抛出InterruptedException异常并返回
wait(long timeout)函数
相比之前的外滩()方法多了一个超时参数
不同点:
如果一个线程调用共享对象的该方法挂起时,没有在指定的超时时间内被其他线程调用该共享线程的notify()和notifyAll()方法唤醒,就会因为超时而返回
如果调用一个负数则会抛出IllegalArgumentException异常
wait(long timeout,int nanos)函数
notify()函数
会唤醒一个在共享变量上调用的wait()系列方法后被挂起的线程.一个共享变量上可能会有很多个被阻塞的线程在等待,具体唤醒那个线程是随机的
被唤醒的线程不是立马就可以从wait()方法返回并运行,该线程必须在获取了对共享对象的监视器锁后才可以返回
没有获取到监视器锁的话则会抛出IllegalMonitorStateException异常
notifyAll()函数
该方法相比于上一个方法来说,该方法会释放该共享变量上面所有的由于调用wait()系列方法而被挂起的线程
注意:在共享变量上调用notifyAll()只会唤醒该方法前调用了wait系列函数而被放入共享变量等待集合里面的线程。而不会唤醒在该方法之后调用wait系列函数。
等待线程6373等待
等待线程执行终止的join方法
在需要等待多个事件完成后才螚继续往下执行的时候就可以使用Thread方法提供的join()方法
该方法是无参无返回值的方法。
线程A调用线程B的join方法后会被阻塞,当其他线程调用线程A的interrupt()方法中断线程A,则A会抛出一个InterruptedException异常
让线程睡眠都sleep方法
sleep是Thread的一个静态方法,调用该方法会暂时让出指定时间的执行权,也就是不参与CPU调度,但是该线程所拥有的监视器资源(锁)还是持有不让出的,指定的休眠时间到了就会正常返回,相当于从阻塞态变成了就绪态,参与CPU的调度,如果获取到CPU资源之后就可以进入运行态。但是如果在睡眠期间其他线程调用了该线程的Interrupt()方法中断了该线程,那么该方法调用sleep的时候会抛出InterruptedException异常而返回。
让出cpu执行权的yield方法
yield是Thread的一个静态方法,当线程调用yield方法时,就是按时线程调度器当前线程请求让出自己的cpu使用 但是线程调度器可以忽略这个暗示。
使用的时候意味着该线程自己占用都时间片没有使用完的部分不想使用了,暗示线程调度器现在就可以进行下一轮的线程调度。当前线程交出cpu使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,也有可能会调度到刚刚让出cpu的那个线程来获取cpu执行权。
总结:
sleep与yield方法的区别
当线程调用sleep方法时会调用线程会被阻塞挂起指定时间,在这期间线程调度器不会去调度该程。
当调用yield时候,线程会让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器在下一次调度的时候就有可能调度刀当前线程执行。
线程中断
线程中断是一种线程间协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。
- void Interrupt() :中断线程 例如当线程a运行时,线程b可以调用线程a的interrupt()方法设置a的中断标志为true并返回。设置标志仅仅是设置标志,线程a并没有被中断,他会继续往下执行,如果线程a调用了wait系列函数,join方法或者sleep等方法被阻塞挂起,这时候若是线程b调用a的interrupt()方法,线程a会在调用这些方法的时候抛出InterruptedException异常
- boolean isInterrupted() :检测当前线程是否被中断。如果是返回true,否则返回false
- boolean interrupted() :检测当前线程是否被中断,如果是返回true。不同点该方法发现当前线程被中断,会清楚中断标志,并且该方法是static方法,可以通过Thread直接调用在interrupted()内部是获取当前线程的中断标志而不是调用interrupted()方法的实例对象的中断标志。
一段Interrupt优雅退出的例子
根据中断标志判断线程是否终止的例子
当线程为了等待一些特定条件到来,一般会调用sleep函数,wait系列函数或者join函数来阻塞挂起当前线程,如果提前满足由阻塞到激活态的条件,这时候可以调用该线程的interrupt方法,强制sleep方法抛出InterruptedException异常而返回,线程恢复到激活状态
理解线程上下文切换
多线程里面,线程数一般是大于CPU个数的,而每个CPU同一时刻只能被一个线程所使用,为了让用户感觉到多个线程在同时使用的,CPU的分配采用了时间片轮转的策略.每一个线程分配一个时间片,线程在该分配的时间片内占用CPU执行任务,当线程片用完之后,就会处于就绪状态并让出CPU让其他线程占用使用.
上下文切换的时候需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场
切换时机:
- 当前线程的CPU时间片使用完处于就绪状态
- 当前线程被其他线程中断的时候
线程死锁
什么是线程死锁
死锁是指两个或者两个以上的线程在执行过程中,因抢夺资源而造成的互相等待的现象,在无外力的作用下,这些线程会一直等待下去而无法运行
产生死锁的四个必要条件
- 互斥条件
线程对已经获取到的资源进行排他性使用,资源同时只能由一个线程占用,如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占用的资源被释放
- 请求持有条件
指一个线程已经占有至少一个资源,但又提出新的资源请求,而新的资源已经被其他线程占用,所以当先线程被阻塞,但阻塞的同时并并不释放自己已经获得的资源
- 不可剥夺条件
指线程获取的资源在自己使用完之前别的线程不能抢占,只有在自己使用完之后释放了才能被其他线程使用
- 环路等待条件
指发生死锁的时候必然存在一个线程一个资源的环形链
避免线程死锁
只需要破坏至少一个死锁的必要条件即可,但是目前其实能被破坏的只有请求并持有和环路等待条件是可以被破坏的
造成死锁其实也和申请资源的顺序有很大的关系,使用资源的有序性原则就可以破坏避免死锁
举例:对上述代码的线程B进行修改
守护线程与用户线程
- daemon线程(守护线程)
- user线程(用户线程)
main函数就是属于用户线程JVM启动的时候会调用main线程,但其实还调用了许多其他的守护线程
两者的区别
当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否由守护线程,也就是说守护线程不影响JVM的退出.也就是说只要有一个用户线程没有结束,正常情况下JVM就不会退出
举例理解守护线程和用户线程的区别
总结
如果希望在主线程结束后JVM立马结束,那么在创建进程的时候可以将其设置为守护线程,否则的话就设置为用户线程
ThreadLocal
多线程访问退役个共享变量的时候容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时,为了保证线程安全,一般使用访问者在访问共享变量时候需要进行适当的同步
同步一般情况下使用加锁,但是这种方式加重了使用者的负担.
那么可以使用创建一个变量后,每一个线程对其访问的时候访问的是自己线程的变量,本节的ThreadLocal就是这个作用.
ThreadLocal
是由JDK提供的,提供了线程本地变量,如果创建了一个ThreadLocal变量,那么访问这个变量的每一个线程都会有这个变量的本地副本
当多个线程操作这个变量的时候,实际操作的是自己本地内存的变量,从而避免了线程安全问题
使用实例
实现原理
set方法
getMap(t)
- 方法的作用是获取线程自己的变量threadLocals,threadlocal变量被绑定到线程的成员变量上面
- 如果个体Map(T)返回值不为空,则把value设置到threadLocals
- 如果返回值为空则说明第一次调用的set方法,这时候创建当前线程的threadLocals变量
createMap(t,value)
创建当前线程的threadLocals变量
T get()方法
void remove()
不支持继承性
同一个ThreadLocal变量在父线程中被设置后,在子线程是获取不到的
在子线程里面调用的get方法是当前线程,而调用的set方法是设置的main线程
两者是不同的线程
InheritableThreadLocal类
继承自ThreadLocal,提供了一个特性,让子线程可以访问在父线程中设置的本地变量
总结:
InheritableThreadLocal类重写代码让本地变量保存到了具体线程的inheritableThreadLocals变量里面,那么线程在通过InheritableThreadLocal类实例的set或者get()方法设置变量时,就会创建当前线程的InheritableThreadLocals变量,当父线程创建子线程时,构造函数就会把父线程的InheritableThreadLocals变量里面的本地变量复制一份保存到子线程的InheritableThreadLocals变量里面.
并发编程的其他基础知识
什么是多线程的并发编程
并发:
同一时间段内多个任务同时都在执行,且执行都没有执行结束,强调的是在一个时间段内同时执行,而一个时间段由多个时间积累而成的,所以并发的多个任务在单位时间内并不一定同时执行
并行:
单位时间内多个任务同时在执行
为什么要进行多线程并发编程
多核CPU时代打破了单核CPU对多线程的性能限制,多个CPU"意味着每个线程可以使用自己的CPU运行,这减少了线程上下文切换的开销,但是随着对应用系统性能和吞吐量要求的提高,出现了海量数据和请求的要求,迫切需要高并发编程
java的线程安全问题
共享资源:
该资源被多个线程所持有或者说多个线程都可以访问该资源
线程安全问题:
当多个线程同时读写一个共享资源并且没有任何同步措施的时候,导致出现脏数据或者不可预见结果的其他问题
java中共享变量的内存可见性问题
将所有的变量都存放在主内存,当线程使用变量的时候,会把主内存的变量复制到自己的工作空间或者工作内存,线程读写变量操作的是自己工作内存中的变量
java中的synchronized关键字
关键字介绍
java提供的一个原子性内置锁,java的每一个对象都可以把他当作同步锁来使用,这些java内置的使用者看不到的锁被称为内部锁,与叫做监视器锁.
也是一种排他锁,也就是一个线程获取了这个锁后,其他线程必须等待该线程释放锁后才能获取该锁
java中的线程和操作系统的原生线程一一对应,当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的,而synchronized的使用就会导致上下文切换
内存语义
把在synchronized块内使用到的变量从线程的工作内存中清楚,这样子在synchronized块内使用的变量就不会从线程的工作内存中获取,而是从主内存中获取,退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存
而synchronized的使用就会导致上下文切换带来线程调度开销
java中的volatile关键字
确保对一个变量的更新对其他线程马上可见,当一个变量使用volatile关键字时,线程在写入变量的时候就不会把值缓存在寄存器或者其他地方,而是会把值刷新到主内存,当其他线程读取该共享变量的时候,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值
内存语义
和synchronized相似,当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存)读取volatile变量值时就相当于进入了同步块(先清空本地内存变量值,再从主内存获取最新值).
synchronized 方法
volatile方法
volatile并不能保证操作的原子性
什么时候使用volatile关键字
写入变量值不依赖变量的当前值
因为如果依赖当前值,将获取-计算-写入三步操作,这三步不是原子性操作,而volatile不能保证原子性
读写变量值没有加锁
因为加锁本身已经保证了内存可见性,这时候不需要啊变量声明为volatile
java中的原子操作
一系列操作时,这些操作要么全部执行,要么全部不执行,不存在执行性其中一部分的情况.
如下代码就是线程不安全的问题
使用synchronized修饰就可以保证原子性
java中的CAS操作
锁在并发处理中占据一席之地,但是当一个线程没有获取到锁时就会被阻塞挂起,导致线程上下文的切换喝调度开销,java提供了非阻塞的volatile关键字来解决共享变量的可见性,一定程度上解决了锁带来的开销问题,但是只能保证共享变量的可见性,不能解决读-改-写等原子性操作
CAS即 Compare and Swap 是JDK提供的非阻塞原子性操作,通过硬件保证了比较--更新操作的原子性
JDK里面Unsafe提供了一系列的compareAndSwap方法
boolean compareAndSwapLong(Object obj,long valueOffset,long expect,long update)方法
比较并交换
四个参数分别是
- 对象内存位置
- 对象中变量的偏移量
- 变量预期值
- 新的值
- 如果对熊obj中内存偏移量为valueOffset的变量的值为expect,则使用新的值update置换旧的值expect
ABA问题
关于CAS 操作有个经典的ABA 问题,具体如下:假如线程I使用 CAS 修改初始值为A的变量X,那么线程I会首先去获取当前变量X 的值(为A),然后使用 CAS 操作尝试修改X的值为 B,如果使用CAS 操作成功了,那么程序运行一定是正确的吗?其实未必,这是因为有可能在线程I获取变量X的值A后,在执行 CAS 前,线程I使用CAS修改了变量X的值为 B,然后又使用 CAS 修改了变量X 的值为 A。所以虽然线程I执行 CAS时X的值是A,但是这个A已经不是线程I获取时的A了。这就是 ABA 问题。
ABA 问题的产生是因为变量的状态值产生了环形转换,就是变量的值可以从A到B,然后再从B到A。如果变量的值只能朝着一个方向转换,比如A到B,B到C,不构成环形,就不会存在问题。JDK 中的AtomicStampedReference 类给每个变量的状态值都配备了一个时间戳,从而避免了 ABA 问题的产生。
Unsafe
Unsafe类的重要方法


如何使用Unsafe类
运行结果
正规渠道不能使用Unsafe方法,要想使用可以用反射方法来获取Unsafe的实例方法
java指定重排序
java内存模型允许编译器喝处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序,在单线程下重排序可以保证最终的执行结果与程序顺序执行结果一致,但是在多线程下存在安全问题
当volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后,读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前
伪共享
伪共享是什么
为了解决计算机系统中内存与CPU之间运行速度差问题,会在CPU与主内存之间添加一级或者多级高速缓存存储器(Cache)
一般被集成在CPU内部(CPU Cache)
在Cache内部是按行存储的,其中一行称为一个Cache行,Cache行是Cache与主内存进行数据交换的单位,每行的大小一般为2的幂次方字节
当CPU访问某个变量的时候,首先回去看CPU Cache内是否存在变量,如果有则直接从其中获取否则就去主内存中获取变量,然后把该变量所在的内存区域的一个Cache行大小的内存复制到Cache,由于存放到Cache行的内存块而不是单个变量,所以可能把多个变量存放到同一个Cache中,当多个线程同时修改一个缓存行的里面的多个变量的时候,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享
多个线程不能同时去修改自己所使用的CPU中相同缓存行里面的变量,更坏的情况是,如果CPU只有一级缓存,则会导致频繁的访问主内存
为什么会出现伪共享
是因为多个变量被放入了一个缓存行,并且多个线程同时去写入缓存行中不同的变量
原因:
因为缓存与内存交换的数据的单位就是缓存行,所以多个变量会被放入同一个缓存行
代码一比代码二快了不少原因
数组中数组元素的内存地址是连续的,当访问数组的第一个元素的时,会把第一个元素后的若干个元素一块存入缓存行,这样子顺序访问数组里面的元素时直接命中,就不会去主内存读取了.
第二个则是跳跃式的访问数组元素,不是顺序的,这样子破坏了程序访问的局部性原则,并且缓存是容量控制的,当缓存满了会根据一定淘汰算法替换缓存行,这会导致从内存置换过来的缓存行的元素还没等读取到就被置换了
所以在单个线程下顺序修改一个缓存行中的多个变量,会充分利用程序员运行的局部性原则,从而加速程序的运行,而在多线程下修改一个缓存行的多个变量时候就会竞争缓存行,从而降低程序运行性能
如何避免伪共享
通过字节填充的方式来避免该问题,也就是创建一个变量时使用填充字段填充改变量所在的缓存行,这样就避免了将多个变量存放在同一个缓存行中
假设缓存行为64个字节'
这六个变量加上volatile变量以及一个FilledLong类对象一共占用64个字节正好是可以存入同一个缓存行

小结
本节讲述了伪共享如何产生,如何避免,并证明在多线程下访问同一个缓存行的多个变量时才会出现伪共享,在单线程下访问一共缓存行里面的多个变量反而会对程序运行起到加速作用
锁的概述
乐观锁与悲观锁
悲观锁:
对数据被外界修改保持保守态度,认为数据很容易被其他线程修改,所以在数据被处理之前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态
实现:
往往依赖数据库提供的锁机制,即数据库中,在对数据记录操作前给记录加排他锁,如果获取锁失败,则说明数据正在被其他线程修改,当前线程则等待或者抛出异常,如果获取锁成功,则对记录进行操作,然后提交事务后释放排他锁
乐观锁:
数据在一般情况下不会造成冲突,所以在访问记录前不会加排他锁,而是在数据提交更新时候,才会正式对数据冲突与否进行检测.
公平锁和非公平锁
根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁
公平锁:
表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程最早获取到锁
非公平锁:
不一定先到先得
ReentrantLock提供了公平锁和非公平锁的实现
- 公平锁:ReentrantLock pairLock=new ReentrantLock(true)
- 非公平锁:ReentrantLock pairLock=new ReentrantLock(false)
如果不传递参数默认是非公平锁
公平锁会带来性能的开销
独占锁和共享锁
根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁
独占锁:
能保证任何时候都只能有一个线程得到锁ReentrantLock就是以独占锁方式实现的
是一种悲观锁,由于每次访问资源都先加上互斥锁,限制了并发性,因为操作并不会影响数据的一致性,而独占锁只允许同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取
共享锁:
ReadWriteLock读写锁,允许一个资源被多个线程同时进行读操作是一种乐观锁,放宽了加锁的条件,允许多个线程同时进行读操作
什么是重入锁
当一个线程要获取一个被其他线程持有的独占锁时候,该线程会阻塞,
那么当一个线程再次获取他自己已经获取的锁时候是否会被阻塞呢?
如果不被阻塞那么该锁是可重入的,也就是说只要该线程获取了该锁,就可以无限次地进入该锁锁住的代码
自旋锁
当前线程在获取锁的时候,如果发现锁已经被其他线程占用,不会马上阻塞自己,在不放弃CPU的使用权的情况下,多次尝试获取(默认次数10)很有可能后面几次尝试中其他线程已经释放了锁,如果尝试指定次数之后仍然没有获取到锁则当前线程则会被阻塞挂起,自旋锁是适应了CPU时间旱区线程阻塞与调度的开销,但是很有可能这些CPU时间白白浪费啦
总结
本章主要讲述并发编程的基础知识,为后面在高级篇讲解并发包源码打下了基础并结合图示形象的讲述了为什么要使用多线程编程,多线程编程存在的线程安全问题,以及什么是内存可见性问题,然后讲述了synchronized和volatile关键字,并且强调前者既保证内存的可见性又保证了原子性,后者则主要保存了内存可见性,但是二者的内存语义非常相似,最后讲解了什么是CAS和线程间同步以及各组锁概念
