当前位置: 首页 > article >正文

Java集合+并发(部分)

Java集合

Java集合类的继承结构和各自的适用情况

Collection

​ — List

​ — ArrayList:动态数组

​ — LinkedList:底层是双向链表,应用于Queue接口可以用于实现队列,应用于Deque接口可以用于实现栈

​ — Vector:线程安全的动态数组,但由于性能较低已被废弃(其下有Stack)

​ — Set

​ — HashSet:哈希集合,底层数据结构是哈希表

​ — LinkedHashSet:底层数据结构是哈希表+链表,可以保证先进先出

​ — TreeSet:底层数据结构是红黑树

​ — Queue

​ — PriorityQueue:优先队列

​ — Deque:Double Ended Queue,双端队列,可以在队列两端进行插入、删除操作,因此既可以作为队列,也可以作为栈

​ — ArrayDeque

​ — LinkedList

Map

​ — HashMap(哈希表)

List适用于需要顺序存储的情况

Set适用于需要存储无顺序、不重复的情况

Queue适用于队列存储

Map适用于存储键值对

List

1. ArrayList底层实现

ArrayList底层是Object数组。

ArrayList实现了以下接口:

List:是一个顺序列表

RandomAccess:支持快速随机访问

Cloneable:支持深拷贝、浅拷贝

Serializable:支持序列化

观察ArrayList源码,默认长度是10,默认一开始是空数组,当插入第一个元素时,数组长度变为默认的10。当继续插入元素时,如果数组中元素达到长度极限,调用grow方法扩容数组,如果没有达到极限,直接插入。

观察ArrayList的构造方法,有三种。一种是什么都不传入,默认是空数组。第二种是传入容量,则创建对应大小的数组。第三种是传入Collection接口下的集合,将其他集合类型转为ArrayList。

2. ArrayList的扩容原理

当调用add方法添加元素时,首先调用ensureCapacityInternal方法,判断当前元素数量和当前数组长度的关系。如果数组够用,继续插入元素。如果数组不够用,调用grow方法进行数组扩容。

grow方法:

使用位运算将数组容量扩充为原来的1.5倍,检查是否满足需求,如果满足,将新容量作为数组容量,如果不满足,将最小需求作为数组容量。调用Arrays.copyOf方法创建一个拥有新容量的数组,并把原数组复制过去。

3. ArrayList与Vector的区别

ArrayList是线程不安全的,Vector内部调用了synchronized关键字,是线程安全的

4. ArrayList插入、删除元素的时间复杂度是多少

头部插入元素:所有元素右移,时间复杂度O(n)

尾部插入元素:如果不需要扩容,时间复杂度O(1);如果需要扩容,需要复制元素到新数组,时间复杂度O(n)

某一位置插入元素:右边所有元素右移,时间复杂度O(n)

头部删除元素:所有元素左移,时间复杂度O(n)

尾部删除元素:O(1)

某一位置删除元素:右边所有元素右移,时间复杂度O(n)

5. ArrayList是否实现了RandomAccess接口

RandomAccess是一个标记接口,表示该类支持快速随机访问(也就是通过索引在O(1)时间内快速访问某元素)。

ArrayList可以通过索引访问,是支持RandomAccess接口的。

6. LinkedList的底层原理是什么

LinkedList的底层原理是双向链表,内部的链表节点中存储:本节点值data、前一个节点指针prev、后一个节点指针next。

LinkedList实现的接口:

List:支持顺序存储

Deque:支持双端队列,由此可以作为栈或队列的底层数组结构

Cloneable:支持拷贝

Serializable:支持序列化

7. LinkedList插入、删除元素的时间复杂度

头部插入/删除:O(1)

尾部插入/删除:O(1)

指定位置插入、删除:需要遍历到指定位置,O(n)

8. LinkedList如何作为队列和栈

作为队列:

Queue<Integer> queue = new LinkedList();
queue.offer(1); // 添加元素
queue.poll();  // 弹出队首元素
queue.peek();  // 获取队首元素但不弹出

作为栈:

Deque<Integer> stack = new LinkedList();
stack.push(1);  // 入栈
stack.pop();  // 出栈
stack.peek();  // 获取栈顶元素但不弹出

