CharlesXiao‘s Blog

  • 首页

  • 归档

  • 分类

  • 标签

  • 关于

  • 搜索

计算机网络基础知识

发表于 2015-03-10 | 更新于 2018-11-20 | 字数统计5.6k字 | 阅读时长19分钟 | 分类于 编程知识

OSI分层模型

TCP/IP四层参考模型

TCP/IP四层参考模型,每一层对应的协议, TCP/IP报文格式,UDP和TCP数据段格式,IP头部(报头)格式以及各字段含义,TCP头部结构,套接字

Http/Https/UDP/TCP/Socket区别与联系

TCP协议

TCP报文Header

  1. ACK :TCP协议规定,只有ACK=1时有效,也规定连接建立后所有发送的报文的ACK必须为1
  2. SYN(SYNchronization) : 在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文。对方若同意建立连接,则应在响应报文中使SYN=1和ACK=1. 因此, SYN置1就表示这是一个连接请求或连接接受报文。
  3. FIN (finis)即完,终结的意思,用来释放一个连接。当 FIN = 1 时,表明此报文段的发送方的数据已经发送完毕,并要求释放连接。

TCP协议中的三次握手和四次挥手

  1. TCP三次握手:实质是client和server之间的报文段交换过程,三次握手之后client和server之间才能相互发送包含数据的报文段

    • 第一次握手:客户端发送一个SYN报文段(该报文段头部SYN标志位=1, ACK标志位=0,起始序号seq=x)到服务器进行连接请求,等待服务器回复确认;此时客户端进入SYN_SENT状态
    • 第二次握手:服务器收到客户端发送的连接请求报文段之后,给这个TCP连接分配TCP缓存和变量等资源,并回复一个允许连接的确认报文段叫做SYNACK报文段(SYN=1,ACK=1,seq=y,确认号ack=x+1),此时服务器进入SYN_RECV状态
    • 第三次握手:客户端收到服务器的SYNACK报文段之后,也给这个TCP连接分配TCP缓存和变量等资源,并向服务器发送确认ACK报文段(ack=y+1,ACK=1,seq=x+1),发送完毕之后客户端和服务器进入ESTABLISHED状态,完成三次握手
    • 如图所示:
  2. TCP四次挥手:实质是client和server之间的报文段交换过程,TCP是全双工模式需要client和server双方各自关闭,比如server接收到client发来的FIN报文段时只意味client将没有数据再发来,但是自己还是可以继续发送数据,client不能发送数据但是依旧可以接收数据。

  3. 相关问题:

    • 为什么连接的时候是三次握手,关闭的时候却是四次握手?
      答:因为在三次握手时,当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文,其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,”你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手
    • 为什么需要三次握手而不是两次,客户端为什么最后还要发送一次确认?
      答:主要目的是为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误;例如:当client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段,但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。
    • 为什么需要图中的起始序号seq=x是一个随机值?
      答:采用随机产生的初始化序列号进行请求,这样做主要是出于网络安全的因素着想;如果不是随机产生初始序列号,黑客将会以很容易的方式获取到你与其他主机之间通信的初始化序列号,并且伪造序列号进行攻击,这已经成为一种很常见的网络攻击手段
    • 为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
      答:虽然按道理,四次握手之后四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假设网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文

TCP流量控制和拥塞控制

发展历程

  1. 最开始的TCP拥塞控制由“慢启动(slow start)”和“拥塞避免(congestion avoidance)”组成;后来TCP Reno版本中又针对性的加入了“快速重传”、“快速恢复”算法;再后来TCP NewReno中又改进了“快速恢复”;最近又出现了选择性应答(SACK)的算法。
  2. TCP拥塞控制主要依赖于“拥塞窗口(cwnd)”,TCP还有一个对端通告的接收窗口(rwnd)用于流量控制。窗口值的大小就代表能够发送出去的但还没有收到ACK的最大数据报文段,显然窗口越大那么数据发送的速度也就越快,但是也有越可能使得网络出现拥塞。TCP的拥塞控制算法就是要在这两者之间权衡,选取最好的cwnd值,从而使得网络吞吐量最大化且不产生拥塞。
  3. 由于需要考虑拥塞控制和流量控制两个方面的内容,因此TCP的真正的发送窗口=min(rwnd, cwnd)。但是rwnd是由对端确定的,网络环境对其没有影响,所以在考虑拥塞的时候我们一般不考虑rwnd的值。我们暂时只讨论如何确定cwnd值的大小。关于cwnd的单位,在TCP中是以Byte来做单位的,我们假设TCP每次传输都是按照MSS大小来发送数据的,因此你可以认为cwnd按照数据包个数来做单位也可以理解,所以有时我们说cwnd增加1也就是相当于字节数增加1个MSS大小。

