2024年2月

今年.NET Conf China 2023技术大会,我给大家分享了 .NET应用国际化-AIGC智能翻译+代码生成的议题

.NET Conf China 2023分享-.NET应用国际化-AIGC智能翻译+代码生成

今天将详细的代码实现和大家分享一下。

一、前提准备

1. 新建一个Console类的Project

2. 引用SK的Nuget包,SK的最新Nuget包

dotnet add package Microsoft.SemanticKernel --version 1.4.0
<ItemGroup>
    <PackageReference Include="Microsoft.SemanticKernel" Version="1.4.0" />
    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
  </ItemGroup>

3. 在Azure OpenAI Service中创建一个GPT4的服务,这个可能大家没有账号,那就先看代码如何实现吧

部署好GPT4模型后,可以拿到以下三个重要的值

Azure OpenAI Deployment Name
Azure OpenAI Endpoint

Azure OpenAI Key
二、编写翻译使用的Prompt
{{$input}}
请将上面的输入翻译为英文,不要返回任何解释说明,
请扮演一个美国电动汽车充电服务运营商(精通中文和英文),用户的输入数据是JSON格式,例如{
"1":"充电站", "2":"充电桩"},
如果不是JSON格式,请返回无效的输入。
请使用以下专业术语进行翻译
{
"充电站":"Charging station","电站":"Charging station","场站":"Charging station","充电桩":"Charging point","充电终端":"Charging point","终端":"Charging point","电动汽车":"Electric Vehicle","直流快充":"DC Fast Charger","超级充电站":"Supercharger","智能充电":"Smart Charging","交流慢充":"AC Slow Charging"}
翻译结果请以JSON格式返回,例如 {
"1":"Charging station", "2":"Charging point"}

类似的还有葡萄牙下的翻译Prompt

{{$input}}
请将上面的输入翻译为葡萄牙语,不要返回任何解释说明,请扮演一个巴西的电动汽车充电服务运营商(精通葡萄牙语、中文和英文)
用户的输入数据是JSON格式,例如{
"1":"充电站", "2":"充电桩"}, 如果不是JSON格式,请返回无效的输入
请使用以下专业术语进行翻译
{
"充电站": "Estação de carregamento","电站": "Estação de carregamento","场站": "Estação de carregamento","充电桩": "Ponto de carregamento","充电终端": "Ponto de carregamento","终端": "Ponto de carregamento","电动汽车": "Veículo Elétrico","直流快充": "Carregador Rápido DC","超级充电站": "Supercharger","智能充电": "Carregamento Inteligente","交流慢充": "Carregamento AC Lento"}
请以JSON格式返回,例如 {
"1":"Estação de carregamento", "2":"Ponto de carregamento"}

在项目工程下新建Plugins目录和TranslatePlugin子目录,同时新建Translator_en和Translator_pt等多个子目录

config.json文件下的内容如下:

{"schema": 1,"type": "completion","description": "Translate.","completion": {"max_tokens": 2000,"temperature": 0.5,"top_p": 0.0,"presence_penalty": 0.0,"frequency_penalty": 0.0},"input": {"parameters": [
{
"name": "input","description": "The user's input.","defaultValue": ""}
]
}
}

三、Translator翻译类,实现文本多语言翻译

这个类主要实现将用户输入的文本(系统处理为JSON格式),翻译为指定的语言

using System.Runtime.InteropServices;
using Microsoft.SemanticKernel;
using Newtonsoft.Json;


namespace LLM_SK;
public class Translator
{
Kernel kernel;
public Translator(Kernel kernel)
{
this.kernel = kernel;
}


public IDictionary<int, string> Translate(IDictionary<int, string> textList, string language)
{
var pluginDirectory = Path.Combine(System.IO.Directory.GetCurrentDirectory(), "Plugins/TranslatePlugin");
var plugin = kernel.CreatePluginFromPromptDirectory(pluginDirectory, "Translator_" + language + "");


var json = JsonConvert.SerializeObject(textList);


if (!string.IsNullOrEmpty(json))
{
var output = kernel.InvokeAsync(plugin["Translator_" + language + ""], new() { ["input"] = json }).Result.ToString();
if (!string.IsNullOrWhiteSpace(output))
{
Console.WriteLine(output);
return JsonConvert.DeserializeObject<Dictionary<int, string>>(output);
}
}


return new Dictionary<int, string>();
}
}

这个类中构造函数中接收传入的Kernel对象,这个Kernel对象是指

Microsoft.SemanticKernel.Kernel
//
//Summary://Provides state for use throughout a Semantic Kernel workload.//
//Remarks://An instance of Microsoft.SemanticKernel.Kernel is passed through to every function//invocation and service call throughout the system, providing to each the ability//to access shared state and services.
public sealed class Kernel

暂且理解为调用各类大模型的Kernel核心类,基于这个Kernel实例对象完成大模型的调用和交互

另外,上述代码中有个Prompt模板文件读取的操作。

var pluginDirectory = Path.Combine(System.IO.Directory.GetCurrentDirectory(), "Plugins/TranslatePlugin");
var plugin = kernel.CreatePluginFromPromptDirectory(pluginDirectory, "Translator_" + language + "");

从Plugins/TranslatePlugin目录下读取指定的KernelPlugin,例如Translator_en英语翻译插件和Translator_pt 葡萄牙翻译插件

var output = kernel.InvokeAsync(plugin["Translator_" + language + ""], new() { ["input"] = json }).Result.ToString();

调用KernelFunction方式实现GPT4大模型调用

 //
    // Summary:
    //     Invokes the Microsoft.SemanticKernel.KernelFunction.
    //
    // Parameters:
    //   function:
    //     The Microsoft.SemanticKernel.KernelFunction to invoke.
    //
    //   arguments:
    //     The arguments to pass to the function's invocation, including any Microsoft.SemanticKernel.PromptExecutionSettings.
    //
    //
    //   cancellationToken:
    //     The System.Threading.CancellationToken to monitor for cancellation requests.
    //     The default is System.Threading.CancellationToken.None.
    //
    // Returns:
    //     The result of the function's execution.
    //
    // Exceptions:
    //   T:System.ArgumentNullException:
    //     function is null.
    //
    //   T:Microsoft.SemanticKernel.KernelFunctionCanceledException:
    //     The Microsoft.SemanticKernel.KernelFunction's invocation was canceled.
    //
    // Remarks:
    //     This behaves identically to invoking the specified function with this Microsoft.SemanticKernel.Kernel
    //     as its Microsoft.SemanticKernel.Kernel argument.
    public Task<FunctionResult> InvokeAsync(KernelFunction function, KernelArguments? arguments = null, CancellationToken cancellationToken = default(CancellationToken))
    {
        Verify.NotNull(function, "function");
        return function.InvokeAsync(this, arguments, cancellationToken);
    }

继续封装GPT4TranslateService,构造Microsoft.SemanticKernel.Kernel 类实例。

usingSystem.Globalization;usingMicrosoft.SemanticKernel;namespaceLLM_SK;public classGPT4TranslateService
{
public IDictionary<int,string> Translate(IDictionary<int, string>texts, CultureInfo cultureInfo)
{
var kernel =BuildKernel();var translator = newTranslator(kernel);returntranslator.Translate(texts, cultureInfo.TwoLetterISOLanguageName );
}
//私有方法,构造IKernel privateKernel BuildKernel()
{
var builder =Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
"xxxxgpt4", //Azure OpenAI Deployment Name "https://****.openai.azure.com/", //Azure OpenAI Endpoint "***************"); //Azure OpenAI Key returnbuilder.Build();
}
}

四、测试调用

这里我们设计了2种语言,英语和葡萄牙的文本翻译