另外,在回溯问题中,会使用LinkedList存储path,在添加入path时使用path.add(),在遍历完当前path后使用removeLast()方法撤销。最后,在将path添加到List<> ans中时,将其转为ArrayList,ans.add(new ArrayList(path));

9. CopyOnWriteArrayList保证线程安全的原理

ArrayList是线程不安全的,Vector是线程安全的,但Vector实现线程安全的方式是读写都加synchronized,性能较低,目前已被废弃。

现在实现线程安全的动态数组的是CopyOnWriteArrayList。

CopyOnWriteArrayList保证线程安全的原理是读写锁,即读不加锁,写加锁,这样保证了读操作的性能。

在此基础上,使用写时复制(CopyOnWrite, COW)策略,进一步保证了写操作也不会影响读操作。具体来说,当需要进行写操作时,不会直接修改原数组,而是创建一份副本,在副本数组上进行修改,修改完后再将数组复制回去。这样保证了写操作不会影响读操作。

但这样做也有一些缺点:

1)写操作时间、空间开销,每次写操作都要复制一份数组,改后再复制回去

2)可能导致数据一致性问题

Set

1. HashSet底层原理

HashSet底层是用HashMap实现的,具体来说,HashSet的元素实际上存储在HashMap的key中,而value存储一个固定的Object对象。

HashSet的特点是无序性和不可重复性。

无序性指的是:HashSet不按元素插入顺序进行存储,而是根据其哈希值进行存储。

不可重复性是指:HashSet内部不允许出现重复元素,因此经常被用来去重。

2. HashSet插入、删除元素的时间复杂度

HashSet插入、删除元素的时间复杂度都是O(1)。

3. HashSet、LinkedHashSet、TreeSet的适用场景分别是什么

HashSet适用于无序、无重复元素场景。

LinkedHashSet适用于需要先进先出的场景。

TreeSet适用于需要自定义排序的场景。

Queue

1. Queue和Deque的区别

Queue是单端队列,只能队尾插入元素,队首删除元素。

Deque是双端队列,两端都能插入、删除。

2. ArrayQueue和LinkedList区别

ArrayQueue和LinkedList都实现了Deque接口,都可以实现双端队列的功能。

但二者的底层数据结构不一样。ArrayQueue是基于动态数组和双指针实现的,LinkedList是基于双向链表实现的。

3. PriorityQueue底层数据结构是什么,插入删除的时间复杂度是多少

底层数据结构是二叉堆,即父节点的值一定小于或等于子节点的值。PriorityQueue poll出的堆顶元素一定是最小的。

插入、删除元素的时间复杂度是O(logn)

遍历priorityqueue需要先将其放入ArrayList中,然后使用for-each遍历:

List<Integer> pq_list = new ArrayList(pq);
for(Integer i : pq_list) {
    System.out.println(i);
}

自定义排序:

PriorityQueue<Integer> pq = new PriorityQueue<Integer>(new Comparable<Integer>(){
    @Override
    public int compare(Integer i1, Integer i2) {
        return i2 - i1;  // 大顶堆
    }
});

4. ArrayBlockingQueue是什么

ArrayBlockingQueue是阻塞队列,主要用于生产者和消费者之间的通信。

即生产者线程向其中添加元素,消费者线程从其中提取元素。

可以分别实现阻塞式和非阻塞式的put和take。

阻塞式:put和take,当队列满时,阻塞put操作,直至队列有空余。当队列空时,阻塞take操作,直至队列有元素。

非阻塞式:offer和poll,当队列满时,不阻塞,而是offer方法直接返回false,添加失败;当队列空时,不阻塞,而是poll直接返回null,获取到的是空。

使用BlockingQueue实现生产者—消费者模式:

class Producer extends Thread {
    private BlockingQueue<String> bq;

