Dawn's Blogs

分享技术 记录成长

0%

String 类

String 是一个 final 类,表示一个不可变的字符串。

String 对象的字符内容是存储在一个字符数组 value[] 中的。

  • String 实现了三个接口:
    • Serializable 接口:表示字符串是可序列化的。
    • Comparable 接口:字符串可以比较大小。
    • CharSequence 接口:字符串实际上是字符数组序列。
1
2
3
4
5
6
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0

字符串存储结构

实例化字符串

  • 当字符串以字面量(直接赋值)的方式声明一个字符串时,字符串常量直接存储在常量池中。

  • 以 new 的方式声明一个字符串时,字符串中非常量对象存储在中,对象中的 value 数组指向常量池。

String str1 = "abc";String str2 = new String("abc"); 的内存存储区别如下:

image-20230130105516940

阅读全文 »

线程的创建和使用

Java 语言的 JVM 允许程序运行多个线程,它通过 java.lang.Thread 类(或者 Runnable 接口)来体现。Thread 类的特性:

  • 每个线程都是通过某个特定 Thread 对象的 run 方法来完成操作的,经常把 run 方法的主体称为线程体
  • 通过 Thread 对象的 start 方法来启动线程。

创建线程方式

JDK1.5 之前创建新执行线程有两种方法:

  • 继承 Thread 类。
  • 实现 Runnable 接口

JDK1.5 之后新增的线程创建方式:

  • 实现 Callable 接口。
  • 使用线程池。

image-20230129180135020

继承 Thread 类

创建方法如下:

  • 定义子类继承 Thread 类
  • 子类中重写 Thread 类中的 run 方法
  • 创建子类对象,调用 start 方法启动线程

注意:一个线程对象只能调用一次 start 方法启动,如果重复调用了会抛出异常 IllegalThreadStateException

实现 Runnable 接口

创建方法如下:

  • 定义子类实现 Runnable 接口
  • 子类中重写 Runnable 接口中的 run 方法
  • 通过 Thread 类含参构造器创建线程对象,将实现 Runnable 接口的对象作为实际参数传递给 Thread 类的构造器中
  • 调用 Thread 类对象的 start 方法开启线程

实现 Runnable 接口的好处:

  • 避免了单继承的局限性。
  • 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源
阅读全文 »

Java 程序在执行过程中所发生的异常事件可分为两类 Error 和 Exception,这两类的父类都是 Throwable

  • Error:Java 虚拟机无法解决的严重问题。如:JVM 系统内部错误、资源耗尽等严重情况。比如:StackOverflowError(栈溢出)、OutOfMemoryError(堆溢出)。
  • Exception:其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。如:空指针访问、数组越界。异常又分为:
    • 编译时异常:其他异常如 IOException、ClassNotFoundException 等。
    • 运行时异常:包括 RuntimeException 异常以及其子类。

image-20230128162007940

常见异常

Java 中常见异常如下:

  • java.lang.RuntimeException:
    • ClassCastException:类型转换异常。
    • ArrayIndexOutOfBoundsException:数组越界。
    • NullPointerException:空指针访问。
    • ArithmeticException:算数异常。
    • NumberFormatException
    • InputMismatchException
  • java.io.IOException:
    • FileNotFoundException
    • EOFException
  • java.lang.ClassNotFondException
  • java.lang.InterruptedException
  • java.sql.SQLException

异常处理机制

Java 提供的是异常处理的抓抛模型

  • Java 程序的执行过程中如出现异常,会生成一个异常类对象,该异常对象将被提交给 Java 运行时系统,这个过程称为抛出(throw)异常

异常对象的生产方式:

  • 由虚拟机自动生成:程序运行过程中,虚拟机检测到程序发生了问题,如果在当前代码中没有找到相应的处理程序,就会在后台自动创建一个对应异常类的实例对象并抛出——自动抛出。
  • 由开发人员手动创建Exception exception = new ClassCastException(); ——创建好的异常对象不抛出对程序没有任何影响,和创建一个普通对象一样。
  • 如果一个方法内抛出异常,该异常对象会被抛给调用者方法中处理。如果异常没有在调用者方法中处理,它继续被抛给这个调用方法的上层方法。这个过程将一直继续下去,直到异常被处理。这一过程称为捕获(catch)异常。如果一个异常回到 main 方法,并且 main 也不处理,则程序运行终止。
阅读全文 »

接口

为什么需要接口

一方面,有时必须从几个类中派生出一个子类,继承它们所有的属性和方法。但是,Java 不支持多重继承。有了接口,就可以得到多重继承的效果

