赞
踩
从今天开始阿Q将陆续更新java并发编程专栏,期待您的订阅。
在系统学习线程之前,我们先来了解一下它的概念,与经常提到的进程做个对比,方便记忆。
线程和进程是操作系统中的两个重要概念,它们都代表了程序运行时的执行单位,它们的出现是为了更好地管理计算机资源和提高系统的运行效率,使用它们可以实现多任务同时运行,从而提高系统资源的利用率。
进程是程序的一次执行过程,系统运行的每一个程序都是一个进程,它是操作系统资源分配的最小单位,也是系统运行程序的基本单位。
在同一个操作系统中,多个进程可以并发执行,每个进程都拥有各自独立的内存空间,相互之间不会产生影响。如下图,windows 系统的任务管理器页面运行的进程就对应着一个一个的应用。
在 Java 中,启动 main 函数就是启动了一个 JVM 进程,而 main 函数所在的线程就是进程中的一个线程,也称主线程。
线程是比进程更小的执行单位,一个进程中包含多个线程,通过 JMX 来看看一个普通的 Java 程序有哪些线程:
public class MultiThread {
public static void main(String[] args) {
// 获取 Java 线程管理 MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
// 遍历线程信息,仅打印线程 ID 和线程名称信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
}
}
}
/**
执行结果:
[5] Attach Listener //添加事件
[4] Signal Dispatcher // 分发处理给 JVM 信号的线程
[3] Finalizer //调用对象 finalize 方法的线程
[2] Reference Handler //清除 reference 线程
[1] main //main 线程,程序入口
*/
线程共享所属进程的堆和方法区,独有程序计数器、虚拟机栈、本地方法栈等资源。如下图:红色为线程共享区域,蓝色为线程独占区域。
线程的创建、销毁、切换的开销比进程小,能够更快地完成任务,并且可以充分利用多核处理器的优势,所以又被称为轻量级进程。同一进程的线程间可能会进行资源的交互,而进程间是不存在的。
总的来说,进程与线程的区别主要有以下几点:
在 java 代码中通过使用 ProcessBuilder 类来创建一个名为 notepad.exe 的进程,即打开记事本应用程序。
public class CreateProcessTest {
public static void main(String[] args) {
try {
ProcessBuilder processBuilder = new ProcessBuilder("notepad.exe");
Process process = processBuilder.start();
int exitCode = process.waitFor();
System.out.println("进程已结束,退出码为:" + exitCode);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
然后调用 waitFor() 方法等待进程结束,并获取进程的退出码。
最后运行这个程序,会看到一个新的记事本窗口弹出来,这就是我们创建的进程。
当关闭记事本窗口时,程序会继续执行,输出进程的退出码。
使用 Thread 类来创建一个新的线程:在 Thread 构造函数中,传入一个 Runnable 接口的实现,该实现定义了线程的任务。
这里只是简单地输出一些信息并模拟一个耗时任务。
public class CreateThreadTest {
public static void main(String[] args) {
// 创建一个新的线程
Thread thread = new Thread(() -> {
System.out.println("线程开始执行");
try {
Thread.sleep(10000); // 模拟线程执行耗时任务
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程执行完成");
});
// 启动线程
thread.start();
System.out.println("主线程继续执行");
}
}
然后调用 start() 方法来启动线程,当线程启动后,它会执行 run() 方法中定义的任务,同时主线程会继续执行。
执行结果如下:
主线程继续执行
线程开始执行
线程执行完成
①. 继承Thread类创建线程类
定义 Thread 类的子类,并重写该类的 run() 方法,该 run() 方法的方法体就代表了线程要完成的任务。因此把 run() 方法称为执行体。
创建 Thread 子类的实例,即创建了线程对象。
调用线程对象的 start() 方法来启动该线程。
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("1111111");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
②. 通过 Runnable 接口创建线程类
定义 Runnable 接口的实现类,并重写该接口的 run() 方法,该 run() 方法的方法体同样是该线程的线程执行体。
创建 Runnable 实现类的实例,并依此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象。
调用线程对象的 start() 方法来启动该线程。
public class MyThread implements Runnable {
@Override
public void run() {
System.out.println("1111111");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
Thread threadNew = new Thread(thread);
threadNew.start();
}
}
③. 通过 Callable 和 Future 创建线程
创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
public class MyThread implements Callable {
@Override
public Object call() throws Exception {
return 1+1;
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
FutureTask futureTask = new FutureTask<>(myThread);
new Thread(futureTask).start();
Object o = null;
try {
o = futureTask.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(o.toString());
}
}
线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。
操作系统层面,线程有 READTY 和 RUNNING 两种状态,而在 JVM 层面,只有 RUNNABLE 一种状态,因为每个线程在获取到 CPU 时间片时只会运行 0.01秒左右就会被切换执行其他的线程,线程切换速度之快,就没必要区分这两种状态了。
在 Java 中,可以通过调用线程的 interrupt() 方法来中止线程。但是这并不意味着线程会立即停止执行,它只是设置了一个中断标志,线程可以通过检查这个标志来自行终止。 具体来说,当线程被中断时,可以通过以下方式来检查中断标志:
在实际应用中,如果需要中止一个线程,可以在执行任务的循环中检查中断标志,如果中断标志被设置,则退出循环,从而中止线程的执行。
notify()/notifyAll()
唤醒或者 wait(long timeout)
timeout 时间到自动唤醒。共同点 :两者都可以暂停线程的执行。
区别 :
wait(long timeout)
超时后线程会自动苏醒。wait 是想让获得对象锁的线程暂停执行,会自动释放当前线程占有的对象锁。每个对象都有对象锁,既然要释放对象锁,就得对对象进行操作。
因为 sleep() 是让当前线程暂停执行,不需要获得对象锁,所以不涉及到对象类。
CPU 通过给每个线程分配 CPU 时间片 来实现线程切换,在进行线程切换时需要保存当前线程正在执行任务的状态(也就是我们所说的上下文),以便下次切回到这个任务时,还可以继续执行该任务,即任务从保存到再加载的过程就是一次上下文切换。
多线程的缺点:多线程并发执行可能会导致内存泄漏、死锁、线程不安全等问题。
在 Java 中,多线程并发操作同一个共享变量时,就可能会发生线程安全问题。 在 Java 中保证线程安全的常用手段有以下三个:
多个线程同时被阻塞,他们都在等待某个资源被释放,由于线程无限期的阻塞,导致程序不可能正常终止,我们把这种现象称为死锁。
如下图所示,线程1拥有资源A的锁A,想要获取资源B的锁B,但是此时资源B的锁B正被线程2拥有,而线程2却想要获取线程1拥有的锁A,所以俩线程会无限等待,造成死锁。
产生死锁的四个必要条件
如何预防和避免线程死锁?
破坏死锁的产生的必要条件即可:
本文我们从大家熟知的线程和进程入手,通过对比他俩的使用场景、代码使用来方便记忆。随后对线程的创建方式以及生命周期进行了详细的讲解。然后介绍了线程使用过程中的一些方法,方便大家更好的入手。最后对多线程的安全问题和死锁问题进行了总结,希望大家做到温故而知新,要不然很容易忘记概念性的东西。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。