var culture = new CultureInfo("en-US");var translator = newGPT4TranslateService();
translator.Translate(
new Dictionary<int, string>(){{ 1,"电站"}, {2,"终端不可用"},{3,"充电桩不可用"} ,
{
4,"场站"},{5,"充电站暂未运营"}},culture);

culture
= new CultureInfo("pt-BR");
translator.Translate(
new Dictionary<int, string>(){{ 1,"电站"}, {2,"终端不可用"},{3,"充电桩不可用"} ,
{
4,"场站"},{5,"充电站暂未运营" }},culture);

输出的结果

{"1":"Charging station","2":"Charging point unavailable","3":"Charging station unavailable","4":"Charging station","5":"Charging station not in operation yet"}
{
"1":"Estação de carregamento","2":"Ponto de carregamento não está disponível","3":"Ponto de carregamento não está disponível","4":"Estação de carregamento","5":"A estação de carregamento ainda não está em operação"}

五、总结

以上是基于SemanticKernel和GPT4实现一个智能翻译服务的Demo和框架,大家可以基于这个示例继续完善,增加更多动态的数据和API调用,例如将JSON数据写入数据库

同时还可以记录翻译不稳定的异常,手工处理或者继续完善Prompt。

周国庆

2024/2/17

并发编程防御装-锁(基础版)

大家好,我是小高先生。在Java并发编程的世界中,锁的地位至关重要。它就像是一道坚固的防线,确保了并发编程运行结果的正确性。你可以不准备攻击装备,但是锁这个防御装备是必不可少的。相信大家在之前都对锁或多或少有些了解,本文将带领大家学习锁的基础知识。

  • 乐观锁和悲观锁
  • synchronized案例
  • synchronized字节码分析
  • synchronized锁的是什么
  • 公平锁和非公平锁
  • 可重入锁
  • 死锁

乐观锁和悲观锁

在并发编程的世界中,悲观锁和乐观锁是两种截然不同的锁定策略,每种策略都有其适用的场合和特定的使用场景。

悲观锁,如其名所示,持有一种对数据冲突的悲观看法。
它假设在共享数据的访问过程中,很可能会遇到其他线程的争用和修改,这可能导致数据的不一致性和其他问题
。因此,为了确保数据修改操作的安全性,悲观锁会采取一种保守的策略:
在修改数据之前,它会加锁,确保在数据被修改的同时,没有其他线程可以访问这些数据
。这种策略的代表是Java中的
synchronized
关键字和
Lock
接口的实现类。悲观锁尤其适用于
写操作较为频繁
的环境,通过预先加锁,它可以保证在进行写入操作时的数据一致性。

相对于悲观锁,乐观锁则持有一种相反的乐观态度。它假设在大多数情况下,共享数据在被访问时不会发生冲突,因此
不会默认进行加锁
。在Java中,乐观锁通常通过
无锁编程
来实现,允许所有线程访问共享数据。但是,只有在数据实际被写入内存时,线程才会检查在此期间是否有其他线程也进行了更新。如果数据未被其他线程修改,当前线程就可以成功地将其更改写入内存。如果检测到数据已被其他线程更新,当前线程可能需要采取其他措施,例如放弃其更改或进入重试循环。

乐观锁的常见实现方式包括:

  • 版本号机制(Version)
  • CAS算法

乐观锁适合读操作较多的环境,不加锁可以提升读取操作的性能。如果使用悲观锁,同一时间只能有一个线程获取到锁,这可能会影响效率。

总的来说,悲观锁和乐观锁各自代表了安全性和效率的两个极端。悲观锁提供了高度的安全性,但可能牺牲了效率;而乐观锁追求高效的操作,但可能在某些情况下牺牲了安全性。在实际的应用场景中,我们需要根据具体的需求和环境来选择最合适的锁定策略,以达到既安全又高效的并发编程。

synchronized案例

阿里的Java开发手册中说明了高并发时,同步调用应该考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用RPC方法。

下面以8个案例说明上述准则

1.有两个线程a和b,是先打印邮件还是先打印短信?(先打印邮件)

class Phone{
    public synchronized void sendEmail(){
        System.out.println("-----sendEmail");
    }
    public synchronized void sendSMS(){
        System.out.println("-----sendSMS");
    }
}
public class Lock8Demo {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        },"a").start();

        //暂停,保证a先启动
        TimeUnit.MILLISECONDS.sleep(200);

        new Thread(() -> {
            phone.sendSMS();
        },"b").start();
    }
}

2.sendEmai方法中暂停3s,先打印邮件还是短信?(先打印邮件)

class Phone{
    public synchronized void sendEmail() throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);
        System.out.println("-----sendEmail");
    }
    public synchronized void sendSMS(){
        System.out.println("-----sendSMS");
    }
}
public class Lock8Demo {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"a").start();

        //暂停,保证a先启动
        TimeUnit.MILLISECONDS.sleep(200);

        new Thread(() -> {
            phone.sendSMS();
        },"b").start();
    }
}

3.添加一个普通的hello方法,请问先打印邮件还是hello?(先打印hello)

class Phone{
    public synchronized void sendEmail() throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);
        System.out.println("-----sendEmail");
    }
    public synchronized void sendSMS(){
        System.out.println("-----sendSMS");
    }
    public void hello(){
        System.out.println("hello");
    }
}
public class Lock8Demo {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"a").start();

        //暂停,保证a先启动
        TimeUnit.MILLISECONDS.sleep(200);

        new Thread(() -> {
            phone.hello();
        },"b").start();
    }
}

4.有两部手机,先打印邮件还是短信?(先打印短信)

class Phone{
    public synchronized void sendEmail() throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);
        System.out.println("-----sendEmail");
    }
    public synchronized void sendSMS(){
        System.out.println("-----sendSMS");
    }
    public void hello(){
        System.out.println("hello");
    }
}
public class Lock8Demo {
    public static void main(String[] args) throws InterruptedException {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            try {
                phone1.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"a").start();

        //暂停,保证a先启动
        TimeUnit.MILLISECONDS.sleep(200);

        new Thread(() -> {
            phone2.sendSMS();
        },"b").start();
    }
}

5.有两个静态方法,有一部手机,先打印邮件还是短信?(先打印邮件)

class Phone{
    public static synchronized void sendEmail() throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);
        System.out.println("-----sendEmail");
    }
    public static synchronized void sendSMS(){
        System.out.println("-----sendSMS");
    }
    public void hello(){
        System.out.println("hello");
    }
}
public class Lock8Demo {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"a").start();

        //暂停,保证a先启动
        TimeUnit.MILLISECONDS.sleep(200);

        new Thread(() -> {
            phone.sendSMS();
        },"b").start();
    }
}

6.有两个静态方法,有两部手机,先打印邮件还是短信?(先打印邮件)

class Phone{
    public static synchronized void sendEmail() throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);
        System.out.println("-----sendEmail");
    }
    public static synchronized void sendSMS(){
        System.out.println("-----sendSMS");
    }
    public void hello(){
        System.out.println("hello");
    }
}
public class Lock8Demo {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            try {
                phone.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"a").start();

        //暂停,保证a先启动
        TimeUnit.MILLISECONDS.sleep(200);

        new Thread(() -> {
            phone2.sendSMS();
        },"b").start();
    }
}

7.有一个静态同步方法,有一个普通同步方法,有一部手机,先打印邮件还是短信?(先打印短信)

class Phone{
    public static synchronized void sendEmail() throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);
        System.out.println("-----sendEmail");
    }
    public synchronized void sendSMS(){
        System.out.println("-----sendSMS");
    }
    public void hello(){
        System.out.println("hello");
    }
}
public class Lock8Demo {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"a").start();

        //暂停,保证a先启动
        TimeUnit.MILLISECONDS.sleep(200);

        new Thread(() -> {
            phone.sendSMS();
        },"b").start();
    }
}

