开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

左手用R右手Python系列——动态网页抓取与seleniu

发表于 2017-12-11

关于基础的网络数据抓取相关内容,本公众号已经做过很多次分享,特别是R语言的爬虫框架(RCurl+XML/httr+rvest[xml2+selectr])已经形成了较为丰富的教程系统。

但是所有这些都是基于静态页面的(抓包与API访问的除外),很多动态网页不提供API访问,这样就只能寄希望于selenium这种基于浏览器驱动技术来完成。

好在R语言中已经有了selenium接口包——RSelenium包,这为我们爬取动态网页提供了可能。我在今年年初写过一个实习僧网站的爬虫,那个是使用R语言中另一个基于selenium驱动的接口包——Rwebdriver来完成的。

实习僧招聘网爬虫数据可视化

当时技术不太成熟,思路也比较幼稚,我使用了导航器硬生生的遍历了500页内容,虽然最后也爬完了所有数据,但是耗时较长(将近40分钟),效率比较低。(感兴趣的小伙伴儿可以参考下上面那篇,不过实习僧的官网近期有较大改版,现在爬取难度肯定要比当初难多了!那个代码可能无法使用了)

最近抽时间学习了下RSelenium包的相关内容,这里感谢陈堰平老师在R语言上海大会现场所做《用RSelenium打造灵活强大的网络爬虫》的演讲,虽然未达现场,但是有幸看完视频版,其中的几个细节解决了我近段时间的一些困惑,这里表示感谢。

陈堰平老师主讲:《用RSelenium打造灵活强大的网络爬虫》 www.xueqing.tv/course/88 一个老外关于RSelenium的入门视频(youtobe请自行翻墙): www.youtube.com/watch?v=ic6…

当前R语言中能做到解析动态网页的有以下几个包(欢迎补充):

  • RSelenium(推荐)
  • Rwebdriver(不很成熟)
  • seleniumpipes(结合RSelenium更高效)
  • rdom(高级封装,灵活性不够)
  • Rcrawler(支持多进程)
  • webshot(专门用于动态网页截图)

本节以下内容正式分享今日案例,目标是拉勾网(不要问为什么,因为之前我还没有爬过拉钩)!

在介绍案例之前,请确保系统具备以下条件:

本地有selenium服务器并添加系统路径;
本地有plantomjs浏览器并添加系统路径;
安装了RSelenium包。

因为涉及到自动化点击操作,Chrome浏览器倒腾一下午硬是在点击环节出故障,找到了原因,因为拉勾网页面很长,而下一页按钮不在默认视窗范围内,使用了js脚本控制滑动条失败,原因不明,看到有人用firefox浏览器测试成功,我还没有试过,这里改用plantomjs无头浏览器(无需考虑元素是否被窗口遮挡的问题。)

R语言版:

1
2
3
4
5
6
7
8
9
10
11
复制代码#!!!这两句是在cmd或者PowerShell中运行的!
#RSelenium服务未关闭之前,请务必保持该窗口状态!
###启动selenium服务:
cd D:\
java -jar selenium-server-standalone-3.3.1.jar
##selenium服务器也可以直接在R语言中启动(无弹出窗口)
system("java -jar \"D:/selenium-server-standalone-2.53.1.jar\"",wait = FALSE,invisible = FALSE)
#加载包
library("RSelenium")
library("magrittr")
library("xml2")

启动服务

1
2
3
4
5
6
7
8
复制代码#给plantomjs浏览器伪装UserAgent
eCap <- list(phantomjs.page.settings.userAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20120101 Firefox/29.0")
###伪装浏览器UserAgent,为什么即使使用plantomjs这种浏览器也需要伪装UA呢,
###因为plantomjs是专门用于web端页面测试的,通常都是在自己的web项目中测试web端功能,直接拿去抓别人的网站,默认的UA就是plantomjs;
###这是公然的挑衅!

###连接plantomjs服务
remDr <- remoteDriver(browserName = "phantomjs", extraCapabilities = eCap)

构建自动化抓取函数:

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
复制代码#自动化抓取函数:
myresult<-function(remDr,url){
###初始化一个数据框,用作后期收据收集之用!
myresult<-data.frame()
###调用后台浏览器(因为是plantomjs这种无头浏览器(headless),所以你看不到弹出窗口)
remDr$open()
###打开导航页面(也就是直达要抓取的目标网址)
remDr$navigate(url)
###初始化一个计时器(用于输出并查看任务进度)
i = 0
while(TRUE){
#计时器开始计数:
i = i+1
#范回当前页面DOM
pagecontent<-remDr$getPageSource()[[1]]
#以下三个字段共用一部分祖先节点,所以临时建立了一个根节点(节省冗余代码)
con_list_item <- pagecontent %>% read_html() %>% xml_find_all('//ul[@class="item_con_list"]/li')
#职位名称
position.name <- con_list_item %>% xml_attr("data-positionname")
#公司名称
position.company <- con_list_item %>% xml_attr("data-company")
#职位薪资
position.salary <- con_list_item %>% xml_attr("data-salary")
#职位详情链接
position.link <- pagecontent %>% read_html() %>% xml_find_all('//div[@class="p_top"]/a') %>% xml_attr("href")
#职位经验要求
position.exprience <- pagecontent %>% read_html() %>% xml_find_all('//div[@class="p_bot"]/div[@class="li_b_l"]') %>% xml_text(trim=TRUE)
#职位所述行业
position.industry <- pagecontent %>% read_html() %>% xml_find_all('//div[@class="industry"]') %>% xml_text(trim=TRUE) %>% gsub("[[:space:]\\u00a0]+|\\n", "",.)
#职位福利
position.bonus <- pagecontent %>% read_html() %>% xml_find_all('//div[@class="list_item_bot"]/div[@class="li_b_l"]') %>% xml_text(trim=TRUE) %>% gsub("[[:space:]\\u00a0]+|\\n", "/",.)
#职位工作环境
position.environment<- pagecontent %>% read_html() %>% xml_find_all('//div[@class="li_b_r"]') %>% xml_text(trim=TRUE)
#收集数据
mydata<- data.frame(position.name,position.company,position.salary,position.link,position.exprience,position.industry,position.bonus,position.environment,stringsAsFactors = FALSE)
#将本次收集的数据写入之前创建的数据框
myresult<-rbind(myresult,mydata)
#系统休眠0.5~1.5秒
Sys.sleep(runif(1,0.5,1.5))
#判断页面是否到尾部
if ( pagecontent %>% read_html() %>% xml_find_all('//div[@class="page-number"]/span[1]') %>% xml_text() !="30"){
#如果页面未到尾部,则点击下一页
remDr$findElement('xpath','//div[@class="pager_container"]/a[last()]')$clickElement()
#但因当前任务进度
cat(sprintf("第【%d】页抓取成功",i),sep = "\n")
} else {
#如果页面到尾部则跳出while循环
break
}
}
#跳出循环后关闭remDr服务窗口
remDr$close()
#但因全局任务状态(也即任务结束)
cat("all work is done!!!",sep = "\n")
#返回最终数据
return(myresult)
}

运行抓取函数

1
2
3
4
复制代码url <- "https://www.lagou.com/zhaopin"
myresult <- myresult(remDr,url)
#预览
DT::datatable(myresult)


Python:

1
2
3
4
5
6
复制代码import os,random,time
import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities
import DesiredCapabilities
from lxml import etree

启动服务

1
2
3
4
5
复制代码dcap = dict(DesiredCapabilities.PHANTOMJS)
#这里也是伪装一下UA:
dcap["phantomjs.page.settings.userAgent"] = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:25.0) Gecko/20100101 Firefox/25.0")
#启动服务(python里面的selenium内置有selenium服务器,需要本地启动)
driver = webdriver.PhantomJS(desired_capabilities=dcap)

构建抓取函数

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
复制代码def getlaogou(driver,url):
#初始化一个长度为0的空字典!以备之后收集数据
myresult = {
"position_name":[],
"position_company":[],
"position_salary":[],
"position_link":[],
"position_exprience":[],
"position_industry":[],
"position_environment":[]
};
#导航到目标网址
driver.get(url)
#计时器初始化
i =0
while True:
#计时器累计计时:
i+=1
#获取当前页面DOM
pagecontent = driver.page_source
#解析HTML文档
result = etree.HTML(pagecontent)
#使用字典内单个list的extend方法累计收集数据
myresult["position_name"].extend(result.xpath('//ul[@class="item_con_list"]/li/@data-positionname'))
myresult["position_company"].extend(result.xpath('//ul[@class="item_con_list"]/li/@data-company'))
myresult["position_salary"].extend(result.xpath('//ul[@class="item_con_list"]/li/@data-salary'))
myresult["position_link"].extend(result.xpath('//div[@class="p_top"]/a/@href'))
myresult["position_exprience"].extend([ text.xpath('string(.)').strip() for text in result.xpath('//div[@class="p_bot"]/div[@class="li_b_l"]')])
myresult["position_industry"].extend([ text.strip() for text in result.xpath('//div[@class="industry"]/text()')])
myresult["position_environment"].extend(result.xpath('//div[@class="li_b_r"]/text()'))
#单次循环任务休眠
time.sleep(random.choice(range(3)))
#判断页面是否到尾部
if result.xpath('//div[@class="page-number"]/span[1]/text()')[0] != '30':
#如果未到达页面尾部,则点击下一页:
driver.find_element_by_xpath('//div[@class="pager_container"]/a[last()]').click()
#同时打印当前任务 状态!
print("第【{}】页抓取成功!".format(i))
else:
#如果所有页面到达尾部,则跳出循环!
break
#打印全局任务状态
print("everything is OK")
#退出并关闭selenium服务!
driver.quit()
#返回数据
return pd.DataFrame(myresult)

运行抓取程序

1
2
复制代码url = "https://www.lagou.com/zhaopin"
mydata = getlaogou(driver,url)

在线课程请点击文末原文链接:

Hellobi Live | R语言可视化在商务场景中的应用
往期案例数据请移步本人GitHub:
github.com/ljtyduyu/Da…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

ggplot2双坐标轴的解决方案

发表于 2017-12-11

本来没有打算写这一篇的,因为在一幅图表中使用双坐标轴确实不是一个很好地习惯,无论是信息传递的效率还是数据表达的准确性而言。

但是最近有好几个小伙伴儿跟我咨询关于ggplot2的次坐标轴问题,平时的一些业务分析中,有些场景出于数据呈现的需要,或者阅读习惯等,往往需要在一幅图中呈现两个量级不等的坐标。

所以我觉得这一篇推送很有必要,确实在最新版的ggplot2(ggplot 2.2.0以上版本)中,已经加入了次坐标轴参数,通过这个次坐标轴的转换,我们可以模拟出不同数量级的次坐标轴效果。

