Java 多线程基本概念及创建线程
文章目录
1 进程(process)与线程(Thread)
-
程序:指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
-
进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。
-
线程:一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
2 Java 多线程编程核心概念
-
单核 CPU 采用频繁切换线程的方法,给人一种多线程的感觉。
-
多核 CPU 是真正的多线程,可以真正的并发执行多个线程。
-
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
-
多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销,可以达到充分利用 CPU 的目的。
-
Java 给多线程编程提供了内置的支持。
-
Java 程序运行时中至少有2个线程并发,一个是主线程调用 main() 方法,一个是垃圾回收线程负责看护和回收垃圾。如果出现了运行时异常,还会多一个处理异常的线程。
-
进程 A 与进程 B 的内存资源不共享。
-
Java 中线程 A 和线程 B 共享堆内存和方法区内存,但是栈内存独立,一个线程一个栈,互不干扰。
3 线程生命周期
-
创建状态:线程对象刚刚
new
出来。 -
就绪状态:
- 线程对象调用了
start()
方法。 - 就绪状态也称可运行状态,可以抢夺 CPU 时间片的权利( CPU 时间片就是执行权)。
- 抢夺到 CPU 时间片后,就会调用
run()
方法。
- 线程对象调用了
-
运行状态:
run()
方法开始执行,就标志着线程进入了运行状态。- 当占有的时间片用完后,会重新进入就绪状态,继续抢夺时间片。
- 再次抢夺到时间片后,会重新进入
run()
方法,并接着上一次的代码继续往下执行。
-
阻塞状态:
- 遇到阻塞事件时,会进入阻塞状态,例如:用户输入、线程休眠等。
- 进入阻塞状态的线程会释放当前占有的 CPU 时间片。
- 阻塞解除后,会回到就绪状态,再次抢夺 CPU 时间片。
-
死亡状态:
run()
方法执行完毕,进入死亡状态。
需要注意的是:
-
线程对象每次抢夺到的时间片的长度是不一致的,所以在一些用循环演示多线程的实例中会出现每次执行的次数都不一样。
-
我对
run()
方法的理解,分支线程中的run()
方法类似于主线程中的main()
方法,都是程序执行的入口。
4 创建线程
实现多线程的方式:
-
继承
java.lang.Thread
类,重写run()
方法,创建线程对象,使用start()
方法启动线程。 -
实现
java.lang.Runnable
接口,重写run()
方法,将java.lang.Runnable
实现类对象 r 传入java.lang.Thread
类对象 t,用 t 调用start()
方法启动线程。 -
实现
java.lang.Callable
接口,重写call
方法,将java.util.concurrent.Callable
的实现类对象 c 传入java.util.concurrent.FutureTask
构造器,将java.util.concurrent.FutureTask
的实例化对象 f 传入java.lang.Thread
构造器,创建线程对象 t。
4.1 继承 Thread 创建线程对象
步骤:
-
定义线程类,该类需要继承
java.lang.Thread
类。 -
重写
run()
方法,编写线程执行体。 -
创建定义类对象,调用
start()
方法启动线程。
public class ThreadTest {
// 主线程
public static void main(String[] args) {
// 创建线程对象
MyThread myThread = new MyThread();
// 直接调用 run() 方法
// 这样不能实现迸发,没有开辟新的栈空间,相当于只是普通对象调用普通的方法。
//myThread.run();
// 启动线程,开启新的栈空间 start() 方法
myThread.start();
for (int i = 0; i < 1000; i++){
System.out.println("<**************main*************>" + i);
}
}
}
// 定义线程类,继承 java.lang.Thread ,重写 run() 方法
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++){
System.out.println("<-----------myThread----------->" + i);
}
}
}
运行结果:
<**************main*************>0
<**************main*************>1
<**************main*************>2
<**************main*************>3
<**************main*************>4
<**************main*************>5
<**************main*************>6
<-----------myThread----------->0
<-----------myThread----------->1
<**************main*************>7
...
注意:
-
线程开启不一定立即执行,由 CPU 调度执行,也就是说谁抢到 CPU 时间片就执行谁。
-
当线程对象调用
start()
方法后,会自动调用run()
方法。 -
分支线程中
run()
方法的地位等价于主线程中main()
方法的地位,都是程序的入口。
4.2 实现 Runnable 接口创建线程对象
步骤:
-
定义线程类实现
java.lang.Runnable
接口。 -
重写
run()
方法,编写线程执行体。 -
创建定义类对象,并作为参数传入
java.lang.Thread
对象。 -
使用
java.lang.Thread
对象调用start()
方法启动线程。
public class RunnableTest {
public static void main(String[] args) {
// 创建 Runnable 实现类
MyRunnable r = new MyRunnable();
// 创建线程
Thread t = new Thread(r);
// 启动线程
t.start();
// 继续执行主线程
for (int i = 0; i < 1000; i++){
System.out.println("<*************main*************>" + i);
}
// 匿名内部类写法
/*
Thread t2 = new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 1000; i++){
System.out.println("<-----------Runnable2----------->" + i);
}
}
});
t2.start();
*/
}
}
// 创建类实现 Runnable 接口,重写 run() 方法
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++){
System.out.println("<-----------myRunnable--------->" + i);
}
}
}
运行结果:
<*************main*************>31
<*************main*************>32
<-----------myRunnable----------->1
<*************main*************>33
<-----------myRunnable----------->2
<*************main*************>34
<-----------myRunnable----------->3
<*************main*************>35
<-----------myRunnable----------->4
<-----------myRunnable----------->5
<-----------myRunnable----------->6
<*************main*************>36
<-----------myRunnable----------->7
<-----------myRunnable----------->8
<*************main*************>37
注意:
-
java.lang.Runnable
只是一个接口,接口中只有一个run()
方法。 -
实现了
java.lang.Runnable
接口的类不具备多线程能力,只是一个可执行类。 -
可以将
java.lang.Runnable
实现类的对象 r 传入java.lang.Thread
创建线程对象 t。 -
此时就具备多线程能力了,使用
t.start()
启动线程,开辟新的栈空间。
两种方式对比:
-
继承
Thread
类-
子类继承
Thread
类具备多线程能力 -
启动线程:
子类对象.start()
-
特点:单继承,不易于扩展,具有局限性。
-
-
实现
Runnable
接口-
实现
Runnable
接口的类本身不具备多线程能力 -
启动线程:使用
Thread
作代理启动,将Runnable
接口实现类对象作为参数创建Thread
类对象,然后调用start()
方法启动线程 -
特点:实现类可以实现多个接口,避免了单继承的局限性,方便灵活。
-
4.3 实现 Callable 接口创建线程对象
java.util.concurrent
包是 JDK8 的新特性,是线程并发包FutureTask<V>
类是可以包装Callable
和Runnable
的对象,实现了Runnable
接口- 使用
Callable
接口,可以获取线程返回值,线程方法为:V call() throws Exception
步骤:
-
定义线程类,实现
Callable
接口,重写call
方法 -
创建
FutureTask
对象,包装Callable
实现类对象 -
创建
Thread
对象,使FutureTask
具备多线程能力 -
启动多线程
start()
方法
public class CallableTest2 {
public static void main(String[] args) throws Exception {
// 1. 创建 Callable 实现对象
MyCallable callable = new MyCallable();
// 2. 创建未来任务类对象,接受 Callable 实现对象
FutureTask<Integer> task = new FutureTask<>(callable);
// 3. 创建线程对象,使 FutureTask 具备多线程能力
Thread t = new Thread(task);
// 4. 启动线程
t.start();
// 5. 在 main 线程中获取 t 线程的执行结果
Object obj = task.get();
System.out.println(obj);
}
}
// 实现 Callable<Integer> 接口,重写 call 方法
class MyCallable implements Callable<Integer> {
@Override
public Integer call() {
System.out.println("call method begin...");
// 3 秒后获取结果
try {
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
int a = 100;
int b = 200;
System.out.println("call method end...");
return a + b; // 会自动装箱 int -> Integer
}
}
-
前两种多线程方式重写的是
public abstract void run()
没有返回值。 -
这种多线程方式重写的是
V call() throws Exception
方法,返回值为泛型。 -
在 a 线程中调用 b 线程调用
get()
方法会使 a 线程进入阻塞状态,直到 b 线程执行完毕并返回结果后,a 线程回到就绪状态。 -
这种多线程实现方式唯一好处就是可以返回线程执行结果。
-
由于会使获取结果的线程进入阻塞状态,所以降低了效率。
参考文章: