文章目录
- 进程与线程的故事
- 1.1 进程的诞生
- 对操作系统的要求进一步提高
- 为什么我们要使用多线程?
- 1.2 上下文切换的故事
- Java多线程入门
- 1. 继承`Thread`类
- 代码示例
- 2. 实现`Runnable`接口
- 代码示例
- 3. Thread类的构造方法和常用方法
- 构造方法
- 常用方法
- 4. Thread类与Runnable接口的比较
- 5. Callable接口、Future接口和FutureTask类
- 5.1 Callable接口
- 代码示例
- 5.2 Future接口
- 5.3 FutureTask类
- 代码示例
- 6. 总结
- 线程组和线程优先级
- 3.1 线程组 (ThreadGroup)
- 示例代码
- 线程组的设计理念
- 3.2 线程的优先级
- 示例代码
- 线程优先级的实际影响
- 示例代码
- 守护线程 (Daemon Thread)
- 线程组的优先级与线程优先级的关系
- 示例代码
- 3.3 线程组的常用方法及数据结构
- 3.3.1 线程组的常用方法
- 3.3.2 线程组的数据结构
进程与线程的故事
1.1 进程的诞生
在计算机的最早期,它们就像一个听话但有点儿笨拙的仆人。每次只能执行一个指令。你告诉它要做什么,它就立刻去做,但你一旦停下思考或输入,它也只能在那里干等着。这种方式效率很低,计算机的大部分时间都在等你,而不是在干活。
后来,聪明的科学家们想到一个办法:既然计算机可以执行多个指令,那为什么不让它一次性做完一大堆事呢?于是,他们发明了批处理操作系统。这个系统允许用户将一系列指令写在磁带上,计算机一次性读取并执行,结果被写在另一个磁带上。这就像让计算机一次性干完很多活儿,然后把成果展示给你。
虽然批处理操作系统提高了效率,但它还有一个很大的缺陷:它只能一次运行一个程序。就像一个工人只能一次干一件事,如果遇到堵塞,比如等材料到位,这个工人就只能干等着,其他工作也都停下来了。因此,尽管批处理让计算机更忙碌,但还是不够高效。
进程的出现解决了这个问题。科学家们问自己:“为什么内存中只能有一个程序在运行呢?能不能同时让多个程序在内存中运行呢?”于是,他们提出了“进程”的概念。
进程是指计算机正在运行的一个程序。每个进程都有自己的内存和运行状态,它们互不干扰。现在,计算机就像一个多任务的工人,虽然在某一时刻只能专注于一个任务,但它能迅速切换任务,每个任务在一段时间内得到处理。这种快速的切换让我们觉得计算机好像在同时做很多事情,这就是并发。
对操作系统的要求进一步提高
虽然进程的引入大大提高了效率,但随着人们对计算机的要求越来越高,问题又出现了。想象一下,你在使用一个杀毒软件,它在扫描病毒的过程中突然卡住了,这时你却不能去使用软件中的其他功能,比如清理垃圾。这是因为,在那个时候,一个进程只能做一件事情。
于是,科学家们想:“能不能让一个进程同时做多件事呢?”这就引出了线程的概念。线程就像是进程中的小工人,每个线程负责一个子任务。这样,一个进程就可以同时处理多个任务,比如在杀毒软件中,一个线程负责扫描病毒,另一个线程可以同时清理垃圾。即使某个任务卡住了,其他任务仍然可以继续进行。
线程的出现,使得一个进程内部也可以并发处理多个任务,这大大提高了计算机的效率。
为什么我们要使用多线程?
虽然多进程也能实现并发,但多线程有着不可忽视的优势。首先,线程之间的通信比进程之间的通信要简单得多,因为它们共享同一块内存。其次,线程的开销要比进程小得多,创建和管理它们需要的资源更少。
进程和线程的区别也很明显。进程是一个独立的运行环境,拥有自己的内存空间,因此各个进程之间是隔离的,彼此不会干扰。但线程却共享进程的内存空间,这让线程之间的数据共享变得容易,但同步问题却变得复杂。如果一个进程崩溃了,其他进程仍能继续运行,但如果一个线程崩溃了,可能会导致整个程序的不稳定。
此外,进程是操作系统分配资源的基本单位,而线程则是操作系统调度的基本单位。CPU的时间片是分配给线程的,而不是进程的。
1.2 上下文切换的故事
上下文切换是另一个有趣的故事。想象一下,CPU是一个非常忙碌的秘书,它不断在不同的任务之间来回切换。每当它要去处理另一个任务时,必须先把当前任务的状态记下来,比如它在哪一步停下的、哪些数据已经处理过等。然后,它再去找到下一个任务的状态,把它恢复出来,继续处理这个新任务。
这个过程就像在多个文件之间来回切换,每次都要记住上一个文件的内容,并快速进入下一个文件的状态。这种操作虽然让计算机看起来在同时做很多事,但实际上,它消耗了大量的时间和资源。
因此,在设计多线程程序时,如何减少这种上下文切换次数,成为了一个非常重要的课题。毕竟,线程并不是越多越好,太多的线程反而可能拖慢整个系统的效率。
Java多线程入门
在Java中,多线程编程是非常重要的技能。多线程允许程序同时执行多个任务,从而提高效率。要实现多线程,Java提供了Thread
类和Runnable
接口。
1. 继承Thread
类
首先,我们可以通过继承Thread
类来创建一个新的线程。Thread
类是Java自带的一个类,它代表了一个线程。要创建一个线程,只需继承Thread
类,然后重写其中的run
方法。
代码示例
public class Demo {public static class MyThread extends Thread {@Overridepublic void run() {System.out.println("MyThread");}}public static void main(String[] args) {Thread myThread = new MyThread();myThread.start();}
}
- 解释:
- 在这个示例中,我们定义了一个
MyThread
类,它继承了Thread
类并重写了run
方法。在run
方法中,我们只是打印出“MyThread”。 - 在
main
方法中,我们创建了一个MyThread
实例,并调用了start()
方法启动线程。 - 注意:调用
start()
方法时,Java虚拟机会创建一个新的线程,并在合适的时间执行run
方法。
- 在这个示例中,我们定义了一个
2. 实现Runnable
接口
另一种创建线程的方法是实现Runnable
接口。与继承Thread
类不同,Runnable
是一个接口,所以需要实现其中的run
方法。
代码示例
public class Demo {public static class MyThread implements Runnable {@Overridepublic void run() {System.out.println("MyThread");}}public static void main(String[] args) {new Thread(new MyThread()).start();// Java 8 函数式编程,可以省略MyThread类new Thread(() -> {System.out.println("Java 8 匿名内部类");}).start();}
}
- 解释:
- 在这个示例中,我们实现了
Runnable
接口,并重写了run
方法。 - 在
main
方法中,我们创建了一个Thread
对象,并传入了MyThread
的实例。 - 同时,Java 8 引入了函数式编程,可以使用Lambda表达式简化代码。这段代码展示了如何用Lambda表达式创建并启动一个线程。
- 在这个示例中,我们实现了
3. Thread类的构造方法和常用方法
构造方法
Thread(Runnable target)
:这个构造方法接收一个Runnable
对象,并将它作为线程的任务。Thread(Runnable target, String name)
:与上面的构造方法类似,但它还允许我们为线程指定一个名字。
常用方法
currentThread()
:这是一个静态方法,返回当前正在执行的线程对象。start()
:启动线程的方法,调用后会启动新线程并调用run()
方法。yield()
:当前线程放弃CPU的使用权,但是否真正让出CPU取决于操作系统的调度。sleep(long millis)
:使当前线程休眠指定的毫秒数,暂停执行但不释放锁。join()
:使当前线程等待另一个线程完成后再继续执行。
4. Thread类与Runnable接口的比较
- 灵活性:由于Java支持单继承多实现,使用
Runnable
接口更灵活。一个类可以实现多个接口,但只能继承一个类。 - 面向对象设计:使用
Runnable
接口有助于将线程任务与线程本身分离,更符合面向对象设计原则。 - 耦合度低:实现
Runnable
接口可以降低线程对象与任务之间的耦合度。
因此,通常推荐使用Runnable
接口来实现线程。
5. Callable接口、Future接口和FutureTask类
5.1 Callable接口
Callable
接口与Runnable
类似,但它有一个重要的区别:Callable
的call()
方法有返回值,并且可以抛出异常。
代码示例
import java.util.concurrent.Callable;class Task implements Callable<Integer> {@Overridepublic Integer call() throws Exception {Thread.sleep(1000); // 模拟任务执行return 2;}public static void main(String[] args) throws Exception {Task task = new Task();Integer result = task.call();System.out.println(result); // 输出2}
}
- 解释:
- 这里我们实现了一个简单的
Callable
,call()
方法在执行后返回一个整数。 - 在
main
方法中,我们直接调用了call()
方法并输出结果。
- 这里我们实现了一个简单的
5.2 Future接口
Future
接口通常与Callable
配合使用,用于获取异步任务的执行结果。
get()
:获取任务的执行结果,这个方法是阻塞的,也就是说它会一直等待直到任务完成。cancel(boolean mayInterruptIfRunning)
:试图取消任务的执行。
5.3 FutureTask类
FutureTask
是Runnable
和Future
的实现类,它既可以作为Runnable
被执行,也可以作为Future
获取任务的结果。
代码示例
import java.util.concurrent.FutureTask;class Task implements Callable<Integer> {@Overridepublic Integer call() throws Exception {Thread.sleep(1000); // 模拟任务执行return 2;}public static void main(String[] args) throws Exception {FutureTask<Integer> futureTask = new FutureTask<>(new Task());Thread thread = new Thread(futureTask);thread.start();System.out.println(futureTask.get()); // 输出2}
}
- 解释:
- 在这个例子中,我们将
Task
封装成FutureTask
并传入Thread
中。 - 线程启动后,我们可以通过
futureTask.get()
获取执行结果。
- 在这个例子中,我们将
FutureTask
可以确保在高并发环境下任务只执行一次,非常适合在多线程环境下使用。
6. 总结
在Java中,我们可以通过继承Thread
类或实现Runnable
接口来创建线程。虽然Thread
类使用起来简单,但Runnable
接口更加灵活,更符合面向对象设计原则。
当需要线程有返回值时,可以使用Callable
接口,而Future
接口和FutureTask
类提供了获取异步任务结果的机制。
线程组和线程优先级
3.1 线程组 (ThreadGroup)
在Java中,ThreadGroup
用于表示线程组,提供对线程的批量控制。每个线程 (Thread
) 必须属于一个线程组 (ThreadGroup
),不能独立于线程组存在。
Java程序的主线程属于默认的线程组“main”,如果新建线程时未显式指定线程组,默认会将当前执行线程的线程组设置为新线程的线程组。
示例代码
public class Demo {public static void main(String[] args) {Thread testThread = new Thread(() -> {System.out.println("testThread当前线程组名字:" +Thread.currentThread().getThreadGroup().getName());System.out.println("testThread线程名字:" +Thread.currentThread().getName());});testThread.start();System.out.println("执行main所在线程的线程组名字: " + Thread.currentThread().getThreadGroup().getName());System.out.println("执行main方法线程名字:" + Thread.currentThread().getName());}
}
输出结果:
执行main所在线程的线程组名字: main
执行main方法线程名字:main
testThread当前线程组名字:main
testThread线程名字:Thread-0
线程组的设计理念
ThreadGroup
和 Thread
之间是典型的向下引用的树状结构。设计这个结构的目的是为了避免“上级”线程被“下级”线程引用,从而无法被垃圾回收器 (GC) 有效回收。
3.2 线程的优先级
Java允许为线程设置优先级,范围是1到10。然而,并不是所有操作系统都支持10级优先级划分。例如,有些操作系统仅支持3级(低、中、高)优先级划分。Java的线程优先级设置仅是给操作系统的一个参考值,操作系统可以决定线程的实际优先级。
示例代码
public class Demo {public static void main(String[] args) {Thread a = new Thread();System.out.println("我是默认线程优先级:"+a.getPriority());Thread b = new Thread();b.setPriority(10);System.out.println("我是设置过的线程优先级:"+b.getPriority());}
}
输出结果
我是默认线程优先级:5
我是设置过的线程优先级:10
线程优先级的实际影响
尽管可以为线程设置优先级,但Java中线程的优先级并不一定可靠,具体执行顺序由操作系统的线程调度算法决定。我们通过以下代码验证这一点:
示例代码
public class Demo {public static class T1 extends Thread {@Overridepublic void run() {super.run();System.out.println(String.format("当前执行的线程是:%s,优先级:%d",Thread.currentThread().getName(),Thread.currentThread().getPriority()));}}public static void main(String[] args) {IntStream.range(1, 10).forEach(i -> {Thread thread = new Thread(new T1());thread.setPriority(i);thread.start();});}
}
某次输出结果
当前执行的线程是:Thread-17,优先级:9
当前执行的线程是:Thread-1,优先级:1
当前执行的线程是:Thread-13,优先级:7
当前执行的线程是:Thread-11,优先级:6
当前执行的线程是:Thread-15,优先级:8
当前执行的线程是:Thread-7,优先级:4
当前执行的线程是:Thread-9,优先级:5
当前执行的线程是:Thread-3,优先级:2
当前执行的线程是:Thread-5,优先级:3
Java的线程调度策略为抢占式,即高优先级的线程通常比低优先级的线程有更大的执行机会。当线程优先级相同时,线程按“先到先得”的原则执行。每个Java程序默认都有一个主线程,即JVM启动的第一个线程 main
线程。
守护线程 (Daemon Thread)
守护线程是优先级较低的一种线程,当所有非守护线程都结束时,守护线程也会自动结束。它通常用于执行一些后台任务。可以通过 Thread
类的 setDaemon(boolean on)
方法将某个线程设置为守护线程。
线程组的优先级与线程优先级的关系
如果线程优先级高于其所在的线程组的最大优先级,则该线程的优先级将失效,使用线程组的最大优先级。
示例代码
public static void main(String[] args) {ThreadGroup threadGroup = new ThreadGroup("t1");threadGroup.setMaxPriority(6);Thread thread = new Thread(threadGroup,"thread");thread.setPriority(9);System.out.println("我是线程组的优先级"+threadGroup.getMaxPriority());System.out.println("我是线程的优先级"+thread.getPriority());
}
输出结果
我是线程组的优先级6
我是线程的优先级6
3.3 线程组的常用方法及数据结构
3.3.1 线程组的常用方法
获取当前的线程组名字:
Thread.currentThread().getThreadGroup().getName();
复制线程组:
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
Thread[] threads = new Thread[threadGroup.activeCount()];
threadGroup.enumerate(threads);
线程组统一异常处理:
public class ThreadGroupDemo {public static void main(String[] args) {ThreadGroup threadGroup1 = new ThreadGroup("group1") {public void uncaughtException(Thread t, Throwable e) {System.out.println(t.getName() + ": " + e.getMessage());}};Thread thread1 = new Thread(threadGroup1, new Runnable() {public void run() {throw new RuntimeException("测试异常");}});thread1.start();}
}
3.3.2 线程组的数据结构
线程组不仅可以包含线程,还可以包含其他线程组。以下是 ThreadGroup
源码中的部分成员变量:
public class ThreadGroup implements Thread.UncaughtExceptionHandler {private final ThreadGroup parent; // 父线程组String name; // 线程组名称int maxPriority; // 线程最大优先级boolean destroyed; // 是否被销毁boolean daemon; // 是否守护线程boolean vmAllowSuspension; // 是否可以中断int nUnstartedThreads = 0; // 未启动的线程数量int nthreads; // 线程组中的线程数量Thread threads[]; // 线程组中的线程数组int ngroups; // 子线程组数量ThreadGroup groups[]; // 子线程组数组
}
ThreadGroup
的构造函数:
// 私有构造函数
private ThreadGroup() { this.name = "system";this.maxPriority = Thread.MAX_PRIORITY;this.parent = null;
}// 默认构造函数
public ThreadGroup(String name) {this(Thread.currentThread().getThreadGroup(), name);
}// 构造函数
public ThreadGroup(ThreadGroup parent, String name) {this(checkParentAccess(parent), parent, name);
}// 私有构造函数,主要的构造函数
private ThreadGroup(Void unused, ThreadGroup parent, String name) {this.name = name;this.maxPriority = parent.maxPriority;this.daemon = parent.daemon;this.vmAllowSuspension = parent.vmAllowSuspension;this.parent = parent;parent.add(this);
}
总结来说,线程组是树状结构,每个线程组可以包含多个线程或子线程组。线程组在统一管理线程优先级和检查线程权限方面发挥作用。