数开面经JAVA

基础

String、StringBuffer、StringBuilder

AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法
在这里插入图片描述

string

string为什么不变

  • 保存字符串的数组被 final 修饰且为私有的并且String 类没有提供/暴露修改这个字符串的方法。
  • String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

string.intern是什么

  • String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
  • 字符串常量池:字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
  • 常量折叠:常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。

== vs equals vs hashcode

  • ==:对于基本数据类型,比较两个变量保存的数据是否相等;对于引用数据类型变量,比较两个对象地址值是否相同。
  • equals:equals时java.lang.object 类里面的方法,如果没有覆盖equals方法就是默认==;如果equals覆盖了,就是比较对象属性值是否相等
  • hashcode:作用是获取对象的哈希码
    重写 equals 时为什么一定要重写 hashCode:
  • equals方法用于判断其对象与该对象是否相等,hashcode用于对对象返回hashcode值
  • 如果重写了equals时,没有重写hashcode方法就会导致equals方法判断相等的hashcode值不相等。
  • hashcode效率大于equals,但是hashcode可能会发生hash冲突
    hash冲突怎么解决?
  • 开放地址法:从发生冲突的位置,按照一定顺序从hash表中找到一个空闲的位置,把发生冲突的元素存进去(ThreadLocal)
  • 链式寻址法:通过单链表解决,(hashmap)

java集合

在这里插入图片描述

List集合

  • ArrayList:底层数据结构是数组,查询快,增删慢,查询是根据数组下标直接查询速度快,增删需要移动后边的元素和扩容,速度慢。线程不安全,效率高
  • LinkedList:底层数据结构是链表,查询慢,增删快,查询需要遍历数组,速度慢,增删只需要增加或删除一个链接即可,速度快,线程不安全,效率高
  • Vector:底层数据结构是数组,查询快,增删慢,线程安全,效率低
    Set集合
  • Hashset:底层数据结构是哈希表,是根据哈希算法来存取对象的,存取速度快,当Hashset中元素个数超过数组大小(默认值位0.75)时,会进行近似两倍的扩容,哈希表依赖两个方法hashcode()和equals()方法,方法的执行顺序,判断hashcode值是否相同,是:执行equals方法看其返回值,true:说明元素重复不添加,false:直接添加到集合,hashcode值不相同直接添加到集合。
  • LinkedHashset:底层数据结构是链表和哈希表,由链表保证元素有序,由哈希表保证元素的唯一
  • Treeset底层数据结构是红黑树(唯一,有序)由自然排序和比较器排序保证有序,根据返回值是否是0判断元素是否唯一
    Map集合
  • HashMap:HashMap是基于散列表实现的,其插入和查询的<k,v>的开销是固定的,可以通过构造器设置容量和负载因子来调整容器的性能,线程不安全,效率低
  • TreeSet:基于红黑树实现,查看<k,v>时,它们会被排序,TreeMap是唯一带有subMap()方法的Map,subMap()方法可以返回一个子树。
  • LInkedHashMap:类似于HashMap,但是迭代遍历它时,取得<K,V>的顺序是其插入次序,或者是最近最少使用(LRU)的次序。

hashmap扩容

当hashmap中的元素数量达到阈值(容量乘以负载因子)时,就会触发扩容操作。扩容操作会创建一个新的数组,长度是原来的两倍,然后将原来数组中的元素重新计算哈希值并分配到新数组中的合适位置。
为什么扩容为原来的两倍呢这是为了保证哈希值的高位不变,从而减少元素的移动次数。因为哈希值是根据数组长度取模得到的,如果数组长度是2的幂次方,那么只有哈希值的最低位会参与取模运算,而高位则不会改变。这样,在扩容时,只需要判断哈希值的最高位是0还是1,就可以确定元素在新数组中的位置,要么不变,要么移动到原来位置加上旧数组长度的位置。这样可以提高扩容的效率和性能。

线程安全集合

  • vector:通过synchronized实现,线程安全
  • hashtable:通过synchronized实现,线程安全
  • CopyONWriteArraylist:通过写时复制算法实现线程安全
  • Concurrenthashmap:JDK1.8之前,Concurrenthashmap加的分段锁,之后就直接在table元素上加锁,实现对每一行进行加锁,减小并发冲突概率。

并发

创建线程

创建线程4中方法

  • 继承Thread类创建线程。这种方法需要定义一个类继承自Thread类,并重写run()方法,然后创建该类的对象并调用start()方法启动线程
  • 实现Runnable接口创建线程。这种方法需要定义一个类实现Runnable接口,并实现run()方法,然后创建该类的对象并作为参数传递给Thread类的构造器,再调用start()方法启动线程。这种方法可以避免单继承的局限性,也可以实现多个线程共享数据的目的。
  • 使用Callable和Future创建线程。这种方法需要定义一个类实现Callable接口,并实现call()方法,然后创建该类的对象并作为参数传递给FutureTask类的构造器,再将FutureTask对象作为参数传递给Thread类的构造器,最后调用start()方法启动线程。这种方法可以让线程有返回值,并且可以抛出异常
  • 使用线程池创建线程。这种方法可以通过Executor框架提供的工具类来创建不同类型的线程池,如固定大小的线程池、单一线程池、可缓存的线程池等,然后将实现了Runnable或Callable接口的对象提交给线程池执行。这种方法可以提高性能和资源利用率,也可以方便地管理和控制线程。

Runnalbe和Callable

java中的callable和runnable都可以用来实现多线程,但是有以下区别:

  • runnable的run方法没有返回值,也不能抛出异常;callable的call方法可以返回泛型结果,也可以抛出异常。
  • runnable的任务只能通过Thread类的start方法来执行;callable的任务可以通过ExecutorService类的submit或execute方法来执行,也可以通过FutureTask类来包装成runnable任务。
  • runnable的任务执行后没有结果,需要通过共享变量或者回调函数来获取;callable的任务执行后可以通过Future接口来获取结果,也可以设置超时时间或取消任务。

因此,runnable和callable都可以实现异步调用,但是callable更灵活,功能更强大。

线程之间通信的方法