另一方面,有时必须从几个类中抽取出一些共同的行为特征,而它们之间又没有 is-a 的关系,仅仅是具有相同的行为特征而已。如:鼠标、键盘、打印机等都支持 USB 连接。

接口就是规范,定义的是一组规则。继承实现的是是不是的逻辑,而接口定义的是能不能的逻辑。

注意点

接口是抽象方法常量定义的集合:

  • 接口中所有的成员变量默认都是 public static final 修饰的。
  • 接口中所有抽象方法默认都是 public abstract 修饰的。
  • 接口中没有构造器
  • 接口与接口之间可以多继承。
  • 与继承关系类似,接口与实现类之间存在多态性
1
2
3
class SubClass extends SuperClass implements InterfaceA, InterfaceB{

}
阅读全文 »

代码块

代码块的作用是:对类或者对象进行初始化

代码块只能被 static 修饰,所以分为:静态代码块和非静态代码块。

静态代码块

  • 静态代码块:用 static 修饰的代码块。
    • 不可以对非静态的属性初始化。即不可以调用非静态的属性和方法。
    • 若有多个静态的代码块,那么按照从上到下的顺序依次执行。
    • 静态代码块的执行要先于非静态代码块。
    • 静态代码块随着类的加载而加载,且只执行一次

非静态代码块

  • 非静态代码块:没有 static 修饰的代码块。
    • 除了调用非静态的结构外,还可以调用静态的变量或方法。
    • 若有多个非静态的代码块,那么按照从上到下的顺序依次执行。
    • 每次创建对象的时候,都会执行一次;且先于构造器执行

抽象类与抽象方法

随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类

abstract 关键字修饰的类或者方法叫做抽象类或者抽象方法。

阅读全文 »

单元测试方法

Java 中的 JUnit 单元测试,步骤:

  • 创建 Java 类,进行单元测试。这个类要求:
    • 此类是 public 的。
    • 此类提供公共的无参数构造器。
  • 此类中声明单元测试方法。单元测试方法要求:
    • 方法的权限是 public。
    • 没有返回值、没有形参。
  • 单元测试方法上需要声明注解 @Test,并 import org.junit.Test;
1
2
3
4
5
6
7
8
9
10
import org.junit.Test;

public class JUnitTest {

@Test
public void testXXX() {
// 单元测试流程
// ....
}
}

包装类

针对八种基本数据类型定义相应的引用类型,即包装类(封装类)。有了类的特点,就可以调用类中的方法,Java 才是真正的面向对象。

image-20230127105607994

阅读全文 »

继承

多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,只要继承那个类即可。继承的类称为子类(派生类),被继承的类被称为父类(超类)

可以理解为:子类 is a 父类

一旦子类继承父类之后,子类就获取到了父类中声明的所有属性和方法。

继承语法:

1
2
class SubClass extends SuperClass{
}

在 Java 中,一个子类只能由一个父类,一个父类可以派生出多个子类。继承的作用

  • 继承的出现减少了代码冗余,提高了代码的复用性
  • 继承的出现,更有利于功能的扩展
  • 继承的出现让类与类之间产生了关系
阅读全文 »

AOF 持久化

数据结构定义

Persister

Persister 是 AOF 持久化中的核心数据结构,它从 channel 中接收消息并且将消息写入到 AOF 文件中。部分字段如下:

  • db:这个 AOF 持久化协程所指向的数据库
  • tmpDBMaker:新建一个临时的数据库,用于 AOF 重新操作。
  • aofChan:Persister 就是从这个通道中监听消息,并且将消息写入到 AOF 文件中的。
  • aofFsync:AOF 文件刷入到磁盘的策略,有三种选项可以选择:FsyncAlways(每一个命令都会进行刷盘操作),FsyncEverySec(每秒钟进行一次刷盘操作),FsyncNo(不主动进行刷盘操作,交给操作系统去决定)。
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
// Listener will be called-back after receiving a aof payload
// with a listener we can forward the updates to slave nodes etc.
type Listener interface {
// Callback will be called-back after receiving a aof payload
Callback([]CmdLine)
}

// Persister receive msgs from channel and write to AOF file
type Persister struct {
ctx context.Context
cancel context.CancelFunc
db database.DBEngine
tmpDBMaker func() database.DBEngine
aofChan chan *payload
aofFile *os.File
aofFilename string
aofFsync string
// aof goroutine will send msg to main goroutine through this channel when aof tasks finished and ready to shut down
aofFinished chan struct{}
// pause aof for start/finish aof rewrite progress
pausingAof sync.Mutex
currentDB int
listeners map[Listener]struct{}
// reuse cmdLine buffer
buffer []CmdLine
}