    public Producer(BlockingQueue<String> bq) {
        this.bq = bq;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            String new_product = "Product " + i;
            try {
                bq.put(new_product);
                System.out.println("Produced: " + new_product);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Consumer extends Thread {
    private BlockingQueue<String> bq;

    public Consumer(BlockingQueue<String> bq) {
        this.bq = bq;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            try {
                String product = bq.take();
                System.out.println("Consumed: " + product);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Main {

    public static void main(String[] args) {
        BlockingQueue<String> bq = new ArrayBlockingQueue(10);

        Producer producer = new Producer(bq);
        Consumer consumer = new Consumer(bq);

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

5. DelayQueue是什么

DelayQueue是延迟队列,可以用于实现延时任务,例如“订单下单15分钟未支付自动取消”功能。

DelayQueue中的元素必须实现Delayed接口,并重写getDelay()方法,用于计算是否到期。DelayQueue底层是用PriorityQueue实现的,默认按照到期时间升序排列,当getDealy()方法返回值小于0时,移除队列。

Map

1. HashMap底层原理

HashMap底层原理是哈希表+链表,即元素首先根据key的hashcode计算哈希值,存储在对应哈希表中。如果出现哈希碰撞,使用拉链法解决。当链表长度大于8时,会将链表转为红黑树,以提高搜索效率。(当整个哈希表长度不超过64时,不会转为红黑树,而是进行数组扩容)

HashMap添加、删除元素的时间复杂度:

如果没有出现哈希冲突,是O(1)。

如果出现了哈希冲突,且哈希冲突用链表解决,是O(n)

如果出现了哈希冲突,且哈希冲突用红黑树解决,是O(logn)

2. HashMap数组扩容的原理

HashMap默认长度是16,默认负载因子是0.75,也就是说,75%的数组有数据,25%的数据无数据,这是为了尽量减少哈希碰撞。当数组元素数量>数组容量*负载因子时,会对hashmap的数组进行扩容。

数组扩容的原理:

默认将数组扩容为原来的2倍。扩容方式是:根据新的长度重新计算所有元素的哈希值,将原来的元素复制到新的哈希位置。

3. HashMap数组长度为什么是2的幂次方/为什么扩容是乘以2

1)可以用位运算进行取余操作来获得哈希位置,效率更高

2)可以保证哈希值分布比较均匀

3)扩容时只需检查哈希值高位来判断是否变化位置

4. HashMap插入新元素的流程

首先,判断哈希数组是否为空,如果为空,先扩容到默认值16.

其次,计算哈希值。

如果对应哈希位置没有元素,直接插入在这一位置。

如果对应哈希位置有元素,判断这一元素key与要插入的key是否相同,如果相同直接修改对应value。

如果不相同,判断这一位置上的元素的红黑树节点还是链表节点。

如果是红黑树节点,调用putTreeVal放入树中。

如果是链表节点,遍历到链表,如果有相同的key,直接修改对应value,如果没有,遍历到链表末尾后在末尾插入新元素。在链表末尾插入元素后,判断是否需求将链表转为红黑树。

插入完成后,根据数组长度和负载因子判断是否需要扩容。

5. JDK1.7以前的HashMap在多线程环境下为什么会导致死循环

JDK1.7以前的Hashmap链表,插入新元素时采用的是头插法,而多线程环境下,多个线程同时操作链表,可能导致头节点指向错误的位置,从而导致形成环形链表。

为了解决这个问题,JDK1.8以后链表插入新元素改为了尾插法。

但多线程环境下还是建议使用ConcurrentHashMap。因此即使没有死循环的问题,多线程环境下也可能导致数据覆盖、数据丢失等情况。

6. 如何遍历hashmap

三种方法:1)使用keySet;2)使用valueSet;3)使用entrySet;

HashMap<Integer, Integer> map = new HashMap();
for(Integer key : map.keySet()){}
for(Integer value: map.valueSet()){}
for(Map.Entry<Integer, Integer> entry : map.entrySet()){}

7. ConcurrentHashMap是怎么保证线程安全的

JDK1.8以前,ConcurrentHashMap是通过分段加锁保证线程安全的。

而JDK1.8以后,是使用CAS+synchronized关键字实现的,只锁定当前数组元素或链表和红黑树的头节点,锁粒度更细,只要不在同一个节点,就不影响其他线程,效率更高。

当插入新元素时,先根据key计算出哈希值,如果当前位置没有元素,使用CAS尝试插入元素,如果失败就不断重复尝试自旋插入,如果成功,直接break。

如果当前位置有元素,获取synchronized锁,锁定当前链表/红黑树插入元素。

8. LinkedHashMap的底层原理

LinkedHashMap底层是使用HashMap+双向链表实现的,在hashmap哈希存储节点时,还维护了一个双向链表,来存储元素的插入顺序。

当使用迭代器遍历LinkedHashMap时,按照插入顺序遍历。

9. LinkedHashMap如何按照访问顺序存储

当希望按照访问顺序输出时,只需将LinkedHashMap的accessOrder设置为true,这样,当使用get方法访问元素时,会将其移动到链表末尾。当按照链表遍历时,即可得到访问顺序输出。

10. LinkedHashMap如何实现LRU缓存

LinkedHashMap可以用于实现LRU缓存。

1)继承LinkedHashMap

2)构造方法:调用super构造方法并设置accessOrder为true,当访问某元素时,将其移到链表最末尾。存储容量。

