首页 文章 精选 留言 我的

精选列表

搜索[学习],共10000篇文章
优秀的个人博客,低调大师

机器学习 | R语言中的方差分析汇总

方差分析,是统计中的基础分析方法,也是我们在分析数据时经常使用的方法。下面我总结一下R语言如何对常用的方差分析进行操作。 1. 方差分析的假定 上面这个思维导图,也可以看出,方差分析有三大假定:正态,独立和齐次,如果不满足,可以使用广义线性模型或者混合线性模型,或者广义线性混合模型去分析。 本次我们的主题有: 2. 数据来源 这里,我们使用的数据来源于R包agridat,它是讲农业相关的论文,书籍中相关的数据收集在了一起,更加符合我们的背景。 包的下载地址:https://cran.r-project.org/web/packages/agridat/index.html 包的介绍 agridat: Agricultural Datasets Datasets from books, papers, and websites related to agriculture. Example graphics and analyses are included. Data come from small-plot trials, multi-environment trials, uniformity trials, yield monitors, and more. 包的安装方式: install.packages("agridat") 3. 单因素方差分析 比如一个处理有A,B,C三个水平,观测值y,想看一下这个处理是否达到显著性水平,这就可以用到方差分析了。 数据描述: Corn yield and nitrogen fertilizer treatment with field characteristics for the Las Rosas farm, Rio Cuarto, Cordoba, Argentina. data(lasrosas.corn) dat <- lasrosas.corn str(dat) 数据结构: > str(dat) 'data.frame': 3443 obs. of 9 variables: $ year : int 1999 1999 1999 1999 1999 1999 1999 1999 1999 1999 ... $ lat : num -33.1 -33.1 -33.1 -33.1 -33.1 ... $ long : num -63.8 -63.8 -63.8 -63.8 -63.8 ... $ yield: num 72.1 73.8 77.2 76.3 75.5 ... $ nitro: num 132 132 132 132 132 ... $ topo : Factor w/ 4 levels "E","HT","LO",..: 4 4 4 4 4 4 4 4 4 4 ... $ bv : num 163 170 168 177 171 ... $ rep : Factor w/ 3 levels "R1","R2","R3": 1 1 1 1 1 1 1 1 1 1 ... $ nf : Factor w/ 6 levels "N0","N1","N2",..: 6 6 6 6 6 6 6 6 6 6 ... 这里数据有很多列,但是我们要演示单因素方差分析,这里的因素为nf,自变量(Y变量)是yield,想要看一下nf的不同水平是否达到显著性差异。 建模: Y变量:yield 因子:nf R中的建模代码: m1 = aov(yield ~ nf, data=dat) m1为模型保存的名称 aov为R中的方差分析代码 yield为数据中的Y变量,这里是yield ~,波浪号前面为Y变量,后面为X变量 nf为分析的因子变量 dat为数据 结果: > m1 = aov(yield ~ nf, data=dat) > summary(m1) Df Sum Sq Mean Sq F value Pr(>F) nf 5 23987 4797 12.4 6.08e-12 *** Residuals 3437 1330110 387 --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 结果可以看出,nf之间达到极显著水平,可以进行多重比较。 方差分析的显著性和多重比较有何关系??? 方差分析,一般会得到显著性(即P值小于0.05,说明显著,小于0.01,说明极显著,大于0.05,说明不显著),显著的意思是因素之间至少有一对水平达到显著性差异,具体是那一对呢?有几对呢?这就需要用到多重比较。所以,多重比较是在方差分析达到显著性之后进行的,只有显著了(P值小于0.05)才有能进行多重比较。多重比较的方法有很多,比如LSD,Tukey,Bonferroni,Holm等等,我们后面系统的介绍。 4. 单因素随机区组 这里区组的设置,可以控制一些环境误差。 数据描述: Switchback trial in dairy with three treatments data(lucas.switchback) dat <- lucas.switchback str(dat) 数据结构: > str(dat) 'data.frame': 36 obs. of 5 variables: $ cow : Factor w/ 12 levels "C1","C10","C11",..: 1 5 6 7 8 9 10 11 12 2 ... $ trt : Factor w/ 3 levels "T1","T2","T3": 1 2 3 1 2 3 1 2 3 1 ... $ period: Factor w/ 3 levels "P1","P2","P3": 1 1 1 1 1 1 1 1 1 1 ... $ yield : num 34.6 22.8 32.9 48.9 21.8 25.4 30.4 35.2 30.8 38.7 ... $ block : Factor w/ 3 levels "B1","B2","B3": 1 1 1 1 1 1 2 2 2 3 ... 建模: Y变量:yield 因子:trt 区组:block R中的建模代码: m2 = aov(yield ~ block +trt, data=dat) summary(m2) 结果: > summary(m2) Df Sum Sq Mean Sq F value Pr(>F) block 2 30.9 15.46 0.306 0.7385 trt 2 273.8 136.88 2.709 0.0823 . Residuals 31 1566.1 50.52 --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 结果可以看出,trt之间达到没有极显著水平。 5. 二因素方差分析(无交互) 这里区组的设置,可以控制一些环境误差。 无交互的意思是,二因素之间不考虑互作。 数据描述: Switchback trial in dairy with three treatments data(lucas.switchback) dat <- lucas.switchback str(dat) 数据结构: > str(dat) 'data.frame': 36 obs. of 5 variables: $ cow : Factor w/ 12 levels "C1","C10","C11",..: 1 5 6 7 8 9 10 11 12 2 ... $ trt : Factor w/ 3 levels "T1","T2","T3": 1 2 3 1 2 3 1 2 3 1 ... $ period: Factor w/ 3 levels "P1","P2","P3": 1 1 1 1 1 1 1 1 1 1 ... $ yield : num 34.6 22.8 32.9 48.9 21.8 25.4 30.4 35.2 30.8 38.7 ... $ block : Factor w/ 3 levels "B1","B2","B3": 1 1 1 1 1 1 2 2 2 3 ... 建模: Y变量:yield 因子1:trt 因子2:period 区组:block R中的建模代码: m3 = aov(yield ~ block +trt +period, data=dat) summary(m3) 结果: > summary(m3) Df Sum Sq Mean Sq F value Pr(>F) block 2 30.9 15.46 0.308 0.7374 trt 2 273.8 136.88 2.725 0.0823 . period 2 109.5 54.74 1.090 0.3497 Residuals 29 1456.6 50.23 --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 结果可以看出,trt之间达到没有极显著水平,period之间没有达到显著性水平。 6. 二因素方差分析(有交互) 这里区组的设置,可以控制一些环境误差。 交互的意思是,二因素之间考虑互作。 数据描述: Switchback trial in dairy with three treatments data(lucas.switchback) dat <- lucas.switchback str(dat) 数据结构: > str(dat) 'data.frame': 36 obs. of 5 variables: $ cow : Factor w/ 12 levels "C1","C10","C11",..: 1 5 6 7 8 9 10 11 12 2 ... $ trt : Factor w/ 3 levels "T1","T2","T3": 1 2 3 1 2 3 1 2 3 1 ... $ period: Factor w/ 3 levels "P1","P2","P3": 1 1 1 1 1 1 1 1 1 1 ... $ yield : num 34.6 22.8 32.9 48.9 21.8 25.4 30.4 35.2 30.8 38.7 ... $ block : Factor w/ 3 levels "B1","B2","B3": 1 1 1 1 1 1 2 2 2 3 ... 建模: Y变量:yield 因子1:trt 因子2:period 区组:block R中的建模代码: m4 = aov(yield ~ block +trt*period, data=dat) summary(m4) 注意,这里的trt*period是R中公式的简写,表示trt + period + trt:period,其中trt:period表示互作效应。 结果: > summary(m4) Df Sum Sq Mean Sq F value Pr(>F) block 2 30.9 15.46 0.339 0.7160 trt 2 273.8 136.88 2.997 0.0681 . period 2 109.5 54.74 1.199 0.3183 trt:period 4 315.0 78.75 1.725 0.1761 Residuals 25 1141.6 45.66 --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 结果可以看出,trt之间达到没有极显著水平,period之间没有达到显著性水平,trt和period交互没有达到显著水平。 7. 多因素方差分析 这里区组的设置,可以控制一些环境误差。 多于两个因素的方差分析 数据描述: Switchback trial in dairy with three treatments data(lucas.switchback) dat <- lucas.switchback str(dat) 数据结构: > str(dat) 'data.frame': 36 obs. of 5 variables: $ cow : Factor w/ 12 levels "C1","C10","C11",..: 1 5 6 7 8 9 10 11 12 2 ... $ trt : Factor w/ 3 levels "T1","T2","T3": 1 2 3 1 2 3 1 2 3 1 ... $ period: Factor w/ 3 levels "P1","P2","P3": 1 1 1 1 1 1 1 1 1 1 ... $ yield : num 34.6 22.8 32.9 48.9 21.8 25.4 30.4 35.2 30.8 38.7 ... $ block : Factor w/ 3 levels "B1","B2","B3": 1 1 1 1 1 1 2 2 2 3 ... 建模: Y变量:yield 因子1:trt 因子2:period 因子3:cow 区组:block R中的建模代码: m5 = aov(yield ~ block + trt*period + cow, data=dat) summary(m5) 注意,这里的trt*period是R中公式的简写,表示trt + period + trt:period,其中trt:period表示互作效应。 结果: > summary(m5) Df Sum Sq Mean Sq F value Pr(>F) block 2 30.9 15.46 11.155 0.000926 *** trt 2 273.8 136.88 98.739 9.96e-10 *** period 2 109.5 54.74 39.486 6.49e-07 *** cow 9 1428.8 158.76 114.523 8.29e-13 *** trt:period 4 5.6 1.41 1.015 0.429042 Residuals 16 22.2 1.39 --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 结果可以看出,trt之间达到极显著水平,period之间达到显著性水平,trt和period交互没有达到显著水平,cow达到极显著水平。 8. 裂区试验方差分析 裂区试验,包括主区(wplot)和裂区(splot),其中裂区是镶嵌在主区中的,主区和裂区的残差不一样,在模型中需要特殊定义。 数据描述: Strip-split plot of barley with fertilizer, calcium, and soil factors. data(cox.stripsplit) dat <- cox.stripsplit str(dat) 数据结构: > str(dat) 'data.frame': 96 obs. of 5 variables: $ rep : Factor w/ 4 levels "R1","R2","R3",..: 1 1 1 1 1 1 1 1 1 1 ... $ soil : Factor w/ 3 levels "S1","S2","S3": 1 1 1 1 1 1 1 1 2 2 ... $ fert : Factor w/ 4 levels "F0","F1","F2",..: 1 1 2 2 3 3 4 4 1 1 ... $ calcium: Factor w/ 2 levels "C0","C1": 1 2 1 2 1 2 1 2 1 2 ... $ yield : num 4.91 4.63 4.76 5.04 5.38 6.21 5.6 5.08 4.94 3.98 ... 建模: Y变量:yield 主区:fert 裂区:soil 区组:brep R中的建模代码: m6 = aov(yield ~ rep + soil*fert + Error(rep/fert),data=dat) summary(m6) 注意,这里的Error(rep/fert)是R中公式的定义残差,主要用于不同因素使用不同残差的情况,这里fert是主区。 结果: > summary(m6) Error: rep Df Sum Sq Mean Sq rep 3 6.28 2.093 Error: rep:fert Df Sum Sq Mean Sq F value Pr(>F) fert 3 7.221 2.4071 3.562 0.0604 . Residuals 9 6.082 0.6758 --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 Error: Within Df Sum Sq Mean Sq F value Pr(>F) soil 2 1.927 0.9633 7.155 0.00146 ** soil:fert 6 0.688 0.1147 0.852 0.53433 Residuals 72 9.693 0.1346 --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 结果可以看出,soil达到极显著,fert不显著,soil和fert的互作不显著。 9. 正态性检验 方差分析中,结果是否可信,在于数据是否满足假定条件。方差分析的假定包括数据正态性,数据的方差齐次性,数据的独立性,其中可以检验的假定有: 数据的正态性 数据的齐次性 这里,我们介绍如何对数据的正态性进行检验。 可以使用球性检验(Shapiro-Wilk)检验数据的正态性,也可以用qqplot查看残差的图,判断数据的正态性,也可以对数据做直方图,查看数据的正态性。 数据描述: A classical N, P, K (nitrogen, phosphate, potassium) factorial experiment on the growth of peas conducted on 6 blocks. Each half of a fractional factorial design confounding the NPK interaction was used on 3 of the plots. data(npk) dat <- npk str(dat) 数据结构: > str(dat) 'data.frame': 24 obs. of 5 variables: $ block: Factor w/ 6 levels "1","2","3","4",..: 1 1 1 1 2 2 2 2 3 3 ... $ N : Factor w/ 2 levels "0","1": 1 2 1 2 2 2 1 1 1 2 ... $ P : Factor w/ 2 levels "0","1": 2 2 1 1 1 2 1 2 2 2 ... $ K : Factor w/ 2 levels "0","1": 2 1 1 2 1 2 2 1 1 2 ... $ yield: num 49.5 62.8 46.8 57 59.8 58.5 55.5 56 62.8 55.8 ... 一般分析时,我们仅对Y变量进行正态性检验,如果是单因素或者多因素,也可以根据因素分组进行正态性检验,数据量大时,对于稍微偏态的数据,即使不太符合正态分布,也不影响结论。 这里,我们对yield进行正态性检验 作直方图 hist(dat$yield) 可以看到,yield大体符合正态分布。 做qq图 使用car软件包中的qqPlot函数。 library(car) qqPlot(dat$yield) 可以看到,数据基本落在置信区间里面,数据符合正态分布。 使用Shapiro-Wilk检验数据正态分布 > shapiro.test(dat$yield) Shapiro-Wilk normality test data: dat$yield W = 0.97884, p-value = 0.8735 可以看到,P值为0.8735,数据符合正态分布,与上图显示的结果一致。 10. 齐次性检验 方差分析中,我们对结果是否自信,在于数据是否满足假定条件,方差分析的假定条件包括数据正态性,数据的方差齐次性,数据的独立性,其中可以检验的假定有: 数据的正态性 数据的齐次性 这里,我们介绍如何对数据的齐次性进行检验。齐次性检验,是检验不同样本的总体方差是否相同,是根据特定的模型,需要考虑不同的因子放到模型中,不能单独对某一列变量进行齐次性检验。 齐次性检验: Bartlet检验 Levene检验 数据描述: A classical N, P, K (nitrogen, phosphate, potassium) factorial experiment on the growth of peas conducted on 6 blocks. Each half of a fractional factorial design confounding the NPK interaction was used on 3 of the plots. data(npk) dat <- npk str(dat) 数据结构: > str(dat) 'data.frame': 24 obs. of 5 variables: $ block: Factor w/ 6 levels "1","2","3","4",..: 1 1 1 1 2 2 2 2 3 3 ... $ N : Factor w/ 2 levels "0","1": 1 2 1 2 2 2 1 1 1 2 ... $ P : Factor w/ 2 levels "0","1": 2 2 1 1 1 2 1 2 2 2 ... $ K : Factor w/ 2 levels "0","1": 2 1 1 2 1 2 2 1 1 2 ... $ yield: num 49.5 62.8 46.8 57 59.8 58.5 55.5 56 62.8 55.8 ... 比如上面数据中,相对N进行单因素方差分析,查看不同N的水平是否满足方差齐次性,可以这样操作: Bartlett检验 Bartlett检验可以比较多个总体的方差 bartlett.test(yield ~ N,data=dat) 结果: > bartlett.test(yield ~ N,data=dat) Bartlett test of homogeneity of variances data: yield by N Bartlett's K-squared = 0.057652, df = 1, p-value = 0.8102 结果可以看出,不同的N之间,方差满足齐次性要求。 Levene检验 Bartlett检验对数据的正态性非常敏感,而Levene检验是一种非参数检验方法,使用范围更广。 library(car) leveneTest(yield ~ N, data=dat) 结果: > leveneTest(yield ~ N, data=dat) Levene's Test for Homogeneity of Variance (center = median) Df F value Pr(>F) group 1 0.0054 0.9421 22 结果可以看出,P值为0.9421,大于0.05,说明数据符合方差齐次性。 11. 多重比较 这里,我们介绍一下方差分析中的多重比较方法。 agricolae包 This package contains functionality for the Statistical Analysis of experimental designs applied specially for field experiments in agriculture and plant breeding. 多重比较方法: LSD scheffe HSD(Tukey) SNK Duncan 数据描述: A classical N, P, K (nitrogen, phosphate, potassium) factorial experiment on the growth of peas conducted on 6 blocks. Each half of a fractional factorial design confounding the NPK interaction was used on 3 of the plots. data(npk) dat <- npk str(dat) 数据结构: > str(dat) 'data.frame': 24 obs. of 5 variables: $ block: Factor w/ 6 levels "1","2","3","4",..: 1 1 1 1 2 2 2 2 3 3 ... $ N : Factor w/ 2 levels "0","1": 1 2 1 2 2 2 1 1 1 2 ... $ P : Factor w/ 2 levels "0","1": 2 2 1 1 1 2 1 2 2 2 ... $ K : Factor w/ 2 levels "0","1": 2 1 1 2 1 2 2 1 1 2 ... $ yield: num 49.5 62.8 46.8 57 59.8 58.5 55.5 56 62.8 55.8 ... 方差分析 这里对N进行单因素方差分析,block为区组: > m9 = aov(yield ~ block + N, data=dat) > summary(m9) Df Sum Sq Mean Sq F value Pr(>F) block 5 343.3 68.66 3.395 0.0262 * N 1 189.3 189.28 9.360 0.0071 ** Residuals 17 343.8 20.22 --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 结果可以看到,N因素达到极显著水平 11.1 LSD多重比较 Multiple comparisons of treatments by means of LSD and a grouping of treatments. The level by alpha default is 0.05. Returns p-values adjusted using one of several methods # LSD re1 = LSD.test(m9,"N") re1$groups 结果: > re1 = LSD.test(m9,"N") > re1$groups yield groups 1 57.68333 a 0 52.06667 b 11.2 scheffe多重比较 Scheffe 1959, method is very general in that all possible contrasts can be tested for significance and confidence intervals can be constructed for the corresponding linear. The test is conservative. 代码: (scheffe.test(m9,"N")) 结果 > (scheffe.test(m9,"N")) $statistics MSerror Df F Mean CV Scheffe CriticalDifference 20.22284 17 4.451322 54.875 8.194955 2.109816 3.873379 $parameters test name.t ntr alpha Scheffe N 2 0.05 $means yield std r Min Max Q25 Q50 Q75 0 52.06667 5.377957 12 44.2 62.8 48.30 52.35 55.625 1 57.68333 5.791347 12 48.8 69.5 54.85 57.85 60.350 $comparison NULL $groups yield groups 1 57.68333 a 0 52.06667 b 11.3 Tukey多重比较 It makes multiple comparisons of treatments by means of Tukey. The level by alpha default is 0.05. 代码: # Turkey (HSD.test(m9,"N")) 结果 > (HSD.test(m9,"N")) $statistics MSerror Df Mean CV MSD 20.22284 17 54.875 8.194955 3.873379 $parameters test name.t ntr StudentizedRange alpha Tukey N 2 2.98373 0.05 $means yield std r Min Max Q25 Q50 Q75 0 52.06667 5.377957 12 44.2 62.8 48.30 52.35 55.625 1 57.68333 5.791347 12 48.8 69.5 54.85 57.85 60.350 $comparison NULL $groups yield groups 1 57.68333 a 0 52.06667 b 11.4 SNK多重比较 SNK is derived from Tukey, but it is less conservative (finds more differences). Tukey controls the error for all comparisons, where SNK only controls for comparisons under consideration. The level by alpha default is 0.05. 代码: # SNK (SNK.test(m9,"N")) 结果 > (HSD.test(m9,"N")) $statistics MSerror Df Mean CV MSD 20.22284 17 54.875 8.194955 3.873379 $parameters test name.t ntr StudentizedRange alpha Tukey N 2 2.98373 0.05 $means yield std r Min Max Q25 Q50 Q75 0 52.06667 5.377957 12 44.2 62.8 48.30 52.35 55.625 1 57.68333 5.791347 12 48.8 69.5 54.85 57.85 60.350 $comparison NULL $groups yield groups 1 57.68333 a 0 52.06667 b 11.5 Duncan多重比较 This test is adapted from the Newman-Keuls method. Duncan’s test does not control family wise error rate at the specified alpha level. It has more power than the other post tests, but only because it doesn’t control the error rate properly. The Experimentwise Error Rate at: 1-(1-alpha)^(a-1); where “a” is the number of means and is the Per-Comparison Error Rate. Duncan’s procedure is only very slightly more conservative than LSD. The level by alpha default is 0.05. 代码: # Duncan (duncan.test(m9,"N")) 结果 > (duncan.test(m9,"N")) $statistics MSerror Df Mean CV 20.22284 17 54.875 8.194955 $parameters test name.t ntr alpha Duncan N 2 0.05 $duncan Table CriticalRange 2 2.98373 3.873379 $means yield std r Min Max Q25 Q50 Q75 0 52.06667 5.377957 12 44.2 62.8 48.30 52.35 55.625 1 57.68333 5.791347 12 48.8 69.5 54.85 57.85 60.350 $comparison NULL $groups yield groups 1 57.68333 a 0 52.06667 b 12. 获得所有代码及注释脚本 公众号回复(R-breeding):R语言方差分析。