慢启动

方法:根据网络情况逐步增加每次发送的数据量。

原因:最初的TCP在连接建立成功后会向网络中发送大量的数据包,这样很容易导致网络中路由器缓存空间耗尽,从而发生拥塞。

描述:当新建连接时,cwnd初始化为1个最大报文段(MSS)大小,发送端开始按照拥塞窗口大小发送数据,每当有一个报文段被确认,cwnd就增加1个MSS大小。这样cwnd的值就随着网络往返时间(Round Trip Time,RTT)呈指数级增长,事实上,慢启动的速度一点也不慢,只是它的起点比较低一点而已。如果带宽为W,那么经过RTT*log2W时间就可以占满带宽。

慢启动引发的性能问题

在海量用户高并发访问的大型网站后台,有一些基本的系统维护需求。比如迁移海量小文件,就是从一些机器拷贝海量小碎文件到另一些机器,来完成一些系统维护的基本需求。

举个简单的例子,我们对每个文件都采用独立的TCP连接来传输(循环使用scp拷贝就是这个例子的实际场景,很常见的用法)。那么工作过程应该是,每传输一个文件建立一个连接,然后连接处于慢启动阶段,传输小文件,每个小文件几乎都处于独立连接的慢启动阶段被传输,这样传输过程所用的TCP包的总量就会增多。更细致的说一说这个事,如果在慢启动过程中传输一个小文件,我们可能需要2至3个小包,而在一个已经完成慢启动的TCP通道中(TCP通道已进入在高速传输阶段),我们传输这个文件可能只需要1个大包。网络拷贝文件的时间基本上全部消耗都在网络传输的过程中(发数据过去等对端ACK,ACK确认归来继续再发,这样的数据来回交互相比较本机的文件读写非常耗时间),撇开三次握手和四次握手那些包,粗略来说,慢启动阶段传输这些文件所用的包的数目是高速通道传输这些文件的包的数目的2-3倍!那么时间上应该也是2-3倍的关系!如果文件的量足够大,这个总时间就会被放大到需求难以忍受的地步。

因此,在迁移海量小文件的需求下,我们不能使用“对每个文件都采用独立的TCP连接来传输(循环使用scp拷贝)“这样的策略,它会使每个文件的传输都处于在一个独立TCP的慢启动阶段。

如何避免慢启动,提升性能

很简单,尽量把大量小文件放在一个TCP连接中排队传输。起初的一两个文件处于慢启动过程传输,后续的文件传输全部处于高速通道中传输,用这样的方式来减少发包的数目,进而降低时间消耗。

实际上这种传输策略带来的性能提升的功劳不仅仅归于避免慢启动,事实上也避免了大量的3次握手和四次握手,这个对海量小文件传输的性能消耗也非常致命。

