参考:绝大部分答案来自网络搜索,特别是JavaGuide。
- 使用 ping 测试某个地址是否能连接。ping命令本身处于应用层,相当于一个应用程序,它直接使用网络层的ICMP协议。执行ping指令会使用ICMP传输协议,发出要求回应的信息。ping无法检查系统端口是否开放。
- Telnet是位于应用层上的一种协议,常用于网页服务器的远端控制,可供使用者在本地主机执行远端主机上的工作。telnet可以检查某个端口是否开放:telnet IP:Port
telnet www.baidu.com 80- 使用curl 测试用个 URL 是否可以访问。一个利用URL规则在命令行下工作的文件传输工具,它支持文件的上传和下载,,可以帮助我们在服务器上模拟HTTP的行为。
netstat是在内核中访问网络及相关信息的程序,它可以显示路由表、实际的网络连接以及每一个网络接口设备的状态信息。
ifconfig 用于查看和配置 Linux 系统的网络接口。 查看所有网络接口及其状态:ifconfig -a 。 使用 up 和
down 命令启动或停止某个接口:ifconfig eth0 up 和 ifconfig eth0 down 。
iptables ,是一个配置 Linux 内核防火墙的命令行工具。功能非常强大,对于开发来说,主要掌握如何开放端口即可. 例子:开启 80
端口,因为web对外都是这个端口 iptables -A INPUT -p tcp --dport 80 -j ACCEP例子:
1、查看哪些端口被打开:netstat -anp
2、查看端口是否开放:netstat -anlp | grep 3006 (如果有内容,说明被放行了,如果没内容,说明没放行)
3、放行7002端口:iptables -I INPUT -p tcp --dport 7002 -j ACCEPT
4、关闭端口号:iptables -A OUTPUT -p tcp --dport 端口号 -j DROP
LISTEN:侦听来自远方的TCP端口的连接请求
SYN-SENT:再发送连接请求后等待匹配的连接请求(如果有大量这样的状态包,检查是否中招了)
SYN-RECEIVED:再收到和发送一个连接请求后等待对方对连接请求的确认(如有大量此状态,估计被flood攻击了)
ESTABLISHED:代表一个打开的连接
FIN-WAIT-1:等待远程TCP连接中断请求,或先前的连接中断请求的确认
FIN-WAIT-2:从远程TCP等待连接中断请求
CLOSE-WAIT:等待从本地用户发来的连接中断请求
CLOSING:等待远程TCP对连接中断的确认
LAST-ACK:等待原来的发向远程TCP的连接中断请求的确认(不是什么好东西,此项出现,检查是否被攻击)
TIME-WAIT:等待足够的时间以确保远程TCP接收到连接中断请求的确认
CLOSED:没有任何连接状态
ps 命令用于显示正在运行中的进程的信息
在 Linux 系统中,一个进程结束了,但是他的父进程没有等待(调用wait / waitpid)他,那么他将变成一个僵尸进程。当用ps命令观察进程的执行状态时,看到这些进程的状态栏为defunct。僵尸进程是一个早已死亡的进程,但在进程表(processs table)中仍占了一个位置(slot)。
1、查看系统是否有僵尸进程
使用Top命令查找,当zombie前的数量不为0时,即系统内存在相应数量的僵尸进程。
2、定位僵尸进程
使用命令ps -A -ostat,ppid,pid,cmd |grep -e '1'定位僵尸进程以及该僵尸进程的父进程
僵尸进程ID:3457,父进程ID:3425
僵尸进程ID:3533,父进程ID:3511
3、使用Kill -HUP 僵尸进程ID来杀死僵尸进程,往往此种情况无法杀死僵尸进程,此时就需要杀死僵尸进程的父进程
kill -HUP 僵尸进程父ID
然后使用上面的语句查询该僵尸进程是否被杀死
top 显示当前系统正在执行的进程的相关信息,包括进程 ID、内存占用率、CPU 占用率等
可以实时查看相对全面的查看系统负载的来源:系统负载情况(uptime)、系统内存使用情况(free)、系统 CPU 使用情况(vmstat)等。同时,top 命令支持排序,可以按照不同的列排序,方便查找出诸如内存占用最多的进程、CPU占用率最高的进程等。
MySQL数据库面试题
第一范式:每个列都不可以再拆分。
第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。
第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。
在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。
原文链接:https://blog.csdn.net/ThinkWon/article/details/104778621
举例
事务是一步或几步数据库操作序列组成逻辑执行单元。
事务最经典也经常被拿出来说例⼦就是转账了。假如⼩明要给⼩红转账1000元,这个转账会涉及到两个关键操作就是:将⼩明的余额减少1000元,将⼩红的余额增加1000元。万⼀在这两个操作之间突然出现错误⽐如银⾏系统崩溃,导致⼩明余额减少⽽⼩红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
- 原⼦性(Atomicity):事务是最⼩的执⾏单位,不允许分割。事务的原⼦性确保动作要么全部完成,要么完全不起作⽤;
- ⼀致性(Consistency):执⾏事务前后,数据保持⼀致,多个事务对同⼀个数据读取的结果是相同的。一致性是对数据可见性的约束,保证在一个事务中的多次操作的数据中间状态对其他事务不可见的。因为这些中间状态,是一个过渡状态,与事务的开始状态和事务的结束状态是不一致的。
- 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
- 持久性(Durability):⼀个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发⽣故障也不应该对其有任何影响。//事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
MySQL的事务隔离级别一共有四个,分别是读未提交、读已提交、可重复读以及可串行化。
MySQL的隔离级别的作用就是让事务之间互相隔离,互不影响,这样可以保证事务的一致性。
MySQL默认的隔离级别也是可重复读
脏读
脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并不一定最终存在的数据,这就是脏读。
不可重复读
不可重复读指的是在同一事务内,不同的时刻读到的同一批数据可能是不一样的,可能会受到其他事务的影响,比如其他事务改了这批数据并提交了。通常针对数据更新(UPDATE)操作。
幻读
幻读是针对数据插入(INSERT)操作来说的。假设事务A对某些行的内容作了更改,但是还未提交,此时事务B插入了与事务A更改前的记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事务A中查询,会发现好像刚刚的更改对于某些数据未起作用,但其实是事务B刚插入进来的,让用户感觉很魔幻,感觉出现了幻觉,这就叫幻读。
读未提交
可以读到其他事务未提交的数据。MySQL 事务隔离其实是依靠锁来实现的,加锁自然会带来性能的损失。而读未提交隔离级别是不加锁的。
读提交
读提交就是一个事务只能读到其他事务已经提交过的数据,也就是其他事务调用 commit 命令之后的数据
可重复读
可重复读是指,事务不会读到其他事务对已有数据的修改,即使其他事务已提交,也就是说,事务开始时读到的已有数据是什么,在事务提交前的任意时刻,这些数据的值都是一样的。但是,对于其他事务新插入的数据是可以读到的,这也就引发了幻读问题。
串行化
串行化是4种事务隔离级别中隔离效果最好的,解决了脏读、可重复读、幻读的问题。但是效果最差,它将事务的执行变为顺序执行,与其他三个隔离级别相比,它就相当于单线程,后一个事务的执行必须等待前一个事务结束。
MyISAM是MySQL的默认数据库引擎(5.5版之前)。虽然性能极佳,⽽且提供了⼤量的特性,包括全⽂索引、压缩、空间函数等,但MyISAM不⽀持事务和⾏级锁,⽽且最⼤的缺陷就是崩溃后⽆法安全恢复。不过,5.5版本之后,MySQL引⼊了InnoDB(事务性数据库引擎),MySQL 5.5版本后默认的存储引擎为InnoDB。
⼤多数时候我们使⽤的都是 InnoDB 存储引擎,但是在某些情况下使⽤ MyISAM 也是合适的⽐如读密集的情况下。(如果你不介意 MyISAM 崩溃恢复问题的话)。
两者的对⽐:
- 是否⽀持⾏级锁 : MyISAM 只有表级锁(table-level locking),⽽InnoDB ⽀持⾏级锁(rowlevel locking)和表级锁,默认为⾏级锁。
- 是否⽀持事务和崩溃后的安全恢复: MyISAM强调的是性能,每次查询具有原⼦性,其执⾏速度⽐InnoDB类型更快,但是不提供事务⽀持。但是InnoDB提供事务⽀持事务,外部键等⾼级数据库功能。具有事务(commit)、回滚(rollback)和崩溃修复能⼒(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。
- 是否⽀持外键: MyISAM不⽀持,⽽InnoDB⽀持。
- 是否⽀持MVCC:仅 InnoDB ⽀持。应对⾼并发事务, MVCC⽐单纯的加锁更⾼效;MVCC只在READ COMMITTED和REPEATABLE READ两个隔离级别下⼯作;MVCC可以使⽤乐观(optimistic)锁和悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统⼀。推荐阅读:MySQL-InnoDB-MVCC多版本并发控制
- …
MVCC(Mutil-Version Concurrency Control),就是多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。在我看来,他无非就是乐观锁的一种实现方式。
当多个用户同时对数据库并发操作时,会带来数据不一致的问题,所以,锁主要用于多用户环境下保证数据库完整性和一致性。
MyISAM和InnoDB存储引擎使⽤的锁:MyISAM采⽤表级锁(table-level locking)。InnoDB⽀持⾏级锁(row-level locking)和表级锁,默认为⾏级锁。
表级锁: MySQL中锁定粒度最⼤的⼀种锁,对当前操作的整张表加锁,实现简单,资源消耗也⽐较少,加锁快,不会出现死锁。其锁定粒度最⼤,触发锁冲突的概率最⾼,并发度最低,MyISAM和 InnoDB引擎都⽀持表级锁。
⾏级锁: MySQL中锁定粒度最⼩的⼀种锁,只针对当前操作的⾏进⾏加锁。⾏级锁能⼤⼤减少数据库操作的冲突。其加锁粒度最⼩,并发度⾼,但加锁的开销也最⼤,加锁慢,会出现死锁。
悲观锁(Pessimistic Lock)
顾名思义,很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人拿这个数据就会block(阻塞),直到它拿锁。
传统的关系数据库里用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。
乐观锁(Optimistic Lock)
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以,不会上锁。但是在更新的时候会判断一下在此期间别人有没有更新这个数据,可以使用版本号等机制。
比较:
悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。
乐观锁,大多是基于数据版本( Version )记录机制实现。
写锁
写锁其实跟字面上的意思差不多,就是写的时候加写锁,直到这个写锁被释放之前,任何事务都不能对这个被锁对象再加任何锁。(注意:是不能加锁,而不是不能读写)
读锁
虽然加了(读)锁,但是其他事务还是能再加一把读锁。而只有对象上所有的读锁都被释放之后,事务才能对该对象加写锁。(同样的,是不能加写锁,而不是不能读写)
//TBC 还有好几种锁
是什么?
数据库索引,是数据库管理系统中一个排序的数据结构。
索引也是要占用存储空间的。索引存储了指向表中某一行的指针。
在MySQL中,当你建立一个主键和候选键之后,MySQL会为它们分别建立索引。
为什么?
索引可以提高查询速度,但是会影响插入记录的速度。因为,向有索引的表中插入记录时,数据库系统会按照索引进行排序,这样就降低了插入记录的速度,插入大量记录时的速度影响会更加明显。
索引里究竟存的是什么?
数据库索引是创建在表的某列上的,并且存储了这一列的所有值,并不存储中其他列的值,而是存储了指向表中某一行的指针
指针是指一块内存区域,该内存区域记录的是对硬盘上记录的相应行的数据的引用。举例来说,索引中的Employee_Name这列的某个值(或者节点)可以描述为 (“Jesus”, 0x82829), 0x82829 就是包含 “Jesus”那行数据在硬盘上的地址。
怎么实现?
MySQL索引使⽤的数据结构主要有BTree索引和哈希索引。
对于哈希索引来说,底层的数据结构就是哈希表,因此在绝⼤多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;
其余⼤部分场景,建议选择BTree索引。B-Tree 是最常用的用于索引的数据结构。因为它们是时间复杂度低, 查找、删除、插入操作都可以在对数时间内完成。另外一个重要原因存储在B-Tree中的数据是有序的。
MySQL的BTree索引使⽤的是B树中的B+Tree。
大规模数据存储中,实现索引查询这样一个实际背景下,树节点存储的元素数量是有限的(如果元素数量非常多的话,查找就退化成节点内部的线性查找了),这样导致二叉查找树结构(二叉查找树,平衡二叉查找树,红黑树)由于树的深度过大而造成磁盘I/O读写过于频繁,进而导致查询效率低下,那么如何减少树的深度(当然是不能减少查询的数据量),一个基本的想法就是:采用多叉树结构(由于树节点元素数量是有限的,自然该节点的子树数量也就是有限的)。
B-树(B-Tree)也即B树,是多路平衡查找树,相对于平衡二叉树,对父结点的直接子结点个数,不再仅限于2。B是指balance。
B+树(B+Tree)B+树是B树变体,相对于B-树,叶子结点的值包含了所有的值,父节点不存储数据,只存储索引,只起索引查找的作用。
同时叶子结点也构成了一条有序的链表。 B+树只要遍历叶子节点就可以实现整棵树的遍历。因此查询效率更高。 搜索画图举例
mysql中存储引擎为innodb的索引,采用的数据结构即是B+树。
MySQL目前主要有以下几种索引类型:
1.普通索引
是最基本的索引,它没有任何限制
2.唯一索引
与普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
3.主键索引
是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。(也就是主键)
4.组合索引
指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合
5.全文索引
查找含有某关键词的text。
CHAR、VARCHAR或TEXT列可创建。
允许在这些索引列中插入重复值和空值
和常用的模糊匹配使用 like + % 不同,全文索引有自己的语法格式,使用 match 和 against 关键字。
联合索引: 对多个字段同时建立的索引(有顺序,ABC,ACB是完全不同的两种联合索引。) 最左匹配原则:(A,B,C)
这样3列,mysql会首先匹配A,然后再B,C.
如果用(B,C)这样的数据来检索的话,就会找不到A使得索引失效。如果使用(A,C)这样的数据来检索的话,就会先找到所有A的值然后匹配C,此时联合索引是失效的。
减少开销:假如对col1、col2、col3创建组合索引,相当于创建了(col1)、(col1,col2)、(col1,col2,col3)3个索引
覆盖索引:假如查询SELECT col1, col2, col3 FROM 表名,由于查询的字段存在索引页中,那么可以从索引中直接获取,而不需要回表查询
效率高:对col1、col2、col3三列分别创建索引,MySQL只会选择辨识度高的一列作为索引。假设有100w的数据,一个索引筛选出10%的数据,那么可以筛选出10w的数据;对于组合索引而言,可以筛选出100w10%10%*10%=1000条数据
MySQL 的主从复制又叫 Replication、AB 复制。至少需要两个 MySQL 服务(可以是同一台机器,也可以是不同机器之间进行)。
比如A服务器做主服务器,B服务器做从服务器,在A服务器上进行数据的更新,通过 binlog 日志记录****同步到B服务器上,并重新执行同步过来的 binlog 数据,从而达到两台服务器数据一致。
MySQL的主从复制并不是数据库磁盘上的文件直接拷贝,而是通过逻辑的 binlog 日志复制到要同步的服务器本地,然后由本地的线程读取日志里面的 SQL 语句,重新应用到 MySQL 数据库中。
可以实时灾备,用于故障切换;
读写分离,提供查询服务,实现负载均衡;
数据热备,避免影响业务。
分区
所谓分区就是将一个表分解成多个区块进行操作和保存,从而降低每次操作的数据,提高性能。
MySQL支持横向分区。什么是横向分区呢?举例来说明一下,假如有100W条数据,分成十份,前10W条数据放到第一个分区,第二个10W条数据放到第二个分区,依此类推。
分库 分表
当一张表随着时间和业务的发展,库里表的数据量会越来越大。数据操作也随之会越来越大。一台物理机的资源有限,最终能承载的数据量、数据的处理能力都会受到限制。这时候就会使用分库分表来承接超大规模的表,单机放不下的那种。
什么是分表,从表面意思上看呢,就是把一张表分成N多个小表。
什么是分库,把表放到不同库之中。
查询速度慢的原因
从程序员的角度:
1 查询语句写的不好
2 没建索引,索引建的不合理或索引失效
3 关联查询有太多的join
从服务器的角度:
4 服务器磁盘空间不足
5 服务器调优配置参数设置不合理
- 设计一个10亿用户收发红包的数据库。
- 写一个SQL:打印某一列的重复值
select cloums from tableA group by cloums having count(cloums)>1;- 一个表id name class_id class_score 写一条语句查询至少选了4门课以上的人的id和name
应⽤层(application-layer)的任务是通过应⽤进程间的交互来完成特定⽹络应⽤。应⽤层协议定义的是应⽤进程(进程:主机中正在运⾏的程序)间的通信和交互的规则。对于不同的⽹络应⽤需要不同的应⽤层协议。在互联⽹中应⽤层协议很多,如域名系统DNS,⽀持万维⽹应⽤的HTTP协议,⽀持电⼦邮件的SMTP协议等等。
我们把应⽤层交互的数据单元称为报⽂。
传输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通⽤的数据传输服务。应⽤进程利⽤该服务传送应⽤层报⽂。特点是复用和分用。
复用:在发送端,多个应用进程公用一个传输层;
分用:在接收端,传输层会根据端口号将数据分给不同的应用进程。
运输层主要使⽤以下两种协议:
⽹络层在计算机⽹络中进⾏通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信⼦⽹。⽹络层的任务就是选择合适的⽹间路由和交换结点,确保数据及时传送。在发送数据时,⽹络层把运输层产⽣的报⽂段或⽤户数据报封装成分组和包进⾏传送。
在 TCP/IP 体系结构中,由于⽹络层使⽤IP 协议,因此分组也叫IP 数据报,简称数据报。
数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输,总是在⼀段⼀段的链路上传送的,这就需要使⽤专⻔的链路层的协议。在两个相邻节点之间传送数据时,数据链路层将⽹络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧。
物理层在物理层上所传送的数据单位是⽐特。物理层(physical layer)的作⽤是实现相邻计算机节点之间⽐特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。使其上⾯的数据链路层不必考虑⽹络的具体传输介质是什么。
物理层:
RJ45、CLOCK、IEEE802.3 (中继器,集线器,网关)
数据链路:
PPP、FR、HDLC、VLAN、MAC
网络层:(网桥,交换机)
IP、ICMP、ARP、RARP、OSPF、IPX、RIP、IGRP、 (路由器)
传输层:
TCP、UDP
应用层:
FTP、DNS、Telnet、SMTP、HTTP、WWW
一条TCP连接的两端就是两个套接字。套接字=IP地址: 端口号。因此,TCP连接=(套接字1,套接字2)=(IP1:端口号1,IP2:端口号2)
为什么要三次握手?
三次握⼿的⽬的是建⽴可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,⽽三次握⼿最主要的⽬的就是双⽅确认⾃⼰与对⽅的发送与接收是正常的。
第⼀次握⼿:Client 什么都不能确认;Server 确认了对⽅发送正常,⾃⼰接收正常
第⼆次握⼿:Client 确认了:⾃⼰发送、接收正常,对⽅发送、接收正常;Server 确认了:对⽅发送正常,⾃⼰接收正常
第三次握⼿:Client 确认了:⾃⼰发送、接收正常,对⽅发送、接收正常;Server 确认了:⾃⼰发送、接收正常,对⽅发送、接收正常
所以三次握⼿就能确认双发收发功能都正常,缺⼀不可。
为什么不能两次握手?
tcp在两次握手后,服务器只能确认客户端到服务器的通路是正常的,并不能保证服务器到客户端的通路正常。
在通路不通的情况下发送消息,结果只能是丢包和白白浪费系统资源。
为什么要四次挥⼿?
两方都可以关闭通信
任何⼀⽅都可以在数据传送结束后发出连接释放的通知,待对⽅确认后进⼊半关闭状态。
当另⼀⽅也没有数据再发送的时候,则发出连接释放通知,对⽅确认后就完全关闭了TCP连接。
举个例⼦:A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,B回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着⾃⼰的节奏结束通话,于是 B 可能⼜巴拉巴拉说了⼀通,最后B 说“我说完了”,A 回答“知道了”,这样通话才算结束。
UDP 在传送数据之前不需要先建⽴连接,远地主机在收到 UDP 报⽂后,不需要给出任何确认。虽然UDP 不提供可靠交付,但在某些情况下 UDP 确是⼀种最有效的⼯作⽅式(⼀般⽤于即时通信),⽐如: QQ 语⾳、 QQ 视频、直播等等
TCP 提供⾯向连接的服务。在传送数据之前必须先建⽴连接,数据传送结束后要释放连接。 TCP 不提供⼴播或多播服务。由于 TCP 要提供可靠的,⾯向连接的传输服务,增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的⾸部增⼤很多,还要占⽤许多处理机资源。TCP ⼀般⽤于⽂件传输、发送和接收邮件、远程登录等场景。
TCP的可靠体现在TCP在传递数据之前,会有三次握⼿来建⽴连接,⽽且在数据传递时,有确认、窗⼝、重传、拥塞控制机制,在数据传完后,还会断开连接⽤来节约系统资源。
- 应⽤数据被分割成 TCP 认为最适合发送的数据块。
- TCP 给发送的每⼀个包进⾏编号,接收⽅对数据包进⾏排序,把有序数据传送给应⽤层。
- 校验和: TCP 将保持它⾸部和数据的检验和。这是⼀个端到端的检验和,⽬的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报⽂段和不确认收到此报⽂段。
- TCP 的接收端会丢弃重复的数据。
- 流量控制: TCP 连接的每⼀⽅都有固定⼤⼩的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收⽅来不及处理发送⽅的数据,能提示发送⽅降低发送的速率,防⽌包丢失。TCP 使⽤的流量控制协议是可变⼤⼩的滑动窗⼝协议。(TCP 利⽤滑动窗⼝实现流量控制)
- 拥塞控制:当⽹络拥塞时,减少数据的发送
- ARQ协议:也是为了实现可靠传输的,它的基本原理就是每发完⼀个分组就停⽌发送,等待对⽅确认。在收到确认后再发下⼀个分组。
- 超时重传:当 TCP 发出⼀个段后,它启动⼀个定时器,等待⽬的端确认收到这个报⽂段。如果不能及时收到⼀个确认,将重发这个报⽂段。
TCP 利⽤滑动窗⼝实现流量控制。流量控制是为了控制发送⽅发送速率,保证接收⽅来得及接收。
接收⽅发送的确认报⽂中的窗⼝字段可以⽤来控制发送⽅窗⼝⼤⼩,从⽽影响发送⽅的发送速率。将窗⼝字段设置为 0,则发送⽅不能发送数据。
拥塞控制是⼀个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低⽹络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。
为了进⾏拥塞控制,TCP发送⽅要维持⼀个拥塞窗⼝(cwnd)的状态变量。拥塞控制窗⼝的⼤⼩取决于⽹络的拥塞程度,并且动态变化。发送⽅让⾃⼰的发送窗⼝取为拥塞窗⼝和接收⽅的接受窗⼝中较⼩的⼀个。
TCP的拥塞控制采⽤了四种算法,即慢开始、拥塞避免、快重传和快恢复。在⽹络层也可以使路由器采⽤适当的分组丢弃策略(如主动队列管理
AQM),以减少⽹络拥塞的发⽣。
1、
慢开始:当主机开始发送数据时,如果⽴即把⼤量数据字节注⼊到⽹络,那么可能会引起⽹络阻塞。经验表明,好的⽅法是先探测⼀下,即由⼩到⼤逐渐增⼤发送窗⼝,也就是由⼩到⼤逐渐增⼤拥塞窗⼝数值。cwnd初始值为1,每经过⼀个传播轮次,cwnd加倍。
2、
拥塞避免:让拥塞窗⼝cwnd缓慢增⼤,即每经过⼀个往返时间RTT就把发送放的cwnd加1.
3、
快重传与快恢复:在 TCP/IP 中,快速重传和恢复(fast retransmit and recovery,FRR)是⼀种拥塞控制算法,它能快速恢复丢失的数据包。
没有 FRR,如果数据包丢失了,TCP 将会使⽤定时器来要求传输暂停。在暂停的这段时间内,没有新的或复制的数据包被发送。
有了 FRR,如果接收机接收到⼀个不按顺序的数据段,它会⽴即给发送机发送⼀个重复确认。如果发送机接收到三个重复确认,它会假定确认件指出的数据段丢失了,并⽴即重传这些丢失的数据段。有了FRR,就不会因为重传时要求的暂停被耽误。
当有单独的数据包丢失时,快速重传和恢复(FRR)能最有效地⼯作。当有多个数据信息包在某⼀段很短的时间内丢失时,它则不能很有效地⼯作。
⾃动重传请求(Automatic Repeat-reQuest,ARQ)是OSI模型中数据链路层和传输层的错误纠正协议之⼀。它通过使⽤确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。
如果发送⽅在发送后⼀段时间之内没有收到确认帧,它通常会重新发送。ARQ包括停⽌等待ARQ协议和连续ARQ协议。
停⽌等待ARQ协议
停⽌等待协议是为了实现可靠传输的,它的基本原理就是每发完⼀个分组就停⽌发送,等待对⽅确认(回复ACK)。如果过了⼀段时间(超时时间后),还是没有收到 ACK 确认,说明没有发送成功,需要重新发送,直到收到确认后再发下⼀个分组;在停⽌等待协议中,若接收⽅收到重复分组,就丢弃该分组,但同时还要发送确认;
优点:简单缺点:信道利⽤率低,等待时间⻓
- ⽆差错情况:发送⽅发送分组,接收⽅在规定时间内收到,并且回复确认.发送⽅再次发送。
- 出现差错情况(超时重传):停⽌等待协议中超时重传是指只要超过⼀段时间仍然没有收到确认,就重传前⾯发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完⼀个分组需要设置⼀个超时计时器,其重传时间应⽐数据在分组传输的平均往返时间更⻓⼀些。这种⾃动重传⽅式常称为⾃动重传请求 ARQ。另外在停⽌等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。连续 ARQ 协议可提⾼信道利⽤率。发送维持⼀个发送窗⼝,凡位于发送窗⼝内的分组可连续发送出去,⽽不需要等待对⽅确认。接收⽅⼀般采⽤累积确认,对按序到达的最后⼀个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了。
- 确认丢失和确认迟到确认丢失:确认消息在传输过程丢失。当A发送M1消息,B收到后,B向A发送了⼀个M1确认消息,但却在传输过程中丢失。⽽A并不知道,在超时计时过后,A重传M1消息,B再次收到该消息后采取以下两点措施:1. 丢弃这个重复的M1消息,不向上层交付。 2. 向A发送确认消息。(不会认为已经发送过了,就不再发送。A能重传,就证明B的确认消息丢失)。
确认迟到:确认消息在传输过程中迟到。A发送M1消息,B收到并发送确认。在超时时间内没有收到确认消息,A重传M1消息,B仍然收到并继续发送确认消息(B收到了2份M1)。此时A收到了B第⼆次发送的确认消息。接着发送其他数据。过了⼀会,A收到了B第⼀次发送的对M1的确认消息(A也收到了2份确认消息)。
处理如下:1. A收到重复的确认后,直接丢弃。2. B收到重复的M1后,也直接丢弃重复的M1。
连续ARQ协议
连续 ARQ 协议可提⾼信道利⽤率。发送⽅维持⼀个发送窗⼝,凡位于发送窗⼝内的分组可以连续发送出去,⽽不需要等待对⽅确认。接收⽅⼀般采⽤累计确认,对按序到达的最后⼀个分组发送确认,表明到这个分组为⽌的所有分组都已经正确收到了。
优点:信道利⽤率⾼,容易实现,即使确认丢失,也不必重传。
缺点:不能向发送⽅反映出接收⽅已经正确收到的所有分组的信息。
⽐如:发送⽅发送了 5条消息,中间第三条丢失(3号),这时接收⽅只能对前两个发送确认。发送⽅⽆法知道后三个分组的下落,⽽只好把后三个全部重传⼀次。这也叫 Go-Back-N(回退 N),表示需要退回来重传已经发送过的N 个消息。
1、通过DNS查询获取主机
IP地址。通过这个IP地址找到客户端到服务器的路径。客户端浏览器在TCP链接建⽴后发送HTTP请求到IP。先通过TCP进行封装数据包,输入到网络层。2、在客户端的传输层(添加TCP头),把HTTP会话请求分成报文段,添加源和目的端口。然后使用IP层的IP地址查找目的端。
3、客户端的网络层(添加IP头)主要做的是通过查找路由表确定如何到达服务器,期间可能经过多个路由器,这些都是由路由器来完成的工作。
4、客户端的链路层(添加MAC头),包通过链路层发送到路由器,通过ARP协议给定IP地址的MAC地址,然后发送ARP请求查找目的地址,如果得到回应后就可以使用ARP的请求应答交换的IP数据包现在就可以传输了,然后发送IP数据包到达服务器的地址。
5、服务器将响应报⽂通过上述的逆过程发送回浏览器
6、浏览器接收HTTP响应,然后根据情况选择关闭TCP连接或者保留重⽤,关闭TCP连接的四次握⼿
7、浏览器检查响应状态吗:是否为1XX,3XX, 4XX, 5XX,这些情况处理与2XX不同
8、解析HTML⽂档,构件DOM树,下载资源,构造CSSOM树,执⾏js脚本. 显示⻚⾯(HTML解析过程中会逐步显示⻚⾯)
状态码是由3位数组成,第一个数字定义了响应的类别,且有五种可能取值:
1xx:指示信息–表示请求已接收,继续处理。
2xx:成功–表示请求已被成功接收、理解、接受。
3xx:重定向–要完成请求必须进行更进一步的操作。
4xx:客户端错误–请求有语法错误或请求无法实现。
5xx:服务器端错误–服务器未能实现合法的请求。
200(成功) 服务器已成功处理了请求
204(无内容) 服务器成功处理了请求,未返回任何内容
205(重置内容) 服务器成功处理了请求,未返回任何内容,重置文档视图,如清除表单内容
206(部分内容) 服务器成功处理了部分 GET 请求
300(多种选择) 服务器根据请求可执行多种操作。服务器可根据请求者 来选择一项操作,或提供操作列表供其选择
301(永久移动) 请求的网页已被永久移动到新位置。服务器返回此响应时,会自动将请求者转到新位置
302(临时移动) 服务器目前正从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。会自动将请求者转到新位置
304(未修改) 自从上次请求后,请求的网页未被修改过,不会返回网页内容
305(使用代理) 请求者只能使用指定的代理访问请求的网页
4xx:客户端错误,请求有语法错误或请求无法实现
400(错误请求) 服务器不理解请求的语法
401(身份验证错误) 此页要求授权
403(禁止) 服务器拒绝请求
404(未找到) 服务器找不到请求的网页
406(不接受) 无法使用请求的内容特性响应请求的网页
408(请求超时) 服务器等候请求时发生超时
414(请求的 URI 过长) 请求的 URI 过长,服务器无法处理
5xx:服务器端错误,无法处理请求
500(服务器内部错误) 服务器遇到错误,无法完成请求。
503(服务不可用) 目前无法使用服务器(由于超载或进行停机维护)。通常,这只是一种暂时的状态。
504(网关超时) 服务器作为网关或代理,未及时从上游服务器接收请求。
505(HTTP 版本不受支持) 服务器不支持请求中所使用的 HTTP 协议版本
端⼝:HTTP的URL由“http://”起始且默认使⽤端⼝80,⽽HTTPS的URL由“https://”起始且默认使⽤端⼝443。
安全性和资源消耗:
HTTP协议运⾏在TCP之上,所有传输的内容都是明⽂,客户端和服务器端都⽆法验证对⽅的身份。
HTTPS是运⾏在SSL/TLS之上的HTTP协议,SSL/TLS 运⾏在TCP之上。所有传输的内容都经过加密,加密采⽤对称加密,但对称加密的密钥⽤服务器⽅的证书进⾏了⾮对称加密。所以说,HTTP 安全性没有 HTTPS⾼,但是 HTTPS ⽐HTTP耗费更多服务器资源。
对称加密:密钥只有⼀个,加密解密为同⼀个密码,且加解密速度快,典型的对称加密算法有DES、AES等;
⾮对称加密:密钥成对出现(且根据公钥⽆法推知私钥,根据私钥也⽆法推知公钥),加密解密使⽤不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度慢,典型的⾮对称加密算法有RSA、DSA等。
在HTTP/1.0中默认使⽤短连接。也就是说,客户端和服务器每进⾏⼀次HTTP操作,就建⽴⼀次连接,任务结束就中断连接。当客户端浏览器访问的某个HTML或其他类型的Web⻚中包含有其他的Web资源(如JavaScript⽂件、图像⽂件、CSS⽂件等),每遇到这样⼀个Web资源,浏览器就会重新建⽴⼀个HTTP会话。
⽽从HTTP/1.1起,默认使⽤⻓连接,⽤以保持连接特性。使⽤⻓连接的HTTP协议。在使⽤⻓连接的情况下,当⼀个⽹⻚打开完成后,客户端和服务器之间⽤于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使⽤这⼀条已经建⽴的连接。
Keep-Alive不会永久保持连接,它有⼀个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现⻓连接需要客户端和服务端都⽀持⻓连接。
HTTP 是⼀种不保存状态,即⽆状态(stateless)协议。也就是说 HTTP 协议⾃身不对请求和响应之间的通信状态进⾏保存。
那么我们保存⽤户状态呢?Session 机制的存在就是为了解决这个问题,Session 的主要作⽤就是通过服务端记录⽤户的状态。
典型的场景是购物⻋,当你要添加商品到购物⻋的时候,系统不知道是哪个⽤户操作的,因为 HTTP 协议是⽆状态的。服务端给特定的⽤户创建特定的Session 之后就可以标识这个⽤户并且跟踪这个⽤户了(⼀般情况下,服务器会在⼀定时间内保存这个Session,过了时间限制,就会销毁这个Session)。
在服务端保存 Session 的⽅法很多,最常⽤的就是内存和数据库(⽐如是使⽤内存数据库redis保存)。既然 Session 存放在服务器端,那么我们如何实现 Session 跟踪呢?⼤部分情况下,我们都是通过在Cookie 中附加⼀个 Session ID 来⽅式来跟踪。
Cookie 和 Session都是⽤来跟踪浏览器⽤户身份的会话⽅式,但是两者的应⽤场景不太⼀样。
Cookie ⼀般⽤来保存⽤户信息⽐如
①我们在 Cookie 中保存已经登录过得⽤户信息,下次访问⽹站的时候⻚⾯可以⾃动帮你登录的⼀些基本信息给填了;
②⼀般的⽹站都会有保持登录也就是说下次你再访问⽹站的时候就不需要重新登录了,这是因为⽤户登录的时候我们可以存放了⼀个 Token 在 Cookie中,下次登录的时候只需要根据 Token 值来查找⽤户即可(为了安全考虑,重新登录⼀般要将 Token重写);
③登录⼀次⽹站后访问⽹站其他⻚⾯不需要重新登录。Session 的主要作⽤就是通过服务端记录⽤户的状态。
典型的场景是购物⻋,当你要添加商品到购物⻋的时候,系统不知道是哪个⽤户操作的,因为 HTTP 协议是⽆状态的。服务端给特定的⽤户创建特定的 Session 之后就可以标识这个⽤户并且跟踪这个⽤户了。
Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。 相对来说 Session 安全性更⾼。如果要在Cookie 中存储⼀些敏感信息,不要直接写⼊ Cookie 中,最好能将 Cookie 信息加密然后使⽤到的时候再去服务器端解密。
1、客户端发送请求到服务器端
2、服务器端返回证书和公开密钥,公开密钥作为证书的一部分而存在 //服务端自己有私有密钥
3、客户端验证证书和公开密钥的有效性,如果有效,则生成共享密钥并使用公开密钥加密发送到服务器端
4、服务器端使用私有密钥解密数据,并使用收到的共享密钥加密数据,发送到客户端
5、客户端使用共享密钥解密数据 //对称密钥
6、SSL加密建立…
非对称加密算法(公钥和私钥)交换对称密钥+数字证书验证身份(验证公钥是否是伪造的)+利用对称密钥加解密后续传输的数据=安全
数字证书(解决身份伪装问题)
HTTP不会对通信的双方进行进行身份的验证所以身份有可能被伪装造成安全问题,所以为了解决这个问题所以产生了数字证书,数字证书的使用流程大概如下:
1、服务器首先向一个大家都信任的第三方机构申请一个身份证书。
2、客户端向服务器建立通信之前首先向服务器请求获得服务器的证书。
3、服务器收到请求后把数字证书发送给客户端。
4、客户端获得服务器的证书之后,然后与可信任的第三方机构证书进行验证,验证通过后则进行正常的内容通信。
时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。如果一个问题的规模是n,解这一问题的某一算法所需要的时间为T(n).
空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述
常数阶O(1)
哈希表是一种非常优秀数据结构,它提供了快速的插入操作和查找操作。对哈希表进行数据的插入,查找(有时也包括删除)的时间复杂度都是O(1)。(在没有哈希冲突的情况下)
对数阶O(logn)
二分查找
线性阶O(n)
for循环
线性对数阶O(nlogn)
快速排序,归并排序
将时间复杂度为O(logn)的代码循环N遍
平方阶O(n²)
两层循环
最好、最坏、平均时间复杂度
数组与字符串
栈与队列
链表
哈希表
二叉树(红黑树)
堆
图
数组、栈、队列、链表几个数据结构的区别,以及它的特性,它们对于内存的要求,它们分别的插入的时间复杂度是多少?
hash表 二叉树 链表 查找的时间复杂度分别为?数据量很大的情况下时间复杂度为?
红黑树是一种什么样的数据结构
说一下二叉树,一般用来解决什么问题
b树和b+树的区别
B+树怎么实现的,是二叉树嘛,B+树为什么快
hashmap为什么用数组。
hashMap和concurrentHashMap的区别?
线程私有的:程序计数器、虚拟机栈、本地⽅法栈
线程共享的:堆、⽅法区、直接内存(⾮运⾏时数据区的⼀部分)
程序计数器
1、字节码解释器通过改变程序计数器来依次读取指令,从⽽实现代码的流程控制,如:顺序执⾏、选择、循环、异常处理。
2、在多线程的情况下,程序计数器⽤于记录当前线程执⾏的位置,从⽽当线程被切换回来的时候能够知道该线程上次运⾏到哪⼉了。
Java虚拟机栈
与程序计数器⼀样,Java虚拟机栈也是线程私有的,它的⽣命周期和线程相同,描述的是 Java ⽅法执⾏的内存模型,每次⽅法调⽤的数据都是通过栈传递的。
局部变量表主要存放了基础数据类型(boolean、byte、char、short、int、float、long、double)、对象引⽤
本地⽅法栈
和虚拟机栈所发挥的作⽤⾮常相似,区别是:虚拟机栈为虚拟机执⾏ Java ⽅法(也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。在 HotSpot 虚拟机中和 Java 虚拟机栈合⼆为⼀。
堆
Java 虚拟机所管理的内存中最⼤的⼀块,Java 堆是所有线程共享的⼀块内存区域,在虚拟机启动时创建。此内存区域的唯⼀⽬的就是存放对象实例,⼏乎所有的对象实例以及数组都在这⾥分配内存。
⽅法区
与 Java 堆⼀样,是各个线程共享的内存区域,它⽤于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把⽅法区描述为堆的⼀个逻辑部分,但是它却有⼀个别名叫做Non-Heap(⾮堆),⽬的应该是与 Java 堆区分开来。⽅法区也被称为永久代。
运⾏时常量池
运⾏时常量池是⽅法区的⼀部分。Class ⽂件中除了有类的版本、字段、⽅法、接⼝等描述信息外,还有常量池信息(⽤于存放编译期⽣成的各种字⾯量和符号引⽤)
直接内存
直接内存并不是虚拟机运⾏时数据区的⼀部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使⽤。⽽且也可能导致 OutOfMemoryError 异常出现。
堆空间的 eden区、s0区、s1区都属于新⽣代,tentired 区属于⽼年代。
⼤部分情况,对象都会⾸先在 Eden 区域分配,在⼀次新⽣代垃圾回收后,如果对象还存活,则会进⼊ s0 或者 s1,并且对象的年龄还会加 1(Eden区i>Survivor 区后对象的初始年龄变为1),当它的年龄增加到⼀定程度(默认为15岁),就会被晋升到⽼年代中。
另外,⼤对象和⻓期存活的对象会直接进⼊⽼年代。
任何一种垃圾回收算法一般要做2件基本的事情:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。
判断一个对象是否应该被回收,主要是看其是否还有引用。判断对象是否存在引用关系的方法包括引用计数法以及可达性分析。
引用计数法:
是一种比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只需要收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
可达性分析:(java用的这一种)
可达性分析的基本思路就是通过一系列可以做为root的对象作为起始点,从这些节点开始向下搜索。当一个对象到root节点没有任何引用链接时,则证明此对象是可以被回收的。
1、标记-清除算法
算法分为“标记”和“清除”阶段:⾸先标记出所有需要回收的对象,在标记完成后统⼀回收所有被标记的对象。它是最基础的收集算法,后续的算法都是对其不⾜进⾏改进得到。
这种垃圾收集算法会带来两个明显的问题:1. 效率问题2. 空间问题(标记清除后会产⽣⼤量不连续的碎⽚)
2、复制算法
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进⾏回收。
3、标记-整理算法
根据⽼年代的特点特出的⼀种标记算法,标记过程仍然与“标记-清除”算法⼀样,但后续步骤不是直接对可回收对象回收,⽽是让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存。(解决了空间问题?)
4、分代收集算法
当前虚拟机的垃圾收集都采⽤分代收集算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation)
⽐如在新⽣代中,每次收集都会有⼤量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。⽽⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“**标记-清除”或“标记-整理”**算法进⾏垃圾收集。
注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
\Java 堆是所有线程共享的⼀块内存区域,在虚拟机启动时创建。此内存区域的唯⼀⽬的就是存放对象实例,⼏乎所有的对象实例以及数组都在这⾥分配内存。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap).从垃圾回收的⻆度,由于现在收集器基本都采⽤分代垃圾收集算法,所以Java堆还可以细分为:新⽣代和⽼年代:再细致⼀点有:Eden空间、From Survivor、To Survivor空间等。进⼀步划分的⽬的是更好地回收内存,或者更快地分配内存
如果说收集算法是内存回收的⽅法论,那么垃圾收集器就是内存回收的具体实现。根据具体场景选择垃圾回收器。
1、Serial /Serial old 收集器
Serial(串⾏)收集器是最基本、历史最悠久的垃圾收集器了。⼤家看名字就知道这个收集器是⼀个单线程收集器了。它的“单线程”的意义不仅仅意味着它只会使⽤⼀条垃圾收集线程去完成垃圾收集⼯作,更重要的是它在进⾏垃圾收集⼯作的时候必须暂停其他所有的⼯作线程(“Stop
The World”),直到它收集结束。
新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
缺点:必须暂停其他所有的⼯作线程
优点:它简单⽽⾼效(与其他收集器的单线程相⽐)。Serial收集器由于没有线程交互的开销,⾃然可以获得很⾼的单线程收集效率。
Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。
2、ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使⽤多线程进⾏垃圾收集外,其余⾏为(控制参数、收集算法、回收策略等等)和Serial收集器完全⼀样。(也会暂停用户线程。)
新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
3、Parallel Scavenge /Parallel Old收集器
Parallel Scavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。
Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和标记-整理算法。
新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
4、CMS收集器
CMS(Concurrent Mark Sweep)收集器是⼀种以获取最短回收停顿时间为⽬标的收集器,在注重⽤户体验的应⽤上使⽤,实现了让垃圾收集线程与⽤户线程(基本上)同时⼯作。
CMS收集器是“标记-清除”算法实现的。
整个过程分为四个步骤:
初始标记:暂停所有的其他线程,并记录下直接与root相连的对象,速度很快;
并发标记:同时开启GC和⽤户线程,⽤⼀个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为⽤户线程可能会不断的更新引⽤域,所以GC线程⽆法保证可达性分析的实时性。所以这个算法⾥会跟踪记录这些发⽣引⽤更新的地⽅。
重新标记:重新标记阶段就是为了修正并发标记期间,因为⽤户程序继续运⾏⽽导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍⻓,远远⽐并发标记阶段时间短
并发清除:开启⽤户线程,同时GC线程开始对为标记的区域做清扫。
主要优点:并发收集、低停顿。
三个明显的缺点:对CPU资源敏感;⽆法处理浮动垃圾;使⽤的回收算法-“标记-清除”算法会导致收集结束时会有⼤量空间碎⽚产⽣。
5、G1收集器
G1 (Garbage-First)是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器.以极⾼概率满⾜GC停顿时间要求的同时,还具备⾼吞吐量性能特征.
并⾏与并发:G1能充分利⽤CPU、多核环境下的硬件优势,使⽤多个CPU(CPU或者CPU核⼼)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执⾏的GC动作,G1收集器仍然可以通过并发的⽅式让java程序继续执⾏。
分代收集:虽然G1可以不需要其他收集器配合就能独⽴管理整个GC堆,但是还是保留了分代的概念。
空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
可预测的停顿:这是G1相对于CMS的另⼀个⼤优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建⽴可预测的停顿时间模型,能让使⽤者明确指定在⼀个⻓度为M毫秒的时间⽚段内。
G1收集器的运作⼤致分为以下⼏个步骤:
初始标记 并发标记 最终标记 筛选回收
G1收集器在后台维护了⼀个优先列表,每次根据允许的收集时间,优先选择回收价值最⼤的Region(这也就是它的名字Garbage-First的由来)。
这种使⽤Region划分内存空间以及有优先级的区域回收⽅式,保证了GF收集器在有限时间内可以尽可能⾼的收集效率(把内存化整为零)。
⼤多数情况下,对象在新⽣代中 eden 区分配。当 eden 区没有⾜够空间进⾏分配时,虚拟机将发起⼀次Minor GC。
新⽣代GC(Minor GC):指发⽣新⽣代的的垃圾收集动作,Minor GC⾮常频繁,回收速度⼀般也⽐较快。
⽼年代GC(Major GC/Full GC):指发⽣在⽼年代的GC,出现了Major GC经常会伴随⾄少⼀次的Minor GC(并⾮绝对),Major GC的速度⼀般会⽐Minor GC的慢10倍以上。
类的加载指的是将类的**.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构**。
类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
类加载过程:加载->连接->初始化。
连接过程⼜可分为三步:验证->准备->解析。
加载:
- 通过全类名获取定义此类的⼆进制字节流
- 将字节流所代表的静态存储结构转换为⽅法区的运⾏时数据结构
- 在内存中⽣成⼀个代表该类的 Class 对象,作为⽅法区这些数据的访问⼊⼝
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。
连接阶段负责把类的二进制数据合并到JRE中
验证:确保加载的类信息符合JVM规范,无安全方面的问题。
准备:为类的静态Field分配内存,并设置初始值。
解析:将类的二进制数据中的符号引用替换成直接引用。
初始化
只有当对类的主动使用的时候才会导致类的初始化(比如 new)
该阶段主要是对静态Field进行初始化,在Java类中对静态Field指定初始值有两种方式:
应用程序都是由这三种类加载器互相配合进行加载的
1
BootstrapClassLoader(启动类加载器):最顶层的加载类,由C++实现,负责加载%JAVA_HOME%/lib⽬录下的jar包和类或者或被-Xbootclasspath参数指定的路径中的所有类。
2.
ExtensionClassLoader(扩展类加载器):主要负责加载⽬录%JRE_HOME%/lib/ext⽬录下的jar包和类,或被java.ext.dirs系统变量所指定的路径下的jar包。
3.
AppClassLoader(应⽤程序类加载器) :⾯向我们⽤户的加载器,负责加载当前应⽤classpath下的所有jar包和类。
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
类加载器之间的“⽗⼦”关系也不是通过继承来体现的,是由“优先级”来决定
好处:
双亲委派模型保证了Java程序的稳定运⾏,可以避免类的重复加载(JVM 区分不同类的⽅式不仅仅根据类名,相同的类⽂件被不同的类加载器加载产⽣的是两个不同的类),也保证了 Java 的核⼼ API 不被篡改。
如果不⽤没有使⽤双亲委派模型,⽽是每个类加载器加载⾃⼰的话就会出现⼀些问题,⽐如我们编写⼀个称为java.lang.Object类的话,那么程序运⾏的时候,系统就会出现多个不同的Object类。如果我们不想⽤双
如果不想使用双亲委派模型呢?
为了避免双亲委托机制,我们可以⾃⼰定义⼀个类加载器,然后重载loadClass()即可。
如何⾃定义类加载器? 除了BootstrapClassLoader其他类加载器均由 Java
实现且全部继承⾃java.lang.ClassLoader。如果我们要⾃定义⾃⼰的类加载器,很明显需要继承ClassLoader。
1、类加载检查:虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程。
2、分配内存:在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存⼤⼩在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从 Java 堆中划分出来。
3、初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。
4、设置对象头:初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运⾏状态的不同,如是否启⽤偏向锁等,对象头会有不同的设置⽅式。
5、执⾏ init ⽅法:在上⾯⼯作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从Java 程序的视⻆来看,对象创建才刚开始,⽅法还没有执⾏,所有的字段都还为零。所以⼀般来说,执⾏ new 指令之后会接着执⾏⽅法,把对象按照程序员的意愿进⾏初始化,这样⼀个真正可⽤的对象才算完全产⽣出来。
List
Arraylist: Object数组
Vector: Object数组
LinkedList:双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)
Map
HashMap:
JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突⽽存在的。JDK1.8以后在解决哈希冲突时有了变化,当链表⻓度⼤于阈值(默认为8)时,将链表转化为红⿊树,以减少搜索时间
LinkedHashMap:
LinkedHashMap 继承⾃HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红⿊树组成。另外,LinkedHashMap在上⾯结构的基础上,增加了⼀条双向链表,使得上⾯的结构可以保持键值对的插⼊顺序。同时通过对链表进⾏相应的操作,实现了访问顺序相关逻辑。
Hashtable:数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的
TreeMap:红⿊树(⾃平衡的排序⼆叉树)
Set
HashSet(⽆序,唯⼀):基于 HashMap 实现的,底层采⽤ HashMap 来保存元素
LinkedHashSet:
LinkedHashSet 继承于 HashSet,并且其内部是通过 LinkedHashMap 来实现的。
TreeSet(有序,唯⼀):红⿊树(⾃平衡的排序⼆树)
- 是否保证线程安全:ArrayList和LinkedList都是不同步的,也就是不保证线程安全;
- 底层数据结构:Arraylist底层使⽤的是Object数组;LinkedList底层使⽤的是双向链表数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下⾯有介绍到!)
- 插⼊和删除是否受元素位置的影响: ① ArrayList采⽤数组存储,所以插⼊和删除元素的时间复杂度受元素位置的影响。② LinkedList采⽤链表存储,所以对于add(E e)⽅法的插⼊,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插⼊和删除元素的话,时间复杂度近似为o(n)。因为需要先移动到指定位置再插⼊。
- 是否⽀持快速随机访问:LinkedList不⽀持⾼效的随机元素访问,⽽ArrayList⽀持。快速随机访问就是通过元素的序号快速获取元素对象。
- 内存空间占⽤:
ArrayList的空间浪费主要体现在在list列表的结尾会预留⼀定的容量空间,⽽LinkedList的空间花费则体现在它的每⼀个元素都需要消耗⽐ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
JDK1.8之前HashMap底层是数组和链表结合在⼀起使⽤也就是链表散列。(见哈希表说明)相⽐于之前的版本,JDK1.8之后在解决哈希冲突时有了变化,当链表⻓度⼤于阈值(默认为8)时,将链表转化为红⿊树,以减少搜索时间。
HashMap 通过 key 的hashCode 经过hash函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这⾥的 n 指的是数组的⻓度)。 如果当前位置存在元素的话,就判断该元素与要存⼊的元素的 hash值以及key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
1.对key的hashCode()做hash运算,计算index;
2.如果没碰撞直接放到bucket(数组)⾥;
3.如果碰撞了,以链表的形式存在buckets后;
4.如果碰撞导致链表过⻓(⼤于等于TREEIFY_THRESHOLD),就把链表转换成红⿊树(JDK1.8中的改动);
5.如果节点已经存在就替换old value(保证key的唯⼀性)
6.如果bucket(数组)满了(超过load factor*current capacity),就要resize
先调用Key的hashcode方法拿到对象的hash值,经过hash函数处理,然后用hash值对第一维数组的长度进行取模,得到数组的下标。
这个数组下标所在的元素就是第二维链表的表头。然后遍历这个链表,使用Key的equals同链表元素进行比较,匹配成功即返回链表元素里存放的值。
时间复杂度o(1)
请说明一下HashMap扩容的过程
当添加完元素后,如果HashMap发现size(元素总数)大于threshold(阈值load factor*current capacity),则会调用resize方法进行扩容
扩容后的table大小变为原来的两倍。扩容需要重新分配一个新数组,新数组是老数组的2倍长,然后遍历整个老结构,把所有的元素挨个重新hash分配到新结构中去。
…
Node:链表节点,包含了key、value、hash、next指针四个元素
table:Node类型的数组,里面的元素是链表,用于存放HashMap元素的实体
size:记录了放入HashMap的元素个数
loadFactor:负载因子
threshold:阈值,决定了HashMap何时扩容,以及扩容后的大小,一般等于table大小乘以loadFactor
为了能让 HashMap 存取⾼效,尽量较少碰撞,也就是要尽量把数据分配均匀。 Hash值的范围值-2147483648到2147483647,前后加起来⼤概40亿的映射空间。⼀个40亿⻓度的数组,内存是放不下的。所以这个散列值⽤之前还要先做对数组的⻓度取模运算,得到的余数才是对应的数组下标。计算⽅法是“ (n - 1) & hash ”。(n代表数组⻓度)。
这也就解释了HashMap 的⻓度为什么是2的幂次⽅。
我们⾸先可能会想到采⽤%取余的操作来实现。但是,重点来了,取余(%)操作中如果除数是2的幂次则 等价于与其除数减⼀的与(&)操作
也就是说hash%length 等价于hash&(length-1)的前提是 length 是2的 n 次⽅
并且 采⽤⼆进制位操作&,相对于%能够提⾼运算效率。这就解释了 HashMap 的⻓度 为什么是2的幂次⽅。
主要原因在于 并发下的Rehash 会造成元素之间会形成⼀个循环链表。不过, jdk 1.8 后解决了这个问 题,但是还是不建议在多线程下使⽤ HashMap,因为多线程下使⽤ HashMap 还是会存在其他问题⽐如数 据丢失。并发环境下推荐使⽤ ConcurrentHashMap 。 详情请查看:
https://coolshell.cn/articles/9606.html
都是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。
- 线程是否安全: HashMap 是⾮线程安全的,HashTable 是线程安全的;HashTable 内部的⽅法基本都经过synchronized修饰。(如果你要保证线程安全的话就使⽤ ConcurrentHashMap吧!);
- 效率:因为线程安全的问题,HashMap 要⽐ HashTable 效率⾼⼀点。另外,HashTable 基本被淘汰,不要在代码中使⽤它;
- 对Null key 和Null value的⽀持: HashMap 中,null
可以作为键,这样的键只有⼀个,可以有⼀个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有⼀个
null,直接抛出 NullPointerException。- 初始容量⼤⼩和每次扩充容量⼤⼩的不同:
①创建时如果不指定容量初始值,Hashtable 默认的初始⼤⼩为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化⼤⼩为16。之后每次扩充,容量变为原来的2倍。
②创建时如果给定了容量初始值,那么 Hashtable 会直接使⽤你给定的⼤⼩,⽽HashMap 会将其扩充为2的幂次⽅⼤⼩。- 底层数据结构: JDK1.8 以后的 HashMap
在解决哈希冲突时有了变化,当链表⻓度⼤于阈值(默认为8)时,将链表转化为红⿊树,以减少搜索时间。Hashtable 没有这样的机制。
都实现了线程安全
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的⽅式上不同。
底层数据结构: JDK1.7的 ConcurrentHashMap 底层采⽤分段的数组+链表实现,JDK1.8 采⽤的数据结构跟HashMap1.8的结构⼀样,数组+链表/红⿊⼆叉树。Hashtable 和 JDK1.8 之前的HashMap 的底层数据结构类似都是采⽤数组+链表的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的;
实现线程安全的⽅式(重要):
① ConcurrentHashMap(分段锁)
在JDK1.7的时候,⾸先将数据分为⼀段⼀段的存储,然后给每⼀段数据配⼀把锁,当⼀个线程占⽤锁访问其中⼀个段数据时,其他段的数据也能被其他线程访问。提高并发效率。
到了 JDK1.8 的时候已经摒弃了Segment的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,并发控制使⽤ synchronized 和CAS 来操作。synchronized只锁定当前链表或红⿊⼆叉树的⾸节点,这样只要hash不冲突,就不会产⽣并发,效率⼜提升N倍。(不分段,而是锁住每个Node)
② Hashtable(同⼀把锁) :使⽤ synchronized 来保证线程安全,效率⾮常低下。当⼀个线程访问同步⽅法时,其他线程也访问同步⽅法,可能会进⼊阻塞或轮询状态,如使⽤ put 添加元素,另⼀个线程不能使⽤ put 添加元素,也不能使⽤ get,竞争会越来越激烈效率越低。
1、 当你把对象加⼊HashSet时,HashSet会先计算对象的hashcode值来判断对象加⼊的位置,同时也会与其他加⼊的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。
2、但是如果发现有相同hashcode值的对象,这时会调⽤equals()⽅法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加⼊操作成功。
hashCode()是jdk根据对象的地址或者字符串或者数字计算该对象的哈希码值的方法。
equals方法用于比较两个对象是否相同,Object类中equals方法的实现是比较引用地址来判断的对象是否是同一个对象,通过覆盖该方法可以实现自定义的判断规则;
注意:在重写父类的equals()方法时,也必须重写hashcode()方法,使相等的两个对象获取的HashCode值也相等
hashCode()与equals()的相关规定:
- 如果两个对象相等,则hashcode⼀定也是相同的
- 两个对象相等,对两个equals⽅法返回true
- 两个对象有相同的hashcode值,它们也不⼀定是相等的
- 综上,equals⽅法被覆盖过,则hashCode⽅法也必须被覆盖
- hashCode()的默认⾏为是对堆上的对象产⽣独特值。如果没有重写hashCode(),则该class的两个对象⽆论如何都不会相等(即使这两个对象指向相同的数据)。
==与equals的区别
==指引⽤是否相同 ,equals()指的是值是否相同
基本数据类型的特点:直接存储在栈(stack)中的数据 • 引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
并发:一台处理器上同时处理任务,这个同是是通过划分cpu处理的时间段,交替执行多个任务的。不是真正意义上的同时。
并行:多个cpu上同时处理多个任务,一个cpu执行一个进程,另一个cpu可以执行另一个进程,两个进程互不抢占资源,可以同时进行。
并发指在一段时间内处理多个任务,并行指同一时刻,多个任务同时执行。
看另一篇。
1 线程和进程最⼤的不同在于基本上各进程是独⽴的,⽽各线程则不⼀定
2 进程是资源分配的最小单位,线程是CPU调度的最小单位
3 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
4 资源分配给进程,同一进程的所有线程共享该进程的所有资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。
5 进程上下文切换开销大,线程开销小
6 进程(process)与线程(thread)最大的区别是进程拥有自己的地址空间,某进程内的线程对于其他进程不可见,即进程A不能通过传地址的方式直接读写进程B的存储区域。进程之间的通信需要通过进程间通信(Inter-process communication,IPC)。与之相对的,同一进程的各线程间之间可以直接通过传递地址或全局变量的方式传递信息。
协程:又称为微线程,是一种用户态的轻量级线程,协程不像线程和进程那样需要系统内核的上下文切换,协程的上下文由用户决定,有自己的上下文,一个线程可以有多个协程,线程和进程是同步机制的,而协程是异步的。java原生语法没有实现协程,目前python、lua、Go支持协程。
cpu上真正运行的是线程。
优点:
1、非常快速的进行上下文切换,不用系统内核的上下文切换,减小开销。
2、单线程即可实现高并发,单核cpu可以支持上万的协程。
3、由于只有一个线程,也不存在同时写变量的冲突,在协程中控制共享资源不需要加锁。
缺点:
1、协程无法利用多核资源,本质也是一个单线程
2、协程需要进程配合才能运行在多cpu上,
3、目前java没有成熟的第三方库,存在风险。
4、调试bug存在难度,不利于发现问题。
原文链接:https://blog.csdn.net/qq_33322074/article/details/104852041
1、继承Thread
继承Thread,然后重写里面的run方法,创建实例,执行start
优点:代码编写简单,直接操作。
缺点:没有返回值,继承一个类后没法继承其他类,扩展比较差。
2、Runnable接口
自定义类实现Runnable,实现里面的run方法,创建Thread类,使用Runnable接口的实现对象,作为参数传递给Thread对象,调用start方法。
优点: 线程类可以实现多个接口,可以再继承一个类。
缺点: 没有返回值,不能直接启动,需要通过构造一个Thread实例传递进去启动。
3、Callable接口
创建Callable接口的实现类,并实现call方法。结合FutureTask类包装Callable对象,实现多线程。
优点:有返回值,扩展性也高
缺点:jdk5之后才支持,需要重写call方法,结合多个类比如:FutureTask和Thread类
4、使用线程池
自定义Runnable接口,实现run方法,创建线程池,调用执行方法传入对象。
优点:安全性能高,复用线程
缺点:jdk5才有,需要结合Runnable进行使用。
1、加锁,Synchronized或者reentrantLock
2、使用volatile声明变量,轻量级同步,不能保证原子性。
3、使用线程安全的安全类(原子类,并发容器,同步容器)ConcurrentHashMap,CopyOnWriteArrayList等。原子类(Atomicxxx)ThreadLocal本地私有变量/信号量Semaphore等。
见另一篇笔记
见另一篇笔记
见另一篇笔记
操作系统(Operating System,简称 OS)是管理计算机硬件与软件资源的程序,是计算机系统的内核与基⽯;
操作系统本质上是运⾏在计算机上的软件程序; 操作系统为⽤户提供⼀个与系统交互的操作界⾯;
操作系统分内核与外壳(我们可以把外壳理解成围绕着内核的应⽤程序,⽽内核就是能操作硬件的程序)。
内核负责管理系统的进程、内存、设备驱动程序、⽂件和⽹络系统等等,决定着系统的性能和稳定性。
进程通信:
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。
管道
- 管道/匿名管道(Pipes):⽤于具有亲缘关系的⽗⼦进程间或者兄弟进程之间的通信。
- 有名管道(Names Pipes) : 匿名管道由于没有名字,只能⽤于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道以磁盘⽂件的⽅式存在,可以实现本机任意两个进程通信
管道的通知机制类似于缓存,就像一个进程把数据放在某个缓存区域,然后等着另外一个进程去拿,并且是管道是单向传输的。这种通信方式有什么缺点呢?显然,这种通信方式效率低下,你看,a 进程给 b 进程传输数据,只能等待 b 进程取了数据之后 a 进程才能返回。
管道:管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的道端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。
管道不适合频繁通信的进程。当然,他也有它的优点,例如比较简单,能够保证我们的数据已经真的被其他进程拿走了。
https://zhuanlan.zhihu.com/p/104713463
那我们能不能把进程的数据放在某个内存之后就马上让进程返回呢?无需等待其他进程来取就返回呢?
可以用消息队列的通信模式来解决这个问题,例如 a 进程要给 b 进程发送消息,只需要把消息放在对应的消息队列里就行了,b
进程需要的时候再去对应的消息队列里取出来。这种通信方式也类似于缓存吧。 这种通信方式有缺点吗?答是有的,如果 a
进程发送的数据占的内存比较大,并且两个进程之间的通信特别频繁的话,消息队列模型就不大适合了。因为 a
发送的数据很大的话,意味发送消息(拷贝)这个过程需要花很多时间来读内存。
管道和消息队列的通信数据都是先进先出的原则。
与管道(⽆名管道:只存在于内存中的⽂件;命名管道:存在于实际的磁盘介质或者⽂件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除⼀个消息队列时,该消息队列才会被真正的删除。
消息队列可以实现消息的随机查询,消息不⼀定要以先进先出的次序读取,也可以按消息的类型读取.⽐ FIFO 更有优势。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
信号(Signal):信号是⼀种比较复杂的通信⽅式,⽤于通知接收进程某个事件已经发⽣;
消息队列(Message Queuing):消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
信号量(Semaphores):信号量是⼀个计数器,⽤于多进程对共享数据的访问,信号量的意图在于进程间同步。它常作为一种锁机制。
共享内存(Shared memory):使得多个进程可以访问同⼀块内存空间,不同进程可以及时看到对⽅进程中对共享内存中数据的更新。这种⽅式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有⽤的进程间通信⽅式。
套接字(Sockets) : 此⽅法主要⽤于在客户端和服务器之间通过⽹络进⾏通信。套接字是⽀持TCP/IP 的⽹络通信的基本操作单元,可以看做是不同主机之间的进程进⾏双向通信的端点,简单的说就是通信的两⽅的⼀种约定,⽤套接字中的相关函数来完成通信过程。
- 互斥量(Mutex):采⽤互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有⼀个,所以可以保证公共资源不会被多个线程同时访问。⽐如 Java 中的synchronized 关键词和各种 Lock 都是这种机制。
- 信号量(Semphares):它允许同⼀时刻多个线程访问同⼀资源,但是需要控制同⼀时刻访问此资源的最⼤线程数量
- 事件(Event) :Wait/Notify:通过通知操作的⽅式来保持多线程同步,还可以⽅便的实现多线程优先级的⽐较。事件是内核对象,事件分为手动置位事件和自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发
如果多线程的程序运行结果是可预期的,而且与单线程的程序运行结果一样,那么说明是“线程安全”的。
为了确定⾸先执⾏哪个进程以及最后执⾏哪个进程以实现最⼤ CPU 利⽤率,计算机科学家已经定义了⼀些算法,它们是:
先到先服务(FCFS)调度算法 : 从就绪队列中选择⼀个最先进⼊该队列的进程为之分配资源,使它⽴即执⾏,并⼀直执⾏到完成或发⽣某事件⽽被阻塞放弃占⽤ CPU 时再重新调度。
短作业优先(SJF)的调度算法 : 从就绪队列中选出⼀个估计运⾏时间最短的进程为之分配资源,使它⽴即执⾏并⼀直执⾏到完成或发⽣某事件⽽被阻塞放弃占⽤ CPU 时再重新调度。
时间⽚轮转调度算法 : 时间⽚轮转调度是⼀种最古⽼,最简单,最公平且使⽤最⼴的算法,⼜称 RR(Round robin)调度。每个进程被分配⼀个时间段,称作它的时间⽚,即该进程允许运⾏的时间。
优先级调度 : 为每个流程分配优先级,⾸先执⾏具有最⾼优先级的进程,依此类推。具有相同优先级的进程以 FCFS ⽅式执⾏。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。
多级反馈队列调度算法 :多级反馈队列调度算法既能使⾼优先级的作业得到响应⼜能使**短作业(**进程)迅速完成,因⽽它是⽬前被公认的⼀种较好的进程调度算法,UNIX 操作系统采取的便是这种调度算法。
简单分为连续分配管理⽅式和⾮连续分配管理⽅式这两种。连续分配管理⽅式是指为⼀个⽤户程序分配⼀个连续的内存空间,常⻅的如 块式管理 。同样地,⾮连续分配管理⽅式允许⼀个程序使⽤的内存分布在离散或者说不相邻的内存中,常⻅的如⻚式管理 和 段式管理。
- 块式管理 : 远古时代的计算机操系统的内存管理⽅式。将内存分为⼏个固定⼤⼩的块,每个块中只包含⼀个进程。如果程序运⾏需要内存的话,操作系统就分配给它⼀块,如果程序运⾏只需要很⼩的空间的话,分配的这块内存很⼤⼀部分⼏乎被浪费了。这些在每个块中未被利⽤的空间,我们称之为碎⽚。
- ⻚式管理 :把主存分为⼤⼩相等且固定的⼀⻚⼀⻚的形式,⻚较⼩,地址非连续,相对相⽐于块式管理的划分⼒度更⼤,提⾼了内存利⽤率,减少了碎⽚。⻚式管理通过⻚表对应逻辑地址和物理地址。
- 段式管理 : ⻚式管理虽然提⾼了内存利⽤率,但是⻚式管理其中的⻚实际并⽆任何实际意义。段式管理把主存分为⼀段段的,每⼀段的空间⼜要⽐⼀⻚的空间⼩很多 。但是,最重要的是段是有实际意义的,每个段定义了⼀组逻辑信息,例如,有主程序段 MAIN、⼦程序段 X、数据段 D及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址。
- 段⻚式管理机制 :段⻚式管理机制结合了段式管理和⻚式管理的优点。简单来说段⻚式管理机制就是把主存先分成若⼲段,每个段⼜分成若⼲⻚,
也就是说 段⻚式管理机制 中段与段之间以及段的内部的都是离散的。
共同点 :
- 分⻚机制和分段机制都是为了提⾼内存利⽤率,较少内存碎⽚。
- 和段都是离散存储的,所以两者都是离散分配内存的⽅式。但是,每个⻚和段中的内存是连续的。
区别 :
- ⻚的⼤⼩是固定的,由操作系统决定;⽽段的⼤⼩不固定,取决于我们当前运⾏的程序。
- 分⻚仅仅是为了满⾜操作系统内存管理的需求,⽽段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满⾜⽤户的需要。
所谓的逻辑地址,是指计算机用户(例如程序开发者),看到的地址,由操作系统决定。例如,当创建一个长度为100的整型数组时,操作系统返回一个逻辑上的连续空间:指针指向数组第一个元素的内存地址。由于整型元素的大小为4个字节,故第二个元素的地址时起始地址加4,以此类推。
事实上,逻辑地址并不一定是元素存储的真实地址,即数组元素的物理地址****(在内存条中所处的位置),并非是连续的,只是操作系统通过地址映射,将逻辑地址映射成连续的,这样更符合人们的直观思维。
⻚表管理机制中有两个很重要的概念:快表和多级⻚表,这两个东⻄分别解决了⻚表管理中很重要的两个问题。
在分⻚内存管理中,很重要的两点是:1. 虚拟地址到物理地址的转换要快。2. 解决虚拟地址空间⼤,⻚表也会很⼤的问题。
为了提⾼内存的空间性能,提出了多级⻚表的概念;但是提到空间性能是以浪费时间性能为基础的,因此为了补充损失的时间性能,提出了快表的概念。 不论是快表还是多级⻚表实际上都利⽤到了程序的局部性原理,局部性原理在后⾯的虚拟内存这部分会介绍到。
快表
加速虚拟地址到物理地址的转换速度。我们可以把块表理解为⼀种特殊的⾼速缓冲存储器(Cache),其中的内容是⻚表的⼀部分或者全部内容。作为⻚表的 Cache,它的作⽤与⻚表相似,但是提⾼了访问速率。由于采⽤
⻚表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问⼀次⾼速缓冲存储器,⼀次主存,这样可加速查找并提⾼指令执⾏速度。
使⽤快表之后的地址转换流程是这样的:
- 根据虚拟地址中的⻚号查快表;
- 如果该⻚在快表中,直接从快表中读取相应的物理地址;
- 如果该⻚不在快表中,就访问内存中的⻚表,再从⻚表中得到物理地址,同时将⻚表中的该映射表项添加到快表中;
- 当快表填满后,⼜要登记新⻚时,就按照⼀定的淘汰策略淘汰掉快表中的⼀个⻚。
多级⻚表
引⼊多级⻚表的主要⽬的是为了避免把全部⻚表⼀直放在内存中占⽤过多空间,特别是那些根本就不需要的⻚表就不需要保留在内存中。多级⻚表属于时间换空间的典型场景
具体可以查看下⾯这篇⽂章:
多级⻚表如何节约内存: https://www.polarxiong.com/archives/多级⻚表如何节约内存.html
虚拟内存 使得应⽤程序认为它拥有连续的可⽤的内存(⼀个连续完整的地址空间),⽽实际上,它通常是被分隔成多个物理内存碎⽚,还有部分暂时存储在外部磁盘存储器上,在需要时进⾏数据交换。与没有使⽤虚拟内存技术的系统相⽐,使⽤这种技术的系统使得⼤型程序的编写变得更容易,对真正的物理内存(例如 RAM)的使⽤也更有效率。
通过虚拟内存可以让程序可以拥有超过系统物理内存⼤⼩的可⽤内存空间。另外,虚拟内存让每个进程好像拥有⼀⽚连续完整的内存空间。这样会更加有效地管理内存并减少出错。
局部性原理是虚拟内存技术的基础,正是因为程序运⾏具有局部性原理,才可以只装⼊部分程序到内存就开始运⾏。
局部性原理是说在某个较短的时间段内,程序执⾏局限于某⼀⼩部分,程序访问的存储空间也局限于某个区域。
时间局部性 :
如果程序中的某条指令⼀旦执⾏,不久以后该指令可能再次执⾏;如果某数据被访问过,不久以后该数据可能再次被访问。产⽣时间局部性的典型原因,是由于在程序中存在着⼤量的循环操作。
空间局部性 :
⼀旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即
程序在⼀段时间内所访问的地址,可能集中在⼀定的范围之内,这是因为指令通常是顺序存放、顺序执⾏的,数据也⼀般是以向量、数组、表等形式簇聚存储的。
实现:
时间局部性是通过将近来使⽤的指令和数据保存到⾼速缓存存储器中,并使⽤⾼速缓存的层次结构实现。空间局部性通常是使⽤较⼤的⾼速缓存,并将预取机制集成到⾼速缓存控制逻辑中实现。
虚拟内存技术实际上就是建⽴了 “内存⼀外存”的两级存储器的结构,利⽤局部性原理实现髙速缓存。
基于局部性原理,在程序装⼊时,可以将程序的⼀部分装⼊内存,⽽将其他部分留在外存,就可以启动程序执⾏。由于外存往往⽐内存⼤很多,所以我们运⾏的软件的内存⼤⼩实际上是可以⽐计算机系统实际的内存⼤⼩⼤的。
在程序执⾏过程中,当所访问的信息不在内存时,由操作系统将所需要的部分调⼊内存,然后继续执⾏程序。另⼀⽅⾯,操作系统将内存中暂时不使⽤的内容换到外存上,从⽽腾出空间存放将要调⼊内存的信息。
这样,计算机好像为⽤户提供了⼀个⽐实际内存⼤的多的存储器——虚拟存储器。
虚拟内存的实现需要建⽴在离散分配的内存管理⽅式的基础上。 虚拟内存的实现有以下三种⽅式:
请求分⻚存储管理 :
建⽴在分⻚管理之上,为了⽀持虚拟存储器功能⽽增加了请求调⻚功能和⻚⾯置换功能。请求分⻚是⽬前最常⽤的⼀种实现虚拟存储器的⽅法。
请求分⻚存储管理系统中,在作业开始运⾏之前,仅装⼊当前要执⾏的部分段即可运⾏。假如在作业运⾏的过程中发现要访问的⻚⾯不在内存,则由处理器通知操作系统按照对应的⻚⾯置换算法将相应的⻚⾯调⼊到主存,同时操作系统也可以将暂时不⽤的⻚⾯置换到外存中。
请求分段存储管理 :
建⽴在分段存储管理之上,增加了请求调段功能、分段置换功能。
请求分段储存管理⽅式就如同请求分⻚储存管理⽅式⼀样,在作业开始运⾏之前,仅装⼊当前要执⾏的部分段即可运⾏;在执⾏过程中,可使⽤请求调⼊中断动态装⼊要访问但⼜不在内存的程序段;当内存空间已满,⽽⼜需要装⼊新的段时,根据置换功能适当调出某个段,以便腾出空间⽽装⼊新的段。
请求段⻚式存储管理
不管是上⾯那种实现⽅式,我们⼀般都需要:
- ⼀定容量的内存和外存:在载⼊程序的时候,只需要将程序的⼀部分装⼊内存,⽽将其他部分留在外存,然后程序就可以执⾏了;
- 缺⻚中断:如果需执⾏的指令或访问的数据尚未在内存(称为缺⻚或缺段),则由处理器通知操作系统将相应的⻚⾯或段调⼊到内存,然后继续执⾏程序;
- 虚拟地址空间 :逻辑地址到物理地址的变换。
地址映射过程中,若在⻚⾯中发现所要访问的⻚⾯不在内存中,则发⽣缺⻚中断 。缺⻚中断 就是要访问的⻚不在主存,需要操作系统将其调⼊主存后再进⾏访问。 在这个时候,被内存映射的⽂件实际上成了⼀个分⻚交换⽂件。
当发⽣缺⻚中断时,如果当前内存中并没有空闲的⻚⾯,操作系统就必须在内存选择⼀个⻚⾯将其移出内存,以便为即将调⼊的⻚⾯让出空间。⽤来选择淘汰哪⼀⻚的规则叫做⻚⾯置换算法。
OPT ⻚⾯置换算法(最佳⻚⾯置换算法)
所选择的被淘汰⻚⾯将是以后永不使⽤的,或者是在最⻓时间内不再被访问的⻚⾯,这样可以保证获得最低的缺⻚率。但由于⼈们⽬前⽆法预知进程在内存下的若千⻚⾯中哪个是未来最⻓时间内不再被访问的,因⽽该算法⽆法实现。⼀般作为衡量其他置换算法的⽅法。
FIFO(First In First Out) ⻚⾯置换算法(先进先出⻚⾯置换算法)
总是淘汰最先进⼊内存的⻚⾯,即选择在内存中驻留时间最久的⻚⾯进⾏淘汰。
LRU (Least Currently Used)⻚⾯置换算法(最近最久未使⽤⻚⾯置换算法)
LRU算法赋予每个⻚⾯⼀个访问字段,⽤来记录⼀个⻚⾯⾃上次被访问以来所经历的时间 T,当须淘汰⼀个⻚⾯时,选择现有⻚⾯中其 T 值最⼤的,即最近最久未使⽤的⻚⾯予以淘汰。
LFU (Least Frequently Used)⻚⾯置换算法(最少使⽤⻚⾯置换算法) : 该置换算法选择在之前时期使⽤最少的⻚⾯作为淘汰⻚。
对于单核单线程CPU而言,在某一时刻只能执行一条CPU指令。
上下文切换(Context Switch)是一种将CPU资源从一个进程分配给另一个进程的机制。从用户角度看,计算机能够并行运行多个进程,这恰恰是操作系统通过快速上下文切换造成的结果。
在切换的过程中,操作系统需要先存储当前进程的状态(包括内存空间的指针,当前执行完的指令等等),再读入下一个进程的状态,然后执行此进程。
7.死锁的条件?以及如何处理死锁问题?
定义:如果一组进程中的每一个进程都在等待仅由该组进程中的其他进程才能引发的事件,那么该组进程就是死锁的。或者在两个或多个并发进程中,如果每个进程持有某种资源而又都等待别的进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗地讲,就是两个或多个进程被无限期地阻塞、相互等待的一种状态。
…
产生死锁的必要条件:
互斥条件(Mutual exclusion):资源不能被共享,只能由一个进程使用。
请求与保持条件(Hold and wait):已经得到资源的进程可以再次申请新的资源。
非抢占条件(No pre-emption):已经分配的资源不能从相应的进程中被强制地剥夺。
循环等待条件(Circular wait):系统中若干进程组成环路,该环路中每个进程都在等待相邻进程正占用的资源。
…
如何处理死锁问题:
忽略该问题。例如鸵鸟算法,该算法可以应用在极少发生死锁的的情况下。为什么叫鸵鸟算法呢,因为传说中鸵鸟看到危险就把头埋在地底下,可能鸵鸟觉得看不到危险也就没危险了吧。跟掩耳盗铃有点像。
检测死锁并且恢复。
仔细地对资源进行动态分配,使系统始终处于安全状态以避免死锁。
通过破除死锁四个必要条件之一,来防止死锁产生。
用户空间就是用户进程所在的内存区域,相对的,系统空间就是操作系统占据的内存区域。用户进程和系统进程的所有数据都在内存中。处于用户态的程序只能访问用户空间,而处于内核态的程序可以访问用户空间和内核空间。
…
用户态切换到内核态的方式如下:
系统调用:程序的执行一般是在用户态下执行的,但当程序需要使用操作系统提供的服务时,比如说打开某一设备、创建文件、读写文件(这些均属于系统调用)等,就需要向操作系统发出调用服务的请求,这就是系统调用。
异常:当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
外围设备的中断:当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
权限不一样。
用户态的进程能存取它们自己的指令和数据,但不能存取内核指令和数据(或其他进程的指令和数据)。
核心态下的进程能够存取内核和用户地址某些机器指令是特权指令,在用户态下执行特权指令会引起错误。在系统中内核并不是作为一个与用户进程平行的估计的进程的集合。
注意函数:Character.isLetterOrDigit(char)
用于验证字符是否为字母和数字
也可以用优先队列哦
两数和问题,无序数组用hashmap
排好序的数组可以用双指针
public static void swap(int[] nums) {
int x = 0;
boolean x_flag = false;
int y = 0;
for (int i = 0; i < nums.length-1; i++) {
if (!x_flag && nums[i+1] < nums[i]) {
x = i; //递减的第一个
x_flag = true;
System.out.println(x);
}
if (nums[i+1] < nums[i ]) {
y = i+1; //递减的最后一个 ,会覆盖
System.out.println(y);
}
}
int temp = nums[x];
nums[x] = nums[y];
nums[y] = temp;
}
实现一个单例模式,要求懒加载
PayPal???让我写了几个算法题,感觉写的还不错。主要是一些背包问题和回溯算法的剪枝。
一个大的英文文本,找到其中出现次数最高的10个单词
https://www.cnblogs.com/orchid/archive/2012/04/22/2464511.html
java中位数_从海量数据中找出中位数
https://blog.csdn.net/weixin_32657693/article/details/114234040
两个字符串,按照规则判断相等(重写equals),规则是两个字符串相同字符出现的次数相同,遍判定相等。例(AAB 和 ABA 相等)。
先想了用两个个数组存字符出现次数,然后遍历比较。
面试官想了一下,不要用数组存,时间复杂度允许高一点
两个字符串先用toCharArray(),然后用Arrays.sort(),时间复杂度o(nlogn)
Zz ↩︎