Java并发编程之Java线程

article/2025/9/16 22:24:09

文章目录

  • 前言
  • 01、线程简介
  • 02、线程池
  • 03、线程间通信
  • 总结


前言

记录一下Java并发编程的知识点。有部分内容是借鉴《Java并发编程的艺术》这本书的。本次先介绍一下线程。


01、线程简介

进程和线程的区别

  • 进程:当一个程序被运行,即把程序的代码从磁盘加载到内存,就是开启了一个进程。进程可以理解为程序的一个实例(例如打开网易云、浏览器)。

  • 线程:一个进程里面可以有多个线程,每个线程执行不同的任务(比如360安全卫士可以一边清理垃圾,一边扫描病毒),线程是现代操作系统调度的最小单位。

线程拥有各自的程序计数器,栈,并且可以共享堆里面的共享内存变量;

线程的状态

这里是从Java API层面上来讲的,先看一下Thread里面的枚举类

public enum State {NEW,RUNNABLE,BLOCKED, WAITING,   TIMED_WAITING,TERMINATED;
}

可以看出这里分成了六种状态:

  • NEW:线程刚被创建,还没有调用start方法
  • RUNNABLE:运行状态,涵盖了操作系统层面的就绪状态、运行状态、阻塞状态
  • BLOCKED:阻塞状态,表示线程阻塞于锁
  • WAITING:等待状态,进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)
  • TIMED_WAITING:超时等待状态,该状态不同于WAITING,它可以在指定的时间后自行返回
  • TERMINATED:终止状态,表示当前线程已经执行完毕

下图来自《Java并发编程的艺术》

在这里插入图片描述

线程创建的方式

1、继承Thread类

Thread t=new Thread(){@Overridepublic void run() {System.out.println("创建了一个线程");}
};
//启动线程
t.start();

2、实现Runnable接口

