2024年最新Java基础问题整理(二)_hash表中命中后的dirty代表什么意思(1),原理+实战讲解

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

1、单表查询中,范围查询不宜建立索引,固定值查询可以建立索引。
对于连接查询,左连接给右表建立索引,右连接给左表建立索引。
2、最左前缀匹配原则。MySQL会一直向右匹配直到遇到范围查询就停止匹配,范围查询后面的索引会失效。
3、尽量选择区分度高的列作为索引。
4、索引列不能参与计算,会导致索引失效而转向全表扫描。
5、尽可能的扩展索引,不要新建立索引。

数据库事务

1、事务是指一组最小的逻辑操作单元,里面有多个操作组成。组成事务的每一部分必须要同时提交成功,如果有一个操作失败,整个操作就回滚。
2、事务的ACID特性
(1)原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
(2)一致性(Consistency)
事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
(3)隔离性(Isolation)
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
(4)持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
一、隔离级别
1、读未提交(READ UNCOMMITTED):一个事务还没提交时,它做的变更就能被别的事务看到。
2、读提交(READ COMMITTED):一个事务提交之后,它做的变更才会被其他事务看到。
3、可重复读(REPEATABLE READ):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
4、串行化(SERIALIZABLE):对于同一行记录,“写”会加“写锁”,“读”会加“读锁”,当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
二、隔离级别解决了哪些问题:
1、脏读(dirty read):如果一个事务读到了另一个未提交事务修改过的数据。
2、不可重复读(non-repeatable read):如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值。
3、幻读(phantom read):如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来。
三、如何设置事务的隔离级别?
我们可以通过下边的语句修改事务的隔离级别:

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;//等级就是上面的几种

MVCC多版本并发控制机制

MVCC,Multi-Version Concurrency Control,多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。

(1)MVCC是为了解决什么?

众所众知,在MySQL中,MyIASM使用的是表锁,InnoDB使用的是行锁。而InnoDB的事务分为四个隔离级别,其中默认的隔离级别REPEATABLE READ需要两个不同的事务相互之间不能影响,而且还支持并发,这点悲观锁是达不到的,所以REPEATABLE READ采用的就是乐观锁,而乐观锁的实现采用的就是MVCC,正是因为有了MVCC,才造就了InnoDB强大的事务处理能力。

(2)MVCC原理

MVCC的实现,通过保存在数据在某个时间点的快照来实现的。
在每行记录后面保存两个隐藏的列,一列保存了行的创建时间,另一列保存了行的过期时间(删除时间),这里存储的时间并不是实际的时间值,而是系统版本号,每开始一个新事物,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来与查询到的每行记录的版本号进行比较。

(3)MVCC特征

  • 每行数据都存在一个版本,每次数据更新时都更新该版本。
  • 修改时copy出当前版本进行修改,各个事务之间互不干扰。
  • 保存时比较版本号,如果成功(commit),则覆盖原记录,失败则放弃copy(rollback回滚)。

(4)MVCC实现

1、redo log:重做日志,确保事务的持久性,防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性。重做日志由两部分组成,一是内存中的重做日志缓冲区,因为重做日志缓冲区在内存中,所以她是易失的;另一个就是在磁盘上的重做日志文件,它是持久的。
2、undo log:回滚日志,保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读在MySQL中,恢复机制是通过回滚日志实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后在对数据库中的对应行进行写入。
3、bin log:二进制日志,用于复制,在主从复制中,从库利用主库上的bin log进行重播,实现主从同步。

实现

在每一行数据中额外保存两个隐藏的列:

  • DATA_TRX_ID :记录最近一次修改(insert/update)本行记录的事务id,大小为6字节。
  • DATA_ROLL_PTR:指向该行回滚段(rollback segment)的undo log record指针,大小为7字节。如果这一行记录被更新,则undo log record包含" 重建该行记录被更新之前内容"所必须的信息。InnoDB便是通过这个指针找到之前版本的数据,若改行记录上存储所有的旧版本,在undo中都通过链表的形式组织。

