一、多线程的发展史
1.进程概念
最初,计算机只能接收特定指令,用户输入一个指令,计算机执行一个操作,当用户在思考活输入时,计算机在等待。效率低下。
后来,将一系列需要操作的指令写下来,形成一个清单,一次性交给计算机。这样一系列指令和数据的集合叫做一个程序。用户将多个程序写在磁带上,一次性交给计算机读取并逐个执行,将结果输出到另一个磁带上。这就是批处理操作系统。一定程度上提高了计算机的效率,但是由于批处理操作系统的运行方式仍是串行的,由于内存中只有一个程序,所以只有这个程序能被CPU运行,后面的程序需要等待前一个执行完成后才开始执行,而前面的程序运行时还可能因为I/O操作、⽹络等原因阻塞,效率仍不高。
为了解决内存中只有一个程序的问题,提出了进程的概念。进程是应用程序在内存中分配的空间,也就是正在运行的程序,各进程互不干扰。CPU采取时间片轮转的方式运行进程:CPU为每个进程分配一个时间段,称为时间片,即该进程允许运行的时间,由于时间片的长度是毫秒,所以 从表面上看,各进程是同时运行的。如果时间片结束时进程还在运行,当前进程会被暂停,CPU被分配给另一个进程,这就是上下文切换。如果进程在时间⽚结束前阻塞或结束,则CPU⽴即进⾏切换,不⽤等待时间⽚⽤完。
单核CPU,在任何时刻都只能处理1个任务,但是我们在使用计算机时,可以同时听歌,上网,打游戏,是怎么做到的呢?
例如有A、B两个任务,CPU为每个任务各分配了一个时间片,A分配100ms,B分配200ms。CPU花费了100ms处理A,如果A没有处理完,就会保存A当前的状态,去处理B,200ms后再切换回来处理A或别的任务。由于CPU的处理速度非常快,一个时间片在ms内就可以处理完,1s时间可以处理很多任务,让人感觉不到切换的过程。在宏观上看起来同一时间在执行多个任务,但事实上,任何时刻只有1个任务在占用CPU资源。
CPU处理任务有优先级,例如键盘和鼠标的优先级最高,CPU需要先中断其他任务,优先处理,所以鼠标键盘动一次就是一次中断,中断一般由硬件发出。中断会导致上下文切换,但上下文切换不是都由中断引起的。
2.线程概念
进程的出现提高了操作系统的性能,但是一个进程在一段时间内只能做一件事,如果一个进程有多个子任务,只能逐个执行。例如杀毒软件是一个进程,不能同时使用扫描病毒和清理垃圾的功能。于是提出了线程。一个线程执行一个子任务,一个进程包含多个线程。线程的执行也遵循时间片轮转。
进程让操作系统的并发性成为了可能,线程让进程的内部并发成为了可能。
使用线程的好处:
1)进程通信较复杂,线程通信较简单
2)进程是重量级的,线程是轻量级的,多线程方式系统开销更小
进程和线程的区别在于是否单独占有内存地址空间及其他系统资源(如I/O)。
1)进程单独占有内存地址空间
- 进程间存在内存隔离,数据是分开的,数据共享复杂,但同步简单,各个进程之间互不⼲扰;⽽线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
- ⼀个进程出现问题不会影响其他进程,不 影响主程序的稳定性,可靠性⾼;⼀个线程崩溃可能影响整个程序的稳定性, 可靠性较低。
- 进程的创建和销毁不仅需要保存寄存器和 栈信息,还需要资源的分配回收以及⻚调度,开销较⼤;线程只需要保存寄存 器和栈信息,开销较⼩。
进程是操作系统进⾏资源分配的基本单位,⽽线程是操作系统进⾏调度的基本单位,即CPU分配时间的单位 。
3.上下文切换
上下文切换是指CPU从一个进程/线程切换到另一个进程/线程,上下文是指某一时间点CPU寄存器和程序计数器的内容,任务从保存到再加载的过程是一次上下文切换。
寄存器:CPU内部的少量的速度很快的闪存,通常存储和访问计算过程的中间值,提⾼计算机程序的运⾏速度。
程序计数器:专用的寄存器,⽤于表明指令序列中CPU正在执⾏的位置。存的值为正在执⾏的指令的位置或者下⼀个将要被执⾏的指令的位置, 具体实现依赖于特定的系统。
线程A切换到线程B:先挂起A,将A在CPU中的状态保存在内存中;在内存中检索B的上下文,并将其在CPU的寄存器中恢复,执行B线程;B的一个时间片时间执行完后,根据程序计数器中指向的位置恢复线程A。
上下⽂切换通常是计算密集型的,意味着此操作会消耗⼤量的CPU时间,故线程也不是越多越好。如何减少系统中上下⽂切换次数,是提升多线程性能的⼀个重点课题。
二、什么是线程
1.概念
1)程序:为完成特定任务,用某种语言编写的一组指令的集合(程序是静态的)
2)进程:是程序的一次执行过程。进程作为分配资源的单位,在内存中为每个正在运行的进程分配不同的内存区域(进程是动态的),进程有生命周期。
3)线程:
一个进程同时执行多个线程,就是多线程。
2.单核CPU与多核CPU
对于在单核CPU上执行的多个线程,CPU按照时间片执行,一个时间片只能执行一个线程,但时间片时间特别短,造成了同时执行多个线程的假象。
多核CPU才真正实现了同一时间多个线程同时执行(每个CPU同一时间执行一个线程)
3.并行和并发
1)并行:多个CPU同时执行多个任务
2)并发:一个CPU同时执行多个任务(时间片切换)
那么之前的程序是单线程吗?
不是,每一个程序都有:main方法的线程(主线程)、处理异常的线程(会影响主线程的执行)、垃圾收集器线程
三、创建线程的三种方式
线程对象必须在主线程代码执行之前创建
1.继承Thread类
1)必须继承Thread类
2)必须重写Thread类中的run()方法
创建TestThread类
/*创建一个线程类,这个类需要
1.继承Thread类
2.重写Thread类中的run()方法
*/
public class TestThread extends Thread{
//输入run,选择public void run(){...}
//需要和主线程争抢资源的代码写在run()中
@Override
public void run() {
//super.run();
//这里编写一个输出1-10的代码
for (int i = 1; i <= 10; i++) {
System.out.println(i);
}
}
}
在main方法中创建子线程对象与主线程的的代码抢夺资源
public class Test {
public static void main(String[] args) {
//1.创建子线程的对象,来和主线程争抢资源
//抢夺资源的子线程必须先创建对象和启动
//否则在执行主线程代码时,子线程的对象还没有创建,也就不能和主线程抢夺资源了
TestThread tt = new TestThread();
//tt.run(); //不能直接调用子线程的run()方法,会被当做一个普通方法
//2.必须启动线程
tt.start(); //start()为父类Thread类的方法
//3.主线程输出11-20
for (int i = 1; i <=10 ; i++) {
System.out.println("main"+i);
}
}
}
运行可以看到主线程运行到一半CPU被子线程抢走
抢票习题:10张票,100个人在3个窗口抢这10张票
public class BuyTicketThread extends Thread{
//3.用构造器设置子线程名,也就是窗口的名字
public BuyTicketThread(String name) {
super(name);
}
/*
1.10张票,这里票数必须使用static修饰
被static修饰后,ticketNum会在类加载时加载到堆中的的静态方法区,先于对象存在
那么后面每个子线程调用run方法时会直接使用ticketNum,且多个子线程共享这一个变量*/
static int ticketNum=10;
@Override
public void run() {
//2.100个人
for (int i = 1; i <= 100 ; i++) {
if (ticketNum>0){
System.out.println("我在"+this.getName()+"买到了第"+ ticketNum +"张票");
ticketNum--;
}
}
}
}
public class BuyTicketTest {
public static void main(String[] args) {
//3个窗口,就是3个线程,于是创建3个线程对象并开启线程
BuyTicketThread bt1 = new BuyTicketThread("窗口1");//传入窗口名
bt1.start();
BuyTicketThread bt2 = new BuyTicketThread("窗口2");
bt2.start();
BuyTicketThread bt3 = new BuyTicketThread("窗口3");
bt3.start();
}
}
运行后发现会出现一张票被两个人重复抢到,这里在下一个笔记里解决
2.实现Runnable接口
1)必须实现Runnable接口
2)必须重写Runnable接口中的run()方法
创建TestThread类
/*创建一个线程类,这个类需要
1.实现Runnable接口
2.重写Runnable接口中的run()方法
*/
public class TestThread implements Runnable{
@Override
public void run() {
//输出1-10
for (int i = 1; i <= 10 ; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
在main方法中创建子线程对象与主线程的的代码抢夺资源
public class Test {
public static void main(String[] args) {
TestThread tt = new TestThread();
//1.对于实现Runnable的线程类,是没有start()的方法
/*public Thread(Runnable target) {
点进Thread可以看到类中的构造器要求传一个Runnable接口的实现类,而tt就是我们实现了Runnable接口的类的具体对象
*/
//Thread t = new Thread(tt);
//也可以直接调用两个参数的构造器传入子线程名字
Thread t = new Thread(tt,"子线程");
//2.然后再调用Thread类中的start()的方法来启动线程
t.start();
Thread.currentThread().setName("主线程");
//主线程打印11-20
for (int i = 11; i <= 20; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
运行可以看到子线程和主线程抢夺资源
对Runnable接口的实现类的创建对象和启动线程源码分析
TestThread tt = new TestThread(); Thread t = new Thread(tt); t.start();
这里的tt没有start()方法,所以必须先创建一个Thread对象
可以看到Thread类中的一个参数的构造器,要求传入一个Runnable接口的实现类,而tt就是我们实现了Runnable接口的类的具体对象。
然后可以调用Thread类中的start()方法,这里调用start()方法也就是调用Thread类的run()方法
点击run()方法中的target可以看到target 是一个Runnable的接口
在调用构造器的时候赋值
与上面同样的抢票习题:10张票,100个人在3个窗口抢这10张票
public class BuyTicketThread implements Runnable{
//这里不用和继承Thread类一样使用static修饰,因为只需要创建一个BuyTicket对象,三个线程共用一个对象,也就共用一个ticketNum
int ticketNum =10;
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (ticketNum >0){
System.out.println("我在"+Thread.currentThread().getName()+"买到了第"+ ticketNum-- +"张火车票");
}
}
}
}
public class BuyTicketTest {
public static void main(String[] args) {
BuyTicketThread bt = new BuyTicketThread();
//只需要创建一个对象,开启3个线程
Thread t1 = new Thread(bt,"窗口1");
t1.start();
Thread t2 = new Thread(bt,"窗口2");
t2.start();
Thread t3 = new Thread(bt,"窗口3");
t3.start();
}
}
在实际开发中,多线程使用实现Runnable接口方式更多。
1)继承只能单继承,不能继承其他的类,而实现接口可以是多实现
2)Runnable接口方式共享资源能力强,共享的属性不需要使用static修饰。
Thread类与Runnable接口的联系是:Thread类也实现了Runnable接口
3.实现Callable接口
无论是继承Thread类还是实现Runnable接口方式,都有局限性:run()方法没有返回值、run()方法不能抛出异常,于是有了实现Callable接口方式。
1)实现Callable接口,Callable<V>是泛型接口,默认泛型为Object,可以在call()方法中指定返回值也就是线程类的泛型
2)重写Callable接口中的call()方法
创建线程类,输出10内的随机数
/*创建一个线程类,这个类需要
1.实现Callable接口,Callable<V>是泛型接口,默认泛型为Object,可以在call()方法中指定返回值也就是线程类的泛型
2.重写Callable接口中的call()方法
*/
import java.util.Random;
import java.util.concurrent.Callable;
public class TestThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
//return null;
//输出10以内的随机数
return new Random().nextInt(10);
}
}
创建-启动线程较麻烦
需要借助FutureTask类将Callable接口的实现类,也就是线程类的对象转换为Runnable接口的实现类,才能传入Thread类,启动线程
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1.创建TestThread的的对象
TestThread tt = new TestThread();
//Thread t = new Thread(tt);Thread类中没有Callable接口的实现类作为参数的构造器,所以不能这样写
//2.于是借助FutureTask这个类
//FutureTask类有Callable接口的实现类作为参数的构造器public FutureTask(Callable<V> callable)
FutureTask ft = new FutureTask(tt);
//3.由于FutureTask实现了Runnable接口,是Runnable接口的实现类,所以ft可以传入Thread
Thread t = new Thread(ft);
//4.启动线程
t.start();
//5.获取call()方法的返回值
Object o = ft.get(); //将异常抛出
System.out.println(o);
}
}
4.读取、设置线程名
1)getName()、setName()
创建子线程对象后使用setName()设置线程名字
TestThread tt = new TestThread();
tt.setName("子线程");
tt.start();
在子线程中使用this.getName()来获取创建对象后设置的线程名字
public class TestThread extends Thread{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(this.getName()+i);
}
}
}
也可以给主线程设置名字
Thread.currentThread():获取当前正在在执行的线程
//设置名称
Thread.currentThread().setName("主线程");
//获取名称
for (int i = 1; i <=10 ; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
2)使用构造器设置线程名字
public class TestThread extends Thread{
//创建有参构造器,参数为名字,在创建对象的时候可以给当前对象设置名字
public TestThread(String name) {
super(name);
}
}
在main方法中创建对象时传入线程名字
TestThread tt = new TestThread("构造器子线程");
四、线程的生命周期
五、线程的常用方法
1.设置线程优先级
对于同级别的线程,运行策略为先到先服务,使用时间片
但是我们可以给线程设置优先级:1-10(低-高),默认级别为5
创建两个线程,同样输出1-10
class TestThread01 extends Thread{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(this.getName()+i);
}
}
public TestThread01(String name) {
super(name);
}
}
class TestThread02 extends Thread{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(this.getName()+i);
}
}
public TestThread02(String name) {
super(name);
}
}
创建线程对象,将线程1设置为低优先级,线程2设置为高优先级,可以看到虽然线程1的对象先创建,但是线程2先运行(Java中的优先级来说不是特别的可靠,Java程序中对线程所设置的优先级只是给操作系统⼀个建议,操作系统不⼀定会采纳。真正的调⽤顺序是由操作系统的线程调度算法决定的。)
public class Test {
public static void main(String[] args) {
TestThread01 tt1 = new TestThread01("TestThread01--");
tt1.setPriority(1); //设置该线程的优先级为1
tt1.start();
TestThread02 tt2 = new TestThread02("TestThread02--");
tt2.setPriority(10); //设置该线程的优先级为10
tt2.start();
}
}
2.join()方法
调用join方法的线程会先于其它线程执行,只有当该线程执行结束后才会去执行其它线程。
必须先启动(start())线程,才能调用join()方法
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread.currentThread().setName("主线程");
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName()+i);
if (i==6){
//当i=6的时候,创建子线程对象
TestThread tt = new TestThread("子线程");
tt.start();
//如果子线程没有调用join()方法,则子线程与主线程争抢资源
//但是如果调用了join(),那么当i=6时子线程执行完成后才会继续执行主线程
tt.join();
}
}
}
}
//创建一个子线程,输出1-10
class TestThread extends Thread{
@Override
public void run() {
for (int i = 1; i <=10; i++) {
System.out.println(this.getName()+i);
}
}
public TestThread(String name) {
super(name);
}
}
运行可以看到当主线程的i=6时,子线程先运行,运行完成后主线程才继续运行
3.sleep()方法
编写秒表功能
//编写秒表功能
public class Test {
public static void main(String[] args) throws InterruptedException {
//1.定义时间格式
SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss");
//使用循环,保证时间一直在输出
while (true){
//2.获取当前时间
Date d = new Date();
//3.按照设置的时间格式格式化当前时间
String time = df.format(d);
System.out.println(time);
//4.到这里会一直输出时间,下面设置为秒表,也就是输出一次时间,程序睡眠1秒
Thread.sleep(1000); //1000ms=1s
}
}
}
4.setDaemon()
伴随线程,可以将子线程设置为主线程的伴随线程,当主线程停止后,子线程也不再执行
当setDaemon()括号中为true时,表示设置为伴随线程
setDaemon()要启动线程之前设置
public class Test {
public static void main(String[] args) {
TestThread tt = new TestThread("子线程");
tt.setDaemon(true); //设置伴随线程状态为true
tt.start();
//主线程输出1-10
Thread.currentThread().setName("主线程");
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
//子线程输出1-100
class TestThread extends Thread{
@Override
public void run() {
for (int i = 1; i <=100 ; i++) {
System.out.println(this.getName()+i);
}
}
public TestThread(String name) {
super(name);
}
}
这里主线程运行完成后子线程仍执行了一些是正常现象,在程序停止的一瞬间子线程也会多执行一些
5.stop() 过期方法,不建议使用
停止线程
public class Test {
public static void main(String[] args) {
for (int i = 1; i <= 100; i++) {
System.out.println(i);
if (i==6){
Thread.currentThread().stop();//过期方法,不建议使用
}
}
}
}
六、线程组
1.线程组ThreadGroup
每个Thread必然存在于一个ThreadGroup中
public class ThreadGroupDemo {
public static void main(String[] args) {
System.out.println("当前线程名字:"+Thread.currentThread().getName());//执⾏main()⽅法线程的名字是main
Thread testThread0 = new Thread(()->{
//如果在new Thread时没有显式指定,那么默认将父线程线程组设置为⾃⼰的线程组。
System.out.println("testThread0的线程组名:"+Thread.currentThread().getThreadGroup().getName());//main
System.out.println("testThread的线程名:"+Thread.currentThread().getName());//Thread-0
});
Thread testThread1 = new Thread(()->{
System.out.println("testThread1的线程组名:"+Thread.currentThread().getThreadGroup());//main
System.out.println("testThread1的线程名:"+Thread.currentThread().getName());//Thread-1
});
testThread0.start();
testThread1.start();
}
}