并发 Concurrence

Siona

Concurrence

Java 进程、线程、协程

Java 进程、线程和协程是多任务编程中的重要概念,它们之间的区别如下:

✅ 进程(Process):是操作系统资源分配和调度的基本单位。进程拥有独立的地址空间、文件描述符、堆栈等资源,进程之间相互独立。在 Java 中,可以通过 ProcessBuilderRuntime.exec() 创建和管理进程。

✅ 线程(Thread):是操作系统调度的最小单位。线程是在进程内部创建和管理的,它共享进程的资源,包括地址空间、文件描述符、堆栈等,但拥有独立的程序计数器和栈空间。在 Java 中,可以通过 Thread 类来创建和管理线程。

✅ 协程(Coroutine):是一种用户级别的轻量级线程,也称为协作式多任务处理。协程通过用户代码调度,不需要操作系统介入,因此比线程更轻量级,开销更小。在 Java 中,可以使用一些第三方库来实现协程,如 QuasarKotlin 协程等。

总体来说,进程和线程是操作系统级别的概念,而协程是更高级别的用户级别概念。进程和线程具有明显的层次性和隔离性,但开销较大;协程具有轻量级和高效的特点,但需要编写更多的代码来实现调度和同步。

需要注意的是,在 Java 中,线程是非常重要的概念,Java 应用程序往往会使用大量的线程来处理并发和并行任务。协程在 Java 中并不是原生支持的概念,需要借助第三方库来实现。


一个进程可以包含多个线程,线程之间共享进程的资源和内存空间。线程可以使用同步机制来避免数据竞争和死锁等问题。在一个线程内部,可以使用协程来实现异步任务和 IO 操作等场景,从而提高并发执行的效率。
img.png
img.png

线程的生命周期、及状态

线程的 5 种状态: 创建、就绪、运行、阻塞、死亡。

阻塞的 3 种情况:

阻塞类型描述
等待阻塞运行的线程执行 wait 方法,该线程会释放占用的所有资源,JVM 会将该线程放入 “等待池” 中。
进入这个状态之后,是不能自动唤醒的,必须依靠其他线程调用 notifynotifyAll 方法才能被唤醒。
wait 是 Object 类的方法。
同步阻塞运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会将该线程放入 “锁池” 中。
其他阻塞运行的线程执行 sleepjoin 方法,或者发出了 I/O 请求 时,JVM 会将该线程置为阻塞状态。
当 sleep 状态超时、join 等待线程终止或超时、I/O 处理完毕时,线程重新转入就绪状态。
sleep 是 Thread 类的方法。
状态描述
新建状态(New)新创建了一个线程对象。
就绪状态(Runnable)线程对象创建后,其他线程调用了该对象的 start 方法。
该状态的线程位于 可运行线程池 中,变得可运行,等待获得 CPU 的使用权。
运行状态(Running)就绪状态的线程获取了 CPU,执行程序代码。
阻塞状态(Blocked)阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
死亡状态(Dead)线程执行完了或者因异常退出了 run 方法,该线程结束生命周期。
线程的生命周期 通常包括五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和终止(Terminated)。其中:


▪ 在线程的生命周期中,线程状态的转换通常是由操作系统调度和控制的。
▪ 当线程的状态发生变化时,需要进行上下文切换,即保存当前线程的状态和上下文信息,并恢复另一个线程的状态和上下文信息,使其能够继续执行。
▪ 上下文切换会带来一定的开销,因此需要尽可能减少线程之间的切换次数。

✅ 线程上下文切换 
		CPU通过分配时间片来执行任务,当一个任务的时间片用完,就会切换到另一个任务。在切换之前会保存上一个任务的状态,当下次再切换到该任务,就会加载这个状态。