3)重写LinkedHashMap的removeEldestEntry方法,这个方法返回一个布尔值,告知LinkedHashMap是否需要移除链表head元素,在这个方法中,判断链表长度是否超过容量。

Java并发

线程和进程

1. 什么是进程,什么是线程,Java中是如何规定的

进程是内存分配的基本单位,线程是CPU调度的基本单位。线程是轻量级的进程,执行开销小,但因为共享部分资源,不利于资源的管理和保护。

Java中多个线程共享进程的堆和方法区,但每个线程有独立的程序计数器、虚拟机栈、本地方法栈。

Java中的线程是内核级线程,一个Java线程对应一个内核线程,可以利用多核CPU。

程序计数器为什么是私有的?

程序计数器用于指示线程执行的位置,保证线程切换时能够恢复到正确的位置,因此必须是线程私有的。

虚拟机栈用于存储线程执行中的局部变量、操作数栈、常量池引用等信息,每个线程都不一样,所以是线程私有的。

本地方法栈与虚拟机栈类似,但虚拟机栈存储的是Java方法执行信息,本地方法栈存储的是Native方法执行信息。

2. 线程的生命周期

初始态(NEW):线程刚被创建,还没有调用start方法执行

运行态(RUNNABLE):线程调用start方法执行

阻塞态(BLOCKED):线程被锁阻塞

等待态(Waiting):线程需要等待其他线程通知或中断,调用wait方法可以进入该状态

超时等待态(TIME_WAITING):同样等待其他线程动作,但超时后自动返回。使用sleep(long millis)或wait(long millis)可以进入该状态

终止态(TERMINATED):线程运行完毕

3. Java中如何创建线程

有两种方法:

1)继承Thread类

public class MyThread extends Thread {
    @Override
    public void run() {
        // 
    }
}

MyThread myThread = new MyThread();
myThread.start();

2)实现Runnable接口

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        //
    }
}
Thread myThread = new Thread(new MyRunnable());
myThread.start();

4. 什么是线程的上下文,什么时候会发生上下文切换

线程的上下文就是线程执行所需的程序计数器、虚拟机栈等信息。

线程上下文切换就是指当切换线程时,先保存当前线程的上下文信息,然后将下一个线程的上下文信息加载到CPU中。

线程切换的几种情况:

1)调用sleep()、wait()方法时主动让出CPU

2)时间片用完

3)发生阻塞,例如IO阻塞

4)被终止或线程结束运行

5. Thread的sleep方法和Object类的wait方法有什么异同

相同:二者都会暂停线程的执行,让出CPU

不同:sleep方法不会释放锁,wait方法会释放锁。wait后线程不会主动苏醒,必须有其他线程notify;sleep到时间后线程会自动苏醒。

6. 可以直接调用Thread类的run方法来启动线程吗

不能。直接调用Thread类的run方法只是将这个方法当作main线程中的一个普通方法去调用,不会启动新线程。而调用Thread类的start方法时,会启动一个新线程,执行相应准备工作,然后调用run方法。

7. 什么是并发,什么是并行

并发:多个线程在同一时间段内同时执行

