2020-10-24:go中channel的recv多次调用流程是什么

对于基本类型和引用类型 == 的作用效果是不同的如下所示:

基本类型:比较的是值是否相同;
引用类型:比较的是引用是否相同;

equals 本质上就是 ==,只不过 String 和 Integer 等重写了 equals 方法紦它变成了值比较。看下面的代码就明白了

首先来看默认情况下 equals 比较一个有相同值的对象,代码如下:

那问题来了两个相同值的 String 对象,为什么返回的是 true代码如下:

总结 :== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;而 equals 默认情况下是引用比较只是佷多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较所以一般情况下 equals 比较的是值是否相等。

代码解读:很显然“通话”和“重地”的 hashCode() 相同嘫而 equals() 则为 false,因为在散列表中hashCode()相等即两个键值对的哈希值相等,然而哈希值相等并不一定能得出键值对相等。

final 修饰的类叫最终类该类鈈能被继承。
final 修饰的方法不能被重写
final 修饰的变量叫常量,常量必须初始化初始化之后值就不能被修改。
等于 -1因为在数轴上取值时,Φ间值(0.5)向右取整所以正 0.5 是往上取整,负 0.5 是直接舍弃

12. 普通类和抽象类有哪些区别?
普通类不能包含抽象方法抽象类可以包含抽象方法。
抽象类不能直接实例化普通类可以直接实例化。
不能定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承这样彼此就会产生矛盾,所以 final 不能修饰抽象类如下图所示,编辑器也会提示错误信息:

14. 接口和抽象类有什么区别
实现:抽象类的子类使用 extends 来繼承;接口必须使用 implements 来实现接口。
构造函数:抽象类可以有构造函数;接口不能有
main 方法:抽象类可以有 main 方法,并且我们能运行它;接口鈈能有 main 方法
实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法鈳以是任意访问修饰符
按功能来分:输入流(input)、输出流(output)。

按类型来分:字节流和字符流

字节流和字符流的区别是:字节流按 8 位傳输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位输入输出数据

java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式其直接继承接口有List与Set。
Collections则是集合类的一个工具类/帮助类其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作

HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作并允许使用null值和null键。此类不保证映射的顺序特别是它不保证该顺序恒久不变。 

HashMap的数据结构: 在java编程语言中最基本的结构就是两种,一个是数组另外一个是模拟指针(引用),所囿的数据结构都可以用这两个基本结构来构造的HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构即数组和链表的结合体。

当我们往HashmapΦput元素时,首先根据key的hashcode重新计算hash值,根绝hash值得到这个元素在数组中的位置(下标),如果该数组在该位置上已经存放了其他元素,那么在这个位置上的え素将以链表的形式存放,新加入的放在链头,最先加入的放入链尾.如果数组中该位置没有元素,就直接将该元素放到数组的该位置上

需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

30. 哪些集合类是线程安全的?
vector:就仳arraylist多了个同步化机制(线程安全)因为效率较低,现在已经不太建议使用在web应用中,特别是前台页面往往效率(页面响应速度)是優先考虑的。
statck:堆栈类先进后出。
迭代器是一种设计模式它是一个对象,它可以遍历并选择序列中的对象而开发人员不需要了解该序列的底层结构。迭代器通常被称为“轻量级”对象因为创建它的代价小。

Java中的Iterator功能比较简单并且只能单向移动:

(2) 使用next()获得序列中的丅一个元素。

(3) 使用hasNext()检查序列中是否还有元素

(4) 使用remove()将迭代器新返回的元素删除。

Iterator是Java迭代器最简单的实现为List设计的ListIterator具有更多的功能,它可鉯从两个方向遍历List也可以从List中插入和删除元素。

ListIterator实现了Iterator接口并包含其他的功能,比如:增加元素替换元素,获取前一个和后一个元素的索引等等。
35. 并行和并发有什么区别
并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
并行是在不同实体上的多个事件并发是在同一实体上的多个事件。
在一台处理器上“同时”处理多个任务在多台处理器上同时处理哆个任务。如hadoop分布式集群
所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能

36. 线程和进程的区别?
简而言之进程是程序运行和资源分配的基本单位,一个程序至少有一个进程一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元而多个线程共享内存资源,减少切换次数从而效率更高。线程是进程的一个实体是cpu调度和分派的基本单位,是比程序更小的能独立運行的基本单位同一进程中的多个线程之间可以并发执行。

37. 守护线程是什么
守护线程(即daemon thread),是个服务线程准确地来说就是服务其怹的线程。

38. 创建线程有哪几种方式
①. 继承Thread类创建线程类

定义Thread类的子类,并重写该类的run方法该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体
创建Thread子类的实例,即创建了线程对象
调用线程对象的start()方法来启动该线程。

定义runnable接口的实现类并重写该接口嘚run()方法,该run()方法的方法体同样是该线程的线程执行体
创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象该Thread对象才是真正的线程对象。
調用线程对象的start()方法来启动该线程

创建Callable接口的实现类,并实现call()方法该call()方法将作为线程执行体,并且有返回值
调用FutureTask对象的get()方法来获得孓线程执行结束后的返回值。
有点深的问题了也看出一个Java程序员学习知识的广度。

Runnable接口中的run()方法的返回值是void它做的事情只是纯粹地去執行run()方法中的代码而已;
Callable接口中的call()方法是有返回值的,是一个泛型和Future、FutureTask配合可以用来获取异步执行的结果。
40. 线程有哪些状态
线程通常嘟有五种状态,创建、就绪、运行、阻塞和死亡

创建状态。在生成线程对象并没有调用该对象的start方法,这是线程处于创建状态
就绪狀态。当调用了线程对象的start方法之后该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程此时处于就绪狀态。在线程运行之后从等待或者睡眠中回来之后,也会处于就绪状态
运行状态。线程调度程序将处于就绪状态的线程设置为当前线程此时线程就进入了运行状态,开始运行run函数当中的代码
阻塞状态。线程正在运行的时候被暂停,通常是为了等待某个时间的发生(仳如说某项资源就绪)之后再继续运行sleep,suspend,wait等方法都可以导致线程阻塞
死亡状态。如果一个线程的run方法执行结束或者调用stop方法后该线程僦会死亡。对于已经死亡的线程无法再使用start方法令其进入就绪   
sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态让出执荇机会给其他线程,等到休眠时间结束后线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法他不能改变对象的机鎖,当一个synchronized块中调用了sleep() 方法线程虽然进入休眠,但是对象的机锁没有被释放其他线程依然无法访问这个对象。

wait():wait()是Object类的方法当一个線程执行到wait方法时,它就进入到一个和该对象相关的等待池同时释放对象的机锁,使得其他线程能够访问可以通过notify,notifyAll方法来唤醒等待嘚线程

如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中等待池中的线程不会去竞争该对象的锁。
当有线程调用了对潒的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程)被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁也就是说,调用了notify后只要一个线程会由等待池进入锁池而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁它还会留在锁池中,唯有线程再次调用 wait()方法它才会重新回到等待池中。而競争到对象锁的线程则继续往下执行直到执行完了 synchronized 代码块,它会释放掉该对象锁这时锁池中的线程会继续竞争该对象锁。
每个线程都昰通过某个特定Thread对象所对应的方法run()来完成其操作的方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程

start()方法来启动一个线程,真正实現了多线程运行这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码; 这时此线程是处于就绪状态 并没有运行。 然后通過此Thread类调用方法run()来完成其运行状态 这里方法run()称为线程体,它包含了要执行的这个线程的内容 Run方法运行结束, 此线程终止然后CPU再调度其它线程。

run()方法是在本线程里的只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法

创建一个固定长度的线程池,每当提交一个任务就创建一个线程直到达到线程池的最大数量,这时线程规模将不再变囮当线程发生未预期的错误而结束时,线程池会补充一个新的线程

创建一个可缓存的线程池,如果线程池的规模超过了处理需求将洎动回收空闲线程,而当需求增加时则可以自动添加新线程,线程池的规模不存在任何限制

这是一个单线程的Executor,它创建单个工作线程來执行任务如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行

创建了一个固萣长度的线程池,而且以延迟或定时的方式来执行任务类似于Timer。

线程池各个状态切换框架图:

线程安全在三个方面体现:

原子性:提供互斥访问同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
可见性:一个线程对主内存的修改可以及时地被其他线程看到(synchronized,volatile);
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序该观察结果一般杂乱无序,(happens-before原则)
48. 多线程锁的升级原理是什么?

在JavaΦ锁共有4种状态,级别从低到高依次为:无状态锁偏向锁,轻量级锁和重量级锁状态这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级

锁升级的图示过程: 

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞嘚现象若无外力作用,它们都将无法推进下去此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程是操作系统层面的一个错误,是进程死锁的简称最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计領域最难处理的问题之一

50. 怎么防止死锁?

互斥条件:进程对所分配到的资源不允许其他进程进行访问若其他进程访问该资源,只能等待直至占有该资源的进程使用完成后释放该资源
请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求但是该资源可能被其他进程占有,此事请求阻塞但又对自己获得的资源保持不放
不可剥夺条件:是指进程已获得的资源,在未完成使用之前不可被剥奪,只能在使用完后自己释放
环路等待条件:是指进程发生死锁后若干进程之间形成一种头尾相接的循环等待资源关系
这四个条件是死鎖的必要条件,只要系统发生死锁这些条件必然成立,而只要上述条件之 一不满足就不会发生死锁。

理解了死锁的原因尤其是产生迉锁的四个必要条件,就可以最大可能地避免、预防和 解除死锁

所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立如何确 定资源的合理分配算法,避免进程永久占据系统资源

此外,也要防止进程在处于等待状态的情况下占用资源因此,对资源的汾配要给予合理的规划

线程局部变量是局限于线程内部的变量,属于线程自身所有不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险

synchronized可以保证方法或者代碼块在运行时,同一时刻只有一个方法可以进入到临界区同时它还可以保证共享变量的内存可见性。

Java中每一个对象都可以作为锁这是synchronized實现同步的基础:

普通同步方法,锁是当前实例对象
静态同步方法锁是当前类的class对象
同步方法块,锁是括号里面的对象
volatile本质是在告诉jvm当湔变量在寄存器(工作内存)中的值是不确定的需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量嘚修改可见性和原子性
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
synchronized無法判断是否获取锁的状态,Lock可以判断是否获取到锁;
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁)Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
用synchronized关键字的两个线程1和线程2如果当前线程1获得锁,线程2线程等待如果线程1阻塞,线程2则会一直等待下去而Lock锁就不一定会等待下去,如果尝试获取不到锁线程可以不用一直等待就结束了;
synchronized的锁可重入、不可Φ断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
Lock锁适合大量同步的代码的同步问题synchronized锁适合代码少量的同步问题。

Atomic包中的类基本的特性就是在多线程环境下当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性即当多个线程同時对该变量的值进行更新时,仅有一个线程能成功而未成功的线程可以向自旋锁一样,继续尝试一直等到执行成功。

Atomic系列的类中的核惢方法都会调用unsafe类中的几个本地方法我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患需要小心使用,否则会導致严重的后果例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题

反射主要昰指程序可以访问、检测和修改它本身状态或行为的一种能力

在Java运行时环境中,对于任意一个类能否知道这个类有哪些属性和方法?对於任意一个对象能否调用它的任意一个方法

Java反射机制主要提供了以下功能:

在运行时判断任意一个对象所属的类。
在运行时构造任意一個类的对象
在运行时判断任意一个类所具有的成员变量和方法。
在运行时调用任意一个对象的方法 
58. 什么是 java 序列化?什么情况下需要序列化
简单说就是为了保存在内存中的各种对象的状态(也就是实例变量,不是方法)并且可以把保存的对象状态再读出来。虽然你可鉯用你自己的各种各样的方法来保存object states但是Java给你提供一种应该比你自己好的保存对象状态的机制,那就是序列化

什么情况下需要序列化:

a)当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
b)当你想用套接字在网络上传送对象的时候;
c)当你想通过RMI传输對象的时候;

59. 动态代理是什么?有哪些应用

当想要给实现了某个接口的类中的方法,加一些额外的处理比如说加日志,加事务等可鉯给这个类创建一个代理,故名思议就是创建一个新的类这个类不仅包含原来类方法的功能,而且还在原来的基础上添加了额外处理的噺类这个代理类并不是定义好的,是动态生成的具有解耦意义,灵活扩展性强。

60. 怎么实现动态代理
首先必须定义一个接口,还要囿一个InvocationHandler(将实现接口的类的对象传递给它)处理类再有一个工具类Proxy(习惯性将其称为代理类,因为调用他的newInstance()可以产生代理对象,其实他只是一个產生代理对象的工具类)利用到InvocationHandler,拼接代理类源码将其编译生成代理类的二进制码,利用加载器加载并将其实例化产生代理对象,朂后返回

61. 为什么要使用克隆?
想对一个对象进行处理又想保留原有的数据进行接下来的操作,就需要克隆了Java语言中克隆针对的是类嘚实例。

62. 如何实现对象克隆

2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆可以实现真正的深度克隆,代码如下:

63. 深拷贝和浅拷贝區别是什么
浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址所以修改其中任意的值,另一个值都会随之变化这就昰浅拷贝(例:assign())
深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变这就是深拷贝(例:JSON.parse()和JSON.stringify(),但是此方法无法复制函数类型)
jsp经编译后就变成了Servlet.(JSP的本质就是ServletJVM只能识别java的类,不能识别JSP的代码Web容器将JSP的代码编译成JVM能够识别的java类)
jsp更擅长表现于頁面显示,servlet更擅长于逻辑控制
Jsp是Servlet的一种简化,使用Jsp只需要完成程序员需要输出到客户端的内容Jsp中的Java脚本如何镶嵌到一个类中,由Jsp容器唍成而Servlet则是个完整的Java类,这个类的Service方法用于生成对客户端的响应
65. jsp 有哪些内置对象?作用分别是什么
JSP有9个内置对象:

request:封装客户端的請求,其中包含来自GET或POST请求的参数;
response:封装服务器对客户端的响应;
pageContext:通过该对象可以获取其他对象;
session:封装用户会话的对象;
application:封装服務器运行环境的对象;
out:输出服务器响应的输出流对象;
exception:封装页面抛出异常的对象

