Wednesday, April 21, 2021

V2EX - 技术

V2EX - 技术


Nodejs 终于刷到了版本 16(LTS)

Posted: 21 Apr 2021 04:00 AM PDT

终于比 java 的版本号还高了。

Android 项目多个 Jar 包有命名空间冲突,有办法解决吗?

Posted: 21 Apr 2021 02:56 AM PDT

简单来说,同一个芯片的 SDK 被两个硬件组装厂家做了封装,我们的项目添加两个 Module 分别引用两个厂家封装后的 SDK(jar 、so),然后他们引用的芯片 SDK 就出现命名空间冲突了,而且他们用的芯片 SDK 版本还不一样,让厂家修改也不现实。
现在就一直报错几个 Module 引用的 Jar 文件中有相同的内容,
尝试把 Module 编译成 dex 动态加载,可是 dex 不能把 jar 和 so 打包进去,
有没有大佬遇到过这种问题?望赐教

使用 vue-cli 创建的项目删掉 node_modules 之后再 npm i 会提示找不到 Python ,为什么 vue create 的时候不会有问题

Posted: 21 Apr 2021 02:56 AM PDT

dart-sass 会用到 python

android studio build 报错,貌似是 gradle 版本问题,网上搜了方法没解决,有大佬帮看看吗?

Posted: 21 Apr 2021 02:31 AM PDT

Task :prepareKotlinBuildScriptModel UP-TO-DATE File /root/.android/repositories.cfg could not be loaded. Checking the license for package Android SDK Build-Tools 29.0.3 in /root/Android/Sdk/licenses Warning: License for package Android SDK Build-Tools 29.0.3 not accepted. Checking the license for package Android SDK Platform 29 in /root/Android/Sdk/licenses Warning: License for package Android SDK Platform 29 not accepted.

Deprecated Gradle features were used in this build, making it incompatible with Gradle 7.0. Use '--warning-mode all' to show the individual deprecation warnings. See https://docs.gradle.org/6.5/userguide/command_line_interface.html#sec:command_line_warnings

BUILD SUCCESSFUL in 16s

请问,自己独立的使用 Springboot+Mybatis+vue 写个博客网站,能找到 Java 开发的工作不. 害怕被面试官乱杀

Posted: 21 Apr 2021 02:22 AM PDT

家人们, Python 在爬 https 的时候会出现一个少见的问题,是本地环境的问题吗?

Posted: 21 Apr 2021 02:05 AM PDT

Exception has occurred: URLError
<urlopen error unknown url type: https>
File "D:\pachong\pachong.py", line 2, in <module>
urllib.request.urlopen('https://baidu.com')

报错代码如上

在爬取 http 的网站是没有这个问题的,一到 https 就会报错。
谷歌了很多,说需要配置这些:SSL 库也是有的(但是无法导入),openssl 库。 无果。
大家怎么看?

微信 Go SDK(支付、公众号、小程序)

Posted: 21 Apr 2021 01:37 AM PDT

gochat

golang GitHub release pkg.go.dev MIT license

微信 Go SDK (支付、公众号、小程序)

目录 对应 功能
/mch 微信支付(普通商户直连模式) 下单、支付、退款、查询、委托代扣、企业付款、企业红包 等
/oa 微信公众号( Official Accounts ) 网页授权、用户管理、模板消息、菜单管理、客服、事件消息 等
/mp 微信小程序( Mini Program ) 小程序授权、数据解密、二维码、消息发送、事件消息 等

获取

go get -u github.com/shenghui0779/gochat 

文档

说明

  • 支持 Go1.11+
  • 注意:因 access_token 每日获取次数有限且含有效期,故服务端应妥善保存 access_token 并定时刷新
  • 配合 yiigo 使用,可以更方便的操作 MySQLMongoDBRedis

Enjoy 😊

如何模拟微信浏览器阅读文章

Posted: 21 Apr 2021 01:32 AM PDT

不是简单模拟微信浏览器,这个设置一下 UA 就可以了,而是很多网站通过微信会员收费,只有通过微信公众号入口进入访问才能显示正确内容,请问一下这个技术实现细节?如何能够欺骗网站?

我想到的:
1 )微信 UA,这个容易模仿
2 ) http header:refer 网站,这个也可以吧
3 )获取订阅者 openid,这个假如我知道 openid,能否欺骗?

如何从一个图片上找到指定文字的位置

Posted: 21 Apr 2021 12:57 AM PDT

请教下大家,我之前处理过,加载图片,然后 OCR 识别出图片指定位置的文字,现在的情况不太一样,图片上文字的位置不固定,但是我知道上面文字的内容,当然也有其他文字,现在我想找到指定文字的位置,不知道可行否?

�� Go 轻量级开发通用库 ������

Posted: 21 Apr 2021 12:44 AM PDT

yiigo

golang GitHub release pkg.go.dev MIT license

Go 轻量级开发通用库

Features

Requirements

Go1.15+

Installation

go get -u github.com/shenghui0779/yiigo 

Usage

Config

  • yiigo.toml
[app] env = "dev" # dev | beta | prod debug = true  [db]      [db.default]     driver = "mysql" # mysql | postgres | sqlite3     dsn = "username:password@tcp(localhost:3306)/dbname?timeout=10s&charset=utf8mb4&collation=utf8mb4_general_ci&parseTime=True&loc=Local" # mysql     # dsn = "host=localhost port=5432 user=root password=secret dbname=test connect_timeout=10 sslmode=disable" # postgres     # dsn = "file::memory:?cache=shared" # sqlite3     max_open_conns = 20     max_idle_conns = 10     conn_max_idle_time = 60 # 秒     conn_max_lifetime = 60 # 秒  [mongo]      [mongo.default]     # 参考: https://docs.mongodb.com/manual/reference/connection-string     dsn = "mongodb://localhost:27017/?connectTimeoutMS=10000&minPoolSize=10&maxPoolSize=20&maxIdleTimeMS=60000&readPreference=primary"  [redis]      [redis.default]     address = "127.0.0.1:6379"     password = ""     database = 0     connect_timeout = 10 # 秒     read_timeout = 10 # 秒     write_timeout = 10 # 秒     pool_size = 10     pool_limit = 20     idle_timeout = 60 # 秒     wait_timeout = 10 # 秒     prefill_parallelism = 0 # 预填充连接数  [nsq] lookupd = ["127.0.0.1:4161"] nsqd = "127.0.0.1:4150"  [email]      [email.default]     host = "smtp.exmail.qq.com"     port = 25     username = ""     password = ""  [log]      [log.default]     path = "app.log"     max_size = 500     max_age = 0     max_backups = 0     compress = true  # 自定义配置  [foo] amount = 100 ports = [80, 81, 82] weight = 50.6 prices = [23.5, 46.7, 45.9] hosts = ["127.0.0.1", "192.168.1.1", "192.168.1.80"] birthday = "2019-07-12 13:03:19" 
  • usage
yiigo.Env("app.env").String() yiigo.Env("app.debug").Bool() yiigo.Env("foo.amount").Int() yiigo.Env("foo.ports").Ints() yiigo.Env("foo.weight").Float() yiigo.Env("foo.price").Floats() yiigo.Env("foo.hosts").Strings() yiigo.Env("foo.birthday").Time("2006-01-02 15:04:05") 

MySQL

// default db yiigo.DB().Get(&User{}, "SELECT * FROM user WHERE id = ?", 1)  // other db yiigo.DB("other").Get(&User{}, "SELECT * FROM user WHERE id = ?", 1) 

ORM(ent)

import "<your_project>/ent"  // default driver client := ent.NewClient(ent.Driver(yiigo.EntDriver()))  // other driver client := ent.NewClient(ent.Driver(yiigo.EntDriver("other"))) 

MongoDB

// default mongodb yiigo.Mongo().Database("test").Collection("numbers").InsertOne(context.Background(), bson.M{"name": "pi", "value": 3.14159})  // other mongodb yiigo.Mongo("other").Database("test").Collection("numbers").InsertOne(context.Background(), bson.M{"name": "pi", "value": 3.14159}) 

Redis

// default redis conn, err := yiigo.Redis().Get(context.Background())  if err != nil {     log.Fatal(err) }  defer yiigo.Redis().Put(conn)  conn.Do("SET", "test_key", "hello world")  // other redis conn, err := yiigo.Redis("other").Get(context.Background())  if err != nil {     log.Fatal(err) }  defer yiigo.Redis("other").Put(conn)  conn.Do("SET", "test_key", "hello world") 

HTTP

// default client yiigo.HTTPGet(context.Background(), "URL", yiigo.WithHTTPTimeout(5*time.Second))  // new client c := yiigo.NewHTTPClient(*http.Client) c.Get(context.Background(), "URL", yiigo.WithHTTPTimeout(5*time.Second)) 

Logger

// default logger yiigo.Logger().Info("hello world")  // other logger yiigo.Logger("other").Info("hello world") 

SQL Builder

😊 为不想手写 SQL 的你生成 SQL 语句,用于 sqlx 的相关方法;

⚠️ 作为辅助方法,目前支持的特性有限,复杂的 SQL (如:子查询等)还需自己手写

builder := yiigo.NewSQLBuilder(yiigo.MySQL) 
  • Query
builder.Wrap(     yiigo.Table("user"),     yiigo.Where("id = ?", 1), ).ToQuery() // SELECT * FROM user WHERE id = ? // [1]  builder.Wrap(     yiigo.Table("user"),     yiigo.Where("name = ? AND age > ?", "shenghui0779", 20), ).ToQuery() // SELECT * FROM user WHERE name = ? AND age > ? // [shenghui0779 20]  builder.Wrap(     yiigo.Table("user"),     yiigo.WhereIn("age IN (?)", []int{20, 30}), ).ToQuery() // SELECT * FROM user WHERE age IN (?, ?) // [20 30]  builder.Wrap(     yiigo.Table("user"),     yiigo.Select("id", "name", "age"),     yiigo.Where("id = ?", 1), ).ToQuery() // SELECT id, name, age FROM user WHERE id = ? // [1]  builder.Wrap(     yiigo.Table("user"),     yiigo.Distinct("name"),     yiigo.Where("id = ?", 1), ).ToQuery() // SELECT DISTINCT name FROM user WHERE id = ? // [1]  builder.Wrap(     yiigo.Table("user"),     yiigo.LeftJoin("address", "user.id = address.user_id"),     yiigo.Where("user.id = ?", 1), ).ToQuery() // SELECT * FROM user LEFT JOIN address ON user.id = address.user_id WHERE user.id = ? // [1]  builder.Wrap(     yiigo.Table("address"),     yiigo.Select("user_id", "COUNT(*) AS total"),     yiigo.GroupBy("user_id"),     yiigo.Having("user_id = ?", 1), ).ToQuery() // SELECT user_id, COUNT(*) AS total FROM address GROUP BY user_id HAVING user_id = ? // [1]  builder.Wrap(     yiigo.Table("user"),     yiigo.Where("age > ?", 20),     yiigo.OrderBy("age ASC", "id DESC"),     yiigo.Offset(5),     yiigo.Limit(10), ).ToQuery() // SELECT * FROM user WHERE age > ? ORDER BY age ASC, id DESC OFFSET 5 LIMIT 10 // [20]  wrap1 := builder.Wrap(     Table("user_1"),     Where("id = ?", 2), )  builder.Wrap(     Table("user_0"),     Where("id = ?", 1),     Union(wrap1), ).ToQuery() // (SELECT * FROM user_0 WHERE id = ?) UNION (SELECT * FROM user_1 WHERE id = ?) // [1, 2]  builder.Wrap(     Table("user_0"),     Where("id = ?", 1),     UnionAll(wrap1), ).ToQuery() // (SELECT * FROM user_0 WHERE id = ?) UNION ALL (SELECT * FROM user_1 WHERE id = ?) // [1, 2]  builder.Wrap(     Table("user_0"),     WhereIn("age IN (?)", []int{10, 20}),     Limit(5),     Union(         builder.Wrap(             Table("user_1"),             Where("age IN (?)", []int{30, 40}),             Limit(5),         ),     ), ).ToQuery() // (SELECT * FROM user_0 WHERE age IN (?, ?) LIMIT ?) UNION (SELECT * FROM user_1 WHERE age IN (?, ?) LIMIT ?) // [10, 20, 5, 30, 40, 5]  builder.Wrap(Table("user")).ToTruncate() // TRUNCATE user 
  • Insert
type User struct {     ID     int    `db:"-"`     Name   string `db:"name"`     Age    int    `db:"age"`     Phone  string `db:"phone,omitempty"` }  builder.Wrap(Table("user")).ToInsert(&User{     Name: "yiigo",     Age:  29, }) // INSERT INTO user (name, age) VALUES (?, ?) // [yiigo 29]  builder.Wrap(yiigo.Table("user")).ToInsert(yiigo.X{     "name": "yiigo",     "age":  29, }) // INSERT INTO user (name, age) VALUES (?, ?) // [yiigo 29] 
  • Batch Insert