优秀的个人博客,低调大师

Go语言学习14-基本流程控制

基本流程控制 Go语言在流程控制结构方面有些像C语言,但是在很多方面都与C不同。特点如下: 在Go语言中没有 do 和 while 循环,只有一个更加广义的 for 语句。 Go语言中的 switch 语句更加灵活多变。Go语言的 switch 语句还可以被用于进行类型判断。 与 for 语句类似,Go语言中的 if 语句和 switch 语句都可以接受一个可选的初始化子语句。 Go语言支持在 break 语句和 continue 语句之后跟一个可选的标记(Label)语句,以标识需要终止或继续的代码块。 Go语言中还有一个类似于多路转接器的 select 语句。 Go语言中的go语句可以被用于灵活地启用 Goroutine。 Go语言中的 defer 语句可以使我们更加方便地执行异常捕获和资源回收任务。 1. 代码块和作用域 代码块就是一个由花括号 “{” 和 “}” 括起来的若干表达式和语句的序列。代码块中也可以不包含任何内容,即为空代码块。 在Go语言的源代码中,除了显式的代码块之外,还有一些隐式的代码块,如下: 所有Go语言源代码形成了一个最大的代码块。这个最大的代码块也被称为全域代码块。 每一个代码包都是一个代码块,即代码包代码块。它们分别包含了当前代码包内的所有Go语言源代码。 每一个源码文件都是一个代码块,即源码文件代码块。它们分别包含了当前文件内的所有Go语言源码。 每一个 if 语句、for 语句、switch 语句和 select 语句都是一个代码块。 每一个在 switch 或 select 语句中的子句都是一个代码块。 在Go语言中,每一个标识符都有它的作用域。使用代码块表示词法上的作用域范围,规则如下: 一个预定义标识符的作用域是全域代码块。 代表了一个常量、类型、变量或函数(不包括方法)的,被声明在顶层的(即在任何函数声明之外被声明的)标识符的作用域是代码包代码块。 一个被导入的代码包的名称的作用域是包含该代码包导入语句的源码文件代码块。 一个代表了方法接收者、方法参数或方法结果的标识符的作用域是方法代码块。 对于一个代表了常量或变量的标识符,如果它被声明在函数内部,那么它的作用域总是包含它的声明的那个最内层的代码块。 对于一个代表了类型的标识符,如果它被声明在函数内部,那么它的作用域就是包含它的声明的那个最内层的代码块。 在Go语言中,可以在某个代码块中对一个已经在包含它的外层代码块中声明过的标识符进行重声明。这种情况下,在外层代码块中声明的那个同名标识符被屏蔽了。例如: package main import ( "fmt" ) var v string = "1, 2, 3" func main(){ v := []int{1, 2, 3} if v != nil { // 此时v代表的是一个切片类型值,因此可以与空值nil进行判等 var v int = 123 fmt.Printf("%v\n",v) } } 运行结果截图如下: 2. if 语句 Go语言的 if 语句总是以关键字 if 开始。之后,可以后跟一条简单语句(当然也可以没有),然后是一个作为条件判断的布尔类型的表达式以及一个用花括号 “{” 和 “}” 括起来的代码块。if 语句也可以由 else 分支,它是 else 关键字和一个用花括号 “{” 和 “}” 括起来的代码块。 常用的简单语句包括短变量声明、赋值语句和表达式语句。除了特殊的内建函数和代码包 unsafe 中的函数,针对其他函数和方法的调用表达式和针对通道类型的接收表达式都可以出现在语句上下文中。在必要时,还可以使用圆括号将它们括起来。其他的简单语句还包括发送语句、自增/自减语句和空语句。 Go语言 if 语句的举例: if 100 < number { number++ } else { number-- } 在上面的 if 语句的条件表达式 100 < number 并没有被圆括号括起来,这是Go语言的流程控制语句的特点之一。同时,强调一点是跟在条件表达式和 else 关键字之后的两个代码块必须由花括号 “{” 和 “}” 括起来,不论代码块中包含几条语句以及是否包含语句。 if 语句可以接受一条初始化子语句,常常用它来初始化一个变量如下: if diff := 100 – number; 100 < diff { // 初始化子句和条件表达式之间是需要用分号“;”分隔的 number++ } else if 200 < diff { number-- } else { number -= 2 } 由于在Go语言中一个函数可以返回多个结果,因此常常会把在函数执行期间出现的常规错误也作为结果之一。例如,标准库代码包 os 中的函数 Open,它的声明如下: func Open(name string) (file *File, err error) 函数 os.Open 返回的第一个结果是与已经被“打开”的文件相对应的 *File 类型的值,而第二个结果是代表了常规错误的 error 类型的值。error 是一个预定义的接口类型,所有实现它的类型都应该被用于描述一个常规错误。在导入代码包 os 之后,调用 Open 函数: f, err := os.Open(name) if err != nil { return err } 如上调用后,先检查 err 的值是否为 nil。如果变量 err 的值不为 nil,那么说明 os.Open 函数在被执行过程中发生了错误,这时变量 f 的的值肯定是不可用的。 在Go语言中,if 语句常被作为卫述语句。卫述语句是指被用来检查关键的先决条件的合法性并在检查未通过的情况下立即终止当前代码块的执行的语句。上面调用 Open 函数之后检查的 if 语句就是卫述语句的一种。 func update (id int, department string) bool { if id <= 0 { return false } // 省略若干语句 return true } // 在函数update开始处的那条if语句就属于卫述语句。 对函数稍加改造如下: func update (id int, department string) error{ // 需要事先导入标准库的代码包errors if id <= 0 { return errors.New("The id is INVALID!") } // 省略若干语句 return nil } // update函数返回的结果不但可以表示在函数执行期间是否发生了错误,而且还可以体现出错误的具体描述。 3. switch语句 语句 switch 可以使用表达式或者类型说明符作为 case 判定方法。switch 语句也就可以被分为两类:表达式switch语句 和 类型switch语句。 3.1 表达式switch语句 在表达式 switch 语句中,switch 表达式和 case 携带的表达式(也称为 case 表达式)都会被求值。对这些表达式的求值是自左向右、自上而下进行的。如果在 switch 语句中没有显示的switch 表达式,那么 true 将会被作为 switch 表达式。例如: switch content { default: fmt.Println("Unknown language") case "Python": fmt.Println("A interpreted language") case "Go": fmt.Println("A compiled language") } switch 关键字之后会紧跟一个 switch 表达式。switch 表达式中涉及的标识符都必须是已经被声明过的。同时还可以在 switch 和 switch 表达式之间插入一条简单语句,如下: switch content := getContent(); content { // content := getContent()会在switch表达式content被求值之前被执行 default: fmt.Println("Unknown language") case "Python": fmt.Println("A interpreted language") case "Go": fmt.Println("A compiled language") } 一条 case 语句由一个 case 表达式和一个语句列表组成,并且这两者之间需要用冒号 : 分隔。一个 case 表达式由一个 case 关键字和一个表达式列表(可以包含多个表达式)组成。例如: switch content := getContent(); content { default: fmt.Println("Unknown language") case "Python", "Ruby": fmt.Println("A interpreted language") case "Go", "C", "Java": fmt.Println("A compiled language") } 在一条 case 语句中的语句列表的最后一条语句可以是 fallthrough 语句。一条 fallthrough 语句会将流程控制权转移下一条 case 语句上。例如: switch content := getContent(); content { default: fmt.Println("Unknown language") case "Ruby": fallthrough case "Python": fmt.Println("A interpreted language") case "Go", "C", "Java": fmt.Println("A compiled language") } 如上当变量 content 的值与 "Ruby" 相等的时候,在标准输出上打印出的内容会是 A interpreted language。fallthrough 语句只能够作为 case 语句中的语句列表的最后一条语句, fallthrough 语句不能出现在最后一条 case 语句的语句列表中。 break 语句也可以出现在 case 语句列表中。一条 break 语句由一个 break 关键字和一个可选的标记组成,这两者之间用空格分隔。例如: switch content := getContent(); content { default: fmt.Println("Unknown language") case "Ruby": break case "Python": fmt.Println("A interpreted language") case "Go", "C", "Java": fmt.Println("A compiled language") } 3.2 类型switch语句 类型 switch 语句将对类型进行判定,而不是值。它的 switch 表达式的表现形式与类型断言表达式相似,但与类型断言表达式不同的是,它使用关键字 type 来充当欲判定的类型,而不是使用一个具体的类型字面量。例如: switch v.(type){ case string: fmt.Printf("The string is '%s'.\n", v.(string)) // v.(string)把v的值转换成了string类型的值 case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64: // v是byte类型或rune类型,也会执行下列分支 fmt.Printf("The string is %d.\n", v) default: fmt.Printf("Unsupported value. (type=%T)\n", v) } } 在类型 switch 语句中,case 表达式中的类型字面量可以是 nil,如果 v 的值是 nil,那么表达式 v.(type) 的结果值也会是 nil。与表达式 switch 语句不同的是,fallthrough 语句不允许出现在类型 switch 语句中。 对类型 switch 语句的 switch 表达式进行变形: switch i:= v.(type){ case string: fmt.Printf("The string is '%s'.\n", i) case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64: fmt.Printf("The integer is %d.\n", i) default: fmt.Printf("Unsupported value. (type=%T)\n", i) } } 第一个case语句相当于: case string: i := v.(string) fmt.Printf("The string is '%s'.\n", i) 对于包含多个类型字面量的 case 表达式,比如第二个 case 语句。例如,如果上面v的动态类型是 uint16 类型,那么第二个 case 语句相当于: case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64: i := v.(uint16) fmt.Printf("The integer is %d.\n", i) 如上通过这种方式后,不需要在每个 case 语句中分别对那个欲判定类型的值进行显示地类型转换了。 在 switch 表达式缺失的情况下,该 switch 语句的判定目标会被视为布尔类型 true,也就是所有 case 表达式的结果值都应该是布尔类型。例如: switch { case number < 100: number++ case number < 200: number-- default: number -= 2 } } 同样可以在 switch 关键字和 switch 表达式中添加一条简单语句,例如: switch number := 123; { // 这里switch表达式缺失,默认switch的判定目标为布尔类型 case number < 100: number++ case number < 200: number-- default: number -= 2 } } 4. for 语句 4.1 for 子句 for 子句的3个部分是由固定顺序组成,即初始化子句在左,条件在中,后置子句在右,并且在它们之间需要用分号“;”来分隔。可以在编写 for 子句的时候省略掉其中的任何部分,为了避免歧义,与省略部分相邻的分隔符“;”也必须被保留。初始化子句总会在充当条件的表达式被第一次求值之前执行,且只会执行一次,而后置子句的执行总会在每次代码块执行完之后紧接着进行。后置子句一定不能是短变量声明。例如: for i := 0; i < 100; i++ { number++ } var j uint = 1 for ; j%5 != 0; j *= 3 { // 省略初始化子句 number++ } for k != 1; k%5 != 0; { // 省略后置子句 k *= 3 number++ } 在 for 子句的初始化子句和后置子句同时被省略或者其中的所有部分都被省略的情况下,分隔符 ; 可以被省略。例如: // number是一个int类型的变量 for number < 200 { number += 2 } 当 for 子句的3个部分都省略,就陷入了死循环。例如: for { number++ } 4.2 range 子句 for 语句使用 range 子句可以迭代出一个数组或切片值中的每个元素,一个字符串值中的每个字符或者一个字典值中的每个键值对,甚至可以被用于持续接收一个通道类型值中的元素。例如: ints := []int{1, 2, 3, 4, 5} for i, d := range ints { fmt.Printf("%d: %d\n", i, d) } 事先声明了标识符,例如: var i, d int ints := []int{1, 2, 3, 4, 5} for i, d = range ints { fmt.Printf("%d: %d\n", i, d) } 运行截图如下: 4.3 range子句的迭代产出 range表达式的类型 第一个产出值 第二个产出值(若显示获取) 备注 a:[n]T、*[n]T或[]T i:int类型的元素索引值 与索引对应的元素的值a[i],类型为T a为range表达式的结果值,N为数组类型的长度,T为数组类型或切片类型的元素类型 s:string类型 i:int类型的元素索引值 与索引对应的元素的值s[i],类型为rune s为range表达式的结果值 m:map[K]T k:键值对中的键的值,类型为K 与键对应的元素值m[k],类型为T m为range表达式的结果值,K为字典类型的键的类型,V为字典类型的元素类型 c:chan T e: 元素的值,类型为T 无 c为range表达式的结果值,T为通道类型的元素类型 对于所有可迭代的数据类型的值来说,可以要求每次迭代只产出第一个迭代值。例如: m := map[uint]string{1: "A", 6: "C", 7: "B"} var maxKey uint for k := range m{ fmt.Printf("k: %d\n", k) if k > maxKey { maxKey = k } } fmt.Printf("maxKey: %d\n", maxKey) 运行截图如下: 忽略第一个迭代值而只使用第二个迭代值的方法,如下: m := map[uint]string{1: "A", 6: "C", 7: "B"} var values []string for _, v := range m { values = append(values, v) } fmt.Printf("values: %v\n", values) 运行截图如下: 在 for 语句中,可以使用 break 语句来终止 for 语句的执行。例如: // 该变量的值包含了某个网络的所有用户昵称及其重复次数 // 这个字典的键表示用户昵称,而值则代表了使用该昵称的用户数量。 var namesCount map[string]int = map[string]int{"霓虹": 3,"Huazie": 1, "Tom": 2, "诗": 4} // 存储查找到所有的只包含中文的用户昵称的计数信息。 targetsCount := make(map[string]int) for k, v := range namesCount { matched := true for _, r := range k { if r < '\u4e00' || r > '\u9fbf' { // 用户昵称中包含了非中文字符 matched = false break // 只会终止直接包含它的那条for语句的执行 } } if matched { targetsCount[k] = v } } fmt.Printf("targetsCount: %v\n", targetsCount) 运行截图如下: 4.4 标记语句 一条标记语句可以成为 goto 语句、break 语句或 continue 语句的目标。标记语句中的标记只是一个标识符,它可以被放置在任何语句的左边以作为这个语句的标签。标记和被标记的语句之间需要用冒号来分隔。例如: (1)break和标记语句的使用 var namesCount map[string]int = map[string]int{"霓虹": 3,"Huazie": 1, "Tom": 2, "诗": 4} targetsCount := make(map[string]int) L: for k, v := range namesCount { for _, r := range k { if r < '\u4e00' || r > '\u9fbf' { break L // 发现第一个非全中文的用户昵称的时候停止查找 } } targetsCount[k] = v } fmt.Printf("targetsCount: %v\n", targetsCount) 运行截图如下: 在Go语言中 continue 语句只能在 for 语句中被使用。continue 语句会使直接包含它的那个 for 循环直接进入下一次迭代。 (2)continue与标记语句的使用 var namesCount map[string]int = map[string]int{"霓虹": 3,"Huazie": 1, "Tom": 2, "诗": 4} targetsCount := make(map[string]int) L: for k, v := range namesCount { for _, r := range k { if r < '\u4e00' || r > '\u9fbf' { continue L // L标记代表的那个for循环直接进入下一次迭代 } } targetsCount[k] = v } fmt.Printf("targetsCount: %v\n", targetsCount) 运行截图如下: 使用Go语言的 for 语句写出反转一个切片类型值中的所有元素值的代码。(不使用 在 for 语句之外声明的任何变量作为辅助): var numbers []int = []int{1,2,3,4,5} fmt.Printf("before numbers: %v\n", numbers) for i, j := 0, len(numbers)-1; i < j; i, j = i+1, j-1 { numbers[i], numbers[j] = numbers[j], numbers[i] } fmt.Printf("after numbers: %v\n", numbers) 运行截图如下: 初始化子句 和 后置子句 的只能是单一语句而不能是多个语句,但是可以使用平行语句来丰富两个子句的语义。 5. goto 语句 一条 goto 语句会把流程控制权无条件地转移到它右边的标记所代表的语句上。goto 语句只能与标记语句连用,并且在它的右边必须要出现一个标记。 goto 语句使用的注意点: (1) 不允许因使用 goto 语句而使任何本不在当前作用域中的变量进入该作用域。例如: goto L v := "B" L: fmt.Printf("V: %v\n", v) 这段代码会造成一个编译错误,主要原因是语句 goto L 恰恰使变量 v 的声明语句被跳过了。 修改上面的代码,保证顺利通过编译,如下: v := "B" goto L L: fmt.Printf("V: %v\n", v) 把某条 goto 语句的直属代码块叫作代码块 A,而把该条 goto 语句右边的标记所指代的那条标记语句的直属代码块叫作代码块 B,那么只要代码块 B 不是代码块 A 的外层代码块,这条 goto 语句就是不合法的。例如: var n int = 10 if n % 3 != 0 { goto L1 } switch { case n % 7 == 0: fmt.Printf("%v is a common multiple of 7 and 3.\n", n) default: L1: fmt.Printf("%v isn't multiple of 3.\n", n) } 如上,标记 L1 所指代的标记语句的直属代码块是由 switch 语句代表的,而 goto L1 语句的直属代码块是由 if 语句代表的,并且前者并不是后者的直属代码块。因此,goto L1 是非法的。上面的代码会出现编译错误。修正上面的编译错误,代码如下: var n int = 10 if n % 3 != 0 { goto L1 } switch { case n % 7 == 0: fmt.Printf("%v is a common multiple of 7 and 3.\n", n) default: } L1: fmt.Printf("%v isn't multiple of 3.\n", n) 利用 goto 语句跳出嵌套的流程控制语句的执行。例如: // 查找name中的第一个非法字符并返回 // 如果返回的是空字符串说明name中不包含任何非法字符 func findEvildoer(name string) string{ var evildoer string for _, r := range name{ switch { case r >= '\u0041' && r <= '\u005a': // a-z case r >= '\u0061' && r <= '\u007a': // A-Z case r >= '\u4e00' && r <= '\u9fbf': // 中文字符 default: evildoer = string(r) goto L2 } } goto L3 L2: fmt.Printf("The first evildoer of name '%s' is '%s'!\n", name, evildoer) L3: return evildoer } 如下使用 break 和 if 语句替换上面的两条 goto 语句: func findEvildoer1(name string) string{ var evildoer string L2: for _, r := range name{ switch { case r >= '\u0041' && r <= '\u005a': // a-z case r >= '\u0061' && r <= '\u007a': // A-Z case r >= '\u4e00' && r <= '\u9fbf': // 中文字符 default: evildoer = string(r) break L2 } } if evildoer != "" { fmt.Printf("The first evildoer of name '%s' is '%s'!\n", name, evildoer) } return evildoer } (2) 另一个适合使用 goto 语句的场景是集中式的错误处理。例如: func checkValidity(name string) error{ var errDetail string for i, r := range name{ switch { case r >= '\u0041' && r <= '\u005a': // a-z case r >= '\u0061' && r <= '\u007a': // A-Z case r >= '\u4e00' && r <= '\u9fbf': // 中文字符 case r == '_' || r == '-' || r == '.': // 其他允许的符号 default: errDetail = "The name contains some illegal characters." goto L3 } if i == 0 { switch r { case '_': errDetail = "The name can not begin with a '_'." goto L3 case '-': errDetail = "The name can not begin with a '-'." goto L3 case '.': errDetail = "The name can not begin with a '.'." goto L3 } } } return nil L3: return errors.New("Validity check failure: "+errDetail) } Go语言可以方便地从错综复杂的流程控制中跳出,但是 goto 语句的代码块的可读性大大下降。因此,要节制地使用 goto 语句。 结语 本篇讲述了Go语言的基本流程控制,下篇继续讲解Go语言流程控制方法中的一些特殊流程控制语句。 最后附上知名的Go语言开源框架: Groupcache: 著名的内存缓存系统 Mencached 的作者用Go语言编写的一个与前者功能类似的函数库。作者想用它作为 Memcached 的 替代者。其源码:https://github.com/golang/groupcache

优秀的个人博客,低调大师

如何在评估机器学习模型时防止数据泄漏

本文讨论了评估模型性能时的数据泄漏问题以及避免数据泄漏的方法。 在模型评估过程中,当训练集的数据进入验证/测试集时,就会发生数据泄漏。这将导致模型对验证/测试集的性能评估存在偏差。让我们用一个使用Scikit-Learn的“波士顿房价”数据集的例子来理解它。数据集没有缺失值,因此随机引入100个缺失值,以便更好地演示数据泄漏。 import numpy as npimport pandas as pdfrom sklearn.datasets import load_bostonfrom sklearn.preprocessing import StandardScalerfrom sklearn.pipeline import Pipelinefrom sklearn.impute import SimpleImputerfrom sklearn.neighbors import KNeighborsRegressorfrom sklearn.model_selection import cross_validate, train_test_splitfrom sklearn.metrics import mean_squared_error#Importing the datasetdata = pd.DataFrame(load_boston()['data'],columns=load_boston()['feature_names'])data['target'] = load_boston()['target']#Split the input and target featuresX = data.iloc[:,:-1].copy()y = data.iloc[:,-1].copy()# Adding 100 random missing valuesnp.random.seed(11)rand_cols = np.random.randint(0,X.shape[1],100)rand_rows = np.random.randint(0,X.shape[0],100)for i,j in zip(rand_rows,rand_cols): X.iloc[i,j] = np.nan #Splitting the data into training and test setsX_train, X_test, y_train, y_test = train_test_split(X,y,test_size=0.2,random_state=11)#Initislizing KNN Regressorknn = KNeighborsRegressor()#Initializing mode imputerimp = SimpleImputer(strategy='most_frequent')#Initializing StandardScalerstandard_scaler = StandardScaler()#Imputing and scaling X_trainX_train_impute = imp.fit_transform(X_train).copy()X_train_scaled = standard_scaler.fit_transform(X_train_impute).copy()#Running 5-fold cross-validationcv = cross_validate(estimator=knn,X=X_train_scaled,y=y_train,cv=5,scoring="neg_root_mean_squared_error",return_train_score=True)#Calculating mean of the training scores of cross-validationprint(f'Training RMSE (with data leakage): {-1 * np.mean(cv["train_score"])}')#Calculating mean of the validation scores of cross-validationprint(f'validation RMSE (with data leakage): {-1 * np.mean(cv["test_score"])}')#fitting the model to the training datalr.fit(X_train_scaled,y_train)#preprocessing the test dataX_test_impute = imp.transform(X_test).copy()X_test_scaled = standard_scaler.transform(X_test_impute).copy()#Predictions and model evaluation on unseen datapred = lr.predict(X_test_scaled)print(f'RMSE on unseen data: {np.sqrt(mean_squared_error(y_test,pred))}') 在上面的代码中,‘X_train’是训练集(k-fold交叉验证),‘X_test’用于对看不见的数据进行模型评估。上面的代码是一个带有数据泄漏的模型评估示例,其中,用于估算缺失值的模式(strategy= ' most_frequent ')在' X_train '上计算。类似地,用于缩放数据的均值和标准偏差也使用' X_train '计算。' X_train的缺失值将被输入,' X_train '在k-fold交叉验证之前进行缩放。 在k-fold交叉验证中,' X_train '被分割成' k '折叠。在每次k-fold交叉验证迭代中,其中一个折用于验证(我们称其为验证部分),其余的折用于训练(我们称其为训练部分)。每次迭代中的训练和验证部分都有已经使用' X_train '计算的模式输入的缺失值。类似地,它们已经使用在' X_train '上计算的平均值和标准偏差进行了缩放。这种估算和缩放操作会导致来自' X_train '的信息泄露到k-fold交叉验证的训练和验证部分。这种信息泄漏可能导致模型在验证部分上的性能估计有偏差。下面的代码展示了一种通过使用管道来避免它的方法。 #Preprocessing and regressor pipelinepipeline = Pipeline(steps=[['imputer',imp],['scaler',standard_scaler],['regressor',knn]])#Running 5-fold cross-validation using pipeline as estimatorcv = cross_validate(estimator=pipeline,X=X_train,y=y_train,cv=5,scoring="neg_root_mean_squared_error",return_train_score=True)#Calculating mean of the training scores of cross-validationprint(f'Training RMSE (without data leakage): {-1 * np.mean(cv["train_score"])}')#Calculating mean of the validation scores of cross-validationprint(f'validation RMSE (without data leakage): {-1 * np.mean(cv["test_score"])}')#fitting the pipeline to the training datapipeline.fit(X_train,y_train) #Predictions and model evaluation on unseen datapred = pipeline.predict(X_test)print(f'RMSE on unseen data: {np.sqrt(mean_squared_error(y_test,pred))}') 在上面的代码中,我们已经在管道中包含了输入器、标量和回归器。在本例中,' X_train '被分割为5个折,在每次迭代中,管道使用训练部分计算用于输入训练和验证部分中缺失值的模式。同样,用于衡量训练和验证部分的平均值和标准偏差也在训练部分上计算。这一过程消除了数据泄漏,因为在每次k-fold交叉验证迭代中,都在训练部分计算归责模式和缩放的均值和标准偏差。在每次k-fold交叉验证迭代中,这些值用于计算和扩展训练和验证部分。 我们可以看到在有数据泄漏和没有数据泄漏的情况下计算的训练和验证rmse的差异。由于数据集很小,我们只能看到它们之间的微小差异。在大数据集的情况下,这个差异可能会很大。对于看不见的数据,验证RMSE(带有数据泄漏)接近RMSE只是偶然的。 因此,使用管道进行k-fold交叉验证可以防止数据泄漏,并更好地评估模型在不可见数据上的性能。 作者:KSV Muralidhar 原文地址:https://ksvmuralidhar.medium.com/how-to-avoid-data-leakage-while-evaluating-the-performance-of-a-machine-learning-model-ac30f2bb8586 deephub翻译组 本文分享自微信公众号 - DeepHub IMBA(deephub-imba)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

Vue - 用小白的学习方式去掌握 Vuex 使用

文章目录 Vuex 是什么 状态管理是什么? 响应式是什么? 不用 Vuex 也是可以的 使用 安装 知识储备 全局 store State computed 计算属性 mapState 辅助函数 Getters Mutations Actions Modules 购物车示例 Vuex 是什么 Vuex是什么 一个专为 Vue.js 应用程序开发的响应式状态管理模式. 状态管理是什么? 从我个人角度来看,状态可以指代数据。而状态管理也就可以看作数据管理,主要是用于分层解耦。 在Vuex中状态管理可以看作是全局变量,这个变量可以通过state取出变量映射到view中,也可以根据用户的输入actions改变该变量 响应式是什么? 从我个人角度来看,响应式就是自适应。 在Vuex中响应式是指数据(状态)的改变能够及时的所有数据变更为最新的数据。 不用 Vuex 也是可以的 从父组件一层一层将数据传递给子组件(麻烦) 通过 EventBus 的订阅/发布模式实现数据数据传递 可看这篇文章介绍:什么情况下我应该使用 Vuex 使用 安装 npm 安装 npm install vuex --save 知识储备 全局 store 参考:shoppint-cart 示例 在 store 目录下创建index.js import Vue from 'vue' import Vuex from 'vuex' import cart from './modules/cart' import products from './modules/products' import createLogger from '../../../src/plugins/logger' Vue.use(Vuex) const debug = process.env.NODE_ENV !== 'production' export default new Vuex.Store({ // 注入其它模块的 store modules: { cart, products }, strict: debug, plugins: debug ? [createLogger()] : [] }) 在app.js中注入store,如: import Vue from 'vue' import App from './components/App.vue' import store from './store' import { currency } from './currency' Vue.filter('currency', currency) new Vue({ el: '#app', store, // 全局注入 render: h => h(App) }) 如在counter组件中通过this.$store去使用 this.$store.commit('increment') State 用于声明数据。 computed 计算属性 对于任何复杂逻辑,你都应当使用计算属性。 例如下面的示例中,第二个 message ,如果我们要在 view 中写也是可以,但是会比较复杂,如下: <p>Computed reversed message: "{ { message.split('').reverse().join('') }}"</p> 那么我们可以将该复杂写法通过计算属性封装成一个方法,直接调用该方法对象即可。 示例: <div id="example"> <p>Original message: "{ { message }}"</p> <p>Computed reversed message: "{ { reversedMessage }}"</p> <!-- 在 Vuex 中如果不用计算属性,我们需要这么写 --> <p>Computed reversed message: "{ { this.$store.state.counter.message.split('').reverse().join('') }}"</p> </div> var vm = new Vue({ el: '#example', data: { message: 'Hello' }, computed: { // 计算属性的 getter reversedMessage: function () { // `this` 指向 vm 实例 // 使用 vuex 则可通过 $store 取到数据之后再转换,如下 // return this.$store.state.counter.message.split('').reverse().join('') return this.message.split('').reverse().join('') } } }) 输出结果: Original message: "Hello" Computed reversed message: "olleH" mapState 辅助函数 当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性。 辅助函数,用于简化this.$store.state,示例如下: <template> <div> <!-- 直接使用 --> Clicked: { { $store.state.counter.count }} times. <br> <!-- 使用 Vuex mapState --> mapState使用: { { count }} times. </div> </template> <script> // 在单独构建的版本中辅助函数为 Vuex.mapState import { mapState } from 'vuex' export default { computed:{ ...mapState({ count: state => state.counter.count }), }, methods: { } } </script> 有时候我们需要从 store 中的 state 中派生出一些状态。 如上面的字符串反转功能就是派生出来的,我们可以使用Getters去实现。 Getters 可以认为是 store 的计算属性。getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。 通过一个示例来看一下getters如何使用。 Counter.vue文件如下: <template> <div> <p>hello 字符串反转:{ { reverseStr }}</p> <p>getter 获取 todo done: { { doneTodosCount }}</p> <p>传入 getters 做参数:{ { doneTodosCount }}</p> <p>直接访问:{ { doneTodos }}</p> </div> </template> <script> // 在单独构建的版本中辅助函数为 Vuex.mapGetters import { mapGetters } from "vuex"; export default { computed: { // 由于使用了命名空间,因此需要加上,否则获取反转的字符串如: reverseStr:'counter/reverseStr' ...mapGetters('counter',{ reverseStr: "reverseStr", doneTodosCount: "doneTodosCount", doneTodos: "doneTodos", }), }, }; </script> counter.js文件如下: const state = () => ({ message: 'hello', todos: [{ id: 1, text: '写日报', done: true }, { id: 2, text: '看一篇英文文章', done: false } ] }) // getters const getters = { doneTodos: state => { return state.todos.filter(todo => todo.done) }, doneTodosCount: (state, getters) => { // 传入 getters 做参数 return getters.doneTodos.length }, reverseStr: state => { return state.message.split('').reverse().join('') } } export default { namespaced: true, state, getters, } Mutations 通过提交 mutation 的方式,而非直接改变 store.state.count。 其实也就是通过方法的方式去操作数据。而且Mutation 必须是同步函数,如果是异步函数请使用下一节的Action 看个示例:每次点击increment文本+2,每次点击decrement文本-2 Counter.vue文件如下 <template> <div> <p @click="mIncrement">mutations 获取 increment: { { mutationsCount }}</p> <p @click="mDecrement">mutations 获取 decrement: { { mutationsCount }}</p> </div> </template> <script> // 在单独构建的版本中辅助函数为 Vuex.mapState import { mapState } from "vuex"; export default { computed: { ...mapState({ count: (state) => state.counter.count, mutationsCount: (state) => state.counter.mutationsCount, }), }, methods: { mIncrement() { this.$store.commit("counter/mIncrement",2); }, mDecrement() { this.$store.commit("counter/mDecrement",{ amount:2 }); }, }, }; </script> counter.js如下: const state = () => ({ mutationsCount:0, }) const mutations = { mIncrement(state, n) { state.mutationsCount += n }, // 通过对象传递 mDecrement(state,payload) { state.mutationsCount -= payload.amount }, } export default { namespaced: true, state, mutations } Actions Action 类似于 mutation,不同在于: Action 提交的是 mutation,而不是直接变更状态。 Action 可以包含任意异步操作。 counter计数器示例: Counter.vue文件如下: <template> <div> Clicked: { { $store.state.counter.count }} times. <br /> <button @click="increment">+</button> <button @click="decrement">-</button> </div> </template> <script> // 在单独构建的版本中辅助函数为 Vuex.mapState import { mapState, mapGetters } from "vuex"; export default { computed: { ...mapState({ count: (state) => state.counter.count, }), }, methods: { increment() { this.$store.dispatch("counter/increment"); }, decrement() { this.$store.dispatch("counter/decrement"); }, }, }; </script> counter.js文件如下: const state = () => ({ count: 0, }) const mutations = { increment(state) { state.count++ }, decrement(state) { state.count-- }, } const actions = { increment: ({ commit }) => commit('increment'), decrement: ({ commit }) => commit('decrement'), } export default { namespaced: true, state, actions, mutations } Modules 也就是分模块。 例如Counter组件,在 store/modules下创建counter.js用于处理store相关的数据,而Counter.vue组件页面就正常写即可。 接着在store/目录下的 index.js添加 module的注入。具体可看上文的store全局注入。 购物车示例 对照上面的分析,你可以很容易看懂Vuex examples 中的 shoppint-cart示例了 shoppint-cart 示例地址 该示例是通过shop.js模拟数据,然后将products 和 cart组件注入全局 store 然后ProductList.vue展示shop.js中的商品列表,而ShoppingCart.vue展示购物车数据。 END~ 本文同步分享在 博客“_龙衣”(CSDN)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

MySQL索引-(B-Tree和B+Tree学习)

B-tree B树的出现是为了弥合不同的存储级别之间的访问速度上的巨大差异,实现高效的 I/O。平衡二叉树的查找效率是非常高的,并可以通过降低树的深度来提高查找的效率。但是当数据量非常大,树的存储的元素数量是有限的,这样会导致二叉查找树结构由于树的深度过大而造成磁盘I/O读写过于频繁,进而导致查询效率低下。另外数据量过大会导致内存空间不够容纳平衡二叉树所有节点的情况。B树是解决这个问题的很好的结构 B树又称多路平衡查找树,B树中所有节点的孩子节点数的最大值称为B树的阶。 一颗m阶的B树定义如下: 每个节点最多有m个子树 每个节点最多有m-1个关键字 根节点最少有1个关键字 非根节点至少有Math.ceil(m/2)-1个关键字 所有叶子节点都位于同一层,或者说根节点到每个叶子节点的长度都相同 每个节点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它 什么是B树的阶 B树中一个节点的子节点数目的最大值,用m表示,假如最大值为10,则为10阶,如图: 所有节点中,节点[13,16,19]拥有的子节点数目最多(4个子节点),所有可以定义上面的图片为4阶B树。 B-tree插入 针对m阶高度h的B树,插入一个元素是,首先在B树中是否存在,如果不存在,即在叶子节点处结束,然后再叶子节点中插入该新的元素。 插入规则: 若该节点元素个数小于m-1,直接插入 若该节点元素个数等于m-1,引起节点分裂;以该节点中间元素为分界,取中间元素(偶数个数,中间两个随机选取)插入到父节点中; 重复上面动作,直到所有节点符合B树的规则;最坏的情况一直分裂到根节点,生成新的根节点,高度增加1; 上面的规则是插入动作的核心,接下来以5阶树为例,详细讲解插入的动作: 下面演示下列数字的插入5阶B-tree树步骤(观看网站): 插入 1 ,3,7,14 如下图: **插入8时,**此时根节点元素个数为5,不符合1<=根节点元素个数<=m-1,提取中间节点7进行分裂。 插入5,11,17时不需要任何分裂操作 **插入元素 13 ,**由于13大于根节点7,向右子节点添加,又由于右节点已经有8,11,14,17四个节点,添加13就要进行分裂。提取中间元素13,插入到父节点中,插入过程如下图: 插入元素6,12,20,23时,不需要进行分裂 **插入26时,**26大于13应该添加到最右的叶子节点中,由于最右的叶子节点【14,17,20,23】空间已经满了,要进行分裂。 **插入4时,**导致最左边叶子节点进行分裂 【1,3,5,6】 插入24,25时不进行分裂 **插入21时,**含有【23,24,25,26】的节点需要分裂,把中间元素24上移到父节点中,但是父节点中空间已经满了,所以也要进行分裂,将父节点的中间元素【13】上移到形成新的根节点。 插入元素27不进行任何分裂 插入元素15,16不进行任何分裂 插入元素18分裂 总结B-tree为: B-tree删除 首先查找B树中需删除的元素,如果该元素在B树中存在,则将该元素在其节点中进行删除; 删除步骤如下: 删除该元素后,首先判断该元素是否有左右孩子节点,如果有,则上移孩子节点中的某相近元素(“左孩子最右边的节点”或“右孩子最左边的节点”)到父节点中(左右孩子节点都可以,如果上移左孩子节点发现左节点个数少于Math.ceil(m/2)-1则上移右孩子节点),然后是移动之后的情况;如果没有,直接删除。 某节点中元素数目小于(m/2)-1,(m/2)向上取整,则需要看其某相邻兄弟节点是否丰满; 如果丰满(节点中元素个数大于(m/2)-1),则向父节点借一个元素来满足条件; 如果其相邻兄弟都不丰满,即其节点数目等于(m/2)-1,则该节点与其相邻的某一兄弟节点进行“合并”成一个节点。合并步骤:首先移动父节点中的元素(该元素在两个需要合并的两个节点元素之间)下移到其子节点中,然后将这两个节点进行合并并成一个节点。; 下面我们以上5阶B树为例来看一下删除的动作: 【1,3,7,14,8,5,11,17,13,6,12,20,23,26,4,24,25,21,27,15,16,18】 关键要领,元素个数小于2 【(m-1)/2-1】就合并,大于 4 【m-1】就分裂 **删除元素8,**首先查找8的位置,发现8在【8,11,12】叶子节点处,删除8后该叶子节点元素个数为2,符合B树规则,直接删除。 **删除元素24,**因为24在中间节点中,删除24之后,左孩子节点23上移,发现左孩子节点只剩一个元素(小于两个元素),而右孩子节点个数为3(大于ceil(5/2)-1),上移右孩子25,此时左右孩子节点都剩两个元素(不小于 ceil(5/2)-1),删除完成 **删除元素25,**删除25节点之后,左孩子节点23上移,发现左孩子节点只剩一个元素(小于ceil(5/2)-1),而右孩子节点个数为2(不大于ceil(5/2)-1)。则进行合并。 **删除元素5,**因为5所在的节点数目刚好满足(ceil(5/2)-1),而相邻的兄弟节点个数都为2(等于ceil(5/2)-1);则进行合并,合并之后如下图: 但是此时删除操作还没哟结束,此时元素7所在的节点个数为一(不符合非根节点元素K必须满足于ceil(5/2)-1=<K<=m-1 也就是2<=k<=4的定义);如果这个问题节点的相邻兄弟比较丰满,则可以向父节点借一个元素,但此时兄弟节点元素个数刚好为两个,只能进行合并。而根结点中的唯一元素【13】下移到子结点,这样,树的高度减少一层。 B+tree B+tree是B-tree的变种,有着比B-tree更高的查询性能: 有m个子树的节点包含有m个元素(B-tree是m-1) 只有叶子结点保存数据,其他节点都只用于索引 根节点和所有分支节点都同时存在于叶子结点中,在子节点元素中是最大或者最小的 叶子节点会包含所有的关键字,以及指向数据记录的指针,并且叶子节点本身是根据关键字的从小到大顺序链接 B+tree插入 B+tree的插入必须保证插入后叶子节点中的记录依然排序,同时需要考虑插入到B+tree的三种情况,每种情况都可能会导致不同的插入算法 。 B+Tree演示地址 依次插入【5,10,25,30,50,55,75,80,85,90,15,20,60,65】 最后结果为下图: **插入70,**此时原先的Leaf Page已经满了,但是Index Page还没有满,符合上表中的第二种情况,这时插入Leaf Page后的情况为 【50,55,60,65,70】,并根据中间的60来拆分叶子节点。 **插入95,**这时符合上表中第三种情况,即Leaf Page和Index Page都满了,这时需要做两次拆分。 最终结构如下图: 可以看到,不管怎么变化,B+tree总是会保持平衡。但是为了保持平衡对于新插入的键值可能需要做大量的拆分页操作。 因为B+tree树结构主要用于磁盘,也的拆分意味着磁盘的操作,所以应该在有可能的情况下尽量减少也的拆分操作。因此,B+树同样提供了类似于平衡二叉树的旋转(Rotation)功能。旋转发生在Leaf Page已经满,但是其左右兄弟节点没有满的情况下。这时B+tree并不急于去做拆分也的操作,而是将记录移到所在页的兄弟节点上。因此再来看插入70的情况,若插入70,其实B+tree并不急于去拆分叶子节点,而是去做旋转操作,得到如下图: 插入70前 做拆分 做旋转 B+tree删除操作 B+tree树使用填充因子(fill factor)来控制树的删除变化。50%即(m/2)是填充因子可设的最小值。 **删除元素70,**该记录符合上表中第一种情况,可以直接删除 **删除55,**首先【50,55】节点只剩50,需要和【25,30】节点进行合并(合并规则和B-tree一样)。此时图如下: 这个时候删除还没有结束,由于【25】节点只剩一个元素,需要和右节点进行合并。最终过程如下: 删除80,此时符合上表中第二种情况: 最终如下图:

优秀的个人博客,低调大师

讲真 这次绝对让你轻松学习线程池

在这里插入图片描述 5分钟了解线程池 老王 是个深耕在帝都的一线码农,辛苦一年挣了点钱,想把钱存储到银行卡里,钱银行卡办理遇到了如下的遭遇 老王 银行门口取号后发现有柜台营业但是没人办理业务 直接办理了。 老王 取号后发现柜台都有人在办理,等待席有空地,去 坐着等办理去了。 老王 取号后发现柜台都有人办理,等待席也人坐满了,这个时候银行经理看到小麦是老实人本着关爱老实人的态度,新开一个 临时窗口给他办理了。 老王 取号后发现柜台都满了,等待座位席也满了, 临时窗口也人满了。这个时候银行经理给出了若干 解决策略。 直接告知人太多不给你办理了。 看到 老王 就来气,也不给不办理也不让他走。 经理让 老王 取尝试跟座位席中最前面的人聊一聊看是否可以加塞,可以就办理,不可以还是被踢走。 经理直接跟 老王 说谁让你来的你找谁去我这办理不了。 上面的这个流程几乎就跟JDK线程池的大致流程类似, 营业中的3个窗口对应核心线程池数:corePoolSize 银行总的营业窗口数对应:maximumPoolSize 打开的临时窗口在多少时间内无人办理则关闭对应:unit 银行里的等待座椅就是等待队列:workQueue 无法办理的时候银行给出的解决方法对应:RejectedExecutionHandler threadFactory 该参数在JDK中是 线程工厂,用来创建线程对象,一般不会动。 5分钟线程池的核心工作流程讲解完毕,更细节的知识看下面。 什么是线程池 简单理解就是 预先创建好一定数量的线程对象,存入缓冲池中,需要用的时候直接从缓冲池中取出,用完之后不要销毁,还回到缓冲池中。 线程池存在必要性 提高线程的利用率,降低资源的消耗。 提高响应速度,线程的创建时间为T1,执行时间T2,销毁时间T3,用线程池可以免去T1和T3的时间。 便于统一管理线程对象 可控制最大并发数 手动实现 如果先不看线程池源码让我们自己手动实现一个线程池你可以考虑到几个重要点? 有若干个初始化好的线程数组来充当线程池。 线程池要去一个 等待的任务队列 中去拿任务。 简单来说就是初始化N个线程充当线程池然后一起去阻塞队列中进行阻塞take,新添加的任务都通过put将任务追加到任务队列,关于任务队列的讲解看这blog 核心类 publicclassMyThreadPool2{//线程池中默认线程的个数为5privatestaticintWORK_NUM=5;//队列默认任务个数为100来不及保存任务privatestaticintTASK_COUNT=100;//工作线程组privateWorkThread[]workThreads;//任务队列,作为一个缓冲privatefinalBlockingQueue<Runnable>taskQueue;//用户在构造这个池,希望的启动的线程数privatefinalintworker_num;//创建具有默认线程个数的线程池publicMyThreadPool2(){this(WORK_NUM,TASK_COUNT);}//创建线程池,worker_num为线程池中工作线程的个数publicMyThreadPool2(intworker_num,inttaskCount){if(worker_num<=0)worker_num=WORK_NUM;if(taskCount<=0)taskCount=TASK_COUNT;this.worker_num=worker_num;taskQueue=newArrayBlockingQueue<>(taskCount);workThreads=newWorkThread[worker_num];for(inti=0;i<worker_num;i++){workThreads[i]=newWorkThread();workThreads[i].start();}Runtime.getRuntime().availableProcessors();}//执行任务,其实只是把任务加入任务队列,什么时候执行有线程池管理器决定publicvoidexecute(Runnabletask){try{taskQueue.put(task);//阻塞放置任务}catch(InterruptedExceptione){e.printStackTrace();}}//销毁线程池,该方法保证在所有任务都完成的情况下才销毁所有线程,否则等待任务完成才销毁publicvoiddestroy(){//工作线程停止工作,且置为nullSystem.out.println("准备关闭线程池");for(inti=0;i<worker_num;i++){workThreads[i].stopWorker();workThreads[i]=null;//helpgc}taskQueue.clear();//清空任务队列}//覆盖toString方法,返回线程池信息:工作线程个数和已完成任务个数@OverridepublicStringtoString(){return"线程池大小:"+worker_num+"等待执行任务个数:"+taskQueue.size();}//内部类,工作线程privateclassWorkThreadextendsThread{@Overridepublicvoidrun(){Runnabler=null;try{while(!isInterrupted()){r=taskQueue.take();//阻塞获得任务if(r!=null){System.out.println(getId()+"准备执行:"+r);r.run();}r=null;//helpgc;}}catch(Exceptione){//TODO:handleexception}}publicvoidstopWorker(){interrupt();}}} 测试类 publicclassTestMyThreadPool{publicstaticvoidmain(String[]args)throwsInterruptedException{//创建3个线程的线程池MyThreadPool2t=newMyThreadPool2(3,5);t.execute(newMyTask("testA"));t.execute(newMyTask("testB"));t.execute(newMyTask("testC"));t.execute(newMyTask("testD"));t.execute(newMyTask("testE"));System.out.println(t);Thread.sleep(10000);t.destroy();//所有线程都执行完成才destorySystem.out.println(t);}//任务类staticclassMyTaskimplementsRunnable{privateStringname;privateRandomr=newRandom();publicMyTask(Stringname){this.name=name;}publicStringgetName(){returnname;}@Overridepublicvoidrun(){//执行任务try{Thread.sleep(r.nextInt(1000)+2000);//随机休眠}catch(InterruptedExceptione){System.out.println(Thread.currentThread().getId()+"被打断:"+Thread.currentThread().isInterrupted());}System.out.println("任务"+name+"完成");}}} ThreadPoolExecutor ThreadPoolExecutor是JDK中所有线程池实现类的父类,构造函数有多个入参通过灵活的组合来实现线程池的初始化,核心构造函数如下。 publicThreadPoolExecutor(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,BlockingQueue<Runnable>workQueue,ThreadFactorythreadFactory,RejectedExecutionHandlerhandler) 重要参数解析 corePoolSize 此值是用来初始化线程池中核心线程数,当线程池中线程池数< corePoolSize时,系统默认是添加一个任务才创建一个线程池。可以通过调用prestartAllCoreThreads方法一次性的启动corePoolSize个数的线程。当线程数 = corePoolSize时,新任务会追加到workQueue中。 maximumPoolSize maximumPoolSize表示允许的最大线程数 = (非核心线程数+核心线程数),当BlockingQueue也满了,但线程池中总线程数 < maximumPoolSize时候就会再次创建新的线程。 keepAliveTime 非核心线程 =(maximumPoolSize - corePoolSize ) ,非核心线程闲置下来不干活最多存活时间。 unit 线程池中非核心线程保持存活的时间 TimeUnit.DAYS; 天 TimeUnit.HOURS; 小时 TimeUnit.MINUTES; 分钟 TimeUnit.SECONDS; 秒 TimeUnit.MILLISECONDS; 毫秒 TimeUnit.MICROSECONDS; 微秒 TimeUnit.NANOSECONDS; 纳秒 workQueue 线程池 等待队列,维护着等待执行的Runnable对象。当运行当线程数= corePoolSize时,新的任务会被添加到workQueue中,如果workQueue也满了则尝试用非核心线程执行任务,另外等待队列尽量用有界的哦!! threadFactory 创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等。 handler corePoolSize、workQueue、maximumPoolSize都不可用的时候执行的 饱和策略。 AbortPolicy :直接抛出异常,默认用此 CallerRunsPolicy:用调用者所在的线程来执行任务 DiscardOldestPolicy:丢弃阻塞队列里最老的任务,队列里最靠前的任务 DiscardPolicy :当前任务直接丢弃 想实现自己的饱和策略,实现RejectedExecutionHandler接口即可 形象流程图如下: 提交 execute 不需要返回 //核心思想跟上面的流程图类似publicvoidexecute(Runnablecommand){if(command==null)//规范性检查thrownewNullPointerException();intc=ctl.get();//当前工作的线程数跟线程状态ctl=AtomicIntegerCAS级别if(workerCountOf(c)<corePoolSize){if(addWorker(command,true))//如果当前线程池中工作线程数小于核心线程数,直接添加任务然后returnreturn;c=ctl.get();//添加失败了重新获得线程池中工作线程数}if(isRunning(c)&&workQueue.offer(command)){//线程池状态是否处于可用,可用就尝试将线程添加到queueintrecheck=ctl.get();//获得线程池状态if(!isRunning(recheck)&&remove(command))reject(command);//如果线程状态不在运行中则remove该任务elseif(workerCountOf(recheck)==0)addWorker(null,false);}elseif(!addWorker(command,false))//尝试将任务用非核心线程执行,reject(command);//失败了则执行失败策略。} submit 需要返回值 ThreadPoolExecutor extends AbstractExecutorService父类中存在一个submit方法, public<T>Future<T>submit(Callable<T>task){if(task==null)thrownewNullPointerException();RunnableFuture<T>ftask=newTaskFor(task);execute(ftask);returnftask;} 关闭线程池 注意线程之间是协作式的哦,所以的关闭只是发出关闭指令。 shutdown() 将线程池状态置为shutdown,并不会立即停止: 停止接收外部submit的任务 内部正在跑的任务和队列里等待的任务,会执行完 等到第二步完成后,才真正停止 shutdownNow() 将线程池状态置为stop。企图立即停止,事实上不一定: 跟shutdown()一样,先停止接收外部提交的任务 忽略队列里等待的任务 尝试将正在跑的任务interrupt中断 返回未执行的任务列表 shutdown 跟shutdownnow简单来说区别如下: shutdownNow()能立即停止线程池,正在跑的和正在等待的任务都停下了。这样做立即生效,但是风险也比较大。shutdown()只是关闭了提交通道,用submit()是无效的;而内部该怎么跑还是怎么跑,跑完再停。 awaitTermination pool.showdown()booleanb=pool.awaitTermination(3,TimeUnit.SECONDS) awaitTermination有两个参数,一个是timeout即超时时间,另一个是unit即时间单位。这个方法会使线程等待timeout时长,当超过timeout时间后,会监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。一般情况下会和shutdown方法组合使用,调用后当前线程会阻塞,直到 等所有已提交的任务(包括正在跑的和队列中等待的)执行完 或者等超时时间到 或者线程被中断,抛出InterruptedException 总结 优雅的关闭,用shutdown() 想立马关闭,并得到未执行任务列表,用shutdownNow() 优雅的关闭,发出关闭指令后看下是否真的关闭了用awaitTermination()。 合理配置线程池 线程在Java中属于稀缺资源,线程池不是越大越好也不是越小越好。任务分为计算密集型、IO密集型、混合型。 计算密集型:大部分都在用CPU跟内存,加密,逻辑操作业务处理等。 IO密集型:数据库链接,网络通讯传输等。 计算密集型一般推荐线程池不要过大,一般是CPU数 + 1,+1是因为可能存在 页缺失(就是可能存在有些数据在硬盘中需要多来一个线程将数据读入内存)。如果线程池数太大,可能会频繁的 进行线程上下文切换跟任务调度。获得当前CPU核心数代码如下: Runtime.getRuntime().availableProcessors(); IO密集型:线程数适当大一点,机器的Cpu核心数*2。 混合型:如果密集型站大头则拆分的必要性不大,如果IO型占据不少有必要,Mark 下。 常见线程池 每个线程池都是一个实现了接口ExecutorService并且继承自ThreadPoolExecutor的具体实现类,这些类的创建统一由一个工厂类Executors来提供对外创建接口。Executors框架图如下: ThreadPoolExecutor中一个线程就是一个Worker对象,它与一个线程绑定,当Worker执行完毕就是线程执行完毕。而Worker带了锁AQS,根据我后面准备写的读写锁的例子,发现线程池是线程安全的。看看图二的类图。下面简单介绍几个常用的线程池模式。 FixedThreadPool 定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程。 使用的 无界的等待队列是 LinkedBlockingQueue。使用时候小心堵满等待队列。 SingleThreadPool 只有一条线程来执行任务,适用于有顺序的任务的应用场景,也是用的无界等待队列 CachedThreadPool 可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。任务队列用的是SynchronousQueue如果生产多快消费慢,则会导致创建很多线程需注意。 WorkStealingPool JDK7以后 基于ForkJoinPool实现。PS:其中FixedThreadPool、SingleThreadPool、CachedThreadPool都用的无界等待队列,因此实际工作中都不建议这样做的哦,阿里巴巴Java编程规范建议如下:最后来个简单的线程使用demo: publicclassUseThreadPool{//工作线程staticclassWorkerimplementsRunnable{privateStringtaskName;privateRandomr=newRandom();publicWorker(StringtaskName){this.taskName=taskName;}publicStringgetName(){returntaskName;}@Overridepublicvoidrun(){System.out.println(Thread.currentThread().getName()+"当前任务:"+taskName);try{TimeUnit.MILLISECONDS.sleep(r.nextInt(100)*5);}catch(Exceptione){e.printStackTrace();}}}staticclassCallWorkerimplementsCallable<String>{privateStringtaskName;privateRandomr=newRandom();publicCallWorker(StringtaskName){this.taskName=taskName;}publicStringgetName(){returntaskName;}@OverridepublicStringcall()throwsException{System.out.println(Thread.currentThread().getName()+"当前任务:"+taskName);returnThread.currentThread().getName()+":"+r.nextInt(100)*5;}}publicstaticvoidmain(String[]args)throwsInterruptedException,ExecutionException{ExecutorServicepool=newThreadPoolExecutor(2,4,3,TimeUnit.SECONDS,newArrayBlockingQueue<Runnable>(10),newThreadPoolExecutor.DiscardOldestPolicy());//ExecutorServicepool=Executors.newCachedThreadPool();for(inti=0;i<5;i++){Workerworker=newWorker("Runnable_"+i);pool.execute(worker);}for(inti=0;i<5;i++){CallWorkercallWorker=newCallWorker("CallWorker_"+i);Future<String>result=pool.submit(callWorker);System.out.println(result.get());}pool.shutdown();}} ScheduledThreadPoolExecutor 周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。看构造函数:调用的还是ThreadPoolExecutor构造函数,区别不同点在于任务队列是用的DelayedWorkQueue,没什么新奇的了。 核心函数讲解: schedule 只执行一次,任务还可以延时执行,传入待执行任务跟延时时间。 scheduleAtFixedRate 提交固定时间间隔的任务,提交任务,延时时间,已经循环时间间隔时间。这个的含义是只是在固定的时间间隔尝试运行该任务。 scheduleWithFixedDelay 提交固定延时间隔执行的任务。上一个任务执行完毕后等多久再执行下个任务,这个中间时间叫 FixedDelay 其中 scheduleAtFixedRate 跟 scheduleWithFixedDelay区别如下图 scheduleAtFixedRate任务超时状态,比如我们设定60s执行一次,其中第一个任务时长 80s,第二个任务20s,第三个任务 50s。 第一个任务第0秒开始,第80s结束. 第二个任务第80s开始,在第100s结束. 第三个任务第120s秒开始,170s结束. 第四个任务从180s开始. 简单Mark个循环任务demo: classScheduleWorkerimplementsRunnable{publicfinalstaticintNormal=0;//普通任务类型publicfinalstaticintHasException=-1;//会抛出异常的任务类型publicfinalstaticintProcessException=1;//抛出异常但会捕捉的任务类型publicstaticSimpleDateFormatformater=newSimpleDateFormat("yyyy-MM-ddHH:mm:ss");privateinttaskType;publicScheduleWorker(inttaskType){this.taskType=taskType;}@Overridepublicvoidrun(){if(taskType==HasException){System.out.println(formater.format(newDate())+"异常产生");thrownewRuntimeException("有异常");}elseif(taskType==ProcessException){try{System.out.println(formater.format(newDate())+"异常产生被捕捉");thrownewRuntimeException("异常被捕捉");//异常导致下个任务无法执行}catch(Exceptione){System.out.println("异常被主播");}}else{System.out.println("正常"+formater.format(newDate()));}}}publicclassSchTest{publicstaticvoidmain(String[]args){ScheduledThreadPoolExecutorschedule=newScheduledThreadPoolExecutor(1);schedule.scheduleAtFixedRate(newScheduleWorker(ScheduleWorker.HasException),1000,3000,TimeUnit.MILLISECONDS);//任务在1秒后执行周期3秒schedule.scheduleAtFixedRate(newScheduleWorker(ScheduleWorker.Normal),1000,3000,TimeUnit.MILLISECONDS);}} CompletionService JDK8中新添加的一个类,摄像一个场景你去询问两个商品价格然后将价格保存数据库。 ExecutorServiceexecutor=Executors.newFixedThreadPool(2);//异步向电商S1询价Future<Integer>f1=executor.submit(()->getPriceByS1());//异步向电商S2询价Future<Integer>f2=executor.submit(()->getPriceByS2());//获取电商S1报价并保存r=f1.get();executor.execute(()->save(r));//获取电商S2报价并保存r=f2.get();executor.execute(()->save(r)); 上面的这个方案本身没有太大问题,但是有个地方的处理需要你注意,那就是如果获取电商 S1 报价的耗时很长,那么即便获取电商 S2 报价的耗时很短,也无法让保存 S2 报价的操作先执行,因为这个主线程都阻塞在了 f1.get(),那我们如何解决了?解决方法:结果都存入到一个阻塞队列中去。 //创建阻塞队列BlockingQueue<Integer>bq=newLinkedBlockingQueue<>();//电商S1报价异步进入阻塞队列executor.execute(()->bq.put(f1.get()));//电商S2报价异步进入阻塞队列executor.execute(()->bq.put(f2.get()));//异步保存所有报价for(inti=0;i<2;i++){Integerr=bq.take();executor.execute(()->save(r));} 在JDK8中不建议上面的工作都手动实现,JDK提供了CompletionService ,它实现原理也是内部维护了一个阻塞队列,它的核心功效就是让先执行的任务先放到结果集。当任务执行结束就把任务的执行结果加入到阻塞队列中,不同的是CompletionService是把任务执行结果的 Future 对象加入到阻塞队列中,而上面的示例代码是把任务最终的执行结果放入了阻塞队列中。CompletionService将Executor和BlockingQueue的功能融合在一起,CompletionService内部有个阻塞队列。CompletionService 接口的实现类是 ExecutorCompletionService,这个实现类的构造方法有两个,分别是: ExecutorCompletionService(Executorexecutor)ExecutorCompletionService(Executorexecutor,BlockingQueue<Future<V>>completionQueue) 这两个构造方法都需要传入一个线程池,如果不指定 completionQueue,那么默认会使用无界的 LinkedBlockingQueue。任务执行结果的 Future 对象就是加入到 completionQueue 中。 //创建线程池ExecutorServiceexecutor=Executors.newFixedThreadPool(2);//创建CompletionServiceCompletionService<Integer>cs=newExecutorCompletionService<>(executor);//异步向电商S1询价cs.submit(()->getPriceByS1());//异步向电商S2询价cs.submit(()->getPriceByS2());//将询价结果异步保存到数据库for(inti=0;i<2;i++){Integerr=cs.take().get();executor.execute(()->save(r));} 来一个整体的demo加深印象: //任务类classWorkTaskimplementsCallable<Integer>{privateStringname;publicWorkTask(Stringname){this.name=name;}@OverridepublicIntegercall(){intsleepTime=newRandom().nextInt(1000);try{Thread.sleep(sleepTime);}catch(InterruptedExceptione){e.printStackTrace();}returnsleepTime;}}publicclassCompletionCase{privatefinalintPOOL_SIZE=Runtime.getRuntime().availableProcessors();privatefinalintTOTAL_TASK=Runtime.getRuntime().availableProcessors();publicvoidselfByQueue()throwsException{longstart=System.currentTimeMillis();//统计所有任务休眠的总时长AtomicIntegercount=newAtomicInteger(0);ExecutorServicepool=Executors.newFixedThreadPool(POOL_SIZE);//创建线程池BlockingQueue<Future<Integer>>queue=newLinkedBlockingQueue<Future<Integer>>();//容器存放提交给线程池的任务,list,map,for(inti=0;i<TOTAL_TASK;i++){Future<Integer>future=pool.submit(newWorkTask("要执行的第几个任务"+i));queue.add(future);//i=0先进队列,i=1的任务跟着进}for(inti=0;i<TOTAL_TASK;i++){intsleptTime=queue.take().get();//检查线程池任务执行结果i=0先取到,i=1的后取到System.out.println("休眠毫秒数="+sleptTime+"ms");count.addAndGet(sleptTime);}pool.shutdown();System.out.println("休眠时间"+count.get()+"ms,耗时时间"+(System.currentTimeMillis()-start)+"ms");}publicvoidtestByCompletion()throwsException{longstart=System.currentTimeMillis();AtomicIntegercount=newAtomicInteger(0);//创建线程池ExecutorServicepool=Executors.newFixedThreadPool(POOL_SIZE);CompletionService<Integer>cService=newExecutorCompletionService<>(pool);//向里面扔任务for(inti=0;i<TOTAL_TASK;i++){cService.submit(newWorkTask("执行任务"+i));}//检查线程池任务执行结果for(inti=0;i<TOTAL_TASK;i++){intsleptTime=cService.take().get();System.out.println("休眠毫秒数="+sleptTime+"ms...");count.addAndGet(sleptTime);}pool.shutdown();System.out.println("休眠时间"+count.get()+"ms,耗时时间"+(System.currentTimeMillis()-start)+"ms");}publicstaticvoidmain(String[]args)throwsException{CompletionCaset=newCompletionCase();t.selfByQueue();t.testByCompletion();}} 在这里插入图片描述 常见考题 为什么用线程池? 线程池的作用? 常用的线程池模版? 7大重要参数? 4大拒绝策略? 常见线程池任务队列,如何理解有界跟无界?7.如何分配线程池个数? 单机线程池执行一般断电了如何考虑? 正在处理的实现事务功能,下次自动回滚,队列实现持久化储存,下次启动自动载入。 设定一个线程池优先级队列,Runable类要实现可对比功能,任务队列使用优先级队列 本文分享自微信公众号 - sowhat1412(sowhat9094)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册