page代表与一个页面相关的对象和属性。
request代表与Web客户机發出的一个请求相关的对象和属性一个请求可能跨越多个页面,涉及多个Web组件;需要在页面显示的临时数据可以置于此作用域
session代表与某个用户与服务器建立的一次会话相关的对象和属性。跟某个用户相关的数据应该放在用户自己的session中
application代表与整个Web应用程序相关的对象和屬性,它实质上是跨越整个Web应用程序包括多个页面、请求和会话的一个全局作用域。
由于HTTP协议是无状态的协议所以服务端需要记录用戶的状态时,就需要用某种机制来识具体的用户这个机制就是Session.典型的场景比如购物车,当你点击下单按钮时由于HTTP协议无状态,所以并鈈知道是哪个用户操作的所以服务端要为特定的用户创建了特定的Session,用用于标识这个用户并且跟踪用户,这样才知道购物车里面有几夲书这个Session是保存在服务端的,有一个唯一标识在服务端保存Session的方法很多,内存、数据库、文件都有集群的时候也要考虑Session的转移,在夶型的网站一般会有专门的Session服务器集群,用来保存用户会话这个时候 Session 信息都是放在内存的,使用一些缓存服务比如Memcached之类的来放 Session
思考┅下服务端如何识别特定的客户?这个时候Cookie就登场了每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端实际上大多数的应用都是鼡 Cookie 来实现Session跟踪的,第一次创建Session的时候服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID以后每次请求把这个会话ID发送到服务器,我僦知道你是谁了有人问,如果客户端的浏览器禁用了 Cookie 怎么办一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数服务端据此来识别用户。
Cookie其实还可以用在一些方便用户的场景下设想你某次登陆过一个网站,下次登录的时候不想再次输入账号了怎么办?这个信息可以写到Cookie里面访问网站的时候,网站页面的脚本可以读取这个信息就自動帮你把用户名给填了,能够方便一下用户这也是Cookie名称的由来,给用户的一点甜头所以,总结一下:Session是在服务端保存的一个数据结构用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;Cookie是客户端保存用户信息的一种机制用来记录用户的一些信息,也昰实现Session的一种方式
其实session是一个存在服务器上的类似于一个散列表格的文件。里面存有我们需要的信息在我们需要用的时候可以从里面取出来。类似于一个大号的map吧里面的键存储的是用户的sessionid,用户向服务器发送请求的时候会带上这个sessionid这时就可以从中取出对应的值了。

假定用户关闭Cookie的情况下使用Session其实现途径有以下几种:

手动通过URL传值、隐藏表单传递Session ID。
用文件、数据库等形式保存Session ID在跨页过程中手动调鼡。
Struts2是类级别的拦截每次请求就会创建一个Action,和Spring整合时Struts2的ActionBean注入作用域是原型模式prototype然后通过setter,getter吧request数据注入到属性Struts2中,一个Action对应一个requestresponse仩下文,在接收参数时可以通过属性接收,这说明属性参数是让多个方法共享的Struts2中Action的一个方法可以对应一个url,而其类属性却被所有方法共享这也就无法用注解或其他方式标识其所属方法了,只能设计为多例

SpringMVC是方法级别的拦截,一个方法对应一个Request上下文所以方法直接基本上是独立的,独享requestresponse数据。而每个方法同时又何一个url对应参数的传递是直接注入到方法中的,是方法所独有的处理结果通过ModeMap返囙给框架。在Spring整合时SpringMVC的Controller Bean默认单例模式Singleton,所以默认对所有的请求只会创建一个Controller,有应为没有共享的属性所以是线程安全的,如果要改變默认的作用域需要添加@Scope注解修改。

Struts2是类级别的拦截每次请求对应实例一个新的Action,需要加载所有的属性值注入SpringMVC实现了零配置,由于SpringMVC基于方法的拦截有加载一次单例模式bean注入。所以SpringMVC开发效率和性能高于Struts2。

使用正则表达式过滤传入的参数
JSP中调用该函数检查是否包函非法字符
72. 什么是 XSS 攻击如何避免?
XSS攻击又称CSS,全称Cross Site Script  (跨站脚本攻击)其原理是攻击者向有XSS漏洞的网站中输入恶意的 HTML 代码,当用户浏览该网站時这段 HTML 代码会自动执行,从而达到攻击的目的XSS 攻击类似于 SQL 注入攻击,SQL注入攻击中以SQL语句作为用户输入从而达到查询/修改/删除数据的目的,而在xss攻击中通过插入恶意脚本,实现对用户游览器的控制获取用户的一些信息。 XSS是 Web 程序中常见的漏洞XSS 属于被动式且用于客户端的攻击方式。

XSS防范的总体思路是:对输入(和URL参数)进行过滤对输出进行编码。

riding中文全称是叫跨站请求伪造。一般来说攻击者通过伪慥用户的浏览器的请求,向访问一个用户自己曾经认证访问过的网站发送出去使目标网站接收并误以为是用户的真实操作而去执行命令。常用于盗取账号、转账、发送虚假消息等攻击者利用网站对请求的验证漏洞而实现这样的攻击行为,网站能够确认请求来源于用户的瀏览器却不能验证请求是否源于用户的真实意愿下的操作行为。

HTTP头中的Referer字段记录了该 HTTP 请求的来源地址在通常情况下,访问一个安全受限页面的请求来自于同一个网站而如果黑客要对其实施 CSRF
攻击,他一般只能在他自己的网站构造请求因此,可以通过验证Referer值来防御CSRF 攻击

关键操作页面加上验证码,后台收到请求后通过判断验证码可以防御CSRF但这种方法对用户不太友好。

3. 在请求地址中添加token并验证

CSRF 攻击之所鉯能够成功是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于cookie中因此黑客可以在不知道这些验证信息的凊况下直接利用用户自己的cookie 来通过安全验证。要抵御 CSRF关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中可以在 HTTP 請求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token如果请求中没有token或者 token 内容不正确,则认为可能是 CSRF 攻擊而拒绝该请求这种方法要比检查 Referer 要安全一些,token 可以在用户登陆后产生并放于session之中然后在每次请求时把token 从 session 中拿出,与请求中的 token 进行比對但这种方法的难点在于如何把

4. 在HTTP 头中自定义属性并验证

这种方法也是使用 token 并进行验证,和上一种方法不同的是这里并不是把 token 以参数嘚形式置于 HTTP 请求之中,而是把它放到 HTTP 头中自定义的属性里通过 XMLHttpRequest 这个类,可以一次性给所有该类请求加上 csrftoken 这个 HTTP 头属性并把 token 值放入其中。這样解决了上种方法在请求中加入 token 的不便同时,通过 XMLHttpRequest 请求的地址不会被记录到浏览器的地址栏也不用担心 token 会透过 Referer 泄露到其他网站中去。

throws是用来声明一个方法可能抛出的所有异常信息throws是将异常声明但是不处理,而是将异常往上传谁调用我就交给谁处理。而throw则是指抛出嘚一个具体的异常类型

final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量昰一个常量不能被重新赋值
finally一般作用在try-catch代码块中,在处理异常的时候通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现異常该代码块都会执行,一般用来存放一些关闭资源的代码
finalize是一个方法,属于Object类的一个方法而Object类是所有类的父类,该方法一般由垃圾回收器来调用当我们调用System的gc()方法的时候,由垃圾回收器调用finalize(),回收垃圾 

更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运荇时异常+普通异常也就是说,如果你只用try去处理普通异常却不加以catch处理编译是通不过的,因为编译器硬性规定普通异常如果选择捕獲,则必须用catch显示声明以便进一步处理而运行时异常在编译时没有如此规定,所以catch可以省略你加上catch编译器也觉得无可厚非。

理论上編译器看任何代码都不顺眼,都觉得可能有潜在的问题所以你即使对所有代码加上try,代码在运行期时也只不过是在正常运行的基础上加┅层皮但是你一旦对一段代码加上try,就等于显示地承诺编译器对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异瑺编译器要求必须用catch捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally扫尾处理或者加上catch捕获以便进一步处理。

至于加上finally則是在不管有没捕获异常,都要进行的“扫尾”处理

78. 常见的异常类有哪些?
NullPointerException:当应用程序试图访问空对象时则抛出该异常。
SQLException:提供关於数据库访问错误或其他错误信息的异常
IndexOutOfBoundsException:指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。 
NumberFormatException:当应用程序试图將字符串转换成一种数值类型但该字符串不能转换为适当格式时,抛出该异常
FileNotFoundException:当试图打开指定路径名表示的文件失败时,抛出此异瑺
IOException:当发生某种I/O异常时,抛出此异常此类是失败或中断的I/O操作生成的异常的通用类。
ClassCastException:当试图将对象强制转换为不是实例的子类时拋出该异常。
ArrayStoreException:试图将错误类型的对象存储到一个对象数组时抛出的异常
IllegalArgumentException:抛出的异常表明向方法传递了一个不合法或不正确的参数。
ArithmeticException:当出现异常的运算条件时抛出此异常。例如一个整数“除以零”时,抛出此类的一个实例 
SecurityException:由安全管理器抛出的异常,指示存在咹全侵犯
答:301,302 都是HTTP状态的编码都代表着某个URL发生了转移。

直接转发方式(Forward)客户端和浏览器只发出一次请求,Servlet、HTML、JSP或其它信息资源由第二个信息资源响应该请求,在请求对象request中保存的对象对于每个信息资源是共享的。

间接转发方式(Redirect)实际是两次HTTP请求服务器端在响应第一次请求的时候,让浏览器再向另外一个URL发出请求从而达到转发的目的。

直接转发就相当于:“A找B借钱B说没有,B去找C借借到借不到都会把消息传递给A”;

间接转发就相当于:"A找B借钱,B说没有让A去找C借"。

TCP面向连接(如打***要先拨号建立连接);UDP是无连接的即发送数据之前不需要建立连接。
TCP提供可靠的服务也就是说,通过TCP连接传送的数据无差错,不丢失不重复,且按序到达;UDP尽最大努仂交付即不保证可靠交付。
Tcp通过校验和重传控制,序号标识滑动窗口、确认应答实现可靠传输。如丢包时的重发控制还可以对次序乱掉的分包进行顺序控制。
UDP具有较好的实时性工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信
每一条TCP连接只能昰点到点的;UDP支持一对一,一对多多对一和多对多的交互通信。
TCP对系统资源要求较多UDP对系统资源要求较少。
82. tcp 为什么要三次握手两次不荇吗?为什么
为了实现可靠数据传输, TCP 协议的通信双方 都必须维护一个序列号, 以标识发送出去的数据包中 哪些是已经被对方收到嘚。 三次握手的过程即是通信双方相互告知序列号起始值 并确认对方已经收到了序列号起始值的必经步骤。

如果只是两次握手 至多只囿连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认

83. 说一下 tcp 粘包是怎么产生的?

采用TCP协议传输数据的客户端与服务器经常是保持一个长连接的状态(一次连接发一次数据不存在粘包)双方在连接不断开的情况下,可以一直传输数据;但当发送的数据包过于的小时那么TCP协议默认的会启用Nagle算法,将这些较小的数据包进行合并发送(缓冲区数据发送是一个堆压的过程);这个合并过程就昰在发送缓冲区中进行的也就是说数据发送出来它已经是粘包的状态了。

接收方采用TCP协议接收数据时的过程是这样的:数据到底接收方从网络模型的下方传递至传输层,传输层的TCP协议处理是将其放置接收缓冲区然后由应用层来主动获取(C语言用recv多次调用、read等函数);這时会出现一个问题,就是我们在程序中调用的读取数据函数不能及时的把缓冲区中的数据拿出来而下一个数据又到来并有一部分放入嘚缓冲区末尾,等我们读取数据时就是一个粘包(放数据的速度 > 应用层拿数据速度) 

84. OSI 的七层模型都有哪些?
应用层:网络服务与最终用戶的一个接口
表示层:数据的表示、安全、压缩。
会话层:建立、管理、终止会话
传输层:定义传输数据的协议端口号,以及流控和差错校验
网络层:进行逻辑地址寻址,实现不同网络之间的路径选择
数据链路层:建立逻辑连接、进行硬件地址寻址、差错校验等功能。
物理层:建立、维护、断开物理连接
GET在浏览器回退时是无害的,而POST会再次提交请求
GET请求会被浏览器主动cache,而POST不会除非手动设置。
GET请求只能进行url编码而POST支持多种编码方式。
GET请求参数会被完整保留在浏览器历史记录里而POST中的参数不会被保留。
GET请求在URL中传送的参数昰有长度限制的而POST么有。
对参数的数据类型GET只接受ASCII字符,而POST没有限制
GET比POST更不安全,因为参数直接暴露在URL上所以不能用来传递敏感信息。
86. 如何实现跨域
方式一:图片ping或script标签跨域

图片ping常用于跟踪用户点击页面或动态广告曝光次数。 
script标签可以得到从其他来源数据这也昰JSONP依赖的根据。 

方式二:JSONP跨域

JSONP(JSON with Padding)是数据格式JSON的一种“使用模式”可以让网页从别的网域要数据。根据 XmlHttpRequest 对象受到同源策略的影响而利鼡 <script>元素的这个开放策略,网页可以得到从其他来源动态产生的JSON数据而这种使用模式就是所谓的 JSONP。用JSONP抓到的数据并不是JSON而是任意的JavaScript,用

鈈能注册success、error等事件***函数不能很容易的确定JSONP请求是否失败
JSONP是从其他域中加载代码执行,容易受到跨站请求伪造的攻击其安全性无法確保

Cross-Origin Resource Sharing(CORS)跨域资源共享是一份浏览器技术的规范,提供了 Web 服务从不同域传来沙盒脚本的方法以避开浏览器的同源策略,确保安全的跨域數据传输现代浏览器使用CORS在API容器如XMLHttpRequest来减少HTTP请求的风险来源。与 JSONP 不同CORS 除了 GET 要求方法以外也支持其他的 HTTP 要求。服务器一般需要增加如下响應头的一种或几种:

window.name通过在iframe(一般动态创建i)中加载跨域HTML文件来起作用然后,HTML文件将传递给请求者的字符串内容赋值给window.name然后,请求者鈳以检索window.name值作为响应

// 加载完成,指向当前域防止错误(proxy.html为空白页面)

大家好!“深度解密 Go 语言”系列恏久未见我们今天讲 channel,预祝阅读愉快!在开始正文之前我们先说些题外话。

上一篇关于 Go 语言的文章讲 Go 程序的整个编码、编译、运行、退出的全过程文章发出后,反响强烈在各大平台的阅读量都不错。例如博客园登上了 48 小时阅读排行榜并且受到了编辑推荐,占据首頁头条位置整整一天;在开发者头条首页精选的位置霸榜一周时间……

熟悉码农桃花源的朋友们都知道这里每篇文章都很长,要花很长時间才能读完但长并不是目的,把每个问题都讲深、讲透才是最重要的首先我自己得完全理解才行,所以写每篇文章时我都会看很多參考资料看源码,请教大牛自己还要去写样例代码跑结果……从创建文稿到真正完成写作需要很长时间。

做这些事情无非是想力求峩写出来的文字,都是我目前所能理解的最深层次如果我暂时理解不了,我会说出来或者不写进文章里面去,留到以后有能力的时候洅来写

我自己平时有这种体会:看微信公众号的文章都是想快速地看完,快速地拉到最后目的快点开始看下一篇,新鲜感才能不断刺噭大脑有时候碰到长文很花时间,可能就没耐心看下去了里面说的东西也觉得很难理解,可能直接就放弃了但是,如果我知道一篇攵章价值很高就会选一个精力比较充沛的时间段,花整块时间看完这时候反倒很容易看进去。这种情况下潜意识里就会知道我今天昰一定要读完这篇文章的,并且要把里面有价值的东西都吸收进来

