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

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


  • 首页

  • 归档

  • 搜索

深入理解Java异常

发表于 2018-08-16

引言

说到异常,大家脑海中第一反应肯定是try-catch-finally这样的固定的组合。的确,这是Java异常处理的基本范式,下面我们就来好好聊聊Java异常机制,看看这个背后还有哪些我们忽略的细节。

Java异常介绍

异常时什么?就是指阻止当前方法或作用域继续执行的问题,当程序运行时出现异常时,系统就会自动生成一个Exception对象来通知程序进行相应的处理。Java异常的类型有很多种,下面我们就使用一张图来看一下Java异常的继承层次结构:

图片中我们看到Java异常的基类是Throwable类型,然后它有两个派生类Error和Exception类型,然后Exception类有分为受检查异常及RuntimeException(运行时异常)。下面我们就来逐一介绍。

Java异常中的Error

Error一般表示编译时或者系统错误,例如:虚拟机相关的错误,系统崩溃(例如:我们开发中有时会遇到的OutOfMemoryError)等。这种错误无法恢复或不可捕获,将导致应用程序中断,通常应用程序无法处理这些错误,因此也不应该试图用catch来进行捕获。

Java异常中的Exception

上面我们有介绍,Java异常的中的Exception分为受检查异常和运行时异常(不受检查异常)。下面我们展开介绍。

Java中的受检查异常

相信大家在写IO操作的代码的时候,一定有过这样的记忆,对File或者Stream进行操作的时候一定需要使用try-catch包起来,否则编译会失败,这是因为这些异常类型是受检查的异常类型。编译器在编译时,对于受检异常必须进行try…catch或throws处理,否则无法通过编译。常见的受检查异常包括:IO操作、ClassNotFoundException、线程操作等。

Java中的非受检查异常(运行时异常)

RuntimeException及其子类都统称为非受检查异常,例如:NullPointExecrption、NumberFormatException(字符串转换为数字)、ArrayIndexOutOfBoundsException(数组越界)、ClassCastException(类型转换错误)、ArithmeticException(算术错误)等。

Java的异常处理

Java处理异常的一般格式是这样的:

1
2
3
4
5
6
7
复制代码try{
///可能会抛出异常的代码
}catch(Type1 id1){
//处理Type1类型异常的代码
}catch(Type2 id2){
//处理Type2类型异常的代码
}

try块中放置可能会发生异常的代码(但是我们不知道具体会发生哪种异常)。如果异常发生了,try块抛出系统自动生成的异常对象,然后异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序,然后进行catch语句执行(不会在向下查找)。如果我们的catch语句没有匹配到,那么JVM虚拟机还是会抛出异常的。

Java中的throws关键字

如果在当前方法不知道该如何处理该异常时,则可以使用throws对异常进行抛出给调用者处理或者交给JVM。JVM对异常的处理方式是:打印异常的跟踪栈信息并终止程序运行。
throws在使用时应处于方法签名之后使用,可以抛出多种异常并用英文字符逗号’,’隔开。下面是一个例子:

1
复制代码public void f() throws ClassNotFoundException,IOException{}

这样我们调用f()方法的时候必须要catch-ClassNotFoundException和IOException这两个异常或者catch-Exception基类。

注意:

throws的这种使用方式只是Java编译期要求我们这样做的,我们完全可以只在方法声明中throws相关异常,但是在方法里面却不抛出任何异常,这样也能通过编译,我们通过这种方式间接的绕过了Java编译期的检查。这种方式有一个好处:为异常先占一个位置,以后就可以抛出这种异常而不需要修改已有的代码。在定义抽象类和接口的时候这种设计很重要,这样派生类或者接口实现就可以抛出这些预先声明的异常。

打印异常信息

异常类的基类Exception中提供了一组方法用来获取异常的一些信息.所以如果我们获得了一个异常对象,那么我们就可以打印出一些有用的信息,最常用的就是void printStackTrace()这个方法,这个方法将返回一个由栈轨迹中的元素所构成的数组,其中每个元素都表示栈中的一帧.元素0是栈顶元素,并且是调用序列中的最后一个方法调用(这个异常被创建和抛出之处);他有几个不同的重载版本,可以将信息输出到不同的流中去.下面的代码显示了如何打印基本的异常信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public void f() throws IOException{
System.out.println("Throws SimpleException from f()");
throw new IOException("Crash");
}
public static void main(String[] agrs) {
try {
new B().f();
} catch (IOException e) {
System.out.println("Caught Exception");
System.out.println("getMessage(): "+e.getMessage());
System.out.println("getLocalizedMessage(): "+e.getLocalizedMessage());
System.out.println("toString(): "+e.toString());
System.out.println("printStackTrace(): ");
e.printStackTrace(System.out);
}
}

我们来看输出:

1
2
3
4
5
6
7
8
9
复制代码Throws SimpleException from f()
Caught Exception
getMessage(): Crash
getLocalizedMessage(): Crash
toString(): java.io.IOException: Crash
printStackTrace():
java.io.IOException: Crash
at com.learn.example.B.f(RunMain.java:19)
at com.learn.example.RunMain.main(RunMain.java:26)

使用finally进行清理

引入finally语句的原因是我们希望一些代码总是能得到执行,无论try块中是否抛出异常.这样异常处理的基本格式变成了下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码try{
//可能会抛出异常的代码
}
catch(Type1 id1){
//处理Type1类型异常的代码
}
catch(Type2 id2){
//处理Type2类型异常的代码
}
finally{
//总是会执行的代码
}

在Java中希望除内存以外的资源恢复到它们的初始状态的时候需要使用的finally语句。例如打开的文件或者网络连接,屏幕上的绘制的图像等。下面我们来看一下案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码public class FinallyException {
static int count = 0;

public static void main(String[] args) {
while (true){
try {
if (count++ == 0){
throw new ThreeException();
}
System.out.println("no Exception");
}catch (ThreeException e){
System.out.println("ThreeException");
}finally {
System.out.println("in finally cause");
if(count == 2)
break;
}
}
}
}

class ThreeException extends Exception{}

我们来看输出:

1
2
3
4
复制代码ThreeException
in finally cause
no Exception
in finally cause

如果我们在try块或者catch块里面有return语句的话,那么finally语句还会执行吗?我们看下面的例子:

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
复制代码public class MultipleReturns {
public static void f(int i){
System.out.println("start.......");
try {
System.out.println("1");
if(i == 1)
return;
System.out.println("2");
if (i == 2)
return;
System.out.println("3");
if(i == 3)
return;
System.out.println("else");
return;
}finally {
System.out.println("end");
}
}

public static void main(String[] args) {
for (int i = 1; i<4; i++){
f(i);
}
}
}

我们来看运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码start.......
1
end
start.......
1
2
end
start.......
1
2
3
end

我们看到即使我们在try或者catch块中使用了return语句,finally子句还是会执行。那么有什么情况finally子句不会执行呢?

有下面两种情况会导致Java异常的丢失

  • finally中重写抛出异常(finally中重写抛出另一种异常会覆盖原来捕捉到的异常)
  • 在finally子句中返回(即return)

Java异常栈

前面稍微提到了点Java异常栈的相关内容,这一节我们通过一个简单的例子来更加直观的了解异常栈的相关内容。我们再看Exception异常的时候会发现,发生异常的方法会在最上层,main方法会在最下层,中间还有其他的调用层次。这其实是栈的结构,先进后出的。下面我们通过例子来看下:

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
复制代码public class WhoCalled {
static void f() {
try {
throw new Exception();
} catch (Exception e) {
for (StackTraceElement ste : e.getStackTrace()){
System.out.println(ste.getMethodName());
}
}
}

static void g(){
f();
}

static void h(){
g();
}

public static void main(String[] args) {
f();
System.out.println("---------------------------");
g();
System.out.println("---------------------------");
h();
System.out.println("---------------------------");

}
}

我们来看输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码f
main
---------------------------
f
g
main
---------------------------
f
g
h
main
---------------------------

可以看到异常信息都是从内到外的,按我的理解查看异常的时候要从第一条异常信息看起,因为那是异常发生的源头。

重新抛出异常及异常链

我们知道每遇到一个异常信息,我们都需要进行try…catch,一个还好,如果出现多个异常呢?分类处理肯定会比较麻烦,那就一个Exception解决所有的异常吧。这样确实是可以,但是这样处理势必会导致后面的维护难度增加。最好的办法就是将这些异常信息封装,然后捕获我们的封装类即可。

我们有两种方式处理异常,一是throws抛出交给上级处理,二是try…catch做具体处理。但是这个与上面有什么关联呢?try…catch的catch块我们可以不需要做任何处理,仅仅只用throw这个关键字将我们封装异常信息主动抛出来。然后在通过关键字throws继续抛出该方法异常。它的上层也可以做这样的处理,以此类推就会产生一条由异常构成的异常链。

通过使用异常链,我们可以提高代码的可理解性、系统的可维护性和友好性。

我们捕获异常以后一般会有两种操作

  • 捕获后抛出原来的异常,希望保留最新的异常抛出点--fillStackTrace
  • 捕获后抛出新的异常,希望抛出完整的异常链--initCause

捕获异常后重新抛出异常

在函数中捕获了异常,在catch模块中不做进一步的处理,而是向上一级进行传递
catch(Exception e){ throw e;},我们通过例子来看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码public class ReThrow {
public static void f()throws Exception{
throw new Exception("Exception: f()");
}

public static void g() throws Exception{
try{
f();
}catch(Exception e){
System.out.println("inside g()");
throw e;
}
}
public static void main(String[] args){
try{
g();
}
catch(Exception e){
System.out.println("inside main()");
e.printStackTrace(System.out);
}
}
}

我们来看输出:

1
2
3
4
5
6
7
复制代码inside g()
inside main()
java.lang.Exception: Exception: f()
//异常的抛出点还是最初抛出异常的函数f()
at com.learn.example.ReThrow.f(RunMain.java:5)
at com.learn.example.ReThrow.g(RunMain.java:10)
at com.learn.example.RunMain.main(RunMain.java:21)

fillStackTrace——覆盖前边的异常抛出点(获取最新的异常抛出点)

在此抛出异常的时候进行设置
catch(Exception e){ (Exception)e.fillInStackTrace();}
我们通过例子看一下:(还是刚才的例子)

1
2
3
4
5
6
7
8
复制代码public void g() throws Exception{
try{
f();
}catch(Exception e){
System.out.println("inside g()");
throw (Exception)e.fillInStackTrace();
}
}

运行结果如下:

1
2
3
4
5
6
复制代码inside g()
inside main()
java.lang.Exception: Exception: f()
//显示的就是最新的抛出点
at com.learn.example.ReThrow.g(RunMain.java:13)
at com.learn.example.RunMain.main(RunMain.java:21)

捕获异常后抛出新的异常(保留原来的异常信息,区别于捕获异常之后重新抛出)

如果我们在抛出异常的时候需要保留原来的异常信息,那么有两种方式

  • 方式1:Exception e=new Exception(); e.initCause(ex);
  • 方式2:Exception e =new Exception(ex);
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
复制代码class ReThrow {
public void f(){
try{
g();
}catch(NullPointerException ex){
//方式1
Exception e=new Exception();
//将原始的异常信息保留下来
e.initCause(ex);
//方式2
//Exception e=new Exception(ex);
try {
throw e;
} catch (Exception e1) {
e1.printStackTrace();
}
}
}

public void g() throws NullPointerException{
System.out.println("inside g()");
throw new NullPointerException();
}
}

public class RunMain {
public static void main(String[] agrs) {
try{
new ReThrow().f();
}
catch(Exception e){
System.out.println("inside main()");
e.printStackTrace(System.out);
}
}
}

