Java并发(一)Java线程基础

2024 年 5 月 27 日 星期一(已编辑)
/
81
3
这篇文章上次修改于 2024 年 7 月 24 日 星期三,可能部分内容已经不适用,如有疑问可询问作者。

Java并发(一)Java线程基础

线程

线程和进程区别?

进程:一个在内存中运行的应用程序,每个正在系统上运行的程序都是一个进程

线程:进程中的一个执行任务(控制单元),它负责在程序里独立执行。

一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

  • 根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度执行的基本单位

    • 当我们运行一个可执行程序的时候,就会创建一个或多个进程,创建进程的时候需要分配空间,比如:栈区、文件映射区、堆区、静态区、常量区、代码段,因此也会说进程是资源分配的最小单位
    • 线程则是程序执行的基本单位,每个进程中都有唯一的主线程,且只能有一个,主线程和进程是相互依存的关系,主线程结束进程也会结束。
  • 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小
  • 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻量级进程
  • 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程与进程之间的地址空间和资源是相互独立的
  • 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃有可能导致整个进程都死掉。所以多进程要比多线程健壮
  • 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

是什么:进程:一个在内存中运行的应用程序,每个正在系统上运行的程序都是一个进程

​ 线程:进程中的一个执行任务(控制单元),必须依存在应用程序中

做什么:进程是操作系统资源分配的基本单位,而线程是任务调度执行的基本单位

怎么做:进程独立,线程共享地址空间和资源,进程包含线程

关系:一个进程至少有一个线程,一个进程可以运行多个线程;在某一个进程出问题时,其他进程一般不受影响,而在多线程的情况下,一个线程执行了非法操作会导致整个进程退出。

资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小

为什么需要线程?

线程能提高并发能力,能够利用多cpu 多核的资源

虽然创建多进程也能提升并发,但是线程的切换开销是比进程小的,而且线程间的通信也比进程更方面,线程可以说是一种轻量级的进程,是一种低成本实现并发能力方式

Java线程有哪些状态?

Java线程有六种状态:

初运阻等超终

  1. NEW:初始状态,线程被创建,但是还没有调用start()方法。
  2. RUNNABLE:运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作"运行中”
  3. BLOCKED:阻塞状态,表示线程阻塞于锁
  4. WAITING:等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些等待动作(通知或中断)
  5. TIME_WAITING:超时等待状态,该状态不同于WAITING状态,它可以在指定的时间自行返回
  6. TERMINATED:终止状态,表示当前线程已经执行完毕

线程创建之后它将处于 NEW(新建) 状态

调用 start() 方法后开始运行进入RUNNABLE,线程先处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态

当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制

当线程进入 synchronized 方法但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态

线程在执行完了 run()方法之后将会进入到 TERMINATED(终止) 状态。

创建线程的方式?

常用的有三种

  1. 实现Runable接口
  2. 实现Callable接口
  3. 继承Thread类,并重写它的run方法

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

Callable 相比 Runnable 提供了更强大的功能,可以有返回值并且可以抛出受检异常,同时可以与 Future 接口配合使用,通过 Future 对象获取任务的执行结果和状态。

  • Runnable 适合用于执行没有返回值的任务,如线程池中的任务处理;
  • Callable 适合用于有返回值的任务,如异步计算、查询数据库等。

实现接口比继承Thread类实现线程要好:

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

实现接口相当于定义任务,需要Thread类调用(Runnable 将Runable类实现对象作为target,Callable 通过把Callable对象传给FutureTask构造方法,再把FutureTask类对象作为target,将target传递给 Thread 类的构造方法,或者调用 ExecutorService 的相关方法)

Callable 比 Runnable强大有返回值并且可以抛出受检异常,同时可以与 Future 接口配合使用,通过 Future 对象获取任务的执行结果和状态

  • Future: 是一个接口,定义了用于操作异步计算结果的方法,可以接受任何实现了 Callable 接口的任务。
  • FutureTask: 是一个实现了 Future 接口的类,可以接受 CallableRunnable 类型的任务。

接口比继承好:继承Thread类就无法继承其它类,继承整个Thread类开销过大

可以直接调用 Thread 类的 run 方法吗

new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

守护线程和用户线程有什么区别呢?

用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程

守护 (Daemon) 线程:运行在后台,为其他前台线程服务。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作。比如垃圾回收线程

线程之间如何通信及线程之间如何同步?

