CurrentHashMap和文件操作IO

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值