线程的生命周期通常包括五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和终止(Terminated)。
状态描述
New (新建状态)线程正在被创建时的状态(已分配相应的内存空间、资源等,但还没开始运行)
Runnable(就绪状态)线程创建完成以后,具备运行条件,但由于没有空闲 CPU 暂时无法运行。
此时线程位于可运行线程池中,等待获取 CPU 的使用权。
(当线程对象调用 start( ) 之后)
Running(运行状态)线程在 CPU 上运行时的状态
Blocked(阻塞状态)线程在某些特定情况下,会被挂起,暂时停止运行。(线程处于阻塞状态,它不会占用 CPU 资源)
Terminated(终止状态)线程结束生命周期(释放资源,彻底消失)

并发编程三要素

并发三大特性

并发、并行、串行

含义场景
并发 (Concurrency)多个任务同一时间间隔发送
宏观上同时发送,微观上交替进行
系统中同时存在多个正在执行的任务,并且这些任务可能会相互影响。
并发通常用来处理多个任务共享资源的情况。
在单核 CPU 中,时间片轮转
并行 (Parallelism)真正意义上的同时处理多个任务
系统中同时存在多个并且相互独立的任务,并且这些任务可以在多个处理器上同时执行。
多个处理器处理多个独立的任务
同步 (Synchronous)为完成某种任务,程序需要在某些位置上协调它们的工作次序而产生的制约关系,又称直接制约关系。
程序按照代码的顺序执行,一行一行的执行,直到当前执行完成后才能继续执行下一行。
同步会阻塞调用者,直到任务完成才能返回。
进程同步
异步 (Asynchronous)程序执行某个任务时,不需要等待该任务完成,可以直接进行下一个任务,等任务完成后再进行相应处理,提高系统并发性能。
异步通常不会阻塞调用者。
并发计算
并发编程
并发和并行针对的是任务的执行方式;
同步和异步针对的是任务执行的阻塞和返回方式。


串行:在时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等着

并发:在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行

并发:允许两个任务彼此干扰。同一时间点,只有一个任务运行,交替执行。

Java 如何开启线程以及保证线程安全?

线程和进程的区别:
进程是操作系统进行资源分配的最小单元。
线程是操作系统进行任务分配的最小单元,线程隶属于进程。

如何开启线程? 
1、继承 Thread 类,重写 run()
2、实现 Runnable 接口,实现 run()
3、实现 Callable 接口,实现 call()。通过 FutureTask 创建一个线程,获取到线程执行的返回值。
4、通过线程池来开启线程。

怎么保证线程安全? 
核心思想:加锁
1JVM 提供的锁,也就是 Synchronized 关键字。 
2JDK 提供的各种锁 Lock(基础 Lock 接口实现的各种锁)。

线程安全

不是线程安全,应该是内存安全,堆是共享内存,可以被所有线程访问。

定义

当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的。

堆是进程和线程共有的空间,分全局堆、局部堆。

  • 全局堆:所有没有分配的空间。
  • 局部堆:用户分配的空间。

堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。

在 Java 中,堆是 Java 虚拟机管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。 堆所存在的内存区域的唯一目的就是 存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

  • 栈是每个线程独有的,保存其运行状态和局部变量。
  • 栈在线程开始的时候初始化,每个线程的栈相互独立。因此,栈是线程安全的。
  • 操作系统在切换线程的时候会自动切换栈。
  • 栈空间不需要在高级语言里,显式的分配和释放。

CountDownLatch 和 Semaphore 区别,及其底层原理。

CountDownLatch 表示计数器,可以给 CountDownLatch 设置一个数字,一个线程调用 COuntDownLatchawait() 将会阻塞,其他线程可以调用 CountDownLatchcountDown() 方法来对 CountDownLatch 中的数字 -1,当数字 == 0 时,所有 await 的线程都将被唤醒。

CountDownLatch 底层原理:
调用 await() 方法的线程,会利用 AQS 排队,一旦数字减为 0,则会将 AQS 中排队的线程依次唤醒。


Semaphore 表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使用该信号量,通过 acquire() 来获取许可,如果没有许可可用则线程阻塞,并通过 AQS 来排队,可以通过 release() 来释放许可,当某个线程释放了某个许可后,会从 AQS 中正在排队的第一个线程开始依次唤醒,直到没有空闲许可。
public class CountDownLatchTest {
		static CountDownLatch countDownLatch = new CountDownLatch(3);
		
