多线程基础
为什么要使⽤线程?
在程序中完成某⼀个功能的时候,我们会将他描述成任务,这个任务需要在线程中完成.
串⾏与并发
如果在程序中,有多个任务需要被处理,此时的处理⽅式可以有串⾏和并发:
串⾏(同步):所有的任务,按照⼀定的顺序,依次执⾏。如果前⾯的任务没有执⾏结束,后⾯的任务等待。
并发(异步):将多个任务同时执⾏,在⼀个时间段内,同时处理多个任务。
并发的原理
其实所谓的并发, 并不是真正意义上的多个任务同时执⾏。 ⽽是CPU快速的在不同的任务之间进⾏切换。 在某⼀个时间点处理任务A, 下⼀个时间点去处理任务B, 每⼀个任务都没有⽴即处理结束。 CPU快速的在不同的任务之间进⾏切换, 只是这个切换的速度⾮常快, ⼈类是识别不了的, 因此会给⼈⼀种“多个任务在同时执⾏”的假象。
因此, 所谓的并发, 其实就是CPU快速的在不同的任务之间进⾏切换的⼀种假象。
进程和线程
程序,一个可执行的文件
进程, 是对⼀个程序在运⾏过程中, 占⽤的各种资源的描述。
线程, 是进程中的⼀个最⼩的执⾏单元。 其实, 在操作系统中, 最⼩的任务执⾏单元并不是线程, ⽽是句柄。 只不过句柄过⼩, 操作起来⾮常的麻烦, 因此线程就是我们可控的最⼩的任务执⾏单元。
进程和线程的异同
相同点: 进程和线程都是为了处理多个任务并发⽽存在的。
不同点: 进程之间是资源不共享的, ⼀个进程中不能访问另外⼀个进程中的数据。 ⽽线程之间是资源共享的, 多个线程可以共享同⼀个数据。 也正因为线程之间是资源共享的, 所以会出现临界资源的问题。
进程和线程的关系
⼀个进程, 在开辟的时候, 会⾃动的创建⼀个线程, 来处理这个进程中的任务。 这个线程被称为是主线程。 在程序运⾏的过程中, 还可以开辟其他线程, 这些被开辟出来的其他线程, 都是⼦线程。
也就是说, ⼀个进程中, 是可以包含多个线程。 ⼀个进程中的某⼀个线程崩溃了,只要还有其他线程存在, 就不会影响整个进程的执⾏。 但是如果⼀个进程中, 所有的线程都执⾏结束了, 那么这个进程也就终⽌了。
总结
程序:⼀个可执⾏的⽂件
进程:⼀个正在运⾏的程序.也可以理解成在内存中开辟了⼀块⼉空间
线程:负责程序的运⾏,可以看做⼀条执⾏的通道或执⾏单元,所以我们通常将进程的⼯作理解成线程的⼯作进程中可不可以没有线程? 必须有线程,⾄少有⼀个.
当有⼀个线程的时候我们称为单线程(唯⼀的线程就是主线程).
当有⼀个以上的线程同时存在的时候我们称为多线程.
线程的⽣命周期
线程的状态
线程的⽣命周期, 指的是⼀个线程对象, 从最开始的创建, 到最后的销毁, 中间所经历的过程。 在这个过程中, 线程对象处于不同的状态。
New: 新⽣态, ⼀个线程对象刚被实例化完成的时候, 就处于这个状态。
Runnable: 就绪态, 处于这个状态的线程, 可以参与CPU时间⽚的争抢。
Run: 运⾏态, 某⼀个线程抢到了CPU时间⽚, 可以执⾏这个线程中的逻辑
Block: 阻塞态, 线程由于种种原因, 暂时挂起, 处于阻塞(暂停)状态。 这个状态的线程, 不参与CPU时间⽚的争抢。
Dead: 死亡态, 线程即将被销毁。
线程的⽣命周期图
理解多线程
对线程并发执⾏的说明
简单理解(cpu单核):从宏观上看,线程有并发执⾏,从微观上看,并没有,在线程完成任务时,实际⼯作的是cpu,我们将cpu⼯作描述为时间⽚(单次获取cpu的时间,⼀般在⼏⼗毫秒).cpu只有⼀个,本质上同⼀时间只能做⼀件事,因为cpu单次时间⽚很短,短到⾁眼⽆法区分,所以当cpu在多个线程之间快速切换时,宏观上给我们的感觉是多件事同时在执⾏.
注意:
1.cpu是随机的,线程之间本质上默认是抢cpu的状态,谁抢到了谁就获得了时间⽚,就⼯作,所以多个线程的⼯作也是默认随机的.
2.在使⽤多线程时,并不是线程数越多越好,本质上⼤家共同使⽤⼀个cpu,完成任务的时间并没有减少.要根据实际情况创建线程,多线程是为了实现同⼀时间完成多件事情的⽬的.⽐如我们⽤⼿机打开⼀个app时,需要滑动界⾯浏览,同时界⾯的图⽚需要下载,对于这两个功能最好同时进⾏,这时可以使⽤多线程
多线程的实例演示
代码演示的是主线程和垃圾回收线程在同时⼯作时的状态
什么叫任务区?
我们将线程⼯作的地⽅称为任务区.
每⼀个线程都有⼀个任务区,任务区通过对应的⽅法产⽣作⽤.
JVM默认是多线程吗?
⾄少要有两个线程:
主线程:任务区: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⽅法是⼀个空⽅法.
public class Demo5 {
public static void main(String[] args) {//为了⽅便研究,先暂时不考虑垃圾回收线程.
//创建⾃⼰的线程对象--还没有线程的功能
Thread t1 = new Thread();
Thread t2 = new Thread();
//当执⾏start⽅法后,他才有了线程的功能--开启线程
t1.start();
t2.start();//有三个线程(主线程+两个⼦线程)
多线程的内存展示
继承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类对象, 在进⾏实例化的时候, 可以同时设置线程的名字。
public class Program {
public static void main(String[] args) {
// 使⽤接⼝的⽅式进⾏线程的实例化
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在继承中的使⽤
}
同步代码块⼉的特点:1.可以保证线程的安全 2.由于每次都要进⾏判断处理,所以降低了执⾏效率
#### ⽐较同步代码块⼉和同步函数
同步代码块⼉使⽤更加的灵活,只给需要同步的部分代码同步即可,⽽同步函数是给这个函数内的所有代码同步.
由于处于同步的代码越少越好,所以最好使⽤同步代码块⼉
什么时候使⽤同步代码块⼉或者同步⽅法
1.多个线程共享⼀个数据
2.⾄少有两个线程
#### synchronized在继承中的使⽤
synchronized关键字是不能继承的,也就是说,基类的⽅法synchronized f(){}在继承类中并不⾃动是synchronized f(){},⽽是变成了f(){}。继承类需要你显式的指定它的某个⽅法为synchronized⽅法;