在这个例子里面,我们先捕获NullPointerException异常,然后在抛出Exception异常,这时候如果我们不使用initCause方法将原始异常(NullPointerException)保存下来的话,就会丢失NullPointerException。只会显示Eception异常。下面我们来看结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码//没有调用initCause方法的输出
inside g()
java.lang.Exception
at com.learn.example.ReThrow.f(RunMain.java:9)
at com.learn.example.RunMain.main(RunMain.java:31)
//调用initCasue方法保存原始异常信息的输出
inside g()
java.lang.Exception
at com.learn.example.ReThrow.f(RunMain.java:9)
at com.learn.example.RunMain.main(RunMain.java:31)
Caused by: java.lang.NullPointerException
at com.learn.example.ReThrow.g(RunMain.java:24)
at com.learn.example.ReThrow.f(RunMain.java:6)
... 1 more

我们看到我们使用initCause方法保存后,原始的异常信息会以Caused by的形式输出。

Java异常的限制

当Java异常遇到继承或者接口的时候是存在限制的,下面我们来看看有哪些限制。

  • 规则一:子类在重写父类抛出异常的方法时,要么不抛出异常,要么抛出与父类方法相同的异常或该异常的子类。如果被重写的父类方法只抛出受检异常,则子类重写的方法可以抛出非受检异常。例如,父类方法抛出了一个受检异常IOException,重写该方法时不能抛出Exception,对于受检异常而言,只能抛出IOException及其子类异常,也可以抛出非受检异常。
    我们通过例子来看下:
1
2
3
4
5
6
复制代码class A {  
public void fun() throws Exception {}
}
class B extends A {
public void fun() throws IOException, RuntimeException {}
}

父类抛出的异常包含所有异常,上面的写法正确。

1
2
3
4
5
6
复制代码class A {  
public void fun() throws RuntimeException {}
}
class B extends A {
public void fun() throws IOException, RuntimeException {}
}

子类IOException超出了父类的异常范畴,上面的写法错误。

1
2
3
4
5
6
复制代码class A {  
public void fun() throws IOException {}
}
class B extends A {
public void fun() throws IOException, RuntimeException, ArithmeticException{}
}

RuntimeException不属于IO的范畴,并且超出了父类的异常范畴。但是RuntimeException和ArithmeticException属于运行时异常,子类重写的方法可以抛出任何运行时异常。所以上面的写法正确。

  • 规则儿:子类在重写父类抛出异常的方法时,如果实现了有相同方法签名的接口且接口中的该方法也有异常声明,则子类重写的方法要么不抛出异常,要么抛出父类中被重写方法声明异常与接口中被实现方法声明异常的交集。
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码class Test {
public Test() throws IOException {}
void test() throws IOException {}
}

interface I1{
void test() throw Exception;
}

class SubTest extends Test implements I1 {
public SubTest() throws Exception,NullPointerException, NoSuchMethodException {}
void test() throws IOException {}
}

在SubTest类中,test方法要么不抛出异常,要么抛出IOException或其子类(例如,InterruptedIOException)。

Java异常与构造器

如果一个构造器中就发生异常了,那我们如何处理才能正确的清呢?也许你会说使用finally啊,它不是一定会执行的吗?这可不一定,如果构造器在其执行过程中遇到了异常,这时候对象的某些部分还没有正确的初始化,而这时候却会在finally中对其进行清理,显然这样会出问题的。

原则:

对于在构造器阶段可能会抛出异常,并且要求清理的类,最安全的方式是使用嵌套的try子句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码try {
InputFile in=new InpputFile("Cleanup.java");
try {
String string;
int i=1;
while ((string=in.getLine())!=null) {}
}catch (Exception e) {
System.out.println("Cause Exception in main");
e.printStackTrace(System.out);
}finally {
in.dispose();
}
}catch (Exception e) {
System.out.println("InputFile construction failed");
}

我们来仔细看一下这里面的逻辑,对InputFile的构造在第一个try块中是有效的,如果构造器失败,抛出异常,那么会被最外层的catch捕获到,这时候InputFile对象的dispose方法是不需要执行的。如果构造成功,那么进入第二层try块,这时候finally块肯定是需要被调用的(对象需要dispose)。

异常的使用指南(下列情况下使用异常)

  • 在恰当的级别处理异常(在知道如何处理的情况下才捕获异常)
  • 努力解决问题并且重新调用产生异常的方法
  • 进行少许修补,然后绕过异常的地方重新执行
  • 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层
  • 把当前运行环境下能做的事情尽量做完,然后把不相同的异常重抛到更高层
  • 努力让类库和程序更安全

本文转载自: 掘金

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

登陆界面模块解析——生成图片验证码

发表于 2018-08-11

文章来自我的博客

正文之前

之前做过一个练手的Web项目登陆界面,其中有一个验证码模块,涉及到JavaIO和Java AWT工具所以打算写一篇介绍,顺便对练手项目的源码做一点小变动

正文

1. 变量

首先先定义几个需要的变量

1
2
3
4
5
复制代码    private int width=50;                               //图片缓冲区的宽
private int height=15; //图片缓冲区的高
private Random random=new Random(); //随机数字
private Color color=new Color(255,255,255); //白色背景
private String text; //图片上的文本
2. 随机字符、颜色

随机字符包括了10位数字和26个字母

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码    //生成随机字符
private char randomChar()
{
//选取随机数字,生成字符
String numbers = "1234567890qwertyuiopasdfghjklzxcvbnm";
int index=random.nextInt(numbers.length());
//转换为char类型
return numbers.charAt(index);
}