type User struct {     ID     int    `db:"-"`     Name   string `db:"name"`     Age    int    `db:"age"`     Phone  string `db:"phone,omitempty"` }  builder.Wrap(Table("user")).ToBatchInsert([]*User{     {         Name: "shenghui0779",         Age:  20,     },     {         Name: "yiigo",         Age:  29,     }, }) // INSERT INTO user (name, age) VALUES (?, ?), (?, ?) // [shenghui0779 20 yiigo 29]  builder.Wrap(yiigo.Table("user")).ToBatchInsert([]yiigo.X{     {         "name": "shenghui0779",         "age":  20,     },     {         "name": "yiigo",         "age":  29,     }, }) // INSERT INTO user (name, age) VALUES (?, ?), (?, ?) // [shenghui0779 20 yiigo 29] 
  • Update
type User struct {     Name   string `db:"name"`     Age    int    `db:"age"`     Phone  string `db:"phone,omitempty"` }  builder.Wrap(     Table("user"),     Where("id = ?", 1), ).ToUpdate(&User{     Name: "yiigo",     Age:  29, }) // "UPDATE user SET name = ?, age = ? WHERE id = ?" // [yiigo 29 1]  builder.Wrap(     yiigo.Table("user"),     yiigo.Where("id = ?", 1), ).ToUpdate(yiigo.X{     "name": "yiigo",     "age":  29, }) // UPDATE user SET name = ?, age = ? WHERE id = ? // [yiigo 29 1]  builder.Wrap(     yiigo.Table("product"),     yiigo.Where("id = ?", 1), ).ToUpdate(yiigo.X{     "price": yiigo.Clause("price * ? + ?", 2, 100), }) // UPDATE product SET price = price * ? + ? WHERE id = ? // [2 100 1] 
  • Delete
builder.Wrap(     yiigo.Table("user"),     yiigo.Where("id = ?", 1), ).ToDelete() // DELETE FROM user WHERE id = ? // [1] 

Documentation

Enjoy 😊

M1 芯片通过 PD 安装 CentOS ARM 问题求助

Posted: 21 Apr 2021 12:42 AM PDT

求各位老大指导一下,尝试了一下用 M1 芯片的 PD 安装 CentOS ARM 的时候,总是安装不上,也没有任何提示,返回跳转安装界面,不知道是咋回事儿,有了解的大佬求指导,谢谢!

不太明白 writer.write() 和 writer.write() await writer.drain()有什么区别

Posted: 21 Apr 2021 12:06 AM PDT

文档:
https://docs.python.org/zh-tw/3/library/asyncio-stream.html#streamwriter

不明白为什么要这么写,
stream.write(data)
await stream.drain()
个人理解 stream.write(data)本来就是非阻塞的,或者是什么场景下用 await stream.drain()呢

大家在学新东西的时候状态是怎样的,最近感觉自己忘的也太快了点

Posted: 21 Apr 2021 12:05 AM PDT

最近在学一些 JavaScrip Es6 的知识,主要是看阮一峰的文档。在看的时候基本上内容还是能搞懂的,但是有时候因为别的事隔了一天没看接着往下看的时候感觉之前的内容就已经忘了不少了。

在看 Promise 、generator 和 iterator 那几部分的时候尤为明显,有些概念交叉的时候经常得往回重新看看,仿佛和没看过似的。。。头疼

github 上 android 项目,有 CI 工具可以自动编译 apk 并上传到 release 里面么

Posted: 21 Apr 2021 12:03 AM PDT

自己的一个小安卓项目,想在 github 上集成 ci 并在 commit 时自动编译并发布 apk 。有什么可靠的方法么?

在 github marketplace 上搜到了一些,但是不知道哪个坑比较少。有人做过类似的么?

github codespaces 有没人试过?

Posted: 21 Apr 2021 12:00 AM PDT

我申请了试用好久都没通过,
想着有时候需要在 github 上直接编辑一些简单修改,但 github 网页修改一次只能一个文件就很坑,
这个 github codespaces 看起来应该能很好的做到在线修改多文件然后提交了吧,

Java on Visual Studio Code 的更新 – 2021 年 3 月

Posted: 20 Apr 2021 11:47 PM PDT

欢迎来到 Java 的 VS Code 更新。在过去的几个月中,我们的工程师一直在专注于一些非常重要的工作。现在,是时候揭开面纱了,开始吧。

类型层次结构(Type hierarchy)

VS Code 已经支持 Java 的调用层次结构(Call Hierarchy),那么类型层次结构呢?我们与 Red Hat 一起非常高兴地宣布,由 Red Hat 发布的最新版本的Java 语言支持扩展已经支持浏览类型层次结构。

该功能使您可以在类,超类型或子类型视图中查看类型层次结构。

类型层次结构

移动文件时的包重构

我们知道很多开发人员都在等待此功能,当.java 文件从一个文件夹移动到另一个文件夹时,VS Code 可以自动更新包声明和导入语句。Red Hat 发布的最新版本的Java 语言支持扩展现在支持此功能。除了自动更新之外,该功能还允许您预览和撤消包更改。

移动文件时的包重构

类路径配置(Classpath configuration)

管理源代码,输出,运行时和库的路径是一项重要的项目管理任务,几乎每个 Java 开发人员都会执行。对于使用诸如 Maven 或 Gradle 之类的构建工具的人,这些工具允许通过其配置文件管理这些路径。但是,对于那些不使用构建工具的人,尤其是像学生,他们需要依赖 IDE /编辑器工具进行管理。为满足此需求,我们发布了类路径配置功能。

启动配置向导

您可以从" JAVA PROJECTS"资源管理器中启动配置向导,也可以单击 Ctrl+Shift+P 打开命令选项板,然后在选项板上键入" configure classpath"。

启动配置向导

此功能作为Java Extension Pack的一部分发布, 请确保您已经安装了最新版本。

Maven 生命周期支持(Maven lifecycle)

最新的Maven for Java 扩展支持 Maven 生命周期。现在,您可以通过单击阶段(phase)旁边的运行图标,直接从 Maven 资源管理器视图中执行常见的生命周期阶段。