		public static void main(String[] args) throws InterruptedException {
				
				// 线程1
				countDownLatch.await();
				
				// 线程5, 再次调用 await() 会解除线程1 的阻塞【方式2:解除阻塞】
				countDownLatch.await();
				
				// 线程2
				countDownLatch.countDown();
				
				// 线程3
				countDownLatch.countDown();
				
				// 线程4, 执行 3 次 countDown() 方法,就是解除线程1 的阻塞【方式1:解除阻塞】
				countDownLatch.countDown();
				
		}
}
public class SemaphoreTest {
		// 3个信号量许可
		static Semaphore semaphore = new Semaphore(3);
		
		public static void main(String[] args) throws InterruptedException {
				
				// 线程1
				semaphore.acquire();
				
				// 线程2
				semaphore.acquire();
				
				// 线程3
				semaphore.acquire();
				
				// 线程4, 没有信号量许可了 → 阻塞
				semaphore.acquire();
				
				// 线程1, 释放许可 → AQS 中的线程4 会被唤醒
				semaphore.release();
				
		}
}

ReentrantLock 中 tryLock() 、lock()

tryLock()
- 尝试加锁,可能加到,也可能加不到。
- 不会阻塞线程
- 返回值:加到锁,返回 true;没有加到锁,返回 false


lock()
- 阻塞加锁。
- 线程会阻塞,直到加到锁。
- 返回值:无
public class ReentrantLockTest {
		// 
		static ReentrantLock reentranLock = new ReentrantLock(3);
		
		public static void main(String[] args) throws InterruptedException {
				
				// 阻塞加锁
				reentranLock.lock();
				
				// 其他代码不会执行,释放锁之后才会执行
			
			
			
				// 尝试加锁,非阻塞
				boolean result = reeentranLock.tryLock();
				
				// 自旋锁,消耗 CPU 多,更加灵活
				while (!reentranLock.tryLock()) {
						// 其他事情【尝试加锁,加到锁之后才会执行】
				}
				// 其他代码
				
		}
}

ReentrantLock 中公平锁、非公平锁

公平锁、非公平锁的底层实现,都会使用 AQS 来进行排队。【默认是非公平锁,因为其性能高】

区别:
线程在使用 lock() 加锁时,
如果是公平锁,会先检查 AQS 队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队;如果是非公平锁,则不会检查是否有现成在排队,而是直接竞争锁。

公平锁/非公平锁,一旦没竞争到锁,都会进行排队。当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了 线程加锁阶段,而没有体现在唤醒阶段。

ReentrantLock 是可重入锁。【公平锁、非公平锁都是可重入锁】
public class ReentrantLockTest {
		static ReentrantLock reentranLock = new ReentrantLock();
		
		public static void main(String[] args) throws InterruptedException {
				
				// 阻塞加锁
				reentranLock.lock();
				
				reentranLock.lock();
				
				// 解锁,由于加锁 2次,故要解锁 2次
				reentranLock.unlock();
				
				reentranLock.unlock();
				
		}
}

sleep()、wait()、join()、yield()

锁池

所有需要 竞争同步锁的线程 都会放在锁池当中, 比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待; 当前面的线程释放同步锁后,锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待 CPU 资源分配

等待池

当我们调用 wait() 方法后,线程会放到等待池中,等待池的线程不会去竞争同步锁。 只有调用了 notify()notifyAll() 后等待池的线程才会开始去竞争同步锁。 notify() 是随机从等待池选出一个线程放到锁池,而 notifyAll() 是将等待池的所有线程放到锁池中。

