Java基础问题整理(二)

Redis分布式锁:

  利用Redis的setnx命令,此命令同样是原子性操作,只有在key不存在的情况下,才能set成功。setnx命令:(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。设置成功,返回 1 。 设置失败,返回 0 。

  1、加锁:最简单的方法是使用setnx命令。key是锁的唯一标识,按业务来决定命名。value可以设置为1,setnx(miaosha_goodsID,1);

  • 当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁
  • 当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败。

  2、解锁:释放锁最简单的方式是执行del指令,del(miaosha_goodsID);
  3、锁超时:如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式的释放锁,这块资源将会永远被锁住(死锁),别的线程将无法访问这块资源。所以,setnx的key必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。setnx不支持超时参数,所以需要额外的指令,expire(miaosha_goodsID,30);

Redis为什么会比MySQL快?

1、Redis是基于内存存储的,MySQL是基于磁盘存储的。

2、Redis存储的是k-v格式的数据。时间复杂度是O(1),常数阶,而MySQL引擎的底层实现是B+Tree,时间复杂度是O(logn),对数阶。Redis会比MySQL快一点点。

3、MySQL数据存储是存储在表中,查找数据时要先对表进行全局扫描或者根据索引查找,这涉及到磁盘的查找,磁盘查找如果是按条点查找可能会快点,但是顺序查找就比较慢;而Redis不用这么麻烦,本身就是存储在内存中,会根据数据在内存的位置直接取出。

4、Redis是单线程的多路复用IO,单线程避免了线程切换的开销,而多路复用IO避免了IO等待的开销,在多核处理器下提高处理器的使用效率可以对数据进行分区,然后每个处理器处理不同的数据。

  MySQL是关系型数据库,是持久化存储的,查询检索的话,会涉及到磁盘IO操作,为了提高性能,可以使用缓存技术,而memcached就是内存数据库,数据存储在内存中(当然也可以进行持久化存储),可以用作缓存数据库。用户首先去memcached查询数据,如果未查询到(即缓存未命中),才去MySQL中查询数据,查询到的数据会更新到缓存数据库中,提供给下次可能进行的查询。提高了数据查询方面的性能。
  Redis是内存数据库,数据保存在内存中,访问速度快。MySQL是关系型数据库,功能强大,存储在磁盘中,数据访问速度慢。像memcached,MongoDB,Redis等,都属于No sql系列。

RabbitMQ

  MQ(Message Queue)消息队列,是基础数据结构中“先进先出”的一种数据结构。一般用来解决应用解耦异步消息流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。

  RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好,

  • 可靠性(Reliablity):使用了一些机制来保证可靠性,比如持久化、传输确认、发布确认。
  • 灵活的路由(Flexible Routing):在消息进入队列之前,通过Exchange来路由消息。对于典型的路由功能,Rabbit已经提供了一些内置的Exchange来实现。针对更复杂的路由功能,可以将多个Exchange绑定在一起,也通过插件机制实现自己的Exchange。
  • 消息集群(Clustering):多个RabbitMQ服务器可以组成一个集群,形成一个逻辑Broker。
  • 高可用(Highly Avaliable Queues):队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
  • 多种协议(Multi-protocol):支持多种消息队列协议,如STOMP、MQTT等。
  • 多种语言客户端(Many Clients):几乎支持所有常用语言,比如Java、.NET、Ruby等。
  • 管理界面(Management UI):提供了易用的用户界面,使得用户可以监控和管理消息Broker的许多方面。
  • 跟踪机制(Tracing):如果消息异常,RabbitMQ提供了消息的跟踪机制,使用者可以找出发生了什么。
  • 插件机制(Plugin System):提供了许多插件,来从多方面进行扩展,也可以编辑自己的插件。

Synchronized

  synchronized加锁,最常用的线程同步手段之一,悲观锁。synchronized,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。
一、使用场景:锁方法、锁代码块和锁对象
1、修饰实例方法,对当前实例对象this加锁
2、修饰静态方法,对当前类的Class对象加锁
3、修饰代码块,指定一个加锁的对象,给对象加锁
二、作用:
1、保证可见性:一个线程对Synchronized修饰的变量进行修改,其他线程需要立即获取到被修改后的值。
2、原子性:在一次或多次操作中,要么所有的操作都执行并且不会受其他因素所干扰而中断,要么所有的操作都不执行。
3、有序性:程序中代码的执行顺序,禁止编译器操作Synchronized修饰的变量指令重排序。
4、可重入性:一个线程可以多次执行synchronized,重复获取同一把锁。synchronized锁对象的时候有个计数器,他会记录下线程获取锁的次数,在执行完对应的代码块之后,计数器就会-1,直到计数器清零,就释放锁了。可以避免一些死锁的情况,也可以让我们更好封装我们的代码。
5、不可中断特性:一个线程获取锁之后,另外一个线程处于阻塞或者等待状态,前一个不释放,后一个会一直阻塞或者等待,不可以被中断。
三、原理
1、synchronized 应用在方法上时,在字节码中是通过方法的 ACC_SYNCHRONIZED 标志来实现的。在方法的flags中加入标识符 ACC_SYNCHRONIZED ,其他线程进这个方法就看看是否有这个标志位,有就代表有别的线程拥有了他,你就别碰了。
2、synchronized 应用在同步块上时,在字节码中是通过 monitorenter 和 monitorexit 实现的。每个对象都会与一个monitor相关联,当某个monitor被拥有之后就会被锁住,当线程执行到monitorenter指令时,就会去尝试获得对应的monitor。每个monitor维护着一个记录着拥有次数的计数器,未被拥有的monitor的该计数器为0,当一个线程获得monitor(执行monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该monitor的时候,计数器再次自增;当不同线程想要获得该monitor的时候,就会被阻塞。当同一个线程释放 monitor(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候,monitor将被释放,其他线程便可以获得monitor。
3、同步方法和同步代码块底层都是通过monitor来实现同步的。
两者的区别:同步方式是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现,同步代码块是通过monitorenter和monitorexit来实现。所以归根究底,还是monitor对象的争夺。在hostpot虚拟机中monitor监视器是由ObjectMonitor实现的,其源码使用c++实现的。monitor并不是随着对象创建而创建的,我们是通过synchronized修饰符告诉JVM需要为我们的某个对象创建关联的monitor对象。

四、平时写代码如何对synchronized进行优化?
1、降低synchronized的使用范围:减少同步代码块中的代码,降低执行时间,减少锁的竞争。
2、降低synchronized锁的粒度:将一个锁拆分为多个锁提高并发度。
3、读写分离:读取时不加锁,写入和删除时加锁。

锁升级过程

  其实就是在源码里面,调用了不同的实现去获取锁,失败就调用更高级的实现,最后升级完成。
  先使用偏向锁优先同一线程再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁。

无锁——偏向锁——轻量级锁——重量级锁
锁只能升级,不能降级。

Java对象的构成

  每个对象都与一个monitor相关联,而monitor可以被线程拥有或释放。在 JVM 中,对象在内存中分为三块区域:
1、对象头
Mark Word(标记字段):默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
Klass Point(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
2、实例数据:这部分主要是存放类的数据信息,父类的信息。
3、对齐填充:由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。一个空对象占8个字节,是因为对齐填充的关系哈,不到8个字节对其填充会帮我们自动补齐。

  在对象头中保存了锁标志位和指向 monitor 对象的起始地址,当 Monitor 被某个线程持有后,就会处于锁定状态,Owner 部分会指向持有 Monitor 对象的线程。另外 Monitor 中还有两个队列分别是EntryList和WaitList,主要是用来存放进入和等待获取锁的线程。如果线程进入,则得到当前对象锁,那么别的线程在该类所有对象上的任何操作都不能进行。

synchronized和Lock的区别:

  • synchronized是关键字,是JVM层面的底层啥都帮我们做了,而Lock是类,是JDK层面的有丰富的API。
  • synchronized会自动释放锁,而Lock必须手动释放锁,如果不释放就会发生死锁现象。
  • synchronized无法获取锁的状态,Lock可以判断是否获取到了锁。
  • synchronized能锁住方法和代码块,而Lock只能锁住代码块。
  • synchronized是悲观锁,非公平锁,ReentrantLock可以控制是否是公平锁。
  • synchronized锁中当一个线程阻塞,另一个线程会一直等待下去,Lock锁不一定会等待下去,tryLock()这个方法可以尝试获取锁。
  • synchronized 可重入锁,不可中断,非公平锁,Lock锁可重入锁,可以中断也可以不中断,非公平(可以设置)。
  • synchronized 适合少量的代码同步问题,Lock适合大量的同步代码。
  • 两者一个是JDK层面的一个是JVM层面的,我觉得最大的区别其实在,我们是否需要丰富的api。

volatile

一、volatile修饰符适用场景
 synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。
 使用volatile必须具备以下2个条件:

1)对变量的写操作不依赖于当前值

2)该变量没有包含在具有其他变量的不变式中

1、volatile可以适合做多线程中的纯赋值操作,如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。
2、volatile可以作为刷新之前操作的触发器,状态标记量,我们可以将某个变量用volatile修饰,其他线程一旦发现该变量的值被修改后,触发获取到的该变量之前的操作都将是最新的且可见。flag被volatile修饰,充当了触发器,一旦值为被修改为true,那么此处对变量之前的操作对于后面的线程都是可见的。
二、作用
1、保证可见性:
第一:使用volatile关键字会强制将修改的值立即写入主存;
  第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
  第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

2、不保证原子性:volatile 只能保证对单次读/写的原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。 
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。

3、禁止指令重排:java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障。
 1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

三、volatile的原理和实现机制
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。
  lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  2)它会强制将对缓存的修改操作立即写入主存;
  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
四、特性
  volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
  volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
  volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见,volatile属性不会被线程缓存,始终从主 存中读取。
  volatile提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作。
  volatile可以使得long和double的赋值是原子的。
  volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。

volatile和synchronized的区别

  • volatile只能修饰实例变量和类变量;而synchronized可以修饰方法,以及代码块。
  • volatile保证数据可见性,但是不保证原子性,不保证线程安全;而synchronized是一种互斥锁(排他)机制,可以保证线程安全。
  • volatile用于禁止指令重排序,可以解决单例双重检查对象初始化代码执行乱序问题。
  • volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。

MySQL数据库中的各种锁

1、从数据操作的类型:读锁,写锁;

  • 读锁:针对同一份数据,多个操作可以同时进行而不会相互影响。
  • 写锁:当前操作没有完成前,它会阻断其他写锁和读锁。
  • 读锁会阻塞写,但是不会阻塞读;
    写锁会把读和写都堵塞;

2、锁按使用方式划分—悲观锁和乐观锁

  • 悲观锁:对数据的冲突采取一种悲观的态度,无论是在数据读取,还是数据修改的过程都需要加锁。适合写入操作比较频繁的场景。表锁、行锁、读锁、写锁。
  • 乐观锁:在数据读取时不会加锁,在数据修改时才会加锁。认为数据总是不会被更改,适合读取操作比较频繁的场景。使用版本号、使用时间戳。

3、锁按级别划分—共享锁(读锁)和排他锁(写锁)

  • 共享锁(Share Lock),S锁,也叫读锁,用于所有的只读数据操作,共享锁是非独占的,允许多个并发事务读取其锁定的资源。共享锁会阻塞写,不会阻塞读。
  • 互斥锁、排他锁(Exclusive Lock),X锁,也叫写锁,用于对数据进行写操作。如果一个事务对对象加了排他锁,其他事务就不能再给它加任何锁了。排它锁写和读都被阻塞。

4、锁按粒度划分—表锁和行锁

  • 行锁是通过给索引上的索引项加锁来实现的,自动加锁,也可以显式加锁。开销大,加锁慢,会出现死锁,锁的粒度小,发生锁冲突的概率低,处理并发的能力强。
  • 表锁,对表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;对表的写操作,则会堵塞其他用户对同一表的读和写操作。自动加锁,也可以显式加锁。开销小,加锁快,无死锁,锁粒度大,发生锁冲突的概率高,并发处理能力低。

5、死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去,此时系统处于死锁状态。等待锁超时、设置死锁检测。
6、公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

  • 优点:所有的线程都能得到资源,不会饿死在队列中。
  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

7、非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

MySQL主从复制原理

1、一主一从:(1)主服务器上面的任何修改都会通过自己的IO线程保存在二进制日志文件中(binary log)。
(2)从机上面也启动一个IO线程,通过配置好的用户名和密码,连接到主服务器上面请求读取二进制日志,然后把读取到的二进制日志写到本地的一个中继日志里面(Relay log)。
(3)从服务器上面同时开启一个SQL thread定时检查Realy log(二进制),如果发现有更新立即把更新的内容在本机的数据库在上面执行一遍。
2、一主多从:如果一主多从的话,这时主机要负责写又要负责为几个从库机提供二进制日志,此时可以稍作调整,将二进制日志只给某一从机,这一从机再将自己的二进制日志再发给其他从机。或者是干脆这个从机不记录只负责将二进制日志转发给其他从机,这样架构起来性能可能要好得多,而且数据之间的延时应该也稍微要好一些。

索引

索引(Index)可以简单理解为排好序的快速查找数据结构,索引文件的形式存储的磁盘上;提高数据检索的效率,降低数据库的IO成本,降低了CPU的消耗。实际上索引也是一张表,该表保存了主键与索引字段,并指向实体表的记录,所以索引表也是要占用空间的。

一、索引有哪些数据结构?
Hash、B+树
二、为什么哈希表、完全平衡二叉树、B树、B+树都可以优化查询,为何Mysql独独喜欢B+树?
1、哈希表的特点就是可以快速的精确查询,但是不支持范围查询,可能出现哈希冲突。Hash表适合等值查询的场景,就只有KV(Key,Value)的情况。
2、二叉树是有序的,所以是支持范围查询的,如果数据多了,树会很高,查询的成本就会随着树高的增加而增加。
3、B树中的一个节点可以存储多个元素,相对于完全平衡二叉树整体的树高降低了,磁盘IO效率提高了。
4、B+树是B树的升级版,把非叶子节点冗余一下,并且叶子节点之间用指针相连,会有指针指向下一个节点的叶子节点,提高了范围查找的效率。

Mysql选用B+树这种数据结构作为索引,可以提高查询索引时的磁盘IO效率,并且可以提高范围查询的效率,并且B+树里的元素也是有序的。

三、B+树中一个节点到底多大合适?
B+树中一个节点为一页或页的倍数最为合适。Mysql的基本存储结构是页,InnoDB存储引擎中默认每个页的大小为16KB。读取这个节点的时候会读出一页或页的倍数,这样不会造成资源的浪费。

四、索引的分类

  • 单列索引:即一个索引只包含单个列,一个表可以有多个单列索引;
  • 唯一索引:索引列的值必须唯一(控制该列不能有相同值),但允许有空值;
  • 主键索引:索引列的值必须唯一(控制该列不能有相同值),不可以有空值,表中只有一个;
  • 复合索引:一个索引包含多个列
  • 覆盖索引:一个索引中包含所有需要查询字段的值,优化查询效率。
    如何实现覆盖索引——将被查询的字段,建立到联合索引里去,
    使用覆盖索引的好处——无需回表,少了一个索引树的扫描,
  • 聚簇索引:主键索引,并不是一种单独的索引类型,而是一种数据存储方式,数据行和相邻的键值紧凑的存储在一起,一个表只能有一个聚簇索引。聚簇索引,叶子结点存储行记录。
    聚簇索引的优点:数据访问更快,聚簇索引将索引和数据保存在同一个索引树中,因此从聚簇索引中获取数据通常比在非聚簇索引中查找更快。
  • 非聚簇索引:又称为普通索引,辅助索引,它的索引树的叶子节点存储的数据是主键值。因此对于非聚簇索引查询来说,它是无法通过一次扫描索引树然后定位行记录的,需要扫描两遍索引树。
  • 回表查询,先通过普通索引定位到主键值,再通过聚簇索引定位到行记录。

五、注意的问题
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以前:哈希表=数组+链表
②jdk1.8以后:哈希表=数组+链表+红黑树
数组结构:对元素进行了分组(相同哈希值的元素在一组),
链表/红黑树结构:将相同哈希值的元素连接到一起。
哈希冲突:两个元素不同,但是哈希值相同,就会发生哈希冲突

  • HashSet存储自定义对象保证元素唯一性,
  • LinkedHashSet底层数据结构是由链表和哈希表实现的,链表保证有序,哈希表保证元素唯一。
  • TreeSet元素唯一,并且可以对元素进行排序,排序有两种方式:自然排序和使用比较器排序。自然排序的原理:利用二叉树的数据结构,现存入一个树根,分两个叉,存储元素时,和树根比较,小的放在左边,大的放在右边,如果相等就不存储。自定义的比较器要实现Comparator接口,重写Compare方法。在创建TreeSet对象的时候,就传递一个比较器对象。

3、List

  元素有序,并且每个元素都存在一个索引,元素可以重复。

  • ArrayList:数组线性表,底层数据结构是数组,查询快,增删慢,线程不安全,效率高,
  • LinkedList:底层数据结构是双向链表,查询慢,增删快,线程不安全,效率高,
  • Vector:底层数据结构是数组,查询慢,增删快,线程安全,效率低,

ArrayList和LinkedList

  • 数据结构实现
      ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
  • 随机访问效率
      ArrayList 比 LinkedList 在随机访问的时候效率要高,因为LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
  • 增加和删除效率
      在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList增删操作要影响数组内的其他数据的下标。

  综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值