最近动态

spring

spring-ioc容器初始化过程

这里先从ClassPathXmlApplicationContext的构造来开始分析
看一下 ClassPathXmlApplicationContext 的UML类图

1
2
3
4
5
6
7
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) throws BeansException {
super(parent);
this.setConfigLocations(configLocations);
if (refresh) {
this.refresh();
}
}

看super(parent)做了些什么,parent卡是传进来的是null

1
2
3
4
public AbstractApplicationContext(@Nullable ApplicationContext parent) {
this();
this.setParent(parent);
}

看this()

1
2
3
4
5
6
7
8
9
10
11
public AbstractApplicationContext() {
this.logger = LogFactory.getLog(this.getClass());
this.id = ObjectUtils.identityToString(this);
this.displayName = ObjectUtils.identityToString(this);
this.beanFactoryPostProcessors = new ArrayList();
this.active = new AtomicBoolean();
this.closed = new AtomicBoolean();
this.startupShutdownMonitor = new Object();
this.applicationListeners = new LinkedHashSet();
this.resourcePatternResolver = this.getResourcePatternResolver();
}

这里给当前类(AbstractApplicationContext)的私有变量初始化值。看看setParent()做了些什么

1
2
3
4
5
6
7
8
9
10
11
public void setParent(@Nullable ApplicationContext parent) {
this.parent = parent;
// 当parent不为空的时候,会对IOC环境进行配置。
if (parent != null) {
Environment parentEnvironment = parent.getEnvironment();
if (parentEnvironment instanceof ConfigurableEnvironment) {
this.getEnvironment().merge((ConfigurableEnvironment)parentEnvironment);
}
}
}

这里super(parent)已经执行完了,继续往下看this.setConfigLocations(configLocations);

1
2
3
4
5
6
7
8
9
10
11
12
public void setConfigLocations(@Nullable String... locations) {
if (locations != null) {
Assert.noNullElements(locations, "Config locations must not be null");
this.configLocations = new String[locations.length];
for(int i = 0; i < locations.length; ++i) {
this.configLocations[i] = this.resolvePath(locations[i]).trim();
}
} else {
this.configLocations = null;
}
}

这里会解析传进来的配置文件路径参数,用的是ant规则匹配。解析完后将路径信息放入configLocations数组里面。往下看refresh默认是true,看refresh方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public void refresh() throws BeansException, IllegalStateException {
// var1 = new Object
Object var1 = this.startupShutdownMonitor;
synchronized(this.startupShutdownMonitor) {
// 调用容器准备刷新的方法,此方法中会获取容器的当前时间,给容器设置同步标识
this.prepareRefresh();
ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
this.prepareBeanFactory(beanFactory);
try {
// beanFactory的后置处理器
this.postProcessBeanFactory(beanFactory);
//调用BeanFactoryPostProcessor,激活各种BeanFactory处理器
this.invokeBeanFactoryPostProcessors(beanFactory);
//注册拦截Bean创建的Bean处理器,这里只是注册,真正调用是在getBean的时候。
this.registerBeanPostProcessors(beanFactory);
//为上下文初始化Message源,国际化处理
this.initMessageSource();
//初始化应用消息广播器,并放入applicationEventMulticaster bean中
this.initApplicationEventMulticaster();
//留给子类来初始化其他的Bea
this.onRefresh();
//在所有注册的bean中查找Listener bean,注册到消息广播器中
this.registerListeners();
//初始化剩下的单实例,非惰性的
this.finishBeanFactoryInitialization(beanFactory);
//完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知别人
this.finishRefresh();
} catch (BeansException var9) {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
}
this.destroyBeans();
this.cancelRefresh(var9);
throw var9;
} finally {
this.resetCommonCaches();
}
}
}

这里重点看一下 obtainFreshBeanFactory 方法,它返回一个beanFactory对象,那么我们这里可以猜测应该读取了配置文件,配置了bean等等。追踪一下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
this.refreshBeanFactory();
ConfigurableListableBeanFactory beanFactory = this.getBeanFactory();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Bean factory for " + this.getDisplayName() + ": " + beanFactory);
}
return beanFactory;
}
```
这里首先调用 refreshBeanFactory 方法刷新beanFactory。源码如下:
```java
protected final void refreshBeanFactory() throws BeansException {
if (this.hasBeanFactory()) {
this.destroyBeans();
this.closeBeanFactory();
}
try {
// 创建一个新的beanFacroty
DefaultListableBeanFactory beanFactory = this.createBeanFactory();
// 配置beanFactory的属性
beanFactory.setSerializationId(this.getId());
this.customizeBeanFactory(beanFactory);
this.loadBeanDefinitions(beanFactory);
Object var2 = this.beanFactoryMonitor;
synchronized(this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
} catch (IOException var5) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + this.getDisplayName(), var5);
}
}

可以看出,首先判断是否已经存在beanFactory,有就关闭,然后再创建一个beanFactory.看一下创建 createBeanFactory 的具体实现

1
2
3
protected DefaultListableBeanFactory createBeanFactory() {
return new DefaultListableBeanFactory(this.getInternalParentBeanFactory());
}

由于前面的parent参数为null,所以这个 getInternalParentBeanFactory 方法返回值为null。这里就实例化了一个 DefaultListableBeanFactory 工厂类。回到前面继续往下看, customizeBeanFactory 方法内部是判断变量赋值的,这里条件都不满足,继续往下看,loadBeanDefinitions 方法是负责给 beanFactory 填充 bean 定义的,看看源码

1
2
3
4
5
6
7
8
9
10
11
12
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// 创建 XML 读取器
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
// 配置 beanDefinitionReader
beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
// 对读取器 设置xml校验。
this.initBeanDefinitionReader(beanDefinitionReader);
// 读取配置文件,加载 bean 的定义
this.loadBeanDefinitions(beanDefinitionReader);
}

看一下 loadBeanDefinitions 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
// Resource 是将配置文件封装的一个对象,首先尝试能否直接获取到 Resource 数组。默认是不行的
Resource[] configResources = this.getConfigResources();
if (configResources != null) {
reader.loadBeanDefinitions(configResources);
}
// 这里就直接获取配置文件的 localtion,后面还是先获取到Resource[] 数组再去读取xml文件
String[] configLocations = this.getConfigLocations();
if (configLocations != null) {
reader.loadBeanDefinitions(configLocations);
}
}

看看 loadBeanDefinitions(configLocations) 的源码,实际调用的是下面这个重载类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {
// 获取 ResourceLoader 加载器
ResourceLoader resourceLoader = this.getResourceLoader();
if (resourceLoader == null) {
throw new BeanDefinitionStoreException("Cannot import bean definitions from location [" + location + "]: no ResourceLoader available");
} else {
int loadCount;
if (!(resourceLoader instanceof ResourcePatternResolver)) {
Resource resource = resourceLoader.getResource(location);
loadCount = this.loadBeanDefinitions((Resource)resource);
if (actualResources != null) {
actualResources.add(resource);
}
if (this.logger.isDebugEnabled()) {
this.logger.debug("Loaded " + loadCount + " bean definitions from location [" + location + "]");
}
return loadCount;
} else {
try {
Resource[] resources = ((ResourcePatternResolver)resourceLoader).getResources(location);
// 这里对配置文件进行读取,返回读取的配置文件数量
loadCount = this.loadBeanDefinitions(resources);
if (actualResources != null) {
Resource[] var6 = resources;
int var7 = resources.length;
for(int var8 = 0; var8 < var7; ++var8) {
Resource resource = var6[var8];
actualResources.add(resource);
}
}
if (this.logger.isDebugEnabled()) {
this.logger.debug("Loaded " + loadCount + " bean definitions from location pattern [" + location + "]");
}
return loadCount;
} catch (IOException var10) {
throw new BeanDefinitionStoreException("Could not resolve bean definition resource pattern [" + location + "]", var10);
}
}
}
}

细看读取过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (this.logger.isInfoEnabled()) {
this.logger.info("Loading XML bean definitions from " + encodedResource.getResource());
}
Set<EncodedResource> currentResources = (Set)this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {
currentResources = new HashSet(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
if (!((Set)currentResources).add(encodedResource)) {
throw new BeanDefinitionStoreException("Detected cyclic loading of " + encodedResource + " - check your import definitions!");
} else {
int var5;
try {
// 获取文件流
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
// 这里加载配置文件
var5 = this.doLoadBeanDefinitions(inputSource, encodedResource.getResource());
} finally {
inputStream.close();
}
} catch (IOException var15) {
throw new BeanDefinitionStoreException("IOException parsing XML document from " + encodedResource.getResource(), var15);
} finally {
((Set)currentResources).remove(encodedResource);
if (((Set)currentResources).isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
return var5;
}
}

来看一下怎么加载的,这个 doLoadBeanDefinitions 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException {
try {
// 理论上一个 inputSource 参数就行了,为什么还要 resource 呢?这里是为了设置xml验证模式引入的。这个方法就将InputSource对象转变成了 Document 对象。
Document doc = this.doLoadDocument(inputSource, resource);
// 这个方法将document对象变成了Bean放入内存中
return this.registerBeanDefinitions(doc, resource);
} catch (BeanDefinitionStoreException var4) {
throw var4;
} catch (SAXParseException var5) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(), "Line " + var5.getLineNumber() + " in XML document from " + resource + " is invalid", var5);
} catch (SAXException var6) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(), "XML document from " + resource + " is invalid", var6);
} catch (ParserConfigurationException var7) {
throw new BeanDefinitionStoreException(resource.getDescription(), "Parser configuration exception parsing XML from " + resource, var7);
} catch (IOException var8) {
throw new BeanDefinitionStoreException(resource.getDescription(), "IOException parsing XML document from " + resource, var8);
} catch (Throwable var9) {
throw new BeanDefinitionStoreException(resource.getDescription(), "Unexpected exception parsing XML document from " + resource, var9);
}
}

