Java线程

线程概述

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程具有多种特性和用途:
线程是独立调度和分派的基本单位。在多线程OS中,线程是能独立运行的基本单位,因此是花费最小开销的实体。
线程具有原子性。原子性指的就是一个操作是不可中断,即使有多个线程执行,一个操作开始也不会受其他线程影响,即可以理解为线程的最小执行单元,不可被分割。
线程具有可见性。可见性指的就是当某个线程修改了其内存中共享变量的值时,其他线程能立刻感知到其值的变化。
线程管理涉及动态地创建、消亡、挂起和恢复线程的操作,这些操作可由操作系统等基础软件根据资源映射策略及同步方式等来调用和管理。
线程调度是指按照特定机制为多个线程分配CPU的使用权。计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。线程调度有两种模型:分时调度模型和抢占式调度模型。
在Java中,线程可以通过以下方式创建:
继承Thread类并重写run()方法。
实现Runnable接口并重写run()方法。
实现Callable接口并重写call()方法。
使用线程池创建线程。
线程在服务器中的文件管理、通信控制、前后台处理、异步处理等方面有广泛应用。在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计可以提高程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术也可以提高程序的执行效率。

线程与进程的区别

进程:执行程序的一次执行过程
线程:进程中的一个执行单元,一个进程至少一个线程
进程是一段正在执行的程序,是资源分配的基本单元,而线程是CPU调度的基本单元。
进程间相互独立进程,进程之间不能共享资源,一个进程至少有一个线程,同一进程的各线程共享整个进程的资源(寄存器、堆栈、上下文)。 线程的创建和切换开销比进程小。
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大 的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己 独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程与进程之间的地址空间和 资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃有 可能导致整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独 执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

线程的生命周期(状态)Java 线程具有五种基本状态

新建、就绪、运行、阻塞、死亡

(1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t= new MyThread();
(2)就绪状态(Runnable):当调用线程对象的 start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待 CPU 调度执行,并不是说执行了 t.start()此线程立即就会执行;
(3)运行状态(Running):当 CPU 开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
(4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对 CPU 的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1)等待阻塞:运行状态中的线程执行 wait()方法,使本线程进入到等待阻塞状态;
2)同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3)其他阻塞:通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状 态 超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
(5)死亡状态(Dead):线程执行完了或者因异常退出了 run()方法,该线程结束生命周期
undefined run()或 call()方法执行完成,线程正常结束。
异常结束
undefined 线程抛出一个未捕获的 Exception 或 Error。调用 stop
undefined 直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。

扩展知识:CPU时间片
操作系统层面有个晶振,有点像和尚在不停的撞钟,每隔很短很短的一段时间撞一次,将CPU的时间拆成一个一个的时间片;各个线程其实是不停的去抢一个个的时间片;时间片到点后,还是要重新抢(保证所有的线程都有机会抢到CPU去执行自己的逻辑;公平性)
新建状态
我们来看下面一段代码:
Thread t1 = newThread();
这里的创建,仅仅是在JAVA的这种编程语言层面被创建,而在操作系统层面,真正的线程还没有被创建。只有当我们调用了 start() 方法之后,该线程才会被创建出来,进入Runnable状态。只有当我们调用了 start() 方法之后,该线程才会被创建出来

多线程

多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。
多线程的好处
可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程 而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完 成各自的任务。
多线程的劣势:
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
多线程需要协调和管理,所以需要 CPU 时间跟踪线程;
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题
项目中用到的多线程
1、发送邮件、短信,使用多线程异步发送;
2、定时任务使用线程池;
多线程应用场景
多线程下载、
数据库连接池、
分批发送短信等
多线程分页查询

多线程处理后台任务

