为什么要使⽤线程
在程序中完成某⼀个功能的时候,我们会将他描述成任务,这个任务需要在线程中完成.
串⾏与并发
如果在程序中,有多个任务需要被处理,此时的处理⽅式可以有串⾏和并发:
- 串⾏(同步):所有的任务,按照⼀定的顺序,依次执⾏。如果前⾯的任务没有执⾏结束,后⾯的任务等待。
- 并发(异步):将多个任务同时执⾏,在⼀个时间段内,同时处理多个任务。
并发的原理
⼀个程序如果需要被执⾏, 必须的资源是CPU和内存。 在内存上开辟空间, 为程序中的变量进⾏数据的存储; 同时需要CPU处理程序中的逻辑。 现在处于⼀个硬件过剩的时代, 但是即便是硬件不发达的时代, 并发任务也是可以实现的。 以单核的CPU为例, 处理任务的核⼼只有⼀个, 那就意味着, 如果CPU在处理⼀个程序中的任务, 其他所有的程序都得暂停。 那么并发是怎么实现的呢?
其实所谓的并发, 并不是真正意义上的多个任务同时执⾏。 ⽽是CPU快速的在不同的任务之间进⾏切换。 在某⼀个时间点处理任务A, 下⼀个时间点去处理任务B, 每⼀个任务都没有⽴即处理结束。 CPU快速的在不同的任务之间进⾏切换, 只是这个切换的速度⾮常快, ⼈类是识别不了的, 因此会给⼈⼀种“多个任务在同时执⾏”的假象。
因此, 所谓的并发, 其实就是CPU快速的在不同的任务之间进⾏切换的⼀种假象。
进程和线程
- 进程, 是对⼀个程序在运⾏过程中, 占⽤的各种资源的描述。
- 线程, 是进程中的⼀个最⼩的执⾏单元。 其实, 在操作系统中, 最⼩的任务执⾏单元并不是线程, ⽽是句柄(Handle)。 只不过句柄过⼩, 操作起来⾮常的麻烦, 因此线程就是我们可控的最⼩的任务执⾏单元。
相同点: 进程和线程都是为了处理多个任务并发⽽存在的。
不同点: 进程之间是资源不共享的, ⼀个进程中不能访问另外⼀个进程中的数据。 ⽽线程之间是资源共享的, 多个线程可以共享同⼀个数据。 也正因为线程之间是资源共享的, 所以会出现临界资源的问题。
概念总结
- 程序:⼀个可执⾏的⽂件
- 进程:⼀个正在运⾏的程序.也可以理解成在内存中开辟了⼀块⼉空间
- 线程:负责程序的运⾏,可以看做⼀条执⾏的通道或执⾏单元,所以我们通常将进程的⼯作理解成线程的⼯作
- 进程中可不可以没有线程? 必须有线程,⾄少有⼀个.
- 当有⼀个线程的时候我们称为单线程(唯⼀的线程就是主线程).
- 当有⼀个以上的线程同时存在的时候我们称为多线程.
- 多线程的作⽤:为了实现同⼀时间⼲多件事情(并发执⾏).
线程的⽣命周期
线程的状态
线程的⽣命周期, 指的是⼀个线程对象, 从最开始的创建, 到最后的销毁, 中间所
经历的过程。 在这个过程中, 线程对象处于不同的状态。
- New: 新⽣态, ⼀个线程对象刚被实例化完成的时候, 就处于这个状态。
- Runnable: 就绪态, 处于这个状态的线程, 可以参与CPU时间⽚的争抢。
- Run: 运⾏态, 某⼀个线程抢到了CPU时间⽚, 可以执⾏这个线程中的逻辑
- Block: 阻塞态, 线程由于种种原因, 暂时挂起, 处于阻塞(暂停)状态。 这个状态的线程, 不参与CPU时间⽚的争抢。
- Dead: 死亡态, 线程即将被销毁。
线程的⽣命周期图
理解多线程
对线程并发执⾏的说明
简单理解(cpu单核):从宏观上看,线程有并发执⾏,从微观上看,并没有,在线程完成任务时,实际⼯作的是cpu,我们将cpu⼯作描述为时间⽚(单次获取cpu的时间,⼀般在⼏⼗毫秒).cpu只有⼀个,本质上同⼀时间只能做⼀件事,因为cpu单次时间⽚很短,短到⾁眼⽆法区分,所以当cpu在多个线程之间快速切换时,宏观上给我们的感觉是多件事同时在执⾏.
注意:
1.cpu是随机的,线程之间本质上默认是抢cpu的状态,谁抢到了谁就获得了时间⽚,就⼯作,所以多个线程的⼯作也是默认随机的.
2.在使⽤多线程时,并不是线程数越多越好,本质上⼤家共同使⽤⼀个cpu,完成任务的时间并没有减少.要根据实际情况创建线程,多线程是为了实现同⼀时间完成多件事情的⽬的.⽐如我们⽤⼿机打开⼀个app时,需要滑动界⾯浏览,同时界⾯的图⽚需要下载,对于这两个功能最好同时进⾏,这时可以使⽤多线程.
示例代码
public static void main(String[] args) {
new Test();
/*
* ⼿动运⾏垃圾回收器
*/
System.gc();
System.out.println("main");
}
class Test{
@Override
protected void finalize() throws Throwable {
System.out.println("finalize");
}
}
- 代码演示的是主线程和垃圾回收线程在同时⼯作时的状态
- 什么叫任务区?
我们将线程⼯作的地⽅称为任务区.
每⼀个线程都有⼀个任务区,任务区通过对应的⽅法产⽣作⽤. - JVM默认是多线程吗? ⾄少要有两个线程:
- 主线程:任务区:main函数
- 垃圾回收线程:任务区:finalize函数
代码说明:
1.gc()⽅法:之前讲过,是垃圾回收器
原理:当执⾏gc时,会触发垃圾回收机制,开启垃圾回收线程,执⾏finalize⽅法
2.finalize()⽅法:垃圾回收线程的任务区
正常情况下,这个函数是由系统调⽤的,重写只是为了更好的观察多线程的发⽣
当Test对象被释放的时候,会⾃动的调⽤finalize⽅法
3.线程和任务的关系
任务区结束,线程随着任务的结束⽽结束,线程随着任务的开始⽽开始.当线程还在⼯作的时候,进程不能结束.
对于主线程来说:当main函数结束时,主任务区结束,主线程结束
对于垃圾回收线程:当finalize函数结束,垃圾回收任务结束,垃圾回收线程结束
创建线程
java将线程⾯向对象了,形成的类就是Thread,在Thread类内部执⾏任务的⽅法叫run()
线程对象的实例化
在Java中, 使⽤Thread类来描述⼀个线程。 实例化⼀个线程, 其实就是⼀个Thread对象。
使⽤Thread类创建线程对象
-
线程对象刚刚被实例化的时候, 线程处于新⽣态,还没有线程的功能。 如果需要
让这个线程执⾏他的任务, 需要调⽤ start() ⽅法, 使线程进⼊到就绪态, 争抢
CPU时间⽚。 -
为什么通过调⽤start()⽅法开启线程,⽽不是通过⼿动调⽤run()?
答:因为线程获取cpu是随机的,run是线程的任务区,代表功能.如果⼿动执⾏run,此
时线程可能没有拿到cpu,⽆法⼯作,操作失败.通过start,让线程处于就绪状态,随时
拥有抢cpu的能⼒,当抢到cpu后,再⾃动执⾏run,实现并发的任务. -
为什么要使⽤Thread类的⼦类对象?
答:我们实现的实际功能,Thread类作为系统类不能提前知晓,所以⽆法将功能代码放⼊
Thread的run⽅法⾥.如果想实现⾃⼰的功能,可以写Thread类的⼦类,重写run⽅法,这
也是为什么Thread的run⽅法是⼀个空⽅法.
内存分析
继承Thread类
- 继承⾃Thread类, 做⼀个Thread的⼦类。 在⼦类中, 重写⽗类中的run⽅法,在这个重写的⽅法中, 指定这个线程需要处理的任务。
- Thread.currentThread() : 可以⽤在任意的位置, 获取当前的线程。如果是Thread的⼦类, 可以在⼦类中, 使⽤this获取到当前的线程。
- 当我们⼿动调⽤run的时候,他失去了任务区的功能,变成了⼀个普通的⽅法. 当run作为⼀个普通⽅法时,内部对应的线程跟调⽤他的位置保持⼀致.
- 结果分析:
主线程和两个⼦线程之间是随机打印的,他们是抢cpu的关系. - 通过创建Thread⼦类的⽅式实现功能,线程与任务绑定在了⼀起,操作不⽅便
我们可以将任务从线程中分离出来,哪个线程需要⼯作,就将任务交给谁,操作⽅便,灵活-使⽤Runnable接⼝
使⽤Runnable接⼝
- 在Thread类的构造⽅法中, 有⼀个重载的构造⽅法, 参数是 Runnable 接⼝。因此, 可以通过Runnable接⼝的实现类对象进⾏Thread对象的实例化。
- 这⾥Thread内部默认有⼀个run,⼜通过runnable传⼊⼀个run,为什么优先调⽤的是传⼊的run?
如果该线程是使⽤独⽴的 Runnable 运⾏对象构造的,则调⽤该 Runnable 对象的 run ⽅法;否则,该⽅法不执⾏任何操作并返回。
优缺点对⽐
- 继承的⽅式: 优点在于可读性⽐较强, 缺点在于不够灵活。 如果要定制⼀个线程, 就必须要继承⾃Thread类, 可能会影响原有的继承体系。
- 接⼝的⽅式: 优点在于灵活, 并且不会影响⼀个类的继承体系。 缺点在于可读性较差。
⽤的⽐较多的⽅式是使⽤接⼝的⽅式。
线程名字的设置
每⼀个线程, 都有⼀个名字。 如果在实例化线程的时候不去设定名字, 那么这个线程会拥有⼀个默认的名字。
- 设置线程的名字, 使⽤⽅法 setName(String name)
Thread thread = new Thread(() -> {
System.out.println("⼦线程的逻辑");
});
// 设置线程的名字
thread.setName("⼦线程的名字");
- Thread类对象, 在进⾏实例化的时候, 可以同时设置线程的名字。
// 使⽤接⼝的⽅式进⾏线程的实例化
Thread thread = new Thread(() -> {}, "线程的名字");
}
- 如果使⽤继承Thread类的⽅式进⾏的实例化, 可以添加⼀个构造⽅法, 进⾏实例化对象的同时进⾏名称的设置。 在构造⽅法中, 使⽤ super(String) 进⾏⽗类⽅法的调⽤。
public class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println("⼦线程的逻辑");
}
}
设置线程名字, 可以使⽤上述三种⽅式, 但是获取线程线程的名字, 只有⼀个⽅法, 就是 getName()
线程的礼让
线程礼让, 就是当前已经抢到CPU资源的正在运⾏的线程, 释放⾃⼰持有的CPU资
源, 回到就绪状态, 重新参与CPU时间⽚的争抢。
Thread.yield();//强制退出运行态并进入就绪态
线程同步
临界资源问题
- 临界资源
在⼀个进程中, 多个线程之间是可以资源共享的。 如果在⼀个进程中的⼀个资源同时被多个线程访问, 这个资源就是⼀个临界资源。
如果多个线程同时访问临界资源, 会对这个资源的值造成影响。
- 临界资源问题
多个线程同时访问⼀个资源的情况下, ⼀个线程在操作这个资源的时候, 将值取出进⾏运算, 在还没来得及进⾏修改这块空间的值之前, 值⼜被其他的线程取⾛了。此时就会出现临界资源的问题, 造成这个资源的值出现不是我们预期的值。
解决
临界资源问题出现的原因就是多个线程在同时访问⼀个资源, 因此解决⽅案也很简单, 就是不让多个线程同时访问即可。
在⼀个线程操作⼀个资源的时候, 对这个资源进⾏“上锁”, 被锁住的资源, 其他的线程⽆法访问。
类似多个⼈去公共卫⽣间, 每⼀个⼈在进到卫⽣间的时候, 都会从⾥⾯进⾏反锁。 此时, 其他⼈如果也需要使⽤这个卫⽣间, 就得在⻔外等待。
线程锁
-
线程锁, 就是⽤来“锁住”⼀个临界资源, 其他的线程⽆法访问。 在程序中, 可以分为对象锁和类锁
-
对作为锁的对象的要求:
- 1.必须是对象
- 2.必须保证被多个线程共享
-
对象锁: 任何的普通对象或者this, 都可以被当做是⼀把锁来使⽤。 但是需要注意, 必须要保证不同的线程看到的锁, 需要是同⼀把锁才能⽣效。 如果不同的线程看到的锁对象是不⼀样的, 此时这把锁将没有任何意义。
-
注意: 不能直接使⽤匿名对象作为锁,因为这样每次都是在重新new,要保证锁是被⼤家共享.
-
类锁: 可以将⼀个类做成锁, 使⽤类.class (类的字节码⽂件对象)来作为锁。因为类的字节码⽂件的使⽤范围太⼤,所以⼀般我们不使⽤他作为锁,只有在静态⽅法中.
synchronized
如果在⼀个⽅法中, 所有的逻辑, 都需要放到同⼀个同步代码段中执⾏。 这样的⽅法, 可以直接做成同步⽅法。
同步⽅法
-
⾮静态同步⽅法,使⽤的对象锁(this)
是某个对象实例内,synchronized aMethod(){}可以防⽌多个线程同时访问这个对象的synchronized⽅法(如果⼀个对象有多个synchronized⽅法,只要⼀个线程访问了其中的⼀个synchronized⽅法,其它线程不能同时访问这个对象中任何⼀个synchronized⽅法)。这时,不同的对象实例的synchronized⽅法是不相⼲扰的。也就是说,其它线程照样可以同时访问相同类的另⼀个对象实例中的synchronized⽅法
-
静态同步⽅法,使⽤的类锁(当前类的.class⽂件)
是某个类的范围,synchronized static aStaticMethod{}防⽌多个线程同时访问这个类中的synchronized static ⽅法。它可以对类的所有对象实例起作⽤。静态同步函数在进内存的时候不会创建对象,但是存在其所属类的字节码⽂件对象,属于class类型的对象,所以静态同步函数的锁是其所属类的字节码⽂件对象。
同步代码块
synchronized关键字⽤于⽅法中的某个区块中,表示只对这个区块的资源实⾏互斥访问。
同步代码段, 是来解决临界资源问题最常⻅的⽅式。 将⼀段代码放⼊到同步代码段中, 将这段代码上锁。
第⼀个线程抢到了锁标记后, 可以对这个紧接资源上锁, 操作这个临界资源。 此时其他的线程再执⾏到synchronized的时候, 会进⼊到锁池, 直到持有锁的线程使⽤结束后, 对这个资源进⾏解锁。 此时, 处于锁池中的线程都可以抢这个锁标记, 哪⼀个线程抢到了, 就进⼊到就绪态, 没有抢到锁的线程, 依然处于锁池中。
- 同步代码块⼉的构成:
synchronized(锁(对象)){
//同步的代码
}
-
同步代码块⼉的特点:
1.可以保证线程的安全
2.由于每次都要进⾏判断处理,所以降低了执⾏效率(这里是与 不同步 对比)
⽐较同步代码块⼉和同步函数
-
同步代码块⼉使⽤更加的灵活,只给需要同步的部分代码同步即可,⽽同步函数是给这个函数内的所有代码同步.
-
由于处于同步的代码越少越好,所以最好使⽤同步代码块⼉
-
什么时候使⽤同步代码块⼉或者同步⽅法:
1.多个线程共享⼀个数据
2.⾄少有两个线程
synchronized在继承中的使⽤
synchronized关键字是不能继承的,也就是说,基类的⽅法synchronized f(){}在继承类中并不⾃动是synchronized f(){},⽽是变成了f(){}。继承类需要你显式的指定它的某个⽅法为synchronized⽅法;
线程通信
package communication.test;
/**
* 实例:打印机打印
* 实现功能:不断输⼊不断输出
* 总结:需要给输⼊任务和输出任务同时加⼀把锁,保证两个任务之间是同步的给两
* 个任务加⼀把锁:可以是desc或者Object.class
* */
public class Demo1 {
public static void main(String[] args) {
// 创建数据对象
Data data = new Data();
// 创建输入输出任务
Input input = new Input(data);
Output output = new Output(data);
// 创建线程
Thread in = new Thread(input);
Thread out = new Thread(output);
// 开始线程
in.start();
out.start();
}
}
//数据类
class Data{
String name;
String sex;
}
//输入任务
class Input implements Runnable{
Data data;
int i = 0;
public Input(Data data) {
this.data = data;
}
@Override
public void run() {
while (true){
synchronized (data) {
if (i == 0) {
data.name = "张三";
data.sex = "男";
} else {
data.name = "李华";
data.sex = "⼥";
}
i = (i + 1) % 2;
}
}
}
}
//输出任务
class Output implements Runnable{
Data data;
public Output(Data data) {
this.data = data;
}
@Override
public void run() {
while (true){
synchronized (data) {
System.out.println(data.name+"\t"+data.sex);
}
}
}
}
package communication.test1;
/**
* 实例:打印机打印
* 功能实现:⼀次输⼊⼀次输出
* 总结:在上⾯线程通信基本实现的基础上改进代码,通过分别给输⼊和输出线程设
* 置wait和notify状态,实现⼀次输⼊⼀次输出
* */
public class Demo {
public static void main(String[] args) {
// 创建数据对象
Data data = new Data();
// 创建输入输出任务
Input input = new Input(data);
Output output = new Output(data);
// 创建线程
Thread in = new Thread(input);
Thread out = new Thread(output);
// 开始线程
in.start();
out.start();
}
}
//数据类
class Data{
String name;
String sex;
boolean flag = false; //用于执行唤醒等待的切换
}
//输入任务
class Input implements Runnable{
Data data;
int i = 0;
public Input(Data data) {
this.data = data;
}
@Override
public void run() {
while (true){
synchronized (data) {
if (data.flag == true){
//让当前的线程等待
//wait⽅法要在同步下使⽤,因为要使⽤同步锁
try {
data.wait();
//当执⾏这⾏代码的时候,这⾥对应的是哪个线程,就操作的是哪个线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (i == 0) {
data.name = "张三";
data.sex = "男";
} else {
data.name = "李华";
data.sex = "⼥";
}
i = (i + 1) % 2;
//状态切换
data.flag = !data.flag;
//唤醒输出线程
data.notify();
}
}
}
}
//输出任务
class Output implements Runnable{
Data data;
public Output(Data data) {
this.data = data;
}
@Override
public void run() {
while (true){
synchronized (data) {
if (data.flag == false){
try {
data.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(data.name+"\t"+data.sex);
data.flag = !data.flag;
data.notify();
}
}
}
}
package communication.test2;
/**
* 实例:打印机打印
* 功能:对⼀次输⼊⼀次输出代码的改进
* 总结:进⾏了代码优化
* ⾯向对象的精髓:谁的活⼉谁⼲,不是你的活⼉不要⼲
* 将数据准备的活⼉从输⼊任务输出任务提出来,放⼊数据类
* */
public class Demo {
public static void main(String[] args) {
Data2 data = new Data2();
Input input = new Input(data);
Output output = new Output(data);
Thread in = new Thread(input);
Thread out = new Thread(output);
in.start();
out.start();
}
}
class Data2{
String name;
String sex;
boolean flag = false;
public void setData(String name,String sex){
if (flag == true){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name;
this.sex = sex;
flag = !flag;
notify();
}
public void getData(){
if (flag == false){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(name+"\t"+sex);
flag = !flag;
notify();
}
}
class Input implements Runnable{
Data2 data;
public Input(Data2 data) {
this.data = data;
}
@Override
public void run() {
int i=0;
while (true) {
synchronized (data) {
if (i == 0) {
data.setData("张三", "男");
}else {
data.setData("李华", "⼥");
}
i=(i+1)%2;
}
}
}
}
class Output implements Runnable{
Data2 data;
public Output(Data2 data) {
this.data = data;
}
@Override
public void run() {
while (true){
synchronized (data) {
data.getData();
}
}
}
}
⽣产者消费者模式
⽣产者消费者问题是研究多线程程序经典问题之⼀,它描述是有⼀块缓冲区作为仓库,⽣产者可以将产品放⼊仓库,消费者则可以从仓库中取⾛产品。在Java中⼀共有四种⽅法⽀持同步,其中前三个是同步⽅法,⼀个是管道⽅法。
(1)Object的wait() / notify()⽅法 (2)Lock和Condition的await() / signal()⽅法
(3)BlockingQueue阻塞队列⽅法 (4)PipedInputStream /PipedOutputStream
单⽣产者消费者
package communication.procon;
/**
* 单⽣产者单消费者
* 需要的线程:两个---⼀个⽣产线程⼀个消费线程
* 需要的任务:两个---⼀个⽣产任务⼀个消费任务
* 需要数据:⼀份---产品
*/
public class ProducerConsumer {
public static void main(String[] args) {
Product product = new Product();
Producter producter = new Producter(product);
Consumer consumer = new Consumer(product);
Thread pro = new Thread(producter);
Thread con = new Thread(consumer);
pro.start();
con.start();
}
}
//数据类——产品
class Product{
String name;//名字
double price;//价格
int number;//数量
//标识--控制唤醒等待
boolean flag = false;
public synchronized void setProduct(String name,double price){
while (flag){//if (flag==true){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name;
this.price = price;
number++;
System.out.println(Thread.currentThread().getName()+"生产了\t名字:"+this.name+"\t价格:"+this.price+"\t当前数量:"+this.number);
flag = !flag;
notifyAll();//notify();
}
public synchronized void getProduct(){
while (!flag){//if (flag==false){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number--;
System.out.println(Thread.currentThread().getName()+"消费了 \t名字:"+this.name+"\t价格:"+this.price);
flag = !flag;
notifyAll();//notify();
}
}
//生产者任务
class Producter implements Runnable{
Product product;
public Producter(Product product) {
this.product = product;
}
@Override
public void run() {
while (true) {
product.setProduct("辣条", 10);
}
}
}
//消费者任务
class Consumer implements Runnable{
Product product;
public Consumer(Product product) {
this.product = product;
}
@Override
public void run() {
while (true) {
product.getProduct();
}
}
}
多⽣产者多消费者
将单⽣产者单消费者代码不做更改,直接再添加⼀个⽣产线程,⼀个消费线程,就形成了多⽣产者多消费者?//error
-
出现的错误1
- 错误描述:当有两个⽣产线程,两个消费线程同时存在的时候,有可能出现⽣产⼀次,消费多次或者⽣产多次消费⼀次的情况.
- 原因:当线程被重新唤醒之后,没有判断标记,直接执⾏之后的代码
- 解决办法:将标记判断的if写入while循环 确保状态正确才执行之后代码
-
出现的错误2
- 问题描述:继续运⾏程序,会出现死锁的情况(4个线程同时处于等待状态)
- 原因:唤醒的是本⽅的线程,最后导致所有的线程都处于等待状态.
- 解决办法:将notify改成notifyAll.保证将对⽅的线程唤醒
Lock锁
- 为什么使⽤Lock锁?
在我们使⽤synchronized进⾏同步的时候,锁对象是Object类的对象,使⽤wait,notify⽅法都来⾃Object类,但是咱们知道并不是所有的对象都会⽤到同步,所以这样⽤法不太合理,⽽且锁相关的功能很多,Lock就是将锁⾯向对象的结果.不光是将锁⾯向对象了,同时将wait,notify等⽅法也做了⾯相对象处理.形成了Condition接⼝.当我们想实现多⽣产者多消费者模式时,可以使⽤Lock实现同步,同时配合Condition接⼝实现唤醒等待.
-
⽐较synchronized和Lock
1.synchronized:从jdk1.0就开始使⽤的同步⽅法-称为隐式同步
synchronized(锁对象){//获取锁 我们将锁还可以称为锁旗舰或者监听器
//同步的代码
}//释放锁
2.Lock:从jdk1.5开始使⽤的同步⽅法-称为显示同步 -
Lock本身是接⼝,要通过他的⼦类创建对象⼲活⼉
常⽤⼦类:ReentrantLock
- ⾸先调⽤lock()⽅法获取锁
- 进⾏同步的代码块⼉
- 使⽤unlock()⽅法释放锁
当进⾏多⽣产者多消费者的功能时,使⽤Lock,其他的都使⽤synchronized
- 效率:Lock⾼于synchronized
唤醒等待机制
Object类中⼏个⽅法如下:
-
wait()
- 等待,让当前的线程,释放⾃⼰持有的指定的锁标记,进⼊到等待队列。
- 等待队列中的线程,不参与CPU时间⽚的争抢,也不参与锁标记的争抢。
-
notify()
- 通知、唤醒。唤醒等待队列中,⼀个等待这个锁标记的随机的线程。
- 被唤醒的线程,进⼊到锁池,开始争抢锁标记。
-
notifyAll()
- 通知、唤醒。唤醒等待队列中,所有的等待这个锁标记的线程。
- 被唤醒的线程,进⼊到锁池,开始争抢锁标记。
wait和sleep的区别
-
sleep()⽅法,在休眠时间结束后,会⾃动的被唤醒。 ⽽wait()进⼊到的阻塞态,需要被notify/notifyAll⼿动唤醒。
-
wait()会释放⾃⼰持有的指定的锁标记,进⼊到阻塞态。sleep()进⼊到阻塞态的时候,不会释放⾃⼰持有的锁标记。
-
⽆论是wait()⽅法,还是notity()/notifyAll()⽅法,在使⽤的时候要注意,⼀定要是⾃⼰持有的锁标记,才可以做这个操作。否则会出现IllegalMonitorStateException 异常。
-
为什么wait,notify⽅法要使⽤锁调⽤?
死锁
出现的情况有两种
-
所有的线程处于等待状态
- ⼤家都处于等待状态,没有⼈获取cpu使⽤
-
锁之间进⾏嵌套调⽤
- 多个线程, 同时持有对⽅需要的锁标记, 等待对⽅释放⾃⼰需要的锁标记。此时就是出现死锁。 线程之间彼此持有对⽅需要的锁标记, ⽽不进⾏释放, 都在等待。
线程其他内容
线程的停⽌
线程的停⽌:3种
- 1.通过⼀个标识结束线程
- 2.调⽤stop⽅法—因为有固有的安全问题,所以系统不建议使⽤.
- 3.调⽤interrupt⽅法----如果⽬标线程等待很⻓时间(例如基于⼀个条件变量),则应使⽤ interrupt ⽅法来中断该等待。
线程的休眠
线程休眠, 就是让当前的线程休眠指定的时间。 休眠的线程进⼊到阻塞状态, 直到
休眠结束。 阻塞的线程, 不参与CPU时间⽚的争抢。
注: 线程休眠的时间单位是毫秒。
try {
// 线程休眠
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
线程的合并
将⼀个线程中的任务, 合并⼊到另外⼀个线程中执⾏, 此时, 合并进来的线程有限
执⾏。 类似于: 插队。
注意:优先级只⽐main线程的⾼.对其他的线程没有影响.
// 先开启
vip.start();
// 再合并
try {
vip.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
线程的优先级设置
设置线程的优先级, 可以决定这个线程能够抢到CPU时间⽚的概率。 线程的优先级范围在 [1, 10], 默认的优先级是5。 数值越⾼, 优先级越⾼。 但是要注意, 并不是优先级⾼的线程⼀定能抢到CPU时间⽚, 也不是优先级的线程⼀定抢不到CPU时间⽚。 线程的优先级只是决定了这个线程能够抢到CPU时间⽚的概率。 即便是优先级最低的线程, 依然可以抢到CPU时间⽚。
// 设置线程的优先级, 必须在这个线程启动之前
thread0.setPriority(1);
thread1.setPriority(10);
thread0.start();
thread1.start();
守护线程
守护线程, ⼜叫后台线程。 是⼀个运⾏在后台, 并且会和前台线程争抢CPU时间⽚的线程。
- 守护线程依然会和前台线程争抢CPU时间⽚, 实现并发的任务。
- 在⼀个进程中, 如果所有的前台线程都结束了, 后台线程即便任务没有执⾏结束, 也会⾃动结束。
// 将⼀个线程设置为守护线程(先设置再开启)
thread.setDaemon(true);
// 开启线程
thread.start();
线程池
线程池, 其实就是⼀个容器, ⾥⾯存储了若⼲个线程。使⽤线程池, 最主要是解决线程复⽤的问题。 之前使⽤线程的时候, 当我们需要使⽤⼀个线程时, 实例化了⼀个新的线程。 当这个线程使⽤结束后, 对这个线程进⾏销毁。 对于需求实现来说是没有问题的, 但是如果频繁的进⾏线程的开辟和销毁,其实对于CPU来说, 是⼀种负荷, 所以要尽量的优化这⼀点。可以使⽤复⽤机制解决这个问题。 当我们需要使⽤到⼀个线程的时候, 不是直接实例化, ⽽是先去线程池中查找是否有闲置的线程可以使⽤。 如果有, 直接拿来使⽤; 如果没有, 再实例化⼀个新的线程。 并且, 当这个线程使⽤结束后, 并不是⻢上销毁, ⽽是将其放⼊到线程池中, 以便下次继续使⽤。
线程池的开辟
在Java中, 使⽤ThreadPoolExecutor类来描述线程池, 在这个类的对象实例化的时
候, 有⼏个常⻅的参数:
BlockingQueue
- ArrayBlockingQueue
- LinkedBlockingQueue
- SynchronouseQueue
RejectedExecutionHandler
- ThreadPoolExecutor.AbortPolicy : 丢弃新的任务,并抛出异常
- RejectedExecutionException
- ThreadPoolExecutor.DiscardPolicy : 丢弃新的任务,但是不会抛出异常
- ThreadPoolExecutor.DiscardOldestPolicy : 丢弃等待队列中最早的任务
- ThreadPoolExecutor.CallerRunsPolicy : 不会开辟新的线程,由调⽤的线程来处理
线程池的⼯作原理
线程池中的所有线程, 可以分为两部分: 核⼼线程 和 临时线程
核⼼线程:
核⼼线程常驻于线程池中, 这些线程, 只要线程池存在, 他们不会被销毁。 只有当线程池需要被销毁的时候, 他们才会被销毁。
临时线程:
就是临时⼯。 当遇到了临时的⾼密度的线程需求时, 就会临时开辟⼀些线程, 处理⼀些任务。 这些临时的线程在处理完⾃⼰需要处理的任务后, 如果没有其他的任务要处理, 就会闲置。 当闲置的时间到达了指定的时间之后, 这个临时线程就会被销毁。
任务分配逻辑:
- 当需要处理并发任务的时候, 优先分配给核⼼线程处理。
- 当核⼼线程都已经分配了任务, ⼜有新的任务出现时,会将这个新的任务存⼊等待队列。
- 当等待队列被填满后, 再来新的任务时, 会从开辟⼀个临时线程,处理这个新的任务。
- 当临时线程加核⼼线程数量已经到达线程池的上限,再来新的任务的时候,就会触发拒绝访问策略。
线程池的常⽤⽅法
线程池的⼯具类
线程池的开辟, 除了可以使⽤构造⽅法进⾏实例化, 还可以通过Executors⼯具类进⾏获取。 实际应⽤中, ⼤部分的场景下, 可以不⽤前⾯的构造⽅法进⾏线程池的实例化, ⽽是⽤Executors⼯具类中的⽅法进⾏获取。
- RejectedExecutionException
- ThreadPoolExecutor.DiscardPolicy : 丢弃新的任务,但是不会抛出异常
- ThreadPoolExecutor.DiscardOldestPolicy : 丢弃等待队列中最早的任务
- ThreadPoolExecutor.CallerRunsPolicy : 不会开辟新的线程,由调⽤的线程来处理
线程池的⼯作原理
线程池中的所有线程, 可以分为两部分: 核⼼线程 和 临时线程
核⼼线程:
核⼼线程常驻于线程池中, 这些线程, 只要线程池存在, 他们不会被销毁。 只有当线程池需要被销毁的时候, 他们才会被销毁。
临时线程:
就是临时⼯。 当遇到了临时的⾼密度的线程需求时, 就会临时开辟⼀些线程, 处理⼀些任务。 这些临时的线程在处理完⾃⼰需要处理的任务后, 如果没有其他的任务要处理, 就会闲置。 当闲置的时间到达了指定的时间之后, 这个临时线程就会被销毁。
任务分配逻辑:
- 当需要处理并发任务的时候, 优先分配给核⼼线程处理。
- 当核⼼线程都已经分配了任务, ⼜有新的任务出现时,会将这个新的任务存⼊等待队列。
- 当等待队列被填满后, 再来新的任务时, 会从开辟⼀个临时线程,处理这个新的任务。
- 当临时线程加核⼼线程数量已经到达线程池的上限,再来新的任务的时候,就会触发拒绝访问策略。
线程池的常⽤⽅法
[外链图片转存中…(img-ZyfN7wgZ-1628306171709)]
线程池的⼯具类
线程池的开辟, 除了可以使⽤构造⽅法进⾏实例化, 还可以通过Executors⼯具类进⾏获取。 实际应⽤中, ⼤部分的场景下, 可以不⽤前⾯的构造⽅法进⾏线程池的实例化, ⽽是⽤Executors⼯具类中的⽅法进⾏获取。