一般线程之间的通信机制有两种:共享内存和消息传递。

Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

  • volatilesynchronized保证可见性
  • wait/notify:等待/通知机制协作多线程运行

    • notify:通知唤醒一个在对象上等待的线程
    • notifyAll:通知唤醒所有等待在该对象上的线程,会将全部线程由等待池移到锁池参与锁的竞争
    • wait():线程调用该方法进入等待(WAITING)状态,返回需要等待另外的线程通知或者被中断,另外注意线程调用wait方法后会释放对象的锁(能调用wait方法的前提也是获取到了对象的锁)
    • wait(Iong):线程调用之后会进入超时等待(TIMED_WAITING)状态,多一种返回方式,就是如果没有通知,也会在等待毫秒后返回
    • wait(long,int):超时时间更细,到纳秒
  • ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()

  • BlockingQueue提供了线程安全的生产者-消费者模式实现,实现线程间的安全通信。

sleep()和wait()有什么区别?

两者都可以暂停线程的执行

  • 类的不同:sleep()是Thread:线程类的静态方法,wait()是Object类的方法。
  • 是否释放锁:sleep()不释放锁;wait()释放锁。
  • 用途不同:Wait通常被用于线程间交互/通信,sleep通常被用于暂停执行。
  • 用法不同:wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法,或者可以使用wait(long timeout)超时后线程会自动苏醒。sleep()方法执行完成后,线程会自动苏醒。

为什么 wait() 方法不定义在 Thread 中?

wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。

类似的问题:为什么 sleep() 方法定义在 Thread 中?

因为 sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁

为什么 wait(),notify()和 notifyAll()必须在同步方法或者同步块中被调用?

当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。

同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。

notify() 和 notifyAll() 有什么区别?

如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。

notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。

Java中interrupt、interrupted和isInterrupted方法的区别?

interrupt:用于中断线程。调用该方法的线程的状态为将被置为"中断"状态。

[!CAUTION]

注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)监视线程的中断状态,一旦线程的中断状态被置为”中断状态”,就会抛出中断异常。

interrupted:是静态方法,需要通过 Thread.interrupted() 来调用,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用interrupted则返回true,第二次和后面的就返回false了。

isInterrupted:实例方法,也是可以返回当前中断信号是true还是false,与interrupted最大的差别是并不会清除中断信号。

线程类的构造方法、静态块是被哪个线程调用的

假设 main 函数中 new 了 Thread2,Thread2 中 new 了Thread1,那么:

Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的

Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是Thread1 自己调用的

一个线程运行时发生异常会怎样?

如果异常没有被捕获该线程将会停止执行。

Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。

当一个未捕获异常将造成线程中断的时候,JVM 会使用 Thread.getUncaughtExceptionHandler()来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 uncaughtException()方法进行处理。

什么是阻塞式方法?

阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket 的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。

死锁

什么是线程死锁

死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的种阻塞的现象,若无外力作用,它们都将无法推进下去。

此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。

死锁的四个必要条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

如何预防和避免线程死锁

如何预防死锁? 破坏死锁的产生的必要条件即可:

  1. 破坏请求与保持条件:一次性申请所有的资源。
  2. 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源(加锁时限、死锁检测,发生死锁就释放锁资源)。
  3. 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

如何避免死锁?

  • 在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,能使其进入安全状态,则将资源分配给进程;否则,进程等待。
  • 直接避免一个线程同时获得多个锁

ThreadLocal

什么是ThreadLocal?说说你对ThreadLocal的理解

ThreadLocalL叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。

ThreadLocal是以ThreadLocal对象为Key,任意对象为值的存储结构(其底层是在线程里维护了一个map,map的key就是各种ThreadLocal对象),当一个Key-Value值被存储之后,会一直附带在线程上,所以你可以在线程执行的任何位置再通过这个ThreadLocal对象取到存入的一个值。另外设定或修改值的方式是set(T),获取值的方式是get();

为什么ThreadLocal会造成内存泄露?如何解决

ThreadLocalMap中的key为ThreadLocal的弱引用,value为强引用。如果ThreadLocal对象没有被外部强引用,垃圾回收时key会被清理掉,但value不会。这时key=null,而value不为null,如果不做处理,value将永远不会被GC掉,就有可能发生内存泄漏。

ThreadLocalMap的实现中考虑了这个问题,在调用get/set/removel时会清理掉key为null的entry。在编程时如果意识到当前编写的run方法里不再会使用ThreadLocal对象了,最好手动调用remove

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...