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

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


  • 首页

  • 归档

  • 搜索

Matplotlib绘制3D统计图

发表于 2021-11-03

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

前言

Matplotlib 是 Python 的绘图库,它提供了一整套和 matlab 相似的命令 API,可以生成你所需的出版质量级别的图形,而制作3D图形的API与2D API非常相似。我们已经学习了一系列2D统计图的绘制,而在统计图中再添加一个维度可以展示更多信息。而且,在进行常规汇报或演讲时,3D图形也可以吸引更多的注意力。在本文中,我们将探讨利用 Matplotlib 绘制三维统计图。

3D散点图

3D散点图的绘制方式与2D散点图基本相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码import numpy as np
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
# 数据生成
a, b, c = 10., 28., 8. / 3.
def lorenz_map(x, dt = 1e-2):
x_dt = np.array([a * (x[1] - x[0]), x[0] * (b - x[2]) - x[1], x[0] * x[1] - c * x[2]])
return x + dt * x_dt
points = np.zeros((2000, 3))
x = np.array([.1, .0, .0])
for i in range(points.shape[0]):
points[i], x = x, lorenz_map(x)
# 绘制
fig = plt.figure()
ax = fig.gca(projection = '3d')
ax.set_xlabel('X axis')
ax.set_ylabel('Y axis')
ax.set_zlabel('Z axis')
ax.set_title('Lorenz Attractor a=%0.2f b=%0.2f c=%0.2f' % (a, b, c))
ax.scatter(points[:, 0], points[:, 1],points[:, 2], zdir = 'z', c = 'c')
plt.show()

3D散点图

Tips:按住鼠标左键移动鼠标可以旋转查看三维图形将旋转。

为了使用 Matplotlib 进行三维操作,我们首先需要导入 Matplotlib 的三维扩展:

1
python复制代码from mpl_toolkits.mplot3d import Axes3D

对于三维绘图,需要创建一个Figure实例并附加一个 Axes3D 实例:

1
2
python复制代码fig = plt.figure()
ax = fig.gca(projection='3d')

之后,三维散点图的绘制方式与二维散点图完全相同:

1
python复制代码ax.scatter(points[:, 0], points[:, 1],points[:, 2], zdir = 'z', c = 'c')

Tips:需要调用 Axes3D 实例的 scatter() 方法,而非plt中的 scatter 方法。只有 Axes3D 中的 scatter() 方法才能解释三维数据。同时2D统计图中的注释也可以在3D图中使用,例如 set_title()、set_xlabel()、set_ylabel() 和 set_zlabel() 等。

同时可以通过使用 Axes3D.scatter() 的可选参数更改统计通的形状和颜色:

1
python复制代码ax.scatter(points[:, 0], points[:, 1],points[:, 2], zdir = 'z', c = 'c', marker='s', edgecolor='0.5', facecolor='m')

修改样式

3D曲线图

与在3D空间中绘制散点图类似,绘制3D曲线图同样需要设置一个 Axes3D 实例,然后调用其plot()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码# 构造数据集
a, b, c = 10., 28., 8. / 3.
def lorenz_map(x, dt = 1e-2):
x_dt = np.array([a * (x[1] - x[0]), x[0] * (b - x[2]) - x[1], x[0] * x[1] - c * x[2]])
return x + dt * x_dt
points = np.zeros((8000, 3))
x = np.array([.1, .0, .0])
for i in range(points.shape[0]):
points[i], x = x, lorenz_map(x)
# Plotting
fig = plt.figure()
ax = fig.gca(projection = '3d')
ax.plot(points[:, 0], points[:, 1], points[:, 2], c = 'c')
plt.show()

3D曲线图

3D标量场

到目前为止,我们看到的3D绘图方式类似与相应的2D绘图方式,但也有许多特有的三维绘图功能,例如将二维标量场绘制为3D曲面:

1
2
3
4
5
6
7
8
python复制代码x = np.linspace(-3, 3, 256)
y = np.linspace(-3, 3, 256)
x_grid, y_grid = np.meshgrid(x, y)
z = np.sinc(np.sqrt(x_grid ** 2 + y_grid ** 2))
fig = plt.figure()
ax = fig.gca(projection = '3d')
ax.plot_surface(x_grid, y_grid, z, cmap=cm.viridis)
plt.show()

3D标量场

Tips: plot_surface() 方法使用 x、y 和 z 将标量场显示为三维曲面。

可以看到曲面上线条带有显著色彩,如果不希望看到三维曲面上显示的曲线色彩,可以使用 plot_surface() 附加可选参数:

1
python复制代码ax.plot_surface(x_grid, y_grid, z, cmap=cm.viridis, linewidth=0, antialiased=False)

3D标量场

同样,我们也可以仅保持曲线色彩,而曲面不使用其他颜色,这也可以通过 plot_surface() 的可选参数来完成:

1
2
3
4
5
6
7
8
python复制代码x = np.linspace(-3, 3, 256)
y = np.linspace(-3, 3, 256)
x_grid, y_grid = np.meshgrid(x, y)
z = np.sinc(np.sqrt(x_grid ** 2 + y_grid ** 2))
fig = plt.figure()
ax = fig.gca(projection = '3d')
ax.plot_surface(x_grid, y_grid, z, edgecolor='b',color='w')
plt.show()

3D标量场

而如果我们希望消除曲面,而仅使用线框进行绘制,这可以使用 plot_wireframe() 函数:

1
python复制代码ax.plot_wireframe(x_grid, y_grid, z, cstride=10, rstride=10,color='c')

3D标量场

Tips:plot_wireframe() 参数与 plot_surface() 相同,使用两个可选参数 rstride 和 cstride 用于令 Matplotlib 跳过x和y轴上指定数量的坐标,用于减少曲线的密度。

绘制3D曲面

在前述方法中,使用 plot_surface() 来绘制标量:即 f(x, y)=z 形式的函数,但 Matplotlib 也能够使用更通用的方式绘制三维曲面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码# 数据生成
angle = np.linspace(0, 2 * np.pi, 32)
theta, phi = np.meshgrid(angle, angle)
r, r_w = .25, 1.
x = (r_w + r * np.cos(phi)) * np.cos(theta)
y = (r_w + r * np.cos(phi)) * np.sin(theta)
z = r * np.sin(phi)
# 绘制
fig = plt.figure()
ax = fig.gca(projection = '3d')
ax.set_xlim3d(-1, 1)
ax.set_ylim3d(-1, 1)
ax.set_zlim3d(-1, 1)
ax.plot_surface(x, y, z, color = 'c', edgecolor='m', rstride = 2, cstride = 2)
plt.show()

绘制3D曲面

同样可以使用 plot_wireframe() 替换对 plot_surface() 的调用,以便获得圆环的线框视图:

1
python复制代码ax.plot_wireframe(x, y, z, edgecolor='c', rstride = 2, cstride = 1)

绘制3D曲面

在3D坐标轴中绘制2D图形

注释三维图形的一种有效方法是使用二维图形:

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码x = np.linspace(-3, 3, 256)
y = np.linspace(-3, 3, 256)
x_grid, y_grid = np.meshgrid(x, y)
z = np.exp(-(x_grid ** 2 + y_grid ** 2))
u = np.exp(-(x ** 2))
fig = plt.figure()
ax = fig.gca(projection = '3d')
ax.set_zlim3d(0, 3)
ax.plot(x, u, zs=3, zdir='y', lw = 2, color = 'm')
ax.plot(x, u, zs=-3, zdir='x', lw = 2., color = 'c')
ax.plot_surface(x_grid, y_grid, z, color = 'b')
plt.show()

注释三维图形

Axes3D 实例同样支持常用的二维渲染命令,如plot():

1
python复制代码ax.plot(x, u, zs=3, zdir='y', lw = 2, color = 'm')

Axes3D 实例对 plot() 的调用有两个新的可选参数:
zdir :用于决定在哪个平面上绘制2D绘图,可选值包括 x、y 或 z ;
zs :用于决定平面的偏移。
因此,要将二维图形嵌入到三维图形中,只需将二维原语用于 Axes3D 实例,同时使用可选参数,zdir 和 zs,来放置所需渲染图形平面。
接下来,让我们实际查看下在3D空间中堆叠2D条形图的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码import numpy as np
from matplotlib import cm
import matplotlib.colors as col
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
# 数据生成
alpha = 1. / np.linspace(1, 8, 5)
t = np.linspace(0, 5, 16)
t_grid, a_grid = np.meshgrid(t, alpha)
data = np.exp(-t_grid * a_grid)
# 绘制
fig = plt.figure()
ax = fig.gca(projection = '3d')
cmap = cm.ScalarMappable(col.Normalize(0, len(alpha)), cm.viridis)
for i, row in enumerate(data):
ax.bar(4 * t, row, zs=i, zdir='y', alpha=0.8, color=cmap.to_rgba(i))
plt.show()

堆叠2D图形

系列链接

Matplotlib常见统计图的绘制

Matplotlib使用自定义颜色绘制统计图

Matplotlib控制线条样式和线宽

Matplotlib自定义样式绘制精美统计图

Matplotlib在图形中添加文本说明

Matplotlib在图形中添加注释

Matplotlib在图形中添加辅助网格和辅助线

Matplotlib添加自定义形状

Matplotlib控制坐标轴刻度间距和标签

Matplotlib使用对数刻度和极坐标

Matplotlib绘制子图

Matplotlib自定义统计图比例

Matplotlib图形的输出与保存

Matplotlib更多实用图形的绘制

本文转载自: 掘金

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

盘一盘Java中的abstract和interface

发表于 2021-11-03

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

  • 备战2022春招或暑期实习,祝大家每天进步亿点点!Day7
  • 本篇总结的是 《盘一盘Java中的abstract和interface》,后续会每日更新~
  • 关于《Redis入门到精通》、《并发编程》等知识点可以参考我的往期博客
  • 相信自己,越活越坚强,活着就该逢山开路,遇水架桥!生活,你给我压力,我还你奇迹!

image.png

1、简介

abstract和interface关键字在Java中随处可见,它是Java三大特性封装、继承、多态特性的实现重要支柱之一。interface关键字用于定义接口抽象,其本质上是用于定义类型、定义类所具有的能力。但是新手往往错误的使用了abstract和interface,小捌其实也一样犯错误,这篇文章我们盘一盘interface接口和abstract抽象类的使用。


文章开始前建议带着两个疑问阅读:

  1. abstract和interface有什么区别?
  2. abstract和interface应该怎么选?

2、准则

定义接口的时候,有一些准则可以参考,根据这些准则可以更好的确定自己应不应该定义接口、或者是否有其他更好的代替方案。(注意小捌说的点不是绝对正确的,实际开发过程中要具体分析,有不对的可以互相交流。)

2.1 接口优先于抽象类

小捌这里用JDK的源码HashMap的继承体系来说明接口优先于抽象类这一点。

HashMap继承体系类图结构:

HashMap的顶层接口:

1
csharp复制代码public interface Map<K,V>{}

HashMap实现的抽象类:

1
csharp复制代码public abstract class AbstractMap<K,V> implements Map<K,V> {}