sleep() 和 wait()

  1. sleep() 是 Thread 类的静态本地方法;wait() 是 Object 类的本地方法。
  2. sleep() 不会释放 lockwait() 会释放 lock,并加入到等待队列中。
  3. sleep() 不依赖同步器 synchronizedwait() 需要依赖 synchronized 关键字。
  4. sleep() 不需要被唤醒(休眠之后退出阻塞);wait() 需要被唤醒(不指定时间需要被别人中断)。
  5. sleep() 一般用于当前线程休眠,或者轮询暂停操作;wait() 则多用于多线程直接的通信。
  6. sleep() 会让出 CPU 执行时间且强制上下文切换;wait() 则不一定,wait 后可能还是有机会重新竞争到锁继续执行的。

yield()

yield() 执行后线程进入就绪状态,马上释放了 CPU 的执行权,但是依然保留了 CPU 的执行资格, 所以有可能 CPU 下次进行线程调度时,还会让这个线程获取到执行权继续执行。

join()

join() 执行后线程进入阻塞状态。例如,在线程 B 中调用线程 A 的 join(),则线程 B 会进入到阻塞队列,直到线程 A 结束或中断。

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("222222");
        }
    });
    t1.start();
    t1.join();
    // 这行代码必须要等 t1 全部执行完毕,才会执行
    System.out.println("111");
}

// 执行结果
222222
111
1. 锁池
所有需要竞争同步锁的线程都会放到锁池中。
比如当前对象中的锁已经被其中一个线程得到,则其他线程需要在这个锁池中进行等待,当前面的线程释放同步锁后,锁池中的线程去竞争同步锁,当某个线程得到锁后,就会进行就绪队列进行等待 CPU 资源分配。


2. 等待池
调用 wait() 后,线程会放到等待池中。
等待池的线程不会去竞争同步锁。只有调用了 notify()notifyAll() 后,等待池的线程才会开始去竞争锁。

notify() 是随机从等待池中选出一个线程放到锁池。
notifyAll() 是将等待池的所有线程放到锁池。



sleep()Thread 类的静态本地方法。
					  不会释放 lock。
					  不依赖于同步器 Sychronized
					  不需要被唤醒(休眠之后退出阻塞)
					  一般用于当前线程休眠,或者轮询暂停操作
					  会让出 CPU 执行时间且强制上下文切换
wait()Object 类的本地方法。
						会释放 lock,并且将线程加入到等待队列。
						依赖于同步器 Sychronized
						需要被唤醒(不指定时间,需要被别人中断)
						多用于多线程之间的通信
						不一定让出 CPUwait() 后可能还是有机会重新竞争到锁继续执行
						
						 
yield() 执行后,线程直接进入就绪状态,马上释放了 CPU 的执行权,但是依然保留了 CPU 的执行资格,所以有可能 CPU 下次进行线程调用还会让这个线程获取到执行权继续执行。

join() 执行后,线程进入阻塞状态。
	例如,在线程 B 中调用线程 A.join(), 则线程 B 会进入到阻塞状态,直到线程 A 结束或中断。

Sychronized 的偏向锁、轻量级锁、重量级锁

自旋锁
自旋锁在线程获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程【阻塞和唤醒线程都是需要操作系统去进行的,比较消耗时间】。
自旋锁是线程通过 CAS 获取预期的一个标记,如果没有获取到锁,则继续循环执行,直到获取到锁为止。
这个过程,线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。


偏向锁
在锁对象的对象头中记录一下当前获取到 该锁的线程 id,该线程下次如果又来获取锁就可以直接获取到!

轻量级锁
由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁。
此时,如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁。
此处,之所以称之为轻量级锁,是为了与重量级锁区分。轻量级锁底层是通过自旋来实现的,并不会阻塞线程

重量级锁
如果自旋次数过多,仍然没有获取到锁,则会升级为重量级锁。
重量级锁会导致线程阻塞。

Sychronized 和 ReentrantLock 区别

sychronizedReentranLock
是一个关键字是一个类
会自动加锁/释放锁需要手动加锁/释放锁
底层是 JVM 层面的锁API 层面的锁
非公平锁公平锁/非公平锁【可选】
锁的是对象,锁信息保存在对象头中通过代码中 int 类型的 state 标识,来标识锁的状态
底层有一个锁升级的过程