一般来说,我们需要在后台处理的任务,通常会使用定时器来开启后台线程处理,比如有些数据表的状态我需要定时去修改、我们搜索引擎里面的数据需要定时去采集、定时生成统计信息、定时清理上传的垃圾文件等。
多线程异步处理任务
当我们需要处理一个耗时操作并且不要立刻知道处理结果时,我们可以开启后台线程异步处理该耗时操作,这样可以提高用户体验。比如我之前做的一个项目需要上传简历附件,后台需要对简历附件进行解析,保存到数据表中,因为涉及多种格式的处理,所以我们开启多线程异步处理此操作,这样用户就不用等到我们的简历解析完就能看到服务端的响应了。再比如用户下载简历时,我们需要将数据表中的数据生成简历附件并且通过邮件发送到用户邮箱,该操作也可以开启多线程异步处理。
多线程分布式计算
当我们处理一个比较大的耗时任务时,我们可以将该任务切割成多个小的任务,然后开启多个线程同时处理这些小的任务,切割的数量一般根据我们服务器CPU的核数,合理利用多核CPU的优势。比如下载操作可以使用多线程下载提高下载速度;清理文件时,开启多个线程,按目录并行处理等等。

多线程的上下文切换

在多线程应用程序中,线程之间的切换是很常见的。当一个线程需要等待一些事件(如I/O操作或锁)时,系统会把该线程挂起,并将处理器分配给其他线程。这个过程被称为上下文切换。
上下文切换(Context Switching)指的是当操作系统从一个线程中断,然后保存该线程的上下文并恢复另一个线程的上下文时所发生的过程。在进行上下文切换时,操作系统必须保存当前线程的执行状态(包括代码指针、寄存器和堆栈等信息),以便稍后再次恢复它的执行。同时,操作系统还需要加载新线程的执行状态,以便让它接着上一个线程的执行位置继续执行。
由于上下文切换涉及到保存和恢复线程执行状态的操作,因此会有一定的开销。如果频繁发生上下文切换,会导致系统开销增大,降低多线程程序的整体性能。因此,在编写多线程应用程序时,应该尽量减少上下文切换的次数,提高系统的效率。
另外,需要注意的是,上下文切换并不一定总是发生在不同的线程之间。对于Java虚拟机(JVM)来说,当一个线程调用了wait()、sleep()或者阻塞式I/O等方法时,也会触发上下文切换,因为这些方法会让当前线程进入阻塞状态,需要将处理器分配给其他线程。当被阻塞的线程重新变得可运行时,操作系统会恢复该线程的执行,并继续执行它之前中断的代码。
在多线程编程中,上下文切换(Context Switching)是指操作系统在多个线程之间进行切换执行的过程。当一个线程正在执行时,操作系统可能会中断该线程的执行,并将它的上下文(包括程序计数器、寄存器状态、堆栈指针等)保存起来,然后切换到另一个线程继续执行。
上下文切换通常发生在以下几种情况下:
时间片耗尽:操作系统分配给每个线程的时间片(执行时间)用完了,需要将执行权交给其他线程。
阻塞操作:某个线程执行了一个阻塞操作,例如等待输入、等待磁盘读写等,此时操作系统会将该线程置为阻塞状态,并切换到可以执行的线程。
线程优先级:操作系统可能会根据线程的优先级来决定线程间的切换。
上下文切换的过程包括保存当前线程的上下文和恢复另一个线程的上下文。对于每个线程,操作系统会为其保留一块内存用于保存上下文信息,当该线程再次获得执行权时,可以从保存的上下文中恢复到之前的执行状态继续执行。
上下文切换的代价是非常高昂的,因为它涉及到保存和恢复大量的上下文信息,并且需要切换硬件资源和操作系统内部的状态。因此,在设计和优化多线程程序时,需要合理使用线程和避免频繁的上下文切换,以提高程序的性能和效率。
要减少多线程中的上下文切换,可以考虑以下几种方法:
减少线程数量:合理控制线程的数量,避免创建过多的线程。如果线程数量超过了系统的物理核心数,那么线程间的竞争会增加,导致上下文切换频繁发生。
使用线程池:使用线程池来管理线程的创建和复用。线程池通过维护一定数量的工作线程,避免了线程的频繁创建和销毁,从而减少了上下文切换的次数。
避免阻塞操作:在多线程程序中,尽量避免阻塞操作,例如IO操作、网络请求等。对于可能导致线程阻塞的操作,可以采用异步非阻塞的方式或者使用专门的线程处理。
使用无锁算法:在设计并发数据结构时,可以考虑使用无锁算法,如CAS(Compare and Swap),避免使用传统的锁机制。无锁算法可以减少线程间的竞争,降低上下文切换的次数。使用无锁编程:无锁编程可以避免线程间的竞争,从而减少上下文切换的次数。常见的无锁编程技术包括CAS(Compare And Swap)操作和volatile关键字等。
减小锁粒度:如果必须使用锁来实现线程同步,可以尽量减小锁的粒度,使得只有必要的代码块被锁定,从而减少线程之间的争用和上下文切换的次数。
使用协程或轻量级线程:使用协程或轻量级线程模型可以避免操作系统级别的上下文切换。协程可以在用户空间内切换,避免了内核态和用户态之间的开销。
合理地使用线程数目:过多的线程数目会导致系统频繁地进行上下文切换,降低系统的效率。因此,在编写多线程程序时应该根据实际情况和系统硬件条件合理地设置线程数目。
避免线程间的竞争:当多个线程访问共享资源时,会发生竞争,从而导致线程的等待和上下文切换。因此,在编写多线程程序时应该避免线程之间的竞争,减少上下文切换的次数。
undefined 使用非阻塞式I/O:阻塞式I/O会让线程进入阻塞状态,从而触发上下文切换。因此,可以使用非阻塞式I/O来减少上下文切换的次数。
线程间通信:使用合适的线程间通信方式,如synchronized关键字、Lock接口等,可以避免线程间的竞争,减少上下文切换的次数。
总之,减少上下文切换的方法是多方面的,需要根据实际情况进行选择和应用。在编写多线程程序时,应该充分考虑系统硬件条件、线程数目、线程间通信方式、I/O操作等因素,从而尽可能地减少上下文切换的次数,提高系统的效率和性能。减少多线程中的上下文切换需要综合考虑线程数量、线程调度算法、同步机制以及任务分解等因素,并选择合适的优化方法来提高程序的并发性能。