因为其中用到了英文月份简写,这里对系统日期显示格式做了特殊设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码lct <- Sys.getlocale("LC_TIME")  
#备份本地默认日期显示格式

Sys.setlocale("LC_TIME", "C")
#指定标准日期显示格式

Sys.setlocale("LC_TIME",lct)
#这一句是恢复默认系统日期显示格式
#(记得要在使用完下面的month函数之后再运行这一句,否则月份返回的是中文)

加载包:
library("lubridate")
library("ggplot2")
library("scales")
library("magrittr")
library("tidyr")

生成作图数据

作图数据1——单序列柱形图

1
2
3
4
5
6
7
8
9
10
11
复制代码data1 <- data.frame(
Month = seq(from = as.Date('2017-01-01'),to=as.Date('2017-06-01'),by='1 month') %>% month(label=TRUE),
Value = runif(6,10,50) %>% round()
)
Month Value
1 Jan 39
2 Feb 38
3 Mar 50
4 Apr 33
5 May 18
6 Jun 49

作图数据2——二分类折线图(带散点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码data2 <- data.frame(
Month = seq(from = as.Date('2017-01-01'),to=as.Date('2017-06-01'),by='1 month') %>% month(label=TRUE),
Categroy1 = runif(6,0.1,0.5) %>% round(2),
Categroy2 = runif(6,0.1,0.5) %>% round(2)
) %>% gather(Category,Value,-1)
Month Category Value
1 Jan Categroy1 0.49
2 Feb Categroy1 0.23
3 Mar Categroy1 0.10
4 Apr Categroy1 0.38
5 May Categroy1 0.34
6 Jun Categroy1 0.13
7 Jan Categroy2 0.48
8 Feb Categroy2 0.38
9 Mar Categroy2 0.48
10 Apr Categroy2 0.15
11 May Categroy2 0.40
12 Jun Categroy2 0.16

以下是整个过程代码,基本是司空见惯的内容,这里不做过多解释,仅提示其中两处重点,注意第二行geom_line内的y参数赋值以及第四行的scale_y_continuous语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码ggplot() +
geom_col( data = data1,aes(x = Month,y = Value),fill="#6794a7") +
geom_line(data = data2,aes(x = Month,y = rescale(Value,c(0,55)),colour=Category,group=Category),size=1.5) +
geom_point(data = data2,aes(x = Month,y = rescale(Value,c(0,55)),colour=Category),shape=21,fill="white",size=4)+
scale_y_continuous(breaks=pretty_breaks(5),sec.axis = sec_axis( ~rescale(.,c(0,0.5)),name = "Categroy",labels=sprintf("%d%%",(0:5)*10)))+
scale_color_manual(label = c("Categroy1", "Categroy2"),values = c("#ee8f71","#C10534")) +
labs(
title="This is a Title!",
subtitle="This is a Subtitle",
caption="This is a Caption"
)+
theme_minimal(base_size=16) %+replace%
theme(
plot.caption = element_text(hjust=0),
plot.margin = unit(c(1,0.5,1,0.5), "lines")
)


这段代码与我们经常用的有两点不同:

第一次自定义映射——折线度量数据的映射转换:

geom_line(geom_point,因为点图是附属于折线图,仅做修饰之用,这里只重点说折线图层)中的y参数指定的对象使用了一个统计变换函数,rescale函数其实很好理解,就是将一个数值向量按照给定的另一个数值向量的极差(range),等比例标准化。

如果你知道如何将一组向量按照0~1标准化的话,那么这个函数就不难理解 ,其实就是将标准化的尺度给了一个自定义的范围。

因为在ggplot2标度系统中,不容许在一个图形中出现两个量级不等的标度(一山不容二虎),但是想要提供度量不等的次坐标轴,折中的方法就是,将次坐标轴的所有量级按照主坐标轴的量级进行缩放(如果次坐标轴量级大于主坐标轴,那么就是等比例放大,如果比主坐标轴量级大则缩小)。

针对本例而言,就是将折线图的数据源量级(0.00.5)放大到035的区间上,所有的单个指标的缩放比例都是相同的,这样你在图上就不会感受到太大的视角误差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码value1<-data1$Value
value21 <- data2[data2$Category == 'Categroy1',"Value"]
value22 <- data2[data2$Category == 'Categroy2',"Value"]

mydata <- data.frame(value1,value21,value22)

mydata$value31 <- rescale(mydata$value21,c(0,50))
mydata$value32 <- rescale(mydata$value22,c(0,50))

value1 value21 value22 value31 value32
1 39 0.49 0.48 50.000000 50.000000
2 38 0.23 0.38 16.666667 34.848485
3 50 0.10 0.48 0.000000 50.000000
4 33 0.38 0.15 35.897436 0.000000
5 18 0.34 0.40 30.769231 37.878788
6 49 0.13 0.16 3.846154 1.515152

这是最终的折现结果,在geom_line中使用rescale函数实际上就是做的这种度量重新自定义映射的过程。

第二次自定义映射——次坐标轴刻度标签转换:

仅仅做以上步骤还不够,因为这只能保障次坐标轴的数据点位置相对于整个坐标系统而言,不会出现太大的视觉误差,但是现在的问题是这个图形对象中有两套不同的度量,所以必须声明不同的y轴度量标准,也就是y轴的刻度线及刻度标签,刻度标签的定义就是本案例的第二个重点,它仍然是通过rescale函数进行了一次度量的重新映射。

不过这次映射的过程刚好是相反的操作,即将之前已经被标准化到050区间内的原始度量标签通过rescale函数再次标准化到00.5区间内,这样保障显示在次坐标轴上的度量是符合原始数据极差范围呢。

说的有些拗口了,实际上以上过程思路很简单,就是先将数据映射到正确的位置,然后将次坐标轴刻度线度量标签再按照真实极差进行分布,一虚一实,正好达到了模拟效果。

1
2
3
4
5
6
复制代码scale_y_continuous(
breaks=pretty_breaks(5), #创建主坐标轴的刻度区间(这里是5个区间6个刻度点)
sec.axis = sec_axis( ~rescale(.,c(0,0.5)), #对次坐标轴刻度标签的二次映射(极差范围指定真实极差即可)
name = "Categroy", #次坐标轴名称
labels=sprintf("%d%%",(0:5)*10)) #刻度标签显示格式(这里是百分号)
)

思路大体上就是这样子,希望这一篇文章可以帮到大家!

在线课程请点击文末原文链接:

Hellobi Live | R语言可视化在商务场景中的应用
往期案例数据请移步本人GitHub:
github.com/ljtyduyu/Da…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Golang网络 核心API实现剖析(二)

发表于 2017-12-11

说明

前面的章节我们基本聊完了golang网络编程的关键API流程,但遗留了一个关键内容:当系统调用返回EAGAIN时,会调用WaitRead/WaitWrite来阻塞当前协程,现在我们接着聊。

WaitRead/WaitWrite

1
2
3
4
5
6
7
8
9
10
11
12
复制代码func (pd *pollDesc) Wait(mode int) error {
res := runtime_pollWait(pd.runtimeCtx, mode)
return convertErr(res)
}

func (pd *pollDesc) WaitRead() error {
return pd.Wait('r')
}

func (pd *pollDesc) WaitWrite() error {
return pd.Wait('w')
}

最终runtime_pollWait走到下面去了:

1
2
复制代码TEXT net·runtime_pollWait(SB),NOSPLIT,$0-0 
JMP runtime·netpollWait(SB)

我们仔细考虑应该明白:netpollWait的主要作用是:等待关心的socket是否有事件(其实后面我们知道只是等待一个标记位是否发生改变),如果没有事件,那么就将当前的协程挂起,直到有通知事件发生,我们接下来看看到底如何实现:

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
复制代码func netpollWait(pd *pollDesc, mode int) int {
// 先检查该socket是否有error发生(如关闭、超时等)
err := netpollcheckerr(pd, int32(mode))
if err != 0 {
return err
}

// As for now only Solaris uses level-triggered IO.
if GOOS == "solaris" {
onM(func() {
netpollarm(pd, mode)
})
}
// 循环等待netpollblock返回值为true
// 如果返回值为false且该socket未出现任何错误
// 那该协程可能被意外唤醒,需要重新被挂起
// 还有一种可能:该socket由于超时而被唤醒
// 此时netpollcheckerr就是用来检测超时错误的
for !netpollblock(pd, int32(mode), false) {
err = netpollcheckerr(pd, int32(mode))
if err != 0 {
return err
}
}
return 0
}

func netpollblock(pd *pollDesc, mode int32, waitio bool) bool {
gpp := &pd.rg
if mode == 'w' {
gpp = &pd.wg
}

// set the gpp semaphore to WAIT
// 首先将轮询状态设置为pdWait
// 为什么要使用for呢?因为casuintptr使用了自旋锁
// 为什么使用自旋锁就要加for循环呢?
for {
old := *gpp
if old == pdReady {
*gpp = 0
return true
}
if old != 0 {
gothrow("netpollblock: double wait")
}
// 将socket轮询相关的状态设置为pdWait
if casuintptr(gpp, 0, pdWait) {
break
}
}
// 如果未出错将该协程挂起,解锁函数是netpollblockcommit
if waitio || netpollcheckerr(pd, mode) == 0 {
f := netpollblockcommit
gopark(**(**unsafe.Pointer)(unsafe.Pointer(&f)), unsafe.Pointer(gpp), "IO wait")
}
// 可能是被挂起的协程被唤醒
// 或者由于某些原因该协程压根未被挂起
// 获取其当前状态记录在old中
old := xchguintptr(gpp, 0)
if old > pdWait {
gothrow("netpollblock: corrupted state")
}
return old == pdReady
}

从上面的分析我们看到,如果无法读写,golang会将当前协程挂起,在协程被唤醒的时候,该标记位应该会被置位。 我们接下来看看这些挂起的协程何时会被唤醒。

事件通知

golang运行库在系统运行过程中存在socket事件检查点,目前,该检查点主要位于以下几个地方:

runtime·startTheWorldWithSema(void):在完成gc后;
findrunnable():这个暂时不知道何时会触发?
sysmon:golang中的监控协程,会周期性检查就绪socket

TODO: 为什么是在这些地方检查socket就绪事件呢?

接下来我们看看如何检查socket就绪事件,在socket就绪后又是如何唤醒被挂起的协程?主要调用函数runtime-netpoll()

我们只关注epoll的实现,对于epoll,上面的方法具体实现是netpoll_epoll.go中的netpoll

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
复制代码func netpoll(block bool) (gp *g) {
if epfd == -1 {
return
}
waitms := int32(-1)
if !block {
// 如果调用者不希望block
// 设置waitsm为0
waitms = 0
}

var events [128]epollevent
retry:
// 调用epoll_wait获取就绪事件
n := epollwait(epfd, &events[0], int32(len(events)), waitms)
if n < 0 {
...
}
goto retry
}

for i := int32(0); i < n; i++ {
ev := &events[i]
if ev.events == 0 {
continue
}
var mode int32
if ev.events&(_EPOLLIN|_EPOLLRDHUP|_EPOLLHUP|_EPOLLERR) != 0 {
mode += 'r'
}
if ev.events&(_EPOLLOUT|_EPOLLHUP|_EPOLLERR) != 0 {
mode += 'w'
}

// 对每个事件,调用了netpollready
// pd主要记录了与该socket关联的等待协程
if mode != 0 {
pd := *(**pollDesc)(unsafe.Pointer(&ev.data))
netpollready((**g)(noescape(unsafe.Pointer(&gp))), pd, mode)
}
}
// 如果调用者同步等待且本次未获取到就绪socket
// 继续重试
if block && gp == nil {
goto retry
}
return gp
}

这个函数主要调用epoll_wait(当然,golang封装了系统调用)来获取就绪socket fd,对每个就绪的fd,调用netpollready()作进一步处理。这个函数的最终返回值就是一个已经就绪的协程(g)链表。

netpollready主要是将该socket fd标记为IOReady,并唤醒等待在该fd上的协程g,将其添加到传入的g链表中。

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
复制代码// make pd ready, newly runnable goroutines (if any) are returned in rg/wg 
func netpollready(gpp **g, pd *pollDesc, mode int32) {
var rg, wg *g
if mode == 'r' || mode == 'r'+'w' {
rg = netpollunblock(pd, 'r', true)
}

if mode == 'w' || mode == 'r'+'w' {
wg = netpollunblock(pd, 'w', true)
}
// 将就绪协程添加至链表中
if rg != nil {
rg.schedlink = *gpp
*gpp = rg
}
if wg != nil {
wg.schedlink = *gpp
*gpp = wg
}
}

// 将pollDesc的状态置为pdReady并返回就绪协程
func netpollunblock(pd *pollDesc, mode int32, ioready bool) *g {
gpp := &pd.rg
if mode == 'w' {
gpp = &pd.wg
}
for {
old := *gpp
if old == pdReady {
return nil
}
if old == 0 && !ioready {
return nil
}
var new uintptr
if ioready {
new = pdReady
}
if casuintptr(gpp, old, new) {
if old == pdReady || old == pdWait {
old = 0
}
return (*g)(unsafe.Pointer(old))
}
}
}

疑问:一个fd会被多个协程同时进行IO么?比如一个协程读,另外一个协程写?或者多个协程同时读?此时返回的是哪个协程就绪呢?

一个socket fd可支持并发读写,因为对于tcp协议来说,是全双工。读写操作的是不同缓冲区,但是不支持并发读和并发写,因为这样会错乱的。所以上面的netFD.RWLock()就是干这个作用的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java 10 新特性解密,引入类型推断机制

发表于 2017-12-11

免费体验IBM Cloud,构建Iot应用 >>>

Java
随着Java开发工具包(JDK)9的发布,大量的注意力都集中在Java的最新特性上,包括引入模块(通过集成项目Jigsaw)。尽管最近的很多关注都集中在这些强大的新功能上,但下一个版本的Java:JDK 10已经开始准备了。在本文中,我们将粗略地介绍一下JDK 10的主要特性,并探讨JDK 10中可能包含的一些特性。

请注意,本文中所包含的信息在写本文时是准确的。但是到发布时,JDK 10特性组预计将会增加。

新功能

与之前的JDK版本一样,对于即将到来的JDK 10也有一些主要特性。这些特性可以分为两个主要类别:(1)目标发布,(2)建议发布。前者表示某些特性已计划在JDK 10中发布,后一种类型表示这些特性还需要增加支持和成熟度。一旦条件允许,它就可以升级为一个目标发布状态。

目标发布

目前有两个主要功能针对JDK 10:

  • 局部变量类型推断,这将删除大部分对象实例化所需的冗长的包含手动类型信息
  • 整合源树source tree的JDK库,即不同的JDK库将被合并成一个单一的存储库。

1. 局部变量类型推断

强类型编程语言有很多优点,包括在编译时发现类型错误,但是它们也引入了大量的样板代码,特别是在定义局部变量时。例如,当我们希望实例化一个对象时,我们被迫在赋值的左侧提供显式类型,并在赋值的右边提供实现类型,如下面的片段所示:

1
复制代码MyObject value = new MyObject();

但是,当这个过程重复出现大量任务时,对象实例化可能变得令人沮丧和乏味。许多最流行的强类型的编程语言,比如C++, C#以及Go,在定义过程中,提供一种局部变量类型推断的功能(例如C++提供了auto 关键字,C#提供var关键字)。但是,Java仍缺乏这样的功能,它要求开发人员显式声明变量的预期清单类型。

为了解决这个问题,Java开发工具包(JDK)改进建议(JEP)286提出了一个上下文敏感的关键字var,允许局部变量被以下方式初始化:

1
2
复制代码var value = new MyObject();
var list = new ArrayList<MyObject>();

由于var关键字是上下文敏感的,它的使用有下面的规则定义:

代码使用var作为一个变量、方法或包名称时将不受影响;而使用var作为类或接口名称的代码将受到影响。

同样,类型推断将受到以下方式的约束:

推断类型将被限制在局部变量的初始化,增强的for循环索引,以及传统的for循环中声明;它(将)不用于方法形式、构造函数形式、方法返回类型、字段、捕获形式,或任何其他类型的变量声明。

考虑到所有的限制和细微差别,这个特性将有助于在开发人员创建的应用程序Java代码中减轻大量的单调无聊的动作,并简化JDK代码库。更多信息可以在官方的JEP 286规范中找到。

2. 整合的JDK库

目前,有8个不同的Mercurial存储库用于存储包含JDK的大量源代码:

  • root
  • corba
  • hotspot
  • jaxp
  • jaxws
  • JDK
  • langtools
  • nashorn

虽然过多的存储库提供了对组成JDK的各种组件并清晰分离,但管理多个存储库存在一些主要的缺点。

其中最重要的一点是,在JDK的两个不同部分,单个错误修复程序不能被原子跟踪。例如,如果一个bug修复需要对独立存储库中包含的系统的两个部分进行更改,那么必须提交两个提交:每个存储库中一个。这种不连续性很容易地降低项目和源代码管理工具的可跟踪性和复杂性。

为了解决这个问题,JEP 296建议将所有现有存储库合并到一个Mercurial存储库中。这种合并的一个次生效应是,这个单一的Mercurial存储库比现有的8个存储库要更容易的被镜像(作为一个Git存储库)。

虽然在这个整合过程中,外部开发人员有一些阻力,但是JDK开发团队似乎已经致力于使这一更改成为JDK 10的一部分。有关更多信息,请参见JEP 296,并提议整合由Michael Redlich发布的JDK 10 OpenJDK Mercurial存储库声明。

建议发布

除了两个目标特性之外,JDK 10目前还有三个建议,其中两个主要是对JDK的垃圾收集器部分进行升级,另一个侧重于对JDK的本地线程功能进行升级。

1 .清理垃圾收集接口

在当前的JDK结构中,组成垃圾收集器(GC)实现的组件分散在代码库的各个部分。尽管这些惯例对于使用GC计划的JDK开发者比较熟悉,但对新的开发人员来说,对于特定GC的源代码,或者创建一个新的GC常常会感到困惑。更重要的是,随着Java modules的出现,我们希望在构建过程中排除不需要的GC,但是GC接口的当前横切结构排除了这种增强。

JEP 304被设计为解决此问题的方案,并建议整合并清理GC接口,以便更容易地实现新的GC,并更好地维护现有的GC。本建议完成后,GC执行将负责提供以下内容:

  • heap,CollectedHeap的子类
  • barrier set,BarrierSet的子类,它实现了运行时的各种障碍
  • 一个CollectorPolicy的实现
  • GCInterpreterSupport的实现,它实现了解释器的GC的各种障碍(使用汇编指令)
  • GCC1Support的实现,它为C1编译器实现了GC的各种障碍
  • GCC2Support的实现,它为C2编译器实现了GC的各种障碍
  • 最终GC特定参数的初始化
  • 设置MemoryService、相关的内存池、内存管理器等。

有关这些更改的更多信息,请参见JEP 304规范;有关Java GC的更多信息,请参阅Oracle提供的垃圾收集器基础指南。

2. G1垃圾收集器并行化

随着JDK 9的发布,Garbage-First(G1)GC取代了Parallel Collector作为默认GC。为了减少JDK 9之外的JDK版本中垃圾收集的影响,G1收集器将被并行化(以匹配并行收集器的特征)。虽然目前还没有关于这个并行化的实现细节的信息,但是可以在JEP 307规范中找到关于此更改的更多细节。

有关GC实现的更多信息,请参阅Oracle的G1指南和并行收集器指南。

3. 项目线程局部握手

当前,停止Java线程是一个“全部或没有”的过程,需要一个Java虚拟机(JVM)的安全点,以使一个线程停止。为了让单独的线程停止,JEP 312提议将回调包含到线程中。这一更改受到了限制,因为它显著地提高了现有JVM功能的性能开销,并且改变了到达JVM全局安全点的现有时间语义。有关这个建议的更多信息,请参阅JEP 312的Thread-Local Handshake OpenJDK讨论。

结论

尽管JDK 9对于许多Java开发人员非常新鲜,但它的发展并没有停止。特别是,JDK 10承诺为局部变量实例化引入类型推断机制,并将现有的JDK存储库合并到一个Mercurial存储库中。

此外,在更成熟和更支持的情况下,JDK 10还可能包括一些重要的升级到GC接口和默认的GC实现,以及升级到JVM中单个线程的可寻址能力。虽然JDK 10的发布在未来仍然相对较远,而且包含的特性很可能会成为Java时间轴上的一个重要里程碑。

来源:CodeBay

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

设计模式学习笔记-策略模式 NEZHA的博客

发表于 2017-12-11

在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。
在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。


介绍

意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
主要解决:在有多种算法相似的情况下,使用 if…else 所带来的复杂和难以维护。
何时使用:一个系统有许多许多类,而区分它们的只是他们直接的行为。
如何解决:将这些算法封装成一个一个的类,任意地替换。
关键代码:实现同一个接口。
应用实例:
1、诸葛亮的锦囊妙计,每一个锦囊就是一个策略。 2、旅行的出游方式,选择骑自行车、坐汽车,每一种旅行方式都是一个策略。 3、JAVA AWT 中的 LayoutManager。
优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。
缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。
使用场景: 1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
2、一个系统需要动态地在几种算法中选择一种。 3、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。


特点

1.封装变化。
2.多用组合,少用继承。
3.针对接口编程,不针对实现编程。


Java实现

我们将创建一个定义活动的 IStrategy 接口和实现了 Strategy 接口的实体策略类。Context 是一个使用了某种策略的类。
StrategyPatternDemo,我们的演示类使用 Context 和策略对象来演示 Context 在它所配置或使用的策略改变时的行为变化。

步骤 1

创建一个接口。
Strategy.java

1
2
3
angelscript复制代码public interface IStrategy {
public int doOperation(int num1, int num2);
}

步骤 2

创建实现接口的实体类。
StrategyAdd.java

1
2
3
4
5
6
angelscript复制代码public class StrategyAdd implements IStrategy{
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}

StrategySubstract.java

1
2
3
4
5
6
angelscript复制代码public class StrategySubstract implements IStrategy {
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}

StrategyMultiply.java

1
2
3
4
5
6
angelscript复制代码public class StrategyMultiply implements IStrategy{
@Override
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}

步骤 3

创建 Context 类。
Context.java

1
2
3
4
5
6
7
8
9
reasonml复制代码public class Context{
private IStrategy strategy;
public Context(IStrategy strategy){
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2){
return strategy.doOperation(num1, num2);
}
}

步骤 4

使用 Context 来查看当它改变策略 Strategy 时的行为变化。
StrategyTest.java

1
2
3
4
5
6
7
8
9
10
11
haxe复制代码public class StrategyTest{
@Test
public void test(){
Context context = new Context(new StrategyAdd());
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
context = new Context(new StrategySubstract());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
context = new Context(new StrategyMultiply());
System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
}
}

参考文献:

www.runoob.com/design-patt…


  • 交流或更多内容请关注我的公众号:nezha_blog
  • 我的技术博客:nezha.github.io

微信公众号

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试整理-Java综合高级篇(吐血整理) NEZHA的博

发表于 2017-12-11

Java面试总结

1.你用过哪些集合类?

大公司最喜欢问的Java集合类面试题
40个Java集合面试问题和答案
java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。
java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。

Collection
├List
│├LinkedList
│├ArrayList
│└Vector
│ └Stack
└Set
Map
├Hashtable
├HashMap
└WeakHashMap


ArrayList、HashMap、TreeMap和HashTable类提供对元素的随机访问。

线程安全

Vector
HashTable(不允许插空值)

非线程安全

ArrayList
LinkedList
HashMap(允许插入空值)
HashSet
TreeSet
TreeMap(基于红黑树的Map实现)

2.你说说 arraylist 和 linkedlist 的区别?

ArrayList和LinkedList两者都实现了List接口,但是它们之间有些不同。
(1)ArrayList是由Array所支持的基于一个索引的数据结构,所以它提供对元素的随机访问
(2)与ArrayList相比,在LinkedList中插入、添加和删除一个元素会更快
(3)LinkedList比ArrayList消耗更多的内存,因为LinkedList中的每个节点存储了前后节点的引用

3.HashMap 底层是怎么实现的?还有什么处理哈希冲突的方法?

处理哈希冲突的方法:

解决HashMap一般没有什么特别好的方式,要不扩容重新hash要不优化冲突的链表结构

1.开放定地址法-线性探测法
2.开放定地址法-平方探查法
3.链表解决-可以用红黑树提高查找效率

image.png

HashMap简介
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
HashMap 的实现不是同步的,这意味着它不是线程安全的,但可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
HashMap
的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。初始容量默认是16。默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本.
HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,当链表长度太长(默认超过8)时,链表就转换为红黑树.

image.png

Java8系列之重新认识HashMap
功能实现-方法

  1. 确定哈希桶数组索引位置 :这里的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算。
1
2
3
4
5
6
7
8
9
10
11
复制代码方法一:
static final int hash(Object key) { //jdk1.8 & jdk1.7
int h;
// h = key.hashCode() 为第一步 取hashCode值
// h ^ (h >>> 16) 为第二步 高位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
方法二:
static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
return h & (length-1); //第三步 取模运算
}
  1. 分析HashMap的put方法
    image.png
  2. 扩容机制:原来的两倍

4.熟悉什么算法,还有说说他们的时间复杂度?

经典排序算法总结与实现

image.png

5.ArrayList和Vector的底层代码和他们的增长策略,它们是如何进行扩容的?

ArrayList 默认数组大小是10,其中ensureCapacity扩容,trimToSize容量调整到适中,扩展后数组大小为((原数组长度1.5)与传递参数中较大者.
Vector的扩容,是可以指定扩容因子,同时Vector扩容策略是:1.原来容量的2倍,2.原来容量+扩容参数值。
\详细内容可以配合阅读源码*

6.jvm 原理。程序运行区域划分

问:Java运行时数据区域?
回答:包括程序计数器、JVM栈、本地方法栈、方法区、堆
问:方法区里存放什么?
本地方法栈:和jvm栈所发挥的作用类似,区别是jvm栈为jvm执行java方法(字节码)服务,而本地方法栈为jvm使用的native方法服务。
JVM栈:局部变量表、操作数栈、动态链接、方法出口。
方法区:用于存储已被虚拟机加载的类信息,常量、静态变量、即时编译器编译后的代码等。
堆:存放对象实例。

7.minor GC 与 Full GC,分别什么时候会触发? 。分别采用哪种垃圾回收算法?简单介绍算法

GC(或Minor GC):收集 生命周期短的区域(Young area)。
Full GC (或Major GC):收集生命周期短的区域(Young area)和生命周期比较长的区域(Old area)对整个堆进行垃圾收集。
新生代通常存活时间较短基于Copying算法进行回收,将可用内存分为大小相等的两块,每次只使用其中一块;当这一块用完了,就将还活着的对象复制到另一块上,然后把已使用过的内存清理掉。在HotSpot里,考虑到大部分对象存活时间很短将内存分为Eden和两块Survivor,默认比例为8:1:1。代价是存在部分内存空间浪费,适合在新生代使用;
老年代与新生代不同,老年代对象存活的时间比较长、比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并、要么标记出来便于下次进行分配,总之目的就是要减少内存碎片带来的效率损耗。
在执行机制上JVM提供了串行GC(Serial
MSC)、并行GC(Parallel MSC)和并发GC(CMS)。

Minor GC ,Full GC 触发条件

  • Minor GC触发条件:当Eden区满时,触发Minor GC。
  • Full GC触发条件:
  • (1)调用System.gc时,系统建议执行Full GC,但是不必然执行
  • (2)老年代空间不足
  • (3)方法去空间不足
  • (4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  • (5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

8.HashMap 实现原理

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

9.java.util.concurrent 包下使用过哪些

1.阻塞队列 BlockingQueue( ArrayBlockingQueue, DelayQueue, LinkedBlockingQueue, SynchronousQueue,LinkedTransferQueue,LinkedBlockingDeque)
2.ConcurrentHashMap
3.Semaphore–信号量
4.CountDownLatch–闭锁
5.CyclicBarrier–栅栏
6.Exchanger–交换机
7.Executor->ThreadPoolExecutor,ScheduledThreadPoolExecutor

1
2
3
4
5
复制代码Semaphore semaphore = new Semaphore(1);  
//critical section
semaphore.acquire();
...
semaphore.release();

8.锁 Lock–ReentrantLock,ReadWriteLock,Condition,LockSupport

1
2
3
4
复制代码Lock lock = new ReentrantLock();  
lock.lock();
//critical section
lock.unlock();

10.concurrentMap 和 HashMap 区别

1.hashMap可以有null的键,concurrentMap不可以有
2.hashMap是线程不安全的,在多线程的时候需要Collections.synchronizedMap(hashMap),ConcurrentMap使用了重入锁保证线程安全。
3.在删除元素时候,两者的算法不一样。
ConcurrentHashMap和Hashtable主要区别就是围绕着锁的粒度以及如何锁,可以简单理解成把一个大的HashTable分解成多个,形成了锁分离。

11.信号量是什么,怎么使用?volatile关键字是什么?

信号量-semaphore:荷兰著名的计算机科学家Dijkstra 于1965年提出的一个同步机制。是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。
整形信号量:表示共享资源状态,且只能由特殊的原子操作改变整型量。
同步与互斥:同类进程为互斥关系(打印机问题),不同进程为同步关系(消费者生产者)。


使用volatile关键字是解决同步问题的一种有效手段。 java volatile关键字预示着这个变量始终是“存储进入了主存”。更精确的表述就是每一次读一个volatile变量,都会从主存读取,而不是CPU的缓存。同样的道理,每次写一个volatile变量,都是写回主存,而不仅仅是CPU的缓存。
Java 保证volatile关键字保证变量的改变对各个线程是可见的。

image.png

12.阻塞队列了解吗?怎么使用

阻塞队列 (BlockingQueue)是Java util.concurrent包下重要的数据结构,BlockingQueue提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于BlockingQueue实现的。

image.png

image.png

以ArrayBlockingQueue为例,我们先来看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}

从put方法的实现可以看出,它先获取了锁,并且获取的是可中断锁,然后判断当前元素个数是否等于数组的长度,如果相等,则调用notFull.await()进行等待,当被其他线程唤醒时,通过enqueue(e)方法插入元素,最后解锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码/**
* Inserts element at current put position, advances, and signals.
* Call only when holding lock.
*/
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length) putIndex = 0;
count++;
notEmpty.signal();
}

插入成功后,通过notEmpty唤醒正在等待取元素的线程。

13.Java中的NIO,BIO,AIO分别是什么?

IO的方式通常分为几种,同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO

1.BIO,同步阻塞式IO,简单理解:一个连接一个线程.BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

在JDK1.4之前,用Java编写网络请求,都是建立一个ServerSocket,然后,客户端建立Socket时就会询问是否有线程可以处理,如果没有,要么等待,要么被拒绝。即:一个连接,要求Server对应一个处理线程。

2.NIO,同步非阻塞IO,简单理解:一个请求一个线程.NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。

NIO本身是基于事件驱动思想来完成的,其主要想解决的是BIO的大并发问题: 在使用同步I/O的网络应用中,如果要同时处理多个客户端请求,或是在客户端要同时和多个服务器进行通讯,就必须使用多线程来处理。也就是说,将每一个客户端请求分配给一个线程来单独处理。这样做虽然可以达到我们的要求,但同时又会带来另外一个问题。由于每创建一个线程,就要为这个线程分配一定的内存空间(也叫工作存储器),而且操作系统本身也对线程的总数有一定的限制。如果客户端的请求过多,服务端程序可能会因为不堪重负而拒绝客户端的请求,甚至服务器可能会因此而瘫痪。

3.AIO,异步非阻塞IO,简单理解:一个有效请求一个线程.AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

14.类加载机制是怎样的

JVM中类的装载是由ClassLoader和它的子类来实现的,Java ClassLoader是一个重要的Java运行时系统组件。它负责在运行时查找和装入类文件的类。
类加载的五个过程:加载、验证、准备、解析、初始化。

从类被加载到虚拟机内存中开始,到卸御出内存为止,它的整个生命周期分为7个阶段,加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸御(Unloading)。其中验证、准备、解析三个部分统称为连接。

15.什么是幂等性

所谓幂等,简单地说,就是对接口的多次调用所产生的结果和调用一次是一致的。
那么我们为什么需要接口具有幂等性呢?设想一下以下情形:

  • 在App中下订单的时候,点击确认之后,没反应,就又点击了几次。在这种情况下,如果无法保证该接口的幂等性,那么将会出现重复下单问题。
  • 在接收消息的时候,消息推送重复。如果处理消息的接口无法保证幂等,那么重复消费消息产生的影响可能会非常大。

16.有哪些 JVM 调优经验

Jvm参数总结:linfengying.com/?p=2470

  • 内存参数
参数 作用
-Xmx 堆大小的最大值。当前主流虚拟机的堆都是可扩展的
-Xms 堆大小的最小值。可以设置成和 -Xmx 一样的值
-Xmn 新生代的大小。现代虚拟机都是“分代”的,因此堆空间由新生代和老年代组成。新生代增大,相应地老年代就减小。Sun官方推荐新生代占整个堆的3/8
-Xss 每个线程的堆栈大小。该值影响一台机器能够创建的线程数上限
-XX:MaxPermSize= 永久代的最大值。永久代是 HotSpot 特有的,HotSpot 用永久代来实现方法区
-XX:PermSize= 永久代的最小值。可以设置成和 -XX:MaxPermSize 一样的值
-XX:SurvivorRatio= Eden 和 Survivor 的比值。基于“复制”的垃圾收集器又会把新生代分为一个 Eden 和两个 Survivor,如果该参数为8,就表示 Eden
-XX:PretenureSizeThreshold= 直接晋升到老年代的对象大小。大于这个参数的对象将直接在老年代分配。默认值为0,表示不启用
-XX:HandlePromotionFailure= 是否允许分配担保失败。在 JDK 6 Update 24 后该参数已经失效。
-XX:MaxTenuringThreshold= 对象晋升到老年代的年龄。对象每经过一次 Minor GC 后年龄就加1,超过这个值时就进入老年代。默认值为15
-XX:MaxDirectMemorySize= 直接内存的最大值。对于频繁使用 nio 的应用,应该显式设置该参数,默认值为0
  • GC参数
垃圾收集器 参数 备注
Serial(新生代) -XX:+UseSerialGC 虚拟机在 Client 模式下的默认值,打开此开关后,使用 Serial + Serial Old 的收集器组合。Serial 是一个单线程的收集器
ParNew(新生代) -XX:+UseParNewGC 强制使用 ParNew,打开此开关后,使用 ParNew + Serial Old 的收集器组合。ParNew 是一个多线程的收集器,也是 server 模式下首选的新生代收集器
-XX:ParallelGCThreads= 垃圾收集的线程数
Parallel Scavenge(新生代) -XX:+UseParallelGC 虚拟机在 Server 模式下的默认值,打开此开关后,使用 Parallel Scavenge + Serial Old 的收集器组合
-XX:MaxGCPauseMillis= 单位毫秒,收集器尽可能保证单次内存回收停顿的时间不超过这个值。
-XX:GCTimeRatio= 总的用于 gc 的时间占应用程序的百分比,该参数用于控制程序的吞吐量
-XX:+UseAdaptiveSizePolicy 设置了这个参数后,就不再需要指定新生代的大小(-Xmn)、 Eden 和 Survisor 的比例(-XX:SurvivorRatio)以及晋升老年代对象的年龄(-XX:PretenureSizeThreshold)了,因为该收集器会根据当前系统的运行情况自动调整。当然前提是先设置好前两个参数。
Serial Old(老年代) 无 Serial Old 是 Serial 的老年代版本,主要用于 Client 模式下的老生代收集,同时也是 CMS 在发生 Concurrent Mode Failure 时的后备方案
Parallel Old(老年代) -XX:+UseParallelOldGC 打开此开关后,使用 Parallel Scavenge + Parallel Old 的收集器组合。Parallel Old 是 Parallel Scavenge 的老年代版本,在注重吞吐量和 CPU 资源敏感的场合,可以优先考虑这个组合
CMS(老年代) -XX:+UseConcMarkSweepGC 打开此开关后,使用 ParNew + CMS 的收集器组合。
-XX:CMSInitiatingOccupancyFraction= CMS 收集器在老年代空间被使用多少后触发垃圾收集
-XX:+UseCMSCompactAtFullCollection 在完成垃圾收集后是否要进行一次内存碎片整理
-XX:CMSFullGCsBeforeCompaction= 在进行若干次垃圾收集后才进行一次内存碎片整理

附图:可以配合使用的收集器组合

image.png

上面有7中收集器,分为两块,上面为新生代收集器,下面是老年代收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用。

  • 其他参数
参数 作用
-verbose:class 打印类加载过程
-XX:+PrintGCDetails 发生垃圾收集时打印 gc 日志,该参数会自动带上 -verbose:gc 和 -XX:+PrintGC
-XX:+PrintGCDateStamps / -XX:+PrintGCTimeStamps 打印 gc 的触发事件,可以和 -XX:+PrintGC 和 -XX:+PrintGCDetails 混用
-Xloggc: gc 日志路径
-XX:+HeapDumpOnOutOfMemoryError 出现 OOM 时 dump 出内存快照用于事后分析
-XX:HeapDumpPath= 堆转储快照的文件路径

17.分布式 CAP 了解吗?

一致性(Consistency)
可用性(Availability)
分区容忍性(Partition tolerance)

18.Java中HashMap的key值要是为类对象则该类需要满足什么条件?

需要同时重写该类的hashCode()方法和它的equals()方法。

当程序试图将一个 key-value 对放入 HashMap 中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但 key 不会覆盖。如果这两个 Entry 的 key 通过 equals
比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。

19.java 垃圾回收会出现不可回收的对象吗?怎么解决内存泄露问题?怎么定位问题源?

一般不会有不可回收的对象,因为现在的GC会回收不可达内存。

20.终止线程有几种方式?终止线程标记变量为什么是 valotile 类型?

1.线程正常执行完毕,正常结束
2.监视某些条件,结束线程的不间断运行
3.使用interrupt方法终止线程

在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值

21.用过哪些并发的数据结构? cyclicBarrier 什么功能?信号量作用?数据库读写阻塞怎么解决

  • 主要有锁机制,然后基于CAS的concurrent包。
  • CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
  • CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。*
  • Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。很多年以来,我都觉得从字面上很难理解Semaphore所表达的含义,只能把它比作是控制流量的红绿灯,比如XX马路要限制流量,只允许同时有一百辆车在这条路上行使,其他的都必须在路口等待,所以前一百辆车会看到绿灯,可以开进这条马路,后面的车会看到红灯,不能驶入XX马路,但是如果前一百辆中有五辆车已经离开了XX马路,那么后面就允许有5辆车驶入马路,这个例子里说的车就是线程,驶入马路就表示线程在执行,离开马路就表示线程执行完成,看见红灯就表示线程被阻塞,不能执行。

22.关于抽象类和接口的关系

简言之抽象类是一种功能不全的类,接口只是一个抽象方法声明和静态不能被修改的数据的集合,两者都不能被实例化。
从某种意义上说,接口是一种特殊形式的抽象类,在java语言中抽象类表示的是一种继承关系,一个类只能继承继承一个抽象类,而一个类却可以实现多个接口。在许多情况下,接口确实可以代替抽象类,如果你不需要刻意表达属性上的继承的话。

23.堆内存和栈内存的区别

寄存器:JVM内部虚拟寄存器,存取速度非常快,程序不可控制。
栈:保存局部变量的值包括:1.保存基本数据类型的值;2.保存引用变量,即堆区对象的引用(指针)。也可以用来保存加载方法时的帧。
堆:用来存放动态产生的数据,比如new出来的对象。注意创建出来的对象只包含属于各自的成员变量,并不包括成员方法。因为同一个类的对象拥有各自的成员变量,存储在各自的堆中,但是他们共享该类的方法,并不是每创建一个对象就把成员方法复制一次。
常量池:JVM为每个已加载的类型维护一个常量池,常量池就是这个类型用到的常量的一个有序集合。包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用(1)。池中的数据和数组一样通过索引访问。由于常量池包含了一个类型所有的对其他类型、方法、字段的符号引用,所以常量池在Java的动态链接中起了核心作用。常量池存在于堆中。
代码段:用来存放从硬盘上读取的源程序代码。
数据段:用来存放static修饰的静态成员(在java中static的作用就是说明该变量,方法,代码块是属于类的还是属于实例的)。

image.png

24.关于Java文件的内部类的解释?匿名内部类是什么?如何访问在其外面定义的变量?

java中的内部类总结
静态内部类不能访问外部类非静态的成员

###25.关于重载和重写的区别
重载是overload,是一个类中同方法名的不同具体实现。然后重写是override,是子类重写父类中的方法。

26.String、StringBuffer与StringBuilder之间区别

1.三者在执行速度方面的比较:StringBuilder > StringBuffer > String

String:字符串常量
StringBuffer:字符串变量
StringBuilder:字符串变量

2.StringBuilder:线程非安全的,StringBuffer:线程安全的
对于三者使用的总结:
1.如果要操作少量的数据用 = String
2.单线程操作字符串缓冲区 下操作大量数据 = StringBuilder
3.多线程操作字符串缓冲区 下操作大量数据 = StringBuffer

27.运行时异常与一般异常有何异同?常见异常

Java提供了两类主要的异常:runtime exception和checked exception
常见异常:NullPointerException、IndexOutOfBoundsException、ClassNotFoundException,IllegalArgumentException,ClassCastException(数据类型转换异常)

###28.error和exception有什么区别?

error 表示恢复不是不可能但很困难的情况下的一种严重问题。比如说内存溢出。不可能指望程序能处理这样的情况。
exception表示一种设计或实现问题。也就是说,它表示如果程序运行正常,从不会发生的情况。

###29.Java异常处理机制

image.png

1.捕获异常:try、catch 和 finally
2.抛出异常
2.1. throws抛出异常

1
2
复制代码methodname throws Exception1,Exception2,..,ExceptionN  
{ }

30.java中有几种方法可以实现一个线程?

Java多线程学习(吐血超详细总结)
40个Java多线程问题总结