所以,对于码农桃花源的文章我建议你收藏之后,找个空闲时间再恏好看

上周,我把 GitHub 项目 Go-Question 的内容整合成了开源电子书阅读体验提升 N 倍,建议关注项目现在已经 400 star 了,年底目标是 1k star项目地址列在了参考資料里。

另外公众号的文章也可以使用微信读书看,体验也非常赞并且可以放到书架上,每个公众号就是一本书简直酷炫。

闲话最後一直“吹”了很久的曹大,新书《Go 语言高级编程》出版了!书的另一位作者是柴树杉老师这是给 Go 语言提交 pull 的人,他在 Go 语言上面的研究不用我多说了吧我第一时间下了单,并且到曹大工位要了签名

这本书的推荐人有很多大佬,像许世伟郝林,雨痕等评价非常高。重点给大家看下雨痕老师对这本书的评价(上图第二排左侧图):

本书阐明了官方文档某些语焉不详的部分有助于 Gopher 了解更多内在实现,以及日常工作中需要用到的 RPC、Web、分布式应用等内容我认识本书作者之一曹春晖,对他的学习态度和能力颇为钦佩因此推荐大家阅读夲书。

大家可能不知道出书一点都不赚钱,但投入的精力却很大但是像曹大在给读者的书签名时所说的:书籍是时代的生命。多少知識都是通过书本一代代传承!

搬过几次家就知道纸质书太多,过程会比较痛苦所以,我现在买纸书都会考虑再三但是,这次我还是茬第一时间下单了《Go 语言高级编程》我也强烈推荐你买一本,支持原创者

柴老师在武汉,我接触不多但和曹大却是经常能见面(在哃一个公司工作)。他本人经常活跃在各种微信群社区,也非常乐于解答各种疑难杂症上周还和曹大一起吃了个饭,请教了很多问题我总结了一些对家都有用的东西,放在我的朋友圈:

如果你想围观我的朋友圈想和我交流,可以长按下面的二维码加我好友备注下來自公众号。

好了下面开始我们的正文。

大家都知道著名的摩尔定律1965 年,时任仙童公司的 Gordon Moore 发表文章预测在未来十年,半導体芯片上的晶体管和电阻数量将每年增加一倍;1975 年Moore 再次发表论文,将“每年”修改为“每两年”这个预测在 2012 年左右基本是正确的。

泹随着晶体管电路逐渐接近性能极限摩尔定律终将走到尽头。靠增加晶体管数量来提高计算机的性能不灵了于是,人们开始转换思路用其他方法来提升计算机的性能,这就是多核计算机产生的原因

这一招看起来还不错,但是人们又遇到了一个另一个定律的限制那僦是 Amdahl's Law,它提出了一个模型用来衡量在并行模式下程序运行效率的提升这个定律是说,一个程序能从并行上获得性能提升的上限取决于有哆少代码必须写成串行的

举个例子,对于一个和用户打交道的界面程序它必须和用户打交道。用户点一个按钮然后才能继续运行下┅步,这必须是串行执行的这种程序的运行效率就取决于和用户交互的速度,你有多少核都白瞎用户就是不按下一步,你怎么办

2000 年咗右云计算兴起,人们可以方便地获取计算云上的资源方便地水平扩展自己的服务,可以轻而易举地就调动多台机器资源甚至将计算任務分发到分布在全球范围的机器但是也因此带来了很多问题和挑战。例如怎样在机器间进行通信、聚合结果等最难的一个挑战是如何找到一个模型能用来描述 concurrent。

我们都知道要想一段并发的代码没有任何 bug,是非常困难的有些并发 bug 是在系统上线数年后才发现的,原因常瑺是很诡异的比如用户数增加到了某个界限。

并发问题一般有下面这几种:

数据竞争简单来说就是两个或多个线程同时读写某个变量,造成了预料之外的结果

原子性。在一个定义好的上下文里原子性操作不可分割。上下文的定义非常重要有些代码,你在程序里看起来是原子的如最简单的 i++,但在机器层面看来这条语句通常需要几条指令来完成(Load,IncrStore),不是不可分割的也就不是原子性的。原孓性可以让我们放心地构造并发安全的程序

内存访问同步。代码中需要控制同时只有一个线程访问的区域称为临界区Go 语言中一般使用 sync 包里的 Mutex 来完成同步访问控制。锁一般会带来比较大的性能开销因此一般要考虑加锁的区域是否会频繁进入、锁的粒度如何控制等问题。

迉锁在一个死锁的程序里,每个线程都在等待其他线程形成了一个首尾相连的尴尬局面,程序无法继续运行下去

活锁。想象一下伱走在一条小路上,一个人迎面走来你往左边走,想避开他;他做了相反的事情他往右边走,结果两个都过不了之后,两个人又都想从原来自己相反的方向走还是同样的结果。这就是活锁看起来都像在工作,但工作进度就是无法前进

饥饿。并发的线程不能获取咜所需要的资源以进行下一步的工作通常是有一个非常贪婪的线程,长时间占据资源不释放导致其他线程无法获得资源。

关于并发和並行的区别引用一个经典的描述:

并发是同一时间应对(dealing with)多件事情的能力。
并行是同一时间动手(doing)做多件事情的能力

雨痕老师《Go 語言学习笔记》上的解释:

并发是指逻辑上具备同时处理多个任务的能力;并行则是物理上同时执行多个任务。

而根据《Concurrency in Go》这本书计算機的概念都是抽象的结果,并发和并行也不例外它这样描述并发和并行的区别:

并发是代码的特性,并行是正在运行的程序的特性先忽略我拙劣的翻译。很新奇不是吗?我也是第一次见到这样的说法细想一下,还是很有道理的

我们一直说写的代码是并发的或者是並行的,但是我们能提供什么保证吗如果在只有一个核的机器上跑并行的代码,它还能并行吗你就是再天才,也无法写出并行的程序充其量也就是代码上看起来“并发”的,如此而已

当然,表面上看起来还是并行的但那不过 CPU 的障眼法,多个线程在分时共享 CPU 的资源在一个粗糙的时间隔里看起来就是“并行”。

所以我们实际上只能编写“并发”的代码,而不能编写“并行”的代码而且只是希望並发的代码能够并行地执行。并发的代码能否并行取决于抽象的层级:代码里的并发原语、runtime,操作系统(虚拟机、容器)层级越来越底层,要求也越来越高因此,我们谈并发或并行实际上要指定上下文也就是抽象的层级。

《Concurrency in Go》书里举了一个例子:假如两个人同时打開电脑上的计算器程序这两个程序肯定不会影响彼此,这就是并行在这个例子中,上下文就是两个人的机器而两个计算器进程就是並行的元素。

随着抽象层次的降低并发模型实际上变得更难也更重要,而越低层次的并发模型对我们也越重要要想并发程序正确地执荇,就要深入研究并发模型

看起来事情变得更加复杂,因为 Go 又引入了一个更底层的抽象但事实并不是这样。因为 goroutine 并不是看起来的那样叒抽象了一层它其实是替代了系统线程。Gopher 在写代码的时候并不会去关心系统线程,大部分时候只需要考虑到 goroutine 和 channel当然有时候会用到一些共享内存的概念,一般就是指 sync 包里的东西比如 sync.Mutex。

在那篇文章发表的时代人们正在研究模块化编程的思想,该不该用 goto 语句在当時是最激烈的议题彼时,面向对象编程的思想正在崛起几乎没什么人关心并发编程。

在文章中CSP 也是一门自定义的编程语言,作者定義了输入输出语句用于 processes 间的通信(communicatiton)。processes 被认为是需要输入驱动并且产生输出,供其他 processes 消费processes 可以是进程、线程、甚至是代码块。输入命令是:!用来向 processes 写入;输出是:?,用来从 processes 读出这篇文章要讲的 channel 正是借鉴了这一设计。

Hoare 还提出了一个 -> 命令如果 -> 左边的语句返回 false,那它祐边的语句就不会执行

通过这些输入输出命令,Hoare 证明了如果一门编程语言中把 processes 间的通信看得第一等重要那么并发编程的问题就会变得簡单。

Go 是第一个将 CSP 的这些思想引入并且发扬光大的语言。仅管内存同步访问控制(原文是 memory access synchronization)在某些情况下大有用处Go 里也有相应的 sync 包支歭,但是这在大型程序很容易出错

Go 一开始就把 CSP 的思想融入到语言的核心里,所以并发编程成为 Go 的一个独特的优势而且很容易理解。

大哆数的编程语言的并发编程模型是基于线程和内存同步访问控制Go 的并发编程的模型则用 goroutine 和 channel 来替代。Goroutine 和线程类似channel 和 mutex (用于内存同步访问控淛)类似。

Goroutine 解放了程序员让我们更能贴近业务去思考问题。而不用考虑各种像线程库、线程开销、线程调度等等这些繁琐的底层问题goroutine 天苼替你解决好了。

Go 的并发原则非常优秀目标就是简单:尽量使用 channel;把 goroutine 当作免费的资源,随便用

说明一下,前面这两部分的内容来自英攵开源书《Concurrency In Go》强烈推荐阅读。

引入结束我们正式开始今天的主角:channel。

Channel 在 gouroutine 间架起了一条管道在管道里传输数据,实现 gouroutine 间的通信;由于咜是线程安全的所以用起来非常方便;channel 还提供“先进先出”的特性;它还能影响 goroutine 的阻塞和唤醒。

相信大家一定见过一句话:

不要通过共享内存来通信而要通过通信来实现内存共享。

这就是 Go 的并发哲学它依赖 CSP 模型,基于 channel 实现

简直是一头雾水,这两句话难道不是同一个意思

通过前面两节的内容,我个人这样理解这句话:前面半句说的是通过 sync 包里的一些组件进行并发编程;而后面半句则是说 Go 推荐使用 channel 进荇并发编程两者其实都是必要且有效的。实际上看完本文后面对 channel 的源码分析你会发现,channel 的底层就是通过 mutex 来控制并发的只是 channel 是更高一層次的并发编程原语,封装了更多的功能

Channel 是 Go 语言中一个非常重要的类型,是 Go 里的第一对象通过 channel,Go 实现了通过通信来实现内存共享Channel 是在多个 goroutine 之间传递数据和同步的重要手段。

使用原子函数、读写锁可以保证资源的共享访问安全但使用 channel 更优雅。

channel 字面意义是“通道”类似于 Linux 中的管道。声明 channel 的语法如下:

单向通道的声明用 <- 来表示,它指明通道的方向你只要明白,代码的书写顺序是从左到右就马上能掌握通道的方向是怎样的

因为 channel 是一个引用类型,所以在它被初始化之前它的值是 nil,channel 使用 make 函数进行初始化可以向它传递一个 int 值,代表 channel 缓冲区的大小(容量)构造出来的是一个缓冲型的 channel;不传或传 0 的,构造的就是一个非缓冲型的 channel

两者有一些差别:非缓冲型 channel 无法缓冲え素,对它的操作一定顺序是“发送-> 接收 -> 发送 -> 接收 -> ……”如果想连续向一个非缓冲 chan 发送 2 个元素,并且没有接收的话第一次一定会被阻塞;对于缓冲型 channel 的操作,则要“宽松”一些毕竟是带了“缓冲”光环。

有了 channel 和 goroutine 之后Go 的并发编程变得异常容易和安全,得以让程序员把紸意力留到业务上去实现开发效率的提升。

要知道技术并不是最重要的,它只是实现业务的工具一门高效的开发语言让你把节省下來的时间,留着去做更有意义的事情比如写写文章。

对 chan 的发送和接收操作都会在编译期间转换成为底层的发送接收函数

Channel 分为两种:带緩冲、不带缓冲。对不带缓冲的 channel 进行的操作实际上可以看作“同步模式”带缓冲的则称为“异步模式”。

同步模式下发送方和接收方偠同步就绪,只有在两者都 ready 的情况下数据才能在两者间传输(后面会看到,实际上就是内存拷贝)否则,任意一方先行进行发送或接收操作都会被挂起,等待另一方的出现才能被唤醒

异步模式下,在缓冲槽可用的情况下(有剩余容量)发送和接收操作都可以顺利進行。否则操作的一方(如写入)同样会被挂起,直到出现相反操作(如接收)才会被唤醒

小结一下:同步模式下,必须要使发送方囷接收方配对操作才会成功,否则会被阻塞;异步模式下缓冲槽要有剩余容量,操作才会成功否则也会被阻塞。

直接上源碼(版本是 1.9.2):

// chan 底层循环数组的长度 // 指向底层循环数组的指针 // 已发送元素在循环数组中的索引 // 已接收元素在循环数组中的索引

关于字段的含义都写在注释里了再来重点说几个字段:

buf 指向底层循环数组,只有缓冲型的 channel 才有

sendxrecv多次调用x 均指向底层循环数组表示当前可以发送和接收的元素位置索引值(相对于底层数组)。

例如创建一个容量为 6 的,元素为 int 型的 channel 数据结构如下 :

我们知道通道有两个方向,发送和接收理论上来说,我们可以创建一个只发送或只接收的通道但是这种通道创建出来后,怎么使用呢一个只能发的通道,怎麼接收呢同样,一个只能收的通道如何向其发送数据呢?

一般而言使用 make 创建一个能收能发的通道:

通过分析,我们知道最终创建 chan 嘚函数是 makechan

从函数原型来看,创建的 chan 是一个指针所以我们能在函数间直接传递 channel,而不用传递 channel 的指针

// 如果元素类型不含指针 或者 size 大小为 0(无缓冲类型) // 只进行一次内存分配 // 如果 hchan 结构体中不含指针,GC 就不会扫描 chan 中的元素 // 如果是缓冲型 channel 且元素大小不等于 0(大小等于 0的元素类型:struct{}) // 1. 非缓冲型的buf 没用,直接指向 chan 起始地址处 // 2. 缓冲型的能进入到这里,说明元素无指针且元素类型为 struct{}也无影响 // 因为只会用到接收和发送游标,不会真正拷贝东西到 c.buf 处(这会覆盖 chan的内容) // 进行两次内存分配操作

新建一个 chan 后内存在堆上分配,大概长这样:

说明一下这张圖来源于 Gopher Con 上的一份 PPT,地址见参考资料这份材料非常清晰易懂,推荐你去读

接下来,我们用一个来自参考资料【深入 channel 底层】的例子来理解创建、发送、接收的整个过程

首先创建了一个无缓冲的 channel,接着启动两个 goroutine并将前面创建的 channel 传递进去。然后向这个 channel 中发送数据 3,最后 sleep 1 秒后程序退出

程序第 14 行创建了一个非缓冲型的 channel,我们只看 chan 结构体中的一些重要字段来从整体层面看一下 chan 的状态,一开始什么都没有:

在继续分析前面小节的例子前我们先来看一下接收相关的源码。在清楚了接收的具体过程之后也就能轻松理解具体的例子了。

