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

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


  • 首页

  • 归档

  • 搜索

equals与==运用和区别(深入源码理解)

发表于 2021-11-23

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」

双等号(==)

双等号比较的两种情况

对于==号只需要考虑两种情况:

  1. 基本数据类型使用==,比较的是值
  2. 引用数据类型使用==,比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。

源码和例子解析

Integer和Integer,Integer和int之间的比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
vbnet复制代码    Integer a1=new Integer(4321);
Integer a2=new Integer(4321);
int a3=4321;
System.out.println("a1==a2:"+(a1==a2));//结果为false,因为a1,a2是两个不同的对象,其堆中的地址值自然不同
System.out.println("a1==a3:"+(a1==a3));//结果为true,因为Java的自动拆箱机制,比较的是两个数值
System.out.println("a3==a1:"+(a3==a1));//结果为false,原因同上
Integer a4=4321;
Integer a5=4321;
/**
* Integer a4=4321;
* Integer a5=4321;
* 相当于执行了
* Integer a4=Integer.valueOf (4321) ;
* Integer a5=Integer.valueOf (4321) ;
*/
System.out.println("a4==a5:"+(a5==a4));//结果为false,相当于创建了两个对象,其地址自然不同

Integer a6=100;
Integer a7=100;
/**
* 为什么这个又不一样呢
* 要根据装箱操作的源码
* public static Integer valueOf(int i) {
* if (i >= -128 && i <= 127)
* return IntegerCache.cache[i + 127];
* //如果i的值大于-128小于127则返回一个缓冲区中已存在的一个Integer对象
* return new Integer(i);
* //否则返回 new 一个Integer 对象
* }
* 可以看出如果是-128-127之间,装箱装的是同一个对象(为了节约内存)
*所以如果是-128到127之间的装箱操作,装的都是同样的对象!
*/
System.out.println("a6==a7:"+(a6==a7));//为true
Integer a8=128;
Integer a9=128;
System.out.println("a8==a9:"+(a8==a9));//为false
Integer a10=-128;
Integer a11=-128;
System.out.println("a10==a11:"+(a10==a11));//为true
a10=127;
a11=127;
System.out.println("a10==a11:"+(a10==a11));//为true
/**
* 注意,这种是直接生成对象的!
* 没有进行装箱操作得到地址值,而是直接在堆创建生成的地址!
* 所有不同的
*/
Integer aa1=new Integer(1);
Integer aa2=new Integer(1);
System.out.println("aa1==aa2:"+(aa1==aa2));//为false

Java自动装箱拆箱机制的类型

image.png

String中==的解析

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码    String s1="12345";
String s2="12345";
String s3=new String("12345");
String s4=new String("12345");
/**
* String s1=="xxx"这种字符串创建方式,并不是在堆中创建
* 为了节省空间,是在运行时常量池,也就是在方法区中创建
* s1和s2指向的是同一个地址
*/
System.out.println("s1==s2:"+(s1==s2));//true
System.out.println("s1==s3:"+(s1==s3));//false,s1指向的是常量池的地址,s3指向的堆中的地址
System.out.println("s3==s4:"+(s3==s4));//false,s3和s4指向的是堆中不同的地址,所以false

equals

用法概述

equals没有被重写就是比较地址,相当于===

原因:
equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断。

equals源码
显然java源码对String类进行了特殊处理,String的equals以及不是比较地址值了,而是比较字符串的内容了。

image.png

image.png

  1. 首先两个对象在堆里面,地址值就不可能相等,所以要比较两个对象内容而不是地址时,必然要重写equals方法,比较内容
  2. hashCode默认情况下表示是对象的存储地址,如果两个东西相同,那么作为HashMap的key时应该是一样的,所以既然相等的规则变化了,自然要重写hashcode方法,跟随变化!

HashSet的equals

HashSet本身就重写了equals!

image.png

1
2
3
4
5
6
7
ini复制代码    HashSet<Integer> set1=new HashSet();
HashSet<Integer> set2=new HashSet();
set1.add(1);
set1.add(2);
set2.add(1);
set2.add(2);
System.out.println(set1.equals(set2));//返回true!

而且比较有意义的是
HashSet底层实现是通过HashMap实现的
image.png

数组的equals

Array的equals用的是原生的

image.png

image.png

1
2
3
4
5
6
ini复制代码    Integer[] array1={1,2,3};
Integer[] array2={1,2,3};
System.out.println(array1.equals(array2));//false

//java.util.Arrays为我们提供了hashCode和euqals
System.out.println(Arrays.equals(array1,array2));//true

但是java.util.Arrays为我们提供了hashCode和euqals,比较的是数组中的内部元素。

image.png

image.png
ps启发:
我们也可以学习这种方式
可以在不重写的方法下写一个通用类其中的方法进行计算hashcode和equals!

JAVA中重写equals()方法为什么要重写hashcode()方法

为什么重载hashCode方法?
答案:一般的地方不需要重载hashCode,只有当类需要放在HashTable、HashMap、HashSet等等hash结构的集合时才会 重载hashCode,那么为什么要重载hashCode呢?就HashMap来说,好比HashMap就是一个大内存块,里面有很多小内存块,小内存块 里面是一系列的对象,可以利用hashCode来查找小内存块hashCode%size(小内存块数量),所以当equal相等时,hashCode必 须相等,而且如果是object对象,必须重载hashCode和equal方法。

例如我们有对象A和对象B,现在重写了equals方法,让两者相等了,那我们用散列表HashSet存储了对象A,然后用set.contains(B),是查不到B存在的!

对象重写equlas和hashcode实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
kotlin复制代码package com.company;

public class Main {
public static void main(String[] args) {
// write your code here
Person p1=new Person("l1",11,"男");
Person p2=new Person("l1",11,"男");
Person p3=new Person("l1",12,"男");
System.out.println(p1==p2);//false
System.out.println(p1.equals(p2));//true
System.out.println(p1.equals(p3));//false
}
}


class Person{
String name;
Integer age;
String sex;
@Override
public int hashCode() {
return 31*(this.name.hashCode()+31*this.age.hashCode()+31*(this.sex.hashCode()));
}

@Override
public String toString() {
return this.name+" "+this.age+" "+this.sex;
}

@Override
public boolean equals(Object obj) {
Person person1=(Person) obj;
return this.hashCode()==person1.hashCode();
}

public Person() {
}

public Person(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
}

outPut

1
2
3
4
vbnet复制代码false
true
false
Process finished with exit code 0

Ps: 如果要用equal方法,请用object<不可能为空>.equal(object<可能为空>))
"target".equals(foo)

本文转载自: 掘金

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

设计模式之策略模式

发表于 2021-11-23

「这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战」

本篇文章是设计模式专题的第二篇文章,我会将遇到的设计模式都一一总结在该专题下,我会把自己对每一种设计模式的感悟写下来,以及在实际工作中我们该如何去灵活应用这些设计模式,欢迎大家关注。本篇文章我们就来讲一讲,最为优雅的策略模式。

策略模式的简单介绍

策略模式就是我们在针对某一个功能有多种实现方式,我们可以根据环境不同或者条件不同,选择不同的实现方式。

策略模式的类图如下:

策略模式类图

解释:Context是上下文,Strategy是策略类的公共接口;ConcreteStrategy是具体策略的实现,可以有N多个,封装了具体的算法或行为,实现Strategy接口。

策略模式的具体实现思路

  • 定义一个上下文对象,可以通过上下文对象获取对应的具体实现类。
  • 定义抽象策略类,定义每个策略或算法必须具有的方法和属性,通常为接口。
  • 定义具体策略类,对抽象策略类的算法提供具体实现。

策略模式的具体实现方案

  • 创建上下文对象

Context上下文,起承上启下的作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
csharp复制代码public class Context {
   
    Strategy strategy;
   
    public Context(Strategy strategy) {
        this.strategy = strategy;
    }
   
    //上下文接口
    public void contextInterface() {
        strategy.algorithmInterface();
    }
​
}
  • 创建抽象策略类

抽象策略类,是对策略、算法的抽象,通常为接口,定义每个策略或算法必须具有的方法和属性。

