每日问题0310

本文详细介绍了Java中的Map存储特点,包括HashMap的底层实现,从put操作开始,详细阐述了键值对存储的过程,包括哈希计算、冲突处理和扩容机制。同时,文章讨论了并发环境下的线程安全问题,提到了多线程的使用原因和创建方式,并简述了线程通信的几种机制。此外,文章还对比了TCP和UDP的区别,以及TCP连接的三次握手和四次挥手过程。
摘要由CSDN通过智能技术生成

每日问题:

0309

Map存储特点?

map是关联键值对关系的集合;

Key(键) : 是无序的,去重的,唯一的;

Value(值): 是无序的.可重复的;

键值可是为任意引用数据类型;

一个key对应一个value. 如果对一个key进行了多次put,后面的将覆盖前面的;

HashMap的底层存储方式?

jdk1.8之前底层实现是数组加链表;

jdk1.8之后HashMap 的底层实现是数组+链表+红黑树的形式的,同时它的数组的默认初始容量是 16、扩容因子为 0.75,每次采用 2 倍的扩容。也就是说,每当我们数组中的存储容量达到 75%的时候,就需要对数组容量进行 2 倍的扩容。

过程:

**1.**调用put方法添加键值对key,value数据

**2.**根据key计算hash值

int hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

**3.**调用putVal(hash,key,value)实现存储键值对数据

**4.**执行putVal方法,第一步就判断哈希表底层的节点数组是否null,或者底层数组的长度0,如果是证明这是第一次添加,底层没有数组或者没有带有容量的数组,先调用resize()方法实现创建新数组

if ((tab = table) == null || (n = tab.length) == 0)

n = (tab = resize()).length;

**5.**根据key的hash值计算位桶索引int index = (n - 1) & hash,判断table[index]==null是否存在单向链表的首节点

if ((p = tab[i = (n - 1) & hash]) == null)

**6.**如果=null,证明不存在单向链表,直接创建新节点,存储这一次要添加的键值对数据,直接放在数组table[index]作为单向链表的首节点

tab[i] = newNode(hash, key, value, null); --> new Node<>(hash, key, value, next)

**7.**如果!=null,证明存在单向链表的首节点,遍历这个链表,判断链表中每一个节点的存储的key与这一次要添加的键值对的key比较是否相等,

if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))

如果相同value覆盖

V oldValue = e.value;
e.value = value;
return oldValue;

如果不相同创建新节点,挂在原单向链表的最后

p.next = newNode(hash, key, value, null);

**8.**如果以上的步骤中执行到了new Node()创建新节点放入哈希表中,数据个数+1,判断是否>扩容阈值threshold,如果满足,就调用resize方法实现扩容

if (++size > threshold)

resize();

什么是hash碰撞?

在计算hash地址的过程中会出现对于不同的关键字出现相同的哈希地址的情况,即key1 ≠ key2,但是f(key1) = f(key2),这种情况就是Hash 冲突。具有相同关键字的key1和key2称之为同义词。

通过优化哈希函数可以减少这种冲突的情况(如:均衡哈希函数),但是在通用条件下,考虑到于表格的长度有限及关键值(数据)的无限,这种冲突是不可避免的,所以就需要处理冲突。

如何解决hash碰撞?

解决哈希冲突的方法

解决哈希冲突的方法一般有:开放定址法、链地址法(拉链法)、再哈希法、建立公共溢出区等方法。

开放定址法

从发生冲突的那个单元起,按照一定的次序,从哈希表中找到一个空闲的单元。然后把发生冲突的元素存入到该单元的一种方法。开放定址法需要的表长度要大于等于所需要存放的元素。 在开放定址法中解决冲突的方法有:线行探查法、平方探查法、双散列函数探查法。 开放定址法的缺点在于删除元素的时候不能真的删除,否则会引起查找错误,只能做一个特殊标记。只到有下个元素插入才能真正删除该元素。

线行探查法

线行探查法是开放定址法中最简单的冲突处理方法,它从发生冲突的单元起,依次判断下一个单元是否为空,当达到最后一个单元时,再从表首依次判断。直到碰到空闲的单元或者探查完全部单元为止。