随着多核服务器的兴起,以及现代网卡的多通道技术的迅猛发展,现在我们解决这一问题的通常做法是绑定多CPU的多核到网卡的多个通道,然后由CPU的核来均分传输这些小文件,每个核用一个TCP连接来排队发送分到的小文件。

  1. 拥塞避免

    条件:TCP使用了一个叫慢启动门限(ssthresh)的变量,当cwnd超过该值后,慢启动过程结束,进入拥塞避免阶段。对于大多数TCP实现来说,ssthresh的值是65536(同样以字节计算)。

    原因:从慢启动可以看到,cwnd可以很快的增长上来,从而最大程度利用网络带宽资源,但是cwnd不能一直这样无限增长下去,一定需要某个限制。

    描述:拥塞避免的主要思想是加法增大,也就是cwnd的值不再指数级往上升,开始加法增加。此时当窗口中所有的报文段都被确认时,cwnd的大小加1,cwnd的值就随着RTT开始线性增加,这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。
    发生拥塞以后怎么办

    拥塞发生:TCP认为网络拥塞的主要依据是它重传了一个报文段。上面提到过,TCP对每一个报文段都有一个定时器,称为重传定时器(RTO),当RTO超时且还没有得到数据确认,那么TCP就会对该报文段进行重传,当发生超时时,那么出现拥塞的可能性就很大,某个报文段可能在网络中某处丢失,并且后续的报文段也没有了消息。

    TCP反应:

  2. 快速重传

    条件:其实TCP还有一种情况会进行重传:那就是收到3个相同的ACK。TCP在收到乱序到达包时就会立即发送ACK,TCP利用3个相同的ACK来判定数据包的丢失,此时进行快速重传。

    描述:

  3. 快速恢复
  • 当收到3个重复ACK时,TCP最后进入的不是拥塞避免阶段,而是快速恢复阶段。快速重传和快速恢复算法一般同时使用。

    思想:“数据包守恒”原则,即同一个时刻在网络中的数据包数量是恒定的,只有当“老”数据包离开了网络后,才能向网络中发送一个“新”的数据包,如果发送方收到一个重复的ACK,那么根据TCP的ACK机制就表明有一个数据包离开了网络,于是cwnd加1。如果能够严格按照该原则那么网络中很少会发生拥塞,事实上拥塞控制的目的也就在修正违反该原则的地方。

    描述:

    <img src="https://raw.githubusercontent.com/Charles-Xiao/Charles-Xiao.github.io/master/images/ssca4.png" class="full-image" width="100%"/>
    
  • 快速重传算法首次出现在4.3BSD的Tahoe版本,快速恢复首次出现在4.3BSD的Reno版本,也称之为Reno版的TCP拥塞控制算法。
    可以看出Reno的快速重传算法是针对一个包的重传情况的,然而在实际中,一个重传超时可能导致许多的数据包的重传,因此当多个数据包从一个数据窗口中丢失时并且触发快速重传和快速恢复算法时,问题就产生了。因此NewReno出现了,它在Reno快速恢复的基础上稍加了修改,可以恢复一个窗口内多个包丢失的情况。具体来讲就是:Reno在收到一个新的数据的ACK时就退出了快速恢复状态了,而NewReno需要收到该窗口内所有数据包的确认后才会退出快速恢复状态,从而更一步提高吞吐量。

UDP协议

HTTP协议

Http1.0, 1.1,2.0和https之间的区别

  1. HTTP1.0:规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个单独的TCP连接,服务器完成请求处理后立即断开释放TCP连接,服务器不跟踪每个客户也不记录过去的请求。此外,由于大多数网页的流量都比较小,一次TCP连接很少能通过slow-start区,不利于提高带宽利用率。HTTP1.0要建立长连接,可以在请求消息中包含Connection: Keep-Alive头域
  2. HTTP 1.1支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。例如:一个包含有许多图像的网页文件的多个请求和应答可以在一个连接中传输,但每个单独的网页文件的请求和应答仍然需要使用各自的连接
  3. HTTP 1.1还允许客户端不用等待上一次请求结果返回,就可以发出下一次请求,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容,这样也显著地减少了整个过程所需要的时间
  4. 三者区别参考链接

Http的get和post之间的区别

Header和响应码

缓存对应的响应头部字段

  1. 浏览器缓存机制
    • Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求;不过Expires 是HTTP 1.0的东西,现在默认浏览器均默认使用HTTP 1.1,所以它的作用基本忽略
    • Cache-Control与Expires的作用一致,都是指明当前资源的有效期,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。只不过Cache-Control的选择更多,设置更细致,如果同时设置的话,其优先级高于Expires
    • Last-Modified:标示这个响应资源的最后修改时间。web服务器在响应请求时,告诉浏览器资源的最后修改时间。
    • Etag:web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器觉得)。Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。
    • 既生Last-Modified何生Etag?:
      • Last-Modified标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间
      • 如果某些文件会被定期生成,当有时内容并没有任何变化,但Last-Modified却改变了,导致文件没法使用缓存
      • 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形
      • Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304
    • 浏览器缓存检查流程

Socket协议

  1. 套接字(socket)是支持TCP/IP协议的网络通信的基本操作单元,也可以说是应用层和传输层之间的一个软件抽象层;它包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远程主机的IP地址,远程进程的协议端口。在创建Socket连接时,可以指定使用的传输层协议,Socket可以支持不同的传输层协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接
  2. 应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题:多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务
  3. 套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。
    • 服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。
    • 客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
    • 连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求
  4. Socket连接与HTTP连接相结合
    • 通常情况下Socket连接其实就是一个TCP连接,因此Socket连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。但在实际网络应用中,客户端到服务器之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 Socket 连接断连,因此也需要通过http轮询告诉网络,该连接处于活跃状态。
    • HTTP连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。
    • 很多情况下,我们需要服务器端主动向客户端推送数据,保持客户端与服务器数据的实时与同步。此时若双方建立的是Socket连接,服务器就可以直接将数据传送给客户端;若双方建立的是HTTP连接,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端,因此,客户端定时向服务器端发送连接请求,不仅可以保持在线,同时也是在“询问”服务器是否有新的数据,如果有就将数据传给客户端

      其它协议

