目录
一、什么是线程
一个线程就是一个 "执行流" 。每个线程之间都可以按照顺序执行自己的代码。多个线程之间 "同时" 执行着多份代码。
我们举个例子看看:
一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。
如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。
此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。
那为什么要有线程呢?主要有以下两点:
- 首先, "并发编程" 成为 "刚需"
单核CPU的发展遇到了瓶颈。要想提高算力,就需要多核CPU。而并发编程能更充分利用多核CPU资源。有些任务场景需要"等待IO",为了让等待IO的时间能够去做一些其他的工作,也需要用到并发编程。
- 其次, 虽然多进程也能实现并发编程, 但是线程比进程更轻量
创建线程比创建进程更快;销毁线程比销毁进程更快;调度线程比调度进程更快。
二、进程与线程区别
- 进程是包含线程的,每个进程至少有一个线程存在,即主线程
- 进程和进程之间不共享内存空间。同一个进程的线程之间共享同一个内存空间
比如之前的多进程例子中,每个客户来银行办理各自的业务,但他们之间的票据肯定是不想让别人知道的,否则钱不就被其他人取走了么。而上面我们的公司业务中,张三、李四、王五虽然是不同的执行流,但因为办理的都是一家公司的业务,所以票据是共享着的。这个就是多线程和多进程的最大区别。
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位
三、Java的线程和操作系统线程的关系
线程是操作系统中的概念。操作系统内核实现了线程这样的机制,并且对用户层提供了一些API供用户使用(例如Linux的pthread库)。
Java 标准库中Thread类可以视为是对操作系统提供的API进行了进一步的抽象和封装。
四、一个简单的多线程程序
我们来看一个简单的多线程代码:
class Mythread extends Thread{
@Override
public void run(){
while(true){
System.out.println("hello thread");
}
}
}
public class Demo1 {
public static void main(String[] args){
Thread t = new Mythread();
t.start();
while(true){
System.out.println("hello main");
}
}
}
我们运行它,可以看到,这两个while循环在“同时执行”。我们所看到的结果是两边的日志都在交替打印。
上述代码中,start和run都是Thread的成员,那为什么要用start呢?
run只是描述了线程的入口(线程要做什么任务),而start才是真正调用了系统API,在系统中创建出线程,让线程再调用run。
我们使用run的话就只会打印“hello thread”这一句话。这里是run里面的while循环,并且不会循环结束。也就无法执行到“hello main”这个循环了。如下图所示:
把t.start改成t.run,代码中不会创建出新的线程,只有一个主线程,这个主线程里面只能一次执行循环,执行完一个再执行另一个。
五、通过jconsole观察进程里面的多线程情况
jconsole是JDK中带有的工具,怎么查找呢?
我们可以点击左上角File然后点击Project Structure选项,然后在跳出的页面里点击SDKs即可查看。如下图所示:
打开之后,我们运行JConsole工具:
在jconsole可以看到一个java进程,即使是最简单的,也包含了很多的线程。其中大部分是JVM自动创建的。
一个java进程启动之后,JVM会在后面默默做很多的事情,比如垃圾回收、资源统计、远程方法调用等。
未来写一些多线程程序时可以通过这个功能来看线程实时运行情况,比如你的线程卡死了。
五、Thread.sleep方法
while循环太快,我们可以使用sleep方法来减速。
Thread.sleep()是Thread类的一个静态方法,使当前线程休眠,进入阻塞状态(暂停执行),如果线程在睡眠状态被中断,将会抛出IterruptedException中断异常。抛异常的同时,该线程的中断状态会被清除。
我们运行下如下代码:
class Mythread extends Thread{
@Override
public void run(){
System.out.println("thread begin");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("thread end");
}
}
public class Demo1 {
public static void main(String[] args){
Thread t = new Mythread();
t.start();
System.out.println("main begin");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("main end");
}
}
注意:sleep方法会抛出异常,此处必须try catch,不能throws。因为在这个代码中是重写了父类的run,而父类的run没有throws,所以子类方法不能有throws。
六、创建线程其他方法
我们知道了创建线程可以重写Thread的run方法,我们也可以实现Runnable类来重写run,即interface。
class MyRunnable implements Runnable{
@Override
public void run(){
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo1 {
public static void main(String[] args){
Runnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
}
}
Runnable表示的是一个“可以运行的任务”,这个任务交给谁来运行Runnable本身并不关心 。使用Runnable的写法,和直接继承Thread之间的区别,主要就是三个字,解耦合。就是把任务本身给提取出来,此时就可以随时把代码改成使用其他方式来执行这个任务。
当然,我们可以使用匿名内部类:
{}相当于Thread的子类,可以重写父类的方法,比如下面代码:
new Thread() {
//重写方法
@Override
public void run() {
// run 方法里面写线程的任务
for (int i = 0; i < 6; i++) {
System.out.println("我是大帅逼");
}
}
}.start();
我们还可以使用lambda表达式创建Runnable子类对象:
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
Thread t4 = new Thread(() -> {
System.out.println("使用匿名类创建 Thread 子类对象");
});
七、Thread类及其常见方法
1、构造方法
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
Thread(ThreadGroup group, Runnable target) | 线程可以被用来分组管理,分好的组即为线程组 |
其中,线程名字可以被Jconsole工具查看。给线程起名字,后续在调试的时候,比较方便区分。
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
2、Thread常见属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- ID是线程的唯一标识,不同线程不会重复,这个id是java给你这个线程分配的,不是系统API提供的线程id,更不是pcb中的id
- 名称是各种调试工具用到
- 状态表示线程当前所处的一个情况
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。相比之下,后台线程不结束不影响整个进程的结束,而前台线程没有执行结束,整个进程一定不会结束。
- 是否存活,即简单的理解,为run方法是否运行结束了