首页 > 其他分享 >多线程

多线程

时间:2024-08-12 20:16:08浏览次数:14  
标签:Java 变量 CAS ThreadLocal 线程 多线程

多线程

1.进程与线程

1.1.什么是进程

进程就是正在运行的程序,它是系统进行资源分配和调度的基本单位,各个进程之间相互独立,系统给每个进程分配不同的地址空间和资源

Win 操作系统任务管理器查看应用程序运行的进程

1.2.什么是线程

线程就是程序(进程)执行的任务(分为单线程和多线程)

1.3.进程与线程的区别

  1. 地址空间:进程之间是独立的地址空间,但同一进程的线程共享本进程的地址空间
  2. 资源占用:同一进程内的线程共享本进程的资源,如内存、I/O、CPU 等,但是进程之间的资源是独立的
  3. 健壮性:一个进程崩溃后不会对其他进程产生影响;一个线程崩溃则整个进程都死掉,所以多进程要比多线程更健壮
  4. 执行过程:进程可以独立执行,线程不能独立执行,线程必须依存于进程
  5. 并发与资源消耗:进程和线程都可以并发(同时)执行,但进程创建和切换消耗资源大,线程创建和切换消耗资源小

2.创建线程

方式一:继承 Thread 类,并重写 run( ) 方法
public class MyThread extends Thread {   
   public void run() {  
         for ( int i = 0; i < 10; i++ )  {  
             System.out.println(“子线程");  
         }  
   }  
   public static void main(String[] args) {  
         MyThread myThread = new MyThread(); 
         myThread.start();  
   } 
}
方式二:实现 Runnable 接口,并实现 run( ) 方法
public class MyThread implements Runnable {   
   public void run() {  
         for ( int i = 0; i < 10; i++ )  {  
             System.out.println(“子线程");  
         }  
   }  
   public static void main(String[] args) {  
         Thread myThread = new Thread(new MyThread); 
         myThread.start();  
   } 
}

3.线程的状态

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

  1. 新建状态:当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值
  2. 就绪状态:当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行
  3. 运行状态:如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态
  4. 阻塞状态:当处于运行状态的线程失去所占用资源之后,便进入阻塞状态
  5. 死亡状态:当线程的 run() 方法执行完成,线程正常结束或抛出一个未捕获的异常,也可以直接调用 stop() 方法结束线程(容易导致死锁,不推荐使用)

3.1.线程暂停执行条件

  • 线程优先级比较低,不能获得 CPU 时间片
  • 使用 sleep( ) 方法使线程睡眠
  • 通过调用 wait( ) 方法,使线程处于等待状态
  • 通过调用 yield( ) 方法,线程主动出让 CPU 控制权
  • 线程由于等待一个I/O事件处于阻塞状态

3.2.线程优先级

Java 中线程优先级是在 Thread 类中定义的常量

  • NORM_PRIORITY : 值为 5

  • MAX_PRIORITY : 值为 10

  • MIN_PRIORITY : 值为 1

缺省优先级为 NORM_PRIORITY

修改和查看线程优先级方法

  • final void setPriority(int newPriority) //修改当前线程的优先级

  • final int getPriority()//查看当前线程的优先级

4.多线程

4.1.多线程并发问题

线程 A 和线程 B 同时操作(读写)同一进程下的共享资源,这将导致数据不一致的问题,被称为多线程并发问题。

4.2.线程同步

多线程共享数据时,可能会发生数据不一致的情况,而线程同步就是为了解决多线程并发问题,它可以确保在任何时间点一个共享的资源只被一个线程使用

4.3.实现线程同步的三种方式

  • 使用同步代码块
  • 使用同步方法
  • 使用互斥锁 (更灵活的代码控制)

4.4.线程死锁

线程 A 和线程 B 都想访问对方的资源,但都不愿意让对方先访问,这样谁也无法继续执行下去,这就是线程死锁(线程死锁很少发生,但一旦发生就很难调试)

4.5.线程池

如果并发的线程数量很多,并且线程执行一个时间很短的任务就结束了,这样就会频繁的创建和销毁线程,导致大大降低系统的运行效率

4.5.1线程池工作原理

  • 程序启动时向线程池中提前创建一批线程对象
  • 当需要执行任务时,从线程池中获取一个空闲的线程对象
  • 任务执行完毕后,不销毁线程对象,而是将其返还给线程池,并再次将状态设置为空闲

4.5.2.线程池优缺点

  • 减少频繁创建和销毁线程对象的时间消耗,提高了程序运行的性能(优点)
  • 线程池中空闲的线程对象,会占用系统更多的内存存储空间(缺点)

线程池是一种以时间换空间的性能优化策略

4.5.3.Java四种内置线程池

Java 语言提供了一系列线程池的实现,以解决实际开发中各种对线程池的需求

  • newCachedThreadPool
  • newFixedThreadPool
  • newScheduledThreadPool
  • newSingleThreadExecutor

4.5.4.Java 两个基础线程池

如果内置四个线程池实现仍无法满足需求,则 Java 语言还提供了两个基础线程池

