前言
在学习Java中的多线程编程之前,需要对进程、线程的基本知识有初步的了解,不懂的同学可以看我之前总结的博客
在Java标准库中,提供了一个Thread类,用来表示/操作线程
1、创建线程的方式
第一种方式: 自定义一个类,继承Thread类,重写run方法
//创建子类,继承父类,重写run方法
class MyThread extends Thread{
@Override
public void run(){
System.out.println("hello,thread");
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
//这里调用了start才是真正在系统中创建了线程,然后开始执行run操作
t.start();
}
}
run方法描述了线程内部需要执行哪些代码,run方法中的逻辑,是在新创建出来的线程中,被执行的代码。
new MyThread()并不是真正的创建线程,当调用start()方法后,才会在操作系统中创建一个线程,并且执行run操作,在调用start()方法之前,系统中是没有创建出线程的
第二种方式: 创建一个类,实现Runnable接口,再创建Runnable实例传给Thread实例
//Runnable 就是在描述一个“任务”
class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println("hello,thread");
}
}
public class Demo2 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}
}
通过Runnable来描述任务的内容,进一步再把描述好的任务交给Thread实例
第三种方式: 使用匿名内部类的方式创建
public class Demo3 {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
System.out.println("hello,thread");
}
};
t.start();
}
}
创建一个匿名内部类,继承自Thread类,重写run方法,同时再new出这个匿名内部类的实例
第四种方式: 创建Runnable匿名内部类的实例,作为参数传给Thread
public class Demo5 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello thread");
}
});
}
}
new的Runnable,针对这个创建的匿名内部类,同时new出Runnable实例传给Thread的构造方法
和第三种方式中的代码对比,通常认为第四种方式的写法更好一点,它能够做到让线程和线程执行的任务,更好的进行解耦。Runnable单纯的只是描述了一个任务,至于这个任务是要通过一个进程来执行,还是线程来执行,还是线程池来执行,还是协程来执行,Runnable本身并不关心Runnable里面的代码也不关心
第五种方式: 使用lambda表达式,是第四种方式的延伸
public class Demo6 {
public static void main(String[] args) {
//利用lambda表达式创建线程
Thread t = new Thread(()->{
System.out.println("hello thread");
});
t.start();
}
}
2、通过代码比较多线程的优势
都说多线程快,那我们就简单证明一下,串行执行两个变量从0增加到10亿和并发执行两个变量从0增加到10亿,看看哪一个更快
串行执行:
public class Demo7 {
private static final long count = 10_0000_0000;
public static void serial() {
//记录程序执行时间
long begin = System.currentTimeMillis();
long a = 0;
for(int i = 0; i < count; ++i) {
a++;
}
long b = 0;
for(int i = 0; i < count; ++i) {
b++;
}
//记录结束时间
long end = System.currentTimeMillis();
System.out.println("serial()消耗时间: " + (end - begin) + "ms");
}
public static void main(String[] args) throws InterruptedException {
serial();
}
}
执行5次,消耗的时间大致都在750ms上下
并发执行:
public class Demo7 {
private static final long count = 10_0000_0000;
public static void concurrency() throws InterruptedException {
long begin = System.currentTimeMillis();
Thread t1 = new Thread(()->{
long a = 0;
for(int i = 0; i <count; ++i) {
a++;
}
});
t1.start();
Thread t2 = new Thread(()->{
long b = 0;
for(int i = 0; i < count; ++i) {
b++;
}
});
t2.start();
//让main线程等待t1和t2执行完了再记录结束时间
t1.join();//让main线程等待t1执行结束
t2.join();//让main线程等待t2执行结束
long end = System.currentTimeMillis();
System.out.println("concurrency()消耗时间: " + (end - begin) + "ms");
}
public static void main(String[] args) throws InterruptedException {
concurrency();
}
}
执行5次,消耗的时间大致都在460ms上下
并发执行的效率提升了将近50%,但是并不是说一个线程600多ms,两个线程就400多ms。这两个线程在底层到底是并行还是并发,是不确定的,真正并行执行的时候,效率才会提升
多线程不是万能的,不是用了多线程,效率就一定高,还得看具体的应用场景
3、Thread类常见的构造方法和属性
常见的构造方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
Thread(ThreadGroup group,Runnable target) | 线程可以被用来分组管理,分好的组即为线程组 |
常见的属性
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
-
ID 是线程的唯一标识,不同线程不会重复
-
名称是各种调试工具用到
-
状态表示线程当前所处的一个情况
-
优先级高的线程理论上来说更容易被调度到
-
关于后台线程,创建的是前台线程,main执行完毕后,进程也不能退出,得等到线程执行完毕后,整个进程才结束,如果是后台线程,main执行完毕后,整个进程就直接退出,线程会被强行终止。需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行
-
是否存活,操作系统中对应的线程是否正在运行。Thread对象的生命周期和内核中对应的线程的生命周期并不完全一致,创建出线程对象之后,在调用start之前,系统中是没有对应的线程的。在run方法执行完毕后,系统中的线程就被销毁了,但线程这个对象可能还在
-
线程的中断问题,中断也就是让一个线程停下来
通过以下代码,来打印线程的各种属性:
public class Demo32 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + ": 我还 活着");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 我即将死去");
});
System.out.println(Thread.currentThread().getName() + ": ID: " + thread.getId());
System.out.println(Thread.currentThread().getName() + ": 名称: " + thread.getName());
System.out.println(Thread.currentThread().getName() + ": 状态: " + thread.getState());
System.out.println(Thread.currentThread().getName() + ": 优先级: " + thread.getPriority());
System.out.println(Thread.currentThread().getName() + ": 后台线程: " + thread.isDaemon());
System.out.println(Thread.currentThread().getName() + ": 活着: " + thread.isAlive());
System.out.println(Thread.currentThread().getName() + ": 被中断: " + thread.isInterrupted());
thread.start();
while (thread.isAlive()) {
}
System.out.println(Thread.currentThread().getName() + ": 状态: " + thread.getState());
}
}
4、启动线程
启动线程就是线程实例调用start()方法,前面已经使用过,不多赘述,这里主要讲一下start和run的区别
start和run的区别
public class Demo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
while(true) {
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
两个线程并发,交替打印
如果将t.start()换成t.run(),则会出现不一样的结果
结果只会打印"hello thread"
run方法只是一个普通的方法,在main线程中调用run,其实并没有创建的新的线程
这个循环仍然是在main线程中执行
既然是在一个线程中执行,代码就得从前到后按顺序执行
先运行第一个循环,再运行第二个循环,但一个循环会一直进行下去
调用 start 方法, 才真的在操作系统的底层创建出一个线程
5、中断线程
线程停下来的关键,就是让线程对应的run方法执行完毕(还有一个特殊的线程,那就是main线程,对于main来说,main方法执行完毕后,线程就完了)
1.)可以手动的设置一个标志位(自己创建的变量,boolean),来控制线程是否要执行结束
public class Demo10 {
private static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (!isQuit) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
//把isQuit设为true,t线程的循环就结束了,再进一步执行run,线程就结束了
Thread.sleep(5000);
isQuit = true;
System.out.println("线程t终止");
}
}
注意:main线程和t线程在同一个进程地址空间,因此,main线程修改的isQuit和t线程判定的isQuit是同一个值
2.)上述代码不够严谨,更好的做法是使用Thread中内置的一个标志位来进行判定
可以通过Thread.interrupted()和Thread.currentThread.isinterrupted()获得这个标志位
前者是一个静态方法,后者是一个实例方法
推荐使用后者,因为一个代码中的线程可能有很多个,随时哪个线程都可能会终止。
Thread.interrupted()判定的标志位是Thread的static成员(一个程序中只有一个标志位)
Thread.currentThread.isinterrupted()判定的标志位是Thread的普通成员,每个线程实例都有自己的标志位
public class Demo11 {
public static void main(String[] args) {
Thread t = new Thread(()->{
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//当触发异常后,立即退出循环
break;
} finally {
System.out.println("这是收尾工作");
}
}
});
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//在主线程中调用 interrupt 方法,来中断这个线程
//t.interrupt()的意思就是让t中断!!
//如果调用这个方法,可能会产生两种情况
//1.如果t线程处在就绪状态,就是设置线程的标志位为true
//2.如果t线程处在阻塞状态(sleep休眠),就会触发一个interruptedException,此时设置标志位就不能起到及时唤醒的作用
t.interrupt();
}
}
6、等待线程
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。因此,我们需要一个方法明确等待线程的结束。
public class Demo12 {
public static void main(String[] args) {
Thread t = new Thread(()->{
for(int i = 0; i < 5; ++i) {
System.out.println("hello,thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
//在主线程中可以使用一个等待操作,来等待t线程的执行结束
try {
t.join(5000);//最多等待5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main等待完毕");
}
}
首先,调用t.join()这个方法的线程是main线程,针对t这个线程对象调用的,此时就是让main等待t
调用join之后,main线程就会进入阻塞状态(暂时无法在cpu上执行),直到t线程执行完毕,main线程才会被唤醒,继续向下执行。
通过线程等待,一定程度上的干预了线程的执行顺序
join()方法默认情况下,是死等(不见不散),如果t线程一直不执行完毕,main线程就一直等下去
如果给join()方法添加参数,例如join(5000),表示main线程最多等5秒,如果在5秒之内,t线程执行完毕,main线程就不会再等。如果t线程5秒之后还在执行,main线程最多等待5秒就不再等待了
7、获取当前线程的引用
public static Thread currentThread()就能够获取到当前线程的引用(Thread实例的引用)。
哪个线程调用的这个方法,就获取到的是哪个线程的实例
public class Demo13 {
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
//获取当前线程的实例
//这个代码是通过继承Thread的方式来创建线程
//此时run方法中,直接通过this,拿到的就是当前的Thread的实例
System.out.println(Thread.currentThread().getName());
System.out.println(this.getName());
}
};
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//此处的this不是指向Thread,而是指向Runnable,而Runnable只是一个单纯的任务,没有name属性的
//System.out.println(this.getName());
//要想拿到线程的名字,只能通过Thread.currentThread().getName()
//lambda表达式效果同Rannable
System.out.println(Thread.currentThread().getName());
}
});
t2.start();
//这个线程是在main线程中调用的,因此拿到的是main这个线程的实例
System.out.println(Thread.currentThread().getName());
}
}
注意:通过Runnable去构造Thread对象,在run方法中,this指向的是Runnable,而不是Thread对象。使用lambda去构造Thread对象和Runnable是一样的