1
2
3
4
5
6
csharp复制代码public abstract class Strategy {
   
    //算法方法
    public abstract void algorithmInterface();

}
  • 创建策略类的具体实现

如果是接口的话需要implements实现接口

如果一个抽象策略的实现多于四个,就需要考虑使用混合模式来解决策略类膨胀的问题

1
2
3
4
5
6
7
8
scala复制代码public class ConcreteStrategyA extends Strategy {
​
   @Override
   public void algorithmInterface() {
       System.out.println("算法A实现");
  }
​
}
1
2
3
4
5
6
7
8
scala复制代码public class ConcreteStrategyB extends Strategy {
​
   @Override
   public void algorithmInterface() {
       System.out.println("算法B实现");
  }
​
}

策略模式的优缺点

优点

  • 算法可以自由切换,灵活性强
  • 避免使用多重条件判断(如果不用策略模式我们可能会使用多重条件语句,不利于维护)
  • 扩展性极强,可以无限扩展策略

缺点

  • 策略类数量会增多,每个策略都是一个类,复用的可能性很小
  • 所有的策略类都需要对外暴露

策略模式的适用场景

适用场景:

  1. 多个类只有算法或行为上略有不同
  2. 算法需要自由切换
  3. 需要屏蔽算法规则

举例:

  • 出行方式
  • 促销方式
  • 导出方式
  • 排序方式
  • 其实我们业务中有很多地方可以用策略模式去优化,尤其是多重if…else…里边含有大量的逻辑处理
  • …

策略模式总结

策略模式是我个人非常喜欢的一种设计模式,我认为这种设计模式能让我们的代码实现起来更加优雅。我非常不喜欢那种用超长的if…else去区分策略,代码可读性非常差,不易调试,策略模式往往能够很好地去改善这种代码,我个人在工作中也非常喜欢使用策略模式。

本文转载自: 掘金

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

Python 系列 -- 网络爬虫

发表于 2021-11-23

网络爬虫的通俗理解

顾名思义,网络爬虫就是你把很多的爬虫放到网络上面去,抓取数据带回来,然后整合在一起,存储起来

网络爬虫的基本步骤

以Python语言为例:

  1. 找到需要爬取内容的网页
  2. 打开该网页的检查页面(即查看HTML代码,按F12快捷键即可进入)
  3. 在HTML代码中找到你要提取的数据
  4. 写python代码进行网页请求、解析
  5. 存储数据

举个例子

目标:爬取知乎上关于“你是如何开始能写Python爬虫?”问题的数据(问题、描述、关注数、浏览量)

image.png

其实这里对python的要求也仅限于你需要会数据类型、变量、运算符、函数、模块之类的简单语法。推荐学习网站:Python 入门指南、Python 学习- 廖雪峰的官方网站

1. 爬取 HTML 源码

这里用到的用于网页请求的库是requests,一个非常流行的http请求库。

Requests库会自动解码来自服务器的内容,大多数 unicode 字符集都能被无缝地解码。

这一切requests都能妥妥地搞定。

代码:

1
2
3
4
5
6
python复制代码import requests
headers = {'User-Agent':你的浏览器headers}
# 传入url和请求头
r = requests.get('https://www.zhihu.com/question/21358581',headers = headers)
# 响应的内容
print(r.text)

我们会接收到服务器返回的页面,requests解析后,呈现下面这样子:

这就是我们需要的html源码!

接下来要做的就是从html中抽取我们需要的四个信息。

XPath 是一门在 XML 文档中查找信息的语言,可用来在 XML 文档中对元素和属性进行遍历。

这里用到另一个有用的库xpath,xpath库可以让你轻松的使用XPath语言查找信息。

既然XPath是在XML文档中才能起作用,然而我们刚刚获取的html只是文本字符串。

接着上面代码:

1
2
python复制代码# 将html文档转换为XPath可以解析的
s = etree.HTML(r.text)

这下我们可以使用xpath库来进行信息的提取了。

xpath的使用方法这里不赘述了,大家可以网上搜搜资料,个半小时也能学会。

这里介绍一种简单的方法,你在开发者页面中找到对应信息的源码后,直接右键复制xpath地址:

接上面代码:

1
2
3
4
5
6
7
python复制代码q_content = s.xpath('//*[@class="QuestionHeader-title"]/text()')[0] # 获取问题内容
q_describe = s.xpath('//*[@class="RichText ztext"]/text()')[0] # 获取问题描述
q_numbers = s.xpath('//*[@class="NumberBoard-itemValue"]/text()') # 获取关注数和浏览量
concern_num = q_numbers[0]
browing_num = q_numbers[1]
# 打印
print('问题:',q_content,'\n','描述:',q_describe,'\n''关注数:',concern_num,'\n''浏览量:',browing_num)

最终呈现的结果:

image.png

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码import requests
from lxml import etree
headers = {'User-Agent':你的浏览器headers}
r = requests.get('https://www.zhihu.com/question/21358581',headers = headers) # 传入url和请求头
s = etree.HTML(r.text) # 将html文档转换为XPath可以解析的
q_content = s.xpath('//*[@class="QuestionHeader-title"]/text()')[0] # 获取问题内容
q_describe = s.xpath('//*[@class="RichText ztext"]/text()')[0] # 获取问题描述
q_numbers = s.xpath('//*[@class="NumberBoard-itemValue"]/text()') # 获取关注数和浏览量
concern_num = q_numbers[0]
browing_num = q_numbers[1]
# 打印
print('问题:',q_content,'\n','描述:',q_describe,'\n''关注数:',concern_num,'\n''浏览量:',browing_num)

例子内容来自于 通俗的讲,网络爬虫到底是什么? - 朱卫军的回答 - 知乎

参考文章

  • 通俗的讲,网络爬虫到底是什么? - 朱卫军的回答 - 知乎
  • 你是如何开始能写 Python 爬虫?
  • 如何入门 Python 爬虫?

本文转载自: 掘金

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

通过 for 循环,比较 Python 与 Ruby 编程思

发表于 2021-11-23

作者:Doug Turnbull

译者:豌豆花下猫@Python猫

原文:softwaredoug.com/blog/2021/1…

Ruby 与 Python 之间的差异在很大程度上可通过 for 循环看出本质。

Python 拥有for语句。对象告诉for如何进行协作,而for的循环体会处理对象返回的内容。

Ruby 则相反。在 Ruby 中,for 本身(通过 each)是对象的一个方法。调用者将for循环体传递给这个方法。

在 Python 的语言习惯中,对象模型服从于 for 循环。而在 Ruby 中,for 循环服从于对象模型。

也就是说,在 Python 中,如果你想自定义迭代的过程,可以让对象告诉解释器该如何作迭代:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码class Stuff:
def __init__(self):
self.a_list = [1,2,3,4]
self.position = 0
def __next__(self):
try:
value = self.a_list[self.position]
self.position += 1
return value
except IndexError:
self.position = 0
raise StopIteration
def __iter__(self):
return self

在这里,Stuff 使用 __next__ 和 __iter__ 魔术方法使自身可迭代(变为了可迭代对象)。

1
2
python复制代码for data in Stuff():
print(data)

然而,在 Ruby 的用法中,你要做的恰恰相反。你要将 for 创建成一个方法,它接收代码(body 体)来运行。Ruby 将过程代码放在代码块中,这样它们就可以被用于传递。

然后,在each方法中,使用yield与代码块进行交互,将值传递给代码块来做你需要做的事情(对于任何方法,代码块都是一种隐式参数)。

如果我们重写上面的代码,会成这样:

1
2
3
4
5
6
7
8
9
10
11
ruby复制代码class Stuff
def initialize
@a_list = [1, 2, 3, 4]
end

def each
for item in @a_list
yield item
end
end
end

使用each进行迭代:

1
2
3
ruby复制代码Stuff.new().each do |item|
puts item
end

不是将数据传给 for 循环(Python),而是将循环代码传给数据(Ruby)。

但区别还远不止于此:

Python 构建类似于 for 的结构,用于各种处理;Ruby 将数据处理工作放到方法中。

优秀的 Python 代码使用列表和字典解析式来实现map 和filter,这些表达式的核心与 for/迭代的语义是相同的。

