0%


一、@Retention 简介

@Retention 是用来定义注解的注解,这类注解也叫 元注解。@Retention 定义了注解的生命周期,生命周期的长短取决于 RetentionPolicy 属性值。

1
2
3
4
5
6
7
8
9
10
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
SOURCE,

/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
CLASS,

/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}

归纳如下:

属性值 描述 使用场景
RetentionPolicy.SOURCE 注解只保留在源文件,当被编译成class文件,注解就会消失 代码预检,如 @Override、@SuppressWarnings
RetentionPolicy.CLASS 注解被保留到class文件,但当jvm加载class文件时被遗弃 编译时进行一些预处理操作,如生成辅助信息(META-INF/services)
RetentionPolicy.RUNTIME 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在 运行时动态获取注解信息,如 @Autowired、@Required

二、通过字节码认识 @Retention

定义三种生命周期的注解:SourcePolicy、ClassPolicy、RuntimePolicy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface SourcePolicy {
}

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface ClassPolicy {
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RuntimePolicy {
}

使用以上注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class RetentionPolicyTest {
@SourcePolicy
public void sourcePolicy() {
}

@ClassPolicy
public void classPolicy() {
}

@RuntimePolicy
public void runtimePolicy() {
}
}

javap -v RetentionPolicyTest 查看字节码:

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
{
public RetentionPolicyTest();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0

public void sourcePolicy();
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 7: 0

public void classPolicy();
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 11: 0
RuntimeInvisibleAnnotations:
0: #11()

public void runtimePolicy();
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 15: 0
RuntimeVisibleAnnotations:
0: #14()
}

可以看到:

  • 编译之后,@SourcePolicy 信息不存在;
  • 编译之后,@ClassPolicy 具有属性:RuntimeInvisibleAnnotations;
  • 编译之后,@RuntimePolicy 具有属性:RuntimeVisibleAnnotations;


自从国内猪肉紧俏以来,市面上就经常买到进口猪肉。外国人养猪比较人性化,不给小猪做阉割,所以猪肉又腥又骚。今天抱着踩雷的心态,买了一份肉,并在叮咚上特意备注道:“只要中国猪肉!”。

今天来做个狮子头。

一、食材准备

五花肉(肥瘦比: 6:4),山药(马蹄/萝卜)、老豆腐、红薯淀粉、辣椒、葱姜蒜、生抽、老抽、蚝油、料酒、八角、鸡蛋。

wyDNmd.jpg

辅料我用的是山药,大家可以根据自己的口味调整。辅料添加要适量,否则可能团不成肉丸子。

二、制作流程
1
2
3
4
5
graph LR
A[拌料] --> B(成型)
B --> C(煎炸)
C --> D(红烧)
D --> E(勾芡)
三、操作步骤
  1. 将五花肉,山药、老豆腐、葱姜蒜、生抽、老抽、蚝油、料酒、鸡蛋依次放入碗中,用手沿着一个方向搅拌上劲,待拌料可以黏在手上,加入少量淀粉后继续搅拌摔打;

    wyD8SO.jpg

  2. 将拌料团成你喜欢的大小;

    wyDJ6e.jpg

  3. 热锅冷油,煎炸二十分钟;油热6分可入锅,全程小火;这一步的主要目的是定型,口味清淡的朋友也可以用沸水来操作;

    wyDGlD.jpg

  4. 将辣椒、葱姜蒜、八角炒香,再加入生抽、老抽、蚝油、料酒,添2/3锅水,加入肉丸子小火慢煮30分钟;

    wyDYOH.jpg

  5. 勾芡 ;待锅内汤汁不多时,盛出肉丸子,然后加入水淀粉熬制挂锅,然后将红烧汁浇在狮子头上;

    wygNnS.jpg

    wygwkj.jpg

四、多说一句

做狮子头比较耗时耗力,只能在周末享用。相比来说,我更喜欢老家的肉丸子。材料比较简单,猪肉、馒头渣、黑胡椒。团成丸子,再兑上半锅水,炖煮一个小时。味浓又不油腻!

一、应用背景

在支付系统中,线程池在批量支付中有着十分重要的作用。随着业务量的增多,支付接口的处理能力越来越弱,甚至出现一个支付批次的处理时间超过5分钟的情况。这对于调用方来说,是无法容忍的。最简单粗暴的解决办法,便是增加线程数和阻塞队列容量。但至于怎么调整,才能做到既保证接口对支付请求的处理能力,又不浪费系统资源,好像只能凭感觉来了。Prometheus + Grafana 是目前比较流行的监控体系,在管理线程池方面的表现也十分出色。

二、线程池配置

采用 Spring 提供的 ThreadPoolTaskExecutor 来完成线程池的配置。根据业务需要,创建两个线程池:同步线程池和异步线程池。具体配置如下:

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
@Configuration
public class ExecutorPoolConfig {

/**
* 线程池参数配置
*/
@Resource
private ExecutorProperties executorProperties;

private static final String ASYNC_EXECUTOR = "asyncExecutor";
private static final String SYNC_EXECUTOR = "syncExecutor";

/**
* @return 同步线程池
*/
@RefreshScope
@Bean(SYNC_EXECUTOR)
public ThreadPoolTaskExecutor serviceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(executorProperties.getSync().getCorePoolSize());
executor.setMaxPoolSize(executorProperties.getSync().getMaxPoolSize());
executor.setQueueCapacity(executorProperties.getSync().getQueueCapacity());
executor.setKeepAliveSeconds((int)executorProperties.getSync()
.getKeepAlive().getSeconds());
executor.setThreadNamePrefix("executor-sync-service-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(executorProperties.getSync()
.getAwaitTerminationSeconds());
return executor;
}

/**
* @Async 注解默认走该线程池
* @return 异步处理线程池
*/
@RefreshScope
@Bean(ASYNC_EXECUTOR)
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(executorProperties.getAsync().getCorePoolSize());
executor.setMaxPoolSize(executorProperties.getAsync().getMaxPoolSize());
executor.setQueueCapacity(executorProperties.getAsync().getQueueCapacity());
executor.setKeepAliveSeconds((int)executorProperties.getAsync()
.getKeepAlive().getSeconds());
executor.setThreadNamePrefix("common-async-executor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(executorProperties.getAsync()
.getAwaitTerminationSeconds());
return executor;
}
}