payload

payload 为 aofChan 通道内的消息,包括:命令行参数(cmdLine)、数据库(dbIndex)、用于同步的信号量(wg)。

1
2
3
4
5
6
7
8
// CmdLine is alias for [][]byte, represents a command line
type CmdLine = [][]byte

type payload struct {
cmdLine CmdLine
dbIndex int
wg *sync.WaitGroup
}

NewPersister 新建一个 AOF 持久化工作协程

数据库可以利用 NewPersister 方法新建一个 AOF 持久化进程进行 AOF 持久化。

1
2
// NewPersister creates a new aof.Persister
func NewPersister(db database.DBEngine, filename string, load bool, fsync string, tmpDBMaker func() database.DBEngine) (*Persister, error)

其流程如下:

  • 新建一个 Persister,填写 AOF 文件名、刷盘策略、数据库等信息。
1
2
3
4
5
6
persister := &Persister{}
persister.aofFilename = filename
persister.aofFsync = strings.ToLower(fsync)
persister.db = db
persister.tmpDBMaker = tmpDBMaker
persister.currentDB = 0
  • 如果需要加载 AOF 文件中已有的数据,则执行 persister.LoadAof 方法加载命令。
1
2
3
if load {
persister.LoadAof(0)
}
  • 打开 AOF 持久化文件,初始化监听命令的通道等。
1
2
3
4
5
6
7
8
aofFile, err := os.OpenFile(persister.aofFilename, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return nil, err
}
persister.aofFile = aofFile
persister.aofChan = make(chan *payload, aofQueueSize)
persister.aofFinished = make(chan struct{})
persister.listeners = make(map[Listener]struct{})
  • 开启一个协程,用于监听 aofChan,将命令写入到 AOF 文件中
1
2
3
go func() {
persister.listenCmd()
}()
  • 如果刷盘策略为 FsyncEverySec,则开启一个协程用于每秒钟刷盘操作
1
2
3
4
5
6
7
ctx, cancel := context.WithCancel(context.Background())
persister.ctx = ctx
persister.cancel = cancel
if persister.aofFsync == FsyncEverySec {
persister.fsyncEverySecond()
}
return persister, nil

fsyncEverySecond 每秒钟的刷盘操作

persister.fsyncEverySecond 方法用于每秒钟的刷盘操作,它每过一秒钟执行一次 persister.aofFile.Sync,将内存中的数据刷入到磁盘中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (persister *Persister) fsyncEverySecond() {
ticker := time.NewTicker(time.Second)
go func() {
for {
select {
case <-ticker.C:
persister.pausingAof.Lock()
if err := persister.aofFile.Sync(); err != nil {
logger.Errorf("fsync failed: %v", err)
}
persister.pausingAof.Unlock()
case <-persister.ctx.Done():
return
}
}
}()
}
阅读全文 »

Pipeline 模式

通常 TCP 客户端的通信模式都是阻塞式的:客户端发送请求 -> 等待服务端响应 -> 发送下一个请求。因为需要等待网络传输数据,完成一次请求循环需要等待较多时间。

针对这种效率低的情景,可以不等待服务端响应直接发送下一条请求

TCP 协议会保证数据流的有序性,同一个 TCP 连接上先发送的请求服务端先接收,先回复的响应客户端先收到。因此不必担心混淆响应所对应的请求。

这种在服务端未响应时客户端继续向服务端发送请求的模式称为 Pipeline 模式。因为减少等待网络传输的时间,Pipeline 模式可以极大的提高吞吐量。

Godis 客户端

Pipeline 模式的 Godis 客户端需要至少有两个后台协程,分别是发送请求协程(写协程)读取响应协程(读协程)。调用方通过 channel 向后台协程发送发送指令,并阻塞等待直到收到响应(或者超时)。

Client 结构

首先定义 Client 客户端,Client 客户端实现 pipeline 的核心在于两个通道

  • pendingReqs:记录等待发送的请求,客户端调用 Send 命令向客户端发送请求时,请求在这个通道内排队等待写协程发送请求
  • waitingReqs:记录等待服务器响应的请求,向服务器发送请求成功后将这个请求加入到这个通道中等待响应。当读协程收到一个服务器响应时就从通道中取出一个请求,此时一个完整的请求+响应完成。