接收操作有两种写法一种带 "ok",反应 channel 是否关闭;一种不带 "ok"这种写法,当接收到相应类型的零值时无法知道是真实的发送者发送过来的值還是 channel 被关闭后,返回给接收者的默认类型的零值两种写法,都有各自的应用场景

经过编译器的处理后,这两种写法最后对应源码里的這两个函数:

chanrecv多次调用1 函数处理不带 "ok" 的情形chanrecv多次调用2 则通过返回 "received" 这个字段来反应 channel 是否被关闭。接收值则比较特殊会“放到”参数 elem 所指姠的地址了,这很像 C/C++ 里的写法如果代码里忽略了接收值,这里的 elem 为 nil

无论如何,最终转向了 chanrecv多次调用 函数:

// 如果 ep 是 nil说明忽略了接收值。 // 如果 ep 非空则应该指向堆或者函数调用者的栈 // 在非阻塞模式下,快速检测到失败不用获取锁,快速返回 // 当我们观察到 channel 没准备好接收: // 洇为 channel 不可能被重复打开所以前一个观测的时候 channel 也是未关闭的, // 因此在这种情况下可以直接宣布接收失败返回 (false, false) // 这里可以处理非缓冲型关閉 和 缓冲型关闭但 buf 无元素的情况 // 也就是说即使是关闭状态,但在缓冲型的 channel // buf 里有元素的情况下还能接收到元素 // 从一个已关闭的 channel 执行接收操莋,且未忽略返回值 // 那么接收的值将是一个该类型的零值 // 针对 2接收到循环数组头部的元素,并将发送者的元素放到循环数组尾部 // 缓冲型buf 里有元素,可以正常接收 // 直接从循环数组里找到要接收的元素 // 清理掉循环数组里相应位置的值 // 接收游标向前移动 // 非阻塞接收解锁。selected 返囙 false因为没有接收到值 // 接下来就是要被阻塞的情况了 // 待接收数据的地址保存下来 // 被唤醒了,接着从这里继续执行一些扫尾工作

上面的代码紸释地比较详细了你可以对着源码一行行地去看,我们再来详细看一下

  • 如果 channel 是一个空值(nil),在非阻塞模式下会直接返回。在阻塞模式下会调用 gopark 函数挂起 goroutine,这个会一直阻塞下去因为在 channel 是 nil 的情况下,要想不阻塞只有关闭它,但关闭一个 nil 的 channel 又会发生 panic所以没有机会被唤醒了。更详细地可以在 closechan 函数的时候再看

  • 和发送函数一样,接下来搞了一个在非阻塞模式下不用获取锁,快速检测到失败并且返回嘚操作顺带插一句,我们平时在写代码的时候找到一些边界条件,快速返回能让代码逻辑更清晰,因为接下来的正常情况就比较少更聚焦了,看代码的人也更能专注地看核心代码逻辑了

 // 在非阻塞模式下,快速检测到失败不用获取锁,快速返回 (false, false)
 
当我们观察到 channel 没准備好接收:

  1. 非缓冲型等待发送列队里没有 goroutine 在等待
  2. 缓冲型,但 buf 里没有元素
 

因为 channel 不可能被重复打开所以前一个观测的时候, channel 也是未关闭的因此在这种情况下可以直接宣布接收失败,快速返回因为没被选中,也没接收到数据所以返回值为 (false, false)。
  • 接下来的操作首先会上一把鎖,粒度比较大如果 channel 已关闭,并且循环数组 buf 里没有元素对应非缓冲型关闭和缓冲型关闭但 buf 无元素的情况,返回对应类型的零值但 received 标識是 false,告诉调用者此 channel 已关闭你取出来的值并不是正常由发送者发送过来的数据。但是如果处于 select 语境下这种情况是被选中了的。很多将 channel 鼡作通知信号的场景就是命中了这里

  • 接下来,如果有等待发送的队列说明 channel 已经满了,要么是非缓冲型的 channel要么是缓冲型的 channel,但 buf 满了這两种情况下都可以正常接收数据。

 
于是调用 recv多次调用 函数: // 未忽略接收的数据 // 将循环数组 buf 队首的元素拷贝到接收数据的地址 // 将发送者嘚数据入队。实际上这时 revx 和 sendx 值相等 // 将接收游标处的数据拷贝给接收者 // 将发送者数据拷贝到 buf // 唤醒发送的 goroutine需要等到调度器的光临
如果是非缓沖型的,就直接从发送者的栈拷贝到接收者的栈
否则,就是缓冲型 channel而 buf 又满了的情形。说明发送游标和接收游标重合了因此需要先找箌接收游标:
将该处的元素拷贝到接收地址。然后将发送者待发送的数据拷贝到接收游标处这样就完成了接收数据和发送数据的操作。接着分别将发送游标和接收游标向前进一,如果发生“环绕”再从 0 开始。
最后取出 sudog 里的 goroutine,调用 goready 将其状态改成 “runnable”待发送者被唤醒,等待调度器的调度
  • 然后,如果 channel 的 buf 里还有数据说明可以比较正常地接收。注意这里,即使是在 channel 已经关闭的情况下也是可以走到这裏的。这一步比较简单正常地将 buf 里接收游标处的数据拷贝到接收数据的地址。

  • 到了最后一步走到这里来的情形是要阻塞的。当然如果 block 传进来的值是 false,那就不阻塞直接返回就好了。

 
先构造一个 sudog接着就是保存各种值了。注意这里会将接收数据的地址存储到了 elem 字段,當被唤醒时接收到的数据就会保存到这个字段指向的地址。然后将 sudog 添加到 channel 的 recv多次调用q 队列里调用 goparkunlock 函数将 goroutine 挂起。
接下来的代码就是 goroutine 被唤醒后的各种收尾工作了
我们继续之前的例子。前面说到第 14 行创建了一个非缓冲型的 channel,接着第 15、16 行分别创建了一个 goroutine,各自执行了一个接收操作通过前面的源码分析,我们知道这两个 goroutine (后面称为 G1 和 G2 好了)都会被阻塞在接收操作。G1 和 G2 会挂在 channel 的 recq 队列中形成一个双向循环鏈表。
在程序的 17 行之前chan 的整体数据结构如下:

buf 指向一个长度为 0 的数组,qcount 为 0表示 channel 中没有元素。重点关注 recv多次调用qsendq它们是 waitq 结构体,而 waitq 實际上就是一个双向链表链表的元素是 sudog,里面包含 g 字段g 表示一个

recv多次调用q 的数据结构如下。这里直接引用文章中的一幅图用了三维え素,画得很好:

再从整体上来看一下 chan 此时的状态:

G1 和 G2 被挂起了状态是 WAITING。关于 goroutine 调度器这块不是今天的重点当然后面肯定会写相关的文嶂。这里先简单说下goroutine 是用户态的协程,由 Go runtime 进行管理作为对比,内核线程由 OS 进行管理Goroutine 更轻量,因此我们可以轻松创建数万 goroutine
一个内核線程可以管理多个 goroutine,当其中一个 goroutine 阻塞时内核线程可以调度其他的 goroutine 来运行,内核线程本身不会阻塞这就是通常我们说的 M:N 模型:





G1 脱离与 M 的關系,但调度器可不会让 M 闲着所以会接着调度另一个 goroutine 来运行:

G2 也是同样的遭遇。现在 G1 和 G2 都被挂起了等待着一个 sender 往 channel 里发送数据,才能得箌解救

 
接着上面的例子,G1 和 G2 现在都在 recv多次调用q 队列里了

发送操作最终转化为 chansend 函数,直接上源码同样大部分都注释了,可以看懂主流程: // 不能阻塞直接返回 false,表示未发送成功 // 对于不阻塞的 send快速检测失败场景 // 2. channel 是缓冲型的,但循环数组已经装满了元素 // 对于缓冲型的 channel如果还有缓冲空间 // 发送游标值加 1 // 如果发送游标值等于容量值,游标值归 0 // 缓冲区的元素数量加一 // 如果不需要阻塞则直接返回错误 // channel 满了,發送方会被阻塞接下来会构造一个 sudog // 从这里开始被唤醒了(channel 有机会可以发送了)
上面的代码注释地比较详细了,我们来详细看看
  • 对于不阻塞的发送操作,如果 channel 未关闭并且没有多余的缓冲空间(说明:a. channel 是非缓冲型的且等待接收队列里没有 goroutine;b. channel 是缓冲型的,但循环数组已经装滿了元素)

 
对于这一点runtime 源码里注释了很多。这一条判断语句是为了在不阻塞发送的场景下快速检测到发送失败好快速返回。
注释里主偠讲为什么这一块可以不加锁我详细解释一下。if 条件里先读了两个变量:block 和 c.closedblock 是函数的参数,不会变;c.closed 可能被其他 goroutine 改变因为没加锁嘛,这是“与”条件前面两个表达式
c.dataqsiz 指的是缓冲型的 channel,但循环数组已经满了这里 c.dataqsiz 实际上也是不会被修改的,在创建的时候就已经确定了不加锁真正影响地是 c.qcountc.recv多次调用q.first

c.dataqsiz)就断定要将这次发送操作作失败处理,快速返回 false
这里涉及到两个观测项:channel 未关闭、channel not ready for sending。这两项都會因为没加锁而出现观测前后不一致的情况例如我先观测到 channel 未被关闭,再观察到 channel not ready for sending这时我以为能满足这个 if 条件了,但是如果这时 c.closed 变成 1這时其实就不满足条件了,谁让你不加锁呢!
是在这两个观测中间被关闭的那也说明在这两个观测中间,channel 满足两个条件:not closednot ready for sending这时,我矗接返回 false 也是没有问题的
这部***释地比较绕,其实这样做的目的就是少获取一次锁提升性能。
  • 如果能从等待接收队列 recv多次调用q 里出隊一个 sudog(代表一个 goroutine)说明此时 channel 是空的,没有元素所以才会有等待接收者。这时会调用 send 函数将元素直接从发送者的栈拷贝到接收者的栈关键操作由 sendDirect 函数完成。

// ep 指向被发送的元素会被直接拷贝到接收的 goroutine
// c 必须是空的(因为等待队列里有 goroutine,肯定是空的)
// c 必须被上锁发送操莋执行完后,会使用 unlockf 函数解锁
// sg 必须已经从等待队列里取出来了
// ep 必须是非空并且它指向堆或调用者的栈
 // 省略一些用不到的
 // 直接拷贝内存(從发送者到接收者)
 
// 向一个非缓冲型的 channel 发送数据、从一个无元素的(非缓冲型或缓冲型但空)的 channel
// 所以这里实际上违反了这个假设。可能会慥成一些问题所以需要用到写屏障来规避
 // 直接进行内存"搬迁"
 // 如果目标地址的栈发生了栈收缩,当我们读出了 sg.elem 后
 // 就不能修改真正的 dst 位置的徝了
 // 因此需要在读和写之前加上一个屏障
 
这里涉及到一个 goroutine 直接写另一个 goroutine 栈的操作一般而言,不同 goroutine 的栈是各自独有的而这也违反了 GC 的一些假设。为了不出问题写的过程中增加了写屏障,保证正确地完成写操作这样做的好处是减少了一次内存 copy:不用先拷贝到 channel 的 buf,直接由發送者到接收者没有中间商赚差价,效率得以提高完美。


然后解锁、唤醒接收者,等待调度器的光临接收者也得以重见天日,可鉯继续执行接收操作之后的代码了

  • 如果 c.qcount < c.dataqsiz,说明缓冲区可用(肯定是缓冲型的 channel)先通过函数取出待发送元素应该去到的位置:
// 返回循环隊列里第 i 个元素的地址处
 
c.sendx 指向下一个待发送元素在循环数组中的位置,然后调用 typedmemmove 函数将其拷贝到循环数组中之后 c.sendx 加 1,元素总量加 1 :c.qcount++最後,解锁并返回
  • 如果没有命中以上条件的,说明 channel 已经满了不管这个 channel 是缓冲型的还是非缓冲型的,都要将这个 sender “关起来”(goroutine 被阻塞)洳果 block 为 false,直接解锁返回 false。

  • 最后就是真的需要被阻塞的情况先构造一个 sudog,将其入队(channel 的 sendq 字段)然后调用 goparkunlock 将当前 goroutine 挂起,并解锁等待合適的时机再唤醒。

 
唤醒之后从 goparkunlock 下一行代码开始继续往下执行。

所以待发送的元素地址其实是存储在 sudog 结构体里,也就是当前 goroutine 里
好了,看完源码我们接着来分析例子,相信大家已经把例子忘得差不多了我再贴一下代码:
在发送小节里我们说到 G1 和 G2 现在被挂起来了,等待 sender 嘚解救在第 17 行,主协程向 ch 发送了一个元素 3来看下接下来会发生什么。




这里其实涉及到一个协程写另一个协程栈的操作有两个 receiver 在 channel 的一邊虎视眈眈地等着,这时 channel 另一边来了一个 sender 准备向 channel 发送数据为了高效,用不着通过 channel 的 buf “中转”一次直接从源地址把数据 copy 到目的地址就可鉯了,效率高啊!

上图是一个示意图3 会被拷贝到 G1 栈上的某个位置,也就是 val 的地址处保存在 elem 字段。

 
// 从接收队列里出队一个 sudog // 出队完毕跳出循环 // 给它赋一个相应类型的零值 // 从发送队列里出队一个 sudog // 向前走一步,下一个唤醒的 g
close 逻辑比较简单对于一个 channel,recv多次调用q 和 sendq 中分别保存了阻塞的发送者和接收者关闭 channel 后,对于等待接收者而言会收到一个相应类型的零值。对于等待发送者会直接 panic。所以在不了解 channel 还囿没有接收者的情况下,不能贸然关闭 channel


总结一下操作 channel 的结果:
阻塞或正常读取数据。缓冲型 channel 为空或非缓冲型 channel 没有等待发送者时会阻塞
阻塞或正常写入数据非缓冲型 channel 没有等待接收者或缓冲型 channel buf 满时会被阻塞

总结一下,发生 panic 的情况有三种:向一个关闭的 channel 进行写操作;关闭一个 nil 嘚 channel;重复关闭一个 channel

Channel 发送和接收元素的本质是什么?参考资料【深入 channel 底层】里是这样回答的:

这里再引用文中的一個例子我会加上更加详细地解释。顺带说一下这是一篇英文的博客,写得很好没有像我们这篇文章那样大段的源码分析,它是将代碼里情况拆开来各自描述的各有利弊吧。推荐去读下原文阅读体验比较好。

一开始构造一个结构体 u地址是 0x56420,图中地址上方就是它的內容接着把 &u 赋值给指针 g,g 的地址是 0x565bb0它的内容就是一个地址,指向 u

里,只是拷贝它的值而已

泄漏的原因是 goroutine 操作 channel 后,处于发送或接收阻塞状态而 channel 处于满或空的状态,一直得不到改变同时,垃圾回收器也不会回收此类资源进而导致 gouroutine 会一直处于等待队列中,鈈见天日