//生成随机颜色
private Color randomColor()
{
//以RGB形式表示颜色
int red=random.nextInt(255);
int green=random.nextInt(255);
int blue=random.nextInt(255);
//返回随机生成的颜色
return new Color(red,green,blue);
}
3. 干扰线
1
2
3
4
5
6
7
8
9
10
复制代码    //画干扰线
private void drawLine(Graphics g){
for (int i = 0; i < 2; i++) {
//设置线的颜色
g.setColor(randomColor());
//线的长度从(x1,y1)到(x2,y2)
g.drawLine(random.nextInt(width), random.nextInt(height),
random.nextInt(width), random.nextInt(height));
}
}
4. 验证码中的文本
1
2
3
4
5
复制代码//返回验证码图片中的文本
public String getText()
{
return text;
}
5. 图片缓冲区
1
2
3
4
5
6
7
8
9
10
11
12
复制代码    //创造图片缓冲区
private BufferedImage createImage()
{
//参数为宽度,高度和图像类型
BufferedImage image=new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
Graphics g=image.getGraphics();
//设置颜色
g.setColor(color);
//填充内容
g.fillRect(0,0,width,height);
return image;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码    //得到图片缓冲区
public BufferedImage getImage()
{
BufferedImage image = createImage(); //创建缓冲区
Graphics g = image.getGraphics(); //得到绘制环境
StringBuilder stringBuilder = new StringBuilder(); //验证码文本

//验证码长度为4
for (int i=0;i<4;i++)
{
String str = String.valueOf(randomChar()); //产生随机字符,将char类型转为String类型
stringBuilder.append(str); //将生成的随机字符添加至stringBuilder
g.setColor(randomColor()); //产生随机颜色
g.drawString(str, i * width / 4, height); //绘制文本
}
drawLine(g); //添加干扰线
text = stringBuilder.toString(); //把生成字符串赋给文本,以便于验证
return image;
}
6. 输出图片
1
2
3
4
5
复制代码    //使用输出流打印图片
public static void output(BufferedImage image, OutputStream out) throws IOException {
//作为JPEG格式输出图片
ImageIO.write(image,"JPG",out);
}
Demo

直接输出图片到我的桌面

运行之后的结果:

就这样,简单的验证码就做完了

本文转载自: 掘金

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

用python来给图片加水印

发表于 2018-08-11

有时候我想在图片上添加自己的水印来防止别人盗图,所以今天给大家分享如何用python给我们的图片添加上水印。我们先来看看效果。

可以看到右下角就有了我们公众号的名称的水印,是不是超级厉害?那我们看看代码吧,也就20行不到。

前提需要下载好库 PIL,没有的先去下载,这里不多说了。

是不是很简单,随便改一下还可以批量添加,还可以改下字体改下文本位置也达到不同的效果。

只需要更改下面两行代码即可

1
2
3
复制代码# 设置字体和字体大小
font = ImageFont.truetype('C:\Windows\Fonts\HYS5GFM.TTF', 100)# 设置水印位置
text_xy = (layer.size[0]//2 - text_size_x//2, layer.size[1] - text_size_y)

这里需要注意的是,在添加水印时中文字体无法显示是你设置的字体没有中文,需要更换有中文对应的字体。

我们还可以添加图片或者说logo在我们的图片上,比如这样:

把我们的logo放在图片上也是不错的,这样就更加盗版不了了,代码也是10行不到,再说一句:人生苦短,我用python:)

python可能真的除了不会生孩子,其他的什么都可以了,你还等什么,敲起来吧!!!

ps:原创不易,如果觉得文章不错的话,欢迎随手点赞转发支持

日常学python

代码不止bug,还有美和乐趣

本文转载自: 掘金

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

如何通过浏览器上网

发表于 2018-08-09

问题

当我们在网页浏览器(Web browser)的地址栏中输入 URL 时,Web 页面是如何呈现的吗?

引言

Web 页面当然不能凭空显示出来。根据 Web 浏览器地址栏中指定的 URL,Web 浏览器从 Web 服务器端获取文件资源(resource)等信息,从而显示出 Web 页面。像这种通过发送请求获取服务器资源的 Web 浏览器等,都可称为客户端(client)。

Web 使用一种名为 HTTP(HyperText Transfer Protocol,超文本传输协议)的协议作为规范,完成从客户端到服务器端等一系列运作流程。而协议是指规则的约定,可以说,Web 是建立在 HTTP 协议上通信的。

超文本传输协议(HTTP,HyperText Transfer Protocol) 是互联网上应用最为广泛的一种网络协议。所有的 WWW 文件都必须遵守这个标准。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。1960 年美国人 Ted Nelson 构思了一种通过计算机处理文本信息的方法,并称之为超文本(hypertext),这成为了 HTTP 超文本传输协议标准架构的发展根基。Ted Nelson 组织协调万维网协会(World Wide Web Consortium)和互联网工程工作小组(Internet
Engineering Task Force )共同合作研究,最终发布了一系列的 RFC,其中著名的 RFC 2616 定义了 HTTP 1.1。

诞生

HTTP 的出生时间是 1989 年 3 月,那时候互联网还属于少数人。

CERN(欧洲核子研究组织)的蒂姆 • 伯纳斯 - 李(Tim BernersLee)博士提出了一种能让远隔两地的研究者们共享知识的设想。

最初设想的基本理念是:借助多文档之间相互关联形成的超文本(HyperText),连成可相互参阅的 WWW(World Wide Web,万维网)。

现在已提出了 3 项 WWW 构建技术,分别是:把 SGML(Standard Generalized Markup Language,标准通用标记语言)作为页面的文本标记语言的 HTML(HyperText Markup Language,超文本标记语言);作为文档传递协议的 HTTP ;指定文档所在地址的 URL(Uniform Resource Locator,统一资源定位符)。

WWW 这一名称,是 Web 浏览器当年用来浏览超文本的客户端应用程序时的名称。现在则用来表示这一系列的集合,也可简称为 Web。

1990 年 11 月,CERN 成功研发了世界上第一台 Web 服务器和 Web 浏览器。

HTTP/0.9

HTTP 于 1990 年问世。那时的 HTTP 并没有作为正式的标准被建立。现在的 HTTP 其实含有 HTTP1.0 之前版本的意思,因此被称为 HTTP/0.9。

HTTP/1.0

HTTP 正式作为标准被公布是在 1996 年的 5 月,版本被命名为 HTTP/1.0,并记载于 [RFC1945]。虽说是初期标准,但该协议标准至今仍被广泛使用在服务器端。

HTTP/1.1

1997 年 1 月公布的 HTTP/1.1 是目前主流的 HTTP 协议版本。当初的标准是 RFC2068,之后发布的修订版 [RFC2616] 就是当前的最新版本。

TCP/IP 简介

为了理解 HTTP,我们有必要事先了解一下 TCP/IP 协议族。通常使用的网络(包括互联网)是在 TCP/IP 协议族的基础上运作的。而 HTTP 属于它内部的一个子集。

计算机与网络设备要相互通信,双方就必须基于相同的方法。比如,如何探测到通信目标、由哪一边先发起通信、使用哪种语言进行通信、怎样结束通信等规则都需要事先确定。不同的硬件、操作系统之间的通信,所有的这一切都需要一种规则。而我们就把这种规则称为协议(protocol)。

TCP/IP 协议族

分层管理

TCP/IP 协议族里重要的一点就是分层。TCP/IP 协议族按层次分别分为以下 4 层:应用层、传输层、网络层和数据链路层。

分层的好处:若某个地方需要改变设计时,不需要把所有部分整体替换掉,只需把变动的层替换掉即可。把各层之间的接口部分规划好之后,每个层次内部的设计就能够自由改动了。

应用层

应用层决定了向用户提供应用服务时通信的活动。

TCP/IP 协议族内预存了各类通用的应用服务。比如,FTP(File Transfer Protocol,文件传输协议)和 DNS(Domain Name System,域名系统)。HTTP 协议也处于该层。

传输层

传输层为应用层,提供处于网络连接中的两台计算机之间的数据传输。

在传输层有两个性质不同的协议:TCP(Transmission Control Protocol,传输控制协议)和 UDP(User Data Protocol,用户数据报协议)。

TCP 提供可靠的字节流服务。所谓的字节流服务(Byte Stream Service)是指,为了方便传输,将大块数据分割成以报文段(segment)为单位的数据包进行管理。而可靠的传输服务是指,能够把数据准确可靠地传给对方。简单来说,TCP 协议为了更容易传送大数据才把数据分割,而且 TCP 协议能够确认数据最终是否送达到对方。而为了准确无误地将数据传输到目标,TCP 采用了三次握手策略。

网络层

网络层用来处理在网络上流动的数据包。数据包是网络传输的最小数据单位。该层规定了通过怎样的路径(所谓的传输路线)到达对方计算机,并把数据包传送给对方。

与对方计算机之间通过多台计算机或网络设备进行传输时,网络层所起的作用就是在众多的选项内选择一条传输路线。

负责传输的 IP 协议

IP(Internet Protocol)网际协议位于网络层。需要注意的是可“IP”和“IP 地址”的区别,“IP”其实是一种协议的名称。

IP 协议的作用是把各种数据包传送给对方。其中两个重要的条件是 IP 地址和 MAC 地址(Media Access Control Address)。

IP 地址指明了节点被分配到的地址,MAC 地址是指网卡所属的固定地址。IP 地址可以和 MAC 地址进行配对。IP 地址可变换,但 MAC 地址基本上不会更改。基本上各大网卡制作厂商都被预制分配了 MAC 地址区间段。

IP 间的通信依赖 MAC 地址。在网络上,通信的双方在同一局域网(LAN)内的情况是很少的,通常是经过多台计算机和网络设备中转才能连接到对方。而在进行中转时,会利用下一站中转设备的 MAC 地址来搜索下一个中转目标。这时,会采用 ARP 协议(Address Resolution Protocol)。ARP 是一种用以解析地址的协议,根据通信方的 IP 地址就可以反查出对应的 MAC 地址。

在到达通信目标前的中转过程中,那些计算机和路由器等网络设备只能获悉很粗略的传输路线。这种机制称为路由选择(routing)。

路由选择

数据链路层

用来处理连接网络的硬件部分。包括控制操作系统、硬件的设备驱动、NIC(Network Interface Card,网络适配器,即网卡),及光纤等物理可见部分(还包括连接器等一切传输媒介)。

硬件上的范畴均在链路层的作用范围之内。

在数据链路层还有一个常见的网络协议 LLDP。了解更多可以查看《数据链路层之 LLDP》。

通信传输流

TCP/IP 通信传输流

为了更好的理解上图,我们使用 HTTP 🌰 说明。

HTTP 通信

  1. 作为发送端的客户端在应用层(HTTP 协议)发出一个想看某个 Web 页面的 HTTP 请求。
  1. 在传输层(TCP 协议)把从应用层处收到的数据(HTTP 请求报文)进行分割,并在各个报文上打上标记序号及端口号后转发给网络层。
  1. 在网络层(IP 协议),增加作为通信目的地的 MAC 地址后转发给链路层。这样一来,发往网络的通信请求就准备齐全了。
  1. 接收端的服务器在链路层接收到数据,按序往上层发送,一直到应用层。当传输到应用层,才能算真正接收到由客户端发送过来的 HTTP 请求。

发送端在层与层之间传输数据时,每经过一层时必定会被打上一个该层所属的首部信息。反之,接收端在层与层传输数据时,每经过一层时会把对应的首部消去。这种把数据信息包装起来的做法称为封装(encapsulate)。

DNS 简述

DNS(Domain Name System)服务是和 HTTP 协议一样位于应用层的协议。它提供域名到 IP 地址之间的解析服务。

计算机既可以被赋予 IP 地址,也可以被赋予主机名和域名。🌰 www.chars.tech。

用户通常使用主机名或域名来访问对方的计算机,而不是直接通过 IP 地址访问。因为与 IP 地址的一组纯数字相比,用字母配合数字的表示形式来指定计算机名更符合人类的记忆习惯。

但要让计算机去理解名称,相对而言就变得困难了。因为计算机更擅长处理一长串数字。为了解决上述的问题,DNS 服务应运而生。DNS 协议提供通过域名查找 IP 地址,或逆向从 IP 地址反查域名的服务。

DNS服务

访问网站

至此,大致可以回答开篇的问题了,我们在浏览器输入框中输入想浏览的网页地址之后,发生了哪些事情呢?

HTTP浏览网页

  1. 客户端发起页面网址请求给 DNS。
  1. DNS 解析出对应 IP 地址返回给客户端。
  1. 客户端填充请求 IP 地址。
  1. 客户端根据 HTTP 交互封装请求数据包。
  1. 请求数据包经过路由机制到达目的地址。
  1. 目的地址服务器返回数据给客户端。

写在最后

文章首发《马小尾成长迹》,专栏主要分享一些日常开发、热点技术、有趣生活文章,不仅仅是马小尾本人的,也欢迎大家投稿。

本文转载自: 掘金

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

再有人问你synchronized是什么,就把这篇文章发给他

发表于 2018-08-09

在再有人问你Java内存模型是什么,就把这篇文章发给他。中我们曾经介绍过,Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如synchronized、volatile、final、concurren包等。

在《深入理解Java虚拟机》中,有这样一段话:

synchronized关键字在需要原子性、可见性和有序性这三种特性的时候都可以作为其中一种解决方案,看起来是“万能”的。的确,大部分并发控制操作都能使用synchronized来完成。

海明威在他的《午后之死》说过的:“冰山运动之雄伟壮观,是因为他只有八分之一在水面上。”对于程序员来说,synchronized只是个关键字而已,用起来很简单。之所以我们可以在处理多线程问题时可以不用考虑太多,就是因为这个关键字帮我们屏蔽了很多细节。

那么,本文就围绕synchronized展开,主要介绍synchronized的用法、synchronized的原理,以及synchronized是如何提供原子性、可见性和有序性保障的等。

synchronized的用法

synchronized是Java提供的一个并发控制的关键字。主要有两种用法,分别是同步方法和同步代码块。也就是说,synchronized既可以修饰方法也可以修饰代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csharp复制代码/**
* @author Hollis 18/08/04.
*/
public class SynchronizedDemo {
//同步方法
public synchronized void doSth(){
System.out.println("Hello World");
}

//同步代码块
public void doSth1(){
synchronized (SynchronizedDemo.class){
System.out.println("Hello World");
}
}
}

被synchronized修饰的代码块及方法,在同一时间,只能被单个线程访问。

synchronized的实现原理

synchronized,是Java中用于解决并发情况下数据同步访问的一个很重要的关键字。当我们想要保证一个共享资源在同一时间只会被一个线程访问到时,我们可以在代码中使用synchronized关键字对类或者对象加锁。

在深入理解多线程(一)——Synchronized的实现原理中我曾经介绍过其实现原理,为了保证知识的完整性,这里再简单介绍一下,详细的内容请去原文阅读。

我们对上面的代码进行反编译,可以得到如下代码:

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
yaml复制代码public synchronized void doSth();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

public void doSth1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #5 // class com/hollis/SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #3 // String Hello World
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return

通过反编译后代码可以看出:对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步。 对于同步代码块。JVM采用monitorenter、monitorexit两个指令来实现同步。

在The Java® Virtual Machine Specification中有关于同步方法和同步代码块的实现原理的介绍,我翻译成中文如下:

方法级的同步是隐式的。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

同步代码块使用monitorenter和monitorexit两个指令实现。可以把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。

无论是ACC_SYNCHRONIZED还是monitorenter、monitorexit都是基于Monitor实现的,在Java虚拟机(HotSpot)中,Monitor是基于C++实现的,由ObjectMonitor实现。

ObjectMonitor类中提供了几个方法,如enter、exit、wait、notify、notifyAll等。sychronized加锁的时候,会调用objectMonitor的enter方法,解锁的时候会调用exit方法。(关于Monitor详见深入理解多线程(四)—— Moniter的实现原理)

synchronized与原子性

原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。

我们在Java的并发编程中的多线程问题到底是怎么回事儿?中分析过:线程是CPU调度的基本单位。CPU有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。

在Java中,为了保证原子性,提供了两个高级的字节码指令monitorenter和monitorexit。前面中,介绍过,这两个字节码指令,在Java中对应的关键字就是synchronized。

通过monitorenter和monitorexit指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。

线程1在执行monitorenter指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程1主动解锁。即使在执行过程中,由于某种原因,比如CPU时间片用完,线程1放弃了CPU,但是,他并没有进行解锁。而由于synchronized的锁是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代码执行完。这就保证了原子性。

synchronized与可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

我们在再有人问你Java内存模型是什么,就把这篇文章发给他。中分析过:Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程1改了某个变量的值,但是线程2不可见的情况。

前面我们介绍过,被synchronized修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。而为了保证可见性,有一条规则是这样的:对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。

所以,synchronized关键字锁住的对象,其值是具有可见性的。

synchronized与有序性

有序性即程序执行的顺序按照代码的先后顺序执行。

我们在再有人问你Java内存模型是什么,就把这篇文章发给他。中分析过:除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如load->add->save 有可能被优化成load->save->add 。这就是可能存在有序性问题。

这里需要注意的是,synchronized是无法禁止指令重排和处理器优化的。也就是说,synchronized无法避免上述提到的问题。

那么,为什么还说synchronized也提供了有序性保证呢?

这就要再把有序性的概念扩展一下了。Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有操作都是天然有序的。如果在一个线程中观察另一个线程,所有操作都是无序的。

以上这句话也是《深入理解Java虚拟机》中的原句,但是怎么理解呢?周志明并没有详细的解释。这里我简单扩展一下,这其实和as-if-serial语义有关。

as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。

这里不对as-if-serial语义详细展开了,简单说就是,as-if-serial语义保证了单线程中,指令重排是有一定的限制的,而只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的。当然,实际上还是有重排的,只不过我们无须关心这种重排的干扰。

所以呢,由于synchronized修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。所以,可以保证其有序性。

synchronized与锁优化

前面介绍了synchronized的用法、原理以及对并发编程的作用。是一个很好用的关键字。

synchronized其实是借助Monitor实现的,在加锁时会调用objectMonitor的enter方法,解锁的时候会调用exit方法。事实上,只有在JDK1.6之前,synchronized的实现才会直接调用ObjectMonitor的enter和exit,这种锁被称之为重量级锁。

所以,在JDK1.6中出现对锁进行了很多的优化,进而出现轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化(自旋锁在1.4就有,只不过默认的是关闭的,jdk1.6是默认开启的),这些操作都是为了在线程之间更高效的共享数据 ,解决竞争问题。

关于自旋锁、锁粗化和锁消除可以参考深入理解多线程(五)—— Java虚拟机的锁优化技术,关于轻量级锁和偏向锁,已经在排期规划中,我后面会有文章单独介绍,将独家发布在我的博客(http://www.hollischuang.com)和公众号(Hollis)中,敬请期待。

好啦,关于synchronized关键字,我们介绍了其用法、原理、以及如何保证的原子性、顺序性和可见性,同时也扩展的留下了锁优化相关的资料及思考。后面我们会继续介绍volatile关键字以及他和synchronized的区别等。敬请期待。

wechat

本文转载自: 掘金

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

嘻哈说:设计模式之里氏替换原则

发表于 2018-08-08

1、定义

按照惯例,首先我们来看一下里氏替换原则的定义。

所有引用基类(父类)的地方必须能透明地使用其子类的对象。
通俗的说,子类可以扩展父类功能,但不能改变父类原有功能。

核心思想是继承。 通过继承,引用基类的地方就可以使用其子类的对象了。例如:

1
ini复制代码Parent parent = new Child();

重点来了,那么如何透明地使用呢?

我们来思考个问题,子类可以改变父类的原有功能吗?

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class Parent {
public int add(int a, int b){
return a+b;
}
}

public class Child extends Parent{
@Override
public int add(int a, int b) {
return a-b;
}
}

这样好不好?

肯定是不好的,本来是加法却修改成了减法,这显然是不符合认知的。

它违背了里氏替换原则,子类改变了父类原有功能后,当我们在引用父类的地方使用其子类的时候,没办法透明使用add方法了。

父类中凡是已经实现好的方法,实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些规范,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。

所以,透明使用的关键就是,子类不能改变父类原有功能。

2、含义

1、子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。

刚才我们已经说过,子类不能改变父类的原有功能,所以子类不能覆盖父类的非抽象方法。

子类可以实现父类的抽象方法,must be,抽象方法本来就是让子类实现的。

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
csharp复制代码package com.fanqiekt.principle.liskov.rapper;

/**
* Rapper抽象类
*
* @Author: 懒人
*/
public abstract class BaseRapper {

/**
* freeStyle
*/
protected abstract void freeStyle();

/**
* 播放伴奏
*/
protected void playBeat(){
System.out.println("从乐库中随机播放一首伴奏:动次打次...");
}

/**
* 表演
* 播放伴奏,并进行freeStyle
*/
public void perform(){
playBeat();
freeStyle();
}
}

BaseRapper是一个抽象类,它代表着Rapper的基类。

Rapper一般的表演方式是随机播放一首伴奏然后进行free style。

freeStyle则各有各的不同,所以将它写成了一个抽象方法,让子类自由发挥。

playBeat流程大多是一样的,从乐库中随意播放伴奏,所以将它写成了一个非抽象方法。

perform的流程大多也是一样的,放伴奏,然后freestyle,也将它写成了非抽象方法。

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
scala复制代码package com.fanqiekt.principle.liskov.rapper;

/**
* Rapper
*
* @author 懒人
*/
public class Rapper extends BaseRapper {

/**
* 播放伴奏
*
* 子类覆盖父类非抽象方法
*/
@Override
protected void playBeat() {
System.out.println("关闭麦克风");
}

/**
* 表演
*
* 子类覆盖父类非抽象方法
*/
@Override
public void perform() {
System.out.println("跳鬼步");

}

/**
* 子类可以覆盖父类抽象方法
*/
@Override
protected void freeStyle() {
System.out.println("药药切克闹,煎饼果子来一套!");
}
}

Rapper是BaseRapper的子类,覆盖了父类的抽象方法freeStyle。

覆盖了父类的非抽象方法playBeat,并将逻辑更改为打开麦克风,明显违背了里氏替换原则。
这显然是非常错误的写法, 原因是父类行为与子类行为不一致,不可以透明的使用父类了。
播放伴奏你却给我打开麦克风,你确定不是在逗我?

我尝试着将playBeat进行下修改。

1
2
3
4
5
6
7
8
9
csharp复制代码/**
* 子类覆盖父类非抽象方法
* 子类方法中调用super方法
*/
@Override
protected void playBeat() {
super.playBeat();
System.out.println("关闭麦克风");
}

在子类方法中调用super方法,这样修改是否可以?

不可以,原因是打开麦克风跟播放伴奏没有任何逻辑上的关系。

透明使用子类的时候,虽然伴奏也会正常的播放,但却在调用者不知情的情况下关闭了麦克风,而关闭麦克风又明显与播放伴奏无关。
这就对于调用者无法做到真正的透明了。

同样覆盖了父类的非抽象方法perform,并将逻辑更改为跳舞,这要是违背了里氏替换原则的。
只跳舞不说唱的表演还叫Rapper吗?

我尝试着将perform进行下修改。

1
2
3
4
5
6
7
8
9
10
csharp复制代码/**
* 表演
* freestyle + 跳舞
* 子类覆盖父类非抽象方法
*/
@Override
public void perform() {
super.perform();
System.out.println("跳鬼步");
}

perform方法我这样修改可以吗?

这个倒是可以的,为什么同样是子类调用super方法,为什么playBeat不可以,perform就可以呢?

perform是表演,跳舞是表演的一种补充,属于表演范畴,调用者可以透明地调用perform方法。

安静的freestyle还是手舞足蹈的freestyle,对于调用者来讲,都属于freestyle表演。

2、子类中可以增加自己特有的方法。

继承一个很重要的特点:子类继承父类后可以新增方法。

1
2
3
4
5
6
7
csharp复制代码/**
* 跳舞
* 子类中增加特有的方法
*/
public void dance(){
System.out.println("跳鬼步!");
}

在Rapper中可以增加dance方法。

3、当子类重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。

注意,是子类重载父类,而不是子类重写父类。

重载的话,相当于一个全新的方法,与父类的同名方法并不冲突。两个是同时存在的,根据传入参数而自动选择方法。

可以重载抽象方法,也可以重载非抽象方法。

方法的形参为什么要比父类更宽松呢?

首先,形参肯定不能一致,一致的话,就是重写了,就又回到第一条含义了。

第二,如果我们更加严格,那会出现什么情况呢?

我们可以来看下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package com.fanqiekt.principle.liskov.rapper;

import java.util.List;

/**
* 父类
*
* @author 懒人
*/
public abstract class Parent {

public void setList(List<String> list){
System.out.println("执行父类setList方法");
}
}

这个是父类,setList方法有个List类型的形参。>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scala复制代码package com.fanqiekt.principle.liskov.rapper;

import java.util.ArrayList;

/**
* 子类
*
* @author 懒人
*/
public class Children extends Parent {

public void setList(ArrayList<String> list) {
System.out.println("执行子类setList方法");
}
}

这个是子类,传入参数类型为ArrayList,比父类更加的严格。

1
2
ini复制代码Children children = new Children();
children.setList(new ArrayList<>());

我们运行这行代码,看下结果。

1
复制代码执行子类setList方法

这个结果有没有问题?

是有问题的,setList(new ArrayList<>())按照里氏替换原则是应该透明的执行父类的setList(List list)方法的。

这块不是很好理解,对于调用者来讲,我想调用的Parent的setList(List list)方法,结果却执行Children的setList(ArrayList list)方法了。

这就好像是子类重写了父类的setList方法,而不是重载了子类的setList方法。

也就是说,方法的形参严格后,在某种情况就变成重写了。

而重写显然是不符合里氏替换原则的。

那我们再来看看宽松版本的。

1
2
3
4
5
6
7
8
9
10
11
scala复制代码/**
* 子类
*
* @author 懒人
*/
public class Children extends Parent {

public void setList(Collection<String> list) {
System.out.println("执行子类setList方法");
}
}

子类,传入参数类型为Collection,比父类更加的宽松。

1
2
ini复制代码Children children = new Children();
children.setList(new ArrayList<>());

同样的,我们运行这行代码,看下结果。

1
复制代码执行父类setList方法
1
2
ini复制代码Children children = new Children();
children.setList(new HashSet<>());

同样的,我们运行这行代码,看下结果。

1
复制代码执行子类setList方法

传入参数类型更加宽松,实现了子类重载父类。

4、当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

注意,这里说的是重写抽象方法,非抽象方法是不能重写的。

为什么说子类实现父类的抽象方法时,返回值要更严格呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码package com.fanqiekt.principle.liskov.rapper;

import java.util.List;

/**
* 父类
*
* @author 懒人
*/
public abstract class Parent {

public abstract List<String> getList();
}

父类,有一个getList的抽象方法,返回值为List。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scala复制代码package com.fanqiekt.principle.liskov.rapper;

import java.util.List;

/**
* 子类
*
* @author 懒人
*/
public class Children extends Parent {

@Override
public Collection<String> getList() {
return new ArrayList<>();
}
}

子类,getList返回为Collection类型,类型更宽松。

会有红线提示:… attempting to use incompatible return type 。

因为,父类返回值是List,子类返回值是List的父类Collection,透明使用父类的时候则需要将Collection转换成List。
类向上转换是安全的,向下转换则不一定是安全了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scala复制代码package com.fanqiekt.principle.liskov.rapper;

import java.util.List;

/**
* 子类
*
* @author 懒人
*/
public class Children extends Parent {

@Override
public ArrayList<String> getList() {
return new ArrayList<>();
}
}

子类,getList返回为ArrayList类型,类型更严格。

将ArrayList转换成List,向上转换是安全的。

2、场景

八大菜系的厨师

番茄餐厅,经过兢兢业业的经营,从一家小型的餐馆成长为一家大型餐厅。

厨师:老板,咱们现在家大业大客流量也大,虽然我精力充沛,但我也架不住这么多人的摧残。

老板:摧残?你确定?

厨师:哪能,您听错了,是照顾,架不住这么多人的照顾。

老板:小火鸡,可以呀,求生欲很强嘛。那你有什么想法?

厨师:我觉得咱们可以引入八大菜系厨师,一来,什么菜系的菜就交给什么菜系的厨师,味道质量会更加的上乘,才能配的上我们这么高规格的餐厅。

老板:嗯,说的有点道理,继续说。

厨师:二来,人手多了,还可以增加上菜的速度,三来……

老板:有道理,马上招聘厨师,小火鸡,恭喜你,升官了,你就是未来的厨师长。因为你求生欲真的很强。

厨师长:谢谢老板。(内心:我求生欲很强?哪里强了?放学你别走,我让你尝尝我的厉害,给你做一桌子好菜)

求生欲果真很强。

3、实现

不废话,撸代码。

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
csharp复制代码package com.fanqiekt.principle.liskov;

/**
* 抽象厨师类
*
* @author 懒人
*/
public abstract class Chef {
/**
* 做饭
* @param dishName 餐名
*/
public void cook(String dishName){
System.out.println("开始烹饪:"+dishName);

cooking(dishName);

System.out.println(dishName + "出锅");
}

/**
* 开始做饭
*/
protected abstract void cooking(String dishName);
}

抽象厨师类,公有cook方法,负责厨师做饭的一些相同逻辑,例如开始烹饪的准备工作,以及出锅。

具体做饭的细节则提供一个抽象方法cooking(正在做饭),具体菜系厨师需要重写该方法。

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
scala复制代码package com.fanqiekt.principle.liskov;

/**
* 山东厨师
*
* @author 懒人
*/
public class ShanDongChef extends Chef{
@Override
protected void cooking(String dishName) {
switch (dishName){
case "西红柿炒鸡蛋":
cookingTomato();
break;
default:
throw new IllegalArgumentException("未知餐品");
}
}

/**
* 炒西红柿鸡蛋
*/
private void cookingTomato() {
System.out.println("先炒鸡蛋");
System.out.println("再炒西红柿");
System.out.println("...");
}
}

鲁菜厨师ShanDongChef继承了厨师抽象类Chef,实现了抽象方法cooking。

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
scala复制代码package com.fanqiekt.principle.liskov;

/**
* 四川厨师
*
* @author 懒人
*/
public class SiChuanChef extends Chef{
@Override
protected void cooking(String dishName) {
switch (dishName){
case "酸辣土豆丝":
cookingPotato();
break;
default:
throw new IllegalArgumentException("未知餐品");
}
}

/**
* 炒酸辣土豆丝
*/
private void cookingPotato() {
System.out.println("先放葱姜蒜");
System.out.println("再放土豆丝");
System.out.println("...");
}
}

川菜厨师SiChuanChef继承了厨师抽象类Chef,实现了抽象方法cooking。

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
csharp复制代码package com.fanqiekt.principle.liskov;

/**
* 服务员
*
* @author 懒人
*/
public class Waiter {
/**
* 点餐
* @param dishName 餐名
*/
public void order(String dishName){
System.out.println("客人点餐:" + dishName);

Chef chef = new SiChuanChef();
switch(dishName) {
case "西红柿炒鸡蛋":
chef = new ShanDongChef();
break;
case "酸辣土豆丝": //取款
chef = new SiChuanChef();
break;
}
chef.cook(dishName);

System.out.println(dishName + "上桌啦,请您品尝!");
}
}

服务员类Waiter有一个点餐order方法,根据不同的菜名去通知相应菜系的厨师去做菜。

这里就用到了里氏替换原则,引用父类Chef可以透明地使用子类ShanDongChef或者SiChuanChef。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package com.fanqiekt.principle.liskov;

/**
* 客人
*
* @author 懒人
*/
public class Client {
public static void main(String args[]){
Waiter waiter = new Waiter();
waiter.order("西红柿炒鸡蛋");
System.out.println("---------------");
waiter.order("酸辣土豆丝");
}
}

我们运行一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
markdown复制代码客人点餐:西红柿炒鸡蛋
开始烹饪:西红柿炒鸡蛋
先炒鸡蛋
再炒西红柿
...
西红柿炒鸡蛋出锅
西红柿炒鸡蛋上桌啦,请您品尝!
---------------
客人点餐:酸辣土豆丝
开始烹饪:酸辣土豆丝
先放葱姜蒜
再放土豆丝
...
酸辣土豆丝出锅
酸辣土豆丝上桌啦,请您品尝!

4、优点

撸过代码后,我们发现替换原则的几个优点。

里氏替换原则的核心思想就是继承,所以优点就是继承的优点。

代码重用
通过继承父类,我们可以重用很多代码,例如厨师烹饪前的准备工作和出锅。

减少创建类的成本,每个子类都拥有父类的属性和方法。

易维护易扩展
通过继承,子类可以更容易扩展功能。

也更容易维护了,公用方法都在父类中,特定的方法都在特定的子类中。

5、缺点

同上可知,它的缺点就是继承的缺点。

破坏封装
继承是侵入性的,所以会让子类与父类之间紧密耦合。

子类不能改变父类
可能造成子类代码冗余、灵活性降低,因为子类拥有父类的所有方法和属性。

6、嘻哈说

接下来,请您欣赏里氏替换原则的原创歌曲。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
makefile复制代码嘻哈说:里氏替换原则
作曲:懒人
作词:懒人
Rapper:懒人

隔壁的说唱歌手可以在乐库播放的beat freestyle歌曲
他们表演默契得体还充满乐趣
非抽象重写不是合理
抽象的重写不需客气
这是属于他们哲理
继承是里氏替换的核心想法
引用父类的地方透明使用子类会让代码更加强大
子类可以有自己特有方法
重载父类时形参更加的广大
不然可能覆盖父类方法
重写抽象方法时返回值类型要往下
因为类向上转换可以把心放下
八大菜系每个厨师都有自己拿手的
那些共有基本功也都掌握透彻
优点是易扩展易维护自动继承父类拥有的

试听请点击这里

闲来无事听听曲,知识已填脑中去;

学习复习新方式,头戴耳机不小觑。

本文转载自: 掘金

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

mongodb基础知识笔记

发表于 2018-08-08

MongoDB介绍#

MongoDB是一个基于分布式文件存储的开源文档数据库。由C++语言编写。旨在为WEB应用提供高性能、高可用性和高伸缩数据存储解决方案。

MongoDB优点

MongoDB优点

MongoDB优点

MongoDB使用场景#

  • 数据缓存

由于性能很高,MongoDB适合作为信息基础设施的缓存层。在系统重启之后,由MongoDB搭建的持久化缓存层可以避免下层的数据源过载。

  • 对象和json存储

MongoDB的BSON(二进制JSON)数据格式非常适合文档化格式的存储及查询,而且JSON格式存储最接近真实对象模型,对开发者友好,方便快速开发迭代,灵活的模式让你不在为了不断变化的需求而去频繁修改数据库字段和结构。

  • 高伸缩性场景

MongoDB通过分片集群,使MongoDB服务能力易于水平扩展。

  • 弱事务类型业务

MongoDB不支持多文档事务,所以像银行系统这种需要大量原子性复杂事务的程序不适合使用MongoDB。(注:MongoDB 4.0将支持跨文档的事务)。

mongodb版本特性介绍

mongodb版本特性介绍

MongoDB概念#

通过和关系型数据库mysql的对照,让我们更容易的理解MongoDB的一些概念

MongoDB概念 关系型数据库(sql)概念 说明
database database 数据库
table collection 数据库表/集合
row document 数据行/文档
column filed 数据字段/域
index index 索引

MongoDB数据关系图

MongoDB数据关系图

数据库#

  • 一个MongoDB中可以建立多个数据库。
  • MongoDB的默认数据库为”db”,该数据库存储在data目录中

集合#

  • 集合名不能以”system.”开头
  • 关系型数据库中的表(table)中的每一条数据(row)的格式是事先约定好的的,而MongoDB中的集合(collection)中文档(document)的数据格式是不固定的,也就是说我们可以将如下数据插入统一文档中。
1
2
复制代码{"site":"www.wuhuan.me"}
{"site":"www.baidu.com","name":"百度"}

文档#

  • 文档中的值不仅可以是在双引号里面的字符串,还可以是其他几种数据类型(甚至可以是整个嵌入的文档)

例如:在关系型数据库中有一张students表和course表,表的结构和数据如下:

students表

id name sex age
1 李雷 0 12
2 韩梅梅 1 12

course表

id course_id course_name score user_id
1 1 语文 99 1
2 2 数学 100 1
3 1 语文 96 2
4 2 数学 98 3

以上数据和结构在MongoDB中可以使用内嵌文档来表示(一对多)的关系:

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
复制代码{
"_id":ObjectId("5349b4ddd2781d08c09890f3"),
"name":"李雷",
"sex":"0",
"age":"12",
"course":[{
"course_id":1,
"course_name":"语文",
"score":99,
},{
"course_id":2,
"course_name":"数学",
"score":100,
}]
}
{
"_id":ObjectId("5349b4ddd2781d08c09890f4"),
"name":"韩梅梅",
"sex":"1",
"age":"12",
"course":[{
"course_id":1,
"course_name":"语文",
"score":96,
},{
"course_id":2,
"course_name":"数学",
"score":98,
}]
}
  • 文档中的键/值对是有序的,文档中的键是不能重复,且区分大小写。

数据类型#

数据类型 描述
String 字符串。存储数据常用的数据类型。在 MongoDB 中,UTF-8 编码的字符串才是合法的。
Integer 整型数值。用于存储数值。根据你所采用的服务器,可分为 32 位或 64 位。
Boolean 布尔值。用于存储布尔值(真/假)。
Double 双精度浮点值。用于存储浮点值。
Min/Max keys 将一个值与 BSON(二进制的 JSON)元素的最低值和最高值相对比。
Array 用于将数组或列表或多个值存储为一个键。
Timestamp 时间戳。记录文档修改或添加的具体时间。
Object 用于内嵌文档。
Null 用于创建空值。
Symbol 符号。该数据类型基本上等同于字符串类型,但不同的是,它一般用于采用特殊符号类型的语言。
Date 日期时间。用 UNIX 时间格式来存储当前日期或时间。你可以指定自己的日期时间:创建 Date 对象,传入年月日信息。
Object ID 对象 ID。用于创建文档的 ID。
Binary Data 二进制数据。用于存储二进制数据。
Code 代码类型。用于在文档中存储 JavaScript 代码。
Regular expression 正则表达式类型。用于存储正则表达式。

ObjectId#

MongoDB文档必须有一个默认的_id键,且在一个集合里_id始终唯一。_id键的值可以是任何类型的,默认是个ObjectId对象,它由MongoDB数据库自动创建。MongoDB使用objectId而不是使用常规做法(自增主键)主要原因是,在多个服务器(分布式)同步自动增加主键费力费时。

ObjectId由12个字节的BSON组成

  • 前4个字节表示时间戳
  • 接下来的3个字节是机器标识码
  • 紧接的两个字节由进程id组成(PID)
  • 最后三个字节是随机数。

创建新的ObjectId

我们可以在命令行通过如下语句来创建一个新的ObjectId

1
复制代码> newId=ObjectId()

上面语句将返回一个唯一的_id

1
复制代码ObjectId("1249b4ddd2712d08c09890f3")

也可以使用生成的ObjectId替换MongoDB自动生成的ObjectId。

MongoDB基本使用#

安装数据库#

在windows安装MongoDB比较简单在官网MongoDB下载地址下载对应的windows安装包一键安装就行了。

安装完之后记得将MongoDB安装目录下的bin目录加入到系统的环境变量中。

启动数据库#

启动数据库使用mongod命令

  • 方式一:普通方式启动
1
复制代码> mongod --dbpath  E:\MongoDB\data\db  #不使用默认端口的话可以加上--port=[端口号]参数

E:\data\db为数据文件路径

  • 方式二:通过配置文件启动
1
复制代码> mongod --config E:\MongoDB\mongo.conf

E:\MongoDB\mongo.conf为配置文件路径,配置文件内容为:

1
2
3
4
5
6
7
8
9
10
复制代码# 服务端口
port=27017
# 数据文件路径
dbpath=E:\mongondb\data\db
# 日志文件路径
logpath=E:\mongondb\log\mongon.log
# 打开日志输出操作
logappend=true
# 不使用任何的验证方式登录
noauth=true

连接数据库#

连接数据库使用mongo [,链接字符串]连接url的标准语法如下

1
复制代码mongodb://[username:password@]host[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]
  • 登录本地默认数据库服务器,无用户名密码,端口默认27017,链接默认的db数据库
1
复制代码> mongo mongodb://localhost/db

或者

1
复制代码> mongo
  • 使用用户名admin、密码123456,登录本地端口为27017的test数据库。
1
复制代码> mongo mongodb://admin:123456@localhost:27017/test

创建数据库#

创建数据库使用use [数据库名],例如创建一个test123的数据库

1
2
3
4
复制代码> use test123
switched to db test123
> db
test123

显示当前所有的数据库可以使用命令show dbs

1
2
3
复制代码> show dbs
db 0.001GB
local 0.000GB

怎么没有我们刚才创建的test123呢?那是因为数据库中还没有内容,我们向test123中插入db.[集合名称].insert(json格式的数据对象)一条数据,再看看!

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码> show dbs
db 0.001GB
local 0.000GB
> use test123
switched to db test123
> db
test123
> db.coll.insert({"title":"not data!"})
WriteResult({ "nInserted" : 1 })
> show dbs
db 0.001GB
local 0.000GB
test123 0.000GB

查看db.[集合名称].find()刚才添加的数据

1
2
3
4
复制代码> use test123
switched to db test123
> db.coll.find()
{ "_id" : ObjectId("5a66e39914fea5f8ff237420"), "title" : "not data!" }

使用use命令如果数据库不存在则创建,存在则切换到指定的数据库。

删除数据库#

删除数据库使用db.dropDatabase()函数进行

首先查看所有的数据库

1
2
3
4
复制代码> show dbs
db 0.001GB
local 0.000GB
test123 0.000GB

接着切换到要删除的数据库test123

1
2
复制代码> use test123
switched to db test123

删除当前数据库

1
2
复制代码> db.dropDatabase()
{ "dropped" : "test123", "ok" : 1 } #删除成功

数据增加#

数据添加方法:insert(),insertOne(),insertMany()

添加一条数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码/** insert()方法 **/

> db.person.insert({name:"张三",age:18,sex:"男"});
WriteResult({ "nInserted" : 1 })
> db.person.find() });
{ "_id" : ObjectId("5a7941c65f6d5986321c8416"), "name" : "张三", "age" : 18, "sex" : "男" }

