3.Map集合
返回值 | 方法 | 描述 |
---|---|---|
V | put(K key, V value) | 将⼀个键值对插⼊到集合中。 在 Map集合中,不允许出现重复的键。 如果添加的键重复了,会⽤新的值覆 盖掉原来的值。并返回被覆盖的原来 的值。 |
V | putIfAbsent(K key, V value) | 将⼀个键值对插⼊到集合中。 向集 合中添加元素的时候,如果这个键已 经存在了,则不进⾏添加。 返回集 合中已经存在的这个键对应的值。 |
void | putAll(Map map | 将⼀个Map集合中所有的键值对添加 到当前集合中。 |
V | remove(Object key) | 通过键,删除⼀个键值对,并返回这 个被删除的键值对中的值。 如果这 个键不存在,则返回null。 |
boolean | remove(Object key, Object value) | 通过键值对进⾏删除。 只有当键和 值⼀⼀匹配的时候, 才会进⾏删 除。 |
void | clear() | 清空集合。 |
V | replace(K key, V value) | 修改指定的键对应的值, 并返回被 覆盖的值。 |
boolean | replace(K key, V oldValue, V newValue) | 只有当key和oldValue是匹配的情况 下,才会将值修改成newValue。 |
void | replaceAll(BiFunction biFunction) | 对集合中的元素进⾏批量的替换 将集合中的每⼀个键值对,带⼊到 BiFunction的⽅法中, 使⽤接⼝⽅法 的返回值替换集合中原来的值。 |
V | get(K key) | 通过键, 获取值。 如果键不存在, 返回null。 |
V | getOrDefault(K key) | 通过键, 获取值。 如果键不存在, 返回默认的值。 |
int | size() | 获取集合中的元素数量(有多少个键 值对) |
boolean | isEmpty() | 判断集合是否为空 |
boolean | containsKey(K key) | 判断是否包含指定的值 |
boolean | containsValue(V value) | 判断是否包含指定的值 |
Set | keySet() | 获取由所有的键组成的集合(因为键是不允许重复的,因此这⾥返回的 是Set集合) |
Collection | values( | 获取由所有的值组成的集合 |
4. HashMap
4.1 HashMap 基本实现
注意:HashMap可以实现排序:因为他的底层数据结构是由数组+链表+⼆叉树共同实现的.所以可以排序.同时这样做的⽬的是提⾼数据存储的效率.
4.2 HashMap 与 Hashtable 的区别
-
HashMap是线程不安全的集合, Hashtable是线程安全的集合。
-
HashMap允许出现null键值, Hashtable是不允许的。
-
HashMap的⽗类是AbstractMap, Hashtable的⽗类是Dictionary。
-
HashMap的Map接⼝的新的实现类, 底层算法效率优于Hashtable。
5. TreeMap
5.1原理实现
与TreeSet⼀样,进⾏排列,只是TreeMap是按照键的⼤⼩实现,对于值是不管的.我们可 以将TreeSet中的值理解成TreeMap中的键.
5.2注意点
-
什么类型的数据类型可以作为key?
-
实现了Comparable接⼝的compareTo()⽅法
-
实现了Comparator接⼝的compare()⽅法
-
经常作为key的有:
String,包装类,⾃定义的实现了要求的类
-
不可以的代表:数组,ArrayList,LinkedList(如果给他们建⽴的⽐较器也可以⽐较,但是不建议使⽤)
-
元素可不可以作为key,跟元素内部的成员有没有关系
元素可不可以作为key,跟元素内部的成员有没有关系
6.其他的实现类
-
LinkedHashMap
-
与HashMap类似的,底层多维护了⼀个链表, 记录每⼀个键的存储顺序。 也就是说, 在LinkedHashMap中, 键值对的添加顺序可以得到保障。 类似于LinkedHashSet与HashSet。
-
7.线程
7.1为什么要使用线程?
在程序中完成某⼀个功能的时候,我们会将他描述成任务,这个任务需要在线程中完成.
7.2串行和并发
如果在程序中,有多个任务需要被处理,此时的处理⽅式可以有串⾏和并发:
-
串⾏(同步):所有的任务,按照⼀定的顺序,依次执⾏。如果前⾯的任务没有 执⾏结束,后⾯的任务等待。
-
并发(异步):将多个任务同时执⾏,在⼀个时间段内,同时处理多个任务。
7.3并发的原理
所谓的并发, 并不是真正意义上的多个任务同时执⾏。 ⽽是CPU快速的在不同 的任务之间进⾏切换。 在某⼀个时间点处理任务A, 下⼀个时间点去处理任务B, 每 ⼀个任务都没有⽴即处理结束。 CPU快速的在不同的任务之间进⾏切换, 只是这个 切换的速度⾮常快, ⼈类是识别不了的, 因此会给⼈⼀种“多个任务在同时执⾏”的 假象。 因此, 所谓的并发, 其实就是CPU快速的在不同的任务之间进⾏切换的⼀种假象。
7.4进程和线程
-
进程,是对⼀个程序在运⾏过程中,占⽤的各种资源的描述。
-
线程,是进程中的⼀个最⼩的执⾏单元。 其实,在操作系统中, 最⼩的任务执 ⾏单元并不是线程,⽽是句柄。只不过句柄过⼩,操作起来⾮常的麻烦,因此线程就是我们可控的最⼩的任务执⾏单元。
每⼀个进程⾄少要处理⼀件任务, 因此, 每⼀个进程中⾄少要包含⼀个线程。 如果 ⼀个进程中所有的线程都结束了, 那么这个进程也就结束了。
多个线程的同时执⾏, 是需要这些线程去争抢CPU资源, ⽽CPU资源的分配是以时 间⽚为单位的。 即某⼀个线程抢到了0.01秒的CPU时间⽚, 在这个时间内, CPU处 理这个线程的任务。 ⾄于哪⼀个线程能够抢到CPU时间⽚, 则由操作系统进⾏资源 调度。
7.5进程和线程的异同
相同点: 进程和线程都是为了处理多个任务并发⽽存在的。
不同点: 进程之间是资源不共享的, ⼀个进程中不能访问另外⼀个进程中的数据。 ⽽线程之间是资源共享的, 多个线程可以共享同⼀个数据。 也正因为线程之间是资源 共享的, 所以会出现临界资源的问题。
7.6进程和线程的关系
⼀个进程, 在开辟的时候, 会⾃动的创建⼀个线程, 来处理这个进程中的任务。 这 个线程被称为是主线程。 在程序运⾏的过程中, 还可以开辟其他线程, 这些被开辟 出来的其他线程, 都是⼦线程。
也就是说, ⼀个进程中, 是可以包含多个线程。 ⼀个进程中的某⼀个线程崩溃了, 只要还有其他线程存在, 就不会影响整个进程的执⾏。 但是如果⼀个进程中, 所有 的线程都执⾏结束了, 那么这个进程也就终⽌了。
7.7总结
-
程序:⼀个可执⾏的⽂件
-
进程:⼀个正在运⾏的程序.也可以理解成在内存中开辟了⼀块空间
-
线程:负责程序的运⾏,可以看做⼀条执⾏的通道或执⾏单元,所以我们通常将进程 的⼯作理解成线程的⼯作 进程中可不可以没有线程? 必须有线程,⾄少有⼀个.
-
当有⼀个线程的时候我们称为单线程(唯⼀的线程就是主线程). 当有⼀个以上的线程同时存在的时候我们称为多线程
-
多线程的作⽤:为了实现同⼀时间⼲多件事情(并发执⾏)
8.线程的生命周期
8.1线程的状态
线程的⽣命周期, 指的是⼀个线程对象, 从最开始的创建, 到最后的销毁, 中间所 经历的过程。 在这个过程中, 线程对象处于不同的状态。
-
New: 新⽣态,⼀个线程对象刚被实例化完成的时候,就处于这个状态。
-
Runnable: 就绪态,处于这个状态的线程,可以参与CPU时间⽚的争抢。
-
Run: 运⾏态, 某⼀个线程抢到了CPU时间⽚,可以执⾏这个线程中的逻辑 Block: 阻塞态,线程由于种种原因,暂时挂起, 处于阻塞(暂停)状态。这个 状态的线程,不参与CPU时间⽚的争抢。
-
Dead: 死亡态, 线程即将被销毁。
8.2线程的生命周期![](https://i-blog.csdnimg.cn/blog_migrate/5da909e613ad0d5b3ce226840a909ff8.png)
9.理解多线程
9.1.对线程并发执行的说明
简单理解(cpu单核):从宏观上看,线程有并发执⾏,从微观上看,并没有,在线程完成任务时,实际⼯作的是cpu,我们将cpu⼯作描述为时间⽚(单次获取cpu的时间,⼀般在⼏⼗毫秒).cpu只有⼀个,本质上同⼀时间只能做⼀件事,因为cpu单次时间⽚很短,短到⾁眼⽆法区分,所以当cpu在多个线程之间快速切换时,宏观上给我们的感觉是多件事同时在执⾏.
注意:
1.cpu是随机的,线程之间本质上默认是抢cpu的状态,谁抢到了谁就获得了时间⽚,就⼯ 作,所以多个线程的⼯作也是默认随机的.
2.在使⽤多线程时,并不是线程数越多越好,本质上⼤家共同使⽤⼀个cpu,完成任务的时间并没有减少.要根据实际情况创建线程,多线程是为了实现同⼀时间完成多件事情 的⽬的.⽐如我们⽤⼿机打开⼀个app时,需要滑动界⾯浏览,同时界⾯的图⽚需要下载, 对于这两个功能最好同时进⾏,这时可以使⽤多线程.
9.2多线程的实例演示
-
代码演示的是主线程和垃圾回收线程在同时⼯作时的状态
-
什么叫任务区?
我们将线程⼯作的地⽅称为任务区.
每⼀个线程都有⼀个任务区,任务区通过对应的⽅法产⽣作⽤
-
JVM默认是多线程吗?
⾄少要有两个线程:
主线程:任务区:main函数
垃圾回收线程:任务区:finalize函数
1.gc()⽅法:之前讲过,是垃圾回收器
原理:当执⾏gc时,会触发垃圾回收机制,开启垃圾回收线程,执⾏finalize⽅法
2.finalize()⽅法:垃圾回收线程的任务区
正常情况下,这个函数是由系统调⽤的,重写只是为了更好的观察多线程的发⽣,当Test对象被释放的时候,会⾃动的调⽤finalize⽅法
3.线程和任务的关系
任务区结束,线程随着任务的结束⽽结束,线程随着任务的开始⽽开始.当线程还在⼯作的时候,进程不能结束.
对于主线程来说:当main函数结束时,主任务区结束,主线程结束
对于垃圾回收线程:当finalize函数结束,垃圾回收任务结束,垃圾回收线程结束
cpu的特性是多个线程之间是抢cpu的关系,cpu有随机性
10.创建线程
10.1原因分析
默认情况下,主线程和垃圾回收线程都是由系统创建的,但是我们需要完成⾃⼰的功能, 所以需要创建⾃⼰的线程
java将线程⾯向对象了,形成的类就是Thread,在Thread类内部执⾏任务的⽅法叫run()
10.2线程对象的实例化
在Java中,使⽤Thread类来描述⼀个线程。实例化⼀个线程,其实就是⼀个 Thread对象。
10.3直接使用Thread类创建线程对象
-
线程对象刚刚被实例化的时候, 线程处于新⽣态,还没有线程的功能。 如果需要让这个线程执⾏他的任务, 需要调⽤ start() ⽅法, 使线程进⼊到就绪态, 争抢 CPU时间⽚。
-
为什么通过调⽤start()⽅法开启线程,⽽不是通过⼿动调⽤run()?
答:因为线程获取cpu是随机的,run是线程的任务区,代表功能.如果⼿动执⾏run,此时线程可能没有拿到cpu,⽆法⼯作,操作失败.通过start,让线程处于就绪状态,随时拥有抢cpu的能⼒,当抢到cpu后,再⾃动执⾏run,实现并发的任务.
-
为什么要使⽤Thread类的⼦类对象?
答:我们实现的实际功能,Thread类作为系统类不能提前知晓,所以⽆法将功能代码放⼊ Thread的run⽅法⾥.如果想实现⾃⼰的功能,可以写Thread类的⼦类,重写run⽅法,这也是为什么Thread的run⽅法是⼀个空⽅法。
10.4继承Thread类
-
继承⾃Thread类,做⼀个Thread的⼦类。 在⼦类中,重写⽗类中的run⽅法,在这个重写的⽅法中,指定这个线程需要处理的任务。
-
Thread.currentThread() : 可以⽤在任意的位置,获取当前的线程。
如果是Thread的⼦类, 可以在⼦类中, 使⽤this获取到当前的线程。
-
当我们⼿动调⽤run的时候,他失去了任务区的功能,变成了⼀个普通的⽅法. 当run作为⼀个普通⽅法时,内部对应的线程跟调⽤他的位置保持⼀致.
-
结果分析:
主线程和两个⼦线程之间是随机打印的,他们是抢cpu的关系.
-
通过创建Thread⼦类的⽅式实现功能,线程与任务绑定在了⼀起,操作不方便,我们可以将任务从线程中分离出来,哪个线程需要⼯作,就将任务交给谁,操作⽅便,灵 活使⽤Runnable接⼝
10.5使用Runable接口
-
在Thread类的构造⽅法中, 有⼀个重载的构造⽅法, 参数是 Runnable 接⼝。 因此, 可以通过Runnable接⼝的实现类对象进⾏Thread对象的实例化。
-
这⾥Thread内部默认有⼀个run,⼜通过runnable传⼊⼀个run,为什么优先调⽤ 的是传⼊的run?
如果该线程是使⽤独⽴的 Runnable 运⾏对象构造的,则调⽤该 Runnable 对象 的 run ⽅法;否则,该⽅法不执⾏任何操作并返回。
10.6优缺点对比
-
继承的⽅式:优点在于可读性⽐较强, 缺点在于不够灵活。 如果要定制⼀个线 程,就必须要继承⾃Thread类,可能会影响原有的继承体系。
-
接⼝的⽅式: 优点在于灵活, 并且不会影响⼀个类的继承体系。 缺点在于可读性较差。
11.线程同步
11.1临界资源问题
11.1.1临界资源
在⼀个进程中, 多个线程之间是可以资源共享的。如果在⼀个进程中的⼀个资源同时被多个线程访问,这个资源就是⼀个临界资源。
如果多个线程同时访问临界资源,会对这个资源的值造成影响。
11.1.2临界资源问题
多个线程同时访问⼀个资源的情况下,⼀个线程在操作这个资源的时候,将值取出进⾏运算, 在还没来得及进⾏修改这块空间的值之前,值⼜被其他的线程取⾛了。此时就会出现临界资源的问题,造成这个资源的值出现不是我们预期的值。
11.1.3解决方案
临界资源问题出现的原因就是多个线程在同时访问⼀个资源,因此解决⽅案也很简 单,就是不让多个线程同时访问即可。
在⼀个线程操作⼀个资源的时候,对这个资源进⾏“上锁”,被锁住的资源,其他的线程⽆法访问。
11.2线程安全问题
-
线程安全问题: 4个线程共⽤了⼀个数据,出现了-1,-2,-3等错误的数据
-
原因分析
出现了临界资源问题 1.共⽤了⼀个数据 2.共享语句有多条,⼀个线程使⽤cpu,没有使⽤完,cpu被抢⾛,当再次抢到cpu的时候, 直接执⾏后⾯的语句,造成了错误的发⽣.
-
解决:在代码中使⽤同步代码块或同步⽅法(同步锁)
-
解释:在某⼀段任务中,同⼀时间只允许⼀个线程执⾏任务,其他的线程即使抢到了 cpu,也⽆法进⼊当前的任务区间,只有当当前的线程将任务执⾏完后,其他的线程才能有资格进⼊
12.线程锁
-
线程锁,就是⽤来“锁住”⼀个临界资源,其他的线程⽆法访问。在程序中,可以分为对象锁和类锁
-
对作为锁的对象的要求: 1.必须是对象
2.必须保证被多个线程共享
-
对象锁: 任何的普通对象或者this,都可以被当做是⼀把锁来使⽤。但是需要 注意,必须要保证不同的线程看到的锁, 需要是同⼀把锁才能⽣效。如果不同 的线程看到的锁对象是不⼀样的,此时这把锁将没有任何意义。注意: 不能直接使⽤匿名对象作为锁,因为这样每次都是在重新new,要保证锁是被⼤家共享.
-
类锁: 可以将⼀个类做成锁, 使⽤类.class (类的字节码⽂件对象)来作为锁。因为类的字节码⽂件的使⽤范围太⼤,所以⼀般我们不使⽤他作为锁,只有在静态⽅法中。
13.synchronized
如果在⼀个⽅法中, 所有的逻辑, 都需要放到同⼀个同步代码段中执⾏。 这样的⽅ 法, 可以直接做成同步⽅法
13.1同步方法
-
⾮静态同步⽅法,使⽤的对象锁(this)
是某个对象实例内,synchronized aMethod(){}可以防⽌多个线程同时访问这个 对象的synchronized⽅法(如果⼀个对象有多个synchronized⽅法,只要⼀个 线程访问了其中的⼀个synchronized⽅法,其它线程不能同时访问这个对象中任 何⼀个synchronized⽅法)。这时,不同的对象实例的synchronized⽅法是不 相⼲扰的。也就是说,其它线程照样可以同时访问相同类的另⼀个对象实例中的 synchronized⽅法
-
静态同步⽅法,使⽤的类锁(当前类的.class⽂件)
是某个类的范围,synchronized static aStaticMethod{}防⽌多个线程同时访问 这个类中的synchronized static ⽅法。它可以对类的所有对象实例起作⽤。 静态同步函数在进内存的时候不会创建对象,但是存在其所属类的字节码⽂件对 象,属于class类型的对象,所以静态同步函数的锁是其所属类的字节码⽂件对象
13.2同步代码块
synchronized关键字⽤于⽅法中的某个区块中,表示只对这个区块的资源实⾏互斥访问。
同步代码段,是来解决临界资源问题最常⻅的⽅式。将⼀段代码放⼊到同步代码段中,将这段代码上锁。
第⼀个线程抢到了锁标记后,可以对这个紧接资源上锁,操作这个临界资源。此时 其他的线程再执⾏到synchronized的时候,会进⼊到锁池,直到持有锁的线程使⽤ 结束后,对这个资源进⾏解锁。此时,处于锁池中的线程都可以抢这个锁标记,哪⼀个线程抢到了,就进⼊到就绪态,没有抢到锁的线程,依然处于锁池中。
-
同步代码块⼉的构成:
synchronized(锁(对象)){ 同步的代码 }
-
同步代码块⼉的特点:1.可以保证线程的安全 2.由于每次都要进⾏判断处理,所以降低了执⾏效率
13.3 比较同步代码块和同步函数
-
同步代码块使⽤更加的灵活,只给需要同步的部分代码同步即可,⽽同步函数是给这个函数内的所有代码同步.
-
由于处于同步的代码越少越好,所以最好使⽤同步代码块
-
什么时候使⽤同步代码块或者同步⽅法
1.多个线程共享⼀个数据 2.⾄少有两个线程
13.4 synchronized在继承中的使用
synchronized关键字是不能继承的,也就是说,基类的⽅法synchronized f(){}在继 承类中并不⾃动是synchronized f(){},⽽是变成了f(){}。继承类需要你显式的指定它的某个⽅法为synchronized⽅法