第十三章 并发
13.1 线程概述
程序划分成若干彼此分离且能独立运行的子任务(线程),一个进程包括多个线程。其应用场景是当希望某个子任务执行却又不想阻碍主程序的运行,则可以创建线程执行该子任务。
13.2基本线程
用户创建自定义线程的办法是继承java.lang.Thread类,子任务逻辑需要定义在run()方法,start()执行特殊的初始化操作(线程启动的钥匙)
让步:调用yield()方法提示线程调度机制,我的任务基本完成可以让给下一个线程使用CPU(即能在需要的时候中断一个线程并切换到另外一个线程)
休眠:调用sleep()方法,线程会休眠一段时间并把CPU资源让给其他线程,特别注意:
try{
sleep(100);
}catch(InterruptedException e){
Throw new RuntimeException(e);
}
通常使用interrupt()中断挂起线程,挂起时最好调用wait()而不是sleep()
优先权:调用setPriority设置线程优先级,调度机制更倾向于让优先级高的线程先执行(概率大)。JDK提供10个优先级别
后台线程:在程序运行时提供后台一种通用服务的线程,当所有非后台线程结束时,程序也终止了。调用setDaemon()方法设置后台线程,调用isDaemon()方法确定线程是否为后台线程,后台线程创建的任何线程均会被自动设置为后台线程。
加入线程:调用join()方法,若某个线程在另一个线程t上调用t.join()方法,此线程会被挂起,直到t执行结束才恢复。
编码变体:
public class RunnableThread implements Runnable{
public void run(){ //仅需要实现run逻辑
}
}
public static void main(String[] args){
new Thread(new RunnableThread(),””).start();
}
尽可能的通过继承Thread类来创建自定义的线程
13.3 共享受限资源
多线程模式解决线程冲突问题多采用序列化访问共享资源,即给定时刻只允许一个线程访问共享资源通常使用锁机制(互斥量)实现。Java中当线程执行被synchronized关键字保护的代码片段时,1)检查信号量是否存在2)获取信号量3)执行代码4)释放信号量,每个对象均含有单一的锁(监视器),调用synchronized方法时,此对象被上锁,其他synchronized方法只能等到前一个方法调用完毕并释放了锁之后才能被调用,即所有的synchronized方法共享同一个锁,能防止多个线程同时访问被编码为公用的内存。JVM负责跟踪对象被加锁的次数,只有首先获得锁的线程才能允许继续获取多个锁,因此每个访问关键共享资源的方法都必须是synchronized。
原子操作不需要进行同步控制即不能被线程调度机制中断的操作,一旦操作开始,一定可以在可能发生的“上下文切换”之前执行完毕。除long或者double以外的基本类型进行简单赋值或者返回值操作时才算是原子操作,只要给long或者double加上volatile关键字(每个线程都可能拥有一个本地栈维护一些变量的复本,告诉编译器不要进行优化,这些优化可能会移除那些使字段与线程里的本地数据复本保持完全同步的读写操作),上述操作即为原子操作
性能上考虑,尽可能多的使用同步控制块进行同步控制即synchronized(Object o){}
13.4 线程状态
1)新建(new):线程对象创建好了,但是没启动
2)就绪(Runnable):只要调度程序把CPU时间片分配给线程,线程便可以运行
3)死亡(Dead):通常是run()运行结束或者异常退出
4)阻塞(Blocked):线程进入阻塞状态,调度机制会忽略线程,不会分配任何CPU时间。
13.5 线程之间的协作
sleep与wait共同点都是将线程挂起,不同之处是前者不会释放锁,而后者会释放锁。可以通过notify()、notifyAll方法或者时间到达从wait()恢复,只能在同步控制方法或者同步控制块里面调用wait()、notify()和notifyAll()方法,上述三种方法在Object类中定义。
生产者-消费者模型可以使用PipedWriter和PipedReader实现,且in=new PipedReader(out)。
13.6 死锁
死锁发生的条件:
1)互斥条件:线程使用的资源中至少有一个不能共享
2)至少有一个线程持有一个资源且正在等待获取一个当前被别的线程占有的资源
3)资源不能被线程抢占
4)必须有循环等待
避免死锁的条件是破坏其中一个必须条件即可,一般破坏条件4