八股打卡五
Java中的锁(回顾复习)
公平锁/非公平锁
公平锁:按照申请锁的顺序获取锁;
非公平锁:不按顺序,可抢占。
java中synchronized为非公平锁,ReentrantLock默认是非公平锁,可以设置为公平锁。
可重入锁
一个线程在外部方法获取了锁,进入内层方法时会自动获取锁。
synchronized和ReentrantLock都是可重入锁。
共享锁/独占锁
共享锁:能被多个线程持有;
独占锁:只能被一个线程持有。
synchronized和reentrantLock都是独占锁,ReadWriteLock中Read是共享锁,Write是独占锁。
乐观锁/悲观锁
悲观锁认为对同一数据的并发读写操作一定发生,需要加锁;
乐观锁任务对同一数据的并发读写操作不会发生,更新数据时采用尝试更新的方式。
悲观锁适用于写操作多的场景,加锁编程;乐观锁适用于读操作多的场景,无锁编程,常采用CAS操作。
偏向锁/轻量级锁/重量级锁
当一段同步代码被一个线程一直访问,那么这个线程就获得了偏向锁,此时如果有其他线程试图访问,那么偏向锁会升级成轻量级锁,其他线程通过自旋的形式尝试获取锁,但是不会阻塞,当自旋到一定的次数时还没有获取到锁,线程就会进入阻塞,此时轻量级锁升级成重量级锁。
自旋锁
尝试获取锁的线程不会立即被阻塞,而是采取循环的方式尝试获取锁,好处是可以减少线程上下文切换的损耗,缺点是循环消耗cpu。
synchronized关键字
- 它是java中的关键字,用于线程同步。被synchronized修饰的方法或代码块会成为一个临界区,某一时刻只能由一个线程进入访问,其他线程必须等待该线程退出后才能访问。
- 用于修饰方法或代码块。修饰方法,则该方法被锁定;修饰代码块,则代码块被锁定;好处是能够选择性地锁定对象的一部分。
- 基于JVM的内置锁实现,被synchronized修饰的方法一旦执行,即使线程阻塞也不会被中断,所以要保证程序设计的合理性,避免线程死锁的发生。
synchronized vs. Lock
两者都是java线程同步的手段。
- synchronized是java关键字,基于JVM的内置锁实现,用于修饰方法或代码块,使用比较简单。
Lock是一个接口,java提供的一种显式锁机制,需要手动获取和释放锁。通过实现类(ReentrantLock)创建锁对象,调用锁的加锁和释放锁方法。 - synchronized灵活性不足。它只能修饰方法和代码块,被修饰的方法或代码块一旦执行,即使线程阻塞也不能中断,没有超时获取锁的机制,通过synchronized获取所得线程得不到锁就会一直等待,没有公平性概念。
Lock相对比较灵活。它提供了尝试获取锁的机制,当锁被其他线程持有,可以选择等待或者中断等待。提供了超时获取锁的能力,可以在指定时间内尝试获取锁,可以设置为公平锁,按照申请顺序获取锁。 - synchronized与wait() / notify()、notifyAll()一起使用。
Lock与Condition接口结合,提供一种更加细粒度的锁机制。await()/ signal()、signalAll()。 - synchronized适用于锁粒度小、竞争不激烈、实现简单的场景, Lock用于复杂的同步控制。
synchronized vs. ReentrantLock
两者都是java线程同步的手段。
- synchronized是java中的关键字,基于JVM的内置锁实现,用于修饰方法和代码块,使用比较简单。ReentrantLock是java.util.concurrent.locks包下的一个锁实现类,需要显示创建,通过lock和unlock方法来管理锁的获取与释放。
- synchronized灵活性不足。只能修饰方法和代码块,被修饰的方法和代码块一旦开始执行,即使线程阻塞也不会中断。没有超时获取锁的能力,得不到锁就一直等待,没有公平性概念。
ReentrantLock灵活性较好。它支持中断,可以在等待锁的过程中响应中断。提供了尝试获取锁的机制,通过tryLock()方法能够设置超时时间,可以设置公平锁,按照申请顺序获取锁,通过isLocked和isFair判断锁的状态。 - synchronized与wait() / notify()、notifyAll()一起使用。
ReentrantLock与Condition接口结合,提供一种更加细粒度的锁机制。await()/ signal()、signalAll()。 - 锁绑定多个条件
synchronized和单个条件关联,需要调用多个方法才能实现复杂的条件判断。
ReentrantLock可以与不同的Condition关联,每个Condition有自己的唤醒和等待逻辑。 - synchronized适用于锁粒度小、竞争不激烈、实现简单的场景, ReentrantLock用于复杂的同步控制。
volatile关键字
volatile关键字通常被喻为轻量级的synchronized,它是java并发编程中比较重要的一个关键字,它不能修饰方法和代码块,只能修饰变量。
它的作用有两个:保证变量的内存可见性、禁止指令重排。
- 保证可见性:当线程对volatile变量修改时,新的值能够对其他线程立即可见。
对非volatile变量进行读写操作时,每个线程从主内存中拷贝变量到cpu缓存中,当计算机有多个cpu的时候,线程可能分别被不同的cpu处理,这意味着线程将变量拷贝到不同的cpu缓存中。而volatile变量不能缓存在寄存器和其他处理器不可见的地方,所以它的读写在主内存中完成,确保一旦修改所有线程都能看到。 - 禁止指令重排:volatile变量的写操作在JVM执行时不会发生指令重排序,保证写操作在读操作之前完成。
指令重排是JVM为优化指令/提高运行效率,在不影响单线程程序执行结果前提下提高并行度的操作。在对volatile变量读写操作时,会在其前后插入内存屏障,后面的指令不能被重排到内存屏障。
虽然volatile保证了可见性,但是无法保证复合操作原子性。
volatile vs. synchronized
两者都是多线程同步的工具。
- 用途:volatile只能修饰变量,保证变量在多线程之间的可见性。synchronized用于修饰方法和代码块,解决的是多线程之间资源访问的同步性。
- 原子性:volatile只能保证单个读写操作的原子性,对于复合操作(自增、自减)无法保证原子性。synchronized能够保证被修饰方法或代码块的原子性,一旦开始执行就不会被打断。
- 性能:volatile更加轻量级,没有获取和释放锁的开销,性能要更好,但是同步级别也低。
- 互斥性:volatile没有互斥性,只是确保变量的可见性。synchronized有互斥性,同一时间只能有一个线程访问被修饰的方法或代码块。
- 使用场景:volatile适用于简单的内存可见性要求,synchronized适合于原子性、可见性、互斥的复杂同步场景。
线程池的好处
- 资源管理:在多线程应用中,每个线程占有内存和cpu资源,如果不加限制地创建线程,那么系统资源会被很快耗尽,引发系统崩溃地问题。所以线程池通过限制线程地创建数量能够避免这个问题。
- 提升性能:可以重用已经创建地线程,减少创建和销毁线程的开销。
- 任务排队:任务队列和工作线程配合,合理分配任务,保证任务按照一定顺序执行,避免线程竞争和冲突。
- 统一管理:线程池提供了统一管理线程的方式,实现对线程的监控和调度。
线程池参数
- corePoolSize核心线程数:线程池中长期存活的线程数。
- maximumPoolSize最大线程数:线程池能够创建的最大线程数量。当任务队列满时,可以创建的最大线程数。
- keepAliveTime空闲线程的存活时间:当线程数大于核心线程数时,多余的空闲线程等待新任务的最长时间。
- TimeUnit:与keepAliveTime一起使用,表示空闲线程存活时间的单位,如秒、分钟等。
- workQueue任务队列线程池中存放所有待执行任务的队列。
- ThreadFactory创建线程的工厂:线程池创建线程会调用该方法,该方法可以指定线程的优先级、线程的命名规则和线程类型。
- RejectedExecutionHandler拒绝策略:当线程池中的任务数大于任务队列所能容纳的最大数量时采取的策略。
BIO/NIO/AIO
三者是Java中的IO模型,在处理输入输出上有着不同的特性。
BIO: 阻塞式IO模型,当线程执行IO操作时,若数据未到达,那么线程会进入阻塞直到数据到达。
NIO: 非阻塞IO模型,采用缓冲区和管道的方式处理数据,提高IO效率,支持面向缓冲区的读写操作。
AIO:异步IO模型,当一个IO操作发起,线程可以继续执行别的任务,IO操作完成,操作系统会通知线程。适合处理大量并发IO操作,但是不会引起线程阻塞的场景。
使用场景:
BIO:低并发、少连接;
NIO:高并发、大量连接;
AIO:高性能,异步IO操作。
Java内存区域
Java内存区域分为以下几个部分:
程序计数器
程序计数器是一块较小的内存空间,可以看作时当前线程执行的字节码的行号指示器。多线程环境下,每个线程都有自己的程序计数器,线程执行方法时,程序计数器记录的是正在执行的虚拟机字节码指令的地址。
Java虚拟机栈
每个Java线程都有一个Java虚拟机栈,与线程同时创建。每个方法执行会创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法接口等信息,方法调用时栈帧入栈,方法返回时出栈。
本地方法栈
本地方法栈与Java虚拟机栈类似,但是为本地方法服务。本地方法用其他语言(c/c++)编写,通过JNI与Java代码交互。
堆
Java堆是Java虚拟机中最大的一块内存,用于存储对象实例。所有对象实例和数组都在堆中分配内存。堆被所有线程共享,在Java虚拟机启动时创建。
方法区
方法区存放虚拟机已经加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。运行时常量池是方法区的一部分,存储编译期产生的类、方法和常量等信息。
Java引用四种方式
这四种引用决定了对象的生命周期,垃圾收集器如何回收垃圾。
- 强引用:Java最常见的引用类型。当一个对象是强引用,垃圾收集器不会回收它。
- 软引用:用于还有用但是非必需的对象。如果一个对象只有软引用指向它,那么在系统内存不足时,垃圾收集器会尝试回收它。常用于内存敏感的缓存,内存不足时释放缓存中的对象。
- 弱引用:它的生命周期比软引用要短,如果一个对象只有弱引用指向它,那么下一次垃圾回收时,不管系统内存是否充足,它都会被回收。常用于实现对象缓存,但不希望缓存的对象影响垃圾回收的情况。
- 虚引用:Java中最弱的引用类型。如果一个对象只有虚引用指向它,那么任何时候它都有可能被垃圾收集器回收。但在回收之前,它会被放入一个队列共程序员处理。虚引用主要用于跟踪对象被垃圾回收的时机,进行一些必要的清理或记录。