Thread 和 Runnable

问出该问题的面试官,水平一般。本质是没什么区别的。

ThreadRunnable 的实质是继承关系,没有可比性。
无论使用 Thread 还是 Runnable,都会 new Thread,然后执行 run()
用法上,如果有复杂的线程操作需求,就选择继承 Thread;如果只是简单的执行一个任务,则实现 Runnable
// 会多出一倍的票
// 共享资源,该观点是错误的 ❌
public class Test {
		public static void main(String[] args) {
				new MyThread().start();
				new MyThread().start();
		}
		
		static class MyThread extends Thread {
				private int ticket = 5;
				public void run() {
						while (true) {
								System.out.println("Thread ticket = " + ticket--);
								if (ticket < 0) {
										break;
								}
						}
				}
		}
		
}
// 正常卖出
public class Test2 {
		public static void main(String[] args) {
				MyThread2 mt = new MyThread2();
				new Thread(mt).start();
				new Thread(mt).start();
		}
		static class MyThread2 implements Runnable {
				private int ticket = 5;
				public void run() {
						while (true) {
								System.out.println("Runnable ticket = " + ticket--);
								if (ticket < 0) {
										break;
								}
						}
				}
		}
}
原因:MyTread 创建了 2 个实例,So 自然会卖出两倍,属于用法错误。

实际上,应该将 ticket 定义为 private static,并且加同步锁 Sychronized

run()、start()

start 开启新的线程
run 和 普通 Java 方法没有区别

ThreadLocal 底层原理

1. ThreadLocalJava 所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据。

2. ThreadLocal 底层是通过 ThreadLocalMap 来实现的,每个 Thread 对象(注意不是 ThreadLocal 对象)中都存在一个 ThreadLocalMapMap 的 key 为 ThreadLocal 对象,Map 的value 为需要缓存的值。

3. 如果在线程池中使用 ThreadLocal 会造成内存泄露,因为当 ThreadLocal 对象使用完之后,应该要把设置的 key、value,也就是 Entry 对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向 ThreadLocalMapThreadLocalMap 也是通过强引用指向 Entry 对象,线程不会被回收,Entry 对象也就不会被回收,从而出现内存泄露。

解决方法:
在使用了 ThreadLocal 对象之后,手动调用 ThreadLocalremove(),手动清除 Entry 对象。

4. ThreadLocal 经典的应用场景:连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)
img.png
img.png
public class ThreadLocalTest {
		private static ThreadLocal<String> s = new ThreadLocal<>();
		
		public static void main(String[] args) {
				s.set("siona");
				String s1 = ThreadLocalTest.s.get();
				
				// 手动清除 Entry 对象
				s.remove();
		}
}

ThreadLocal 内存泄露问题,如何避免?

内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果严重,无论多少内存,迟早会被占光。

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
OOM:内存已经不足!!!

强引用:
使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不回收这种对象。

弱引用:
JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
Java 中,用 java.lang.ref.WeakReference 类来表示。
可以在缓存中使用弱引用。

ThreadLocal 实现原理,每一个 Thread 维护一个 ThreadLocalMap,key 为使用弱引用的 ThreadLocal 实例,value 为线程变量的副本。
ThreadLocal 内存泄露.png
ThreadLocal 内存泄露.png
ThreadLocalMap 使用 ThreadLocal 的弱引用作为 key,如果一个 ThreadLocal 不存在外部强引用时,key(ThreadLocal) 势必会被 GC 回收,这样就导致 ThreadLocalMap 中 key 为 null,而 value 还存在着强引用,只有 Thread 线程退出之后,value 的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些 key 为 nullEntry 的 value 就会一直存在一条强引用链(红色链条)

key 使用强引用:
ThreadLocalMap 的 key 为强引用,回收 ThreadLocal 时,因为 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,导致 Entry 内存泄露。

