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

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


  • 首页

  • 归档

  • 搜索

LUA源码分析2---字符串 1 LUA字符串定义 2

发表于 2021-03-02
  1. LUA字符串定义

本文主要源码分析基于Lua5.3.1.

之前已经做过Lua内部类型的分析。

LUA源码分析1—lua的内部的类型分析

在上文中,已经分析了LUA的数据类型抽象。
本文就来分析下Lua语言是如何利用这些抽象的数据类型来保存字符串类型。

lua中关于字符串的操作都放到了如下两个文件中:

  • lstring.h
  • lstring.c

lua中对于字符串类型的定义放在lobject.h中。

1.1 TString定义

先看下TString定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
C复制代码
/*
** Header for string value; string bytes follow the end of this structure
** (aligned according to 'UTString'; see next).
*/
typedef struct TString {
CommonHeader; //代表这是一个可以垃圾回收的对象
lu_byte extra; /* reserved words for short strings; "has hash" for longs */
lu_byte shrlen; /* length for short strings */
unsigned int hash;
union {
size_t lnglen; /* length for long strings */
struct TString *hnext; /* linked list for hash table */
} u;
} TString;

从注释可以看出TString可以表示两种字符串:

  • short string
  • long string

1.2 创建TString对象

上篇文章中提到,对于GCObject都会调用一个工厂函数:luaC_newobj (lua_State *L, int tt, size_t sz)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
C复制代码/*
** creates a new string object
*/
static TString *createstrobj (lua_State *L, const char *str, size_t l,
int tag, unsigned int h) {
TString *ts;
GCObject *o;
size_t totalsize; /* total size of TString object */
totalsize = sizelstring(l);
o = luaC_newobj(L, tag, totalsize);
ts = gco2ts(o);
ts->hash = h;
ts->extra = 0;
memcpy(getaddrstr(ts), str, l * sizeof(char));
getaddrstr(ts)[l] = '\0'; /* ending 0 */
return ts;
}

每次存放LUA字符串的变量,实际上存放的并不是一个真正的字符串数据, 而是一个字符串的引用。比如如下代码:

1
2
lua复制代码a = "str1"
b = "str1"

其实a和b引用的是同一份数据。

在lua内部有一个大的hash表,里面存放了所有了字符串。
如下图所示:

这个结构在LUA虚拟机中叫做stringtable。

定义如下:

1
2
3
4
5
C复制代码typedef struct stringtable {
TString **hash;
int nuse; /* number of elements */
int size;
} stringtable;

这个结构保存在global_State中。
下面看看LUA虚拟机已启动的时候,是怎么初始化的。

  1. 虚拟机启动字符串表的初始化

在LUA程序开始执行一个LUA脚本的时候,其实都会调用一个函数f_luaopen.在这个函数中会调用luaS_init(L);这个函数,这个函数其实就是对上文提到的hashtable进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
C复制代码/*
** Initialize the string table and the string cache
*/
void luaS_init (lua_State *L) {
global_State *g = G(L);
int i;
luaS_resize(L, MINSTRTABSIZE); /* initial size of string table */
/* pre-create memory-error message */
g->memerrmsg = luaS_newliteral(L, MEMERRMSG);
luaC_fix(L, obj2gco(g->memerrmsg)); /* it should never be collected */
for (i = 0; i < STRCACHE_SIZE; i++) /* fill cache with valid strings */
g->strcache[i][0] = g->memerrmsg;
}

看到一上来,就调用了一个叫做luaS_resize的函数,从这个函数的名字就能看出来,这个函数两种场景会调用:

  • 场景1:初始调用(我们现在代码看到的)
  • 场景2:当hash桶太小,而元素太多,导致数据寻找渐渐的退化为链表搜索的时候;(这个调用时机我们待会看)

场景1:这个函数其实就是按照MINSTRTABSIZE的大小来初始化hash桶。

下面来看看这个函数怎么实现的:

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
C复制代码/*
** resizes the string table
*/
void luaS_resize (lua_State *L, int newsize) {
int i;
stringtable *tb = &G(L)->strt;
if (newsize > tb->size) { /* grow table if needed */
luaM_reallocvector(L, tb->hash, tb->size, newsize, TString *); //这里还涉及到垃圾回收的内容,我们本次分析不考虑,暂时认为就是普通的realloc
for (i = tb->size; i < newsize; i++) //把后面申请那一块hash桶都初始化
tb->hash[i] = NULL;
}
for (i = 0; i < tb->size; i++) { /* rehash,把前面原有的hash桶进行重新分配 */
TString *p = tb->hash[i];
tb->hash[i] = NULL;
while (p) { /* for each node in the list */
TString *hnext = p->u.hnext; /* save next */
unsigned int h = lmod(p->hash, newsize); /* new position */
p->u.hnext = tb->hash[h]; /* chain it */
tb->hash[h] = p;
p = hnext;
}
}
if (newsize < tb->size) { /* shrink table if needed ,注释很清晰,如果是newsize小于原来大小,证明这是一个缩容操作*/
/* vanishing slice should be empty */
lua_assert(tb->hash[newsize] == NULL && tb->hash[tb->size - 1] == NULL);
luaM_reallocvector(L, tb->hash, tb->size, newsize, TString *);
}
tb->size = newsize;
}
  1. 创建一个字符串

3.1 Lua中字符串的定义

在lua虚拟机里,字符串类型对象的数据结构做了封装和抽象,一般结构UTString是这样的:

1
2
3
4
5
6
7
C复制代码/*
** Ensures that address after this type is always fully aligned.
*/
typedef union UTString {
L_Umaxalign dummy; /* ensures maximum alignment for strings */
TString tsv;
} UTString;

而一个TString的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
C复制代码/*
** Header for string value; string bytes follow the end of this structure
** (aligned according to 'UTString'; see next).
*/
typedef struct TString {
CommonHeader;
lu_byte extra; /* reserved words for short strings; "has hash" for longs */
lu_byte shrlen; /* length for short strings */
unsigned int hash;
union {
size_t lnglen; /* length for long strings */
struct TString *hnext; /* linked list for hash table */
} u;
} TString;

字符的内容都保存在TString的后面。最终使用\0结尾。

大致结构如下:

3.2 Lua中字符串的创建

在lua虚拟机里面创建一个字符串调用的函数是luaS_newlstr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
C复制代码/*
** new string (with explicit length)
*/
TString *luaS_newlstr (lua_State *L, const char *str, size_t l) {
if (l <= LUAI_MAXSHORTLEN) /* short string? */
return internshrstr(L, str, l);
else {
TString *ts;
if (unlikely(l >= (MAX_SIZE - sizeof(TString))/sizeof(char)))
luaM_toobig(L);
ts = luaS_createlngstrobj(L, l);
memcpy(getstr(ts), str, l * sizeof(char));
return ts;
}
}

看出字符串分为两种,小于等于LUAI_MAXSHORTLEN称为短字符串,否则称为长字符串。

对于短字符串来说,调用的是函数internshrstr.看下实现:

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
C复制代码/*
** checks whether short string exists and reuses it or creates a new one
*/
static TString *internshrstr (lua_State *L, const char *str, size_t l) {
TString *ts;
global_State *g = G(L);
unsigned int h = luaS_hash(str, l, g->seed); //先计算下当前这个str的hash值
TString **list = &g->strt.hash[lmod(h, g->strt.size)]; //找到对应的hash桶的入口
for (ts = *list; ts != NULL; ts = ts->u.hnext) { //链表查找
if (l == ts->shrlen &&
(memcmp(str, getstr(ts), l * sizeof(char)) == 0)) {
/* found! */
if (isdead(g, ts)) /* dead (but not collected yet)? */
changewhite(ts); /* resurrect it */
return ts;
}
}
//发现链表已经过长,触发resize操作
if (g->strt.nuse >= g->strt.size && g->strt.size <= MAX_INT/2) {
luaS_resize(L, g->strt.size * 2);
list = &g->strt.hash[lmod(h, g->strt.size)]; /* recompute with new size */
}
ts = createstrobj(L, str, l, LUA_TSHRSTR, h);
ts->shrlen = cast_byte(l);
ts->u.hnext = *list; //新创建的ts的尾指针指向hash桶口
*list = ts; //将
g->strt.nuse++;
return ts;
}

如果当前hash桶中没有找到这个字符串,就需要创建一个新的。

如果是长字符串,LUA的处理逻辑其实是和短字符串处理差不多的,只是两个Obj的tag不同而已。

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
C复制代码#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <string.h>


int main(void)
{
char buff[256] = {0};
int error;
lua_State *L = luaL_newstate();
luaopen_base(L);
luaopen_table(L);
luaopen_io(L);
luaopen_string(L);
luaopen_math(L);

const char *luaStr = "print(\"Hello World\")";

error = luaL_loadbuffer(L, luaStr, (size_t)strlen(luaStr), NULL) || lua_pcall(L,0,0,0);
if(error)
{
fprintf(stderr, "%s", lua_tostring(L, -1));
lua_pop(L, 1);
}


// while(fgets(buff, sizeof(buff), stdin) != NULL)
// {
// error = luaL_loadbuffer(L, buff, (size_t)strlen(buff), NULL) || lua_pcall(L,0,0,0);
// if(error)
// {
// fprintf(stderr, "%s", lua_tostring(L, -1));
// lua_pop(L, 1);
// }
// }
lua_close(L);
return 0;

}

本文转载自: 掘金

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

还在用 open 读文件?out 了,这个库比 open 好

发表于 2021-03-02

使用 open 函数去读取文件,似乎是所有 Python 工程师的共识。

今天明哥要给大家推荐一个比 open 更好用、更优雅的读取文件方法 – 使用 fileinput

fileinput 是 Python 的内置模块,但我相信,不少人对它都是陌生的。今天我把 fileinput 的所有的用法、功能进行详细的讲解,并列举了一些非常实用的案例,对于理解和使用它可以说完全没有问题。

  1. 从标准输入中读取

当你的 Python 脚本没有传入任何参数时,fileinput 默认会以 stdin 作为输入源

1
2
3
4
5
python复制代码# demo.py
import fileinput

for line in fileinput.input():
print(line)

效果如下,不管你输入什么,程序会自动读取并再打印一次,像个复读机似的。

1
2
3
4
5
6
shell复制代码$ python demo.py 
hello
hello

python
python
  1. 单独打开一个文件

脚本的内容如下

1
2
3
4
5
python复制代码import fileinput

with fileinput.input(files=('a.txt',)) as file:
for line in file:
print(f'{fileinput.filename()} 第{fileinput.lineno()}行: {line}', end='')

其中 a.txt 的内容如下

1
2
复制代码hello
world

执行后就会输出如下

1
2
3
shell复制代码$ python demo.py
a.txt 第1行: hello
a.txt 第2行: world

需要说明的一点是,fileinput.input() 默认使用 mode='r' 的模式读取文件,如果你的文件是二进制的,可以使用mode='rb' 模式。fileinput 有且仅有这两种读取模式。

  1. 批量打开多个文件

从上面的例子也可以看到,我在 fileinput.input 函数中传入了 files 参数,它接收一个包含多个文件名的列表或元组,传入一个就是读取一个文件,传入多件就是读取多个文件。

1
2
3
4
5
python复制代码import fileinput

with fileinput.input(files=('a.txt', 'b.txt')) as file:
for line in file:
print(f'{fileinput.filename()} 第{fileinput.lineno()}行: {line}', end='')

a.txt 和 b.txt 的内容分别是

1
2
3
4
5
6
shell复制代码$ cat a.txt
hello
world
$ cat b.txt
hello
python

运行后输出结果如下,由于 a.txt 和 b.txt 的内容被整合成一个文件对象 file ,因此 fileinput.lineno() 只有在读取一个文件时,才是原文件中真实的行号。

1
2
3
4
5
shell复制代码$ python demo.py
a.txt 第1行: hello
a.txt 第2行: world
b.txt 第3行: hello
b.txt 第4行: python

如果想要在读取多个文件的时候,也能读取原文件的真实行号,可以使用 fileinput.filelineno() 方法