https://blog.csdn.net/qq_42411214/article/details/107767326

  • volatile:关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
  • synchronized :根据synchronized原理使用Object类提供了线程间通信的方法:wait()、notify()、notifyaAl()方法来实现多个线程互斥访问临界区资源,Object类这几个方法必须配合synchronized来进行使用。
  • ReentrantLock:ReentrantLock使用Condition阻塞队列的await()、signal()、signalAll()三个方法来实现线程阻塞和运行两个状态的切换,进而实现线程间的通信。

线程安全的方法

使用synchronized关键字,可以修饰代码块或者方法,表示同一时间只有一个线程可以执行该代码块或者方法,从而保证共享数据的一致性。例如:

//同步代码块
synchronized (this) {
    //可能会发生线程冲突的代码
}

//同步方法
public synchronized void method() {
    //可能会发生线程冲突的代码
}

使用Lock接口及其实现类,可以手动地加锁和解锁,实现更灵活的同步控制。例如:

//创建一个Lock对象
Lock lock = new ReentrantLock();

//加锁
lock.lock();

try {
    //可能会发生线程冲突的代码
} finally {
    //解锁
    lock.unlock();
}

使用volatile关键字,可以保证变量的可见性,即当一个线程修改了该变量的值,其他线程能够立即看到修改后的值。volatile关键字也可以防止指令重排,保证程序的有序性。例如:

//声明一个volatile变量
private volatile int count;

//修改变量的值
count++;

使用原子类,可以实现无锁的线程安全操作,原子类是利用CAS(Compare And Swap)算法实现的,它可以保证更新操作是原子性的,即要么成功要么失败。java.util.concurrent.atomic包下提供了多种原子类,如AtomicInteger, AtomicLong, AtomicReference等。例如:

//创建一个原子整数对象
AtomicInteger ai = new AtomicInteger();

//自增操作
ai.incrementAndGet();
  • Synchronized是基于操作系统的Mutex Lock(互斥锁)来实现的,它不直接使用AQS和CAS,但是它在JDK1.6之后引入了锁升级的机制,其中轻量级锁的实现就是基于CAS操作的。Synchronized是一个内置锁,它不需要显示地获取和释放锁,但是它也没有提供更多的功能和灵活性
  • ReentrantLock是基于AQS框架来实现的,它使用了AQS内部的state变量和等待队列来管理同步状态和等待线程。ReentrantLock是一个显式锁,它需要手动地获取和释放锁,但是它也提供了更多的功能和灵活性,例如公平模式、非公平模式、多个条件变量等。ReentrantLock在获取和释放锁时也会使用CAS操作来保证原子性。
  • 原子类是直接使用CAS操作来实现的,它们不需要使用AQS或者锁来保证线程安全,而是利用CPU指令来实现无锁的原子操作。原子类提供了一系列的原子操作方法,例如get、set、increment、decrement等,这些方法都是基于CAS操作的。

volatile、Synchronized、ReentrantLock底层实现原理

volatile是一个关键字,用于修饰变量,表示该变量在每次被线程访问时,都强制从主内存中读取该变量的值,并且在修改后立即写回主内存。这样可以保证多个线程对该变量的可见性,即一个线程修改了该变量的值,其他线程能够立即看到修改后的值。volatile的实现原理是通过内存屏障(memory barrier)来实现的,内存屏障是一种CPU指令,用于防止指令重排序和保证特定操作的执行顺序。在Java中,volatile变量在读取前会插入一个load屏障,在写入后会插入一个store屏障。

Synchronized是一个关键字,用于修饰方法或代码块,表示该方法或代码块是同步的,即同一时刻只能有一个线程执行该方法或代码块。Synchronized的实现原理是通过对象监视器(monitor)来实现的,每个对象都有一个与之关联的monitor对象,当一个线程要执行Synchronized修饰的方法或代码块时,它需要先获取monitor对象的所有权,然后才能执行,执行完毕后再释放monitor对象。如果获取失败,则进入阻塞状态,直到monitor对象被释放。Synchronized在JVM层面上是通过monitorenter和monitorexit两个指令来实现的,monitorenter指令在同步代码块开始处插入,用于获取monitor对象;monitorexit指令在同步代码块结束处和异常处插入,用于释放monitor对象。

ReentrantLock是一个类,实现了Lock接口,表示它是一个可重入的互斥锁,即同一个线程可以多次获取同一把锁。ReentrantLock的实现原理是基于Java中的一个同步工具类AbstractQueuedSynchronizer(AQS),AQS是一个抽象类,提供了一套基于FIFO队列的同步器框架,用于构建各种同步组件。AQS内部维护了一个原子整数state和一个双向链表(CLH队列),state表示同步状态,CLH队列表示等待队列。ReentrantLock通过继承AQS并重写其中的方法来实现锁的获取和释放。当一个线程要获取锁时,它会先尝试通过CAS操作修改state值,如果成功,则表示获取锁成功;如果失败,则表示锁已经被占用,该线程会被封装成一个节点加入到CLH队列中,并进入阻塞状态;当锁被释放时,会唤醒CLH队列中的头节点对应的线程35。

并发编程三性质

原子性:原子性指的是一个操作或多个操作要么全部执行且执行过程不被中断,要么不执行;

可见性:可见性指的是多个线程修改同一个共享变量时,一个线程修改后,其他线程能马上获得修改后的值;

有序性:有序性指的是程序执行的顺序按照代码的先后顺序执行。只要有一个没有被保证,就有可能会导致程序运行不正确。