[

更多信息

请不要犹豫,尝试一下!您的反馈和建议对我们非常重要,将有助于将来塑造我们的产品。

InfluxDB (InfluxQL) 中如何按 tag 计算总和?

Posted: 20 Apr 2021 11:42 PM PDT

假定有一个用于统计流量的表 traffic,格式如下

|  ts  |  field  |         tags        | +------+---------+---------------------+ | time | tx | rx | device_id | conn_id | +------+----+----+-----------+---------+ 

原始数据每秒提交一次,tx rx 是网络连接的累计字节数。目前已经实现了降采样和历史数据清理

alter retention policy autogen on mydb duration 3d shard duration 1d create retention policy rp_10s on mydb duration 3d replication 1 create retention policy rp_1m  on mydb duration inf replication 1 create retention policy rp_10m on mydb duration inf replication 1 create retention policy rp_1h  on mydb duration inf replication 1 create retention policy rp_1d  on mydb duration inf replication 1  create continuous query cq_10s on mydb begin     select max(tx), max(rx) into rp_10s.traffic from traffic group by time(10s), * end  create continuous query cq_1m  on mydb begin     select max(tx), max(rx) into rp_1m.traffic  from traffic group by time(1m),  * end  create continuous query cq_10m on mydb begin     select max(tx), max(rx) into rp_10m.traffic from traffic group by time(10m), * end  create continuous query cq_1h  on mydb begin     select max(tx), max(rx) into rp_1h.traffic  from traffic group by time(1h),  * end  create continuous query cq_1d  on mydb begin     select max(tx), max(rx) into rp_1d.traffic  from traffic group by time(1d),  * end 

现在想要统计每个用户的总字节数,累加后写入到另一个 device_traffic 表,请问要如何用 continuous query 实现呢?

求 mac vscode svn 插件推荐?

Posted: 20 Apr 2021 11:34 PM PDT

目前没找到好用的 vscode 插件来管理 svn,只能切到 Cornerstone 进行操作。有 vscode 插件推荐吗?

Redux 源码专精视频课 [免费完整版]

Posted: 20 Apr 2021 11:18 PM PDT

我之前不是写过《面试官叫我手写 Redux 》系列文章嘛,现在我已将其录制成了视频课程!

课程名为《 Redux 源码专精》( 17 集完整版),现在全部免费观看!

目录

  1. 全局 state 的读写
  2. reducer 的来历
  3. dispatch 的来历
  4. connect 的来历
  5. 利用 connect 减少 render
  6. Redux 乍现
  7. connect 支持 selector
  8. 精准渲染
  9. mapDispatchToProps
  10. connect 的意义
  11. 封装 Provider 和 createStore
  12. Redux ReactRedux 各概念总结
  13. API 封装技巧
  14. 让 Redux 支持函数 Action
  15. 让 Redux 支持 Promise Action
  16. 阅读 redux-thunk 和 redux-promise 源码
  17. 答疑与作业

在哪里看?

https://www.bilibili.com/video/BV1254y1L7UP

制作不易,只求一键三连!

看了有什么收获

没什么很大的收获,也就是在面试的时候可以装装 X,问 Redux/redux-thunk/redux-promise 的时候直接说「我自己写过一个」。

SpringSecurity 的无权访问异常理器 AccessDeniedHandler 与统一异常处理器 DefaultHandlerExceptionResolver 冲突

Posted: 20 Apr 2021 10:57 PM PDT

项目使用 SpringSecurity 进行 API 权限控制,在项目中实现了 AccessDeniedHandler 接口用于被拒绝时的异常处理:

@Slf4j @Component public class MyAccessDeniedHandler implements AccessDeniedHandler {     @Override     public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {         log.info("AccessDeniedHandler 异常....."); //打印被拒绝日志         httpServletResponse.setContentType("text/json;charset=utf-8");         httpServletResponse.getWriter().write(JSON.toJSONString(ResultGenerator.noPermission()));     }  } 

并且在 WebSecurityConfigurerAdapter 的 http 配置中配置了异常处理:

@Bean public AccessDeniedHandler getAccessDeniedHandler() { 	return new MyAccessDeniedHandler(); }  @Override protected void configure(HttpSecurity http) throws Exception { 	// .... 	 	//异常处理 	http.exceptionHandling().accessDeniedHandler(getAccessDeniedHandler()); 	 	// ... } 

项目中还是用了统一异常处理器 DefaultHandlerExceptionResolver:

@Slf4j @Component @ControllerAdvice public class MyGlobalExceptionHandler extends DefaultHandlerExceptionResolver {      @ResponseBody     @ExceptionHandler(Exception.class)     public R defaultErrorHandler(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) { 		 	e.printStackTrace(); //打印堆栈         log.info("DefaultHandlerExceptionResolver 统一异常处理器...");//打印异常日志 		     } } 

问题来了,当无权访问时,并不是执行 MyAccessDeniedHandler (尽管它已经在 SpringSecurity 中已经注册异常处理器),这个异常始终会在 MyGlobalExceptionHandler 中处理,打印了堆栈信息和异常日志:

2021-04-21 13:52:30.226  INFO 32684 --- [io-18000-exec-2] c.d.m.s.TokenAuthenticationFilter        : TokenAuthenticationFilter...token = d72b29cbaa074cba99377cec73999575 2021-04-21 13:52:30.235  INFO 32684 --- [io-18000-exec-2] c.d.mp.support.MyGlobalExceptionHandler  : DefaultHandlerExceptionResolver 统一异常处理器... 2021-04-21 13:52:30.235  INFO 32684 --- [io-18000-exec-2] c.d.mp.support.MyGlobalExceptionHandler  : 无权限访问...http://127.0.0.1:18000/test/hello  R(code=403, data=null, message=没有权限访问, time=1618984350235) org.springframework.security.access.AccessDeniedException: 不允许访问 	at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:73) 	at org.springframework.security.access.intercept.AbstractSecurityInterceptor.attemptAuthorization(AbstractSecurityInterceptor.java:238) 	at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:208) 	at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:58) 	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) 	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) 	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692) 	at com.dkdy.mp.controller.business.TestController$$EnhancerBySpringCGLIB$$fe1a52b5.hello(<generated>) 	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 	at java.lang.reflect.Method.invoke(Method.java:498) 	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197) 	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141) 	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106) 	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:894) 	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) 	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) 	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1060) 	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:962) 	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) 	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) 	at javax.servlet.http.HttpServlet.service(HttpServlet.java:626) 	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) 	at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) 	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) 	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) 	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) 	.............. 

如何让无权访问的异常被 MyAccessDeniedHandler 捕获处理,而不是让统一异常处理器来处理 SpringSecurity 的无权访问异常呢?

一键管理你的 Linux 环境

Posted: 20 Apr 2021 09:51 PM PDT

https://github.com/wenshunbiao/docker

好用给个 star 哦!

该项目主要是为了让你更加方便的使用 docker 服务,目前本人在开发环境,生产环境都在使用中,使用该服务,将使你避免重复搭建环境的烦恼,以及带来集中式,结构式管理目录的便捷。

刚开始可以使用虚拟机尝试体验。

如何能检测 Nginx 实际配置变更和配置变更备注的等价性?

Posted: 20 Apr 2021 09:34 PM PDT

我们的 nginx 变更平台当前阶段最多通过 ansible 把 nginx.conf 拷贝到目标机器的临时文件夹上,然后用 nginx -t 验证一下配置,这样可以解决配置语法的问题。但是如果比如变更备注写的减少某个 upstream 的参与负载机器,结果有个兄弟不小心把配置文件该 upstream 机器全都注释掉了,就变成了 0 台机器了。但是这样可以通过 nginx 的语法检查。类似的这种例子还很多,比如 server_name 域名少打一位等等。我们现在 nginx 配置变更就相当于直接修改 http 块的配置,整个 http 里面的内容都直接编辑,然后通过模板渲染一些固定参数后推送,并没有把 nginx 的所有配置表单化,而且变更备注也没有做成表单化,这样带来的问题是,没法验证实际变更和备注里面的逻辑的等价性。但是把 nginx 配置完全表单化难度也不小,因为 nginx 配置文件里面也可以写 lua 脚本什么的,变更备注表单话也是比较复杂的,因为能变更的种类真的太多太多。但是如果不表单化想检查逻辑可能就要用 NLP 等技术去解析语义了,有点弄复杂了。大家这块有啥好的方法没?

适合学习的 Go 语言开源项目、书籍(附学习路线图)

Posted: 20 Apr 2021 07:57 PM PDT

Go 新手,入门搜了一些资料,总结一下

适合学习的 Go 语言开源项目、书籍(附学习路线图)

v 友自取,欢迎补充

上次发得为 PHP 写 FFI 库添加了性能测试

Posted: 20 Apr 2021 07:43 PM PDT

目前只加了一个项目。 大佬们看看有没有什么问题,或者要补充啥得。 https://github.com/TianLiangZhou/ffi-pinyin

tinymce 格式刷

Posted: 20 Apr 2021 06:42 PM PDT

哪位大佬有 tinymce5 格式刷插件啊?不胜感激

全球首款消费级 Linux 平板 JingPad A1 视频已发布

Posted: 20 Apr 2021 02:28 PM PDT

Magisk 如何对单个 apk 文件内部的文件进行替换?

Posted: 20 Apr 2021 02:22 PM PDT

根据网上的一些制作 magisk 模块的模板, 制作一个模块如果要对 apk 进行修改则只能用修改后的 apk 文件整体替换原文件. 一个 apk 文件可能会达到几十 MB, 但实际需要修改的可能只有几百 KB. 而且整体替换会因系统版本更新导致有几率变砖.

谈谈 Java 线程池

Posted: 20 Apr 2021 11:02 AM PDT

摘要

  1. 线程池任务执行机制
  2. 任务调度
  3. 任务缓冲
  4. 任务申请
  5. 拒绝策略
  6. Worker 线程为什么要采用 AQS 实现
  7. Worker 线程初始化
  8. Worker 线程工作
  9. Worker 线程获取任务
  10. Worker 线程销毁
  11. 线程池关闭

1. 线程池任务执行机制

作为一个开发初始化线程池通常会使用 Executors 类,然后调用 newFixedThreadPool 或者其他方法来初始化一个线程池,方法如下:

public static ExecutorService newFixedThreadPool(int nThreads) {     return new ThreadPoolExecutor(nThreads, nThreads,                                   0L, TimeUnit.MILLISECONDS,                                   new LinkedBlockingQueue<Runnable>()); } 

Executors 中其实最终是初始了 ThreadPoolExecutor 类,上一篇Java 线程池前传已经讲了 ThreadPoolExecutor 线程池的一些关键属性。

public ThreadPoolExecutor(int corePoolSize,                           int maximumPoolSize,                           long keepAliveTime,                           TimeUnit unit,                           BlockingQueue<Runnable> workQueue,                           ThreadFactory threadFactory,                           RejectedExecutionHandler handler) {     if (corePoolSize < 0 ||         maximumPoolSize <= 0 ||         maximumPoolSize < corePoolSize ||         keepAliveTime < 0)         throw new IllegalArgumentException();     if (workQueue == null || threadFactory == null || handler == null)         throw new NullPointerException();     this.corePoolSize = corePoolSize;     this.maximumPoolSize = maximumPoolSize;     this.workQueue = workQueue;     this.keepAliveTime = unit.toNanos(keepAliveTime);     this.threadFactory = threadFactory;     this.handler = handler; } 

ThreadPoolExecutor 的构造方法中需要指定一些参数,并且这些参数会被线程池的一些属性所使用,这些我们会在后续的剖析线程池中都会提到。

1.1 任务调度

任务调度是整个线程池的入口,当客户端提交了一个任务以后便进入整个阶段,整个任务的调度过程由 execute 方法完成,如下:

public void execute(Runnable command) {     if (command == null)         throw new NullPointerException();      int c = ctl.get();     if (workerCountOf(c) < corePoolSize) {         // 1 & 2. addWorker 方法会检查线程池是否是 RUNNING 状态         if (addWorker(command, true))             return;         c = ctl.get();     }     // 1 & 3     if (isRunning(c) && workQueue.offer(command)) {         int recheck = ctl.get();         if (! isRunning(recheck) && remove(command))             reject(command);         else if (workerCountOf(recheck) == 0)             addWorker(null, false);     }     else if (!addWorker(command, false))         reject(command); } 

上述代码的执行过程大致如下:

  1. 首先检查线程池是否是 RUNNING 状态,如果不是 RUNNING 状态,则拒绝任务
  2. 如果工作线程数( workerCount )小于核心线程数( corePoolSize ),直接开启新的线程去执行任务
  3. 如果工作线程数( workerCount )大于等于核心线程数 (corePoolSize),并且阻塞任务队列为满时,将任务放入阻塞队列
  4. 如果工作线程数( workerCount )大于等于核心线程数 (corePoolSize),并且阻塞任务队列已满,但工作线程数小于最大线程数( maximumPoolSize ),则创建并启动一个新的线程来执行
  5. 如果工作线程数( workerCount )大于等于最大线程数( maximumPoolSize ),并且阻塞任务队列已满,则执行具体的 RejectedExecutionHandler 策略。

其中 workerCountOf(recheck) == 0 这一步也很关键,这一步主要是为了确保线程池中至少有一个线程去执行任务。

在上述流程中我们提到了阻塞任务队列(用于任务缓冲)、addWorker 方法(任务申请)、以及 reject 方法(任务拒绝策略),下面我们再来分析一下这三个关键点。

1.2 任务缓冲

线程池的本质是对线程和任务的管理,为了做到这一点必须要将线程和任务解耦,不再直接关联,通过缓冲队列恰好可以解决这一点。线程池中的缓冲队列类似于生产者消费者模式,客户端线程往缓冲队列里提交任务,线程池中的线程则从缓冲队列中获得任务去执行。

目前 Java 线程中的默认缓冲队列是阻塞队列模式,主要有以下几种,这些缓冲队列必须要实现 BlockingQueue 接口:

队列 描述
ArrayBlockingQueue 使用数组实现的有界阻塞队列,先进先出,支持公平锁和非公平锁
LinkedBlockingQueue 使用链表实现的有界阻塞队列,先进先出,默认链表长度 Integer.MAX_VALUE
PriorityBlockingQueue 一个使用数组实现支持线程优先级排序的无界队列,默认自然排序,也可以自定义实现 Comparator 来进行排序
DelayQueue 一个采用 PriorityBlockingQueue 实现的延迟队列(组合的方式),在创建该对象中,可以指定任务添加至队列后才能被获取
SynchronousQueue 一个不存储元素的阻塞队列,每一个任务入队操作必须等待一个任务的出队,否则不能添加新的任务
LinkedTransferQueue 一个使用链表实现的无解阻塞队列,该队列支持将任务立即传递给消费者
LinkedBlockingDeque 一个由双向链表实现的有界阻塞队列,队头队尾都可以添加任务消费任务

1.3 任务拒绝

当工作线程数( workerCount )大于等于最大线程数( maximumPoolSize ),并且阻塞任务队列已满,线程池会执行具体的 RejectedExecutionHandler 策略。目前 Java 默认的拒绝策略主要有以下几种:

策略 描述
AbortPolicy 丢弃任务并抛出 RejectedExecutionException 异常
DiscardPolicy 直接丢弃任务
DiscardOldestPolicy 丢弃阻塞队列队头的任务,并重新提交被拒绝的任务
CallerRunsPolicy 直接由调用线程处理被拒绝的任务

1.4 任务申请

在工作线程池数未达到最大线程数并且阻塞队列未满时,我们可以将任务提交至线程池(有可能是开启新的线程,也有可能是将任务提交至阻塞队列)等待执行。其中 addWorker 方法便是开启新的线程执行任务。下面我们来看一下 addWorker 方法:

private boolean addWorker(Runnable firstTask, boolean core) {     retry:     for (;;) {         int c = ctl.get();         int rs = runStateOf(c);          // 线程池如果不是运行状态,线程池根据以下条件来决定是否增加 Work 线程         // 1. 如果线程池不是 SHUTDOWN 状态,那么不允许在增加任何线程,返回 false         // 2. 如果线程池是 SHUTDOWN 状态(不允许接受新的任务),如果 firstTask 不为空表明是新的任务,不应该接受,所以返回 false         // 3. 如果线程池是 SHUTDOWN 状态,并且队列已空,此时也不需要增加线程所以返回 false         if (rs >= SHUTDOWN &&             ! (rs == SHUTDOWN &&                firstTask == null &&                ! workQueue.isEmpty()))             return false;          for (;;) {             // 获取工作线程的数量             int wc = workerCountOf(c);             // 工作线程数与线程池容量比较,超过不允许增加线程             // core 为 true,工作与核心线程数比较,超过不允许增加线程             // core 为 false,工作与最大线程数比较,超过不允许增加线程             if (wc >= CAPACITY ||                 wc >= (core ? corePoolSize : maximumPoolSize))                 return false;             // CAS 增加工作线程数,增加成功跳出 retry 循环             if (compareAndIncrementWorkerCount(c))                 break retry;             // CAS 增加工作线程数失败,则重新获取 ctl 的值             c = ctl.get();  // Re-read ctl             // 判断线程池状态是否改变,如果线程池状态已经改变,则重新执行 retry 循环,否则执行内部循环,尝试增加线程数             if (runStateOf(c) != rs)                 continue retry;             // else CAS failed due to workerCount change; retry inner loop         }     }      boolean workerStarted = false;     boolean workerAdded = false;     Worker w = null;     try {         // 根据 firstTask 来创建 Worker 对象         w = new Worker(firstTask);         final Thread t = w.thread;         if (t != null) {             final ReentrantLock mainLock = this.mainLock;             mainLock.lock();             try {                 // 重新获取线程池的状态,防止在 mainLock 的 lock 方法执行之前,线程池状态改变                 int rs = runStateOf(ctl.get());                  // 只有线程池是运行状态或者线程池是 shutdown 并且任务是来自阻塞队列中( firstTask==null )才可以向线程池中增加线程                 if (rs < SHUTDOWN ||                     (rs == SHUTDOWN && firstTask == null)) {                     if (t.isAlive()) // precheck that t is startable                         throw new IllegalThreadStateException();                     // 增加 worker,workers 是一个 HashSet                     workers.add(w);                     // 更新 largestPoolSize,largestPoolSize 代表了线程池中曾经出现过的最大线程数                     int s = workers.size();                     if (s > largestPoolSize)                         largestPoolSize = s;                     workerAdded = true;                 }             } finally {                 mainLock.unlock();             }             if (workerAdded) {                 // 启动线程                 t.start();                 workerStarted = true;             }         }     } finally {         // 如果 worker 创建或启动失败,修正线程池中的 worker 和 ctl 值         if (! workerStarted)             addWorkerFailed(w);     }     return workerStarted; } 

上述代码的核心逻辑就是根据线程池当前状态来决定是否开启新的线程来执行任务,线程具体的实现方式是采用一个 Worker 类来进行封装。

2. Worker 线程管理

Worker 实现了 Runnable 接口,并继承了 AbstractQueuedSynchronizer ( AQS )。

不熟悉 AQS 的读者可以戳这里

2.1 Worker 线程的基本属性

final Thread thread;  Runnable firstTask;  volatile long completedTasks; 

Worker 中存储了真实的线程( Thread )、该线程需要执行的第一个任务( firstTask )以及线程执行的任务数( completedTasks )。

2.2 Worker 线程为什么要采用 AQS 实现

Worker(Runnable firstTask) {     setState(-1); // inhibit interrupts until runWorker     this.firstTask = firstTask;     // 使用 ThreadFactory 创建线程     this.thread = getThreadFactory().newThread(this); }   protected boolean tryAcquire(int unused) {     if (compareAndSetState(0, 1)) {         setExclusiveOwnerThread(Thread.currentThread());         return true;     }     return false; }  protected boolean tryRelease(int unused) {     setExclusiveOwnerThread(null);     setState(0);     return true; }  public void lock()        { acquire(1); } public boolean tryLock()  { return tryAcquire(1); } public void unlock()      { release(1); } public boolean isLocked() { return isHeldExclusively(); }  void interruptIfStarted() {     Thread t;     if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {         try {             t.interrupt();         } catch (SecurityException ignore) {         }     } } 

Worker 线程采用 AQS 实现,使用 AQS 的独占锁功能,通过其 tryAcquire 方法可以看出 Worker 线程是不允许重入的,Worker 线程有以下特点:

  • 通过 lock 方法成功获取锁以后,则表示 Worker 线程正在执行任务
  • 如果正在执行任务,则不应该中断线程
  • 如果该线程现在不是独占锁状态(也就是空闲状态),说明该线程没有任务处理,可以对线程进行中断

构造方法中为什么要执行 setState(-1)方法 ?

setState 是 AQS 中的方法,默认值为 0,tryAcquire 方法是根据 state 是否是 0 来判断的,所以将 state 设置为-1 是为了禁止在执行任务前对线程进行中断,不明白的读者可以看一下 AQS 的 acquire(int arg)方法,如下:

public final void acquire(int arg) {     if (!tryAcquire(arg) &&         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))         selfInterrupt(); } 

构造方法中的 getThreadFactory().newThread(this)作用是什么?

ThreadFactory 是在我们构造 ThreadPoolExecutor 时传入的,通过 ThreadFactory 我们可以设置线程的分组、线程的名字、线程的优先级、以及线程是否是 daemon 线程等相关信息。

2.3 Worker 线程工作

public void run() {     runWorker(this); } 

Worker 线程获取任务工作是通过调用 ThreadPoolExecutor 中的 runWorker 方法,该方法的参数是 Worker 本身,下面我们看一下 Worker 线程的具体工作原理。

final void runWorker(Worker w) {     Thread wt = Thread.currentThread();     Runnable task = w.firstTask;     w.firstTask = null;     // 将 AQS 中的 state 修改为 0     w.unlock(); // allow interrupts     // 线程在执行任务中是否异常     boolean completedAbruptly = true;     try {         // 获取立即执行的任务,或者从阻塞队列中获取         while (task != null || (task = getTask()) != null) {             // 获取独占锁             w.lock();             // If pool is stopping, ensure thread is interrupted;             // if not, ensure thread is not interrupted.  This             // requires a recheck in second case to deal with             // shutdownNow race while clearing interrupt             if ((runStateAtLeast(ctl.get(), STOP) ||                  (Thread.interrupted() &&                   runStateAtLeast(ctl.get(), STOP))) &&                 !wt.isInterrupted())                 wt.interrupt();             try {                 // 任务执行前的操作                 beforeExecute(wt, task);                 Throwable thrown = null;                 try {                     // 任务执行                     task.run();                 } catch (RuntimeException x) {                     thrown = x; throw x;                 } catch (Error x) {                     thrown = x; throw x;                 } catch (Throwable x) {                     thrown = x; throw new Error(x);                 } finally {                     // 任务执行后的操作                     afterExecute(task, thrown);                 }             } finally {                 task = null;                 w.completedTasks++;                 // 释放独占锁                 w.unlock();             }         }         completedAbruptly = false;     } finally {         processWorkerExit(w, completedAbruptly);     } } 

线程工作的大致流程是:

  1. 通过 getTask 方法获取任务执行
  2. 如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态;
  3. 通过 task.run()方法执行任务
  4. 如果阻塞队列中没有任务,则跳出循环执行 processWorkerExit 方法
  5. runWorker 方法执行完毕,也代表着 Worker 中的 run 方法执行完毕,销毁线程。

这里的 beforeExecute 方法和 afterExecute 方法在 ThreadPoolExecutor 类中是空的,留给子类来实现。

completedAbruptly 变量来表示在执行任务过程中是否出现了异常,在 processWorkerExit 方法中会对该变量的值进行判断。

此部分代码的流程图如下:

WX20210417-181912@2x.png

2.4 Worker 线程获取任务(getTask 方法)

private Runnable getTask() {     // timeOut 变量的值表示上次从阻塞队列中取任务时是否超时     boolean timedOut = false; // Did the last poll() time out?      for (;;) {         int c = ctl.get();         int rs = runStateOf(c);          // Check if queue empty only if necessary.         /*          * 如果线程池状态是非 RUNNING 状态,需要进行以下判断:          * 1. rs >= STOP,线程池是否正在 stop ;          * 2. 阻塞队列是否为空。          * 如果以上条件有一个满足,则将 workerCount 减 1 并返回 null 。          * 因为如果当前线程池状态的值是 SHUTDOWN 或以上时,不允许再向阻塞队列中添加任务。          */         if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {             decrementWorkerCount();             return null;         }          int wc = workerCountOf(c);          // timed 变量用于判断是否需要进行超时控制。         // allowCoreThreadTimeOut 默认是 false,也就是核心线程不允许进行超时;         // wc > corePoolSize,表示当前线程池中的线程数量大于核心线程数量;         // 对于超过核心线程数量的这些线程,需要进行超时控制         boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;          /*          * 重点          * wc > maximumPoolSize 的情况是因为可能在此方法执行阶段同时执行了 setMaximumPoolSize 方法;          * timed && timedOut 如果为 true,表示当前操作需要进行超时控制,并且上次从阻塞队列中获取任务发生了超时          * 接下来判断,如果有效线程数量大于 1,或者阻塞队列是空的,那么尝试将 workerCount 减 1 ;          * 如果减 1 失败,则返回重试。          * 如果 wc == 1 时,也就说明当前线程是线程池中唯一的一个线程了。          */         if ((wc > maximumPoolSize || (timed && timedOut))             && (wc > 1 || workQueue.isEmpty())) {             if (compareAndDecrementWorkerCount(c))                 return null;             continue;         }          try {             /*              * 根据 timed 来判断,如果为 true,则通过阻塞队列的 poll 方法进行超时控制,如果在 keepAliveTime 时间内没有获取到任务,则返回 null ;              * 否则通过 take 方法,如果这时队列为空,则 take 方法会阻塞直到队列不为空。              *               */             Runnable r = timed ?                 workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :                 workQueue.take();             if (r != null)                 return r;             // 如果 r == null,说明已经超时,timedOut 设置为 true             timedOut = true;         } catch (InterruptedException retry) {             // 如果获取任务时当前线程发生了中断,则设置 timedOut 为 false 并返回循环重试             timedOut = false;         }     } } 

这里重要的地方是第二个 if 判断,目的是控制线程池的有效线程数量。由上文中的分析可以知道,在执行 execute 方法时,如果当前线程池的线程数量超过了 corePoolSize 且小于 maximumPoolSize,并且 workQueue 已满时,则可以增加工作线程,但这时如果超时没有获取到任务,也就是 timedOut 为 true 的情况,说明 workQueue 已经为空了,也就说明了当前线程池中不需要那么多线程来执行任务了,可以把多于 corePoolSize 数量的线程销毁掉,保持线程数量在 corePoolSize 即可。

什么时候会销毁? runWorker 方法执行完之后,也就是 Worker 中的 run 方法执行完,由 JVM 自动回收。

getTask 方法返回 null 时,在 runWorker 方法中会跳出 while 循环,然后会执行 processWorkerExit 方法。

获取任务的流程图如下:

WX20210417-190032@2x.png

2.5 Worker 退出(processWorkerExit 方法)

private void processWorkerExit(Worker w, boolean completedAbruptly) {     // 如果是 Worker 线程正常结束,工作数量-1     if (completedAbruptly)         decrementWorkerCount();      final ReentrantLock mainLock = this.mainLock;     mainLock.lock();     try {         // 线程池的任务完成数量增加该 Worker 线程的任务完成数量         completedTaskCount += w.completedTasks;         // 从线程池维护的线程中移除该线程         workers.remove(w);     } finally {         mainLock.unlock();     }          // 根据线程池状态进行判断是否结束线程池     tryTerminate();      int c = ctl.get();     /*      * 当线程池是 RUNNING 或 SHUTDOWN 状态时,如果 worker 是异常结束,那么会直接 addWorker ;      * 如果 allowCoreThreadTimeOut=true,并且等待队列有任务,至少保留一个 worker ;      * 如果 allowCoreThreadTimeOut=false,workerCount 不少于 corePoolSize 。      */     if (runStateLessThan(c, STOP)) {         if (!completedAbruptly) {             int min = allowCoreThreadTimeOut ? 0 : corePoolSize;             if (min == 0 && ! workQueue.isEmpty())                 min = 1;             if (workerCountOf(c) >= min)                 return; // replacement not needed         }         addWorker(null, false);     } } 

3. 线程池关闭(shutdown)

public void shutdown() {     final ReentrantLock mainLock = this.mainLock;     mainLock.lock();     try {         checkShutdownAccess();         advanceRunState(SHUTDOWN);         interruptIdleWorkers();         onShutdown(); // hook for ScheduledThreadPoolExecutor     } finally {         mainLock.unlock();     }     tryTerminate(); } 

在 runWorker 方法中,执行任务时对 Worker 对象 w 进行了 lock 操作,为什么要在执行任务的时候对每个工作线程都加锁(lock)呢?

  • 在 getTask 方法中,如果这时线程池的状态是 SHUTDOWN 并且 workQueue 为空,那么就应该返回 null 来结束这个工作线程,而使线程池进入 SHUTDOWN 状态需要调用 shutdown 方法;
  • shutdown 方法会调用 interruptIdleWorkers 来中断空闲的线程,interruptIdleWorkers 持有 mainLock,会遍历 workers 来逐个判断工作线程是否空闲。但 getTask 方法中没有 mainLock ;
  • 在 getTask 中,如果判断当前线程池状态是 RUNNING,并且阻塞队列为空,那么会调用 workQueue.take()进行阻塞;
  • 如果在判断当前线程池状态是 RUNNING 后,这时调用了 shutdown 方法把状态改为了 SHUTDOWN,这时如果不进行中断,那么当前的工作线程在调用了 workQueue.take()后会一直阻塞而不会被销毁,因为在 SHUTDOWN 状态下不允许再有新的任务添加到 workQueue 中,这样一来线程池永远都关闭不了了;

由上可知,shutdown 方法与 getTask 方法(从队列中获取任务时)存在竞态条件;

  • 解决这一问题就需要用到线程的中断,也就是为什么要用 interruptIdleWorkers 方法。在调用 workQueue.take()时,如果发现当前线程在执行之前或者执行期间是中断状态,则会抛出 InterruptedException,解除阻塞的状态;
  • 但是要中断工作线程,还要判断工作线程是否是空闲的,如果工作线程正在处理任务,就不应该发生中断;

所以 Worker 继承自 AQS,在工作线程处理任务时会进行 lock,interruptIdleWorkers 在进行中断时会使用 tryLock 来判断该工作线程是否正在处理任务,如果 tryLock 返回 true,说明该工作线程当前未执行任务,这时才可以被中断。

关于 Python 中 os 模块怎么获取环境变量的问题

Posted: 20 Apr 2021 09:56 AM PDT

import os os.getenv("hello") 

如上所示, 如果我在终端中使用 sudo python3 执行上面代码, 获取到的就是系统的环境变量. 我的有一个脚本是使用 root 权限启动的, 导致我获取到的环境变量是系统级别的环境变量, 那么我这边有没有什么 python 方法在 root 用户运行脚本的时候获取到这个"hello"的用户级别的环境变量呢, 各位大佬有方法么?

[提问]如何在一个包内一次性加载模块

Posted: 20 Apr 2021 09:20 AM PDT

小白提问,不知道怎么搜索,求轻喷。 比如经常要用 sys,os,re 等模块,包内的每个文件基本都需要 import 他们,有没有能够一劳永逸地 import 的方法?

我目前想到的是,在 __init__.py 中加载,然后包中每个模块开头都 from mypackage import * 来实现?这样有什么危害吗?

感觉自己一直对 import 还有命名空间这一套东西比较迷惑,有比较好的解答文章也恳请赐教一个链接之类的~

js 里的正则怎么像 Python 的 re 用()保留需要的字段?

Posted: 20 Apr 2021 09:00 AM PDT

在 python 里我用 re.findall(r"\d+,\d+,-1,'(.*?)',(\d+),(\d+),'(\d+)-(\d+)'",xxx)
直接可以拿到列表 [ [元组] [元组] ] 和()里的可用元素

在 js 里我用,
res.match(/\d+,\d+,-1,'(.*?)',(\d+),(\d+),'(\d+)-(\d+)'/);
不光括号里的拿到, 括号外的也拿到了,而且用 res[0][0]得不到一个元素,只能得到一个字符,怎么像 python 里 re.findall 那样方便的用在 js 里呢? 哪里有这样的教程呢? 菜鸟里面的 js 正则教程啥都没写

求助,关于 spring 源码环境搭建。

Posted: 20 Apr 2021 08:23 AM PDT

从 github 下载的 spring 源码后,执行.\gradlew build 并去除 aspect 模块导入到 idea 里面后,新建自定义 moudles 无法导入 spring 组件。(环境) gradle-6.8.3,jdk-8.0.282,idea 2019.2.4

Nginx 在同一 vhost 的同一 server 内,想代理完全相同的 url 的后端盖怎么做?

Posted: 20 Apr 2021 08:09 AM PDT

比如配置一个 server,监听了 443 端口,代理了 3 个 url 完全相同的后端(比如代理了 3 个 grafana:a 、b 、c ),因此我们想在访问 nginx 时 url 加上一个特别文根区分它们,比如 https://172.16.103.14/a/代表访问后端 a 的 grafana,以此类推。但是我们发现当我们加上这个标识后,比如请求 grafana 的 a 服务器会返回个重定向到 https://172.16.103.14/login,但是我们的 nginx 不能配上 /login 这个文根,因为三个后端是相同的,无论我访问 /a 、/b 还是 /c 都会重定向到 login,这样就没法区分到底应该访问哪个后端了。大家有啥办法吗?

No comments:

Post a Comment