Skip to content

Latest commit

 

History

History
469 lines (377 loc) · 31 KB

🌟重要知识点汇总.md

File metadata and controls

469 lines (377 loc) · 31 KB

[[9-集合#^f41718|HashSet]]实现原理

HashSet采用哈希表作为存储结构。

哈希表的特点:

  1. 快速查找或插入 哈希表的主要优点是能够在平均情况下以常数时间复杂度(O(1))进行查找、插入和删除操作,使得HashSet在处理大量数据时具有高效的性能。
  2. 无序性 哈希表不保证元素的顺序,因此HashSet中的元素是无序的,与插入的顺序无关。
  3. 唯一性 HashSet中不允许存储重复的元素,每个元素只能出现一次。这是由哈希表的特性所决定的,每个键(元素)只会对应一个哈希值。

哈希表的实现原理:

1. 哈希函数

哈希表依赖于哈希函数。哈希函数可以将对象映射为一个整型的哈希值。Object类中的hashCode()方法就是用于返回对象的哈希值的。这个哈希值是通过对象的内容得出来的,对于不同的对象,其哈希值应当尽量不同

2. 哈希冲突

由于哈希算法的缺陷,哈希函数的映射是有限的,而对象的状态的无限的。因此哈希冲突不可避免。解决哈希冲突的方法通常有开放寻址法链表法。HashSet采用链表法来避免哈希冲突:每个bucket不仅可以存储元素,还可以存储链表或者其他数据结构。当出现哈希冲突时,将相同哈希值的元素放在同一个桶中。

3. equals()方法

equals()方法用于辅助hashCode()方法,保证哈希表能够准确无误的找到想要的值。当哈希表通过哈希函数的计算找到目标元素的地址时,还需要通过equals()方法对比该地址 (桶) 中的元素内容,以确保找到正确的目标元素。


列表新增元素的底层原理

void add(int index,E element) 先看源码:

public void add(int index, E element) {  
    rangeCheckForAdd(index);  
    modCount++;  
    final int s;  
    Object[] elementData;  
    if ((s = size) == (elementData = this.elementData).length)  
        elementData = grow();  
    System.arraycopy(elementData, index,  
                     elementData, index + 1,  
                     s - index);  
    elementData[index] = element;  
    size = s + 1;  
}

这里介绍一下System.arraycopy()方法,该方法的五个参数含义:

  1. Object src 源数组
  2. int srcPos 源数组中的起始位置
  3. Object dest 目标数组
  4. int destPos 目标数组中的起始位置
  5. int length 要复制的元素数量 以上add操作用画图抽象一下: ![[add(int index,E element).png]] 我们假设index=5,数组对象elementData长度为10。那么上述copy操作的源数组和目的数组是同一个,也就是说他在自身操作数据。src = 5 ,destPos = 5+1 ,length=10-5。所以该函数将索引5-9的数据复制到了索引6-10的位置上,此时索引5的位置上元素值没有改变。然后通过elementData[index] = element; 语句将要插入的值赋给索引为5的位置,添加完成后数组元素数量+1。

简述List和Set的区别

Set接口:无序,唯一

实现类: HashSet :基于哈希表,不保证顺序 LinkedHashSet : 在HashSet的基础上维护元素插入顺序 TreeSet :基于红黑树,按照自然顺序或自定义比较器 (内部比较器实现Comparable接口和外部比较器实现Comparator接口) 顺序排序。

向Set集合中存入对象数据时,该对象类应当重写equals()hashCode()方法,以确保Set的正常运作

List接口:有序,不唯一

实现类: ArrayList :基于动态数组,支持随机访问,插入和删除效率低下 LinkedList :基于双向链表,插入和删除效率高,但是随机访问效率低下 Vector :类似于ArrayList,JDK早期版本的列表接口实现类。线程安全,但是效率低下。已经被弃用


迭代器

迭代器专为集合而生,专门实现集合遍历。该接口右三个方法,分别是hasNext() , next(), remove().

  • boolean hashNext() : 检查是否还有下一个元素可以迭代。
  • E next(): 获取下一个元素,并将迭代器的状态前移。
  • void remove(): 从集合中安全地移除上一个 next 方法返回的元素。这是迭代器提供的唯一一种可以在遍历中修改集合的方式。

迭代器正向遍历

	Iterator iterator = list.iterator();  
	while (iterator.hasNext()){   
		iterator.next();
	}

迭代器反向遍历

要在 Java 中实现反向遍历(从后往前遍历)集合,可以使用 ListIterator 接口。ListIteratorIterator 的一个子接口,它提供了一些额外的方法,使你能够在列表中双向移动和操作元素。

  • boolean hasNext(): 检查是否还有下一个元素可以迭代(从前往后)。
  • E next(): 获取下一个元素,并将迭代器的状态前移。
  • boolean hasPrevious(): 检查是否还有上一个元素可以迭代(从后往前)。
  • E previous(): 获取上一个元素,并将迭代器的状态后移。
  • int nextIndex(): 返回下一个元素的索引。
  • int previousIndex(): 返回上一个元素的索引。
  • void add(E e): 在迭代器的当前位置添加一个元素。
  • void set(E e): 用指定的元素替换上一个 nextprevious 方法返回的元素。
  • void remove(): 移除上一个 nextprevious 方法返回的元素。
	ListIterator<String> listIterator = list.listIterator(list.size()); //从最后一个元素开始
	while (listIterator.hasPrevious()){
		listIterator.previous();
	}

使用迭代器移除集合中的元素

Iterator 提供的 remove 方法可以安全地移除元素,是因为迭代器在遍历集合的过程中维护了一个状态,知道当前元素的位置,从而能够在不破坏遍历的一致性的情况下进行元素的移除。

底层原理如下:

  1. 当你通过迭代器的 next 方法获取下一个元素时,迭代器会记录当前元素的索引位置以及其他相关信息。
  2. 当你调用迭代器的 remove 方法时,迭代器会根据记录的位置信息,将对应的元素从底层的数据结构(例如列表)中移除。
  3. 迭代器还会更新内部的状态,以确保它继续从正确的位置继续遍历,而不会遗漏或重复元素。

这种方式可以确保在迭代过程中进行元素的移除而不会破坏迭代的状态。这是因为迭代器与底层集合的结构交互得到了控制,从而保证了一致性。

使用BufferedInputStream的readAllBytes方法代替缓冲数组

方法介绍:`byte[] readAllBytes()`

此方法从输入流中读取所有剩余字节, 该方法会引发线程阻塞,直到读取完所有的剩余字节并检测到字节流关闭,或抛出异常,此方法才会退出。

对比使用readAllBytes方法和使用一般缓冲数组

使用readAllBytes方法:

byte[] bytes = bis.readAllBytes();  //输入流读数据
bos.write(bytes);   //输出流写数据
bos.close();  
bis.close();

使用一般缓冲数组:

byte[] bytes = new byte[1024 * 8];   //8KB缓冲数组
int len;  
while ((len=bis.read(bytes))!=-1){     //进入循环
	bos.write(bytes, 0, len);   //将缓冲数组中的数据全部写入输出流
}  
bos.close();    //关流
bis.close();

readAllBytes()方法使得代码更加简洁。但是请注意,该方法是JDK9加入的方法。

反射面试题

问题一:创建Person对象,以后用new Person()创建还是用反射创建?

通常情况下,使用 new Person() 来创建对象是一种更常见和直观的方法。这是因为它是面向对象编程的基本原则之一,也是大多数编程语言的主流做法,包括Java、C#、Python等。通过直接调用类的构造函数来创建对象,不仅代码简单明了,而且易于理解和维护。

而使用反射来创建对象则是一种更加灵活但也更加复杂的方式。反射允许你在运行时动态地获取和操作类的信息,包括构造函数、方法和字段等。尽管反射在某些情况下很有用,比如在框架、库和一些高级应用中,但它也会带来性能损失和代码可读性下降等问题。此外,使用反射创建对象还可能导致编译器无法检测到的错误,增加了调试的难度。

因此,一般情况下推荐使用 new Person() 这种常规的方式来创建对象,而将反射保留给特殊情况或者需要动态处理类信息的情况下使用。

问题二:反射是否破坏了面向对象的封装性?

反射本质上可以让你绕过编程语言提供的封装性和访问限制机制,直接访问和修改类的私有成员,比如私有字段、方法等。从这个角度来看,反射可能会破坏面向对象编程的封装性原则,因为它允许代码在运行时绕过了编译时的访问控制规则。

然而,是否认为反射会完全破坏封装性是一个有争议的问题。封装性是面向对象编程的一个重要原则,它帮助我们隐藏实现细节,提供更清晰的接.口,并降低代码耦合度。使用反射可能导致代码更加脆弱,容易受到变化的影响,而且可能降低代码的可维护性。

因此,虽然反射可以突破封装性,但在使用时需要慎重考虑。在大多数情况下,遵循封装性原则并通过类的公共接口进行操作是更好的做法,除非特殊情况需要使用反射。总的来说,反射是一把双刃剑,可以带来便利性,但也要考虑其可能带来的后果。

数据库面试题

什么是索引下推,回表,索引覆盖?

要想理解这个问题,先了解一下mysql数据库的架构:

  1. 索引下推 (index pushdown) 索引下推是一种数据查询优化技术,它允许数据库系统在查询时尽可能多地使用索引来过滤数据,从而减少需要扫描的表格行数,提高查询性能。通常,查询会包含一个 WHERE 子句,用于筛选满足特定条件的行。索引下推的思想是,数据库查询优化器会尽量将这些筛选条件应用到索引上,以减少实际访问表格数据的需求。这可以通过将筛选条件直接应用于索引上来实现。
  2. 回表 (table lookup) 回表是指当数据库查询优化器使用了索引来找到满足条件的行后,仍然需要回到原始表格(通常是主表格)上执行进一步的查询操作。这是因为索引通常只包含了部分数据列或者索引键,而不是整个行的数据。当查询需要访问索引中没有的其他列数据时,就需要进行回表操作。回表操作会增加查询的成本,因此索引覆盖可以帮助减少回表的需求。
  3. 索引覆盖 (index covering) 索引覆盖是一种查询优化技术,它通过确保索引包含了查询中需要的所有列,从而避免回表操作。当一个索引能够满足查询的所有需求,包括筛选条件和选择的列,就称为索引覆盖。索引覆盖可以显著提高查询性能,因为它减少了回表操作,减少了对原始表格的访问,从而减少了查询的开销。

为什么脏读能读到事务未提交的数据?

目前主流的关系型数据库,如mysql, oracle等都是基于文件系统进行数据存储的,即数据是持久化到文件系统的。但基于文件系统的随机IO是非常低效的,故数据库都会引入内存池,完成对磁盘数据的缓存,提高性能。 内存池是对所有事务共享的。所谓的未提交,指的是事务未提交到物理文件中,但是数据已经存储到了共享内存中。 当事务a和事务b同时访问一行数据,事务b从数据库读到了事务a未提交的数据,但是事务a进行了回滚,就会导致事务b读到的数据和最终存储空间中的数据不一致的情况,这就是脏读的过程。

innoDB的优点

  1. 事务支持:InnoDB支持ACID(原子性、一致性、隔离性和持久性)事务属性,这使得它非常适合处理需要数据完整性和可靠性的应用程序,如金融系统或在线交易处理(OLTP)应用。

  2. 行级锁定:InnoDB使用行级锁定,而不是表级锁定。这意味着多个事务可以同时修改表中不同的行,而不会相互阻塞,从而提高了并发性能。

  3. 外键约束:InnoDB支持外键约束,这是确保数据完整性的关键特性。您可以定义外键关系,以保持数据的引用完整性,这对于建立复杂的数据模型非常有用。

  4. 崩溃恢复:InnoDB具有崩溃恢复机制,可以在数据库发生崩溃或非正常关闭时自动恢复数据一致性。这有助于减少数据丢失和损坏的风险。

  5. 自动增加列:InnoDB支持自动增加列,使得插入新记录时可以轻松地为主键生成唯一值。

  6. 热备份和恢复:InnoDB支持在线热备份,这意味着您可以在不中断应用程序运行的情况下备份数据库。此外,它支持部分恢复,可以快速还原部分数据,而不需要完全恢复整个数据库。

  7. 高并发性能:由于InnoDB使用行级锁定,它在高并发环境中表现出色。这使得它非常适合需要大量并发读写操作的应用程序。

  8. 支持全文搜索:InnoDB支持全文搜索功能,使得您可以执行复杂的文本搜索操作。

数据结构补充

二叉树的遍历代码实现

二叉树结构可以大致分为根节点(D),左子节点(L),右子节点(R) 二叉树的遍历有前序遍历,中序遍历,后序遍历,层次遍历 在代码实现之前,我们先来定义一个二叉树

class TreeNode {
    int val;
    TreeNode left;  //左节点
    TreeNode right;  //右节点

    TreeNode(int val) {
        this.val = val;
        this.left = null;
        this.right = null;
    }
}
  1. 前序遍历DLR 就是按照根节点,左子节点,右子节点的顺序,递归地遍历树

    👆前序遍历 代码实现:

void preorderTraversal(TreeNode root) {
    if (root == null) {
        return;
    }
    System.out.print(root.val + " ");
    preorderTraversal(root.left);
    preorderTraversal(root.right);
}
  1. 中序遍历LDR 就是按照左子节点,根节点,右子节点的顺序,递归地遍历树

    👆中序遍历 代码实现:

void inorderTraversal(TreeNode root) {
    if (root == null) {
        return;
    }
    inorderTraversal(root.left);
    System.out.print(root.val + " ");
    inorderTraversal(root.right);
}
  1. 后序遍历LRD 就是按照左子节点,右子节点,根节点的顺序,递归地遍历树

    👆后序遍历 代码实现:

void postorderTraversal(TreeNode root) {
    if (root == null) {
        return;
    }
    postorderTraversal(root.left);
    postorderTraversal(root.right);
    System.out.print(root.val + " ");
}

网络编程

BIO, NIO , AIO

5种IO模型、阻塞IO和非阻塞IO、同步IO和异步IO - 知乎 (zhihu.com) JAVA BIO与NIO、AIO的区别(容易理解) - 知乎 (zhihu.com) 别挠头了!我教你什么是BIO,NIO,AIO - 知乎 (zhihu.com)

雪花算法如何解决时钟回拨问题?

雪花算法的详解及时间回拨解决方案 - 白露~ - 博客园 (cnblogs.com) 面试题:雪花算法(SnowFlake)如何解决时钟回拨问题_雪花算法时间回拨处理_XP-Code的博客-CSDN博客 雪花算法是twitter公司开源的,用于在分布式系统中生成唯一的id标识的算法。 该id包含 1bit的符号位, 41bit的时间戳,表示特定时间到当前时间的毫秒数,支持约69年的运行期限 10bit的机器码, 12bit的序列号 雪花算法允许每台机器在1ms内生成4096个唯一id 雪花算法的时钟回拨问题提供以下几种思路来解决:

  1. 异常处理 当系统检测到时钟回拨时,抛出异常,系统终止运行。 这种做法虽然不会导致id重复,但是会影响系统的可用性
  2. 等待时钟回复 系统检测到时钟回拨后,让线程睡眠等待。当系统时间回复,重新开始运作。
  3. 生成备用id 当时钟回拨发生后,通过其他机制来生成备用id。例如引用一个新的时钟系统等。这种方式通常需要人工兜底来确保系统的正确运行。
  4. 采用日志系统 记录过去一段时间内 每台机器在当前毫秒内产生的id的最大值 。当发生时钟回拨问题时,只需要从该 最大值 开始继续递增即可

单例模式的五种实现方式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例。 单例模式的五种实现方式(饿汉式、懒汉式、DCL懒汉式、静态内部类式、枚举单例)、优缺点比较_Marvellous丶的博客-CSDN博客 懒汉式单例演进到DCL懒汉式 深度全面解析_dcl 演变过程-CSDN博客

HashMap多线程死循环问题

hashMap的死循环问题只发生在JDK7之前,从JDK8开始,hashMap的链式寻址改为了尾插法,解决了链表死循环的问题。

hashMap死循环的主要原因是由hashMap的底层实现决定的。因为hashMap在扩容时,采用的是头插法

假如现在hashMap需要扩容,hashMap会创一个新的hashMap,然后使用头插法将旧hashMap中的数据插入新hashMap中。此时新hashMap中一个桶内的数据就会变成反序的。

此时有两个线程,线程1正在准备遍历桶中的元素,此时他的头指针指向A的引用。next()指向B的引用。 与此同时,线程2向hashMap中添加了一个元素,触发了hashMap扩容机制,由于采用头插法,现在,新的hashMap中桶中的元素顺序反过来了。此时线程1的头指针仍然为A,next()指向B。开始遍历,走到B中去,但是在这个新的hashMap中,B的next()又是A,于是他又走到A中去,又由于在扩容前,A的next()是B,这样 线程1 就会陷入死循环,在AB间反复横跳,从而导致计算机CPU占用暴涨,系统发生故障。

如何解决?

  1. 使用线程安全的ConcurrentHashMap代替HashMap,推荐
  2. 使用HashTable代替HashMap,但是性能低
  3. 使用synchronizedlock 加锁,会影响性能

在JDK8之后,HashMap的插入改为了尾插法,此问题也是得到了解决。

工厂模式

乐观锁和悲观锁

在多线程环境下,对共享变量进行数据更新的两种模式: 乐观锁和悲观锁模式

悲观锁模式认认为在数据更新时会有别的线程来争夺资源。因此悲观锁的做法是,第一个获取资源的线程会将资源锁定起来,其他没争夺资源的线程只能进入阻塞队列。等第一个线程释放锁后,其他线程才有机会来争夺资源。synchronized是java中悲观锁的典型实现

乐观锁模式认为在数据更新的时候,其他线程不会争夺资源。因此乐观锁的做法是,在数据更新时不会对共享数据加锁。但是在正式更新数据之前会检查该数据是否被其他线程修改过,如果未修改就将变量更新为最新值,否则就重试,知道成功为止。CAS机制是乐观锁的典型实现。

CAS( compare and swap )机制

CAS机制有三个核心参数:

  • 主内存中存放的共享变量值 V
  • 线程工作内存中存放的共享变量快照 ( 预期值 ) A
  • 需要将共享变量更新到的最新值 B

👆如图。 主存中保存V值,线程要使用V值先要把V值读取到自己的工作内存中,然后计算变成B值,最后写回到主存中去。多个线程共用V值都是如此操作。CAS核心就是B值在写回到主存时会比较主存中的V值与线程中的A值是否相同。如果相同则直接写入,否则 ( 说明V值被其他线程修改过 ) 就会重新从主存中读取V值,作为自己的A值,经过计算得到B值。

值得注意的是,CAS机制中的这步是原子性的 ,是CPU支持的原子性操作,属于硬件层面的保证。所以CAS可以解决多线程并发问题中对共享变量读写的原子性问题。

CAS机制的缺点

1. ABA问题

CAS在操作的时候会检查变量的值是否被修改过,如果没有则更新。但是有一个问题,如果更新后还是一样的内容怎么办?下次检测会检测为未被修改,但是实际上内容已经被修改了。为了解决这个问题,在每次操作的时候加上一个版本号,每次操作就是两个值,一个版本号和某个值。原来的 A->B->A 变成了 1A->2B->3A .在JDK中提供了 AtomicStampedReference类来解决ABA问题。用Pair内部类实现,包含两个属性,分别是版本号和引用。在compareAndSet中先对当前引用进行检查,在对版本号进行检查,全部相等时才更新值。

2. 高资源消耗

如果每次都有很多线程在竞争资源的话,CAS机制会一直重试,这样就会非常耗费CPU性能。因此,如果线程竞争小,CAS是一个很好的选择;反之,使用锁可能是个更好的选择。

synchronized和lock的区别

来源

lock是一个接口,synchronized是java关键字,synchronized是内置的语言实现(指令码:monitorenter和moniterexit)

性能区别

synchronized是托管给JVM来执行的 而lock是由程序员自己来决定上锁与开锁的时机 在JAVA5中,synchronized性能很低,因为这是一个重量级操作,需要调用操作接口,导致有可能加锁操作的耗时比运算本身还多。相比之下lock性能更高一些 到了JAVA6,synchronized在语义上已经很清晰了,可以进行很多优化,导致在JAVA6上synchronized的性能并不比lock差。

机制的区别

synchronized采用的是悲观锁机制,即线程获得的是独占锁。 lock采用的是乐观锁机制。

异常是否释放锁

synchronized在线程发生异常时会自动释放锁,因此不会发生异常死锁。lock异常时不会自动释放锁,所以需要在finally中释放锁。

是否响应中断

lock等待锁的过程可以通过interrupt来中断等待。而synchronized只能等待锁的释放,不可中断。

线程池如何判断线程都已完成?

池化技术

Java池化技术是一种常见的编程技巧。在请求量大时能明显优化应用性能,降低系统频繁建立连接的系统开销。数据库连接池,线程池,对象池等,都是池化技术的具体实现。他们的特点都是将【昂贵的】,【费时的】资源维护在一个特定的池子中,规定其最小连接数,最大连接数,阻塞队列等配置,方便进行统一的管理和复用,通常还会附带一些探活机制,强制回收,监控等功能。

线程池

线程池是一种利用池化技术思想实现的线程管理技术,主要是为了复用线程,便利地管理线程和任务,并将线程的创建和任务执行解耦。创建线程池可以复用已经创建的线程来降低频繁创建和销毁线程所带来的资源开销。Java中主要使用ThreadPoolExecutor类来创建线程池。

线程池生命周期

img img

判断方法

1. isTerminated

使用终止状态来判断线程池任务是否执行完毕。但想要线程池状态发生改变,必须先调用线程池的shutdown方法,否则线程池会一直出于running状态。

缺点: 需要关闭线程池

2. getCompletedTaskCount

获取线程池中已完成的线程数量。如果计划执行任务数( getTaskCount )=已完成任务数 ( getCompletedTaskCount ),则线程池任务就全部执行完毕。

这种方法无需关闭线程池 缺点:这两个方法返回的是一个近似值

3. CountDownLatch

创建一个包含N个任务的计数器,每个任务执行完计数器-1,直到减为0,表示所有任务执行完毕。

CountDownLatch是java中一个类

CountDownLatch的缺点是只能使用一次,被创建后不能重复使用

4. CyclicBarrier

CyclicBarrier和CountDownLatch类似,是一个可以重复使用的循环计数器。它可以调用reset方法重置自己的状态。具体参考JDK文档

他的缺点是使用难度较高。

阻塞队列在线程池中的作用

什么是阻塞队列

阻塞队列说白了就是这样一种思想: 你既然拿不了( 队列是空的 ) ,我就不让你拿 ( 阻塞该线程 )。 你既然放不进去 ( 队列是满的 ) ,我就不让你放 ( 阻塞该线程 )。

为何在线程池中使用阻塞队列

在线程池中活跃线程数达到corePoolSize时,线程池会将后续的task提交到阻塞队列中。 因为 :

  • 线程池创建线程需要获取mainlock全局锁,影响并发效率。因此阻塞队列可以很好的缓冲
  • 如果新任务到达速率快过了线程池处理的速率,那么将耗尽线程池资源。

因此,为了避免线程池中任务堆积,我们使用了阻塞队列来处理多余的任务。

阻塞队列的作用

一个任务被提交到线程池以后,首先会找有没有空闲存活线程:如果有则直接将任务交给这个空闲线程来执行;如果没有则会缓存到工作队列( BlockingQueue workQueue)中;如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。

用阻塞队列来保存那些暂时没有空闲线程可以直接执行的任务,等到线程空闲之后再从阻塞队列中弹出任务来执行。一旦队列为空,那么线程就会被阻塞,直到有新任务被插入为止。
阻塞队列是一种数据结构,用来存储任务,由线程池来控制对阻塞队列的操作(插入、弹出等)。

参考:线程池中的锁

死锁

产生的原因

死锁产生的四个必要条件:

  1. 互斥条件 至少有一个资源只能被一个线程占用,不能同时被多个占用
  2. 请求和保持 线程或进程已经获取了一些资源,并且在请求其他资源时阻塞等待
  3. 不可剥夺 资源不能强制性地从一个线程或进程中释放。只能由持有者主动释放
  4. 循环等待 一组线程或进程之间形成了循环等待资源的的关系,每个线程都在等待下一个线程持有的资源

避免死锁

只需要破坏死锁产生的条件即可。其中互斥条件不能被破坏,这是多线程并发执行的基础。因此只能从后面三个条件入手。.

  1. 破坏请求和保持条件 让一个线程在一开始就申请全部所需资源,而不是在执行过程中等待额外的资源。
  2. 破坏不可剥夺条件 引入资源剥夺机制,允许系统在必要时强制剥夺某个进程或线程所持有的资源
  3. 破坏循环等待条件 使用资源分级策略,确保线程只按照一定的顺序访问资源,这样就不会形成等待环路。

并发编程的三特性: 原子性,有序性,可见性

并发编程想要正确地执行,必须同时保证原子性,有序性和可见性。三者缺一,就可能导致安全问题。

三特性

原子性 : 一个操作或多个操作要么全部执行,要么都不执行。 可见性: 多个线程修改同一个共享变量时,要保证其可见性。即一个线程修改完毕,其他线程能够立即获取修改后的值 有序性: 线程执行顺序按照代码先后顺序

具体实现

原子性: 通过synchronized关键字和lock来实现原子性 可见性和有序性: 通过volatile关键字保证可见性和有序性 当一个变量被volatile修饰时,他会保证修改的值立即被更新到主存中,因此保证了共享变量地可见性。

volatile关键字

volatile 字面意思为 不稳定的 易挥发的 易变的 一旦一个共享变量被volatile修饰后,就具备了两层语义:

  1. 保证了不同线程对这个变量进行操作时的可见性
  2. 禁止进行指令重排序

把一个变量定义为valotile就是在告诉编译器,这个变量可能会被意想不到的改变,你不要试图去“假设”这个变量的值,而是要每次都去小心的重新读取这个变量的值。

通常编译器为了优化代码,会对重复出现的值进行缓存处理,但是这种优化行为有时候会出现问题。例如,如果一个变量是一个共享变量,在多线程环境下,通过缓存来获取值显然是不可取的,因为共享变量随时可能在其他线程中发生变化。

通过使用volatile关键字修饰该变量,编译器和运行时系统就不会对该变量的操作进行优化处理(例如缓存和重排序)。从而保证了对该变量的读写操作具有可见性和有序性,确保了不同线程对该变量访问的时效性和一致性。

参考:volatile关键字解析

blocked和waiting的区别?

👉 线程的生命周期

首先,二者都会暂停线程的执行。二者最大区别是: 进入 waiting 状态是线程主动的,而进入blocked状态是被动的。

waiting: 线程根据某种条件主动等待的状态,通常用于线程之间的协调和通讯。比如等待某个时间或通知的发生 blocked: 是线程在尝试获取锁或资源时,被其他线程占用而被动挂起的状态,通常与锁竞争相关。

事务底层原理

务底层原理

mysql日志

MySQL三大日志