在之前写过的代码,都是只能使用“一个核心”,此时无论怎么优化代码,也只能使用到一个CPU核心,把这个核心填满了,其他核心也是闲着,所以就可以通过编写特殊的代码,把多个CPU核心利用起来,这样的代码就称为“并发编程”,多进程编程就是一种并发编程。
虽然多进程可以解决问题,但是随着对效率的要求越来越高,多进程编程的弊端就暴露出来了:创建进程和销毁进程的时间开销过大,如果需要频繁的进行这样的操作,例如服务器开发(针对每一个发送请求的客户端,都需要创建一个进程),这样的时间开销是非常庞大的,也就出现了线程的概念。
1. 多线程的概念
线程:线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程的实际运作单位
下面这些每一个能够运行的软件就是一个进程
进程在系统中是通过PCB这样的结构体来描述,通过链表的形式来组织的,线程也同样是通过PCB来描述的,一个进程就是一组PCB,也就是一个进程包含了多个线程,每一个线程都可以独立的到CPU上执行
对于一个可执行程序,运行时操作系统就会创建进程,给这个程序分配各种系统资源(CPU,内存,硬盘,网络带宽...),同时也会在这个进程中创建多个线程,这些线程再到CPU上调度执行,同一个进程中的这些线程,是共用一份系统资源的
线程相比于进程更加轻量,省去了创建线程资源分配的过程和销毁线程释放资源的过程
并发:在同一时刻,有多个指令在单个CPU上交替执行
并行:在同一时刻,有多个指令在多个CPU上同时执行
也就是,有多个核心,每个核心都可以执行一个线程,这些核心之间的执行过程是同时执行的,也就是并行,此外,一个核心也可以按照“分时复用”,来切换多个线程,虽然说多个线程是一个接一个执行的,由于调度速度足够快,就好像同时进行一样,这也叫做并发
进程和线程的概念和区别:
- 进程包括线程,一个进程里面可以有单个或多个线程,但不能没有
- 进程是系统资源分配的最小单位,线程是系统调度执行的最小单位
- 同一个进程里的线程之间共用一份系统资源
- 线程是当下实现并发编程的主流方式,通过多线程,就可以充分利用好多核CPU,但是线程也不是越多越好,线程数目达到一定程度,把多个核心充分利用之后,此时再增加线程,就无法提升效率了,还可能影响效率(线程调度也是有开销的)
- 多个线程之间可能会相互影响,如果出现线程安全问题,一个线程抛出异常,也可能会把其他线程一起带走
- 多个进程之间一般不会相互影响,一个进程崩溃了,不会影响到其他进程(进程的隔离性)
2. 多线程的实现方式
2.1. 继承Thread类的方式进行实现
实现方式:
1. 定义一个类,继承Thread
2. 重写run方法
3. 创建子类对象,并启动线程
public class MyThread extends Thread{ public void run() { for (int i = 0; i < 10; i++) { System.out.println(getName() + "hello"); } } } public class ThreadDemo1 { public static void main(String[] args) { MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); //设置线程对象名称 t1.setName("线程一:"); t2.setName("线程二:"); //开启线程 t1.start(); t2.start(); } }
在开启线程之后,会交替执行线程一和线程二
在上面的方法中,run方法没有手动的进行调用,最终也执行了,像这样的没有手动调用,最终这个方法被系统,库或者框架进行调用了,这种方法就称为“回调函数”
当调用start()
方法时,会启动一个新的线程来执行run()
方法中的代码。如果不调用start()
方法,仅仅创建了线程对象,并不会创建新的线程执行任务。
也可以通过内部类的方式实现:
public class ThreadDemo1 { public static void main(String[] args) { Thread thread = new Thread() { public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Hello"); } }; thread.start(); System.out.println("main"); } }
使用匿名内部类的形式,一般就是一次性的类,比较方便,内聚性也比较好
2.2. 实现Runnable接口的方式进行实现
实现方式:
- 自定义一个类,实现Runnable接口
- 重写里面的Run方法
- 创建自定义类的对象
- 创建一个Thread类的对象,并开启线程
public class MyRun implements Runnable{ public void run() { for(int i = 0;i < 10;i++){ //获取当前线程对象 Thread t = Thread.currentThread(); System.out.println(t.getName() + "hello"); } } }
public class ThreadDemo2 { public static void main(String[] args) { //创建MyRun对象,表示多线程要执行的任务 MyRun myRun = new MyRun(); //创建线程的对象 Thread t1 = new Thread(myRun); Thread t2 = new Thread(myRun); //给线程设置名字 t1.setName("线程一:"); t2.setName("线程二:"); //开启线程 t1.start(); t2.start(); } }
开启线程之后也是交替执行线程一和线程二
还可以通过匿名内部类的方式来实现:
public class ThreadDemo2 { public static void main(String[] args) { Thread thread = new Thread(new Runnable() { int n = 10; public void run() { while (n-- != 0) { System.out.println("hello"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }); thread.start(); while (true) { System.out.println("main"); Thread.sleep(1000); } } }
既然可以使用内部类的形式了,那么也可以用lambda表达式来进行简化:
public class ThreadDemo2 { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(()->{ while (true){ System.out.println("thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); while (true){ System.out.println("main"); Thread.sleep(1000); } } }
2.3. 利用Callable接口和Future接口方式实现
实现方式:
1. 创建一个类MyCallable实现Callable接口
2. 重写call方法(返回值代表多线程运行的结果)
3. 创建MyCallable对象(表示多线程要执行的任务)
4. 创建FutureTask对象(作用管理多线程运行的结果)
5. 创建Thread类的对象并启动(表示线程)
import java.util.concurrent.Callable; public class MyCallable implements Callable<Integer> { public Integer call() throws Exception { int sum = 0; for (int i = 0; i < 100; i++) { sum += i; } return sum; } }
public class ThreadDemo3 { public static void main(String[] args) throws ExecutionException, InterruptedException { //创建MyCallable对象(表示多线程要执行的任务) MyCallable myCallable = new MyCallable(); //创建FutureTask对象(作用管理多线程运行的结果) FutureTask<Integer> ft = new FutureTask<>(myCallable); //创建Thread类的对象并启动(表示线程) Thread t1 = new Thread(ft); t1.start(); Integer res = ft.get(); System.out.println(res); } }
2.4. 三种实现方式对比
优点 |
缺点 |
|
继承Thread类 |
编程简单,可以直接使用Thread中的方法 |
可拓展性差,不能再继承其他类 |
实现Runnable接口 |
拓展性强,实现该接口同时还可以继承其他类 |
编程相对复杂,不能直接使用Thread中的方法 |
实现Callable接口 |
3. 常见的成员方法
3.1. getName()和setName()
对于setName()来说,如果没有给线程设置名称,也是有默认的名字的,格式:Thread - X(x序号从0开始)
根据Thread类的空参构造可以看出,在创建对象时的默认名称格式
那怎么在自定义类中创建对象时就传入对象名称进行构造呢?
在多线程第一种实现方式中,自定义的类继承了Thread类,但是构造方法并没有继承,所以还需要在自定义类中手动的去实现构造方法
3.2. currentThread()和sleep()
currentThread可以获取当前线程的对象
当JVM虚拟机启动之后,会自动开启多条线程,其中一条线程就叫做main线程,作用就是调用main方法,并执行里面的代码
sleep()是让线程休眠指定的时间,单位是毫秒,哪条线程执行到这个方法,那么哪条线程就会休眠,时间到了之后会继续执行下面的操作
这里的异常可以直接抛出
再来看MyThread类,这里的异常处理不能使用Throw抛出了,因为父类Thread没有抛出,这里只能使用try-catch处理异常
3.3. getPriority()和setPriority()
3.3.1. CUP的调度方式
CUP的调度方式是有两种的:分为抢占性调度和非抢占性调度
抢占式调度是一种允许高优先级线程中断低优先级线程的执行,从而立即获得CPU资源的调度方式。在这种模式下,操作系统会定期检查线程的优先级,并根据需要切换线程的执行, 对线程的访问是随机的
非抢占式调度是一种允许线程独占CPU直到其主动放弃或执行完毕的调度方式。在这种模式下,线程的执行时间由线程本身控制,调度器不会中断正在执行的线程,轮流执行线程
3.3.2. 优先级
来看Thread类中优先级的设置,最小为1,最大为10,默认是5,优先级越高抢占到CUP的概率越高,只是说概率高,并不是优先级高的就肯定比优先级低的要先抢占到CUP
public class ThreadTest2 { public static void main(String[] args) { //创建线程要执行参数的对象 MyRunable myRunable = new MyRunable(); //创建线程对象 Thread thread1 = new Thread(myRunable,"线程一"); Thread thread2 = new Thread(myRunable,"线程二"); //获取线程优先级 System.out.println(thread1.getPriority()); thread2.setPriority(10); System.out.println(thread2.getPriority()); System.out.println(Thread.currentThread().getPriority()); } }
3.4. setDaemon()
设置为守护(后台)线程也类似于备胎线程,当前台线程执行完毕之后,守护线程会陆续结束(并不是立即结束)
public class ThreadTest3 { public static void main(String[] args) { MyThread1 thread1 = new MyThread1(); MyThread2 thread2 = new MyThread2(); thread1.setName("女神"); thread2.setName("备胎"); //把第二个线程设置为守护线程 thread2.setDaemon(true); thread1.start(); thread2.start(); } }
应用场景:例如在聊天软件中,当打开聊天窗口之后,开启聊天窗口的线程和发送文件的线程,这时就可以把发送文件设置为守护线程,当聊天窗口关闭之后,守护线程也就没有存在的必要了
3.5. isAlive()
代码中,创建的new Thread 对象,生命周期和内核中实际的线程是不一样的,可能就会出现这种情况:Thread对象仍然存在,但是内核中的线程不存在了的情况(但不会出现相反的情况),原因就是调用start()之前,还没有创建线程,或者是run执行完了,内核的线程就没有了,但是Thread对象还存在
isAive()是判断线程是否存活的,返回一个boolean值
public class ThreadDemo3 { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(()->{ for (int i = 0; i < 3; i++) { System.out.println("thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); System.out.println(thread.isAlive()); thread.start(); System.out.println(thread.isAlive()); Thread.sleep(2000); System.out.println(thread.isAlive()); } }
由于线程之前调度顺序是不确定的,休眠结束后谁先执行不一定(也并不是都是一半的概率,这种概率会随着系统的不同和代码运行环境的不同,都可能存在差异)