看一下这个方法 registerBeanDefinitions

1
2
3
4
5
6
7
8
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// 获取一个专为 BeanDefinitionDocument 的 reader。
BeanDefinitionDocumentReader documentReader = this.createBeanDefinitionDocumentReader();
int countBefore = this.getRegistry().getBeanDefinitionCount();
// 这里就是开始读取xml定义的文件了
documentReader.registerBeanDefinitions(doc, this.createReaderContext(resource));
return this.getRegistry().getBeanDefinitionCount() - countBefore;
}

看一下读取方法 registerBeanDefinitions 的细节

1
2
3
4
5
6
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
this.logger.debug("Loading bean definitions");
Element root = doc.getDocumentElement();
this.doRegisterBeanDefinitions(root);
}

看具体加载根节点的方法 doRegisterBeanDefinitions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protected void doRegisterBeanDefinitions(Element root) {
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = this.createDelegate(this.getReaderContext(), root, parent);
if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute("profile");
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(profileSpec, ",; ");
if (!this.getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (this.logger.isInfoEnabled()) {
this.logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec + "] not matching: " + this.getReaderContext().getResource());
}
return;
}
}
}
//xml解析的预处理,可以自己定义一些节点属性等,此方法Spring默认实现为空
this.preProcessXml(root);
//把Document对象解析为BeanDefinition对象
this.parseBeanDefinitions(root, this.delegate);
//xml解析的后处理,可以在解析完xml之后,实现自己的逻辑。Spring默认实现为空。
this.postProcessXml(root);
this.delegate = parent;
}

看一下parseBeanDefinitions方法源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
// 判断是不是Spring的默认空间命名(dubbo的就不是)
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for(int i = 0; i < nl.getLength(); ++i) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element)node;
if (delegate.isDefaultNamespace(ele)) {
this.parseDefaultElement(ele, delegate);
} else {
delegate.parseCustomElement(ele);
}
}
}
} else {
// dubbo配置文件读取就是从这里开始的
delegate.parseCustomElement(root);
}
}

到这里为止就差不多了,SpringIOC里面配置的bean就是这么来的。

总结

  • IOC容器初始化入口是在构造方法中调用refresh开始的。
  • 通过ResourceLoader来完成资源文件位置的定位,DefaultResourceLoader是默认的实现,同时上下文本身就给除了ResourceLoader的实现。
  • 创建的IOC容器是DefaultListableBeanFactory。
  • IOC对Bean的管理和依赖注入功能的实现是通过对其持有的BeanDefinition进行相关操作来完成的。
  • 通过BeanDefinitionReader来完成定义信息的解析和Bean信息的注册。
  • XmlBeanDefinitionReader是BeanDefinitionReader的实现了,通过它来解析xml配置中的bean定义。
  • 实际的处理过程是委托给BeanDefinitionParserDelegate来完成的。得到Bean的定义信息,这些信息在Spring中使用BeanDefinition对象来表示。
  • BeanDefinition的注册是由BeanDefinitionRegistry实现的registerBeanDefiition方法进行的。内部使用ConcurrentHashMap来保存BeanDefiition。

Read More

默认配图
计算机网络

TCP与UDP

进程与进程通信

在数据链路层,数据之间通过点对点的方式进行传输(局域网)。在网络层,数据通过主机对主机方式进行通信,IP数据包的发送与接收。在传输层,进程或应用程序通过端口进行通信,也叫进程与进程之间的通信,TCP与UDP协议就是在这一层次工作的。

端口和套接字

端口是标识指定应用程序的一种方式,用16位的bit表示,共有 2^16-1=65535 个端口总数。端口分为已定义端口和临时端口

  1. 已定义端口:
    • 小于255的端口是为公共应用保留的
    • 255~1023之间的端口被分配给一些公司的市场应用
    • 大于1023的端口未定义。
  2. 临时端口
    • 客户端与服务器连接的时候,主要客户端需要,运行客户端进程的主机就会给客户端临时分配一个临时端口。
      套接字(socket)是我们在网络中可以命名和寻址的通信连接点(端点)。通常情况下,一台主机采用典型的系统配置作为服务器,其它主机作为客户端。客户端连接服务器,并与之交换信息,最后断开连接,相当于螺帽一样的存在。
  3. 套接字连接类型
    • 面向连接(TCP)7
    • 无连接(UDP)

UDP

  1. UDP 工作流程:
    • 将上层数据封装,填充上UDP头部,发送给下层协议(IP协议),
  2. UDP不做什么:
    • 发送数据前建立连接
    • 检测数据的丢失并重传
    • 保证接受数据的顺序与发送数据的顺序一致
    • 提供拥塞处理机制,以及两个应用之间的流控制机制
  3. UDP消息格式
    UDP数据报分为两个部分:UDP头部和UDP数据区域。头部数据的组成如下:
    • 源端口号:16位,表明发送UDP数据的源进程的端口
    • 目标端口号: 16位,表明接收UDP协议的目标进程的端口
    • 长度:16位,表明用户数据报的长度,以字节计。包括头部长度。
    • 校检和:一个可选的16位字段,是伪IP头部,UDP头部,UDP数据的反码和(异或运算)。
      数据区域: 长度可变,被封装在上层消息中被发送出去。

TCP

  1. TCP连接的建立(三次握手)
    这里的客户端个服务端的概念是相对的。客户端有可能是服务端,服务端也有可能是客户端。
    TCP的建立是通过元数据SYN+ACK来确认连接信息的。 SYN元数据用于标识报文,以方便将报文重组。ACK元数据表示的是一个“确认回复”消息。值和接收到的syn值有关,一般是+1的关系。为了简单,我们将SYN报文的序列号称作SEQ
    1. 客户端发送SEQ=J,ACK=0给服务端请求建立连接,如果超过一定时间没收到服务端的响应(计时器超时),会重新发送请求。
    2. 服务端收到请求,回复SEQ=K(k是随机值),ACK=J+1表示接收到请求,同意建立连接。
    3. 客户端发送 SEQ=J+1,ACK=K+1,确认号ACK=K+1表示客户端已经接收到服务端的响应,开始建立连接
  2. TCP连接的四次挥手
    TCP建立一次连接需要三次握手,而结束一个连接却需要四次,这是因为TCP的半关闭状态决定的(一端处于开启状态,一端处于关闭状态)。由于TCP是全双工通信,所以两端都要关闭才能算是关闭。
    1. 客户端首先调用关闭,发送FIN码给服务端,假设这里的FIN 值为M.
    2. 服务端接收到带有FIN码的请求,执行被动关闭,并回复确认信息给客户端,此时ACK的值为M+1.同时传输给用户一个结束文件,这是因为接收到FIN码意味着服务端不再向客户端发送文件。
    3. 服务端会请求客户端进行关闭连接,发送FIN码的请求,这里假设值为N。同时会开启一个计时器,若超时,没收到客户端发送的确认信息,会自动关闭连接
    4. 客户端同意服务端的关闭确认,回复ACK=N+1给服务端同意连接关闭。
      当客户端发送的FIN码,服务端没收到,会引发客户端重传,当经过N次重传失败的时候,客户端就会放弃重传,关闭连接。但当发送者关闭连接,服务端并不知道,所以服务端会处于完全活动状态,这就导致了半关闭状态。为了避免这种问题,一种解决方案是,当服务端在指定的时间没接收到客户端发送的数据,就自动单方面关闭连接。那么这个半关闭状态问题就解决了。
  3. TCP头部数据格式

| 00~15 | 16~31|
| :—–: | :—–: | :—–:| | :—–: | :——: | :——–: | :—: | :—-: |
| 源端口号 | 目的端口号|
| 序号 |
| 确认号 |
| 4位头部长度 | 6位保留 | URG | ACK | PSH | RST | SYN | 窗口大小 |
| 校检和 | 紧急指针 |
| 选项 |
| 数据 |

Read More

默认配图
计算机网络

LAN技术

广播网络链路

一个节点发送信号,多个节点接收信号,多个节点接收信号的状态有所不同细分成以下三个网络。

单工广播网络

单通道通信,在一个网络环境中,一个只能发送信息,不能接收信息。其它主机只能接收信息,不能发送信息。即单工广播网络

半双工广播网络

在一个网络环境中,存在双向网络通信,这些节点既能发送消息,也能接收消息,但同一时刻只能有单个节点在发送信息。即半双工广播网络

全双工广播网络

在一个网络环境中,存在双向网络通信,一个节点技能发送消息,也能接收消息,这些节点同一时刻既要能发送消息,也需要接收消息。即全双工广播网络。比如说大合唱活动。

点对点网络链路

Read More

默认配图
计算机网络

OSI模型和TCP/IP模型

协议栈

​ 协议栈是一整套完整的网络协议层次,它们一起协同工作,提供网络互联能。它被成为栈,是因为它一般被设计为等级层次结构,每一层都能支持上层通讯使用下层通讯。

​ 层次的数量因模型的不同而不同,TCP/IP 模型有5层(应用层,传输层,网络层,数据链路层和物理层)。OSI模型有7层(应用层,表示层,会话层,传输层,网络层,数据链路层和物理层)。

​ 两个设备间想正常的通讯必须拥有相同的协议栈,这样不同的操作系统之间的电脑才能够正常的通讯。

OSI模型

应用层

定义数据传输协议,例如文件传输用的是FTP还是HTTP

表示层

将应用层传输的数据转化成下层能理解的标准格式,同样的,表示层将接受自会话层的数据转换成应用层能理解的格式。

会话层

​ 一旦资料被转换成正确的格式,发送端主机必须与接收端主机建立一个会话。它主要负责建立、维持和终止与远端主机的会话。会话通常在应用程序间建立。属于软件层面的应用。

传输层

​ 传输层负责维持流控制。例如:windows操作系统上各个应用程序和自身都需要进行网络通讯,传输层提取来自不同程序的数据,并把他们整合为一个单独的数据流。

​ 传输层也负责错误检查和在必要时进行数据恢复。实质上,传输层确保所有的数据能从传输端主机发送到接收端主机。

网络层

​ 网络层决定数据如何到达接收者。它处理像寻址、路由和逻辑协议之类的工作。网络层在资源与目的主机之间创造逻辑路径,也叫虚电路,虚电路为每个数据包提供一条通往目的地的通道。网络层也负责它自己的错误处理,以及数据包的排序和拥塞控制。

​ 数据包的排序是非常重要的,因为许多协议限制了最大数据包的大小,当一个数据过大时,会将数据拆分成多个数据包,这时,网络层就会为每个数据包分配一个序列号。当接收端的网络层接收到数据时,就会检查数据包序列号,并重新组装数据,并检查是否有数据包遗失。

数据链路层

​ 数据链路层可以进一步分为两层,介质访问控制层(MAC)和逻辑链路控制层(LLC),MAC层通过它的MAC地址建立计算机在网络上的身份,MAC地址是硬件给网络适配器分配的地址,它是最终发送和接受数据包时使用的地址。LLC层控制帧同步,并提供一定程度上的错误检查机制。

物理层

​ 物理层是硬件上的规范,定义了什么是用于传输和接收数据的(硬件设备)。

OSI模型的工作原理

​ 数据从主机发送到接收端时,首先从应用层到物理层流动,这时候数据每流经一层都会附带对应层次的协议头,协议头是附加在数据开头处的一段信息,除了物理层不会。接收端接受的数据是自下而上流动的,从物理层到应用层,每一层都会读取对应协议头的信息,用来决定接收端每一层该如何处理接收的信息。读取完之后会去除掉对应层次的协议头信息。

​ 这些协议头根据它们收到的协议头的层次不同而包含不同的信息,但是,都能够告诉对等层许多重要的信息,包括数据包大小,帧和数据报。

​ 每一层的协议头和数据被称为数据包。虽然看起来有点混乱,但每一层给它的服务数据单元都有不同的名字,以下是OSI模型每一层的服务数据单元的通用名称。

层次 封装单元 设备 关键词/描述
应用层 数据 PC 为应用程序提供网洛服务
表示层 数据 为应用层提供标准化的接口
会话层 数据 主机之间的通讯
传输层 报文段 提供端到端的消息传递和数据恢复(可靠性)。按合适的顺序分隔/组传数据。
网络层 数据包 路由器 逻辑选址和路由选择、路由、报告传输错误。
数据链路层 网桥、交换机、NIC 物理地址和介质访问。
物理层 中继器、集线器、收发器 二进制信号传输和编码

TCP/IP模型

应用层

​ 是OSI参考模型中的应用层、展示层、会话层三者综合体。它定义了TCP/IP应用协议与传输层服务交互的主机应用程序接口如何使用网络。

​ 应用层包含所有高级协议: DNS,HTTP,Telnet,FTP,SNMP(简单网络管理协议),SMTP,DHCP(动态主机配置协议),RDP(远程桌面连接协议)。

传输层

​ 和OSI模型中的传输层功能对应,有错误重传和错误诊断功能,充当应用层的传输服务,使用的是TCP或者UDP协议。一个是基于连接的,一个是基于性能的。

互联网层

​ 和OSI的网络层对应,他们的目的是将数据包通过独立的路径选择传输到目的地。

​ 数据的路由和传输时互联网层的主要作用,也是这个体系的关键元素。这一层的主要协议有:IP(网络互连协议),ICMP(互联网控制消息协议),ARP(地址解析协议),RARP(逆地址解析协议)和IGMP(互联网组管理协议)。

网络接口层

​ 定义了数据传输的物理细节,和OSI的数据链路层和物理层功能类似。它主要包含的协议有:以太网、令牌环网、FDDI、X.25、帧中继等。

TCP/IP模型和OSI模型主要区别

  • OSI模型是参考实现,TCP/IP是OSI模型的具体实现

  • TCP/IP协议被认为是互联网发展的标准,而OSI模型则是一个通用的独立于具体协议的标准。

  • TCP/IP由四个等级层次组成,OSI有7层。

Read More

默认配图
未分类

title: 导论
author: steve
date: 2018-05-03
tags:

  • category:

  • 计算机网络

往返时间

​ 在TCP协议中,当一个主机向服务器发送报文段(数据包)时,它就会启动一个计时器。如果计时器在主机接收到报文段确认数据之前发生超时,则主机会重新发送请求给服务器。从计时器开始计时到过期的时间称作计时器超时时间。超时时间包括往返时间和处理时间,其中主要是往返时间。

​ 往返时间的计算(平均偏差算法):

用SampleRTT表示报文段发送出去的时刻到报文段确认的时刻。每个报文段都有与其对应的SampleRTT,显然每个报文段的SampleRTT是不同的。TCP维护了一个SampleRTT的平均值,叫EstimatedRTT。一旦接收到报文段确认并得到SampleRTT,EstimatedRTT的值就会更新。更新公式为:

1
EstimatedRTT = (1 - a) * EstimatedRTT + aSampleRTT

一般取 a = 0.1,则公式变为如下形式:

1
EstimatedRTT = 0.9*EstimatedRTT + 0.1*SampleRTT

串行延迟

​ 串行是指储存在计算机中的数据字节转换成串行的比特流,从而在通信介质上进行传输。串行延迟也被称为传输延迟或延迟。串行所消耗的时间可以用如下公式计算得出:

1
串行延迟 = 数据包大小(比特) / 传输速率(比特每秒)

吞吐量和带宽

​ 带宽是给定时间周期内能通过网络传输的最大数据量,而吞吐量是给定时间周期内能通过网络传输的实际数据量。

Read More

默认配图
机器学习

高等数学

微积分

微积分阐述的是一种微分和积分的思想,从几何上来讲,主要是求曲线函数在某时刻的增量(微分)…

极限

这里将函数的极限,函数的极限存在意义是在某个点上,该函数的左右极限都存在,这样才称该函数存在极限。在该点上函数不一定需要有定义. 比如说 $$f(x) = \frac{1}{x} $$. 当 $$ x \to 0$$ 时,极限为无穷大。虽然该函数在 $$f(0)$$ 处没定义。

连续

函数连续的意义是该函数在该点的左右都连续,且在该点有定义。一个函数连续一定表示该函数有极限,但有极限并不一定表示该函数是连续的。

导数

导数代表的是该函数在某一时刻的变化率大小.从极限的定义上来讲就是当变化率趋近于0时,函数值的变化/变化率 的极限。

微分

微分描述的是一种变化量,除了这一点外,和导数没啥区别,它的定义就是 $$d_y = f’(x) * d_x$$

matlab上计算极限用的时limit命令,导数用的时diff命令。声明变量用的是 syms 命令.

罗尔定理

成立条件:

  1. $$f(x)$$ 在区间 [a,b] 上是连续的
  2. $$f(x)$$在区间(a,b)上是可导的
  3. $$f(a) = f(b)$$

结论: 在$$(a,b)$$上存在一点$$\epsilon$$,使得 $$f’(\epsilon) = 0$$

拉格朗日微分中值定理

成立条件:

  1. $$f(x)$$在区间 [a,b] 上是连续的
  2. $$f(x)$$在区间(a,b)上是可导的

结论: 在$$(a,b)$$上存在一点$$\epsilon$$,使得 $$ f(b)-f(a) = f’(\epsilon)(b-a)$$ 成立

柯西定理

成立条件:

设函数 $$f(x),g(x)$$,满足:

  1. 在闭区间$$[a,b]$$上连续
  2. 在开区间$$(a,b)$$上可导
  3. 对任意 $$x (a,b), g(x) \ne 0$$

结论: 在开区间$$(a,b)$$上存在一点$$\epsilon$$ 使得$$\frac{f(b)-f(a)}{g(b)-g(a)} = \frac{f’(\epsilon)}{g’(\epsilon)}$$成立

泰勒定理

泰勒定理主要是用来对在区间求具体变量 $(x_0)$ 的函数值时将其等价于求在该区间内 $(x-x_0)$ 的 $n$ 项多项式之和 $P_n(x)$。

$f(x)$ $ \approx $ $a_0 + a_1(x-x_0) + a_2(x-x_0)^2 + a_3(x-x_0)^3+ … + a_n(x-x_0)^n$

这个就可以近似的表示 $f(x)$ , 要求 $|f(x) - P_n(x)| = o\lfloor x-x_0 \rfloor ^n$ (关于 $x_0$ 的一个高阶无穷小), 接下来我们来求变量 $a_0,a_1,a_2, … , a_n$ 的表达式.

假设 $f(x) = P_n(x) $ 的倒数随着 $n$ 的增加一直存成立。

因为 $$P_n(x) = a_0 + a_1(x-x_0) + a_2(x-x_0)^2 + a_3(x-x_0)^3+ … + a_n(x-x_0)^n$$ , 有

$$ P_n’(x) = a_1 + 2 a_2(x-x_0) + 3 a_3 (x-x_0)^2 + … + n a_n * (x-x_0)^{(n-1)}$$

$$ P_n’’(x)= 2a_2 + 32a_3(x-x_0) + … + n(n-1)a_n(x-x_0)^{(n-2)}$$

$$ P_n^{(n)}(x) = n!*a_n$$

可得:

$$a_0 = f(x_0)$$

$$a_1 = P_1’(x) = f’(x_0)$$

$$a_2 = \frac{1}{2} * f’’(x_0)$$

$$a_n = \frac{1}{n!} * f^n(x_0)$$

代入原式可得:

$$P_n(x) = f(x_0) + f’(x_0)(x-x_0) + \frac{1}{2!}f’’(x_0)(x-x_0)^2 + … +\frac{1}{n!} f^n(x_0)*(x-x_0)^n \approx f(x)$$

只要能够证明它的余项式为 $\lfloor x-x_0 \rfloor$ 的高阶无穷小量,那么我们就可以证明 $f(x)$ 和 $P_n(x)$ 近似相等。

以上就是我们的 泰勒中值定理 的推论过程, 它的具体定义是:

函数 $f(x)$ 在开区间 $(a,b)$ 具有 $(n+1)$ 阶导数,则当 $x \in (a,b)$ 时,$f(x)$ 可以表示为一个 $(x-x_0)$ 的一个$n$ 次多项式与余项 $R_n(x)$ 的和。公式表示如下

$$f(x) = f(x_0) + f’(x_0)(x-x_0) + \frac{1}{2!}f’’(x_0)(x-x_0)^2 + … +\frac{1}{n!} f^n(x_0)*(x-x_0)^n + R_n(x)$$

其中 $R_n(x)$ 的值一般为拉格朗日型余项。值为:

$$R_n(x) = f(x) - P_n(x) = \frac{1}{(n+1)!}f^{(n+1)}(\xi)(x-x_0)^{(n+1)}$$

所以可得 泰勒公式

$$f(x) = f(x_0) + f’(x_0)(x-x_0) + \frac{1}{2!}f’’(x_0)(x-x_0)^2 + … +\frac{1}{n!} f^n(x_0)*(x-x_0)^n +\frac{1}{(n+1)!}f^{(n+1)}(\xi)(x-x_0)^{(n+1)}$$

matlab实践:

求函数在$[-4,2]$极值: $ x^3 + 2x^2 - 5x +1$

首先求该函数的一阶导数:

1
2
3
4
5
>> syms x
>> y = x^3 + 2*x^2 - 5*x +1
>> dy = diff(y)
3*x^2 + 4*x - 5

再求驻点

1
2
3
4
5
6
>> xz = solve(dy)
>> xz = double(xz)
xz =
-2.1196
0.7863

得到两个驻点 $-2.1196$ 和 $0.7863$

再求二阶导数,判断该驻点对应的值是极大值还是极小值

1
2
3
>> ddy = diff(dy)
6*x + 4

代入两驻点得

1
2
3
4
5
6
7
8
>> x1 = limit(ddy,x,-2.1196)
x1 =
-10897/1250
>> x2 = limit(ddy,x,0.7863)
x2 =
43589/5000

可知当$x$趋向于两驻点时,它的二阶导数的值一个为正数,一个为负数。可知在 $x_1$ 点,函数存在极大值点。在 $x_2$ 点,函数存在极小值点。也可以通过画图明显看出结果

1
>> fplot('x^3 + 2*x^2 - 5*x + 1',[-4,2])

注意这里的 $fplot$ 函数第一个参数只接受常量表达式.

也可以用 $fminbnd$ 函数来直接求在区间上的最小值。

1
2
3
4
>> fminbnd('x^3 + 2*x^2 -5*x + 1',-4,2)
ans =
0.7863

求最大值只需给函数加个负号即可

积分

不定积分

要了解积分的概念先了解一下原函数的概念:

$f(x)$的定义域属于 $I$ ,设 存在 函数 $F(x)$ 也在区间 $I$ 上有意义并且可导。 当 $F’(x) = f(x) $ $x \in I$ 或者 $dF(x) = f(x) dx $ $x \in I$ 成立时,则称 $F(x)$ 为 $f(x)$ 的原函数。

设 $C$ 为常数,$\because (F(x) + C)’ = F’(x) = f(x) \therefore F(x) + c $ 也是$f(x)$ 的原函数。

原函数的主要性质:

  1. 无穷性。
  2. 相关性(原函数之间的差的导数为一个常数)。
  3. 在某个区间内连续的函数必有原函数,因为初等函数在定义域内时连续的,所以初等函数的原函数有无穷多个。

定义:

函数 $f(x)$ 在某区间的所有原函数的集合称为 $f(x)$ 的不定积分,记作: $\lmoustache f(x)d_x$

不定积分的主要性质:

  • 不定积分的导数等于原函数. $(\lmoustache f(x)d_x)’ = f(x)$
  • 不定积分的微分等于被积表达式. $\lmoustache f’(x) d_x = f(x) + C $ 或者 $\lmoustache df(x) = f(x) + C$

不定积分的表达式: $\lmoustache f(x)dx = F(x) + C$

定积分

定义:

函数 $f(x)$ 在区间 $[a,b]$ 上是有界函数,在其上取 $n-1$ 个分点,使得 $a = x_0 < x_1 < x_2 < …. < x_n = b$,把区间 $[a,b]$ 划分成 $n$ 个小区间 $[x_0,x_1],[x_1,x_2],[x_3,x4],[x{i -1},xi],[x{n-1},x_n]$, 各个小区间的长度为$\vartriangle x = xi - x {i-1}$ $ x=(1,2,…,n)$, 在每个小区间 $[x_{i-1},x_i]$ 上任取一点 $\xii$ , 作和式 $\sum{i=1}^{n}f(\xi_i)\vartriangle x_i$, 记 $\vartriangle x = max{\vartriangle x_1,\vartriangle x_2, \vartriangle x_3 … \vartriangle x_n}$ 。无论区间 $[a,b]$ 怎样划分,也不管 $\xii$ 怎样的取获取,当 $\vartriangle x \to 0 $ 时,极限 $lim {\vartriangle x \to 0} \sum_{i=1}^n f(\xi_i)\vartriangle x_i$ 存在且为值 $I$ , 则称 $I$ 为函数 $f(x)$ 在区间 $[a,b]$ 上的定积分,简称为积分,记作: $\lmoustache _a^b f(x) dx$

定积分的函数表达式:

$\lmoustache a ^b f(x)dx = lim {\vartriangle x \to 0} \sum_{i=1}^n f(\xi_i)\vartriangle x_i = F(b) - F(a)$

这也就是 牛顿-布莱尼茨公式 .

matlab实践

matlab种对积分求不定积分表达式和定积分对的积分值用的都是int方法,下面来看一下对 $y = sin(x)$ 的不定积分和在 $(0,\frac{\pi}{2})$ 的积分值怎么求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>> syms x
>> y = sin(x)
y =
sin(x)
>> int(y)
ans =
-cos(x)
>> int(y,x,0,pi/2)
ans =
1

求解微分方程:

matlab 中用 $dsolve$ 方法来求解微分方程的通解和特解。其中表示y的 $k$ 阶导数用 $Dky$ 来表示。例如:

求 ,$(1+x^2)y’’ = 2x*y’$ 当 $y(0) = 1$ $y’(0) = 3$ 的特解:

1
2
3
4
5
6
7
8
9
10
11
12
>> syms x
>> eqn='(1+x^2)*D2y = 2*x*Dy'
eqn =
'(1+x^2)*D2y = 2*x*Dy'
>> dsolve(eqn,'y(0)=1,Dy(0)=3','x')
ans =
x*(x^2 + 3) + 1

$dsolve$ 方法的第二个参数为方程的初始条件,若是求通解,就不需要加上这个参数了。$x$ 是表示对 $x$ 求微分。默认是对 $t$ 求微分

好想法(习题解析)

$question1: $ 有隐式函数 $( w(x) + 1 )* exp(w(x)) = x$ , 当 $x = 1$ 时,$w(x) = 0$ .求 $w(3/2)$ 的近似值.

$answer:$ 这个题目需要用到线性逼近的思路来做,因为所求的点为 3/2,可以近似的认为 $w(\frac{3}{2})$ 和 $w(1)$ 的值在一条直线上。所以根据 $n=1$ 项的泰勒展开式可得在 $x=1$ 时,该线性函数方程式为 $w(x) \approx w(1) + w’(1)(x-1) $ .现在只需求得 $w’(1)$ 的值即可。这个对两边求导化简即可。

Read More

默认配图
java

坑爹的面经

map的底层结构

jdk8中map底层结构是 数组+链表+红黑树组成的。
在map初始化会赋予一个固定长度的数组,放入数据的时候,先对key进行hash操作,计算对应的数组下标,并放入对应数组元素的链表头中,当再次放入key的hash值一样时,会将数据放在对应链表的头部,最先放入的元素在后面.当放入的链表长度大于8时,会用一个红黑树结构的treemap来替换掉原来的链表结构,它的查找时间是O(logn).

map有哪些实现 以及ConcurrentHashMap的线程安全是怎么实现的.

HashMap, LinkedHashMap, ConcurrentHashMap.
ConcurrentHashMap的线程安全主要是 Synchorized + CAS + volatile 实现的。get方法由于变量是用了Volatile修饰,所以不加锁也能读取到最新的值。put方法加上了Synchorized锁。

拓展延伸:红黑树

Object的方法

getClass 方法,hashCode方法,toString方法,wait方法,notify方法,clone方法,finalize方法.

讲讲NIO

NIO是非阻塞的同步I/O,非阻塞体现在连接是非阻塞的,同步体现在进行I/O操作时是同步等待的。
NIO主要有三个主要的概念,Channel,缓冲区Buffer和Selector。Channel是一个通道的意思,负责连接,缓冲区Buffer是一个容器对象,发送给一个Channel的所有对象必须先放入缓冲区Buffer中,同理,从Channel中读取的所有对象也必须从Buffer中读取。Selector就是一个你对发生在Channel上感兴趣的事件做出响应的一个对象,由它告诉你发生了什么事情。
注: 深入问问题就是buffer中数据的读写,为什么要调用flip方法(单索引),

OSI 7层网络模型

物理层: 利用传输介质为数据链路层提供物理连接,实现比特流的透明传输

数据链路层: 将物理层的位流形式的数据封装成帧,传输到上一层. 有MAC/LLC协议..

网络层: 将数据链路层的数据转换成数据包然后通过路径选择,分段组合,顺序,进/出路由控制将数据从一个网络设备传输到另一个传输设备.

传输层: 该层起承上启下的作用,将上层协议段数据变成报文的来进行正确传输. 有TCP/IP, UDP协议等等

会话层: 维持用户程序和网络之间的会话通信,使用远程地址建立连接(域名)

表示层: 通信数据的加密解密,数据格式的处理

应用层: 用户与应用程序的交互层.

第1-3层为通信层,5-7层为资源层,4层为承上启下层。

用户输入 baidu.com 到 baidu.com 返回数据,期间发生了什么

客户端通过域名访问DNS服务器得到具体的ip地址,通过这个ip地址发送HTTP会话,建立HTTP长连接, 通过TCP协议对数据进行封装,传输到网络层,网络层找到对应的baidu服务器,服务器再通过负载均衡找到对应的响应服务器.响应服务器再组织数据通过网络层返回数据给用户所在主机,用户的浏览器就会解析返回的数据并渲染显示在用户面前。

注: 要详细讲的话,很长很长。主要是协议,负载均衡等知识。

TCP 协议为什么是可靠的

确认机制: 三次握手同步双方的“序列号+确认号+窗口大小信息”

重传机制: CheckSum校验失败,丢包或者延时,发送端重传

数据排序: TCP有专门的序列号SN字段,可提供数据的re-Order

流量控制: 窗口和计数器的使用,TCP窗口中会指明双方能够发送接收的最大数据量

拥塞控制:

  • “慢启动”(Slow Start)
  • “拥塞避免”(Congestion avoidance)
  • “快速重传 ”(Fast Retransmit)
  • “快速恢复”(Fast Recovery)

为什么wait和notify方法只能在 Synchorized 块中使用

抛开再java中不加Synchorized块出现的异常问题,还有可能会导致竞态条件的问题。当另一个线程先调用notify,再调用wait的话,有可能会永远的wait下去。

SpringBoot启动流程

先将应用打包成一个jar/war包,再用JarLancher根据jar/war的文件所在url创建一个Archive,然后构建一个LaunchedURLClassLoader类加载器,再去加载我们的应用主类,从而启动引用程序。

Spring如何处理一个请求

DispatcherServlet –> HandlerMapping
DispathcerServlet –> HandlerMapping –> Controller 方法 –> ModelAndView –> ViewResolver –> 渲染 –> DispatcherServlet // 返回视图

方法区OOM

方法区保存的是Class文件和Meta的相关信息,有类名,访问修饰符,常量池,字段描述,方法描述等。当运行时产生大量的类时就很容易发生OOM(动态代理).

线程池原理

除了ScheduledThreadPool的实现原理有点不同,其余几个都是通过 ThreadPoolExecutor 来实现的. ThreadPoolExecutor 内部是通过一个 AtomicInteger 的变量 ctl 来控制, 这个变量的前三位是用来表示线程词的运行状态,后29位表示当前安线程池的数量.

Read More

默认配图
健康生活

痔疮免开刀疗法

黄柏,龙胆草,紫花地丁,白鲜皮,苦参,地肤子,土茯苓,荆芥各20g熬水外洗,每付可洗两天,每日一次,

再配合吃一般的消炎药,一个月后就会看到效果了。但需要根治还是需要手术。

Read More

默认配图
Java

动态规划

从典例开始了解动态规划的前世今生

问: 有4种面值为1,5,10,25的货币,可以重复使用,需要凑成1000元,问共有多少种货币组成方案?
分析问题:

  • 当使用0张面值为5元的货币,剩下由{1,10,25}组成1000元的方案总数计为 res1
  • 当使用1张面值为5元的货币,剩下由{1,10,25}组成的995元的方案总数计为 res2
  • 当使用199张面值为5元的货币,剩下由{1,10,25}组成的5元的方案总数计为 res200
  • 当使用200张面值为5元的货币,剩下由{1,10,25}组成的0元的方案总数计为 res201

上述求方案总数的方法都会用到一个递归函数。
那么这里总的方案数为上述结果的累加,即: res1 + res2 + … + res201

这里就可以定义这个递归函数: int(int[] arr, index , aim);
这个递归函数表示的含义是如果用arr[index … N-1] 的面值货币组成的 aim 目标钱数的方案总数
于是就可以得到以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
private int violence(int[] source, int index, int aim) {
int res = 0; // 方案总数
if(index == source.length){
res = aim == 0 ? 1 : 0; // aim 为 0 表示这种方案可行,所以res = 1; 结果累加即可
}else{
for(int i = 0; source[index] * i <= aim ; i++){
// 固定一种货币情况下的递归函数求计算总数
res += violence(source,index ++ , aim - source[index] * i);
}
}
return res;
}

这种方法又叫暴力搜索算法。但是有个重复计算的问题,比如说在已经使用0张5元和1张10元的情况下,会求volience(source,2,990)的值。当使用2张5元和0张10元的情况下,也会求一次violence(source,2,990)的值。这就造成了重复计算的问题,这种问题在这个题目中出现的次数非常之多,所以我们需要将这些计算过结果的情况保存下来,所以出现了下面将要介绍的记忆搜索算法。

这里的记忆搜索算法的记忆是怎么实现的呢?需要怎样记忆?记忆什么内容?这些问题将是我们需要进一步探讨的。
从上面的分析可知,因为source参数是不变的,只有index和aim值是变化的。所以我们需要记忆的就是violence(index, aim)的值。有两个可变参数,所以我们用一个二维数组的map来记忆这个值。记为 int[][] map = new int[i][j];
i 为index 的大小 +1 ,j 为 aim的大小 + 1。 map[i][j]的值就表示当index = i, aim = j的情况时的方案总数。代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private int memory(int[] source, int index, int aim,int[][] map) {
int res = 0;
if(index == source.length){
res = aim == 0 ? 1 : 0;
}else{
int mapValue = 0;
for(int i = 0; source[index] * i <= aim ; i++){
mapValue = map[index + 1][aim-source[index]*i];
if(mapValue != 0){
res += mapValue == -1 ? 0 : mapValue;
}else{
res += violence(source,index ++ , aim - source[index] * i);
}
}
}
map[index][aim] = res == 0 ? -1 : res;
return res;
}

上面的这个记忆搜索算法就相当于动态规划的一种形态了。下面讲讲两者的区别和联系:

  • 记忆搜索不关心达到某一个递归过程的路径,只是单纯的对计算过的递归过程进行记录,避免重复递归的过程。
  • 动态规划严格规定了到达目标的路径,只能依次计算得到结果。后面的结果严格依赖前面的计算结果。
  • 两者都是用空间换时间的做法,也都有枚举的过程,一个有顺序,一个没顺序。

就多了一个有顺序的枚举过程,为什么一定要去学动态规划呢?那正是因为有顺序的枚举,才有了可以优化算法的空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private int dpm(int[ ] source, int aim, int[][] dp){
dp[0][0] = 1;
for(int i=0;i<dp.length;i++){
dp[i][0] = 1;
for(int j=1;j<dp[0].length;j++){
if( i == 0 ){
dp[i][j] = (j % source[i] == 0) ? 1 : 0;
}else{
// 未优化
// for(int k = 0; k <= aim / source[i] && (j - source[i] * k) >= 0; k++){
// dp[i][j] += dp[i-1][j - source[i]*k ];
// }
// 优化后,少了枚举过程
dp[i][j] = dp[i-1][j];
dp[i][j] += (j - source[i] >= 0) ? dp[i][j - source[i]] : 0 ;
}
}
}
return dp[source.length-1][aim];
}

具体的动态规划过程参考动态规划(1)

Read More

默认配图
Java

java并发编程

1. 线程状态

一张图详解java多线程的多种状态以及状态之间的关系

2. 多线程的实现方式

在java应用中,共有8中实现多线程的方式,如果是spring应用,还可以有spring的基于注解的多线程开启方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.*;
/**
* @Author: steve
* @Date: Created in 13:33 2018/1/22
* @Description: 线程的9种创建方式
* @Modified By:
*/
@Configuration
@ComponentScan()
@EnableAsync
public class Main {
/**
* 继承Thread类重些run方法来实现新线程
*/
static class Demo1 extends Thread{
@Override
public void run() {
System.out.println("Thread change run implements running ....");
}
}
/**
* 实现Runnable接口来实现新线程的创建
*/
static class Demo2 implements Runnable{
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Runnable implements method ... ");
}
}
static class Demo3 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("返回值的方式创建线程");
Thread.sleep(3000);
System.out.println("返回值的方式创建线程");
return 1;
}
}
@Service
class Demo4{
@Async
public void printA() throws InterruptedException {
while(true) {
Thread.sleep(300);
System.out.println("a");
}
}
@Async
public void printB() throws InterruptedException {
while(true) {
Thread.sleep(300);
System.out.println("b");
}
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1
Demo1 demo1 = new Demo1();
demo1.run();
// 2
Thread thread = new Thread(new Demo2());
thread.start();
// 3
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Demo3());
Thread thread1 = new Thread(futureTask);
thread1.start();
System.out.println("结束了吗?");
System.out.println("计算的结果: " + futureTask.get());
// 4 匿名内部类
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类的方式创建线程!!!");
}
});
thread2.start();
// 5
Thread thread3 = new Thread(){
@Override
public void run() {
System.out.println("重写run方法");
}
};
thread3.start();
// 6 Spring实现多线程技术
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Main.class);
Demo4 threadService = applicationContext.getBean(Demo4.class);
threadService.printA();
threadService.printB();
// 7 lambda 方式实现多线程技术
List<Integer> lists = new ArrayList<Integer>(){
{
add(1);
add(2);
add(3);
add(4);
}
};
lists.parallelStream().forEach(System.out :: println);
// 8 线程池的实现方法
ExecutorService executorService = Executors.newFixedThreadPool(10); // 这里可以有多种不同的实现选择
for(int i=0; i<100; i++){
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
// 9 定时器实现多线程
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("定时器的多线程");
}
},1000,3000);
}
}