线程上下文切换

在上下文切换过程中,CPU 会停止处理当前运行的程序,并保存当前程序运行 的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时 阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。在 程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。
PCB 还经常被称作“切换桢”(switchframe)。“页码”信息会一直保存到 CPU 的内存中,直到他们被再次使用。 上下文切换是存储和恢复 CPU 状态的过程,它使得线程执行能够从中断点恢复 执行。上下文切换是多任务操作系统和多线程环境的基本特征。
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线 程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于 一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便 下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下 文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的 切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文 切换和模式切换的时间消耗非常少。
巧妙地利用了时间片轮转的方式, CPU 给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务,任务的状态保存及再加载, 这段过程就叫做上下文切换。时间片轮转的方式使多个任务在同一颗 CPU 上执行变成了可能。
守护线程和用户线程
用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用 户线程
守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线
程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作
守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程,这是它的作用——而其他的线程只有一种,那就是用户线程。所以java里线程分2种,
1、守护线程,比如垃圾回收线程,就是最典型的守护线程。
2、用户线程,就是应用程序里的自定义线程。
1、守护线程,专门用于服务其他的线程,如果其他的线程(即用户自定义线程)都执行完毕,连main线程也执行完毕,那么jvm就会退出(即停止运行)——此时,连jvm都停止运行了,守护线程当然也就停止执行了。
2、再换一种说法,如果有用户自定义线程存在的话,jvm就不会退出——此时,守护线程也不能退出,也就是它还要运行,干嘛呢,就是为了执行垃圾回收的任务啊
用户自定义线程
1、应用程序里的线程,一般都是用户自定义线程。
2、用户也可以在应用程序代码自定义守护线程,只需要调用Thread类的设置方法设置一下即可。

线程IO