如果表没有主键,则还会有一个隐藏的主键列DB_ROW_ID。

  • DB_ROW_ID:行标识(隐藏单调自增ID),大小为6字节,如果表没有主键,InnoDB会自动生成一个隐藏主键。

(5)可重复读隔离级别下,MVCC具体的操作流程

  • SELECT:InnoDB只查找版本早于当前事务版本的数据行;行的删除版本,要么未定义,要么大于当前事务版本号,这样可以确保事务读取到的行在事务开始之前未被删除。
  • INSERT:InnoDB为插入的每一行保存当前系统版本号作为行版本号。
  • DELETE:InnoDB为删除的每一行保存当前系统版本号作为删除标识,标记为删除、而不是实际删除。
  • UPDATE: InnoDB会把原来的行复制一份到回滚段中,保存当前系统版本号作为行版本号,同时,保存当前系统版本号到原来的行作为删除标识。

数据库操作

(1)DDL(Data Definition Language)用来定义数据库的库,表,列等。
(2)DML(Data Manipulation Language)用来定义数据库记录(增、删、改)
(3)DCL(Data Control Language)用来定义访问权限和安全级别;
MySQL数据库权限问题:root拥有所有权限(可以干任何事情),我们可以分配权限账户。
(4)DQL(Data Query Language)用来查询记录(数据)。
在MySQL中主要使用的是MyISAM引擎和InnDB引擎:

对比MyISAMInnoDB
主外键不支持支持
事务不支持支持
行表锁表锁行锁
缓存只缓存索引,不缓存真实数据不仅缓存索引还要缓存真实数据
表空间
关注点性能事务

分组分页

分组查询:group by ,一般配合聚合函数使用查出的数据才有意义。
查询的字段:1.分组字段本身 2.聚合函数
分页查询:limit 起始索引,每页的索引数
起始索引=(页码-1)*每页的条数

三大范式

数据库的设计原则:尽量遵守三大范式。
(1)第一范式
要求表的每个字段必须是不可分割的独立单元。

student:name   张小凡|狗蛋  -- 违反第一范式
student:name 张小凡 old_name 狗娃 -- 符合第一范式

(2)第二范式
在第一范式的基础上,要求每张表只表达一个意思,表的每个字段都和表的主键有关系。

employee:员工编号 员工姓名 部门名称 订单名称 -- 违反第二范式
员工表:员工编号 员工姓名 部门名称
订单表:订单编号 订单名称    -- 符合第二范式

(3)第三范式
在第二范式基础,要求每张表的主键之外的其他字段都只能和主键有直接决定依赖关系。

员工表: 员工编号(主键) 员工姓名  部门编号  部门名 --符合第二范式,违反第三范式 (数据冗余高)
员工表:员工编号(主键) 员工姓名  部门编号    --符合第三范式(降低数据冗余)
部门表:部门编号  部门名 

HashMap

一、数据结构
  由数组和链表组合构成的数据结构。数组里面每个地方都存了Key-Value这样的实例,在Java7叫Entry在Java8中叫Node。本身所有的位置都为null,在put插入的时候会根据key的hash去计算一个index值。
数组长度是有限的,在有限的长度里面我们使用哈希,哈希本身就存在概率性,极端情况会hash到一个值上,那就形成了链表。
每一个节点都会保存自身的hash、key、value、以及下个节点。

HashMap的扩容机制:数组容量是有限的,数据多次插入的,到达一定的数量就会进行扩容,也就是resize。

二、怎么扩容?
扩容:创建一个新的Entry空数组,长度是原数组的2倍。
ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。
因为长度扩大以后,Hash的规则也随之改变,所以要重新hash。
三、什么时候resize呢?
Capacity:HashMap当前长度。
LoadFactor:负载因子,默认值0.75f。