8.有一个静态同步方法,有一个普通同步方法,有两部手机,先打印邮件还是短信?(先打印短信)

class Phone{
    public static synchronized void sendEmail() throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);
        System.out.println("-----sendEmail");
    }
    public synchronized void sendSMS(){
        System.out.println("-----sendSMS");
    }
    public void hello(){
        System.out.println("hello");
    }
}
public class Lock8Demo {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        Phone phone2 = new Phone();

        new Thread(() -> {
            try {
                phone.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"a").start();

        //暂停,保证a先启动
        TimeUnit.MILLISECONDS.sleep(200);

        new Thread(() -> {
            phone2.sendSMS();
        },"b").start();
    }
}

八案例总结:

  • 1-2:
    • 当一个对象中有多个使用synchronized关键字修饰的方法时,在同一时刻,只能有一个线程能够调用其中的任何一个方法。这是因为synchronized关键字锁定的是当前对象的this引用,就是new出来的Phone。一旦一个线程获得了某个对象的锁,其他线程将无法进入该对象的任何其他被synchronized修饰的方法,直到锁被释放。
  • 3-4:
    • 没有加synchronized的方法不受synchronized的影响
    • 有两个对象后,synchronized锁的不是同一个对象,两个phone互不影响,相当于是两把锁
  • 5-6:
    • 在涉及静态资源的加载时,synchronized关键字锁定的是类的字节码对象,而非实例对象。这意味着无论创建了多少个类的实例,synchronized修饰的静态方法始终是针对类本身进行加锁。因此,在多线程环境中,即便创建了多个Phone对象,当一个线程正在执行某个synchronized修饰的静态方法时,其他线程将无法同时执行该类的任何其他synchronized静态方法,它们必须等待锁被释放后才能继续。
    • 对于普通同步方法,锁的是当前对象,通常指this,所有普通同步方法使用的是同一把锁。
    • 对于静态同步方法,锁的是当前类的Class对象,如Phone。
    • 对于同步代码块,锁的是synchronized括号里的对象
  • 7-8:
    • 类锁和对象锁互不干涉

synchronized字节码分析

synchronized是 Java 中用于实现线程同步的关键字,它有三种常见的应用方式:

  1. 锁代码块(Synchronized Blocks):
    通过在方法内部使用 synchronized关键字修饰一个代码块,可以确保在同一时刻只有一个线程能够执行该代码块。这种方式通常用于保护共享资源的访问,以避免多线程并发访问导致的数据不一致问题。示例如下:
Object lock = new Object();

void someMethod() {
    synchronized (lock) {
        // 需要同步的代码块
        // ...
  1  }
}

  1. 锁静态方法(Synchronized Static Methods):
    当一个方法被声明为static有实例共享。如果一个静态方法使用了 synchronized关键字进行修饰,那么它将锁定整个类对象,而不仅仅是单个实例。这意味着在同一时刻,只有一个线程能够执行该类的任何静态同步方法。示例如下:
class MyClass {
    static void myStaticMethod() {
        synchronized (MyClass.class) {
            // 需要同步的静态方法代码
            // ...
        }
    }
}

  1. 锁普通方法(Synchronized Instance Methods):
    对于非静态方法,可以使用
    synchronized
    关键字直接修饰方法。这样,当一个线程调用该方法时,它将获取到对象锁,从而确保在同一时刻只有一个线程能够执行该方法。示例如下:
class MyClass {
    synchronized void myMethod() {
        // 需要同步的实例方法代码
        // ...
    }
}

我们从字节码的角度分析一下synchronized实现,通过
javap -c xxx.class命令
对字节码文件反编译,如果想看更多信息,可以用
javap -v xxx.class

synchronized关键字的实现细节可以通过分析字节码来深入理解。字节码是Java代码编译后的中间表示形式,它描述了程序执行时的各个指令和操作。通过使用javap命令行工具,我们可以对字节码文件进行反编译,以便查看其中的内容。

1.同步代码块

public class LockSyncDemo {
    Object object = new Object();

    public void m1(){
        synchronized (object){
            System.out.println("hello");
        }
    }

    public static void main(String[] args) {

    }
}

在IDEA打开终端,输入
javap -c .\LockSyncDemo.class
运行。

第6行monitorenter为进入synchronized同步代码块的指令,第16行monitorexit为退出同步代码块的指令,是一对的,但是发现22行多出来一个monitorexit,这是哪来的?在正常情况下,第一个monitorexit指令用于在同步代码块执行完毕后释放锁。而第二个monitorexit指令则是为了确保在同步代码块中发生异常时,锁能够被正确释放,从而避免死锁或资源泄漏的问题。

一般情况下就是一个monitorenter对应两个monitorexit,但也有极端情况,就是在代码块里抛出异常,就会发现只有一个monitorexit了。

2.同步方法

public class LockSyncDemo {
    /*
    Object object = new Object();

    public void m1(){
        synchronized (object){
            System.out.println("hello");
        }
    }
    */

    public synchronized void m2(){
        System.out.println("hello");
    }
    public static void main(String[] args) {

    }
}

javap -v .\LockSyncDemo.class
进行反编译,可以发现方法处有ACC_SYNCHRONIZED标志,代表方法被synchronized修饰,同步方法没有monitor指令,JVM遇到这个标志时,它会在执行方法前自动获取锁,并在方法执行完毕后释放锁,以确保在同一时刻只有一个线程能够执行被synchronized修饰的方法。

3.同步静态方法

public class LockSyncDemo {
    /*
    Object object = new Object();

    public void m1(){
        synchronized (object){
            System.out.println("hello");
        }
    }
    */

    public synchronized static void m2(){
        System.out.println("hello");
    }

    public static void main(String[] args) {

    }
}

静态方法和普通方法没差太多,就是多一个ACC_STATIC标志。

synchronized锁的是什么

大家想一个问题,为什么任何对象都能成为一个锁?Java是以C++为基础进行改进得到的,那我们就通过反编译从底层C++的角度来分析一下。

先来了解一个概念,
管程/监视器(monitor)
,具体概念太复杂,可以直接晚上搜索一下,直白说就是锁。JVM可以支持同步方法和同步代码块,这两种同步结构就是通过管程monitor实现的。比如同步方法调用时,调用指令会检查方法的ACC_SYNCHRONIZED标志是否被设置,如果设置了,执行线程就要先获取Monitor才能执行方法,最后不管方法是否成功完成都要释放Monitor。在线程持有Monitor的期间,其他线程不可获取同一个Monitor。

那为什么Java中任何一个对象都能成为一个锁呢?因为在HotSpot虚拟机中,monitor采用ObjectMonitor实现的。我们用C++源码解读:

java源码中可以找到ObjectMonitor.java文件,但JVM真正运行的是ObjectMonitor.cpp文件,这个文件又有引入头文件ObjectMonitor.hpp。在ObjectMonitonr.hpp文件中,有一段初始化monitor的代码,里面有一大堆预先设置的变量,看一下ObjectMonitor中几个关键属性:

属性 作用
_owner 指向持有ObjectMonitor对象的线程
_WaitSet 存放处于wait状态的线程队列
_EntryList 存放处于等待锁block状态的线程队列
_recursions 锁的重入次数
_count 用来记录该线程获取锁的次数

这个
_owner
就是锁的关键,这个锁对象被哪个线程持有了,这个ObjectMonitor就会把该属性设置为对应线程,谁持有谁记录,谁释放谁取消。

在Java中,Object和ObjectMonitor之间的关联是
通过对象头中锁的状态来实现的
。每个Java对象都有一个与之关联的ObjectMonitor,这个关联是通过对象头中的重量级锁指针实现的。当一个线程尝试获取对象的监视器锁(即synchronized关键字所表达的锁)时,它会将对象头的锁状态标记为重量级锁(有关锁升级后续会讲到,这里先知道大部分情况下我们说的多个线程去抢synchronized的锁是重量级锁),并将对象头中的指针指向对应的ObjectMonitor对象,ObjectMonitor是在对象被锁定时动态创建的。具体来说,
当一个线程尝试获取对象的监视器锁时,如果发现该对象尚未关联ObjectMonitor,则会创建一个与该对象关联的Monitor
。这个ObjectMonitor负责维护持有锁的线程、等待锁释放的线程队列、以及因调用wait()方法而阻塞的线程队列。这就是为什么每个对象都能成为锁。

总的来说,ObjectMonitor是Java同步机制背后的功臣,它通过维护锁的状态和线程之间的交互,使得每个对象都能成为一个有效的锁,从而简化了多线程编程的复杂性。

公平锁和非公平锁

根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁。以售票代码为例,使用ReentrantLock,默认为非公平锁,下面代码运行结果全是a线程卖票,也就是a线程一直抢到锁,b和c线程都没有抢到,体现出非公平性。

class Ticket{
    private int number = 50;
    //非公平
    ReentrantLock lock = new ReentrantLock();
    public void sale(){
        lock.lock();
        try {
            if(number > 0){
                System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number);
            }
        }finally {
            lock.unlock();
        }
    }
}
public class SelectTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int i = 0;i < 55;i++){
                ticket.sale();
            }
        },"a").start();
        new Thread(() -> {
            for (int i = 0; i < 55; i++) {
                ticket.sale();
            }
        },"b").start();
        new Thread(() -> {
            for (int i = 0; i < 55; i++) {
                ticket.sale();
            }
        },"c").start();
    }

}