3.线程安全性问题

先来了解一下java多线程引发安全问题的3个前提

  1. 多线程条件下
  2. 有共享资源
  3. 非原子性操作

看个例子

1
2
3
4
5
6
7
8
9
10
11
public class SingletonHungry {
private SingletonHungry(){}
private static SingletonHungry singletonHungry = new SingletonHungry();
public static SingletonHungry getSingletonHungry() {
return singletonHungry;
}
}

这是典型的饿汉式单例模式,不存在线程安全性问题,因为getSingletonHungry()方法是原子性的。再看一个懒汉式的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SingletonLazy {
private SingletonLazy(){}
private static SingletonLazy singletonLazy;
public static SingletonLazy getSingletonLazy() throws InterruptedException {
if(singletonLazy == null){
synchronized (SingletonLazy.class){
if(singletonLazy == null){
singletonLazy = new SingletonLazy();
}
}
}
return singletonLazy;
}
}

表面看这个代码没问题,也加了锁,但是这里还有一个涉及到指令重排序的问题,就是说jvm执行上述代码的时候并不一定是按照正常顺序来执行的。比如,正常来讲,jvm实例化对象是首先申请一块内存,然后实例化对象,最后再指向对象。然而经过指令重排序的优化过程,有可能会先实例化对象,再去执行其他步骤,这样就可能造成第二个if(singletonLazy == null)的判断不准确。所以这里需要给singletonLazy加个 volatile 关键字,意思是不给这个对象进行指令重排序等jvm执行优化操作。
在这里我们研究研究 synchronized 这个关键字,在现在的这个版本(jdk8),这个关键字已经被进行了多重优化,在某些情况下可以摆脱重型锁的束缚了。在这里我们讲讲它的应用步骤:

  • 偏向锁:jvm执行引擎会一直获取锁信息,只有当发生竞争的时候,它才会放弃锁,然后再重新获取对象锁信息(锁信息保存在对象头中)。相比较以前每次进入方法前都需要获取锁信息操作性能提升了不少
  • 轻量级锁: 在多线程竞争的环境下,线程尝试获取被加锁的对象时,会先将对象头(里面包含对象的锁信息)复制一份,然后尝试获取对象锁,如果获取锁失败,再重复一次复制操作。这叫做自旋锁。直到获取成功。
  • 重量级锁: 多次自旋操作失败后就会升级为重量级锁。

参考:Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)

3.1 synchorized

synchroized从1.6开始变得不再像以前那么的重量级了,它有三个转变阶段,分别为偏向锁,轻量级锁,重量级锁。它的主要作用是锁住对象,保证只有一个线程能获取到该对象的实例,从而保证线程安全。它可以使用在方法体中和方法声明中,在方法声明中代表的是锁住这个类的对象,方法体中看实现可以随意锁住某个对象。日常使用建议锁的细粒度越细越好。以前jdk中的Vector类就是通过锁方法而保证线程安全的,现在已经不再使用它了,而Collections中以synchronized 开头的方法锁的是一个mutex对象,而不是类对象。
SynchronizedList的get方法

1
2
3
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}

Vector的部分方法

1
2
3
4
5
6
7
8
9
10
11
public synchronized int capacity() {
return elementData.length;
}
/**
* Returns the number of components in this vector.
*
* @return the number of components in this vector
*/
public synchronized int size() {
return elementCount;
}

3.2 volatile 关键字

有时候只需要保证共享变量在各个线程中的数据的一致性,就不需要使用synchorized这个锁了,只需要在变量声明时加上volatile关键词就行了,JMM实现了对volatile的保证:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作,它的汇编语义就是一个Lock指令,这个指令会将CPU里的缓存写入系统内存。所以每个线程读取到的数据都是一致的。然而这样也有一个弊端,随着CPU的三级缓存越来越重要了,这样做无非会降低系统的性能。而且volatile的使用也有一个条件,变量真正独立于其他变量和自己以前的值。比如什么 x++ 操作就不行,依赖于以前的x值。 具体使用请参考正确使用 Volatile 变量

3.3 Lock 锁

lock更强调的是锁住代码块,与对象无关。而且他的实现更有针对性。由于是java实现的,所以它拥有许多synchorized没有的特性,比如说公平与非公平锁(内部维护一个FIFO队列)。

  • ReentrantLock 这是Lock的一个常用实现,身为一个可重入锁,底层是基于AQS实现的,使用也很简单
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class ReentrantLockDemo {
    private int value;
    private Lock lock = new ReentrantLock(); // 需要保证多个线程拿到的是同一个lock对象。
    public int getNext(){
    lock.lock();
    try {
    return value++;
    }finally {
    lock.unlock();
    }
    }

3.4 ReadWriteLock 锁

在某些情况下,读和写操作并不一定一样多,所以就诞生了这个读写锁。

  • ReentrantReadWriteLock 时ReadWriteLock的一个实现,通过这个实例,可以获取到对应的读锁和写锁,从而根据实际需求使用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    /**
    * @Author: steve
    * @Date: Created in 11:20 2018/1/23
    * @Description: 读写锁使用实例
    * 读锁为共享锁,写锁为独占锁。
    *
    * @Modified By:
    */
    public class ReadAndWriteDemo {
    private Map<String, String> map = new HashMap<>();
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private Lock readLock = lock.readLock();
    private Lock writeLock = lock.writeLock();
    /**
    * 读锁 方法
    * @param key
    * @return
    */
    public String get(String key){
    readLock.lock();
    System.out.println(Thread.currentThread().getName() + " 读取锁开始 。。。。。。 ");
    try {
    Thread.sleep(300);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    try{
    return map.get(key);
    } finally {
    readLock.unlock();
    System.out.println(Thread.currentThread().getName() + " 读取锁结束 。。。。。。");
    }
    }
    /**
    * 写锁 方法
    * @param key
    * @param value
    */
    public void put(String key, String value){
    writeLock.lock();
    System.out.println(Thread.currentThread().getName() + " 写入锁开始 。。。。。。 ");
    try{
    Thread.sleep(300);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    try{
    map.put(key,value);
    }finally {
    writeLock.unlock();
    System.out.println(Thread.currentThread().getName() + " 写入锁结束 。。。。。。 ");
    }
    }
    private boolean isUpdate = true;
    /**
    * 降级锁 方法
    * 由写锁降级为读锁
    */
    public String readWrite(String key,String value){
    readLock.lock(); // 加读锁,保证变量在线程间的一致性
    if(isUpdate){
    readLock.unlock();
    writeLock.lock();
    map.put(key,value);
    readLock.lock();
    writeLock.unlock(); // 在写锁释放前给读锁加锁
    }
    try{
    return map.get(key);
    }finally {
    readLock.unlock();
    }
    }
    public static void main(String[] args) {
    ReadAndWriteDemo demo = new ReadAndWriteDemo();
    readWriteDemo(demo);
    }
    public static void readWriteDemo(ReadAndWriteDemo demo){
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.put("steve1","liu");
    }
    }).start();
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.get("steve1");
    }
    }).start();
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.put("steve2","liu");
    }
    }).start();
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.get("steve1");
    }
    }).start();
    }
    public static void readDemo(ReadAndWriteDemo demo){
    demo.put("steve1","liu");
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.get("steve1");
    }
    }).start();
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.get("steve1");
    }
    }).start();
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.get("steve1");
    }
    }).start();
    }
    /**
    * 写锁测试用例
    * @param demo
    */
    public static void writeDemo(ReadAndWriteDemo demo){
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.put("steve","liu");
    }
    }).start();
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.put("steve","liu");
    }
    }).start();
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.put("steve","liu");
    }
    }).start();
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.put("steve","liu");
    }
    }).start();
    }
    }

3.5 StampedLock 锁

ReentrantReadWriteLock 在沒有任何读写锁时,才可以取得写入锁,这可用于实现了悲观读取(Pessimistic Reading),即如果执行中进行读取时,经常可能有另一执行要写入的需求,为了保持同步,ReentrantReadWriteLock 的读取锁定就可派上用场。

然而,如果读取执行情况很多,写入很少的情况下,使用 ReentrantReadWriteLock 可能会使写入线程遭遇饥饿(Starvation)问题,也就是写入线程吃吃无法竞争到锁定而一直处于等待状态。

StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。

所谓的乐观读模式,也就是若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定,程序可以查看读取资料之后,是否遭到写入执行的变更,再采取后续的措施(重新读取变更信息,或者抛出异常) ,这一个小小改进,可大幅度提高程序的吞吐量!!

3.6 重入锁

当被锁住的方法里面调用了其他也加了锁的方法时,如果可以调用成功,就说这个锁是可重入锁,这个调用操作成为锁重入。话不多说,看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ThreadReentrant {
/**
* synchronized 标志的方法锁住的是当前的类实例
*/
public synchronized void a(){
System.out.println("a ...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
b();
}
public synchronized void b() {
System.out.println("b ... ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ThreadReentrant threadReentrant1 = new ThreadReentrant();
new Thread(() -> threadReentrant1.b()).start();
new Thread(() -> threadReentrant1.a()).start();
}
}

3.7 自旋锁

就是等待其他线程结束的一个操作,相当于while-true。话不多说,看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程开始 。。。 ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "线程结束 。。。 ");
}).start();
while(Thread.activeCount() != 2){
}
System.out.println("over 。。。");
}

3.8 死锁

