创建线程
线程通常可以通过三种方式来创建,第一种是继承Thread类,第二中是实现Runnable接口,第三种则是使用FutureTask类,其中第二种方式是最重要的,而且Thread类也是实现的Runnable接口
继承Thread类
public class MyThread extends Thread {
private String url;
private String name;
public MyThread(String url, String name) {
this.url = url;
this.name = name;
}
@Override
public void run() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url, name);
System.out.println("下载了文件名为:" + name);
}
public static void main(String[] args) {
MyThread myThread1 = new MyThread("https://static.runoob.com/images/demo/demo2.jpg", "1.jpg");
MyThread myThread2 = new MyThread("http://img.desktx.com/d/file/wallpaper/scenery/20170107/080145c3a7460e7fa0369052a11467db.jpg", "2.jpg");
MyThread myThread3 = new MyThread("http://img.ewebweb.com/uploads/20190623/21/1561296521-HiSnYbhyeE.jpg", "3.jpg");
myThread1.start();
myThread2.start();
myThread3.start();
}
}
class WebDownloader {
public void downloader(String url, String name) {
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader方法出现问题");
}
}
}
实现Runnable接口
首先自定义线程类实现Runnable接口,然后在Run方法中编写线程执行体,然后创建线程对象(Thread),然后将实现Runnable接口的实例对象放进Thread实例对象里面,再调用该Thread对象的start方法即可。
public class TestThread implements Runnable{
String title;
public TestThread(String title){
this.title=title;
}
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(title+":"+i);
}
}
public static void main(String[] args) {
TestThread testThread1=new TestThread("线程1");
TestThread testThread2=new TestThread("线程2");
new Thread(testThread1).start();
new Thread(testThread2).start();
}
}
通过阅读Runnable接口的源码,可以看到是一个函数式接口,因此可以使用Lambda表达式简化过程。
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " is running");
});
t1.setName("t1");
t1.start();
}
注意:
- 直接继承Thread类是线程和任务耦合在了一起,而实现Runnable接口则是将线程和任务分开
- 使用实现Runnable接口的方式更加容易与线程池等高级API配合使用
- 实现Runnable接口让任务类脱离了Thread的继承体系,并且Java只允许单继承
使用FutureTask类
FutureTask类也是一个任务类,把它的实例对象放到Thread类里面就可以了。并且,FutureTask类还可以有线程执行的返回结果,因此执行一个线程并且需要在其它线程中获得这个线程的返回结果可以使用这种方法。
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName() + " is running");
return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果 Integer result = task3.get();
System.out.println("结果是:" + result + ",from " + Thread.currentThread().getName());
}
线程的运行原理
栈和栈帧
当每个线程启动后,也就是调用了 start 方法后,jvm就会为这个线程分配一块栈内存,而当这个线程每进入到一个方法,就会为这个方法在该线程的栈内存中分配一块属于该方法的栈帧,如下图所示:
每个栈由多个栈帧组成,但是只有位于栈顶的栈帧才是正在被线程执行的方法。在方法中的局部变量都会放到对应栈帧的局部变量表中,如果是基本数据类型则直接存放在局部变量表里面,如果是引用类型则存放一个引用,这个引用指向堆内存的对象;此外,每个栈帧还需要记录这个方法执行完毕后要返回到那行代码,即返回地址;而栈帧中的程序计数器则是不断地向cpu 核心提供需要在当前线程执行的下一条指令。
线程上下文切换
线程上下文切换就是因为某些原因导致 cpu 不再执行当前线程的指令流,转而执行另一个线程的指令流,原因一般如下:
- 分配给该线程的当前的时间片刚好用完
- jvm开启垃圾回收,停止所有用户线程
- 有更高优先级的线程待执行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当线程上下文发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住所在线程的下一条 jvm 指令的执行地址,是线程私有的。如果 cpu 频繁的进行线程上下文切换,那么就会影响到程序的性能