1.Object类是所有类的父类(Object提供了11 个方法)
1. clone()
保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。
2. getClass()
final方法,返回Class类型的对象,反射来获取对象。
3. toString()
该方法用得比较多,一般子类都有覆盖,来获取对象的信息。
4. finalize()
该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用。
5. equals()
比较对象的内容是否相等
6. hashCode()
该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。
7. wait()
wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生。
其他线程调用了该对象的notify方法。
其他线程调用了该对象的notifyAll方法。
其他线程调用了interrupt中断该线程。
时间间隔到了。
此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
8. notify()
该方法唤醒在该对象上等待的某个线程。
9. notifyAll()
该方法唤醒在该对象上等待的所有线程。
2.JVM的构成
1. 类加载子系统(Class Loader)
功能:负责从文件系统或网络中加载Class信息。加载的类信息被存放在方法区。
特点:类加载器只管加载,只要符合文件结构就加载,至于能否运行,则由执行引擎负责。
启动类加载器(Bootstrap ClassLoader):用于加载Java核心类库。
扩展类加载器(Extension ClassLoader):用于加载额外的类库。
应用程序类加载器(Application ClassLoader):用于加载应用程序的类。
2. 运行时数据区(Runtime Data Areas)
运行时数据区是JVM的核心内存空间结构模型,包含以下几个部分:
程序计数器(Program Counter Register)
功能:指示Java虚拟机下一条需要执行的字节码指令。
特点:占用内存空间较小,是线程私有的。
Java虚拟机栈(VM Stack)
功能:每个虚拟机线程都有一个私有的栈,用来保存局部变量、方法参数以及方法的调用和返回值等信息。
特点:线程私有,随线程的创建而创建,随线程的结束而销毁。
本地方法栈(Native Method Stack)
功能:与Java虚拟机栈类似,但主要用于执行本地方法(如C语言编写的方法)。
特点:线程私有。
Java堆(Heap)
功能:是Java程序最主要的内存工作区域,用于存放对象实例和数组。
特点:线程共享,在JVM启动时创建。堆被划分为年轻代、老年代和永久代(在JDK8中取消了永久代,引入了元空间Meta Space)。
方法区(Method Area)
功能:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
特点:线程共享。在JDK8之前,方法区属于永久代;在JDK8及之后,永久代被移除,方法区被移到了本地内存中,即元空间。
3. 执行引擎(Execution Engine)
功能:执行虚拟机的字节码,将其转换成机器码后执行。
特点:JVM的核心组件,负责解释和执行Java字节码。
4. 本地库接口(Native Interface)
功能:作为JVM与操作系统之间的桥梁,融合不同的开发语言为Java所用。
特点:允许Java调用其他语言(如C语言)编写的代码和库。
5. 垃圾收集系统(Garbage Collection System)
功能:自动管理内存,回收不再被使用的对象所占用的内存空间。
特点:Java的核心机制之一,减轻了开发人员的内存管理负担。
2.1
类加载系统的运行流程
1.加载(Loading)
功能:通过一个类的全限定名获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
方式:加载.class文件的方式多样,可以从本地系统中直接加载、从网络获取、从zip压缩文件中读取(如jar、war包)、运行时计算生成(如动态代理技术)、由其他文件生成(如JSP)等。
2.连接(Linking)
验证(Verification):确保加载的类信息符合JVM规范,没有安全方面的问题。主要包括文件格式验证、元数据验证、字节码验证和符号引用验证。
准备(Preparation):为类的静态变量分配内存,并设置这些变量的默认初始值(通常是零值,但final修饰的static变量在编译时就已经赋值)。
解析(Resolution):将常量池中的符号引用替换为直接引用的过程。这通常发生在初始化阶段之前,但也可能在某些情况下在初始化阶段之后进行,以支持Java语言的运行时绑定。
3.初始化(Initialization)
功能:执行类构造器<clinit>()方法的过程。这个方法由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生。
特点:<clinit>()方法不需要定义,且在多线程环境下被正确加锁和同步,以确保一个类的<clinit>()方法在多线程环境中只被执行一次。
4.使用(Using)
功能:类被初始化后,其方法就可以被JVM调用,类的实例也可以被创建和使用。
5.卸载(Unloading)
功能:当类不再被需要时(如程序执行完毕或类加载器被回收),JVM会卸载这个类,释放其占用的资源。
1.双亲委派模型(Parent Delegation Model)
功能:这是一种类加载器的层次结构模型,要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。当一个类加载器需要加载某个类时,它会先委托给父类加载器去加载,只有当父类加载器无法加载时,才由自己去加载。
目的:这种模型的主要目的是确保Java核心库的类型安全,防止用户自定义的类覆盖Java的核心类。
2.缓存机制
功能:类加载器在加载类时会缓存已加载的类信息,以避免重复加载。这通常是通过在内存中维护一个已加载类的映射表来实现的。
目的:提高类加载的效率,减少内存消耗。
3.动态代理技术
功能:动态代理是一种在运行时动态创建接口实现类的技术。通过动态代理,可以在不修改原有类代码的情况下,为类添加新的行为。
实现方式:在类加载过程中,动态代理技术可以通过加载时计算生成代理类的字节码,并将其加载到JVM中,从而实现对原有类的增强。
举例说明
假设我们有一个自定义的类com.example.MyClass,位于项目的classpath目录下,现在需要加载这个类。
应用程序类加载器(Application ClassLoader)接收任务:
当Java虚拟机需要加载com.example.MyClass类时,首先会由系统类加载器(即应用程序类加载器)接收这个任务。
向上委托:
应用程序类加载器会先检查自己是否已经加载过这个类(即检查方法区中是否已经存在该类的Class对象)。如果没有,它会将这个加载请求委托给它的父类加载器——扩展类加载器。
扩展类加载器同样会先检查自己是否已经加载过这个类,如果没有,它会继续将这个任务委托给它的父类加载器——引导类加载器。
引导类加载器尝试加载:
引导类加载器会检查自己负责的区域(主要是Java的核心库)中是否存在这个类。由于com.example.MyClass显然不是Java的核心类,因此引导类加载器无法加载这个类。
向下查找加载:
既然父类加载器无法加载这个类,任务就会回传给子类加载器。首先,扩展类加载器会在它负责的区域(JRE_HOME/lib/ext目录)中查找这个类,但同样找不到。
最后,任务回传给应用程序类加载器,它会在用户类路径(classpath)下查找这个类,并成功找到com.example.MyClass的.class文件,然后加载这个类到JVM中。
2.1 垃圾回收的运行流程
1. 判定垃圾对象
JVM通过一定的算法来判断哪些对象是“垃圾”,即不再被使用的对象。常用的判定算法有两种:
引用计数法(Reference Counting):为每个对象维护一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器就减1。任何时刻计数器为0的对象就是不可达对象,即垃圾对象。但这种方法存在循环引用的问题,因此JVM实际使用中并未采用。
可达性分析算法(Reachability Analysis):通过一系列称为“GC Roots”的根对象作为起始点,从这些根对象开始向下搜索,所走过的路径称为引用链(Reference Chain)。如果一个对象到GC Roots没有任何引用链相连,即该对象是不可达的,则证明此对象是不可用的,可以判定为垃圾对象。
2.垃圾回收的执行阶段
选择垃圾回收算法:
标记-清除算法(Mark-Sweep):首先标记出所有需要回收的对象,然后统一回收这些对象所占用的内存空间。这种方法简单但容易产生内存碎片。
复制算法(Copying):将内存分为大小相同的两块,每次只使用其中一块。当这块内存快满时,就将还存活的对象复制到另一块内存上,然后清理掉正在使用的内存块中的所有对象。这种方法效率高,但内存利用率低。
标记-整理算法(Mark-Compact):结合了标记-清除算法和复制算法的特点,首先标记出所有需要回收的对象,然后将所有存活的对象压缩到内存的一端,最后清理掉边界以外的内存。这种方法避免了内存碎片的产生,同时提高了内存利用率。
执行垃圾回收:
根据选择的垃圾回收算法,JVM会执行相应的垃圾回收操作。以复制算法为例,当新生代中的Eden区(伊甸区)快满时,JVM会触发一次Minor GC(年轻代垃圾回收):
将Eden区中还存活的对象复制到一个Survivor区(幸存区)中;
如果Survivor区也满了,就将该Survivor区中还存活的对象复制到另一个Survivor区中,并清空原Survivor区的所有对象;
经过多次GC后,如果对象仍然存活,则将其晋升到老年代中。
现在使用可达性分析算法而不是引用计数法作为JVM(Java虚拟机)中的主流垃圾回收算法,主要基于以下几个原因:
一、引用计数法的缺陷
无法解决循环引用问题:
引用计数法通过为每个对象维护一个引用计数器来记录其被引用的次数。然而,当两个或多个对象相互引用形成循环时,即使这些对象已经不再被程序中的其他部分所使用,它们的引用计数器也不会变为零,因此无法被垃圾回收器回收。这种情况下,会导致内存泄漏。
增加内存开销:
引用计数法需要为每个对象额外存储一个引用计数器,这增加了内存的占用。在对象数量庞大的情况下,这种额外的内存开销会变得非常显著。
影响性能:
每次对象的引用发生变化时(如赋值、解引用等),都需要更新相应的引用计数器。这个操作会增加程序的执行时间,尤其是在多线程环境下,为了保证引用计数的准确性,还需要进行额外的同步操作,进一步降低了性能。
二、可达性分析算法的优势
能够处理循环引用:
可达性分析算法通过一组称为“GC Roots”的根对象作为起始点,从这些根对象开始向下搜索,能够被搜索到的对象称为“可达对象”,而不能被搜索到的对象则被认为是垃圾对象。这种方法可以有效地解决循环引用的问题,避免了内存泄漏的发生。
更高的准确性:
可达性分析算法通过对整个对象图进行搜索来确定哪些对象是可达的,哪些是不可达的。这种方法的准确性高于引用计数法,因为它能够处理更加复杂的引用关系。
更好的内存利用率:
在可达性分析算法中,垃圾回收器会进行两次标记过程来优化内存布局。第一次标记过程将所有可达对象标记为“存活对象”,第二次标记过程则会对存活对象进行再次标记,并重新整理内存布局,消除内存碎片。这个过程可以提高内存的利用率和系统的性能。
适应现代JVM的需求:
随着JVM的发展,现代应用对内存管理的需求越来越高。可达性分析算法作为一种更加成熟和高效的垃圾回收算法,能够更好地满足现代JVM的需求,提供稳定可靠的内存管理服务。
综上所述,由于引用计数法存在无法解决循环引用、增加内存开销和影响性能等缺陷,而可达性分析算法则具有能够处理循环引用、更高的准确性和更好的内存利用率等优势,因此现在JVM中普遍采用可达性分析算法作为主流的垃圾回收算法。
3.java中多线程与线程池
3.1线程的状态:
1.新建状态(New):
新创建了一个线程对象,但还没有调用start()方法。此时,线程处于初始化阶段,尚未开始执行。
2.就绪状态(Runnable):
当线程对象创建后,其他线程调用了该对象的start()方法,线程进入就绪状态。在这个状态下,线程已经做好了运行的准备,位于“可运行线程池”中,只等待CPU的分配来执行程序代码。此时,线程除CPU之外,其他运行所需资源都已全部获得。
3.运行状态(Running):
当就绪状态的线程获取了CPU时间片后,线程进入运行状态,执行程序代码。这是线程执行过程中最重要的一个状态。
4.阻塞状态(Blocked):
线程因为某种原因放弃CPU使用权,暂时停止运行。阻塞状态是线程生命周期中的一种重要状态,它可以进一步细分为以下几种情况:
等待阻塞:线程执行了wait()方法,会释放占用的所有资源,JVM会把该线程放入“等待池”中。这个状态不能自动唤醒,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒。
同步阻塞:线程在获取对象的同步锁时,如果该同步锁被其他线程占用,则JVM会把该线程放入“锁池”中等待。
其他阻塞:线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程会重新转入就绪状态。
5.等待状态/超时等待(Waiting/Timed_Waiting):
线程进入等待状态通常是因为调用了sleep()或wait()等方法。与阻塞状态类似,但等待状态更侧重于线程主动放弃CPU使用权并进入等待,而阻塞状态可能由外部因素(如I/O请求)导致。
sleep()是Thread类中的方法,用于让当前线程暂停执行一段时间,但不释放锁资源。wait()是Object类中的方法,用于让线程放弃当前对象的锁,并等待其他线程调用notify()或notifyAll()方法。
6.终止状态(Terminated/Dead):
线程执行完了或者因异常退出了run()方法,此时线程结束生命周期,进入终止状态。一旦线程进入终止状态,就不能再被复生。
3.2线程池的创建方式
1. Executors.newFixedThreadPool(int nThreads)
创建方式:通过Executors工厂类的newFixedThreadPool方法,可以创建一个固定大小的线程池。
优点:
线程池大小固定,可预测性较好,控制线程数量有助于避免过多的线程竞争资源。
线程可以重用,减少创建和销毁线程的开销。
缺点:
如果任务过多,超过线程池的大小,那么超出的任务需要等待线程池中的线程空闲出来才能继续执行,可能会增加任务的等待时间。
如果任务执行时间长短不一,长时间执行的任务可能会导致后续任务长时间等待。
2. Executors.newCachedThreadPool()
创建方式:通过Executors工厂类的newCachedThreadPool方法,可以创建一个可缓存的线程池。
优点:
线程池的大小不是固定的,而是根据需要动态调整的。
如果线程池中的线程空闲超过一定时间(默认为60秒),则会被自动回收。
适用于执行大量短期异步任务的场景,可以提高程序效率。
缺点:
可能会创建大量的线程,如果任务执行时间很短,且任务提交频繁,可能会导致系统创建大量线程,增加系统资源消耗。
线程的生命周期较短,频繁地创建和销毁线程也会带来一定的开销。
3. Executors.newSingleThreadExecutor()
创建方式:通过Executors工厂类的newSingleThreadExecutor方法,可以创建一个单线程的线程池。
优点:
线程池中只有一个线程,可以保证任务按照提交的顺序执行,避免了并发执行带来的问题。
适用于需要顺序执行任务的场景。
缺点:
线程池只有一个线程,如果任务执行时间较长,后续任务会长时间等待。
并发性能较低,不适合高并发的场景。
4. Executors.newScheduledThreadPool(int corePoolSize)
创建方式:通过Executors工厂类的newScheduledThreadPool方法,可以创建一个支持定时及周期性任务的线程池。
优点:
支持定时任务和周期性任务的执行。
线程池中的线程可以重用,减少线程创建和销毁的开销。
缺点:
与固定大小的线程池类似,如果任务过多,超出的任务需要等待线程空闲。
如果任务执行时间不确定,可能会影响定时任务的准时性。
5. ThreadPoolExecutor(直接创建)
创建方式:直接使用ThreadPoolExecutor的构造函数创建线程池,这种方式提供了最全面的配置选项。
优点:
完全自定义线程池的行为,包括核心线程数、最大线程数、空闲线程存活时间、任务队列类型等。
可以根据不同的业务场景和需求,灵活地配置线程池。
1.corePoolSize(核心线程数):
定义:线程池中的核心线程数量,即在没有任务需要执行时线程池的基本大小。这些线程会一直保持存活,即使它们处于空闲状态。
重要性:决定了线程池能够处理的最小并发任务数。
2.maximumPoolSize(最大线程数):
定义:线程池中允许的最大线程数量。当工作队列已满,且已创建的线程数小于最大线程数时,线程池会尝试继续创建新线程来处理任务。
重要性:限制了线程池能够同时处理的最大任务数,有助于避免系统资源耗尽。
3.keepAliveTime(线程存活时间):
定义:当线程数大于核心线程数时,多余的空闲线程在终止前等待新任务的最长时间。
重要性:有助于减少空闲线程的数量,从而节省系统资源。
4.unit(时间单位):
定义:keepAliveTime参数的时间单位,如秒(SECONDS)、毫秒(MILLISECONDS)等。
重要性:与keepAliveTime配合使用,确保时间参数的准确性。
5.workQueue(工作队列):
定义:用于存放待执行任务的阻塞队列。当所有核心线程都在工作时,新任务会被添加到这个队列中等待执行。
重要性:决定了任务调度的顺序和线程池的饱和策略。常见的实现类有LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue等。
6.threadFactory(线程工厂):
定义:用于创建新线程的工厂类,可以定制线程对象的创建,例如设置线程名字、是否是守护线程等。
重要性:提供了对线程创建过程的控制,有助于实现更复杂的线程池配置。
7.handler(拒绝策略):
定义:当线程池和工作队列都满了,无法再处理新任务时,会触发拒绝策略。
重要性:确保了线程池在饱和状态下的行为是可预测和可控的。常见的拒绝策略有AbortPolicy(直接抛出异常)、CallerRunsPolicy(调用者线程执行)、DiscardPolicy(丢弃任务)和DiscardOldestPolicy(丢弃最老的任务)等。
LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue 都是 Java 中常用的阻塞队列实现,它们在线程池等多线程场景中扮演着重要角色,用于保存等待执行的任务。以下是它们之间的主要区别:
1. 容量与存储机制
LinkedBlockingQueue:
容量:可以选择无界或有界。无界情况下,容量可以非常大,实际上是 Integer.MAX_VALUE。有界情况下,可以在创建队列时指定容量。
存储机制:基于链表的数据结构,内部维持着一个数据缓冲队列(该队列由链表构成)。
ArrayBlockingQueue:
容量:有界,且容量在创建队列时需要指定,一旦设置就无法更改。
存储机制:基于数组的数据结构,内部维持着一个定长数据缓冲队列(该队列由数组构成)。
SynchronousQueue:
容量:无容量,或者说其容量为1(但这里的1并不代表实际存储的容量,而是指其操作特性)。
存储机制:不真正存储元素,每个插入操作必须等待相应的删除操作,反之亦然。
2. 并发性能与锁机制
LinkedBlockingQueue:
并发性能:通常表现较好,尤其是在线程多、队列长度长的情况下。
锁机制:对生产者端和消费者端分别采用了独立的锁来控制数据同步,提高了并发性能。
ArrayBlockingQueue:
并发性能:一般,且在生产者放入数据和消费者获取数据时共用同一个锁对象,这意味着两者无法真正并行运行。
锁机制:基于 ReentrantLock 锁和 Condition 条件变量实现线程安全。
SynchronousQueue:
并发性能:在特定场景下(如线程少、队列长度短)表现稳定且高效。
锁机制:根据构造时的公平性或非公平性选择,使用公平锁或非公平锁。
3. 使用场景
LinkedBlockingQueue:
适用于任务量不断增加且无限制的场景,可以无限制地添加任务。
适用于高并发、队列长度可能很长的场景。
ArrayBlockingQueue:
适用于任务量有限且已知的场景,可以根据需求设置合理的容量。
适用于需要控制队列大小,避免内存占用过大的场景。
SynchronousQueue:
适用于任务执行的过程需要严格的同步,任务的执行和处理是一对一的关系。
适用于生产者-消费者模型中,生产者线程和消费者线程之间的直接传递,避免任务堆积。
4. 优缺点总结
LinkedBlockingQueue:
优点:适用于任务量不断增加的情况,可以无限制地添加任务,适合使用在不限制任务数量的场景。
缺点:无固定容量可能导致内存占用过大,需要注意内存管理。
ArrayBlockingQueue:
优点:适用于任务量有限且已知的情况,可以根据需求设置合理的容量,避免内存占用过大。
缺点:队列容量固定,可能导致任务被丢弃,如果没有设置合理的容量,可能会导致任务阻塞。
SynchronousQueue:
优点:适用于任务执行的过程需要严格的同步,任务的执行和处理是一对一的关系。
缺点:没有缓冲区,如果没有立即找到匹配的生产者或消费者,插入和删除操作都会被阻塞。
4.java中常用的锁
1. 乐观锁与悲观锁
乐观锁:认为一个线程去拿数据的时候不会有其他线程对数据进行更改,所以不会上锁。乐观锁的实现通常依赖于CAS(Compare-And-Swap)机制或版本号机制。在Java中,以Atomic开头的包装类(如AtomicBoolean、AtomicInteger、AtomicLong)就是乐观锁的一种实现。
悲观锁:认为一个线程去拿数据时一定会有其他线程对数据进行更改,所以一个线程在拿数据的时候都会顺便加锁,这样别的线程此时想拿这个数据就会阻塞。Java中的synchronized关键字和Lock的实现类(如ReentrantLock)都是悲观锁的实现。
2. 公平锁与非公平锁
公平锁:按照请求锁的顺序来获取锁,即“先来后到”。这种锁可以确保线程获得锁的顺序是公平的,但可能会降低并发性能。
非公平锁:不保证获取锁的顺序,即线程获取锁的顺序可能与请求锁的顺序不一致。这种锁可以提高并发性能,但可能会导致某些线程长时间等待。
3. 可重入锁与不可重入锁
可重入锁:同一个线程可以多次获取同一把锁,而不会发生死锁。在Java中,ReentrantLock就是一个可重入锁的实现。
不可重入锁:一个线程针对同一把锁加锁两次,会发生死锁。Java标准库中并没有直接提供不可重入锁的实现,但可以通过自定义锁来模拟。
4. 独享锁与共享锁
独享锁:只有一个线程可以访问资源,如Java中的synchronized关键字和ReentrantLock默认都是独享锁。
共享锁:允许多个线程同时读取资源,但写入时仍需独占访问。Java中的ReadWriteLock接口及其实现类(如ReentrantReadWriteLock)就是共享锁的一种实现,其中读锁是共享锁,写锁是独享锁。
5. 自旋锁与适应性自旋锁
自旋锁:尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。这种锁的好处是减少线程上下文切换的消耗,但缺点是循环会消耗CPU。在Java中,自旋锁通常作为锁升级过程中的一个阶段(如从偏向锁升级到轻量级锁时)。
适应性自旋锁:是自旋锁的一种改进,自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。这种锁能够更智能地适应不同的场景,提高并发性能。
6. 其他特殊锁
偏向锁:针对加锁对象的一种优化手段,当一个线程第一次访问一个对象时,会将该对象设置为偏向自己的状态,这样当该线程再次访问该对象时,就不需要再进行加锁操作了。这是Java 1.6中引入的一种锁优化机制。
轻量级锁:在锁升级过程中,如果锁的竞争不是很激烈,JVM会将锁从偏向锁升级为轻量级锁。轻量级锁的实现依赖于CAS机制和Mark Word中的锁标志位。
重量级锁:如果锁的竞争非常激烈,JVM会将锁从轻量级锁升级为重量级锁。重量级锁的实现依赖于操作系统的Mutex Lock(互斥锁),并涉及线程上下文切换和内核态与用户态之间的转换,因此性能开销较大。
7. 其他常见锁
Synchronized锁:Java中最基本的锁机制,基于对象的内置监视器(或称为锁)来实现线程同步。
ReentrantLock锁:Java提供的可重入锁,具有与synchronized锁相似的功能,但提供了更高的灵活性和扩展性。
ReadWriteLock(读写锁):允许多个线程同时读取数据,但只允许一个线程写入数据。读写锁通常用于读多写少的场景。
Condition锁:与锁相关联的条件对象,允许线程在特定条件满足时等待或被唤醒。Condition锁通常与ReentrantLock结合使用。
StampedLock锁:Java 8中引入的一种乐观锁机制,提供了一种读写锁的变体,允许乐观读取操作,从而提供更高的并发性能。
LockSupport锁:一个线程阻塞工具,可以用于创建锁和其他同步类的基本线程阻塞原语。它与每个线程都关联了一个许可证,可以用于阻塞和解除阻塞线程。
Synchronized和volatile的区别
1.同步级别:volatile仅能用于变量级别,而synchronized可以用于变量、方法或类级别。
2.可见性与原子性:volatile仅能保证变量的可见性和禁止指令重排,但不能保证原子性;而synchronized既能保证变量的可见性和原子性,又能防止多个线程同时访问共享资源导致的数据不一致问题。
3.性能开销:由于volatile的轻量级特性,其性能开销通常小于synchronized。但是,在需要保证原子性的场景下,volatile可能无法满足需求,此时需要使用synchronized或其他同步机制。
4.使用场景:volatile适用于状态标记量、双重检查等场景;而synchronized适用于对共享变量的访问和修改、对类实例化的构造函数进行同步等场景。
5.HasMap
Java为数据结构中的映射定义了一个接口java.util.Map,此接口主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap和TreeMap
(1) HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
(2) Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
(4) TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
内部实现(存储结构-字段)
从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下如所示。
(1)HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组,明显它是一个Node的数组。
(2)HashMap就是使用哈希表来存储的。哈希表为解决冲突,可以采用开放地址法和链地址法等来解决问题,Java中HashMap采用了链地址法。链地址法,简单来说,就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。