1
2
3
4
5
python复制代码In [2]: [item for item in Stuff()]
Out[2]: [1, 2, 3, 4]

In [3]: [item for item in Stuff() if item % 2 == 0]
Out[3]: [2, 4]

Ruby 则继续使用方法优先的方式,除了each 方法,还有一系列常用于处理集合的新方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ruby复制代码class Stuff
...

def select
out = []
each do |e|
# If block returns truthy on e, append to out
if yield(e)
out << e
end
end
out
end

def map
out = []
# One line block syntax, append output of block processed on e to out
each {|e| out << yield(e) }
out
end
1
2
ruby复制代码puts Stuff.new().map {|item| item}
puts Stuff.new().select{|item| item.even?}

Python 说:“你告诉我们如何迭代你的实例,我们将决定如何处理你的数据。” Python 有一些基于语言的用作迭代和处理的原语,如果要自定义迭代,只需将正确的代码添加到 for 循环体(或表达式)中。

Ruby 反转了剧本,赋予对象更深层的可定制性。是的,在某些情况下,我们可以在代码块中添加更多的控制流。是的,我们也可以把 each 方法用来做 map。但是 Ruby 允许对象们实现不同的 map 和 each(如果将“each”的实现用于“map”,可能会非常不理想,甚至不安全)。Ruby 的对象在处理其数据方面,有着更好的方法。

在 Ruby 中,对象控制着功能可见性。而在 Python 中,是语法做着控制。

地道的 Python 对数据处理有着强势的看法。Python 说:“看,90% 的代码都能很好地融入这些想法,只要遵从它,完成工作就行了。”把你的对象变成可以 for-循环的,别再烦我了。

然而 Ruby 说:“在一些重要的情况下,我们不想给调用者太多能力。”所以 Ruby 让对象去控制它们被处理的方式,并要求开发人员遵循对象想要被交互的方式。Ruby 在数据处理上没那么强势。

Python 更像是基于 C 语言的“面向对象”编程的扩展。在基于 C 的 OO 中,就像 posix 文件描述符或 Win32 窗口句柄一样,语言并不强制将“方法”与对象本身绑定。相反,对象到方法的绑定只是基于约定。

Python 认为这个过程世界是可以进化的——它升级了这种思维方式,使之更安全。自由函数是存在的(Python猫注:应该指的是内置函数,因不依赖于任何类对象,故是“自由的”),而且确实经常比对象方法更受推荐。对象是存在的,但以一种相对犹豫的方式。

类方法接收“self”作为其第一个参数,几乎与 Win32 或 Posix API 中的 C 函数接受句柄的方式相同。当函数被传递时,它们几乎被当作 C 函数指针来对待。

Python 认为程序范式(procedural paradigm)是最重要的,它是一切的关键基础,在它之上是面向对象的语义层。

然而,Ruby 却将其颠倒过来。Ruby 将面向对象作为金字塔的基础。Ruby 在代码块中包含了混乱的过程世界,让对象使用这些过程块。

Ruby 并没有为了遵循语言的过程性基础而破坏对象,而是使过程性代码适应对象的世界观。Ruby 有真正的私有方法,不像 Python 的私有方法/参数,只是出于约定。

毫无疑问,当我从系统编程的角度接触 Python 时,它对我的观感来说是很自然的。具备着在必要的时候编写 C 语言的能力,它进化了,令那个世界更加安全。也许这就是为什么它在系统资源密集的数值计算领域中,找到了用武之地。

难怪 Ruby 很适合开发人员构建更流畅、也许更安全的 API 和 DSL。Ruby 希望程序员对领域进行建模,而不是对编程环境进行建模,这对于许多工作来说,似乎是正确的方法。

本文转载自: 掘金

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

rm命令弱爆了!

发表于 2021-11-23

大家好,我是良许。

创建、删除和修改文件是用户在 Linux 系统中执行的非常常见操作。大家都知道,在 Linux 系统里使用 rm 命令删除单个文件时,几乎一瞬间就完成了。但是如果文件数量很大,那么删除操作就需要很长时间才能完成。

你有没想过,删除 50 万个小文件,需要花费多少时间?

我写这篇文章的目的,是为了找出在 Linux 中删除巨量文件的最快方法。通过测试发现,rm 命令简直弱爆了!

我们将从一些简单的文件删除方法开始,然后比较不同方法完成文件删除任务的速度。看看哪种方式删除速度最快。

  1. 文件删除的几种方式

在 Linux 系统中删除文件,最常用的命令就是 rm 命令。这个命令相信大家都已经很熟悉了,我们来简单回顾一些 rm 命令的例子。

1
shell复制代码$ rm -f testfile

-f 选项在上面的命令中,表示将在不要求确认的情况下强行删除文件。

1
shell复制代码$ rm -rf testdirectory

这个命令将删除名为 testdirectory 的目录以及该目录中的所有内容(使用的 -r 选项是递归删除文件)。

而删除目录,我们还有另一个命令,那就是 rmdir ,但是它只有在目录为空时才会删除该目录。

1
shell复制代码$ rmdir testdirectory

现在我们看看在 Linux 中删除文件的一些其它不同方法。

我最喜欢的方法之一是使用 find 命令,再进行删除操作。find 命令是一个非常方便的工具,可用于根据文件的类型、大小、创建日期、修改日期和更多不同的条件来搜索文件。

我们来看一个 find 命令使用 -exec 来调用 rm 命令的例子。

1
shell复制代码$ find /test -type f -exec rm {} \;

上述命令将删除 /test 目录中的所有文件。首先 find 命令将查找目录中的所有文件,然后对于每个搜索结果,它会执行 rm 命令。

我们再看看可以与 find 命令一起使用的一些不同方法来删除文件。

1
shell复制代码$ find /test -mtime +7 -exec rm {} \;

在上述示例中,find 命令将搜索 /test 目录中 7 天前修改过的所有文件,然后删除每个文件。

1
shell复制代码$ find /test -size +7M -exec rm {} \;

上述示例中,将搜索目录 /test 目录中所有大于 7M 的文件,然后再删除它们。

在以上我们列出来的所有 find 命令示例中,都会为找到的每个文件调用 rm 命令。例如,在上面的最后一个 find 命令中,如果结果中有 50 个大于 7M 的文件,那么将调用 50 次 rm 命令删除文件。而这样的操作将需要花费更长的时间。

除了在 find 中借助 -exec 参数调用 rm 命令外,还有一个更好的选择,那就是使用 -delete 选项。比如:

1
arduino复制代码$ find /test -size +7M -delete

达到的效果与上一条命令一样。

  1. 删除巨量文件时用什么命令最快?

话不多说,我们直接上测试。

首先借助一个简单的 bash for 循环创建 50 万个文件。

1
shell复制代码$ for i in $(seq 1 500000); do echo testing >> $i.txt; done

上述命令中,将在当前工作目录中创建 50 万个 txt 文件,名称从 1.txt 到 500000.txt,每个文件都包含 testing 的文本内容,因此文件大小至少在几千字节的范围。

在创建了 50 万个文件后,我们将尝试使用多方式来删除它们,看看哪种方式删除巨量文件速度最快。

Round 1:rm 命令

首先让我们使用简单的 rm 命令,同时我们使用 time 命令来计时。

1
2
3
4
5
sql复制代码$ time rm -f *
-bash: /bin/rm: Argument list too long
real 0m11.126s
user 0m9.673s
sys 0m1.278s

我们可以看到 rm 命令的执行结果是 Argument list too long ,这意味着该命令没有完成删除,因为给 rm 命令的文件数量太大而无法完成,所以它直接就躺平罢工了。

不要注意 time 命令显示的时间,因为 rm 命令没有完成它的操作,time 命令只管显示你命令执行了多长时间,而不关心命令的最终结果。

Round 2:使用 -exec 参数的 find 命令

现在让我们使用我们之前看到的带有 -exec 参数的 find 命令。

1
2
3
4
shell复制代码$ time find ./ -type f -exec rm {} \;
real 14m51.735s
user 2m24.330s
sys 9m48.743s