/** insertOne()方法插入一条数据 **/

> db.person.insertOne({name:"张三",age:18,sex:"男"});
{ dered:true})
"acknowledged" : true,
"insertedId" : ObjectId("5a7965855f6d5986321c8422")
}
> db.person.find()
{ "_id" : ObjectId("5a7965855f6d5986321c8422"), "name" : "张三", "age" : 18, "sex" : "男" }
>

添加多条数据

方式一 把要插入的数据放在一个数组中进行批量插入

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
复制代码
/** insert()方法 **/

> db.person.insert( [ {name:"张三",age:18,sex:"男"}, {name:"李四",age:21,sex:"女"}, {name:"王五",age:20,sex:"男"}, {name:"赵六",age:19,sex:"女"} ],{ordered:true})
BulkWriteResult({
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 4,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
})

/** insertMany()方法 **/

> db.person.insertMany( [ {name:"张三",age:18,sex:"男"}, {name:"李四",age:21,sex:"女"}, {name:"王五",age:20,sex:"男"}, {name:"赵六",age:19,sex:"女"} ],{ordered:true})
{
"acknowledged" : true,
"insertedIds" : [
ObjectId("5a7969ec5f6d5986321c8430"),
ObjectId("5a7969ec5f6d5986321c8431"),
ObjectId("5a7969ec5f6d5986321c8432"),
ObjectId("5a7969ec5f6d5986321c8433")
]
}