三、监控指标

综合线程池的核心要素和生产业务的关键要素,提出以下几种监控指标:

  • 核心线程数;
  • 最大线程数;
  • 活跃线程数;
  • 当前线程数;
  • 队列中任务数;
  • 已完成任务数;

    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
    import io.micrometer.core.instrument.Gauge;
    import io.micrometer.core.instrument.MeterRegistry;
    import io.micrometer.core.instrument.Metrics;

    @Component
    public class ExecutorMetricsSupport implements InitializingBean {

    /**
    * Prometheus 数据采集中心
    */
    @Resource
    private MeterRegistry meterRegistry;

    @Autowired
    @Qualifier(SYNC_EXECUTOR)
    private ThreadPoolTaskExecutor syncExecutor;

    @Autowired
    @Qualifier(ASYNC_EXECUTOR)
    private ThreadPoolTaskExecutor asyncExecutor;

    @Override
    public void afterPropertiesSet() throws Exception {
    initServiceExecutorMetrics(syncExecutor, "executor.sync");
    initServiceExecutorMetrics(asyncExecutor, "executor.async");
    }

    /**
    * 线程池metrics指标监控
    * @param serviceExecutor 线程池
    * @param namePrefix 指标名称前缀
    */
    private void initServiceExecutorMetrics(ThreadPoolTaskExecutor serviceExecutor, String namePrefix) {
    Gauge
    .builder(namePrefix.concat(".active"),
    serviceExecutor, ThreadPoolTaskExecutor::getActiveCount)
    .register(meterRegistry);

    Gauge
    .builder(namePrefix.concat(".core"),
    serviceExecutor, ThreadPoolTaskExecutor::getCorePoolSize)
    .register(meterRegistry);

    Gauge
    .builder(namePrefix.concat(".max"),
    serviceExecutor, ThreadPoolTaskExecutor::getMaxPoolSize)
    .register(meterRegistry);

    Gauge
    .builder(namePrefix.concat(".pool"),
    serviceExecutor, ThreadPoolTaskExecutor::getPoolSize)
    .register(meterRegistry);

    Gauge
    .builder(namePrefix.concat(".queue"), serviceExecutor,
    executor -> executor.getThreadPoolExecutor().getQueue().size())
    .register(meterRegistry);

    Gauge
    .builder(namePrefix.concat(".completetask"), serviceExecutor,
    executor -> executor.getThreadPoolExecutor().getCompletedTaskCount())
    .register(meterRegistry);
    }
    }

四、指标分析

