<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>学习笔记归档 - 枫阿雨&#039;s blog</title>
	<atom:link href="https://www.crazyfay.com/category/study-notes/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.crazyfay.com/category/study-notes/</link>
	<description>CrazyFay</description>
	<lastBuildDate>Sun, 28 May 2023 06:27:16 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.5.2</generator>

<image>
	<url>https://www.crazyfay.com/wp-content/uploads/2023/04/cropped-DockerGopher-32x32.png</url>
	<title>学习笔记归档 - 枫阿雨&#039;s blog</title>
	<link>https://www.crazyfay.com/category/study-notes/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>面向对象设计模式与Go语言实现 &#8211; 创建型模式</title>
		<link>https://www.crazyfay.com/2023/05/28/%e8%bd%af%e4%bb%b6%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e5%88%9b%e5%bb%ba%e5%9e%8b%e6%a8%a1%e5%bc%8f/</link>
					<comments>https://www.crazyfay.com/2023/05/28/%e8%bd%af%e4%bb%b6%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e5%88%9b%e5%bb%ba%e5%9e%8b%e6%a8%a1%e5%bc%8f/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Sun, 28 May 2023 05:17:53 +0000</pubDate>
				<category><![CDATA[学习笔记]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[设计模式]]></category>
		<guid isPermaLink="false">https://www.crazyfay.com/?p=349</guid>

					<description><![CDATA[<p>创建型模式（Creational Patterns） 设计模式概览 创建型模式（Creational Patt [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/05/28/%e8%bd%af%e4%bb%b6%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e5%88%9b%e5%bb%ba%e5%9e%8b%e6%a8%a1%e5%bc%8f/">面向对象设计模式与Go语言实现 &#8211; 创建型模式</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<h2>创建型模式（Creational Patterns）</h2>
<h3>设计模式概览</h3>
<ul>
<li>
<p><strong>创建型模式（Creational Patterns）：</strong></p>
<ol>
<li>
<p><strong>单例模式（Singleton）</strong></p>
</li>
<li>
<p><strong>工厂方法模式（Factory Method）</strong></p>
</li>
<li>
<p><strong>抽象工厂模式（Abstract Factory）</strong></p>
</li>
<li>
<p><strong>建造者模式（Builder）</strong></p>
</li>
<li>
<p><strong>原型模式（Prototype）</strong></p>
</li>
</ol>
</li>
<li>
<p>结构型模式（Structural Patterns）：</p>
<ol start="6">
<li>
<p>适配器模式（Adapter）</p>
</li>
<li>
<p>桥接模式（Bridge）</p>
</li>
<li>
<p>组合模式（Composite）</p>
</li>
<li>
<p>装饰者模式（Decorator）</p>
</li>
<li>
<p>外观模式（Facade）</p>
</li>
<li>
<p>享元模式（Flyweight）</p>
</li>
<li>
<p>代理模式（Proxy）</p>
</li>
</ol>
</li>
<li>
<p>行为型模式（Behavioral Patterns）：</p>
<ol start="13">
<li>
<p>责任链模式（Chain of Responsibility）</p>
</li>
<li>
<p>命令模式（Command）</p>
</li>
<li>
<p>解释器模式（Interpreter）</p>
</li>
<li>
<p>迭代器模式（Iterator）</p>
</li>
<li>
<p>中介者模式（Mediator）</p>
</li>
<li>
<p>备忘录模式（Memento）</p>
</li>
<li>
<p>观察者模式（Observer）</p>
</li>
<li>
<p>状态模式（State）</p>
</li>
<li>
<p>策略模式（Strategy）</p>
</li>
<li>
<p>模板方法模式（Template Method）</p>
</li>
<li>
<p>访问者模式（Visitor）</p>
</li>
</ol>
</li>
</ul>
<h3>0. 简单工厂模式</h3>
<blockquote>
<p>简单工厂模式（Simple Factory Pattern）</p>
<p>又叫做静态工厂方法模式（Static Factory Method Pattern），是一种对象创建型模式，并不属于GoF 的23种设计模式之一，但是是学习其他工厂模式的的基础。它定义了一个工厂类，用于创建不同类型的对象。简单来说，简单工厂模式就是将对象的创建过程封装起来，使得客户端可以通过一个工厂类来创建不同类型的对象，而无需知道具体的创建过程。</p>
<p>实现简单工厂，通常需要定义一个工厂方法来创建不同类型的对象。这个工厂方法通常是一个静态方法，它接收一个参数来指定要创建的对象类型，并返回一个实例化的对象。</p>
</blockquote>
<pre><code class="language-go">type MemTableType = int8
const (
    Btree MemTableType = iota
    ART
)

type MemTable interface {
    // Put Stores the Pos information for key pairs in the index
    Put(key, value []byte) bool

    // Get Retrieve the Pos information based on the key
    Get(key []byte) []byte

    // Del Delete the Pos information based on the key
    Del(key []byte) bool
}

type BTree struct {
    tree *btree.BTree
    lock *sync.RWMutex
}

// NewBTree Init BTree struct
func NewBTree() *BTree {
    return &amp;BTree{
        tree: btree.New(32),
        lock: new(sync.RWMutex),
    }
}

type AdaptiveRadixTree struct {
    tree art.Tree
    lock *sync.RWMutex
}

func NewAdaptiveRadixTree() *AdaptiveRadixTree {
    return &amp;AdaptiveRadixTree{
        tree: art.New(),
        lock: new(sync.RWMutex),
    }
}

func NewMemTable(typ MemTableType) MemTable {
    switch typ {
    case Btree:
        return NewBTree()
    case ART:
        return NewAdaptiveRadixTree()
    default:
        return NewBTree()
    }
}
func main()  {
    memTable := NewMemTable(Btree)
    memTable.Put([]byte(&quot;key&quot;), []byte(&quot;value&quot;))
}</code></pre>
<h3>1. 工厂方法模式</h3>
<blockquote>
<p><strong>工厂方法模式（Factory Method Pattern）</strong></p>
<p><strong>工厂方法模式也被称为虚拟构造器模式（Virtual Constructor Pattern）或多态工厂模式（Polymorphic Factory Pattern）</strong></p>
<p><strong>简单工厂模式虽然简单，但存在一个很严重的问题，违背了开闭原则。工厂方法模式，继承了简单工厂模式的优点，同时做出了修改以达到符合开闭原则的要求。在工厂方法模式中，不再提供一个统一的工厂类来创建所有的产品对象，而是针对不同的产品提供不同的工厂，系统提供一个与产品等级结构对应的工厂等级结构。</strong></p>
<p><strong>在很多场合有其实际应用，这种模式主要用于创建复杂对象，创建对象的逻辑可能包含一些业务需要的约束。</strong></p>
<p><strong>假设我们正在开发一个日志记录系统，这个系统可以将日志记录到不同的地方，例如，控制台，文件，或者是远程服务器。我们可以使用工厂方法模式来创建适合不同场景的日志记录器。</strong></p>
</blockquote>
<pre><code class="language-go">// Logger 是所有日志记录器的接口
type Logger interface {
    Log(message string)
}

// ConsoleLogger 是 Logger 的一个实现，它将日志记录到控制台
type ConsoleLogger struct{}

func (l ConsoleLogger) Log(message string) {
    fmt.Println(&quot;Console logger: &quot;, message)
}

// FileLogger 是 Logger 的一个实现，它将日志记录到文件
type FileLogger struct{}

func (l FileLogger) Log(message string) {
    fmt.Println(&quot;File logger: &quot;, message)
}

// LoggerFactory 是所有日志记录器工厂的接口
type LoggerFactory interface {
    CreateLogger() Logger
}

// ConsoleLoggerFactory 是 LoggerFactory 的一个实现，它创建 ConsoleLogger
type ConsoleLoggerFactory struct{}

func (f ConsoleLoggerFactory) CreateLogger() Logger {
    return ConsoleLogger{}
}

// FileLoggerFactory 是 LoggerFactory 的一个实现，它创建 FileLogger
type FileLoggerFactory struct{}

func (f FileLoggerFactory) CreateLogger() Logger {
    return FileLogger{}
}

func main() {
    var factory LoggerFactory

    factory = ConsoleLoggerFactory{}
    logger := factory.CreateLogger()
    logger.Log(&quot;This is a message&quot;)

    factory = FileLoggerFactory{}
    logger = factory.CreateLogger()
    logger.Log(&quot;This is another message&quot;)
}
</code></pre>
<h3>2. 抽象工厂模式</h3>
<blockquote>
<p><strong>抽象工厂模式（Abstract Factory Pattern）</strong></p>
<p><strong>抽象工厂模式是一种创建型设计模式，它提供了一种在不指定具体类的情况下创建一系列相关或相互依赖的对象的接口。抽象工厂允许你根据需求切换不同的具体工厂实现，以便在运行时根据配置或其他条件创建适当的对象。</strong><br />
<strong>简单来说，抽象工厂模式就是将多个工厂类的接口进行抽象，然后再用一个工厂类来封装这些工厂类的接口。在创建具体对象时，我们通常需要使用其他对象或者数据结构，因此我们还需要定义一些相关的产品接口和产品结构体。</strong></p>
</blockquote>
<pre><code class="language-go">// User 表示用户实体
type User struct {
    ID   int
    Name string
}

// UserDAO 定义了操作用户的接口
type UserDAO interface {
    GetUser(id int) (*User, error)
    SaveUser(*User) error
    DeleteUser(id int) error
}

// DBFactory 定义了抽象工厂接口
type DBFactory interface {
    CreateUserDAO() UserDAO
}

// MysqlUserDAO 实现了UserDAO接口，提供了对MySQL数据库的操作
type MysqlUserDAO struct{}

func (dao *MysqlUserDAO) GetUser(id int) (*User, error) {
    // 实现从MySQL数据库获取用户的具体操作
    // ...
    fmt.Println(&quot;Get user from MySQL database&quot;)
    return &amp;User{}, nil
}

func (dao *MysqlUserDAO) SaveUser(u *User) error {
    // 实现保存用户到MySQL数据库的具体操作
    // ...
    fmt.Println(&quot;Save user to MySQL database&quot;)
    return nil
}

func (dao *MysqlUserDAO) DeleteUser(id int) error {
    // 实现从MySQL数据库删除用户的具体操作
    // ...
    fmt.Println(&quot;Delete user from MySQL database&quot;)
    return nil
}

// MysqlFactory 实现了DBFactory接口，用于创建MysqlUserDAO
type MysqlFactory struct{}

func (f *MysqlFactory) CreateUserDAO() UserDAO {
    return &amp;MysqlUserDAO{}
}

// MongoUserDAO 实现了UserDAO接口，提供了对MongoDB数据库的操作
type MongoUserDAO struct{}

func (dao *MongoUserDAO) GetUser(id int) (*User, error) {
    // 实现从MongoDB数据库获取用户的具体操作
    // ...
    fmt.Println(&quot;Get user from MongoDB database&quot;)
    return &amp;User{}, nil
}

func (dao *MongoUserDAO) SaveUser(u *User) error {
    // 实现保存用户到MongoDB数据库的具体操作
    // ...
    fmt.Println(&quot;Save user to MongoDB database&quot;)
    return nil
}

func (dao *MongoUserDAO) DeleteUser(id int) error {
    // 实现从MongoDB数据库删除用户的具体操作
    // ...
    fmt.Println(&quot;Delete user from MongoDB database&quot;)
    return nil
}

// MongoFactory 实现了DBFactory接口，用于创建MongoUserDAO
type MongoFactory struct{}

func (f *MongoFactory) CreateUserDAO() UserDAO {
    return &amp;MongoUserDAO{}
}

func main() {
    mysqlFactory := &amp;MysqlFactory{}
    mongoFactory := &amp;MongoFactory{}

    userDAO := mysqlFactory.CreateUserDAO()
    userDAO.GetUser(1)
    userDAO.SaveUser(&amp;User{})
    userDAO.DeleteUser(1)

    userDAO = mongoFactory.CreateUserDAO()
    userDAO.GetUser(1)
    userDAO.SaveUser(&amp;User{})
    userDAO.DeleteUser(1)
}</code></pre>
<p>工厂方法模式和抽象工厂模式都属于创建型设计模式，它们都是用来创建对象的。但是，两者有一些重要的区别：</p>
<p><strong>工厂方法模式（Factory Method Pattern）：</strong></p>
<ul>
<li>工厂方法模式中的每个工厂只创建一种产品。</li>
<li>工厂方法模式主要用于创建一种类型的对象，但是这个对象的构造过程可能比较复杂。</li>
<li>在工厂方法模式中，客户端通常只需要知道具体工厂的接口，而不需要知道具体工厂类的类型。具体工厂类的类型可以在运行时通过配置或者其他方式决定。</li>
<li>例如，在上面的日志记录器的例子中，我们有一个日志记录器的工厂接口和多个实现这个接口的工厂类，每个工厂类都用于创建一种特定的日志记录器。</li>
</ul>
<p><strong>抽象工厂模式（Abstract Factory Pattern）：</strong></p>
<ul>
<li>抽象工厂模式中的每个工厂可以创建多种类型的产品。</li>
<li>抽象工厂模式主要用于创建相关的或者是依赖的对象组。对象组中的每个对象都是一个产品，但是这些产品需要一起工作以完成一些更大的功能。</li>
<li>在抽象工厂模式中，客户端通常只需要知道具体工厂的接口，而不需要知道具体工厂类的类型。具体工厂类的类型可以在运行时通过配置或者其他方式决定。</li>
<li>例如，在上面的数据库访问的例子中，我们有一个数据库访问对象的工厂接口和多个实现这个接口的工厂类，每个工厂类都用于创建一组相关的数据库访问对象，如用户数据访问对象，订单数据访问对象等。</li>
</ul>
<p>总的来说，如果你只需要创建一种类型的产品，那么使用工厂方法模式可能更简单；如果你需要创建多种类型的相关产品，那么使用抽象工厂模式可能更合适。</p>
<h3>3. 建造者模式</h3>
<blockquote>
<p><strong>建造者模式（Builder Pattern）</strong></p>
<p><strong>建造者模式是一种对象创建型模式，它可以将复杂对象的构建过程与表示分离开来，使得相同的构建过程可以创建不同的表示。</strong></p>
<p><strong>简单来说，建造者模式就是将一个复杂对象的创建过程基于链式调用封装起来，使得这个过程可以有不同的表示方式。</strong></p>
</blockquote>
<pre><code class="language-go">// RequestBuilder 是网络请求的建造者接口
type RequestBuilder interface {
    SetURL(url string) RequestBuilder
    SetMethod(method string) RequestBuilder
    SetHeader(key, value string) RequestBuilder
    SetBody(body string) RequestBuilder
    Build() *Request
}

// Request 是网络请求对象
type Request struct {
    URL     string
    Method  string
    Headers map[string]string
    Body    string
}

// HTTPRequestBuilder 是 HTTP 请求的具体建造者
type HTTPRequestBuilder struct {
    request *Request
}

// SetURL 设置请求的 URL
func (b *HTTPRequestBuilder) SetURL(url string) RequestBuilder {
    b.request.URL = url
    return b
}

// SetMethod 设置请求的方法
func (b *HTTPRequestBuilder) SetMethod(method string) RequestBuilder {
    b.request.Method = method
    return b
}

// SetHeader 设置请求的头信息
func (b *HTTPRequestBuilder) SetHeader(key, value string) RequestBuilder {
    b.request.Headers[key] = value
    return b
}

// SetBody 设置请求的消息体
func (b *HTTPRequestBuilder) SetBody(body string) RequestBuilder {
    b.request.Body = body
    return b
}

// Build 构建网络请求对象
func (b *HTTPRequestBuilder) Build() *Request {
    return b.request
}

// NewHTTPRequestBuilder 创建 HTTP 请求的建造者
func NewHTTPRequestBuilder() RequestBuilder {
    return &amp;HTTPRequestBuilder{
        request: &amp;Request{
            Headers: make(map[string]string),
        },
    }
}

func main() {
    // 创建一个 HTTP 请求的建造者
    builder := NewHTTPRequestBuilder()

    // 使用建造者设置请求的属性
    request := builder.
        SetURL(&quot;https://api.example.com&quot;).
        SetMethod(&quot;GET&quot;).
        SetHeader(&quot;Authorization&quot;, &quot;Bearer token&quot;).
        SetBody(&quot;data&quot;).
        Build()

    // 打印请求对象
    fmt.Println(request)
}</code></pre>
<h3>4. 原型模式</h3>
<blockquote>
<p><strong>原型模式（Prototype Pattern）</strong></p>
<p><strong>原型模式是一种创建型设计模式，它允许通过克隆现有对象来创建新对象，而无需通过实例化和配置新对象来完成。在原型模式中，我们创建一个原型对象，然后通过复制该对象来创建新的对象，而不是使用常规的构造函数和初始化流程。</strong></p>
<p><strong>应该注意原型模式不是用来获得性能优势的。它仅用于从原型实例创建新对象！</strong></p>
<p><strong>原型模式的实现需要满足以下要素：</strong></p>
<ul>
<li>
<p><strong>定义一个原型对象，该对象实现了 Clone() 方法，用于克隆自身并返回新的克隆对象。</strong></p>
</li>
<li>
<p><strong>定义一个工厂函数或方法，用于创建原型对象并初始化其属性。</strong></p>
</li>
<li>
<p><strong>客户端代码可以通过调用原型对象的 Clone() 方法来创建新的对象，而不是直接调用构造函数或工厂函数。</strong></p>
</li>
</ul>
</blockquote>
<pre><code class="language-go">// HTTPRequest 是HTTP请求的原型对象
type HTTPRequest struct {
    Method  string
    URL     string
    Headers map[string]string
    Body    []byte
}

// Clone 通过复制原型对象创建新的HTTP请求对象
func (r *HTTPRequest) Clone() *HTTPRequest {
    clone := *r
    return &amp;clone
}

// HTTPRequestPrototype 是HTTP请求的原型管理器
type HTTPRequestPrototype struct {
    prototypes map[string]*HTTPRequest
}

// Register 注册HTTP请求原型对象
func (p *HTTPRequestPrototype) Register(name string, request *HTTPRequest) {
    p.prototypes[name] = request
}

// Retrieve 根据名称从原型管理器中检索HTTP请求原型对象
func (p *HTTPRequestPrototype) Retrieve(name string) *HTTPRequest {
    prototype, ok := p.prototypes[name]
    if !ok {
        return nil
    }
    return prototype.Clone()
}

func main() {
    // 创建HTTP请求原型管理器
    prototypeManager := &amp;HTTPRequestPrototype{
        prototypes: make(map[string]*HTTPRequest),
    }

    // 注册不同的HTTP请求原型对象
    prototypeManager.Register(&quot;service1&quot;, &amp;HTTPRequest{
        Method: &quot;GET&quot;,
        URL:    &quot;http://service1.example.com&quot;,
        Headers: map[string]string{
            &quot;Content-Type&quot;: &quot;application/json&quot;,
        },
    })

    prototypeManager.Register(&quot;service2&quot;, &amp;HTTPRequest{
        Method: &quot;POST&quot;,
        URL:    &quot;http://service2.example.com&quot;,
        Headers: map[string]string{
            &quot;Content-Type&quot;: &quot;application/xml&quot;,
        },
        Body: []byte(`&lt;data&gt;Hello, Service 2&lt;/data&gt;`),
    })

    // 启动中介服务
    http.HandleFunc(&quot;/&quot;, func(w http.ResponseWriter, r *http.Request) {
        // 根据请求路径选择对应的HTTP请求原型对象
        name := r.URL.Path[1:] // 从路径中获取名称，例如 &quot;/service1&quot; 将匹配到 &quot;service1&quot;
        requestPrototype := prototypeManager.Retrieve(name)
        if requestPrototype == nil {
            http.NotFound(w, r)
            return
        }

        // 复制原型对象，并根据实际请求进行自定义修改
        request := requestPrototype.Clone()
        request.URL = fmt.Sprintf(&quot;%s%s&quot;, request.URL, r.URL.Path)

        // 发送请求并获取响应
        client := &amp;http.Client{}
        httpRequest, err := http.NewRequest(request.Method, request.URL, nil)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        // 设置请求标头
        for key, value := range request.Headers {
            httpRequest.Header.Set(key, value)
        }

        // 发送请求
        response, err := client.Do(httpRequest)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        defer response.Body.Close()

        // 读取响应内容
        body, err := ioutil.ReadAll(response.Body)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        // 将响应内容写入到HTTP响应中
        w.Header().Set(&quot;Content-Type&quot;, response.Header.Get(&quot;Content-Type&quot;))
        w.Write(body)
    })

    // 启动HTTP服务器，监听端口
    err := http.ListenAndServe(&quot;:8080&quot;, nil)
    if err != nil {
        fmt.Println(&quot;Failed to start server:&quot;, err)
    }
}</code></pre>
<h3>5. 单例模式</h3>
<blockquote>
<p><strong>单例模式（Singleton Pattern）</strong></p>
<p><strong>顾名思义,指的就是一个类只能生成一个实例，且该类能自行创建这个实例的一种模式。</strong><br />
<strong>在服务的生命周期内，一个类生成的一个实例对象只能存在一个，调用时复用该单例对象即可，这样既节省了内存空间,也节省了创建新对象时的资源消耗，也便于资源管理。</strong></p>
<p><strong>在日常开发中,很多场景其实都可以被设计成单例，像线程池、全局日志、数据库等等，单例模式总结下来有几个特点:</strong></p>
<ul>
<li><strong>单例类只有一个实例对象；</strong></li>
<li><strong>该单例对象必须由单例类自行创建；</strong></li>
<li><strong>单例类对外提供一个访问该单例的全局访问点</strong></li>
</ul>
</blockquote>
<p>单例模式一般有两种实现方式</p>
<p><strong>饿汉式（Eager Initialization）：</strong></p>
<ul>
<li>
<p>在程序启动时或单例类被加载时，就立即创建并初始化单例实例。</p>
</li>
<li>
<p>单例实例在整个程序生命周期中都存在，无论是否使用。</p>
</li>
<li>
<p>线程安全，因为在实例创建时就完成了初始化。</p>
</li>
<li>
<p>示例代码：</p>
<pre><code class="language-go">type singleton struct {
}

var _instance *singleton

func init() {
_instance = &singleton{}
}

func GetInstance() *singleton {
return _instance
}</code></pre>
</li>
</ul>
<p><strong>懒汉式（Lazy Initialization）：</strong></p>
<ul>
<li>
<p>在第一次调用获取单例实例的方法时，才创建并初始化单例实例。</p>
</li>
<li>
<p>单例实例是按需创建的，可能存在多个线程同时请求获取单例实例的情况。</p>
</li>
<li>
<p>需要考虑线程安全性，通常使用锁或其他同步机制来保证只有一个实例被创建。</p>
</li>
<li>
<p>示例代码：</p>
<pre><code class="language-go">type singleton struct {
}

var _instance *singleton
var once sync.Once

// GetInstance lazy
func GetInstance() *singleton {
once.Do(func() {
    _instance = &singleton{}
})
return _instance
}</code></pre>
</li>
</ul>
<h3>extra 配置模式</h3>
<blockquote>
<p>选项模式（Options Pattern）是一种常用的设计模式，得益于对函数式编程的支持，选项模式Go语言中经常使用。选项模式用于在函数或结构体中设置多个可选参数，以便在调用时提供更灵活的配置选项。</p>
</blockquote>
<pre><code class="language-go">type Options struct {
   Path    string
   MaxSize int
   Sync    bool
}

// DefaultOptions 返回默认配置选项
func DefaultOptions() *Options {
   return _defaultConf
}

var _defaultConf = &amp;Options{
   Path:    &quot;/temp/opt&quot;,
   MaxSize: 100,
   Sync:    true,
}

// NewOptions 处理配置选项
func NewOptions(opts ...func(*Options)) *Options {
   // 获取默认配置选项
   options := _defaultConf

   // 遍历传入的选项函数列表，逐个应用选项函数
   for _, opt := range opts {
      opt(options)
   }

   return options
}

func WithPath(value string) func(*Options) {
   return func(opts *Options) {
      opts.Path = value
   }
}

func WithMaxSize(value int) func(*Options) {
   return func(opts *Options) {
      opts.MaxSize = value
   }
}

func WithSync(value bool) func(*Options) {
   return func(opts *Options) {
      opts.Sync = value
   }
}

func main() {
   // 使用选项模式调用函数
   opt := NewOptions(
      WithPath(&quot;custom value&quot;),
      WithMaxSize(200),
      WithSync(false),
   )

   fmt.Println(opt)
}</code></pre>
<hr />
<p><img decoding="async" src="https://www.crazyfay.com/wp-content/uploads/2023/05/工厂方法.png" alt="工厂方法模式" /><br />
<img decoding="async" src="https://www.crazyfay.com/wp-content/uploads/2023/05/抽象工厂.png" alt="抽象工厂模式" /><br />
<img decoding="async" src="https://www.crazyfay.com/wp-content/uploads/2023/05/生成器.png" alt="建造者模式" /><br />
<img decoding="async" src="https://www.crazyfay.com/wp-content/uploads/2023/05/原型.png" alt="原型模式" /><br />
<img decoding="async" src="https://www.crazyfay.com/wp-content/uploads/2023/05/单例.png" alt="单例模式" /></p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/05/28/%e8%bd%af%e4%bb%b6%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e5%88%9b%e5%bb%ba%e5%9e%8b%e6%a8%a1%e5%bc%8f/">面向对象设计模式与Go语言实现 &#8211; 创建型模式</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2023/05/28/%e8%bd%af%e4%bb%b6%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e5%88%9b%e5%bb%ba%e5%9e%8b%e6%a8%a1%e5%bc%8f/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>面向对象设计模式与Go语言实现 – 面向对象设计原则</title>
		<link>https://www.crazyfay.com/2023/05/27/%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e5%8e%9f/</link>
					<comments>https://www.crazyfay.com/2023/05/27/%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e5%8e%9f/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Sat, 27 May 2023 02:13:22 +0000</pubDate>
				<category><![CDATA[学习笔记]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[设计模式]]></category>
		<guid isPermaLink="false">https://www.crazyfay.com/?p=353</guid>

					<description><![CDATA[<p>本系列文章是针对设计模式的系列文章，网上的很多设计模式的文章只讲了个大概和一部分纯理论，示例代码也是实际场景中 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/05/27/%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e5%8e%9f/">面向对象设计模式与Go语言实现 – 面向对象设计原则</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p>本系列文章是针对设计模式的系列文章，网上的很多设计模式的文章只讲了个大概和一部分纯理论，示例代码也是实际场景中根本遇不到，往往只会出现在课本中的无生产意义的代码模型。本系列文章则希望在讲述设计模式的同时，采用更贴合生产实践中的代码，在实战中学习设计模式的相关概念，理解设计模式的精髓。</p>
<h2>面向对象设计原则</h2>
<p>关于面向对象设计原则众说纷纭，有人说是6种有人说是7种，但无论有几种实际上内容都是一致的。</p>
<p>本文将基于 亚历山大 · 什韦茨（Alexander Shvets）的《深入设计模式》中的分类，介绍面软件设计原则。</p>
<h3>1. 封装</h3>
<p>尽可能的封装变化的内容，即找到程序中的变化内容并将其与不变的内容区分开。该原则的主要目的是将变更造成的影响最小化。</p>
<p>可进行方法层面的封装与类层面的封装</p>
<h3>2. 面向接口</h3>
<p>面向接口进行开发， 而不是面向实现； 依赖于抽象类型，而不是具体类。</p>
<p>如果无需修改已有代码就能轻松对类进行扩展，那就可以说这样的设计是灵活的。</p>
<p>当你需要两个类进行合作时，可以让其中一个类依赖于另一个类。但是，还有另外一种更灵活的方式来设置对象之间的合作关系。</p>
<ol>
<li>确定一个对象对另一对象的确切需求：它需执行哪些方法？</li>
<li>在一个新的接口或抽象类中描述这些方法。</li>
<li>让被依赖的类实现该接口。</li>
<li>现在让有需求的类依赖于这个接口， 而不依赖于具体的类。你仍可与原始类中的对象进行互动，但现在其连接将会灵活得多。</li>
</ol>
<h3>3. 组合</h3>
<p>组合优于继承。继承可能是类之间最明显、最简便的代码复用方式。如果你有两个代码相同的类， 就可以为它们创建一个通用的基类，<br />
然后将相似的代码移动到其中。轻而易举！</p>
<p>不过，继承这件事通常只有在程序中已包含大量类，且修改任何东西都非常困难时才会引起关注。下面就是此类问题的清单。</p>
<ul>
<li><strong>子类不能减少超类的接口。</strong>你必须实现父类中所有的抽象方法，即使它们没什么用。</li>
<li><strong>在重写方法时，你需要确保新行为与其基类中的版本兼容。</strong>这一点很重要，因为子类的所有对象都可能被传递给以超类<br />
对象为参数的任何代码，相信你不会希望这些代码崩溃的。</li>
<li>继承打破了超类的封装，因为子类拥有访问父类内部详细内容的权限。此外还可能会有相反的情况出现，那就是程序员为了进一步扩展的方便而让超类知晓子类的内部详细内容。</li>
<li><strong>子类与超类紧密耦合。</strong>超类中的任何修改都可能会破坏子类的功能。</li>
<li><strong>通过继承复用代码可能导致平行继承体系的产生。</strong>继承通常仅发生在一个维度中。只要出现了两个以上的维度，你就必须创建数量巨大的类组合，从而使类层次结构膨胀到不可思议的程度。</li>
</ul>
<p>好在Golang在语言层面就解决了这个问题，因为Go语言中没有继承 XD</p>
<h3>4. SOLID原则</h3>
<p>SOLID 的五条原则是在罗伯特·马丁的著作《敏捷软件开发：原则、模式与实践》中首次提出的。SOLID 是让软件设计更易于理解、更加灵活和更易于维护的五个原则的简称。</p>
<h4>S：单一职责原则</h4>
<blockquote>
<p>单一职责原则（Single Responsibility Principle）</p>
<p>修改一个类的原因只能有一个。</p>
</blockquote>
<p>尽量让每个类只负责软件中的一个功能，并将该功能完全封装（你也可称之为隐藏）在该类中。<br />
这条原则的主要目的是减少复杂度。你不需要费尽心机地去构思如何仅用200 行代码来实现复杂设计，实际上完全可以使用十几个清晰的方法。<br />
当程序规模不断扩大、变更不断增加后，真实问题才会逐渐显现出来。到了某个时候，类会变得过于庞大，以至于你无法记住其细节。查找代码将变得非常缓慢，你必须浏览整个类，甚至整个程序才能找到需要的东西。程序中实体的数量会让你的大脑堆栈过载，你会感觉自己对代码失去了控制。<br />
还有一点：如果类负责的东西太多，那么当其中任何一件事发生改变时，你都必须对类进行修改。而在进行修改时，你就有可能改动类中自己并不希望改动的部分。</p>
<h4>O：开闭原则</h4>
<blockquote>
<p>开闭原则（open/closed Principle）</p>
<p>对于扩展， 类应该是“开放”的； 对于修改， 类则应是“封闭”的。</p>
</blockquote>
<p>本原则的主要理念是在实现新功能时能保持已有代码不变。</p>
<p>如果你可以对一个类进行扩展，可以创建它的子类并对其做任何事情（如新增方法或成员变量、重写基类行为等）， 那么它就是开放的。有些编程语言允许你通过特殊关键字（例如final ） 来限制对于类的进一步扩展， 这样类就不再是“开放”的了。如果某个类已做好了充分的准备并可供其他类使用的话（即其接口已明确定义且以后不会修改），那么该类就是封闭（你可以称之为完整）的。</p>
<p>如果一个类已经完成开发、测试和审核工作，而且属于某个框架或者可被其他类的代码直接使用的话，对其代码进行修改就是有风险的。你可以创建一个子类并重写原始类的部分内容以完成不同的行为，而不是直接对原始类的代码进行修改。这样你既可以达成自己的目标，但同时又无需修改已有的原始类客户端。</p>
<p>这条原则并不能应用于所有对类进行的修改中。如果你发现类中存在缺陷，直接对其进行修复即可，不要为它创建子类。<br />
子类不应该对其父类的问题负责。</p>
<h4>L：里氏替换原则</h4>
<blockquote>
<p>里氏替换原则（Liskov Substitution Principle）</p>
<p>当你扩展一个类时， 记住你应该要能在不修改客户端代码的情况下将子类的对象作为父类对象进行传递。</p>
</blockquote>
<p>这意味着子类必须保持与父类行为的兼容。在重写一个方法时，你要对基类行为进行扩展，而不是将其完全替换。<br />
替换原则是用于预测子类是否与代码兼容，以及是否能与其超类对象协作的一组检查。这一概念在开发程序库和框架时<br />
非常重要， 因为其中的类将会在他人的代码中使用——你是无法直接访问和修改这些代码的。<br />
与有着多种解释方式的其他设计模式不同，替代原则包含一组对子类（特别是其方法）的形式要求。</p>
<ul>
<li>
<p><strong>子类方法的参数类型必须与其超类的参数类型相匹配或更加抽象。</strong></p>
<p>即设计子类方法时，尽可能的将入参类型设置为父类的类型</p>
</li>
<li>
<p><strong>子类方法的返回值类型必须与超类方法的返回值类型或是其子类别相匹配。</strong></p>
<p>即设计子类方法时，尽可能的将入参类型设置为子类的类型</p>
</li>
<li>
<p><strong>子类中的方法不应抛出基础方法预期之外的异常类型</strong></p>
<p>在C++、Java语言中，如果抛出基础方法预期之外的异常类型，编译时会发生错误，而Golang、Rust等使用ErrCode处理错误的语言则需要额外注意返回错误的处理</p>
</li>
<li>
<p><strong>子类不应该加强其前置条件</strong></p>
<p>例如，基类的方法有一个<code>int</code>类型的参数。如果子类重写该方法时，要求传递给该方法的参数值必须为正数（如果该值为负则抛出异常）， 这就是加强了前置条件。客户端代码之前将负数传递给该方法时程序能够正常运行，但现在使用子类的对象时会使程序出错。</p>
</li>
<li>
<p><strong>子类不能削弱其后置条件</strong></p>
<p>假如你的某个类中有个方法需要使用数据库，该方法应该在接收到返回值后关闭所有活跃的数据库连接。<br />
你创建了一个子类并对其进行了修改，使得数据库保持连接以便重用。但客户端可能对你的意图一无所知。由于它认为<br />
该方法会关闭所有的连接，因此可能会在调用该方法后就马上关闭程序，使得无用的数据库连接对系统造成“污染”。</p>
</li>
<li>
<p><strong>超类的不变量必须保留</strong></p>
<p>不变量是让对象有意义的条件。因此，扩展一个类的最安全做法是引入新的成员变量和方法，而不要去招惹超类中已<br />
有的成员。当然在实际中，这并非总是可行。</p>
</li>
<li>
<p><strong>子类不能修改超类中私有成员变量的值</strong></p>
<p>有些编程语言允许通过反射机制来访问类的私有成员。还有一些语言（Python 和JavaScript）没有对私有成员进行任何保护。</p>
</li>
</ul>
<blockquote>
<p>其实总结一点就是，暴露给用户的总是父类/接口，所以在子类实现父类/接口的方法时，要以上层为准。</p>
</blockquote>
<h4>I：接口隔离原则</h4>
<blockquote>
<p>接口隔离原则（Interface Segregation Principle）</p>
<p>客户端不应被强迫依赖于其不使用的方法。</p>
</blockquote>
<p>尽量缩小接口的范围，使得客户端的类不必实现其不需要的行为。</p>
<p>根据接口隔离原则，你必须将“臃肿”的方法拆分为多个颗粒度更小的具体方法。客户端必须仅实现其实际需要的方法。否则，对于“臃肿”接口的修改可能会导致程序出错，即使客户端根本没有使用修改后的方法。</p>
<p>继承只允许类拥有一个超类，但是它并不限制类可同时实现的接口的数量。因此，你不需要将大量无关的类塞进单个接口。你可将其拆分为更精细的接口，如有需要可在单个类中实现所有接口，某些类也可只实现其中的一个接口。</p>
<p>请过度使用这条原则。不要进一步划分已经非常具体的接口。记住，创建的接口越多，代码就越复杂。因此要保持平衡。</p>
<h4>D：依赖倒置原则</h4>
<blockquote>
<p>依赖倒置原则（Dependency Inversion Principle）</p>
<p>高层次的类不应该依赖于低层次的类。两者都应该依赖于抽象接口。抽象接口不应依赖于具体实现。具体实现应该依赖于抽象接口。</p>
</blockquote>
<p>通常在设计软件时，你可以辨别出不同层次的类。</p>
<ul>
<li>低层次的类实现基础操作（例如磁盘操作、传输网络数据和<br />
连接数据库等）。</li>
<li>高层次类包含复杂业务逻辑以指导低层次类执行特定操作。</li>
</ul>
<p>有时人们会先设计低层次的类， 然后才会开发高层次的类。当你在新系统上开发原型产品时，这种情况很常见。由于低层次的东西还没有实现或不确定，你甚至无法确定高层次类能实现哪些功能。如果采用这种方式，业务逻辑类可能会更依赖于低层原语类。</p>
<p>依赖倒置原则通常和开闭原则共同发挥作用：你无需修改已有类就能用不同的业务逻辑类扩展低层次的类。</p>
<h2>面向对象设计原则与设计模式</h2>
<p>在1995 年，GoF（Gang of Four，四人组/四人帮）合作出版了《设计模式：可复用面向对象软件的基础》一书，共收录了 23 种设计模式，从此树立了软件设计模式领域的里程碑，人称「GoF设计模式」。<br />
这23种设计模式包括了三大类型，即<strong>创建型设计模式、结构型设计模式、行为型设计模式</strong>，以此为分类依据，这23种设计模式的概览如下：</p>
<ul>
<li>
<p>创建型模式（Creational Patterns）：</p>
<ol>
<li>
<p>单例模式（Singleton）</p>
</li>
<li>
<p>工厂方法模式（Factory Method）</p>
</li>
<li>
<p>抽象工厂模式（Abstract Factory）</p>
</li>
<li>
<p>建造者模式（Builder）</p>
</li>
<li>
<p>原型模式（Prototype）</p>
</li>
</ol>
</li>
<li>
<p>结构型模式（Structural Patterns）：</p>
<ol start="6">
<li>
<p>适配器模式（Adapter）</p>
</li>
<li>
<p>桥接模式（Bridge）</p>
</li>
<li>
<p>组合模式（Composite）</p>
</li>
<li>
<p>装饰者模式（Decorator）</p>
</li>
<li>
<p>外观模式（Facade）</p>
</li>
<li>
<p>享元模式（Flyweight）</p>
</li>
<li>
<p>代理模式（Proxy）</p>
</li>
</ol>
</li>
<li>
<p>行为型模式（Behavioral Patterns）：</p>
<ol start="13">
<li>
<p>责任链模式（Chain of Responsibility）</p>
</li>
<li>
<p>命令模式（Command）</p>
</li>
<li>
<p>解释器模式（Interpreter）</p>
</li>
<li>
<p>迭代器模式（Iterator）</p>
</li>
<li>
<p>中介者模式（Mediator）</p>
</li>
<li>
<p>备忘录模式（Memento）</p>
</li>
<li>
<p>观察者模式（Observer）</p>
</li>
<li>
<p>状态模式（State）</p>
</li>
<li>
<p>策略模式（Strategy）</p>
</li>
<li>
<p>模板方法模式（Template Method）</p>
</li>
<li>
<p>访问者模式（Visitor）</p>
</li>
</ol>
</li>
</ul>
<p>设计模式是践行面向对象设计原则的良好案例，本系列文章将按照这三种分类，讲述这23种经典的设计模式，并适当扩展在此23种设计模式之外的常用设计模式</p>
<h2>写在后面</h2>
<p>面向对象设计原则与设计模式，不一定总是适用的，在一些复杂场景下，运用设计模式可以将系统解耦，增加系统的可维护性、可拓展性，降低系统的复杂度。但是过度运用设计模式反而会大大增加系统的复杂度和理解难度，所以请适当使用设计模式。<br />
而且面向对象中的很多设计都过于理想化，实际生产过程中很大一部分都没有完全利用到面向对象理想中的最大的优势，因为将一个系统模块进行面向对象的抽象见面在一些复杂场景下往往是极具挑战性，或者说从实践角度上来说是意义不大的。正如如今在服务端开发领域用的最多的MVC模型，其本质上就是典型的面向过程的贫血模型，而真正面向对象充血模式的DDD，多数是难以落地的，多是进行进一步封装抽象，过度追求面向对象往往只会徒增系统复杂度。<br />
所以说，学习设计模式的过程是一个则其善者而从之，其不善者而改之的过程，软件设计没有银弹，只有在合适的场合运用合适的设计。</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/05/27/%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e5%8e%9f/">面向对象设计模式与Go语言实现 – 面向对象设计原则</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2023/05/27/%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e5%8e%9f/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>LazySkiplist: A Simple Optimistic skip-list Algorithm论文翻译与实现</title>
		<link>https://www.crazyfay.com/2023/05/14/lazyskiplist-a-simple-optimistic-skip-list-algorithm%e8%ae%ba%e6%96%87%e7%bf%bb%e8%af%91%e4%b8%8e%e5%ae%9e%e7%8e%b0/</link>
					<comments>https://www.crazyfay.com/2023/05/14/lazyskiplist-a-simple-optimistic-skip-list-algorithm%e8%ae%ba%e6%96%87%e7%bf%bb%e8%af%91%e4%b8%8e%e5%ae%9e%e7%8e%b0/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Sun, 14 May 2023 06:47:28 +0000</pubDate>
				<category><![CDATA[学习笔记]]></category>
		<category><![CDATA[数据结构]]></category>
		<category><![CDATA[论文笔记]]></category>
		<guid isPermaLink="false">https://www.crazyfay.com/?p=318</guid>

					<description><![CDATA[<p>最近在研究无锁跳表，无意间发现了这篇论文，虽然是有锁的实现，但是采用了乐观同步的机制，是一个理论上简单且高效的 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/05/14/lazyskiplist-a-simple-optimistic-skip-list-algorithm%e8%ae%ba%e6%96%87%e7%bf%bb%e8%af%91%e4%b8%8e%e5%ae%9e%e7%8e%b0/">LazySkiplist: A Simple Optimistic skip-list Algorithm论文翻译与实现</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p>最近在研究无锁跳表，无意间发现了这篇论文，虽然是有锁的实现，但是采用了乐观同步的机制，是一个理论上简单且高效的并发安全的跳表实现。苦于国内网上少有针对此篇论文的翻译与解读，本篇博客致力于翻译此篇论文，并基于 <a href="https://github.com/zhangyunhao116/skipmap">zhangyunhao116/skipmap</a> 的开源代码逻辑，深入理解篇论文的思想。</p>
<blockquote>
<p>论文原文:<br />
<a href="https://people.csail.mit.edu/shanir/publications/LazySkipList.pdf">https://people.csail.mit.edu/shanir/publications/LazySkipList.pdf</a></p>
</blockquote>
<h2>论文翻译</h2>
<h3>摘要</h3>
<p>由于跳跃表具有高度分布式的特性和缺乏全局再平衡机制，它们正在成为并发应用中越来越重要的对数搜索结构。不幸的是，文献中的并发跳跃表实现，无论是基于锁还是无锁的，都没有被证明是正确的。此外，这些算法的复杂结构很可能是缺乏证明的原因，也给希望扩展和修改算法或在其基础上构建新结构的软件设计人员带来了障碍。</p>
<p>本文提出了一种简单的基于锁的并发跳跃表算法。与其他并发跳跃表算法不同，该算法始终保持跳跃表的性质，便于推理其正确性。尽管它是基于锁的，但该算法通过一种新颖的乐观同步方式实现了高度的可扩展性：在添加或删除节点之前，它在不获取锁的情况下进行搜索，仅需要进行短暂的基于锁的验证。实验证据表明，在最常见的搜索结构使用模式下，这种更简单的算法与已知的最佳无锁算法的性能相当。</p>
<h3>1 引言</h3>
<p>跳跃表（Skip-lists）^[11]^是一种越来越重要的数据结构，用于存储和检索有序的内存中数据。在本文中，我们提出了一种新的基于锁的并发跳跃表算法，该算法在大多数常见的使用条件下似乎与现有的最佳并发跳跃表实现表现相当。我们实现的主要优点在于它更简单，更易于推理。<br />
Pugh^[10]^提出的原始基于锁的并发跳跃表实现由于使用指针反转而变得相当复杂，据我们所知，尚未证明其正确性。Doug Lea^[8]^根据Fraser和Harris^[2]^的工作编写的ConcurrentSkipListMap，并作为Java SE 6平台的一部分发布，是我们所知的最有效的并发跳跃表实现。该算法是无锁的，并且在实践中表现良好。这种实现的主要限制在于它过于复杂。某些交错操作可能导致通常的跳跃表不变式被违反，有时是暂时性的，有时是永久性的。这些违规似乎不会影响性能或正确性，但它们使得很难推理算法的正确性。相比之下，此处介绍的算法基于锁，并且始终保持跳跃表的不变性。该算法足够简单，我们能够提供直接的正确性证明。<br />
我们的新颖基于锁的算法的关键是两种互补的技术的结合。首先，它是乐观的：方法在不获取锁的情况下遍历列表。此外，在遍历列表时，它们可以忽略其他线程获取的锁。只有当方法发现所需的项时，才会锁定该项及其前驱，并验证列表是否未更改。其次，我们的算法是延迟的：删除项涉及在物理上将其移除（取消链接）之前，通过标记逻辑上删除它。<br />
Lea^[7]^观察到最常见的搜索结构使用模式中，搜索操作明显占主导地位，插入操作占主导地位，而删除操作占很小比例。一个典型的模式是90％的搜索操作，9％的插入操作和仅有1％的删除操作（参见^[3]^）。在Sun Fire T2000多核和Sun Enterprise 6500上进行的初步实验测试表明，尽管我们新的乐观基于锁的算法非常简单，但在这种常见的使用模式下，它的性能与Lea的ConcurrentSkipListMap算法相当。实际上，只有在多程序环境中出现极端争用的不常见条件下，我们的新算法才比ConcurrentSkipListMap算法表现更好。这是因为我们的原始实验实现没有添加任何争用控制。 </p>
<p>因此，我们相信在需要理解并可能修改基本跳跃表结构的应用程序中，所提出的算法可能是ConcurrentSkipListMap算法的一个可行替代方案。</p>
<p><img decoding="async" src="http://43.130.6.58:8080/wp-content/uploads/2023/05/f1.png" alt="Figure 1" /><br />
Figure 1: 一个最大高度为4的跳表。每个节点下方的数字（即next指针数组）是该节点的键，其中左侧哨兵节点的键为-∞，右侧哨兵节点的键为+∞。</p>
<h3>2 背景</h3>
<p>跳跃表（Skip-list）^[11]^是一种按键排序的链表。每个节点被分配一个随机的高度，高度最大值有限。在任何一个高度上的节点数以指数方式减少。跳跃表节点在每个高度上都有一个后继节点。例如，一个高度为3的节点有三个下一个指针，一个指向高度为1的下一个节点，另一个指向高度为2的下一个节点，依此类推。图1显示了一个带有整数键的跳跃表。</p>
<p>我们将跳跃表看作具有多个层次的列表，并且我们讨论每个层次上节点的前驱和后继。除了底层之外，每个层次上的列表是下一层级列表的子列表。由于更高的高度上节点的数量指数级减少，因此我们可以通过首先在较高的层次上进行搜索来快速找到键，跳过较短节点的大量数量，并逐渐向下工作，直到找到具有所需键的节点，或者到达底层。因此，跳跃表操作的期望时间复杂度是列表长度的对数。</p>
<p>在列表的开始和结束处，有左哨兵和右哨兵节点，这样比较方便。这些节点具有最大的高度，当跳跃表为空时，右哨兵在每个层次上都是左哨兵的后继节点。左哨兵的键值比任何可能添加到集合中的键都要小，而右哨兵的键值比任何可能添加到集合中的键都要大。因此，搜索跳跃表始终从左哨兵开始。</p>
<pre><code class="language-java">class Node {
    int key;
    int topLayer;
    Node ∗∗ nexts;
    bool marked;
    bool fullyLinked;
    Lock lock;
} </code></pre>
<p>Figure 2: A node</p>
<h3>3 我们的算法</h3>
<p>我们在支持三个方法的集合对象的实现的背景下介绍我们的并发跳跃表算法，这三个方法是add、remove和contains：<code>add(v)</code>将v添加到集合中，并且当v不在集合中时返回true；<code>remove(v)</code>从集合中删除v，并且当v在集合中时返回true；<code>contains(v)</code>当v在集合中时返回true。我们展示了我们的实现是可线性化的^[6]^，也就是说，每个操作似乎在其调用和响应之间的某个点（线性化点）上以原子方式执行。我们还展示了该实现是无死锁的，并且contains操作是无等待的；也就是说，只要线程不停地执行步骤，它就保证能完成contains操作，而不受其他线程活动的影响。</p>
<p>我们的算法建立在Heller等人的懒惰列表算法^[4]^基础上，这是一个简单的并发链表算法，其add和remove操作使用乐观细粒度锁定机制，而contains操作是无等待的：我们在跳跃表的每个层次上使用懒惰列表。与懒惰列表一样，每个节点的Key严格大于其前驱节点的Key，并且每个节点都有一个标记标志，用于使remove操作看起来是原子的。然而，与简单的懒惰列表不同，我们可能需要在多个层次上链接节点，因此可能无法通过单个原子指令插入节点，该指令可以作为成功添加操作的线性化点。因此，在懒惰跳跃表中，我们为每个节点添加了一个额外的标志fullyLinked，在节点在所有层次上都被链接之后将其设置为true；设置此标志是我们跳跃表实现中成功添加操作的线性化点。图2显示了一个节点的字段。</p>
<p>如果且仅当列表中存在一个未标记、完全链接的具有该Key的节点时（即从左哨兵可达），Key就在抽象集合中。 </p>
<p>为了维护跳跃表的不变性，即每个列表都是较低层次列表的子列表，在对需要修改的所有节点获取锁时才会对列表结构（即next指针）进行更改。（有一个例外是涉及添加操作的规则，稍后讨论。）</p>
<p>在对算法的下面详细描述中，我们假设存在垃圾收集器来回收从跳跃表中删除的节点，因此在任何线程可能仍然访问这些节点时，删除的节点不会被重新利用。在证明（第4节）中，我们假设节点永远不会被重新利用。在没有垃圾回收的编程环境中，我们可以使用解决重复使用问题的解决方案^[5]^或危险指针[hazard pointers] ^[9]^来实现相同的效果。我们还假设Key是从MinInt+1到MaxInt-1的整数。我们使用MinInt和MaxInt作为LSentinel和RSentinel的Key，它们分别是左哨兵和右哨兵节点。 </p>
<p>在跳跃表中进行搜索是通过findNode辅助函数完成的（见图3），该函数接受一个键v和两个最大高度的节点指针数组preds和succs，并且与顺序跳跃表完全一样进行搜索，从最高层开始，每次遇到大于或等于v的节点时向下一个更低的层继续搜索。线程在preds数组中记录了在每个层次上遇到的最后一个键小于v的节点及其后继节点（后继节点的键必须大于或等于v）。如果找到具有所需Key的节点，findNode返回找到此类节点的第一个层次的索引；否则，返回-1。为了简化演示，即使在更高层次找到具有所需键的节点，findNode也会继续执行到底层，因此在findNode终止后，preds数组和succs数组中的所有条目都被填充（请参见第3.4节，了解实际实现中使用的优化）。请注意，findNode不获取任何锁，也不会在出现与其他线程冲突的访问时进行重试。现在我们逐个考虑每个操作。</p>
<pre><code class="language-java">int findNode( int v, Node∗ preds[], Node∗ succs[]) {
    int lFound = -1;
    Node∗ pred = &amp;LSentinel ;
    for(int layer = MaxHeight −1; layer ≥ 0; layer−−){
        Node∗ curr = pred −&gt; nexts[layer];
        while(v &gt; curr−&gt;key) {
            pred = curr;
            curr = pred−&gt;nexts[layer];
    }
    if(lFound == −1 &amp;&amp; v == curr −&gt; key){
        lFound = layer;
    }
    preds[layer] = pred;
    succs [layer] = curr;
    }
    return lFound;
}</code></pre>
<p>Figure 3: The findNode helper function</p>
<h5>3.1 添加操作</h5>
<p>添加操作（见图4）调用findNode函数来确定列表中是否已存在具有该键的节点。如果存在（第59-66行），并且该节点未被标记，则添加操作返回false，表示键已经在集合中。然而，如果该节点尚未完全链接，则线程将等待，直到它完全链接（因为在节点完全链接之前，该键不在抽象集合中）。如果该节点被标记，则表示另一个线程正在删除该节点，因此执行添加操作的线程只需重试。</p>
<p>如果没有找到具有适当键的节点，则线程会锁定并验证findNode返回的所有前驱节点，直到达到新节点的高度（第69-84行）。该高度由add操作的开始处使用randomLevel函数确定。验证（第81-83行）检查对于每个层次i ≤ topNodeLayer，preds[i]和succs[i]是否在第i层仍然相邻，并且都没有被标记。如果验证失败，线程遇到了冲突的操作，因此释放它获取的锁（在第97行的finally块中）并重试。</p>
<p>如果线程成功锁定并验证findNode结果直到新节点的高度，则添加操作将保证成功，因为线程将持有所有锁，直到完全链接其新节点。在这种情况下，<strong>线程分配一个具有对应键和高度的新节点，将其链接到列表中，设置新节点的fullyLinked标志（这是添加操作的线性化点）</strong>，然后在释放所有锁之后返回true（第86-97行）。写入newNode-&gt;nexts[i]的线程是唯一一种情况，其中线程修改了它没有锁定的节点的nexts字段。这是安全的，因为在线程将preds[i]-&gt;nexts[i]设置为newNode之后，newNode不会在第i层链接到列表中，而是在写入newNode-&gt;nexts[i]之后才链接。</p>
<pre><code class="language-java">bool add (int v){
    int topLayer = randomLevel(MaxHeight);
    Node∗ preds [MaxHeight] , succs[MaxHeight];
    while(true){
        int lFound = findNode(v, preds, succs);
        if(lFound != −1){
            Node∗ nodeFound = succs[lFound];
            if(!nodeFound−&gt;marked){
                while (!nodeFound−&gt;fullyLinked){;}
                return false;
            }
            continue;
        }
        int highestLocked = −1;
        try{
            Node ∗pred, ∗succ, ∗prevPred = null;
            bool valid = true;
            for (int layer = 0;  valid &amp;&amp; (layer ≤ topLayer); layer++){
                pred = preds[layer];
                succ = succs[layer];
                if(pred != prevPred) {
                    pred−&gt;lock.lock();
                    highestLocked = layer;
                    prevPred = pred;
                }
                valid = !pred−&gt;marked &amp;&amp; !succ−&gt;marked &amp;&amp; pred−&gt;nexts[layer] == succ;
            }
            if(!valid) continue;
            Node∗ newNode = new Node (v, topLayer);
            for(int layer = 0; layer ≤ topLayer; layer++){
                newNode−&gt;nexts[layer] = succs [layer];
                preds[layer] −&gt; nexts[layer] = newNode;
            }
            newNode −&gt; fullyLinked = true;
            return true;
        } finally{
            unlock(preds, highestLocked);
        }
    }
}</code></pre>
<h5>3.2 删除操作</h5>
<p>删除操作（见图5）同样调用findNode函数来确定列表中是否存在具有适当键的节点。如果存在，则线程检查该节点是否“可以删除”（图6），这意味着它完全链接、未被标记，并且它是在其顶层找到的节点。如果节点满足这些要求，线程将锁定该节点并验证它是否仍未被标记。如果是这样，线程将标记该节点，逻辑上删除它（第111-121行）；也就是说，<strong>节点的标记是删除操作的线性化点</strong>。</p>
<p>该过程的其余部分完成了“物理”删除操作，通过先锁定其所有层次上的前驱节点（直到删除节点的高度为止）（第124-138行），然后一次将节点从列表中剪接出来（第140-142行），从而将节点从列表中删除。为了维护跳跃表结构，节点在从更高层次拼接出来之前先从较低层次拼接出来（尽管为了确保没有死锁，如第4节所讨论的，锁的获取顺序是从较低层次向上获取）。与添加操作类似，在更改任何已删除节点的前驱节点之前，线程验证这些节点确实是已删除节点的前驱节点。这是通过weakValidate函数完成的，该函数与validate相同，只是当后继节点被标记时不会失败，因为在这种情况下，后继节点应该是刚刚被标记的待删除的节点。如果验证失败，则线程释放对旧前驱节点的锁定（但不包括已删除的节点），并尝试通过再次调用findNode来找到已删除节点的新前驱节点。然而，此时它已经设置了局部的isMarked标志，以便不会尝试标记另一个节点。在成功从列表中删除已删除节点后，线程释放所有锁，并返回true。</p>
<p>如果未找到节点，或者找到的节点不是“可以删除”的（即已被标记、未完全链接或未在其顶层找到），则操作简单地返回false（第148行）。容易看出，如果节点未被标记，这是正确的，因为对于任何键，跳跃表中最多只有一个具有该键的节点（即从左哨兵可达），并且一旦节点被放入列表中（必须通过findNode找到），它就不会被删除，直到被标记。然而，如果节点已被标记，该论证则变得更加棘手，因为在找到该节点时，它可能不在列表中，而列表中可能有具有相同键的某个未标记节点。然而，正如我们在第4节中所论证的那样，这种情况下，在执行删除操作期间必定存在某个时刻，该键不在抽象集合中。</p>
<pre><code class="language-java">bool remove (int v){
    Node∗ nodeToDelete = null;
    bool isMarked = false;
    int topLayer = −1;
    Node∗ preds[MaxHeight], succs[MaxHeight];
    while(true){
        int lFound = findNode(v, preds, succs);
        if(isMarked || (lFound != −1 &amp;&amp; okToDelete(succs[lFound],lFound))){
            if(!isMarked){
                nodeToDelete = succs[lFound];
                topLayer = nodeToDelete −&gt; topLayer;
                nodeToDelete −&gt; lock.lock();
                if(nodeToDelete−&gt;marked) {
                    nodeToDelete−&gt;lock.unlock ();
                    return false;
                }
            nodeToDelete−&gt;marked = true;
            isMarked = true;
            }
            int highestLocked = −1;
            try {
                Node ∗pred, ∗succ, ∗prevPred = null;
                bool valid = true;
                for (int layer = 0; valid &amp;&amp; (layer ≤ topLayer); layer++) {
                    pred = preds[layer];
                    succ = succs[layer];
                    if (pred != prevPred) {
                        pred−&gt;lock.lock();
                        highestLocked = layer;
                        prevPred = pred;
                    }
                    valid = ! pred−&gt;marked &amp;&amp; pred−&gt;nexts[layer] == succ;
                }
                if (!valid) continue;
                for (int layer = topLayer; layer ≥ 0; layer−−) {
                    preds [layer]−&gt;nexts [layer] = nodeToDelete−&gt;nexts [layer];
                }
                nodeToDelete−&gt;lock.unlock();
                return true;
            } finally {
             unlock (preds, highestLocked);
            }
        }
        else return false;
    }
}</code></pre>
<p>Figure 5: The remove method</p>
<pre><code class="language-java">bool okToDelete ( Node∗ candidate , int lFound ) {
    return (candidate−&gt;fullyLinked
                &amp;&amp; candidate−&gt;topLayer==lFound
                &amp;&amp; !candidate−&gt;marked);
}</code></pre>
<p>Figure 6: The okToDelete method</p>
<pre><code class="language-java">bool contains (int v) {
    Node∗ preds [MaxHeight] , succs[MaxHeight] ;
    int lFound = findNode (v, preds, succs) ;
    return ( lFound != −1
                &amp;&amp; succs [lFound]−&gt;fullyLinked
                &amp;&amp; !succs [lFound]−&gt;marked ) ;
}</code></pre>
<p>Figure 7: The contains method</p>
<h5>3.3 包含操作</h5>
<p>最后，我们考虑包含操作（见图7），它只是调用findNode，并且当且仅当找到一个未标记、完全链接的具有适当键的节点时返回true。如果找到这样的节点，则根据定义可以立即得出结论，该键在抽象集合中。然而，如上所述，如果该节点已被标记，那么很难看出返回false是安全的。我们在第4节中对此进行了论证。</p>
<h5>3.4 实现问题</h5>
<p>我们使用Java编程语言实现了该算法，以便与Doug Lea在java.util.concurrent包中的非阻塞跳跃表实现进行比较。伪代码中的数组堆栈变量被线程本地变量所取代，我们使用了直接的锁实现（我们无法使用内置的对象锁，因为我们的获取和释放模式无法始终使用synchronized块来表示）。</p>
<p>所呈现的伪代码针对简单性而不是效率进行了优化，可以改进的方式有很多，我们在实现中应用了其中许多方式。例如，如果找到具有适当键的节点，则添加和包含操作无需继续查找；它们只需要确定该节点是否完全链接且未标记。如果是这样，包含操作可以返回true，添加操作可以返回false。如果不是，则包含操作可以返回false，添加操作要么在返回false之前等待（如果节点未完全链接），要么必须重试。然而，删除操作确实需要搜索到底层以找到要删除的节点的所有前驱节点，但是一旦找到并标记了某一层上的节点，它可以在较低层次上搜索确切的节点，而无需比较键。这是正确的，因为一旦线程标记了节点，其他线程就无法取消链接它。</p>
<p>此外，在伪代码中，findNode总是从可能的最高层开始搜索，尽管我们预期大部分时间最高层将是空的（即只有两个哨兵节点）。通过维护一个跟踪最高非空层的变量，很容易因为每当该变量发生变化时，导致变化的线程必须锁定左哨兵。与非阻塞版本相比，这种容易性存在差异，因为并发的删除和添加操作之间的竞争可能导致记录的跳跃表高度小于其最高节点的实际高度。</p>
<h3>4 正确性</h3>
<p>在本节中，我们概述我们的跳跃表算法的证明。我们要展示四个属性：算法实现了可线性化的集合、它是无死锁的、contains操作是无等待的，以及底层数据结构维护了一个正确的跳跃表结构，我们将在下面更详细地定义。</p>
<h4>4.1 可线性化性</h4>
<p>在证明过程中，我们对初始化做出了以下简化假设：节点被初始化为具有其键和高度，它们的nexts数组被初始化为全部为null，它们的fullyLinked和marked字段被初始化为false。此外，为了推理的目的，我们假设节点永远不会被回收，并且有无穷无尽的新节点可用（否则，我们需要修改算法以处理节点耗尽的情况）。</p>
<p>我们首先得出以下观察结果：节点的键永远不会更改（即key = k是稳定的），节点的marked和fullyLinked字段永远不会被设置为false（即marked和fullyLinked是稳定的）。虽然初始时nexts[i]为null，但它永远不会被写入null（即nexts[i] 6= null是稳定的）。此外，线程只有在持有节点的锁时才会写入节点的marked或nexts字段（唯一的例外是在链接节点到第i层之前的add操作中写入节点的nexts[i]）。</p>
<p>根据这些观察结果和对代码的检查，我们可以很容易地看出，在任何操作中，在调用findNode后，对于所有的i，我们有preds[i]-&gt;key &lt; v和succs[i]-&gt;key ≥ v，并且对于i &gt; lFound（即findNode返回的值），有succs[i]-&gt;key &gt; v。此外，在remove操作中，只有在该节点被其他线程标记之前，nodeToDelete才会被设置一次，并且此线程在完成操作之前，线程的isMarked变量将始终为true。我们还通过okToDelete知道节点是完全链接的（实际上只有完全链接的节点才能被标记）。</p>
<p>此外，验证和在写入节点之前锁定节点的要求确保在成功验证后，验证检查的属性（对于add和remove而言略有不同）在释放锁之前保持为true。</p>
<p>我们可以利用这些属性得出以下基本引理（fundamental lemma）：</p>
<p>Lemma 1 For a node n and 0 ≤ i ≤ n-&gt;topLayer:</p>
<pre><code>                                        n->nexts[i] 6= null =⇒ n->key < n->nexts[i]->key</code></pre>
<p>我们定义关系→i，表示在第i层上，如果m-&gt;nexts[i] = n或者存在m0使得m →i m0且m0-&gt;nexts[i] = n，则表示m导向n（读作“m leads to n at layer i”）。因为一个节点在任何层上最多只有一个直接后继节点，所以→i关系在第i层上“遵循”一个链表的结构，特别地，跳表中的第i层列表包括那些满足LSentinel →i n（以及LSentinel本身）的节点n。另外，根据引理1，如果m →i n且m →i n0且n-&gt;key &lt; n0-&gt;key，则n →i n0。</p>
<p>基于这些观察，我们可以证明，在算法的任何可达状态下，如果m →i n，则在任何后续状态中，除非存在将n从第i层列表中切割的操作（即执行第141行），否则m →i n将继续成立。这个结论在最近的一篇论文中正式证明了懒惰列表算法，并且该证明可以适应这个算法。因为在n被切割出列表之前，它必须已经被标记，而且由于fullyLinked标志在初始化后不会被设置为false，这个结论意味着只有通过标记节点才能从抽象集合中移除键，而我们之前已经证明标记节点是成功删除操作的线性化点。</p>
<p>类似地，我们可以观察到，如果LSentinel →i n在算法的某个可达状态中不成立，则除非在某个执行中出现n = newNode的情况（如前所述，前一行不会改变第i层的列表，因为newNode尚未链接）。然而，该行的执行发生在newNode被插入但尚未完全链接的过程中。因此，将节点添加到任何级别的列表的唯一操作是设置节点的fullyLinked标志。</p>
<p>最后，我们论证如果一个线程找到了一个标记节点，那么该节点的键在该线程执行操作的过程中必定不在列表中。有两种情况：如果节点在线程调用操作时已经被标记，则该节点必须在那个时刻已经存在于跳表中，因为标记节点不能添加到跳表中（只能将新分配的节点添加到跳表中），并且由于跳表中没有两个具有相同键的节点，因此跳表中没有未标记的节点具有该键。因此，在调用操作时，键不在跳表中。另一种情况是，如果线程调用操作时节点没有被标记，那么它必须在第一个线程发现它之前被其他线程标记，然后，根据我们之前的论证，当其他线程首次标记节点时，键在该线程的操作执行期间不在抽象集合中。这个结论在简单懒惰列表[1]的形式证明中也得到了证实，而且可以适用于这个算法。</p>
<p>综上所述，我们通过观察和代码分析，证明了一些重要的性质。在任何操作中，在调用findNode之后，我们有preds[i]-&gt;key &lt; v且succs[i]-&gt;key ≥ v对于所有i成立，并且对于i &gt; lFound（findNode返回的值），succs[i]-&gt;key &gt; v。对于remove操作中的线程，nodeToDelete只被设置一次，除非该节点被其他线程标记，该线程将标记该节点，并且在完成操作之前，线程的isMarked变量将保持为true。我们还通过okToDelete知道该节点是完全链接的（实际上，只有完全链接的节点才能被标记）。</p>
<p>此外，验证和在写入节点之前要求锁定节点的要求确保在成功验证之后，验证所检查的属性（对于add和remove来说略有不同）在释放锁之前保持不变。</p>
<p>基于这些性质，我们可以得出以下基本引理的证明。</p>
<h4>4.2 维护跳跃表的不变性</h4>
<p>我们的算法保证跳跃表的不变性在任何时候都得到保持。通过“跳跃表的不变性”，我们指的是每个层次的列表是低层次列表的子列表。保持这种结构的重要性在于，跳跃表的复杂度分析要求具备这种结构。 要看到算法如何保持跳跃表的结构，注意到将新节点链接到跳跃表的过程始终从底层到顶层进行，并且在插入节点时持有所有即将成为该节点前驱节点的锁。另一方面，当从列表中删除节点时，高层节点先于低层节点被取消链接，同样在删除节点的直接前驱节点上持有锁定。</p>
<p>保持跳跃表结构的性质，在锁无关的算法中并不保证。在该算法中，在将节点链接到底层后，将节点从顶层到底层在其余的层次上链接。这可能导致节点仅在其顶层和底层上链接，使得顶层的列表不是其下一层列表的子列表，例如。此外，在除底层之外的任何层次上尝试链接节点时，不会进行重试，因此这种不符合跳跃表结构的状态可能会无限期地持续存在。</p>
<p>这是我们基于锁的算法与锁无关算法之间的一个重要区别。我们的基于锁的算法通过明确获取锁并在从底层到顶层链接节点时维护必要的锁定纪律，以确保在任何时候都保持跳跃表的结构。</p>
<p><img decoding="async" src="http://43.130.6.58:8080/wp-content/uploads/2023/05/f8.png" alt="Figure 8" /><br />
Figure 8: 在执行1,000,000次操作的情况下，每毫秒的吞吐量，其中有9%的添加操作，1%的删除操作和90%的包含操作，键范围分别为200,000和2,000,000。</p>
<h4>4.3 死锁自由和无等待性</h4>
<p>该算法是死锁自由的，因为线程总是先获取具有较大键的节点上的锁。更具体地说，如果一个线程持有具有键v的节点上的锁，则它不会尝试获取具有大于或等于v的键的节点上的锁。我们可以看到这是正确的，因为添加和删除方法都从底层向上获取前驱节点的锁，并且前驱节点的键小于较低层的其他前驱节点的键。唯一的其他锁获取是删除操作删除的节点。这是该操作获取的第一个锁，它的键大于任何前驱节点的键。</p>
<p>包含操作是无等待的也很容易理解：它不获取任何锁，也不会进行重试；它只搜索一次列表。</p>
<h3>5 性能</h3>
<p>我们通过在Java编程语言中实现我们的跳跃表算法来评估其性能，正如之前所描述的那样。我们将我们的实现与Doug Lea在java.util.concurrent包的ConcurrentSkipListMap类中的非阻塞跳跃表实现进行了比较。据我们所知，这是目前最好的广泛可用的并发跳跃表实现。我们还实现了一个简单的顺序跳跃表，其中的方法通过同步来确保线程安全，作为实验中的基准。本节中，我们描述了从这些实验中获得的一些结果。</p>
<p>我们使用两个具有不同架构的多处理器系统进行实验。第一个系统是基于单个UltraSPARC°r T1处理器的Sun Fire T2000服务器，该处理器包含8个计算核心，每个核心具有4个硬件线程，主频为1200 MHz。每个具有4个线程的核心具有一个8KB级1数据缓存和一个16KB指令缓存。所有8个核心共享一个3MB级2统一（指令和数据）缓存和一个4路交错的32GB主存储器。数据访问延迟比大约为1:8:50（L1:L2:内存访问）。另一个系统是较旧的Sun Enterprise 6500服务器，它包含15个系统板，每个板上有两个主频为400 MHz的UltraSPARC°r II处理器和2GB内存，总共有30个处理器和60GB内存。每个处理器在芯片上有一个16KB的数据级1缓存和一个16KB的指令缓存，以及一个8MB的外部缓存。系统时钟频率为80 MHz。</p>
<p>我们展示了从一个空的跳跃表开始，每个线程执行一百万（1,000,000）个随机选择的操作的实验结果。我们变化了线程数量，添加、删除和包含操作的相对比例，以及选择键的范围。每个操作的键是从指定范围均匀随机选择的。 在接下来的图表中，我们比较每毫秒操作的吞吐量，所显示的结果是每组参数的六次运行的平均值。</p>
<p>图8展示了实验结果，其中9%的操作是添加操作，1%的操作是删除操作，剩下的90%是包含操作，键的范围为20万或200万。不同的范围会产生不同程度的竞争，其中200,000范围的竞争更高，与2,000,000范围相比。从这些实验中可以看出，我们的实现和Lea的实现都具有良好的可扩展性（而顺序算法如预期的那样相对平坦）。除了在旧系统上使用200,000范围的情况下，我们的实现在所有情况下都稍微占优势。</p>
<p>图9展示了一系列实验的结果，其中添加操作的百分比变化，而删除操作的百分比固定为1%，剩余的百分比分配给包含操作。键的范围设置为两百万。如图所示，我们的实现在不同的添加操作百分比下优于Lea的实现，展示了它的可伸缩性和效率。</p>
<p>总体而言，实验结果表明，我们基于锁的并发跳表算法的性能与最佳的无锁实现相当，甚至更好。它在常见的使用模式下实现了高度的可伸缩性和效率，其中搜索操作占主导地位，插入操作占主导地位。我们的算法始终保持跳表的特性，简化了对正确性的推理，并且它是无死锁和无等待的，确保所有线程都能取得进展。</p>
<p>这些性能结果验证了我们基于锁的并发跳表算法的有效性，并突显了它作为现有并发跳表实现的可行替代方案的潜力，特别是在强调简单性、可伸缩性和性能的应用中。</p>
<p>在下一组实验中，我们运行了更高比例的添加和删除操作，分别为20%和10%（剩余70%的包含操作）。结果如图9所示。可以看到，在T2000系统上，两种实现的性能相似，Lea在多程序环境下，当范围较小（更高的竞争）时略有优势。而在范围较大时情况则相反。这种现象在旧系统上更为明显：在范围较小、64个线程的情况下，Lea的实现优势达到13%，而在范围较大、相同数量的线程下，我们的算法有20%的优势。</p>
<p>为了探究这种现象，我们进行了一项具有显著较高竞争水平的实验：半数添加操作和半数删除操作，范围为20万。结果如图10所示。可以清楚地看到，在这种竞争水平下，我们的实现的吞吐量在接近多程序区域时迅速下降，特别是在T2000系统上。这种下降并不令人意外：在我们当前的实现中，当添加或删除操作无法通过验证或无法立即获取锁时，它只是调用yield；没有适当的机制来管理竞争。由于添加和删除操作要求在锁定之前搜索阶段看到的前驱节点不发生更改，我们预计在高竞争下，它们将反复失败。因此，我们预计引入退避机制或其他形式的竞争控制将极大地提高性能。为了验证高冲突水平确实是问题所在，我们添加了计数器来计算实验期间每个线程执行的重试次数。计数器确实显示，在64个线程运行时执行了许多次重试，特别是在T2000系统上。大多数重试是由添加方法执行的，这是有道理的，因为删除方法在搜索更低层的前驱节点之前标记要删除的节点，这可以防止并发添加操作更改这些前驱节点的next指针。<br />
<img decoding="async" src="http://43.130.6.58:8080/wp-content/uploads/2023/05/f9.png" alt="Figure 9" /></p>
<p>Figure 9: 在执行1,000,000次操作的情况下，每毫秒的吞吐量，其中有20%的添加操作，10%的删除操作和70%的包含操作，键范围分别为200,000和2,000,000。</p>
<p><img decoding="async" src="http://43.130.6.58:8080/wp-content/uploads/2023/05/f10.png" alt="Figure 10" /><br />
Figure 10: 在执行1,000,000次操作的情况下，每毫秒的吞吐量，其中有50%的添加操作和50%的删除操作，键范围为200,000。</p>
<h3>6 结论</h3>
<p>我们展示了如何使用一种非常简单的算法构建可扩展的高并发跳表。我们的实现还不完善，显然在处理高度竞争的情况下可以从更好的竞争控制机制中获益。尽管如此，我们相信即使在其原始形式下，对于大多数使用情况来说，它仍然是一种有趣且可行的替代方案，可以替代ConcurrentSkipListMap。</p>
<h3>引用</h3>
<p>[1] Colvin, R., Groves, L., Luchangco, V., and Moir, M. Formal verification of a lazy concurrent list-based set. In Proceedings of Computer-Aided Verification (Aug. 2006). </p>
<p>[2] Fraser, K. Practical Lock-Freedom. PhD thesis, University of Cambridge, 2004.11 </p>
<p>[3] Fraser, K., and Harris, T. Concurrent programming without locks. Unpublished manuscript, 2004. </p>
<p>[4] Heller, S., Herlihy, M., Luchangco, V., Moir, M., Shavit, N., and Scherer III, W. N. A lazy concurrent list-based set algorithm. In Proceedings of 9th International Conference on Principles of Distributed Systems (Dec. 2005). </p>
<p>[5] Herlihy, M., Luchangco, V., and Moir, M. The repeat offender problem: A mechanism for supporting dynamic-sized, lock-free data structures. In Proceedings of Distributed Computing: 16th International Conference (2002). [6] Herlihy, M., and Wing, J. Linearizability: A correctness condition for concurrent objects. ACM Transactions on Programming Languages and Systems 12, 3 (July 1990), 463–492. </p>
<p>[7] Lea, D. Personal communication, 2005. </p>
<p>[8] Lea, D. ConcurrentSkipListMap. In java.util.concurrent. </p>
<p>[9] Michael, M. Hazard pointers: Safe memory reclamation for lock-free objects. IEEE Transactions on Parallel and Distributed Systems 15, 6 (June 2004), 491–504. </p>
<p>[10] Pugh, W. Concurrent maintenance of skip lists. Tech. Rep. CS-TR-2222, 1990. </p>
<p>[11] Pugh, W. Skip lists: A probabilistic alternative to balanced trees. Communications of the ACM 33, 6 (June 1990), 668–676</p>
<hr />
<h2>论文实现</h2>
<p>原本我是打算按照此篇论文自行实现一个并发安全跳表的，但是我在github上找到了一个基于此论文的优秀的golang开源实现，代码的质量很高，在此篇博客中，我将基于这个开源代码进行论文实现的代码分析。</p>
<blockquote>
<p>原仓库：<br />
<a href="https://github.com/zhangyunhao116/skipmap">https://github.com/zhangyunhao116/skipmap</a></p>
</blockquote>
<p>源作者的代码使用了Golang的泛型，但由于在本篇文章中基于泛型实现来分析源码会导致理解成本的增加，因此我在原作者的代码上进行了部分修改，以降低额外的学习难度。</p>
<p>先来看一下我们最后要怎么使用这个跳表，从一个使用案例作为代码分析的入口</p>
<pre><code class="language-go">func TestSKL(t *testing.T) {
    // 构建跳表
    skl := NewSkipList()
    // 构建测试用的K、V
    k := &amp;Item{Key: []byte(&quot;key&quot;)}
    // 测试 key2 为 nil
    k2 := &amp;Item{Key: nil}
    v1 := &quot;val&quot;
    v2 := &quot;val2&quot;
    v3 := &quot;val3&quot;
    // 加入 k v1
    skl.Store(k, v1)
    checkLoad(skl.Load(k))

    // 删除 k
    skl.Delete(k)
    checkLoad(skl.Load(k))

    // 加入 k v2
    skl.Store(k, v2)
    checkLoad(skl.Load(k))

    // 加入 k v1
    skl.Store(k, v1)
    checkLoad(skl.Load(k))

    // 不加入 k2 的时候，能否查找到 nil
    checkLoad(skl.Load(k2))

    skl.Store(k2, v3)
    checkLoad(skl.Load(k2))
}

type Item struct {
    Key []byte
}

func (i *Item) Less(key ComparableKey) bool {
    return bytes.Compare(i.Key, key.(*Item).Key) == -1
}

func checkLoad(v any, ok bool) {
    if ok {
        fmt.Println(v.(string))
    } else {
        fmt.Println(&quot;not found key&quot;)
    }
}</code></pre>
<h3>NewSkipList()</h3>
<p>先来看一下核心的结构体</p>
<pre><code class="language-go">// ConcurrentSkipList 跳表的顶层结构
type ConcurrentSkipList struct {
    length       int64
    highestLevel uint64 // highest level for now
    header       *Node
}</code></pre>
<p>ConcurrentSkipList是这个跳表的顶层结构体，拥有三个字段，维护着跳表的一些元信息，包括长度和最大高度，然后维护着头节点的引用</p>
<pre><code class="language-go">// 跳表的节点
type Node struct {
    key   ComparableKey
    value unsafe.Pointer // *any
    flags bitflag
    next  tower // [level]*orderednode
    mu    sync.Mutex
    level uint32
}</code></pre>
<p>Node是跳表最核心的节点，是跳表最基小单元。</p>
<p>每个Node中维护着当前节点的Key、Value，节点状态标记(flags)、指向下一个节点的指针列表(tower)、必要的锁、当前节点的高度的信息</p>
<p>其中Key的类型是 ComparbleKey，是一个接口，需要在用户的调用方自行实现</p>
<pre><code class="language-go">type ComparableKey interface {
    Less(than ComparableKey) bool
}</code></pre>
<p>bitflag结构体本质上就是两个bool值，用来标记是否已经属于全连接状态(前置节点和后继节点的指针全部已经准备好，此时这个节点已经可以被查询、删除、修改了)和处于删除标记状态，主要包含了set和get方法用来读取和修改标记位</p>
<pre><code class="language-go">const (
    fullyLinked = 1 &lt;&lt; iota
    deletedMarked
)

// concurrent-safe bitflag.
type bitflag struct {
    data uint32
}

func (f *bitflag) SetTrue(flags uint32) {
    for {
        old := atomic.LoadUint32(&amp;f.data)
        if old&amp;flags != flags {
            // Flag is 0, need set it to 1.
            n := old | flags
            if atomic.CompareAndSwapUint32(&amp;f.data, old, n) {
                return
            }
            continue
        }
        return
    }
}

func (f *bitflag) SetFalse(flags uint32) {
    for {
        old := atomic.LoadUint32(&amp;f.data)
        check := old &amp; flags
        if check != 0 {
            // Flag is 1, need set it to 0.
            n := old ^ check
            if atomic.CompareAndSwapUint32(&amp;f.data, old, n) {
                return
            }
            continue
        }
        return
    }
}

func (f *bitflag) Get(flag uint32) bool {
    return (atomic.LoadUint32(&amp;f.data) &amp; flag) != 0
}

func (f *bitflag) MGet(check, expect uint32) bool {
    return (atomic.LoadUint32(&amp;f.data) &amp; check) == expect
}</code></pre>
<p>tower是当前节点的指向下一个节点的指针列表，因为在跳表的示意图中，一个跳表有很多个后继指针，摞起来像个塔一样，所以是不是用tower来表示非常形象hhh，在tower中主要也就是get和set方法，用来读取修改节点在某层的后继指针</p>
<pre><code class="language-go">type tower [maxLevel]*Node

func (t *tower) load(i int) *Node {
    return t[i]
}

func (t *tower) store(i int, node *Node) {
    t[i] = node
}

// 这里看上去有些负载，因为atomic.LoadPointer方法接收的是*unsafe.Pointer参数，
// 而unsafe.Pointer(&amp;t[i]))只能转换到unsafe.Pointer类型，而且不能直接取地址
// 所以还要通过(*unsafe.Pointer)来转化类型，才能将数据转成atomic.LoadPointer能接收的类型
func (t *tower) atomicLoad(i int) *Node {
    return (*Node)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&amp;t[i]))))
}

// 和上面类似
func (t *tower) atomicStore(i int, node *Node) {
    atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&amp;t[i])), unsafe.Pointer(node))
}</code></pre>
<p>NewSkipList()方法</p>
<p>从函数名上就可以得知，这个函数的作用是新建一个跳表的实例，在内部实现中，创建了跳表的第一个节点，并且给节点设置为<code>全连接</code>的状态，创建跳表实例并返回这个实例给调用方</p>
<pre><code class="language-go">func NewSkipList() *ConcurrentSkipList {
    // 创建第一个节点
    h := newNode(nil, nil, maxLevel)
    // 设置为全连接状态
    h.flags.SetTrue(fullyLinked)
    return &amp;ConcurrentSkipList{
        header:       h,
    }
}</code></pre>
<p>newNode方法</p>
<p>传入key、value、level，构建一个Node并返回</p>
<pre><code class="language-go">func newNode(key ComparableKey, value any, level int) *Node {
    node := &amp;Node{
        key:   key,
        level: uint32(level),
    }
    node.storeVal(value)
    return node
}</code></pre>
<p>一些Node的基本操作方法，包括对值的get/set和对后继节点的get/set还包含了原子操作</p>
<pre><code class="language-go">func (n *Node) storeVal(value any) {
    atomic.StorePointer(&amp;n.value, unsafe.Pointer(&amp;value))
}

func (n *Node) loadVal() any {
    return *(*any)(atomic.LoadPointer(&amp;n.value))
}

func (n *Node) loadNext(i int) *Node {
    return n.next.load(i)
}

func (n *Node) storeNext(i int, node *Node) {
    n.next.store(i, node)
}

func (n *Node) atomicLoadNext(i int) *Node {
    return n.next.atomicLoad(i)
}

func (n *Node) atomicStoreNext(i int, node *Node) {
    n.next.atomicStore(i, node)
}</code></pre>
<h3>findNode(...)</h3>
<p>查找某一个节点，如果找到立即返回，未找到则返回nil，这是一个内部使用的辅助方法。</p>
<pre><code class="language-go">// findNode takes a key and two maximal-height arrays then searches exactly as in a sequential skipmap.
// The returned preNodes and nextNodes always satisfy preNodes[i] &gt; key &gt;= nextNodes[i].
// (without fullpath, if find the node will return immediately)
func (s *ConcurrentSkipList) findNode(key ComparableKey, preNodes *[maxLevel]*Node, nextNodes *[maxLevel]*Node) *Node {
    x := s.header
    // 从高最高节点开始查询
    for i := int(atomic.LoadUint64(&amp;s.highestLevel)) - 1; i &gt;= 0; i-- {
        // 原子获取最高层的下一个节点
        nextNode := x.atomicLoadNext(i)
        // 循环，直到找到目标节点的后继点击（或者尾节点）
        for nextNode != nil &amp;&amp; (nextNode.key.Less(key)) {
            // 如果下一个节点的键值小于目标键值，说明还需要继续向后遍历，更新当前节点为下一个节点，
            // 并更新下一个节点为当前节点的下一个节点。这个过程会在当前层级上持续进行，
            // 直到找到一个下一个节点的键值大于等于目标键值，或者下一个节点为nil。
            x = nextNode
            nextNode = x.atomicLoadNext(i)
        }
        // 此时next node

        // 在每个层级上，代码都会记录当前节点作为前驱节点，并记录下一个节点作为后继节点，存储在preNodes和nextNodes数组中。
        // 这样，当查找结束时，preNodes和nextNodes数组中存储的节点序列就构成了一个部分有序的路径，满足preNodes[i] &gt; key &gt;= nextNodes[i]的条件。
        preNodes[i] = x
        nextNodes[i] = nextNode

        // Check if the key already in the skipmap.
        if nextNode != nil &amp;&amp; nextNode.key == key {
            // 如果在查找过程中找到一个节点的键值与目标键值相等，说明目标键值已经存在于跳表中，直接返回该节点
            return nextNode
        }
    }
    // 如果没有找到，返回nil
    return nil
}</code></pre>
<p>用于delete的find辅助方法</p>
<pre><code class="language-go">// findNodeDelete takes a key and two maximal-height arrays then searches exactly as in a sequential skip-list.
// The returned preNodes and nextNodes always satisfy preNodes[i] &gt; key &gt;= nextNodes[i].
func (s *ConcurrentSkipList) findNodeDelete(key ComparableKey, preNodes *[maxLevel]*Node, nextNodes *[maxLevel]*Node) int {

    // lFound represents the index of the first layer at which it found a node.
    lFound, x := -1, s.header
    // 代码通过循环遍历每个层级的链表，从最高层级开始向下搜索，找到满足条件的节点。
    for i := int(atomic.LoadUint64(&amp;s.highestLevel)) - 1; i &gt;= 0; i-- {
        nextNode := x.atomicLoadNext(i)
        // 在每个层级上，代码比较节点的键与目标键的大小关系，根据节点的键是否小于目标键决定向后移动到下一个节点。
        for nextNode != nil &amp;&amp; (nextNode.key.Less(key)) {
            x = nextNode
            nextNode = x.atomicLoadNext(i)
        }
        // 同时，代码记录节点的前驱节点和后继节点到预定义的数组中。
        preNodes[i] = x
        nextNodes[i] = nextNode

        // Check if the key already in the skip list.
        if lFound == -1 &amp;&amp; nextNode != nil &amp;&amp; nextNode.key == key {
            // 如果在搜索过程中找到了目标节点（即节点的键等于目标键），则记录该节点所在的层级索引为lFound，表示找到了节点。
            lFound = i
        }
    }
    // 最后，代码返回lFound，即找到的节点所在的最高层级索引。
    return lFound
}</code></pre>
<p>可以看到，findNode和findNodeDelete都是实际查找节点的辅助方法，使用的时候会传入目标节点的前置指针列表和后继指针列表的引用，在方法执行过程中对其赋值，在查找过程中都是从最高层向下逐级查找，在返回值上面，findNode会返回找到的节点的引用，而findNodeDelete会返回能找到此节点的最高层级，这部分的差异在下文会有所解释</p>
<h3>Load(key)</h3>
<p>供外层调用的核心方法，用于加载一个Key所对应的Value</p>
<pre><code class="language-go">// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
func (s *ConcurrentSkipList) Load(key ComparableKey) (value any, ok bool) {
    // 从头节点开始
    x := s.header
    // 从最高节点开始
    for i := int(atomic.LoadUint64(&amp;s.highestLevel)) - 1; i &gt;= 0; i-- {
        nex := x.atomicLoadNext(i)
        for nex != nil &amp;&amp; (nex.key.Less(key)) {
            x = nex
            nex = x.atomicLoadNext(i)
        }

        // Check if the key already in the skip list.
        // 如果找到了数据
        if nex != nil &amp;&amp; nex.key == key {
            // 检查数据是否是就绪状态(处于全链接状态)
            if nex.flags.Get(fullyLinked) {
                // 如果就绪，直接返回
                return nex.loadVal(), true
            }
            return
        }
    }
    return
}</code></pre>
<p>可以看到，这Load方法是完全不加锁的，而是在返回数据时进行一个原子操作，如果节点处于就绪状态，直接返回，如果处于未就绪状态，我们可以认为在逻辑上，这个节点在这个时间点上是不在这个跳表中的，直接返回空</p>
<h3>Store(key, value)</h3>
<p>供外层调用的核心方法，用于存储一对Key-Value</p>
<pre><code class="language-go">// Store sets the value for a key.
func (s *ConcurrentSkipList) Store(key ComparableKey, value any) {
    // 获取一个随机的高度
    level := s.randomlevel()
    // 初始化目标节点的前置、后继节点的指针列表
    var preNodes, nextNodes [maxLevel]*Node
    for {
        nodeFound := s.findNode(key, &amp;preNodes, &amp;nextNodes)
        // 代码首先调用findNode函数查找指定键的节点。如果找到了节点，则表示目标键已经存在于跳表中。
        if nodeFound != nil { // indicating the key is already in the skip-list
            if !nodeFound.flags.Get(deletedMarked) {
                // We don&#039;t need to care about whether or not the node is fully linked,
                // just replace the value.
                // 代码检查该节点是否被标记（deletedMarked），如果没有被标记，则说明该节点是完全链接的，直接替换其值即可。
                nodeFound.storeVal(value)
                return
            }
            // If the node is deletedMarked, represents some other goroutines is in the process of deleting this node,
            // we need to add this node in next loop.
            // 如果节点被标记了，表示其他协程正在删除该节点的过程中，需要继续下一轮循环。
            continue
        }
        // 如果没有找到节点，则表示目标键不存在于跳表中，需要将其添加到跳表中。

        // Add this node into skip list.
        var (
            highestLocked               = -1 // the highest level being locked by this process
            valid                       = true
            preNode, nextNode, prevPred *Node
        )
        for layer := 0; valid &amp;&amp; layer &lt; level; layer++ {
            // 代码进入一个循环，在每次循环中尝试添加节点到不同的层级。
            // 在循环内部，代码对前驱节点和后继节点进行加锁，并进行一系列有效性检查。
            preNode = preNodes[layer]   // target node&#039;s previous node
            nextNode = nextNodes[layer] // target node&#039;s next node
            if preNode != prevPred {    // the node in this layer could be locked by previous loop
                preNode.mu.Lock()
                highestLocked = layer
                prevPred = preNode
            }
            // valid check if there is another node has inserted into the skip list in this layer during this process.
            // It is valid if:
            // 1. The previous node and next node both are not deletedMarked.
            // 2. The previous node&#039;s next node is nextNode in this layer.
            // 在有效性检查中，代码检查前驱节点和后继节点是否被标记，并且前驱节点的下一个节点是否为后继节点。
            // 这些检查用于确保在添加节点过程中没有其他协程插入节点。
            valid = !preNode.flags.Get(deletedMarked) &amp;&amp; (nextNode == nil || !nextNode.flags.Get(deletedMarked)) &amp;&amp; preNode.loadNext(layer) == nextNode
        }
        if !valid {
            // 如果检查失败，说明在此过程中有其他节点插入了跳表，代码需要释放已加的锁，并继续下一轮循环。
            unlockordered(preNodes, highestLocked)
            continue
        }
        // 如果有效性检查通过，代码创建一个新的节点（nn），设置其键值和层级，并将其链接到各个层级的前驱节点和后继节点之间。
        nn := newNode(key, value, level)
        for layer := 0; layer &lt; level; layer++ {
            nn.storeNext(layer, nextNodes[layer])
            preNodes[layer].atomicStoreNext(layer, nn)
        }
        // 最后，将新节点标记为完全链接（fullyLinked），释放之前加的锁，并增加跳表的长度计数。
        nn.flags.SetTrue(fullyLinked)
        unlockordered(preNodes, highestLocked)
        atomic.AddInt64(&amp;s.length, 1)
        return
    }
}

func unlockordered(preNodes [maxLevel]*Node, highestLevel int) {
    var prevPred *Node
    // 从高层向低层逐一解锁
    for i := highestLevel; i &gt;= 0; i-- {
        if preNodes[i] != prevPred { // the node could be unlocked by previous loop
            preNodes[i].mu.Unlock()
            prevPred = preNodes[i]
        }
    }
}</code></pre>
<p>总的来说，Store操作就是先通过findNode找节点，如果存在，直接通过原子操作修改值就OK，如果不存在，则给待加入节点的前驱后继节点自底向上逐层加锁，加锁成功后再次检查安全性，查看是否在加锁过程总有其它加入的节点，在通过检测后进行添加节点，逐层修改前驱节点的指向，最后将该新节点设置为全链接状态，逐层释放锁，增加跳表长度信息，添加完成。</p>
<p>多数的逻辑都很清晰，主要难理解的地方在于那个校验的过程，我们来梳理一下</p>
<pre><code class="language-go">valid = !preNode.flags.Get(deletedMarked) &amp;&amp; (nextNode == nil || !nextNode.flags.Get(deletedMarked)) &amp;&amp; preNode.loadNext(layer) == nextNode</code></pre>
<ol>
<li><code>!preNode.flags.Get(deletedMarked)</code>: 检查前驱节点<code>preNode</code>的<code>flags</code>属性是否包含<code>deletedMarked</code>标记，即确认前驱节点没有处在删除过程中。</li>
<li><code>nextNode == nil || !nextNode.flags.Get(deletedMarked)</code> : 待插入节点的后继节点是否为空或者没有在删除过程中，即确认后继节点的指针是可以指向的。</li>
<li><code>preNode.loadNext(layer) == nextNode</code>: 确认前驱节点在当前层的后继节点为待插入节点的后继节点。</li>
</ol>
<p>当以上三点全部通过的时候，则正面前后的节点都是安全的，所以可以执行插入操作</p>
<pre><code class="language-go">// 生成随机高度，并在必要的时候更新最高高度
// randomlevel returns a random level and update the highest level if needed.
func (s *ConcurrentSkipList) randomlevel() int {
    // Generate random level.
    // 可以直接使用抛硬币的方式生成节点高度
    level := randomLevel()
    // Update highest level if possible.
    for {
        hl := atomic.LoadUint64(&amp;s.highestLevel)
        if uint64(level) &lt;= hl {
            break
        }
        if atomic.CompareAndSwapUint64(&amp;s.highestLevel, hl, uint64(level)) {
            break
        }
    }
    return level
}</code></pre>
<h3>Delete(key)</h3>
<p>供外层调用的核心方法，用于删除一对Key-Value</p>
<pre><code class="language-go">// Delete deletes the value for a key.
func (s *ConcurrentSkipList) Delete(key ComparableKey) bool {

    var (
        nodeToDelete        *Node
        isMarked            bool // represents if this operation mark the node
        topLayer            = -1
        preNodes, nextNodes [maxLevel]*Node
    )
    for {
        // 代码首先调用findNodeDelete函数查找指定键的节点，并检查是否可以进行删除操作。如果满足以下条件之一，则可以进行删除操作：
        lFound := s.findNodeDelete(key, &amp;preNodes, &amp;nextNodes)
        // 如果前一次循环已经标记了要删除的节点（isMarked为true）。
        if isMarked || // this process mark this node or we can find this node in the skip list
            // 如果找到了目标节点，并且该节点(待删除的节点)被完全链接（fullyLinked），且该节点在当前层级的最高层级。
            lFound != -1 &amp;&amp; nextNodes[lFound].flags.Get(fullyLinked) &amp;&amp; (int(nextNodes[lFound].level)-1) == lFound {
            if !isMarked { // we don&#039;t mark this node for now
                nodeToDelete = nextNodes[lFound]
                topLayer = lFound
                nodeToDelete.mu.Lock()
                if nodeToDelete.flags.Get(deletedMarked) {
                    // The node is deletedMarked by another process,
                    // the physical deletion will be accomplished by another process.
                    // 如果上述条件不满足，则返回false，表示删除操作无法执行，当前节点被其它线程删除中。
                    nodeToDelete.mu.Unlock()
                    return false
                }
                // 添加删除标记
                nodeToDelete.flags.SetTrue(deletedMarked)
                isMarked = true
            }
            // Accomplish the physical deletion.
            var (
                highestLocked               = -1 // the highest level being locked by this process
                valid                       = true
                preNode, nextNode, prevPred *Node
            )
            // 如果可以执行删除操作，代码开始进行物理删除操作(从下到上)。首先，获取节点的前驱节点和后继节点，并加锁以确保操作的正确性。
            for layer := 0; valid &amp;&amp; (layer &lt;= topLayer); layer++ {
                preNode, nextNode = preNodes[layer], nextNodes[layer]
                if preNode != prevPred { // the node in this layer could be locked by previous loop
                    preNode.mu.Lock()
                    highestLocked = layer
                    prevPred = preNode
                }
                // valid check if there is another node has inserted into the skip list in this layer
                // during this process, or the previous is deleted by another process.
                // It is valid if:
                // 1. the previous node exists.
                // 2. no another node has inserted into the skip list in this layer.
                // 在有效性检查中，代码检查前驱节点和后继节点是否被标记，并且前驱节点的下一个节点是否为后继节点。
                // 这些检查用于确保在删除节点过程中没有其他协程插入节点或删除前驱节点。如果检查失败，说明在此过程中有其他节点插入了跳表，代码需要释放已加的锁，并继续下一轮循环。

                valid = !preNode.flags.Get(deletedMarked) &amp;&amp; preNode.atomicLoadNext(layer) == nextNode
            }
            if !valid {
                unlockordered(preNodes, highestLocked)
                continue
            }
            // 如果有效性检查通过，代码开始进行物理删除操作。
            for i := topLayer; i &gt;= 0; i-- {
                // Now we own the `nodeToDelete`, no other goroutine will modify it.
                // So we don&#039;t need `nodeToDelete.loadNext`
                // 在每个层级上，代码将前驱节点的下一个节点设置为要删除节点的下一个节点，从而将要删除节点从跳表中移除。
                preNodes[i].atomicStoreNext(i, nodeToDelete.loadNext(i))
            }
            // 最后，释放已加的锁，并更新跳表的长度计数。
            nodeToDelete.mu.Unlock()
            unlockordered(preNodes, highestLocked)
            atomic.AddInt64(&amp;s.length, -1)
            return true
        }
        return false
    }
}</code></pre>
<p>删除的逻辑与插入的逻辑大体相同，先调用findNodeDelete，如果没有找到则直接返回false，如果找到了则校验待删除节点状态是否为可删除状态(全链接，未被删除标记)，如果校验通过，进行物理删除操作，在通过原子操作，从最高层开始将待删除节点的前驱节点的后继指针指向待删除节点的后继节点，最后释放锁，更新跳表长度</p>
<h3>Len()</h3>
<p>供外层调用的核心方法，用于获取跳表的元素数量</p>
<pre><code class="language-go">// Len returns the length of this skipmap.
func (s *ConcurrentSkipList) Len() int {
    return int(atomic.LoadInt64(&amp;s.length))
}</code></pre>
<h3>Range(...)</h3>
<p>供外层调用的核心方法，用于获取跳表的元素数量</p>
<pre><code class="language-go">// Range calls f sequentially for each key and value present in the skipmap.
// If f returns false, range stops the iteration.
//
// Range does not necessarily correspond to any consistent snapshot of the Map&#039;s
// contents: no key will be visited more than once, but if the value for any key
// is stored or deleted concurrently, Range may reflect any mapping for that key
// from any point during the Range call.
func (s *ConcurrentSkipList) Range(f func(key any, value any) bool) {
    x := s.header.atomicLoadNext(0)
    for x != nil {
        if !x.flags.MGet(fullyLinked|deletedMarked, fullyLinked) {
            x = x.atomicLoadNext(0)
            continue
        }
        if !f(x.key, x.loadVal()) {
            break
        }
        x = x.atomicLoadNext(0)
    }
}</code></pre>
<p>从头节点开始遍历所有节点</p>
<hr />
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/05/14/lazyskiplist-a-simple-optimistic-skip-list-algorithm%e8%ae%ba%e6%96%87%e7%bf%bb%e8%af%91%e4%b8%8e%e5%ae%9e%e7%8e%b0/">LazySkiplist: A Simple Optimistic skip-list Algorithm论文翻译与实现</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2023/05/14/lazyskiplist-a-simple-optimistic-skip-list-algorithm%e8%ae%ba%e6%96%87%e7%bf%bb%e8%af%91%e4%b8%8e%e5%ae%9e%e7%8e%b0/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>速通HDD与SSD基本原理</title>
		<link>https://www.crazyfay.com/2022/10/17/%e9%80%9f%e9%80%9ahdd%e4%b8%8essd%e5%9f%ba%e6%9c%ac%e5%8e%9f%e7%90%86/</link>
					<comments>https://www.crazyfay.com/2022/10/17/%e9%80%9f%e9%80%9ahdd%e4%b8%8essd%e5%9f%ba%e6%9c%ac%e5%8e%9f%e7%90%86/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Mon, 17 Oct 2022 12:17:52 +0000</pubDate>
				<category><![CDATA[学习笔记]]></category>
		<category><![CDATA[SSD]]></category>
		<category><![CDATA[存储]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=126</guid>

					<description><![CDATA[<p>速通HDD与SSD基本原理 作为MIS专业的小弱鸡，对硬件组成的了解并不深入，而本人正在学习LSM-T结构的存 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/10/17/%e9%80%9f%e9%80%9ahdd%e4%b8%8essd%e5%9f%ba%e6%9c%ac%e5%8e%9f%e7%90%86/">速通HDD与SSD基本原理</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<h1>速通HDD与SSD基本原理</h1>
<p>作为MIS专业的小弱鸡，对硬件组成的了解并不深入，而本人正在学习LSM-T结构的存储引擎，发现有些内容一定是要对硬件有所了解之后才能又更好的认知，比如在阅读了LSM-T的经典开山论文后，有了解到LSM-T结构化随机写为顺序写的特性，可以契合HDD的特点可以极大的加快磁盘写入的速度。而在Wiskey论文中，又提出了对SSD友好的KV分离式存储结构，并通过并行读取最大化发挥SSD的优势。在软件性能上再多的优化都是建立在硬件之上的，而底层硬件的改变可能对整体的性能产生极大的质变，所以，还是很有必要对存储硬件有些基本的学习与认知</p>
<blockquote>
<p>因为本人有尖端恐惧症，看电子元器件的针脚等精细尖锐的东西会产生比较强烈的生理不适感，故曾经发誓绝对不会去做硬件相关的东西 XD<br />
本文整理自网络，源地址已丢失</p>
</blockquote>
<h2>机械硬盘工作原理</h2>
<p>机械硬盘的内部结构主要由马达、磁盘、磁头臂、磁头组成。</p>
<p>机械硬盘在工作的时候，磁头会悬浮于磁盘面上方几纳米的距离。磁盘面上有很多的小格子，小格子内有很多的小磁粒。</p>
<p>这些磁盘上的磁粒有一定的极性，当磁粒极性朝下的时候记为0，磁粒极性朝上的时候记为1，这样磁头就可以通过识别磁盘磁粒的极性读取数据了。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/20221122202028.png" alt="" /></p>
<p>而磁头也可以利用其变化的磁场改变磁盘磁粒的极性，这样就做到写入和改写磁盘数据了。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/20221122202047.png" alt="" /></p>
<p>为了能够精准定位数据所在磁盘面上的位置，磁盘本身又被划分了无数的扇区和磁道。</p>
<p><strong>假设：</strong></p>
<p>数据存放在磁盘的第五磁道的第七扇区上：</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/640" alt="图片" /></p>
<p>那磁头就会先摆动到第五磁道上空，然后等待第七扇区转过来当第七扇区转到磁头下面的时候，才可以读取数据。</p>
<p>这就是机械硬盘的工作原理，也正是因为机械硬盘是利用磁性极粒来存储数据的，所以机械硬盘通常又被称作<strong>磁盘</strong>。</p>
<p>而固态硬盘同机械硬盘的工作原理完全不同，固态硬盘采用<strong>纯电子结构</strong>。</p>
<h2>固态硬盘工作原理</h2>
<p>固态硬盘存储数据的基本单元叫<strong>浮栅晶体管</strong>，基本结构有：存储电子的浮栅层，控制极G、衬底P、源极D与漏极S。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/20221122202102.png" alt="" /></p>
<p><strong>我们将浮栅层中的电子数量高于一定值计为0，低于一定值计为1。</strong></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/20221122202128.png" alt="" /></p>
<p><strong>那固态硬盘具体是如何工作的呢？接着往下看。</strong></p>
<h3>写入数据</h3>
<p>写入数据时，需要在控制极G施加一个高压，这样电子就可以穿过隧穿层，进入浮栅层，因为有绝缘层的存在，电子不能再向前移动了，就被囚禁在了浮栅层。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/20221122202151.png" alt="" /></p>
<p>而当我们把电压撤去，这些电子依然会被囚禁在浮栅层，因为隧穿层本质上也相当于绝缘体，所以电子们只能被关押着，这样一位数据就被存储进去了。</p>
<p><strong>这些电子能被“囚禁”多长时间也就是固态硬盘能够存储数据的年限，一般一块新的固态硬盘能够保存数据的年限为10年。</strong>因为随着时间的流逝，不断地有电子“越狱”成功。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/20221122202217.png" alt="" /></p>
<p>等“越狱”的电子多到一定的数量，我们保存的数据就不见了。</p>
<h3>读取数据</h3>
<p>关于它读取数据的原理也非常简单。</p>
<p>当浮栅层中不存在电子时（存储数据为1），我们给控制级一个低压，由于电压低，电子只能被吸引到靠近隧穿层的位置，却无法穿过隧穿层，因而源极漏极可以导通，形成电流。</p>
<p><strong>如果检测到电流，那么说明它没有储存电子，则读取数据为1。</strong></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/20221122202321.png" alt="" /></p>
<p>当浮栅层中存在电子时（存储数据为0），我们还给控制极一个低压，由于浮栅层里面的电子对这些电子有排斥作用，所以电子无法被吸引到靠近隧穿层的位置，源极漏极不会导通，不会形成电流。</p>
<p><strong>如果无法检测到电流，那么说明浮栅层储存一定量电子，则读取数据为0。</strong></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/20221122202354.png" alt="" /></p>
<p>无数的浮栅晶体管堆叠在一块就可以存储大量的0和1，它们就类似于图书馆当中的书架一样，存储着无限的0101数据。</p>
<p>相对于机械硬盘这种机械结构，固态硬盘这种纯电子结构在存取速度方面的优势就非常突出。</p>
<h2>总结</h2>
<p>在机械硬盘在读取数据之前，需要先摆动磁头臂到对应的磁道上方，再等待对应的扇区转过来。</p>
<p>尽管目前的机械硬盘大部分都是7200转/分钟或者5400转/分钟的，看起来已经很快了，但是这两个操作依然会导致大约十几毫秒的延迟。</p>
<p>这种延迟对于人类来讲确实微不足道，但是对于计算机内存和CPU来讲，就确实会产生显著影响。</p>
<p>而固态硬盘全程都是电子交互，电子信号的速度要远超磁头臂和磁盘这种机械结构。</p>
<p>如果你的数据是随机分散在磁盘的各个角落，那机械硬盘需要经过多次的寻道和寻址，多次等待扇区转动到磁头底下，所以机械硬盘在读取分散性文件的时候，性能就显得非常弱，速度很慢，即<strong>随机读写性能低下</strong>。</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/10/17/%e9%80%9f%e9%80%9ahdd%e4%b8%8essd%e5%9f%ba%e6%9c%ac%e5%8e%9f%e7%90%86/">速通HDD与SSD基本原理</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2022/10/17/%e9%80%9f%e9%80%9ahdd%e4%b8%8essd%e5%9f%ba%e6%9c%ac%e5%8e%9f%e7%90%86/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>速通WebSocket协议 &#8211; 建立在HTTP之上的应用层&#8221;TCP&#8221;协议</title>
		<link>https://www.crazyfay.com/2022/10/11/%e9%80%9f%e9%80%9awebsocket%e5%8d%8f%e8%ae%ae-%e5%bb%ba%e7%ab%8b%e5%9c%a8http%e4%b9%8b%e4%b8%8a%e7%9a%84%e5%ba%94%e7%94%a8%e5%b1%82tcp%e5%8d%8f%e8%ae%ae/</link>
					<comments>https://www.crazyfay.com/2022/10/11/%e9%80%9f%e9%80%9awebsocket%e5%8d%8f%e8%ae%ae-%e5%bb%ba%e7%ab%8b%e5%9c%a8http%e4%b9%8b%e4%b8%8a%e7%9a%84%e5%ba%94%e7%94%a8%e5%b1%82tcp%e5%8d%8f%e8%ae%ae/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Tue, 11 Oct 2022 15:09:30 +0000</pubDate>
				<category><![CDATA[学习笔记]]></category>
		<category><![CDATA[WebSocket]]></category>
		<category><![CDATA[网络]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=142</guid>

					<description><![CDATA[<p>由于最近在看一些IM系统相关的内容，学习到了webSocket协议，感觉很有意思，但是感觉感觉网上没有找到很清 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/10/11/%e9%80%9f%e9%80%9awebsocket%e5%8d%8f%e8%ae%ae-%e5%bb%ba%e7%ab%8b%e5%9c%a8http%e4%b9%8b%e4%b8%8a%e7%9a%84%e5%ba%94%e7%94%a8%e5%b1%82tcp%e5%8d%8f%e8%ae%ae/">速通WebSocket协议 &#8211; 建立在HTTP之上的应用层&#8221;TCP&#8221;协议</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p>由于最近在看一些IM系统相关的内容，学习到了webSocket协议，感觉很有意思，但是感觉感觉网上没有找到很清晰描述webSocket的文章，故有此篇，文章整理了网络上的一些资料并加上我的个人理解，如有错误，欢迎指出</p>
<h1>WebSocket</h1>
<p><strong>WebSocket是一种网络传输协议</strong>，可在单个TCP连接上进行全双工通信，WebSocket使得客户端和服务器之间的数据交换变得更加简单，允许服务端主动向客户端推送数据。在WebSocket API中，浏览器和服务器只需要完成一次握手，两者之间就可以建立持久性的连接，并进行双向数据传输。WebSocket是为了在浏览器中使用长连接而定义的协议。因此，它是在http超文本传输协议的基础上升级而来，<code>Http和WebSocket协议都是基于TCP协议的</code>。</p>
<p>WebSocket 与 HTTP/2 一样，都是为了解决 HTTP 某方面的缺陷而诞生的。HTTP/2 针对的是“队头阻塞”，而 WebSocket 针对的是“请求 - 应答”通信模式。</p>
<p><strong>那么，“请求 - 应答”有什么不好的地方呢？</strong></p>
<p>“请求 - 应答”是一种“半双工”的通信模式，虽然可以双向收发数据，但同一时刻只能一个方向上有动作，传输效率低。更关键的一点，它是一种“被动”通信模式，服务器只能“被动”响应客户端的请求，无法主动向客户端发送数据。</p>
<p>虽然后来的 HTTP/2、HTTP/3 新增了 Stream、Server Push 等特性，但“请求 - 应答”依然是主要的工作方式。这就导致 HTTP 难以应用在动态页面、即时消息、网络游戏等要求“实时通信”的领域。</p>
<p>在 WebSocket 出现之前，在浏览器环境里用 JavaScript 开发实时 Web 应用很麻烦。因为浏览器是一个“受限的沙盒”，不能用 TCP，只有 HTTP 协议可用，所以就出现了很多“变通”的技术，“轮询”（polling）就是比较常用的的一种。</p>
<p>简单地说，轮询就是不停地向服务器发送 HTTP 请求，问有没有数据，有数据的话服务器就用响应报文回应。如果轮询的频率比较高，那么就可以近似地实现“实时通信”的效果。</p>
<p>但轮询的缺点也很明显，反复发送无效查询请求耗费了大量的带宽和 CPU 资源，非常不经济。</p>
<p>所以，为了克服 HTTP“请求 - 应答”模式的缺点，WebSocket 就“应运而生”了。它原来是 HTML5 的一部分，后来“自立门户”，形成了一个单独的标准，RFC 文档编号是 6455。</p>
<p>WebSocket 采用了二进制帧结构，语法、语义与 HTTP 完全不兼容，但因为它的主要运行环境是浏览器，为了便于推广和应用，就不得不“搭便车”，在使用习惯上尽量向 HTTP 靠拢，这就是它名字里“Web”的含义。</p>
<p>服务发现方面，WebSocket 没有使用 TCP 的“IP 地址 + 端口号”，而是延用了 HTTP 的 URI 格式，但开头的协议名不是“http”，引入的是两个新的名字：“ws”和“wss”，分别表示明文和加密的 WebSocket 协议。</p>
<p>WebSocket 的默认端口也选择了 80 和 443，因为现在互联网上的防火墙屏蔽了绝大多数的端口，只对 HTTP 的 80、443 端口“放行”，所以 WebSocket 就可以“伪装”成 HTTP 协议，比较容易地“穿透”防火墙，与服务器建立连接。</p>
<h2>WebSocket 协议格式</h2>
<pre><code class="language-shell"> 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+
</code></pre>
<p>WebSocket 和 HTTP/2 的关注点不同，WebSocket 更侧重于“实时通信”，而 HTTP/2 更侧重于提高传输效率，所以两者的帧结构也有很大的区别。</p>
<p>WebSocket 虽然有“帧”，但却没有像 HTTP/2 那样定义“流”，也就不存在“多路复用”“优先级”等复杂的特性，而它自身就是“全双工”的，也就不需要“服务器推送”。所以综合起来，WebSocket 的帧学习起来会简单一些。</p>
<p>上面就是 WebSocket 的帧结构定义，长度不固定，最少 2 个字节，最多 14 字节，看着好像很复杂，实际非常简单。</p>
<p>开头的两个字节是必须的，也是最关键的。</p>
<p>第一个字节的第一位“<strong>FIN</strong>”是消息结束的标志位，相当于 HTTP/2 里的“END_STREAM”，表示数据发送完毕。一个消息可以拆成多个帧，接收方看到“FIN”后，就可以把前面的帧拼起来，组成完整的消息。</p>
<p>“FIN”后面的三个位是保留位，目前没有任何意义，但必须是 0。</p>
<p>第一个字节的后 4 位很重要，叫“Opcode”，操作码，其实就是帧类型，比如 1 表示帧内容是纯文本，2 表示帧内容是二进制数据，8 是关闭连接，9 和 10 分别是连接保活的 PING 和 PONG。</p>
<p>第二个字节第一位是掩码标志位“MASK”，表示帧内容是否使用异或操作（xor）做简单的加密。目前的 WebSocket 标准规定，客户端发送数据必须使用掩码，而服务器发送则必须不使用掩码。</p>
<p>第二个字节后 7 位是“Payload len”，表示帧内容的长度。它是另一种变长编码，最少 7 位，最多是 7+64 位，也就是额外增加 8 个字节，所以一个 WebSocket 帧最大是 2^64。</p>
<p>长度字段后面是“Masking-key”，掩码密钥，它是由上面的标志位“MASK”决定的，如果使用掩码就是 4 个字节的随机数，否则就不存在。</p>
<p>这么分析下来，其实 WebSocket 的帧头就四个部分：“结束标志位 + 操作码 + 帧长度 + 掩码”，只是使用了变长编码的“小花招”，不像 HTTP/2 定长报文头那么简单明了。</p>
<h2>WebSocket 握手</h2>
<p>和 TCP、TLS 一样，WebSocket 也要有一个握手过程，然后才能正式收发数据。</p>
<p>这里它还是搭上了 HTTP 的“便车”，利用了 HTTP 本身的“协议升级”特性，“伪装”成 HTTP，这样就能绕过浏览器沙盒、网络防火墙等等限制，这也是 WebSocket 与 HTTP 的另一个重要关联点。</p>
<p>WebSocket 的握手是一个标准的 HTTP GET 请求，但要带上两个协议升级的专用头字段：</p>
<ul>
<li>“Connection: Upgrade”，表示要求协议“升级”；</li>
<li>“Upgrade: websocket”，表示要“升级”成 WebSocket 协议。</li>
</ul>
<p>另外，为了防止普通的 HTTP 消息被“意外”识别成 WebSocket，握手消息还增加了两个额外的认证用头字段（所谓的“挑战”，Challenge）：</p>
<ul>
<li>Sec-WebSocket-Key：一个 Base64 编码的 16 字节随机数，作为简单的认证密钥；</li>
<li>Sec-WebSocket-Version：协议的版本号，当前必须是 13。</li>
</ul>
<p><strong>来自客户端的握手如下所示</strong>：</p>
<pre><code class="language-http">GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13</code></pre>
<p>服务器收到 HTTP 请求报文，看到上面的四个字段，就知道这不是一个普通的 GET 请求，而是 WebSocket 的升级请求，于是就不走普通的 HTTP 处理流程，而是构造一个特殊的“101 Switching Protocols”响应报文，通知客户端，接下来就不用 HTTP 了，全改用 WebSocket 协议通信。（有点像 TLS 的“Change Cipher Spec”）</p>
<p>WebSocket 的握手响应报文也是有特殊格式的，要用字段“Sec-WebSocket-Accept”验证客户端请求报文，同样也是为了防止误连接。</p>
<p>具体的做法是把请求头里“Sec-WebSocket-Key”的值，加上一个专用的 UUID “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”，再计算 SHA-1 摘要。</p>
<pre><code class="language-shell">encode_base64(sha1(Sec-WebSocket-Key + &#039;258EAFA5-E914-47DA-95CA-C5AB0DC85B11&#039; ))</code></pre>
<p>客户端收到响应报文，就可以用同样的算法，比对值是否相等，如果相等，就说明返回的报文确实是刚才握手时连接的服务器，认证成功。返回握手信息。</p>
<p>握手完成，后续传输的数据就不再是 HTTP 报文，而是 WebSocket 格式的二进制帧了。</p>
<p><strong>来自服务器的握手如下所示</strong>:</p>
<pre><code class="language-http">HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat</code></pre>
<p>大致意思就是告诉服务器，我要升级为websocket长连，如果服务器同意，就必须返回101状态，告诉客户端，升级成功。</p>
<blockquote>
<p>服务器不可返回200状态码</p>
</blockquote>
<h2>总结</h2>
<p>浏览器是一个“沙盒”环境，有很多的限制，不允许建立 TCP 连接收发数据，而有了 WebSocket，我们就可以在浏览器里与服务器直接建立“TCP 连接”，获得更多的自由。</p>
<p>不过自由也是有代价的，WebSocket 虽然是在应用层，但使用方式却与“TCP Socket”差不多，过于“原始”，用户必须自己管理连接、缓存、状态，开发上比 HTTP 复杂的多，所以是否要在项目中引入 WebSocket 必须慎重考虑。</p>
<p><strong>WebSocket 特点</strong>：</p>
<ol>
<li>全双工通信，相当于对 TCP 做了一层“薄薄的包装”，让它运行在浏览器环境里。</li>
<li>WebSocket 使用二进制帧，结构比较简单，特殊的地方是有个“掩码”操作，客户端发数据必须掩码，服务器则不用。</li>
<li>Websocket与HTTP和HTTPS使用相同的TCP端口，可以绕过大多数防火墙的限制。</li>
<li>Websocket协议使用80端口；运行在TLS之上时，默认使用443端口。</li>
<li>协议头大小可变，节省空间。</li>
<li>opcode：自带心跳ping/pong协议码。</li>
</ol>
<p><strong>相比TCP缺点</strong>：</p>
<ol>
<li>增加了协议头，对流量有一点影响。</li>
<li>客户端发送数据载体时必须做一个编码(MASK=1)，这会增加cpu负载。</li>
<li>服务端解析HTTP Upgrade头会增加消耗。</li>
</ol>
<blockquote>
<p>tips:</p>
<ol>
<li>WebSocket 标准诞生于2011年，HTTP/2 诞生于2015年</li>
<li>WebSocket 不兼容URI后面的 <code>#</code> 标识，需要编码为 <code>%23</code></li>
<li>WebSocket 强制要求客户端发送数据使用掩码，是为了提供最基本的安全防护，让每次发送的消息都是随机、不可预测的，抵御”缓存中毒“攻击。但如果运行在SSL/TLS上，采用加密通信，那么掩码就没有必要了</li>
<li>WebSocket 协议里的 <code>PING</code>、<code>PONG</code> 帧，对于保持长连接很重要，可以让链路上总有数据在传输，防止被服务器、路由、网关认为是”无效连接“而意外关闭</li>
</ol>
</blockquote>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/10/11/%e9%80%9f%e9%80%9awebsocket%e5%8d%8f%e8%ae%ae-%e5%bb%ba%e7%ab%8b%e5%9c%a8http%e4%b9%8b%e4%b8%8a%e7%9a%84%e5%ba%94%e7%94%a8%e5%b1%82tcp%e5%8d%8f%e8%ae%ae/">速通WebSocket协议 &#8211; 建立在HTTP之上的应用层&#8221;TCP&#8221;协议</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2022/10/11/%e9%80%9f%e9%80%9awebsocket%e5%8d%8f%e8%ae%ae-%e5%bb%ba%e7%ab%8b%e5%9c%a8http%e4%b9%8b%e4%b8%8a%e7%9a%84%e5%ba%94%e7%94%a8%e5%b1%82tcp%e5%8d%8f%e8%ae%ae/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>《Java并发编程的艺术》学习笔记(四) – Volatile 全解读</title>
		<link>https://www.crazyfay.com/2022/03/06/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e5%9b%9b-volatile-%e5%85%a8%e8%a7%a3%e8%af%bb/</link>
					<comments>https://www.crazyfay.com/2022/03/06/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e5%9b%9b-volatile-%e5%85%a8%e8%a7%a3%e8%af%bb/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Sun, 06 Mar 2022 12:26:46 +0000</pubDate>
				<category><![CDATA[学习笔记]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[并发编程]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=172</guid>

					<description><![CDATA[<p>Volatile 全解读 Volatile 的定义 在多线程并发编程中synchronized和volatil [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/03/06/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e5%9b%9b-volatile-%e5%85%a8%e8%a7%a3%e8%af%bb/">《Java并发编程的艺术》学习笔记(四) – Volatile 全解读</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<h1>Volatile 全解读</h1>
<h2>Volatile 的定义</h2>
<p>在多线程并发编程中synchronized和volatile都扮演着重要的角色，volatile是轻量级的synchronized，它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时，另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话，它比synchronized的使用和执行成本更低，因为它不会引起线程上下文的切换和调度。</p>
<p>Java语言规范第3版中对volatile的定义如下：Java编程语言允许线程访问共享变量，为了确保共享变量能被准确和一致地更新，线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile，在某些情况下比锁要更加方便（读多写少）。如果一个字段被声明成volatile，Java线程内存模型确保所有线程看到这个变量的值是一致的。</p>
<p><strong>保证可见性； 禁止指令重排序。</strong></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/8cfc4044d600e46e7befa11cb614af76.png" alt="截图" /></p>
<h2>Volatile 的实现原理 - 可见性</h2>
<p>volatile是如何来保证可见性的呢？对volatile进行写操作时，CPU会做什么事情。</p>
<p>如 Java代码如下。</p>
<pre><code class="language-java">instance = new Singleton();                 // instance是volatile变量</code></pre>
<p>转变成汇编代码，如下。</p>
<pre><code class="language-shell">0x01a3de1d: movb $0×0,0×1104800(%esi);
0x01a3de24: lock addl $0×0,(%esp);</code></pre>
<p>有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码，通过查IA-32架构软件开发者手册可知，Lock前缀的指令在多核处理器下会引发了两件事情。</p>
<p><strong>1）将当前处理器缓存行的数据写回到系统内存（主存。声言Lock信号）。 2）这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。</strong></p>
<ul>
<li>Lock前缀指令会引起处理器缓存回写到内存。Lock前缀指令导致在执行指令期间，声言处理器的LOCK#信号。在多处理器环境中，LOCK#信号确保在声言该信号期间，处理器可以独占任何共享内存（主存只有当前处理器一个人可以访问，锁总线）。但是，<strong>在最近的处理器里，LOCK＃信号一般不锁总线，而是锁缓存，毕竟锁总线开销的比较大。</strong> 对于Intel486和Pentium处理器，在锁操作时，总是在总线上声言LOCK#信号。但在P6和目前的处理器中，如果访问的内存区域已经缓存在处理器内部，则不会声言LOCK#信号。相反，它会锁定这块内存区域的缓存并回写到内存（总线锁定声言lock信号的，为了提高性能，高级的处理器走的是缓存锁定，改哪里锁定哪里，所以不需要声言lock信号。），并使用缓存一致性机制来确保修改的原子性，此操作被称为“<strong>缓存锁定</strong>”，缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。</li>
<li>一个处理器的缓存回写到内存会导致其他处理器的缓存无效。IA-32处理器和Intel 64处理器使用MESI（修改、独占、共享、无效）控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候，IA-32和Intel 64处理器能嗅探其他处理器访问系统内存和它们的内部缓存。处理器使用<strong>嗅探技术</strong>保证它的内部缓存、系统内存和其他处理器的缓存的<strong>数据在总线上保持一致</strong>。例如，在Pentium和P6 family处理器中，如果通过嗅探一个处理器来检测其他处理器打算写内存地址，而这个地址当前处于共享状态，那么正在嗅探的处理器将使它的缓存行无效，在下次访问相同内存地址时，强制执行缓存行填充。</li>
</ul>
<p><strong>volatile可见性原理总结：</strong></p>
<p>为了提高处理速度，<strong>处理器不直接和内存进行通信，而是先将系统内存的数据读到内部缓存</strong>（L1，L2或其他）后再进行操作，但操作完不知道何时会写到内存。如果<strong>对声明了volatile的变量进行写操作，JVM就会向处理器发送一条Lock前缀的指令，将这个变量所在缓存行的数据写回到系统内存</strong>。但是，就算写回到内存，如果其他处理器缓存的值还是旧的，再执行计算操作就会有问题。所以，在多处理器下，为了保证各个处理器的缓存是一致的，就会<strong>实现缓存一致性协议</strong>，每<strong>个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期</strong>了，当处理器<strong>发现自己缓存行对应的内存地址被修改，就会将当前处理器的缓存行设置成无效状态</strong>，当处理器对这个数据进行修改操作的时候，会<strong>重新从系统内存中把数据读到处理器缓存里</strong>。</p>
<h2>Volatile 使用的优化</h2>
<p>著名的Java并发编程大师Doug lea在JDK 7的并发包里新增一个队列集合类Linked-TransferQueue，它在使用volatile变量时，用一种追加字节的方式来优化队列出队和入队的性能。LinkedTransferQueue的代码如下。</p>
<pre><code class="language-java">//队列中的头部节点
private transient final PaddedAtomicReference&lt;QNode&gt; head;
//队列中的尾部节点
private transient final PaddedAtomicReference&lt;QNode&gt; tail;
static final class PaddedAtomicReference &lt;T&gt; extends AtomicReference T&gt; {
  //使用很多4个字节的引用追加到64个字节
  Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
  PaddedAtomicReference(T r) {
    super(r);
  }
}
public class AtomicReference &lt;V&gt; implements java.io Serializable {
  private volatile V value;
  //省略其他代码
}</code></pre>
<p>追加字节能优化性能？这种方式看起来很神奇，但如果深入理解处理器架构就能理解其中的奥秘。让我们先来看看LinkedTransferQueue这个类，它使用一个内部类类型来定义队列的头节点（head）和尾节点（tail），而这个内部类PaddedAtomicReference相对于父类AtomicReference只做了一件事情，就是将共享变量追加到64字节。我们可以来计算下，一个对象的引用占4个字节，它追加了15个变量（共占60个字节），再加上父类的value变量，一共64个字节。</p>
<h2>Volatile 实现原理 - 禁止指令重排序</h2>
<p>为了实现volatile的内存语义，编译器在生成字节码时，会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说，发现一个最优布置来最小化插入屏障的总数几乎不可能。为此，JMM采取保守策略。</p>
<p>下面是基于保守策略的JMM内存屏障插入策略。</p>
<ul>
<li>在每个volatile<strong>写操作的前面</strong>插入一个<strong>StoreStore</strong>屏障。</li>
<li>在每个volatile<strong>写操作的后面</strong>插入一个<strong>StoreLoad</strong>屏障。</li>
<li>在每个volatile<strong>读操作的后面</strong>插入一个<strong>LoadLoad</strong>屏障。</li>
<li>在每个volatile<strong>读操作的后面</strong>插入一个<strong>LoadStore</strong>屏障。</li>
</ul>
<p>上述内存屏障插入策略非常保守，但它可以保证在任意处理器平台，任意的程序中都能得到正确的volatile内存语义。</p>
<p>如图：StoreStore屏障可以保证在volatile写之前，其前面的所有普通写操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/201f8f17656a670fa90994eeed14edf5.png" alt="截图" /></p>
<p>这里比较有意思的是，volatile写后面的StoreLoad屏障。此屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面是否需要插入一个StoreLoad屏障（比如，一个volatile写之后方法立即return）。<strong>为了保证能正确实现volatile的内存语义，JMM在采取了保守策略：在每个volatile写的后面</strong>，<del>或者在每个volatile读的前面</del> <strong>插入一个StoreLoad屏障</strong>。<strong>从整体执行效率的角度考虑，JMM最终选择了在每个volatile写的后面插入一个StoreLoad屏障</strong>。<strong>因为volatile写-读内存语义的常见使用模式是：一个写线程写volatile变量，多个读线程读同一个volatile变量。</strong> 当读线程的数量大大超过写线程时，选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里可以看到JMM在实现上的一个特点：<strong>首先确保正确性，然后再去追求执行效率</strong>。</p>
<p>图中的LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/3e5e1212181221a5a85127a22fe906cf.png" alt="截图" /></p>
<p>上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时，只要不改变volatile写-读的内存语义，编译器可以根据具体情况省略不必要的屏障。下面通过具体的示例代码进行说明。</p>
<pre><code class="language-java">class VolatileBarrierExample {
  int a;
  volatile int v1 = 1;
  volatile int v2 = 2;
  void readAndWrite() {
    int i= v1;  //第一个volatile读
    int j = v2; //第二个volatile读
    a = i+j;  //普通写
    v1 = i+ 1;  //第一个volatile写
    v2 = j * 2; //第二个volatile写
  }
  ... //其他方法
}</code></pre>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/5d46837ff8fdcff20412469456f9956d.png" alt="截图" /></p>
<h2>JSR-133增强volatile的内存语义</h2>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/11a82582e16a251bbdd77c6532e3e494.png" alt="截图" /></p>
<ul>
<li><strong>当第二个操作是volatile写时，不管第一个操作是什么，都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。</strong></li>
<li><strong>当第一个操作是volatile读时，不管第二个操作是什么，都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。</strong></li>
<li>当第一个操作是volatile写，第二个操作是volatile读时，不能重排序。</li>
</ul>
<p>在JSR-133之前的旧Java内存模型中，虽然不允许volatile变量之间重排序，但旧的Java内存模型允许volatile变量与普通变量重排序。这样会产生问题。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/7dbbf2f6971b6b2c076e1628ff01b4e4.png" alt="截图" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/6175e55461d86a8ff60df5322a3e1054.png" alt="截图" /></p>
<p>在旧的内存模型中，当1和2之间没有数据依赖关系时，1和2之间就可能被重排序（3和4类似）。其结果就是：读线程B执行4时，不一定能看到写线程A在执行1时对共享变量的修改。</p>
<h2>未使用 Volatile 下的双重检查锁</h2>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/c3fe2d403cc0161d26b49cb4baedebf2.png" alt="截图" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/e6dd888a2f11cfb8109d0ff4cd328aa5.png" alt="截图" /></p>
<h2>基于 Volatile 的解决方案</h2>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/1d27a9989c6448fe04d4c94272235c3e.png" alt="截图" /></p>
<p>大众式的讲解。因为面试过程中，绝大多数面试官都认为是volatile禁止了 new 对象里边三行代码的重排序。 因为new instance 他是一个 JVM 指令码，对应的是 new 指令。 Volatile能够保障单个JVM 指令的原子性，所以此处， <strong>new instace相当于是volatiole写</strong>，<strong>会在 new instance前加 storestore，后加storeload屏障，b线程就必须在storeload 屏障后边读取</strong>。（不建议面试使用。）实质上，new对象里边的三个小步骤，依然可以重排序，真正的控制是在外层的内存屏障控制。</p>
<h2>基于类初始化的解决方案</h2>
<p>JVM在类的初始化阶段（即在Class被加载后，且被线程使用之前），会执行类的初始化。<strong>在执行类的初始化期间，JVM会去获取一个锁</strong>。这个锁可以同步多个线程对同一个类的初始化。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/fa45697037d57f5a8f5520c95f24c950.png" alt="截图" /></p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/03/06/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e5%9b%9b-volatile-%e5%85%a8%e8%a7%a3%e8%af%bb/">《Java并发编程的艺术》学习笔记(四) – Volatile 全解读</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2022/03/06/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e5%9b%9b-volatile-%e5%85%a8%e8%a7%a3%e8%af%bb/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>《Java并发编程的艺术》学习笔记(三) – Java内存模型 &#8211; JMM</title>
		<link>https://www.crazyfay.com/2022/02/21/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%b8%89-java%e5%86%85%e5%ad%98%e6%a8%a1%e5%9e%8b-jmm/</link>
					<comments>https://www.crazyfay.com/2022/02/21/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%b8%89-java%e5%86%85%e5%ad%98%e6%a8%a1%e5%9e%8b-jmm/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Mon, 21 Feb 2022 03:23:58 +0000</pubDate>
				<category><![CDATA[学习笔记]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[并发编程]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=170</guid>

					<description><![CDATA[<p>Java内存模型 - JMM Java内存模型技术 Java的并发采用的是共享内存模型，Java线程之间的通信 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/02/21/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%b8%89-java%e5%86%85%e5%ad%98%e6%a8%a1%e5%9e%8b-jmm/">《Java并发编程的艺术》学习笔记(三) – Java内存模型 &#8211; JMM</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<h1>Java内存模型 - JMM</h1>
<h2>Java内存模型技术</h2>
<p>Java的并发采用的是共享内存模型，Java线程之间的通信总是隐式进行，整个通信过程对程序员完全透明。</p>
<p>在Java中，所有实例域、静态域和数组元素都存储在堆内存中，堆内存在线程之间共享（“共享变量”这个术语代指实例域，静态域和数组元素）。局部变量（Local Variables），方法定义参数（Java语言规范称之为Formal Method Parameters）和异常处理器参数（Exception Handler Parameters）不会在线程之间共享，它们不会有内存可见性问题，也不受内存模型的影响。</p>
<p>Java线程之间的通信由Java内存模型（本文简称为JMM）控制，JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看，JMM定义了线程和主内存之间的抽象关系：线程之间的共享变量存储在主内存（Main Memory）中，每个线程都有一个私有的本地内存（Local Memory），本地内存中存储了该线程以读/写共享变量的副本。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/980d5d9e76d066f5ab71aec43658e7df.png" alt="截图" /></p>
<h2>指令重排序</h2>
<p>在执行程序时，为了提高性能，编译器和处理器常常会对指令做重排序。重排序分3种类型。</p>
<p>1）编译器优化的重排序。编译器在<strong>不改变单线程程序语义</strong>的前提下，可以重新安排语句的执行顺序。 2）指令级<strong>并行</strong>的重排序。现代处理器采用了指令级并行技术（Instruction-Level Parallelism，ILP）来将多条指令重叠执行。如果不存在数据依赖性，处理器可以改变语句对应机器指令的执行顺序。 3）内存系统的重排序。由于处理器使用缓存和读/写缓冲区，这使得加载和存储操作看上去可能是在乱序执行。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/88cc1dd68b863112013f0f5288979aa7.png" alt="截图" /></p>
<p>对于处理器重排序，JMM的处理器重排序规则会要求 <strong>Java编译器</strong> 在生成指令序列时，插入特定类型的<strong>内存屏障</strong>（Memory Barriers，Intel称之为Memory Fence）指令，通过内存屏障指令来禁止特定类型的处理器重排序。</p>
<p>JMM属于语言级的内存模型，它确保在不同的编译器和不同的处理器平台之上，通过禁止特定类型的编译器重排序和处理器重排序，为程序员提供一致的内存可见性保证。</p>
<h2>内存屏障</h2>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/96f7b943fcc0dfc86cdc594e25c758fc.png" alt="截图" /></p>
<p><strong>StoreLoad Barriers</strong>是一个“全能型”的屏障，它同时具有其他3个屏障的效果。执行该屏障开销会很昂贵，因为当前处理器通常要把<strong>写缓冲区中的数据全部刷新到内存中</strong>（Buffer Fully Flush）。</p>
<h2>Happen-Before 原则</h2>
<p>happens-before是JMM最核心的概念。对应Java程序员来说，理解happens-before是理解JMM的关键。</p>
<p>从JDK 5开始，Java使用新的JSR-133内存模型。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中，如果一个操作执行的结果需要对另一个操作可见，那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内，也可以是在不同线程之间。</p>
<p>与程序员密切相关的happens-before规则如下。 1）程序顺序规则：一个线程中的每个操作，happens-before于该线程中的任意后续操作。 2）监视器锁规则：对一个锁的解锁，happens-before于随后对这个锁的加锁。 3）volatile变量规则：对一个volatile域的写，happens-before于任意后续对这个volatile域的读。 4）传递性：如果A happens-before B，且B happens-before C，那么A happens-before C。 5）start()规则：如果线程A执行操作ThreadB.start()（启动线程B），那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。 6）join()规则：如果线程A执行操作ThreadB.join()并成功返回，那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。</p>
<p>两个操作之间具有happens-before关系，并不意味着前一个操作必须要在后一个操作之前执行！happens-before仅仅要求前一个操作（执行的结果）对后一个操作可见，且前一个操作按顺序排在第二个操作之前</p>
<h2>as-if-serial语义</h2>
<p>as-if-serial语义的意思是：不管怎么重排序（编译器和处理器为了提高并行度），（<strong>单线程</strong>）程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。</p>
<p>为了遵守as-if-serial语义，编译器和处理器不会对存在数据依赖关系的操作做重排序，因为这种重排序会改变执行结果。但是，如果操作之间不存在数据依赖关系，这些操作就可能被编译器和处理器重排序。</p>
<p><strong>happens-before关系本质上和as-if-serial语义是一回事。</strong></p>
<p><strong>as-if-serial语义保证单线程内程序的执行结果不被改变，happens-before关系保证正确同步的多线程程序的执行结果不被改变。 as-if-serial语义给编写单线程程序的程序员创造了一个幻境：单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境：正确同步的多线程程序是按happens-before指定的顺序来执行的。 as-if-serial语义和happens-before这么做的目的，都是为了在不改变程序执行结果的前提下，尽可能地提高程序执行的并行度。</strong></p>
<h2>锁的获取与释放的内存语义</h2>
<pre><code class="language-java">class MonitorExample {
    int a = 0;
    public synchronized void writer() {　　　　 // 1
        a++;　　　　　　　　　　                // 2
    }　　　　　　　　　　　　                   // 3
    public synchronized void reader() {　　　   // 4
        int i = a;　　　　　　　　              // 5
        ……
    }　　　　　　　　　　　　                   // 6
}</code></pre>
<ul>
<li>线程A释放一个锁，实质上是线程A向接下来将要获取这个锁的某个线程发出了（线程A对<strong>共享变量所做修改</strong>的）消息。</li>
<li>线程B获取一个锁，实质上是线程B接收了之前某个线程发出的（在释放这个锁之前对共享变量所做修改的）消息。</li>
<li>线程A释放锁，随后线程B获取这个锁，这个过程实质上是线程A通过主内存向线程B发送消息。（<strong>隐式通信</strong>）</li>
</ul>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/02/21/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%b8%89-java%e5%86%85%e5%ad%98%e6%a8%a1%e5%9e%8b-jmm/">《Java并发编程的艺术》学习笔记(三) – Java内存模型 &#8211; JMM</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2022/02/21/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%b8%89-java%e5%86%85%e5%ad%98%e6%a8%a1%e5%9e%8b-jmm/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>《Java并发编程的艺术》学习笔记(二) – synchronized 全解读</title>
		<link>https://www.crazyfay.com/2022/02/08/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%ba%8c-synchronized-%e5%85%a8%e8%a7%a3%e8%af%bb/</link>
					<comments>https://www.crazyfay.com/2022/02/08/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%ba%8c-synchronized-%e5%85%a8%e8%a7%a3%e8%af%bb/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Tue, 08 Feb 2022 08:17:12 +0000</pubDate>
				<category><![CDATA[学习笔记]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[并发编程]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=168</guid>

					<description><![CDATA[<p>synchronized 全解读 Synchronized的特性 有序性 读读、写写、写读、读写 都是互斥的， [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/02/08/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%ba%8c-synchronized-%e5%85%a8%e8%a7%a3%e8%af%bb/">《Java并发编程的艺术》学习笔记(二) – synchronized 全解读</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<h1>synchronized 全解读</h1>
<h2>Synchronized的特性</h2>
<ol>
<li>
<p>有序性</p>
<p>读读、写写、写读、读写 都是互斥的，只有一条线程拿到当前的锁，当前锁不释放，其他线程只能处于BLOCK状态，等待锁的释放，然后加入下一步的竞争</p>
</li>
<li>
<p>可见性</p>
<p>完全排他</p>
</li>
<li>
<p>原子性</p>
<p>本质上是线程互斥保证的原子性</p>
</li>
<li>
<p>可重入性</p>
</li>
</ol>
<h2>Synchronized锁升级 - Mark Word（32bit）</h2>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/2654f92ba41f65199f03211c67f7397c.png" alt="截图" /></p>
<h2>Synchronized锁升级 - 偏向锁</h2>
<ol>
<li>
<p>至少JDK1.6 版本且开启了偏向锁配置。 偏向锁在Java 6和Java 7里是默认启用的，但是它在应用程序启动几秒钟之后才激活，如有必要可以使用JVM参数来关闭延迟：-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态，可以通过JVM参数关闭偏向锁：-XX:-UseBiasedLocking=false，那么程序默认会进入轻量级锁状态。</p>
</li>
<li>
<p>被加锁的对象，没有真正、或者隐式的调用父类 Object 里边的hashcode方法。</p>
<p>如果一旦调用了object的hashcode方法，那么我们的对象头里边就有真正的hashcode值了，如果偏向锁来进行markword的替换，至少要提供一个保存hashcode的地方吧？可惜的是，偏向锁并没有地方进行markword的保存，只有轻量级锁才会有“displace mark word”</p>
</li>
</ol>
<p>为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时，会在<strong>对象头</strong>（存储线程id） <strong>和栈帧中的锁记录里</strong>（线程有自己的栈帧，LOCK RECORD: 存储当前线程id） 存储锁偏向的线程ID，以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁，只需简单地测试一下对象头的Mark Word里是否存储着指向 <strong>当前线程的偏向锁。</strong>（id的匹配） 如果测试成功，表示线程已经获得了锁。如果测试失败，则需要再测试一下Mark Word中偏向锁的标识是否设置成1（表示当前是偏向锁）：如果没有设置，则使用CAS竞争锁；如果设置了，则尝试使用CAS将对象头的 <strong>偏向锁指向当前线程</strong>（ 其实是cas竞争替换 线程id）。</p>
<blockquote>
<p>注：相当于给每个每个对象固定“偏向”某个线程，没有竞争时只需要线程确认栈内记录的身份是否还偏向自己，如果身份还是匹配，则不需要真正的加锁。如果mark word中偏向的线程不是自己，则检测是否是偏向锁，如果不是则CAS锁升级争夺轻量级锁。如果目标是偏向锁，则用CAS更改mark word中偏向的线程</p>
</blockquote>
<h2>Synchronized锁升级 - 偏向锁的撤销</h2>
<p>偏向锁使用了一种等到竞争出现才释放锁的机制，一旦有竞争则升级到轻量级锁（简单且不严谨的说法）</p>
<p><strong>偏向锁使用了一种等到竞争出现才释放锁的机制</strong>，所以当其他线程尝试竞争偏向锁时，持有偏向锁的线程才会释放锁。偏向锁的撤销，需要等待全局安全点（在这个时间点上没有正在执行的字节码）。它会首先暂停拥有偏向锁的线程，然后检查持有偏向锁的线程是否活着，如果线程不处于活动状态，则将对象头设置成无锁状态；如果线程仍然活着，拥有偏向锁的栈会被执行，遍历偏向对象的锁记录，栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程，要么恢复到无锁或者标记对象不适合作为偏向锁，最后唤醒暂停的线程。 （包括锁不升级时的严谨说法）</p>
<ol>
<li>A线程获取偏向锁，并且A线程死亡退出。B线程争抢偏向锁，会直接升级当前对象的锁为轻量级锁。这只是针对我们争抢了一次。</li>
<li>A线程获取偏向锁，并且A线程没有释放偏向锁，还在syhnc的代码块里边。B线程此时过来争抢偏向锁，会直接升级为重量级锁。</li>
<li>A线程获取偏向锁，并且A线程释放了锁，但是A线程并没有死亡还在活跃状态。B线程过来争抢，会直接升级为轻量级锁。 综上所述，当我们尝试第一次竞争偏向锁时，如果A线程已经死亡，升级为轻量级锁；如果A线程未死亡，并且未释放锁，直接升级为重量级锁；如果A线程未死亡，并且已经释放了锁，直接升级为轻量级锁。</li>
<li><strong>A线程获取偏向锁，并且A线程没有释放偏向锁，还在syhnc的代码块里边。B线程多次争抢锁，会在加锁过程中采用重量级锁；但是，一旦锁被释放，当前对象还是会以轻量级锁的初始状态执行。</strong></li>
<li><strong>A线程获取偏向锁，并且A线程释放了锁，但是A线程并没有死亡还在活跃状态。B线程过来争抢。部分争抢会升级为轻量级锁；部分争抢会依旧保持偏向锁。</strong></li>
</ol>
<h2>Synchronized锁升级 - 偏向锁的重偏向与批量撤销</h2>
<p>偏向锁状态变化与最终升级为轻量级锁：</p>
<ol>
<li>A 线程获取偏向锁成功，已经退出执行不再是活跃线程； B线程过来获取偏向锁，默认前20次直接升级为轻量级锁 （触发批量重偏向阈值之前， 默认为 20次争抢，不同机器环境参数配置不一样）；</li>
<li>A 线程获取偏向锁成功，已经退出执行不再是活跃线程； B线程过来获取偏向锁，默认20次以后，直接偏向线程 B。达到40次阈值后，若再有其他线程C过来争抢，则触发批量撤销。该对象不再有任何偏向锁的情况。</li>
</ol>
<p><strong>批量重偏向：</strong> 当我们的一个对象，Object 类，在经过<strong>默认 20次</strong>的争抢的情况下，会将后边的所有争抢从新偏向争抢的线程。当B线程争抢第 18 次的时候，触发了批量重偏向的阈值；在第20次以及以后的争抢里，jvm会将线程偏向线程b，因为jvm认为，这个对象更加适合线程B</p>
<p><strong>批量撤销：</strong> 如果基于批量重偏向的基础上，还在继续进行争抢达到40次，并且有第三条线程C加入了，这个时候会触发批量撤销。JVM会标记该对象不能使用偏向锁，以后新创建的对象，直接以轻量级锁开始。 这个时候，才是真正的完成了锁升级。</p>
<p><strong>真正的锁升级，是依赖于 class 的（加锁对象实例对应的类），而并不是依赖于 某一个 new出来的对象（偏向锁升级为轻量级锁）。</strong></p>
<h2>Synchronized锁升级 - 轻量级锁加锁与解除</h2>
<p>（1）轻量级锁加锁 线程在执行同步块之前，JVM会先在当前线程的栈桢中创建用于存储锁记录的空间（Lock Record记录），并将对象头中的Mark Word（前30位 （25位的hashcode，4位的分代年龄，1位是否为偏向锁））复制到锁记录中，官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针（指向线程栈帧里边的Lock Record的指针）。如果成功，当前线程获得锁，如果失败，表示其他线程竞争锁，当前线程便尝试使用<strong>自旋</strong>来获取锁。</p>
<p>（2）轻量级锁解锁 轻量级解锁时，会使用原子的CAS操作将Displaced Mark Word（Lock Record记录）替换回到对象头，如果成功，则表示没有竞争发生。如果失败，表示当前锁存在竞争，锁就会膨胀成重量级锁。</p>
<p>轻量级锁升级为重量级锁：这个时候，只要我们的线程发生了竞争，并且CAS替换失败，就会发起锁膨胀，升级为重量级锁（针对的是一个对象实例）</p>
<h2>Synchronized锁升级 - 轻量级锁升级为重量级锁</h2>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/e778fea7130be50e20d80f744622e3c6.png" alt="截图" /></p>
<p>轻量级锁—重量级锁： 释放锁（前四步）并唤醒等待线程</p>
<ol>
<li>线程1 初始化monitor 对象；</li>
<li>将状态设置为膨胀中（inflating）；</li>
<li>将monitor里边的header属性，set称为对象的markword；（将自己lock record里边的存放的mark word的hashcode，分代年龄，是否为偏向锁 set 到 objectmonitor对象的header属性里）</li>
<li>设置对象头为重量级锁状态（标记为改为00）；然后将前30位指向第1步初始化的monitor 对象；（真正的锁升级是由线程1操控的）</li>
<li>唤醒线程2；</li>
<li>线程2 开始争抢重量级锁。（线程2就干了一件事儿，就是弄了一个临时的重量级锁指针吧？还不是最后的重量级锁指针。因为最后的重量级锁指针是线程1初始化的并且是线程1修改的。 而且，线程2被唤醒之后，还不一定能够抢到这个重量级锁。Sync是非公平锁。 线程2费力不讨好，但是线程2做了一件伟大的事情：他是锁升级的奠基者。）</li>
</ol>
<h2>Synchronized锁升级 - Markword转化过程（重难点）</h2>
<p>创建一个对象，此时对象里边没有hashcode，所以该对象可以使用我们的偏向锁，偏向锁不会考虑hashcode， 他会直接<strong>将自己的线程id放到我们的markword里边</strong>，不需要考虑后续的替换问题。 所以呢，一旦我们的对象主动调用了Object的hashcode方法，我们的偏向锁就自动不可用了。</p>
<p>如果我们的对象有了hashcode和分代年龄和是否为偏向锁（30位）。在轻量级锁的状态下，<strong>这30位会被复制到我们的轻量级锁线程持有者的栈帧里的lock record里边记录</strong>。与此同时，我们的<strong>对象的markword里边存放的是我们的指向轻量级锁线程持有者的栈帧的lock recod里</strong>。如果一直存在轻量级锁竞争，在未发生锁膨胀的前提下，一直会保持轻量级锁，<strong>A线程释放的时候，会将markword替换回对象的markword里边</strong>，B线程下次再从新走一遍displace mark word；</p>
<p>一旦发生了轻量级膨胀为重量级锁。前提，A线程持有锁；B线程争抢。 B线程将marikword里边A线程的指针替换成一个临时的（过渡的）重量级锁指针，为了让A线程在cas往回替换markword的时候失败。 A线程替换回markword失败后，会发起：1.初始化monitor对象；2. 将状态设置为膨胀中；3 <strong>将替换失败的 markword 放到 objectmonitr o的head属性里</strong>； 4。改变markword的锁标志为10；将markword里的 30 位设置为指向自己第一步初始化的那个monitor对象；5唤醒B线程； 6以后这个对象只能作为重量级锁；</p>
<p>Markword从未丢失。</p>
<h2>死锁 - 产生条件与避免</h2>
<p>（学院派的严谨理论）</p>
<p><strong>死锁产生的四个必要条件：</strong></p>
<ul>
<li>互斥：一个资源每次只能被一个进程使用 (资源独立)。</li>
<li>请求与保持：一个进程因请求资源而阻塞时，对已获得的资源保持不放 (不释放锁)。</li>
<li>不剥夺：进程已获得的资源，在未使用之前，不能强行剥夺 (抢夺资源)。</li>
<li>循环等待：若干进程之间形成一种头尾相接的循环等待的资源关闭 (死循环)。</li>
</ul>
<p><strong>如何避免死锁：</strong></p>
<ol>
<li>破坏” 互斥” 条件：系统里取消互斥、若资源一般不被一个进程独占使用，那么死锁是肯定不会发生的，但一般 “互斥” 条件是无法破坏的，因此，在死锁预防里主要是破坏其他三个必要条件，而不去涉及破坏 “互斥” 条件。</li>
<li>破坏 “请求和保持” 条件： 方法 1：所有的进程在开始运行之前，必须一次性的申请其在整个运行过程各种所需要的全部资源。 优点：简单易实施且安全。 缺点：因为某项资源不满足，进程无法启动，而其他已经满足了的资源也不会得到利用，严重降低了资源的利用率，造成资源浪费。 方法 2：该方法是对第一种方法的改进，允许进程只获得运行初期需要的资源，便开始运行，在运行过程中逐步释放掉分配到，已经使用完毕的资源，然后再去请求新的资源。这样的话资源的利用率会得到提高，也会减少进程的饥饿问题。</li>
<li>破坏 “不剥夺” 条件：当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时，它必须释放已经保持的所有资源，待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂的释放或者说被抢占了。</li>
<li>破坏 “循环等待” 条件：可以通过定义资源类型的线性顺序来预防，可以将每个资源编号，当一个进程占有编号为 i 的资源时，那么它下一次申请资源只能申请编号大于 i 的资源。</li>
</ol>
<p>（简易说法）</p>
<p><strong>避免死锁的几个常见方法：</strong></p>
<ul>
<li>避免一个线程同时获取多个锁。</li>
<li>避免一个线程在锁内同时占用多个资源，尽量保证每个锁只占用一个资源。</li>
<li>尝试使用定时锁，使用lock.tryLock（timeout）来替代使用内部锁机制。</li>
<li>对于数据库锁，加锁和解锁必须在一个数据库连接里（分布式数据库），否则会出现解锁失败的情况。</li>
</ul>
<h2>ObjectMonitor的五个重要属性</h2>
<ol>
<li>header ： 重量级锁保存markword的地方</li>
<li>own: 指向我们持有锁的线程；对象的markword里边也保存了指向monitor的指针；</li>
<li>_cxq 队列： 竞争队列。 A线程持有锁没有释放； B和C线程同时过来争抢锁，都被block了，此时会将B和C线程加入到 该队列。</li>
<li>EntryList队列：同步队列。A线程释放锁，B和C线程中会选定一个继承者（可以去争抢锁的这个线程），另外一个线程会被放入我们的EntryList队列里边。</li>
<li>waitset：等待队列。Object wait的线程。</li>
</ol>
<p>A线程持有锁，BC线程过来竞争失败，进入cxq – 下轮竞争会把 cxq里的线程移动到EntrylIst中。假设B线程竞争到了锁，然后B线程调用了 Object.Wait方法，这时候B线程进入waitset，并释放锁。C线程拿到了锁，然后唤醒B线程。B线程会从waitset里边出来，直接竞争锁。如果竞争失败进入cxq，继续轮回，如果竞争成功，ok了。</p>
<h2>CPU的用户态与内核态</h2>
<p>CPU 的两种工作状态：内核态（管态）和用户态（目态）。</p>
<p><strong>内核态：</strong></p>
<ol>
<li>系统中既有操作系统的程序，也有普通用户程序。为了安全性和稳定性，操作系统的程序不能随便访问，这就是内核态。即需要执行操作系统的程序就必须转换到内核态才能执行！</li>
<li>内核态可以使用计算机所有的硬件资源！</li>
</ol>
<p><strong>用户态：</strong> 不能直接使用系统资源，也不能改变 CPU 的工作状态，并且只能访问这个用户程序自己的存储空间！</p>
<p>当一个进程在执行用户自己的代码时处于用户运行态（用户态），此时特权级最低，为 3 级，是普通的用户进程运行的特权级，大部分用户直接面对的程序都是运行在用户态。Ring3 状态不能访问 Ring0 的地址空间，包括代码和数据；当一个进程因为系统调用陷入内核代码中执行时处于内核运行态（内核态），此时特权级最高，为 0 级。执行的内核代码会使用当前进程的内核栈，每个进程都有自己的内核栈。</p>
<p>用户运行一个程序，该程序创建的进程开始时运行自己的代码，处于用户态。如果要执行文件操作、网络数据发送等操作必须通过 write、send 等系统调用，这些系统调用会调用内核的代码。进程会切换到 Ring0，然后进入内核地址空间去执行内核代码来完成相应的操作。内核态的进程执行完后又会切换到 Ring3，回到用户态。这样，用户态的程序就不能随意操作内核地址空间，具有一定的安全保护作用。这说的保护模式是指通过内存页表操作等机制，保证进程间的地址空间不会互相冲突，一个进程的操作不会修改另一个进程地址空间中的数据。</p>
<h2>用户态与内核态切换的触发条件</h2>
<p>当在系统中执行一个程序时，大部分时间是运行在用户态下的，在其需要操作系统帮助完成一些用户态自己没有特权和能力完成的操作时就会切换到内核态。</p>
<p>用户态切换到内核态的 3 种方式 </p>
<p>（1）系统调用 这是用户态进程主动要求切换到内核态的一种方式。用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。例如 fork（）就是执行了一个创建新进程的系统调用。系统调用的机制是使用了操作系统为用户特别开放的一个中断来实现，如 Linux 的 int 80h 中断。 </p>
<p>（2）异常 当 cpu 在执行运行在用户态下的程序时，发生了一些没有预知的异常，这时会触发由当前运行进程切换到处理此异常的内核相关进程中，也就是切换到了内核态，如缺页异常。</p>
<p>（3）外围设备的中断 当外围设备完成用户请求的操作后，会向 CPU 发出相应的中断信号，这时 CPU 会暂停执行下一条即将要执行的指令而转到与中断信号对应的处理程序去执行，如果前面执行的指令时用户态下的程序，那么转换的过程自然就会是 由用户态到内核态的切换。如硬盘读写操作完成，系统会切换到硬盘读写的中断处理程序中执行后边的操作等。</p>
<p>这三种方式是系统在运行时由用户态切换到内核态的最主要方式，其中系统调用可以认为是用户进程主动发起的，异常和外围设备中断则是被动的。从触发方式上看，切换方式都不一样，但从最终实际完成由用户态到内核态的切换操作来看，步骤有事一样的，都相当于执行了一个中断响应的过程。系统调用实际上最终是中断机制实现的，而异常和中断的处理机制基本一致。</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/02/08/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%ba%8c-synchronized-%e5%85%a8%e8%a7%a3%e8%af%bb/">《Java并发编程的艺术》学习笔记(二) – synchronized 全解读</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2022/02/08/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%ba%8c-synchronized-%e5%85%a8%e8%a7%a3%e8%af%bb/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>《Java并发编程的艺术》学习笔记(一) &#8211; 并发编程初探</title>
		<link>https://www.crazyfay.com/2022/01/24/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%b8%80-%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e5%88%9d%e6%8e%a2/</link>
					<comments>https://www.crazyfay.com/2022/01/24/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%b8%80-%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e5%88%9d%e6%8e%a2/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Mon, 24 Jan 2022 10:21:23 +0000</pubDate>
				<category><![CDATA[学习笔记]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[并发编程]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=166</guid>

					<description><![CDATA[<p>并发编程初探 Java天生的多线程 一个main函数就是一个JVM进程，在IDEA中可以看到共有6条线程 查看 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/01/24/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%b8%80-%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e5%88%9d%e6%8e%a2/">《Java并发编程的艺术》学习笔记(一) &#8211; 并发编程初探</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<h1>并发编程初探</h1>
<h2>Java天生的多线程</h2>
<p>一个main函数就是一个JVM进程，在IDEA中可以看到共有6条线程</p>
<p>查看线程信息：jps（拿到线程的tid） + jstack（查看线程日志）</p>
<p>prio是进程中的优先级，os_prio是操作系统给线程定义的优先级，prio前加上deamo则为守护线程</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/6211ae1188865a265d98848f6bc2286d.png" alt="截图" /></p>
<ul>
<li>
<p><strong>[6] Monitor Ctrl-Break</strong> （跟JVM 关系不大，他是 IDEA 通过反射的方式，开启一个随着我们运行的jvm进程开启与关闭的一个监听线程。）</p>
<p>daemon prio=5 （可延迟开启）</p>
</li>
<li>
<p><strong>[5] Attach Listener </strong>（附加监听器。 简单来说，他是jdk里边一个工具类提供的jvm 进程之间通信的工具。 cmd – java -version; jvm – jstack、jmap、dump） 进程间的通信。</p>
<p>daemon prio=5 （可延迟开启）</p>
<p>开启我们这个线程的两个方式： 1. 通过jvm参数开启。-XX: StartAttachListener延迟开启： cmd – java -version –&gt; JVM 适时开启A L 线程</p>
</li>
<li>
<p><strong>[4] Signal Dispatcher</strong> （信号分发器。 我们通过cmd 发送jstack，传到了jvm进程，这时候信号分发器就要发挥作用了。）</p>
<p>daemon prio=9</p>
</li>
<li>
<p><strong>[3] Finalizer</strong> JVM 垃圾回收相关的内容。</p>
<ol>
<li>daemon prio=10 高优先级的守护线程。</li>
<li>只有当开始一轮垃圾收集的时候，才会开始调用finalize方法。</li>
<li>jvm在垃圾收集的时候，会将失去引用的对象封装到我们的 Fianlizer 对象（Reference）， 放入我们的 F-queue 队列中。由 Finalizer 线程执行inalize方法</li>
<li>Finalizer 专注垃圾收集，垃圾收集 – 并行收集，不阻碍用户线程，低优先级线程。 prio=8 他是一个守护线程啊。而且这个线程目前并没有真正的开启，不足以发生minorgc或者是 full gc</li>
</ol>
</li>
<li>
<p><strong>[2] Reference Handler</strong> （引用处理的线程。强，软，弱，虚。 -GC 有不同表现 - JVM深入分析）</p>
<p>引用处理线程-GC相关线程</p>
<p>daemon prio=10（GC很重要，优先级较高）</p>
</li>
<li>
<p><strong>[1] main</strong> 主线程</p>
<p>prio=5</p>
<p>操作系统面向的是JVM 进程，JVM 进程里面向的是 我们的main函数，。所以对于我们的操作系统如何看待我们的main函数优先级，无所谓。 只要os 给我们jvm进程足够公平的优先级就行。</p>
</li>
</ul>
<h2>线程的状态</h2>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/a9af99ee75a683abaefcfa37ac82d984.png" alt="截图" /></p>
<blockquote>
<p>注：上图的 <code>Obejct.join()</code> 应为 <code>Thread.join()</code></p>
</blockquote>
<h2>Thread.Sleep() 和 Object.Wait()</h2>
<p>Thread.Sleep()</p>
<ul>
<li>不释放锁</li>
<li>响应中断（对中断敏感）</li>
<li>会释放CPU</li>
</ul>
<p>Obeject.wait()</p>
<ul>
<li>会释放锁</li>
<li>响应中断（对中断敏感）</li>
<li>会释放CPU，让出时间片并进入等待队列</li>
</ul>
<blockquote>
<p>注：wait(0)表示永久等待</p>
</blockquote>
<h2>Thread.join()</h2>
<p>底层调用Object.Wait()</p>
<ul>
<li>
<p>会释放锁</p>
<ul>
<li>
<p>Thread的join方法，释放的是当前调用 join方法的那个对象的锁。</p>
</li>
<li>
<pre><code class="language-java">synchronized (obj){
thread.join();//不释放锁
}
synchronized (Thread.currentThread){
thread.join();//释放锁
}</code></pre>
</li>
</ul>
</li>
<li>
<p>响应中断（对中断敏感）</p>
</li>
</ul>
<h2>线程间的通讯方式</h2>
<ol>
<li>
<p>volitate 、synchronize、lock。（都保证可见性）</p>
</li>
<li>
<p>wait、notify、await() 、 signal</p>
</li>
<li>
<p>管道输入、输出流<strong>（已过时）</strong></p>
<p>管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于，它主要用于线程之间的数据传输，而传输的媒介为内存。管道输入/输出流主要包括了如下4种具体实现：PipedOutputStream、PipedInputStream、PipedReader和PipedWriter，前两种面向字节，而后两种面向字符。</p>
</li>
<li>
<p>Thread.join() ： 隐式唤醒。等待其他线程执行完成，其他线程会发送唤醒信号。</p>
</li>
<li>
<p>ThradLocal()：支持子线程集成的一种形式。</p>
</li>
<li>
<p>线程中断</p>
<p>线程中断时，sleep()会先清理中断标记，再抛出异常，导致thread.interupted = flase</p>
</li>
</ol>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/01/24/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%b8%80-%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e5%88%9d%e6%8e%a2/">《Java并发编程的艺术》学习笔记(一) &#8211; 并发编程初探</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2022/01/24/%e3%80%8ajava%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e7%9a%84%e8%89%ba%e6%9c%af%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%b8%80-%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e5%88%9d%e6%8e%a2/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Redis学习笔记(一)&#8211;基础命令</title>
		<link>https://www.crazyfay.com/2021/12/11/redis%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%b8%80-%e5%9f%ba%e7%a1%80%e5%91%bd%e4%bb%a4/</link>
					<comments>https://www.crazyfay.com/2021/12/11/redis%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%b8%80-%e5%9f%ba%e7%a1%80%e5%91%bd%e4%bb%a4/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Sat, 11 Dec 2021 03:32:28 +0000</pubDate>
				<category><![CDATA[学习笔记]]></category>
		<category><![CDATA[Redis]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=174</guid>

					<description><![CDATA[<p>Redis 基础 第一节 基本命令 通用命令 select 示例： select 0 # 选择0号数据库 ke [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2021/12/11/redis%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%b8%80-%e5%9f%ba%e7%a1%80%e5%91%bd%e4%bb%a4/">Redis学习笔记(一)&#8211;基础命令</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<h1>Redis 基础</h1>
<h2>第一节 基本命令</h2>
<h3>通用命令</h3>
<ul>
<li>
<p><font color=red><strong>select</strong></font></p>
<p>示例：</p>
<pre><code class="language-shell">select 0
# 选择0号数据库</code></pre>
</li>
<li>
<p><font color=red><strong>keys</strong></font></p>
<p>示例：</p>
<pre><code class="language-shell">keys he*
keys he[h-l]*
keys ph?
# 根据Pattern表达式查询符合条件的Key
# 注意：不要在生产环境中使用，会阻塞线程</code></pre>
</li>
<li>
<p><font color=red><strong>dbsize</strong></font></p>
<p>示例：</p>
<pre><code class="language-shell">dbsize
# 返回key的总数</code></pre>
</li>
<li>
<p><font color=red><strong>exists</strong></font></p>
<p>示例：</p>
<pre><code class="language-shell">exists a
# 查询key=a是否存在</code></pre>
</li>
<li>
<p><font color=red><strong>del</strong></font></p>
<p>示例：</p>
<pre><code class="language-shell">del a
# 删除key=a的数据</code></pre>
</li>
<li>
<p><font color=red><strong>expire</strong></font></p>
<p>示例：</p>
<pre><code class="language-shell">expire hello 20
# 设置key=hello 在20s后过期</code></pre>
</li>
<li>
<p><font color=red><strong>ttl</strong></font></p>
<p>示例：</p>
<pre><code class="language-shell">ttl hello
# 查看key=a的过期剩余时间</code></pre>
</li>
<li>
<p><font color=red><strong>flashdb</strong></font></p>
<p>示例：</p>
<pre><code class="language-shell">flashdb
# 清空当前数据库中的所有数据</code></pre>
</li>
<li>
<p><font color=red><strong>flashall</strong></font></p>
<p>示例：</p>
<pre><code class="language-shell">flashall
# 删除所有数据库中的所有数据</code></pre>
</li>
</ul>
<h2>第二节 Redis 常用的五种数据结构</h2>
<h3>Redis 数据类型 - 字符串</h3>
<p>value可以是字符串，可以是数字，可以是位数字</p>
<p>String最大512MB，建议单个KV不超过100KB</p>
<h4>字符串结构应用类型</h4>
<ol>
<li>缓存</li>
<li>秒杀</li>
<li>分布式锁</li>
<li>配置中心</li>
<li>对象序列化</li>
<li>计数器</li>
</ol>
<h4>Redis 字符串指令</h4>
<ul>
<li>
<p><font color=red><strong>get</strong></font></p>
<p>示例：</p>
<pre><code class="language-shell">get hello
# 获得key=hello的结果</code></pre>
</li>
<li>
<p><font color=red><strong>set</strong></font></p>
<p>示例：</p>
<pre><code class="language-shell">set hello world
# 设置key=hello , value=world</code></pre>
</li>
<li>
<p><font color=red><strong>mset /  mget</strong></font></p>
<p>示例：</p>
<pre><code class="language-shell">mset hello world java best
mget hello java
# 一次性 设置 或者 获取 多个kv</code></pre>
</li>
<li>
<p><font color=red><strong>getset</strong></font></p>
<p>示例：</p>
<pre><code class="language-shell">setget db mongodb
# 设置新值返回旧值，若没有旧值则返回null并赋予新值</code></pre>
</li>
<li>
<p><font color=red><strong>setnx</strong></font></p>
<pre><code class="language-shell">setnx job programmer
setnx job farmer # 执行失败
# 指定key不存在时设置，若存在则不设置</code></pre>
</li>
<li>
<p><font color=red><strong>del</strong></font></p>
<p>示例：</p>
<pre><code class="language-shell">del hello
# 删除key=hello的数据</code></pre>
</li>
<li>
<p><font color=red><strong>incr / decr</strong></font></p>
<p>示例：</p>
<pre><code class="language-shell">incr count
decr count
# key值 自增 / 自减 1</code></pre>
</li>
<li>
<p><font color=red><strong>incrby / decrby</strong></font></p>
<p>示例：</p>
<pre><code class="language-shell">incrby count 99
decrby count 99
# key值 自增 / 自减 指定步长</code></pre>
</li>
</ul>
<h3>Redis 数据类型 - hash</h3>
<p>Redis hash 是一个 string 类型的 field (字段) 和 value (值) 的映射表，hash 特别适合用于存储对象。Redis 中<strong>每个 hash</strong> 可以存储 2^32^ -1 个键值对（40多亿）。可以理解为每个key存储了一个HashMap对象，通过一个Key可以保存一个数据结构。</p>
<h4>hash应用场景</h4>
<p>Hash用于存储结构化数据，比如员工i谢谢你，用户信息等等</p>
<h4>Redis Hash 命令</h4>
<ul>
<li>
<p><font color=red><strong>hset</strong></font></p>
<p>示例：</p>
<pre><code class="language-shell">hset myhash field "foo"
# 设置单个hash字值段</code></pre>
</li>
<li>
<p><font color=red><strong>hmset</strong></font></p>
<pre><code class="language-shell">hmset myhash field1 "hello" field2 "world"
# 设置多个hash字值段</code></pre>
</li>
<li>
<p><font color=red><strong>hget</strong></font></p>
<pre><code class="language-shell">hget myhash field1
# 提取一个redis的key的某个哈希值</code></pre>
</li>
<li>
<p><font color=red><strong>hgetall</strong></font></p>
<pre><code class="language-shell">hgetall myhash
# 提取一个redis的key的所有哈希值，以键/n值/n的形式返回</code></pre>
</li>
<li>
<p><font color=red><strong>hexists</strong></font></p>
<pre><code class="language-shell">hexists myhash field</code></pre>
</li>
<li>
<p><font color=red><strong>hdel</strong></font></p>
<pre><code class="language-shell">hdel myhash field1 filed2
# 删除n条记录</code></pre>
</li>
<li>
<p><font color=red><strong>hval</strong></font></p>
<pre><code class="language-shell">hval myhash
# 返回hash的value的列表，key不存在返回空表</code></pre>
</li>
</ul>
<h3>Redis 数据类型 - List</h3>
<p>List列表是简单的字符串列表，按照插入顺序排序。可以添加元素到列表的头部（左边）或者尾部（右边）</p>
<p>一个列表最多可以包含 2^32^-1个元素</p>
<h4>List 应用场景</h4>
<p>保存有需数据队列，例如：排行榜ID队列 / XX购物车商品ID队列 / XX班级ID队列</p>
<p>获取到ID队列后，再根据每一个ID获取对应的HASH对象</p>
<h4>Redis List 命令</h4>
<ul>
<li>
<p><font color=red><strong>rpush</strong></font></p>
<pre><code class="language-shell">rpush listkey c b a
# 在队尾插入新数据</code></pre>
</li>
<li>
<p><font color=red><strong>lpush</strong></font></p>
<pre><code class="language-shell">lpush listkey f e d
# 在队首插入新数据</code></pre>
</li>
<li>
<p><font color=red><strong>lrange</strong></font></p>
<pre><code class="language-shell">lrange listkey 0 -1
# d e f c b a
# 获取所有
lrange listkey 0 2
# d e f
# 获取队列数据</code></pre>
</li>
<li>
<p><font color=red><strong>rpop</strong></font></p>
<pre><code class="language-shell">rpop listkey
# a
# 从右侧弹出数据</code></pre>
</li>
<li>
<p><font color=red><strong>lpop</strong></font></p>
<pre><code class="language-shell">lpop listkey
# d
# 从左侧弹出数据</code></pre>
</li>
</ul>
<h3>Redis 数据类型 - Set</h3>
<p>Redis 的 Set 是 String 类型的<strong>无序</strong>集合。集合成员是唯一的，即不会存在重复的数据</p>
<p>Redis 中 Set 集合是通过 哈希表 实现的，故添加、删除。查找的复杂度都是O(1)，对Set操作的执行效率极高</p>
<p>集合中的最大成员数为 2^32^-1 个元素</p>
<h4>Redis Set 应用场景</h4>
<p>Set数据类型常用于交际 / 并集 / 差集 运算</p>
<p>典型场景为：</p>
<ul>
<li>我与XX位好友共同关注了XXX</li>
<li>将两个数据源的数据清单进行合并</li>
<li>找出X与Y集合的差异</li>
<li>抽奖活动</li>
</ul>
<h4>Redis Set 命令</h4>
<ul>
<li>
<p><font color=red><strong>sadd</strong></font></p>
<pre><code class="language-shell">sadd user:1:follow fay
sadd user:1:follow kirov faye harry
# 新增set数据</code></pre>
</li>
<li>
<p><font color=red><strong>smembers</strong></font></p>
<pre><code class="language-shell">smembers user:1:follow 
# fay harry faye kirov (不保证顺序)
# 查看set数据</code></pre>
</li>
<li>
<p><font color=red><strong>srandmember</strong></font></p>
<pre><code class="language-shell">srandmember user:1:follow 2
# fay kirov (不保证顺序,不改变原集合中的顺序)</code></pre>
</li>
<li>
<p><font color=red><strong>spop</strong></font></p>
<pre><code class="language-shell">spop user:1:follow
# fay (随机且改变原集合)
# 随机弹出一个元素</code></pre>
</li>
<li>
<p><font color=red><strong>sdiff</strong></font></p>
<pre><code class="language-shell">sdiff user:2:follow user:1:follow
# sports ent news
# sdiff计算的是以左侧Set为基准，查找在右侧Set不存在的条目</code></pre>
</li>
<li>
<p><font color=red><strong>sinter</strong></font></p>
<pre><code class="language-shell">sinter user:1:follow user:2:follow
# 计算交集</code></pre>
</li>
<li>
<p><font color=red><strong>sunion</strong></font></p>
<pre><code class="language-shell">sunion user:1:follow user:2:follow
# 计算并集</code></pre>
</li>
</ul>
<h3>Redis 数据类型 - ZSet</h3>
<p>Redis 的 ZSet 是 String 类型的<strong>有序</strong>Set集合。集合成员是唯一的，即不会存在重复的数据</p>
<p>ZSet 和 List 的区别</p>
<p>List 强调数据是按存储顺序有序排列，存储顺序与迭代顺序是一致的</p>
<p>ZSet 则是给出分数进行排序，存储顺序与迭代顺序是不一致的</p>
<h4>Redis ZSet 应用场景</h4>
<p>各种多维度排行榜</p>
<h4>Redis ZSet 命令</h4>
<ul>
<li>
<p><font color=red><strong>zadd</strong></font></p>
<pre><code class="language-shell">zadd player:ranke 1000 fay 900 kirov 800 pinkman 600 grovy
# 新增数据</code></pre>
</li>
<li>
<p><font color=red><strong>zrange</strong></font></p>
<pre><code class="language-shell">zrange player:rank 0 -1
# grovy pinkman kirov fay
zrange player:rank 0 -1 with scores
# grovy 600 pinkman 800 kirov 900 fay 1000 (将分值一并写出)
# 根据分值从小到大进行排列</code></pre>
</li>
<li>
<p><font color=red><strong>zscore</strong></font></p>
<pre><code class="language-shell">zscore play:rank fay
# 获取分数</code></pre>
</li>
<li>
<p><font color=red><strong>zrank</strong></font></p>
<pre><code class="language-shell">zrank play:rank fay
# 获取排名</code></pre>
</li>
<li>
<p><font color=red><strong>zrevrank</strong></font></p>
<pre><code class="language-shell">zrank play:rank fay
# 反向排名</code></pre>
</li>
<li>
<p><font color=red><strong>zrevrange</strong></font></p>
<pre><code class="language-shell">zrevrange player:rank 0 -1
zrevrange player:rank 0 -1 with scores
# 反向排序</code></pre>
</li>
<li>
<p><font color=red><strong>zrem</strong></font></p>
<pre><code class="language-shell">zrem play:rank grovy
# 移除指定元素</code></pre>
</li>
<li>
<p><font color=red><strong>zcount</strong></font></p>
<pre><code class="language-shell">zcount player:rank 700 1000
# 3
# 获取符合分数要求数据量</code></pre>
</li>
<li>
<p><font color=red><strong>zrangebyscore</strong></font></p>
<pre><code class="language-shell">zrangebyscore player:rank 700 1000 wiht scores
# pinkman 800 kirov 900 fay 1000</code></pre>
</li>
</ul>
<h2>第三节 Redis 开发规约</h2>
<h3>Key 的设计</h3>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/image-20220413002604596.png" alt="image-20220413002604596" /></p>
<h3>不同类型的应用场景</h3>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/image-20220413002627850.png" alt="image-20220413002627850" /></p>
<h3>Redis 的安全建议</h3>
<ul>
<li>Redis 不要在外网被访问，禁止：bind 0.0.0.0
<ul>
<li>- bind 192.168.132.128</li>
</ul>
</li>
<li>更改 Redis 端口，不要6379
<ul>
<li>port : 8838</li>
</ul>
</li>
<li>Redis 使用非 root 启动</li>
<li>Redis 没有设置密码或者弱密码，不要与登陆密码相同
<ul>
<li>requirepass 与 masterauth</li>
</ul>
</li>
<li>定期备份，save 命令</li>
<li>配置号 Linux 防火墙规则</li>
</ul>
<p><a rel="nofollow" href="https://www.crazyfay.com/2021/12/11/redis%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%b8%80-%e5%9f%ba%e7%a1%80%e5%91%bd%e4%bb%a4/">Redis学习笔记(一)&#8211;基础命令</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2021/12/11/redis%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0%e4%b8%80-%e5%9f%ba%e7%a1%80%e5%91%bd%e4%bb%a4/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