1.class Thread1 extends Thread{},然后重写run方法
2.class Thread2 implements Runnable{},然后重写run方法
3.class Thread3 implements Callable
{},然后new FutureTask(thread3),再用new Thread(future)封装。

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
复制代码class Thread1 extends Thread {
private String name;
public Thread1(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + "运行--->>>" + i);
}
}

public static void main(String[] args) {
Thread1 mTh11=new Thread1("A");
Thread1 mTh12=new Thread1("B");
mTh1.start();
mTh2.start();
}
}
class Thread2 implements Runnable {
private String name;
private int count = 15;
public Thread2() {
}
public Thread2(String name) {
this.name = name;
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "运行 : " + count--);
}
}
public static void main(String[] args) {
Thread2 mTh2 = new Thread2();
new Thread(mTh2, "C").start();
new Thread(mTh2, "D").start();
}
}
class MyCallableThread implements Callable<Integer>{
public Integer call() throws Exception {
int i = 0;
for(;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}

public static void main(String[] args) {
MyCallableThread mct = new MyCallableThread();
FutureTask<Integer> ft = new FutureTask<Integer>(mct);
for(int i = 0;i < 100;i++)
{
System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
if(i==20)
{
new Thread(ft,"有返回值的线程").start();
}
}
try
{
System.out.println("子线程的返回值:"+ft.get());
} catch (InterruptedException e)
{
e.printStackTrace();
} catch (ExecutionException e)
{
e.printStackTrace();
}
}
}

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

31.Java中常用的类,包,接口。

class: ‘Date’,’System’,’Calender’,’Math’,’ArrayList’,’HashMap’
package: ‘java.lang’,’java.util’,’java.io’,’java.sql’,’java.net’
interface: ‘Collection’,’Map’,’List’,’Runnable’,’Callable’

32.java在处理线程同步时,常用方法有:

1、synchronized关键字。
2、Lock显示加锁。
3、信号量Semaphore。
4、CAS算法
5、concurrent包

33.Spring IOC/AOP?

回答了IOC/DI、AOP的概念。
AOP(Aspect-OrientedProgramming,面向方面编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。
OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。
也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。
对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,
在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
依赖注入(Dependency
Injection)和控制反转(Inversion of Control)是同一个概念。
当某个角色(可能是一个Java实例,调用者)需要另一个角色(另一个Java实例,被调用者)的协助时,在传统的程序设计过程中,通常由调用者来创建被调用者的实例。
但在Spring里,创建被调用者的工作不再由调用者来完成,因此称为控制反转;创建被调用者 实例的工作通常由Spring容器来完成,然后注入调用者,因此也称为依赖注入。
不管是依赖注入,还是控制反转,都说明Spring采用动态、灵活的方式来管理各种对象。对象与对象之间的具体实现互相透明。
在理解依赖注入之前,看如下这个问题在各种社会形态里如何解决:一个人(Java实例,调用者)需要一把斧子(Java实例,被调用者)。

34.对JVM的垃圾回收的认识?

垃圾回收器的作用是查找和回收(清理)无用的对象。以便让JVM更有效的使用内存。

35.进程与线程的区别,及其通信方式

线程与进程的区别及其通信方式
区别
1.一个程序至少有一个进程,一个进程至少有一个线程.
2.进程在执行过程中拥有独立的内存单元,而多个线程共享内存
3.线程是进程的一个实体,是CPU调度和分派的基本单位

  • 进程间通信
1
2
3
4
5
6
复制代码1.管道(Pipe)及有名管道(named pipe)
2.信号(Signal)
3.消息队列(Message)
4.共享内存
5.信号量(semaphore)
6.套接口(Socket)

36.JVM如何GC,新生代,老年代,持久代,都存储哪些东西?

JVM的GC算法有:引用计数器算法,根搜索方法

新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

持久代主要存放的是Java类的类信息

37.JVM分为哪些区,每一个区干嘛的?

问:Java运行时数据区域?

回答:包括程序计数器、JVM栈、本地方法栈、方法区、堆

问:方法区里存放什么?

本地方法栈:和jvm栈所发挥的作用类似,区别是jvm栈为jvm执行java方法(字节码)服务,而本地方法栈为jvm使用的native方法服务。

JVM栈:局部变量表、操作数栈、动态链接、方法出口。

方法区:用于存储已被虚拟机加载的类信息,常量、静态变量、即时编译器编译后的代码等。

堆:存放对象实例。

38.GC用的引用可达性分析算法中,哪些对象可作为GC Roots对象?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

39.用什么工具调试程序?jmap、jstack,JConsole,用过吗?

虚拟机性能监控与调优实战–博客

40.线程池用过吗?

线程池–并发编程网 - ifeve.com

线程池(Thread Pool)对于限制应用程序中同一时刻运行的线程数很有用。因为每启动一个新线程都会有相应的性能开销,每个线程都需要给栈分配一些内存等等。

我们可以把并发执行的任务传递给一个线程池,来替代为每个并发执行的任务都启动一个新的线程。只要池里有空闲的线程,任务就会分配给一个线程执行。在线程池的内部,任务被插入一个阻塞队列(Blocking Queue ),线程池里的线程会去取这个队列里的任务。当一个新任务插入队列时,一个空闲线程就会成功的从队列中取出任务并且执行它。

41.操作系统如何进行分页调度?–要考LRU

1.最讲置换原则-OPT
2.先进先出原则-FIFO
3.最近最少使用置换算法-LRU
4.时钟置换算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码//扩展一下LinkedHashMap这个类,让他实现LRU算法
class LRULinkedHashMap<K,V> extends LinkedHashMap<K,V>{
//定义缓存的容量
private int capacity;
private static final long serialVersionUID = 1L;
//带参数的构造器
LRULinkedHashMap(int capacity){
//调用LinkedHashMap的构造器,传入以下参数
super(16,0.75f,true);
//传入指定的缓存最大容量
this.capacity=capacity;
}
//实现LRU的关键方法,如果map里面的元素个数大于了缓存最大容量,则删除链表的顶端元素
@Override
public boolean removeEldestEntry(Map.Entry<K, V> eldest){
System.out.println(eldest.getKey() + "=" + eldest.getValue());
return size()>capacity;
}
}

42.讲讲LinkedHashMap

Java8 LinkedHashMap工作原理及实现

LinkedHashMap是通过哈希表和链表实现的,它通过维护一个链表来保证对哈希表迭代时的有序性,而这个有序是指键值对插入的顺序。

LinkedHashMap 的大致实现如下图所示,当然链表和哈希表中相同的键值对都是指向同一个对象,这里把它们分开来画只是为了呈现出比较清晰的结构。

image.png

LinkedHashMap是Hash表和链表的实现,并且依靠着双向链表保证了迭代顺序是插入的顺序。

三个重点实现的函数

在HashMap中提到了下面的定义:

1
2
3
4
5
6
7
复制代码// Callbacks to allow LinkedHashMap post-actions
//1.把当前节点e移至链表的尾部。因为使用的是双向链表,所以在尾部插入可以以O(1)的时间复杂度来完成。并且只有当accessOrder设置为true时,才会执行这个操作。在HashMap的putVal方法中,就调用了这个方法。
void afterNodeAccess(Node<K,V> p) { }
//2.afterNodeInsertion方法是在哈希表中插入了一个新节点时调用的,它会把链表的头节点删除掉,删除的方式是通过调用HashMap的removeNode方法。通过afterNodeInsertion方法和afterNodeAccess方法,是不是就可以简单的实现一个基于最近最少使用(LRU)的淘汰策略了?当然,我们还要重写removeEldestEntry方法,因为它默认返回的是false。
void afterNodeInsertion(boolean evict) { }
//3.这个方法是当HashMap删除一个键值对时调用的,它会把在HashMap中删除的那个键值对一并从链表中删除,保证了哈希表和链表的一致性。
void afterNodeRemoval(Node<K,V> p) { }

LinkedHashMap继承于HashMap,因此也重新实现了这3个函数,顾名思义这三个函数的作用分别是:节点访问后、节点插入后、节点移除后做一些事情。

43.线程同步与阻塞的关系?同步一定阻塞吗?阻塞一定同步吗?,同步和异步有什么区别?

同步与非同步:主要是保证互斥的访问临界资源的情况
阻塞与非阻塞:主要是从 CPU 的消耗上来说的

44.int与Integer的区别,分别什么场合使用

1
2
3
4
复制代码1、Integer是int提供的封装类,而int是Java的基本数据类型
2、Integer默认值是null,而int默认值是0;
3、声明为Integer的变量需要实例化,而声明为int的变量不需要实例化;
4、Integer是对象,用一个引用指向这个对象,而int是基本类型,直接存储数值。

int是基本数据类型,Integer是包装类,类似HashMap这样的结构必须使用包装类,因为包装类继承自Object,都需要实现HashCode,所以可以使用在HashMap这类数据结构中。

45.RPC的详细过程

RPC主要的重点有:
动态代理,主要是invoke反射原理
序列化,使用Thrift的效率高
通信方式,使用Netty的NIO能提高效率
服务发现,使用zookeeper可以实现

  • 1)服务消费方(client)调用以本地调用方式调用服务;
  • 2)client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
  • 3)client stub找到服务地址,并将消息发送到服务端;
  • 4)server stub收到消息后进行解码;
  • 5)server stub根据解码结果调用本地的服务;
  • 6)本地服务执行并将结果返回给server stub;
  • 7)server stub将返回结果打包成消息并发送至消费方;
  • 8)client stub接收到消息,并进行解码;
  • 9)服务消费方得到最终结果。

  • 交流或更多内容请关注我的公众号:nezha_blog
  • 我的技术博客:nezha.github.io

微信公众号

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JVM——Java虚拟机架构

发表于 2017-12-11

0. 前言

Java虚拟机(Java virtualmachine) 实现了Java语言最重要的特征:即平台无关性。

平台无关性原理:编译后的 Java程序(.class文件)由 JVM执行。JVM屏蔽了与具体平台相关的信息,使程序 可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。因此实现Java平台无关性 。

本文主要介绍JVM中的架构知识,转载请注明出处:blog.csdn.net/seu_calvin/…

1. JVM结构图

JVM = 类加载器 classloader+ 执行引擎 executionengine + 运行时数据区域 runtime data area

首先Java源代码文件被Java编译器编译为字节码文件,然后 JVM中的类加载器加载完毕之后,交由JVM执行引擎执行 。在整个程序执行过程中,JVM中的运行时数据区(内存)会用来存储程序执行期间需要用到的数据和相关信息。

因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。

2. ClassLoader

classloader把硬盘上的class文件加载到 JVM中的运行时数据区域,但是它不负责这个类文件能否执行,而这个是执行引擎负责的。