从我们使用 time 命令得到的输出可以看出,从单个目录中删除 50 万个文件需要 14 分 51 秒。 这是相当长的时间,因为对于每个文件,都会执行一个单独的 rm 命令,直到删除所有文件。

Round 3:使用 -delete 参数的 find 命令

现在让我们通过在 find 命令中使用 -delete 选项来测试消耗的时间。

1
2
3
4
sql复制代码$ time find ./ -type f -delete
real 5m11.937s
user 0m1.259s
sys 0m28.441s

删除速度大大提高,只用了 5 分 11 秒!当你在 Linux 中删除数百万个文件时,这是速度的惊人改进。

Round 4:Perl 语言

现在让我们看看使用 Perl 语言删除文件是如何工作的,以及它与我们之前看到的其他删除方式相比的速度。

1
2
3
4
scss复制代码$ time perl -e 'for(<*>){((stat)[9]<(unlink))}'
real 1m0.488s
user 0m7.023s
sys 0m27.403s

从结果可以看出,Perl 只用了大约 1 分钟就删除了该目录中的 50 万个文件,与我们之前看到的其他 find 命令和 rm 命令相比,这个速度非常之快!

但是,如果您有兴趣在使用 Perl 时用到更复杂的选项,则需要对 Perl 正则表达式有一定的了解。

Round 5:rsync 命令

还有一种较少使用且鲜为人知的方法可用于删除文件夹内的大量文件,这种方法正是我们著名的工具 rsync ,它的基本用法是用于在 Linux 中的两个本地和远程位置之间传输和同步文件。

现在我们来看看如何使用 rsync 命令的来删除文件夹内所有文件。其实很简单,我们可以通过将具有大量文件的目标目录与空目录进行同步来实现删除的操作。

在我们的例子中, /test 目录(目标目录)有 50 万个文件,我们再创建一个名为 blanktest 的空目录(源目录)。现在,我们将在 rsync 命令中使用 -delete 选项,这将删除目标目录中的所有源目录中不存在文件。

1
2
3
4
sql复制代码$ time rsync -a --delete blanktest/ test/
real 2m52.502s
user 0m2.772s
sys 0m32.649s

可以看到,仅用 2 分钟 52 秒就完成删除。

因此与 find 命令相比,如果您想清空包含数百万个文件的目录,使用 rsync 命令会更好。

  1. 小结

下表总结了 Linux 中采用不同方式删除 50 万个文件的速度,方便大家参考。

命令 花费时间
rm 命令 无法删除大量文件
使用 -exec 参数的 find 命令 14 分 51 秒
使用 -delete 参数的 find 命令 5 分 11 秒
Perl 1 分钟
rsync 命令 2 分 52 秒


最后,最近很多小伙伴找我要Linux学习路线图,于是我根据自己的经验,利用业余时间熬夜肝了一个月,整理了一份电子书。无论你是面试还是自我提升,相信都会对你有帮助!

免费送给大家,只求大家金指给我点个赞!

电子书 | Linux开发学习路线图

也希望有小伙伴能加入我,把这份电子书做得更完美!

有收获?希望老铁们来个三连击,给更多的人看到这篇文章

推荐阅读:

  • 干货 | 程序员进阶架构师必备资源免费送
  • 书单 | 程序员必读经典书单(高清PDF版)

欢迎关注我的博客:良许Linux教程网,满满都是干货!

本文转载自: 掘金

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

设计模式之单例模式详解 单例模式

发表于 2021-11-23

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

单例模式

定义

  • 是指确保一个类在任何情况下都只有一个实例,并只有一个全局访问点
  • 它属于创建型模式
  • 隐藏其所有的构造方法;因为单例模式全局只有一个实例,所以不可以让用户通过构造方法进行创建对象,只能通过我开放的接口来创建对象

单例模式常见的写法

饿汉式单例:在单例类第一次加载的时候创建实例

代码展示

第一种写法

1
2
3
4
5
6
7
8
9
10
java复制代码public class SingletonPatternOne {

private static final SingletonPatternOne instance = new SingletonPatternOne();

private SingletonPatternOne() {}

public SingletonPatternOne getInstance(){
return instance;
}
}

这里需要把构造方法进行私有化,因为在单例中,全局只能有一个实例,所以这里要把构造私有了防止用户通过new 的方法来创建对象

其实这个饿汉模式还有一些写法,就是将其创建对象的代码到一个静态代码块里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class SingletonPatternOne {

private static final SingletonPatternOne instance;

static {
instance = new SingletonPatternOne();
}

private SingletonPatternOne() {}

public SingletonPatternOne getInstance(){
return instance;
}
}

注:这两种写法的效果是一样

问:为啥称这种写法是饿汉式的呢?

答:因为这种创建对象的方式好比一个饥饿的汉子,看到吃的东西,上来就吃,和这个创建对象的方法一样,当这个类加载时就直接创建了一个对象(野史,小编不知道这种说法对不对,听别人说的,(^__^) 嘻嘻……)

优缺点

优点:

  • 执行效率高,性能高,没有任何的锁

缺点:

  • 在特定的条件下,可能会出现内存浪费的情况(比如:因为是这个变量是通过static修饰的,当类进行加载的时候,就算不使用它也会创建出来,所以这里会浪费内存),如果项目中有大量的单例的时候不适合用饿汉式单例

懒汉式单例:只有当其它类使用它的时候才会创建

懒汉式的写法是在类中先声名一个变量,只有在使用的时候才会创建它,第一次使用的时候创建出来,后面直接返回这个变量就好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class LazySingletonPattern {

private static LazySingletonPattern instance;

private LazySingletonPattern(){}

public static LazySingletonPattern getInstance(){
if(instance == null){
instance = new LazySingletonPattern();
}
return instance;
}

}

同样和饿汉式的写法类似,构造方法也需要私有化,因为不可以让用户自行通过构造方法来创建对象

在获取实例对象的时候通过if判断出是否创建过这个对象如果创建过就直接返回结果,否则就创建出一个对象

但是:这种写法在多线程模式下会出现线程不安全的问题

优缺点

优点:

  • 节省了内存,省下了不必要的内存消耗

缺点:

  • 会出现线程不安全的问题

这个懒汉式线程不安全是怎么一回事呢?

先来写个测试多线程下出现问题的栗子

1
2
3
4
5
6
7
java复制代码public class TestTaskSingletonRunnable implements Runnable{
@Override
public void run() {
LazySingletonPattern instance = LazySingletonPattern.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + instance);
}
}

这个实现在Runnable接口,在run方法里面获取到单例模式下创建的实例

1
2
3
4
5
6
7
8
9
java复制代码public class TestSingletonPattern {
public static void main(String[] args) {
Thread thread1 = new Thread(new TestTaskSingletonRunnable());
Thread thread2 = new Thread(new TestTaskSingletonRunnable());

thread1.start();
thread2.start();
}
}

这里要启动两个线程才可以看出获取的是不是同一个实例

我这里执行了两个这个代码,结果有一次是同一个实例,有一次是不同的实例

但是这里要注意一点,同一实例的那个不一定是只创建了一次实例,它也可能是创建了两次实例

运行结果有如下情况:

  • 相同的结果
    • 正常运行顺序执行
    • 第二次创建的对象在第一次打印前覆盖第一次创建的对象
  • 不同的结果
    • 同时进行if条件,按执行的顺序返回

第一种(正常运行顺序执行)的情况就不说了,这个也很好理解,执行完线程一,打印完成后,再执行第二个线程,第二种 情况和第三种情况要好好聊聊

聊聊产生错误的原因

一.第二次创建的对象在第一次打印前覆盖第一次创建的对象

image-20211123163454512

我们先来解释下这张图来聊聊产生这一问题的原因.

当程序启动的时候,代码执行到了图1的第10行代码,因为我在这里打了一个断点

看图2:我将Thread-0线程的代码执行到11行代码,因为这个时候instance变量是null,

看图3:这个时候我切换到了Thread-1线程,这个时候可以看到第10行代码的if判断的条件还是true,在图2虽然进入了if条件,因为我并没有让图2中的代码执行创建对象,所以在图3中这里的判断条件仍然是为true的