同步
当一个同步调用发出后,调用者要一直等待返回消息(结果)通知后,才能进行后续的执行;
(选择排队等候)
异步
当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
(择取一个小纸条上面有我的号码,等到排到我这一号时由柜台的人通知我轮到我去办理业务了;)
阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。
(只能等待,什么都不能做)
非阻塞
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
(可以一边排队一边玩手机)
同步阻塞形式
(就是你专心排队,什么别的事都不做)
如果这个线程在等待当前函数返回时,没有执行其他消息处理,而是处于挂起等待状态,那这种情况就叫做同步阻塞;
异步阻塞形式
(只需要等大堂经理通知,在这段时间里他不能离开银行做其它的事情,)
如果这个线程在等待当前函数返回时,仍在执行其他消息处理,那这种情况就叫做同步非阻塞;
同步非阻塞形式
(一边打着电话一边还需要抬头看到底队伍排到你了没有)
异步非阻塞形式
(只需要等大堂经理通知,期间可以离开银行随意外出玩手机抽烟等)

Java 线程数过多会造成什么异常

(1)线程的生命周期开销非常高
(2)消耗过多的 CPU 资源
如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会
占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU 资源时还将产生其他
性能的开销。
(3)降低稳定性
JVM 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承
受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层
操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出 OutOfMemoryError 异常。

父子线程之间怎么共享数据

undefined 通过共享变量:父线程和子线程可以通过共享同一个变量来进行数据的传递和共享。这个变量可以是基本类型、对象引用或者集合等。需要注意的是,为了保证线程安全,需要对共享变量进行适当的同步操作(如使用synchronized关键字)。
undefined 通过共享对象:父线程和子线程可以共享同一个对象,在该对象中存储需要共享的数据。父线程可以将数据设置到对象中,而子线程可以通过访问该对象获取数据。需要注意的是,对于多个线程同时修改对象中的数据,需要进行适当的同步操作,以避免数据不一致或竞态条件的问题。
undefined 使用线程间通信机制:Java提供了一些线程间通信的机制,如wait/notify机制或者CountDownLatch等。通过这些机制,父线程可以等待子线程完成某个任务,并获取子线程的结果或共享数据。这种方式可以实现更复杂的线程间协作。
undefined 使用ThreadLocal变量:ThreadLocal是一种特殊的变量,它为每个线程提供独立的变量副本,因此可以通过ThreadLocal在父子线程之间传递数据。父线程可以设置ThreadLocal变量的值,子线程可以通过获取ThreadLocal变量的值来获取数据。需要注意的是,ThreadLocal变量只能在同一个线程内共享,不适用于多个父子线程之间的直接共享。
需要根据具体的需求和场景选择合适的方式来实现父子线程之间的数据共享,同时要注意线程安全和数据一致性的问题。

线程的方法

在多线程编程中,锁其实不是性能开销的根源,竞争锁才是。
线程相关的基本方法
wait、sleep需要捕获InterruptedException异常,
park不需要捕获InterruptedException异常,但是也会响应中断。
wait、park休眠线程时,线程的状态都是WAITING
sleep下线程的状态为TIMED_WAITING
sleep跟park不会释放持有锁,但是wait会释放
notify不能唤醒指定线程,unpark能唤醒指定线程

线程相关的基本方法有 wait,notify,notifyAll,sleep,join,yield 、interrupt
sleep():强迫一个线程睡眠N毫秒
isAlive(): 判断一个线程是否存活
join(): 等待线程终止
activeCount(): 程序中活跃的线程数
enumerate(): 枚举程序中的线程
currentThread(): 得到当前线程
isDaemon(): 一个线程是否为守护线程
setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线
程依赖于主线程结束而结束)
setName(): 为线程设置一个名称
wait(): 强迫一个线程等待
notify(): 通知一个线程继续运行
setPriority(): 设置一个线程的优先级
getPriority()::获得一个线程的优先级
如何停止一个线程
线程正常执行完毕,正常结束
调用stop方法
调用interrupt

join()