平方探查法

平方探查法即是发生冲突时,用发生冲突的单元d[i], 加上 1²、 2²等。即d[i] + 1²,d[i] + 2², d[i] + 3²…直到找到空闲单元。 在实际操作中,平方探查法不能探查到全部剩余的单元。不过在实际应用中,能探查到一半单元也就可以了。若探查到一半单元仍找不到一个空闲单元,表明此散列表太满,应该重新建立。

双散列函数探查法

这种方法使用两个散列函数hl和h2。其中hl和前面的h一样,以关键字为自变量,产生一个0至m—l之间的数作为散列地址;h2也以关键字为自变量,产生一个l至m—1之间的、并和m互素的数(即m不能被该数整除)作为探查序列的地址增量(即步长),探查序列的步长值是固定值l;对于平方探查法,探查序列的步长值是探查次数i的两倍减l;对于双散列函数探查法,其探查序列的步长值是同一关键字的另一散列函数的值。

链地址法(拉链法

链接地址法的思路是将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。

如何解决并发问题?

应用场景建议方案?

0310&0311

程序、进程、线程之间的区别和联系?

1)进程是程序及其数据在计算机的一次运行活动,是一个运行过程,是一个动态的概念。进程的运行实体是程序,离开程序的进程没有存在的意义。而程序是一组有序的指令集合,是一种静态概念。

2)进程是程序的一次执行过程,它是动态地创建和消亡的,具有一定的生命周期,是暂时存在的;而程序则是一组代码的集合,它是永久存在的,可长期保存。

3)一个进程可以执行一个或几个程序,一个程序也可以构成多个进程。进程可以创建进程,而程序不能形成新的程序。

4)进程和程序的组成不同。从静态角度看,进程由程序、数据和进程控制块(PCB)三部分组成。而程序是一组有序的指令集合©著作权归作者所有:来自51CTO博客作者姜兴琪的原创作品,请联系作者获取转载授权,否则将追究法律责任

进程与程序的区别和联系

为什么要使用多线程?

选择多线程的原因就是一个 字,使用多线程就是在正确的场景下通过设置正确个数的线程来最大化程序的运行速度。

  • 充分的利用 CPU 和 I/O 的利用率

  • 合理的场景+合理的线程数 得到运行效率的提升。

具体来说

1、多线程和进程相比,它是一种非常花销小,切换快,更"节俭"的多任务操作方式。

在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。

而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,

而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。

2、方便的通信机制。

对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。

线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。

当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

原文链接:https://blog.csdn.net/JMW1407/article/details/107222065

为什么要使用多线程?

线程的创建方式有哪些?

(1)继承Thread类并重写run()方法;

(2)实现Runnable接口;

(3)匿名内部类;

(4)实现Callabe接口;

(5)定时器(java.util.Timer);

(6)线程池;

(7)并行计算(Java8+);

(8)Spring异步方法;

参考https://developer.aliyun.com/article/720284

如何完成线程的通信?以及它的通信机制有哪些?

线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。

锁机制:包括互斥锁、条件变量、读写锁、自旋锁

wait/notify等待

Volatile内存共享

信号量机制(Semaphore)

信号机制(Signal)

https://blog.csdn.net/J080624/article/details/87454764

https://juejin.cn/post/6969122698563682311

==拓展==

请详细描述一下线程的状态?

  • 新建状态:

使用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序start()这个线程。

  • 就绪状态:

当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:

如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

  • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

  • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

  • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

  • 死亡状态:

一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

如何能够保证更加高效的使用线程?

Java提供了不同层面的线程安全支持。在传统集合框架内部,除了Hashtable等同步容器,还提供了所谓的同步包装器(Synchronized Wrapper),我们可以调用Collections工具类提供的包装方法,来获取一个同步的包装容器(Collections.synchronizedMap),但是它们都是利用非常粗粒度的同步方式,在高并发情况下,性能比较低下

另外,更加普遍的选择是利用并发包提供的线程安全容器类,它提供了:

各种并发容器,比如ConcurrentHashMap、CopyOnWriteArrayList

各种线程安全队列(Queue/Deque),如ArrayBlockingQueue、SynchronousQueue

各种有序容器的线程安全版本等。

具体保证线程安全的方式,包括有从简单的synchronize方式,到基于更加精细化的,比如基于分离锁实现的ConcurrentHashMap等并发实现等。具体选择要看开发的场景需求,总体来说,并发包内提供的容器通用场景,远优于早期的简单同步实现。

0313

TCP和UDP区别:

网络分层模型:

“三次握手四次挥手

第一次握手

客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。

第二次握手

服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。

第三次握手

当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

这里可能大家会有个疑惑:为什么 TCP 建立连接需要三次握手,而不是两次?这是因为这是为了防止出现失效的连接请求报文段被服务端接收的情况,从而产生错误。

TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。

第一次握手

若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。

第二次握手

B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明 A 到 B 的连接已经释放,不再接收 A 发的数据了。但是因为 TCP 连接是双向的,所以 B 仍旧可以发送数据给 A。

第三次握手

B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。

第四次握手

A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

编写流程

tcp协议下实现双向登录流程:客户端

1、定义我是客户端 Socket

2、键盘输入接受用户输入的登陆信息

3、获取输入流,向服务器发送数据

4、刷出

5、获取输入流,读取服务器响应的结果

6、处理结果

7、关闭

服务端:

1、定义我是服务端 ServerSocket

2、阻塞式监听 .accept( )

3、获取输入流,读取客户端发送的数据

4、处理数据

5、获取输出流,将结果响应给客户端

6、刷出

7、关闭

客户端:

public class Class005_LoginTwoWayClient {
    public static void main(String[] args) throws IOException {
        System.out.println("--------------我是客户端--------------");
        //1.定义我是客户端
        Socket client = new Socket("localhost",8989);
        System.out.println("连接建立起来了....");
        //2.准备数据
        BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
        System.out.println("请输入用户名:");
        String username = bf.readLine();
        System.out.println("请输入密码:");
        String password = bf.readLine();
        //3.获取输出流,向服务器发送数据
        DataOutputStream os = new DataOutputStream(client.getOutputStream());
        os.writeUTF("username="+username+"&password="+password);
        //4.刷出
        os.flush();
        //5.获取输入流,读取服务器响应的结果
        DataInputStream is = new DataInputStream(client.getInputStream());
        String result = is.readUTF();
        //6.处理结果
        System.out.println(result);
        //7.关闭
        is.close();
        os.close();
        bf.close();
        client.close();
    }
}
服务端:

public class Class006_LoginTwoWayServer {
    public static void main(String[] args) throws IOException {
        System.out.println("--------------我是服务器--------------");
        //1.定义我是服务器 ServerSocket
        ServerSocket server = new ServerSocket(8989);
        //2.阻塞式监听
        Socket client = server.accept();
        System.out.println("与一个客户端建立连接了.............");
        //3.获取输入流,读取客户端发送的数据
        DataInputStream is = new DataInputStream(client.getInputStream());
        String msg = is.readUTF();
        //4.处理数据
        //1)校验方式
        /*if("username=admin&password=1234".equals(msg)){
            System.out.println("登录成功");
        }else{
            System.out.println("用户名或密码错误");
        }*/
        //2)校验方式
        String uname=null;
        String upwd=null;
        String[] arr = msg.split("&");
        for(String str:arr){
            String[] arr2 = str.split("=");
            if("username".equals(arr2[0])){
                uname = arr2[1];
            }else if("password".equals(arr2[0])){
                upwd = arr2[1];
            }
        }
        //5.获取输出流,将结果响应给客户端
        DataOutputStream os = new DataOutputStream(client.getOutputStream());
        if("admin".equals(uname) && "1234".equals(upwd)){
            os.writeUTF("登录成功");
        }else{
            os.writeUTF("用户名或密码错误");
        }
        //6.刷出
        os.flush();
        //7.关闭
        os.close();
        is.close();
        client.close();
        server.close();
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值