将ReentrantLock设置为公平锁,运行结果可以看出三个线程抢锁次数很平均,体现出公平性。

class Ticket{
    private int number = 50;
    //公平
    ReentrantLock lock = new ReentrantLock(true);
    public void sale(){
        lock.lock();
        try {
            if(number > 0){
                System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number);
            }
        }finally {
            lock.unlock();
        }
    }
}
public class SelectTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int i = 0;i < 55;i++){
                ticket.sale();
            }
        },"a").start();
        new Thread(() -> {
            for (int i = 0; i < 55; i++) {
                ticket.sale();
            }
        },"b").start();
        new Thread(() -> {
            for (int i = 0; i < 55; i++) {
                ticket.sale();
            }
        },"c").start();
    }

}

两个案例就很好的体现出公平锁和非公平锁的特点。
公平锁
是指多个线程按照申请锁的顺序来获取锁,就类似于食堂排队打饭,先来的先买,按顺序排好队,很公平。
非公平锁
是指多个线程获取锁的顺序不是按照申请锁的顺序,有后来居上的情况,就像在打饭的时候有人插队一样,所以就可能出现有的线程一直无法获取锁。

存在就有存在的道理,那为什么要把锁设置为非公平锁和公平锁呢?又为什么默认为非公平锁?这是因为在锁的分配策略对系统的性能有重要影响。

默认情况下,synchronized和ReentrantLock都是非公平锁,是因为
非公平锁性能更好
。因为要
考虑线程切换带来的开销
,当一个线程获取锁失败会被阻塞,当它从阻塞状态恢复到就绪状态再到真正获取锁的过程是有一定时间消耗的,从CPU角度看这个时间消耗还是很明显的。使用非公平锁时,当一个线程请求锁获取同步状态,然后释放锁,刚释放锁的线程更容易获取锁,可以减少线程的切换,所以使用非公平锁更可以利用CPU的时间片。

可重入锁

可重入锁是这
一个线程已经得到这个对象的锁,但是它再次遇到这个锁的时候还可以再次获取,锁必须是同一个对象,不会因为之前已经持有锁而阻塞
。一个线程在多个流程中可获取同一把锁,持有这把同步锁可再次进入,就比如一个线程调用一个同步方法获取到一把锁,然后再方法里又调用了一个同步方法,两个方法用的是同一把锁,线程仍可进入第二个方法。synchronized和ReentrantLock都是可重入锁,可重入锁的一个优点是一定程度上可避免死锁。

下面这个例子就体现出在一个synchronized修饰的方法内部调用本类其他synchronized修饰的方法时,是可以获取锁的。

public class Hello {
    public synchronized void helloA(){
        System.out.println("hello");
    }
    
    public synchronized void helloB(){
        System.out.println("hello B");
        helloA();
    }
}

ObjectMonitor中recursions (锁的重入次数 )和owner(指向持有ObjectMonitor对象的线程 )两个参数就是synchronized的可重入关键,每个锁对象都有一个锁计数器以及指向持有锁的线程的指针。当一个线程获取锁时发现计数器为0,这说明该锁没被占据,计数器就会变成1,并将_owner指向这个线程,其他线程获取锁的时候发现锁的计数器不为0并且锁的拥有者不是自己,就会被阻塞挂起。

当获取到该锁的线程再次获取锁是发现锁的主人是自己,就会把计数器值+1,当线程释放锁后就会把值-1,当计数器为0时,锁的_owner就会置为null,这时被阻塞的线程会唤醒来抢锁。

死锁

死锁这个Bug想必大家都应该知道,先回忆一下这个概念。死锁是指两个或两个以上的线程在执行过程中,因争夺共享资源而出现
一种相互等待
的现象,若无外力干涉则他们都将无法推进下去。如果资源充足,进程的资源请求都能得到满足,死锁出现的概率很低,如果资源不充足,就很可能陷入死锁。

下面是死锁案例,让我想起我去年秋招面试一家互联网公司,二面的时候面试官让我写个死锁,当时很绝望,从没想过面试会让手撕一个bug哈哈哈。