多加了一个参数{ordered:true}表示有序插入, 有序插入碰到异常时它会直接返回,不会继续插入数组中其余的文档记录。不加此参数或者{ordered:false}为无序插入, 无序的插入会在遇到异常时继续执行

方式二 使用bluk对象进行数据的批量添加

  • 第一步:初始化一个批量操作对象
1
复制代码var bulk = db.person.initializeUnorderedBulkOp();
  • 第二步:把要添加的数据添加到bulk对象中
1
2
3
复制代码bulk.insert({name:"赵六",age:19,sex:"女"});
bulk.insert({name:"赵六",age:19,sex:"女"});
bulk.insert({name:"赵六",age:19,sex:"女"});
  • 第三步:真正添加到数据库的方法
1
复制代码bulk.execute();

插入文档你也可以使用 db.集合名称.save(document) 命令。如果不指定 _id 字段 save() 方法类似于 insert() 方法。如果指定 _id 字段,则会更新该 _id 的数据。

insert()方法既可以插入一个数组也可插入一个对象,insertOne()方法只能插如一个对象,insertMany()只能插入一个数组,insert()返回插入成功的记录条数,而insertOne()和insertMany()方法返回成功标志和插入成功的_objectId

数据查询#

查询命令:find(),findOne()

findOne()方法查询的结果已经格式化输出了,find()方法要想格式化输出数据调用pertty()修饰查询方法也能达到相同的效果。

