HashMap本身是线程不安全的
HashTable是线程安全的,关键方法都提供了synchronized,但是现在HashTable不能满足现在的需求和效率了
所以优秀的程序员大佬又搞出一个hash表 ConcurrentHashMap
ConcurrentHashMap 线程安全的hash表
我们先看看HashTable是怎么工作的以及缺点:
hash表先得有一个数组
hash表之所以能够以O(1)这样的复杂度进行增删改查,主要还是得益于数组他的随机访问下标的能力,hash表关键就是,把这里面的key通过hash函数转换成数组下标,这样我们就能比较高效地获取这个元素的位置
这时候如果有两个key,key1和key2,同时通过一个hash函数获得同一个数组下标该咋办呢,这个就是我们所说的哈希冲突,虽然可以进行二次探测的方式,但是真实的hash表根本不会出现这样的方式(这个方式只是出现在教科书中),实际上我们是通过哈希桶或者链表的方式解决这个问题
就是说如果出现hash冲突我们就挂链表,每个数组的元素都是一根链表,链表上面就可以包含多个元素了,另外我们就可以通过扩容的方式或者负载因子的方式,限制这里面链表的长度,要么对链表进行扩容,要么转换成红黑树,所以我们控制好链表不要太长,那我们增删改查的效率还是O(1)
如果我们针对两个不同的链表上的元素进行修改,是否会出现线程安全问题??明显是不会的
但是如果正好当前这个插入操作触发了扩容,是有可能有影响的,扩容操作是极其重量的操作,把整个哈希表给重新搬运一遍,相比之下,锁的开销就微乎其微了,就是说如果这个插入操作已经触发扩容了,这个时候加锁和不加锁整体的效率差不多
既然增删查改没有线程安全问题,是不是就不要加锁了呢?不能完全不加锁,因为有可能两个线程往同一个链表的同一个位置上插入元素,那就有可能出现线程安全问题
所以我们具体的做法就是,给每一个链表都安排一把锁,所以我们操作同一个链表才会有锁冲突
一个hash表上面的链表个数那么多,两个线程正好同时操作同一个链表的概率本身就是较低的,对每个链表加锁就可以使整体锁的开销大大降低
由于synchronized 随便拿个对象都可以用来加锁,所以就可以简单地使用每个链表的头结点作为锁对象即可
ConcurrentHashMap 改进:
1.[核心]减小了锁的粒度,每个链表都有一把锁,大部分情况都不会涉及锁冲突
2.广泛使用了CAS操作,例如size++这样的操作也不会产生锁冲突
3.写操作进行了加锁(链表级),读操作不加锁了
4.针对扩容操作进行了优化,渐进式扩容
HashTable一旦触发扩容,就会立即的一口气完成所有元素的搬运,这个过程相当耗时
如果遇到问题,比如大部分请求都很顺畅,突然某个请求就卡了很久
ConcurrentHashMap采取化整为零,当我们进行扩容的时候,会创建出另一个更大的数组,然后把旧的数组上的数据逐渐往新的数组上搬运,所以会出现一段时间,旧数组和新数组会同时存在
1)新增元素,就往新数组上插入
2)删除数组,就把数组的元素给删掉即可
3)查找元素,新数组旧数组都要查找
4)修改元素,统一把这个元素搞到新数组上
与此同时,这四个操作都会触发一定程度的搬运,每次都搬运一点,就可以保证整体的时间不是很长,积少成多之后,就能逐渐完成搬运,也就可以销毁之前的旧数组了
这个是一个非常经典的面试题,要重点掌握
上面我们说的是HashTable和CurrentHashMap之间的区别
HashMap和CurrentHashMap的区别就是线程安全和线程不安全的问题
介绍一下ConcurrentHashMap的锁分段技术,这个在Java8之前,ConcurrentHashMap是使用分段锁,从Java8开始,就是每个链表子自己一把锁了
锁分段技术就是几个链表一把锁,这个能提高效率,但是不如每个链表一把锁,而且这个实现起来也更复杂
文件=>在硬盘上存储数据的方式
操作系统帮我们把硬盘的细节都封装起来了
程序员只需要了解文件的相关接口即可
硬盘是用来存储数据的
和内存相比,硬盘的存储空间更大,访问速度更慢,成本更低,持久化存储
操作系统通过"文件系统"这样的模块来管理硬盘,就比如我们电脑中的C盘和D盘,事实上我的电脑只有一个硬盘,操作系统可以通过文件系统把这个硬盘抽象成多个硬盘
NTFS是Windows上的文件系统,背后有一定的格式来组织硬盘的数据
EXT4是Linux上常见的文件系统
不同的文件系统,管理文件的方式都是类似的
通过 目录-文件 构成了"N叉数"树形结构,比如这样
路径:举个例子就是,D盘=>tmp=>cat.jpg,通过这个路径我们就能找到并且确定电脑上唯一的一个文件,Windows上使用/或者\来分割不同的目录,比如D:/tmp/cat.jpg 或者 D:\tmp\cat.jpg,这个路径以盘符开头,也叫作"绝对路径",绝对路径相当于是从"此电脑"这里出发,找文件的过程
以.或者..开头的路径,叫做相对路径,相对路径需要有一个"基准目录"或者"相对目录",表示从这个基准目录出发,怎么走能找到这个文件
1.如果以D:为基准目录
./tmp/cat.jpg // .表示当前所在目录
2.如果以D:/tmp为基准
./cat.jpg
3.如果以D:/tmp/111为基准
../cat.jpg // .. 表示上一层目录
4.如果以D:/tmp/111/aaa为基准
../../cat.jpg
同样是一个cat.jpg的文件,站在不同的基准目录上,查找的路径是不相同的
文件系统上存储的文件,具体来说又分成两大类
1.文本文件
存储的是字符,utf8就是一个大类,这个表上的数据的组合就可以称为是字符
2.二进制文件
二进制的数据
一个最简单的方式判断文件是二进制还是文本,拿记事本打开,如果能看懂就是文本文件,看不懂就是二进制文件
后续针对文件的操作,文本和二进制的操作方式是不同的
文件操作系统
创建文件,删除一个文件,创建目录......
C语言标准库,不支持文件系统操作,使用C删除一个文件是非常费劲的
Java.io.File IO 是input和output
这里的输入输出是站在CPU的角度看待的
站在CPU的角度,从硬盘到内存时离自己近了,所以是输入input,从内存到硬盘是离自己远了,所以是输出output
通过File对象来描述一个具体的文件,File对象可以对应到一个真实存在调度文件,也可以对应到一个不存在的文件
以下是file类支持的相关方法
站在操作系统的角度看,目录也是文件,操作系统中的文件是一个更广义的概念,具体来说里面有很多种不同的类型
1.普通文件(通常见到的文件)
2.目录文件(通常见到的文件夹)
接下来我们看看file相关的代码
import java.io.File;
import java.io.IOException;
//file的使用
public class Demo1 {
public static void main(String[] args) throws IOException {
File file = new File("d:/test.txt");//windows可以使用正斜杠也可以使用反斜杠
System.out.println(file.getParent());
System.out.println(file.getName());
System.out.println(file.getPath());
System.out.println(file.getAbsolutePath());
System.out.println(file.getCanonicalPath());
}
}
运行结果如图
当我们将代码修改为./test.txt时再运行代码
import java.io.File;
public class Demo2 {
public static void main(String[] args) {
File file = new File("./test.txt");
System.out.println(file.exists());//文件是不是存在
System.out.println(file.isFile());//是不是文件
System.out.println(file.isDirectory());//是不是目录
}
}
加上创建文件的代码之后
public class Demo2 {
public static void main(String[] args) throws IOException {
File file = new File("./test.txt");
//创建文件
file.createNewFile();
System.out.println(file.exists());//文件是不是存在
System.out.println(file.isFile());//是不是文件
System.out.println(file.isDirectory());//是不是目录
}
}
创建新文件是可能抛出异常的
比如当前写入的路径是非法路径
比如创建的这个文件,对于所在的目录没有权限操作
下面是删除文件的代码
public class Demo3 {
public static void main(String[] args) {
File file = new File("./text.txt");
file.delete();
}
}
这个是另一种删除,程序结束才删除
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
File file = new File("./text.txt");
// file.delete();
file.deleteOnExit();//这个不是立即删除,而是等我们程序退出才删除
Thread.sleep(5000);//五秒以后程序结束,就会删除
}
}
有的时候,可能会用到这样的功能,临时文件,程序运行的时候,搞一个临时文件,程序结束了,临时文件自动删掉
下面我们看看如何创建目录
public class Demo4 {
public static void main(String[] args) {
File file = new File("./testDir/111/222/333");
//mk => make ,dir => directory
//file.mkdir();//一次只能创建一层目录
file.mkdirs();//一次可以创建多级目录
}
}
创建完以后的效果如图
接下来我们看看如何进行文件重命名
//文件重命名
public class Demo5 {
public static void main(String[] args) {
File file = new File("./text.txt");
File file2 = new File("./text2.txt");
file.renameTo(file2);
}
}
修改前修改后
我们还可以修改文件所在目录
public class Demo5 {
public static void main(String[] args) {
File file = new File("./text.txt");
File file2 = new File("./src/text2.txt");
file.renameTo(file2);
}
}
这样就把文件修改至src里面了
以上文件的操作都是基于File类来完成的
另外还需要文件内容的操作,用到的类是文件流,操作系统就管这个叫做流
比如我想读100字节的文件数据,我可以一次读完,一次读一百字节,也可以分两次读,一次读50字节,还可以分十次读,一次读10字节
文件这里的内容本质来自于硬盘,硬盘又是操作系统管理的
使用某个编程语言操作文件,本质上都是需要调用系统的API
虽然不同的编程语言,操作文件的API有所差别,但是基本的步骤都是一样的
文件内容的操作核心步骤,有四个
1.打开文件
2.关闭文件
3.读文件
4.写文件
Java这里头我们是通过标准库中的一系列类去完成这里的操作的,大致可以分为两个类别
1.字节流(InputStream,OutputStream):后续的一些操作字节的类都是衍生自这两个类,是操作字节为单位(针对二进制文件)
2.字符流(Reader,Writer):后续操作字符的类,衍生自这两个类,是操作字符为单位(针对文本文件)
Java IO流是一个比较庞大的体系,涉及到非常多的类,这些不同类都有各自不同的特性,但是总的来说,使用方法都是类似的
1.其中我们就使用类的构造方法打开文件,当我们创建这个流对象的时候就会打开文件
2.close方法,关闭文件
3.如果衍生自InputStream或者Read,就可以使用read方法来读数据(读就是把数据从硬盘读到内存里,咱是站在CPU的视角)
4.如果衍生自OutputStream 或者Writer就可以使用write方法来写数据了
接下来我们看看Reader的使用
//Reader使用
public class Demo6 {
public static void main(String[] args) throws IOException {
//FileReader 构造方法,里面可以填写一个文件路径(绝对路径/相对路径),也可以填写一个构造好的file对象
Reader reader = new FileReader("d:/text.txt");
try{
//中间的代码无论什么情况,最后都能保证close
}
finally{
reader.close();//这个操作非常重要,释放必要的资源,我们从操作系统打开文件是需要申请一定的资源的(这个占用了进程的pcb里的文件描述符表中的一个表项)
}//文件描述符表是一个长度有限并且不会自动扩容的顺序表,所以如果不释放,就会出现"文件资源泄露"很严重的问题
} //一旦一直打开文件,而不去关闭不用的文件,文件描述符表就会被占满,后续就无法打开新的文件了
}
但是上面的代码比较啰嗦,不够优雅,所以我们还有别的办法
public static void main(String[] args) throws IOException{
//使用try with resource 才是更好的解决方案
try(Reader reader = new FileReader("d:/text.txt");
Reader reader1 = new FileReader("c:/test.txt")){//try里面可以写多个打开文件的操作
}//只要try代码块执行完毕了,就会自动调用到close方法
}
文件流中的任意对象,都可以按照上述的讨论来进行close