第四章 Java并发编程基础
一、线程简介
使用多线程的原因:
1.更多的处理器核心:一个线程在一个时刻只能运行在一个处理器核心上
2.更快的响应时间
3.更好的编程模型
线程优先级:
操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,
当线程的时间片用完了就会发生线程调度,并等待着下次分配,线程分配到的时间片多少就决定了线程使用
处理器资源的多少。
线程的状态:6种,在给定的时刻只能处于一种状态
NEW:初始状态,线程被构建,但还没有调用start方法
RUNNABLE:运行状态,java线程将就绪和运行两种状态统称为运行状态
BLOCKED:阻塞状态,表明线程阻塞于锁
WAITING:等待状态,等待其他线程的通知或中断
TIME_WAITING:超时等待状态,可以在指定的时间自行返回
TERMINATED:终止状态,表示当前线程已经执行完毕
Daemon线程(守护线程)
当一个java虚拟机中不存在非守护线程时,虚拟机将会退出。
ps. 在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑
二、启动和终止线程
一个新构造的线程对象是由其父线程来进行空间分配的,而子线程的各种属性继承自父线程,同时还会分配一个唯一的ID。
启动线程:start方法:当前线程同步告知虚拟机:只要线程规划器空闲,应立即启动调用start方法的线程
中断:
可以理解为线程的一个标识符属性,它标识一个运行中的线程是否被其他线程进行了中断操作。
线程通过isInterrupted()方法判断是否被中断,也可以调用静态方法Thread.interrupted()
对当前的线程中断标识位进行复位。
从java的API可以看出,许多声明抛出InterruptedException的方法在抛出这个异常前,java虚拟机会
先将该线程的中断标志位清除,然后再抛异常。此时调用isInterrupted返回false
三、线程间通信(重点)
1、volatile和synchronized关键字
volatile:告知程序任何对该变量的访问均需要从共享内存中获取,而对他的改变必须同步刷新回主内存,
他能保证所有线程对变量访问的可见性。
synchronized:确保多个线程在同一个时刻,只能有一个线程处于方法或同步块中,
保证了线程对变量访问的可见性和排他性。
关于synchronized:本质是对一个对象的监视器(monitor)的获取,而这个获取过程是排他的,也就是同一时刻
只能有一个线程获取到有synchronized所保护对象的监视器。任意一个对象都拥有自己的监视器。
任意线程对Object的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,
线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作
唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
2、等待/通知机制(生产者-消费者模型)
等待/通知机制,是指一个线程A调用了对象O的wait方法进入等待状态,而另一个线程B调用了对象O的notify
或notifyall方法,线程A收到通知后从对象O的wait方法返回,进而执行后续操作。
注意:
(1)使用wait、notify、notifyAll时需要先对调用对象加锁
(2)调用wait方法后,线程状态由RUNNING变为WAITING,并将当前线程防止到对象的等待队列
(3)notify或notifyAll方法调用后,等待线程依旧不会从wait返回,需要调用notify或
notifyAll的线程释放锁之后,等待线程才有机会从wait返回
(4)notify方法将等待队列中的等待线程从等待队列中移到同步队列中,被移动的线程从WAITING变为BLOCKED
(5)从wait方法返回的前提是获得了调用对象的锁
P101页的图
3、等待/通知经典范式
等待方:1.获取对象的锁
2.如果条件不满足,那么调用对象的wait方法,被通知后仍要检查条件
3.条件满足则执行对应的逻辑
伪代码:
synchronized(对象){
while(条件不满足){
对象.wait();
}
对应的处理逻辑
}
通知方:1.获得对象的锁
2.改变条件
3.通知所有等待在对象上的线程
伪代码:
synchronized(对象){
改变条件
对象.notify();
}
4、管道输入输出流
PipedOutputStream、PipedInputStream(字节数据)
PipedReader、PipedWriter(字符)
对于piped类型的流,使用时必须先调用connect方法进行绑定,否则会抛出异常
5、Thread.join()
含义:当前线程等待thread线程终止之后才从thread.join返回。
另外还有两个join(long millis)具备超时特性的方法(如果线程在给定的时间内没有终止,那么将会从该超时方法返回)。
6、ThreadLocal
四、应用实例
第五章 java中的锁(使用与实现)
一、Lock接口
提供了synchronized不具有的特性:
1.尝试非阻塞地获取锁:tryLock(),调用方法后立刻返回
2.能被中断地获取锁:lockInterruptibly():在锁的获取中可以中断当前线程
3.超时获取锁:tryLock(time,unit),超时返回
Lock接口的实现基本都是通过聚合了一个同步器的子类来完成线程访问控制的。
二、队列同步器
队列同步器AbstractQueuedSynchronizer是用来构建锁或其他同步组件的基础框架。
它使用一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。
理解两者的关系:
锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;
同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待
和唤醒等底层操作。
队列同步器的实现分析
1.同步队列
通过一个FIFO双向队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息
构造成一个Node并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试
获取同步状态。
首节点是获取同步状态成功的节点,首节点在释放同步状态时,会唤醒后继节点,而后继节点在获取同步状态成功时将自己设置为首节点。
2.独占式同步状态获取和释放
public final void acquire(int arg) {
if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
代码分析:
首先尝试获取同步状态,如果获取失败,构造独占式同步节点并将其加入到节点的尾部,
然后调用acquireQueued,使节点一死循环的方式去获取同步状态,如果获取不到就阻塞节点中的线程。
两个死循环:入队、入队后
只有前驱节点是头结点才能尝试获取同步状态,原因:
头结点是成功获取到同步状态的节点,而头结点的线程释放了同步状态后,将会唤醒其后继节点,后继节点的线程
被唤醒后需要检查自己的前驱节点是否为头节点。维护同步队列的FIFO原则
p128页的图非常重要
总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋,
移出队列(停止自旋)的条件是前驱节点是头结点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease
方法释放同步状态,然后唤醒头结点的后继节点
3.共享式同步状态获取和释放
主要区别:同一时刻是否有多个线程同时获取到同步状态
共享式访问资源时,其他共享式的访问均被允许,而独占式访问被阻塞。独占式访问资源时,同一时刻其他访问均被阻塞。
三、重入锁(ReentrantLock)
synchronized关键字隐式地支持重入
ReentrantLock不像synchronized隐式支持,在调用lock方法时,已经获取到锁的线程,能够再次调用lock方法获取锁而不被阻塞。
事实上,公平的锁机制往往没有非公平的效率高,但是公平锁的好处在于:公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到
优先满足。
1.重入的实现
两个问题:再次获取锁、最终释放
2.公平锁与非公平锁的区别:
如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO
公平锁:CAS成功,且是队列的首节点
非公平锁:CAS成功即可
重入锁的默认实现是非公平锁,原因:虽然会导致饥饿,但是非公平锁的的开销少(线程切换次数少),从而可以有更高的吞吐量。
四、读写锁(ReentrantReadWriteLock)
前文中的锁基本都是排他锁,在同一时刻只允许一个线程访问。
读写所在同一时刻可以允许多个读线程访问,但在写线程访问时,所有读线程和其他写线程均被阻塞。(保证了写操作的可见性)
读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
读写锁的实现分析
1.读写状态的设计
依赖自定义同步器,读写锁的自定义同步器需要在同步状态(一个int值)上维护多个读线程和一个写线程的状态,高16位表示读,低16位
表示写。
位运算
2.写锁的获取与释放
写锁是一个支持重入的排他锁,如果当前线程已经获取了写锁,则增加写状态。
如果当前线程在获取写锁时,读锁已经被获取或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。
3.读锁的获取与释放
在没有其他写线程访问时,读锁总会被成功地获取。如果写锁已经被其他线程获取,则进入等待状态。
读状态的线程安全由CAS保证
4.锁降级(写锁降级成为读锁)
定义:把持住写锁,再获取到读锁,随后释放写锁的过程
writeLock.lock();
readLock.lock();
writeLock.unlock();
这边不是很理解。。。。
锁降级中读锁获取的必要性:
为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程获取了写锁
并修改了数据,那么当前线程无法感知到数据的更新.如果当前线程获取读锁,则另一个线程会被阻塞,
直到当前线程使用数据并释放锁之后,另一个线程才能获取写锁进行数据更新。
五、LockSupport工具
略,感觉不是很重要
六、Condition接口
略,等待通知模式,有空回头再看