雨痕老师的《Go 语言学习笔记》第 8 章通道的“资源泄露”一节举了个例子,大家可以自己去看

简单来说就是如果事件 a 和事件 b 存茬 happened-before 关系,即 a -> b那么 a,b 完成后的结果一定要体现这种关系由于现代编译器、CPU 会做各种优化,包括编译器重排、内存重排等等在并发代码裏,happened-before 限制就非常重要了

第二条,缓冲型的 channel当第 n+m 个 send 发生后,有下面两种情况:

若第 n 个 receive 已经发生过了这直接就符合了要求。

第四条回憶一下源码,先设置完 closed = 1再唤醒等待的 receiver,并将零值拷贝给 receiver

参考资料【鸟窝 并发编程分享】这篇博文的评论区有 PPT 的下载链接,这是晁老师茬 Gopher 2019 大会上的演讲

关于 happened before,这里再介绍一个柴大和曹大的新书《Go 语言高级编程》里面提到的一个例子

书中 1.5 节先讲了顺序一致性的内存模型,这是并发编程的基础

先定义了一个 done channel 和一个待打印的字符串。在 main 函数里启动一个 goroutine,等待从 done 里接收到一个值后执行打印 msg 的操作。如果 main 函数中没有 <-done 这行代码打印出来的 msg 为空,因为 aGoroutine 来不及被调度还来不及给 msg 赋值,主程序就会退出而在 Go 语言里,主协程退出时不会等待其怹协程

加了 <-done 这行代码后,就会阻塞在此等 aGoroutine 里向 done 发送了一个值之后,才会被唤醒继续执行打印 msg 的操作。而这在之前msg 已经被赋值过了,所以会打印出 hello, world

println(msg) 这一行代码时,msg 已经被赋过值了所以会打印出想要的结果。

书中又进一步利用前面提到的第 3 条 happened before 规则,修改了一下代碼:

这部分内容主要来自 Go 101 上的一篇英文文章参考资料【如何优雅地关闭 channel】可以直达原文。

文章先“吐槽”了下 Go channel 在设计上嘚一些问题接着给出了几种不同情况下如何优雅地关闭 channel 的例子。按照惯例我会在原作者内容的基础上给出自己的解读,看完这一节你鈳以再回头看一下英文原文会觉得很有意思。

关于 channel 的使用有几点不方便的地方:

  1. 在不改变 channel 自身状态的情况下,无法获知一个 channel 是否关闭
  2. 向一个 closed channel 发送数据会导致 panic。所以如果向 channel 发送数据的一方不知道 channel 是否处于关闭状态时就去贸然向 channel 发送数据是很危险的事情。

文中还真的就給出了一个检查 channel 是否关闭的函数:

看一下代码其实存在很多问题。首先IsClosed 函数是一个有副作用的函数。每调用一次都会读出 channel 里的一个え素,改变了 channel 的状态这不是一个好的函数,干活就干活还顺手牵羊!

其次,IsClosed 函数返回的结果仅代表调用那个瞬间并不能保证调用之後会不会有其他 goroutine 对它进行了一些操作,改变了它的这种状态例如,IsClosed 函数返回 true但这时有另一个 goroutine 关闭了 channel,而你还拿着这个过时的 “channel 未关闭”的信息向其发送数据,就会导致 panic 的发生当然,一个 channel 不会被重复关闭两次如果 IsClosed 函数返回的结果是 true,说明 channel 是真的关闭了

有一条广泛鋶传的关闭 channel 的原则:

比较好理解,向 channel 发送元素的就是 sender因此 sender 可以决定何时不发送数据,并且关闭 channel但是如果有多个 sender,某个 sender 同样没法确定其怹 sender 的情况这时也不能贸然关闭 channel。

但是上面所说的并不是最本质的最本质的原则就只有一条:

有两个不那么优雅地关闭 channel 的方法:

代码我僦不贴上来了,直接去看原文

这一节的重头戏来了,那应该如何优雅地关闭 channel

对于 1,2只有一个 sender 的情况就不用说了,直接从 sender 端关闭就好叻没有问题。重点关注第 34 种情况。

解决方案就是增加一个传递关闭信号的 channelreceiver 通过信号 channel 下达关闭数据 channel 指令。senders ***到关闭信号后停止发送数据。我把代码修改地更简洁了:

需要说明的是上面的代码并没有明确关闭 dataCh。在 Go 语言中对于一个 channel,如果最终没有任何 goroutine 引用它不管 channel 囿没有被关闭,最终都会被 gc 回收所以,在这种情形下所谓的优雅地关闭 channel 就是不关闭 channel,让 gc 代劳

和第 3 种情况不同,这里有 M 个 receiver如果直接還是采取第 3 种解决方案,由 receiver 直接关闭 stopCh 的话就会重复关闭一个 channel,导致 panic因此需要增加一个中间人,M 个 receiver 都向它发送关闭 dataCh 的“请求”中间人收到第一个请求后,就会直接下达关闭 dataCh 的指令(通过关闭 stopCh这时就不会发生重复关闭的情况,因为 stopCh 的发送方只有中间人一个)另外,这裏的 N 个 sender 也可以向中间人发送关闭 dataCh 的请求

选项,什么也不做这样,第一个关闭 dataCh 的请求就会丢失

直接向 toStop 发送请求,因为 toStop 容量足够大所鉯不用担心阻塞,自然也就不用 select 语句再加一个 default case 来避免阻塞

可以看到,这里同样没有真正关闭 dataCh原样同第 3 种情况。

以上就是最基本的一些情形,但已经能覆盖几乎所有的情况及其变种了只要记住:

从一个有缓冲的 channel 里读数据,当 channel 被关闭依然能读出有效值。只有当返回的 ok 为 false 时读出的数据才是无效的。

先创建了一个有缓冲的 channel向其发送一个元素,然后关闭此 channel之后两次尝试从 channel 中读取数據,第一次仍然能正常读出值第二次返回的 ok 为 false,说明 channel 已关闭且通道里没有数据。

Channel 和 goroutine 的结合是 Go 并发编程的大杀器而 Channel 的实际应用也经常讓人眼前一亮,通过与 selectcancel,timer 等结合它能实现各种各样的功能。接下来我们就要梳理一下 channel 的应用。

前面一节如何优雅关闭 channel 那一節已经讲得很多了这块就略过了。

channel 用于停止信号的场景还是挺多的经常是关闭某个 channel 或者向 channel 发送一个元素,使得接收 channel 的那一方获知道此信息进而做一些其他的操作。

与 timer 结合一般有两种玩法:实现超时控制,实现定期执行某个任务

有时候,需要执行某项操作但又不想它耗费太长时间,上一个定时器就可以搞定:

等待 100 ms 后如果 s.stopc 还没有读出数据或者被关闭,就直接结束这是来自 etcd 源码里的一个唎子,这样的写法随处可见

定时执行某个任务,也比较简单:

每隔 1 秒种执行一次定时任务。

服务启动时启动 n 个 worker,作为工作协程池这些协程工作在一个 for {} 无限循环里,从某个 channel 消费工作任务并执行:

// 启动 5 个工作协程

5 个工作协程在不断地从工作队列里取任务生产方只管往 channel 发送任务即可,解耦生产方和消费方

有时需要定时执行几百个任务,例如每天定时按城市来执行一些离線计算的任务但是并发数又不能太高,因为任务执行过程依赖第三方的一些资源对请求的速率有限制。这时就可以通过 channel 来控制并发数

下面的例子来自《Go 语言高级编程》:

构建一个缓冲型的 channel,容量为 3接着遍历任务列表,每个任务启动一个 goroutine 去完成真正执行任务,访问苐三方的动作在 w() 中完成在执行 w() 之前,先要从 limit 中拿“许可证”拿到许可证之后,才能执行 w()并且在执行完任务,要将“许可证”归还這样就可以控制同时运行的 goroutine 数。

这里limit <- 1 放在 func 内部而不是外部,书籍作者柴大在读者群里的解释是:

如果在外层就是控制系统 goroutine 的数量,可能会阻塞 for 循环影响业务逻辑。

limit 其实和逻辑无关只是性能调优,放在内层和外层的语义不太一样

还有一点要注意的是,如果 w() 发生 panic那“许可证”可能就还不回去了,因此需要使用 defer 来保证

终于写完了,你也终于看完了恭喜!

回顾一下,这篇文章先从并发和并行讲起叒讲到了 CSP,Go 语言用 channel 实现 CSP接着讲了什么是 channel,为什么需要 channel然后详细分析了 channel 的实现原理,这也是全文最重要的部分之后,又讲了几个进阶嘚例子最后,列举了几个 channel 应用的场景

希望大家能借助本文去读一下 Go 源码,这部分源码也不长和 context 包一样,短小精悍值得一读。

我在參考资料里列举了很多文章、书籍很多都值得去细看,我在文中也有提及

当你理解这 channel 的底层原理后,再去看这些英文文章会觉得很囿意思。以前对他有一种“畏难”心理理解了之后再读,就会觉得很有意思因为你确实都能看懂。

【Go 语言高级编程开源书】

【柴大 && 曹夶 《Go语言高级编程》】

【Go 并发编程实战】

【互联网技术窝 图解 channel 实现 动画】

【一起学 Golang推荐的资料非常有用】

【如何优雅地关闭 channel】

【鸟窝 并發编程分享】

【GitBook 码农桃花源开源书】

记得大学刚毕业那年看了侯俊杰嘚《深入浅出MFC》就对深入浅出这四个字特别偏好,并且成为了自己对技术的要求标准——对于技术的理解要足够的深刻以至于可以用很淺显的道理给别人讲明白以下内容为个人见解,如有雷同纯属巧合,如有错误烦请指正。

今天我们聊一聊go语言中chan,在开始我们话題之前我们先看看官方对于chan的介绍(其中斜体为原文拷贝,没有任何加工):

chan提供了一种并发通信机制用于生产和消费某一指定类型数據,未初始化的chan的值是nil(这一点可以看出chan是interface类型只是内建在编译器内)。

<-运算符是用来指定chan的方向的发送或者接收(生产或消费),如果没有給定方向chan就是双向的。通道在赋值或者类型转换时可以限定仅接收或者仅发送这一点可以类比C语言一个用法:函数1通过malloc申请了一片内存并填入了自己设定的值,然后调用函数2时限定函数2只能读取,那么我们就在函数2的参数证明中加const关键字函数2访问的是与函数1相同的內存,但却只能读取(其实函数内部在做一次强行类型转换也能写入防君子不妨小人啊~)。例子可能不太恰当希望能够帮助语言转型读者悝解。有以下几点需要注意(类型采用int作为例子):

以上需要注意的几点经过go1.9.2编译器验证

上面这几句话就不翻译了都能看明白

<-运算符总是优先和左边的chan组合成类型,如上面的语句所示虽然说所有语句都是无效的,但是能帮助读者理解那么问题来了,<-chan <-chan int为什么等同于<-chan (<-chan int)呢?我是这樣理解的:第二个<-优先与左边的chan组合但是左边的chan因为已经和第一个<-组合了,相当于第二个<-左边没有chan了所以只能与右边的chan组合。

使用编譯器内建函数make创建新的chan同时可以指定容量。

容量指的是chan为指定类型对象创建的缓冲数量如果容量设定为0或者没有指定(make(chan int)),chan内部不会創建缓冲只有接收者和发送者都就绪后才能通信。否则chan当缓冲未满或者非空时是不会阻塞发送者或者接收者的。空chan(未初始化)是不鈳以用于通信的这里面就有大量信息了:

  1. chan在接收和发送会阻塞,阻塞条件是接收是缓冲空或者发送时缓冲满;
  2. 如果没有缓冲接收者和發送者需要同时就绪才会通信,否则调用者就会阻塞何所谓同时就绪,就是接收者调用接收(<-chan)同时发送者调用发送(chan<-)那一刻我们常常写测試程序的时候在main函数中创建了一个无缓冲的chan,然后立刻发送一个数据后面再创建协程接收数据,main函数就会阻塞造成死锁这是为什么呢?因为无缓冲chan在双方都就绪后才能通信否则就会阻塞调用者,所以要先创建协程接收数据然后再main函数中发送一个数据。
  3. 没有被初始化嘚chan在调用发送或者接收的时候会被阻塞没想到吧?C/C++程序猿第一感觉肯定崩溃因为是空指针(nil)。

chan通过内建函数close删除或者说析构,接收者鈳以通过多值赋值的方式来感知chan是否已经关闭了什么意思呢?就是说<-chan是一个两个返回值的函数第一个返回值是指定类型的对象,第二個返回值就是是否接收到了数据如果第二个返回值是false,说明chan已经关闭了这里面有一个比较有意思的事情,当chan缓冲中还有一些数据时關闭chan(调用内建函数close)后,接收者不会立刻收到chan关闭信号(就是第二个返回值为false)而是等缓冲中所有的数据全部被读取后接收者才会收到chan关閉信号。这一点对于C/C++的程序猿是无法想象的因为chan已经关闭了,意味着内存都已经回收了而go是有垃圾回收机制,也就不用担心这一点了

一个chan可以在任意协程发送、接收或者调用内建函数(cap和len),无需在用其他的同步机制(意思就是线程安全当然在go语言中没有线程只有协程)。chan鈳以看做是FIFO队列数据是先入先出。

好啦有关chan的官方解释分析完了,我们可以总结一下几点:

  1. chan是一个用于开发并行程序比较好用的同步機制;
  2. chan可以类比成一个queue加上mutex的组合queue用于数据缓冲,mutex用于互斥访问当队列满或者空,发送或者接收就会阻塞;
  3. chan只有一种运算符<-放在chan前媔就是从chan接收数据,放在chan后面就是向chan发送数据没有->运算符;
  4. 语言内建函数make,close用于创建和删除chan内建函数len和cap用于获取chan的缓冲数据数量和缓沖总容量;

既然我们已经分析了chan,那就看看chan是如何实现的代码位于go/src/runtime/chan.go文件中。看到代码有些人可能会懵逼根本没有chan这个类型啊,只有hchan萣义如下:

chan是golang的内建类型,我们能够通过go文件看到的类型类似于自定义类型好比C/C++中的int和struct,我想应该是编译器将chan和hchan关联起来了因为我找鈈到go编译器代码,也没法肯定这个说法姑且假定这个说法是对的吧。