看图4:将Thread-1线程代码执行到11行代码(也就是if里面),到这里细心的小伙伴就会发现,此时,Thread-0和Thread-1的代码现在都执行到了11行代码(也就是同时都在if判断条件里面)

看图5:我们再次来到Thread-0线程将代码执行到第13行代码.这时我还没有让代码执行打印实例地址.再切换到Thread-1让代码执行到第13行代码.你就会发现,Thread-0线程一开始创建的对象就会被替换成Thread-1线程所创建的对象了

image-20211123165456937

这就是当在多线程模式下,打印相同地址有可能产生的一个原因,还有一个原因就是线程顺序执行(这个比较好理解就不说了)

二.不同的结果

image-20211123170015695

看图1:当程序启动的时候来到第10行代码(这个是Thread-0)

看图2:我让Thread-0线程的代码执行到了第11行代码,这个时候还没有创建对象

看图3:切换到了Thread-1线程,这个时候因为Thread-0线程还没有创建对象,所里这里的if判断的结果是true,

看图4:将Thread-1线程的代码执行到第11行代码,

看图5:切换到Thread-0线程,点击绿色的小按扭后,会自动来到Thread-1线程的断点处,再次点击这个按扭.你就会发现这两个线程打印结果的地址不一样

image-20211123170507005

这个就是在多线程环境下产生不是一个实例的原因

如何解决在多线程环境下产生错误的问题

先来一个简单粗暴的方法,直接给getInstance()方法加一个synchronized关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class LazySingletonPattern {

private static LazySingletonPattern instance;

private LazySingletonPattern(){}

public synchronized static LazySingletonPattern getInstance(){
if(instance == null){
instance = new LazySingletonPattern();
}
return instance;
}

}

注:在使用synchronized锁静态方法的时候,它其实是锁的这个类,也可以称为类锁

image-20211123171451448

加了synchronized修饰后不管怎么运行代码(多线程环境下),使用的都是同一个实例,不会出现上面我后说的那两种情况(第二次创建的对象在第一次打印前覆盖第一次创建的对象和不同的结果)

为啥加了synchronized关键字就没有问题了呢?这个关键字为啥这么牛?

还是接着能过断点方式来查明原因

image-20211123171829054

看上图,当Thread-0线程执行到这个方法的时候,你们看下Thread-1线程,这个时候Thread-1线程已经处于一个MONITOR的状态,只有等Thread-1线程执行完成之后,它才会继续执行,所以这种情况下,它不会出现上面所说的错误的情况

但是通过synchronized修饰后,会出现一个新的问题,它会降低系统的性能

为啥会这样说,我先来张图你们就明白了,这时我再新加一个线程Thread-2

image-20211123172327946

看这张图,当程序中的Thread-0线程运行到这个方法里面的时候Thread-1和Thread-2都阻塞住了,它们都在等待Thread-0线程执行完成

这里可以举一个例子

image-20211123172823732
比如这一个马戏团里面,只有一个门,但是这个时候门坏掉了,工作人员在修理(这个是好比Thread-0在创建对象),其它的人(这个好比其它的线程)只能在后面干等着,

这样不就影响了性能了嘛

这里还有一种优化的写法,双重检查锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class LazyDoubleCheckSingletonPattern {

private static volatile LazyDoubleCheckSingletonPattern instance;

private LazyDoubleCheckSingletonPattern(){}

public LazyDoubleCheckSingletonPattern getInstance(){
if(instance == null){
synchronized (LazyDoubleCheckSingletonPattern.class){
if(instance == null){
instance = new LazyDoubleCheckSingletonPattern();
}
}
}
return instance;
}

}

这和写法虽然也用到了synchronized,当一个线程进入后其它的线程也会阻塞掉,但是总比写在方法上好一些,这样写可以过滤掉一些不需要阻塞的时候(当一线程要获取实例的时候,这个实例已经被创建了出来.)就可以直接返回了

两个if判断的作用:

  • 第一个if
    • 它的作用是过滤出一些不需要创建对象的情况,(比如已经创建过对象)
  • 第二个if
    • 它虽然也是判断了这个实例是否为空,加它的原因是,在多线程环境下,如果两个线程都进入到了第一个if里面,然后其中一个线程创建了对象,第二个线程进入的时候通过if判断下这个实例对象是否为空,如果不为空就不再进行创建对象了,是起到这么一个作用

这里最好是加上volatile关键字,这里有可能会发生指令重排的情况

懒汉式其它写法:内部静态类
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class LazyStaticClassSingletonPattern {

private LazyStaticClassSingletonPattern(){}

public LazyStaticClassSingletonPattern getInstance(){
return LazyHold.INSTANCE;
}

private static class LazyHold{
private static LazyStaticClassSingletonPattern INSTANCE = new LazyStaticClassSingletonPattern();
}

}

这里只有使用这个类的时候,里面的静态内部类才会创建,所以这种也是属于懒汉式

优缺点

优点:

  • 能避免浪费内存,性能高

缺点:

  • 能被反射破坏(不但但这种可以被反射破坏,懒汉式其它那几种写法也可以)
如何被反射破坏
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class TestSingletonPattern {
public static void main(String[] args) {
try{
Class<?> clazz = LazyStaticClassSingletonPattern.class;
Constructor<?> constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
Object o1 = constructor.newInstance();
System.out.println(o1);
}catch (Exception e){
e.printStackTrace();
}
}
}

image-20211123184533411

这样的话就算不通过公开的方法也可以创建出对象.那怎么办呢?

我们可以在构造方法中判断下这个实例是否被创建出来,如果创建出来就抛出一个异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class LazyStaticClassSingletonPattern {

private LazyStaticClassSingletonPattern(){
if(LazyHold.INSTANCE != null){
throw new RuntimeException("不可以非法创建");
}
}

public LazyStaticClassSingletonPattern getInstance(){
return LazyHold.INSTANCE;
}

private static class LazyHold{
private static LazyStaticClassSingletonPattern INSTANCE = new LazyStaticClassSingletonPattern();
}

}

image-20211123184759063

为啥可以这样写?

因为在类加载的时候静态内部类已经创建出了一个实例, 所以在通过反射创建的时候在构造方法里面,判断条件已经成立了,所以就会抛出这个异常

ThreadLocal单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class ThreadLocatlSingletonPattern {

private static final ThreadLocal<ThreadLocatlSingletonPattern> threadlocalInstance =
new ThreadLocal<ThreadLocatlSingletonPattern>(){
@Override
protected ThreadLocatlSingletonPattern initialValue() {
return new ThreadLocatlSingletonPattern();
}
};


private ThreadLocatlSingletonPattern(){}

public static ThreadLocatlSingletonPattern getInstance(){
return threadlocalInstance.get();
}
}

注意:这种方法只有在同一个线程里面的才是单例,如果不是在同一线程里面获取到的不是同一个对象,这个有时也会用到,这里只说下,有兴趣的可以看看

