Java--多线程并发

线程 / 进程

进程

进程 -进程是程序的一次动态执行过程,它需要经历从代码加载,代码执行到执行完毕的一个完整的过程,这个过程也是进程本身从产生,发展到最终消亡的过程。 进程是具有一定独立功能的程序(例如QQ.exe),关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。

线程

线程 - 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位;线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

  • 一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。

多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发的一个基本单位。线程是比进程更小的执行单位,线程是进程的基础之上进行进一步的划分。所谓多线程是指一个进程在执行过程中可以产生多个更小的程序单元,这些更小的单元称为线程,这些线程可以同时存在,同时运行,一个进程可能包含多个同时执行的线程。进程与线程的区别如图所示:

并发 / 并行

并发:一个CPU以极快的速度交替执行多个任务。
(串行,逻辑上同时发生,就是表面看上去是同时发生的。)

并行:多个CPU同时执行多个任务。
(并行,物理上同时执行,就是实际上真的同时执行。)

线程种操作系统的五大状态

  1. 创建状态
  2. 就绪状态
  3. 运行状态
  4. 阻塞状态
  5. 死亡状态
    image
    原文链接:https://blog.csdn.net/phliny/article/details/107142113
public enum State {
    //1,线程刚创建
    NEW,
    //2,在JVM中正在运行的线程
    RUNNABLE,
    //3,线程阻塞
    BLOCKED,
    //4,等待状态
    WAITING,
    //5,调用sleep() join() wait()方法可能导致线程处于等待状态
    TIMED_WAITING,
    //6,线程执行完毕,已经退出
    TERMINATED;
}

JAVA线程的状态图

image

Java有三种使用线程的方法:

  • 实现 Runnable 接口;
  • 实现 Callable 接口;
  • 继承 Thread 类。

实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。

(一)继承Thread类实现线程

Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。
image

(二)实现Runnable接口

如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个Runnable接口。实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target.

image
(三)实现 Callable 接口

与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。有返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。
image

(四)基于线程池的方式

线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。创建一个固定数量大小的线程池,当线程任务数超过固定大小时,未执行的线程任务进行阻塞队列,等待线程池调度。

Executors类中newFixedThreadPool()方法。

// 创建线程池
  public static void main(String[] args) {
        // 创建一个固定数量的线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);
		
        for (int i = 0; i < 10; i++) {
            final int idx = i;
			
            executor.execute(new Runnable() { // 创建线程并添加到线程池中, 并启动运行线程
                @Override
                public void run() {
                    System.out.println("当前线程名称:" + Thread.currentThread().getName() + ", 参数idx = " + idx);
                }
            });
        }
        executor.shutdown(); // 关闭线程池
    }
创建一个固定数量为5的线程池,并创建10个线程任务添加到线程池中,线程池调度器会分配线程来处理任务

该方式创建线程池时,会使用LinkedBlockingQueue无界队列默认构造方法,该默认构造方法大小为Integer.MAX_VALUE,所以当线程任务超多时,可能会堆积大量的请求,从而导致 OOM。但一般的系统直接使用该方式即可。
解决方式:直接用new ThreadPoolExecutor的方式创建线程池,指定具体的参数信息。

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

4种线程池

  • newCachedThreadPool
    创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。
    创建一个缓存线程池,当线程数过大,可回收多余线程,如线程数不足,创建新线程。

  • newFixedThreadPool
    创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在

  • newScheduledThreadPool
    创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。建一个定时调度线程池,内置一个延迟队列,可按照设定的固定时间周期性的执行任务。

  • newSingleThreadExecutor
    Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!
    创建一个单例线程池, 只存在一个线程运行, 多的线程任务进行阻塞状态。

实现接口 VS 继承 Thread

实现接口会更好一些,因为: Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口; 类可能只要求可执行就行,继承整个 Thread 类开销过大。

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直"霸占"着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换

  • 新建状态(NEW)当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值、

  • 就绪状态(RUNNABLE):当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

  • 运行状态(RUNNING):如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。

  • 阻塞状态(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
    image

  • 线程死亡(DEAD)线程会以下面三种方式结束,结束后就是死亡状态。
    image

终止线程4种方式

  • 正常运行结束程序运行结束,线程自动结束。

  • 使用退出标志退出线程

定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false.在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值。

  • Interrupt方法结束线程
    image

  • stop方法终止线程(线程不安全)

程序中可以直接使用thread.stop()来强行终止线程,但是stop方法是很危险的,就象突然关闭计 算机电源, 而不是按正 常程序关机 一样,可能 会产生不可 预料的结果 ,不安全主 要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用stop方法来终止线程。

链接:https://www.pdai.tech/md/java/thread/java-thread-x-thread-basic.html

sleep()
Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。 sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

yield()
对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

join()
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。

wait() notify() notifyAll()
调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。 它们都属于 Object 的一部分,而不属于 Thread。 只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。 使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。

await() signal() signalAll()
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。

interrupted()
如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。 但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。

InterruptedException的中断
通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。

Executor 的中断操作
用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。

线程互斥同步
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized ,而另一个是 JDK 实现的 ReentrantLock。

  • synchronized关键字 :同步一个代码块, 同步一个方法,同步一个类,同步一个静态方法。它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。
    image
  • ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。
    image

链接:https://www.pdai.tech/md/java/thread/java-thread-x-thread-basic.html

比较 synchronized VS ReentrantLock
  1. 锁的实现 synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
  2. 性能 新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
  3. 等待可中断 当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。 ReentrantLock 可中断,而 synchronized 不行。
  4. 公平锁 公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。 synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
  5. 锁绑定多个条件 一个 ReentrantLock 可以同时绑定多个 Condition 对象。

¶ 使用选择 除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

总结:

image

wait() 和 sleep() 的区别

  • wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
  • wait() 线程会放弃对象锁,进入等待此对象的等待锁定池,sleep() 线程不会释放对象锁。

start与run区别

image

JAVA后台线程

image

posted on   白嫖老郭  阅读(1454)  评论(0编辑  收藏  举报

编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5
点击右上角即可分享
微信分享提示