就是两个线程,两个资源之间的一种竞争关系,A线程持有资源A,接下来想要取到资源B,但是B线程同时持有资源B,想要获取资源A。然后两个线程一直处于等待状态,导致死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class DeadLockDemo {
private static final String A = "A";
private static final String B = "B";
public static void main(String... args){
new DeadLockDemo().deadLock();
}
private void deadLock(){
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(A) {
try {
Thread.sleep(2); //sleep 方法可是演示并发案例的好工具
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(B){
System.out.print("1");
}
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(B){
synchronized(A){
System.out.println("2");
}
}
}
});
thread1.start();
thread2.start();
}
}

3.9 AQS的原理

AQS是jdk8并发包里的抽象类AbstractQueuedSynchronizer的简称。先来看看他的文档,看看他的设计:

为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。子类必须定义更改此状态的受保护方法,并定义哪种状态对于此对象意味着被获取或被释放。假定这些条件之后,此类中的其他方法就可以实现所有排队和阻塞机制。子类可以维护其他状态字段,但只是为了获得同步而只追踪使用 getState()、setState(int) 和 compareAndSetState(int, int) 方法来操作以原子方式更新的 int 值。

这一段表明了它的作用是用于实现相关同步器,还讲了部分原理,他的内部实现是依靠一个FIFO的等待队列,来实现对象的等待唤醒机制。内部有一个int类型的变量保存对象的同步状态。

应该将子类定义为非公共内部帮助器类,可用它们来实现其封闭类的同步属性。类 AbstractQueuedSynchronizer 没有实现任何同步接口。而是定义了诸如 acquireInterruptibly(int) 之类的一些方法,在适当的时候可以通过具体的锁和相关同步器来调用它们,以实现其公共方法。

这一段表明了它的使用方法。

此类支持默认的独占 模式和共享 模式之一,或者二者都支持。处于独占模式下时,其他线程试图获取该锁将无法取得成功。在共享模式下,多个线程获取某个锁可能(但不是一定)会获得成功。此类并不“了解”这些不同,除了机械地意识到当在共享模式下成功获取某一锁时,下一个等待线程(如果存在)也必须确定自己是否可以成功获取该锁。处于不同模式下的等待线程可以共享相同的 FIFO 队列。通常,实现子类只支持其中一种模式,但两种模式都可以在(例如)ReadWriteLock 中发挥作用。只支持独占模式或者只支持共享模式的子类不必定义支持未使用模式的方法。

这一段表明AQS支持两种锁模式,一个是独占模式,典型的应用是ReentrantLock,另一种是共享模式,这个例子就比较多了,如CountDownLatch和CyclicBarrier。然后介绍了一些常见的使用情景。

为了将此类用作同步器的基础,需要适当地重新定义以下方法,这是通过使用 getState()、setState(int) 和/或 compareAndSetState(int, int) 方法来检查和/或修改同步状态来实现的:

  • tryAcquire(int)
  • tryRelease(int)
  • tryAcquireShared(int)
  • tryReleaseShared(int)
  • isHeldExclusively()

默认情况下,每个方法都抛出 UnsupportedOperationException。这些方法的实现在内部必须是线程安全的,通常应该很短并且不被阻塞。定义这些方法是使用此类的 唯一 受支持的方式。其他所有方法都被声明为 final,因为它们无法是各不相同的。

这一段就是告诉你要实现自定义的同步器需要适当的重写这几个方法.下面我们就 ReentrantLock 这个独占锁类来看看AQS给我们提供的功能吧。
lock方法

1
2
3
public void lock() {
sync.lock();
}

看源码可以知道sync是ReentrantLock内部的一个AQS实现的抽象类,它还有两个实现类NonfairSync和FairSync,就是公平锁和非公平锁的实现。它的lock方法是个抽象方法,所以我们需要找到它的默认实现,看源码可知默认实现是非公平锁,所以我们就看非公平锁的lock方法实现

1
2
3
4
5
6
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

这里首先用CAS方法尝试将State的值变为1(state就是维持同步器的状态的那个int值,CAS内部实现可以理解为一个while无限循环,当当前的值等于expect时,会将当前的值设置为update的值),设置成功后就调用setExclusiveOwnerThread方法,并将当前线程传入进去,这个方法就是将传入的线程赋值给了exclusiveOwnerThread 变量,这个变量就是保存的当前的线程。设置失败后就会调用AQS的acquire方法,来看这个方法的实现

1
2
3
4
5
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

arg就是1,这个是state的值。表示同步器的状态计数器。如果if里面的条件不成立,这个对象(ReentranLock实例)就不会上锁(state没增加就表明当前lock失败)。我们继续来看它的tryAcquire(arg)的实现,这个方法内部只有一行,调用了nonfairTryAcquire方法。来看源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

首先令current为当前的变量,将当前的state赋值给c,当state的值等于0时,会进行一次CAS,将state修改为acquires的值,也就是1。返回true。当state的值不为0的时候并且当前线程等于内存中存的值(getExclusiveOwnerThread 方法返回的就是exclusiveOwnerThread的值)时(这一步判断是为了防止这不是重入进来的),将c 和 acquires 的值相加并赋值给nextc变量,当nextc的值出现小于0的情况时,表明内存发生了溢出。抛出异常。如果没发生内存溢出就将state的值设为(c + acquires),返回true; 还有其它的情况返回false。 当tryAcquire(arg)方法返回true时,那么表明acquire方法获取成功了。当tryAcquire返回false,我们再来看acquireQueued(addWaiter(Node.EXCLUSIVE), arg))这个方法,首先来看addWaiter的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}

这里面的node表示的是FIFO里面的一个个对象,这个FIFO是通过双向链表实现的。传入的mode是Node.EXCLUSIVE,看源码可知它是null的,表示的是独占模式下的一个等待节点(Node源码自行查看,这里就不提了)。然后令pred = tail节点,如果tail节点不为空,则会尝试将node节点设置为尾节点(tail),设置失败就只是一个单向链表指向,设置成功就变成双向链表了并返回node。如果tail节点为空(表明刚初始化)。执行enq(node)方法,继续来看它的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

有了前面的基础,再看这段代码会觉得很简单,就是一个无限循环,强制保证node这个传入的节点设置为tail节点。回到上一个方法addWaiter, 往下执行就是返回这个node了。在回溯到acquire方法,这时候是到了acquireQueued(addWaiter(Node.EXCLUSIVE), arg))这个方法了,来看它的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

这里的两个参数我们可知node为链表的尾节点(FIFO的尾部),arg为1.看这个无限for循环,首先令p为node的前一个节点(predecessor方法就是返回前一个节点),判断p是否为head节点并且试图设置容器的状态值,两个条件都成立就执行setHead方法将node节点设置为head节点,并将p节点的下一个节点设置为null,就是解除了node节点和node前一个节点的关联关系。来看看setHead方法

1
2
3
4
5
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}

这里将node节点的部分属性都设置为null了。返回上一个调用方法acquireQueued,这里将failed变量设置为false,并返回了一个false结果(根据变量名可以猜到这是一个表示当前锁没被中断的结果),结束了无限循环。如果前面的条件不成立,进入到shouldParkAfterFailedAcquire(p, node)和parkAndCheckInterrupt() 两个方法的判断,这里主要是判断锁状态是否被中断。看看他们的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

这一段首先令ws变量为节点pred的waitStatus(等待状态值),它共有以下几种情形

  • CANCELLED(1) 表示线程退出了
  • SIGNAL(-1) 表示线程状态为等待唤醒(unparking)
  • CONDITION(-2) 表示线程在等待指定的条件(condition)成立
  • PROPAGATE(-3) 表示一个共享的释放锁应该传播给其它的节点
  • 0 默认值,无意义
    首先判断ws是否为SIGNAL,是的话返回true,表示这个节点处于等待唤醒状态,可以释放(ralease)它了,如果ws的值大于0的话,执行一个do-while循环,循环代码表示意义是当pred的等待状态值(waitStatus)大于0的时候,令pred指向pred的前一个节点,node的前一个节点指向pred节点,这样一步一步往FIFO队列的前方挤去。当条件不成立退出循环时令pred的next节点指向node节点,继续使这两个节点为双向链表指定。当ws值小于0的时候,执行CAS方法将pred节点的waitStatus变为SIGNAL,就是进入等待唤醒状态。然后返回false. 返回到上一个方法中看第二个判断条件parkAndCheckInterrupt方法。
    1
    2
    3
    4
    private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
    }

这段代码简单点理解就是返回当前线程是否被中断。我们可以继续看看park方法的实现

1
2
3
4
5
6
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}

这里就是将blocker对象设置为阻塞状态,这段代码有点难以理解,以后再看看。
到这里为止,我们就能大概的理解开头的AQS文档了,剩下的代码也不多了,看看也能懂个大概。就不再细述了,其实AQS的底层原理还是依赖了unsafe类的CAS和park方法,CAS设置状态值,park设置线程状态。设计模式使用的是模板方法设计模式,只要子类实现它的5个方法就可以直接用了。

参考资料:
CAS详解:JAVA CAS原理深度分析
Unsafe: JAVA并发编程学习笔记之Unsafe类

4.线程间通信

这里就举个生产者与消费者的例子来演示线程间的通信。

4.1 wait/notify 方法

wait是一个等待方法,和本文开头的流程图说的一样,当线程执行到wait时,进入到等待状态,并同时释放对象的锁,然后只能等待对象的notify或者notifyAll方法唤醒。如果这时候有关闭线程的操作发出,则会抛出 InterruptedException 异常。被唤醒后会继续执行后面的代码。

4.2 await/signal 方法