举例:db.person.find({age:18})
查询年龄等于18的人的所有信息

举例:db.person.find({age:{$gt:18}},{name:1,sex:1})
查询db数据库中person集合中年龄大于18的人的姓名和性别

说明:如果年龄小于18可以使用$lt操作符,第二个参数{name:1,sex:1}里面表示显示的字段,如果不想显示某个字段那么就不用写如果第二个参数是{name:1},那么表示只显示姓名字段,如果整个第二个参数都不写,那么默认显示所有的字段

修饰查询的方法:limit()【限制条数】,sort()【排序】,skip()【跳过】,pretty()【美化格式】

举例:db.person.find({age:{$gt:18}},{name:1,sex:1,age:1}).sort({age:-1}).limit(3).skip(1).pretty()
查询db数据库中person集合中年龄大于18的人的姓名和性别,然后按照年龄降序排列,然后取排列后的数据的前三条,然后再跳过一条数据后的集合

说明:sort({age:-1})中的【-1】表示降序排列,如果升序排列写成sort({age:1})就可以。

数据删除#

删除方法:remove(),drop()

1、remove()和drop()方法的区别

举例:db.person.remove({})
remove方法中传递一个空数对象,会删掉db数据库中的person集合中的所有的文档,但是不会删除索引

举例:db.person.drop()
会删除db数据库中的person集合中的所有的文档,并且还会删除person集合中所有的索引。效率更高。

2、删除匹配条件的文档

举例:db.person.remove({name:"张三"})
删除db数据库中person集合中name等于张三的所有文档。

3、删除一条记录

举例:
方法1 db.person.remove({name:"张三"},{justOne:true});
方法2 db.person.remove({name:"张三"},1);

只删除一个匹配条件的文档

数据修改#

修改方法:update()

1、$set操作符
举例:db.person.update({name:"张三"},{$set:{age:19}})
修改名字为张三的人的年龄为19岁,只修改一条记录

2、$currentDate操作符的作用
举例:db.person.update({name:"张三"},{ $set:{age:"123456"},$currentDate: { lastModified: true }})
为当前修改的文档添加一个最后修改时间的字段

3、{multi:true}参数的作用
举例:db.person.update({name:"张三"},{$set:{age:20},$currentDate: { lastModified: true }},{multi:true})
默认情况下只修改符合条件的一个文档,如果多个文档符合条件并且都要修改只需要添加第三个参数{multi:true}就可以修改多个文档了。

4、upsert选项的作用
举例:db.person.update({name:"张三"},{name:'张三三',age:20,sex:"男"},{upsert:true})
默认情况下匹配不到更新条件的文档,update将不做任何操作,如果添加了{upsert:true}参数,在没有找到匹配文档的情况下,它将会插入一个新的文档。

注意:mongondb在修改数据的时候回根据数据的类型自动修改文档中原始的数据类型,例如一个文档中的年龄为数字类型,你修改这个记录的时候把年龄传入一个字符串,那么此文档中年龄字段的类型就变成了字符串类型。

索引#

索引通常能够极大的提高查询的效率,就像书的目录一样,如果没有索引mongodb就会去扫描集合中的每个文件并选取符合查询条件的数据,在数据量大的时候这种查询相率很低下

使用db.集合名称.getIndexes()获取集合索引

1
2
3
4
5
6
7
8
9
10
11
复制代码> db.person.getIndexes()
[
{ //person集合的默认索引
"v" : 1, //升序排列
"key" : {
"_id" : 1 //索引列
},
"name" : "_id_", //索引名称
"ns" : "test.person" //指定集合
}
]

创建索引

创建索引的方法:createIndex()

举例:db.person.createIndex({"name":1})
在person集合中针对name字段创建一个升序排列的索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码> db.person.getIndexes()
[
{ // 默认索引
"v" : 1,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "test.person"
},
{ //新创建的索引
"v" : 1,
"key" : {
"name" : 1
},
"name" : "name_1",
"ns" : "test.person"
}
]

删除索引

删除索引使用命令:dropIndex()
举例:db.person.dropIndexes({“name”:1})

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
复制代码> db.person.getIndexes() //查询索引
[
{
"v" : 1,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "test.person"
},
{
"v" : 1,
"key" : {
"name" : 1
},
"name" : "name_1",
"ns" : "test.person"
}
]
> db.person.dropIndex({"name":1}) //删除
{ "nIndexesWas" : 2, "ok" : 1 }
> db.person.getIndexes() //查询索引
[
{
"v" : 1,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "test.person"
}
]

删除全部索引使用命令:dropIndexes()
举例:db.person.dropIndexes()

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
复制代码> db.person.getIndexes() //查询索引
[
{
"v" : 1,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "test.person"
},
{
"v" : 1,
"key" : {
"name" : 1
},
"name" : "name_1",
"ns" : "test.person"
}
]
> db.person.dropIndexes() //删除全部索引
{
"nIndexesWas" : 2,
"msg" : "non-_id indexes dropped for collection",
"ok" : 1
}
> db.person.getIndexes() //查询索引
[
{
"v" : 1,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "test.person"
}
]

删除全部索引指的是:name为非_id_的索引(默认索引)

导出数据文件#

1
复制代码mongodump -h IP --port 端口 -u 用户名 -p 密码 -d 数据库 -o 文件存在路径
  • 如果没有用户谁,可以去掉-u和-p。
  • 如果导出本机的数据库,可以去掉-h。
  • 如果是默认端口,可以去掉–port。
  • 如果想导出所有数据库,可以去掉-d。

导出全部数据数据库

1
复制代码mongodump -h 127.0.0.1 -o E:\mongondb\dump

导入数据文件#

1
复制代码> mongorestore -h IP --port 端口 -u 用户名 -p 密码 -d 数据库 --drop 文件存在路径

–drop的意思是,先删除所有的记录,然后恢复。

导入全部数据库

1
复制代码> mongorestore E:\mongondb\dump

导入test123数据库

1
复制代码> mongorestore -d user E:\mongondb\dump\test123  #test123这个数据库的备份路径

本文完

本文如有误,请不吝赐教!

原文标题:mongodb基础知识原文链接:www.wuhuan.me/2018/01/22/…
版权声明:自由转载-非商用-非衍生-保持署名及原文链接 | Creative Commons BY-NC-ND 3.0

本文转载自: 掘金

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

Redis协议规范(译文)

发表于 2018-08-08

原文地址: haifeiWu的博客
博客地址:www.hchstudio.cn
欢迎转载,转载请注明作者及出处,谢谢!

Redis客户端使用名为RESP(Redis序列化协议)的协议与Redis服务器进行通信。 虽然该协议是专为Redis设计的,但它可以用于其他CS软件项目的通讯协议。

RESP是以下几方面的考虑:

  • 易于实现
  • 快速解析
  • 可读性高

RESP可以序列化不同的数据类型,如整型,字符串,数组。 还有一种特定的错误类型。 请求将要执行的命令作为字符串数组从Redis客户端发送到Redis服务器。Redis使用特定数据类型的命令进行回复。

RESP是二进制安全的,不需要处理从一个进程传输到另一个进程的批量数据,因为它使用前缀长度来传输批量数据。

注意: 此处概述的协议仅用于客户端 - 服务器通信。 Redis Cluster使用不同的二进制协议,以便在节点之间交换消息。

网络层

客户端连接到Redis服务器,是创建TCP连接到端口6379。
虽然RESP在技术上是非TCP特定的,但在Redis的上下文中,协议仅用于TCP连接(或类似的面向流的连接,如Unix套接字)。

请求 - 响应模型