java中不同种类的锁

  • 乐观锁和悲观锁是两种不同的思想,乐观锁认为读多写少,遇到并发写的可能性低,所以不会上锁,但是在更新时会判断数据是否被修改,一般使用CAS操作来实现+版本号控制;悲观锁是一种防止数据冲突的机制,它是以一种预防的姿态在修改数据之前把数据锁住,然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后下一个人数据加锁才可对数据进行操作。悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁(如MySQL中的排它锁)
  • 独占锁和共享锁共享锁和独占锁是Java中的两种锁。独占锁指该锁一次只能被一个线程所持有,对ReentrantLock和Synchronized而言都是独占锁。共享锁指该锁可被多个线程所持有,ReentrantReadWriteLock,其读锁是共享锁,其写锁是独占锁。读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程中都必须互斥。
  • 公平锁和非公平锁是两种不同的策略,公平锁要求多个线程按照申请锁的顺序来获取锁;非公平锁则允许线程插队,提高了吞吐量。synchronized是非公平锁,ReentrantLock可以通过构造函数指定是否为公平锁,默认是非公平的。
  • 互斥锁和读写锁是两种不同的类型,互斥锁只能有一个线程持有,不区分读写操作;读写锁则可以区分读操作和写操作,允许多个线程同时进行读操作,但只能有一个线程进行写操作。互斥锁可以用synchronized或ReentrantLock来实现;读写锁可以用ReentrantReadWriteLock来实现 。
    可重入锁是一种特性,它允许同一个线程多次获取同一把锁,不会造成死锁。synchronized和ReentrantLock都是可重入的 。
  • 分段锁是一种设计,它将一个大的数据结构分成若干个小的段(Segment),每个段都有自己的锁,这样可以减少竞争和提高并发性。ConcurrentHashMap就是使用了分段锁的设计 。
  • 锁升级是一种优化机制,在JDK1.6之后引入了四种状态:无锁、偏向锁、轻量级锁和重量级锁。它们会随着竞争情况逐渐升级,但不能降级。无锁表示没有任何线程获取过该对象的锁;偏向锁表示该对象被某个线程频繁访问,所以偏向于该线程;轻量级锁表示该对象被多个线程交替访问,所以使用CAS操作来尝试获取或释放锁;重量级锁表示该对象被多个线程同时访问,所以使用操作系统的互斥量来实现 。

线程池

基本思想

预先分配、循环使用、复用

线程池优点

  1. 第一个优点,能够控制服务器资源,应该说合理的分配服务器资源,不至于过高的QPS,导致服务器资源分配完,从而导致整个服务器。因为单独的创建线程,就会单独给线程创建(虚拟机栈、程序计数器、本地方法栈),这些都是要占用内存的。瘫痪。
  2. 第二个优点,线程复用,因为反复的创建和销毁线程对于性能的消耗也是有影响的,线程池反而能够降低线程创建和销毁资源的消耗。
  3. 第三个优点,优化系统,在大多数的情况下,线程池相比串行化的操作,异步的执行我们的任务,由原来的串行操作,修改成异步操作,降低了系统的响应时间。所以,对于线程池最常用的一个操作和场景就是:把原来很多串行的查询操作,可以修改成异步,然后在服务层做聚合,返回给前端,提系统的响应时间(前提上下文之间没有数据的依赖)。

线程池缺点

  1. 线程池的数量配置的不合理,会导致系统资源的耗尽、可能直接导致系统出现OOM异常。

  2. 另外的话使用线程池,也会带来多线程的问题,比如:数据的一致性、业务的复杂性、测试的复杂性(一般线程池的使用,都要结合测试,不断的进行压测,然后观察内存和CPU的变化影响怎么样)。

线程池参数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
         // 非法参数校验
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        // workQueue&&threadFactory &&handler 都不能为空
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

corePoolSize :它表示核心线程数,为啥叫做核心线程数呢?因为还有非核心线程数,核心线程数+非核心线程数=最大线程数
maximumPoolSize:这个参数就是表示核心线程和非核心线程的总数,也就是线程池的最大线程数,当线程数超过这个最大线程数时,就会走拒绝策略。
keepAliveTime:表示空闲线程最多空闲的时间,超过这个阈值就会被回收掉。
unit:空闲时间的单位,例如 TimeUnit.SECONDS
workQueue:是存放任务的队列。上面提及到队列这种东西,workQueue队列有很多种,这里主要列举常用的几种:

  1. ArrayBlockingQueue :是由数组实现的有界的阻塞队列,在初始化的时候,必须指定大小。

  2. LinkedBlockingQueue :是由链表实现的无界的阻塞队列,默认是Integer.MAX_VALUE,也可以初始化的时候指定大小。

  3. DelayQueue:延迟队列,只有延迟期满足才会从队列中获取元素。

  4. SynchronousQueue:是一个不存储元素的阻塞队列。若是插入的时候,已经有一个元素,就会阻塞等待,直到这个元素被移除,反之亦然。

  5. LinkedBlockingDeque:是一个由链表组成的双向阻塞队列。

  6. 当核心线程数没有满时,就会创建核心线程数来执行任务

  7. 核心线程数满了,就会把任务放在任务队列里面

  8. 若是任务队列也满了,才会创建非核心线程数来执行任务,最后核心线程数+非核心线程数总和已经小于maximumPoolSize

  9. 最后,如果线程数的总和已经达到了maximumPoolSize,就会走拒绝策略。

拒绝策略

handler:这个就是拒绝策略,有四种拒绝策略,如下:

  1. DiscardPolicy:直接丢弃任务,不做处理,不抛出异常,一般是对应无关紧要的任务。

  2. DiscardOldestPolicy:丢弃队列中最前面的任务,也就是最老的任务,然后尝试执行新任务。

  3. CallerRunsPolicy:由调用者线程进行处理。

  4. AbortPolicy:抛出异常。

threadFactory:从名字来看是线程工厂,主要是给线程一个标识,比如:阿里规定使用线程池时,建议给线程池一个名字,方便追溯和排查

private static final ThreadPoolExecutor pool; 
    static {
        // 定义线程池的名字
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("po-detail-pool-%d").build();
        pool = new ThreadPoolExecutor(4, 8, 60L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(512),
            threadFactory, new ThreadPoolExecutor.AbortPolicy());
        pool.allowCoreThreadTimeOut(true);
    }

不推荐默认线程池的原因

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM;

CachedThreadPool:允许创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM

详细原理

AQS

AbstractQueuedSynchronizer(AQS)提供了一套可用于实现锁同步机制的框架,不夸张地说,AQS是JUC同步框架的基石。AQS通过一个FIFO队列维护线程同步状态,实现类只需要继承该类,并重写指定方法即可实现一套线程同步机制。
AQS根据资源互斥级别提供了独占和共享两种资源访问模式;同时其定义Condition结构提供了wait/signal等待唤醒机制。在JUC中,诸如ReentrantLock、CountDownLatch等都基于AQS实现。

AQS原理