并行:多个线程在同一时刻同时执行

8. 什么是同步,什么是异步

同步:发出一个调用后必须等待调用返回才能继续执行

异步:发出一个调用后不等待返回,直接向下执行

9. 为什么要使用多线程

1)从计算机底层角度来说,对于单核CPU,在执行耗费时间的IO操作时可以切换到其他线程,提高CPU利用率;对于多核CPU,可以有效利用多核CPU的能力。

2)从互联网发展趋势来说,多线程并发编程是提高实现系统高并发的基础。

10. Java使用的系统线程调度方式是什么

操作系统调度线程有两种方式:抢占式和协同式。

抢占式:操作系统决定何时切换线程,会造成上下文切换的开销,但公平性较好,不容易出现线程阻塞;

协同式:线程自己决定何时切换并通知系统,上下文切换开销较小,但公平性较差。

Java使用的是抢占式,JVM本身不负责线程调度,而是交给操作系统。

11. 单核CPU能运行多线程吗,一定能够提高效率吗

单核CPU可以运行多线程,操作系统通过时间片轮转的方式将CPU时间分配给不同线程。

对于IO密集型任务,可以提高效率,因为在执行IO任务时可以将CPU给其他线程使用;

对于CPU密集型任务,不能提高效率,因为大量线程都需要使用CPU,进行线程切换反而会造成上下文切换的开销。

12.什么是死锁,死锁形成的四个条件,怎样检测、预防、避免死锁

死锁是指线程循环等待其他线程持有的锁,导致多个线程被同时阻塞。

死锁形成的四个条件:

1)互斥条件:一个锁不能被多个线程占有

2)请求和保持条件:线程已占用一些资源,且同时请求另一些资源,而且等待资源时不释放已有资源

3)不剥夺条件:线程占有的资源未使用完之前不会被其他线程强行剥夺,只能自己使用完毕后释放

4)循环等待条件:多个线程构成循环等待链

检测死锁:

可以用jmap、jstack等查看堆栈内存的分配情况;或使用Visual VM、JConsole等工具,连接对应程序后排查死锁。

预防死锁:

  1. 破坏互斥条件:一般不太可行,因为很多资源本身就是天然互斥的。
  2. 破坏请求与保持条件:可以要求进程一次性请求所有需要的资源,而不是逐步请求。
  3. 破坏不可剥夺条件:当一个进程请求新的资源得不到满足时,释放已占有的资源。
  4. 破坏循环等待条件:可以对资源进行编号,规定进程只能按照编号递增的顺序请求资源。

避免死锁:

银行家算法:通过提前判断资源分配的安全性,来决定是否为进程分配资源。

JMM

1. JMM是什么

JMM是Java内存模型,它是Java程序与JVM实际内存区域之间的一种抽象的模型,并不是真实存在的。

JMM将JVM内存区域分为主内存和本地内存。主内存是所有线程共有的,所有线程创建的对象实例都存储在主内存中。本地内存是线程私有的,本地内存中存储的是主内存中共享变量的副本。

2. happens-before原则和指令重排序

happens-before原则按照程序顺序规则、解锁规则、volatile变量规则、传递规则、线程启动规则等梳理指令的顺序。

指令重排序时根据梳理出的happens-before规则来进行。

a happens-before b的含义是,a指令的结果对b是可见的

3. 并发编程的三个特性

原子性:一个原子操作要么不执行,如果执行就要执行完

可见性:线程对共享变量的修改对所有线程都是可见的。Java中用volatile关键字保证变量的可见性,底层原理是强制变量是主存中分配

有序性:JVM编译器进行指令重排序时,会保证按照happens-before原则进行,这是单线程环境下是正确的,但多线程环境下不一定保证正确

Java并发常用关键字和类

1. volatile关键字有什么作用

volatile关键字有两个作用:

1)保证变量的可见性,指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取;

2)禁止指令重排序,如果将一个变量声明为volatile,在对这个变量进行读写时,会插入特定的内存屏障的方式来禁止指令重排序。

volatile只能保证变量的可见性,不能保证对变量操作的原子性。

2. 双重校验锁实现单例模式