Java多进程和多线程编程总结

发表于 2015-03-09 | 更新于 2018-11-07 | 字数统计4.4k字 | 阅读时长16分钟 | 分类于 java学习笔记

进程与线程之间关系

一个进程对应一个程序的执行,它具有文本、数据、堆栈片段以及它自己的资源;而一个线程则是进程执行过程中的一个单独的执行序列,线程有时候也被称为轻量级进程. 一个进程可以包含多个线程,这些线程共享进程的资源。

首先我们明确一点,就是我们创建的每一个Java程序都是运行在一个单独的Java虚拟机进程中的,每启动一个java程序就会同时开启一个单独对应的JVM进程(也叫做JVM实例),然后JVM进程会开始初始化类,包括初始化静态变量和静态代码块,普通变量,构造器等;然后再去寻找main()主线程作为程序执行入口,继续执行其他线程直至结束.(Android应用程序和dalvik虚拟机的关系也一样如此)

常见的进程间通信方式(IPC)

  1. 管道与命名管道:管道允许一个进程和另一个与它有共同祖先的进程之间进行通信,命名管道允许无亲缘关系的进程间的通信,命名管道在文件系统中有对应的文件名,通过命令mkfifo或系统调用mkfifo来创建
  2. 套接字:可用于不同机器之间的进程间通信
  3. 共享内存:多个进程可以访问同一块内存空间,是最快的可用IPC形式
  4. 文件内存映射:内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它; 需要文件锁做同步
  5. 信号量:主要作为进程间以及同一进程不同线程之间的同步手段
  6. 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身
  7. 消息队列:消息队列是消息的链接表,包括Posix消息队列和system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息

进程的状态:就绪-运行-阻塞