AQS的原理并不复杂,AQS维护了一个volatile int state变量和一个CLH(三个人名缩写)双向队列,队列中的节点持有线程引用,每个节点均可通过getState()、setState()和compareAndSetState()对state进行修改和访问。当线程获取锁时,即试图对state变量做修改,如修改成功则获取锁;如修改失败则包装为节点挂载到队列中,等待持有锁的线程释放锁并唤醒队列中的节点。

img

AQS核心结构Node

img

Node主要包括五个核心字段

waitstatus:当前节点状态,该字段共有5种取值

  • cancelled=1。节点引用线程由于等待超时或被打断时的状态。
  • signal=-1。后继节点线程需要被唤醒时的当前节点状态。当队列中加入后继节点被挂起(block)时,其前驱节点会被设置为SIGNAL状态,表示该节点需要被唤醒。
  • CONDITION = -2。当节点线程进入condition队列时的状态。(见ConditionObject)
  • PROPAGATE = -3。仅在释放共享锁releaseShared时对头节点使用。(见共享锁分析)
  • 0。节点初始化时的状态。

prev:前驱节点

next:后继节点

thread:引用线程,头节点不包含线程

nextWaiter:condition条件队列。

ConditionObject

AQS中Node除了组成阻塞队列外,还在ConditionObject中得到应用,ConditionObject的核心定义为:

public class ConditionObject implements Condition, java.io.Serializable {
    ... 
    private transient Node firstWaiter;
    private transient Node lastWaiter;
    ...
}

ConditionObject通过Node也构成了一个FIFO的队列,那么ConditionObject为AQS提供了怎样的功能呢?

public interface Condition {
    ...
    void await() throws InterruptedException;
    void signal();
    void signalAll();
    ...
}

查看Condition接口的定义,可以看到其定义的方法与Object类的wait/notify/notifyAll功能是一致的。
在Synchronized详解中笔者曾对ObjectMonitor做过简单介绍,其中ObjectMonitor包含_WaitSet_EntryList两个队列,分别用于存储wait调用和sychronized锁竞争时挂起的线程,而AQS通过ConditionObject同样也提供了wait/notify机制的阻塞队列。

img

ConditionObject机制如上图,在条件队列中,Node采用nextWaiter组成单向链表,当持有锁的线程发起condition.await调用后,会包装为Node挂载到Condition条件阻塞队列中;当对应condition.signal被触发后,条件阻塞队列中的节点将被唤醒并挂载到锁阻塞队列中。ConditionObject的队列逻辑与前述的acquire/release大同小异,不再赘述。

AQS对CLH的改进

CLH 锁作为自旋锁的改进,有以下几个优点:

  1. 性能优异,获取和释放锁开销小。CLH 的锁状态不再是单一的原子变量,而是分散在每个节点的状态中,降低了自旋锁在竞争激烈时频繁同步的开销。在释放锁的开销也因为不需要使用 CAS 指令而降低了。
  2. 公平锁。先入队的线程会先得到锁。
  3. 实现简单,易于理解。
  4. 扩展性强。下面会提到 AQS 如何扩展 CLH 锁实现了 j.u.c 包下各类丰富的同步器。

当然,它也有两个缺点:

  1. 第一是因为有自旋操作,当锁持有时间长时会带来较大的 CPU 开销。
  2. 第二是基本的 CLH 锁功能单一,不改造不能支持复杂的功能。

针对 CLH 的缺点,AQS 对 CLH 队列锁进行了一定的改造。针对第一个缺点,AQS 将自旋操作改为阻塞线程操作。针对第二个缺点,AQS 对 CLH 锁进行改造和扩展,原作者 Doug Lea 称之为“CLH 锁的变体”。下面将详细讲 AQS 底层细节以及对 CLH 锁的改进。AQS 中的对 CLH 锁数据结构的改进主要包括三方面:扩展每个节点的状态、显式的维护前驱节点和后继节点以及诸如出队节点显式设为 null 等辅助 GC 的优化。正是这些改进使 AQS 可以支撑 j.u.c 丰富多彩的同步器实现。

扩展每个节点的状态
volatile int waitStatus;

AQS 同样提供了该状态变量的原子读写操作,但和同步器状态不同的是,节点状态在 AQS 中被清晰的定义,如下表所示:

状态名描述
SIGNAL表示该节点正常等待
PROPAGATE应将 releaseShared 传播到其他节点
CONDITION该节点位于条件队列,不能用于同步队列节点
CANCELLED由于超时、中断或其他原因,该节点被取消
显式的维护前驱节点和后继节点

过在节点中显式地维护前驱节点,CLH 锁就可以处理“超时”和各种形式的“取消”:如果一个节点的前驱节点取消了,这个节点就可以滑动去使用前面一个节点的状态字段。对于通过自旋获取锁的 CLH 锁来说,只需要显式的维护前驱节点就可以实现取消功能。

辅助GC

JVM 的垃圾回收机制使开发者无需手动释放对象。但在 AQS 中需要在释放锁时显式的设置为 null,避免引用的残留,辅助垃圾回收。

为什么CLH锁中,一个线程释放自己的锁时,后续线程的高速缓存会失效

CLH锁中,一个线程释放自己的锁时,后续线程的高速缓存会失效是因为锁状态去中心化,让每个线程在不同的状态变量中自旋,这样当一个线程释放它的锁时,只能使其后续线程的高速缓存失效,缩小了影响范围,从而减少了CPU的开销。

CLH锁数据结构很简单,类似一个链表队列,所有请求获取锁的线程会排列在链表队列中,自旋访问队列中前一个节点的状态。当一个节点释放锁时,只有它的后一个节点才可以得到锁。CLH锁本身有一个队尾指针Tail,它是一个原子变量,指向队列最末端的CLH节点。每一个CLH节点有两个属性:所代表的线程和标识是否持有锁的状态变量。当一个线程要获取锁时,它会对Tail进行一个getAndSet的原子操作。该操作会返回Tail当前指向的节点,也就是当前队尾节点,然后使Tail指向这个线程对应的CLH节点,成为新的队尾节点。入队成功后,该线程会轮询上一个队尾节点的状态变量,当上一个节点释放锁后,它将得到这个锁。

ReentrantLock锁

ReentrantLock是一个可重入的互斥锁,又被称为“独占锁”。 ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性