Join 等待其他线程终止
join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸
让当前线程加入父线程,加入后父线程会一直wait,直到子线程执行完毕后父线程才能执行。当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。
将某个线程加入到当前线程中来,一般某个线程和当前线程依赖关系比较强,必须先等待某个线程执行完毕才能执行当前线程。一般在run()方法内使用
很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要
在子线程结束后再结束,这时候就要用到 join() 方法
System.out.println(Thread.currentThread().getName() + “线程运行开始!”);
Thread6 thread1 = new Thread6();
thread1.setName(“线程 B”);
thread1.join();
System.out.println(“这时 thread1 执行完毕之后才能执行主线程”);
在线程编程中,join()方法是一个非常有用的方法。调用线程的join()方法会将当前线程阻塞,直到被调用的线程执行完毕。在多线程编程中,主线程通常会等待所有子线程完成后再结束程序,这时就可以使用join()方法。
join()方法的语法如下:
复制thread.join()
其中,thread是需要等待的线程对象。
当调用join()方法时,主线程会等待被调用的线程执行完毕,然后再继续执行。如果被调用的线程已经执行完毕,调用join()方法会立即返回。
需要注意的是,如果一个线程在执行join()方法时发生了异常,异常会被传递到调用join()方法的线程中。
如果在多线程编程中没有使用join()方法,程序可能会在所有子线程执行完毕之前就结束,这可能会导致一些问题。因此,在多线程编程中,使用join()方法是非常重要的。
yield()线程让步

yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。
交出CPU的执行时间,不会释放锁,让线程进入就绪状态,等待重新获取CPU执行时间,yield就像一个好人似的,当CPU轮到它了,它却说我先不急,先给其他线程执行吧, 此方法很少被使用到
sleep(long millis) 与 yeid()
sleep(long millis): 需要指定具体睡眠的时间,不会释放锁,睡眠期间CPU会执行其它线程,睡眠时间到会立刻执行
yeid(): 交出CPU的执行权,不会释放锁,和sleep不同的时当再次获取到CPU的执行,不能确定是什么时候,而sleep是能确定什么时候再次执行。两者的区别就是sleep后再次执行的时间能确定,而yeid是不能确定的
yield会把CPU的执行权交出去,所以可以用yield来控制线程的执行速度,当一个线程执行的比较快,此时想让它执行的稍微慢一些可以使用该方法,想让线程变慢可以使用sleep和wait,但是这两个方法都需要指定具体时间,而yield不需要指定具体时间,让CPU决定什么时候能再次被执行,当放弃到下次再次被执行的中间时间就是间歇等待的时间
setDaemon(boolean on)
线程分两种:
● 用户线程:如果主线程main停止掉,不会影响用户线程,用户线程可以继续运行。
● 守护线程:如果主线程死亡,守护线程如果没有执行完毕也要跟着一块死(就像皇上死了,带刀侍卫也要一块死),GC垃圾回收线程就是守护线程
线程等待(wait)
调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的
是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。

线程睡眠(sleep)
sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致
线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态

线程中断(interrupt)
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。
undefined 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
undefined 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出InterruptedException,从而使线程提前结束 TIMED-WATING 状态。
undefined 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
undefined 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程。

线程唤醒(notify)
Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象
上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调
用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继
续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞
争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
为什么 wait()方法和 notify()/notifyAll()方法要在同步块中被调用这是 JDK 强制的,wait()方法和 notify()/notifyAll()方法在调用前都必须先获得对象的锁多线程同步有哪几种方法
Synchronized 关键字,Lock 锁实现,分布式锁等
当使用多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。

Synchronized关键字
使用Volatile关键字实现线程同步。Volatile关键字的主要作用有两个:
① 内存可见性,即线程A对volatile变量的修改,其他线程获取的volatile变量都是最新的。
② 可以禁止指令重排序
使用重入锁实现线程同步
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁

sleep与wait的区别
sleep在Thread类中,wait在Object类中
sleep不会释放锁,wait会释放锁
sleep使用interrupt()来唤醒,wait需要notify或者notifyAll来通知
类的不同:sleep() 来自 Thread,wait() 来自 Object
释放锁:sleep() 不释放锁;wait() 释放锁
用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。
wait():将同步中的线程处于冻结状态。释放了执行权,释放了资格。同时将线程对象存储到线程池中
sleep:必须指定时间,时间到自动从冻结状态转成运行状态(临时阻塞状态)。
Object.wait()方法,会释放锁资源以及CPU资源。
Thread.sleep()方法,不会释放锁资源,但是会释放CPU资源

怎么唤醒一个阻塞的线程

如果线程是因为调用了 wait()、sleep()或 者 join()方法而导致的阻塞,可以中断线程,并且通过抛出 InterruptedException 来唤醒它;如果线程遇到了 IO 阻塞,无能为力,因为 IO 是操作系统实现的,Java 代码并没有办法直接接触到操作系统。

终止线程 4 种方式

使用线程中断方法 interrupt 停止线程。使用 stop 停止线程 暴力
1.正常运行结束,程序运行结束,线程自动结束
2.使用退出标志退出线程,一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false 来控制 while
循环是否退出,代码示例:

public class ThreadSafe extends Thread {
    public volatile boolean exit = false; 
    public void run() { 
        while (!exit){
            //do something
        }
    } 
}

定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false.在定义 exit时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。
3. Interrupt 方法结束线程
使用 interrupt()方法来中断线程有两种情况:
线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正
常结束 run 方法。
2. 线程未处于阻塞状态:使用 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 跳出循环
            }
        }
    } 
  1. stop 方法终止线程(线程不安全)
    程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关
    闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。

线程调度算法和策略

计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获 得 CPU 的使用权才能执行指令.所谓多线程的并发运行,其实是指从宏观上看, 各个线程轮流获得 CPU 的使用权,分别执行各自的任务.在运行池中,会有多个 处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度, 线程调度是指按照特定机制为多个线程分配 CPU 的使用权.
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线 程占用的 CPU 的时间片这个也比较好理解。
Java 虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用 CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用 CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。
抢占式调度:
抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。
协同式调度:
协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。
JVM 的线程调度实现(抢占式调度)
java 使用的线程调使用抢占式调度,Java 中线程会按优先级分配 CPU 时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。
线程让出 cpu 的情况:
undefined 当前运行线程主动放弃 CPU,JVM 暂时放弃 CPU 操作(基于时间片轮转调度的 JVM 操作系
统不会让线程永久放弃 CPU,或者说放弃本次时间片的执行权),例如调用 yield()方法。
undefined 当前运行线程因为某些原因进入阻塞状态,例如阻塞在 I/O 上。
undefined 当前运行线程结束,即运行完 run()方法里面的任务。
线程的调度策略
线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:
(1)线程体中调用了 yield 方法让出了对 cpu 的占用权利
(2)线程体中调用了 sleep 方法使线程进入睡眠状态
(3)线程由于 IO 操作受到阻塞
(4)另外一个更高优先级线程出现
(5)在支持时间片的系统中,该线程的时间片用完

线 程a,b,c,d运行任务,怎么保证当a,b,c线程执完再执行d线程?

、CountDownLatch类
⼀个同步辅助类,常用于某个条件发⽣后才能执⾏后续进程。给定计数初始化CountDownLatch,调⽤countDown()方法,在计
数到达零之前,await方法一直受阻塞。
重要方法为countdown( )与await( );
2、join方 法
将线程B加入到线程A的尾部,当A执行完后B才执行。
public static void main(String[] args) throws Exception {
Th t = new Th(“t1”); Th t2 = new Th(“t2”);
t.start();
t.join();
t2.start();
}

3、notify、wait方法,Java中的唤醒与等待方法,关键为synchronized代码块,参数线程间应相同,也常用Object作为参数。

wait 和 notify 这个为什么要在synchronized 代码块中