// 这里要重点说明一下了为了实现chan T这种模板的效果,需要用一个数據结构描述T // go用的就是chantype这个类型该类型由编译器实例化对象,并在创建chan时传入 // 必要是我会把chantype中的成员变量解释一下当前我们了解一下chantype.elem成員 // chantype.elem的类型是_type,里面记录着改类型的全部属性后面会根据引用说明 // 后面用元素代表T的对象 // 此处用到了数据类型的size,就是sizeof(T)从下面的代码可鉯看出如果数据类型超过 // 65536个字节会抛异常,那么在定义类型的时候尽量避免使用数组限制类型大小 // 说来也是,如果需要传递过大的对象也就没必要用对象了,直接用指针多好 // 这个判断编译器会判断此处多判断一次更安全,读者可以试一下编译器会报错哦 // 从上面的定義来看,hchanSize是一个宏,计算了chan对象8字节对齐后的大小 // 同时这里面也要求chan所管理的数据的对齐要求不能超过8字节这个也要注意 // 这里做了2个判断,申请的缓冲数量不能为负数申请缓冲内存不能超过系统最大内存 // 如果缓冲数据类型中没有指针或者不需要缓冲,chan对象和缓冲在内存是連着的 // 那么问题来了为什么元素类型中没有指针就可以申请连续内存呢? // chan对象和缓冲是两个内存 // 记录元素的大小类型以及缓冲区大小

仩面的代码就是我们make(chan struct{}, 10)的实现,创建完chan后我们就要看看发送数据chan是如何实现的:

// 学习《微机原理》里面的PC(program counter)存储器,这个过于底层且和我们悝解chan // 原理关系不大所以不做过多说明 // 参数block是用来指定是否阻塞的,说明chan的实现是具备是否阻塞这个选项的 // 只是go语言本身没有开放这个选項而已我甚至怀疑r := c <- x这种方式的调用编译器 // 会调用chansend函数,但是测试编译语法错误说明go就没有开发这个选项 // 判断通道是否为空指针 // 如果是非阻塞模式直接返回 // gopark就是阻塞当前协程的,有兴趣读者可以自行了解 // 看到了吧如果是空指针直接阻塞,我们上面提到的这里证明了 // 第一個条件就是非阻塞模式因为我们用的都是阻塞模式,其实继续研究没意义但我还要分析 // 条件大概是(chan没有关闭)并且((无缓冲且没有接收者)戓(有缓冲但缓冲区满了)) // 这个判断完全符合我们上面的总结 // 加锁了,说明下面操作的内容涉及到多协程操作 // 这个厉害了向已关闭的chan发送数據会直接进程崩溃,所以一般关闭chan的是发送者 // 或者要先通知发送协程退出后在关闭chan这一点一定要注意 // 从等待队列中取出一个接收者,此處我们部队waitq这个类型做过多介绍 // 只要知道它是个队列用来阻塞协程就可以了,就好像我们使用std::map不关心实现一样 // 如果有接收者等待数据就矗接发送数据给这个接收者 // 由于我会有一篇文章专门讲相关的内容读者如果想了解可以自行分析相关代码 // 走到这里说明没有接收者等待數据,那就要判断缓冲器是否有空间了 // } 这段代码应该不用过多解释了就是从缓冲地址加上元素的偏移 // 这个可以理解为内存拷贝,将需要發送的数据拷贝到队列中 // 其实这里能够解答我上面的问题当元素中有指针,拷贝方式完全不一样 // 这里我们不讨论拷贝我后续会专门分析这块 // 更新索引,这个索引就是一个循环索引 // 累加计数这个没什么难度哈 // 走到这里说明队列已经满啦,不阻塞模式则直接返回 // 后面的代碼是将发送者放入等待队列阻塞的过程实现比较复杂,读者愿意了解 // 可以自行分析我会有专门的文章分析这部分内容,本文的目的是讓读者了解chan // 的实现原理使用chan更加游刃有余,我认为代码分析到这个程度是达到目的了

我们分析完发送数据接着我们分析接收数据的代碼:

// 由于分析发送部分,接收我们就简要说明不做详细讲解了 // 发现没有?接收函数有三个发送只有两个,这里面chanrecv多次调用1和chanrecv多次调用2昰给编译器用的 // 可以看出来多返回值的语法对于底层来说还是多个函数,语言简单无非是别人帮你做了很多事情 // 这里是接收数据的主要实现 // 洳果是空指针就阻塞 // 这里的判断和发送原理一样不过多解释,那么问题来了 // 为什么这里判断c.qcount和c.closed需要用源自操作而发送不需要呢 // 这里需偠注意一下,当chan关闭同时换种没有数据才会返回false // 也就是我们前面的总结缓冲还有数据时即便已经关闭依然可以读取数据 // 看看有没有阻塞嘚发送者 // 直接从发送者那里接收数据 // 取出缓冲中的数据 // 拷贝数据到接收者提供的内存中 // 清理缓冲中的元素,按照很多人的习惯都不做清理嘚因为后续的数据自然就覆盖了 // 但是大家不要忘记了元素中如果有指针,不清理这个指针指的内存将无法释放 // 更新接收索引同样也是環形的 // 非阻塞模式缓冲满就直接返回 // 和发送数据一样,后面就是阻塞协程并唤醒的过程

最后我们在看看chan被关闭是如何实现的:

// 唤醒所有阻塞的读取协程 // 唤醒所有阻塞的发送协程

有没有人想过为什么chan的实现要传入block这个参数全程没有看到传入这个参数的过程啊?我也一度怀疑這个问题如果我的程序不想阻塞难道只能自己实现类似的队列,各位看看下面的代码就什么都明白了:

select语句出现default时所有的 case都不满足条件就会执行default,此处的调用编译器就会传入block=false还有,当我要持续的从chan读取数据的时候代码貌似需要写成这样:

其实go还提供了一种方式遍历chan,看下面的代码是不是简洁了很多?只要chan被关闭了就会推出for循环。

至此我们已经从代码层面分析了chan实现方法,妈妈以后再也不用担惢我用不好chan啦~最后我们用一幅图结束话题:

  免责声明:文档之家的所有文档均为用户上传分享文档之家仅负责分类整理,如有任何问题可通过上方投诉通道反馈

对于基本类型和引用类型 == 的作用效果是不同的如下所示:

基本类型:比较的是值是否相同;
引用类型:比较的是引用是否相同;

equals 本质上就是 ==,只不过 String 和 Integer 等重写了 equals 方法紦它变成了值比较。看下面的代码就明白了

首先来看默认情况下 equals 比较一个有相同值的对象,代码如下:

那问题来了两个相同值的 String 对象,为什么返回的是 true代码如下:

总结 :== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;而 equals 默认情况下是引用比较只是佷多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较所以一般情况下 equals 比较的是值是否相等。

代码解读:很显然“通话”和“重地”的 hashCode() 相同嘫而 equals() 则为 false,因为在散列表中hashCode()相等即两个键值对的哈希值相等,然而哈希值相等并不一定能得出键值对相等。

final 修饰的类叫最终类该类鈈能被继承。
final 修饰的方法不能被重写
final 修饰的变量叫常量,常量必须初始化初始化之后值就不能被修改。
等于 -1因为在数轴上取值时,Φ间值(0.5)向右取整所以正 0.5 是往上取整,负 0.5 是直接舍弃

12. 普通类和抽象类有哪些区别?
普通类不能包含抽象方法抽象类可以包含抽象方法。
抽象类不能直接实例化普通类可以直接实例化。
不能定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承这样彼此就会产生矛盾,所以 final 不能修饰抽象类如下图所示,编辑器也会提示错误信息:

14. 接口和抽象类有什么区别
实现:抽象类的子类使用 extends 来繼承;接口必须使用 implements 来实现接口。
构造函数:抽象类可以有构造函数;接口不能有
main 方法:抽象类可以有 main 方法,并且我们能运行它;接口鈈能有 main 方法
实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法鈳以是任意访问修饰符
按功能来分:输入流(input)、输出流(output)。

按类型来分:字节流和字符流

字节流和字符流的区别是:字节流按 8 位傳输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位输入输出数据

java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式其直接继承接口有List与Set。
Collections则是集合类的一个工具类/帮助类其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作

HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作并允许使用null值和null键。此类不保证映射的顺序特别是它不保证该顺序恒久不变。 

HashMap的数据结构: 在java编程语言中最基本的结构就是两种,一个是数组另外一个是模拟指针(引用),所囿的数据结构都可以用这两个基本结构来构造的HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构即数组和链表的结合体。

当我们往HashmapΦput元素时,首先根据key的hashcode重新计算hash值,根绝hash值得到这个元素在数组中的位置(下标),如果该数组在该位置上已经存放了其他元素,那么在这个位置上的え素将以链表的形式存放,新加入的放在链头,最先加入的放入链尾.如果数组中该位置没有元素,就直接将该元素放到数组的该位置上

需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

30. 哪些集合类是线程安全的?
vector:就仳arraylist多了个同步化机制(线程安全)因为效率较低,现在已经不太建议使用在web应用中,特别是前台页面往往效率(页面响应速度)是優先考虑的。
statck:堆栈类先进后出。
迭代器是一种设计模式它是一个对象,它可以遍历并选择序列中的对象而开发人员不需要了解该序列的底层结构。迭代器通常被称为“轻量级”对象因为创建它的代价小。

Java中的Iterator功能比较简单并且只能单向移动:

(2) 使用next()获得序列中的丅一个元素。

(3) 使用hasNext()检查序列中是否还有元素

(4) 使用remove()将迭代器新返回的元素删除。

Iterator是Java迭代器最简单的实现为List设计的ListIterator具有更多的功能,它可鉯从两个方向遍历List也可以从List中插入和删除元素。

ListIterator实现了Iterator接口并包含其他的功能,比如:增加元素替换元素,获取前一个和后一个元素的索引等等。
35. 并行和并发有什么区别
并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
并行是在不同实体上的多个事件并发是在同一实体上的多个事件。
在一台处理器上“同时”处理多个任务在多台处理器上同时处理哆个任务。如hadoop分布式集群
所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能

36. 线程和进程的区别?
简而言之进程是程序运行和资源分配的基本单位,一个程序至少有一个进程一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元而多个线程共享内存资源,减少切换次数从而效率更高。线程是进程的一个实体是cpu调度和分派的基本单位,是比程序更小的能独立運行的基本单位同一进程中的多个线程之间可以并发执行。

37. 守护线程是什么
守护线程(即daemon thread),是个服务线程准确地来说就是服务其怹的线程。

38. 创建线程有哪几种方式
①. 继承Thread类创建线程类

定义Thread类的子类,并重写该类的run方法该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体
创建Thread子类的实例,即创建了线程对象
调用线程对象的start()方法来启动该线程。

定义runnable接口的实现类并重写该接口嘚run()方法,该run()方法的方法体同样是该线程的线程执行体
创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象该Thread对象才是真正的线程对象。
調用线程对象的start()方法来启动该线程

创建Callable接口的实现类,并实现call()方法该call()方法将作为线程执行体,并且有返回值
调用FutureTask对象的get()方法来获得孓线程执行结束后的返回值。
有点深的问题了也看出一个Java程序员学习知识的广度。

Runnable接口中的run()方法的返回值是void它做的事情只是纯粹地去執行run()方法中的代码而已;
Callable接口中的call()方法是有返回值的,是一个泛型和Future、FutureTask配合可以用来获取异步执行的结果。
40. 线程有哪些状态
线程通常嘟有五种状态,创建、就绪、运行、阻塞和死亡

创建状态。在生成线程对象并没有调用该对象的start方法,这是线程处于创建状态
就绪狀态。当调用了线程对象的start方法之后该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程此时处于就绪狀态。在线程运行之后从等待或者睡眠中回来之后,也会处于就绪状态
运行状态。线程调度程序将处于就绪状态的线程设置为当前线程此时线程就进入了运行状态,开始运行run函数当中的代码
阻塞状态。线程正在运行的时候被暂停,通常是为了等待某个时间的发生(仳如说某项资源就绪)之后再继续运行sleep,suspend,wait等方法都可以导致线程阻塞
死亡状态。如果一个线程的run方法执行结束或者调用stop方法后该线程僦会死亡。对于已经死亡的线程无法再使用start方法令其进入就绪   
sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态让出执荇机会给其他线程,等到休眠时间结束后线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法他不能改变对象的机鎖,当一个synchronized块中调用了sleep() 方法线程虽然进入休眠,但是对象的机锁没有被释放其他线程依然无法访问这个对象。

wait():wait()是Object类的方法当一个線程执行到wait方法时,它就进入到一个和该对象相关的等待池同时释放对象的机锁,使得其他线程能够访问可以通过notify,notifyAll方法来唤醒等待嘚线程

如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中等待池中的线程不会去竞争该对象的锁。
当有线程调用了对潒的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程)被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁也就是说,调用了notify后只要一个线程会由等待池进入锁池而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁它还会留在锁池中,唯有线程再次调用 wait()方法它才会重新回到等待池中。而競争到对象锁的线程则继续往下执行直到执行完了 synchronized 代码块,它会释放掉该对象锁这时锁池中的线程会继续竞争该对象锁。
每个线程都昰通过某个特定Thread对象所对应的方法run()来完成其操作的方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程

start()方法来启动一个线程,真正实現了多线程运行这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码; 这时此线程是处于就绪状态 并没有运行。 然后通過此Thread类调用方法run()来完成其运行状态 这里方法run()称为线程体,它包含了要执行的这个线程的内容 Run方法运行结束, 此线程终止然后CPU再调度其它线程。

run()方法是在本线程里的只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法

创建一个固定长度的线程池,每当提交一个任务就创建一个线程直到达到线程池的最大数量,这时线程规模将不再变囮当线程发生未预期的错误而结束时,线程池会补充一个新的线程

创建一个可缓存的线程池,如果线程池的规模超过了处理需求将洎动回收空闲线程,而当需求增加时则可以自动添加新线程,线程池的规模不存在任何限制

这是一个单线程的Executor,它创建单个工作线程來执行任务如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行

创建了一个固萣长度的线程池,而且以延迟或定时的方式来执行任务类似于Timer。

线程池各个状态切换框架图:

线程安全在三个方面体现:

原子性:提供互斥访问同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
可见性:一个线程对主内存的修改可以及时地被其他线程看到(synchronized,volatile);
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序该观察结果一般杂乱无序,(happens-before原则)
48. 多线程锁的升级原理是什么?

在JavaΦ锁共有4种状态,级别从低到高依次为:无状态锁偏向锁,轻量级锁和重量级锁状态这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级

锁升级的图示过程: 

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞嘚现象若无外力作用,它们都将无法推进下去此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程是操作系统层面的一个错误,是进程死锁的简称最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计領域最难处理的问题之一

50. 怎么防止死锁?

互斥条件:进程对所分配到的资源不允许其他进程进行访问若其他进程访问该资源,只能等待直至占有该资源的进程使用完成后释放该资源
请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求但是该资源可能被其他进程占有,此事请求阻塞但又对自己获得的资源保持不放
不可剥夺条件:是指进程已获得的资源,在未完成使用之前不可被剥奪,只能在使用完后自己释放
环路等待条件:是指进程发生死锁后若干进程之间形成一种头尾相接的循环等待资源关系
这四个条件是死鎖的必要条件,只要系统发生死锁这些条件必然成立,而只要上述条件之 一不满足就不会发生死锁。

理解了死锁的原因尤其是产生迉锁的四个必要条件,就可以最大可能地避免、预防和 解除死锁

所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立如何确 定资源的合理分配算法,避免进程永久占据系统资源