ReentrantLock是一个重入锁,表现在state上,如果持有锁的线程重复获取锁时,它会将state状态进行递增,也就是获得一个信号量,当释放锁时,同时也是释放了信号量,信号量跟随减少,如果上一个线程还没有完成任务,则会进行入队等待操作。

ReentrantLock内部类SyncAbstractQueuedSynchronizer 首先获取state的值,当state=0时,执行compareAndSetState(0, acquires)方法。

ReentrantLock是个典型的独占模式AQS,同步状态为0时表示空闲。 当有线程获取到空闲的同步状态时,它会将同步状态加1,将同步状态改为非空闲,于是其他线程挂起等待。

Synchronized锁升级

在这里插入图片描述

理论简介

monitorenter指令是在编译后插入到同步代码块的开始位置;monitorexit是插入到方法结束和异常的位置(实际隐藏了try和finally),每个对象都有一个monitor与之关联,当一个线程执行到monitorenter指令时,就会获得对象所对应的monitor的所有权,也就是获得了对象的锁。

当另外一个线程执行到同步块的时候,由于他没有对应monitor的所有权,就会被阻塞,此时控制权只能交给操作系统,也就会从user mode切换到kernel mode,由操作系统来负责线程间的调度和线程的状态变更,需要频繁的在这两个模式下切换(上下文切换)。这种竞争就找内核的行为很不好,会引起很大的开销。所以叫重量级锁,这也就给大家留下一个印象,synchronized关键字相比于其他同步机制性能不好。

偏向锁

偏向锁是一种针对无竞争情况下的同步锁进行优化的锁机制。如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。线程第二次到达同步代码块时,会判断此时持有锁的线程是否就是自己,如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,偏向锁几乎没有额外开销,性能极高。1

如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。偏向锁通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。一旦有第二个线程加入 锁竞争 ,偏向锁就升级为轻量级锁(自旋锁)。升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致 STW (stop the word) 操作。

STW(Stop The World)操作是指在进行垃圾回收时,暂停所有的应用线程,直到垃圾回收完成。在这个过程中,应用程序无法执行任何代码,因此会导致应用程序的停顿。

在JVM中,为了方便线程中断管理,提出了安全点的概念。所有的线程在安全点位置挂起等待。JVM在进行可达性分析时,需要枚举遍历GC Roots,如果针对引用链挨个遍历,在几百上千兆的内存大小遍历也是非常耗时间的。所以JVM会在一些安全点位置上设置一个标志位,表示这个位置是安全点。当线程到达安全点时,JVM会检查标志位是否被设置,如果没有设置,则等待直到下一个安全点再次检查。

在 HotSpot 虚拟机中是有锁降级的, 但是仅仅只发生在 STW 的时候 ,只有垃圾回收线程能够观测到它,也就是说, 在我们正常使用的过程中是不会发生锁降级的,只有在 GC 的时候才会降级。

轻量级锁

当有新的线程也需要对该对象上锁,发现该对象已经被别的线程持有偏向锁(观察到MarkWord中有别的线程ID),此时的竞争升级。系统将会撤销偏向锁,并将锁升级为轻量级锁(自旋锁)。设置轻量级锁,首先需要在当前线程的栈帧中创建一个Lock Record,通过使用CAS 尝试将 Lock Record与 Object的 MarkWord 互换,从而进行锁的争抢。

重量级锁

重量级锁是指当多个线程同时访问同一个同步块时,它们会进入阻塞状态,并且会被挂起。当其他线程释放了这个同步块的控制权之后,被挂起的线程才能够继续执行

Synchronized和reentrantLock区别

底层实现

synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法,ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。

synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁,ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。

是否可手动释放

synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。

是否可中断

synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。

是否公平锁

synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。

锁是否可绑定条件Condition

synchronized不能绑定; ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。

锁的对象

synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。

Java原子类(CAS)

原子类是具有原子性的类,原子性的意思是对于一组操作,要么全部执行成功,要么全部执行失败,不能只有其中某几个执行成功。原子类作用和锁有类似之处,是为了保证并发情况下的线程安全。
java提供多个原子类(AtomicInteger,AtomicLong等),他们提供一种线程安全的方式进行数值变量操作。

和锁比优势

  1. 粒度更细,原子变量可以把竞争范围缩小到变量级别,通常情况下锁的粒度也大于原子变量的粒度
  2. 效率更高,除了在高并发之外,使用原子类的效率往往比使用同步互斥锁的效率更高,因为原子类底层利用了CAS,不会阻塞线程。

三种重排序

编译器重排序

针对程序代码语而言,编译器可以在不改变单线程程序语义的情况下,可以对代码语句顺序进行调整重新排序。

指令集并行重排序

这个是针对于CPU指令级别来说的,处理器采用了指令集并行技术来讲多条指令重叠执行,如果不存在数据依赖性,处理器可以改变语句对应的机器指令执行顺序。

指令集并行的重排序会导致可见性问题。因为处理器采用了指令集并行技术来讲多条指令重叠执行,如果不存在数据依赖性,处理器可以改变语句对应的机器指令执行顺序。因为CPU缓存使用缓冲区的方式进行延迟写入,这个过程会造成多个CPU缓存可见性的问题,这种可见性的问题导致结果的对于指令的先后执行显示不一致,从表面结果上来看好像指令的顺序被改变了,内存重排序其实是造成可见性问题的主要原因所在,其原理可在上一篇文章中详细了解。编译器和处理指令也并非什么场景都会进行指令重排序的优化,而是会遵循一定的原则,只有在它们认为重排序后不会对程序结果产生影响的时候才会进行重排序的优化,如果重排序会改变程序的结果,那这样的性能优化显然是没有意义的。而遵守as-if-serial语义规则就是重排序的一个原则,as-if-serial 的意思是说,可以允许编译器和处理器进行重排序,但是有一个条件,就是不管怎么重排序都不能改变单线程执行程序的结果。

内存重排序

因为CPU缓存使用 缓冲区的方式(Store Buffere )进行延迟写入,这个过程会造成多个CPU缓存可见性的问题,这种可见性的问题导致结果的对于指令的先后执行显示不一致,从表面结果上来看好像指令的顺序被改变了,内存重排序其实是造成可见性问题的主要原因所在,其原理可在上一篇可中详细了解。