wait 和 notify 用来实现多线程之间的协调,wait 表示让线程进入到阻塞状态,notify 表示让阻塞的线程唤醒。
wait 和 notify 必然是成对出现的,如果一个线程被 wait()方法阻塞,那么必然需要另外一个线程通过 notify()方法来唤醒这个被阻塞的线程,从而实现多线程之间的通信。
在多线程里面,要实现多个线程之间的通信,除了管道流以外,只能通过共享变量的方法来实现,也就是线程 t1 修改共享变量 s,线程 t2 获取修改后的共享变量 s,从而完成数据通信。但是多线程本身具有并行执行的特性,也就是在同一时刻,多个线程可以同时执行。在这种情况下,线程 t2 在访问共享变量 s 之前,必须要知道线程 t1 已经修改过了共享变量 s,否则就需要等待。同时,线程 t1 修改过了共享变量 S 之后,还需要通知在等待中的线程 t2。所以要在这种特性下要去实现线程之间的通信,就必须要有一个竞争条件控制线程在什么条件下等待,什么条件下唤醒。
而 Synchronized 同步关键字就可以实现这样一个互斥条件,也就是在通过共享变量来实现多个线程通信的场景里面,参与通信的线程必须要竞争到这个共享变量的锁资源,才有资格对共享变量做修改,修改完成后就释放锁,那么其他的线程就可以再次来竞争同一个共享变量的锁来获取修改后的数据,从而完成线程之前的通信。
所以这也是为什么 wait/notify 需要放在 Synchronized 同步代码块中的原因,有了 Synchronized 同步锁,就可以实现对多个通信线程之间的互斥,实现条件等待和条件唤醒。另外,为了避免 wait/notify 的错误使用,jdk 强制要求把 wait/notify 写在同步代码块里面,否则会抛出 IllegalMonitorStateException
最后,基于 wait/notify 的特性,非常适合实现生产者消费者的模型,比如说用wait/notify 来实现连接池就绪前的等待与就绪后的唤醒。

wait()、notify()、notifyAll()
三个方法是 java.lang.Object 的 final native 方法
wait():导致线程进入等待状态,直到它被其他线程通过notify()或者notifyAll唤醒,该方法只能在同步方法中调用。
notify():随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态,该方法只能在同步方法或同步块内部调用。
notifyAll():解除所有那些在该对象上调用wait方法的线程的阻塞状态,同样该方法只能在同步方法或同步块内部调用。
调用这三个方法中任意一个,当前线程必须是锁的持有者,如果不是会抛出一个 IllegalMonitorStateException 异常

为什么阿里巴巴不建议使用 Executors

因为默认的 Executors 线程池底层是基于 ThreadPoolExecutor 构造函数封装的,采用无界队列存放缓存任务,会无限缓存任务容易发生 内存溢出,会导致我们最大线程数会失效。

多线程同步和互斥有几种实现方法

线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线 程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤 醒。线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。 当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使 用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。
用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。
内核 模式下的方法有:事件,信号量,互斥量

什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )

线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建 一个线程并启动它,它的执行便依赖于线程调度器的实现。
时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线 程优先级或者线程等待的时间。
线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你 的程序依赖于线程的优先级)。

线程 B 怎么知道线程 A 修改了变量

1)volatile 修饰变量
2)synchronized 修饰修改变量的方法
3)wait/notify
4)while 轮询

synchronized、volatile、CAS 比较
1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。
3)CAS 是基于冲突检测的乐观锁(非阻塞)

fifinal不可变对象,它对写并发应用有什么帮助

不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就
不能改变,反之即为可变对象(Mutable Objects)。
不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如
String、基本类型的包装类、BigInteger 和 BigDecimal 等。
只有满足如下状态,一个对象才是不可变的;
它的状态不能在创建后再被修改;
所有域都是 fifinal 类型;并且,它被正确创建(创建期间没有发生 this 引用的逸出)。
不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?

用 join 方法。
怎么控制同一时间只有 3 个线程运行?
用 Semaphore。

Java 怎么获取多线程的返回值

主线程等待。
使用 Thread 的 join 阻塞当前线程等待。
实现 Callable 接口(通过 FutureTask 或线程池的 Future)

  • 16
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

思静语

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值