负载因子较小,发生哈希冲突的概率也会小,但是每次元素个数达到数组长度一半时就要进行扩容,这样岂不是造成了很大的空间浪费。
负载因子较大,空间利用率大大提高了,哈希冲突发生的概率肯定会增大,并且链表长度和红黑树的高度肯定会变大,增删改查的效率也会很低。

HashMap的默认初始化长度是16。位与运算比算数计算的效率高了很多,之所以选择16,是为了服务将Key映射到index的算法。
Hashmap中的链表大小超过八个时会自动转化为红黑树,当删除小于六时重新变为链表,根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。

计算索引的方法index = HashCode(Key) & (Length- 1),0不能出现在index的二进制编码数里面,意思就是要全部为1,二进制编码全为1的数加1一定是一个2的整次幂的数。这也是为什么数组长度在与hash做与运算的时候要减1的原因。

put方法:通过hash方法求出key的hash值(扰动函数)
get方法:返回为key的元素的结点的value值
remove方法:返回要删除的元素的value

jdk1.8中的hashmap相对于jdk1.7中的有什么不同?

(1)jdk1.7是数组+链表的结构,jdk1.8中是数组+链表+红黑树的结构;
(2)jdk1.7链表中数据插入方式是头插法,插入元素放到桶中,原来的元素作为插入元素的后继元素。而jdk1.8中采用的是尾插法,直接放到链表的尾部。
(3)jdk1.7在扩容时需要对元素进行重新哈希以确定元素在新数组中的位置,而jdk1.8中不需要重新哈希,要么存储在和原数组相同的位置,要么存储在原数组位置+原数组长度的位置。
(4)jdk1.7中是先判断是否需要扩容,再插入,而jdk1.8是先插入,再扩容。
改进后的优点
(1)减少哈希冲突,提高操作效率,
(2)因为1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环。

HashMap 和 Hashtable 的区别

(1)HashMap是线程不安全的,Hashtable是线程安全的,每个方法都加了synchronized。
(2)HashMap的效率高于Hashtable,因为Hashtable加了synchronized会发生阻塞,严重影响了效率。
(3)HashMap中允许有null键(一个)null值(多个),Hashtable不允许有。Hashtable在我们put 空值的时候会直接抛空指针异常,安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据。如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理。

  • 实现方式不同:Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类。Dictionary 是 JDK 1.0 添加的,貌似没人用过这个,我也没用过。
  • 初始化容量不同:HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。
  • 扩容机制不同:当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。
  • 迭代器不同:HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的。

所以,当其他线程改变了HashMap 的结构,如:增加、删除元素,将会抛出ConcurrentModificationException 异常,而 Hashtable 则不会。

ConcurrentHashMap

HashMap在多线程环境下存在线程安全问题,那你一般都是怎么处理这种情况的?

  • 使用Collections.synchronizedMap(Map)创建线程安全的map集合;
  • Hashtable
  • ConcurrentHashMap

ConcurrentHashMap 底层是基于 数组 + 链表 组成的。
  在JDK1.7版本中,采用分段锁的思想,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,Segment 是 ConcurrentHashMap 的一个内部类,HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了他的数据Value还有下一个节点next。真正实现线程安全是采用ReentrantLock来实现。
  缺点:无论是put还是get元素都需要进行两次hash,效率慢,并且锁的粒度大。在JDK 1.8中,ConcurrentHashMap采用CAS+Synchronized加锁,锁的粒度减小。

原理上来说,ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。他先定位到Segment,然后再进行put操作。

首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。
尝试自旋获取锁。如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。

get 逻辑比较简单,只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。

存在问题:基本上还是数组加链表的方式,我们去查询的时候,还得遍历链表,会导致效率很低。

jdk1.8他的数据结构是怎么样子的呢?
  其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。跟HashMap很像,也把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰,保证了可见性,并且也引入了红黑树,在链表大于一定值的时候会转换(默认是8)。
  根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。