1
2
3
4
5
python复制代码import fileinput

with fileinput.input(files=('a.txt', 'b.txt')) as file:
for line in file:
print(f'{fileinput.filename()} 第{fileinput.filelineno()}行: {line}', end='')

运行后,输出如下

1
2
3
4
5
shell复制代码$ python demo.py
a.txt 第1行: hello
a.txt 第2行: world
b.txt 第1行: hello
b.txt 第2行: python

这个用法和 glob 模块简直是绝配

1
2
3
4
5
6
7
python复制代码import fileinput
import glob

for line in fileinput.input(glob.glob("*.txt")):
if fileinput.isfirstline():
print('-'*20, f'Reading {fileinput.filename()}...', '-'*20)
print(str(fileinput.lineno()) + ': ' + line.upper(), end="")

运行效果如下

1
2
3
4
5
6
7
python复制代码$ python demo.py
-------------------- Reading b.txt... --------------------
1: HELLO
2: PYTHON
-------------------- Reading a.txt... --------------------
3: HELLO
4: WORLD
  1. 读取的同时备份文件

fileinput.input 有一个 backup 参数,你可以指定备份的后缀名,比如 .bak

1
2
3
4
5
6
python复制代码import fileinput


with fileinput.input(files=("a.txt",), backup=".bak") as file:
for line in file:
print(f'{fileinput.filename()} 第{fileinput.lineno()}行: {line}', end='')

运行的结果如下,会多出一个 a.txt.bak 文件

1
2
3
4
5
6
7
8
9
10
shell复制代码$ ls -l a.txt*
-rw-r--r-- 1 MING staff 12 2 27 10:43 a.txt

$ python demo.py
a.txt 第1行: hello
a.txt 第2行: world

$ ls -l a.txt*
-rw-r--r-- 1 MING staff 12 2 27 10:43 a.txt
-rw-r--r-- 1 MING staff 42 2 27 10:39 a.txt.bak
  1. 标准输出重定向替换

fileinput.input 有一个 inplace 参数,表示是否将标准输出的结果写回文件,默认不取代

请看如下一段测试代码

1
2
3
4
5
6
7
python复制代码import fileinput

with fileinput.input(files=("a.txt",), inplace=True) as file:
print("[INFO] task is started...")
for line in file:
print(f'{fileinput.filename()} 第{fileinput.lineno()}行: {line}', end='')
print("[INFO] task is closed...")

运行后,会发现在 for 循环体内的 print 内容会写回到原文件中了。而在 for 循环体外的 print 则没有变化。

1
2
3
4
5
6
7
8
9
10
11
shell复制代码$ cat a.txt
hello
world

$ python demo.py
[INFO] task is started...
[INFO] task is closed...

$ cat a.txt
a.txt 第1行: hello
a.txt 第2行: world

利用这个机制,可以很容易的实现文本替换。

1
2
3
4
5
6
7
8
python复制代码import sys
import fileinput

for line in fileinput.input(files=('a.txt', ), inplace=True):
#将Windows/DOS格式下的文本文件转为Linux的文件
if line[-2:] == "\r\n":
line = line + "\n"
sys.stdout.write(line)

附:如何实现 DOS 和 UNIX 格式互换以供程序测试,使用 vim 输入如下指令即可

1
2
ini复制代码DOS转UNIX::setfileformat=unix
UNIX转DOS::setfileformat=dos
  1. 不得不介绍的方法

如果只是想要 fileinput 当做是替代 open 读取文件的工具,那么以上的内容足以满足你的要求。

  • fileinput.filenam()
    返回当前被读取的文件名。 在第一行被读取之前,返回 None。
  • fileinput.fileno()
    返回以整数表示的当前文件“文件描述符”。 当未打开文件时(处在第一行和文件之间),返回 -1。
  • fileinput.lineno()
    返回已被读取的累计行号。 在第一行被读取之前,返回 0。 在最后一个文件的最后一行被读取之后,返回该行的行号。
  • fileinput.filelineno()
    返回当前文件中的行号。 在第一行被读取之前,返回 0。 在最后一个文件的最后一行被读取之后,返回此文件中该行的行号。

但若要想基于 fileinput 来做一些更加复杂的逻辑,也许你会需要用到如下这几个方法

  • fileinput.isfirstline()
    如果刚读取的行是其所在文件的第一行则返回 True,否则返回 False。
  • fileinput.isstdin()
    如果最后读取的行来自 sys.stdin 则返回 True,否则返回 False。
  • fileinput.nextfile()
    关闭当前文件以使下次迭代将从下一个文件(如果存在)读取第一行;不是从该文件读取的行将不会被计入累计行数。 直到下一个文件的第一行被读取之后文件名才会改变。 在第一行被读取之前,此函数将不会生效;它不能被用来跳过第一个文件。 在最后一个文件的最后一行被读取之后,此函数将不再生效。
  • fileinput.close()
    关闭序列。
  1. 进阶一点的玩法

在 fileinput.input() 中有一个 openhook 的参数,它支持用户传入自定义的对象读取方法。

若你没有传入任何的勾子,fileinput 默认使用的是 open 函数。

fileinput 为我们内置了两种勾子供你使用

  1. fileinput.hook_compressed(*filename*, *mode*)

使用 gzip 和 bz2 模块透明地打开 gzip 和 bzip2 压缩的文件(通过扩展名 '.gz' 和 '.bz2' 来识别)。 如果文件扩展名不是 '.gz' 或 '.bz2',文件会以正常方式打开(即使用 open() 并且不带任何解压操作)。使用示例: fi = fileinput.FileInput(openhook=fileinput.hook_compressed)
2. fileinput.hook_encoded(*encoding*, *errors=None*)

返回一个通过 open() 打开每个文件的钩子,使用给定的 encoding 和 errors 来读取文件。使用示例: fi = fileinput.FileInput(openhook=fileinput.hook_encoded("utf-8", "surrogateescape"))

如果你自己的场景比较特殊,以上的三种勾子都不能满足你的要求,你也可以自定义。

这边我举个例子来抛砖引玉下

假如我想要使用 fileinput 来读取网络上的文件,可以这样定义勾子。

  1. 先使用 requests 下载文件到本地
  2. 再使用 open 去读取它
1
2
3
4
5
6
7
8
python复制代码def online_open(url, mode):
import requests
r = requests.get(url)
filename = url.split("/")[-1]
with open(filename,'w') as f1:
f1.write(r.content.decode("utf-8"))
f2 = open(filename,'r')
return f2

直接将这个函数传给 openhoos 即可

1
2
3
4
5
6
python复制代码import fileinput

file_url = 'https://www.csdn.net/robots.txt'
with fileinput.input(files=(file_url,), openhook=online_open) as file:
for line in file:
print(line, end="")