通过 PSQL 将以上各项指标展示在 Grafana 中。 Prometheus 默认 15s 拉取一次数据,对于线程池这种波定性较大的指标,建议将拉取时间调整至 10s,以便灵活且准确地反应线程池的运行情况。

  • 异步线程池

dDNE9g.png

  • 同步线程池

dDN6vd.png

由监控结果可以作出以下分析:

  • 异步线程池的负担非常小,目前还没出现过队列积压的情况,可以适当减少核心线程和最大线程数;
  • 同步线程池配置较为妥当,峰值期间未启用非核心线程,队列积压任务量在合理区间,请求处理效率非常高。

一、需求背景

本周负责做调账线上化,其中一个业务需求比较棘手:提交调账申请后,需在数据表记录对应的调账类型;而在调账复核页面,列表筛选项-调账类型,需支持多选。简单枚举如下:

1
2
3
4
5
6
7
8
9
10
11
public enum AdjustTypeEnum {
/**
* 调账类型枚举; 一次调账申请可能对应多种调账类型,在数据表中枚举值之间以英文 , 分隔
*/
FINE(0, "应还罚息"),
TOTAL_REPAYMENT(1, "应还总额"),
FACT_REPAYMENT(2, "实还总额"),
FACT_PAYMENT_DATE(3, "实还日期"),
OVERDUE_DAYS(4, "逾期天数"),
PAYMENT_STATUS(5, "还款状态");
}

假如调账申请A的调账类型包括应还罚息和应还总额,则数据表相应字段值为0,1。在复核页面,若调账类型复选项中包含应还罚息或应还总额,则需查询到该申请。此时,我们发现SQL内置函数FIND_IN_SET只支持单选项查询,而无法灵活拆分筛选条件和存储数据。因此,有必要调整一下数据的存储规则和查询规则。

二、位与运算的妙用

首先,我们需要进一步抽象:一方面,为了不浪费存储空间,科学地设计数据表,我们需要将目标数据列表整合存储在一个字段中;另一方面,数据库引擎需要灵活使用整合后的字段,以执行相应的查询语句。说白了,就是看起来像做了整合,实际上还是原始的目标数据列表。

事实证明,上面提出的方案根本满足不了业务需求,是一个非常差的设计。除了以逗号分隔枚举值的存储方式,还有一种更为科学的存储方式——二进制。简单做几个位与运算:

1
2
3
4
5
6
7
8
// 存储值: 3; 查询条件: 1
// 3 == 2 + 1
// 1 & 3 == 1
0001
&
0011
=
0001
1
2
3
4
5
6
7
8
// 存储值: 7; 查询条件: 1
// 7 == 1 + 2 + 4
// 1 & 7 == 1
0111
&
0001
=
0001
1
2
3
4
5
6
7
8
9
// 存储值: 7; 查询条件: 3
// 7 == 1 + 2 + 4
// 3 == 1 + 2
// 3 & 7 == 3
0111
&
0011
=
0011
1
2
3
4
5
6
7
8
9
// 存储值: 7; 查询条件: 15
// 7 == 1 + 2 + 4
// 15 == 1 + 2 + 4 + 8
// 15 & 7 == 7
0111
&
1111
=
0111

从以上位与运算实例,可以发现一个有趣的现象:若将枚举值设置为2n次幂,则只需将目标数据列表加合,即可保证每一个目标数据对应到二进制中的一位。

此时,对于任何一个查询条件,只需要将其与整合值进行位与运算,即可得到匹配的值。

由此,我们调整枚举为:

1
2
3
4
5
6
7
8
9
10
11
public enum AdjustTypeEnum {
/**
* 调账类型枚举; 一次调账申请可能对应多种调账类型,在数据表中存储各值之和
*/
FINE(1, "应还罚息"),
TOTAL_REPAYMENT(2, "应还总额"),
FACT_REPAYMENT(4, "实还总额"),
FACT_PAYMENT_DATE(8, "实还日期"),
OVERDUE_DAYS(16, "逾期天数"),
PAYMENT_STATUS(32, "还款状态");
}

调整SQL为:

1
2
3
4
5
SELECT
*
FROM `boss-account`.acct_adjust_account_record
WHERE
adjust_type_sum & #{调账类型筛选条件值之和} > 0

一、技术背景