  • ThreadPoolExecutor 类
  • ScheduledThreadPoolExecutor 类

这两个基础线程池用于用户创建自定义线程池,以获得更大的灵活度,同时开发难度也更大

5.线程间通信

5.1.wait-notify 机制

线程同步能够解决多线程并发问题,但它没有实现线程间的通信

Java 提供了一个精心设计的线程间通信机制,使用wait()、notify() 和 notifyAll() 方法,这些方法是作为 Object 类中的 final 方法实现的。这三个方法仅在 synchronized 方法中才能被调用

  • wait() 方法:方法告知被调用的线程退出监视器并进入等待状态,直到其他线程进入相同的监视器并调用 notify( ) 方法
  • notify( ) 方法:通知同一对象上第一个调用 wait( )线程
  • notifyAll() 方法:通知调用 wait() 的所有线程,具有最高优先级的线程将先运行

6.线程定时器

6.1.Time 类

Timer是一个普通的类,其中有几个重要的方法

  • schedule() 方法:启动定时器一个定时任务
  • cancel()方法:终止定时器所有定时任务

启动一个定时任务就创建一个线程,线程会一直执行下去,直到调用终止定时任务

6.2.TimerTask 类

TimerTask 是一个抽象类,需要实现该类

  • run()方法:定时任务逻辑
//创建一个定时任务
Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        // 任务执行代码
    }
}, 5000,1000); //延时 5s 每间隔1s 执行一次

7.CAS

7.1.引言

在 JDK 5 之前 Java 语言是靠 synchronized 关键字保证线程同步的,这会导致有锁。

锁机制存在以下问题:

  • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
  • 一个线程持有锁会导致其它所有需要此锁的线程挂起。
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

7.1.1.悲观锁

synchronized 是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。

7.1.2.乐观锁

CAS 操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

7.2.什么是 CAS

CAS,compare and swap 的缩写,中文翻译成比较并交换。

我们都知道,在 Java 语言之前,并发就已经广泛存在并在服务器领域得到了大量的应用。所以硬件厂商老早就在芯片中加入了大量直至并发操作的原语,从而在硬件层面提升效率。在 intel 的 CPU 中,使用 cmpxchg 指令。

在 Java 发展初期,Java 语言是不能够利用硬件提供的这些便利来提升系统的性能的。而随着 Java 不断的发展,Java 本地方法 (JNI) 的出现,使得 Java 程序越过 JVM 直接调用本地方法提供了一种便捷的方式,因而 Java 在并发的手段上也多了起来。而在 Doug Lea 提供的 cucurenct 包中,CAS 理论是它实现整个 Java 包的基石。

7.3.CAS 基本原理

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法 可以对该操作重新计算。

7.4.CAS 的优势

利用 CPU 的 CAS 指令,同时借助 JNI 来完成 Java 的非阻塞算法。其它原子操作都是利用类似的特性完成的。而整个J.U.C 都是建立在 CAS 之上的,因此对于 synchronized 阻塞算法,J.U.C 在性能上有了很大的提升。

7.5.CAS 存在的问题

CAS 虽然很高效的解决原子操作,但是 CAS 仍然存在三大问题。

  • ABA问题。因为 CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

  • 从 Java1.5 开始 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解决ABA问题。这个类的compareAndSet 方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

    关于ABA问题参考文档: http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html

  • 循环时间长开销大。自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

  • 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作 ij。从 Java1.5 开始 JDK 提供了AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

8.ThreadLocal

8.1.ThreadLocal 是什么

早在 JDK1.2 的版本中就提供了java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

ThreadLocal,顾名思义,它不是一个线程,而是线程的一个本地化对象。

当工作于线程中的对象使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量,这也是类名中"Local“所要表达的意思。

线程局部变量并不是 Java 的新发明,很多语言在语法层面就提供线程局部变量。在 Java 中没有提供语言级支持,而以一种变通的方法,通过 ThreadLocal 的类提供支持。

所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,这也是为什么线程局部变量没有在 Java 开发者中得到很好的普及的原因。

8.2.ThreadLocal 的接口方法

8.2.1.public void set(Object value)

设置当前线程的线程局部变量的值

8.2.2.public Object get()

返回当前线程所对应的线程局部变量

8.2.3.public void remove()

将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是 JDK5.0 新增的方法。

需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

8.2.4.protected Object initialValue()

返回该线程局部变量的初始值

该方法是一个 protected 的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第一次调用 get() 或 set(object)时才执行,并且仅执行一次。

ThreadLocal 中的默认实现直接返回一个 null

8.2.5.注意事项

在 JDK5.0 中,ThreadLocal 已经支持泛型,该类的类名已经变为 ThreadLocal。API 方法也相应进行了调整。

8.3.ThreadLocal 实现原理

ThreadLocal 是如何做到为每一个线程维护一份独立的变量副本,实现的思路很简单:在 ThreadLocal 类中维护一个 Map 结构,用于存储每一个线程的变量副本,Map 中元素的 key 为线程对象,而 value 对应线程的变量副本