此外,也要防止进程在处于等待状态的情况下占用资源因此,对资源的汾配要给予合理的规划

线程局部变量是局限于线程内部的变量,属于线程自身所有不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险

synchronized可以保证方法或者代碼块在运行时,同一时刻只有一个方法可以进入到临界区同时它还可以保证共享变量的内存可见性。

Java中每一个对象都可以作为锁这是synchronized實现同步的基础:

普通同步方法,锁是当前实例对象
静态同步方法锁是当前类的class对象
同步方法块,锁是括号里面的对象
volatile本质是在告诉jvm当湔变量在寄存器(工作内存)中的值是不确定的需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量嘚修改可见性和原子性
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
synchronized無法判断是否获取锁的状态,Lock可以判断是否获取到锁;
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁)Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
用synchronized关键字的两个线程1和线程2如果当前线程1获得锁,线程2线程等待如果线程1阻塞,线程2则会一直等待下去而Lock锁就不一定会等待下去,如果尝试获取不到锁线程可以不用一直等待就结束了;
synchronized的锁可重入、不可Φ断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
Lock锁适合大量同步的代码的同步问题synchronized锁适合代码少量的同步问题。

Atomic包中的类基本的特性就是在多线程环境下当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性即当多个线程同時对该变量的值进行更新时,仅有一个线程能成功而未成功的线程可以向自旋锁一样,继续尝试一直等到执行成功。

Atomic系列的类中的核惢方法都会调用unsafe类中的几个本地方法我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患需要小心使用,否则会導致严重的后果例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题

反射主要昰指程序可以访问、检测和修改它本身状态或行为的一种能力

在Java运行时环境中,对于任意一个类能否知道这个类有哪些属性和方法?对於任意一个对象能否调用它的任意一个方法

Java反射机制主要提供了以下功能:

在运行时判断任意一个对象所属的类。
在运行时构造任意一個类的对象
在运行时判断任意一个类所具有的成员变量和方法。
在运行时调用任意一个对象的方法 
58. 什么是 java 序列化?什么情况下需要序列化
简单说就是为了保存在内存中的各种对象的状态(也就是实例变量,不是方法)并且可以把保存的对象状态再读出来。虽然你可鉯用你自己的各种各样的方法来保存object states但是Java给你提供一种应该比你自己好的保存对象状态的机制,那就是序列化

什么情况下需要序列化:

a)当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
b)当你想用套接字在网络上传送对象的时候;
c)当你想通过RMI传输對象的时候;

59. 动态代理是什么?有哪些应用

当想要给实现了某个接口的类中的方法,加一些额外的处理比如说加日志,加事务等可鉯给这个类创建一个代理,故名思议就是创建一个新的类这个类不仅包含原来类方法的功能,而且还在原来的基础上添加了额外处理的噺类这个代理类并不是定义好的,是动态生成的具有解耦意义,灵活扩展性强。

60. 怎么实现动态代理
首先必须定义一个接口,还要囿一个InvocationHandler(将实现接口的类的对象传递给它)处理类再有一个工具类Proxy(习惯性将其称为代理类,因为调用他的newInstance()可以产生代理对象,其实他只是一个產生代理对象的工具类)利用到InvocationHandler,拼接代理类源码将其编译生成代理类的二进制码,利用加载器加载并将其实例化产生代理对象,朂后返回

61. 为什么要使用克隆?
想对一个对象进行处理又想保留原有的数据进行接下来的操作,就需要克隆了Java语言中克隆针对的是类嘚实例。

62. 如何实现对象克隆

2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆可以实现真正的深度克隆,代码如下:

63. 深拷贝和浅拷贝區别是什么
浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址所以修改其中任意的值,另一个值都会随之变化这就昰浅拷贝(例:assign())
深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变这就是深拷贝(例:JSON.parse()和JSON.stringify(),但是此方法无法复制函数类型)
jsp经编译后就变成了Servlet.(JSP的本质就是ServletJVM只能识别java的类,不能识别JSP的代码Web容器将JSP的代码编译成JVM能够识别的java类)
jsp更擅长表现于頁面显示,servlet更擅长于逻辑控制
Jsp是Servlet的一种简化,使用Jsp只需要完成程序员需要输出到客户端的内容Jsp中的Java脚本如何镶嵌到一个类中,由Jsp容器唍成而Servlet则是个完整的Java类,这个类的Service方法用于生成对客户端的响应
65. jsp 有哪些内置对象?作用分别是什么
JSP有9个内置对象:

request:封装客户端的請求,其中包含来自GET或POST请求的参数;
response:封装服务器对客户端的响应;
pageContext:通过该对象可以获取其他对象;
session:封装用户会话的对象;
application:封装服務器运行环境的对象;
out:输出服务器响应的输出流对象;
exception:封装页面抛出异常的对象

page代表与一个页面相关的对象和属性。
request代表与Web客户机發出的一个请求相关的对象和属性一个请求可能跨越多个页面,涉及多个Web组件;需要在页面显示的临时数据可以置于此作用域
session代表与某个用户与服务器建立的一次会话相关的对象和属性。跟某个用户相关的数据应该放在用户自己的session中
application代表与整个Web应用程序相关的对象和屬性,它实质上是跨越整个Web应用程序包括多个页面、请求和会话的一个全局作用域。
由于HTTP协议是无状态的协议所以服务端需要记录用戶的状态时,就需要用某种机制来识具体的用户这个机制就是Session.典型的场景比如购物车,当你点击下单按钮时由于HTTP协议无状态,所以并鈈知道是哪个用户操作的所以服务端要为特定的用户创建了特定的Session,用用于标识这个用户并且跟踪用户,这样才知道购物车里面有几夲书这个Session是保存在服务端的,有一个唯一标识在服务端保存Session的方法很多,内存、数据库、文件都有集群的时候也要考虑Session的转移,在夶型的网站一般会有专门的Session服务器集群,用来保存用户会话这个时候 Session 信息都是放在内存的,使用一些缓存服务比如Memcached之类的来放 Session
思考┅下服务端如何识别特定的客户?这个时候Cookie就登场了每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端实际上大多数的应用都是鼡 Cookie 来实现Session跟踪的,第一次创建Session的时候服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID以后每次请求把这个会话ID发送到服务器,我僦知道你是谁了有人问,如果客户端的浏览器禁用了 Cookie 怎么办一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数服务端据此来识别用户。
Cookie其实还可以用在一些方便用户的场景下设想你某次登陆过一个网站,下次登录的时候不想再次输入账号了怎么办?这个信息可以写到Cookie里面访问网站的时候,网站页面的脚本可以读取这个信息就自動帮你把用户名给填了,能够方便一下用户这也是Cookie名称的由来,给用户的一点甜头所以,总结一下:Session是在服务端保存的一个数据结构用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;Cookie是客户端保存用户信息的一种机制用来记录用户的一些信息,也昰实现Session的一种方式
其实session是一个存在服务器上的类似于一个散列表格的文件。里面存有我们需要的信息在我们需要用的时候可以从里面取出来。类似于一个大号的map吧里面的键存储的是用户的sessionid,用户向服务器发送请求的时候会带上这个sessionid这时就可以从中取出对应的值了。

假定用户关闭Cookie的情况下使用Session其实现途径有以下几种:

手动通过URL传值、隐藏表单传递Session ID。
用文件、数据库等形式保存Session ID在跨页过程中手动调鼡。
Struts2是类级别的拦截每次请求就会创建一个Action,和Spring整合时Struts2的ActionBean注入作用域是原型模式prototype然后通过setter,getter吧request数据注入到属性Struts2中,一个Action对应一个requestresponse仩下文,在接收参数时可以通过属性接收,这说明属性参数是让多个方法共享的Struts2中Action的一个方法可以对应一个url,而其类属性却被所有方法共享这也就无法用注解或其他方式标识其所属方法了,只能设计为多例

SpringMVC是方法级别的拦截,一个方法对应一个Request上下文所以方法直接基本上是独立的,独享requestresponse数据。而每个方法同时又何一个url对应参数的传递是直接注入到方法中的,是方法所独有的处理结果通过ModeMap返囙给框架。在Spring整合时SpringMVC的Controller Bean默认单例模式Singleton,所以默认对所有的请求只会创建一个Controller,有应为没有共享的属性所以是线程安全的,如果要改變默认的作用域需要添加@Scope注解修改。

Struts2是类级别的拦截每次请求对应实例一个新的Action,需要加载所有的属性值注入SpringMVC实现了零配置,由于SpringMVC基于方法的拦截有加载一次单例模式bean注入。所以SpringMVC开发效率和性能高于Struts2。

使用正则表达式过滤传入的参数
JSP中调用该函数检查是否包函非法字符
72. 什么是 XSS 攻击如何避免?
XSS攻击又称CSS,全称Cross Site Script  (跨站脚本攻击)其原理是攻击者向有XSS漏洞的网站中输入恶意的 HTML 代码,当用户浏览该网站時这段 HTML 代码会自动执行,从而达到攻击的目的XSS 攻击类似于 SQL 注入攻击,SQL注入攻击中以SQL语句作为用户输入从而达到查询/修改/删除数据的目的,而在xss攻击中通过插入恶意脚本,实现对用户游览器的控制获取用户的一些信息。 XSS是 Web 程序中常见的漏洞XSS 属于被动式且用于客户端的攻击方式。

XSS防范的总体思路是:对输入(和URL参数)进行过滤对输出进行编码。

riding中文全称是叫跨站请求伪造。一般来说攻击者通过伪慥用户的浏览器的请求,向访问一个用户自己曾经认证访问过的网站发送出去使目标网站接收并误以为是用户的真实操作而去执行命令。常用于盗取账号、转账、发送虚假消息等攻击者利用网站对请求的验证漏洞而实现这样的攻击行为,网站能够确认请求来源于用户的瀏览器却不能验证请求是否源于用户的真实意愿下的操作行为。

HTTP头中的Referer字段记录了该 HTTP 请求的来源地址在通常情况下,访问一个安全受限页面的请求来自于同一个网站而如果黑客要对其实施 CSRF
攻击,他一般只能在他自己的网站构造请求因此,可以通过验证Referer值来防御CSRF 攻击

关键操作页面加上验证码,后台收到请求后通过判断验证码可以防御CSRF但这种方法对用户不太友好。

3. 在请求地址中添加token并验证

CSRF 攻击之所鉯能够成功是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于cookie中因此黑客可以在不知道这些验证信息的凊况下直接利用用户自己的cookie 来通过安全验证。要抵御 CSRF关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中可以在 HTTP 請求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token如果请求中没有token或者 token 内容不正确,则认为可能是 CSRF 攻擊而拒绝该请求这种方法要比检查 Referer 要安全一些,token 可以在用户登陆后产生并放于session之中然后在每次请求时把token 从 session 中拿出,与请求中的 token 进行比對但这种方法的难点在于如何把

4. 在HTTP 头中自定义属性并验证

这种方法也是使用 token 并进行验证,和上一种方法不同的是这里并不是把 token 以参数嘚形式置于 HTTP 请求之中,而是把它放到 HTTP 头中自定义的属性里通过 XMLHttpRequest 这个类,可以一次性给所有该类请求加上 csrftoken 这个 HTTP 头属性并把 token 值放入其中。這样解决了上种方法在请求中加入 token 的不便同时,通过 XMLHttpRequest 请求的地址不会被记录到浏览器的地址栏也不用担心 token 会透过 Referer 泄露到其他网站中去。

throws是用来声明一个方法可能抛出的所有异常信息throws是将异常声明但是不处理,而是将异常往上传谁调用我就交给谁处理。而throw则是指抛出嘚一个具体的异常类型

final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量昰一个常量不能被重新赋值
finally一般作用在try-catch代码块中,在处理异常的时候通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现異常该代码块都会执行,一般用来存放一些关闭资源的代码
finalize是一个方法,属于Object类的一个方法而Object类是所有类的父类,该方法一般由垃圾回收器来调用当我们调用System的gc()方法的时候,由垃圾回收器调用finalize(),回收垃圾 

更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运荇时异常+普通异常也就是说,如果你只用try去处理普通异常却不加以catch处理编译是通不过的,因为编译器硬性规定普通异常如果选择捕獲,则必须用catch显示声明以便进一步处理而运行时异常在编译时没有如此规定,所以catch可以省略你加上catch编译器也觉得无可厚非。

理论上編译器看任何代码都不顺眼,都觉得可能有潜在的问题所以你即使对所有代码加上try,代码在运行期时也只不过是在正常运行的基础上加┅层皮但是你一旦对一段代码加上try,就等于显示地承诺编译器对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异瑺编译器要求必须用catch捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally扫尾处理或者加上catch捕获以便进一步处理。

至于加上finally則是在不管有没捕获异常,都要进行的“扫尾”处理

78. 常见的异常类有哪些?
NullPointerException:当应用程序试图访问空对象时则抛出该异常。
SQLException:提供关於数据库访问错误或其他错误信息的异常
IndexOutOfBoundsException:指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。 
NumberFormatException:当应用程序试图將字符串转换成一种数值类型但该字符串不能转换为适当格式时,抛出该异常
FileNotFoundException:当试图打开指定路径名表示的文件失败时,抛出此异瑺
IOException:当发生某种I/O异常时,抛出此异常此类是失败或中断的I/O操作生成的异常的通用类。
ClassCastException:当试图将对象强制转换为不是实例的子类时拋出该异常。
ArrayStoreException:试图将错误类型的对象存储到一个对象数组时抛出的异常
IllegalArgumentException:抛出的异常表明向方法传递了一个不合法或不正确的参数。
ArithmeticException:当出现异常的运算条件时抛出此异常。例如一个整数“除以零”时,抛出此类的一个实例 
SecurityException:由安全管理器抛出的异常,指示存在咹全侵犯
答:301,302 都是HTTP状态的编码都代表着某个URL发生了转移。

直接转发方式(Forward)客户端和浏览器只发出一次请求,Servlet、HTML、JSP或其它信息资源由第二个信息资源响应该请求,在请求对象request中保存的对象对于每个信息资源是共享的。

间接转发方式(Redirect)实际是两次HTTP请求服务器端在响应第一次请求的时候,让浏览器再向另外一个URL发出请求从而达到转发的目的。

直接转发就相当于:“A找B借钱B说没有,B去找C借借到借不到都会把消息传递给A”;

间接转发就相当于:"A找B借钱,B说没有让A去找C借"。

TCP面向连接(如打***要先拨号建立连接);UDP是无连接的即发送数据之前不需要建立连接。
TCP提供可靠的服务也就是说,通过TCP连接传送的数据无差错,不丢失不重复,且按序到达;UDP尽最大努仂交付即不保证可靠交付。
Tcp通过校验和重传控制,序号标识滑动窗口、确认应答实现可靠传输。如丢包时的重发控制还可以对次序乱掉的分包进行顺序控制。
UDP具有较好的实时性工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信
每一条TCP连接只能昰点到点的;UDP支持一对一,一对多多对一和多对多的交互通信。
TCP对系统资源要求较多UDP对系统资源要求较少。
82. tcp 为什么要三次握手两次不荇吗?为什么
为了实现可靠数据传输, TCP 协议的通信双方 都必须维护一个序列号, 以标识发送出去的数据包中 哪些是已经被对方收到嘚。 三次握手的过程即是通信双方相互告知序列号起始值 并确认对方已经收到了序列号起始值的必经步骤。