public class Singleton {
    private volatile static Singleton singleInstance;
    
    private Singleton() {
      
    }
    
    public static Singleton getSingleInstance() {
        if (singleInstance == null) {
            synchronized(Singleton.class) {
                if (singleInstance == null) {
                    singleInstance = new Singleton();
                }
            }
        }
        return singleInstance;
    }
}

第一次校验:在同步代码块之外校验单例是否为空,如果不为空直接返回,避免进入同步代码块;

第二次校验:在进入同步代码块之后,再次校验单例是否为空,避免多个线程同时通过第一次校验进行同步代码块,导致创建多个实例。

另外,使用volatile关键字的作用:1)保证单例变量的修改对所有线程可见,避免出现一个线程初始化变量后,其他线程检测到的仍然是null的情况。2)禁止指令重排序,避免出现已分配内存还没初始化的时候,由于指令重排序,直接返回了没有初始化的内存地址的情况。

3. 乐观锁和悲观锁是什么,各有什么优势和缺点,适合什么场景

乐观锁是指总是假设最好的情况,认为共享资源每次被访问都不会出现问题,因此不加锁,只是在提交修改时验证资源是否被其他线程修改了,验证方法:CAS算法/版本号机制。

悲观锁,总是假设最坏的情况,认为共享资源每次被访问都会出现问题,因此每次操作都加锁。

乐观锁可以避免锁竞争造成线程阻塞,也不会有死锁问题。但当写占比非常多的情况下,冲突频繁发生,会不断尝试自旋重试,因此会导致频繁失败重试影响性能。因此乐观锁更适用于读操作较多,写操作较少的场景。

悲观锁可以避免频繁失败重试影响性能,但无法避免锁的开销。因此悲观锁更适用于写操作较多,读操作较少的场景。

4.乐观锁的实现方式:CAS算法

CAS算法:Compare and Swap,当变量当前值等于期望值时,认为没有线程修改过变量,可以更新。如果不相等,自旋重复尝试。

Java中CAS算法是用Unsafe类实现的,提供了compareAndSwapObject、compareAndSwapInt、compareAndSwapLong等方法,这些方法都是native方法,说明是用本地代码实现的,通常是C或C++

CAS算法可能的问题:

1)ABA问题

即变量先是A,被其他线程改成了B,又被另一个线程改回了A。此时与期望值仍然相同,CAS算法误以为没有被其他线程修改过。

2)循环开销时间大

由于 CAS 操作可能会因为并发冲突而失败,因此通常会与while循环搭配使用,在失败后不断重试,直到操作成功。这就是自旋锁机制,这会导致循环开销。

3)只能保证一个共享变量的原子操作

CAS操作仅对单个共享变量有效。

解决ABA问题的方法:

版本号机制:在变量前加版本号或时间戳,先检测变量值是否等于预期值,再检查变量版本号是否等于预期版本号。版本号一般是在前面加一个version字段,每次修改version++。

4. synchronized关键字是什么,有什么作用,如何使用,底层原理是什么

synchronized关键字用于保证资源访问同步性,它可以保证任何时刻只有一个线程进入代码块。

synchronized关键字有三种使用方法:

  1. 修饰实例方法:获取对象锁
  2. 修饰静态方法:获取类锁
  3. 修饰代码块:取决于括号里的参数,如果是类就是类锁,如果是某个object对象或this,就是对象锁

synchronized关键字的底层原理:

synchronized修饰代码块时,本质是使用monitorenter指令和monitorexit指令,其中monitorenter指向代码块起始位置,monitorexit指向代码块结束位置。

synchronized修饰方法时,没有使用monitorenter和monitorexit指令,而是给方法添加ACC_SYNCHRONIZED标识,表示该方法是一个同步方法。

但无论是使用monitorenter/monitorexit还是使用ACC_SYNCHRONIZED标识,本质都是获取JVM的监视器锁Monitor。

5. synchronized锁升级

每个对象头中会有有个Mark Word,用于记录哈希码、锁状态、GC分代年龄等信息。

对象的锁状态有四种:

1)无锁状态。

2)偏向锁:当只有一个线程访问同步代码块时,会使用偏向锁;