Redis接受由不同参数组成的命令。 收到命令后,将对其进行处理并将回复发送回客户端。
这是最简单的模型,但有两个例外:

  • Redis支持流水线操作(本文档稍后介绍)。 因此,客户端可以一次发送多个命令,并等待稍后的回复。
  • 当Redis客户端处于 Pub/Sub 时,协议会更改语义并成为推送协议,即客户端不再需要发送命令,因为服务器会在它们接收到命令时发自动向客户端发送新消息。

排除上述两个例外,Redis协议是一个简单的请求 - 响应协议。

RESP 协议描述

RESP协议在Redis 1.2中引入,但它成为与Redis 2.0中的Redis服务器通信的标准方式。 这是每一个Redis客户端中应该实现的协议。

RESP实际上是一个支持以下数据类型的序列化协议:单行字符串,错误信息,整型,多行字符串和数组。
RESP在Redis中用作请求 - 响应协议的方式如下:

  • 客户端将命令作为字符串数组发送到Redis服务器。
  • 服务器根据命令实现回复一种RESP类型数据。

在 RESP 中, 一些数据的类型通过它的第一个字节进行判断:

  • 单行回复:回复的第一个字节是 “+”
  • 错误信息:回复的第一个字节是 “-“
  • 整形数字:回复的第一个字节是 “:”
  • 多行字符串:回复的第一个字节是 “$”
  • 数组:回复的第一个字节是 “*“

此外,RESP能够使用稍后指定的Bulk Strings或Array的特殊变体来表示Null值。
在RESP中,协议的不同部分始终以“\ r \ n”(CRLF)结束。

RESP 单行字符串(简单字符串)

简单字符串按以下方式编码:加号字符,后跟不能包含CR或LF字符的字符串(不允许换行),由CRLF终止(即“\ r \ n”)。

Simple Strings用于以最小的开销传输非二进制安全字符串。 例如,许多Redis命令成功回复时只有“OK”,因为RESP 单行字符串使用以下5个字节进行编码:

1
复制代码"+OK\r\n"

为了发送二进制安全字符串,使用RESP 多行字符串代替。

当Redis使用Simple String回复时,客户端库应该向调用者返回一个字符串,该字符串由“+”之后的第一个字符组成,直到字符串结尾,不包括最终的CRLF字节。

RESP 错误信息

RESP具有错误的特定数据类型。 实际上错误与RESP 单行字符串完全相同,但第一个字符是减号’ - ‘字符而不是加号。

RESP中单行字符串和错误之间的真正区别在于客户端将错误视为异常,组成错误类型的字符串是错误消息本身。
基本格式如下:

1
复制代码"-Error message\r\n"

错误回复仅在发生错误时发送,例如,如果您尝试对错误的数据类型执行操作,或者命令不存在等等。 收到错误回复时,客户端应将异常抛出。

以下是错误回复的示例:

1
2
复制代码-ERR unknown command 'foobar'
-WRONGTYPE Operation against a key holding the wrong kind of value

“ - ”之后的第一个单词,直到第一个空格或换行符,表示返回的错误类型。 这只是Redis使用的约定,不是RESP错误格式的一部分。

例如,ERR是一般错误,而WRONGTYPE是一个更具体的错误,意味着客户端尝试对错误的数据类型执行操作。 这称为错误前缀,是一种允许客户端理解服务器返回的错误类型的方法,而不依赖于给定的确切消息,这可能随时间而变化。

客户端实现可以针对不同的错误返回不同类型的异常,或者可以通过直接将错误名称作为字符串提供给调用者来提供捕获错误的通用方法。

但是,这样的功能不应该被认为是至关重要的,因为它很少有用,并且有限的客户端实现可能只返回通用的错误条件,例如false。

RESP 整型数据

此类型只是一个CRLF终止的字符串,表示一个以“:”字节为前缀的整数。 例如“:0 \ r \ n”或“:1000 \ r \ n”是整数回复。
许多Redis命令返回RESP 整型,如INCR,LLEN和LASTSAVE。

返回的整数没有特殊含义,它只是INCR的增量编号,LASTSAVE的UNIX时间等等。 但是,返回的整数应保证在有符号的64位整数范围内。

整数回复也被广泛使用,以便返回真或假。 例如,EXISTS或SISMEMBER之类的命令将返回1表示true,0表示false。

如果实际执行操作,其他命令(如SADD,SREM和SETNX)将返回1,否则返回0。

以下命令将回复整数回复:SETNX,DEL,EXISTS,INCR,INCRBY,DECR,DECRBY,DBSIZE,LASTSAVE,RENAMENX,MOVE,LLEN,SADD,SREM,SISMEMBER,SCARD。

RESP 多行字符串

多行字符串用于表示长度最大为512 MB的单个二进制安全字符串。

多行字符串按以下方式编码:

  • 一个“$”字节后跟组成字符串的字节数(一个前缀长度),由CRLF终止。
  • 字符串数据。
  • 最终的CRLF。

所以字符串“foobar”的编码如下:

1
复制代码"$6\r\nfoobar\r\n"

当只是一个空字符串时:

1
复制代码"$0\r\n\r\n"

RESP 多行字符串也可用于使用用于表示Null值的特殊格式来表示值的不存在。 在这种特殊格式中,长度为-1,并且没有数据,因此Null表示为:

1
复制代码"$-1\r\n"

当服务器使用Null 多行字符串回复时,客户端库API不应返回空字符串,而应返回nil对象。 例如,Ruby库应返回’nil’,而C库应返回NULL(或在reply对象中设置特殊标志),依此类推。

RESP 数组

客户端使用RESP 数组将命令发送到Redis服务器。 类似地,某些Redis命令将元素集合返回给客户端使用RESP 数组是回复类型。 一个例子是LRANGE命令,它返回列表的元素。

RESP数组使用以下格式发送:

  • *字符作为第一个字节,后跟数组中的元素数作为十进制数,后跟CRLF。
  • 数组的每个元素的附加RESP类型。

所以空数组就是以下内容:

1
复制代码"*0\r\n"

那么两个RESP批量字符串“foo”和“bar”的数组编码为:

1
复制代码"*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"

正如您在数组前面加上* CRLF部分之后所看到的那样,组成数组的其他数据类型将一个接一个地连接起来。 例如,三个整数的数组编码如下:

1
复制代码"*3\r\n:1\r\n:2\r\n:3\r\n"

数组可以包含混合类型,元素不必具有相同的类型。 例如,四个整数和批量字符串的列表可以编码如下:

1
2
3
4
5
6
7
复制代码*5\r\n
:1\r\n
:2\r\n
:3\r\n
:4\r\n
$6\r\n
foobar\r\n

服务器发送的第一行是* 5 \ r \ n,以指定将跟随五个回复。 然后发送构成多重回复项目的每个回复。

Null 数组的概念也存在,并且是指定Null值的替代方法(通常使用Null 多行字符串,但由于历史原因,我们有两种格式)。

例如,当BLPOP命令超时时,它返回一个计数为-1的Null数组,如下例所示:

1
复制代码"*-1\r\n"

当Redis使用Null数组回复时,客户端库API应返回空对象而不是空数组。 这是区分空列表和不同条件(例如BLPOP命令的超时条件)所必需的。

RESP中可以使用数组中嵌套数组。 例如,两个数组的数组编码如下:

1
2
3
4
5
6
7
8
复制代码*2\r\n
*3\r\n
:1\r\n
:2\r\n
:3\r\n
*2\r\n
+Foo\r\n
-Bar\r\n

第二个元素是Null。 客户端库应返回如下内容:

1
复制代码["foo",nil,"bar"]

注意,这不是前面部分中所述的例外,而只是进一步指定协议的示例。

发送命令到 Redis 服务端

既然熟悉RESP序列化格式,那么编写Redis客户端库的实现将很容易。 我们可以进一步讲述客户端和服务器之间的交互如何工作:

  • 客户端向Redis服务器发送仅由Bulk Strings组成的RESP阵列。
  • Redis服务器回复发送任何有效RESP数据类型作为回复的客户端。

因此,例如,典型的交互可以是以下所示。

客户端发送命令LLEN mylist以获取存储在密钥mylist中的列表长度,服务器回复一个Integer回复,如下例所示(C:是客户端,S:服务器)。

1
2
3
4
5
6
复制代码C: *2\r\n
C: $4\r\n
C: LLEN\r\n
C: $6\r\n
C: mylist\r\n
S: :48293\r\n

通常我们将协议的不同部分与换行符分开以简化,但实际的交互是客户端发送 * 2 \ r \ n 4 \ r \ nLLEN \ r \ n 6 \ r \ nmylist \ r \ n 整体。

小结

这是楼主第一次尝试翻译一篇技术文档,相对来说技术文档的英文阅读起来还是比较舒服的,相信有了第一次尝试,之后肯定会越来越顺利。由于楼主水平有限,文章中难免有纰漏,期望小伙伴的指出,感谢……。

参考链接

  • Redis Protocol specification

本文转载自: 掘金

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

四张图带你了解Tomcat系统架构--让面试官颤抖的Tomc

发表于 2018-08-06

俗话说,站在巨人的肩膀上看世界,一般学习的时候也是先总览一下整体,然后逐个部分个个击破,最后形成思路,了解具体细节,Tomcat的结构很复杂,但是 Tomcat 非常的模块化,找到了 Tomcat最核心的模块,问题才可以游刃而解,了解了Tomcat的整体架构对以后深入了解Tomcat来说至关重要!

一、Tomcat顶层架构

先上一张Tomcat的顶层结构图(图A),如下:

这里写图片描述

Tomcat中最顶层的容器是Server,代表着整个服务器,从上图中可以看出,一个Server可以包含至少一个Service,用于具体提供服务。

Service主要包含两个部分:Connector和Container。从上图中可以看出 Tomcat 的心脏就是这两个组件,他们的作用如下:

1、Connector用于处理连接相关的事情,并提供Socket与Request和Response相关的转化; 2、Container用于封装和管理Servlet,以及具体处理Request请求;

一个Tomcat中只有一个Server,一个Server可以包含多个Service,一个Service只有一个Container,但是可以有多个Connectors,这是因为一个服务可以有多个连接,如同时提供Http和Https链接,也可以提供向相同协议不同端口的连接,示意图如下(Engine、Host、Context下边会说到):

这里写图片描述

多个 Connector 和一个 Container 就形成了一个 Service,有了 Service 就可以对外提供服务了,但是 Service 还要一个生存的环境,必须要有人能够给她生命、掌握其生死大权,那就非 Server 莫属了!所以整个 Tomcat 的生命周期由 Server 控制。

另外,上述的包含关系或者说是父子关系,都可以在tomcat的conf目录下的server.xml配置文件中看出,下图是删除了注释内容之后的一个完整的server.xml配置文件(Tomcat版本为8.0)

这里写图片描述

详细的配置文件文件内容可以到Tomcat官网查看:tomcat.apache.org/tomcat-8.0-…

上边的配置文件,还可以通过下边的一张结构图更清楚的理解:

这里写图片描述

Server标签设置的端口号为8005,shutdown=”SHUTDOWN” ,表示在8005端口监听“SHUTDOWN”命令,如果接收到了就会关闭Tomcat。一个Server有一个Service,当然还可以进行配置,一个Service有多个,Service左边的内容都属于Container的,Service下边是Connector。

二、Tomcat顶层架构小结:

(1)Tomcat中只有一个Server,一个Server可以有多个Service,一个Service可以有多个Connector和一个Container;
(2) Server掌管着整个Tomcat的生死大权;
(4)Service 是对外提供服务的;
(5)Connector用于接受请求并将请求封装成Request和Response来具体处理;
(6)Container用于封装和管理Servlet,以及具体处理request请求;

知道了整个Tomcat顶层的分层架构和各个组件之间的关系以及作用,对于绝大多数的开发人员来说Server和Service对我们来说确实很远,而我们开发中绝大部分进行配置的内容是属于Connector和Container的,所以接下来介绍一下Connector和Container。