Runnable t2= new Runnable() {@Overridepublic void run(){// 要执行的任务}
};
// 创建线程对象
Thread t = new Thread(t2,"t2");
// 启动线程
t.start(); 

3、实现Callable接口(需要借助FutureTask来接收返回结果)

FutureTask<Integer> t3 = new FutureTask<>(new Callable<Integer>() {@Overridepublic Integer call() throws Exception {return 404;}
});new Thread(t3,"t").start();
//主线程阻塞,同步等待t3执行完毕返回结果
Integer result = t3.get();

4、使用线程池(第二部分详细介绍)

常见方法

方法名功能说明注意
start()启动一个线程,在新的线程运行run方法中的代码如果start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException
run()新线程启动后会调用的方法
join()等待目标线程运行结束
getPriority()获取线程优先级
setPriority()设置线程优先级java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
getState()获取线程状态
interrupt()打断线程如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记 ;如果打断的正在运行的线程,则会设置打断标记 ;park 的线程被打断,也会设置打断标记
isInterrupted()判断是否被打断不会清除打断标记
interrupted()判断当前线程是否被打断会清除打断标记
isAlive()线程是否存活
sleep(long n)让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程
yield()提示线程调度器让出当前线程对CPU的使用

sleepyield的区别

sleep

  • 调用 sleep 会让当前线程从 RUNNABLE 进入 TIMED_WAITING 状态
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,而且会把中断标识位清除

yield

  • 调用 yield 会让当前线程让出CPU的使用权,注意当前线程会继续参与下一轮CPU使用权的争抢中,所以调用改方法后有可能还是当前线程继续运行

让线程“优雅”退出(线程中断)

中断可以理解为线程的一个标识位属性,它标识一个运行中的线程是否被其他线程进行了中断操作。其他线程可以通过调用该线程的interrupt方法对其进行中断操作。

线程通过检查自身是否被中断来进行响应,调用isInterrupted方法来进行判断是否被中断。

利用线程中断我们可以实现线程的“优雅”退出。代码如下

class TPTInterrupt {private Thread thread;public void start(){thread = new Thread(() -> {while(true) {Thread current = Thread.currentThread();//当我们调用stop对线程进行中断标记后这里就可以感知中断而结束if(current.isInterrupted()) {log.debug("收拾东西走人");break;}try {Thread.sleep(1000);log.debug("摸鱼ing......");} catch (InterruptedException e) {//如果是在睡眠中被打断,会抛出异常,那么我们需要自己进行中断标记current.interrupt();}}},"小明");thread.start();}public void stop() {thread.interrupt();}
}

在主方法调用

TPTInterrupt t = new TPTInterrupt();
t.start();
Thread.sleep(3500);
System.out.println("你被辞退了");
t.stop();

运行结果

12:56:11.123 [小明] DEBUG xyx.product.web.TPTInterrupt - 摸鱼ing......
12:56:12.142 [小明] DEBUG xyx.product.web.TPTInterrupt - 摸鱼ing......
12:56:13.147 [小明] DEBUG xyx.product.web.TPTInterrupt - 摸鱼ing......
12:56:13.522 [main] DEBUG xyx.product.web.Test - 你被辞退了
12:56:13.522 [小明] DEBUG xyx.product.web.TPTInterrupt - 收拾东西走人

我们用一个图来看看执行流程

02、线程池

核心参数

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)

主要有7大核心参数:

  • corePoolSize 核心线程数目 (最多保留的线程数)
  • maximumPoolSize 最大线程数目
  • keepAliveTime 生存时间
  • unit 时间单位
  • workQueue 阻塞队列
  • threadFactory 线程工厂
  • handler 拒绝策略

执行流程

核心线程数大小设置

  • CPU 密集型任务(N+1):这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止某些原因导致的任务暂停(线程阻塞,如io操作,等待锁,线程sleep)而带来的影响。一旦某个线程被阻塞,释放了cpu资源,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  • I/O 密集型任务(2N):系统会用大部分的时间来处理 I/O 操作,而线程等待 I/O 操作会被阻塞,释放 cpu资源,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法:最佳线程数 = CPU核心数 * (1/CPU利用率) = CPU核心数 * (1 + (I/O耗时/CPU耗时)),一般可设置为2N。

线程池的创建

使用ThreadPoolExecutor的构造方法创建线程池

ThreadPoolExecutor threadsPool = new ThreadPoolExecutor(9,20, 60,TimeUnit.SECONDS, new LinkedBlockingDeque<>(100),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

创建完线程池后有两种方式提交任务,分别是executesubmit方法

execute方法用于提交不需要返回值的任务

public void execute(Runnable command)

submit方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过future可以获取返回值,下面是它的三个接口

Future<?> submit(Runnable task);
<T> Future<T> submit(Runnable task, T result);
<T> Future<T> submit(Callable<T> task);

03、线程间通信

线程开始运行,拥有自己的栈空间,如果它们仅仅是孤立地运行,那么价值很少,如果多个线程之间可以相互配合完成工作,那么会带来更大的价值。

volatilesynchronized关键字

Java多线程访问共享变量时,每个线程都会有这个变量的拷贝副本,但是在执行过程中它可能看到的变量值不是最新的。

关键字volatile可以用来修饰共享变量,保证它对所有线程的“可见性”,即每个线程都可以读到最新值,详细原理我们下篇文章再介绍。

而关键字synchronized可以修饰方法或者同步代码块来确保同一个时刻,只能有一个线程处于方法或者同步块中,从而实现线程对共享变量访问的可见性和排他性

等待/通知机制

这个可以有3种实现方式,我们分别介绍一下:

1、Object类的wait和notify方法

我们先看一下下面表格了解一下方法
在这里插入图片描述
再加上下面这个例子理解一下

final static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {new Thread(() -> {synchronized (obj) {log.debug("执行....");try {obj.wait(); // 让线程在obj上一直等待下去} catch (InterruptedException e) {e.printStackTrace();}log.debug("其它代码....");}}).start();new Thread(() -> {synchronized (obj) {log.debug("执行....");try {obj.wait(); // 让线程在obj上一直等待下去} catch (InterruptedException e) {e.printStackTrace();}log.debug("其它代码....");}}).start();// 主线程两秒后执行sleep(2);log.debug("唤醒 obj 上其它线程");synchronized (obj) {obj.notify(); // 唤醒obj上一个线程sleep(1000);// obj.notifyAll(); // 唤醒obj上所有等待线程}
}

执行结果

09:36:09.796 [Thread-0] DEBUG xyx.product.web.Test - 执行....
09:36:09.799 [Thread-1] DEBUG xyx.product.web.Test - 执行....
09:36:10.797 [main] DEBUG xyx.product.web.Test - 唤醒 obj 上其它线程
09:36:11.801 [Thread-0] DEBUG xyx.product.web.Test - 其它代码....

注意事项

  • 使用上面的方法时需要获取到对象的锁
  • 使用wait方法后,线程状态由RUNNING变为WAITING,同时会释放对象锁
  • 调用notify或notifyAll方法后,需要等待notify或notifyAll的线程释放锁以后,等待线程才有机会从wait返回。
  • 从wait返回的前提是获取到了锁
  • 还有就是wait和notify的顺序不可以颠倒

2、Condition接口的await和signal方法

当我们调用Condition定义的方法时,需要获取到Condition对象关联的锁。而Condition对象是由Lock对象创建的,换句话说,Condition是依赖Lock对象的。

我们看一个例子(使用上和上面的第一种方法其实差不多)

class ConditionUseCase {ReentrantLock lock = new ReentrantLock();Condition condition = lock.newCondition();public void conditionWait() throws InterruptedException {lock.lock();try {condition.wait();} finally {lock.unlock();}}public void conditionSignal() {lock.lock();try {condition.signal();} finally {lock.unlock();}}
}

我们再来看看第二种方法对于第一种方法的提升,也可以理解为synchronized和ReentrantLock的一点区别:

  • 第一种方式在等待状态中不响应中断,第二种方式支持
  • 第一种方式不可以指定等待时间,第二种方式可以
  • 第一种方式只能随机唤醒或唤醒全部,第二种方式可以利用多个Condition实现指定目标唤醒

3、LockSupport工具类的park和unpark方法

LockSupport里面的方法提供了最基本的线程阻塞和唤醒功能。它的最大优势就是不需要先获取锁就可以直接使用,而且park和unpark方法无需顺序执行

我们直接看代码

Thread a = new Thread(() -> {log.debug("执行....");LockSupport.park();log.debug("其它代码....");});a.start();Thread b = new Thread(() -> {log.debug("执行....");//给指定线程放行LockSupport.unpark(a);log.debug("其它代码....");});b.start();

Thread.join() 方法

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止后才继续执行。我们还可以调用join(long millis)和join(long millis,int nacos)两个超时特性的方法。

我们可以看看下面的例子

Thread t1 = new Thread(() -> {try {sleep(1);} catch (InterruptedException e) {e.printStackTrace();}r1 = 10;
});
Thread t2 = new Thread(() -> {try {sleep(2);} catch (InterruptedException e) {e.printStackTrace();}r2 = 10;
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);

运行结果

10:47:53.667 [main] DEBUG xyx.product.web.Test - r1: 10 r2: 10 cost: 3

总结

以上就是本篇文章的所有内容了,主要是对Java并发编程知识中的线程做一个介绍,后面再详细说一说底层的一些实现原理。


http://chatgpt.dhexx.cn/article/4LRbydnF.shtml

相关文章

Java并发编程的艺术

1、并发编程的挑战 1、上下文切换 CPU通过给每个线程分配CPU时间片来实现多线程机制。时间片是CPU分配给各个线程的时间&#xff0c;这个时间非常短&#xff0c;一般是几十毫秒。 CPU通过时间片分配算法来循环执行任务&#xff0c;当前任务执行一个时间片后会切换到下一个任务…

Java并发编程简介

并发编程简介 1. 什么是并发编程 所谓并发编程是指在一台处理器上“同时”处理多个任务。并发是在在同一实体上的多个事件。多个事件在同一时间间隔发生。 并发编程 ①从程序设计的角度来讲&#xff0c;是希望通过某些机制让计算机可以在一个时间段内&#xff0c;执行多个任务…

【java】Java并发编程系列-基础知识(非常详细哦)

文章目录 一、Java并发编程基础1.1 并发编程基本概念1.1.1原⼦性1.1.2 可⻅性1.1.3 有序性 二、内存模型三、重排序四、内存屏障五、总结 一、Java并发编程基础 主要讲解Java的并发编程的基础知识&#xff0c;包括原⼦性、可⻅性、有序性&#xff0c;以及内存模型JMM&#xff…

理解Java并发编程

计算机基础 要想理解Java多线程&#xff0c;一定离不开计算机组成原理和操作系统&#xff0c;因为&#xff0c;java的多线程是JVM虚拟机调用操作系统的线程来实现的 /*Thread.start() 方法中调用了原生的start0()方法 */ public synchronized void start() {if (threadStatus…

【并发编程】JAVA并发编程面试题合集

1.在Java中守护线程和本地线程的区别&#xff1f; Java中的线程分为两种&#xff1a;守护线程&#xff08;Daemon&#xff09;和用户线程&#xff08;User&#xff09;任何线程都可以设置为守护线程和用户线程&#xff0c;通过方法Thread.setDaemon(boolean)&#xff1b;true表…

Java并发编程概述

在学习并发编程之前&#xff0c;我们需要稍微回顾以下线程相关知识&#xff1a; 线程基本概念 程序&#xff1a;静态的代码&#xff0c;存储在硬盘中 进程&#xff1a;运行中的程序&#xff0c;被加载在内存中&#xff0c;是操作系统分配内存的基本单位 线程&#xff1a;是cpu执…

java并发编程(并发编程的三个问题)

什么是并发编程? 首先我们要知道什么是并发? 什么是并行? 并行: 多件事情在同一时刻同时发生 并发: 在同一时间内,多个事情交替执行 并发编程: 比如抢票,秒杀等在同一场景下,有大量的请求访问同一资源, 会出现一些安全性的问题,所以要通过编程来控制多个线程依次访问资源,称…

java并发编程(荣耀典藏版)

大家好 我是月夜枫&#xff0c;聊一聊java中的并发编程&#xff0c;面试工作中也许都会用到&#xff0c;参考了很大博主的博客&#xff0c;整理了很久的文章&#xff0c;虽然还没有全部整理完&#xff0c;后续慢慢更新吧。 并发编程 一、线程的基础概念 一、基础概念 1.1 进…

Java并发编程基础(一篇入门)

1 并发编程简介 1.1 什么是并发编程 所谓并发编程是指在一台处理器上 “同时” 处理多个任务。并发是在同一实体上的多个事件。多个事件在同一时间间隔发生。 并发编程&#xff0c;从程序设计的角度来说&#xff0c;是希望通过某些机制让计算机可以在一个时间段内&#xff0…

关于Java并发编程的总结和思考

编写优质的并发代码是一件难度极高的事情。Java语言从第一版本开始内置了对多线程的支持&#xff0c;这一点在当年是非常了不起的&#xff0c;但是当我们对并发编程有了更深刻的认识和更多的实践后&#xff0c;实现并发编程就有了更多的方案和更好的选择。本文是对并发编程的一…

Java 并发编程

目录 回顾线程 并发编程 并发编程 Java 内存模型(JMM) 编程核心问题--可见性,原子性,有序性 可见性 有序性 原子性 valatile 关键字 CAS&#xff08;Compare-And-Swap&#xff0c;比较并交换&#xff09; 原子类 java中的锁 乐观锁/悲观锁 可重用锁&#xff08;…

JAVA并发编程

并发编程 1.进程与线程 进程 程序由指令和数据组成&#xff0c;但这些指令要运行&#xff0c;数据要读写&#xff0c;就必须将指令加载至 CPU&#xff0c;数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。当一个…

阿里面试失败后,一气之下我图解了Java中18把锁

号外号外&#xff01;《死磕 Java 并发编程》系列连载中&#xff0c;大家可以关注一波&#xff1a; 「死磕 Java 并发编程04」说说Java Atomic 原子类的实现原理 「死磕 Java 并发编程03」阿里二面&#xff0c;面试官&#xff1a;说说 Java CAS 原理&#xff1f; 「死磕 Jav…

Java 是否应该使用通配符导入( wildcard imports)

这个问题应该是所有使用过 Java 第一课的人都会告诉你不要使用通配符导入。 主要问题 主要的问题是它使你的本地命名空间变得混乱。 用最简单的说法就是 Date 这个对象&#xff0c;你可能在 java.sql.Date 和 java.util.Date 都会有这个对象。 如果你使用通配符导入的话&…

Makefile中wildcard函数的应用理解

文章目录 前言 1 "*"通配符使用场景 2 "*"通配符实例 总结 前言 如果我们想定义一系列比较类似的文件&#xff0c;我们很自然地就想起使用通配符。make 支持三种通配符&#xff1a;"*"&#xff0c;"?" 和 "[...]"。这…

DNS Wildcard(DNS泛域名)

在DNS中&#xff0c;泛域名&#xff08;wildcard Resource Record&#xff09;可以被认为是一种合成RR的机制&#xff0c;借助于它&#xff0c;DNS服务器可以响应本来不存在的域名的请求&#xff0c;它的设计初衷是用来把所有邮件都转发到一个邮件系统&#xff08;当然&#xf…

Es 模糊查询 match,wildcard

Es 模糊查询的方式 要求&#xff1a; Es查询&#xff1a; 查询工单信息&#xff0c; 输入 “测试”&#xff0c;查出 form_name 为字段中有查询出含有符合内容的数据 match&#xff1a;分词模糊查询&#xff1a; 比如“Everything will be OK, All is well”&#xff0c;会被…

wildcard

[ruskyrhel7 test]$ lstest1 test123 test2 test317 test33 test335 test336 test44 testtest[ruskyrhel7 test]$ ls test?3test33[ruskyrhel7 test]$ ls test??3test123[ruskyrhel7 test]$ ls test*test1 test123 test2 test317 test33 test335 test336 tes…

Makefile中wildcard使用方法

Makefile中wildcard函数使用方法 在Makefile规则中&#xff0c;通配符会被自动展开。但在变量的定义和函数引用时&#xff0c;通配符将失效。这种情况下如果需要通配符有效&#xff0c;就需要使用函数“wildcard”&#xff0c;它的用法是&#xff1a;$(wildcard PATTERN…) 。…

通配符(WildCard)的使用

一、关于WildCard&#xff1a;一个web应用&#xff0c;有成千上万个action声明&#xff0c;可以利用struts2提供的映射机制把多个彼此相似的映射关系简化成一个映射关系&#xff0c;即通配符。 1.新建类 ActionWildCard&#xff0c;验证通配符的方法 2.1添加Student需要实践的两…