可以看到HashMap继承了AbstractMap抽象类实现了Map接口,但为什么说接口优先于抽象类呢?这些因为Java是单继承多实现,HashMap继承了AbstractMap抽象类之后就无法继承其他类了,如果是接口就没有这个限制,比如HashMap还需要提供序列化和克隆的功能,HashMap就可以实现三个接口Map<K,V>, Cloneable, Serializable。

既然这样为什么HashMap还要去继承AbstractMap抽象类呢?

这是因为在JDK源码设计中,Map结构JDK需要提供部分方法的默认实现,因此JDK的作者们单独拉取了一个抽象类来实现这些方法;尽管Java8 Oracle尝试在接口中提供静态方法和普通方法,但是小捌认为没有到一定的需求程度,尽量、甚至完全不应该将方法实现定义在接口中。

abstract和interface有什么区别呢?

其实在Java8之后区别在不断的缩小,但是总体上来说还是两个完全不同的概念:

抽象类abstract的特点:

  • 抽象方法和抽象类都必须被abstract关键字修饰
  • 一个类中有抽象方法,那么这个类一定是抽象类
  • 抽象类中不一定有抽象方法
  • 抽象类中可以存在构造方法
  • 抽象类中可以存在普通属性、方法、静态属性和静态方法
  • 抽象类的方法必须在子类中实现,否则子类也需要定义为抽象类
  • 抽象类不可以用new创建对象,因为调用抽象方法没有实现就没有意义

接口interface的特点:

  • 接口中的方法,都被public来修饰
  • 接口中没有构造方法,不能实例化接口对象
  • 接口中只有常量,如果定义变量,则默认加上public static final
  • 使用接口可以实现多继承
  • 接口中只有方法的声明,没有方法体(适用于Java8之前,当我没说,但是很多人都是这么认为的,这种错误的认为往往能正确的设计代码)
  • 接口中可以声明静态方法,必须是public修饰(默认),静态方法无法被子类重写
  • 接口中可以声明普通方法,必须是default修饰
比较项 抽象类(abstract) 接口(interface)
多继承 不支持(只能继承一个抽象类) 支持(类可以实现很多个接口)
方法 抽象类则可以同时包含抽象和非抽象的方法 接口中所有的方法隐含都是抽象的(Java可以定义静态方法)
构造器 允许 不允许
实例化 不能实例化 不能实例化
访问修饰符 抽象类可以使用public、default;抽象方法可以使用public、default、protected;普通方法可以使用public、default、protected、private 接口可以使用public、default;方法默认public;

总结:

  1. 在整个抽象实现体系中,必须提供一些方法的默认实现,可以使用抽象类(因为非常不建议在接口中直接实现某些方法)
  2. 如果不需要提供默认实现,且需要实现多继承的功能就使用接口

2.2 接口中不应该实现方法

接口无处不在,接口作为类体系结构的最顶层,接口提供的一切约束和规范都是直接影响下层实现类。因此不建议在接口中实现具体的方法,尽管Java8之后的接口定义可以提供静态方法实现和普通方法实现,但是这种实现方式有很大的风险,除非你的接口设计真的很完美,完美到能对所有的实现类都负责任的说你的逻辑永远不会变。要不然接口的具体实现方法逻辑修改后,下面那些使用了该方法的类都得遭殃。

因此接口尽可能的只用来定义类型、定义类所具有的能力。如果一定要定义实现,可以考虑使用抽象类来定义。


2.3 接口不应该用于导出常量

由于接口中定义常量非常方便,因此有一些小伙伴会使用接口直接作为常量导出类,比如如下这种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码/**
* <p>
* 缓存key
* </p>
*
* @Author: Liziba
* @Date: 2021/11/2 23:12
*/
public interface CacheKey {

String USER = "user";

String ORDER = "order";

String MAIL = "mail";

}

它虽然看起来非常简便、使用上也没什么问题。但是问题就出在接口它不是用来给你导出常量的,如果需要定义常量我们可以使用枚举或者常量类,比如如下这种方法:

1
2
3
4
5
6
7
8
9
arduino复制代码public class CacheKey {

public static final String USER = "user";

public static final String ORDER = "order";

public static final String MAIL = "mail";

}

注意小捌这里说的是不要拿接口仅仅只作为常量导出类,而不是说不能在接口中定义常量,如果部分常量是类抽象类型中统一使用的可以考虑这样设计(但是也不推荐啦!),单独抽出常量类来管理这些常量往往要更好一些的。

本文转载自: 掘金

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

优化的三子棋 三子棋

发表于 2021-11-03

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

三子棋

这是一个用C语言写的简单小游戏,也是我第一个用C语言写的小游戏,游戏虽小,代码俱全。

test.c文件

为了方便自述,还是从mian进入(我也优化过三子棋代码,不能无限输错)

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
int复制代码{
int input = 0;
int timer = 3;//游戏剩余次数
int flag = 1;//进入游戏标志
srand((unsigned)time(NULL));//设置随机起点(运用时间戳)
/*为了体会一下游戏的可玩性,先玩一把然后再决定玩与不玩
因此用do while循环*/
do
{
Menu(); //菜单函数
printf("请选择:>");
scanf("%d",&input);
switch (input)//选择模式
{
case 1:
flag = 0; //玩游戏时游戏标志清零
game();
break;
case 0:
printf("退出\n");
break;
default:
if (flag == 0) //将游戏标记置一
{
flag = 1;
timer = 3;//可玩次数重新置三
}
timer--;
if (timer == 0)//当没有游戏次数时就不能玩了
{
printf("没有选择机会了,已退出游戏。");
break;
}
printf("选择错误,可以重新选择,还有%d错误就强制退出。time = %d\n",timer,timer);
break;
}
} while(input&&timer);//可玩次数为0,自己不想玩都可以退出游戏
return 0;
}

菜单函数

1
2
3
4
5
void复制代码{
printf("**********************\n");
printf("***1.play 2.exit***\n");
printf("**********************\n");
}

game函数

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
void复制代码{
char ret = 0;
char board[ROW][COL];//棋盘数组
InitBoard(board,ROW,COL);//初始化棋盘
DisplayBoard(board,ROW,COL);//打印棋盘
/*玩家和电脑对打是有来有回的因此循环*/
while (1)
{
PlayerMove(board, ROW, COL); //玩家移动
DisplayBoard(board, ROW, COL);//打印棋盘
ret = IsWin(board, ROW, COL); //判断输赢函数
if (ret != 'C') //不能继续就退出
{
break;
}
ComputerMove(board, ROW, COL);//电脑移动
DisplayBoard(board, ROW, COL);//打印棋盘
ret = IsWin(board, ROW, COL); //判断输赢函数
if (ret != 'C') //不能继续就退出
{
break;
}
}
if (ret == '*')
{
printf("玩家赢\n");
}
else if (ret == '#')
{
printf("电脑赢\n");
}
else
{
printf("平局\n");
}

}

game.c文件

InitBoard初始化棋盘函数

这里为什么要用传过来的row,col呢,有人会说不是有ROW,COL吗理论上是没有问题的,但不用自己传过来的参数,用COL,ROW是被限制了的,独立性下降了许多,受到到了限制,用自己传过来的参数,是灵活变化的,不是依赖于COL,ROW的

1
2
3
4
5
6
7
8
9
10
11
void复制代码{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0 ; j < col; j++)
{
board[i][j] = ' ';
}
}
}

DisplayBoard打印棋盘函数

棋盘设计每个人可能多多少少有点不同,这个就是棋盘界面,没有花里胡哨,只有普通的分割行,数据行

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
void复制代码{
/*int i = 0;
for ( i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("%c", board[i][j]);
}
printf("\n");
}*/
int i = 0;
for (i = 0; i <= row; i++)
{
int j = 0;
for ( j = 0; j <= col; j++)
{
//打印分割行
printf("*");
if(j<col)
printf("---");
}
printf("\n");
if (i < row)
{
for (j = 0; j <= col; j++)
{
//打印数据行
printf("|");
if (j < col)
printf(" %c ", board[i][j]);
}
printf("\n");
}
}
}

PlayerMove玩家移动函数

玩家移动,为了体现自己的价值,所以先手永远都是给玩家先走,诶我可以给电脑走可是就不给,就是玩。为了方便视觉观察,玩家移动电脑移动都用汉字提示

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
void复制代码{
int x = 0;
int y = 0;
int time = 3;//我们也不能放纵玩家瞎玩,连续瞎移动3次也让你此局报废
int flag = 1;
printf("玩家移动:>\n");
while (time)
{
printf("请输入坐标;>");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (flag == 0)//到了这里说明你还是想玩的,标记置1,次数充满
{
flag = 1;
time = 3;
}
if (board[x - 1][y - 1] == ' ')
{
board[x - 1][y - 1] = '*';
break;
}
else
{
printf("已经被占用,请重新落子:\n");
}
}
else
{
flag = 0;//到了这个代码块首先标记置0
time--;//可玩次数减少
if (time == 0)//这里我们也赋予棋盘爆怒情绪
{
printf("你不想玩就滚,当我没脾气啊.\n");
break;
}
printf("坐标非法,超出范围:你还有%d次机会\n", time);
}
}
}

ComputerMove电脑移动函数

电脑移动,你永远都是后手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void复制代码{
int x = 0;
int y = 0;
printf("电脑移动:>");
while (1)
{
x = rand() % row;//这里的rand是和srand配合使用的
y = rand() % col;
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}

IsFull判断棋盘是否下满函数

1
2
3
4
5
6
7
8
9
10
11
12
13
int复制代码{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')
return 0;
}
}
return 1;//两层循环都没有空格说明满了
}

IsWin判断是否赢函数

判断游戏输赢 返回4种不同的状态: return ‘*‘ 玩家赢 return ‘#’ 电脑赢 return ‘Q’ 平局 return ‘C’ 继续

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
char复制代码{
//判断输赢
int i = 0;
for ( i = 0; i < row; i++)//行判断
{
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][0] != ' ')
return board[i][0]; //这步非常不错省了不少代码,因为打印的符号和返回的符号相同
}

for (i = 0; i < col; i++)//列判断
{
if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != ' ')
return board[0][i];
}
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[0][0] != ' ')//对角线判断
return board[0][0];
if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[1][1] != ' ')//对角线判断
return board[1][1];
//判断平局,只需要看一下棋盘是不是满了就行
if (IsFull(board, row, col))
return 'Q';
//游戏继续
return 'C';
}

game.h文件

宏

宏的出现大大加大了代码的可移植性

1
#define复制代码#define COL 3//列

函数申明

1
2
3
4
5
6
7
8
9
10
void复制代码 
void DisplayBoard(char board[ROW][COL], int row, int col);

void PlayerMove(char board[ROW][COL], int row, int col);

void ComputerMove(char board[ROW][COL], int row, int col);

char IsWin(char board[ROW][COL], int row, int col);

int IsFull(char board[ROW][COL], int row, int col);

测试图

瞎选的后果

只要在最后以后选正确,次数充满

下面多条测试

本文转载自: 掘金

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

还在重复造轮子?duck不必,hutool一键搞定👍

发表于 2021-11-03

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」。PS:已经更文多少天,N就写几。一定要写对文案,否则文章不计入在内。

点赞再看,养成习惯。👏👏

一、前言