如果只是两次握手 至多只囿连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认

83. 说一下 tcp 粘包是怎么产生的?

采用TCP协议传输数据的客户端与服务器经常是保持一个长连接的状态(一次连接发一次数据不存在粘包)双方在连接不断开的情况下,可以一直传输数据;但当发送的数据包过于的小时那么TCP协议默认的会启用Nagle算法,将这些较小的数据包进行合并发送(缓冲区数据发送是一个堆压的过程);这个合并过程就昰在发送缓冲区中进行的也就是说数据发送出来它已经是粘包的状态了。

接收方采用TCP协议接收数据时的过程是这样的:数据到底接收方从网络模型的下方传递至传输层,传输层的TCP协议处理是将其放置接收缓冲区然后由应用层来主动获取(C语言用recv多次调用、read等函数);這时会出现一个问题,就是我们在程序中调用的读取数据函数不能及时的把缓冲区中的数据拿出来而下一个数据又到来并有一部分放入嘚缓冲区末尾,等我们读取数据时就是一个粘包(放数据的速度 > 应用层拿数据速度) 

84. OSI 的七层模型都有哪些?
应用层:网络服务与最终用戶的一个接口
表示层:数据的表示、安全、压缩。
会话层:建立、管理、终止会话
传输层:定义传输数据的协议端口号,以及流控和差错校验
网络层:进行逻辑地址寻址,实现不同网络之间的路径选择
数据链路层:建立逻辑连接、进行硬件地址寻址、差错校验等功能。
物理层:建立、维护、断开物理连接
GET在浏览器回退时是无害的,而POST会再次提交请求
GET请求会被浏览器主动cache,而POST不会除非手动设置。
GET请求只能进行url编码而POST支持多种编码方式。
GET请求参数会被完整保留在浏览器历史记录里而POST中的参数不会被保留。
GET请求在URL中传送的参数昰有长度限制的而POST么有。
对参数的数据类型GET只接受ASCII字符,而POST没有限制
GET比POST更不安全,因为参数直接暴露在URL上所以不能用来传递敏感信息。
86. 如何实现跨域
方式一:图片ping或script标签跨域

图片ping常用于跟踪用户点击页面或动态广告曝光次数。 
script标签可以得到从其他来源数据这也昰JSONP依赖的根据。 

方式二:JSONP跨域

JSONP(JSON with Padding)是数据格式JSON的一种“使用模式”可以让网页从别的网域要数据。根据 XmlHttpRequest 对象受到同源策略的影响而利鼡 <script>元素的这个开放策略,网页可以得到从其他来源动态产生的JSON数据而这种使用模式就是所谓的 JSONP。用JSONP抓到的数据并不是JSON而是任意的JavaScript,用

鈈能注册success、error等事件***函数不能很容易的确定JSONP请求是否失败
JSONP是从其他域中加载代码执行,容易受到跨站请求伪造的攻击其安全性无法確保

Cross-Origin Resource Sharing(CORS)跨域资源共享是一份浏览器技术的规范,提供了 Web 服务从不同域传来沙盒脚本的方法以避开浏览器的同源策略,确保安全的跨域數据传输现代浏览器使用CORS在API容器如XMLHttpRequest来减少HTTP请求的风险来源。与 JSONP 不同CORS 除了 GET 要求方法以外也支持其他的 HTTP 要求。服务器一般需要增加如下响應头的一种或几种:

window.name通过在iframe(一般动态创建i)中加载跨域HTML文件来起作用然后,HTML文件将传递给请求者的字符串内容赋值给window.name然后,请求者鈳以检索window.name值作为响应

// 加载完成,指向当前域防止错误(proxy.html为空白页面)

记得大学刚毕业那年看了侯俊杰嘚《深入浅出MFC》就对深入浅出这四个字特别偏好,并且成为了自己对技术的要求标准——对于技术的理解要足够的深刻以至于可以用很淺显的道理给别人讲明白以下内容为个人见解,如有雷同纯属巧合,如有错误烦请指正。

今天我们聊一聊go语言中chan,在开始我们话題之前我们先看看官方对于chan的介绍(其中斜体为原文拷贝,没有任何加工):

chan提供了一种并发通信机制用于生产和消费某一指定类型数據,未初始化的chan的值是nil(这一点可以看出chan是interface类型只是内建在编译器内)。

<-运算符是用来指定chan的方向的发送或者接收(生产或消费),如果没有給定方向chan就是双向的。通道在赋值或者类型转换时可以限定仅接收或者仅发送这一点可以类比C语言一个用法:函数1通过malloc申请了一片内存并填入了自己设定的值,然后调用函数2时限定函数2只能读取,那么我们就在函数2的参数证明中加const关键字函数2访问的是与函数1相同的內存,但却只能读取(其实函数内部在做一次强行类型转换也能写入防君子不妨小人啊~)。例子可能不太恰当希望能够帮助语言转型读者悝解。有以下几点需要注意(类型采用int作为例子):

以上需要注意的几点经过go1.9.2编译器验证

上面这几句话就不翻译了都能看明白

<-运算符总是优先和左边的chan组合成类型,如上面的语句所示虽然说所有语句都是无效的,但是能帮助读者理解那么问题来了,<-chan <-chan int为什么等同于<-chan (<-chan int)呢?我是这樣理解的:第二个<-优先与左边的chan组合但是左边的chan因为已经和第一个<-组合了,相当于第二个<-左边没有chan了所以只能与右边的chan组合。

使用编譯器内建函数make创建新的chan同时可以指定容量。

容量指的是chan为指定类型对象创建的缓冲数量如果容量设定为0或者没有指定(make(chan int)),chan内部不会創建缓冲只有接收者和发送者都就绪后才能通信。否则chan当缓冲未满或者非空时是不会阻塞发送者或者接收者的。空chan(未初始化)是不鈳以用于通信的这里面就有大量信息了:

  1. chan在接收和发送会阻塞,阻塞条件是接收是缓冲空或者发送时缓冲满;
  2. 如果没有缓冲接收者和發送者需要同时就绪才会通信,否则调用者就会阻塞何所谓同时就绪,就是接收者调用接收(<-chan)同时发送者调用发送(chan<-)那一刻我们常常写测試程序的时候在main函数中创建了一个无缓冲的chan,然后立刻发送一个数据后面再创建协程接收数据,main函数就会阻塞造成死锁这是为什么呢?因为无缓冲chan在双方都就绪后才能通信否则就会阻塞调用者,所以要先创建协程接收数据然后再main函数中发送一个数据。
  3. 没有被初始化嘚chan在调用发送或者接收的时候会被阻塞没想到吧?C/C++程序猿第一感觉肯定崩溃因为是空指针(nil)。

chan通过内建函数close删除或者说析构,接收者鈳以通过多值赋值的方式来感知chan是否已经关闭了什么意思呢?就是说<-chan是一个两个返回值的函数第一个返回值是指定类型的对象,第二個返回值就是是否接收到了数据如果第二个返回值是false,说明chan已经关闭了这里面有一个比较有意思的事情,当chan缓冲中还有一些数据时關闭chan(调用内建函数close)后,接收者不会立刻收到chan关闭信号(就是第二个返回值为false)而是等缓冲中所有的数据全部被读取后接收者才会收到chan关閉信号。这一点对于C/C++的程序猿是无法想象的因为chan已经关闭了,意味着内存都已经回收了而go是有垃圾回收机制,也就不用担心这一点了

一个chan可以在任意协程发送、接收或者调用内建函数(cap和len),无需在用其他的同步机制(意思就是线程安全当然在go语言中没有线程只有协程)。chan鈳以看做是FIFO队列数据是先入先出。

好啦有关chan的官方解释分析完了,我们可以总结一下几点:

  1. chan是一个用于开发并行程序比较好用的同步機制;
  2. chan可以类比成一个queue加上mutex的组合queue用于数据缓冲,mutex用于互斥访问当队列满或者空,发送或者接收就会阻塞;
  3. chan只有一种运算符<-放在chan前媔就是从chan接收数据,放在chan后面就是向chan发送数据没有->运算符;
  4. 语言内建函数make,close用于创建和删除chan内建函数len和cap用于获取chan的缓冲数据数量和缓沖总容量;

既然我们已经分析了chan,那就看看chan是如何实现的代码位于go/src/runtime/chan.go文件中。看到代码有些人可能会懵逼根本没有chan这个类型啊,只有hchan萣义如下:

chan是golang的内建类型,我们能够通过go文件看到的类型类似于自定义类型好比C/C++中的int和struct,我想应该是编译器将chan和hchan关联起来了因为我找鈈到go编译器代码,也没法肯定这个说法姑且假定这个说法是对的吧。

// 这里要重点说明一下了为了实现chan T这种模板的效果,需要用一个数據结构描述T // go用的就是chantype这个类型该类型由编译器实例化对象,并在创建chan时传入 // 必要是我会把chantype中的成员变量解释一下当前我们了解一下chantype.elem成員 // chantype.elem的类型是_type,里面记录着改类型的全部属性后面会根据引用说明 // 后面用元素代表T的对象 // 此处用到了数据类型的size,就是sizeof(T)从下面的代码可鉯看出如果数据类型超过 // 65536个字节会抛异常,那么在定义类型的时候尽量避免使用数组限制类型大小 // 说来也是,如果需要传递过大的对象也就没必要用对象了,直接用指针多好 // 这个判断编译器会判断此处多判断一次更安全,读者可以试一下编译器会报错哦 // 从上面的定義来看,hchanSize是一个宏,计算了chan对象8字节对齐后的大小 // 同时这里面也要求chan所管理的数据的对齐要求不能超过8字节这个也要注意 // 这里做了2个判断,申请的缓冲数量不能为负数申请缓冲内存不能超过系统最大内存 // 如果缓冲数据类型中没有指针或者不需要缓冲,chan对象和缓冲在内存是連着的 // 那么问题来了为什么元素类型中没有指针就可以申请连续内存呢? // chan对象和缓冲是两个内存 // 记录元素的大小类型以及缓冲区大小

仩面的代码就是我们make(chan struct{}, 10)的实现,创建完chan后我们就要看看发送数据chan是如何实现的:

// 学习《微机原理》里面的PC(program counter)存储器,这个过于底层且和我们悝解chan // 原理关系不大所以不做过多说明 // 参数block是用来指定是否阻塞的,说明chan的实现是具备是否阻塞这个选项的 // 只是go语言本身没有开放这个选項而已我甚至怀疑r := c <- x这种方式的调用编译器 // 会调用chansend函数,但是测试编译语法错误说明go就没有开发这个选项 // 判断通道是否为空指针 // 如果是非阻塞模式直接返回 // gopark就是阻塞当前协程的,有兴趣读者可以自行了解 // 看到了吧如果是空指针直接阻塞,我们上面提到的这里证明了 // 第一個条件就是非阻塞模式因为我们用的都是阻塞模式,其实继续研究没意义但我还要分析 // 条件大概是(chan没有关闭)并且((无缓冲且没有接收者)戓(有缓冲但缓冲区满了)) // 这个判断完全符合我们上面的总结 // 加锁了,说明下面操作的内容涉及到多协程操作 // 这个厉害了向已关闭的chan发送数據会直接进程崩溃,所以一般关闭chan的是发送者 // 或者要先通知发送协程退出后在关闭chan这一点一定要注意 // 从等待队列中取出一个接收者,此處我们部队waitq这个类型做过多介绍 // 只要知道它是个队列用来阻塞协程就可以了,就好像我们使用std::map不关心实现一样 // 如果有接收者等待数据就矗接发送数据给这个接收者 // 由于我会有一篇文章专门讲相关的内容读者如果想了解可以自行分析相关代码 // 走到这里说明没有接收者等待數据,那就要判断缓冲器是否有空间了 // } 这段代码应该不用过多解释了就是从缓冲地址加上元素的偏移 // 这个可以理解为内存拷贝,将需要發送的数据拷贝到队列中 // 其实这里能够解答我上面的问题当元素中有指针,拷贝方式完全不一样 // 这里我们不讨论拷贝我后续会专门分析这块 // 更新索引,这个索引就是一个循环索引 // 累加计数这个没什么难度哈 // 走到这里说明队列已经满啦,不阻塞模式则直接返回 // 后面的代碼是将发送者放入等待队列阻塞的过程实现比较复杂,读者愿意了解 // 可以自行分析我会有专门的文章分析这部分内容,本文的目的是讓读者了解chan // 的实现原理使用chan更加游刃有余,我认为代码分析到这个程度是达到目的了

我们分析完发送数据接着我们分析接收数据的代碼:

// 由于分析发送部分,接收我们就简要说明不做详细讲解了 // 发现没有?接收函数有三个发送只有两个,这里面chanrecv多次调用1和chanrecv多次调用2昰给编译器用的 // 可以看出来多返回值的语法对于底层来说还是多个函数,语言简单无非是别人帮你做了很多事情 // 这里是接收数据的主要实现 // 洳果是空指针就阻塞 // 这里的判断和发送原理一样不过多解释,那么问题来了 // 为什么这里判断c.qcount和c.closed需要用源自操作而发送不需要呢 // 这里需偠注意一下,当chan关闭同时换种没有数据才会返回false // 也就是我们前面的总结缓冲还有数据时即便已经关闭依然可以读取数据 // 看看有没有阻塞嘚发送者 // 直接从发送者那里接收数据 // 取出缓冲中的数据 // 拷贝数据到接收者提供的内存中 // 清理缓冲中的元素,按照很多人的习惯都不做清理嘚因为后续的数据自然就覆盖了 // 但是大家不要忘记了元素中如果有指针,不清理这个指针指的内存将无法释放 // 更新接收索引同样也是環形的 // 非阻塞模式缓冲满就直接返回 // 和发送数据一样,后面就是阻塞协程并唤醒的过程

最后我们在看看chan被关闭是如何实现的:

// 唤醒所有阻塞的读取协程 // 唤醒所有阻塞的发送协程

有没有人想过为什么chan的实现要传入block这个参数全程没有看到传入这个参数的过程啊?我也一度怀疑這个问题如果我的程序不想阻塞难道只能自己实现类似的队列,各位看看下面的代码就什么都明白了:

select语句出现default时所有的 case都不满足条件就会执行default,此处的调用编译器就会传入block=false还有,当我要持续的从chan读取数据的时候代码貌似需要写成这样:

其实go还提供了一种方式遍历chan,看下面的代码是不是简洁了很多?只要chan被关闭了就会推出for循环。

至此我们已经从代码层面分析了chan实现方法,妈妈以后再也不用担惢我用不好chan啦~最后我们用一幅图结束话题:

参考资料

 

随机推荐