ConcurrentHashMap在1.8数据的插入put过程?

  • 如果没有初始化就先调用initTable()方法来进行初始化过程。
  • 如果没有hash冲突就直接CAS插入。
  • 如果当前哈希表正在进行扩容操作就先进行扩容。
  • 如果存在hash冲突,就加锁(Synchronized)来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入。
  • 最后一个如果Hash冲突时会形成Node链表,在链表长度超过8,Node数组超过64时会将链表结构转换为红黑树的结构,break再一次进入循环。
  • 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容。

ConcurrentHashMap的get操作又是怎么样子的呢?
  根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。如果是红黑树那就按照树的方式获取值。就不满足那就按照链表的方式遍历获取值。

CAS

CAS(Compare And Swap 比较并且替换)CAS操作包含三个操作数——内存位置(V),期望值(A)和新值(B),如果内存中的值和期望值匹配,那么处理器会自动将位置值更新为新值。是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的。整个比较并替换的操作是一个原子操作。

1、CAS 是怎么实现线程安全的?
  线程在读取数据时不进行加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。
  当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
  在线程开启的时候,会从主存中给每个线程拷贝一个变量副本到线程各自的运行环境中,CAS算法中包含三个参数(V,E,N),V表示要更新的变量(也就是从主存中拷贝过来的值)、E表示预期的值、N表示新值。

2、优点
  这个算法相对synchronized是比较“乐观的”,它不会像synchronized一样,当一个线程访问共享数据的时候,别的线程都在阻塞。synchronized不管是否有线程冲突都会进行加锁。由于CAS是非阻塞的,它死锁问题天生免疫,并且线程间的相互影响也非常小,更重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,所以它要比锁的方式拥有更优越的性能。

3、存在什么问题呢?

  • CUP开销:CAS操作长时间不成功的话,会导致一直自旋,相当于死循环了,CPU的压力会很大。
  • ABA问题:线程1读取了数据A,线程2读取了数据A,线程2通过CAS比较,发现值是A没错,可以把数据A改成数据B,线程3读取了数据B,线程3通过CAS比较,发现数据是B没错,可以把数据B改成了数据A,线程1通过CAS比较,发现数据还是A没变,就写成了自己要改的值。值被改变了,线程1却没有办法发现。
  • 只能保证一个共享变量原子操作:CAS操作单个共享变量的时候可以保证原子操作,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性,JDK 5之后 AtomicReference可以用来保证对象之间的原子性,就可以把多个对象放入CAS中操作。

4、那开发过程中ABA你们是怎么保证的?

  • 加标志位,例如搞个自增的字段,操作一次就自增加一。
  • 加时间戳,比较时间戳的值。
  • 加版本号,判断原来的值和版本号是否匹配。

ReentrantLock

AQS:(AbstractQueuedSynchronizer)也就是队列同步器,这是实现 ReentrantLock 的基础。AQS 有一个 state 标记位,值为1 时表示有线程占用,其他线程需要进入到同步队列等待,同步队列是一个双向链表。当获得锁的线程需要等待某个条件时,会进入 condition 的等待队列,等待队列可以有多个。当 condition 条件满足时,线程会从等待队列重新进入同步队列进行获取锁的竞争。

ReentrantLock 就是基于 AQS 实现的,ReentrantLock 内部有公平锁和非公平锁两种实现,差别就在于新来的线程是否比已经在同步队列中的等待线程更早获得锁。和 ReentrantLock 实现方式类似,Semaphore 也是基于 AQS 的,差别在于 ReentrantLock 是独占锁,Semaphore 是共享锁。
  ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync和非公平锁NonfairSync两个子类。
ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。

线程、线程池

创建线程

进程是拥有资源的基本单位,线程是CPU调度的基本单位。
1、继承Thread类
2、实现Runnable接口
3、实现 Callable 接口。 相较于实现 Runnable 接口的方式,方法可以有返回值,并且可以抛出异常。
4、通过线程池创建

线程的状态

//新生 NEW,
//运行 RUNNABLE,
//阻塞 BLOCKED,
//等待 WAITING,
//超时等待,过时不候 TIMED_WAITING,
//终止TERMINATED;