key 使用弱引用:
ThreadLocalMap 的 key 为弱引用,回收 ThreadLocal 时,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。
当 key 为 null,在下一次 ThreadLocalMap 调用 set()get()remove() 时,value 值会被清除。

因此,ThreadLocal 内存泄露的根源:
由于 ThreadLocalMap 的生命周期与 Thread 一样长,如果没有手动删除对应 key,就会导致内存泄露,而不是因为弱引用。


ThreadLocal 正确的使用方法:
1. 每次使用完 ThreadLocal 后,都调用它的 remove() 清楚数据。
2.ThreadLocal 变量定义成 private static,这样就一直存在 ThreadLocal 的强引用,也就能保证任何时候都能通过 ThreadLocal 的弱引用访问到 Entry 的 value 值,进而清除掉。

ThreadLocal 父子线程共享数据

Volatile 和 Sychronized 区别。Volatile 能否保证线程安全?DCL 单例为何要加 Volatile?

1Synchronized 关键字,用来加锁。 
Volatile 只是保持变量的线程可见性。通常适用于一个线程写,多个线程读的场景。

2、不能。Volatile 关键字只能保证线程可见性,不能保证原子性。

3Volatile 防止指令重排。在 DCL 中,防止高并发情况下,指令重排造成的线程安全问题。
public class VolatileDemo {
		public static /*volatile*/ boolean flag = true;
		
		public static void main(String[] args) {
				new Thread(() -> {
						while (flag) {
								
						}
						System.out.println("====== End of Thread1 ======");
				}).start();
				
				try {
						Thread.sleep(100);
				} catch (InterruptedException e) {
						e.printStackTrace();
				}
				
				System.out.println("true flag off");
				flag = false;
		}
}
# 不加 Volatile 关键字,输出以下结果
# 分析:主线程执行完,并设置 flag = true
#      但线程1 一直在 while 循环,即 flag = true,并未感知到 flag = false
true flag off



# 加上 volatile 关键字之后,输出以下结果
# 分析:线程1 感知到 flag = false,执行结束。
true flag off
====== End of Thread1 ======
public class SingleDemo {
		private static SingleDemo singleDemo = new SingleDemo();
		private SingleDemo() {}
		
		// 多个线程创建时,线程不安全 → 加锁
		// 方法 1. 加在方法上,锁粒度太大
		// 方法 2. 方法内加锁
		public static /*sychronized*/ SingleDemo getInstance () {
				if (null == singleDemo) {
						/* 2. 后进来的线程会卡在这里,依然线程不安全
						sychronized(SingleDemo.class) {
								singleDemo = new SingleDemo();
						}
						*/
						
						sychronized(SingleDemo.class) {
								// 检查,加锁之后,再检查一次,就是 DCL (Double Check Lock) 单例
								if (null == SingleDemo) {
										singleDemo = new SingleDemo();
								}
						}
						
				}
				return singleDemo;
		}
}

Volatile 关键字,如何保证可见性、有序性?

指令重排序

“内存可见性” 问题

主内存、工作内存

Java 线程锁机制,锁升级过程。

1JAVA 的锁就是在对象的 Markword 中记录一个锁状态。
		无锁,偏向锁,轻量级锁,重量级锁对应不同的锁状态。

2JAVA 的锁机制就是根据资源竞争的激烈程度不断进行锁升级的过程。
锁机制升级过程.png
锁机制升级过程.png
/**
	* 打印对象在内存中的布局
	*/
public class JOLTest {
		public static void main(String[] args) {
				Object o = new Object();
				System.out.println(ClassLayout.parseInstance(o).toPrintable());
				
				sychronized(o) {
						System.out.println(ClassLayout.parseInstance(o).toPrintable());
				}
		}
}
<!-- pom.xml 文件 -->
<!-- 作用:对象在内存中布局 -->
<dependency>
		<groupId>org.openjdk.jol</groupId>
		<artifactId>jol-core</artifactId>
		<version>0.10</version>
</dependency>
img.png
img.png
img.png
img.png

Java 锁升级的过程及对象头中的信息open in new window