扩展:序列化如何破坏单例模式

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
java复制代码public class TestSingletonPattern {
public static void main(String[] args) {
SingletonPatternOne s1 = null;
SingletonPatternOne s2 = SingletonPatternOne.getInstance();

FileOutputStream fos = null;

try{
fos = new FileOutputStream("SingletonPatternOne.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();

FileInputStream fis = new FileInputStream("SingletonPatternOne.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SingletonPatternOne)ois.readObject();
ois.close();

System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);

}catch (Exception e){

}

}
}

这种破坏的方法不过是通过序列化和反序列化的方式实现的.

这个过程是怎么一回事呢?

先是序列化

它将内存中的对象转换成字节码的形式保存在硬盘上

然后将数据从硬盘上取出,通过IO将数据加载到内存里面,将其转换成一个Java对象

如何避免这种问题,只需要在单例类里面加上readResolve方法就好,里面返回对应的实例,

这里以饿汉式为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class SingletonPatternOne implements Serializable {

private static final SingletonPatternOne instance;

static {
instance = new SingletonPatternOne();
}

private SingletonPatternOne() {}

public static SingletonPatternOne getInstance(){
return instance;
}

private Object readResolve(){
return instance;
}
}

本文转载自: 掘金

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

CAS优缺点

发表于 2021-11-23

前面讲过CAS原理CAS 并发编程思想CAS 是有很多优点的,比如可以避免加互斥锁,可以提高程序的运行效率,但是同样 CAS 也有非常明显的缺点。所以我们在使用 CAS 的时候应该同时考虑到它的优缺点,合理地进行技术选型。

下面我们就来看一下 CAS 有哪几个主要的缺点。

ABA 问题

首先,CAS 最大的缺点就是 ABA 问题。

决定 CAS 是否进行 swap 的判断标准是“当前的值和预期的值是否一致”,如果一致,就认为在此期间这个数值没有发生过变动,这在大多数情况下是没有问题的。

但是在有的业务场景下,我们想确切知道从上一次看到这个值以来到现在,这个值是否发生过变化。例如,这个值假设从 A 变成了 B,再由 B 变回了 A,此时,我们不仅认为它发生了变化,并且会认为它变化了两次。

在这种场景下,我们使用 CAS,就看不到这两次的变化,因为仅判断“当前的值和预期的值是否一致”就是不够的了。CAS 检查的并不是值有没有发生过变化,而是去比较这当前的值和预期值是不是相等,如果变量的值从旧值 A 变成了新值 B 再变回旧值 A,由于最开始的值 A 和现在的值 A 是相等的,所以 CAS 会认为变量的值在此期间没有发生过变化。所以,CAS 并不能检测出在此期间值是不是被修改过,它只能检查出现在的值和最初的值是不是一样。

我们举一个例子:假设第一个线程拿到的初始值是 100,然后进行计算,在计算的过程中,有第二个线程把初始值改为了 200,然后紧接着又有第三个线程把 200 改回了 100。等到第一个线程计算完毕去执行 CAS 的时候,它会比较当前的值是不是等于最开始拿到的初始值 100,此时会发现确实是等于 100,所以线程一就认为在此期间值没有被修改过,就理所当然的把这个 100 改成刚刚计算出来的新值,但实际上,在此过程中已经有其他线程把这个值修改过了,这样就会发生 ABA 问题。

如果发生了 ABA 问题,那么线程一就根本无法知晓在计算过程中是否有其他线程把这个值修改过,由于第一个线程发现当前值和预期值是相等的,所以就会认为在此期间没有线程修改过变量的值,所以它接下来的一些操作逻辑,是按照在此期间这个值没被修改过”的逻辑去处理的,比如它可能会打印日志:“本次修改十分顺利”,但是它本应触发其他的逻辑,比如当它发现了在此期间有其他线程修改过这个值,其实本应该打印的是“本次修改过程受到了干扰”。

解决

那么如何解决这个问题呢?添加一个版本号就可以解决。
我们在变量值自身之外,再添加一个版本号,那么这个值的变化路径就从 A→B→A 变成了 1A→2B→3A,这样一来,就可以通过对比版本号来判断值是否变化过,这比我们直接去对比两个值是否一致要更靠谱,所以通过这样的思路就可以解决 ABA 的问题了。

在 atomic 包中提供了 AtomicStampedReference 这个类,它是专门用来解决 ABA 问题的,解决思路正是利用版本号,AtomicStampedReference 会维护一种类似 <Object,int> 的数据结构,其中的 int 就是用于计数的,也就是版本号,它可以对这个对象和 int 版本号同时进行原子更新,从而也就解决了 ABA 问题。因为我们去判断它是否被修改过,不再是以值是否发生变化为标准,而是以版本号是否变化为标准,即使值一样,它们的版本号也是不同的。

以上就是对 CAS 的第一个缺点—— ABA 问题的介绍。

自旋时间过长

CAS 的第二个缺点就是自旋时间过长。

由于单次 CAS 不一定能执行成功,所以 CAS 往往是配合着循环来实现的,有的时候甚至是死循环,不停地进行重试,直到线程竞争不激烈的时候,才能修改成功。

可是如果我们的应用场景本身就是高并发的场景,就有可能导致 CAS 一直都操作不成功,这样的话,循环时间就会越来越长。而且在此期间,CPU 资源也是一直在被消耗的,这会对性能产生很大的影响。所以这就要求我们,要根据实际情况来选择是否使用 CAS,在高并发的场景下,通常 CAS 的效率是不高的。

范围不能灵活控制

CAS 的第三个缺点就是不能灵活控制线程安全的范围。

通常我们去执行 CAS 的时候,是针对某一个,而不是多个共享变量的,这个变量可能是 Integer 类型,也有可能是 Long 类型、对象类型等等,但是我们不能针对多个共享变量同时进行 CAS 操作,因为这多个变量之间是独立的,简单的把原子操作组合到一起,并不具备原子性。因此如果我们想对多个对象同时进行 CAS 操作并想保证线程安全的话,是比较困难的。

有一个解决方案,那就是利用一个新的类,来整合刚才这一组共享变量,这个新的类中的多个成员变量就是刚才的那多个共享变量,然后再利用 atomic 包中的 AtomicReference 来把这个新对象整体进行 CAS 操作,这样就可以保证线程安全。

相比之下,如果我们使用其他的线程安全技术,那么调整线程安全的范围就可能变得非常容易,比如我们用 synchronized 关键字时,如果想把更多的代码加锁,那么只需要把更多的代码放到同步代码块里面就可以了。

总结
下面我们进行总结,本课时介绍了 CAS 的三个缺点,分别是 ABA 问题、自旋时间过长以及线程安全的范围不能灵活控制。我们了解了它的缺点之后,在进行技术选型的时候就可以有的放矢了。

本文转载自: 掘金

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

常见的几种垃圾回收算法,背就完了~

发表于 2021-11-23

这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

垃圾收集算法

常见的垃圾收集算法包括:

  • 标记-清除算法
  • 复制算法
  • 标记-整理算法
  • 分代收集算法

JVM 的垃圾收集算法是使用了分代收集算法,复制算法、标记-整理算法。三种算法都有使用。使用分代收集算法,将 JVM 中的内存分为新生代和老年代,新生代采用复制算法收集,而老年代采用的是标记-整理算法。

1 标记-清除算法

标记-清除算法分为“标记”和“清除”两个阶段,首先通过可达性分析,标记出所有需要回收的对象,然后统一回收所有被标记的对象。

标记-清除算法

标记-清除算法有两个缺陷,一个是效率问题,标记和清除的过程效率都不高,另外一个就是,清除结束后会造成大量的碎片空间。有可能会造成在申请大块内存的时候因为没有足够的连续空间导致再次 GC。

2 复制算法

为了解决碎片空间的问题,出现了“复制算法”。复制算法的原理是,将内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。

复制算法

复制算法解决了空间碎片的问题。但是也带来了新的问题。因为每次在申请内存时,都只能使用一半的内存空间。内存利用率严重不足。

JVM 中新生代采用的就是复制算法进行的GC。针对内存利用率不足的问题做了一些优化。

IBM公司的专门研究表明,新生代中的对象 98% 是“朝生夕死”的,意思是说,在新生代中,经过一次 GC 之后能够存活下来的对象仅有 2% 左右。

所以并不需要按照1:1的比例划分出两块内存空间。而是将内存划分出三块,一块较大的 Eden 区,和两块较小的 Survivor 区。其中 Eden 区占 80% 的内存,两块 Survivor 各占 10% 的内存。在创建新的对象时,只使用 Eden 区和其中的一块 Survivor 区,当进行 GC 时,把 Eden 区和 Survivor 区存活的对象全部复制到另一块 Survivor 区中,然后清理掉 Eden 区和刚刚用过的 Survivor 区。

这种内存的划分方式就解决了内存利用率的问题,每次在创建对象时,可用的内存为 90%(80% + 10%) 当前内存容量。

3 标记-整理算法

复制算法在 GC 之后存活对象较少的情况下效率比较高,但如果存活对象比较多时,会执行较多的复制操作,效率就会下降。而老年代的对象在 GC 之后的存活率就比较高,所以就有人提出了“标记-整理算法”。

标记-整理算法的“标记”过程与“标记-清除算法”的标记过程一致,但标记之后不会直接清理。而是将所有存活对象都移动到内存的一端。移动结束后直接清理掉剩余部分。

标记-整理算法

4 分代收集算法

分代收集是将内存划分成了新生代和老年代。分配的依据是对象的生存周期,或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值(默认是 15,可以通过参数 -XX:MaxTenuringThreshold 来设定)后,如果对象还存活,那么该对象会进入老年代。

本文转载自: 掘金

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

【Go实战 电商平台】(3) 数据库建表 写在前面 1

发表于 2021-11-23

这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

写在前面

第一节,我们确定了ER图,数据字典。
第二节,我们已经配置了MySQL。
结合前两章,我们就可以建立数据库表了。

  1. 初始化连接

在这里插入图片描述

  • init.go
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
go复制代码package model

import (
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"time"
)

var DB *gorm.DB

func Database(connString string) {
db, err := gorm.Open("mysql", connString)
db.LogMode(true) //GORM的打印
if err != nil {
panic(err)
}
if gin.Mode() == "release" {
db.LogMode(false)
}
db.SingularTable(true) //默认不加复数s
db.DB().SetMaxIdleConns(20) //设置连接池,空闲
db.DB().SetMaxOpenConns(100) //打开
db.DB().SetConnMaxLifetime(time.Second * 30)
DB = db
migration()
}
  1. 建立表

  • user

用户表

1
2
3
4
5
6
7
8
9
10
go复制代码type User struct {
gorm.Model
UserName string `gorm:"unique"`
Email string //`gorm:"unique"`
PasswordDigest string
Nickname string `gorm:"not null"`
Status string
Avatar string `gorm:"size:1000"`
Money int
}
  • product 商品表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码type Product struct {
gorm.Model
Name string `gorm:"size:255;index"`
Category Category `gorm:"ForeignKey:CategoryID"`
CategoryID uint `gorm:"not null"`
Title string
Info string `gorm:"size:1000"`
ImgPath string
Price string
DiscountPrice string
OnSale bool `gorm:"default:false"`
Num int
BossID int
BossName string
BossAvatar string
}
  • ProductImg 商品图片表
1
2
3
4
5
6
go复制代码type ProductImg struct {
gorm.Model
Product Product `gorm:"ForeignKey:ProductID"`
ProductID uint `gorm:"not null"`
ImgPath string
}
  • ProductParamImg 商品参数表
1
2
3
4
5
6
go复制代码type ProductParamImg struct {
gorm.Model
Product Product `gorm:"ForeignKey:ProductID"`
ProductID uint `gorm:"not null"`
ImgPath string
}
  • ProductInfoImg 商品详情表
1
2
3
4
5
6
go复制代码type ProductInfoImg struct {
gorm.Model
Product Product `gorm:"ForeignKey:ProductID"`
ProductID uint `gorm:"not null"`
ImgPath string
}
  • 购物车表
1
2
3
4
5
6
7
8
9
10
go复制代码type Cart struct {
gorm.Model
UserID uint
Product Product `gorm:"ForeignKey:ProductID"`
ProductID uint `gorm:"not null"`
BossID uint
Num uint
MaxNum uint
Check bool
}
  • Order 订单表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码type Order struct {
gorm.Model
User User `gorm:"ForeignKey:UserID"`
UserID uint `gorm:"not null"`
Product Product `gorm:"ForeignKey:ProductID"`
ProductID uint `gorm:"not null"`
Boss User `gorm:"ForeignKey:BossID"`
BossID uint `gorm:"not null"`
Address Address `gorm:"ForeignKey:AddressID"`
AddressID uint `gorm:"not null"`
Num uint
OrderNum uint64
Type uint
Money int
}
  • 地址表
1
2
3
4
5
6
7
8
go复制代码type Address struct {
gorm.Model
User User `gorm:"ForeignKey:UserID"`
UserID uint `gorm:"not null"`
Name string `gorm:"type:varchar(20) not null"`
Phone string `gorm:"type:varchar(11) not null"`
Address string `gorm:"type:varchar(50) not null"`
}
  • 收藏表
1
2
3
4
5
6
7
8
9
go复制代码type Favorite struct {
gorm.Model
User User `gorm:"ForeignKey:UserID"`
UserID uint `gorm:"not null"`
Product Product `gorm:"ForeignKey:ProductID"`
ProductID uint `gorm:"not null"`
Boss User `gorm:"ForeignKey:BossID"`
BossID uint `gorm:"not null"`
}
  • 分类表
1
2
3
4
go复制代码type Category struct {
gorm.Model
CategoryName string
}
  • 管理员表
1
2
3
4
5
6
go复制代码type Admin struct {
gorm.Model
UserName string
PasswordDigest string
Avatar string `gorm:"size:1000"`
}
  • 轮播图表
1
2
3
4
5
6
go复制代码type Carousel struct {
gorm.Model
ImgPath string
Product Product `gorm:"ForeignKey:ProductID"`
ProductID uint `gorm:"not null"`
}
  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
go复制代码func migration() {
//自动迁移模式
DB.Set("gorm:table_options", "charset=utf8mb4").
AutoMigrate(&User{}).
AutoMigrate(&Product{}).
AutoMigrate(&Carousel{}).
AutoMigrate(&Category{}).
AutoMigrate(&Favorite{}).
AutoMigrate(&ProductImg{}).
AutoMigrate(&ProductInfoImg{}).
AutoMigrate(&ProductParamImg{}).
AutoMigrate(&Order{}).
AutoMigrate(&Cart{}).
AutoMigrate(&Admin{}).
AutoMigrate(&Address{})
DB.Model(&Cart{}).AddForeignKey("product_id","Product(id)","CASCADE","CASCADE")
DB.Model(&Order{}).AddForeignKey("user_id","User(id)","CASCADE","CASCADE")
DB.Model(&Order{}).AddForeignKey("address_id","Address(id)","CASCADE","CASCADE")
DB.Model(&Order{}).AddForeignKey("product_id","Product(id)","CASCADE","CASCADE")
DB.Model(&Order{}).AddForeignKey("boss_id","User(id)","CASCADE","CASCADE")
DB.Model(&Favorite{}).AddForeignKey("boss_id","User(id)","CASCADE","CASCADE")
DB.Model(&Favorite{}).AddForeignKey("user_id","User(id)","CASCADE","CASCADE")
DB.Model(&Favorite{}).AddForeignKey("product_id","Product(id)","CASCADE","CASCADE")
DB.Model(&Product{}).AddForeignKey("category_id","Category(id)","CASCADE","CASCADE")
DB.Model(&ProductImg{}).AddForeignKey("product_id","Product(id)","CASCADE","CASCADE")
DB.Model(&ProductInfoImg{}).AddForeignKey("product_id","Product(id)","CASCADE","CASCADE")
DB.Model(&ProductParamImg{}).AddForeignKey("product_id","Product(id)","CASCADE","CASCADE")
DB.Model(&Address{}).AddForeignKey("user_id","User(id)","CASCADE","CASCADE")
}
  • AutoMigrate函数是把代码映射到数据库中
  • AddForeignKey函数是添加外键

举例子

1
go复制代码DB.Model(&Cart{}).AddForeignKey("product_id","Product(id)","CASCADE","CASCADE")

这个函数就是在Cart表中把product_id字段关联到product的id中,后面两个就是update和delete的时候进行级联更新或是级联删除。

本文转载自: 掘金

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

老手也常误用!详解 Go channel 内存泄漏问题

发表于 2021-11-23

一说到 go channel,很多人会使用“优秀”“哲学”这些词汇来描述。殊不知,go channel 恐怕还是 golang 中最容易造成问题的特性之一。很多情况下,我们使用 go channel 时,常常以为可以关闭 channel,但实际上却没有关闭,这就是导致 go channel 内存泄漏的元凶。

阅读本文前要求读者熟悉 go channel 的基本知识。如果你不够了解 go channel,那么可以先阅读《新手使用 go channel 需要注意的问题》。本文会默认你已经了解相关内容。

情境一:select-case 误用导致的内存泄露

废话说少,先看代码。

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
go复制代码func TestLeakOfMemory(t *testing.T) {
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
chanLeakOfMemory()
time.Sleep(time.Second * 3) // 等待 goroutine 执行,防止过早输出结果
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
}

func chanLeakOfMemory() {
errCh := make(chan error) // (1)
go func() { // (5)
time.Sleep(2 * time.Second)
errCh <- errors.New("chan error") // (2)
fmt.Println("finish sending")
}()

var err error
select {
case <-time.After(time.Second): // (3) 大家也经常在这里使用 <-ctx.Done()
fmt.Println("超时")
case err = <-errCh: // (4)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(nil)
}
}
}