内存重排序会导致可见性问题的主要原因是因为CPU缓存使用缓冲区的方式进行延迟写入,这个过程会造成多个CPU缓存可见性的问题,这种可见性的问题导致结果的对于指令的先后执行显示不一致,从表面结果上来看好像指令的顺序被改变了,内存重排序其实是造成可见性问题的主要原因所在。具体来说,当一个线程修改了共享变量的值时,另一个线程可能无法立即看到这个修改,因为它们可能使用了不同的CPU缓存。如果没有采取措施来保证可见性,那么就会出现数据不一致的情况。

在Java中,可以通过使用volatile关键字来保证可见性。当一个变量被声明为volatile时,它会被立即更新到主内存中,并且每次读取该变量时都会从主内存中读取最新值。这样就可以保证多个线程之间对于该变量的操作是同步的。

除了使用volatile关键字之外,还可以使用synchronized关键字或者Lock接口来保证可见性。这些机制都可以保证同一时刻只有一个线程能够访问共享变量,并且在释放锁之前会将修改后的值刷新到主内存中。

总之,在多线程编程中,保证可见性是非常重要的。如果没有采取措施来保证可见性,那么就会出现数据不一致的情况,从而导致程序出现各种奇怪的问题。

as-if-serial语义规则

as-if-serial语义规则是指,可以允许编译器和处理器进行重排序,但是有一个条件,就是不管怎么重排序都不能改变单线程执行程序的结果。

在编译器层面和CPU层面都提供了一套内存屏障来禁止重排序的指令,编码人员需要识别存在数据依赖的地方加上一个内存屏障指令,那么此时计算机将不会对其进行指令优化。

Java 内存模型 (JMM)定义了几个happens before原则来指导并发程序编写的正确性。程序员可以通过Volatile、synchronized、final几个关键字告诉编译器和处理器哪些地方是不允许进行重排序的。

volatile

缓存一致性协议MESI协议(在多核CPU下)

最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。

  1. Modify(修改):当缓存行中的数据被修改时,该缓存行置为M状态
  2. Exclusive(独占):当只有一个缓存行使用某个数据时,置为E状态
  3. Shared(共享):当其他CPU中也读取某数据到缓存行时,所有持有该数据的缓存行置为S状态
  4. Invalid(无效):当某个缓存行数据修改时,其他持有该数据的缓存行置为I状态

它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

而这其中,监听和通知又基于总线嗅探机制来完成。

总线嗅探机制

嗅探机制其实就是一个监听器,回到我们刚才的流程,如果是加入MESI缓存一致性协议和总线嗅探机制之后:

  1. CPU1读取数据a=1,CPU1的缓存中都有数据a的副本,该缓存行置为(E)状态
  2. CPU2也执行读取操作,同样CPU2也有数据a=1的副本,此时总线嗅探到CPU1也有该数据,则CPU1、CPU2两个缓存行都置为(S)状态
  3. CPU1修改数据a=2,CPU1的缓存以及主内存a=2,同时CPU1的缓存行置为(S)状态,总线发出通知,CPU2的缓存行置为(I)状态
  4. CPU2再次读取a,虽然CPU2在缓存中命中数据a=1,但是发现状态为(I),因此直接丢弃该数据,去主内存获取最新数据

当我们使用volatile关键字修饰某个变量之后,就相当于告诉CPU:我这个变量需要使用MESI和总线嗅探机制处理。从而也就保证了可见性。

禁止指令重排序

加入内存屏障。JMM(java memeory model)Java内存模型级别

  1. LoadLoad屏障: 对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

  2. StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

  3. LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

  4. StoreLoad屏障: 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

CAS有哪些缺点

ABA问题,引入版本号

高竞争下的开销问题 在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。

功能限制 CAS的功能是比较受限的,例如CAS只能保证单个变量(或者说单个内存值)操作的原子性,这意味着:

(1)原子性不一定能保证线程安全,例如在Java中需要与volatile配合来保证线程安全;

(2)当涉及到多个变量(内存值)时,CAS也无能为力。

ThreadLocal

定义

ThreadLocal又叫做线程局部变量,全称thread local variable,它的使用场合主要是为了解决多线程中因为数据并发产生不一致的问题。ThreadLocal为每一个线程都提供了变量的副本,使得每一个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享,这样的结果无非是耗费了内存,也大大减少了线程同步所带来的性能消耗,也减少了线程并发控制的复杂度。

ThreadLocal和Synchronized区别

虽然ThreadLocal和Synchonized都用于解决多线程的并发访问,但是它们之间还是会有一些本质上的区别的:

Synchronized是利用锁的机制,使得变量或者是代码块在某一时刻里只能被一个线程来进行访问。ThreadLocal是为每一个线程都提供了一个变量的副本,这样就是的每一个线程在某一时刻里访问到的不是同一个对象,这样就隔离了多个线程对数据的数据共享,Synochronized正好相反,可以用于多个线程之间通信能够获得数据共享。

注:ThreadLocal不可以使用原子类型,只能使用Object类型。ThreadLocal不能使用原子类型的原因是因为ThreadLocal是线程本地变量,每个线程都有自己的一个副本,而原子类型是共享变量,多个线程可以同时访问。如果使用原子类型,那么每个线程都会共享这个变量,这样就失去了线程本地变量的意义。

核心应用场景

对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。

ThreaLocal作用在每个线程内都都需要独立的保存信息,这样就方便同一个线程的其他方法获取到该信息的场景,由于每一个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息之后,后续方法可以通过ThreadLocal可以直接获取到,避免了传参,这个类似于全局变量的概念。比如像用户登录令牌解密后的信息传递、用户权限信息、从用户系统中获取到的用户名。

img

如上图所示,就好比如线程A的方法一创建了变量A,方法二是跟方法一在同一个线程内,那么创建的变量A就是共享的。

#用户微服务配置token解密信息传递例子
public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();
                LoginUser loginUser = new LoginUser();
                loginUser.setId(id);
                loginUser.setName(name);
                loginUser.setMail(mail);
                loginUser.setHeadImg(headImg);
                threadLocal.set(loginUser);
            
后续想直接获取到直接threadLocal.getxxx就可以了
使用ThreadLocal解决线程安全问题

