赞
踩
进程是程序的⼀次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行⼀个程序即是⼀个进程从创建,运行到消亡的过程。
在 Java 中,当我们启动 main 函数时其实就是启动了⼀个 JVM 的进程,而main 函数所在的线程就是这个进程中的⼀个线程,也称主线程。
线程与进程相似,但线程是⼀个比进程更小的执行单位。⼀个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地⽅法栈,所以系统在产生⼀个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
Java 程序天生就是多线程程序,我们可以通过 JMX 来看看⼀个普通的 Java 程序有哪些线程,代码如下
⼀个 Java 程序的运⾏是 main 线程和多个其他线程同时运行。
public class MultiThread { public static void main(String[] args) { // 获取 Java 线程管理 MXBean ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息 ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); // 遍历线程信息,仅打印线程 ID 和线程名称信息 for (ThreadInfo threadInfo : threadInfos) { System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()); } } } //[5] Attach Listener //添加事件 //[4] Signal Dispatcher // 分发处理给 JVM 信号的线程 //[3] Finalizer //调⽤对象 finalize ⽅法的线程 //[2] Reference Handler //清除 reference 线程 //[1] main //main 线程,程序⼊⼝
⼀个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈
总结: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不⼀定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
程序计数器主要有下面两个作用:
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下⼀条指令的地址。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
虚拟机栈和本地方法栈
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
了解堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的⼀块内存,主要用于存放新创建的对象(几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
并发:两个及两个以上的作业在同⼀时间段内执行。
并行:两个及两个以上的作业在同⼀ 时刻执行。
最关键的点是:是否是 同时 执行。
同步 : 发出⼀个调用之后,在没有得到结果之前, 该调用就不可以返回,⼀直等待。
异步 :调用在发出之后,不用等待返回结果,该调用直接返回。
总体来说:
计算机底层: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
互联网发展趋势: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
计算机底层来说:
单核时代: 在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了⼀个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有⼀个线程,此线程被IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有⼀个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,⼀个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。
多核时代: 多核时代多线程主要是为了提高进程利用多核 CPU 的能里。举个例子:假如我们要
计算⼀个复杂的任务,我们只用⼀个线程的话,不论系统有几个 CPU 核心,都只会有⼀个CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。
多线程编程也存在一些挑战,如线程同步、资源竞争和死锁等问题,需要合理设计和管理线程的执行。
说说线程的⽣命周期和状态?
实现多线程的方式有以下几种:
继承Thread类:创建一个继承自Thread类的子类,重写run()方法来定义线程的执行逻辑。
实现Runnable接口:创建一个实现了Runnable接口的类,并实现其中的run()方法,然后将该类的实例传递给Thread类的构造函数创建线程对象。
实现Callable接口:创建一个实现了Callable接口的类,并实现其中的call()方法,可以通过ExecutorService提交Callable对象来创建并执行线程,并返回执行结果。
使用线程池:通过Executor框架创建线程池,可以管理和复用线程,提高线程的执行效率和资源利用率。
并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。
Java 线程在运行的生命周期中的指定时刻只可能处于下面6种不同状态的其中⼀个状态:
线程在生命周期中并不是固定处于某⼀个状态而是随着代码的执行在不同状态之间切换。
Java 线程状态变迁图
由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调⽤ start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于RUNNING(运行) 状态
在操作系统层⾯,线程有 READY 和 RUNNING 状态;⽽在 JVM 层⾯,只能看到 RUNNABLE状态,所以 Java 系统⼀般将这两个状态统称为 RUNNABLE(运⾏中) 状态 。
为什么 JVM 没有区分这两种状态呢?
现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是⽤所谓的“时间分⽚(time quantum or time slice)”⽅式进⾏抢占式(preemptive)轮转调度(round-robin式)。这个时间分⽚通常是很⼩的,⼀个线程⼀次最多只能在 CPU 上运⾏⽐如 10-20ms 的时间(此时处于 running 状态),也即⼤概只有 0.01 秒这⼀量级,时间⽚⽤后就要被切换下来放⼊调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。
创建线程的方式包括以下几种:
继承Thread类:创建一个继承自Thread类的子类,重写run()方法,并通过调用start()方法来启动线程。
实现Runnable接口:创建一个实现了Runnable接口的类,并实现其中的run()方法,然后创建Thread对象,将实现了Runnable接口的对象作为参数传递给Thread的构造函数,最后通过调用start()方法来启动线程。
实现Callable接口:创建一个实现了Callable接口的类,并实现其中的call()方法,通过创建ExecutorService对象,使用submit()方法提交Callable对象,并返回一个Future对象,可以通过Future对象获取线程执行的结果。
使用线程池:通过Executor框架创建线程池,可以通过ThreadPoolExecutor或Executors工具类来创建线程池对象,然后将实现Runnable或Callable接口的任务提交给线程池执行。
线程安全指的是在多线程环境下,对共享资源的访问不会导致不确定的结果或产生并发错误。多线程访问共享资源时可能会出现竞态条件(Race Condition)、数据不一致等问题,因此需要采取措施来保证线程安全。
共同点 :两者都可以暂停线程的执行。
区别 :
wait() 是让获得对象锁的线程实现等待,会动释放当前线程占有的对象锁。每个对象( Object )都拥有对象锁,既然要释放当前线程占有的对象锁并让其进⼊ WAITING 状态,自然是要操作对应的对象( Object )而非当前的线程( Thread )。
类似的问题:为什么 sleep() 方法定义在 Thread 中?
因为 sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁
new ⼀个 Thread ,线程进入了新建状态。调用 start() 方法,会启动⼀个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行run() 方法的内容,这是真正的多线程共作。 但是,直接执行 run() 方法,会把 run() 方法当成⼀个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调⽤ start() 方法方可启动线程并使线程进入就绪状态,直接执行run() 方法的话不会以多线程的方式执行
如何保证变量的可见性?
在 Java 中, volatile 关键字可以保证变量的可见性,如果我们将变量声明为 volatile ,这就指示JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
volatile 关键字其实并非是 Java 特有的,在 C 也有,它最原始的意义就是禁用 CPU 缓存。如果我们将⼀个变量使用 volatile 修饰,这就指示编译器,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
volatile 关键字能保证数据的可见性,但不能保证数据的原子性。 synchronized 关键字两者都能保证。
如何禁止指令重排序?
在 Java 中, volatile 关键字除了可以保证变量的可⻅性,还有⼀个重要的作用就是防止 JVM 的指令重排序。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插⼊特定的内存屏障的方式来禁止指令重排序。
在 Java 中, Unsafe 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异:
public native void loadFence();
public native void storeFence();
public native void fullFence();
理论上来说,你通过这个三个方法也可以实现和 volatile 禁止重排序⼀样的效果,只是会麻烦⼀些。 下面我以⼀个常见的面试题为例讲解⼀下
volatile 关键字禁止指令重排序的效果。 “单例模式了解吗?手写⼀下!给我解释⼀下双重检验锁方式实现单例模式的原理呗!”
双重校验锁实现对象单例(线程安全) :
public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { //先判断对象是否已经实例过,没有实例化过才进⼊加锁代码 if (uniqueInstance == null) { //类对象加锁 synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执行了 1和 3,此时 T2 调用getUniqueInstance () 后发现 uniqueInstance 不为空,因此返回 uniqueInstance ,但此时 uniqueInstance 还未被初始化。
volatile 可以保证原⼦性么?
volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。
/** * 微信搜 JavaGuide 回复"⾯试突击"即可免费领取个⼈原创的 Java ⾯试⼿册 * * @author Guide哥 * @date 2022/08/03 13:40 **/ public class VolatoleAtomicityDemo { public volatile static int inc = 0; public void increase() { inc++; } public static void main(String[] args) throws InterruptedException { ExecutorService threadPool = Executors.newFixedThreadPool(5); VolatoleAtomicityDemo volatoleAtomicityDemo = new VolatoleAtomicityDemo(); for (int i = 0; i < 5; i++) { threadPool.execute(() -> { for (int j = 0; j < 500; j++) { volatoleAtomicityDemo.increase(); } }); } // 等待1.5秒,保证上⾯程序执⾏完成 Thread.sleep(1500); System.out.println(inc); threadPool.shutdown(); } }
正常情况下,运行上面的代码理应输出 2500 。但你真正运行了上面的代码之后,你会发现每次输出结果都小于 2500 。
为什么会出现这种情况呢?不是说好了, volatile 可以保证变量的可见性嘛!
也就是说,如果 volatile 能保证 inc++ 操作的原子性的话。每个线程中对 inc 变量自增完之后,其他线程可以立即看到修改后的值。5 个线程分别进行了 500 次操作,那么最终 inc 的值应该是5*500=2500。
很多人会误认为自增操作 inc++ 是原⼦性的,实际上,inc++ 其实是⼀个复合操作,包括三步:
volatile 是无法保证这三个操作是具有原子性的,有可能导致下面这种情况出现:
这也就导致两个线程分别对 inc 进行了⼀次自增操作后, inc 实际上只增加了 1。
其实,如果想要保证上面的代码运行正确也非常简单,利用 synchronized 、 Lock 或者 AtomicInteger 都可以。
使用 synchronized 改进:
public synchronized void increase() {
inc++;
}
使⽤ AtomicInteger 改进:
public AtomicInteger inc = new AtomicInteger();
public void increase() {
inc.getAndIncrement();
}
使⽤ ReentrantLock 改进:
Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
try {
inc++;
} finally {
lock.unlock();
}
}
说⼀说自己对于 synchronized 关键字的了解
synchronized 翻译成中文是同步的的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有⼀个线程执行。
在 Java 早期版本中, synchronized 属于 重量级锁,效率低下。 因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒⼀个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对长的时间,时间成本相对较高。
不过,在 Java 6 之后,官方对从 JVM 层⾯对 synchronized 较大优化,所以现在的synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。所以,你会发现目前的话,不论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字。
synchronized 关键字最主要的三种使用方式:
//给当前对象实例加锁,进⼊同步代码前要获得 当前对象实例的锁
synchronized void method() {
//业务代码
}
/*给当前类加锁,会作⽤于类的所有对象实例 ,进⼊同步代码前要获得 当前 class 的锁。
这是因为静态成员不属于任何⼀个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。*/
synchronized static void method() {
//业务代码
}
静态 synchronized ⽅法和⾮静态 synchronized ⽅法之间的调⽤互斥么?不互斥!如果⼀个线程 A调⽤⼀个实例对象的⾮静态 synchronized ⽅法,⽽线程 B 需要调⽤这个实例对象所属类的静态synchronized ⽅法,是允许的,不会发⽣互斥现象,因为访问静态 synchronized ⽅法占⽤的锁是当前类的锁,⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁
/*synchronized(object) 表示进⼊同步代码库前要获得 给定对象的锁。
synchronized(类.class) 表示进⼊同步代码前要获得 给定 Class 的锁*/
synchronized(this) {
//业务代码
}
总结:
不可以,构造方法本身就属于线程安全的,不存在同步的构造方法⼀说
synchronized 关键字底层原理属于 JVM 层面
synchronized 同步语句块的情况
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
通过 JDK ⾃带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:⾸先切换到类的对应⽬录执⾏ javac SynchronizedDemo.java 命令⽣成编译后的 .class ⽂件,然后执⾏ javap -c -s -v -lSynchronizedDemo.class 。
线程安全指的是在多线程环境下,对共享资源的访问不会导致不确定的结果或产生并发错误。多线程访问共享资源时可能会出现竞态条件(Race Condition)、数据不一致等问题,因此需要采取措施来保证线程安全。
以下是一些保证线程安全的方法:
需要根据具体的场景和需求选择适当的方法来保证线程安全,以确保多线程程序的正确性和可靠性。
synchronized
来实现。要使用互斥锁来实现线程同步,可以按照以下步骤:
定义共享资源:确定需要被多个线程共享的资源。
使用synchronized
关键字:在访问共享资源的代码段之前,使用synchronized
关键字来标记该代码段,以确保同一时间只有一个线程可以访问。
synchronized (sharedResource) {
// 访问共享资源的代码
}
或者,也可以使用synchronized
修饰方法来实现对整个方法的同步:
public synchronized void sharedMethod() {
// 方法体
}
在使用synchronized
关键字标记的代码段执行期间,其他线程将被阻塞,直到当前线程释放锁定。
访问共享资源:在获取锁定之后,线程可以安全地访问共享资源或执行需要同步的代码段。
死锁(Deadlock)是指两个或多个线程被永久地阻塞,因为每个线程都在等待其他线程所持有的资源,从而无法继续执行。
线程死锁:多个线程同时被阻塞,它们中的⼀个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
public class DeadLockDemo { private static Object resource1 = new Object();//资源 1 private static Object resource2 = new Object();//资源 2 public static void main(String[] args) { new Thread(() -> { synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource2"); synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); } } }, "线程 1").start(); new Thread(() -> { synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource1"); synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); } } }, "线程 2").start(); } } //Thread[线程 1,5,main]get resource1 //Thread[线程 2,5,main]get resource2 //Thread[线程 1,5,main]waiting get resource2 //Thread[线程 2,5,main]waiting get resource1
线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过 Thread.sleep(1000); 让线程 A 休眠 1s 为的是让线程 B 得到执⾏然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对⽅的资源,然后这两个线程就会陷⼊互相等待的状态,这也就产⽣了死锁
在Java中,可以采取以下几种方式来避免死锁:
tryLock()
方法来尝试获取锁,并设定一个超时时间,在超时后放弃获取锁,避免长时间等待。破坏死锁的产生的必要条件即可:
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态
安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3…Pn)来为每个线程分配所需资源,直到满⾜每个线程对资源的最⼤需求,使每个线程都可顺利完成。称 <P1、P2、P3…Pn> 序列为安全序列
new Thread(() -> { synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource2"); synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); } } }, "线程 2").start(); //Thread[线程 1,5,main]get resource1 //Thread[线程 1,5,main]waiting get resource2 //Thread[线程 1,5,main]get resource2 //Thread[线程 2,5,main]get resource1 //Thread[线程 2,5,main]waiting get resource2 //Thread[线程 2,5,main]get resource2 //Process finished with exit code 0
线程 1 ⾸先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占⽤,线程 2 获取到就可以执⾏了。这样就破坏了破坏循环等待条件,因此避免了死锁
线程池(ThreadPool)是一种用于管理和重用线程的机制。它由一组预先创建的线程组成,用于执行提交给池的任务,从而减少了线程创建和销毁的开销,并提高了线程的利用率。
在Java中,可以使用java.util.concurrent
包下的ExecutorService
接口来创建线程池。以下是创建线程池的一般步骤:
创建线程池对象:使用Executors
类提供的静态方法之一创建一个ExecutorService
实例,例如:
ExecutorService executorService = Executors.newFixedThreadPool(10);
上述代码创建了一个固定大小为10的线程池。
提交任务:使用execute()
方法或submit()
方法向线程池提交任务,例如:
executorService.execute(new MyTask());
其中,MyTask
是一个实现Runnable
接口的任务。
关闭线程池:在不再需要线程池时,调用shutdown()
方法来关闭线程池,例如:
executorService.shutdown();
这将停止线程池接受新的任务,并在所有任务执行完成后关闭线程池。
线程优先级(Thread Priority)是用于指定线程相对执行顺序的一种机制。每个线程都有一个优先级,用于决定在竞争CPU资源时获得执行的几率。
在Java中,线程的优先级通过整数表示,范围从1到10,其中1为最低优先级,10为最高优先级。可以使用setPriority()
方法来设置线程的优先级,例如:
Thread thread = new Thread();
thread.setPriority(Thread.MAX_PRIORITY); // 设置最高优先级
请注意,线程优先级只是给出建议,实际的线程调度行为受到底层操作系统和线程调度器的影响。
在Java中,可以使用Thread
类的静态方法yield()
来暗示线程调度器当前线程可以放弃CPU资源,让其他线程执行。此外,还可以使用sleep()
方法来让线程休眠一段时间,从而影响线程的调度行为。
线程调度算法的具体实现取决于操作系统和虚拟机的实现细节,包括时间片轮转、优先级调度、公平调度等策略。这些策略通常是操作系统和虚拟机的核心组件,用于合理分配CPU资源并控制线程的执行顺序。
线程调度(Thread Scheduling)是操作系统或线程调度器决定哪个线程在给定时间点执行的过程。Java中的线程调度算法取决于底层操作系统和Java虚拟机的实现。
在Java中,可以使用Thread
类的静态方法yield()
来暗示线程调度器当前线程可以放弃CPU资源,让其他线程执行。此外,还可以使用sleep()
方法来让线程休眠一段时间,从而影响线程的调度行为。
线程调度算法的具体实现取决于操作系统和虚拟机的实现细节,包括时间片轮转、优先级调度、公平调度等策略。这些策略通常是操作系统和虚拟机的核心组件,用于合理分配CPU资源并控制线程的执行顺序。
在Java中,有多种方式可以实现线程间通信,包括:
wait()
和notify()
方法实现线程的等待和唤醒,允许线程在特定条件下等待并释放资源,等待其他线程满足条件后进行唤醒。在Java中,线程的等待和唤醒机制通常是通过以下方法来实现:
wait()
方法:使当前线程进入等待状态,并释放持有的锁。调用wait()
方法的线程将等待其他线程调用相同对象的notify()
或notifyAll()
方法来唤醒它。notify()
方法:唤醒在相同对象上调用wait()
方法进入等待状态的线程中的一个线程。如果有多个线程在等待,哪个线程被唤醒是不确定的。notifyAll()
方法:唤醒在相同对象上调用wait()
方法进入等待状态的所有线程。这种等待和唤醒机制通常与共享变量、锁机制和同步代码块一起使用,以实现多个线程之间的协调和通信。
守护线程(Daemon Thread)是一种在后台运行的线程,它的存在不会阻止程序的终止。当所有非守护线程结束时,守护线程会自动终止,无论它是否执行完任务。
在Java中,可以通过设置线程的setDaemon(true)
方法将线程设置为守护线程。这个方法需要在线程启动之前调用,否则会抛出IllegalThreadStateException
异常。
下面是创建守护线程的示例代码:
Thread daemonThread = new Thread(new MyRunnable());
daemonThread.setDaemon(true);
daemonThread.start();
在上述示例中,MyRunnable
是一个实现了Runnable
接口的任务。
需要注意的是,守护线程不能访问一些必要的资源,例如打开的文件或数据库连接等,因为它们可能在非守护线程执行完毕时被强制关闭。
线程局部变量(Thread Local Variable)是一种特殊类型的变量,它为每个线程都创建了一个独立的副本,每个线程都可以独立地操作和访问自己的副本,互不干扰。
在Java中,可以使用ThreadLocal
类来创建线程局部变量。以下是使用线程局部变量的一般步骤:
创建线程局部变量:通过实例化ThreadLocal
类来创建线程局部变量,例如:
ThreadLocal<String> threadLocal = new ThreadLocal<>();
上述代码创建了一个存储String
类型的线程局部变量。
设置和获取变量的值:使用set()
方法设置线程局部变量的值,使用get()
方法获取线程局部变量的值,例如:
threadLocal.set("value"); // 设置线程局部变量的值
String value = threadLocal.get(); // 获取线程局部变量的值
每个线程操作自己的线程局部变量副本,互不干扰。
清除变量的值:在使用完线程局部变量后,可以调用remove()
方法将线程局部变量的值清除,释放对应的资源,例如:
threadLocal.remove();
清除线程局部变量的值是一种良好的实践,以避免潜在的内存泄漏问题。
线程组的作用主要包括:
uncaughtException()
方法来处理线程组中任何线程抛出的未捕获异常。在Java中,可以使用ThreadGroup
类来创建线程组和管理线程组。以下是创建线程组的示例代码:
ThreadGroup threadGroup = new ThreadGroup("myThreadGroup");
Thread thread1 = new Thread(threadGroup, new MyRunnable());
Thread thread2 = new Thread(threadGroup, new MyRunnable());
在上述示例中,使用ThreadGroup
类的构造函数创建了一个名为"myThreadGroup"的线程组,并将两个线程分别添加到该线程组中。通过线程组,可以方便地对这两个线程进行集中管理和操作。
线程的中断(Interrupt)机制是一种用于通知线程停止执行的机制。当一个线程被中断时,它会收到一个中断信号,可以根据需要做出相应的响应。中断机制允许线程在执行期间检查中断状态并做出相应的处理。
在Java中,线程的中断通过调用线程的interrupt()
方法来触发。以下是中断一个线程的示例代码:
Thread thread = new Thread(new MyRunnable());
thread.start();
// 在适当的时候中断线程
thread.interrupt();
上述示例中,使用interrupt()
方法中断了一个线程。被中断的线程可以通过检查isInterrupted()
方法来获取中断状态,然后根据需要做出相应的处理。
请注意,中断只是向线程发送一个中断信号,并不会立即停止线程的执行。线程可以根据自己的逻辑和需要,在合适的时候响应中断信号并停止执行。
在Java中,线程的阻塞和非阻塞通常与线程的状态相关:
线程的状态可以通过调用线程的getState()
方法来获取。
线程的阻塞和非阻塞是根据线程当前的执行状态来描述的,并与线程所处的上下文环境和特定条件相关。线程的状态会随着执行过程的不同而发生变化。
线程的状态指的是线程在不同时间点的状态或条件。在Java中,线程的状态由Java Thread类中的枚举Thread.State表示。下面是Java中线程的几种状态:
通过监视线程的状态,我们可以了解线程在执行过程中的行为和条件,以便进行适当的控制和管理。
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。
这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用CPU 的时候恢复现场。并加载下⼀个将要占用 CPU 的线程上下文。这就是所谓的上下文切换。
上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有⼀定损耗,如果频繁切换就会造成整体效率低下。
上下文切换会带来一些开销,包括以下方面:
上下文切换的开销是非常昂贵的,会有⼀定损耗,因为它涉及到多个复杂的操作和数据结构更新。因此,在设计和实现多线程应用程序时,应尽量减少上下文切换的次数,以提高系统的性能和效率。
**活锁(Livelock)**是指线程在执行过程中相互响应对方的动作,导致它们无法继续执行任务。在活锁中,线程不断改变自己的状态,但整体上并未取得任何进展,因此无法完成任务。活锁通常是由于线程的行为逻辑不正确或互相干扰导致的。例如,两个线程互相让步或互相请求共享资源,导致它们无法前进。
**饥饿现象(Starvation)**是指某个线程无法获取所需的资源以执行其任务,从而一直处于等待状态。饥饿通常是由于资源分配不公平或优先级设置不当导致的。当一个或多个线程无法获得所需的资源,而其他线程占用资源时间过长,就会出现饥饿现象。被饥饿的线程可能无法正常执行,甚至长时间无法获得资源,从而导致系统性能下降或任务无法完成。
活锁和饥饿都是多线程并发编程中的问题,它们会导致线程无法正常执行或无法取得进展,从而影响系统的稳定性和性能。
**并发(Concurrency)**是指多个任务在同一个时间段内执行,它们之间通过时间片轮转或并发调度算法进行交替执行。在并发中,任务之间并不一定是同时进行的,而是根据调度算法在时间上交替执行。并发能够提高系统资源的利用率和吞吐量,但并不一定加快任务的完成时间。
**并行(Parallelism)**是指多个任务同时执行,每个任务在不同的处理器核心或计算单元上独立执行。在并行中,任务之间是真正同时进行的,它们可以通过多个独立的执行路径同时完成。并行可以加快任务的完成时间,特别是在多核处理器或分布式系统中。
简而言之,并发是指多个任务交替执行,而并行是指多个任务同时执行。并发是通过时间上的分片来实现任务的并发执行,而并行是通过物理上的并行处理单元来实现任务的同时执行。
**原子操作(Atomic Operation)**是指在并发环境下不可中断的单个操作。原子操作要么完全执行,要么不执行,不会出现中间状态或部分执行的情况。在多线程编程中,原子操作保证了数据的一致性和可靠性,避免了数据竞争和并发冲突的问题。
在Java中,Java.util.concurrent.atomic包提供了一些原子操作的类。以下是一些常见的Java原子操作类:
这些原子操作类提供了线程安全的原子操作方法,可以保证操作的原子性,减少并发编程中的竞态条件和数据冲突问题。
线程安全的集合是指多个线程可以同时访问和修改的集合,而不会引发数据不一致或产生竞态条件等并发问题。在并发编程中,线程安全的集合可以确保多个线程对集合的操作是正确的、可靠的和一致的。
Java中提供了许多线程安全的集合类,它们位于java.util.concurrent
包下,以下是其中几个常用的线程安全集合类:
ConcurrentHashMap
:线程安全的哈希表实现,适用于多线程环境下的高并发读写操作。CopyOnWriteArrayList
:线程安全的动态数组实现,通过复制整个数组来实现并发安全,适用于读操作频繁而写操作较少的场景。CopyOnWriteArraySet
:线程安全的集合类,基于CopyOnWriteArrayList
实现,适用于读操作频繁而写操作较少的场景。ConcurrentLinkedQueue
:线程安全的队列实现,适用于高并发的生产者-消费者模式。ConcurrentSkipListMap
:线程安全的有序映射表实现,基于跳表(Skip List)数据结构,适用于高并发读写操作的有序映射。ConcurrentSkipListSet
:线程安全的有序集合类,基于ConcurrentSkipListMap
实现,适用于高并发读写操作的有序集合。线程的可见性问题是指当多个线程同时访问共享变量时,一个线程对共享变量的修改可能对其他线程不可见,导致数据不一致或意外的行为。这是由于每个线程都有自己的工作内存,线程在执行过程中将共享变量从主内存中拷贝到自己的工作内存中进行操作,操作完成后再写回主内存,这个过程中可能存在延迟、重排序或缓存等问题,从而导致可见性问题。
Java提供了以下机制来解决线程的可见性问题:
volatile
关键字:使用volatile
关键字修饰的变量可以确保对该变量的读写操作都是直接在主内存中进行的,而不是在线程的工作内存中进行。这样可以保证当一个线程修改了volatile
变量的值时,其他线程能够立即看到最新的值。synchronized
、Lock
):使用同步机制可以确保在同一个锁范围内,对共享变量的读写操作都是原子性的,且在释放锁之前会将修改的值刷新回主内存,从而保证可见性。final
关键字:对于final
修饰的字段,一旦初始化完成后,其值对于其他线程是可见的。因此,可以使用final
关键字来确保共享变量的可见性。java.util.concurrent
工具类:Java提供了许多并发工具类,如CountDownLatch
、CyclicBarrier
、Semaphore
等,这些工具类不仅提供了线程间的同步机制,同时也提供了可见性保证,确保在特定的同步点前后对共享变量的修改是可见的。乐观锁(Optimistic Locking)和悲观锁(Pessimistic Locking)是并发编程中用于处理共享资源访问冲突的两种不同策略。
悲观锁是一种保守的策略,它假设会发生冲突,因此在访问共享资源之前会先获取锁,以防止其他线程对资源进行修改。悲观锁常用的实现方式是使用synchronized关键字或Lock接口的具体实现类来实现对共享资源的加锁。当一个线程获取到悲观锁后,其他线程需要等待该线程释放锁才能访问共享资源。悲观锁适用于并发冲突频繁的场景,但它可能会带来额外的开销和竞争。
乐观锁则是一种乐观的假设,它认为冲突的概率较低,因此不会立即加锁,而是在修改共享资源时先进行检查,如果发现其他线程已经修改了该资源,则进行回退、重试或放弃操作。乐观锁常用的实现方式是使用版本号(Versioning)或时间戳(Timestamp)等机制来判断资源是否被修改。在Java中,乐观锁常用的实现方式是通过Atomic类(如AtomicInteger、AtomicLong)和CAS(Compare and Swap)操作来实现。CAS操作是一种原子操作,它可以比较当前值与期望值,如果相等则进行更新,否则不进行更新,可以保证操作的原子性和可见性。
Java中的java.util.concurrent.atomic包提供了一系列原子类,如AtomicInteger、AtomicLong、AtomicReference等,它们利用乐观锁的思想来实现对共享资源的线程安全访问。这些原子类的方法使用了CAS操作,避免了显式加锁,从而提高了并发性能。
读写锁(ReentrantReadWriteLock)是Java中提供的一种高级锁机制,用于解决读写操作的并发性问题。读写锁允许多个线程同时读共享资源,但只允许一个线程进行写操作。这种锁设计适用于读多写少的场景,可以提高并发读操作的性能。
读写锁内部维护了两个锁:读锁和写锁。多个线程可以同时获取读锁,只要没有线程持有写锁。当某个线程持有写锁时,其他线程无法获取读锁或写锁,从而保证了对共享资源的独占性。
读写锁的使用方式如下:
import java.util.concurrent.locks.ReentrantReadWriteLock; // 创建读写锁实例 ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); // 读操作 rwLock.readLock().lock(); try { // 读共享资源的操作 } finally { rwLock.readLock().unlock(); } // 写操作 rwLock.writeLock().lock(); try { // 写共享资源的操作 } finally { rwLock.writeLock().unlock(); }
在读操作中,通过readLock().lock()
获取读锁,对共享资源进行读操作,然后使用readLock().unlock()
释放读锁。
在写操作中,通过writeLock().lock()
获取写锁,对共享资源进行写操作,然后使用writeLock().unlock()
释放写锁。
线程的条件变量(Condition)是Java中用于线程间通信和线程调度的一种机制。它通常与锁(Lock)结合使用,用于实现线程的等待和唤醒操作。
条件变量可以让一个或多个线程在某个条件满足时等待,直到其他线程通过唤醒操作通知它们继续执行。条件变量提供了await()
方法用于线程等待和释放锁,并提供了signal()
和signalAll()
方法用于唤醒等待的线程。
使用条件变量的一般步骤如下:
创建一个锁(Lock)实例,并通过该锁创建一个条件变量(Condition)实例:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
在获取锁后,通过条件变量的await()
方法进入等待状态,同时释放锁:
lock.lock();
try {
while (条件不满足) {
condition.await();
}
} finally {
lock.unlock();
}
在某个线程中满足条件时,通过条件变量的signal()
或signalAll()
方法唤醒等待的线程:
lock.lock();
try {
// 修改条件
condition.signal(); // 或 condition.signalAll();
} finally {
lock.unlock();
}
被唤醒的线程会重新尝试获取锁,并继续执行。
条件变量的使用允许线程在满足特定条件之前等待,从而有效地实现线程间的通信和协调。
线程的原始锁(Lock)是Java中提供的一种用于同步访问共享资源的机制。与传统的synchronized关键字相比,原始锁提供了更多的灵活性和功能。
原始锁提供了显式的加锁和释放锁操作,可以实现更细粒度的锁定控制。它允许更灵活的锁定和解锁方式,支持可重入性、公平性和超时控制等特性。
使用原始锁的一般步骤如下:
创建一个锁(Lock)实例:
Lock lock = new ReentrantLock();
在需要加锁的代码块前调用锁的lock()
方法获取锁:
lock.lock();
try {
// 访问共享资源的操作
} finally {
lock.unlock();
}
注意在使用try-finally
块时,确保在任何情况下都会释放锁,以避免死锁的发生。
原始锁的使用方式允许更细粒度的锁定和解锁控制,可以提供更高的灵活性和性能。此外,原始锁还提供了一些其他方法,如tryLock()
用于尝试非阻塞地获取锁,newCondition()
用于创建条件变量等,以满足不同的并发编程需求。
线程的自旋锁(Spin Lock)是一种基于忙等待(Busy-Waiting)的锁机制,它在获取锁时不会立即进入阻塞状态,而是循环检测锁的状态,直到获取到锁为止。自旋锁适用于临界区较小且锁竞争短暂的情况。
使用自旋锁的一般步骤如下:
定义一个标志位作为锁的状态:
private volatile boolean locked = false;
这里使用volatile
关键字确保线程之间的可见性。
在获取锁时,通过循环检测锁的状态,直到获取到锁:
while (!locked) {
// 自旋等待,不执行任何操作或进行短暂的延时
}
在释放锁时,将标志位设置为未锁定状态:
locked = false;
自旋锁的特点是在竞争激烈的情况下,可能会造成较高的CPU占用,因为线程会忙碌地进行循环检测锁的状态。因此,自旋锁通常在以下情况下使用:
线程的可重入锁(ReentrantLock)是Java中提供的一种独占锁机制,它允许同一个线程多次获取同一个锁,即支持重入性。可重入锁是一种线程安全的锁,它提供了与synchronized关键字类似的功能,但更加灵活和可扩展。
可重入锁内部维护了一个持有锁的线程和一个计数器,当一个线程第一次获取锁时,计数器加1,当同一个线程再次获取锁时,计数器再次加1。只有当线程释放锁的次数与获取锁的次数相等时,其他线程才能获取该锁。
使用可重入锁的一般步骤如下:
创建一个可重入锁实例:
ReentrantLock lock = new ReentrantLock();
在需要加锁的代码块前调用锁的lock()
方法获取锁:
lock.lock();
try {
// 访问共享资源的操作
} finally {
lock.unlock();
}
注意在使用try-finally
块时,确保在任何情况下都会释放锁,以避免死锁的发生。
可重入锁允许同一个线程多次获取同一个锁,这使得线程可以重复进入自己已经拥有的锁所保护的临界区。这种机制提供了更大的灵活性和控制权,可以防止死锁,并允许线程在已经获取锁的情况下递归地调用同步方法。
可重入锁还提供了一些其他方法,如tryLock()
用于尝试非阻塞地获取锁,newCondition()
用于创建条件变量等,以满足不同的并发编程需求。
总之,可重入锁是一种强大而灵活的锁机制,它可以替代synchronized关键字,并提供更多的功能和控制选项。
线程的阻塞队列(BlockingQueue)是Java中提供的一种线程安全的队列实现,它在队列为空时会阻塞获取操作,并在队列已满时会阻塞插入操作,从而实现线程间的同步和协调。
阻塞队列提供了一组阻塞操作,包括阻塞的插入和获取元素的方法,使得线程可以安全地进行等待和唤醒操作,而不需要手动地实现等待/唤醒的逻辑。
使用阻塞队列的一般步骤如下:
创建一个阻塞队列实例:
BlockingQueue<T> queue = new ArrayBlockingQueue<>(capacity);
这里的T
表示队列中元素的类型,capacity
表示队列的容量。
插入元素到队列中:
queue.put(element); // 阻塞插入操作,如果队列已满则阻塞等待
获取队列中的元素:
T element = queue.take(); // 阻塞获取操作,如果队列为空则阻塞等待
注意,如果队列为空,则获取操作会被阻塞,直到队列中有元素可供获取。
阻塞队列还提供了一些其他的方法,如offer()
用于尝试插入元素而不阻塞,poll()
用于尝试获取元素而不阻塞,以及一些用于获取队列状态的方法,如size()
和isEmpty()
等。
线程的同步器(Synchronizer)是一种用于控制多个线程之间协调和同步的机制。它可以用来实现线程间的互斥访问和临界区保护,以及线程间的等待和唤醒操作。
Java中提供了几种常用的同步器,包括:
线程的线程局部变量(ThreadLocal)是Java中的一种特殊变量,它为每个线程提供了独立的变量副本,每个线程可以独立地操作自己的变量副本,互不干扰。线程局部变量在多线程环境下可以实现线程间数据的隔离,每个线程都可以独立地设置和获取自己的变量值。
Java中使用线程局部变量的一般步骤如下:
创建一个线程局部变量实例:
ThreadLocal<T> threadLocal = new ThreadLocal<>();
这里的T
表示线程局部变量的类型。
在某个线程中设置变量值:
threadLocal.set(value);
这样就将value
设置为当前线程的线程局部变量的值。
在其他地方获取线程局部变量的值:
T value = threadLocal.get();
这样就可以获取当前线程的线程局部变量的值。
线程局部变量的特点是每个线程都有自己的独立副本,线程之间的变量操作互不干扰,因此可以实现线程间的数据隔离。线程局部变量常用于以下场景:
需要注意的是,使用线程局部变量时要小心内存泄漏问题,因为线程局部变量的生命周期与线程的生命周期一致,如果没有适时地清理线程局部变量的值,可能会导致内存泄漏。可以通过remove()
方法或使用ThreadLocal
的initialValue()
方法进行清理和初始化操作。
线程的线程池(Executor)是一种用于管理和复用线程的机制,它可以提高多线程应用程序的性能和资源利用率。线程池通过预先创建一定数量的线程,并维护一个任务队列,来执行提交给它的任务。
Java中提供了一些线程池类,常用的线程池类包括:
这些线程池类提供了不同的功能和使用方式,可以根据具体的需求选择合适的线程池类。通常情况下,可以通过Executors
类的静态方法来创建线程池,例如:
ExecutorService executor = Executors.newFixedThreadPool(10);
这样就创建了一个固定大小为10的线程池。
使用线程池的好处包括:
使用线程池可以更好地管理多线程任务,提高程序性能和资源利用率,减少线程创建和销毁的开销,并提供更好的线程管理和控制机制。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。