执行代码(需注意,使用测试和 main 线程执行的输出略有不同)

大家认为输出的结果是什么?正确的输出结果如下:

1
2
3
makefile复制代码NumGoroutine: 2
超时
NumGoroutine: 3

这是 go channel 导致内存泄漏的经典场景。根据输出结果(开始有两个 goroutine,结束时有三个 goroutine),我们可以知道,直到测试函数结束前,仍有一个 goroutine 没有退出。原因是由于 (1) 处创建的 errCh 是不含缓存队列的 channel,如果 channel 只有发送方发送,那么发送方会阻塞;如果 channel 只有接收方,那么接收方会阻塞。

我们可以看到由于没有发送方往 errCh 发送数据,所以 (4) 处代码一直阻塞。直到 (3) 处超时后,打印“超时”,函数退出,(4) 处代码都未接收成功。而 (2) 处的所在的 goroutine 在“超时”被打印后,才开始发送。由于外部的 goroutine 已经退出了,errCh 没有接收者,导致 (2) 处一直阻塞。因此 (2) 处代码所在的协程一直未退出,造成了内存泄漏。如果代码中有许多类似的代码,或在 for 循环中使用了上述形式的代码,随着时间的增长会造成多个未退出的 gorouting,最终导致程序 OOM。

这种情况其实还比较简单。我们只需要为 channel 增加一个缓存队列。即把 (1) 处代码改为 errCh := make(chan error, 1) 即可。修改后输出如下所示,可知我们创建的 goroutine 已经退出了。