public class DeadLockDemo {
    public static void main(String[] args) {
        final Object objectA = new Object();
        final Object objectB = new Object();

        new Thread(() -> {
            synchronized (objectA){
                System.out.println(Thread.currentThread().getName() +"\t 自己持有A锁,希望获得B锁");
                try {
                    TimeUnit.SECONDS.sleep(1);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                synchronized (objectB){
                    System.out.println(Thread.currentThread().getName() + "\t 成功获取B锁");
                }
            }
        },"a").start();
        new Thread(() -> {
            synchronized (objectB){
                System.out.println(Thread.currentThread().getName() +"\t 自己持有B锁,希望获得A锁");
                try {
                    TimeUnit.SECONDS.sleep(1);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                synchronized (objectA){
                    System.out.println(Thread.currentThread().getName() + "\t 成功获取A锁");
                }
            }
        },"b").start();
    }
}

运行就是都卡住了。现在是我们自己写的bug,知道是死锁,那如果工作中遇到这种问题该怎么排查是不是死锁了呢?

  • 用jdk的命令排查,在终端依次输入命令
    • jps -l
      :查询进程id
    • jstack 1396

双方互相锁着,持有并等待。

  • 用jconsole,看图形化界面。



总结

  1. 我们通过8个锁案例知道了synchronzied锁可以分为类锁和对象锁
  2. 抢锁机制可分为公平锁和非公平锁,synchronized和ReentrantLock默认都是非公平锁,因为非公平锁性能好,避免线程切换。
  3. 一个线程抢到一把锁之后,这个线程还可以继续获取这把锁,这是锁的可重入性
  4. 死锁是一种bug,要避免。可以通过终端命令和jconsole来排查死锁
  5. 从底层角度分析每一个Java对象都可以做为锁,关键就是ObjectMonitor。

以上是本文几个重点,在这篇文章中,我们聚焦于Java中的锁机制,以synchronized关键字为我们的主线,深入探讨了它的核心特性。我们从宏观的角度出发,对锁的基本概念和特性进行了阐述,旨在为大家提供一个清晰的认识。

后续我将带领朋友们进一步深入到synchronized的内部世界,详细解析其背后的工作原理,包括锁的升级过程。此外,我还会涉猎Java并发编程中的其他常用锁机制,如ReentrantLock、ReadWriteLock等,帮助大家更全面地理解和掌握Java中的锁机制。

敬请期待后续的内容,让我们一起探索Java并发编程的奥秘。

本文分享自华为云社区《
面试必问 | 如何设计一款高并发的消息中间件?
》,作者:冰 河。

消息中间件涉及的知识点

要想设计一个具有高并发的消息中间件,那么首先就要了解下消息中间件涉及哪些具体的知识点。通常,设计一个良好的消息中间件最少需要满足如下条件:

  • 生产者、消费者模型。
  • 支持分布式架构。
  • 数据的高可用。
  • 消息数据不丢失。

接下来,我们就针对消息中间件来分别谈谈这些技术点。

生产者消费者模型

相信很多小伙伴对于生产者和消费者模型都比较了解了,简单的说:就是消息中间件能够使其他应用来生产消息,也能够使其他应用来消费相应的消息。

对于生产者和消费者模型,我们需要考虑的问题点就比较多了。接下来,我就一步步来引导大家进行思考。

首先,我们来思考这样一个问题:如果生产者生产了消息,那么消息中间件应该怎样存储相应的数据呢?存储在内存? 存储在磁盘?还是同时存储在内存和磁盘中呢?

如果是将消息数据同时存储在内存和磁盘中,我们又该如何处理这些数据呢?是生产者将消息投递到消息中间件之后,我们就立刻将数据写入磁盘?还是说数据先驻留到内存,然后每隔一段时间刷到磁盘上?

如果是每隔一段时间刷到磁盘上,那我们又要考虑磁盘文件的切分问题,也就是说,需要将消息数据分成多少个磁盘文件?(总不能把所有的数据放到一个磁盘文件中吧)。如果是需要切分成多个磁盘文件,那切分的规则又是什么呢?

上面这些问题都是我们在设计一个消息中间件时需要考虑的问题。然而,这还只是一小部分问题。如果想在面试时脱颖而出,那就还需要继续往下看,还有一些重要的问题点需要注意。

如果文件按照一定的规则切分到多个磁盘文件中了,那是不是还需要管理元数据来标识数据的具体消息(就像是Hadoop中的NameNode节点中存储着DataNode的元数据信息,NameNode节点通过这些元数据信息就能够更好的管理DataNode节点)?

这些元数据可以包括:消息数据的偏移量、也可以是消息数据的唯一ID。

考虑完数据的存储问题,我们还需要考虑的是:消息中间件是如何将数据投递到对应的消费者的?

在设计生产者和消费者时,还一个很重要的问题需要我们考虑:我们在设计消息中间件时,采用的消费模式是什么?会不会将数据均匀的分配给消费者?还是会通过一些其他的规则将数据投递到消费者?

支持分布式架构

如果我们设计的消息中间件,每天会承载TB级别的数据高并发和高吞吐量的写入操作。这里,我们就需要考虑将消息中间件设计成分布式架构。

在设计分布式架构时,我们还需要考虑将存储的比较大的数据,做成分片存储,对数据进行分片等操作。

除了这些,我们还需要考虑另外一个核心问题:对于消息中间件来说,需要支持自动扩容操作。

还有就是是否支持数据分片,如何实现数据分片的扩容和自动数据负载均衡迁移等。

数据的高可用

一般互联网应用的高可用,是通过本地堆内存,分布式缓存,和一份数据在不同的服务器上都搞一个副本来实现的。此时,任何一个存储节点宕机,都不会影响整体的高可用。我们在设计消息中间件时也可以参考这个思路。

消息数据不丢失

此时,我们就需要提供手动ACK的机制,也就是说:当消费者真正消费消息完毕后,向消息中间件返回“ 处理完成” 的标识,消息中间件删除相应的已处理的消息。

但是,细化的话,这里,我们就需要两套ACK机制:

  • 一种ACK对应的是生产端。如果一直没有接收到ACK消息,则需要通过生产者来重新发送一条消息来保证生产消息成功。
  • 另一种ACK对应的是消费端。一旦一条消息消费并处理成功,必须返回一个ack给消息中间件,然后消息中间件才能删除这条消息。否则一旦消费者宕机,就必须重发这条消息给其他的消费者实例,保证消息一定会被处理成功。

点击关注,第一时间了解华为云新鲜技术~

原文 | Eric Erhardt

翻译 | 郑子铭

开放式遥测

OpenTelemetry 是一个可观察性框架,允许开发人员从外部了解他们的系统。它在云应用程序中很流行,并且是
云原生计算基金会
的一部分。 .NET OpenTelemetry 库必须修复一些地方才能与 AOT 兼容。
open-telemetry/opentelemetry-dotnet#3429
是跟踪必要修复的主要 GitHub 问题。

第一个阻止该库在本机 AOT 应用程序中使用的修复是
open-telemetry/opentelemetry-dotnet#4542
。问题是使用工具无法静态分析的值类型调用 MakeGenericType。

当调用 RegisterSlot
() 或 RegisterSlot () 时,此代码使用反射动态填充泛型类型,然后调用 ContextSlotType 的构造函数。由于此 API 是公共的,因此可以在 ContextSlotType 上设置任何开放的通用类型。然后任何值类型都可以填充到 RegisterSlot 方法中。

修复方法是进行一个小的重大更改,并且只接受在 ContextSlotType 上设置 2 或 3 个特定类型,这实际上是客户使用的唯一类型。

这些类型是硬编码的,因此不会被删除。现在,AOT 工具可以看到完成这项工作所需的所有代码。

另一个问题是如何在 ActivityInstrumentationHelper 类中使用 System.Linq.Expressions。这是使用私有反射来解决没有公共 API 的另一种情况。
open-telemetry/opentelemetry-dotnet#4513
更改了表达式代码以确保保留必要的属性。

修剪工具无法静态确定 Expression.Property(Expression, string propertyName) 引用了哪个属性,并且 API 已被注释以在调用它时生成警告。相反,如果您使用重载 Expression.Property(Expression, PropertyInfo) 并以工具可以理解的方式获取 PropertyInfo,则可以使代码修剪兼容。

然后使用
open-telemetry/opentelemetry-dotnet#4695
完全删除库中的 System.Linq.Expressions 使用。

虽然表达式可以在本机 AOT 应用程序中使用,但当您 Lambda.Compile() 表达式时,它会使用解释器来计算表达式。这并不理想,并且可能导致性能下降。如果可能,建议在本机 AOT 应用程序中删除 Expression.Compile() 的使用。

接下来是修剪警告的常见误报案例。使用 EventSource 时,通常会将 3 个以上的原始值或不同类型的值传递给 WriteEvent 方法。但是,当您与原始重载不匹配时,您就会陷入使用 object[] args 作为参数的重载。由于这些值是使用反射进行序列化的,因此该 API 带有 [RequiresUnreferencedCode] 注释,并在调用时发出警告。打开
open-telemetry/opentelemetry-dotnet#4428
以添加这些抑制。

这种误报发生的频率非常高,因此 .NET 8 中的
EventSource 中的新 API
使这种误报几乎完全消失。

open-telemetry/opentelemetry-dotnet#4688
中进行了另一个简单的修复,以使 [DynamicallyAccessedMembers] 属性通过库。例如:

接下来,OpenTelemetry 中的几个导出器使用 JSON 序列化将对象数组转换为字符串。如前所述,在没有 JsonTypeInfo 的情况下使用 JsonSerializer.Serialize 与修剪或 AOT 不兼容。
open-telemetry/opentelemetry-dotnet#4679
将这些位置转换为使用 OpenTelemetry 中的 System.Text.Json 源生成器。

internal static string JsonSerializeArrayTag(Array array)
{
    return JsonSerializer.Serialize(array, typeof(Array), ArrayTagJsonContext.Default);
}

[JsonSerializable(typeof(Array))]
[JsonSerializable(typeof(char))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(byte))]
[JsonSerializable(typeof(sbyte))]
[JsonSerializable(typeof(short))]
[JsonSerializable(typeof(ushort))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(uint))]
[JsonSerializable(typeof(long))]
[JsonSerializable(typeof(ulong))]
[JsonSerializable(typeof(float))]
[JsonSerializable(typeof(double))]
private sealed partial class ArrayTagJsonContext : JsonSerializerContext
{
}