限于篇幅,类加载器的组织结构,加载类的机制原理等会在JVM——类加载器总结一文中描述。

双亲委派模型以及自定义类加载器会在JVM——自定义类加载器一文中描述。

3. 执行引擎

作用:执行字节码,或者执行本地方法。

4. Runtime DataArea

JVM在运行期间,在运行时数据区对 JVM内存空间的划分和分配,划分为了以下几个区域来存储。

(图注:JDK1.7 已经把常量池转移到堆里面了!)

PC计数器(The pc Register)

(1)每一个Java线程 都有一个PC寄存器,用以记录比如在线程切换回来后恢复到正确的执行位置。

(2)如该线程正在执行一个Java方法,则计数器记录的是正在执行的虚拟机字节码地址 ,如执行native方法,则计数器值为空。

(3)此内存区域是唯一一个在JVM中没有规定任何OutOfMemoryError情况 的区域。

JVM栈(Java Virtual MachineStacks)

(1)JVM栈是 线程私有的,并且生命周期与线程相同。并且当线程运行完毕后,相应内存也就被自动回收。

(2)栈里面存放的元素叫栈帧 ,每个方法从调用到执行结束,其实是对应一个栈帧的入栈和出栈。

栈帧用于存储执行方法时的一些数据,如局部变量表、操作数栈(执行引擎计算时需要), 方法出口等等。

(3)这个区域可能有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError异常(如:将一个函数反复递归自己,最终会出现这种异常)。如果 JVM栈可以动态扩展(大部分JVM是可以的),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。

本地方法栈(Native Method Stacks)

(1)本地方法栈与虚拟机栈所发挥的作用很相似,他们的区别在于 虚拟机栈为执行Java代码方法服务,而本地方法栈是为Native方法服务。

(2)和JVM栈一样,这个区域也会抛出 StackOverflowError和OutOfMemoryError异常。

方法区(Method Area)

(1)方法区域是全局共享 的,比如每个线程都可以访问同一个类的静态变量。在方法区中,存储了已被JVM加载的类的信息、 静态变量、编译器编译后的代码等。如,当程序中通过getName、isInterface 等方法来获取信息时,这些数据来源于方法区。

(2)由于使用反射机制的原因,虚拟机很难推测哪个类信息不再使用,因此这块区域的 回收很难!另外,对这块区域主要是针对常量池回收,值得注意的是JDK1.7 已经把常量池转移到堆里面了。

(3)同样,当方法区无法满足内存分配需求时,会抛出 OutOfMemoryError。

运行时常量池(Runtime Constant Pool)

(1)存放类中固定的常量信息、方法引用信息等,其空间从方法区域(JDK1.7后为堆空间)中分配。

(2)Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有就是 常量表,用于存放编译期已可知的常量,这部分内容将在类加载后进入方法区(永久代)存放。但是 Java语言并不要求常量一定只有编译期预置入Class的常量表的内容才能进入方法区常量池,运行期间也可将新内容放入常量池(最典型的 String.intern()方法)。

(3)当常量池无法在申请到内存时会抛出OutOfMemoryError 异常,上面也分析过了。

Java堆

(1)Java堆是JVM所管理的最大的一块内存 。它是被所有线程共享的一块内存区域,在虚拟机启动时创建。

(2)几乎所有的实例对象都是在这块区域中存放。(JIT编译器貌似不是这样的)。

(3)Java堆是垃圾收集管理的主要战场。所有 Java堆可以细分为:新生代和老年代。再细致分就是把新生代分为: Eden空间、FromSurvivor空间、To Survivor 空间。JVM具体的垃圾回收机制总结请查看我的另外一篇JVM——内存管理和垃圾回收。

(4)根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

5. 堆和栈的区别

这是一个非常常见的面试题,主要从以下几个方面来回答。

(1)各司其职

最主要的区别就是栈内存用来存储局部变量和方法调用信息。
而堆内存用来存储Java 中的对象。无论是成员变量、局部变量还是类变量,它们指向的对象都存储在堆内存中。

(2)空间大小

栈的内存要远远小于堆内存,如果你使用递归的话,那么你的栈很快就会充满并产生StackOverFlowError。
关于如何设置堆栈内存的大小,可以查看JVM——内存管理和垃圾回收中的相关介绍。

(3)独有还是共享

栈内存归属于线程的私有内存,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见。
而堆内存中的对象对所有线程可见,可以被所有线程访问。

(4)异常错误

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常。

如果JVM栈可以动态扩展(大部分JVM是可以的),当扩展时 无法申请到足够内存则抛出OutOfMemoryError异常。

而堆内存没有可用的空间存储生成的对象,JVM会抛出 java.lang.OutOfMemoryError。

以上便是关于JVM架构的相关知识。

转载请注明出处:blog.csdn.net/seu_calvin/…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

NIO相关基础篇一

发表于 2017-12-11

转载请注明原创出处,谢谢!

说在前面

NIO相关知识是很多后续的一些基础知识,所以今天这篇文章仅仅是简单介绍,后续会继续有一到二篇相关NIO内容。

什么是NIO

Java NIO( New IO) 是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同, NIO支持面向缓冲区的、基于通道的IO操作。 NIO将以更加高效的方式进行文件的读写操作。

NIO与普通IO的主要区别

IO NIO
面向流(Stream Oriented) 面向缓冲区(Buffer Oriented)
阻塞IO(Blocking IO) 非阻塞IO(Non Blocking IO)
(无) 选择器(Selectors)
  • Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
  • Asynchronous IO(异步IO):Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
  • Selectors(选择器):Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。

**说明:**Asynchronous IO(异步IO)、Selectors(选择器)等后续文章会继续介绍的。

缓冲区(Buffer)

通过上面NIO与普通IO的主要区别也可以看到在基本的IO操作中所有的操作都是基于流进行操作的,而在NIO中所有的操作都是基于缓冲区继续操作的,所有的读写操作都是通过缓存区来进行完成,缓冲区(Buffer)是一个线性的、有序的数据集,只能容纳特定的数据类型(基本就是基本数据类型对应的Buffer或者起子类)。

各各数据类型的缓存区类

缓存区类 相关描述
ByteBuffer 存储字节的Buffer
CharBuffer 存储字符的Buffer
ShortBuffer 存储短整型的Buffer
IntBuffer 存储整型的Buffer
LongBuffer 存储长整型的Buffer
FloatBuffer 存储单精度浮点型Buffer
DoubleBuffer 存储双精度浮点型Buffer

**备注:**看到上面这几类是不是想起了JAVA的8种基本数据类型,唯一缺少boolean对于的类型。

第一问:为什么boolean不需要缓存呢?
可以查阅之前写的:java二进制相关基础,里面有描述规范中数字的内部表示和存储,boolean所占位数1bit(取值只有true或者false),由于字节(byte)是操作系统和所有I/O设备使用的基本数据类型,所以基本都是以字节或者连续的一段字节来存储表示,所以就没有boolean,感觉也没有必要boolean类型的缓存操作(像RocketMQ源码里面可能把一个Int里面的某位来表示boolean,其他位继续来存储数据,欢迎关注我的公众号【匠心零度】,后续RocketMQ源码类分析的时候如何运用上述技巧进行说明等,其实上面我写的好几篇文章都是为了后续RocketMQ源码分析做准备的)。

Buffer使用

读数据:

  • flip()方法
    • 将Buffer从写模式切换到读模式
    • 调用flip()方法会将position设回0,并将limit设置成之前position的值。
    • buf.flip();
  • buf.get()
    • 读取数据
  • Buffer.rewind()
    • 将position设回0,所以你可以重读Buffer中的所有数据
    • limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)
  • Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用。
  • Buffer.reset()方法,恢复到Buffer.mark()标记时的position。
  • clear()方法会:
    • 清空整个缓冲区。
    • position将被设回0,limit被设置成 capacity的值
  • compact()方法:
    • 只会清除已经读过的数据;任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
    • 将position设到最后一个未读元素正后面,limit被设置成 capacity的值。

写数据:
buf.put(127);

缓冲区的基本属性

  • 容量 (capacity):表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。
  • 限制 (limit):第一个不应该读取或写入的数据的索引,即位于 limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。
  • 位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制。

**备注:**标记、 位置、 限制、 容量遵守以下不变式: 0 <= position <= limit <= capacity。

为了更形象解释上面重要属性,准备配上简单代码以及图来进行说明就容易懂了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码//第一步,获取IntBuffer,通过IntBuffer.allocate操作
IntBuffer buf = IntBuffer.allocate(10) ; // 准备出10个大小的缓冲区

//第二步未操作前输出属性值
System.out.println("position = " + buf.position() + ",limit = " + buf.limit() + ",capacty = " + buf.capacity()) ;

//第三步进行设置数据
buf.put(6) ; // 设置一个数据
buf.put(16) ; // 设置二个数据

//第四步操作后输出属性值
System.out.println("position = " + buf.position() + ",limit = " + buf.limit() + ",capacty = " + buf.capacity()) ;

//第五步将Buffer从写模式切换到读模式 postion = 0 ,limit = 原本position
buf.flip() ;

//第六步操作后输出属性值
System.out.println("position = " + buf.position() + ",limit = " + buf.limit() + ",capacty = " + buf.capacity()) ;

程序输出结果:

1
2
3
复制代码position = 0,limit = 10,capacty = 10
position = 2,limit = 10,capacty = 10
position = 0,limit = 2,capacty = 10

查看下图来进行说明:

通道(Channel)

通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。Channel 负责传输, Buffer 负责存储。通道是由 java.nio.channels 包定义的。 Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel本身不能直接访问数据, Channel 只能与Buffer 进行交互。

通道都是操作缓存区完成全部的功能的。

Java中所有已知 Channel 实现类:

  • AbstractInterruptibleChannel
  • AbstractSelectableChannel
  • DatagramChannel
  • FileChannel
  • Pipe.SinkChannel
  • Pipe.SourceChannel
  • SelectableChannel
  • ServerSocketChannel
  • SocketChannel

常用的有入下几个:

  • FileChannel:用于读取、写入、映射和操作文件的通道。
  • DatagramChannel:通过 UDP 读写网络中的数据通道。
  • SocketChannel:通过 TCP 读写网络中的数据。
  • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。

获取通道

获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下:

  • FileInputStream
  • FileOutputStream
  • RandomAccessFile
  • DatagramSocket
  • Socket
  • ServerSocket

获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。或者通过通道的静态方法 open() 打开并返回指定通道。