AQS,AQS 如何实现可重入锁?

1AQS 是一个 JAVA 线程同步的框架。是 JDK 中很多锁工具的核心实现框架。

2、在 AQS 中,维护了一个信号量 state 和一个线程组成的双向链表队列。
其中,这个线程队列,就是用来给线程排队的,而 state 就像是一个红绿灯,用来控制线程排队或者放行的。
在不同的场景下,有不用的意义。

3、在可重入锁这个场景下,state 就用来表示加锁的次数。
		0 标识无锁,每加一次锁,state就 +1。释放锁 state 就 -1

如何避免 Java 死锁?

死锁问题的解决策略

如何查看线程死锁?

1. 通过 jstack 命令进行查看,jstack 命令中会显示发生了死锁的线程
2. 两个线程去操作数据库时,导致数据库发生了死锁。
		此时,可以查询数据库的死锁情况。
1. 查询是否锁表
show OPEN TABLES where In_use > 0;

2. 查询进程
show processlist;

3. 查看正在锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;

4. 查看等待锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

线程之间如何进行通信?

1. 线程之间可以通过共享内存 / 基于网络 来进行通信
2. 如果是通过共享内存来进行通信,则需要考虑并发问题,什么时候阻塞,什么时候唤醒
3.Java 中的 wait()notify() 就是阻塞和唤醒
4. 通过网络就比较简单了,通过网络连接将通信数据发送给对方,当然也要考虑到并发问题,处理方式就是加锁等方式

如何保证多个线程之间的执行顺序?同时、依次、有序交错执行

CountDownLatch, CylicBarrier, Semaphore

如何对一个字符串快速进行排序?

Fork/Join框架

守护线程

线程池及其参数解释

package com.siona.study;

import java.util.concurrent.*;

/**
 * 定制线程池
 *
 * @Description TODO
 * @Version 1.0.0
 * @Date 2024/01/05 13:35
 * @Created by Siona
 */
public class ThreadStudy {
    public static void main(String[] args) {

        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        ExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        ExecutorService singleThreadScheduleExecutor = Executors.newSingleThreadScheduledExecutor();


        /**
         * 参数信息:
         * int corePoolSize     核心线程大小
         * int maximumPoolSize  线程池最大容量大小
         * long keepAliveTime   线程空闲时,线程存活的时间
         * TimeUnit unit        时间单位
         * ArrayBlockingQueue<Runnable> workQueue   任务队列
         * threadFactory        线程工厂
         * handler              执行拒绝策略的对象
         */
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                1,
                1,
                0L,
                TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(1),
                new CustomThreadFactory(),
                new CustomThreadRejectionHandler()
        );

        executor.execute(() -> {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 测试:每 5 秒去抛出一个错误
            throw new RuntimeException("我报错了~");
        });

        executor.execute(() -> {
            System.out.println("-");
        });

        executor.execute(() -> {
            System.out.println("这是一个超过队列大小的任务");
        });

        executor.shutdown();
    }


    /**
     * 自定义线程工厂
     */
    static class CustomThreadFactory implements ThreadFactory {

        @Override
        public Thread newThread(Runnable runnable) {
            Thread thread = new Thread(runnable);
            thread.setName("自定义线程工厂");

            // 异常自定义处理
            thread.setUncaughtExceptionHandler((t, e) -> {
                System.out.println(t.getName() + "::::::" + e.getMessage());
            });

            return thread;
        }
    }

    /**
     * 自定义线程拒绝策略
     */
    static class CustomThreadRejectionHandler implements RejectedExecutionHandler {

        @Override
        public void rejectedExecution(Runnable runnable, ThreadPoolExecutor threadPoolExecutor) {
            // 打印日志、暂存任务、重新执行等拒绝策略
            System.out.println("这个任务被拒绝了~");
        }
    }

}
img.png
img.png
核心线程大小:根据业务类型和 CPU 核心数考虑,如果线程平均工作时间所占比例越高,那么线程数量应该少一些;如果线程平均等待时间比较高,则需要多一些线程; 