现在可以在AOT应用程序中安全地使用此Jsonserializearraytag方法。请注意,它不支持任何对象序列化 - 仅支持数组和列出的原始类型。如果将不支持的对象传递到此方法中,则在应用程序的情况下,它将始终如一地失败。

更复杂的更改之一是
open-telemetry/opentelemetry-dotnet#4675
,它使属性fetcher类与本机AOT兼容。顾名思义,属性fetcher的专门设计用于从对象中检索属性值。它大量使用反射和制作型。因此,最终仍然用[requiensunreferencedCode]注释。呼叫者的责任是确保手动保留必要的属性。幸运的是,此API是内部的,因此OpenTelemetry团队控制所有呼叫者。

PropertyFetcher的其余问题是确保MakeErictype调用始终在本机AOT应用程序中起作用。

这里的缓解措施利用了以下事实:如果仅使用参考类型(即类型而不是结构)调用MakeGenerictype,则.NET运行时将重用所有参考类型的相同机器代码。

现在,该属性开采已更改为与本机AOT一起工作,现在可以解决的地方可以解决。 OpenTelemetry所需的方法之一是收听诊断程序,注册事件何时启动的回调,然后检查事件的“有效负载”,以记录相应的遥测事件。有3个执行此操作并使用PropertyFetcher的仪器库。

前2个PR能够抑制装饰警告,因为基础诊断代码(
HttpClient

ASP.NET Core
)可确保有效载荷上的重要属性保留在修剪和AOT应用程序中。

对于SQL客户端,情况并非如此。而且,由于基础SQLCLCLIENT库不兼容,因此决定将OpenTElemetry.SqlClient库标记为[quiendunreferencedCode]。

最后,
open-telemetry/opentelemetry-dotnet#4859
修复了OpentElemetry.exporter.opentelemetryprotocol库中的最后一个警告。

这里的问题与上面 StackExchange.Redis 库中的问题相同。此代码对 Google.Protobuf 库中的对象使用私有反射,并生成 DynamicMethod 以提高性能。较新版本的 Google.Protobuf 添加了 .Clear() API,这使得不再需要此私有反射。因此,修复方法很简单,就是更新到新版本,并使用新的 API。

dotnet/扩展

https://github.com/dotnet/extensions
中的新 Microsoft.Extensions.* 库填补了构建真实世界、大规模和高可用性应用程序所需的一些缺失场景。有一些库可以增加应用程序的弹性、更深入的诊断和合规性。

这些库利用其他 Microsoft.Extensions.* 功能,即将 Option 对象绑定到 IConfiguration 并使用 System.ComponentModel.DataAnnotations 属性验证 Option 对象。传统上,这两个功能都使用无界反射来获取和设置 Option 对象的属性,这与修剪不兼容。为了允许在精简的应用程序中使用这些功能,.NET 8 添加了两个新的 Roslyn 源生成器。

dotnet/extensions 库的初始提交已经使用了选项验证源生成器。要使用此源生成器,您需要创建一个实现 IValidateOptions
的分部类并应用 [OptionsValidator] 属性。

[OptionsValidator]
internal sealed partial class HttpStandardResilienceOptionsValidator : IValidateOptions<HttpStandardResilienceOptions>
{
}

源生成器将在构建时检查 HttpStandardResilienceOptions 类型的所有属性,查找 System.ComponentModel.DataAnnotations 属性。对于它找到的每个属性,它都会生成代码来验证属性的值是否可接受。

然后可以使用依赖项注入 (DI) 注册验证器,以将其添加到应用程序中的服务中。

在这种情况下,验证器被注册为在应用程序启动时立即执行,而不是在第一次使用 HttpStandardResilienceOptions 时执行。这有助于在网站接受流量之前发现配置问题。它还确保第一个请求不需要产生此验证的成本。

dotnet/extensions#4625
为 dotnet/extensions 库启用了配置绑定程序源生成器,并修复了另一个小 AOT 问题。

要启用配置联编程序源生成器,可以在项目中设置一个简单的 MSBuild 属性:

<PropertyGroup>
  <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>

启用后,此源生成器会查找对 Microsoft.Extensions.Configuration.ConfigurationBinder 的所有调用,并生成用于根据 IConfiguration 值设置属性的代码,因此不再需要反射。调用将重新路由到生成的代码,并且不需要修改现有代码。这允许绑定在修剪的应用程序中工作,因为每个属性都是由代码显式设置的,因此它们不会被修剪。

最后,一些代码检查枚举的所有值。在 .NET 的早期版本中,执行此操作的方法是调用 Enum.GetValues(typeof(MyEnum))。但是,该 API 与 AOT 不兼容,因为需要在运行时创建 MyEnum 数组,并且 AOT 代码可能不包含 MyEnum[] 的特定代码。

修复方法是在支持它的目标框架上运行时利用相对较新的 API:Enum.GetValues
()。此 API 确保生成 TEnum[] 代码。当不在新的 .NET 目标框架上时,代码将继续使用旧的 API。

Dapper

Dapper
是一个简单的微型 ORM,用于简化 ADO.NET 的使用。它的工作原理是在运行时基于所使用的 ADO.NET 库(例如 Microsoft.Data.SqlClient 或 Npgsql)以及应用程序中使用的强类型(客户、订单等)生成动态 IL。这可以减少锅炉的工作量-应用程序中将对象读/写到数据库所需的板代码。

有时,您的库中只有少数 API 与本机 AOT 不兼容。您可以将它们归为此类,并添加专为 AOT 兼容性而设计的新 API。但就 Dapper 而言,其核心设计本质上与原生 AOT 不兼容。在运行时生成 IL 与使用原生 AOT 的原因完全相反。因此,Dapper 无法修改以支持本机 AOT。

但它支持的场景仍然很重要,并且使用 Dapper 的开发人员体验比使用纯 ADO.NET API 好得多。为了实现这种体验,需要新的设计。

输入
Dapper.AOT
,它是 Dapper 的重写版本,它在构建时生成 ADO.NET 代码,而不是在运行时动态生成 IL。在与本机 AOT 兼容的同时,这还减少了非 AOT 应用程序的启动时间,因为代码已经生成并编译,无需在应用程序启动时生成它。

深入探讨这是如何实现的,值得单独写一篇博客文章,并且您可以
在文档中找到简短的解释
。如果您发现自己需要完全重写库才能使用 Roslyn 源生成器,请查看
源生成器入门
文档。尽管开发成本高昂,但源生成器可以消除使用无界反射或在运行时生成 IL 的必要性。

从不支持原生 AOT

有些 .NET 代码永远不会支持本机 AOT。库可能存在本质上的基本设计,使其不可能兼容。一个例子是可扩展性框架,例如
托管可扩展性框架
。该库的全部目的是在运行时加载原始可执行文件不知道的扩展。这就是 Visual Studio 的可扩展性的构建方式。您可以为 Visual Studio 构建插件来扩展其功能。此场景不适用于本机 AOT,因为扩展可能需要从原始应用程序中删除的方法(例如 string.Replace)。

Newtonsoft.Json 属于库可能决定不支持本机 AOT 的另一种情况。图书馆需要考虑现有客户。如果不进行重大更改,使现有 API 兼容可能是不可行的。这也将是一项相当大的工作量。在这种情况下,有一个已经兼容的替代方案。所以这里的好处可能不值得付出代价。