Hello 大家好,我是l拉不拉米,今天给大家分享一个国产开源的顶级项目,Java开发的利器👉【Hutool】。

本文已收录到 Github-java3c ,里面有我的系列文章,欢迎大家Star。

二、Hutool简介

Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以“甜甜的”。

Hutool中的工具方法来自每个用户的精雕细琢,它涵盖了Java开发底层代码中的方方面面,它既是大型项目开发中解决小问题的利器,也是小型项目中的效率担当;

  • Web开发
  • 与其它框架无耦合
  • 高度可替换

Hutool由上百个工具类组成,涵盖了日常开发中的方方面面,可谓工具类集大成者。

image.png

三、Hutool设计哲学

Hutool的设计思想是尽量减少重复的定义,让项目中的util这个package尽量少,总的来说有如下的几个思想:

  • 方法优先于对象
  • 自动识别优于用户定义
  • 便捷性与灵活性并存
  • 适配与兼容
  • 可选依赖原则
  • 无侵入原则

开发者不再需要重复造轮子,或引入各种依赖包,Hutool帮您 All In One。

四、包含组件

一个Java基础工具类,对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装,组成各种Util工具类,同时提供以下组件:

模块 介绍
hutool-aop JDK动态代理封装,提供非IOC下的切面支持
hutool-bloomFilter 布隆过滤,提供一些Hash算法的布隆过滤
hutool-cache 简单缓存实现
hutool-core 核心,包括Bean操作、日期、各种Util等
hutool-cron 定时任务模块,提供类Crontab表达式的定时任务
hutool-crypto 加密解密模块,提供对称、非对称和摘要算法封装
hutool-db JDBC封装后的数据操作,基于ActiveRecord思想
hutool-dfa 基于DFA模型的多关键字查找
hutool-extra 扩展模块,对第三方封装(模板引擎、邮件、Servlet、二维码、Emoji、FTP、分词等)
hutool-http 基于HttpUrlConnection的Http客户端封装
hutool-log 自动识别日志实现的日志门面
hutool-script 脚本执行封装,例如Javascript
hutool-setting 功能更强大的Setting配置文件和Properties封装
hutool-system 系统参数调用封装(JVM信息等)
hutool-json JSON实现
hutool-captcha 图片验证码实现
hutool-poi 针对POI中Excel和Word的封装
hutool-socket 基于Java的NIO和AIO的Socket封装
hutool-jwt JSON Web Token (JWT)封装实现

可以根据需求对每个模块单独引入,也可以通过引入hutool-all方式引入所有模块。

五、安装

🍊Maven

在项目的pom.xml的dependencies中加入以下内容:

1
2
3
4
5
xml复制代码<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.15</version>
</dependency>Copy to clipboardErrorCopied

🍐Gradle

1
arduino复制代码implementation 'cn.hutool:hutool-all:5.7.15'

📥下载jar

点击以下链接,下载hutool-all-X.X.X.jar即可:

  • Maven中央库

🔔️注意 Hutool 5.x支持JDK8+,对Android平台没有测试,不能保证所有工具类或工具方法可用。 如果你的项目使用JDK7,请使用Hutool 4.x版本(不再更新)

🚽编译安装

访问Hutool的Gitee主页:gitee.com/dromara/hut… 下载整个项目源码(v5-master或v5-dev分支都可)然后进入Hutool项目目录执行:

1
bash复制代码./hutool.sh installCopy to clipboardErrorCopied

然后就可以使用Maven引入了。

六、常用轮子

类型转换工具类-Convert

Convert类可以说是一个工具方法类,里面封装了针对Java常见类型的转换,用于简化类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码//转换为字符串
int a = 1;
//aStr为"1"
String aStr = Convert.toStr(a);

//转换为指定类型数组
String[] b = {"1", "2", "3", "4"};
Integer[] bArr = Convert.toIntArray(b);

//转换为日期对象
String dateStr = "2017-05-06";
Date date = Convert.toDate(dateStr);

//转换为列表
String[] strArr = {"a", "b", "c", "d"};
List<String> strList = Convert.toList(String.class, strArr);

日期时间工具-DateUtil

主要提供日期和字符串之间的转换,以及提供对日期的定位(一个月前等等)。

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
java复制代码//当前时间 
Date date = DateUtil.date();

//Calendar转Date
Date date = DateUtil.date(Calendar.getInstance());

//时间戳转Date
Date date = DateUtil.date(System.currentTimeMillis());

//自动识别格式转换
String dateStr = "2017-03-01";
Date date = DateUtil.parse(dateStr);

//自定义格式化转换
Date date = DateUtil.parse(dateStr, "yyyy-MM-dd");

//格式化输出日期
String format = DateUtil.format(date, "yyyy-MM-dd");

//获得年的部分
int year = DateUtil.year(date);

//获得月份,从0开始计数
int month = DateUtil.month(date);

//获取某天的开始、结束时间
Date beginOfDay = DateUtil.beginOfDay(date);
Date endOfDay = DateUtil.endOfDay(date);

//计算偏移后的日期时间
Date newDate = DateUtil.offset(date, DateField.DAY_OF_MONTH, 2);

//计算日期时间之间的偏移量
long betweenDay = DateUtil.between(date, newDate, DateUnit.DAY);

文件工具类-FileUtil

在IO操作中,文件的操作相对来说是比较复杂的,但也是使用频率最高的部分,我们几乎所有的项目中几乎都躺着一个叫做FileUtil或者FileUtils的工具类,我想Hutool应该将这个工具类纳入其中,解决用来解决大部分的文件操作问题。

