- 创建和运行线程
- 方法1-Thread
- 方法2-Runnable
- 方法3-Callable
Java线程
创建和运行线程
方法1-Thread
Thread 创建线程方式:创建线程类,匿名内部类方式。
1
2
3
4
5
6
7
8
9
10
11
12public class ThreadDemo{
public static void main(String[] args) {
Thread t = new Thread(){
public void run(){
System.out.println("running");
}
};
t.setName("t1");
t.start();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14public class ThreadDemo {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
}
}
class MyThread extends Thread {
public void run() {
for(int i = 0 ; i < 100 ; i++ ) {
System.out.println("子线程输出:"+i)
}
}
}- start() 方法底层其实是给 CPU 注册当前线程,并且触发 run() 方法执行
- 线程的启动必须调用 start() 方法,如果线程直接调用 run() 方法,相当于变成了普通类的执行,此时主线程将只有执行该线程
- 建议线程先创建子线程,主线程的任务放在之后,否则主线程(main)永远是先执行完
继承 Thread 类的优缺点:
- 优点:编码简单
- 缺点:线程类已经继承了 Thread 类无法继承其他类了,功能不能通过继承拓展(单继承的局限性)
方法2-Runnable
Runnable 创建线程方式:创建线程类,匿名内部类方式。
1
2
3
4
5
6
7
8
9
10
11
12
13public class ThreadDemo{
public static void main(String[] args) {
// 创建线程对象
Thread t = new Thread(new Runnable(){
public void run(){
// 要执行的任务
}
};);
// 启动线程
t.start();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class ThreadDemo {
public static void main(String[] args) {
Runnable target = new MyRunnable();
Thread t1 = new Thread(target,"1号线程");
t1.start();
Thread t2 = new Thread(target);//Thread-0
}
}
public class MyRunnable implements Runnable{
public void run() {
for(int i = 0 ; i < 10 ; i++ ){
System.out.println(Thread.currentThread().getName() + "->" + i);
}
}
}Runnable 方式的优缺点:
- 缺点:代码复杂一点。
- 优点:
- 线程任务类只是实现了 Runnable 接口,可以继续继承其他类,避免了单继承的局限性
- 同一个线程任务对象可以被包装成多个线程对象
- 适合多个多个线程去共享同一个资源
- 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立
- 线程池可以放入实现 Runnable 或 Callable 线程任务对象
Thread
是把线程和任务合并在了一起,Runnable
是把线程和任务分开了。
方法3-Callable
实现 Callable 接口:
- 定义一个线程任务类实现 Callable 接口,申明线程执行的结果类型
- 重写线程任务类的 call 方法,这个方法可以直接返回执行的结果
- 创建一个 Callable 的线程任务对象
- 把 Callable 的线程任务对象包装成一个未来任务对象
- 把未来任务对象包装成线程对象
- 调用线程的 start() 方法启动线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class ThreadDemo {
public static void main(String[] args) {
FutureTask<String> task = new FutureTask<>(new Callable<String>() {
public String call() throws Exception {
return Thread.currentThread().getName() + "->" + "Hello World";
}
});
Thread t = new Thread(task);
t.start();
try {
String s = task.get(); // 等待task结果返回
System.out.println(s);
} catch (Exception e) {
e.printStackTrace();
}
}
}public FutureTask(Callable<V> callable)
:未来任务对象,在线程执行完后得到线程的执行结果- FutureTask 就是 Runnable 对象,因为 Thread 类只能执行 Runnable 实例的任务对象,所以把 Callable 包装成未来任务对象
public V get()
:同步等待 task 执行完毕的结果,如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步- get() 线程会阻塞等待任务执行完成
- run() 执行完后会把结果设置到 FutureTask 的一个成员变量,get() 线程可以获取到该变量的值
优缺点:
- 优点:同 Runnable,并且能得到线程执行的结果
- 缺点:编码复杂
线程原理
Java Virtual Machine Stacks(Java 虚拟机栈):每个线程启动后,虚拟机就会为其分配一块栈内存
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch):一些原因导致 CPU 不再执行当前线程,转而执行另一个线程
- 线程的 CPU 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park 等方法
程序计数器(Program Counter Register):记住下一条 JVM 指令的执行地址,是线程私有的
当 Context Switch 发生时,需要由操作系统保存当前线程的状态(PCB 中),并恢复另一个线程的状态,包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
JVM 规范并没有限定线程模型,以 HotSopot 为例:
- Java 的线程是内核级线程(1:1 线程模型),每个 Java 线程都映射到一个操作系统原生线程,需要消耗一定的内核资源(堆栈)
- 线程的调度是在内核态运行的,而线程中的代码是在用户态运行,所以线程切换(状态改变)会导致用户与内核态转换进行系统调用,这是非常消耗性能
线程安全问题
Q:
- 成员变量和静态变量是否线程安全?
A:
- 如果它们没有共享,则线程安全。
- 如果它们被共享了,根据它们的状态是否发生改变,又分为两种情况:
- 如果只有读操作,则线程安全。
- 如果有读写操作,则需要考虑线程安全问题。
Q:
- 局部变量是否线程安全?
A:
- 因为对于局部变量来说,每个线程都会创建自己的局部变量,所以局部变量是线程安全的。
- 但是局部变量引用的对象不一定是线程安全的:
- 如果该对象没有逃离方法的作用范围,则它是线程安全的。
- 如果该对象逃离方法的作用范围,则需要考虑线程安全问题。