为什么要有多线程
在没有多线程的时候,计算机的工作是依靠进程之间并发执行的,但是由于进程之间创建销毁或者是切换的开销较大,所以延伸出"多线程"的概念.
线程可以理解为"轻量级的进程",线程的创建销毁以及线程之间的切换的开销比进程要小得多.
除此之外,由于进程之间是相互隔离的,彼此要进行通信比较麻烦,但是对于同一个进程的多个线程,彼此共享同一份资源,线程之间的通信也较为简单.
多线程
多线程,就是多个线程并发执行,满足同时完成多个任务的需求.
多线程的特点
- 每个线程都是独立的执行流,彼此之间并发执行
- 可以通过代码实现对线程执行顺序的控制
多线程的代码实现
java标准库提供了一个Thread类,用来控制一个线程.
继承Thread类重写run方法
class MyThread extends Thread {
@Override
public void run() {
//方法实现
}
}
public class ThreadDemo1 {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
}
}
上述方法涉及到两个线程,一个是main方法对应的主线程(一个进程中至少需要一个线程),另外一个是t对应的线程
当我们点击运行按钮,运行我们的代码时,这个时候idea对应的进程就会帮我们自动创建一个新的Java进程,这个进程用来执行我们写的代码,这个进程包含两个线程,一个时主线程,一个是t线程
各个线程之间是独立的执行流代码展示
class MyThread extends Thread{
@Override
public void run() {
while (true) {
System.out.println("hello t");
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
while (true) {
System.out.println("hello main");
}
}
}
运行结果展示
分析
- 对于以上代码,如果是单核CPU,代码的执行顺序是执行到t.statr()方法后,开启新线程,之后这两个线程交替执行,但是由于线程之间的调度是随机的,所以不一定严格遵守两个线程轮流执行
- 如果是多核CPU,代码的执行顺序是执行到t.statr()方法后,开启新线程,那么在当前只有两个线程的情况下,main和t线程同时执行
实现Runnable接口重写run方法
public class MyRunnable implement Runnable {
@Override
public void run() {
// 重写run方法
}
}
public class ThreadDemo2 {
public static void main (String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t = new Thread(myRunnable);
t.start();
}
}
继承Thread类的匿名内部类写法
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
// 方法体
}
};
}
实现Runnable接口的匿名内部类写法
public static void main(String[] args) {
Thread t = new Thread(new Runnable () {
@Override
public void run() {
// 方法体
}
});
}
以上四种多线程的代码实现都不是最推荐的写法,最推荐的写法是通过lambda表达式实现多线程
lambda表达式实现
lambda表达式的基本写法 (参数列表)->{ // 方法体 }
public static void main(String[] args[]) {
Thread t = new Thread(()->{
// 方法体
});
}
Thread类
多线程的控制类,一个Thread对象控制一个线程.通过Thread对象实现对线程的开始/暂停/结束的控制
Thread的属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否是后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- ID是线程的唯一标识,不同线程不会重复,类似于人类世界的身份证
- 名称是线程的名字,对线程命名有利于进行调试
- 状态是标识线程当前所处的状态,线程的状态有NEW , RUNNABLE , BLOCKED , WAITING , TIMED_WAITING , TERMINATED
- 优先级高的线程理论上来说会更容易被调度到
- 后台线程,表示该线程不需要等到所有线程结束才能结束
- 存活,当thread类的run方法还没有被调用或者是运行结束就意味着当前线程不存活了,但是该线程对应的实例可能还是存在的.
- 线程的中断,该线程执行过程中被打断了
后台线程和前台线程
- 前台线程:应用程序(进程)必须运行完所有的前台线程才能退出
- 后台线程:应用程序可以不考虑后台线程是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时自动结束.
举个例子:银行的对外下班时间是五点,那么五点过后,就不会再继续坐在柜台对顾客提供服务了,但是在五点过后,银行内部需要对今天的业务进行整理和总结后才能下班.
那么在这个例子中,银行就是应用程序,来银行的顾客就是后台线程,我们的银行不需要等到将所有顾客都服务好了才关门,才是到点了不管你有没有被服务都关门,而银行的业务则是前台线程,只有把业务全部完成了,银行工作人员才能真正的下班.
前台线程和后台线程的区别和联系:
- 前台线程会阻止进程的结束,必须进程中的所有前台进程执行完,进程才能结束;后台线程不会阻止进程的结束,哪怕后台进程还没执行完,进程也会结束。
- 属于某个进程的所有前台线程都终止后,该进程就会被终止。所有剩余的后台线程都会停止且不会完成。
- 托管线程池中的线程都是后台线程,使用new Thread方式创建的线程默认都是前台线程。可以通过setDaemon()方法将前台进程设置为后台进程
Thread的方法
构造方法
方法 | 说明 |
Thread() | 创建一个线程对象 |
Thread(Runnable target) | 使用runnable对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target ,String name) | 使用Runnable对象创建线程对象,并命名 |
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("我是线程的名字");
Thread t4 = new Thread(new MyRunnable(), "我是线程的名字");
如果不对线程进行命名,那么系统会自动以 Thread-数字的形式对线程进行命名,对线程命名有利于对代码的调试
Thread的普通方法
启动一个线程
调用start()方法意味着真正的创建了一个线程,线程内部会自动执行run方法
而run()方法则是说明了线程要做的事情,仅仅调用run()方法无法真正创建出一个线程.
中断一个线程
获取当前线程引用
方法 | 说明 |
public static Thread currentThread(); | 返回当前线程对象的引用 |
该方法是静态方法,使用Thread类调用,就会返回当前所在的线程
public class Demo {
public static void main(String[] args) {
Thread t = new Thread(()->{
// 获取t线程的引用
System.out.println(Thread.currentThread());
});
t.start();
// 获取主线程的引用
System.out.println(Thread.currentThread());
}
}
等待一个线程
在一些情况下,线程之间存在先后关系,某些线程的执行要等另外的线程执行完毕才能执行.例如,在银行存钱,必须先开户,只有开户了才能存钱.
方法 | 说明 |
public void join() | 等待线程结束 |
public void join(long mills) | 等待线程结束,最多等待mills毫秒 |
休眠当前线程