这两个比上面的两个方法细粒度更小一点,可以指定对象等待和唤醒。可控度更高一点,具体看示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
public class ConditionDemo {
private int signal = 0; // 控制等待状态。
Lock lock = new ReentrantLock();
Condition a = lock.newCondition();
Condition b = lock.newCondition();
Condition c = lock.newCondition();
private void a(){
try {
lock.lock();
while (signal != 0){
try {
a.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("a方法执行");
signal ++ ;
b.signal();
}finally {
lock.unlock();
}
}
private void b(){
try {
lock.lock();
while (signal != 1){
try {
b.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("b方法执行");
signal ++ ;
c.signal();
}finally {
lock.unlock();
}
}
private void c(){
try {
lock.lock();
while (signal != 2){
try {
c.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("c方法执行");
signal = 0 ;
a.signal();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
ConditionDemo demo = new ConditionDemo();
ExecutorService service = Executors.newFixedThreadPool(3);
service.execute(new Runnable() {
@Override
public void run() {
while(true) {
demo.c();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
while(true) {
demo.b();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
while(true) {
demo.a();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
service.shutdown();
}
}

5.并发工具类(jdk8)

5.1 CountDownLatch

countDownLatch 简单来说就是一个线程等待其它N个线程执行到某个状态的一个计数器,也就是一等多。其它线程完成指定的任务后(countDown方法)还可以继续做它自己的事,而等待的线程需要等所有的线程完成指定的任务时才能继续它接下来的事。看示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(3);
CountDownLatchDemo demo = new CountDownLatchDemo();
long start = System.currentTimeMillis();
for(int i=0; i< 3; i++){
new Thread(new Runnable() {
@Override
public void run() {
demo.execute(countDownLatch);
}
}).start();
}
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println("所有线程准备完毕,准备时间: " + (end-start) + " 开始执行下一段任务。" );
}
public void execute(CountDownLatch countDownLatch){
System.out.println(Thread.currentThread().getName() + "执行2秒任务");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName() + " 线程 执行完毕,返回主线程执行");
}
}

5.2 CyclicBarrier

CyclicBarrier 相当于一个栏杆,当线程都到达了栏杆位置处(await方法)才可以继续往下执行,当一个没到达(执行出异常)就不放行。简单来说就是多等多。N个线程互相等待。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class CyclicBarrierDemo {
private void eatting(CyclicBarrier cyclicBarrier, Random random) {
System.out.println(Thread.currentThread().getName() + "ready to eat");
try {
Thread.sleep(random.nextInt(2000));
cyclicBarrier.await();
}catch (Exception e){
}
System.out.println(Thread.currentThread().getName() + " finally, I just can eat now.");
}
public static void main(String[] args) {
CyclicBarrierDemo demo = new CyclicBarrierDemo();
Random random = new Random();
CyclicBarrier cyclicBarrier = new CyclicBarrier(4, new Runnable() {
@Override
public void run() {
System.out.println("all member are rady to eat, Then just go to eat");
}
});
ExecutorService executor = Executors.newCachedThreadPool();
for(int i=0;i < 4; i++){
executor.execute(new Runnable() {
@Override
public void run() {
demo.eatting(cyclicBarrier,random);
}
});
}
executor.shutdown();
}
}

5.3 Semaphore

Semaphore 相当于一个停车库,当车太多的时候(并发访问),每次只能放行指定数量的车(构造函数指定)。当一辆车走后(release方法),才会放另一辆车进来(acquire方法)。相当于是一个限制资源访问量的工具类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class SemaphoreDemo {
/**
* 这里每次只有5个线程能同时访问
* @param semaphore
*/
private void demo(Semaphore semaphore){
try {
semaphore.acquire(); // 获取资源
System.out.println(Thread.currentThread().getName() + " 开始执行1s的任务");
Thread.sleep(1000);
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(10);
SemaphoreDemo demo = new SemaphoreDemo();
ExecutorService exec = Executors.newCachedThreadPool();
long start = System.currentTimeMillis();
for(int i=0;i<100;i++){
exec.execute(new Runnable() {
@Override
public void run() {
demo.demo(semaphore);
}
});
}
exec.shutdown();
while(Thread.activeCount() != 2){} // 等待所有线程执行完毕
long end = System.currentTimeMillis();
System.out.printf("任务执行完毕,共耗时: " + (end-start)); // 理论上是要多于10秒,而且线程切换的时间并不高,平均每3个线程间切换需要5ms。所以不会大于11秒。
}
}

5.4 Exchanger

Exchanger的使用场景有两个线程执行的结果进行比较(exchange方法),还有就是线程间交换对象。相当于一个栅栏,当两个线程到这里之后就进行数据之间的交换。看示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
public class ExchangerDemo {
/**
* 使用Exchanger进行数据比较的两个方法
* @param exch
*/
private void a(Exchanger<Integer> exch){
System.out.println(Thread.currentThread().getName() + " 的a方法正在执行抓取任务。。。");
try {
Thread.sleep(2500);
Integer result = 12345;
System.out.println(Thread.currentThread().getName() + " 的a方法抓取任务结束,结果为: " + result);
// 这一方法会阻塞直到另一线程比较完毕。
Integer res = exch.exchange(result);
System.out.println("a方法两个线程的结果是否相等: " + res.equals(result));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void b(Exchanger<Integer> exch){
System.out.println(Thread.currentThread().getName() + " 的b方法正在执行抓取任务。。。");
try {
Thread.sleep(2500);
Integer result = 12344;
System.out.println(Thread.currentThread().getName() + " 的b方法抓取任务结束,结果为: " + result);
// 这一方法会阻塞直到另一线程比较完毕。
Integer res = exch.exchange(result);
System.out.println("b方法两个线程的结果是否相等: " + res.equals(result));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 使用Exchanger进行数据交换的例子
* @param exchanger
*/
private void c(Exchanger<List<String>> exchanger){
List<String> list = new CopyOnWriteArrayList<String>(){
{
add("steve");
add("steve1");
add("steve2");
add("steve3");
}
};
System.out.println("c方法负责生产对象");
try {
Thread.sleep(1000);
System.out.println("a 方法生产完毕");
// 这个方法开始执行
exchanger.exchange(list);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void d(Exchanger<List<String>> exchanger){
List<String> list = null;
System.out.println("d方法负责消费对象");
try {
Thread.sleep(2000);
list = exchanger.exchange(list);
System.out.println("d方法对象交换完毕。。。");
list.stream().forEach(e -> System.out.print("d " + e + " "));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(2);
ExchangerDemo demo = new ExchangerDemo();
Exchanger<Integer> exch = new Exchanger<>();
Exchanger<List<String>> exchanger = new Exchanger<>();
exec.execute(new Runnable() {
@Override
public void run() {
demo.c(exchanger);
}
});
exec.execute(new Runnable() {
@Override
public void run() {
demo.d(exchanger);
}
});
// exec.execute(new Runnable() {
// @Override
// public void run() {
// demo.a(exch);
// }
// });
// exec.execute(new Runnable() {
// @Override
// public void run() {
// demo.b(exch);
// }
// });
exec.shutdown();
}
}

参考资料:
Java并发新构件之Exchanger
Java并发编程:CountDownLatch、CyclicBarrier和Semaphore

5.5 CountDownLatch,CyclicBarrier,Semaphore 源码分析

前面说过,在jdk8里面几乎所有的并发容器都是基于AQS(AbstractQueuedSynchronizer)实现的。这里首先来看CountDownLatch中对AQS的应用(共享锁实现)。
CountDownLatch的CountDown方法

1
2
3
public void countDown() {
sync.releaseShared(1);
}

来看sync类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}

可以知道sync是AQS抽象类的一个实现,countDown方法调用了AQS的一个releaseShared方法,并传入了一个1,接下来看这个方法

1
2
3
4
5
6
7
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

这里调用了tryReleaseShared 方法判断,如果结果为false,方法返回false。这个判断方法在sync里面被重写了,下面来分析这段代码

1
2
3
4
5
6
7
8
9
10
11
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}

首先getState方法获取同步器的状态值(在这里是初始化CountDownLatch时传入的值),当状态值为0的时候,返回false,退出循环,当状态值不为0的时候,令nextc = c - 1,调用compareAndSetState(CAS)方法尝试将内存中的state值设置为nextc,如果成功赋值,判断nextc是否为0.并返回结果。这里的compareAndState就是我们常说的CAS。这里不管结果,只需关注state的值是否减少了。当c == 1 的时候,进入到doReleaseShared方法,看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}

看过上面分析的AQS源码看这段代码就很简单了,这里主要做的事是当条件成立时将FIFO队列里面的头节点的下一个节点设置为等待状态。看unparkSuccessor的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); //这里就是释放线程的代码的实现
}

接下来继续看它的await方法

1
2
3
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1); // 可被中断的增加容器计数器的方法
}

看AQS的实现

1
2
3
4
5
6
7
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}

当没发生中断的时候,看sync实现的tryAcquireShared方法

1
2
3
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}

这里很简单,就是判断当前同步器的状态值是否为0,若为0.表示其他线程都到达了指定的位置,然后调用AQS的doAcquireSharedInterruptibly方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

具体分析和上面AQS差不多,主要作用是把FIFO队列的node前一节点设置为等待唤醒状态。
再来看看CyclicBarrier的await方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
int index = --count;
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}

这个它的await里面调用的doAwait方法。首先对这段执行的代码进行lock操作,然后根据Generation的boolean属性broken判断是否被中断(初始化为false)。下一步判断线程是否被中断,如果被中断,将broken值设置true,并唤醒所有在等待中的线程,令等待中线程的数量等于初始化CyclicBarrier传入的值。如果没被中断,往下执行,令index等于count-1,index表示的是等待中线程的数量。如果index等于0,表示没有线程处在等待状态了,那么就执行这个线程命令。接下来看这个无限循环代码。timed默认为false(await方法传入),这个awaitNanos方法表示当前线程等待指定的nanos时间被唤醒。后面还有一系列操作,也挺简单的。就不细说了。

Read More

默认配图
返回顶部