多线程之间通信方式

  1. “共享变量”:实现Runnable接口实现线程的共享变量或者内部类共享外围类的变量
  2. “管道流”:

    /* 管道输出流和生产者线程绑定, 管道输入流和消费者线程绑定,输入输出流绑定,启动两个线程互相之间通过write和read函数就行通信 */
    PipedOutputStream pos = new PipedOutputStream();
    Producer p = new Producer(pos);
    PipedInputStream pis = new PipedInputStream();
    Consumer c = new Consumer(pis);
    pos.connect(pis);
    p.start();
    c.start();
    
    class Producer extends Thread {
        private PipedOutputStream pos;
        public Producer(PipedOutputStream pos) {
            this.pos = pos;
        }
        public void run() {
            int i = 8;
            try {
                pos.write(i);
                // pis.read()
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

线程的四种创建方式

  1. 定义Thread类的子类,并重写该类的run()方法;创建Thread子类的实例,即创建了线程对象,调用线程对象的start()方法来启动该线程
  2. 定义Runnable接口的实现类,并重写该接口的run()方法,创建Runnable实现类的实例,并以此实例作为Thread的参数来创建Thread对象,该Thread对象才是真正的线程对象;然后调用线程对象的start()方法来启动线程
  3. 创建Callable接口的实现类,并实现call()方法,该call()方法有返回值;创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值,使用FutureTask对象作为Thread对象的target创建并启动新线程,调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

    public class MyCallableTest implements Callable<Integer>{
        // 实现call方法,作为线程执行体
        public Integer call(){
            int i = 0;
            for ( ; i < 100 ; i++ ){
                System.out.println(Thread.currentThread().getName()+ "\t" + i);
            }
            // call()方法可以有返回值,而且可以抛出异常
            return i;
        }
        public static void main(String[] args) {
            // 创建Callable对象
            MyCallableTest myCallableTest = new MyCallableTest();
            // 使用FutureTask来包装Callable对象
            FutureTask<Integer> task = new FutureTask<Integer>(myCallableTest);
            for (int i = 0 ; i < 100 ; i++){
                System.out.println(Thread.currentThread().getName()+ " \t" + i);
                if (i == 20){
                    // 实质还是以Callable对象来创建、并启动线程
                    new Thread(task , "callable").start();
                    // 线程池执行FutureTask
                    Executor executor = Executors.newSingleThreadExecutor();
                    executor.execute(task);
                }
            }
            try{
                // 获取线程返回值
                System.out.println("callable返回值:" + task.get());
            }
            catch (Exception ex){
                ex.printStackTrace();
            }
        }
    }
    
  4. 线程池:

    • 线程池的好处1在于可以更好地控制并发线程数目,提高资源利用率并防止阻塞
    • 好处2可以更好地重用线程, 减少线程创建和销毁带来的系统开销,可以设置线程定时定期执行
    • 核心构造函数ThreadPoolExecutor,可以在参数中设置核心池大小,最大线程数等
    • ExecutorService接口用于实现和管理线程池,其生命周期包括三种状态:运行、关闭、终止。
    • 线程池四个基本组成部分:
      1. 线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
      2. 工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
      3. 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
      4. 任务队列(taskQueue):用于存放没有处理的任务, 提供一种缓冲机制。
    • 四种Executors接口提供的通过ThreadFactory新建的线程池

      1. newFixedThreadPool(): 固定数目线程池,任意时间点最多只能有固定数目的活动线程存在;有新任务到达时创建线程,只能放在另外的队列中等待,直到达到线程池最大大小为止,有异常则补充

        ExecutorService threadPool = Executor.newFixedThreadPool(3);
        Runnable r = new Runnable() {
            @Override
            public void run() {
        
            }
        };
        // 参数可以是Thread以及Runnable对象
        threadPool.execute(r);
        
      2. newCachedThreadPool:缓存型线程池,新任务到达则新建线程,当线程数目大于处理需要时,则回收空闲的线程,无大小限制

      3. newSingleThreadExecutor: 单线程池,只创建唯一的工作线程来执行任务,保证任务被顺序执行
      4. newScheduledThreadPool: 调度型线程池,固定线程数目,而且提供任务被定时和周期性执行的功能
    • 自定义线程池

       ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
      
      /*
      corePoolSize - 核心池中所保存的线程数,包括空闲线程;也就是正在运行的线程数目。
      maximumPoolSize-线程池中允许的最大线程数
      keepAliveTime - 当线程池的工作线程空闲后,保持存活的时间
      unit - keepAliveTime 参数的时间单位
      workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute方法提交的 Runnable任务
      threadFactory - 执行程序创建新线程时使用的工厂
      handler - 由于超出线程范围和队列容量而使新到达的任务被阻塞时采取的处理策略,
      默认为AbortPolicy,表示无法处理新任务时抛出异常;DiscardPolicy:不能执行的任务将被删除
      ThreadPoolExecutor是Executors类的底层实现
      */
      
       public class ThreadPoolTest{   
          public static void main(String[] args){   
              //创建等待队列   
              BlockingQueue<Runnable> bqueue = new ArrayBlockingQueue<Runnable>(20);   
              //创建线程池,池中保存的线程数为3,允许的最大线程数为5  
              ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,50,TimeUnit.MILLISECONDS,bqueue);   
              //创建七个任务   
              Runnable t1 = new MyThread();   
              Runnable t2 = new MyThread();   
              Runnable t3 = new MyThread();   
              Runnable t4 = new MyThread();   
              Runnable t5 = new MyThread();   
              Runnable t6 = new MyThread();   
              Runnable t7 = new MyThread();   
              //每个任务会在一个线程上执行  
              pool.execute(t1);   
              pool.execute(t2);   
              pool.execute(t3);   
              pool.execute(t4);   
              pool.execute(t5);   
              pool.execute(t6);   
              pool.execute(t7);   
              //关闭线程池   
              pool.shutdown();   
          }   
      }   
      
      class MyThread implements Runnable{   
          @Override   
          public void run(){   
              System.out.println(Thread.currentThread().getName() + "正在执行。。。");   
              try{   
                  Thread.sleep(100);   
              }catch(InterruptedException e){   
                  e.printStackTrace();   
              }   
          }   
      }  
      
    • 线程池工作流程
    • 线程池的排队策略

      1. 默认选项是SynchronousQueue,它将任务直接提交给线程而不保持它们,一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态
      2. 无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过corePoolSize。(因此,maximumPoolSize的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
      3. 有界队列。当使用有限的 maximumPoolSizes时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
      4. PriorityBlockingQueue: 一个具有优先级的无限阻塞队列
    • 线程池的风险

      1. 死锁: 死锁的产生是因为一组线程或者进程互相等待资源的释放而永远互相等待;线程池中容易产生一种新的死锁: 当核心池中所有线程都在等待阻塞队列中的某个线程的执行结果,但是该线程却因为池中没有空闲线程而没有办法执行,这样就导致互相等待的死锁.
      2. 并发错误: 线程池和其它排队机制依靠使用 wait() 和 notify() 方法,易出现问题
    • 线程池的执行: execute和submit两个方法都可以向线程池提交任务, execute()方法的返回类型是void,它定义在Executor接口中;submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,便于捕获异常,利用FutureTask.get()函数

    • 线程池的关闭

      1. 通过调用线程池的shutdown或shutdownNow方法来关闭线程池,它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
    1. 只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow。
    • 合理配置线程池
      1. 任务的性质:CPU密集型任务,IO密集型任务和混合型任务;CPU密集型任务配置尽可能小的线程,如配置Ncpu+1个线程的线程池。IO密集型任务则由于线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu
      2. 任务的优先级:高,中和低;PriorityBlockingQueue会导致优先级低的线程永远不被执行
      3. 任务的执行时间:长,中和短;
      4. 任务的依赖性:是否依赖其他系统资源,如数据库连接:依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。

线程的状态变化

死锁及其解决办法

/**
 * 一个简单的死锁类 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒
 * 而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
 * td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定; td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;
 * td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁
 */
public class DeadLock implements Runnable {
    public int flag = 1;
    // 静态对象是类的所有对象共享的
    private static Object o1 = new Object(), o2 = new Object();

    public void run() {
        System.out.println("flag=" + flag);
        if (flag == 1) {
            synchronized (o1) {
                System.out.println("thread1锁定 o1");
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                // thread1等待资源o2释放
                synchronized (o2) {
                    System.out.println("1");
                }
            }
        }
        if (flag == 2) {
            synchronized (o2) {
                System.out.println("thread2锁定 o2");
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                // thread2等待资源o1释放
                synchronized (o1) {
                    System.out.println("2");
                }
            }
        }
    }

    public static void main(String[] args) {

        DeadLock td1 = new DeadLock();
        DeadLock td2 = new DeadLock();
        td1.flag = 1;
        td2.flag = 2;
        // td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
        // td2的run()可能在td1的run()之前运行
        new Thread(td1).start();
        new Thread(td2).start();

    }
}
  1. 死锁的必要条件死锁是指是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待对方占有的资源的现象,若无外力作用,它们都将无法推进下去。
    • 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
    • 请求和保持条件:指进程请求新资源发生阻塞时对自己已经获得的其它资源保持不放。
    • 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
    • 环路等待条件:指在发生死锁时,进程之间会形成一种头尾相接的循环等待资源关系,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
  2. 死锁的避免、预防和解决方法
    • 采用资源有序分配法,破坏环路等待形成
    • 允许进程剥夺其他进程占有的资源
    • 银行家算法:把操作系统看作是银行家,操作系统管理的资源相当于银行家管理的资金,进程向操作系统请求分配资源相当于用户向银行家贷款。操作系统按照银行家制定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配。当进程在执行中继续申请资源时,先测试该进程已占用的资源数与本次申请的资源数之和是否超过了该进程对资源的最大需求量。若超过则拒绝分配资源,若没有超过则再测试系统现存的资源能否满足该进程尚需的最大资源量,若能满足则按当前的申请量分配资源,否则也要推迟分配。
    • 互斥条件无法被破坏

多线程相关问题

  1. yield表示暂停当前线程,执行其他线程(包括自身线程)由cpu决定
  2. join:阻塞所在线程,等调用它的线程执行完毕,再向下执行
  3. sleep()方法属于Thread类中的,而wait()方法则是属于Object类;sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然保持,当指定的时间到了又会自动恢复运行状态,线程不会释放对象锁;而当调用wait()方法的时候,线程会放弃对象锁,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备就绪

多线程同步的方法

  1. Synchronized代码块
  2. Synchronized方法
  3. RetrantLock可重入锁
  4. ThreadLocal线程局部变量
  5. Volatile可见变量
1…1718
CharlesXiao

CharlesXiao

在码农炼成之路不断挣扎……stay hungry……keep learning……

87 日志
18 分类
78 标签
github weibo Daijiale的个人站点
推荐阅读
  • RocksDB
  • Google FE
© 2015.05.16 – 2019 CharlesXiao
本站总访问量:
|
总访客数:
|
博客全站共169.6k字