线程间的四种通信方式

  • 方式一:同步

这里讲的同步是指多个线程通过synchronized关键字这种方式来实现线程间的通信。这种方式,本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。

  • 方式二:while轮询的方式

在这种方式下,线程A不断地改变条件,线程ThreadB不停地通过while语句检测这个条件(例如,list.size==5)是否成立 ,从而实现了线程间的通信。但是这种方式会浪费CPU资源。之所以说它浪费资源,是因为JVM调度器将CPU交给线程B执行时,它没做啥“有用”的工作,只是在不断地测试某个条件是否成立。

  • 方式三:wait/notify机制

这里用到了Object类的 wait 和 notify 方法。

当条件未满足时(list.size !=5),线程A调用wait 放弃CPU,并进入阻塞状态。—不像while轮询那样占用CPU

当条件满足时,线程B调用 notify通知线程A,所谓通知线程A,就是唤醒线程A,并让它进入可运行状态。这种方式的一个好处就是CPU的利用率提高了。

但是也有一些缺点:比如,线程B先执行,一下子添加了5个元素并调用了notify发送了通知,而此时线程A还执行;当线程A执行并调用wait时,那它永远就不可能被唤醒了。因为,线程B已经发了通知了,以后不再发通知了。这说明:通知过早,会打乱程序的执行逻辑。

  • 方式四:管道通信

就是使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信,更像消息传递机制,也就是说:通过管道,将一个线程中的消息发送给另一个。

线程池的五种运行状态

在这里插入图片描述

  • RUNNING : 该状态的线程池既能接受新提交的任务,又能处理阻塞队列中任务。
  • SHUTDOWN:该状态的线程池不能接收新提交的任务但是能处理阻塞队列中的任务。处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。
    注意: finalize() 方法在执行过程中也会隐式调用shutdown()方法。
  • STOP: 该状态的线程池不接受新提交的任务,也不处理在阻塞队列中的任务,还会中断正在执行的任务。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态;
  • TIDYING: 如果所有的任务都已终止,workerCount (有效线程数)=0 。线程池进入该状态后会调用 terminated() 钩子方法进入TERMINATED 状态。
  • TERMINATED: 在terminated()钩子方法执行完后进入该状态,默认terminated()钩子方法中什么也没有做。

线程池的关闭(shutdown或者shutdownNow方法)
  可以通过调用线程池的shutdown或者shutdownNow方法来关闭线程池:遍历线程池中工作线程,逐个调用interrupt方法来中断线程。

shutdown方法与shutdownNow的特点:
  shutdown方法将线程池的状态设置为SHUTDOWN状态,只会中断空闲的工作线程。
  shutdownNow方法将线程池的状态设置为STOP状态,会中断所有工作线程,不管工作线程是否空闲。

调用两者中任何一种方法,都会使isShutdown方法的返回值为true;线程池中所有的任务都关闭后,isTerminated方法的返回值为true。
通常使用shutdown方法关闭线程池,如果不要求任务一定要执行完,则可以调用shutdownNow方法。

如何控制线程池线程的优先级
思路:
  设定一个 orderNum,每个线程执行结束之后,更新 orderNum,指明下一个要执行的线程。并且唤醒所有的等待线程。在每一个线程的开始,要 while 判断 orderNum 是否等于自己的要求值,不是,则 wait,是则执行本线程。

线程池三大分类:

  • //1.创建单个线程的线程池
    ExecutorService executorService = Executors.newSingleThreadExecutor();
  • //2.创建固定线程数量的线程
    ExecutorService executorService1 = Executors.newFixedThreadPool(5);
  • //3.根据任务的多少创建线程数量
    ExecutorService executorService2 = Executors.newCachedThreadPool();

