基础篇
概述TCP/IP网络模型
TCP/IP网络模型自顶向下一共分为4层。
-
应用层
直接为用户提供服务,该层常见的协议比如HTTP协议、DNS协议、FTP协议。
- 我们手机或电脑的应用软件就都是在应用层实现的,当两个不同的应用需要通信时,应用就把应用数据传给下一层,也就是传输层。应用层专注为用户提供应用功能,而不关心数据如何传输,比如支持Web应用的HTTP协议、DNS协议、支持文件传输的FTP协议等。
OSI七层网络体系中,将应用层继续划分
- 应用层:提供系统与用户的接口。比如文件传输、访问Web应用。涉及到FTP协议,HTTP协议。
- 表示层:将数据从主机持有的格式转换为网络持有的标准传输格式。
- 会话层:管理主机间会话进程。包括建立、管理、终止进程间的会话。
-
传输层
传输层为应用层提供网络支持,主要使用TCP和UDP两个协议。比如TCP协议可以实现可靠传输,进行流量控制、超时重传、拥塞控制等。
-
TCP 的全称叫传输控制协议(Transmission Control Protocol),大部分应用使用的正是 TCP 传输层协议,比如 HTTP 应用层协议。TCP 相比 UDP 多了很多特性,比如流量控制、超时重传、拥塞控制等,这些都是为了保证数据包能可靠地传输给对方。
UDP 相对来说就很简单,简单到只负责发送数据包,不保证数据包是否能抵达对方,但它实时性相对更好,传输效率也高。
-
当设备作为接收方时,传输层则要负责把数据包传给应用,但是一台设备上可能会有很多应用在接收或者传输数据,因此需要用一个编号将应用区分开来,这个编号就是端口。
-
-
网络层
负责将应用数据从一个设备传输到另一个设备,使用IP协议,让数据包通过路由器找到目的主机。IP地址分为网络号和主机号。IP可以通过寻址,告诉我们下一个方向应该怎么走,路由可以根据下一个目的地选择路径。
- 我们不希望传输层协议处理太多的事情,只需要服务好应用即可,让其作为应用间数据传输的媒介,帮助实现应用到应用的通信,而实际的传输功能就交给下一层,也就是网络层(Internet Layer)。
-
网络接口层
网络接口层为网络层提供链路级别的传输服务,负责在以太网、WIFI这样的底层网络上传输数据。比如会使用MAC地址来标识网络上的设备。
- 以太网就是一种在「局域网」内,把附近的设备连接起来,使它们之间可以进行通讯的技术。
OSI七层网络体系中,将网络接口层划分为物理层和数据链路层
-
物理层:传输比特流,为设备提供传输的数据通路。
-
数据链路层:将IP数据报封装成帧。(帧中添加MAC地址) 在相邻节点的链路中传输数据。
网络接口层的传输单位是帧(frame),IP 层的传输单位是包(packet),TCP 层的传输单位是段(segment),HTTP 的传输单位则是消息或报文(message)。但这些名词并没有什么本质的区分,可以统称为数据包。
键入网址到网页显示,期间发生了什么?
首先,浏览器通过解析URL生成HTTP请求,确定Web服务器和应用名。
之后使用DNS查询服务器的域名,获取到IP地址。
- 如果浏览器本地有缓存,就直接返回,或者配置的hosts文件,如果都没有,就去访问本地DNS服务器。
随后就将传输工作交给协议栈,应用程序可以通过调用Socket库,委托给操作系统中的协议栈工作。
协议栈的上半部分有两块,分别是负责收发数据的 TCP 和 UDP 协议,这两个传输协议会接受应用层的委托,执行收发数据的操作。
- 如果使用的是TCP协议,TCP会通过三次握手建立连接,确保双方都有发送和接收的能力。(TCP头部中包含源端口号和目标端口号,指明发送给哪个应用,还有窗口大小,目的是进行流量控制。)HTTP报文+TCP头部组成TCP报文。
协议栈的下面一半是用 IP 协议控制网络包收发操作,在互联网上传数据时,数据会被切分成一块块的网络包,而将网络包发送给对方的操作就是由 IP 负责的。
- IP协议:将报文封装成IP数据包。(IP头部中有源IP地址和目的IP地址,这样就可以确定路由)HTTP报文+TCP头部+IP头部组成数据包。
- IP协议中包括ICMP协议和ARP协议
ICMP
用于告知网络包传送过程中产生的错误以及各种控制信息。ARP
用于根据 IP 地址查询相应的以太网 MAC 地址。
- IP协议中包括ICMP协议和ARP协议
生成IP头部之后,还要加上MAC头部,MAC头部是以太网使用的头部,包含了接收方和发送方的MAC地址等信息。
-
MAC包头包含接收方MAC地址、发送方MAC地址、协议类型。
为了获得发送方MAC地址,需要ARP协议,ARP协议会在以太网中以广播的形式,对所有的设备发送请求,请求对应IP地址的MAC地址。随后将本次查询到的结果放到ARP缓存的内存空间。
在后续操作中,会先查询ARP缓存,如果已经保存了对方的MAC地址,就不需要发送ARP查询,直接使用ARP缓存中的地址。
网卡:网卡可以将包转为电信号,通过网线发送出去。
- 网卡驱动程序获取网络包之后,会复制到网卡内的缓冲区,在开头加上报头和起始帧分界符,在末尾加上FCS(检测错误的帧校验序列)。
交换机:交换机工作在MAC层,也称为二层网络设备。电信号到达网线接口,将电信号转为数字信号,通过FCS校验错误。如果没问题,就放到缓冲区中。随后根据交换机的MAC地址表查找MAC地址对应的端口,然后将信号发送到对应的端口。
路由器:路由器和交换机不同,路由器是基于IP设计的,俗称三层网络设备,路由器的各个端口都有MAC地址。当转发包时,会检查接收方MAC地址,检查是不是发给自己的包,之后根据IP头部中的内容,查询路由表来判断转发的目标。
- 完成包接收操作之后,路由器就会去掉包开头的MAC头部,因为MAC头部的作用就是将包送达路由器。
服务器与客户端:通过一系列取消头部的操作,确认该数据包就是发送给自己的,之后向客户端发送响应报文。最后,客户端发起TCP四次挥手,双方断开连接。
Java并发常见面试题(中)
volatile关键字
-
如何保证变量的可见性
如果一个变量使用volatile关键字,那么这个线程本地内存中的变量就会强制刷新到主内存中。每次读取时,都应该到主内存中进行读取变量。
volatile关键字保证了数据的可见性,但是不保证数据的原子性
- Java的原子性即:不允许多线程操作,具有原子性的量,同一时刻只能有一个线程来进行操作。
-
如何禁止指令重排序
volatile除了保证变量的可见性,还可以防止JVM的指令重排序。如果将变量声明为volatile,对变量进行读写时,会通过特定的内存屏障的方式来禁止指令重排序。
-
volatile关键字使用—单例模式—双重校验锁(double-checking-locking)
单例模式:保证一个类只有一个实例。
使用双锁机制,安全且在多线程情况下能保持高性能。
public class Singleton { private volatile static Singleton singleton; private Singleton (){ } public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
为什么两次使用if ( singleton == null) ?
- 第一次校验:单例模式只需要创建一次实例,如果后面再调用getSingleton 方法,直接返回实例对象即可。
- 第二次校验:在syncronized内部进行校验,防止多个线程创建多个实例。
需要加入volatile关键字,由于JVM存在指令重排优化,在多线程下,可能一个线程还没有调用构造方法,但此时可能已经为对象分配了内存空间并将字段设置为默认值了。如果再调用Signleton方法,可能获取到不正确的对象。
-
volatile可以保证原子性吗?
inc ++ 就是一个复合操作,如果变量不能保证原子性,那么多个线程操作inc时,inc可能不会是你想得到的值。
- 读取 inc 的值、对 inc 加 1、将 inc 的值写回内存。
可以使用synchronized、AtomicInteger、ReentrantLock进行改进。
乐观锁和悲观锁
-
什么是悲观锁,使用场景是什么?
悲观锁总是假设最坏的场景,也就是共享资源访问时每次都会出现问题,所以每次在获取资源时,都对该资源加锁,其他线程想要访问到该资源,那么就要阻塞直到上一个锁被释放。
像 Java 中
synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。悲观锁通常多用于写多比较多的情况下(多写场景),避免频繁失败和重试影响性能。
-
什么是乐观锁,使用场景是什么?
乐观锁总是假设最好的情况,也就是共享资源访问时每次都不会出现问题,线程无需加锁和等待,只是在提交修改时去验证对应的资源是否被其他线程修改。
乐观锁通常多于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。
-
如何实现乐观锁?
乐观锁一般使用版本号机制或CAS算法来实现,使用CAS算法相对多一些。
-
版本号机制
在数据表中加一个字段version,表示数据被修改的次数。数据被修改时,version值会加一,线程读取新数据值时,也会读取version。提交更新时,读到的version和数据库中的version一致,才会进行更新操作。
-
CAS算法
CAS全称是:Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值(Expected)和要更新的变量值(Var)进行比较,两值相等才会进行更新。CAS是一条原子操作,依赖于一条CPU原子指令。
当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N (拟写入的新值)来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。
-
-
乐观锁的ABA问题
如果变量V在读取的时候是A值,准备赋值的时候还是A值,那么CAS就认为它的值没有被其他线程修改过。
ABA问题的解决思路是在变量前加上版本号或者时间戳。
CAS的循环时间长,开销大:
CAS经常会用到自旋锁来进行重试,也就是不成功就会一直循环直到成功。使用PAUSE指令(暂时终止程序的运行)可以提升一定的效率。
- 可以延迟流水线执行指令,使CPU不会消耗更多资源,延迟的实现取决于实现的版本。
- 可以避免在退出循环的时候,因为内存顺序冲突引起CPU流水线被清空。
- 内存顺序冲突——当自旋锁快要释放的时候,持锁线程会有一个store命令(寄存器中的数据存入内存),外面自旋的线程会发出各自的load命令(内存中的数据装载到寄存器),而此处并没任何 happen-before 排序,所以处理器是乱序执行。
- PAUSE给处理器提了个醒:这段代码序列是个循环等待。处理器利用这个提示可以避免在大多数情况下的内存顺序违规,这将大幅提升性能。
只能保证一个共享变量的原子操作:
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了
AtomicReference
类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference
类把多个共享变量合并成一个共享变量来操作。
synchronized关键字
-
synchronized是什么?有什么用?
synchronized
是 Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。Java早期版本,synchronized属于重量级锁。在 Java 6 之后,
synchronized
引入了大量的优化如自旋锁、适应性自旋锁、轻量级锁等技术来减少锁操作的开销,这些优化让synchronized
锁的效率提升了很多。因此,synchronized
还是可以在实际项目中使用的,像 JDK 源码、很多开源框架都大量使用了synchronized
。 -
如何使用synchronized?
-
修饰实例方法
给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 。
-
修饰静态方法
给当前类加锁,会作用于类的所有对象实例。进入同步代码前要获得 当前 class 的锁。
- 因为静态方法不属于任何一个实例对象,属于整个类。
-
修饰代码块
synchronized(object)
表示进入同步代码库前要获得 给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 给定 Class 的锁
-
-
构造方法可以用synchronized修饰吗?
**构造方法不能使用 synchronized 关键字修饰。**构造方法本身就属于线程安全的,不存在同步的构造方法一说。
-
synchronized底层原理
synchronized修饰语句块
synchronized
同步语句块的实现使用的是monitorenter
和monitorexit
指令,其中monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。当执行
monitorenter
指令时,线程试图获取锁也就是获取 对象监视器monitor
的持有权。-
在执行
monitorenter
时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。对象锁的的拥有者线程才可以执行
monitorexit
指令来释放锁。在执行monitorexit
指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。
synchronized修饰方法
synchronized
修饰的方法并没有monitorenter
指令和monitorexit
指令,取得代之的确实是ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。JVM 通过该ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。不过两者的本质都是对对象监视器 monitor 的获取。
-
-
JDK1.6 之后的 synchronized 底层做了哪些优化?
JDK1.6 对锁的实现引入了大量的优化,引入了很多锁来提高获取锁的效率。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
-
synchronized和volatile有什么区别?
synchronized
关键字和volatile
关键字是两个互补的存在,而不是对立的存在!volatile
关键字是线程同步的轻量级实现,所以volatile
性能肯定比synchronized
关键字要好 。但是volatile
关键字只能用于变量而synchronized
关键字可以修饰方法以及代码块 。volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。volatile
关键字主要用于解决变量在多个线程之间的可见性,而synchronized
关键字解决的是多个线程之间访问资源的同步性。
ReentrantLock
-
ReentrantLock是什么?
ReentrantLock
实现了Lock
接口,是一个可重入且独占式的锁,和synchronized
关键字类似。不过,ReentrantLock
更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。ReentrantLock
里面有一个内部类Sync
,Sync
继承 AQS(AbstractQueuedSynchronizer
),添加锁和释放锁的大部分操作实际上都是在Sync
中实现的。Sync
有公平锁FairSync
和非公平锁NonfairSync
两个子类。 -
公平锁和非公平锁有什么区别?
公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
非公平锁 :锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。
-
synchronized和ReentrantLock有什么区别?
-
两者都是可重入锁。可重入锁 也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。
JDK 提供的所有现成的
Lock
实现类,包括synchronized
关键字锁都是可重入的。 -
synchronized依赖于JVM 而 ReentrantLock 依赖于API
synchronized
是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为synchronized
关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock
是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 -
ReentrantLock比synchronized 增加了一些高级功能
相比
synchronized
,ReentrantLock
增加了一些高级功能。主要来说主要有三点:- 等待可中断 :
ReentrantLock
提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()
来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 - 可实现公平锁 :
ReentrantLock
可以指定是公平锁还是非公平锁。而synchronized
只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock
默认情况是非公平的,可以通过ReentrantLock
类的ReentrantLock(boolean fair)
构造方法来制定是否是公平的。 - 可实现选择性通知(锁可以绑定多个条件):
synchronized
关键字与wait()
和notify()
/notifyAll()
方法相结合可以实现等待/通知机制。ReentrantLock
类当然也可以实现,但是需要借助于Condition
接口与newCondition()
方法。
如果你想使用上述功能,那么选择
ReentrantLock
是一个不错的选择。 - 等待可中断 :
-
-
可中断锁和不可中断锁有什么区别?
可中断锁 :获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。
ReentrantLock
就属于是可中断锁。不可中断锁 :一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。
synchronized
就属于是不可中断锁。
ReentrantReadWriteLock(了解)
ReentrantReadWriteLock
实现了 ReadWriteLock
,是一个可重入的读写锁,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。
ReentrantReadWriteLock
其实是两把锁,一把是 WriteLock
(写锁),一把是 ReadLock
(读锁) 。读锁是共享锁,写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。
-
ReentrantReadWriteLock适合什么情况?
由于
ReentrantReadWriteLock
既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。因此,在读多写少的情况下,使用ReentrantReadWriteLock
能够明显提升系统性能。 -
共享锁和独占锁的区别?
- 共享锁 :一把锁可以被多个线程同时获得。
- 独占锁 :一把锁只能被一个线程获得。
-
线程持有读锁,还能获取写锁吗?
在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
-
读锁为什么不能升级为写锁?
写锁可以降级为读锁,但是读锁却不能升级为写锁。这是因为读锁升级为写锁会引起线程的争夺,毕竟写锁属于是独占锁,这样的话,会影响性能。
另外,还可能会有死锁问题发生。举个例子:假设两个线程的读锁都想升级写锁,则需要对方都释放自己锁,而双方都不释放,就会产生死锁。
StampedLock
相比于传统读写锁多出来的乐观读是StampedLock
比 ReadWriteLock
性能更好的关键原因。StampedLock
的乐观读允许一个写线程获取写锁,所以不会导致所有写线程阻塞,也就是当读多写少的时候,写线程有机会获取写锁,减少了线程饥饿的问题,吞吐量大大提高。
和 ReentrantReadWriteLock
一样,StampedLock
同样适合读多写少的业务场景,可以作为 ReentrantReadWriteLock
的替代品,性能更好。