1
2
3
4
5
6
7
8
9
10
type Client struct {
conn net.Conn // 与服务端的 tcp 连接
pendingReqs chan *Request // 等待发送的请求
waitingReqs chan *Request // 等待服务器响应的请求
ticker *time.Ticker // 用于触发心跳包的计时器
addr string

status int32 // 服务器状态(创建/运行/关闭)
working *sync.WaitGroup // 有请求正在处理不能立即停止,用于实现 graceful shutdown
}

接着定义客户端的请求 Request:

1
2
3
4
5
6
7
8
type Request struct {
id uint64 // 请求id
args [][]byte // 上行参数
reply redis.Reply // 收到的返回值
heartbeat bool // 标记是否是心跳请求
waiting *wait.Wait // 调用协程发送请求后通过 waitgroup 等待请求异步处理完成
err error
}

wait.Wait

客户端请求中 waiting 属性的类型为 wait.Wait,实际上就是 sync.WaitGroup 加上了超时功能。

1
2
3
4
// Wait is similar with sync.WaitGroup which can wait with timeout
type Wait struct {
wg sync.WaitGroup
}

wait.Wait 和 sync.WaitGroup 一样也有 Add、Done、Wait 方法。不同的是,它额外有一个 WaitWithTimeout 方法,这个方法阻塞 WaitGroup 直到计数器为零或者超时。其实现的方法是:

  • 定义一个管道,用于接收完成信号
  • 开启一个协程,调用 Wait 阻塞协程, WaitGroup 计数器为零。当计数器为零时向管道中发送信号,表明完成任务。
  • 使用 Select 阻塞,当超时或者完成任务(在管道中检测到信号)时,方法结束阻塞返回
阅读全文 »

A Simple yet Effective Relation Information Guided Approach for Few-Shot Relation Extraction

年份:2022

会议:ACL

作者:Yang Liu, Jinpeng Hu, Xiang wan, Tsung-Hui Chang

机构:The Chinese University of Hong Kong

数据集:FewRel 1.0

motivation:在 few-shot 关系抽取任务上应用原型网络有两个限制,因此论文提出了一种直接而有效的方法将关系信息融合进网络之中。

  • 局限一:现有模型中的大多数都采用了隐式约束,如对比学习或者关系图,而不是直接融合关系信息到网络中。这会导致在面对 remote samples 时,表现较弱。
  • 局限二:它们通常采用复杂的设计或网络,如混合特征或精心设计的注意力网络,这可能会带来太多甚至是有害的参数。
  • 论文中的具体做法是:
    • 使用相同的编码器来编码关系信息和句子,并将它们映射到相同的语义空间中。
    • 通过连接两个关系视图(比如 CLS token embedding 和所有 tokens 的平均)来为每个关系类生成关系表示 relation representation,这使得关系表示和原型有相同的维度。然后,将生成的关系表示直接与原型相加

image-20230108113527122

GitHub:https://github.com/lylylylylyly/SimpleFSRE

因为关系抽取数据集人工标注困难,所以开始研究 Few-Shot Relation Extraction(FSRE)。这个任务在现有关系的大规模数据集上进行训练,然后快速迁移到新关系类型的少量数据上。

原型网络(Prototypical Networks)是 Few-Shot 的方法,就是先把样本投影到一个空间,计算每个样本类别的中心,在分类的时候,通过对比目标到每个中心的距离,从而分析出目标的类别。

image-20230108111547412

原型网络具体实现就是:

  • 首先是把输入投影到新的特征空间,通过神经网络,把输入转化为一个新的特征向量,使得同一类的向量之间的距离比较接近,不同类的向量距离比较远。
  • 计算每个类别的均值表示该类的原型。

针对现有方法在 RE 上应用原型网络的两个缺陷,论文提出通过连接两个关系视图(比如 CLS token embedding 和所有 tokens 的平均)来为每个关系类生成关系表示,这使得关系表示和原型有相同的维度。然后,在训练和预测时将生成的关系表示直接与原型相加。至于为什么直接相加的方法适用于 few-shot RE,作者是这样解释的:

  • 直接相加更具有鲁棒性,在面对 remote samples 时。
  • 直接相加不会带来额外的参数,简化了模型。由于过拟合的问题,较少的参数总是比较多的参数更好,特别是对于较少数据量的任务。

模型结构

最重要的是以下两个部分:

  • 为了将句子和关系信息的表示映射到同一语义空间中,使用了共享句子编码器

  • 接着连接两个关系表示的两个视图,用于得到与原型相同的维度。通过直接加法,将关系表示集成到原始原型中

image-20230108114447540

阅读全文 »