实现发布订阅的中间件有许多,包括时下最热的KafkaRabbitMQActiveMQ,不太常见的Guava下的EventBus,以及Redis等。关于MQ之流的分析已经够多了,今天详细介绍下Redis的发布订阅机制及其实现。事实上,Redis的发布订阅机制在具体项目中的应用非常少,主要有两方面原因:其一,Redis的可靠性比较差,一旦出现断网等情况,则发布的消息便全部丢失;其二,Redis的消息处理方式是通过单线程循环遍历实现的,若存在大量的消息发布,则可能导致输出缓冲区膨胀,甚至服务崩溃。但对数据安全性和稳定性要求不高的场景来说,Redis不失为最佳的选择。

二、基本操作

Redis 的发布订阅模式包括普通订阅,普通订阅取消,模式订阅,模式订阅取消这四个场景。用命令实现如下:

  • 启动 Redis 服务(以 Windows 平台为例)

    1
    2
    3
    C:\Tools\Redis>redis-server.exe redis.windows.conf
    ...
    The server is now ready to accept connections on port 6379
  • 创建客户端client1,并以普通订阅渠道channel1

    1
    2
    3
    4
    5
    6
    redis-cli.exe -h 127.0.0.1 -p 6379
    127.0.0.1:6379> SUBSCRIBE channel1
    Reading messages... (press Ctrl-C to quit)
    1) "subscribe"
    2) "channel1"
    3) (integer) 1
  • 创建客户端client2,并普通订阅渠道channel2

    1
    2
    3
    4
    5
    6
    redis-cli.exe -h 127.0.0.1 -p 6379
    127.0.0.1:6379> SUBSCRIBE channel2
    Reading messages... (press Ctrl-C to quit)
    1) "subscribe"
    2) "channel2"
    3) (integer) 1
  • 创建客户端client3,并模式订阅渠道channel*

    1
    2
    3
    4
    5
    6
    redis-cli.exe -h 127.0.0.1 -p 6379
    127.0.0.1:6379> PSUBSCRIBE channel*
    Reading messages... (press Ctrl-C to quit)
    1) "psubscribe"
    2) "channel*"
    3) (integer) 1
  • 向渠道channel2发布一条消息

    1
    2
    3
    4
    redis-cli.exe -h 127.0.0.1 -p 6379
    127.0.0.1:6379> PUBLISH channel2 "msg from channel2"
    (integer) 2
    127.0.0.1:6379>
  • client2client3 接收到消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # ----------client2---------
    1) "message"
    2) "channel2"
    3) "msg from channel2"

    # ----------client3---------
    1) "pmessage"
    2) "channel*"
    3) "channel2"
    4) "msg from channel2"

因此,我们可以猜测,消息发布者与消息订阅者之间是通过渠道连接的,包括精准匹配(普通订阅)和模糊匹配(模式订阅)。经过分析其结构设计,可表示为用例:

三、原理分析

Redis发布订阅的核心实现在pubsub.c文件。从头文件server.h中可以读取相关函数声明:

1
2
3
4
5
6
void subscribeCommand(client *c);      /* 普通订阅 */
void unsubscribeCommand(client *c); /* 普通订阅取消 */
void psubscribeCommand(client *c); /* 模式订阅 */
void punsubscribeCommand(client *c); /* 模式订阅取消 */
void publishCommand(client *c);
void pubsubCommand(client *c);
  • 普通订阅模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#define CLIENT_PUBSUB (1<<18)      /* Client is in Pub/Sub mode. */
/*-----------------------------------------------------------------------------
* Pubsub commands implementation
*----------------------------------------------------------------------------*/

void subscribeCommand(client *c) {
int j;
/* 分析client结构,c->argv[0]为命令本身,第2位开始为渠道参数 */
for (j = 1; j < c->argc; j++)
/* 订阅一个渠道 */
pubsubSubscribeChannel(c,c->argv[j]);
/* 设置客户端为发布订阅模式,此模式在processCommand中为校验值 */
c->flags |= CLIENT_PUBSUB;
}

/* 一个客户端订阅一个渠道。若返回 1,则订阅成功;若返回 0,则该客户端已订阅该渠道 */
int pubsubSubscribeChannel(client *c, robj *channel) {
dictEntry *de;
list *clients = NULL;
int retval = 0;

/* 将该渠道添加到该客户端的pubsub_channels哈希表中; key: channel,value: NULL*/
if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
retval = 1;
/* 引用自增 1 */
incrRefCount(channel);
/* 在服务端pubsub_channels中查找该channel */
de = dictFind(server.pubsub_channels,channel);
/* 若该channel不存在,则新建 */
if (de == NULL) {
clients = listCreate();
/* 将该客户端添加到该服务端的pubsub_channels哈希表中; key: channel,value: client*/
dictAdd(server.pubsub_channels,channel,clients);
/* 引用自增 1 */
incrRefCount(channel);
} else {
clients = dictGetVal(de);
}
/* 将客户端c记录在订阅客户端列表 */
listAddNodeTail(clients,c);
}
/* 通知客户端订阅结果 */
addReplyPubsubSubscribed(c,channel);
return retval;
}

普通订阅模式主要做了两件事:将订阅渠道添加至客户端pubsub_channels哈希表中;将订阅客户端添加至服务端的pubsub_channels哈希表中。可如下表示:



今天特别想吃我妈做的油馍,也就是大家常说的葱油饼。虽然出租屋的条件有限,但我打算尝试一次。


一、原料

小麦粉,小香葱,香油,花生油,食盐,佐料(看个人喜好选择添加)。

二、步骤

1. 和面

加入适量面粉(尽量选高筋粉),边用筷子搅拌边兑温水,待面结成絮状,手动和成面团。建议面团和软一点,保证很好的延展性。和好后,套上保鲜膜,醒面20分钟。

N1pDh9.jpg

2. 擀面

面醒好后,揉捏成条状。

N19cCj.jpg

葱油饼是多层叠加起来的,所以需要尽可能地将面擀薄。

N1C3Mq.jpg

3. 加料

将适量香油滴在面皮上,然后不断对折面皮,直至香油全部铺开。

N1PkY4.jpg

然后,均匀撒上葱花,食盐少量。

N1P4NF.jpg

4. 成型

将铺满葱花的面从一边角叠起。

N1ZtPK.jpg

将面擀平实。

N1VM7t.jpg

5. 煎饼

注意控制油温。家用燃气灶,小火慢热即可。

N1VaBn.jpg

N1VUns.jpg

N1VtXj.jpg


一、线程的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行线程1");
}
});
thread1.start();

Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行线程2");
}
});
thread2.run();
}

创建以上两个线程,执行结果:

1
2
执行线程1
执行线程2

线程1与线程2都执行了 run(),但线程1是通过 start() 执行的,线程2是直接调用 run() 执行的。

二、start() 与 run() 的区别

为了更精准地找到两种调用之间的区别,需要打印线程相关的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
System.out.println("执行线程1; 线程名称:" + currentThread.getName());
}
});
thread1.start();

Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
System.out.println("执行线程2; 线程名称:" + currentThread.getName());
}
});
thread2.run();
}

执行结果:

1
2
执行线程1; 线程名称:Thread-0
执行线程2; 线程名称:main

从执行结果来看,start() 调用,是通过开启一个新线程来执行 run() 的;而直接调用 run(),是主线程直接执行方法内容的。

此外,我们还可以做另一种尝试:多次执行 run() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
System.out.println("执行线程1; 线程名称:" + currentThread.getName());
}
});
thread1.run();
thread1.run();

Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
System.out.println("执行线程2; 线程名称:" + currentThread.getName());
}
});

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

执行结果:

1
2
3
4
5
6
执行线程1; 线程名称:main
执行线程1; 线程名称:main
执行线程2; 线程名称:Thread-1
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at com.example.springbatchdemo.job.Test.main(Test.java:29)

从执行结果可知,可以多次调用 run(),而 start() 只能被执行一次,这是为什么呢?我们看下 start 方法说明:

1
2
3
4
5
6
7
8
9
10
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();

调用 start(),首先需要判断当前线程的状态是否为 0 (NEW),如果不是,则抛异常。当第一次调用 start() 时,线程状态从 NEW (新建)变为 RUNNABLE(就绪),当第二次调用 start() 时,线程状态肯定不是 NEW。

三、总结

Thread 的 start() 与 run() 都可以完成任务执行,主要区别在:

  • 作用机制不同:直接调用 run(),相当于在当前线程中执行普通方法;调用 start(),是开启一个新的线程来执行 run();
  • 执行速度不同:调用 run() 会立即执行任务,调用 start() 是将线程的状态改为就绪状态,等待空闲 CPU,不会立即执行;
  • 调用次数不同:run() 可以被重复调用,而 start() 只能被调用一次。


一、 NexT 集成第三方搜索服务

根据官方文档, NexT 主题框架集成的搜索方式有四种:

  • SwiftType
  • 微搜索
  • Local Search
  • Algolia