在我们平常的SpringWeb项目中,我们通常会把业务分成Controller、Service、Dao等等,也知道注解@Autowired默认使用单例模式。那有没有想过,当不同的请求线程进来后,因为Dao层使用的是单例,那么负责连接数据库的Connection也只有一个了,这时候如果请求的线程都去连接数据库的话,就会造成这个线程不安全的问题,Spring是怎样来解决的呢?

在Dao层里装配的Connection线程肯定是安全的,解决方案就是使用ThreadLocal方法。当每一个请求线程使用Connection的时候,都会从ThreadLocal获取一次,如果值为null,那就说明没有对数据库进行连接,连接后就会存入到 ThreadLocal里,这样一来,每一个线程都保存有一份属于自己的Connection。每一线程维护自己的数据,达到线程的隔离效果。

TheadLocal慎用的场景

  1. (线程池里线程调用ThreadLocal):因为线程池里对线程的管理都是线程复用的方法,所以在线程池里线程非常难结束,更有可能的是永远不会结束。这就意味着线程的持续时间是不可估测的,甚至会与JVM的生命周期一致。在线程池中使用ThreadLocal会存在内存泄漏的问题。在线程池中,线程是被重复利用的,如果在线程中使用了ThreadLocal,那么ThreadLocalMap就会一直存在,上次变量的变更,下次依然在上面使用。这样就会导致内存泄漏。解决方案有两种:1.在使用完ThreadLocal后,调用remove方法清除;2.使用InheritableThreadLocal代替ThreadLocal。InheritableThreadLocal可以让子线程继承父线程的变量,但是这样会增加内存开销。
  2. (在异步程序里):ThreadLocal的参数传递是不可靠的,因为线程将请求发送后,不会在等待远程返回结果就继续向下运行了,真正的返回结果得到以后,可能是其它的线程在处理。(但是在异步程序中,由于异步任务可能会在不同的线程上执行,因此ThreadLocal可能会导致问题。例如,如果您在一个线程上设置了ThreadLocal变量,然后将任务提交到异步执行器,那么该任务将在另一个线程上执行,而该线程没有访问该变量的权限。这可能会导致意外的行为和错误。如果您需要在线程之间共享数据,则可以使用ConcurrentHashMap或InheritableThreadLocal等其他机制来实现。这些机制可以确保数据在线程之间正确地传递,并且不会导致意外的行为和错误。)
  3. 在使用完ThreadLocal,推荐要调用一下remove()方法,这样会防止内存溢出这种情况的发生,因为ThreadLocal为弱引用。如果ThreadLocal在没有被外部强引用的情况下,在垃圾回收的时候是会被清理掉的,如果是强引用那就不会被清理。

为什么ThreadLocal的键是弱引用,如果是强引用会有什么问题呢?

在java里,除了基础的数据类型以外,其他的都为引用类型,而java根据生命周期的长短又把引用类型分为强引用、软引用、弱引用和虚引用。正常的情况下我们平时基本上只适用到了强引用的类型,而其他的引用类型也就在面试中或者阅读源码的时候才能看到。

强引用:像new了一个对象就是强引用 Object obj = new Object()

软引用的话,生命周期会比强引用短一些,是通过SoftReference类实现的,当内存有足够的空间,那么垃圾回收器就不会回收它;因为当JVM认为内存空间出现不足的时候,就会尝试回收软引用指定的对象,就是说在JVM会在抛出OutOfMemoryError这个异常之前,会清理软引用对象。

软引用的使用场景:比较适合用来实现缓存,当内存空间充足的时候,将缓存存放到内存当中,如果内存不足了就可以把缓存回收掉

弱引用:弱引用就是通过WeakReference类来实现的,它的生命周期比软引用还要短(一个比一个短),在进行垃圾回收的时候,不管内存的空间够不够都会回收掉这对象

使用场景:如果一个对象只是偶尔来使用的话,希望在使用的时候能够随时的获取,但是呢,也不想影响到该对象的垃圾收集,这时候就可以考虑到使用弱引用来指向这个对象。

ThreadLocal为什么是WeakReference呢?

第一、如果是强引用的话,即使ThreadLocal的值是为null,但是的话ThreadLocalMap还是会有ThreadLocal的强引用状态,如果没有手动进行删除的话,ThreadLocal就不会被回收,这样就会导致Entry内存的泄漏。

第二、如果是弱引用的话,引用ThreadLocal的对象被回收掉了,ThreadLocalMap还保留有ThreadLocal的弱引用,即使没有进行手动删除,ThreadLocal也会被回收掉。value在下一次的ThreadLocalMap调用set/get/remove方法的时候就会被清除掉。

高并发性能五个指标

https://zhuanlan.zhihu.com/p/337708438

QPS:Queries Per Second 每秒查询

TPS:Transactions Per Second 每秒事务

RT(Response-time)响应时间

Concurrency,并发数

吞吐量

系统的吞吐量(承压能力)和处理对CPU的消耗、外部接口、IO等因素紧密关联。单个处理请求对CPU消耗越高,外部系统接口、IO速度越慢,系统吞吐能力越低,反之越高。

