读《Java高并发编程详解》笔记,这本书笔者的收获很大,配合王文君汪老师的视频教学基本上可以将线程学的很透彻,很感谢前辈带给的这些智慧结晶。以下未我读本书和看视频对书的一些摘录和总结,以及一些补充。顺序按照书的思路走的。
本篇是最基础的部分,笔者花了很长时间去学习,看视频,敲实例,同时对照着java8的官方文档,再点进去学习源码,旨意是完全搞懂。笔者认为基础知识学习多久都是值得的。
1.线程
定义:
进程是操作系统动态执行的基本单元,每一个进程至少要有一个线程运行,线程是执行进程的基本单元。
线程生命周期:
网图-线程生命周期
①. 创建状态(New):当线程对象创建后,即进入创建状态。
new关键字仅为创建一个线程类,在没有调用start()方法前,它就是一个普通的java类。
②. 就绪状态(Runnable):调用start()方法,线程即进入就绪状态。处于就绪状态的线程,随时等待cpu调度执行。
当执行start()方法后,才会在jvm中创建线程,此时线程等待cpu调度运行。除去意外失望,该状态只能进入Running状态。
③. 运行状态(Running):当CPU调度处于就绪状态的线程后,线程才得以执行。
此时线程根据不同的指令可转换转台为Runable,Blocked和Dead状态。①.cup轮询调度和yeild()进入Runable.②.sleep()/wait()/join()方法进入Blocked.③.执行完毕进入Dead.
④. 阻塞状态(Blocked):处于运行状态的线程,由于原因,暂时放弃对cpu的使用权,停止执行,此时进入阻塞状态,直到进入就绪状态。
此时线程可转换成Dead和Runable状态。①.线程意外死亡进入Dead。②.线程被其他线程notify/notifyall方法唤醒,完成休眠,interrupt打断等进入Runnable.
⑤. 死亡状态(Dead):线程执行结束,或异常退出,结束生命周期。
线程的设计模式
3.1. Thread中的模板设计模式
thread启动调用过程
1.当Thread处于Runable状态,此时调用start()方法启动线程。
2.start()方法调用本地方法栈(JNI)中方法start)()。
3.由JNI的start0()方法调用Run()方法,执行线程逻辑。
4.线程在运行状态和死亡状态再次调用start()方法,会抛IllegalThreadStateException异常
模板设计模式应用
1.由父类抽象出一个模板,而子类通过重写该方法而重写实现具体逻辑,其中父类控制逻辑调用。
2.模板模式主要由抽象模板(Abstract Template)角色和具体模板(Concrete Template)角色组成。父类实现模板方法以final修饰,子类实现模板方法所控制的具体的操作方法
3.Thread的run和start就是一个模板设计模式的设计。
3.2. 使用Runable()接口的策略模式
Runable接口
1.由于多个线程所实现的run方法控制具体逻辑,每次创建线程的时候都需要隐式创建该方法,故此java提供一个接口Runnable来将线程的控制和业务逻辑分离开。
2.创建线程只有一种方式,即构造Thread类,而实现现成的执行单元有两种方式,第一种是重写Thread的run方法,第二种是实现Runnable接口的run方法,并将Runable接口实例用作构造Thread的参数。
3.策略模式的目的是将线程的控制本身和业务逻辑的运行分离开,达到职责分明,功能单一的原则。
4.Thread类的run方法不能共享,而实现Runnable的实例则可以构造不同的Thread实例。
ps:
2.Thread构造函数
线程名
1.在构造Thread时,可以手动为该线程命名。
2.若创建线程没有命名,默认以"Thread-"作为前缀与一个以0开始自增的数字进行组合,这个自增函数在整个jvm进程中将不断自增。
3.若同时创建2个线程,第一个线程使用指定名,第二个线程使用默认名,则第二个线程以"Thread-1"命名,即在jvm中没有一个线程,命名数字都将自增,不论是否适用该线程默认名。
4.mian()线程的线程名为"mian"。
5.在线程启动前,可以通过方法setName()修改线程名,但是启动后,名字不再被修改。
6.静态方法:Thread.currentThread().getName() 获取线程名
线程父子关系
一个线程必然是由另一个线程创建出来的,而主线程mian是由jvm创建出来的。
线程与线程组关系
1.每一个线程创建的时候,都会分配线程组,若构造线程的时候,未显示的指定线程组,则该线程回默认与其父线程同一线程组。
2.mian线程的线程组名为mian。
3.若子线程与父线程同一个线程组,其与父线程的优先级一样。
线程与jvm虚拟机栈
守护线程
1.设置守护线程:setDaemon()
2.设置守护线程在线程的启动前才会起作用。可以使用isDaemon判断该线程是否为守护线程
3.线程死亡后,设置setDaemon会抛出异常
3.线程API
sleep方法
1.使用Thread.sleep()会使线程进入Blocked状态,直至休眠完成。
2.jdk1.5以后可以使用更加灵活的枚举类TimeUnit来替代该方法。
3.sleep与wait的区别
1.sleep是Thread的方法,休眠是并不释放资源,不需要定义同步(synchronized),不需要唤醒。
2.wait是Object方法,休眠会释放资源,允许其他线程访问,需要定义同步,需要使用notify()/notifyAll()唤醒。
yield方法
1.使用yield方法可以式当前线程从Running状态进入Runable.方法不常用。
2.CPU在资源不紧张的情况下会自动忽略该方法。
3.当CPU对该线程执行了yield方法,该线程将从新位于调度队列中,和其他线程一样拥有获得CPU调度权的机会。
4.yield与sleep的区别:
1.CPU收到sleep指令后,线程一定会进入Blocked状态,此时该线程仍然占据资源。
2.CPU接受到yield命令后,根据资源使用情况可以选择不执行,若执行,则线程释放资源,,进入可执行队列,且该线程可以从新竞争CPU使用权。
线程优先级
1.线程优先级范围(1
value
10)。
2.mian线程优先级为5,线程不设置优先级,默认为5。
3.由于优先级设置也是一个提示操作(和yield类似),所以在CPU空闲时,不起作用,而对于非root权限用户,也会被忽略。
4.由于优先级的特性,所以在设置事务的先后以及重要性上,不要绑定优先级。
5.thread设置优先级:setPriority(num);线程组设计优先级:group.setMaXPriority(num)
6.若某线程组中的线程的优先级大于线程组的优先级,则该线程的优先级最大为该线程组优先级。
interrupt方法
1.调用interrupt方法会是陷入堵塞(Blocked)状态的线程被打断阻塞。
2.对于死亡的线程,该方法将被忽略。
3.每一个线程内部都有一个标志,interrupt flag,来标识当前线程是否被打断,该标识默认为flag=flase
4.当线程被打断时,会抛出InterruptedException异常,可以通过捕获异常来处理后续操作,是否继续执行该线程和结束该线程。
5.使用interrupt方法打断线程,可以使用isInterrupt()方法(对象方法),或使用interrupted方法打断线程(静态类方法)方法判断线程打断状况。
6.使用isInterrupt()方法(对象方法)判断线程是否被中断:
①:若打断正在执行(Running)的线程(未堵塞状态),该线程的中断信号会变为true,但是该方法并不起作用,线程也不会被打断Running状态。
②:若打断正在堵塞状态(Blocked)的线程,则会抛出InterruptedException异常,线程并不会死亡,是否结束和处理接下来的逻辑事务看程序的需要,此时中断信号会变成true,但在抛出异常后恢复为false,程序此时输出标识为false,避免影响接下来线程方法的调用。
7.使用interrupted方法打断线程(静态类方法)判断线程是否被中断。情况大致与上述相同,唯一不一样的是,第二种情况下的,中断信号在本次打断堵塞后一定会变为true,程序此时第一次输出标识为true,之后会恢复为false。仅此区别而已。
join方法
1.只有该线程启动后,才能调用join方法。
2.join方法前启动的线程,无法被join方法约束,只有在join后启动的线程,会在调用join方法的线程后执行。
3.某线程执行join方法,则该线程所在的主线程main线程会等待执行,知道主线程中所有的有join方法的线程执行完毕。
PS:join方法详解
关闭线程
1.若线程中有执行可中断方法(阻塞等)操作,可以通过捕获中断信号来决定是否退出。
2.由于线程标识可能被擦拭,或者没有可中断方法,使用volatile修饰的开关flag关闭线程
4.线程同步
synchronized关键字
①.作用:实现简单策略防止线程干扰和内存一致性错误,若对象对多个线程可见,则对该对象的读写将通过线程同步的方式进行。
②.表现:某线程获取与mutex(互斥)关联的monitor(监控)锁,确保共享变量的互斥访问,即同一时刻只有一个线程方法同步资源。从而避免数据不一致的问题。
③.synchronized可以对代码块或方法进行修饰,但是不能对class以及变量修饰。
synchronized本质
1.每一个对象都与一个monitor相关联,该monitor的锁只能被一个线程同一时间获取,其他线程进入阻塞状态。
2.对象的monitor锁都有一个计数器,初始为0,表示未被任何线程获取,若某线程获取该锁后,对计数器加一,此时该线程为该对象monitor的所有者。此时其他线程会陷入堵塞状态,知道monitor计数器变为0,参与从新竞争该锁。
3.若已经拥有该monitor的线程重入,会导致monitor计数器再次累加。
4.当monitor计数器减为0后,此时该线程释放锁的所有权,并和其他线程再次参数对锁的竞争。
This Monitor(对象锁)
1.即某个类中的对象方法或对象方法中的代码块被synchronized修饰
2.若同一个类中有多个对象方法或对象方法中的代码块被synchronized修饰,若多个线程同时访问一个该类的实例对象的不同方法,则只有一个线程能够访问,因为一个对象实例的对象方法是同一个monitor锁。
Class Monitor(类锁)
1.即某一个类中有多个静态方法或静态方法中的代码块被synichronized修饰.由于静态方法又称类方法,所以该锁称之为类锁,并不是关键字修饰在类上。
2.若同一个类中有多个静态方法或静态方法中的代码块被synichronized修饰,若多个线程同时访问该类不同静态方法,则只有一个线程能够访问,因为该类的静态方法使用同一个monitor锁。
死锁
1.死锁情况
①.交叉死锁,即A线程获取R1锁,等待获取R2锁,同时B线程获取R2锁,等待获取R1锁。
②.假死死锁,CPU占用率升高,难以检测。
②.内存不足,即A,B线程执行需要占用30内存,此时A占据10,B占据20,彼此等待释放内存。
③.一问一答式数据交互,客户端发送请求,服务端由于某种原因错过此时请求,之后客户端等待回复请求,服务端也等待请求。
④.数据库锁,文件锁,死循环引起假死的死锁
2.死锁检测
①.交叉死锁:一般交叉死锁会陷入Blocked状态,CPU资源占用不高,很容易借助工具实现。
5.线程通信
6.线程组