七大参数:创建线程池的方法底层都是调用ThreadPoolExecutor这个方法进行的,它有七个参数。

  • int corePoolSize,//核心线程数
  • int maximumPoolSize,//最大线程数
  • long keepAliveTime,//超时没有人调用会被释放
  • TimeUnit unit,//超时单位
  • BlockingQueue workQueue,//阻塞队列
  • ThreadFactory threadFactory,//线程工厂:创建线程
  • RejectedExecutionHandler handler) //拒绝策略

四种拒绝策略
1、不处理直接抛出异常
new ThreadPoolExecutor.AbortPolicy()
java.util.concurrent.RejectedExecutionException
2、哪来的去哪去,交由主线程处理
new ThreadPoolExecutor.CallerRunsPolicy()
3、直接丢掉任务,不会抛出异常
new ThreadPoolExecutor.DiscardPolicy()
4、去尝试和线程开启最早的竞争cpu,也不会抛出异常
new ThreadPoolExecutor.DiscardOldestPolicy()

为什么要使用线程池(必考)

Java的线程池是运用场景最多的并发框架,几乎所有需要异步或者并发执行任务的程序都可以使用线程池。

合理使用线程池能带来的好处:

  • 降低资源消耗。 通过重复利用已经创建的线程降低线程创建的和销毁造成的消耗。例如,工作线程Woker会无线循环获取阻塞队列中的任务来执行。
  • 提高响应速度。 当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。 线程是稀缺资源,Java的线程池可以对线程资源进行统一分配、调优和监控。

如何合理的设置最大线程数?

分析下线程池处理的程序是CPU密集型,还是IO密集型
(1)CPU密集型
  CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。在多重程序系统中,大部分时间用来做计算、逻辑等CPU动作称之为CPU bound,例如一个计算圆周率至小数点一千位以下的程序,在执行的过程当中绝大部分时间用在三角函数和开根号的计算,便是属于CPU bound的程序。CPU bound的程序一般而言CPU占用率相当高。这可能是因为任务本身不太需要访问I/O设备,也可能是因为程序是多线程实现因此屏蔽掉了等待I/O的时间。

(2)IO密集型
  IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU等等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。I/O bound的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而pipeline做得不是很好,没有充分利用处理器能力。

(3)获取电脑的cpu核数

 System.out.println(Runtime.getRuntime().availableProcessors());

(4)设置

  • CPU密集型:CPU的核数是多少,最大线程数设为多少。
  • IO密集型:判断程序中是否耗IO的线程的数量,最大线程数就设置为这个的2倍。

在IO优化中,线程等待时间所占比例越高,需要越多线程,线程CPU时间所占比例越高,需要越少线程。这样的估算公式可能更适合:最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

集合map,set,list

1、Map
  双列集合,存储的是键值对,键唯一。每个键最多只能映射到一个值。Map集合的数据结构针对键有效,跟值无关。

  • HashMap键的数据结构是哈希表。
  • LinkedHashMap键的数据结构是链表和哈希表,键的特点:有序且唯一,链表保证有序,哈希表保证唯一。
  • TreeMap底层数据结构是二叉树,能够对元素进行排序。

2、set
  单列集合,元素不可以重复

HashSet底层数据结构是哈希表,元素唯一且无序。利用hashmap实现,k固定的不变的值,值就是存储的数据。
  哈希值:对象的地址值,是一个逻辑地址,是模拟出来得到地址,不是数据实际存储的物理地址。
①jdk1.8以前:哈希表=数组+链表

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

链表和哈希表,键的特点:有序且唯一,链表保证有序,哈希表保证唯一。

  • TreeMap底层数据结构是二叉树,能够对元素进行排序。

2、set
  单列集合,元素不可以重复

HashSet底层数据结构是哈希表,元素唯一且无序。利用hashmap实现,k固定的不变的值,值就是存储的数据。
  哈希值:对象的地址值,是一个逻辑地址,是模拟出来得到地址,不是数据实际存储的物理地址。
①jdk1.8以前:哈希表=数组+链表

[外链图片转存中…(img-5apAe2PV-1715415936563)]
[外链图片转存中…(img-82YVP55d-1715415936563)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值