FileChannel

  • 为了更形象解释说明的Channel,下面准备以FileChannel的一些简单代码进行说明就容易懂了。
  • 准备以FileOutputStream类为准,这两个类都是支持通道操作的。
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
复制代码String info[] = {"欢迎","关注","匠心零度","的","公众号","谢谢!!"} ;
File file = new File("d:" + File.separator + "testfilechannel.txt") ;
FileOutputStream output = null ;
FileChannel fout = null;
try {
output = new FileOutputStream(file) ;
fout = null;
fout = output.getChannel() ; // 得到输出的通道
ByteBuffer buf = ByteBuffer.allocate(1024) ;
for(int i=0;i<info.length;i++){
buf.put(info[i].getBytes()) ; // 字符串变为字节数组放进缓冲区之中
}
buf.flip() ;
fout.write(buf) ; // 输出缓冲区的内容
} catch (Exception e) {
e.printStackTrace();
}finally{
if(fout!=null){
try {
fout.close() ;
} catch (IOException e) {
e.printStackTrace();
}
}
if(output!=null){
try {
output.close() ;
} catch (IOException e) {
e.printStackTrace();
}
}
}

程序运行效果:

**说明:**今天只是NIO相关基础篇一,所以有很多并没有涉及到,希望上面说的这样让大家有一个新的了解,未完待续……

如果读完觉得有收获的话,欢迎点赞、关注、加公众号【匠心零度】。


个人公众号,欢迎关注,查阅更多精彩历史!!!

匠心零度公众号

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

jvm系列(二) JVM内存结构

发表于 2017-12-11

所有的Java开发人员可能会遇到这样的困惑?我该为堆内存设置多大空间呢?OutOfMemoryError的异常到底涉及到运行时数据的哪块区域?该怎么解决呢?其实如果你经常解决服务器性能问题,那么这些问题就会变的非常常见,了解JVM内存也是为了服务器出现性能问题的时候可以快速的了解那块的内存区域出现问题,以便于快速的解决生产故障。

先看一张图,这张图能很清晰的说明JVM内存结构布局。

JVM内存结构主要有三大块:堆内存、方法区和栈。堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;

方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。

在通过一张图来了解如何通过参数来控制各区域的内存大小

控制参数

  • -Xms设置堆的最小空间大小。
  • -Xmx设置堆的最大空间大小。
  • -XX:NewSize设置新生代最小空间大小。
  • -XX:MaxNewSize设置新生代最大空间大小。
  • -XX:PermSize设置永久代最小空间大小。
  • -XX:MaxPermSize设置永久代最大空间大小。
  • -Xss设置每个线程的堆栈大小。

没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。

老年代空间大小=堆空间大小-年轻代大空间大小

从更高的一个维度再次来看JVM和系统调用之间的关系

方法区和对是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。

下面我们详细介绍每个区域的作用

Java堆(Heap)

对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。

如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

方法区(Method Area)

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。

Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。

根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

方法区有时被称为持久代(PermGen)。

所有的对象在实例化后的整个运行周期内,都被存放在堆内存中。堆内存又被划分成不同的部分:伊甸区(Eden),幸存者区域(Survivor Sapce),老年代(Old Generation Space)。

方法的执行都是伴随着线程的。原始类型的本地变量以及引用都存放在线程栈中。而引用关联的对象比如String,都存在在堆中。为了更好的理解上面这段话,我们可以看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码import java.text.SimpleDateFormat;
import java.util.Date;
import org.apache.log4j.Logger;

public class HelloWorld {
private static Logger LOGGER = Logger.getLogger(HelloWorld.class.getName());
public void sayHello(String message) {
SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.YYYY");
String today = formatter.format(new Date());
LOGGER.info(today + ": " + message);
}
}

这段程序的数据在内存中的存放如下:

通过JConsole工具可以查看运行中的Java程序(比如Eclipse)的一些信息:堆内存的分配,线程的数量以及加载的类的个数;

程序计数器(Program Counter Register)

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

JVM栈(JVM Stacks)

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

本地方法栈(Native Method Stacks)

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

哪儿的OutOfMemoryError

对内存结构清晰的认识同样可以帮助理解不同OutOfMemoryErrors:

1
复制代码Exception in thread “main”: java.lang.OutOfMemoryError: Java heap space

原因:对象不能被分配到堆内存中

1
复制代码Exception in thread “main”: java.lang.OutOfMemoryError: PermGen space

原因:类或者方法不能被加载到持久代。它可能出现在一个程序加载很多类的时候,比如引用了很多第三方的库;

1
复制代码Exception in thread “main”: java.lang.OutOfMemoryError: Requested array size exceeds VM limit

原因:创建的数组大于堆内存的空间

1
复制代码Exception in thread “main”: java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?

原因:分配本地分配失败。JNI、本地库或者Java虚拟机都会从本地堆中分配内存空间。

1
复制代码Exception in thread “main”: java.lang.OutOfMemoryError: <reason> <stack trace>(Native method)

原因:同样是本地方法内存分配失败,只不过是JNI或者本地方法或者Java虚拟机发现


扫描关注:纯洁的微笑

(转载本站文章请注明作者和出处 纯洁的微笑-ityouknow)

Show Disqus Comments
Gitalking …

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

AbstractQueuedSynchronizer整体解析

发表于 2017-12-11

AbstractQueuedSynchronizer整体解析

前言

在此之前,我们深入源码分析过ReentrantLock系列,在那里就探讨过AbstractQueuedSynchronizer(下称AQS)类,称其是同步组件乃至整个并发包的基础类。这篇文章就深入AQS,从AQS的角度了解同步器以及ReentrantLock、ReentrantReadWriteLock等的实现机制,实现自定义的同步组件,以窥探整个同步框架的全貌。

AQS及同步器整体介绍

有关类字段及方法的介绍,在ReentrantLock原理探究(一)就已说过,今天我们换种方式,就围绕两个问题介绍AQS:

  1. AQS是干什么的?
  2. 它是怎样做到的?

回答出这两个问题,我们就从整体性上理解了AQS,乃至整个同步器组件。

AQS类源码注释说得很多,重点有,该类是一个用于构建锁或其他同步器的基础框架,使用一个int的成员变量表示同步状态。另外,还有一个内置的先进先出的队列可储存竞争同步状态时排队的线程。

从洋洋洒洒的类注释及其他资料,我们不难还原出AQS要做的事:有一个共享资源state(int类型的变量),各个线程去竞争这个资源,竞争到的拥有资源,去处理自己的逻辑;没竞争到去排队(进入先进先出队列),等拥有资源的线程释放共享资源后,队列中线程的再去竞争。

AQS基本实现了以上功能,相当于搭好了整体框架,我们需要实现哪个具体的功能,重写AQS某些指定方法即可。下面是两个同步器类实现的大体思路

  1. ReentrantLock,是排他锁,某个线程获取锁后其他线程就会阻塞直至锁的释放。共享资源state初始值为0,表示资源未被占有。某线程访问并设置state为1,表示该线程占有了锁。当其他线程读取到state不为0后进入队列等待,直到占有锁的线程将其设为0后,队列线程才会得到通知,重新竞争锁。(事实上ReentrantLock作为可重入锁,占有锁的线程再次进入锁会使state加1,退出一次state减1)
  2. CountDownLatch,共享锁。可用于控制线程执行、结束的时机。如我们想要主线程在2个子线程执行完后再结束,这时使用CountDownLatch通过构造函数将共享变量state设为2,将主线程锁住,每个子线程结束后state减一,state为0后表示两子线程执行完毕,此时主线程才得以释放。

也即是说,通过AQS,我们将能很简单的实现同步的要求。这也是模板方法模式的运用。

AQS主要模板方法如下

方法 描述
acquire / acquireInterruptibly 独占式获取同步状态,若获取失败,将进入同步队列。后者与前者的区别在于,后者能在同步队列中响应中断
acquireShared / acquireSharedInterruptibly 共享式获取同步状态,后者能响应中断
release 独占式释放同步状态,成功后将同步队列的第一个线程唤醒
releaseShared 共享式释放同步状态

需要子类实现的方法如下

方法 实现思路
tryAcquire 独占式获取同步状态,实现该方法需要查询当前状态,并判断状态是否符合预期(根据各子类不同功能判断条件各异),然后再根据CAS设置同步状态
tryRelease 独占式释放同步状态
tryAcquireShared 共享式获取同步状态,若返回值大于等于0,表示获取成功,否则表示失败
tryReleaseShared 共享式释放同步状态
isHeldExclusively 在独占模式下,同步状态是否被占用

AQS会在合适的时候调用子类的方法,以实现不同功能。以acquire 方法为例

1
2
3
4
5
reasonml复制代码  public final void acquire(int arg) {
if (!tryAcquire(arg) && //会调用子类的tryAcquire方法,实现不同的acquire含义
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

另外,AQS还提供了有关获取设置state状态的有关方法,我们在自定义子类中会用到。

方法 描述
getState 获取同步状态
setState(state) 设置同步状态
compareAndSetState(except,update) 使用CAS设置同步状态,只有当同步状态值为except时,才将其设置update

一个简单的锁

根据上面提到的,我们来自制一个独占类型的锁。

根据AQS的建议,实现AQS的类最好为同步器的内部类。下面是自制锁MyLock代码示例

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
aspectj复制代码
public class MyLock implements Lock {

private Sync sync = new Sync();

//AQS的子类,由于是独占锁,实现tryAcquire和tryRelease两方法
private static class Sync extends AbstractQueuedSynchronizer {

@Override
protected boolean tryAcquire(int arg) {
//若状态为1,说明有其他线程已占有锁,直接返回false
if(getState()==arg){
return false;
}
//若状态为0,将其设为1,表示占有锁
return compareAndSetState(0, arg);
}

@Override
protected boolean tryRelease(int arg) {
//设置状态为0,表示释放锁
setState(0);
return true;
}
}

//其他Lock接口方法,直接调用Sync类实现

@Override
public void lock() {
sync.acquire(1);

}

@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);

}

@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}

@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1,unit.toNanos(time));
}


@Override
public void unlock() {
sync.release(1);

}

@Override
public Condition newCondition() {
return null;
}
}

这样我们就实现了一个简单的锁。不过这个锁相比ReentrantLock来说,没有实现可重入性(也没有实现关联条件Condition)。也就是说它会被自己锁死:当某个线程在获取锁后再次尝试获取锁,会导致死锁。不过,实现类似i++的同步倒是可以做到的。

1
2
3
4
5
6
7
8
csharp复制代码       public void run() {
myLock.lock();
try {
total++;
} finally {
myLock.unlock();
}
}

对于可重入锁,需要记住持有锁的线程,当加锁时,判断当前线程是否持有锁,若持有,直接进入同步块,同时将state加1,当试图释放锁时,将state减1。若state减到0,释放锁。其他过程与其他一致。感兴趣你可以试试。

对于AQS的大致理解就到这了,有时间我们再深入源码分析其具体实现。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…910911912…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%