开诚布公地告诉客户您的目标和计划对客户很有帮助。这样客户就可以了解他们的应用程序和库并为其制定计划。如果您不打算在图书馆中支持本机 AOT,请告诉客户,让他们知道制定替代计划。如果这需要大量工作,但最终可能会发生,那么了解这些信息也很有帮助。在我看来,有效的沟通是软件开发中最有价值的特质之一。

概括

Native AOT正在扩展.NET可以成功使用的场景。与传统的独立 .NET 应用程序相比,应用程序可以更快地启动,使用更少的内存,并且磁盘大小更小。但为了让应用程序使用这种新的部署模型,它们使用的库需要与本机 AOT 兼容。

我希望您发现本指南有助于使您的库与本机 AOT 兼容。

原文链接

How to make libraries compatible with native AOT

知识共享许可协议

本作品采用
知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接:
http://www.cnblogs.com/MingsonZheng/
),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

如有任何疑问,请与我联系 (
MingsonZheng@outlook.com
)

写在开头

开年第一篇,先祝各位新的一年
身体健康,学业有成,事业有成哈
,春节期间就是咔咔乱吃,咔咔乱玩,把学习都抛一边子去了,已经9天没有学习了,深深的懊悔,从今天开始,2024年的学习正式开启,一起给我猛冲!!!
书接上回,我们开启了Java集合部分的学习,今天我们就来看一下List,其中它的核心有两个,一个ArrayList,一个LinkedList,而ArrayList的使用频率在集合中至少排第二,可以和HashMap掰掰手腕子!
盘点Java集合(容器)概览,Collection和Map在开发中谁用的最多?

问一:ArrayList和Array的区别?

Array(数组)是一种引用类型,主要作用是用来存储数据,即可存储基本数据类型也可存储对象,但在定义一个数组时需要注意:必须指定数组的数据类型及数组长度,即数组中存放的元素个数固定并且类型相同。故我们也可以将Array称作静态数组。

【代码示例1】

//数组的几种声明方式
int[] array = new int[3];
int array [] = new int[3];
int[] array = {1, 2, 3};
int[] array = new int[]{1, 2, 3};

而ArrayList的底层是通过动态数组实现,长度动态可变,会自动扩容。不使用泛型的时候,可以添加不同类型元素。

【代码示例2】

List list = new ArrayList(4);
list.add(1);
list.add("1");
list.add(new Double("1.1"));
list.add("Happy New Year");
for (Object o : list) {
    System.out.println(o);
}

区别总结:

1、ArrayList会根据实际存储的元素动态地扩容或缩容,而 Array 被创建之后就不能改变它的长度了。
2、ArrayList 允许你使用泛型来确保类型安全,Array 则不可以。
3、ArrayList 中只能存储对象。对于基本类型数据,需要使用其对应的包装类(如 Integer、Double 等)。Array 可以直接存储基本类型数据,也可以存储对象。
4、ArrayList 支持插入、删除、遍历等常见操作,并且提供了丰富的 API 操作方法,比如 add()、remove()等。Array 只是一个固定长度的数组,只能按照下标访问其中的元素,不具备动态添加、删除元素的能力。
5、ArrayList创建时不需要指定大小,而Array创建时必须指定大小。

问二:ArrayList和Vector的区别?

二者都是List的实现类,底层都通过object[]数组实现,但Vector是早起JDK支持的集合类,目前几乎全部ArrayList替代,二者有着相似的增删改查功能,但不同的是,Vector的方法都是同步的,可以保证线程安全,而ArrayList则不是,因此,ArrayList相较于Vector拥有良好的性能;两者的扩容也存在着不同,默认初始化容量都是10,Vector 扩容默认会翻倍,可指定扩容的大小;ArrayList只增加 50%

问三:ArrayList和LinkedList的区别?

作为List的另外一个子类,LinkedList的出镜率虽然没有ArrayList高,但总有一些场景会用到,我们还是要学一下的哈。
LinkedList的底层是用过双向链表实现,我们简单的感受一下它的使用,后面会针对LinkedList专门总结一篇博客。

【代码示例3】

  // 创建LinkedList集合
  LinkedList link = new LinkedList();
  // 1、添加元素
  link.add("happy");
  link.add("new");
  link.offer("year"); // 向集合尾部追加元素
  link.push("javabuild"); // 向集合头部添加元素
  System.out.println(link); // 输出集合中的元素
  // 2、获取元素
  Object object = link.peek(); //获取集合第一个元素
  System.out.println(object); // 输出集合中的元素
  // 3、删除元素
  link.removeFirst(); // 删除集合第一个元素
  link.pollLast(); // 删除集合最后一个元素
  System.out.println(link);

【输出】

[javabuild, happy, new, year]
javabuild
[happy, new]

区别总结:

  1. ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
  2. ArrayList 底层使用的是 Object 数组;LinkedList 底层使用的是双向链表数据结构;
  3. LinkedList 不支持高效的随机元素访问,而 ArrayList(实现了 RandomAccess 接口) 支持。
  4. ArrayList存在扩容问题,LinkedList不存在,直接放在集合尾部,修改指针即可;

问四:知道ArrayList的扩容机制吗?

要想充分的了解ArrayList的扩容,必须静心阅读其底层源码,JDK1.8源码如下:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * 默认初始容量大小
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 空数组(用于空实例)。
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    //用于默认大小空实例的共享空数组实例。
    //我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 保存ArrayList数据的数组
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * ArrayList 所包含的元素个数
     */
    private int size;

    /**
     * 带初始容量参数的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始大小)
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            //如果传入的参数大于0,创建initialCapacity大小的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //如果传入的参数等于0,创建空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //其他情况,抛出异常
            throw new IllegalArgumentException("Illegal Capacity: " +
                    initialCapacity);
        }
    }

    /**
     * 默认无参构造函数
     * DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。
     */
    public ArrayList(Collection<? extends E> c) {
        //将指定集合转换为数组
        elementData = c.toArray();
        //如果elementData数组的长度不为0
        if ((size = elementData.length) != 0) {
            // 如果elementData不是Object类型数据(c.toArray可能返回的不是Object类型的数组所以加上下面的语句用于判断)
            if (elementData.getClass() != Object[].class)
                //将原来不是Object类型的elementData数组的内容,赋值给新的Object类型的elementData数组
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 其他情况,用空数组代替
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

    /**
     * 修改这个ArrayList实例的容量是列表的当前大小。 应用程序可以使用此操作来最小化ArrayList实例的存储。
     */
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
                    ? EMPTY_ELEMENTDATA
                    : Arrays.copyOf(elementData, size);
        }
    }