1
2
3
makefile复制代码NumGoroutine: 2
超时
NumGoroutine: 2

可能会有人想要使用 defer close(errCh) 关闭 channel。比如把 (1) 处代码改为如下形式(错误):

1
2
go复制代码errCh := make(chan error)
defer close(errCh)

由于 (2) 处代码没有接收者,所以一直阻塞。直到 close(errCh) 运行,(2) 处仍在阻塞。这导致关闭 channel 时,仍有 goroutine 在向 errCh 发送。然而在 golang 中,在向 channel 发送时不能关闭 channel,否则会 panic。因此这种方式是错误的。

又或在 (5) 处 goroutine 的第一句加上 defer close(errCh)。由于 (2) 处阻塞, defer close(errCh) 会一直得不到执行。因此也是错误的。 即便对调 (2) 处和 (4) 处的发送者和接收者,也会因为 channel 关闭,导致输出无意义的零值。

情景二:for-range 误用导致的内存泄露

上述示例中只有一个发送者,且只发送一次,所以增加一个缓存队列即可。但在其他情况下,可能不止有一个发送者(或者不只发送一次),所以这个方案要求,缓存队列的容量需要和发送次数一致。一旦缓存队列容量被用完后,再有发送者发送就会阻塞发送者 goroutine。如果恰好此时接收者退出了,那么仍然至少会有一个 goroutine 无法退出,从而造成内存泄漏。就比如下面的代码。不知道经过上面的讲解,读者是否能够发现其中的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
go复制代码func TestLeakOfMemory2(t *testing.T) {
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
chanLeakOfMemory2()
time.Sleep(time.Second * 3) // 等待 goroutine 执行,防止过早输出结果
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
}

func chanLeakOfMemory2() {
ich := make(chan int, 100) // (3)
// sender
go func() {
defer close(ich)
for i := 0; i < 10000; i++ {
ich <- i
time.Sleep(time.Millisecond) // 控制一下,别发太快
}
}()
// receiver
go func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
for i := range ich { // (2)
if ctx.Err() != nil { // (1)
fmt.Println(ctx.Err())
return
}
fmt.Println(i)
}
}()
}

// Output:
// NumGoroutine: 2
// 0
// 1
// ...(省略)...
// 789
// context deadline exceeded
// NumGoroutine: 3

执行代码

我们聪明地使用了 channel 的缓存队列。我们以为我们循环发送,发完之后就会把 channel 关闭。而且我们使用 for range 获取 channel 的值,会一直获取,直到 channel 关闭。但在代码 (1) 处,接收者的 goroutine 中,我们加了一个判断语句。这会让代码 (2) 处的 channel 还没被接收完就退出了接收者 goroutine。尽管代码 (3) 处有缓存,但是因为发送 channel 在 for 循环中,缓存队列很快就会被占满,阻塞在第 101 的位置。所以这种情况我们要使用一个额外的 stop channel 来终结发送者所在的 goroutine。方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
go复制代码func TestLeakOfMemory2(t *testing.T) {
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
chanLeakOfMemory2()
time.Sleep(time.Second * 3) // 等待 goroutine 执行,防止过早输出结果
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
}

func chanLeakOfMemory2() {
ich := make(chan int, 100)
stopCh := make(chan struct{})
// sender
go func() {
defer close(ich)
for i := 0; i < 10000; i++ {
select {
case <-stopCh:
return
case ich <- i:
}
time.Sleep(time.Millisecond) // 控制一下,别发太快
}
}()
// receiver
go func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
for i := range ich {
if ctx.Err() != nil {
fmt.Println(ctx.Err())
close(stopCh)
return
}
fmt.Println(i)
}
}()
}

// Output:
// NumGoroutine: 2
// 0
// 1
// ...(省略)...
// 789
// context deadline exceeded
// NumGoroutine: 2

执行代码

可能有人会问,要是接收者 goroutine 关闭 stop channel 的时候,发送者又继续发送了怎么办?不会内存泄漏吗?

答案是不会的。因为只可能存在两种情况,一种是发送者把数据发送到了缓存中,发送者想要继续发送时,select 发现 stop channel 已经关闭,发送者 goroutine 会退出;一种是 channel 没有缓存了,发送者只能阻塞,此时 select 发现 stop channel 已经关闭,发送者 goroutine 也会退出。

总之,通常情况下,我们只会遇到这两种 go channel 造成内存泄漏的情况(一个发送者导致的内存泄漏和多个发送者导致的内存泄漏)。如果你了解其他 go channel 造成的内存泄漏情况,也欢迎在评论区留言。

让我们仔细观察上述两个内存泄漏的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
go复制代码func chanLeakOfMemory() {
errCh := make(chan error) // (1)
go func() chan error { // (5)
time.Sleep(2 * time.Second)
errCh <- errors.New("chan error") // (2)
return errCh
}()

var err error
select {
case <-time.After(time.Second): // (3) 大家也经常在这里使用 <-ctx.Done()
fmt.Println("超时")
case err = <-errCh: // (4)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(nil)
}
}
}

func chanLeakOfMemory2() {
ich := make(chan int, 100) // (3)
// sender
go func() {
defer close(ich)
for i := 0; i < 10000; i++ {
ich <- i
time.Sleep(time.Millisecond) // 控制一下,别发太快
}
}()
// receiver
go func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
for i := range ich { // (2)
if ctx.Err() != nil { // (1)
fmt.Println(ctx.Err())
return
}
fmt.Println(i)
}
}()
}

可以发现:

不论发送者发送一次还是多次,如果接收者所在 goroutine 能够在接收完 channel 中的数据之后结束,那么就不会造成内存泄漏;或者说接收者能够在发送者停止发送后再结束,就不会造成内存泄露。

如果接收者需要在 channel 关闭之前提前退出,为防止内存泄漏,在发送者与接收者发送次数是一对一时,应设置 channel 缓冲队列为 1;在发送者与接收者的发送次数是多对多时,应使用专门的 stop channel 通知发送者关闭相应 channel。

参考文章

  • 如何退出协程 goroutine (超时场景)
  • go channel 关闭的那些事儿

本文转载自: 掘金

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

1…215216217…956

开发者博客

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