三、Connector和Container的微妙关系

由上述内容我们大致可以知道一个请求发送到Tomcat之后,首先经过Service然后会交给我们的Connector,Connector用于接收请求并将接收的请求封装为Request和Response来具体处理,Request和Response封装完之后再交由Container进行处理,Container处理完请求之后再返回给Connector,最后在由Connector通过Socket将处理的结果返回给客户端,这样整个请求的就处理完了!

Connector最底层使用的是Socket来进行连接的,Request和Response是按照HTTP协议来封装的,所以Connector同时需要实现TCP/IP协议和HTTP协议!

Tomcat既然处理请求,那么肯定需要先接收到这个请求,接收请求这个东西我们首先就需要看一下Connector!

四、Connector架构分析

Connector用于接受请求并将请求封装成Request和Response,然后交给Container进行处理,Container处理完之后在交给Connector返回给客户端。

因此,我们可以把Connector分为四个方面进行理解:

(1)Connector如何接受请求的?
(2)如何将请求封装成Request和Response的?
(3)封装完之后的Request和Response如何交给Container进行处理的?
(4)Container处理完之后如何交给Connector并返回给客户端的?

首先看一下Connector的结构图(图B),如下所示:

这里写图片描述

Connector就是使用ProtocolHandler来处理请求的,不同的ProtocolHandler代表不同的连接类型,比如:Http11Protocol使用的是普通Socket来连接的,Http11NioProtocol使用的是NioSocket来连接的。

其中ProtocolHandler由包含了三个部件:Endpoint、Processor、Adapter。

(1)Endpoint用来处理底层Socket的网络连接,Processor用于将Endpoint接收到的Socket封装成Request,Adapter用于将Request交给Container进行具体的处理。

(2)Endpoint由于是处理底层的Socket网络连接,因此Endpoint是用来实现TCP/IP协议的,而Processor用来实现HTTP协议的,Adapter将请求适配到Servlet容器进行具体的处理。

(3)Endpoint的抽象实现AbstractEndpoint里面定义的Acceptor和AsyncTimeout两个内部类和一个Handler接口。Acceptor用于监听请求,AsyncTimeout用于检查异步Request的超时,Handler用于处理接收到的Socket,在内部调用Processor进行处理。

至此,我们应该很轻松的回答(1)(2)(3)的问题了,但是(4)还是不知道,那么我们就来看一下Container是如何进行处理的以及处理完之后是如何将处理完的结果返回给Connector的?

五、Container架构分析

Container用于封装和管理Servlet,以及具体处理Request请求,在Connector内部包含了4个子容器,结构图如下(图C):

这里写图片描述

4个子容器的作用分别是:

(1)Engine:引擎,用来管理多个站点,一个Service最多只能有一个Engine;
(2)Host:代表一个站点,也可以叫虚拟主机,通过配置Host就可以添加站点;
(3)Context:代表一个应用程序,对应着平时开发的一套程序,或者一个WEB-INF目录以及下面的web.xml文件;
(4)Wrapper:每一Wrapper封装着一个Servlet;

下面找一个Tomcat的文件目录对照一下,如下图所示:

这里写图片描述

Context和Host的区别是Context表示一个应用,我们的Tomcat中默认的配置下webapps下的每一个文件夹目录都是一个Context,其中ROOT目录中存放着主应用,其他目录存放着子应用,而整个webapps就是一个Host站点。

我们访问应用Context的时候,如果是ROOT下的则直接使用域名就可以访问,例如:www.ledouit.com,如果是Host(webapps)下的其他应用,则可以使用www.ledouit.com/docs进行访问,当然默认指定的根应用(ROOT)是可以进行设定的,只不过Host站点下默认的主营用是ROOT目录下的。

看到这里我们知道Container是什么,但是还是不知道Container是如何进行处理的以及处理完之后是如何将处理完的结果返回给Connector的?别急!下边就开始探讨一下Container是如何进行处理的!

六、Container如何处理请求的

Container处理请求是使用Pipeline-Valve管道来处理的!(Valve是阀门之意)

Pipeline-Valve是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将处理后的请求返回,再让下一个处理着继续处理。

这里写图片描述

但是!Pipeline-Valve使用的责任链模式和普通的责任链模式有些不同!区别主要有以下两点:

(1)每个Pipeline都有特定的Valve,而且是在管道的最后一个执行,这个Valve叫做BaseValve,BaseValve是不可删除的;

(2)在上层容器的管道的BaseValve中会调用下层容器的管道。

我们知道Container包含四个子容器,而这四个子容器对应的BaseValve分别在:StandardEngineValve、StandardHostValve、StandardContextValve、StandardWrapperValve。

Pipeline的处理流程图如下(图D):

这里写图片描述

(1)Connector在接收到请求后会首先调用最顶层容器的Pipeline来处理,这里的最顶层容器的Pipeline就是EnginePipeline(Engine的管道);

(2)在Engine的管道中依次会执行EngineValve1、EngineValve2等等,最后会执行StandardEngineValve,在StandardEngineValve中会调用Host管道,然后再依次执行Host的HostValve1、HostValve2等,最后在执行StandardHostValve,然后再依次调用Context的管道和Wrapper的管道,最后执行到StandardWrapperValve。

(3)当执行到StandardWrapperValve的时候,会在StandardWrapperValve中创建FilterChain,并调用其doFilter方法来处理请求,这个FilterChain包含着我们配置的与请求相匹配的Filter和Servlet,其doFilter方法会依次调用所有的Filter的doFilter方法和Servlet的service方法,这样请求就得到了处理!

(4)当所有的Pipeline-Valve都执行完之后,并且处理完了具体的请求,这个时候就可以将返回的结果交给Connector了,Connector在通过Socket的方式将结果返回给客户端。

总结

至此,我们已经对Tomcat的整体架构有了大致的了解,从图A、B、C、D可以看出来每一个组件的基本要素和作用。我们在脑海里应该有一个大概的轮廓了!如果你面试的时候,让你简单的聊一下Tomcat,上面的内容你能脱口而出吗?当你能够脱口而出的时候,这位面试官一定会对你刮目相看的!

本文转载自: 掘金

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

为什么使用0x61c88647

发表于 2018-07-30

在Java1.4之前,ThreadLocals会导致线程之间发生竞争。在新的设计里,每一个线程都有他们自己的ThreadLocalMap,用来提高吞吐量,然而,我们仍然面临内存泄漏的可能性,因为长时间运行线程的ThreadLocalMap中的值不会被清除

在Java的早期版本中,ThreadLocals在多个线程进行访问的时候存在竞争问题,使得它们在多核应用程序中几乎无用。在Java 1.4中,引入了一个新的设计,设计者把ThreadLocals直接存储在Thread中。当我们现在调用ThreadLocal的get方法时,将会返回一个当前线程里的实例ThreadLocalMap(ThreadLocal的一个内部类)

当一个线程退出时,它会删除它ThreadLocal里的所有值。这发生在exit()方法中,垃圾回收之前,如果我们在使用ThreadLocal后忘记调用remove()方法,那么当线程退出后值还会存在。

ThreadLocalMap包含了对ThreadLocal的弱引用以及值的强引用,但是,它并不会判断ReferenceQueue里面哪些弱引用的值已经被清除,因为Entry不可能立即从ThreadLocalMap中清除。

从线程Thread的角度来看,每个线程内部都会持有一个对ThreadLocalMap实例的引用,ThreadLocalMap实例相当于线程的局部变量空间,存储着线程各自的数据,具体如下:

Entry

Entry继承自WeakReference类,是存储线程私有变量的数据结构。ThreadLocal实例作为引用,意味着如果ThreadLocal实例为null,就可以从table中删除对应的Entry。

1
2
3
4
5
6
7
复制代码class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

ThreadLocalMap

内部使用table数组存储Entry,默认大小INITIAL_CAPACITY(16),先介绍几个参数:

size:table中元素的数量。

threshold:table大小的2/3,当size >= threshold时,遍历table并删除key为null的元素,

如果删除后size >= threshold*3/4时,需要对table进行扩容。

ThreadLocal.set() 实现

1
2
3
4
5
6
7
8
9
10
11
12
复制代码public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

从上面代码中看出来:

从当前线程Thread中获取ThreadLocalMap实例。

ThreadLocal实例和value封装成Entry。

接下去看看Entry存入table数组如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

1.通过ThreadLocal的nextHashCode方法生成hash值。

1
2
3
4
复制代码private static AtomicInteger nextHashCode = new AtomicInteger();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}

从nextHashCode方法可以看出,ThreadLocal每实例化一次,其hash值就原子增加HASH_INCREMENT。

2.通过 hash & (len -1) 定位到table的位置i,假设table中i位置的元素为f。

3.如果f != null,假设f中的引用为k:

  • 如果k和当前ThreadLocal实例一致,则修改value值,返回。
  • 如果k为null,说明这个f已经是stale(陈旧的)的元素。调用replaceStaleEntry方法删除table中所有陈旧的元素(即entry的引用为null)并插入新元素,返回。
  • 否则通过nextIndex方法找到下一个元素f,继续进行步骤3。
    如果f == null,则把Entry加入到table的i位置中。
    通过cleanSomeSlots删除陈旧的元素,如果table中没有元素删除,需判断当前情况下是否要进行扩容。

4.如果f == null,则把Entry加入到table的i位置中。

5.通过cleanSomeSlots删除陈旧的元素,如果table中没有元素删除,需判断当前情况下是否要进行扩容。

table扩容

如果table中的元素数量达到阈值threshold的3/4,会进行扩容操作,过程很简单:

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
复制代码private void resize() {
//旧数组
Entry[] oldTab = table;

//旧数组长度
int oldLen = oldTab.length;
//新数组长度 = 旧数组长度*2
int newLen = oldLen * 2;
//新数组
Entry[] newTab = new Entry[newLen];
//计数
int count = 0;

for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();

if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}

setThreshold(newLen);
size = count;
table = newTab;
}

1.新建新的数组newTab,大小为原来的2倍。

2.复制table的元素到newTab,忽略陈旧的元素,假设table中的元素e需要复制到newTab的i位置,如果i位置存在元素,则找下一个空位置进行插入。

ThreadLocal.get() 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}

获取当前的线程的threadLocals。

如果threadLocals不为null,则通过ThreadLocalMap.getEntry方法找到对应的entry,如果其引用和当前key一致,则直接返回,否则在table剩下的元素中继续匹配。

如果threadLocals为null,则通过setInitialValue方法初始化,并返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}

魔数0x61c88647

  • 生成hash code间隙为这个魔数,可以让生成出来的值或者说ThreadLocal的ID较为均匀地分布在2的幂大小的数组中。
1
2
3
4
5
复制代码private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
  • 可以看出,它是在上一个被构造出的ThreadLocal的ID/threadLocalHashCode的基础上加上一个魔数0x61c88647的。
  • 这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。
  • 斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。换句话说
    (1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是1640531527也就是0x61c88647
    。
  • 通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。
  • ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。。为了优化效率。

ThreadLocal与内存泄漏

  • 之所以有关于内存泄露的讨论是因为在有线程复用如线程池的场景中,一个线程的寿命很长,大对象长期不被回收影响系统运行效率与安全。如果线程不会复用,用完即销毁了也不会有ThreadLocal引发内存泄露的问题
  • 当我们仔细读过ThreadLocalMap的源码,我们可以推断,如果在使用的ThreadLocal的过程中,显式地进行remove是个很好的编码习惯,这样是不会引起内存泄漏。
  • 如果您必须使用ThreadLocal,请确保在您完成该操作后立即删除该值,并且最好在将线程返回到线程池之前。 最佳做法是使用remove()而不是set(null),因为这将导致WeakReference立即被删除,并与值一起被删除。

本文转载自: 掘金

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

1…887888889…956

开发者博客

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