//下面是ArrayList的扩容机制
//ArrayList的扩容机制提高了性能,如果每次只扩充一个,
//那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制避免了这种情况。

    /**
     * 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量
     *
     * @param minCapacity 所需的最小容量
     */
    public void ensureCapacity(int minCapacity) {
        //如果是true,minExpand的值为0,如果是false,minExpand的值为10
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                // any size if not default element table
                ? 0
                // larger than default for default empty table. It's already
                // supposed to be at default size.
                : DEFAULT_CAPACITY;
        //如果最小容量大于已有的最大容量
        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

    // 根据给定的最小容量和当前数组元素来计算所需容量。
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 如果当前数组元素为空数组(初始情况),返回默认容量和最小容量中的较大值作为所需容量
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        // 否则直接返回最小容量
        return minCapacity;
    }

    // 确保内部容量达到指定的最小容量。
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    //判断是否需要扩容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            //调用grow方法进行扩容,调用此方法代表已经开始扩容了
            grow(minCapacity);
    }

    /**
     * 要分配的最大数组大小
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * ArrayList扩容的核心方法。
     */
    private void grow(int minCapacity) {
        // oldCapacity为旧容量,newCapacity为新容量
        int oldCapacity = elementData.length;
        //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
        //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //再检查新容量是否超出了ArrayList所定义的最大容量,
        //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
        //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    //比较minCapacity和 MAX_ARRAY_SIZE
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
    }

    /**
     * 返回此列表中的元素数。
     */
    public int size() {
        return size;
    }

    /**
     * 如果此列表不包含元素,则返回 true 。
     */
    public boolean isEmpty() {
        //注意=和==的区别
        return size == 0;
    }

    /**
     * 如果此列表包含指定的元素,则返回true 。
     */
    public boolean contains(Object o) {
        //indexOf()方法:返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1
        return indexOf(o) >= 0;
    }

    /**
     * 返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1
     */
    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i] == null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                //equals()方法比较
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

    /**
     * 返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1。.
     */
    public int lastIndexOf(Object o) {
        if (o == null) {
            for (int i = size - 1; i >= 0; i--)
                if (elementData[i] == null)
                    return i;
        } else {
            for (int i = size - 1; i >= 0; i--)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

    /**
     * 返回此ArrayList实例的浅拷贝。 (元素本身不被复制。)
     */
    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            //Arrays.copyOf功能是实现数组的复制,返回复制后的数组。参数是被复制的数组和复制的长度
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // 这不应该发生,因为我们是可以克隆的
            throw new InternalError(e);
        }
    }

    /**
     * 以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。
     * 返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。
     * 因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。
     */
    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

    /**
     * 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素);
     * 返回的数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组,则返回其中。
     * 否则,将为指定数组的运行时类型和此列表的大小分配一个新数组。
     * 如果列表适用于指定的数组,其余空间(即数组的列表数量多于此元素),则紧跟在集合结束后的数组中的元素设置为null 。
     * (这仅在调用者知道列表不包含任何空元素的情况下才能确定列表的长度。)
     */
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // 新建一个运行时类型的数组,但是ArrayList数组的内容
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        //调用System提供的arraycopy()方法实现数组之间的复制
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

    // Positional Access Operations

    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

    /**
     * 返回此列表中指定位置的元素。
     */
    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    /**
     * 用指定的元素替换此列表中指定位置的元素。
     */
    public E set(int index, E element) {
        //对index进行界限检查
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        //返回原来在这个位置的元素
        return oldValue;
    }

    /**
     * 将指定的元素追加到此列表的末尾。
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //这里看到ArrayList添加元素的实质就相当于为数组赋值
        elementData[size++] = e;
        return true;
    }

    /**
     * 在此列表中的指定位置插入指定的元素。
     * 先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大;
     * 再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
     */
    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //arraycopy()这个实现数组之间复制的方法一定要看一下,下面就用到了arraycopy()方法实现数组自己复制自己
        System.arraycopy(elementData, index, elementData, index + 1,
                size - index);
        elementData[index] = element;
        size++;
    }

    /**
     * 删除该列表中指定位置的元素。 将任何后续元素移动到左侧(从其索引中减去一个元素)。
     */
    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index + 1, elementData, index,
                    numMoved);
        elementData[--size] = null; // clear to let GC do its work
        //从列表中删除的元素
        return oldValue;
    }

    /**
     * 从列表中删除指定元素的第一个出现(如果存在)。 如果列表不包含该元素,则它不会更改。
     * 返回true,如果此列表包含指定的元素
     */
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    /*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index + 1, elementData, index,
                    numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

    /**
     * 从列表中删除所有元素。
     */
    public void clear() {
        modCount++;

        // 把数组中所有的元素的值设为null
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

    /**
     * 按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。
     */
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

    /**
     * 将指定集合中的所有元素插入到此列表中,从指定的位置开始。
     */
    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                    numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

    /**
     * 从此列表中删除所有索引为fromIndex (含)和toIndex之间的元素。
     * 将任何后续元素移动到左侧(减少其索引)。
     */
    protected void removeRange(int fromIndex, int toIndex) {
        modCount++;
        int numMoved = size - toIndex;
        System.arraycopy(elementData, toIndex, elementData, fromIndex,
                numMoved);

        // clear to let GC do its work
        int newSize = size - (toIndex - fromIndex);
        for (int i = newSize; i < size; i++) {
            elementData[i] = null;
        }
        size = newSize;
    }

    /**
     * 检查给定的索引是否在范围内。
     */
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    /**
     * add和addAll使用的rangeCheck的一个版本
     */
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    /**
     * 返回IndexOutOfBoundsException细节信息
     */
    private String outOfBoundsMsg(int index) {
        return "Index: " + index + ", Size: " + size;
    }

    /**
     * 从此列表中删除指定集合中包含的所有元素。
     */
    public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        //如果此列表被修改则返回true
        return batchRemove(c, false);
    }

    /**
     * 仅保留此列表中包含在指定集合中的元素。
     * 换句话说,从此列表中删除其中不包含在指定集合中的所有元素。
     */
    public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, true);
    }


    /**
     * 从列表中的指定位置开始,返回列表中的元素(按正确顺序)的列表迭代器。
     * 指定的索引表示初始调用将返回的第一个元素为next 。 初始调用previous将返回指定索引减1的元素。
     * 返回的列表迭代器是fail-fast 。
     */
    public ListIterator<E> listIterator(int index) {
        if (index < 0 || index > size)
            throw new IndexOutOfBoundsException("Index: " + index);
        return new ListItr(index);
    }

    /**
     * 返回列表中的列表迭代器(按适当的顺序)。
     * 返回的列表迭代器是fail-fast 。
     */
    public ListIterator<E> listIterator() {
        return new ListItr(0);
    }

    /**
     * 以正确的顺序返回该列表中的元素的迭代器。
     * 返回的迭代器是fail-fast 。
     */
    public Iterator<E> iterator() {
        return new Itr();
    }

通过阅读源码和其中注释,我们不难得出这样的结论:ArrayList的无参构造默认初始化长度为10,在添加元素大于初始容量后,会触发扩容,而数组的扩容是将原数组中的元素拷贝到一个新数组中,将数组容量增加为原数组1.5倍。

问五:ArrayList增删改查的时间复杂度?

  • 查询时间复杂度: O(1),因为 ArrayList 内部使用数组来存储元素,所以可以直接根据索引来访问元素。
  • 增加时间复杂度:添加一个元素(调用 add() 方法时)的时间复杂度最好情况为 O(1),最坏情况为 O(n)。
  • 删除时间复杂度:删除一个元素(调用 remove(Object) 方法时)的时间复杂度最好情况 O(1),最坏情况 O(n)。
  • 修改时间复杂度:修改一个元素(调用 set()方法时)与查询操作类似,可以直接根据索引来访问元素,时间复杂度为 O(1)。

注:最好和最快情况分别是在列别尾部操作和头部或中间操作的差距。

问六:Vector、ArrayList、LinkedList 的存储性能和特性?

1、ArrayList 和 Vector 都是使用数组存储数据
2、ArrayList 允许直接按序号索引元素
3、ArrayList 插入元素涉及数组扩容、元素移动等内存操作
4、ArrayList 根据下标找元素快,存在扩容的情况下插入慢
5、Vector 对元素的操作,使用了 synchronized 方法,性能比 ArrayList 差
6、Vector 属于遗留容器,早期的 JDK 中使用的容器
7、LinkedList 使用双向链表存储元素
8、LinkedList 按序号查找元素,需要进行前向或后向遍历,所以按下标查找元素,效率较低
9、LinkedList 非线程安全
10、LinkedList 使用的链式存储方式与数组的连续存储方式相比,对内存的利用率更高
11、LinkedList 插入数据时只需要移动指针即可,所以插入速度较快

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得
留言+点赞+收藏
呀。原创不易,转载请联系Build哥!