运行后按预期一样将 CSDN 的 robots 的文件打印了出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
makefile复制代码User-agent: * 
Disallow: /scripts
Disallow: /public
Disallow: /css/
Disallow: /images/
Disallow: /content/
Disallow: /ui/
Disallow: /js/
Disallow: /scripts/
Disallow: /article_preview.html*
Disallow: /tag/
Disallow: /*?*
Disallow: /link/

Sitemap: https://www.csdn.net/sitemap-aggpage-index.xml
Sitemap: https://www.csdn.net/article/sitemap.txt
  1. 列举一些实用案例

案例一:读取一个文件所有行

1
2
3
python复制代码import fileinput
for line in fileinput.input('data.txt'):
print(line, end="")

案例二:读取多个文件所有行

1
2
3
4
5
6
7
python复制代码import fileinput
import glob

for line in fileinput.input(glob.glob("*.txt")):
if fileinput.isfirstline():
print('-'*20, f'Reading {fileinput.filename()}...', '-'*20)
print(str(fileinput.lineno()) + ': ' + line.upper(), end="")

案例三:利用fileinput将CRLF文件转为LF

1
2
3
4
5
6
7
8
python复制代码import sys
import fileinput

for line in fileinput.input(files=('a.txt', ), inplace=True):
#将Windows/DOS格式下的文本文件转为Linux的文件
if line[-2:] == "\r\n":
line = line + "\n"
sys.stdout.write(line)

案例四:配合 re 做日志分析:取所有含日期的行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码
#--样本文件--:error.log
aaa
1970-01-01 13:45:30 Error: **** Due to System Disk spacke not enough...
bbb
1970-01-02 10:20:30 Error: **** Due to System Out of Memory...
ccc

#---测试脚本---
import re
import fileinput
import sys

pattern = '\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'

for line in fileinput.input('error.log',backup='.bak',inplace=1):
if re.search(pattern,line):
sys.stdout.write("=> ")
sys.stdout.write(line)

#---测试结果---
=> 1970-01-01 13:45:30 Error: **** Due to System Disk spacke not enough...
=> 1970-01-02 10:20:30 Error: **** Due to System Out of Memory...

案例五:利用fileinput实现类似于grep的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码import sys
import re
import fileinput

pattern= re.compile(sys.argv[1])
for line in fileinput.input(sys.argv[2]):
if pattern.match(line):
print(fileinput.filename(), fileinput.filelineno(), line)
$ ./test.py import.*re *.py
#查找所有py文件中,含import re字样的
addressBook.py 2 import re
addressBook1.py 10 import re
addressBook2.py 18 import re
test.py 238 import re
  1. 写在最后

fileinput 是对 open 函数的再次封装,在仅需读取数据的场景中, fileinput 显然比 open 做得更专业、更人性,当然在其他有写操作的复杂场景中,fileinput 就无能为力啦,本身从 fileinput 的命名上就知道这个模块只专注于输入(读)而不是输出(写)。


文章最后给大家介绍两个我自己写的在线文档:

第一个文档:PyCharm 中文指南 1.0 文档

整理了 100 个 PyCharm 的使用技巧,为了让新手能够直接上手,我花了很多的时间录制了上百张 GIF 动图,有兴趣的前往在线文档阅读。

第二个文档:PyCharm 黑魔法指南 1.0 文档

系统收录各种 Python 冷门知识,Python Shell 的多样玩法,令人疯狂的 Python 炫技操作,Python 的超详细进阶知识解读,非常实用的 Python 开发技巧等。

本文转载自: 掘金

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

关于 Synchronized 的一个点,网上99%的文章都

发表于 2021-03-02

本来是在写面霸系列的,写着写着就写到了这一题:

Synchronized 原理知道不?

而关于 Synchronized 我去年还专门翻阅 JVM HotSpot 1.8 的源码来研究了一波,那时候我就发现有一个点,一个几乎网上所有文章包括《Java并发编程的艺术》也是这样说的一个点。

锁升级想必网上有太多文章说过了,这里提到当轻量级锁 CAS 失败,则当前线程会尝试使用自旋来获取锁。

其实起初我也是这样认为的,毕竟都是这样说的,而且也很有道理。

因为重量级锁会阻塞线程,所以如果加锁的代码执行的非常快,那么稍微自旋一会儿其他线程就不需要锁了,就可以直接 CAS 成功了,因此不用阻塞了线程然后再唤醒。

但是我看了源码之后发现并不是这样的,这段代码在 synchronizer.cpp 中。

所以 CAS 失败了之后,并没有什么自旋操作,如果 CAS 成功就直接 return 了,如果失败会执行下面的锁膨胀方法。

我去锁膨胀的代码ObjectSynchronizer::inflate翻了翻,也没看到自旋操作。

所以从源码来看轻量级锁 CAS 失败并不会自旋而是直接膨胀成重量级锁。

不过为了优化性能,自旋操作在 Synchronized 中确实却有。

那是在已经升级成重量级锁之后,线程如果没有争抢到锁,会进行一段自旋等待锁的释放。

咱们还是看源码说话,单单注释其实就已经说得很清楚了:

毕竟阻塞线程入队再唤醒开销还是有点大的。

我们再来看看 TrySpin 的操作,这里面有自适应自旋,其实从实际函数名就 TrySpin_VaryDuration 就可以反映出自旋是变化的。

至此,有关 Synchronized 自旋问题就完结了,重量级锁竞争失败会有自旋操作,轻量级锁没有这个动作(至少 1.8 源码是这样的),如果有人反驳你,请把这篇文章甩给他哈哈。

不过都说到这儿了,索性我就继续讲讲 Synchronized 吧,毕竟这玩意出镜率还是挺高的。

这篇文章关于 Synchronized 的深度到哪个程度呢?

之后如有面试官问你看过啥源码?

看完这篇文章,你可以回答:我看过 JVM 的源码。

当然源码有点多的,我把 Synchronized 相关的所有操作都过了一遍,还是有点难度的。

不过之前看过我的源码分析的读者就会知道,我都会画个流程图来整理的,所以即使代码看不懂,流程还是可以搞清楚的!

好,发车!

从重量级锁开始说起

Synchronized 在1.6 之前只是重量级锁。

因为会有线程的阻塞和唤醒,这个操作是借助操作系统的系统调用来实现的,常见的 Linux 下就是利用 pthread 的 mutex 来实现的。

我截图了调用线程阻塞的源码,可以看到确实是利用了 mutex。

而涉及到系统调用就会有上下文的切换,即用户态和内核态的切换,我们知道这种切换的开销还是挺大的。

所以称为重量级锁,也因为这样才会有上面提到的自适应自旋操作,因为不希望走到这一步呀!

我们来看看重量级锁的实现原理

Synchronized 关键字可以修饰代码块,实例方法和静态方法,本质上都是作用于对象上。

代码块作用于括号里面的对象,实例方法是当前的实例对象即 this ,而静态方法就是当前的类。

这里有个概念叫临界区。

我们知道,之所以会有竞争是因为有共享资源的存在,多个线程都想要得到那个共享资源,所以就划分了一个区域,操作共享资源资源的代码就在区域内。

可以理解为想要进入到这个区域就必须持有锁,不然就无法进入,这个区域叫临界区。

当用 Synchronized 修饰代码块时

此时编译得到的字节码会有 monitorenter 和 monitorexit 指令,我习惯按照临界区来理解,enter 就是要进入临界区了,exit 就是要退出临界区了,与之对应的就是获得锁和解锁。

实际上这两个指令还是和修饰代码块的那个对象相关的,也就是上文代码中的lockObject。

每个对象都有一个 monitor 对象于之关联,执行 monitorenter 指令的线程就是试图去获取 monitor 的所有权,抢到了就是成功获取锁了。

这个 monitor 下文会详细分析,我们先看下生成的字节码是怎样的。

图片上方是 lockObject 方法编译得到的字节码,下面就是 lockObject 方法,这样对着看比较容易理解。

从截图来看,执行 System.out 之前执行了 monitorenter 执行,这里执行争锁动作,拿到锁即可进入临界区。

调用完之后有个 monitorexit 指令,表示释放锁,要出临界区了。

图中我还标了一个 monitorexit 指令时,因为有异常的情况也需要解锁,不然就死锁了。

从生成的字节码我们也可以得知,为什么 synchronized 不需要手动解锁?

是有人在替我们负重前行啊!编译器生成的字节码都帮咱们做好了,异常的情况也考虑到了。

当用 synchronized 修饰方法时

修饰方法生成的字节码和修饰代码块的不太一样,但本质上是一样。

此时字节码中没有 monitorenter 和 monitorexit 指令,不过在当前方法的访问标记上做了手脚。

我这里用的是 idea 的插件来看字节码,所以展示的字面结果不太一样,不过 flag 标记是一样的:0x0021 ,是 ACC_PUBLIC 和 ACC_SYNCHRONIZED 的结合。

原理就是修饰方法的时候在 flag 上标记 ACC_SYNCHRONIZED,在运行时常量池中通过 ACC_SYNCHRONIZED 标志来区分,这样 JVM 就知道这个方法是被 synchronized 标记的,于是在进入方法的时候就会进行执行争锁的操作,一样只有拿到锁才能继续执行。

然后不论是正常退出还是异常退出,都会进行解锁的操作,所以本质还是一样的。

这里还有个隐式的锁对象就是我上面提到的,修饰实例方法就是 this,修饰类方法就是当前类(关于这点是有坑的,我写的这篇文章分析过)。

我还记得有个面试题,好像是面字节跳动时候问的,面试官问 synchronized 修饰方法和代码块的时候字节码层面有什么区别?。

怎么说?不知不觉距离字节跳动又更近了呢。

我们再来继续深入 synchronized

从上文我们已经知道 synchronized 是作用于对象身上的,但是没细说,我们接下来剖析一波。

在 Java 中,对象结构分为对象头、实例数据和对齐填充。

而对象头又分为:MarkWord 、 klass pointer、数组长度(只有数组才有),我们的重点是锁,所以关注点只放在 MarkWord 上。

我再画一下 64 位时 MarkWord 在不同状态下的内存布局(里面的 monitor 打错了,但是我不准备改,留个印记哈哈)。

MarkWord 结构之所以搞得这么复杂,是因为需要节省内存,让同一个内存区域在不同阶段有不同的用处。

记住这个图啊,各种锁操作都和这个 MarkWord 有很强的联系。

从图中可以看到,在重量级锁时,对象头的锁标记位为 10,并且会有一个指针指向这个 monitor 对象,所以锁对象和 monitor 两者就是这样关联的。

而这个 monitor 在 HotSpot 中是 c++ 实现的,叫 ObjectMonitor,它是管程的实现,也有叫监视器的。

它长这样,重点字段我都注释了含义,还专门截了个头文件的注释:

暂时记忆一下,等下源码和这几个字段关联很大。

synchronized 底层原理

先来一张图,结合上面 monitor 的注释,先看看,看不懂没关系,有个大致流转的印象即可:

好,我们继续。

前面我们提到了 monitorenter 这个指令,这个指令会执行下面的代码:

我们现在分析的是重量级锁,所以不关心偏向的代码,而 slow_enter 方法文章一开始的截图就是了,最终会执行到 ObjectMonitor::enter 这个方法中。

可以看到重点就是通过 CAS 把 ObjectMonitor 中的 _owner 设置为当前线程,设置成功就表示获取锁成功。

然后通过 recursions 的自增来表示重入。

如果 CAS 失败的话,会执行下面的一个循环:

EnterI 的代码其实上面也已经截图了,这里再来一次,我把重要的入队操作加上,并且删除了一些不重要的代码:

先再尝试一下获取锁,不行的话就自适应自旋,还不行就包装成 ObjectWaiter 对象加入到 _cxq 这个单向链表之中,挣扎一下还是没抢到锁的话,那么就要阻塞了,所以下面还有个阻塞的方法。

可以看到不论哪个分支都会执行 Self->_ParkEvent->park(),这个就是上文提到的调用 pthread_mutex_lock。

至此争抢锁的流程已经很清晰了,我再画个图来理一理。

接下来再看看解锁的方法

ObjectMonitor::exit 就是解锁时会调用的方法。

可重入锁就是根据 _recursions 来判断的,重入一次 _recursions++,解锁一次 _recursions–,如果减到 0 说明需要释放锁了。

然后此时解锁的线程还会唤醒之前等待的线程,这里有好几种模式,我们来看看。

如果 QMode == 2 && _cxq != NULL的时候:

如果QMode == 3 && _cxq != NULL的时候,我就截取了一部分代码:

如果 QMode == 4 && _cxq != NULL的时候:

如果 QMode 不是 2 的话,最终会执行:

至此,解锁的流程就完毕了!我再画一波流程图:

接下来再看看调用 wait 的方法

没啥花头,就是将当前线程加入到 _waitSet 这个双向链表中,然后再执行 ObjectMonitor::exit 方法来释放锁。

接下来再看看调用 notify 的方法

也没啥花头,就是从 _waitSet 头部拿节点,然后根据策略选择是放在 cxq 还是 EntryList 的头部或者尾部,并且进行唤醒。

至于 notifyAll 我就不分析了,一样的,无非就是做了个循环,全部唤醒。

至此 synchronized 的几个操作都齐活了,出去可以说自己深入研究过 synchronized 了。

现在再来看下这个图,应该心里很有数了。

为什么会有_cxq 和 _EntryList 两个列表来放线程?

因为会有多个线程会同时竞争锁,所以搞了个 _cxq 这个单向链表基于 CAS 来 hold 住这些并发,然后另外搞一个 _EntryList 这个双向链表,来在每次唤醒的时候搬迁一些线程节点,降低 _cxq 的尾部竞争。

引入自旋

synchronized 的原理大致应该都清晰了,我们也知道了底层会用到系统调用,会有较大的开销,那思考一下该如何优化?

从小标题就已经知道了,方案就是自旋,文章开头就已经说了,这里再提一提。

自旋其实就是空转 CPU,执行一些无意义的指令,目的就是不让出 CPU 等待锁的释放。

正常情况下锁获取失败就应该阻塞入队,但是有时候可能刚一阻塞,别的线程就释放锁了,然后再唤醒刚刚阻塞的线程,这就没必要了。

所以在线程竞争不是很激烈的时候,稍微自旋一会儿,指不定不需要阻塞线程就能直接获取锁,这样就避免了不必要的开销,提高了锁的性能。

但是自旋的次数又是一个难点,在竞争很激烈的情况,自旋就是在浪费 CPU,因为结果肯定是自旋一会让之后阻塞。

所以 Java 引入的是自适应自旋,根据上次自旋次数,来动态调整自旋的次数,这就叫结合历史经验做事。

注意这是重量级锁的步骤,别忘了文章开头说的~。

至此,synchronized 重量级锁的原理应该就很清晰了吧? 小结一下

synchronized 底层是利用 monitor 对象,CAS 和 mutex 互斥锁来实现的,内部会有等待队列(cxq 和 EntryList)和条件等待队列(waitSet)来存放相应阻塞的线程。

未竞争到锁的线程存储到等待队列中,获得锁的线程调用 wait 后便存放在条件等待队列中,解锁和 notify 都会唤醒相应队列中的等待线程来争抢锁。

然后由于阻塞和唤醒依赖于底层的操作系统实现,系统调用存在用户态与内核态之间的切换,所以有较高的开销,因此称之为重量级锁。

所以又引入了自适应自旋机制,来提高锁的性能。

现在要引入轻量级锁了

我们再思考一下,是否有这样的场景:多个线程都是在不同的时间段来请求同一把锁,此时根本就用不需要阻塞线程,连 monitor 对象都不需要,所以就引入了轻量级锁这个概念,避免了系统调用,减少了开销。

在锁竞争不激烈的情况下,这种场景还是很常见的,可能是常态,所以轻量级锁的引入很有必要。

在介绍轻量级锁的原理之前,再看看之前 MarkWord 图。

轻量级锁操作的就是对象头的 MarkWord 。

如果判断当前处于无锁状态,会在当前线程栈的当前栈帧中划出一块叫 LockRecord 的区域,然后把锁对象的 MarkWord 拷贝一份到 LockRecord 中称之为 dhw(就是那个set_displaced_header 方法执行的)里。

然后通过 CAS 把锁对象头指向这个 LockRecord 。

轻量级锁的加锁过程:

如果当前是有锁状态,并且是当前线程持有的,则将 null 放到 dhw 中,这是重入锁的逻辑。

我们再看下轻量级锁解锁的逻辑:

逻辑还是很简单的,就是要把当前栈帧中 LockRecord 存储的 markword (dhw)通过 CAS 换回到对象头中。

如果获取到的 dhw 是 null 说明此时是重入的,所以直接返回即可,否则就是利用 CAS 换,如果 CAS 失败说明此时有竞争,那么就膨胀!

关于这个轻量级加锁我再多说几句。

每次加锁肯定是在一个方法调用中,而方法调用就是有栈帧入栈,如果是轻量级锁重入的话那么此时入栈的栈帧里面的 dhw 就是 null,否则就是锁对象的 markword。

这样在解锁的时候就能通过 dhw 的值来判断此时是否是重入的。

现在要引入偏向锁

我们再思考一下,是否有这样的场景:一开始一直只有一个线程持有这个锁,也不会有其他线程来竞争,此时频繁的 CAS 是没有必要的,CAS 也是有开销的。

所以 JVM 研究者们就搞了个偏向锁,就是偏向一个线程,那么这个线程就可以直接获得锁。

我们再看看这个图,偏向锁在第二行。

原理也不难,如果当前锁对象支持偏向锁,那么就会通过 CAS 操作:将当前线程的地址(也当做唯一ID)记录到 markword 中,并且将标记字段的最后三位设置为 101。

之后有线程请求这把锁,只需要判断 markword 最后三位是否为 101,是否指向的是当前线程的地址。

还有一个可能很多文章会漏的点,就是还需要判断 epoch 值是否和锁对象的类中的 epoch 值相同。

如果都满足,那么说明当前线程持有该偏向锁,就可以直接返回。

这 epoch 干啥用的?

可以理解为是第几代偏向锁。

偏向锁在有竞争的时候是要执行撤销操作的,其实就是要升级成轻量级锁。

而当一类对象撤销的次数过多,比如有个 Yes 类的对象作为偏向锁,经常被撤销,次数到了一定阈值(XX:BiasedLockingBulkRebiasThreshold,默认为 20 )就会把当代的偏向锁废弃,把类的 epoch 加一。

所以当类对象和锁对象的 epoch 值不等的时候,当前线程可以将该锁重偏向至自己,因为前一代偏向锁已经废弃了。

不过为保证正在执行的持有锁的线程不能因为这个而丢失了锁,偏向锁撤销需要所有线程处于安全点,然后遍历所有线程的 Java 栈,找出该类已加锁的实例,并且将它们标记字段中的 epoch 值加 1。

当撤销次数超过另一个阈值(XX:BiasedLockingBulkRevokeThreshold,默认值为 40),则废弃此类的偏向功能,也就是说这个类都无法偏向了。

至此整个 Synchronized 的流程应该都比较清楚了。

我是反着来讲锁升级的过程的,因为事实上是先有的重量级锁,然后根据实际分析优化得到的偏向锁和轻量级锁。

包括期间的一些细节应该也较为清楚了,我觉得对于 Synchronized 了解到这份上差不多了。

我再搞了张 openjdk wiki 上的图,看看是不是很清晰了:

最后

之所以分析源码,是因为看了资料,但是很多细节不清晰,然后很难受,所以没办法只能硬着头皮上了。

对于我这个 c++ 基本上不会的人来说,这个确实有点难度….断断续续写了一个星期。

其实没打算写这么多的,就只是想写自旋那一部分的…搞着搞着就停不下来了。

还有,如果有什么错误,赶紧联系我。

这文章代码有点多,不知道有多少人可以耐着性子看到这里…

我觉得看到这里的都是高手啊!能不能扣个 1 给我看看?

巨人的肩膀

《深入拆解Java虚拟机》郑雨迪

wiki.openjdk.java.net/display/Hot…

docs.oracle.com/javase/spec…

微信搜一搜【yes的练级攻略】更多文章等你来阅,我的文章汇总:github.com/yessimida/y… 欢迎 star !


我是 yes,从一点点到亿点点,欢迎在看、转发、留言,我们下篇见。

本文转载自: 掘金

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

3最长递增子序列(附带动态规划知识点讲解)|刷题打卡

发表于 2021-03-01

一、题目描述:


二、思路分析:

这是一道很简单的动态规划,在这里讲解下动态规划的一般思路
动态规划算法能解决的问题有以下几种特征:

  1. 具有相同子问题:首先我们必须要保证这个问题能够分解出几个子问题,并且能够通过这些子问题来解决这个问题
  2. 满足最优化原理(最优子结构):问题的最优解包含子问题的最优解。反过来说就是,我们可以通过子问题的最优解,推导出问题的最优解。
  3. 具有无后效性:每个子问题的决策,不能够对解决其他未来的问题产生影响稍后会解释本题如何消除后效性

解题步骤

第一步:确定问题的子问题 ,要点:注意分析哪些量随着问题规模的变小是会变小的,哪些变量与问题无关。

第二步:确定状态, 要点:结合子问题,敢想敢试,不要轻易否定一个状态,多思考,不要希望每个题都能一蹴而就!

第三步:推出状态转移方程 要点:注意验证适用条件是否满足,注意不要漏掉条件。

第四步:确定边界条件 要点:先根据状态含义确定,确定后验证第一层是否正确,如果不正确或
者无法按含义确定,则也可以采用第一层的值反推的方式确定边界。

第五步:确定实现方式 要点:根据拓扑序是否明显和个人习惯确定!

第六步:如果需要的话,确定优化方法! 要点:注意优先考虑能否降维,不要局限于单调队列,四边形不等式这些标准的用于譤議优化的东西,扩宽思维,避免定式!

实践出真知

第一步:在这道题中,问题规模无疑是数组最大长度(你可以理解为右指针),而随着最大长度减小,递增子序列的长度也会减小。但是!长度也可能不变,因为最大长度减小时排除的那个数,未必包含在当前的最长递增子序列中。

第二步:之前说到后效性问题,很明显,是否选择了第i个点,作为递增子序列的一部分,这对后面的答案一定是有影响的,因此我们设置状态为dp[i][0]为长度为i时,不选择第i个点加入递增子序列的长度,dp[i][1]为长度为i时,选择第i个点加入递增子序列的长度。

第三步:推出状态转移方程,不选择第i个点加入递增子序列时,最大长度就相当于dp[i - 1][*]的最大值,而选择第i个点加入递增子序列时,最大长度相当于一个dp[过去的点]+1,而这个点的值必须严格小于第i个点。当然如果没有点满足要求,则将其设置为1。具体如下

1
2
3
C++复制代码dp[i][0] = max(dp[i - 1][0] , dp[i - 1][1]);
dp[i][1] = 1;
if(nums[j] < nums[i])dp[i][1] = max(dp[j][1] + 1, dp[i][1]);

第四步: 边界条件,方程需要根据前面的节点而得到答案,因此i == 0时,是步具备前面的节点的,需要单独指定

第五步:实现方式略

第六步:优化略

三、AC 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
C++复制代码class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int dp[nums.size()][2];
dp[0][0] = 0;
dp[0][1] = 1;
for(int i = 1 ; i < nums.size(); i++){

dp[i][0] = max(dp[i - 1][0], dp[i - 1][1]);
int j = i - 1;
dp[i][1] = 1;
while(j >= 0){
if(nums[j] < nums[i])dp[i][1] = max(dp[j][1] + 1, dp[i][1]);
j--;
}
// printf("%d:%d ", dp[i][0],dp[i][1]);
}

return max(dp[nums.size() - 1][0] , dp[nums.size() - 1][1]);

}
};

四、总结:

这道题在不难的同时,需要自己去消除无后效性的影响,我觉得有作为一道例题讲的价值。
接下来我去再看看动态规划的知识,学成以后继续发这个系列。

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情

本文转载自: 掘金

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

分布式高级篇(九)- 商城服务 - 订单业务

发表于 2021-03-01
  • 订单确认页
  • feign远程调用丢失请求头的问题
  • feign异步调用丢失请求头的问题
  • 原子验证令牌:redis lua脚本
  • 下单:订单创建、验证令牌、验价、锁定库存
  • 本地事务
  • 效果演示

订单业务

页面环境搭建

  • 静态资源拷贝进nginx

订单 order 下的 列表、支付、详情、确认订单等页面资源

image-20210208150909237

image-20210208151336834

修改html页面的静态资源引用地址

image-20210208151658881

image-20210208151900864

  • 配置 订单服务域名

C:\Windows\System32\drivers\etc\hosts

image-20210208151010928

  • 配置网关路由并重启网关服务

image-20210208151439103

  • 页面展示

image-20210218100225862

订单中心

  • 电商系统涉及到3流,分别是信息流、资金流、物流,而订单系统作为中枢将三者有机的集合起来
  • 订单模块是电商系统的枢纽,在订单这个环节上需求获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息流通

订单构成

  • 订单构成图

image-20210218105831213

  • 1、用户信息

用户信息包括用户账号、用户等级、用户的收货地址、收货人、收货人电话等组成,用户账户需要绑定手机号码,但是用户绑定的手机号码不一定是收获信息上的电话。用户可以添加多个收获信息,用户等级信息可以用来和促销系统进行匹配,获取商品折扣,同时用户等级还可以获取积分的奖励等

  • 2、订单基础信息

订单基础信息是订单流转的核心,其包括订单类型、父/子订单、订单编号、订单状态、订单流转的时间等

+ 订单类型包括实体商品订单和虚拟订单商品等,这个根据商城商品和服务类型进行区分
+ 同时订单都需要做父子订单处理,之前在初创公司一直只有一个订单,没有父子订单处理后期需要进行拆单的时候就比较麻烦,尤其是多商户商场,和不同仓库商品时,父子订单就是为后期做拆单准备的
+ 订单编号不多说了,需要强调的一点是父子订单都需要有订单编号,需要完善的时候可以对订单编号的每个字段进行统一定义和诠释
+ 订单状态记录订单每次流转过程,后面会对订单状态进行单独的说明
+ 订单流转时间需要记录下单时间,支付时间,发货时间,结束时间/关闭时间等
  • 3、商品信息

商品信息从商品库中获取商品的SKU信息、图片、名称、属性规格、商品单价、商户信息等,从用户下单行为记录的用户下单数量、商品合计价格等

  • 4、优惠信息

优惠信息记录用户参与的优惠活动,包括优惠促销活动,比如满减、满赠、秒杀等,用户使用的优惠券信息,优惠券满足条件的优惠券需要默认展示出来,虚拟币抵扣信息等进行记录

  • 5、支付信息
+ 支付流水单号,这个流水单号实在唤起网关支付后支付通道返给电商业务平台的支付流水号,财务通过订单号和流水号与支付通道进行对账使用
+ 支付方式用户使用的支付方式,比如微信支付、支付宝支付、钱包支付、快捷支付等。支付方式有时候可能有两个--余额支付+第三方支付
+ 商品总金额,每个商品加总后的金额;运费、物流产生的费用;优惠总金额,包括促销活动和优惠金额,优惠券优惠金额,虚拟积分或者虚拟币抵扣的金额,会员折扣的金额之和;实付金额,用户时间需要付款的金额
+ 用户实付金额 = 商品总金额 + 运费 - 优惠总金额
  • 6、物流信息

物流信息包括配送方式,物流公司、物流单号、物流状态,物流状态可以通过第三方接口来获取和向用户展示物流每个状态节点

订单状态

  • 1、待付款

用户提交订单后,订单进行预下单,目前主流电商网站都会唤起支付,便于用户快速完成支付,需要注意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超时后将自动取消订单,订单变更关闭状态

  • 2、已付款/待发货

用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到WMS系统,仓库进行调拨、配货、分拣、出库等操作

  • 3、待收货/已发货

仓储将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时知悉物品物流状态

  • 4、已完成

用户确认收货后,订单交易完成,后续支付进行结算,如果订单存在问题进入售后状态

  • 5、已取消

付款之前取消订单,包括超时未付款或用户商户取消订单都会产生这种订单状态

  • 6、售后中

用户在付款后申请退款,或商家发货后用户申请退换货

售后也同样存在各种状态,当发起售后后生成售后订单,售后订单状态变为待审核,等待商家审核,商家审核通过后订单状态变更为待退货,等待用户将商品寄回,商家收货后订单状态变更为待退款,退款到用户原账户后订单更新为售后成功

订单流程

  • 订单流程是指从订单产生到完成整个流转的过程,从而形成了一套标准流程规则,而不同的产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单的流程,线上实物订单与O2O订单等,所以需要根据不同的类型进行构建订单流程
  • 不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程,正向流程就是一个正常的网购步骤:订单生成 –> 支付订单 –> 卖家发货 –> 确认收货 –> 交易成功

而每个步骤的背后,订单是如何在多个系统之间交互流转的,可概括如下图

image-20210218135923824

订单创建与支付

  • 1、订单创建前需要预览订单,选择收货信息等
  • 2、订单创建需要锁定库存,库存有才可以创建,否则不能创建
  • 3、订单创建后超时未支付需要解锁库存
  • 4、支付成功后,需要进行拆单,根据商品打包方式、所在仓库、物流等进行拆单
  • 5、支付的每笔流水都需要记录,以待查账‘
  • 6、订单创建,支付成功等状态都需要给MQ发送消息,方便其他系统感知订阅

逆向流程

  • 1、修改订单,用户没有提交订单,可以对订单一些信息进行修改,比如配送信息、优惠信息以及其他一些订单可修改范围的内容,此时只需对数据进行变更即可
  • 2、订单取消,用户主动取消订单和用户超时未支付,两种情况下订单都会取消订单,而超时情况是系统自动关闭订单,所以在支付订单的响应机制上面要做支付的限时处理

幂等性处理

什么是幂等性

  • 接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生副作用;比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣没了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条…这就是没有保证接口的幂等性

哪些情况需要防止

  • 用户多次点击按钮
  • 用户页面回退再次提交
  • 微服务互相调用,由于网络问题,导致请求失败,feign触发重试机制
  • 其他业务情况

什么情况下需要幂等

  • 以SQL为例,有些操作是天然幂等的
1
2
3
4
5
6
7
sql复制代码SELECT * FROM table WHERE id=?,无论执行多少次都不会改变状态,是天然的幂等
UPDATE table1 SET col1=1 WHERE col2=2,无论执行成功多少次状态都是一致的,也是幂等操作
DELETE FROM USER WHERE userid=1,多次操作,结果一样,具备幂等性
INSERT into user(userid,name) VALUES (1,'a') 如userid为唯一主键,即重复操作上面的业务,只会插入一条用户数据,具备幂等性
----------------------------
UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,不是幂等的
INSERT into user(userid,name) values (1,'a'),如 userid不是主键,可以重复,那上面业务多次操作,数据都会新增多条,不具备幂等性

幂等解决方案

1、token机制
  • 1.1、服务端提供了发送token的接口,我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取token,服务器会把token保存到redis中
  • 1.2、然后调用业务接口请求时,把token携带过去,一般放在请求头部
  • 1.3、服务器判断token是否存在redis中,存在表示第一次请求,然后删除token,继续执行业务
  • 1.4、如果token不存在redis中,就表示重复操作,直接返回重复标记给client,这样就保证了业务代码,不被重复执行
  • 危险性:
+ 先删除token还是后删除token


    - 先删除,可能导致业务确实没有执行,重试还带上之前token,由于防重设计导致,请求还是不能执行
    - 后删除,业务处理成功,但是服务闪断,出现超时,没有成功删除token,别人继续重试,导致业务被执行了两次
    - 我们最好设计为先删除token,如果业务调用失败,就重新获取token再次请求
+ token获取、比较和删除必须是原子性


    - `redis.get(token)、token.equals、redis.del(token)`,如果这两个操作不是原子,可能导致,高并发下,都get到同样的数据,判断都成功,继续业务并发执行
    - 可以在 redis 中是 lua 脚本完成这个操作



    
1
kotlin复制代码if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end
2、各种锁机制
  • 2.1、数据库悲观锁

SELECT * FROM xxxx WHERE id=1 for UPDATE

悲观锁使用时一般伴随着事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。另外要注意的是,id字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会非常麻烦

  • 2.2、数据库乐观锁

这种方法适合在更新的场景中

update t_goods set count = count-1,version = version +1 where good_id=2 and version =1
根据 version 版本,也就是在操作库存前先获取当前商品的version版本号,然后操作的时候带上此version号,我们梳理下,我们第一次操作库存时,得到version为1,调用库存服务version变成了2,但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订单服务传的version还是1,再执行上面的sql语句时,就不会执行;因为version已经变2了,where条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。乐观锁主要使用于处理读多写少的问题

  • 2.3、业务层分布式锁

如果多个机器可能在同一时间同时处理相同的数据,比如多台机器定时任务都拿到了相同数据处理,我们就可以加分布式锁,锁定此数据,处理完成后释放锁,获取到锁的必须先判断这个数据是否被处理过

3、各种唯一约束
  • 3.1、数据库唯一约束

插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入。我们在数据库层面防止重复

这个机制是利用了数据库的主键唯一约束的特性,解决了在insert场景时幂等问题,但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键

如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关

  • 3.2、redis set 防重

很多数据需要处理,只能被处理一次,比如我们可以计算数据的MD5将其放入redis的set,每次处理数据,先看这个MD5是否已经存在,存在就不处理

4、防重表
  • 使用订单号 orderNo 作为去重表的唯一索引,把唯一索引插入进去重表,再进行业务操作,且他们在同一个事务中,这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里要注意的是,去重表和业务表应该在同一个库中,这样就能保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性
  • 之前说的 redis 防重也算
5、全局请求唯一id
  • 调用接口时,生成一个唯一id,redis将数据保存到集合中(去重),存在即处理过;可以使用nginx设置每一个请求的唯一id;
1
bash复制代码proxy_set_header X-Request-Id $request_id

订单业务

订单确认页

  • 可以发现订单结算页,包含以下信息:
+ 1、收货人信息:有更多地址、即有多个收货地址,其中有一个默认收货地址
+ 2、支付方式:货到付款、在线支付、不需要后台提供
+ 3、送货清单:配送方式(不做)以及商品列表(根据购物车选中的skuId到数据库中查询)
+ 4、发票:不做
+ 5、优惠:查询用户领取的优惠券(不做)以及可以积分
1
2
csharp复制代码# 订单确认页需要用到的数据
OrderConfirmVo.class
  • 远程查询所有的收货地址

远程查询购物车所有选中的购物项

查询用户积分

Feign远程调用丢失请求头的问题
  • feign在远程调用之前要构造请求,调用很多拦截器

RequestInterceptor interceptor:requestInterceptors

image-20210219093406683

image-20210219093944404

image-20210219112008566

Feign异步情况丢失上下文的问题
  • 多次远程调用,耗费时间,异步编排优化,这时又会出现请求拦截器中上下文为null的情况

image-20210219112502257

  • 异步模式,线程池中多个线程,ThreadLocal 无法跨线程共享数据

线程打印情况

image-20210219132203647

image-20210219132408363

  • 解决方法:在进入请求拦截器之前,获取最先的上下文内容,每个异步线程都重新为上下文set一次

image-20210219132856942

订单确认页整体流程
  • 流程图

image-20210222113713719

image-20210222113831187

创建订单

下单流程图
  • 提交订单流程:验证令牌–创建订单号

image-20210223090208412

image-20210223090428059

原子验证令牌
  • 使用redis的方式验证删除令牌,必须保证原子性

可以使用 lua 脚本 确报原子性

+ 未使用 lua 脚本,存在多次验证成功的隐患


![image-20210223085630714](https://gitee.com/songjianzaina/juejin_p12/raw/master/img/1c6a881060504ebccd7f8c57e84af14bbe457b6588c2b9a1d504fa3a0be22468)
+ 使用 lua 脚本,保证原子性


![image-20210223085742777](https://gitee.com/songjianzaina/juejin_p12/raw/master/img/054d5b66babdd858214ba101f937e9b9eead8007f2fc9531feee38a1d10a192e)
构造订单数据
  • 用户提交的订单所需数据

image-20210224171007969

锁定库存
  • 锁定库存的逻辑流程图

image-20210223143717117

本文转载自: 掘金

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

JDK动态代理:不仅要学会用,更要掌握其原理

发表于 2021-03-01

微信搜索:码农StayUp

主页地址:gozhuyinglong.github.io

源码分享:github.com/gozhuyinglo…

JDK动态代理是指:代理类实例在程序运行时,由JVM根据反射机制动态的生成。也就是说代理类不是用户自己定义的,而是由JVM生成的。

由于其原理是通过Java反射机制实现的,所以在学习前,要对反射机制有一定的了解。传送门:Java反射机制:跟着代码学反射

下面是本篇讲述内容:

  1. JDK动态代理的核心类

JDK动态代理有两大核心类,它们都在Java的反射包下(java.lang.reflect),分别为InvocationHandler接口和Proxy类。

1.1 InvocationHandler接口

代理实例的调用处理器需要实现InvocationHandler接口,并且每个代理实例都有一个关联的调用处理器。当一个方法在代理实例上被调用时,这个方法调用将被编码并分派到其调用处理器的invoke方法上。

也就是说,我们创建的每一个代理实例都要有一个关联的InvocationHandler,并且在调用代理实例的方法时,会被转到InvocationHandler的invoke方法上。

1
2
java复制代码public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;

该invoke方法的作用是:处理代理实例上的方法调用并返回结果。

其有三个参数,分别为:

  • proxy:是调用该方法的代理实例。
  • method:是在代理实例上调用的接口方法对应的Method实例。
  • args:一个Object数组,是在代理实例上的方法调用中传递的参数值。如果接口方法为无参,则该值为null。

其返回值为:调用代理实例上的方法的返回值。

1.2 Proxy类

Proxy类提供了创建动态代理类及其实例的静态方法,该类也是动态代理类的超类。

代理类具有以下属性:

  • 代理类的名称以 “$Proxy” 开头,后面跟着一个数字序号。
  • 代理类继承了Proxy类。
  • 代理类实现了创建时指定的接口(JDK动态代理是面向接口的)。
  • 每个代理类都有一个公共构造函数,它接受一个参数,即接口InvocationHandler的实现,用于设置代理实例的调用处理器。

Proxy提供了两个静态方法,用于获取代理对象。

1.2.1 getProxyClass

用于获取代理类的Class对象,再通过调用构造函数创建代理实例。

1
2
3
java复制代码public static Class<?> getProxyClass(ClassLoader loader,
Class<?>... interfaces)
throws IllegalArgumentException

该方法有两个参数:

  • loader:为类加载器。
  • intefaces:为接口的Class对象数组。

返回值为动态代理类的Class对象。

1.2.2 newProxyInstance

用于创建一个代理实例。

1
2
3
4
java复制代码public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException

该方法有三个参数:

  • loader:为类加载器。
  • interfaces:为接口的Class对象数组。
  • h:指定的调用处理器。

返回值为指定接口的代理类的实例。

1.3 小结

Proxy类主要用来获取动态代理对象,InvocationHandler接口主要用于方法调用的约束与增强。

  1. 获取代理实例的代码示例

上一章中已经介绍了获取代理实例的两个静态方法,现在通过代码示例来演示具体实现。

2.1 创建目标接口及其实现类

JDK动态代理是基于接口的,我们创建一个接口及其实现类。

Foo接口:

1
2
3
4
5
java复制代码public interface Foo {

String ping(String name);

}

Foo接口的实现类RealFoo:

1
2
3
4
5
6
7
8
9
java复制代码public class RealFoo implements Foo {

@Override
public String ping(String name) {
System.out.println("ping");
return "pong";
}

}

2.2 创建一个InvocationHandler

创建一个InvocationHandler接口的实现类MyInvocationHandler。该类的构造方法参数为要代理的目标对象。

invoke方法中的三个参数上面已经介绍过,通过调用method的invoke方法来完成方法的调用。

这里一时看不懂没关系,后面源码解析章节会进行剖析。

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

// 目标对象
private final Object target;

public MyInvocationHandler(Object target) {
this.target = target;
}


@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("proxy - " + proxy.getClass());
System.out.println("method - " + method);
System.out.println("args - " + Arrays.toString(args));
return method.invoke(target, args);
}
}

2.3 方式一:通过getProxyClass方法获取代理实例

具体实现步骤如下:

  1. 根据类加载器和接口数组获取代理类的Class对象
  2. 过Class对象的构造器创建一个实例(代理类的实例)
  3. 将代理实例强转成目标接口Foo(因为代理类实现了目标接口,所以可以强转)。
  4. 最后使用代理进行方法调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Test
public void test1() throws Exception {
Foo foo = new RealFoo();
// 根据类加载器和接口数组获取代理类的Class对象
Class<?> proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), Foo.class);

// 通过Class对象的构造器创建一个实例(代理类的实例)
Foo fooProxy = (Foo) proxyClass.getConstructor(InvocationHandler.class)
.newInstance(new MyInvocationHandler(foo));

// 调用 ping 方法,并输出返回值
String value = fooProxy.ping("杨过");
System.out.println(value);

}

输出结果:

1
2
3
4
5
arduino复制代码proxy - class com.sun.proxy.$Proxy4
method - public abstract java.lang.String io.github.gozhuyinglong.proxy.Foo.ping(java.lang.String)
args - [杨过]
ping
pong

通过输出结果可以看出:

  • 代理类的名称是以$Proxy开头的。
  • 方法实例为代理类调用的方法。
  • 参数为代理类调用方法时传的参数。

2.4 方式二:通过newProxyInstance方法获取代理实例

通过这种方法是最简单的,也是推荐使用的,通过该方法可以直接获取代理对象。

注:其实该方法后台实现实际与上面使用getProxyClass方法的过程一样。

1
2
3
4
5
6
7
8
9
10
java复制代码@Test
public void test2() {
Foo foo = new RealFoo();
// 通过类加载器、接口数组和调用处理器,创建代理类的实例
Foo fooProxy = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
new Class[]{Foo.class},
new MyInvocationHandler(foo));
String value = fooProxy.ping("小龙女");
System.out.println(value);
}

2.5 通过Lambda表达式简化实现

其实InvocationHander接口也不用创建一个实现类,可以使用Lambad表达式进行简化的实现,如下代码:

1
2
3
4
5
6
7
8
9
10
java复制代码@Test
public void test3() {
Foo foo = new RealFoo();

Foo fooProxy = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
new Class[]{Foo.class},
(proxy, method, args) -> method.invoke(foo, args));
String value = fooProxy.ping("雕兄");
System.out.println(value);
}
  1. 源码解析

3.1 代理类$Proxy是什么样子

JVM为我们自动生成的代理类到底是什么样子的呢?下面我们先来生成一下,再来看里面的构造。

3.1.1 生成$Proxy的.class文件

JVM默认不创建该.class文件,需要增加一个启动参数:
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

在IDEA中点击【Edit Configurations…】,打开 Run/Debug Configurations 配置框。


将上面启动参数加到【VM options】中,点击【OK】即可。

再次运行代码,会在项目中的【com.sun.proxy】目录中找到这个.class文件,我这里是“$Proxy4.class”

3.1.2 为什么加上这段启动参数就能生成$Proxy的字节码文件

在Proxy类中有个ProxyClassFactory静态内部类,该类主要作用就是生成静态代理的。

其中有一段代码ProxyGenerator.generateProxyClass用来生成代理类的.class文件。

其中变量saveGeneratedFiles便是引用了此启动参数的值。将该启动参数配置为true会生成.class文件。

3.1.3 这个代理类$Proxy到底是什么样子呢

神秘的面纱即将揭露,前面很多未解之迷在这里可以找到答案!

打开这个$Proxy文件,我这里生成的是$Proxy4,下面是内容:

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
java复制代码// 该类为final类,其继承了Proxy类,并实现了被代理接口Foo
public final class $Proxy4 extends Proxy implements Foo {

// 这4个Method实例,代表了本类实现的4个方法
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;

// 静态代码块根据反射获取这4个方法的Method实例
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("io.github.gozhuyinglong.proxy.Foo").getMethod("ping");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}

// 一个公开的构造函数,参数为指定的 InvocationHandler
public $Proxy4(InvocationHandler var1) throws {
super(var1);
}

public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

// Foo接口的实现方法,最终调用了 InvocationHandler 中的 invoke 方法
public final String ping(String var1) throws {
try {
return (String)super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
}

通过该文件可以看出:

  • 代理类继承了Proxy类,其主要目的是为了传递InvocationHandler。
  • 代理类实现了被代理的接口Foo,这也是为什么代理类可以直接强转成接口的原因。
  • 有一个公开的构造函数,参数为指定的InvocationHandler,并将参数传递到父类Proxy中。
  • 每一个实现的方法,都会调用InvocationHandler中的invoke方法,并将代理类本身、Method实例、入参三个参数进行传递。这也是为什么调用代理类中的方法时,总会分派到InvocationHandler中的invoke方法的原因。

3.2 代理类是如何创建的

我们从Proxy类为我们提供的两个静态方法开始getProxyClass和newProxyInstance。上面已经介绍了,这两个方法是用来创建代理类及其实例的,下面来看源码。

3.2.1 getProxyClass 和 newProxyInstance方法

getProxyClass方法

newProxyInstance方法

通过上面源码可以看出,这两个方法最终都会调用getProxyClass0方法来生成代理类的Class对象。只不过newProxyInstance方法为我们创建好了代理实例,而getProxyClass方法需要我们自己创建代理实例。

3.2.2 getProxyClass0 方法

下面来看这个统一的入口:getProxyClass0
getProxyClass0方法

从源码和注解可以看出:

  • 代理接口的最多不能超过65535个
  • 会先从缓存中获取代理类,则没有再通过ProxyClassFactory创建代理类。(代理类会被缓存一段时间。)

3.2.3 WeakCache类

这里简单介绍一下WeakCache<K, P, V> 类,该类主要是为代理类进行缓存的。获取代理类时,会首先从缓存中获取,若没有会调用ProxyClassFactory类进行创建,创建好后会进行缓存。
WeakCache类的get方法

3.2.4 ProxyClassFactory类

ProxyClassFactory是Proxy类的一个静态内部类,该类用于生成代理类。下图是源码的部分内容:

ProxyClassFactory类

  • 代理类的名称就是在这里定义的,其前缀是$Proxy,后缀是一个数字。
  • 调用ProxyGenerator.generateProxyClass来生成指定的代理类。
  • defineClass0方法是一个native方法,负责字节码加载的实现,并返回对应的Class对象。

3.3 原理图

为了便于记录,将代理类的生成过程整理成了一张图。

源码分享

完整代码请访问我的Github,若对你有帮助,欢迎给个⭐,感谢~~🌹🌹🌹

github.com/gozhuyinglo…

推荐阅读

  • Java反射机制:跟着代码学反射

关于作者

项目 内容
公众号 码农StayUp(ID:AcmenStayUp)
主页 gozhuyinglong.github.io
CSDN blog.csdn.net/gozhuyinglo…
掘进 juejin.cn/user/123990…
Github github.com/gozhuyinglo…
Gitee gitee.com/gozhuyinglo…

本文转载自: 掘金

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

Java 多线程 漫谈 Volatile

发表于 2021-02-27

总文档 :文章目录

Github : github.com/black-ant

volatile005.jpg

一 . volatile 基础

1
2
3
4
5
6
7
java复制代码> volatile 保证内存的可见性 并且 禁止指令重排
> volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的
> 保证线程可见性且提供了一定的有序性



// Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。

二 . volatile 深入知识点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码> 读写主存中的数据没有 CPU 中执行指令的速度快 , 为了提高效率 , 使用 CPU 高速缓存来提高效率

> CPU 高速缓存 : CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关


// 原理 @ https://www.cnblogs.com/xrq730/p/7048693.html
Step 1 : 先说说 CPU 缓存 , CPU 有多级缓存 , 查询数据会由一级到三级中
一级缓存:简称L1 Cache,位于CPU内核的旁边,是与CPU结合最为紧密的CPU缓存
二级缓存:简称L2 Cache,分内部和外部两种芯片,内部芯片二级缓存运行速度与主频相同,外部芯片二级缓存运行速度则只有主频的一半
三级缓存:简称L3 Cache,部分高端CPU才有

// 缓存的加载次序
1 > 程序以及数据被加载到主内存
2 > 指令和数据被加载到CPU缓存
3 > CPU执行指令,把结果写到高速缓存
4 > 高速缓存中的数据写回主内存

// Step End : 因为不同的缓存 , 就出现了数据不一致 , 所以出现了规则
当一个CPU修改缓存中的字节时,服务器中其他CPU会被通知,它们的缓存将视为无效

三 . volatile 和 synchronized 的区别

  1. volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取。synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile 仅能使用在变量级别。synchronized 则可以使用在变量、方法、和类级别的。
  3. volatile 仅能实现变量的修改可见性,不能保证原子性。而synchronized 则可以保证变量的修改可见性和原子性。
  4. volatile 不会造成线程的阻塞。synchronized 可能会造成线程的阻塞。
  5. volatile 标记的变量不会被编译器优化。synchronized标记的变量可以被编译器优化。

注意 :volatile 不能取代 synchronized

四 . volatile 原理

观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码发现,加入volatile 关键字时,会多出一个 lock 前缀指令。lock 前缀指令,其实就相当于一个内存屏障。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。volatile 的底层就是通过内存屏障来实现的

  • Step 1 : 写volatile的时候生成汇编码是 lock addl $0x0, (%rsp)
  • Step 2 : 在写操作之前使用了lock前缀,锁住了总线和对应的地址,这样其他的写和读都要等待锁的释放。
  • Step 3 : 当写完成后,释放锁,把缓存刷新到主内存。
  1. 读volatile就很好理解了,不需要额外的汇编指令,CPU发现对应地址的缓存被锁了,等待锁的释放,缓存一致性协议会保证它读到最新的值。
  2. 只需要对写volatile的使用用lock对总线加锁就行了,这样其他的读、写操作等待总线释放才能继续读。Lock会让其他CPU的缓存invalide,从内存重新加载数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码// volatile 的内存语义
- 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值,立即刷新到主内存中。
- 当读一个 volatile 变量时,JMM 会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
> 所以 volatile 的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。

// volatile 的内存语义实现原理 : 为了实现 volatile 的内存语义,JMM 会限制重排序
1. 如果第一个操作为 volatile 读,则不管第二个操作是啥,都不能重排序。
?- 这个操作确保volatile 读之后的操作,不会被编译器重排序到 volatile 读之前;
2. 如果第二个操作为 volatile 写,则不管第一个操作是啥,都不能重排序。
?- 这个操作确保volatile 写之前的操作,不会被编译器重排序到 volatile 写之后;
3. 当第一个操作 volatile 写,第二个操作为 volatile 读时,不能重排序。

// volatile 的底层实现 : 内存屏障 , 有了内存屏障, 就可以避免重排序
-> 对于编译器来说,发现一个最优布置来最小化插入内存屏障的总数几乎是不可能的,所以,JMM 采用了保守策略
• 在每一个 volatile 写操作前面,插入一个 StoreStore 屏障
- StoreStore 屏障:保证在 volatile 写之前,其前面的所有普通写操作,都已经刷新到主内存中。
• 在每一个 volatile 写操作后面,插入一个 StoreLoad 屏障
- StoreLoad 屏障:避免 volatile 写,与后面可能有的 volatile 读 / 写操作重排序。
• 在每一个 volatile 读操作后面,插入一个 LoadLoad 屏障
- LoadLoad 屏障:禁止处理器把上面的 volatile读,与下面的普通读重排序。
• 在每一个 volatile 读操作后面,插入一个 LoadStore 屏障
- LoadStore 屏障:禁止处理器把上面的 volatile读,与下面的普通写重排序。

五. volatile 原子性

1
2
3
4
java复制代码> 我们需要区别 volatile 变量和 atomic 变量
// volatile 并不能很好的保证原子性
volatile 变量,可以确保先行关系,即写操作会发生在后续的读操作之前,但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。
AtomicInteger 类提供的 atomic 方法,可以让这种操作具有原子性。例如 #getAndIncrement() 方法,会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

六 . volatile 源码

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
java复制代码TODO : 涉及到源码 ,先留坑 , 具体可以先看 @ https://www.cnblogs.com/xrq730/p/7048693.html

// 主要节点 :
0x0000000002931351: lock add dword ptr [rsp],0h ;
*putstatic instance;
- org.xrq.test.design.singleton.LazySingleton::getInstance@13 (line 14)

> 将双字节的栈指针寄存器+0 , 保证volatile关键字的内存可见性

// 基本概念一 : LOCK# 的作用
- 锁总线
- 其它CPU对内存的读写请求都会被阻塞,直到锁释放
- 不过实际后来的处理器都采用锁缓存替代锁总线
- 因为锁总线的开销比较大,锁总线期间其他CPU没法访问内存

- lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据
- 不是内存屏障却能完成类似内存屏障的功能,阻止屏障两遍的指令重排序


// 基本概念二 : 缓存行
- 缓存是分段(line)的,一个段对应一块存储空间 , 即缓存行
- CPU看到一条读取内存的指令时,它会把内存地址传递给一级数据缓存
- 一级数据缓存检测是否由缓存段 , 没有加载这缓存段


// 原因 : volatile 基于 缓存一致性来实现
Step1 : 因为LOCK 效率问题 ,所以基于缓存一致性来处理
Step2 : 缓存一致性作用时 使用多组缓存,但是它们的行为看起来只有一组缓存那样
Step3 : 常见的协议是 snooping 和 MESI
Step4 : snooping 的作用是 : 仲裁所有的内存访问操作

七 . volatile 实测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码// 测试原子性 , 结果 ThreadC   : ------> count :9823 <-------
// Thread 中操作
public static void addCount() {
for (int i = 0; i < 100; i++) {
count++;
}
logger.info("------> count :{} <-------", count);
}


ThreadC[] threadCS = new ThreadC[100];

for (int i = 0; i < 100; i++) {
threadCS[i] = new ThreadC();
}

for (int i = 0; i < 100; i++) {
threadCS[i].start();
}


// 添加 synchronized 后 -- > ThreadD count :10000 <-------
synchronized public static void addCount()

volatile006.jpg

本文转载自: 掘金

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

Java多线程 细说 synchronized

发表于 2021-02-27

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

1.1 synchronized 简述

synchronized 被称为重量级锁 , 但是 1.6 版本后得到了优化 , 相对轻量了很多, 它可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块 .

  • 主要操作对象是方法或者代码块中存在的共享数据, 同时可保证一个线程的变化(主要是共享数据的变化)被其他线程所看
  • synchronized 的核心原理为 Java 对象头 以及 Monitor
    • JVM基于进入和退出Monitor对象来实现方法同步和代码块同步

1.2 Java 对象头 和 Monitor

对象头结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码// 原理 -->  
1 Java 对象头 和 Monitor
|-> 对象头 :
|-> Mark Word(标记字段)、Klass Pointer(类型指针)
|-> Klass Pointer : 类元数据指针,决定是何数据
|-> Mark Word : 自身运行时数据 (hashcode,锁状态,偏向,标志位等)
|-> Monitor :
|-> 互斥 :一个 Monitor 锁在同一时刻只能被一个线程占用


// 关系 -->
- Monitor 是一种对象类型 , 任何Java 对象都可以是 Monitor 对象
- 当Java 对象被 synchronized 修饰时 , 就可以当成 Monitor 对象进行处理


// Mark Word 和 Class Metadata Address 组成结构
--------------------------------------------------------------------------------------------------
虚拟机位数 头对象结构 说明
|---------|-----------------------|---------------------------------------------------------------|
32/64bit Mark Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
32/64bit Class Metadata Address 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。
--------------------------------------------------------------------------------------------------

32 位虚拟机 Mark Word >>>>

64 位虚拟机 Mark Word >>>>

image.png

数据结构

1
2
3
4
java复制代码// Monitor 的实现方式 @ https://blog.csdn.net/javazejian/article/details/70768369
ObjectMonitor中有两个队列以及一个区域
_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象)
_owner (指向持有ObjectMonitor对象的线程) 区域
  • 1 当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合, 此时开始尝试获取monitor
  • 2 当线程获取到对象的monitor 后进入 _Owner 区域 ,并把 monitor中的owner变量 设置为当前线程同时monitor中的计数器count加1
  • 3 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。
  • 4 若当前线程执行完毕 也将 释放monitor(锁) 并 复位变量的值,以便 其他线程进入获取monitor(锁)

Monitor 指令

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之匹配

查看汇编情况 :

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
java复制代码// Step 1 : 准备简单的Demo
public class SynchronizedService {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}

// Step 2 : 查看汇编码
javap -c -s -v -l SynchronizedService.class


// Step 3 : 注意其中 3 和 13 以及 19
public void method();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String synchronized 代码块
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return

处理逻辑详情 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码// synchronized 源码分析 @ https://blog.csdn.net/javazejian/article/details/70768369  
// 具体的流程可以参考上面博客的具体分析, 此处仅总结 , 大佬们肝真好


// synchronized 代码块底层逻辑 ( monitorenter 和 monitorexit )
> synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,
其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置

Start : 当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权
Thread-1 : objectref.monitor = 0 --> 获取monitor --> 设置计数器值为 1
Thread-2 : 发现objectref.monitor = 0 --> 阻塞等待 --> Thread-1 执行 monitorexit ,
计数器归 0 --> Thread-2 正常流程获取获取monitor

注意点 :
编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令 ,
方法异常时通过异常处理器处理异常结束

synchronized 方法底层逻辑 (ACC_SYNCHRONIZED标识)

  • 方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。
  • JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。
  1. 方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置
    |- 如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。
  2. 在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor

异常处理 : 如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放

synchronized 内存级原理

1
2
java复制代码// 最后生成的汇编语言 
lock cmpxchg %r15, 0x16(%r10) 和 lock cmpxchg %r10, (%r11)
  • synchronized的底层操作含义是先对对象头的锁标志位用lock cmpxchg的方式设置成”锁住”状态
  • 释放锁时,在用lock cmpxchg的方式修改对象头的锁标志位为”释放”状态,写操作都立刻写回主内存。
  • JVM会进一步对synchronized时CAS失败的那些线程进行阻塞操作,这部分的逻辑没有体现在lock cmpxchg指令上,我猜想是通过某种信号量来实现的。
  • lock cmpxchg 指令前者保证了可见性和防止重排序,后者保证了操作的原子性。

1.3 synchronized 用法

1
2
3
4
java复制代码// 加锁方式 ,当前实例 ,当前class , 自定义object
> synchronized(this)
> synchronized(object)
> synchronized(class) 或者静态代码块

synchronized关键字最主要的三种使用方式:

  • 修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。
    • 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。
    • 所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
    • 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。
    • synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。

这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。

另外需要注意的是 尽量不要使用 synchronized(String a), 部分字符串常量会缓冲到常量池里面, 不过可以试试 new String(“a”)

1.4 synchronized 其他知识点

解释 : synchronized 提供了一种独占式的加锁方式 ,其添加和释放锁的方式由JVM实现
阻塞 : 当 synchronized 尝试获取锁的时候,获取不到锁,将会一直阻塞

谈谈 synchronized和ReenTrantLock 的区别

  • 两者都是可重入锁
  • synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API
  • ReenTrantLock 比 synchronized 增加了一些高级功能
    • 等待可中断;可实现公平锁;可实现选择性通知(锁可以绑定多个条件)

synchronized 与等待唤醒机制 (notify/notifyAll和wait)
等待唤醒机制需要处于synchronized代码块或者synchronized方法中 , 调用这几个方法前必须拿到当前对象的监视器monitor对象

synchronized 与 线程中断
线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用

1.5 多线程中的锁概念

1.5.1 锁按照等级分类

锁可以按照以下等级进行升级 : 偏向锁 -> 轻量级锁 -> 重量级锁 , 锁的升级是单向的

  • 偏向锁
    • 减少同一线程获取锁的代价 (在大多数情 况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得)
    • 如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程
    • 偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁
  • 轻量级锁
    • 对绝大部分的锁,在整个同步周期内都不存在竞争
    • 轻量级锁所适应的场景是线程交替执行同步块的场合
    • 如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁
  • 自旋锁
    • 假设等待后当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环
    • 在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起
    • 可以减少了线程上下文切换,但是增加了CPU消耗
  • 重量级锁

1.5.2 锁的操作

锁清除 :

Java虚拟机在 JIT编译时 (可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间

Java 常见的锁

  • synchronized 关键字 , 重量锁
  • ReentrantLock 重入锁
  • ReadWriteLock 读写锁

1.5.3 其他锁概念

内部锁 :

  • synchronized : 锁对象的引用 , 锁保护的代码块
  • 每个Java 对象都可以隐式地扮演一个用于同步的锁的角色 ,这些内置的锁被称为 内部锁 或 监视器锁 .

公平锁/非公平锁

  • 公平锁是指多线程按照申请锁的顺序来获取锁,非公平锁指多个线程获取锁的顺序不是按照申请锁的顺序,有可能造成优先级反转或者饥饿现象,
  • 非公平锁的优点在于吞吐量比公平锁大,ReentrantLock默认非公平锁,可通过构造函数选择公平锁,Synchronized是非公平锁。

可重入锁
可重入锁指在一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,ReentrantLock与Synchronized都是可重入的。

独享锁/共享锁
独享锁是指一个锁只能一个线程独有,共享锁指一个锁可被多个线程共享,对于ReadWriteLock,读锁是共享锁,写锁是独享所。

互斥锁/读写锁
独享锁/共享锁是一种广义的说法,互斥锁/读写锁是其具体实现。

乐观锁/悲观锁

  • 乐观锁与悲观锁是看待同步的角度不同,乐观锁认为对于同一个数据的修改操作,是不会有竞争的,会尝试更新,如果失败,不断重试。
  • 悲观锁与此相反,直接获取锁,之后再操作,最后释放锁。

分段锁

  • 分段锁是一种设计思想,通过将一个整体分割成小块,在每个小块上加锁,提高并发。

1.6 锁的转换过程

1
2
3
4
5
6
7
8
9
10
java复制代码对象头的变化可以看下图 , 说的很清楚了  @ https://www.cnblogs.com/jhxxb/p/10983788.html

// 之前知道 , 锁的状态改变是单向的 , 由 偏向锁 -> 轻量级锁 -> 重量级锁 ,我们分别捋一下

// 偏向锁 -> 偏向锁, 即重偏向操作
1 对象先偏向于某个线程, 执行完同步代码后 , 进入安全点时,若需要重偏向,会把类对象中 epoch 值增加
2 退出安全点后 , 当有线程需要尝试获取偏向锁时, 直接检查类实例对象中存储的 epoch 值与类对象中存储的 epoch 值是否相等, 如果不相等, 则说明该对象的偏向锁已经无效了, 可以尝试对此对象重新进行偏向操作。

// 偏向锁 -> 轻量级锁
1 当发现对象已被锁定 ,且 ThreadID 不是自己 , 转为 偏向锁 , 在该线程的栈帧中建立 Lock Record 空间

lock_satus.jpg

1.7 为什么锁会转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码// 这要从机制说起 , 每一种锁都有各自的特点

> 偏向锁
- 优点 : 无 CAS ,消耗少 , 性能高 , 可重入
- 缺点 : 锁竞争时撤销锁消耗高
- 场景 : 同一个线程执行同步代码

> 轻量级锁
- 优点 : 竞争的线程不会阻塞
- 缺点 : 轻量级锁未获取锁时会通过自旋获取 , 消耗资源
- 场景 : 线程交替执行同步块或者同步方法,追求响应时间,锁占用时间很短

> 重量级锁
- 优点 : 线程竞争不使用自旋 , 只会唤醒和等待
- 缺点 : 造成线程阻塞 , 锁的改变也消耗资源
- 场景 : 追求吞吐量,锁占用时间较长

// 针对不同的需求 , 选择合适的锁 , 达到业务目的

锁状态pg.jpg

1.8 Synchoized 源码

1
2
3
4
5
6
7
8
9
10
java复制代码
synchronized 是一个修饰符 , 我们需要从 C 的角度去看


Step 1 : 下载 OpenJDK 代码 https://blog.csdn.net/leisure_life/article/details/108367675
Step 2 : 根据代码索引 .c 文件



// TODO

1.9 Synchoized 用法

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
java复制代码public void operation(Integer check) {

/**
* 校验无锁情况
*/
public void functionShow(Integer check) {
logger.info("------> check is {} <-------", check);
if (check == 0) {
showNum = 100;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (check == 1) {
showNum = 200;
}
logger.info("------> check is Over {} :{}", check, showNum);
}

/**
* 同步方法 , 校验 synchronized 方法
*/
synchronized public void functionShowSynchronized(Integer check) {
logger.info("------> check is {} <-------", check);
if (check == 0) {
showNum = 100;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (check == 1) {
showNum = 200;
}
logger.info("------> check is Over synchronized {} :{}", check, showNum);
}

/**
* 校验 synchronized 代码块
*/
public void statementShowSynchronized(Integer check) {
logger.info("------> check is {} <-------", check);
synchronized (this) {
if (check == 0) {
showNum = 100;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (check == 1) {
showNum = 200;
}
}
logger.info("------> check is Over synchronized {} :{}", check, showNum);
}

// 校验 代码块以 Class 为对象
public void classShowSynchronized(Integer check) {
logger.info("check is {} <-------", check);
synchronized (CommonTO.class) {
if (check == 0) {
showNum = 100;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (check == 1) {
showNum = 200;
}
}
logger.info("check is Over synchronized {} :{}", check, showNum);
}

// 同步代码块 Object
public void objectShowSynchronized(Integer check) {
logger.info("check is {} <-------", check);
synchronized (lock) {
if (check == 0) {
showNum = 100;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (check == 1) {
showNum = 200;
}
}
logger.info("check is Over synchronized {} :{}", check, showNum);
}

// 同步代码块 Object
public void objectStringShowSynchronized(Integer check) {
logger.info("check is {} <-------", check);
synchronized (lock2) {
if (check == 0) {
showNum = 100;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (check == 1) {
showNum = 200;
}
}
logger.info("check is Over synchronized {} :{}", check, showNum);
}

总结

逐步更新 , 逐步完善

更新日记

  • V20210727 : 补充细节点 , 优化内容
  • V20210813 : 补充深入细节 , Java 并发编程的艺术
  • V20210814 : 补充汇编细节

参考文档

《深入浅出 Java 多线程》

《Java并发编程的艺术》

本文转载自: 掘金

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

Java多线程(一) 快查手册

发表于 2021-02-27
1
2
3
4
5
!复制代码其实多线程之前发了一个几万字的集合 , 但是感觉效果并不好 , 涉及的知识点过多 , 细节也不够碎 .
所以后面会一部分一部分的理出来 , 尽量做到清楚详细 .

我写笔记的喜欢会在开头写上一个类似于目录的快查手册 , 它比目录会更加详细 , 也足够精简 , 这一篇即是这个目的
这篇现在别看很简单 , 以后会越来越大

友情链接

  • Java JVM 篇

多线程篇
Java多线程 : 细说 synchronized

Java 多线程: 漫谈 Volatile

Java 多线程 : 漫谈 CAS

Java 多线程 : 阻塞队列 没啥好说的

快查手册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码// 乐观锁/悲观锁
java悲观锁:synchronized、lock的实现类
java乐观锁:乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

// 独享锁/共享锁
synchronized、ReentrantLock是独享锁。
ReadWriteLock其读锁是共享锁,其写锁是独享锁。

// 可重入锁
synchronized、ReentrantLock

// 公平锁/非公平锁
synchronized是非公平锁
ReetrantLock(通过构造函数指定该锁是否是公平锁,默认是非公平锁)

JVM 参数变量

1
2
3
sql复制代码> User user = new User()
- new User 会创建到 Heap 中
- User user 为对象得引用 ,放在方法栈中

JVM 多线程的变量同步

本文转载自: 掘金

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

15个好用到哭的python库,太牛了!

发表于 2021-02-27

为什么我喜欢Python?对于初学者来说,这是一种简单易学的编程语言,另一个原因:大量开箱即用的第三方库,正是23万个由用户提供的软件包使得Python真正强大和流行。

在本文中,我挑选了15个最有用的软件包,介绍它们的功能和特点。

  1. Dash

Dash是比较新的软件包,它是用纯Python构建数据可视化app的理想选择,因此特别适合处理数据的任何人。Dash是Flask,Plotly.js和React.js的混合体。

  1. Pygame

Pygame是SDL多媒体库的Python装饰器,SDL(Simple DirectMedia Layer)是一个跨平台开发库,旨在提供对以下内容的低级接口:

  • 音频
  • 键盘
  • 鼠标
  • 游戏杆
  • 基于OpenGL和Direct3D的图形硬件

Pygame具有高度的可移植性,几乎可以在所有平台和操作系统上运行。尽管它具有完善的游戏引擎,但您也可以使用此库直接从Python脚本播放MP3文件。

  1. Pillow

Pillow专门用于处理图像,您可以使用该库创建缩略图,在文件格式之间转换,旋转,应用滤镜,显示图像等等。如果您需要对许多图像执行批量操作,这是理想的选择。

为了快速了解它,看以下代码示例(加载并渲染图片):

  1. Colorama

Colorama允许你在终端使用颜色,非常适合Python脚本,文档简短而有趣,可以在Colorama PyPI页面上找到。

  1. JmesPath

在Python中使用JSON非常容易,因为JSON在Python字典上的映射非常好。此外,Python带有自己出色的json库,用于解析和创建JSON。对我来说,这是它最好的功能之一。如果我需要使用JSON,可以考虑使用Python。

JMESPath使Python处理JSON更加容易,它允许您明确的地指定如何从JSON文档中提取元素。以下是一些基本示例,可让您对它的功能有所了解:

  1. Requests

Requests建立在世界上下载量最大的Python库urllib3上,它令Web请求变得非常简单,功能强大且用途广泛。

以下代码示例说明requests的使用是多么简单。

Requests可以完成您能想到的所有高级工作,例如:

  • 认证
  • 使用cookie
  • 执行POST,PUT,DELETE等
  • 使用自定义证书
  • 使用会话Session
  • 使用代理
  1. Simplejson

Python中的本地json模块有什么问题?没有!实际上,Python的json是simplejson。意思是,Python采用了simplejson的一个版本,并将其合并到每个发行版中。但是使用simplejson具有一些优点:

  • 它适用于更多Python版本。
  • 它比Python随附的版本更新频率更高。
  • 它具有用C编写的(可选)部分,因此非常快速。

由于这些事实,您经常会在使用JSON的脚本中看到以下内容:

我将只使用默认的json,除非您特别需要:

  • 速度
  • 标准库中没有的东西

Simplejson比json快很多,因为它用C实现一些关键部分。除非您正在处理数百万个JSON文件,否则您不会对这种速度感兴趣。

  1. Emoji

Emoji库非常有意思,但并非每个人都喜欢表情包,分析视角媒体数据时,Emoji包非常有用。

以下是简单的代码示例:

  1. Chardet

您可以使用chardet模块来检测文件或数据流的字符集。例如,这在分析大量随机文本时很有用。但是,当您不知道字符集是什么时,也可以在处理远程下载的数据时使用它。

  1. Python-dateutil

python-dateutil模块提供了对标准datetime模块的强大扩展。我的经验是,常规的Python日期时间功能在哪里结束,而python-dateutil就出现了。

您可以使用此库做很多很棒的事情。我将这些示例限制为我发现特别有用的示例:模糊分析日志文件中的日期,例如:

有关更多功能,请参见完整文档,例如:

  • 计算相对增量(下个月,明年,下周一,该月的最后一周等)和两个给定日期对象之间的相对增量。
  • 使用iCalendar规范的超集,根据重复规则计算日期。
  • tzfile文件(/ etc / localtime,/ usr / share / zoneinfo等)的时区(tzinfo)实现,TZ环境字符串(所有已知格式),iCalendar格式文件,给定范围(在相对增量的帮助下),本地计算机 时区,固定偏移时区,UTC时区和基于Windows注册表的时区。
  • 基于奥尔森数据库的内部最新世界时区信息。
  • 使用Western,Orthodox或Julian算法计算任意一年的复活节周日日期。
  1. 进度条:progress和tqdm

这里有点作弊,因为这是两个包,但忽略其中之一是不公平的。

您可以创建自己的进度条,这也许很有趣,但是使用progress或tqdm程序包更快,更不容易出错。

progress

借助这个软件包,您可以轻松创建进度条:

tqdm

tqdm的功能大致相同,但似乎是最新的。首先以gif动画形式进行一些演示:

  1. IPython

我确定您知道Python的交互式外壳,这是运行Python的好方法。但是您也知道IPython shell吗?如果您经常使用交互式外壳程序,但您不了解IPython,则应该检查一下!

增强的IPython shell提供的一些功能包括:

  • 全面的对象自省。
  • 输入历史记录,跨会话持续存在。
  • 在具有自动生成的引用的会话期间缓存输出结果。
  • 制表符补全,默认情况下支持python变量和关键字,文件名和函数关键字的补全。
  • “魔术”命令,用于控制环境并执行许多与IPython或操作系统相关的任务。
  • 会话记录和重新加载。
  • 对pdb调试器和Python分析器的集成访问。
  • IPython的一个鲜为人知的功能:它的体系结构还允许并行和分布式计算。

IPython是Jupyter Notebook的核心,它是一个开放源代码Web应用程序,可让您创建和共享包含实时代码,方程式,可视化效果和叙述文本的文档。

  1. Homeassistant

我喜欢家庭自动化。这对我来说是一种嗜好,但我至今仍对此深表歉意,因为它现在控制着我们房屋的大部分。我使用Home Assistant将房子中的所有系统捆绑在一起。尽管它确实是一个完整的应用程序,但是您也可以将其安装为Python PyPI软件包。

  • 我们的大多数灯具都是自动化的,百叶窗也是如此。
  • 我监视我们的天然气用量,电力用量和产量(太阳能电池板)。
  • 我可以跟踪大多数电话的位置,并在进入一个区域时开始操作,例如当我回家时打开车库灯。
  • 它还可以控制我们所有的娱乐系统,例如三星电视和Sonos扬声器。
  • 它能够自动发现网络上的大多数设备,因此上手起来非常容易。

我已经每天使用Home Assistant已有3年了,它仍处于测试阶段,但这是我尝试过的所有平台中最好的平台。它能够集成和控制各种设备和协议,并且都是免费和开源的。

如果您有兴趣将房屋自动化,请确保有机会!如果您想了解更多,请访问他们的官方网站。如果可以,请将其安装在Raspberry Pi上。到目前为止,这是最简单,最安全的入门方法。我将其安装在Docker容器内功能更强大的服务器上。

  1. Flask

Flask是我的入门库,用于创建快速的Web服务或简单的网站。这是一个微框架,这意味着Flask旨在使核心保持简单但可扩展。有700多个官方和社区扩展。

如果您知道自己将开发一个大型的Web应用程序,则可能需要研究一个更完整的框架。该类别中最受欢迎的是Django。

  1. BeautifulSoup

如果您从网站上提取了一些HTML,则需要对其进行解析以获取实际所需的内容。Beautiful Soup是一个Python库,用于从HTML和XML文件中提取数据。它提供了导航,搜索和修改解析树的简单方法。它非常强大,即使损坏了,也能够处理各种HTML。相信我,HTML经常被破坏,所以这是一个非常强大的功能。

它的一些主要功能:

  • Beautiful Soup会自动将传入文档转换为Unicode,将传出文档转换为UTF-8。您无需考虑编码。
  • Beautiful Soup位于流行的Python解析器(如lxml和html5lib)的顶部,使您可以尝试不同的解析策略或提高灵活性。
  • BeautifulSoup会解析您提供的任何内容,并为您做遍历树的工作。您可以将其告诉“查找所有链接”,或“查找带有粗体的表格标题,然后给我该文字。”
  • 好了,今天的分享就到这,如果你对Python感兴趣,欢迎加入我们【python学习交流裙】,免费领取学习资料和源码。

本文转载自: 掘金

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

1…714715716…956

开发者博客

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