其中,Local Search 最为简单方便,本文将进一步介绍其配置与使用。

  • 安装插件 hexo-generator-searchdb

    1
    npm install hexo-generator-searchdb --save
  • 编辑站点配置文件,新增以下配置:

    1
    2
    3
    4
    5
    search:
    path: search.xml
    field: post
    format: html
    limit: 10000
  • 编辑主题配置文件,启用本地搜索功能:

    1
    2
    3
    4
    # Local Search
    # Dependencies: https://github.com/theme-next/hexo-generator-searchdb
    local_search:
    enable: true

部署博客,访问首页,可以看到新增一个搜索控件:

使用搜索功能,查看所有跟 Spring 相关的博客:

可以看到,Local Search 不仅支持标题检索,还支持内容检索,十分好用。

食材准备

鱼,玉米油,佐料(大葱、姜、蒜头、生抽、老抽、醋、料酒、蚝油、豉油、花椒、八角、桂树皮、黄豆酱)

GO4pTK.jpg

1. 煎鱼

GO4kSH.jpg

为了不糊锅,建议事先在鱼的身上糊上一层面粉。入锅之前,需要提起鱼来,把多余的面粉抖掉,防止热油变苦。煎鱼的目的有两个:高油温可以锁住水分,使鱼肉外焦里嫩;鱼本身的腥味主要来自于表皮和腹腔,高油温可以迅速使鱼皮和腹腔角质化,起到去腥的作用。

GO4MtS.jpg

待鱼炸至两面金黄后捞出,放凉备用。

GO4t00.jpg

2. 烧鱼

GO4g76.jpg

热油锅将佐料翻炒出味,兑水至 2/3 满锅。

GO4ojA.jpg

大火烧开,约 10 分钟后转至文火慢炖。约 1 小时后,大火收汁,出锅。

GO5VCF.jpg

3. 心得

红烧鱼是我最常做的荤菜,我也试过很多做法,有的味道很好,有的就做砸了。综合实操经验来看,我觉得做鱼的关键,并不是那盘佐料,也不是热油锅炸至两面金黄,而是时间———煮鱼的时间。我试过将一条鲜鱼直接放入一锅水中,然后只加入油盐酱醋,慢煮 2 小时后,别是一番风味———鱼的原味。所以,以上很多步骤可能只是花拳绣腿,关键还是在 2/3 满锅的水和 1 小时的文火慢炖。大家都可以尝试下,感受下那 1 小时内到底发生了什么。



又是个美好的周末,照例要吃点好的。今天做了虎皮蛋和红烧鱼。老是写技术博客也没啥意思,不如放点食谱上来吧,毕竟饮食男女嘛。这里先说虎皮蛋的制法。

制作流程

食材准备

新鲜鸡蛋,玉米油,佐料(大葱、小葱、姜、蒜头、生抽、老抽、蚝油、花椒、八角)

GOoYnA.jpg

1. 煮蛋

将鸡蛋在开水中煮 20 分钟(一定要煮熟,否则做不出沙沙的蛋黄),然后捞出放入流动冷水下冲洗3分钟,方便剥壳。

GOoHBR.jpg

2. 剥蛋

尽量剥得完整,防止油炸的时候出现裂痕。沥干鸡蛋表皮的水分,防止入油锅时发生爆炸。

GOoON6.jpg

3. 炸蛋

玉米油的味道最轻,对鸡蛋原味的影响最小,推荐。油入锅后,大火升温;将木筷子插入热油中,当出现气泡后,可将鸡蛋下入油中。调至文火,慢炸 20 分钟,直至鸡蛋表面布满虎皮。捞出后放入凉水中浸泡,然后沥干水分。

GOTZ8S.jpg

GOT35V.jpg

4. 卤蛋

将佐料放入油中炸至金黄,再加入生抽老抽蚝油,兑水熬卤汁。卤水翻开后,放入虎皮蛋,调至文火慢炖 30 分钟。大火收汁,捞出放入碗中。留存的卤汁需要没过鸡蛋。

GOTar9.jpg

5. 腌蛋

GOTy8O.jpg

卤制好的蛋,不要着急吃。虎皮蛋的精髓不在外面的虎皮,而在里面的蛋黄。一颗合格的虎皮蛋,除去 Q 弹的虎皮,还有饱满多汁、沙瓤的蛋黄。要达到这种效果,必须将做好的虎皮蛋静置 4 至 6 小时。天气暖和的时候,需放入冰箱,防止变质。