8.4.ThreadLocal 与线程同步比较

ThreadLocal 和线程同步都是为了解决多线程中相同变量的访问冲突问题,那么ThreadLocal 与线程同步机制相比有如下特点:

  • 在线程同步中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序清楚什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

  • ThreadLocal 则从另一个角度来解决多线程的并发访问。ThreadLocal 为每个线程提供了一个独立的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal 提供了线程安全的对象封装,在编写多线程代码时,可以把不安全的变量封装进 ThreadLocal。

8.5.总结

对于多线程资源共享的问题,线程同步机制采用了”以时间换空间“的方式:即访问串行化,对象共享化;ThreadLocal 采用了”以空间换时间“的方式:即访问并行化,对象独享化

线程同步仅提供一份变量,让不同的线程排队访问,ThreadLocal 为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

标签:Java,变量,CAS,ThreadLocal,线程,多线程
From: https://www.cnblogs.com/tubby233/p/18355676

相关文章

  • 关于异步编程和多线程的高级.NET Core面试题
    以下是一些关于异步编程和多线程的高级.NETCore面试题。这些问题涵盖了从基础概念到复杂应用的各个方面,可以帮助评估候选人在异步编程和多线程开发方面的能力。1.异步编程基础在.NETCore中,异步编程的基本原理是什么?async和await关键字的作用是什么?如何在.NETCore中使用......
  • 线程与多线程
    1.线程1.线程状态线程状态分为5种newrunable->分为ready和running阻塞等待->分为waiting和time_waiting销毁1.2线程数量配置IO密集型=CPU*2cpu密集型=CPU+12.线程池2.1线程池核心参数线程池一共7个核心参数,分别是核心线程数、最大线程数......
  • 多线程复习总结
     1基本概念1什么是进程什么是线程进程:是程序执行一次的过程,他是动态的概念,是资源分配的基本单位。一个应用程序(1个进程是一个软件)。线程:一个进程可以有多个线程,线程是cpu调度的单位,一个进程中的执行场景/执行单元。对于java程序来说,当在DOS命令窗口中输入:javaHelloWorld回......
  • 【Redis进阶】Redis单线程模型和多线程模型
    目录单线程为什么Redis是单线程处文件事件理器的结构文件处理器的工作流程总结文件事件处理器连接应答处理器命令请求处理器命令回复处理器多线程为什么引入多线程多线程架构多线程执行流程关于Redis的问题Redis为什么采用单线程模型Redis为什么要引入多线程呢......
  • Linux C++ 多线程编程
    LinuxC++多线程编程参考教程:c++:互斥锁/多线程的创建和unique_lock<mutex>的使用_mutex头文件vc++-CSDN博客1.编写unique_mutex1.1创建文件夹通过终端创建一个名为unique_mutex的文件夹以保存我们的VSCode项目,在/unique_mutex目录下打开vscode。rosnoetic@rosnoetic-Virt......
  • Python和多线程(multi-threading)
    在Python中,实现并行处理的方法有几种,但由于Python的全局解释器锁(GIL,GlobalInterpreterLock)的存在,传统意义上的多线程(使用threading模块)并不总能有效利用多核CPU来实现真正的并行计算。GIL确保任何时候只有一个线程在执行Python字节码。不过,仍然有几种方法可以绕过这个限制,......
  • Java - 多线程
    三种实现方式常用成员方法1.线程name默认“Thread-”+"序号"2.可以通过重写构造方法在创建时给线程命名线程的生命周期与状态同步代码块格式synchronized(锁对象){操作共享数据的代码}1.锁对象随机,但只要是有static修饰的唯一对象,一般写本类class文件,如MyTh......
  • C#多线程并发编程深度探索:解锁async、await、Task与lock等关键字的奥秘
    一、多线程介绍1.什么是多线程多线程是指在一个应用程序中同时执行多个线程的能力。每个线程都是独立运行的,拥有自己的执行路径和资源。多线程编程能够充分利用多核处理器的计算能力,提高应用程序的性能和响应性,特别是在处理耗时任务和并行计算时效果显著。在C#中,线程是程序......
  • 在国产芯片上实现YOLOv5/v8图像AI识别-【2.3】RK3588上使用C++启用多线程推理更多内容
    本专栏主要是提供一种国产化图像识别的解决方案,专栏中实现了YOLOv5/v8在国产化芯片上的使用部署,并可以实现网页端实时查看。根据自己的具体需求可以直接产品化部署使用。B站配套视频:https://www.bilibili.com/video/BV1or421T74f基础背景对于国产化芯片来说,是采用NPU进......
  • JAVA多线程的使用和创建的几种方式
    Thrad创建和使用创建实体类,继承Thread实现run()方法调用start()方法publicclassThreadDemoextendsThread{Loggerlogger=LoggerFactory.getLogger(ThreadDemo.class);privateStringtaskName;publicStringgetTaskName(){return......