并发编程 (线程)
我们都知道现在硬件水平越来越强,执行速度也越来越快。为充分利用CPU资源,我们可以通过多线程来执行分片任务提升整体的执行性能。
如何创建线程#
在Java中通过new Thread().start()方法来开启一个线程,实际是调用系统层的native方法
private native void start0();
Java使用线程常用方式有四种:
1、继承
Thread
类,Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法2、实现
Runnable
接口,如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个Runnable接口3、实现
Callable
接口,重写call()方法,可以返回一个 Future类型的返回值。4、通过线程池来使用线程,将任务交给线程池执行
public class CreateThreadDemo {
public static void main(String[] args){
// 单继承
new ThreadDemo().start();
// 实现runnable
new Thread(new RunnableDemo()).start();
// futureTask获取返回值
FutureTask futureTask = new FutureTask(new CallableDemo());
new Thread(futureTask).start();
//get()方法获取返回值
System.out.println(futureTask.get());
}
// 缺点:单继承
public static class ThreadDemo extends Thread {
@Override
public void run() {
System.out.println("ThreadDemo:" + Thread.currentThread());
}
}
// 不用担心单继承,但没有返回值
public static class RunnableDemo implements Runnable {
@Override
public void run() {
System.out.println("RunnableDemo:" + Thread.currentThread());
}
}
//
public static class CallableDemo implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("CallableDemo:" + Thread.currentThread());
return "return CallableDemo:" + Thread.currentThread();
}
}
}
本质上都是调用Thread类的start0()方法来启用
线程的生命周期#
1、新建状态(NEW) 当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配 内存,并初始化其成员变量的值
2、就绪状态(RUNNABLE):当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和 程序计数器,等待调度运行。
3、运行状态(RUNNING):如果处于就绪状态的线程获得了CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。
4、阻塞状态(BLOCKED):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。 直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状 态。
阻塞的情况分三种: 等待阻塞(o.wait->等待对列): 运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue) 中。 同步阻塞(lock->锁池) 运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线 程放入锁池(lock pool)中。 其他阻塞(sleep/join) 运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时, JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入可运行(runnable)状态。
5、线程死亡(DEAD) 线程会以下面三种方式结束,结束后就是死亡状态。
正常结束 1. run()或 call()方法执行完成,线程正常结束。 异常结束 2. 线程抛出一个未捕获的 Exception 或 Error。 调用 stop 3. 直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用
线程的6种状态
1、初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2、运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3、阻塞(BLOCKED):表示线程阻塞于锁。
4、等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5、超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6、终止(TERMINATED):表示该线程已经执行完毕。
如何关闭线程#
1、设置退出标志,使线程正常退出
public class ThreadSafe extends Thread {
public volatile boolean exit = false;
public void run() {
while (!exit){
//do something
}
}
}
定义了一个退出标志,在定义exit时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。
2、使用interrupt()方法中断线程,在线程中使用isInterrupted()来判断是否存在中断标志
-
线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时, 会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。 阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让 我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的,一定要先捕获 InterruptedException 异常之后通过break来跳出循环,才能正常结束run方法。
-
线程未处于阻塞状态:使用isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
try{
Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
}catch(InterruptedException e){
e.printStackTrace();
break;//捕获到异常之后,执行 break 跳出循环
}
}
}
}
3、使用stop方法强行终止线程
程序中可以直接使用 thread.stop()来强行终止线程,安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误
sleep()与wait()区别#
1、对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的
2、sleep()方法是Thread类的静态方法,可以直接通过Thread.sleep()调用。它使当前线程暂停执行一段时间,让出CPU给其他线程,但是不会释放对象的锁。
3、在调用sleep()方法的过程中,线程不会释放对象锁。而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
4、sleep()方法可以在任何地方调用,而wait()方法只能在同步方法或同步代码块中调用。sleep()是阻塞当前线程,wait()是放弃对象锁的竞争,所以必须在同步代码块中,不然会造成线程安全问题
wait()为什么需要在同步代码块#
我们都知道wait()和notify()配合使用可以实现线程间的通信控制,wait()会阻塞当前线程并放弃锁,notify()会唤醒线程。相当于是对监视器锁的count值进行加减的操作。为了保证原子性,所以必须要在同步代码块中调用。否则会出现上下文切换,导致对锁失控,出现Lost Wake-Up Problem
问题
wait()和await()的区别#
wait()是Object的方法(因为在java中每个对象有锁),必须先持有锁,否则会报监视器异常。通常配合notify()使用。用于线程之间的通信作用
await()是并发框架AQS中的Condition接口中,跟wait()一样,都必须在线程获取到锁后使用。但比wait()更加灵活
优势:
1、可以指定等待的条件和等待的超时时间,可中断
2、可以初始化多个Condition协同工作
class Example {
final Lock lock = new ReentrantLock();
// condition 依赖于 lock 来产生
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
// 生产
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 队列已满,等待,直到 not full 才能继续生产
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal(); // 生产成功,队列已经 not empty 了,发个通知出去
} finally {
lock.unlock();
}
}
// 消费
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 队列为空,等待,直到队列 not empty,才能继续消费
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal(); // 被我消费掉一个,队列 not full 了,发个通知出去
return x;
} finally {
lock.unlock();
}
}
}
守护线程#
1、守护线程--也称“服务线程”,他是后台线程,它有一个特性,即为用户线程提供公共服务,在没有用户线程可服务时会自动离开。
2、守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
3、通过 setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程 的方式是在 线程对象创建 之前 用线程对象的 setDaemon 方法。
4、在Daemon线程中产生的新线程也是Daemon的。
5、生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周 期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则JVM不会退出
进程、线程、协程的区别#
1、进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
2、线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相⽐进程不够稳定容易丢失数据。
3、协程是一种用户态的轻量级线程,协程的调度完全由用户控制,协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了