线程池最大容量大小:最好是根据 CPU 密集型或者 IO 密集型区分开来设计;

线程空闲时间、存活时间:空闲时间达到多少之后线程才会销毁;

线程池的相关参数:核心线程大小、线程池最大容量大小、线程空闲时间及线程存活时间、任务队列、执行拒绝策略

线程池处理流程

线程池底层工作原理

线程池 -> 线程复用原理

img.png
img.png
// 原来 Thread 去创建线程时,必须去对应一个任务
new Thread(new Runnable() {
    @Override
    public void run() {
        
    }
}).start();
线程池会使用一个固定线程大小,或者说可变数量的线程执行任务。

实际应用场景中,设置的线程数量远远小于任务数量。

故线程池可以通过 “线程复用”,让同一个线程执行不同的任务。

实现 “线程复用” 原理:线程池可以将线程和任务解耦,线程是线程、任务是任务。

摆脱了,原来通过 Thread 去创建线程时,必须对应一个任务。

- 线程池中,同一个线程可以从 BlockingQueue 阻塞队列中不断读取新任务,
    
    原理:线程池对 Thread 进行封装,每一个线程去执行一个循环任务,在循环任务中,不断检查是否有新任务。如果有新任务,则去调用该任务的 run() 方法。

CAS 同步机制

compare and swap,解决多线程并行情况下使用锁造成性能损耗的一种机制,

CAS 操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)

如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。

无论哪种情况,它都会在CAS指令之前返回该位置的值。

CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。

优点:

- 避免加锁
- 提高前期的运行效率

CAS 缺点

1. ABA 问题
当前的值是否与我们预期的值是否是一样的,
ABA
比较的时候已经是 A 了,CAS 不会去关注是否发生了变化。
改进:加上版本号,A1B2C3

# 2. 自旋时间过长
轻量级锁:
单次 CAS 不一定能执行成功的,配合循环(死循环)去进行重试,直到线程竞争不激烈的时候才成功。
如果是高并发场景下,CAS 算法效率是不高的。
故,synchronized 会从轻量级锁,升级成重量级锁。

3. 范围不能灵活控制
针对某一个共享变量(intlong、对象)去进行 CAS
不能针对多个共享变量去执行,因为多个共享变量是相互独立的状态。
不能简单的把原子操作组合到一起,它们不具有原子性。
如果想对多个共享变量同时进行 CAS 操作,需要保证线程安全,Java 提供了解决方案:new 一个新的类,整合刚才的那一组共享变量。
首先什么是CAS,CAS是一个算法,主要包括三个操作数–内存位置V,预期原值A,新值B

分为三个步骤: 
1.读取内存中的值 
2.将读取的值和预期的值进行比较 
3.如果比较的结果符合预期,则写入新值;如果不符合,则什么都不做 


那它有什么缺点呢:
1.ABA问题 
2.自旋时间过长 
3.范围不能灵活控制 

ABA问题:使用CAS算法实现乐观锁时会存在这个问题,
简单来说就是内存的V值被两个线程AB读取到,值都为0,其中线程A将值改为1提交到内存中.然后线程C又将值A改回0,线程B提交发现值还是0,没有变化,所以它也成功提交.解决ABA问题可以使用版本号机制.JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力. 

自旋时间过长:自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销.synchronized的自旋锁就存在这种问题,这是一种忙等,通过短时间的忙等,换取线程在用户态和内核态之间切换的开销。 

范围不能灵活控制:从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作.

简述线程池原理,FixedThreadPool 的阻塞队列。

线程池中阻塞队列的作用?为什么是先添加列队,而不是先创建最大线程?

如果提交任务时,线程池队列已满,此时会发生什么?

线程池的拒绝策略

AtomicInteger 在高并发下,性能问题

如何瞬间完成海量数据检索?

如何用 10 行代码实现一个负载均衡?

ServerLess ❓

为什么多线程会带来性能问题?

Last Updated 8/18/2024, 2:26:38 PM
目录