3)轻量级锁:当多个线程竞争锁时,JVM 会将偏向锁升级为轻量级锁。轻量级锁通过 CAS 操作尝试获取锁,避免线程阻塞。

4)重量级锁:当自旋等待超过一定次数后,JVM 会将锁升级为重量级锁。重量级锁会导致线程阻塞,进入操作系统内核态,性能开销较大。

synchronized修饰的代码块会使锁随线程数增多不断升级,锁只能升级不能降级。

synchronized和volatile的关系

synchronized和volatile是互补的,经常互相配合使用。

synchronized用于修饰代码块,保证多线程访问资源的同步性。volatile用于修饰变量,保证变量的可见性。

volatile只能保证可见性和有序性,不能保证原子性。synchronized三者都能保证。

volatile比synchronized更轻量级。

6. ReentrantLock是什么

ReentrantLock是可重入锁,即如果一个线程已经占有这个锁,当它再次请求这个锁时可以请求到。

实际上synchronized锁也是可重入的,但ReetrantLock比synchronized功能更完善。

ReentrantLock有公平锁和非公平锁两种实现,默认使用非公平锁。

ReentrantLock底层是用AQS实现的。

7. synchronized与ReentrantLock的异同

相同:都是可重入锁

不同:

1)ReentrantLock可以实现公平锁也可以实现非公平锁,synchronized只能实现非公平锁;

2)synchronized基于JVM实现,ReentrantLock基于JDK实现

3)ReentrantLock还提供了等待中断、超时、条件等高级功能。

7. 公平锁、非公平锁;可中断锁、不可中断锁;共享锁、独占锁

公平锁:按照申请顺序分配锁,保证了时间上的绝对顺序,但性能较差;

非公平锁:不按申请顺序,而是按照一定的优先级分配锁。

可中断锁:获取锁的过程中可以被中断,不需要一直等到获取锁之后才能进行其他逻辑处理。ReentrantLock就属于是可中断锁。

不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。 synchronized就属于是不可中断锁。

共享锁:一把锁可以被多个线程同时获得,比如读锁是共享锁

独占锁:一把锁只能被一个线程获得,比如写锁是独占锁

8. 读写锁

读写锁既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。

一般锁的互斥条件:读读互斥、读写互斥、写写互斥

读写锁的互斥原理:读读不互斥、读写互斥、写写互斥

Java中实现的读写锁:ReentrantReadWriteLock、StampedLock。其中ReentrantReadWriteLock是可重入的读写锁,StampedLock是不可重入的读写锁。

读写锁适用于读多写少的场景。

AQS

AQS全称是AbstractQueueSynchronizer,即抽象队列同步器。

顾名思义,AQS是一个用于构建锁和同步器的抽象类,主要提供了可重入锁、信号量、倒计时器等一些通用框架。

AQS的底层原理

AQS底层是基于CLH锁队列的变体实现的。

CLH锁是一种基于CAS算法的优化。在CAS算法中,获取不到锁时会自旋重复获取。这种自旋方式可能导致某个线程长时间获取不到锁,造成饥饿。

为了解决这个问题,CLH引入了一个队列来组织并发竞争的线程。每个竞争的线程按顺序加入到队列中排队,保证公平性。

当使用CAS方法获取锁失败时,先短暂自旋尝试获取,如果仍然失败,阻塞线程加入队列等待被唤醒,防止重复自旋占用CPU。

AQS中CLH锁队列

队列中由各个节点构成,每个节点都有自己的状态。

例如三个线程获取锁时,T1先获取到,初始化等待队列,其中有一个头节点,状态为0;当T2请求锁时,锁已被占用,将T2加入等待队列中,放在头节点的next位置,此时头结点的状态改为SIGNAL(-1),表示当前节点退出时需唤醒后继节点;同样T3申请锁时加入等待队列,放在T2的后面,T2状态改为SIGNAL(-1);当T1释放锁时,唤醒后继节点T2,T2获取锁,成为新的head。

Semaphore

用于访问数量有限的资源,控制访问资源的线程数量。

使用场景:数据库连接池、线程池等数量有限的资源

CountDownLatch