总体来说,FileUtil类包含以下几类操作工具:

  1. 文件操作:包括文件目录的新建、删除、复制、移动、改名等
  2. 文件判断:判断文件或目录是否非空,是否为目录,是否为文件等等。
  3. 绝对路径:针对ClassPath中的文件转换为绝对路径文件。
  4. 文件名:主文件名,扩展名的获取
  5. 读操作:包括类似IoUtil中的getReader、readXXX操作
  6. 写操作:包括getWriter和writeXXX操作
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码//新建文件夹
FileUtil.mkdir("D:\");

//新建文件
FileUtil.file("D:\", "F:\");

//新建临时文件
FileUtil.createTempFile(FileUtil.file("D:\"));

//是否非空,是否为目录,是否为文件
FileUtil.isEmpty(FileUtil.file("D:\"));
FileUtil.isDirectory("D:\");
FileUtil.isFile("D:\");

字符串工具-StrUtil

这个工具的用处类似于Apache Commons Lang中的StringUtil,之所以使用StrUtil而不是使用StringUtil是因为前者更短,而且Str这个简写我想已经深入人心了,大家都知道是字符串的意思。常用的方法例如isBlank、isNotBlank、isEmpty、isNotEmpty这些我就不做介绍了,判断字符串是否为空,下面我说几个比较好用的功能。

1
2
3
4
5
6
7
8
9
10
11
java复制代码//判断是否为空字符串 String str = "test"; 
StrUtil.isEmpty(str);
StrUtil.isNotEmpty(str);

//去除字符串的前后缀
StrUtil.removeSuffix("a.jpg", ".jpg");
StrUtil.removePrefix("a.jpg", "a.");

//格式化字符串
String template = "这只是个占位符:{}";
String str2 = StrUtil.format(template, "我是占位符");

反射工具-ReflectUtil

Java的反射机制,可以让语言变得更加灵活,对对象的操作也更加“动态”,因此在某些情况下,反射可以做到事半功倍的效果。Hutool针对Java的反射机制做了工具化封装,封装包括:

  1. 获取构造方法
  2. 获取字段
  3. 获取字段值
  4. 获取方法
  5. 执行方法(对象方法和静态方法)
1
2
3
4
5
6
7
8
9
10
11
java复制代码//获取某个类的所有方法 
Method[] methods = ReflectUtil.getMethods(PmsBrand.class);

//获取某个类的指定方法
Method method = ReflectUtil.getMethod(PmsBrand.class, "getId");

//使用反射来创建对象
PmsBrand pmsBrand = ReflectUtil.newInstance(PmsBrand.class);

//反射执行对象的方法
ReflectUtil.invoke(pmsBrand, "setId", 1);

数字工具-NumberUtil

数字工具针对数学运算做工具性封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码double n1 = 1.234; 
double n2 = 1.234; double result;

//对float、double、BigDecimal做加减乘除操作
result = NumberUtil.add(n1, n2);
result = NumberUtil.sub(n1, n2);
result = NumberUtil.mul(n1, n2);
result = NumberUtil.div(n1, n2);

//保留两位小数
BigDecimal roundNum = NumberUtil.round(n1, 2);
String n3 = "1.234";

//判断是否为数字、整数、浮点数
NumberUtil.isNumber(n3);
NumberUtil.isInteger(n3);
NumberUtil.isDouble(n3);

数组工具-ArrayUtil

数组工具类主要针对原始类型数组和泛型数组相关方案进行封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码//判空
int[] a = {};
int[] b = null;
ArrayUtil.isEmpty(a);
ArrayUtil.isEmpty(b);

//新建泛型数组
String[] newArray = ArrayUtil.newArray(String.class, 3);
//泛型数组合并
int[] result = ArrayUtil.addAll(a, b);

//过滤
Integer[] a = {1,2,3,4,5,6};
// [2,4,6]
Integer[] filter = ArrayUtil.filter(a, (Editor<Integer>) t -> (t % 2 == 0) ? t : null);

//是否包含
boolean flag = ArrayUtil.contains(a, 1);
//使用间隔符将一个数组转为字符串
String result = ArrayUtil.join(new int[]{1,2,3,4}, "-");

信息脱敏工具-DesensitizedUtil

在数据处理或清洗中,可能涉及到很多隐私信息的脱敏工作,因此Hutool针对常用的信息封装了一些脱敏方法。

现阶段支持的脱敏数据类型包括:

  1. 用户id
  2. 中文姓名
  3. 身份证号
  4. 座机号
  5. 手机号
  6. 地址
  7. 电子邮件
  8. 密码
  9. 中国大陆车牌,包含普通车辆、新能源车辆
  10. 银行卡

整体来说,所谓脱敏就是隐藏掉信息中的一部分关键信息,用*代替,自定义隐藏可以使用StrUtil.hide方法完成。

1
2
3
4
5
6
7
8
java复制代码// 身份证脱敏 5***************1X 
DesensitizedUtil.idCardNum("51343620000320711X", 1, 2);

// 手机号脱敏 180****1999
DesensitizedUtil.mobilePhone("18049531999");

// 密码脱敏 **********
DesensitizedUtil.password("1234567890");

身份证工具-IdcardUtil

IdcardUtil现在支持大陆15位、18位身份证,港澳台10位身份证。

工具中主要的方法包括:

  1. isValidCard 验证身份证是否合法
  2. convert15To18 身份证15位转18位
  3. getBirthByIdCard 获取生日
  4. getAgeByIdCard 获取年龄
  5. getYearByIdCard 获取生日年
  6. getMonthByIdCard 获取生日月
  7. getDayByIdCard 获取生日天
  8. getGenderByIdCard 获取性别
  9. getProvinceByIdCard 获取省份
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
java复制代码String ID_18 = "321083197812162119"; 
String ID_15 = "150102880730303";

//是否有效 boolean valid = IdcardUtil.isValidCard(ID_18);
boolean valid15 = IdcardUtil.isValidCard(ID_15);

//转换
String convert15To18 = IdcardUtil.convert15To18(ID_15);
Assert.assertEquals(convert15To18, "150102198807303035");

//年龄
DateTime date = DateUtil.parse("2017-04-10");
int age = IdcardUtil.getAgeByIdCard(ID_18, date);
Assert.assertEquals(age, 38);
int age2 = IdcardUtil.getAgeByIdCard(ID_15, date);
Assert.assertEquals(age2, 28);

//生日
String birth = IdcardUtil.getBirthByIdCard(ID_18);
Assert.assertEquals(birth, "19781216");
String birth2 = IdcardUtil.getBirthByIdCard(ID_15);
Assert.assertEquals(birth2, "19880730");

//省份
String province = IdcardUtil.getProvinceByIdCard(ID_18);
Assert.assertEquals(province, "江苏");
String province2 = IdcardUtil.getProvinceByIdCard(ID_15);
Assert.assertEquals(province2, "内蒙古");

Bean工具-BeanUtil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码// Bean转Map
SubPerson person = new SubPerson();
person.setAge(14);
person.setOpenid("11213232");
person.setName("测试A11");
person.setSubName("sub名字");
Map<String, Object> map = BeanUtil.beanToMap(person);

//Map转Bean
SubPerson person = BeanUtil.mapToBean(map, SubPerson.class, false);

//Bean属性拷贝
SubPerson copyPerson = new SubPerson();
BeanUtil.copyProperties(person, copyPerson);

集合工具-CollUtil

这个工具主要增加了对数组、集合类的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码//数组转换为列表 
String[] array = new String[]{"a", "b", "c", "d", "e"};
List<String> list = CollUtil.newArrayList(array);

//join:数组转字符串时添加连接符号
String joinStr = CollUtil.join(list, ",");

//将以连接符号分隔的字符串再转换为列表
List<String> splitList = StrUtil.split(joinStr, ',');

//创建新的Map、Set、List
HashMap<Object, Object> newMap = CollUtil.newHashMap();
HashSet<Object> newHashSet = CollUtil.newHashSet();
ArrayList<Object> newList = CollUtil.newArrayList();

//判断列表是否为空
CollUtil.isEmpty(list);

Map工具-MapUtil

MapUtil是针对Map的一一列工具方法的封装,包括getXXX的快捷值转换方法。

  • isEmpty、isNotEmpty 判断Map为空和非空方法,空的定义为null或没有值
  • newHashMap 快速创建多种类型的HashMap实例
  • createMap 创建自定义的Map类型的Map
  • of 此方法将一个或多个键值对加入到一个新建的Map中,下面是栗子:
1
2
3
4
5
java复制代码Map<Object, Object> colorMap = MapUtil.of(new String[][] { 
{"RED", "#FF0000"},
{"GREEN", "#00FF00"},
{"BLUE", "#0000FF"}
});

网络工具-NetUtil

NetUtil 工具中主要的方法包括:

  1. longToIpv4 根据long值获取ip v4地址
  2. ipv4ToLong 根据ip地址计算出long型的数据
  3. isUsableLocalPort 检测本地端口可用性
  4. isValidPort 是否为有效的端口
  5. isInnerIP 判定是否为内网IP
  6. localIpv4s 获得本机的IP地址列表
  7. toAbsoluteUrl 相对URL转换为绝对URL
  8. hideIpPart 隐藏掉IP地址的最后一部分为 * 代替
  9. buildInetSocketAddress 构建InetSocketAddress
  10. getIpByHost 通过域名得到IP
  11. isInner 指定IP的long是否在指定范围内
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码String ip= "127.0.0.1"; 
long iplong = 2130706433L;

//根据long值获取ip v4地址
String ip= NetUtil.longToIpv4(iplong);

//根据ip地址计算出long型的数据
long ip= NetUtil.ipv4ToLong(ip);

//检测本地端口可用性
boolean result= NetUtil.isUsableLocalPort(6379);

//是否为有效的端口
boolean result= NetUtil.isValidPort(6379);

//隐藏掉IP地址
String result =NetUtil.hideIpPart(ip);

图片工具-ImgUtil

针对awt中图片处理进行封装,这些封装包括:缩放、裁剪、转为黑白、加水印等操作。

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
java复制代码//缩放图片
ImgUtil.scale(
FileUtil.file("d:/face.jpg"),
FileUtil.file("d:/face_result.jpg"),
0.5f //缩放比例
);

//裁剪图片
ImgUtil.cut(
FileUtil.file("d:/face.jpg"),
FileUtil.file("d:/face_result.jpg"),
new Rectangle(200, 200, 100, 100) //裁剪的矩形区域
);
//`slice` 按照行列剪裁切片(将图片分为20行和20列)
ImgUtil.slice(FileUtil.file("e:/test2.png"), FileUtil.file("e:/dest/"), 10, 10);

//转换图片格式
ImgUtil.convert(FileUtil.file("e:/test2.png"), FileUtil.file("e:/test2Convert.jpg"));

//转黑白
ImgUtil.gray(FileUtil.file("d:/logo.png"), FileUtil.file("d:/result.png"));

//添加文字水印
ImgUtil.pressText(
FileUtil.file("e:/pic/face.jpg"),
FileUtil.file("e:/pic/test2_result.png"),
"版权所有", Color.WHITE, //文字
new Font("黑体", Font.BOLD, 100), //字体
0, //x坐标修正值。 默认在中间,偏移量相对于中间偏移
0, //y坐标修正值。 默认在中间,偏移量相对于中间偏移
0.8f//透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
);

//添加图片水印
ImgUtil.pressImage(
FileUtil.file("d:/picTest/1.jpg"),
FileUtil.file("d:/picTest/dest.jpg"),
ImgUtil.read(FileUtil.file("d:/picTest/1432613.jpg")), //水印图片
0, //x坐标修正值。 默认在中间,偏移量相对于中间偏移
0, //y坐标修正值。 默认在中间,偏移量相对于中间偏移
0.1f
);

// 旋转180度
BufferedImage image = ImgUtil.rotate(ImageIO.read(FileUtil.file("e:/pic/366466.jpg")), 180); ImgUtil.write(image, FileUtil.file("e:/pic/result.png"));

JSON工具-JSONUtil

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
java复制代码//Json字符解析
String html = "{\"name\":\"Something must have been changed since you leave\"}";
JSONObject jsonObject = JSONUtil.parseObj(html); jsonObject.getStr("name");

//Json字符创建
SortedMap<Object, Object> sortedMap = new TreeMap<Object, Object>() {
private static final long serialVersionUID = 1L;
{
put("attributes", "a");
put("b", "b");
put("c", "c");
}
};
JSONUtil.toJsonStr(sortedMap);

//XML字符转json
String s = "<sfzh>123</sfzh><sfz>456</sfz><name>aa</name><gender>1</gender>";
JSONObject json = JSONUtil.parseFromXml(s);
json.get("sfzh");
json.get("name");

//json转Bean
String json = "{\"ADT\":[[{\"BookingCode\":[\"N\",\"N\"]}]]}";
Price price = JSONUtil.toBean(json, Price.class);
price.getADT().get(0).get(0).getBookingCode().get(0);

加密解密工具-SecureUtil

对称加密

  • SecureUtil.aes
  • SecureUtil.des

摘要算法

  • SecureUtil.md5
  • SecureUtil.sha1
  • SecureUtil.hmac
  • SecureUtil.hmacMd5
  • SecureUtil.hmacSha1

非对称加密

  • SecureUtil.rsa
  • SecureUtil.dsa

UUID

  • SecureUtil.simpleUUID 方法提供无“-”的UUID

密钥生成

  • SecureUtil.generateKey 针对对称加密生成密钥
  • SecureUtil.generateKeyPair 生成密钥对(用于非对称加密)
  • SecureUtil.generateSignature 生成签名(用于非对称加密)

Http客户端工具类-HttpUtil

HttpUtil是应对简单场景下Http请求的工具类封装,此工具封装了HttpRequest对象常用操作,可以保证在一个方法之内完成Http请求。

此模块基于JDK的HttpUrlConnection封装完成,完整支持https、代理和文件上传。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码// 最简单的HTTP请求,可以自动通过header等信息判断编码,不区分HTTP和HTTPS 
String result1= HttpUtil.get("https://www.baidu.com");

// Post请求文件上传
HashMap<String, Object> paramMap = new HashMap<>();
//文件上传只需将参数中的键指定(默认file),值设为文件对象即可,对于使用者来说,文件上传与普通表单提交并无区别
paramMap.put("file", FileUtil.file("D:\\face.jpg"));
String result= HttpUtil.post("https://www.baidu.com", paramMap);

//文件下载
String fileUrl = "http://mirrors.sohu.com/centos/8.4.2105/isos/x86_64/CentOS-8.4.2105-x86_64-dvd1.iso";
//将文件下载后保存在E盘,返回结果为下载文件大小
long size = HttpUtil.downloadFile(fileUrl, FileUtil.file("e:/"));
System.out.println("Download size: " + size);

Excel工具-ExcelUtil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码//读取excel文件
ExcelReader reader = ExcelUtil.getReader("d:/aaa.xlsx");
List<List<Object>> readAll = reader.read();
List<Map<String,Object>> readAll = reader.readAll();
List<Person> all = reader.readAll(Person.class);

//写出到客户端
// 通过工具类创建writer,默认创建xls格式
ExcelWriter writer = ExcelUtil.getWriter();
// 一次性写出内容,使用默认样式,强制输出标题
writer.write(rows, true);
//out为OutputStream,需要写出到的目标流
//response为HttpServletResponse对象
response.setContentType("application/vnd.ms-excel;charset=utf-8");
//test.xls是弹出下载对话框的文件名,不能为中文,中文请自行编码
response.setHeader("Content-Disposition","attachment;filename=test.xls");
ServletOutputStream out=response.getOutputStream();
writer.flush(out, true);
// 关闭writer,释放内存
writer.close();
//此处记得关闭输出Servlet流
IoUtil.close(out);

图形验证码-CaptchaUtil

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码//定义图形验证码的长和宽 
LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100);
//图形验证码写出,可以写出到文件,也可以写出到流
lineCaptcha.write("d:/line.png");
//输出code
Console.log(lineCaptcha.getCode());
//验证图形验证码的有效性,返回boolean值
lineCaptcha.verify("1234");
//重新生成验证码
lineCaptcha.createCode();
lineCaptcha.write("d:/line.png");
//新的验证码
Console.log(lineCaptcha.getCode());
//验证图形验证码的有效性,返回boolean值
lineCaptcha.verify("1234");

七、项目地址

  • 官网地址:www.hutool.cn/
  • API文档地址:apidoc.gitee.com/dromara/hut…
  • 参考文档地址:www.hutool.cn/docs/#/
  • GitHub地址:github.com/dromara/hut…
  • Gitee地址:gitee.com/dromara/hut…
  • 视频介绍地址:www.bilibili.com/video/BV1bQ…

本文转载自: 掘金

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

Java 并发基础:synchronized 锁同步

发表于 2021-11-03

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

本文被《从小工到专家的 Java 进阶之旅》收录。

你好,我是看山。

synchronized 是 java 内置的同步锁实现,一个关键字实现对共享资源的锁定。synchronized 有 3 种使用场景,场景不同,加锁对象也不同:

  1. 普通方法:锁对象是当前实例对象
  2. 静态方法:锁对象是类的 Class 对象
  3. 方法块:锁对象是 synchronized 括号中的对象

synchronized 实现原理

synchronized 是通过进入和退出 Monitor 对象实现锁机制,代码块通过一对 monitorenter/monitorexit 指令实现。在编译后,monitorenter 指令插入到同步代码块的开始位置,monitorexit 指令插入到方法结束和异常处,JVM 要保证 monitorenter 和 monitorexit 成对出现。任何对象都有一个 Monitor 与之关联,当且仅当一个 Monitor 被持有后,它将处于锁状态。

在执行 monitorenter 时,首先尝试获取对象的锁,如果对象没有被锁定或者当前线程持有锁,锁的计数器加 1;相应的,在执行 monitorexit 指令时,将锁的计数器减 1。当计数器减到 0 时,锁释放。如果在 monitorenter 获取锁失败,当前线程会被阻塞,直到对象锁被释放。

在 JDK6 之前,Monitor 的实现是依靠操作系统内部的互斥锁实现(一般使用的是 Mutex Lock 实现),线程阻塞会进行用户态和内核态的切换,所以同步操作是一个无差别的重量级锁。

后来,JDK 对 synchronized 进行升级,为了避免线程阻塞时在用户态与内核态之间切换线程,会在操作系统阻塞线程前,加入自旋操作。然后还实现 3 种不同的 Monitor:偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)、重量级锁。在 JDK6 之后,synchronized 的性能得到很大的提升,相比于 ReentrantLock 而言,性能并不差,只不过 ReentrantLock 使用起来更加灵活。

适应性自旋(Adaptive Spinning)

synchronized 对性能影响最大的是阻塞的实现,挂起线程和恢复线程都需要操作系统帮助完成,需要从用户态转到内核态,状态转换需要耗费很多 CPU 时间。

在我们大多数的应用中,共享数据的锁定状态只会持续很短的一段时间,为了这段时间挂起和回复线程消耗的时间不值得。而且,现在大多数的处理器都是多核处理器,如果让后一个线程再等一会,不释放 CPU,等前一个释放锁,后一个线程立马获取锁执行任务就行。这就是所谓的自旋,让线程执行一个忙循环,自己在原地转一会,每转一圈看看锁释放没有,释放了直接获取锁,没有释放就再转一圈。

自旋锁是在 JDK 1.4.2 引入(使用-XX:+UseSpinning参数打开),JDK 1.6 默认打开。自旋锁不能代替阻塞,因为自旋等待虽然避免了线程切换的开销,但是它要占用 CPU 时间,如果锁占用时间短,自旋等待效果挺好,反之,则是性能浪费。所以在 JDK 1.6 中引入了自适应自旋锁:如果同一个锁对象,自旋等待刚成功,且持有锁的线程正在运行,那本次自旋很有可能成功,会允许自旋等待持续时间长一些。反之,如果对于某个锁,自旋很少成功,那之后很有可能直接省略自旋过程,避免浪费 CPU 资源。

锁升级

Java 对象头

synchronized 用的锁存在于 Java 对象头里,对象头里的 Mark Word 里存储的数据会随标志位的变化而变化,变化如下:

图片

Java 对象头 Mark Word

偏向锁(Biased Locking)

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引入偏向锁。

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁,只需简单地测试一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。

偏向锁获取

  1. 当锁对象第一次被线程获取时,对象头的标志位设为 01,偏向模式设为 1,表示进入偏向模式。
  2. 测试线程 ID 是否指向当前线程,如果是,执行同步代码块,如果否,进入 3
  3. 使用 CAS 操作把获得到的这个锁的线程 ID 记录在对象的 Mark Word 中。如果成功,执行同步代码块,如果失败,说明存在过其他线程持有锁对象的偏向锁,开始尝试当前线程获取偏向锁
  4. 当到达全局安全点时(没有字节码正在执行),会暂停拥有偏向锁的线程,检查线程状态。如果线程已经结束,则将对象头设置成无锁状态(标志位为“01”),然后重新偏向新的线程;如果线程仍然活着,撤销偏向锁后升级到轻量级锁状态(标志位为“00”),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。

偏向锁释放

偏向锁的释放采用的是惰性释放机制:只有等到竞争出现,才释放偏向锁。释放过程就是上面说的第 4 步,这里不再赘述。

关闭偏向锁

偏斜锁并不适合所有应用场景,撤销操作(revoke)是比较重的行为,只有当存在较多不会真正竞争的同步块时,才能体现出明显改善。实践中对于偏斜锁的一直是有争议的,有人甚至认为,当你需要大量使用并发类库时,往往意味着你不需要偏斜锁。

所以如果你确定应用程序里的锁通常情况下处于竞争状态,可以通过 JVM 参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

轻量级锁(Lightweight Locking)

轻量级锁不是用来代替重量级锁的,它的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能损耗。

轻量级锁获取

  1. 如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如下图所示:图片
  2. 拷贝对象头中的 Mark Word 复制到锁记录(Lock Record)中。
  3. 拷贝成功后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针,并将 Lock record 里的 owner 指针指向 object mark word。
  4. 如果成功,当前线程持有该对象锁,将对象头的 Mark Word 锁标志位设置为“00”,表示对象处于轻量级锁定状态,执行同步代码块。这时候线程堆栈与对象头的状态如下图所示:图片
  5. 如果更新失败,检查对象头的 Mark Word 是否指向当前线程的栈帧,如果是,说明当前线程拥有锁,直接执行同步代码块。
  6. 如果否,说明多个线程竞争锁,如果当前只有一个等待线程,通过自旋尝试获取锁。当自旋超过一定次数,或又来一个线程竞争锁,轻量级锁膨胀为重量级锁。重量级锁使除了拥有锁的线程以外的线程都阻塞,防止 CPU 空转,锁标志的状态值变为“10”,Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

轻量级锁解锁

轻量级锁解锁的时机是,当前线程同步块执行完毕。

  1. 通过 CAS 操作尝试把线程中复制的 Displaced Mark Word 对象替换当前的 Mark Word。
  2. 如果成功,整个同步过程完成
  3. 如果失败,说明存在竞争,且锁膨胀为重量级锁。释放锁的同时,会唤醒被挂起的线程。

重量级锁

轻量级锁适应的场景是线程近乎交替执行同步块的情况,如果存在同一时间访问相同锁对象时(第一个线程持有锁,第二个线程自旋超过一定次数),轻量级锁会膨胀为重量级锁,Mark Word 的锁标记位更新为 10,Mark Word 指向互斥量(重量级锁)。

重量级锁是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)。操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 JDK 1.6 之前,synchronized 重量级锁效率低的原因。

下图是偏向锁、轻量级锁、重量级锁之间转换对象头 Mark Word 数据转变:

图片

偏向锁、轻量级锁、重量级锁之间转换

网上有一个比较全的锁升级过程:

图片

锁升级过程

锁消除(Lock Elimination)

锁消除说的是虚拟机即时编译器在运行过程中,对于一些同步代码,如果检测到不可能存在共享数据竞争情况,就会删除锁。也就是说,即时编译器根据情况删除不必要的加锁操作。

锁消除的依据是逃逸分析。简单地说,逃逸分析就是分析对象的动态作用域。分三种情况:

  • 不逃逸:对象的作用域只在本线程本方法
  • 方法逃逸:对象在方法内定义后,被外部方法所引用
  • 线程逃逸:对象在方法内定义后,被外部线程所引用

即时编译器会针对对象的不同情况进行优化处理:

  • 对象栈上分配(Stack Allocations,HotSpot 不支持):直接在栈上创建对象。
  • 标量替换(Scalar Replacement):将对象拆散,直接创建被方法使用的成员变量。前提是对象不会逃逸出方法范围。
  • 同步消除(Synchronization Elimination):就是锁消除,前提是对象不会逃逸出线程。

对于锁消除来说,就是逃逸分析中,那些不会逃出线程的加锁对象,就可以直接删除同步锁。

通过代码看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public void elimination1() {
    final Object lock = new Object();
    synchronized (lock) {
        System.out.println("lock 对象没有只会作用域本线程,所以会锁消除。");
    }
}

public String elimination2() {
    final StringBuffer sb = new StringBuffer();
    sb.append("Hello, ").append("World!");
    return sb.toString();
}

public StringBuffer notElimination() {
    final StringBuffer sb = new StringBuffer();
    sb.append("Hello, ").append("World!");
    return sb;
}

elimination1()中的锁对象lock作用域只是方法内,没有逃逸出线程,elimination2()中的sb也就这样,所以这两个方法的同步锁都会被消除。但是notElimination()方法中的sb是方法返回值,可能会被其他方法修改或者其他线程修改,所以,单看这个方法,不会消除锁,还得看调用方法。

锁粗化(Lock Coarsening)

原则上,我们在编写代码的时候,要将同步块作用域的作用范围限制的尽量小。使得需要同步的操作数量尽量少,当存在锁竞争时,等待线程尽快获取锁。但是有时候,如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机检测到有一串零碎的操作都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

比如上面例子中的elimination2()方法中,StringBuffer的append是同步方法,频繁操作时,会进行锁粗化,最后结果会类似于(只是类似,不是真实情况):

1
2
3
4
5
6
7
java复制代码public String elimination2() {
    final StringBuilder sb = new StringBuilder();
    synchronized (sb) {
        sb.append("Hello, ").append("World!");
        return sb.toString();
    }
}

或者

1
2
3
4
5
arduino复制代码public synchronized String elimination3() {
    final StringBuilder sb = new StringBuilder();
    sb.append("Hello, ").append("World!");
    return sb.toString();
}

文末总结

  1. 同步操作中影响性能的有两点:
    1. 加锁解锁过程需要额外操作
    2. 用户态与内核态之间转换代价比较大
  2. synchronized 在 JDK 1.6 中有大量优化:分级锁(偏向锁、轻量级锁、重量级锁)、锁消除、锁粗化等。
  3. synchronized 复用了对象头的 Mark Word 状态位,实现不同等级的锁实现。

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

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

持续集成环境-Jenkins安装图文版

发表于 2021-11-03

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

@[toc]

前言

大家好,我是互联网老辛,今天我们来探讨jenkins的安装与使用

Jenkins的安装

1. 环境

最低配置:

  • 256M 内存,建议大于512M
  • 10G 硬盘空间

需要安装一下软件:

  • Java8
  • docker

我使用的实验机器:

cpu:2c
硬盘: 20G
内存: 4G

2. 安装java

安装java我们只需要用yum安装即可,如下图所示:

在这里插入图片描述
在这里插入图片描述

3. 下载Jenkins

jenkins.war包的下载网址:点击开始下载

下载好后:

在这里插入图片描述

4. 安装Jenkins

java -jar jenkins.war –httpPort=8080

gitlab和Jenkins尽量安装在不同服务器

5. 访问测试

浏览器输入ip:8080
在这里插入图片描述
需要查看管理员密码:
路径:网页上有提示:/root/.jenkins/secrets/initialAdminPassword

1
2
bash复制代码[root@ecs-c13b ~]# cat .jenkins/secrets/initialAdminPassword 
583802ee57b34025a5392933ba33fec0

输入密码并继续
在这里插入图片描述

6. 安装插件

Jenkins官方插件需要连接默认官网下载,速度非常慢,而且经常会失败,所以我们先跳过插件安装。 所以我们选择插件,然后选择无插件模式安装。
在这里插入图片描述
在这里插入图片描述

7. 创建管理员账号

在这里插入图片描述

这里是默认的: 在这里插入图片描述

8. 安装完成:

在这里插入图片描述

总结

本文我们主要讨论了Jenkins的安装,而且在安装的时候我们没有选择任何插件,下篇文章开始,我们将来讨论Jenkins的使用。

本文转载自: 掘金

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

捋一捋Python中的List(下) list比tuple多

发表于 2021-11-02

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

正式的Python专栏第27篇,同学站住,别错过这个从0开始的文章!

上篇学委对照tuple文章的操作一一罗列了list列表数据的相应的操作。

这次我们继续把list的其他操作看完。

list比tuple多了这些操作支持

前面学委提到tuple(元组)就是焊死了的一串串车厢,list支持元素编辑,明显灵活多了。

我们先看看删除操作,python中的list就支持了3中删除元素的操作。

假设我们定义一个列表对象 list_obj, 那么我们可以执行下面任意一个操作,进行元素删除。

1
2
3
less复制代码del list_obj[下标]
list_obj.remove(某个元素值)
list_obj.pop(下标) #返回元素值

好,我们看看下面完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
python复制代码#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/10/31 10:36 下午
# @Author : LeiXueWei
# @CSDN/Juejin/Wechat: 雷学委
# @XueWeiTag: CodingDemo
# @File : list_demo4.py
# @Project : hello

# 删除列表元素
mylist = [1, 0, 2, 4, "雷学委"]
print("mylist:", mylist)
del mylist[0]
print("after remove first element, mylist:", mylist)
mylist.remove("雷学委")
print("after remove first element, mylist:", mylist)
removed_value = mylist.pop(1) # 移除并返回的元素值
print("after remove first element, mylist:", mylist)
print("removed value:", removed_value)

效果如下:

屏幕快照 2021-11-02 下午11.56.09.png

特别需要注意的是:删除元素不能超过list的下标范围,否则报错!

除了删除,list怎么添加/扩充元素呢?

先不说修改的,list也支持定位查找元素,我们先看看。

1
bash复制代码list_obj.index(某个元素值) #通过某个元素值定位到第一个匹配的下标,从0位置开始找。

假设list_obj = [3, 2, 1] 那么list_obj.index(2) 则是什么?

答案是:1。

好,我们继续说插入新元素。

1
2
3
bash复制代码# python中的list支持下面两种方式追加元素
list_obj.insert(指定下标, 元素)
list_obj.append(元素) #末尾追元素

那么一次性追加多个,或者直接扩充某个列表到现有列表呢?

我们找到了extend函数,使用list_obj.extend(补充列表) 就能把list_obj直接扩充了,效果是依次追加补充列表的元素到末尾。

说这么多操作,我们直接复制运行下面的代码看看:

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
perl复制代码#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/10/31 10:36 下午
# @Author : LeiXueWei
# @CSDN/Juejin/Wechat: 雷学委
# @XueWeiTag: CodingDemo
# @File : list_demo5.py
# @Project : hello

# 列表的其他函数

mylist = [6, 6, 6]
print("mylist:", mylist)
mylist.append("雷学委")
print("mylist:", mylist)
print("列表多少个6?:", mylist.count(6))
print("第一个6的位置下标?:", mylist.index(6))
mylist.insert(2, 1024)
print("第一个1024的位置下标?:", mylist.index(1024))
last = mylist.pop() #删除操作前面说过了,这几举例一个。
print("最后的元素是:",last)
print("mylist:", mylist)


# 直接追加新列表
mylist.extend(mylist) # 相当于mylist = mylist * 2
print("mylist:", mylist)
mylist.extend(['持续学习', '持续开发'])
print("mylist:", mylist)

这是代码运行效果:

屏幕快照 2021-11-02 下午11.57.30.png

非常简便,我们继续看看列表元素排序

list的排序

前面说的都是编辑操作,list也可以进行数据排列,也就是按照一定逻辑进行顺序排列。

list提供了一个sort函数和reverse函数。

先说简单的,reverse函数相当于把整个串串车厢直接调头。也就是list:[1,2,3] 经过reverse函数处理后,变成了[3,2,1]。

sort则更加弹性,默认安装元素面值(比如数字,数字串),还支持传入一个lambda函数,指定排序逻辑。

以上函数默认会对一个数字组成的数组进行按数字面值大小排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
python复制代码#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/10/31 10:36 下午
# @Author : LeiXueWei
# @CSDN/Juejin/Wechat: 雷学委
# @XueWeiTag: CodingDemo
# @File : list_demo6.py
# @Project : hello

# 列表的其他函数

mylist = [2, 3, 1]
#mylist = ["2", "3", "1"]
mylist.sort()
print("mylist:", mylist)

mylist.extend(['持续学习', '持续开发'])
print("mylist:", mylist)

mylist.sort(key=lambda e: len(str(e)), reverse=True)
print("sorted mylist:", mylist)

mylist.reverse()
print("reversed mylist:", mylist)

效果如下,读者可以仔细看看是否如学委所说。

屏幕快照 2021-11-02 下午11.57.30.png

特别注意:学委上面示例代码也展示了,如果一个列表内元素不是同一类型(都是数字,都是字符串或者都是某个类型),开发者必须实现一个lambda函数给sort函数作为参考进行排序。

总结

list有很多功能,进行元素的操作(添加/删除/定位)等非常方便。

而且还能轻易扩充,排序,逆序等,这让list的使用非常广泛,每个学习python的务必多敲代码,掌握熟练。

对了,喜欢Python的朋友,请关注学委的 Python基础专栏 or Python入门到精通大专栏

持续学习持续开发,我是雷学委!

编程很有趣,关键是把技术搞透彻讲明白。

欢迎关注微信,点赞支持收藏!

本文转载自: 掘金

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

linux系列之 你知道查看文件空间的两种方法吗? 简介

发表于 2021-11-02

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

简介

linux系统中查看文件空间大小应该是一个非常常见的命令了,今天给大家介绍linux系统中查看文件空间的两种方法和在使用中可能会遇到的奇怪问题.

为什么会有两种方法呢? 因为我们可以使用du命令来看空间的占用情况,也可以使用df来查看空间的剩余情况,就像一个硬币的正反两面,怎么用着舒服怎么来.

话不多说,开始我们今天精彩的内容吧.

du命令

查看空间大小最直接的命令就是du了, 这个命令的全称就是disk usage. 表示的是磁盘空间的占用情况.

先看下du命令的基本语法:

1
2
css复制代码du [-Aclnx] [-H | -L | -P] [-g | -h | -k | -m] [-a | -s | -d depth] [-B blocksize]
[-I mask] [-t threshold] [file ...]

du命令主要用来显示文件系统的使用情况,默认情况是显示当前目录的信息,当然也可以指定具体的目录.

du的参数有很多,这里就不一一列举了,这里我们讲一下最常用的一些用法.

其中-h表示是人类可识别的读法,所以我们一般都会带上-h.

比如查看当前目录的空间使用情况可以用:

1
shell复制代码# du -ah

上面的命令显示的是目录中的所有文件.如果要将所有的文件都统计累加的话,那么可以用:

1
shell复制代码 # du -hs

如果要指定特定的目录, 直接在后面加上目录名即可.

如果你又想查看目录中具体文件的大小,又想统计总的大小,那么可以使用:

1
bash复制代码 du -ch

上面的命令会将总的大小添加在后面.

有时候我们可能发现目录占用的空间太大,但是我们又不知道具体是哪个目录,怎么办呢?

du提供了一个–max-depth=1的参数,可以指定统计目录的层级,大家可以根据需要进行调整,非常的方便.

df命令

df命令和du命令类似,但是他统计的是目录的剩余空间.

df的命令如下:

1
css复制代码df [-b | -h | -H | -k | -m | -g | -P] [-ailn] [-t] [-T type] [file | filesystem ...]

那么df和du的统计是不是一致的呢?

大多数情况下是一样的,但是在某些情况两者的统计会出现较大的误差.

大家可能会有过这样的经历,就是一个很大的日志文件,还在源源不断的写入,如果这时候把这个日志文件删除了,会发生什么情况呢?

对于du来说是统计文件大小相加,而df是统计数据块使用情况.

在上面的例子中,虽然文件删除了,但是文件句柄并没有释放,所以du的数据显示文件已经删除了,但是df显示文件还在.直到这个打开大文件的进程被Kill掉。

可以通过下面的命令来查看文件的打开情况:

1
复制代码fuser -u

总结

当然,还有最简单的ls命令,也可以简单的查看文件的大小.希望大家能够喜欢.

本文已收录于 www.flydean.com/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

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

Van♂Python 某星球的简单爬取

发表于 2021-11-02

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

0x1、引言

无独有偶,跟上节《Van ♂ Python | 某站点课程的简单爬取》一样,爬取的原因都是付费服务即将过期 (根本原因还是贫穷T﹏T)

技术方案同样是: 点点点 + 抓包,本节顺带试试想用很久的自动化神器 → Pyppeteer

严正声明:

本文仅用于记录爬虫技术研究学习,不会提供直接爬取脚本,所爬数据已删且未传播。请勿用于非法用途,如有其它非法用途造成损失,于本文无关。


0x2、Pyppeteer速成

1、与Puppeteer的渊源?

Puppeteer 是Google官方出品的通过DevTools协议口控制 Headless Chrome 的 NodeJS 库,通过其提供的API可直接控制Chrome模拟大部分用户操作,来进行UI Test、爬虫访问页面采集数据。Pyppeter 可以理解为 puppeteer 的python版本。

2、与Selenium相比?

与Selenium库相比,Pyppeteer无需繁琐的环境配置,在首次运行时会检测是否按照Chromium,未安装程序会帮我们自动安装和配置。而且Pyppeteer基于Python的新特性async实现(Python 3.5以上),故它的一些执行也支持异步操作,相比之下效率提高了不少。

3、API文档

  • 官方仓库:github.com/pyppeteer/p…
  • 官方文档:pyppeteer.github.io/pyppeteer/r…
  • 官方文档(中文-puppeteer):github.com/zhaoqize/pu…

4、Puppeteer架构图

简述 (了解就行,不用记):

  • Puppeteer:通过 DevTools协议 与浏览器进行通信;
  • Browser:可持有浏览器上下文;
  • BrowserContext:定义了一个浏览器会话,并可拥有多个页面;
  • Page:至少有一个框架,主框架;
  • Frame:至少有一个执行上下文,默认的执行上下文(框架的JavaScript)被执行;
  • Worker:具有单一执行上下文,切便于与WebWorkers进行交互;

5、Pyppeteer安装

  • Step 1:pip安装pyppeteer
1
bash复制代码pip install pyppeteer
  • Step 2:安装chromium

随手写个用到pyppeteer库的简单程序,跑一下,会自动下载,比如生成掘金首页截图的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码import asyncio
from pyppeteer import launch


async def screen_shot():
browser = await launch()
page = await browser.newPage()
await page.goto('https://juejin.cn/')
await page.screenshot({'path': 'juejin.jpg'})
await browser.close()


if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(screen_shot())

当然,谷歌源,下载起来不一定顺畅,有时可能卡住不动,可利用淘宝源,下载压缩包解压,然后在 launch() 时指定 executablePath,方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码# ① 获取原下载地址
from pyppeteer import chromium_downloader

# 根据系统版本替换:win32,win64,linux,mac
print(chromium_downloader.downloadURLs.get("win64"))

# 运行输出示例:
# https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/588429/chrome-win32.zip

# ② storage.googleapis.com替换为淘宝源npm.taobao.org/mirrors,如:
https://npm.taobao.org/mirrors/chromium-browser-snapshots/Win_x64/588429/chrome-win32.zip

# 也可以进站点自行选择:
https://npm.taobao.org/mirrors/chromium-browser-snapshots

# 3、launch时指定userDataDir
await launch({'headless': headless,'args': launch_args, 'executablePath': './userData')

附:代码流程解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码async # 声明一个异步操作
await # 声明一个耗时操作

# 创建异步池并执行screen_shot()函数
asyncio.get_event_loop().run_until_complete(screen_shot())

# 创建浏览器对象,可传入字典类型的参数
browser = await launch()

# 创建一个页面对象,页面操作在此对象上执行
page = await browser.newPage()

await page.goto('https://juejin.cn/') # 页面跳转
await page.screenshot({'path': 'juejin.jpg'}) # 截图保存
await browser.close() # 关闭浏览器对象

关于API就了解这些,更多的可自行查阅官方文档,或者善用搜索引擎,接着分析下爬取流程。


0x3、数据爬取

① 访问登录页

访问登录页:未登录 → 会显示下述登录面板,已登录 → 自动跳转至主页。

爬取流程

  • 请求登录页,判断是否有登录二维码结点,有的话休眠10s等待扫码登录;
  • 没有二维码,说明进入主页;
  • 上述两步都进入分组获取;

② 分组获取

左侧面板可以看到:创建/管理的星球 及 加入的星球:

F12看下结点:

爬取流程

  • 通过selector选择器定位到两处结点,获取所有的星球名及链接,输出供用户选择想爬取的星球;
  • 附选择器示例:div.created-group > div:nth-child(2) > a

③ 内容爬取

一开始只是想爬下 精华 分类的,后面发现有的星球可能是像这样没数据:

索性就直接爬全部,内容列表做的分页,滚动到底部再加载更多,Ajax无疑了,在不尝试破解接口规则的情况下,最简单获取数据的方式莫过于:模拟滚动 + 解析节点 的形式了。

但在这个场景,解析节点的效率太低了,标签+图文+链接的搭配样式太多了,需要写很多解析规则,而采用 拦截特定请求的方式 就更无脑和高效一些了。

接着看下这个ajax请求的特点,如组成url,等下过滤请求用到,打开Network选项卡,清空,选中XHR,网页滚动到底部,看下加载的请求:

打开看下,确定是所需数据,拦截到这样的请求,把数据保存到本地,最后再统一进行批处理。

④ 确定滚动何时停止的两种思路

一直向下滚动,需要确定数据何时爬完,停止滚动,说下笔者的两个思路:

  • 方法一:死循环 + asyncio.sleep() + pyppeteer查找底部结点

就是死循环,一直去检查底部结点是否可见,如此站点滑动到底部:

1
ini复制代码<div _ngcontent-isv-c98="" class="no-more">没有更多了</div>
  • 方法二:js定时器 + 滑动距离与高度判断

就是开启开启一个定时器,记录滚动距离与当前页面高度,比较前者>=后者时,就可能滑动到底部。

对,是可能,因为存在列表没load的情况,所以可以加入一个重试次数,当重试次数达到阈值时才算完成。

Tips:笔者使用的方法二,以为对js语法不了解,不知道怎么暂停和启动一个计时器,所以把重试阈值设置得很大,也算间接实现休眠。

⑤ 初始化浏览器

流程摸清楚了,接着开始写代码实现爬取,先初始化浏览器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
python复制代码import asyncio
import os
import time
from pyppeteer import launch

import cp_utils

# 启动配置参数
launch_args = [
"--no-sandbox", # 非沙盒模式
"--disable-infobars", # 隐藏信息栏
# 设置UA
"--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/83.0.4103.97 Safari/537.36 ",
"--log-level=3", # 日志等级
]

# 启动浏览器
async def init_browser(headless=False):
return await launch({'headless': headless,
'args': launch_args,
'userDataDir': './userData',
'dumpio': True,
'ignoreHTTPSErrors ': True})

⑥ 新建页面

接着通过browser.newPage()来新建一个浏览器页面,除了常规设置外,还添加防WebDrivder检测~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
python复制代码# 新建页面
async def init_page(browser):
page = await browser.newPage()
await page.setViewport({'width': 1960, 'height': 1080}) # 设置页面宽高
await page.setJavaScriptEnabled(True)
await prevent_web_driver_check(page)
return page


# 防WebDriver检测
async def prevent_web_driver_check(page):
if page is not None:
# 隐藏webDriver特征
await page.evaluateOnNewDocument("""() => {
Object.defineProperty(navigator, 'webdriver', { get: () => undefined })}
""")
# 某些站点会为了检测浏览器而调用js修改结果
await page.evaluate('''() =>{ window.navigator.chrome = { runtime: {}, }; }''')
await page.evaluate(
'''() =>{ Object.defineProperty(navigator, 'lang uages', { get: () => ['en-US', 'en'] }); }''')
await page.evaluate(
'''() =>{ Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5,6], }); }''')

⑦ 登陆与拉取星球列表

利用page.waitForSelector()设置超时,检测是否有登陆二维码结点,执行后续逻辑:

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
python复制代码# 登录
async def login(page, timeout=60):
await page.goto(login_url, options={'timeout': int(timeout * 1000)})
try:
await page.waitForSelector('img.qrcode', {'visible': 'visible', 'timeout': 3000})
# 等待扫码登录
print("检测到未登录,等待扫码登录...")
await asyncio.sleep(10)
await fetch_group(page)
except errors.TimeoutError:
print("检测到处于登录状态,直接拉取列表...")
await fetch_group(page)


# 提取群组
async def fetch_group(page):
global choose_group_id, choose_group_name
# 获取所有分组
group_list = []
created_groups = await page.JJ('div.created-group > div:nth-child(2) > a')
joined_groups = await page.JJ('div.joined-group > div:nth-child(2) > a')
for item in created_groups + joined_groups:
group_name = await page.evaluate('item => item.textContent', item)
group_url = await (await item.getProperty('href')).jsonValue()
group_list.append([group_name.strip(), group_url])
print("检测到如下星球列表如下:")
for index, group in enumerate(group_list):
print(index, '、', group)
choose_group_index = input("请输入待爬取群组编号 (注:下标从0开始)")
choose_group = group_list[int(choose_group_index)]
choose_group_id = choose_group[1].split('/')[-1]
choose_group_name = choose_group[0]
await fetch_data(page, choose_group[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
python复制代码# 拦截请求
async def intercept_request(req):
# 禁止获取图片、多媒体资源和发起websocket请求
if req.resourceType in ['image', 'media', 'eventsource', 'websocket']:
await req.abort()
else:
await req.continue_()


# 拦截响应
async def intercept_response(resp):
resp_type = resp.request.resourceType
if resp_type in ['xhr'] and 'https://xxx/v2/groups/{}/topics?scope=all&count=20'.format(
choose_group_id) in resp.url:
content = await resp.text()
if len(content) > 0:
temp_dir = os.path.join(content_save_dir, choose_group_name)
cp_utils.is_dir_existed(temp_dir)
content = await resp.text()
print(resp.url + ' → ' + content)
json_save_path = os.path.join(temp_dir, str(int(time.time() * 1000)) + '.json')
cp_utils.write_str_data(content, json_save_path)
print("保存文件:", json_save_path)
return resp

⑨ 无限滚动

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
python复制代码# 拉取内容
async def fetch_data(page, url, timeout=60):
# 拦截请求抓取
await page.setRequestInterception(True)
page.on('request', lambda req: asyncio.ensure_future(intercept_request(req)))
page.on('response', lambda resp: asyncio.ensure_future(intercept_response(resp)))
print("开始爬取:", choose_group_name)
await page.goto(url, options={'timeout': int(timeout * 1000)})
# 休眠3秒等待加载
await asyncio.sleep(3)
# 一直向下滚动
await page.evaluate('''async () => {
await new Promise(((resolve, reject) => {
// 每次滑动距离
const distance = 100;

// 当前高度
var totalHeight = 0;

// 最大重试次数和当前重试次数
var maxTries = 20000;
var curTries = 0;

var timer = setInterval(() => {
var scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance)
totalHeight += distance
console.log(totalHeight + "-" + scrollHeight)
if (totalHeight >= scrollHeight) {
if(curTries > maxTries) {
clearInterval(timer)
resolve();
} else {
curTries += 1;
totalHeight -= distance
}
} else {
curTries = 0;
}
}, 100)
}));
}''')
print("星球【{}】数据爬取完毕...".format(choose_group_name))
# 取消拦截
await page.setRequestInterception(False)

最后调用下:

1
2
3
4
python复制代码if __name__ == '__main__':
cur_browser = asyncio.get_event_loop().run_until_complete(init_browser())
cur_page = asyncio.get_event_loop().run_until_complete(init_page(cur_browser))
asyncio.get_event_loop().run_until_complete(login(cur_page))

运行后可以看到控制台输出对应的爬取信息:

也可以看到爬取到本地的json文件:

呦西,数据都保存到本地了,接着到数据处理环节~


0x4、数据处理

① 关键数据提取

随手打开几个爬取的json样本,很好看出json中的关键部分:

定义出提取实体类:

1
2
3
4
5
6
python复制代码class Talk:
def __init__(self, name=None, text=None, images=None, files=None):
self.name = name
self.text = text
self.images = images
self.files = files

接着就是遍历文件,json.loads转成dict,按需拿字段,非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
python复制代码import cp_utils
import json
import os

zsxq_save_dir = os.path.join(os.getcwd(), "zsxq")
result_json_path = os.path.join(os.getcwd(), "zsxq_result.json")
talk_list = []
talk_dict = {'data': None}


# 数据实体
class Talk:
def __init__(self, name=None, text=None, images=None, files=None):
self.name = name
self.text = text
self.images = images
self.files = files

def to_json_str(self):
return json.dumps({'name': self.name, 'text': self.text, 'images': self.images, 'files': self.files},
ensure_ascii=False)

def to_dict(self):
return {'name': self.name, 'text': self.text, 'images': self.images, 'files': self.files}


# 提取json文件内容
def extract_json_file(file_path):
global talk_list
content = cp_utils.read_content_from_file(file_path)
content_dict = json.loads(content)
topics = content_dict['resp_data'].get('topics')
print("解析文件:{}".format(file_path))
if topics is not None and len(topics) > 0:
for topic in topics:
talk_entity = Talk()
talk = topic.get('talk')
if talk is not None:
# 依次获取名称、文本、图片、文件
owner = talk.get('owner')
if owner is not None:
owner_name = owner.get("name")
if owner is not None:
talk_entity.name = owner_name
text = talk.get('text')
if text is not None:
talk_entity.text = text
images = talk.get('images')
if images is not None and len(images) > 0:
image_urls = []
for image in images:
original = image.get('original')
if original is not None:
image_urls.append(original.get('url'))
talk_entity.images = image_urls
files = talk.get('files')
if files is not None and len(files) > 0:
file_list = []
for file in files:
file_id = file.get('file_id')
file_name = file.get('name')
file_list.append({file_id: file_name})
talk_entity.files = file_list
talk_list.append(talk_entity.to_dict())
else:
print("数据为空,跳过文件...")


if __name__ == '__main__':
dir_list = cp_utils.fetch_all_file(zsxq_save_dir)
print("可操作目录:\n")
for index, path in enumerate(dir_list):
print("{}、{}".format(index, path))
choose_index = input("\n请输入要处理的目录序号 => ")
choose_path = dir_list[int(choose_index)]
print("当前选中目录:{}".format(choose_path))
json_file_list = cp_utils.filter_file_type(choose_path, '.json')
for json_file in json_file_list[:10]:
extract_json_file(json_file)
talk_dict['data'] = talk_list
talk_json = json.dumps(talk_dict, ensure_ascii=False, indent=2)
cp_utils.write_str_data(talk_json, result_json_path)
print("文件写入完毕:{}".format(result_json_path))

遍历10个文件试试效果:

打开json文件看看:

② 将json转成Markdown

json肯定不是和便于阅读的,可以生成一波Markdown,拼接字符串而已,这里的主要难点是:

text的解析,有标签、普通文本、外部链接、表情…

可以通过 re.sub() + 反向引用 替换一波标签和外部链接,写个测试代码试试水:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码    # 替换正则
hash_tag_pattern = re.compile(r'(<e type="hashtag" .*? title=")(.*?)(".*?/>)')
web_pattern = re.compile(r'(<e type="web" href=")(.*?)(" title=")(.*?)(" cache=.*?/>)')

# 测试用例
xml_str = """
<e type="hashtag" hid="51288155841824" title="%23%E6%8A%80%E5%B7%A7%23"/>
<e type="hashtag" hid="28518452544211" title="%23%E6%95%88%E7%8E%87%E5%B7%A5%E5%85%B7%23"/> 今天推荐一个命令行辅助工具:fig,一图胜千言,直接看图

打开官网即可安装:
<e type="web" href="https%3A%2F%2Ffig.io%2Fwelcome" title="Fig+%7C+Welcome+to+Fig" cache=""/>
"""
temp_result = unquote(hash_tag_pattern.sub(r"\g<2>", xml_str), 'utf-8')
temp_result = unquote(web_pattern.sub(r"[\g<4>](\g<2>)", temp_result), 'utf-8')
temp_result = temp_result.strip().replace("\n", "")
print(temp_result)

看下解析结果:

Good,接着补全图片及文件相关:

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
python复制代码# 转换成md文件
def json_to_md(file_path):
content = cp_utils.read_content_from_file(file_path)
data_list = json.loads(content)['data']
md_content = ''
for data in data_list:
name = data['name']
if name is not None:
md_content += name + "\n"
text = data['text']
if text is not None:
temp_result = unquote(hash_tag_pattern.sub(r"\g<2>", text), 'utf-8').replace("#", "`")
temp_result = unquote(web_pattern.sub(r"[\g<4>](\g<2>)", temp_result), 'utf-8')
md_content += temp_result.strip()
images = data['images']
if images is not None:
md_content += '\n'
for image_url in images:
img_file_name = str(int(time.time() * 1000)) + ".jpg"
img_save_path = os.path.join(image_save_dir, str(int(time.time() * 1000)) + ".jpg")
cp_utils.download_pic(img_save_path, image_url)
relative_path = 'images/{}'.format(img_file_name)
md_content += '![]({})'.format(relative_path)
files = data['files']
if files is not None:
md_content += '\n文件:'
for file in files:
file_id = file.get('file_id')
file_name = file.get('name')
md_content += "《{}》".format(file_name)
md_content += '\n\n---\n\n'
cp_utils.write_str_data(md_content, result_md_path)

细心的你可能发现了,代码中把图片给download下来了,并没有采用远程图片的方式,原因是站点图片资源url,没有图片后缀名,Markdown语法识别不了,导致预览时图片显示不出来。生成后的md文件:

46257个字符,PyCharm打开预览直接卡死,MarkdwonPad2也难逃一劫,滚一下卡一下,还是有必要分成几个md文件存~


0x5、小结

本文借着爬某星球的契机,把Pyppeteer的用法过了一波,爬虫技巧 Level Up↑,另外,文件类这里只存了一个id,真实下载地址还得调用另外的接口获取,而且还得处于登录态,感兴趣的同学可自行尝试一波。

好的,就说这么多,有问题欢迎评论区指出,感谢~


参考文献:

  • Pyppeteer入门及中文教程
  • Pyppetter - 你的自动化利器!
  • 爬虫系列之Pyppeteer:比selenium更高效的爬虫界的新神器
  • (最新版)如何正确移除 Pyppeteer 中的window.navigator.webdriver
  • puppeteer自动化测试系列之三—端对端测试中常用的 Puppeteer 操作

本文转载自: 掘金

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

谈谈java中的unsafe类 前言 主要方法 Unsafe

发表于 2021-11-02

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

前言

看过JUC并发包里面的源码,就一定明白Unsafe类是整个java并发包底层实现的核心。Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,Unsafe类提供了硬件级别的原子操作,Unsafe里面的方法都是 native方法,通过使用JNI的方式来访问本地C++实现库。下面就继续深入的看一下Unsafe类。

主要方法

Unsafe有很多public native修饰的方法,还有几十个基于public native方法的其他方法。但是大体可分为以下几类:

(1)初始化操作

(2)操作对象属性

(3)操作数组元素

(4)线程挂起和回复

(5)CAS机制

下面就对Unsafe源码进一步分析。

操作属性方法

1
2
3
vbnet复制代码//通过给定的Java变量获取引用值。这里实际上是获取一个Java对象o中,获取偏移地址为offset的属性的值,此方法可以突破修饰符的抑制,也就是无视private、protected和default修饰符。类似的方法有getInt、getDouble等等。同理还有putObject方法。

public native Object getObject(Object o, long offset);
1
2
3
java复制代码//强制从主存中获取属性值。类似的方法有getIntVolatile、getDoubleVolatile等等。同理还有putObjectVolatile。

public native Object getObjectVolatile(Object o, long offset);
1
2
3
java复制代码//设置o对象中offset偏移地址offset对应的Object型field的值为指定值x。这是一个有序或者有延迟的putObjectVolatile方法,并且不保证值的改变被其他线程立即看到。只有在field被volatile修饰并且期望被修改的时候使用才会生效。类似的方法有putOrderedInt和putOrderedLong。

public native void putOrderedObject(Object o, long offset, Object x);
1
2
3
4
5
6
7
8
java复制代码//返回给定的静态属性在它的类的存储分配中的位置(偏移地址)。
public native long staticFieldOffset(Field f);

//返回给定的非静态属性在它的类的存储分配中的位置(偏移地址)。
public native long objectFieldOffset(Field f);

//返回给定的静态属性的位置,配合staticFieldOffset方法使用。
public native Object staticFieldBase(Field f);

操作数组

1
2
3
4
5
java复制代码//返回数组类型的第一个元素的偏移地址(基础偏移地址)
public native int arrayBaseOffset(Class arrayClass);

//返回数组中元素与元素之间的偏移地址的增量。这两个方法配合使用就可以定位到任何一个元素的地址。
public native int arrayIndexScale(Class arrayClass);

内存管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码//获取本地指针的大小(单位是byte),通常值为4或者8。常量ADDRESS_SIZE就是调用此方法。
public native int addressSize();

//获取本地内存的页数,此值为2的幂次方。
public native int pageSize();

//分配一块新的本地内存,通过bytes指定内存块的大小(单位是byte),返回新开辟的内存的地址
public native long allocateMemory(long bytes);

//通过指定的内存地址address重新调整本地内存块的大小,调整后的内存块大小通过bytes指定(单位为byte)。
public native long reallocateMemory(long address, long bytes);

//将给定内存块中的所有字节设置为固定值(通常是0)。
public native void setMemory(Object o, long offset, long bytes, byte value);

内存屏障

1
2
3
4
5
6
7
8
csharp复制代码//在该方法之前的所有读操作,一定在load屏障之前执行完成。
public native void loadFence();

//在该方法之前的所有写操作,一定在store屏障之前执行完成
public native void storeFence();

//在该方法之前的所有读写操作,一定在full屏障之前执行完成,这个内存屏障相当于上面两个(load屏障和store屏障)的合体功能。
public native void fullFence();

线程挂起和恢复

1
2
3
4
5
java复制代码//释放被park创建的在一个线程上的阻塞。由于其不安全性,因此必须保证线程是存活的
public native void unpark(Object thread);

//阻塞当前线程,一直等道unpark方法被调用
public native void park(boolean isAbsolute, long time);`

CAS机制

Unsafe类中给出了大量比较并设置和比较并交换方法。这些方法都指向某个native的方法作为底层实现。

1
2
3
4
5
java复制代码public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作

Unsafe的使用

获取Unsafe实例

Unsafe 提供了getUnsafe方法,如下所示:

1
2
3
4
5
typescript复制代码public class Main {
public static void main(String[] args) {
Unsafe.getUnsafe();
}
}

直接这样用会报异常:

1
2
3
php复制代码Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at com.bendcap.java.jvm.unsafe.Main.main(Main.java:13)

因为Unsafe类主要是JDK内部使用,并不提供给普通用户调用。但是仍让可以通过反射获取到实例:

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码public static Unsafe testUnsafe() {
try {
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
return unsafe;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

改变私有字段

假设有如下类:

1
2
3
4
5
6
7
arduino复制代码public class UnsafeDemo {
private int juejin = 0;

public boolean juejinDisclosed() {
return juejin == 1;
}
}

下面通过Unsafe来改变私有属性juejin的值。

1
2
3
4
5
6
7
8
9
10
ini复制代码UnsafeDemo ud = new UnsafeDemo();
Field field = null;
try {
field = ud.getClass().getDeclaredField("juejin");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
Unsafe unsafe = testUnsafe();
unsafe.putInt(ud, unsafe.objectFieldOffset(field), 1);
return ud.juejinDisclosed();

通过unsafe.putInt直接改变了ud的私有属性的值。一旦通过反射获得了类的私有属性字段,就可以直接操作它的值。

总结

虽然Unsafe看起来不会被用到,但是能帮助我们更好的理解JUC的并发原理,因此还是很有必要学习的。

本文转载自: 掘金

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

1…431432433…956

开发者博客

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