设计模式

  • **创建型模式(Creational Patterns):这类模式关注对象的创建和初始化,它们提供了一种将对象的实例化和使用分离的方式。**创建型模式有五种,分别是工厂方法模式(Factory Method Pattern)、抽象工厂模式(Abstract Factory Pattern)、单例模式(Singleton Pattern)、建造者模式(Builder Pattern)和原型模式(Prototype Pattern)
  • **结构型模式(Structural Patterns):这类模式关注对象和类的组织结构,它们描述了如何将类或对象组合在一起形成更大的结构。**结构型模式有七种,分别是适配器模式(Adapter Pattern)、桥接模式(Bridge Pattern)、装饰器模式(Decorator Pattern)、组合模式(Composite Pattern)、外观模式(Facade Pattern)、享元模式(Flyweight Pattern)和代理模式(Proxy Pattern
  • **行为型模式(Behavioral Patterns):这类模式关注对象之间的交互和通信,它们描述了不同对象之间如何协作实现单个对象无法完成的任务。**行为型模式有十一种,分别是策略模式(Strategy Pattern)、模板方法模式(Template Method Pattern)、观察者模式(Observer Pattern)、迭代器模式(Iterator Pattern)、责任链模式(Chain of Responsibility Pattern)、命令模式(Command Pattern)、备忘录模式(Memento Pattern)、状态模式(State Pattern)、访问者模式(Visitor Pattern)、中介者模式(Mediator Pattern)和解释器模式(Interpreter Pattern)。

单例模式

Java单例模式怎么用?看这篇就够了 - 知乎 (zhihu.com)
单例模式是一种常见的设计模式,它可以保证一个类只有一个实例,并提供一个全局访问点。单例模式的实现方式有很多种,其中比较常见的有懒汉式和饿汉式两种。懒汉式是指在第一次使用时才创建实例,而饿汉式则是在类加载时就创建实例。除此之外,还有一些其他的实现方式,比如双重检查锁、静态内部类等。

下面是一个简单的单例模式示例代码:

public class Singleton {
    // 声明一个静态变量,但不初始化
    private static Singleton instance;

    // 私有化构造方法,防止外部创建实例
    private Singleton() {}

    // 提供公共的静态方法,返回唯一的实例
    public static Singleton getInstance() {
        // 如果实例为空,就创建一个
        if (instance == null) {
            // 同步代码块,保证线程安全
            synchronized (Singleton.class) {
                // 再次判断实例是否为空,防止重复创建
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这个示例代码是懒汉式的实现方式,它使用了一个静态变量 instance 来保存唯一的实例。在 getInstance 方法中,如果 instance 为 null,则创建一个新的实例并返回;否则直接返回 instance。

枚举类单例模式是一种利用枚举类型的特性来实现单例模式的方法。枚举类型是Java语言中的一种特殊的类,它可以定义一组常量,每个常量都是一个枚举对象,而且这些对象是在类加载时就创建好的,因此它们是线程安全的。枚举类型还有一个特点,就是它们不能被反射或者序列化破坏,因为枚举类没有构造方法,也没有属性,所以反射无法创建新的对象,而序列化也无法改变对象的状态。这样就保证了枚举类单例模式的安全性和唯一性。

// 枚举类
public enum Singleton {
    // 单例对象
    INSTANCE;
    // 其他方法
    public void doSomething() {
        // ...
    }
}

工厂模式

工厂模式是一种常见的设计模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。工厂模式可以分为简单工厂模式、工厂方法模式和抽象工厂模式三种。

简单工厂模式是指由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类的实例。这种方式虽然简单,但是不够灵活,因为每次新增一个产品类时都需要修改工厂类的代码。

工厂方法模式是指定义一个用于创建对象的接口或抽象类,让子类决定实例化哪一个类。这种方式可以解决简单工厂模式的问题,但是需要为每个产品类都定义一个工厂类。

抽象工厂模式是指提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。这种方式可以解决工厂方法模式需要为每个产品类都定义一个工厂类的问题。

下面是一个简单工厂模式示例代码:

public class SimpleFactory {
    public static Product createProduct(String type) {
        if (type.equals("A")) {
            return new ConcreteProductA();
        } else if (type.equals("B")) {
            return new ConcreteProductB();
        } else {
            return null;
        }
    }
}

public interface Product {}

public class ConcreteProductA implements Product {}

public class ConcreteProductB implements Product {}

这个示例代码中,SimpleFactory 类是简单工厂模式的实现方式。它根据传入的参数 type 来动态决定应该创建哪一个产品类的实例。Product 接口和 ConcreteProductA、ConcreteProductB 类分别代表了产品和具体产品。

生产者-消费者模式

生产者-消费者模式是一种常见的并发同步模式,用于解决生产者和消费者之间的数据交换问题。在这种模式中,生产者和消费者之间通过一个共享的缓冲区进行通信。生产者向缓冲区中插入数据,而消费者从缓冲区中取出数据。这种模式可以有效地解耦生产者和消费者之间的关系,从而提高系统的可扩展性和可维护性。

生产者-消费者模式可以分为两种:有界缓冲区和无界缓冲区。有界缓冲区指缓冲区的大小是固定的,而无界缓冲区则没有大小限制。

下面是一个简单的 Java 示例代码:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ProducerConsumerExample {
    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);

        Thread producer = new Thread(() -> {
            try {
                int i = 0;
                while (true) {
                    queue.put(i++);
                    System.out.println("Produced " + i);
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread consumer = new Thread(() -> {
            try {
                while (true) {
                    int i = queue.take();
                    System.out.println("Consumed " + i);
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        producer.start();
        consumer.start();
    }
}

这个示例代码中,我们使用了 Java 的 BlockingQueue 接口来实现生产者-消费者模式。BlockingQueue 接口提供了 put() 和 take() 方法来插入和删除元素。在这个示例代码中,我们使用了 ArrayBlockingQueue 类来实现有界缓冲区。

策略模式

策略模式是一种行为型设计模式,它定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式可以让算法独立于使用它的客户端而变化,从而提高系统的灵活性和可维护性。

策略模式包含三个角色:抽象策略类、具体策略类和环境类。其中,抽象策略类定义了一个公共接口,具体策略类实现了这个接口,并提供了具体的算法实现,环境类持有一个抽象策略类的引用,并调用其定义的方法来完成具体的操作。

下面是一个简单的 Java 示例代码:

public interface Strategy {
    void execute();
}

public class ConcreteStrategyA implements Strategy {
    @Override
    public void execute() {
        System.out.println("Executing strategy A...");
    }
}

public class ConcreteStrategyB implements Strategy {
    @Override
    public void execute() {
        System.out.println("Executing strategy B...");
    }
}

public class Context {
    private Strategy strategy;

    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    public void executeStrategy() {
        strategy.execute();
    }
}

public class Main {
    public static void main(String[] args) {
        Context context = new Context(new ConcreteStrategyA());
        context.executeStrategy();

        context = new Context(new ConcreteStrategyB());
        context.executeStrategy();
    }
}

在这个示例代码中,我们定义了一个 Strategy 接口,其中包含了一个 execute() 方法。然后我们定义了两个具体的策略类 ConcreteStrategyA 和 ConcreteStrategyB,它们分别实现了 Strategy 接口,并提供了具体的算法实现。最后我们定义了一个 Context 类,它持有一个 Strategy 类型的引用,并调用其 execute() 方法来完成具体的操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值