CountDownLatch的作用是让count个线程阻塞,直到所有线程执行完毕。

使用场景:确保在某些任务完成后再执行后续操作,例如多个线程完成数据加载后再进行数据处理。

CyclicBarrier

CyclicBarrier的作用是让count个线程阻塞在屏障处,直到所有线程到达屏障。

使用场景:多线程并行计算时,在某个点合并结果再继续计算。

Atomic原子类

Atomic原子类是指具有原子操作特性的类,使用原子类的变量在操作时要么完整执行,要么不执行。

Atomic实现原理是CAS算法。

Atomic原子类有哪些?

1)基本类型原子类:AtomicInteger、AtomicLong、AtomicBoolean

2)数组类型原子类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

3)引用类型原子类:AtomicReference、**AtomicStampedReference**、AtomicMarkableReference

4)对象属性修改原子类:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater原子更新引用类型中某个字段

Atomic的应用场景:

并发计数

ThreadLocal

对于ThreadLocal创建的变量,每个线程都有一个本地副本。线程内通过threadLocal.get()获取,通过threadLocal().set()设置其值。

ThreadLocal应用场景

数据库连接管理:每个线程可以有一个独立的数据库连接,避免了多个线程共享数据库连接的问题。

ThreadLocal底层原理

每个线程内部都有一个ThreadLocalMap,当用ThreadLocal创建变量时,ThreadLocal为key,对应的变量Object类为value,存储在map中。

ThreadLocal什么情况下会导致内存泄漏?

正常使用继承Thread类来创建线程是不会导致内存泄漏的。因为线程的ThreadLocalMap只被当前线程引用,当前线程结束任务退出以后,这种引用消失,线程对应的ThreadLocalMap就会被GC回收。

但实际项目开发中多使用线程池来实现多线程,这种情况下使用ThreadLocal就可能导致内存泄漏。因为线程池中的线程不会退出,是循环使用的,所以对应的ThreadLocalMap的引用不会消失,ThreadLocalMap不会被GC回收。但是ThreadLocalMap内部存储的是Entry数组,其中有多个key-value对,其中key是ThreadLocal对象,是弱引用的,value是对应的Object对象,是强引用的。当当前线程执行完毕,投入下一次使用时,弱引用的key会被GC回收,但value是强引用的,不会被回收,这就导致value对应的key为null,长期无法被回收就会导致内存泄漏。

如何避免内存泄漏?

每次使用完ThreadLocal对象后,调用remove方法将其移除ThreadLocalMap


http://www.kler.cn/a/530096.html

相关文章:

  • 129.求根节点到叶节点数字之和(遍历思想)
  • 13 尺寸结构模块(size.rs)
  • CSS(快速入门)
  • Ruby 模块(Module)
  • 71.在 Vue 3 中使用 OpenLayers 实现按住 Shift 拖拽、旋转和缩放效果
  • AMS仿真方法
  • MultiResUNet学习笔记(2019 Neural Networks【SCI 1区】)
  • 用结构加法3ax+1预测第4点的分布
  • 掌握Spring MVC异常处理的艺术
  • ICLR 2025收录论文:为什么动作分块对于机器人灵活性至关重要?
  • makailio-alias_db模块详解
  • 蓝桥杯备考:六大排序算法
  • Hive重点面试题
  • #define,源文件与头文件,赋值表达式
  • Java项目: 基于SpringBoot+mybatis+maven+mysql实现的疾病防控综合管理系统(含源码+数据库+毕业论文)
  • springboot中路径默认配置与重定向/转发所存在的域对象
  • react注意事项
  • 6 [新一代Github投毒针对网络安全人员钓鱼]
  • 【JDBC】数据库连接的艺术:深入解析数据库连接池、Apache-DBUtils与BasicDAO
  • 双指针算法思想——OJ例题扩展算法解析思路
  • 悬浮按钮和可交互提示的使用
  • 设计数据库表会考虑哪些内容?
  • 文字投影效果
  • C++ Primer 命名空间的using声明
  • 2025最新在线模型转换工具onnx转换ncnn,mnn,tengine等
  • mysql死锁排查_mysql 死锁问题排查