2024年7月

1.Java socket 获取gps定位的方法

在Java中使用Socket来直接获取GPS定位信息并不直接可行,因为GPS数据通常不是通过Socket通信来获取的。GPS数据通常由设备(如智能手机、GPS接收器)上的GPS硬件模块生成,并通过操作系统或专门的GPS软件库来访问。

然而,如果我们的目的是通过Socket从某个服务器(可能是GPS数据服务器或物联网设备)获取GPS数据,那么我们可以模拟这样一个过程。这里,我将提供一个简单的Java Socket客户端示例,它从服务器接收GPS数据(这里我们假设服务器发送的是模拟的GPS数据,格式为纬度,经度)。

1.1 服务器端(假设我们已经有一个发送GPS数据的服务器)

服务器端代码不在此详细展开,因为它可以基于多种语言和框架实现。假设服务器在监听某个端口(例如8080),并发送格式如下的GPS数据:

复制代码

34.0522,-118.2437

1.2 客户端(Java Socket客户端)

以下是Java Socket客户端的示例代码,用于连接服务器并接收GPS数据:

import java.io.BufferedReader;  
import java.io.InputStreamReader;  
import java.net.Socket;  
  
public class GPSClient {  
  
    public static void main(String[] args) {  
        String hostname = "localhost"; // 服务器地址,实际使用时替换为服务器IP或域名  
        int port = 8080; // 服务器端口,根据实际服务器配置  
  
        try (Socket socket = new Socket(hostname, port);  
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {  
  
            // 读取并打印GPS数据  
            String inputLine;  
            while ((inputLine = in.readLine()) != null) {  
                System.out.println("GPS Data from Server: " + inputLine);  
                // 这里可以添加逻辑来处理GPS数据,比如解析纬度和经度  
                  
                // 假设我们只读取一行数据作为示例  
                break;  
            }  
  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

1.3 注意事项

(1)
服务器地址和端口
:确保
hostname

port
变量设置正确,以匹配我们的服务器配置。

(2)
数据格式
:上面的代码假设服务器发送的数据是以换行符分隔的文本行。如果实际的数据格式不同,我们可能需要调整
BufferedReader
的读取逻辑或添加额外的解析逻辑。

(3)
异常处理
:示例中使用了try-with-resources语句来自动关闭资源,同时捕获并打印了异常信息。在实际应用中,我们可能需要更详细的异常处理逻辑。

(4)
安全性
:如果GPS数据包含敏感信息,我们应该考虑使用加密的Socket连接(如SSL/TLS)来保护数据传输的安全性。

以上代码提供了一个基础的框架,用于通过Socket从服务器接收GPS数据。根据我们的具体需求,我们可能需要对其进行调整或扩展。

2. 实际的代码示例

当然,我可以给我们一个更具体的Java Socket客户端代码示例,这个示例将连接到一个假设的服务器(在本例中,我们不会实现服务器,但假设它存在并发送GPS数据)。客户端将接收来自服务器的GPS数据(假设为经纬度字符串),然后解析这些数据。

首先,这是Java Socket客户端的代码,它将连接到服务器并读取一行数据(我们假设服务器只发送一行GPS数据):

import java.io.BufferedReader;  
import java.io.InputStreamReader;  
import java.net.Socket;  
  
public class GPSClient {  
  
    public static void main(String[] args) {  
        String hostname = "localhost"; // 这里应该是我们的服务器IP地址或域名  
        int port = 8080; // 这是服务器监听的端口号  
  
        try (Socket socket = new Socket(hostname, port);  
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {  
  
            // 读取一行GPS数据  
            String gpsData = in.readLine();  
            if (gpsData != null) {  
                System.out.println("Received GPS Data: " + gpsData);  
  
                // 假设GPS数据格式为 "纬度,经度"  
                String[] parts = gpsData.split(",");  
                if (parts.length == 2) {  
                    double latitude = Double.parseDouble(parts[0]);  
                    double longitude = Double.parseDouble(parts[1]);  
  
                    System.out.printf("Latitude: %.6f, Longitude: %.6f%n", latitude, longitude);  
                } else {  
                    System.out.println("Invalid GPS data format.");  
                }  
            } else {  
                System.out.println("No GPS data received.");  
            }  
  
        } catch (Exception e) {  
            e.printStackTrace();  
            System.out.println("Failed to connect to server or read data.");  
        }  
    }  
}

在这个示例中,我们创建了一个
Socket
对象来连接到指定的服务器和端口。然后,我们使用
BufferedReader
来读取来自服务器的数据流。我们假设服务器会发送一行包含经纬度信息的字符串,格式为“纬度,经度”。

客户端读取这行数据后,我们使用
split
方法按逗号分隔字符串,得到纬度和经度的字符串表示。然后,我们使用
Double.parseDouble
将这些字符串转换为
double
类型的数值,并打印出来。

请注意,这个示例代码假设服务器已经运行并监听在指定的IP地址和端口上,且它会发送格式正确的GPS数据。如果我们没有这样的服务器,我们需要自己实现一个,或者使用现有的模拟服务来测试这个客户端。

此外,由于网络编程涉及到很多潜在的问题(如网络延迟、服务器不可用、数据格式错误等),因此在实际应用中,我们可能需要添加更多的错误处理和重试逻辑来增强代码的健壮性。

1. 介绍

上一篇文章分享了
Vue3 如何如何接入 i18n 实现国际化多语言
,这里继续和大家分享 Flask 后端如何接入 i18n 实现国际化多语言。

用户请求 API 的多语言化其实有两种解决方案:

  1. 后端返回:"USER_ERROR"  => 前端渲染:"用户错误"
  1. 后端接收请求中 "Accept-Language" 信息为 "zh-CN" => 后端返回:"用户错误" => 前端渲染:"用户错误"


这里我们采用的是第二种方案,也就是后端直接处理 i18n 逻辑。

对于 Flask 我们常用
flask-babel
,这个包对于将原本单语言的程序转为国际化多语言非常友好,我们只要用 gettext() 包裹我们原来的文本:

@api.route("/", methods=["GET"])
def info():
    return jsonify({"msg": gettext("欢迎访问 Githubstar API 服务器.")}), 200

然后工具就可以自动生成翻译文件,我们只要编辑不同语言的翻译文件就可以了:

#: githubstar_api/api.py:39
msgid "欢迎访问 Githubstar API 服务器."
msgstr "Welcome to the Githubstar API server."

2. 基本实现


本文以
GithubStar.Pro
后端的实现为例进行介绍。

要将 flask-babel 引入项目,这里首先安装 flask-babel:

pip install flask-babel

如果你开发包的话,这个依赖也要加入到项目依赖中。

然后,在 Flask 初始化时安装 Babel 插件:

from flask import Flask
from flask_babel import Babel

app = Flask('githubstar_api')
babel = Babel(app)

如果你需要使用工厂模式初始化你的 Flask 实例,可以用:

from flask import Flask
from flask_babel import Babel

app = Flask('githubstar_api')
babel = Babel()

def init_app():
    babel.init_app(app)

然后,将你的所有业务相关的文字都包裹上 gettext():

return (
    jsonify(
        {"errors": [gettext('用户 "%(username)s" 已被封禁.', username=user.username)]}
    ),
    400,
)

句子中的变量,可以用 %()s 包裹,其中 s 代表字符串。

更多的使用方法详见:
文档

然后,在项目根目录新建一个 babel.cfg:

[python: githubstar_api/**.py]

​这里的路径应该指向你的所有需要国际化的代码文件。

然后,运行命令,将这些文件中被 gettext() 包裹的文字都提取到模板文件中:

pybabel extract -F babel.cfg -o messages.pot .

这会在项目根目录生成 messages.pot 文件,可以看到包含了所有需要翻译的文本:

#: githubstar_api/api.py:39
msgid "欢迎访问 Githubstar API 服务器."
msgstr ""

随后,我们需要开始进行一个新语言的翻译,例如英文,运行命令:

pybabel init -i messages.pot -d ./githubstar_api/res/locales -l en

我们就可以在 ./githubstar_api/res/locales 中看到生成了一个 en 文件夹, 里面有 messages.po 文件,这就是一个空的全新的翻译文件了。

这里我们可以将文本内容全部发送给 OpenAI GPT4 或者是 Github Copilot,告诉他:

这是一个 babel 翻译文件,请根据中文翻译为英文并填入msgstr中,以下是文件内容:
...

AI 可以很好地完成翻译任务,只需要检查并稍微调整即可。

完成翻译后,需要将翻译文件编译为二进制文件:

pybabel compile -d ./githubstar_api/res/locales

这样,locale 文件夹中的所有语言都被翻译,生产了 messages.po 文件。

现在,文件目录应该是这样的:

./githubstar
├── githubstar_api
│   ├── __init__.py
│   ├── res
│   │   ├── __init__.py
│   │   └── locales
│   │       └── en
│   │           └── LC_MESSAGES
│   │               ├── messages.mo
│   │               └── messages.po
│   ├── api.py
│   ├── app.py
│   └── cli.py
├── messages.pot
└── pyproject.toml

所以,用加载包数据的方式导入:

import importlib.resources as pkg_resources

from . import res

traversable = pkg_resources.files(res)
with pkg_resources.as_file(traversable) as path:
    babel.init_app(
        app,
        default_translation_directories=str(path / "locales"),
    )

接下来,我们需要设定,需要检测请求的 Accept-Language,以下就是完整的 app.py 文件:

from flask import Flask, request
from flask_babel import Babel

app = Flask("githubstar_api")
babel = Babel()

def get_locale():
    return request.accept_languages.best_match(["zh_CN", "en"])

def init_app():
    traversable = pkg_resources.files(res)
    with pkg_resources.as_file(traversable) as path:
        babel.init_app(
            app,
            default_translation_directories=str(path / "locales"),
            locale_selector=get_locale,
            default_locale="zh_CN",
        )

这样,就实现了 Flask API 后端根据请求的 Accept-Language 自动调整返回值的语言了。

注意:如果你要将翻译文件包含在输出的 Python 包中,你需要调整你的 pyproject.toml:

[tool.setuptools]
zip-safe = false
include-package-data = true

[tool.setuptools.packages]
find = {namespaces = false}

[tool.setuptools.package-data]
"githubstar_api.res" = ["**/*.mo"]

3. 与 Vue3 前端联动

接下来,需要让前端发送的请求以当前语言作为 Accept-Language。

这里,我们以用户操作相关 API 为例,这里我们使用一个
Pinia Store
来管理所有的用户状态和相关请求。API 请求用
axios
发送,详见开源仓库:
Github: GithubStarPro

export const useUserStore = defineStore('user', {
  state: () => {
    const { locale } = useI18n({ inheritLocale: true, useScope: 'local' });
    const user = useStorage<User | null>('user', null, undefined, { serializer: StorageSerializers.object });
    const api = axios.create({ baseURL: import.meta.env.VITE_API_URL });
    api.interceptors.request.use((config) => {
      config.headers['Accept-Language'] = locale.value;
      if (user.value && user.value.token) {
        config.headers.Authorization = `Bearer ${user.value.token.token}`;
      }
      return config;
    });
  },
  actions: {
    login(username: string, password: string) {
      const payload = {
        username: username,
        password: password,
      };
      this.api.post('/user/login', payload)
        .then((response) => {
          this.user = response.data;
        });
    }
  },
});

​我们重点关注的是:

api.interceptors.request.use((config) => {
  config.headers['Accept-Language'] = locale.value;
  if (user.value && user.value.token) {
    config.headers.Authorization = `Bearer ${user.value.token.token}`;
  }
  return config;
});


这一块是在 axios 对象上定义了一个预处理器,也就是在每个发送的请求上加入当前的 locale,如果用户已登录,还需要加入用户的 Token,这里的 locale 参见我的上一篇文章:
Vue3 如何如何接入 i18n 实现国际化多语言

这样,就实现了在每次发送请求时候自动发送当前的 locale。

这样我们就实现了前后端的国际化,如果这篇文章对您有帮助的话,欢迎关注我,我会分享更多全栈网页开发的实用经验。

您也可以关注 Github 互赞平台
GithubStar.Pro
,快速提升您的项目关注度。

感谢阅读!

好家伙,

0、一个例子

<!DOCTYPE html>
<html lang="zh-CN">

<head>
<meta charset="UTF-8">
<title>Vue 父子组件通信示例</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>

<body>
<div id="app">
<parent-component></parent-component>
</div>

<script>
//子组件
Vue.component('child-component', {
template: `
<div>
<button @click="sendDataToParent">发送数据给父组件</button>
</div>
`,
methods: {
sendDataToParent() {
this.$emit('data-sent', '这是从子组件发送的数据');
}
}
});
//父组件
Vue.component('parent-component', {
template: `
<div>
<child-component @data-sent="handleDataReceived"></child-component>
<p>从子组件接收到的数据:{{ receivedData }}</p>
</div>
`,
data() {
return{
receivedData:
''};
},
methods: {
handleDataReceived(data) {
this.receivedData =data;
}
}
});
//创建Vue实例
let vm = newVue({
el:
'#app'});</script>
</body>

</html>

1、$emit、$on源码

源码实现,我们来看$emit、$on的源码实现部分

Vue.prototype.$on = function(event, fn) {var vm = this;if(isArray(event)) {for (var i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn);
}
}
else{
(vm._events[event]
|| (vm._events[event] =[])).push(fn);//optimize hook:event cost by using a boolean flag marked at registration
//instead of a hash lookup
if(hookRE.test(event)) {
vm._hasHookEvent
= true;
}
}
returnvm;
};

Vue.prototype.$emit
= function(event) {var vm = this;//处理大小写
{var lowerCaseEvent =event.toLowerCase();if (lowerCaseEvent !== event &&vm._events[lowerCaseEvent]) {
tip(
"Event \"".concat(lowerCaseEvent, "\" is emitted in component ") +
"".concat(formatComponentName(vm), " but the handler is registered for \"").concat(event, "\". ") +
"Note that HTML attributes are case-insensitive and you cannot use " +
"v-on to listen to camelCase events when using in-DOM templates. " +
"You should probably use \"".concat(hyphenate(event), "\" instead of \"").concat(event, "\"."));
}
}
var cbs =vm._events[event];if(cbs) {
cbs
= cbs.length > 1 ?toArray(cbs) : cbs;var args = toArray(arguments, 1);var info = "event handler for \"".concat(event, "\"");for (var i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info);
}
}
returnvm;
};
functioninvokeWithErrorHandling(handler, context, args, vm, info) {varres;try{
res
= args ?handler.apply(context, args) : handler.call(context);if (res && !res._isVue && isPromise(res) && !res._handled) {
res.
catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
res._handled
= true;
}
}
catch(e) {
handleError(e, vm, info);
}
returnres;
}

2.代码解释

看着比较复杂,所以我们精简一下,去掉性能优化和一些正则表达式还有一些数组处理

精简下来无非几句代码

$on

(vm._events[event] || (vm._events[event] = [])).push(fn);

$emit

var cbs =vm._events[event];

invokeWithErrorHandling(cbs[i], vm, args, vm, info);
functioninvokeWithErrorHandling(handler, context, args, vm, info) {

res
= args ?handler.apply(context, args) : handler.call(context);returnres;

}

分析:

$emit、$on的实现使用了观察者模式的设计思想

$on
方法用于在当前Vue实例上注册事件监听器。

vm._events
:维护一个事件与其处理函数的映射。每个事件对应一个数组,数组中存放了所有注册的处理函数。

$emit
方法用于触发事件,当事件被触发时,调用所有注册在该事件上的处理函数。

非常简单

3.源码注释版本

//在Vue的原型上定义一个方法$on
Vue.prototype.$on = function(event, fn) {//vm指的是Vue的实例
var vm = this;//如果event是一个数组,那么对每个事件递归调用$on方法
if(isArray(event)) {for (var i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn);
}
}
//如果event不是一个数组,那么将函数fn添加到vm._events[event]中
else{
(vm._events[event]
|| (vm._events[event] =[])).push(fn);//如果event是一个钩子事件,那么设置vm._hasHookEvent为true
if(hookRE.test(event)) {
vm._hasHookEvent
= true;
}
}
//返回Vue的实例
returnvm;
};
//在Vue的原型上定义一个方法$emit
Vue.prototype.$emit = function(event) {//vm指的是Vue的实例
var vm = this;//处理事件名的大小写
{var lowerCaseEvent =event.toLowerCase();//如果事件名的小写形式和原事件名不同,并且vm._events中有注册过小写的事件名
if (lowerCaseEvent !== event &&vm._events[lowerCaseEvent]) {//那么提示用户事件名的大小写问题
tip("Event \"".concat(lowerCaseEvent, "\" is emitted in component ") +
"".concat(formatComponentName(vm), " but the handler is registered for \"").concat(event, "\". ") +
"Note that HTML attributes are case-insensitive and you cannot use " +
"v-on to listen to camelCase events when using in-DOM templates. " +
"You should probably use \"".concat(hyphenate(event), "\" instead of \"").concat(event, "\"."));
}
}
//获取vm._events[event]中的所有回调函数
var cbs =vm._events[event];//如果存在回调函数
if(cbs) {//如果回调函数的数量大于1,那么将其转换为数组
cbs = cbs.length > 1 ?toArray(cbs) : cbs;//获取除event外的其他参数
var args = toArray(arguments, 1);//定义错误处理信息
var info = "event handler for \"".concat(event, "\"");//对每个回调函数进行错误处理
for (var i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info);
}
}
//返回Vue的实例
returnvm;
};
//定义一个错误处理函数
functioninvokeWithErrorHandling(handler, context, args, vm, info) {varres;try{//如果存在参数args,那么使用apply方法调用handler,否则使用call方法调用handler
res = args ?handler.apply(context, args) : handler.call(context);//如果返回结果res存在,且res不是Vue实例,且res是一个Promise,且res没有被处理过
if (res && !res._isVue && isPromise(res) && !res._handled) {//那么对res进行错误处理,并标记res已经被处理过
res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
res._handled
= true;
}
}
//如果在执行过程中抛出错误,那么进行错误处理
catch(e) {
handleError(e, vm, info);
}
//返回结果res
returnres;
}

泛型 集合(List,Set) Map

泛型

泛型的本质是参数化类型,即允许在编译时对集合进行类型检查,从而避免安全问题,提高代码的复用性

泛型的具体定义与作用

  • 定义:
    泛型是一种在编译阶段进行类型检查的机制,它允许在类,方法,接口后通过
    <>
    来声明类型参数.这些参数在编译时会被具体的类型替换.java在运行时,会通过类型擦除机制,将泛型类型擦除,变为原始类型(如,
    String,Integer
    ),具体的例子将在”
    泛型的使用
    ”中演示
  • 作用:
  1. 类型安全
    :通过泛型,在编译阶段可以检查到更多的类型错误,就不用再运行时抛出
    ClassCastException
  2. 消除强制转化
    :在使用了泛型之后,很多的类型转换都可以自动执行,减少了代码中的显性强制转换
  3. 提高了代码的复用性

泛型的使用

  • 泛型类的使用:
    在类名后添加参数声明部分(用
    <>
    包括起来),如
    class Box<T>
public class Box<T>
{
    private  T value;//定义泛型值
    public T getValue() {
        return value;
    }
    public void setValue(T value) {
        this.value = value;
    }
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();//实例化String类的泛型对象
        stringBox.setValue("这是一个String类型范式");
        System.out.println(stringBox.getValue());
        Box<Integer> integerBox = new Box<>();//实例化Integer类的泛型对象
        integerBox.setValue(123);
        System.out.println(integerBox.getValue());
    }
}
  • 泛型类的使用:
    与泛型类类似,在接口定义后添加类似参数声明.如
    interface<K,V>
public interface Pair <K,V>{  //泛型接口
    K getKey();  //Pair 接口定义了两个抽象方法 类型分别为 K,V
    V getValue(); //K,V都是待定义的类型
}
public class SimplePair<K,V> implements Pair<K,V>{
    private K key;
    private V value;
             //SimplePair类实现了 Pair接口 必须从写其中的抽象方法
    public SimplePair(K key, V value) {
        this.key = key;    //SimplePair的构造函数
        this.value = value;
    }
    @Override
    public K getKey() {
        return key;
    }
    @Override
    public V getValue() {
        return value;
    }
    public static void main(String[] args) {
        //实例化 一个SimplePair对象 传入参数为 Integer,String
        SimplePair<Integer, String> integerStringSimplePair = new SimplePair<Integer, String>(1,"one");
        //则K就代表Integer V就代表String
        System.out.println(integerStringSimplePair.getKey());
        System.out.println(integerStringSimplePair.getValue());
    }
}
  • 泛型方法:
    在方法返回类型前声明类型参数,如
    public <T> void printArray(T[] inputArray)
    。泛型方法可以定义在泛型类中,也可以定义在非泛型类中。
public class Method {
    public static <T> void printArray(T[] inputArray){ //定义了一个返回值为<T>的泛型函数
        for (T element : inputArray) {
            System.out.println(element);
        }
    }
    public static void main(String[] args) {
        Integer [] integers={1,2,3,4,5};
        String [] strings={"abcdefg"};
        printArray(integers);//调用泛型函数
        printArray(strings);
    }
}
  • 类型通配标识符:
    使用
    ?
    表示类型实参,表示不确定的类型(或者是待定的类型),通常用于泛型方法,泛型类,泛型接口;通常应用于当你不确定该数据类型时
 public static void printElements(List<?> list)//假设你要写一个打印集合元素的方法
 //当你不确定该集合的类型 则可以使用通配符
    {
        for (Object o : list) {
            System.out.println(o);
        }
    }
    public static void main(String[] args) {
        List<String> stringList=new ArrayList<>();
        List<Integer> integerList=new ArrayList<>();
        printElements(stringList);//可以打印String
        printElements(integerList);//也可以打印Integer
    }
  • 上限通配符:
    上限通配符用于知道一个类型的上限,它允许你指定一个类型及其子类型,其使用格式为
    <?extends Type >
List<? extends Number> listOfNum=new ArrayList<Integer>();//使用Integer是合法的
        //因为Number的子类包括 Integer Double 等等
        listOfNum.add(null);//也是合法的null属于一切类型

在这个例子中,
List<? extends Number>
表示列表可以持有
Number
类型或其子类型(如
Integer

Double
等)的对象,但你不能往这个列表中添加除了
null
之外的任何元素,因为编译器不知道列表的确切类型

泛型中常见的类型参数

  • T
    :表示任意类型,是Type的缩写,常用于泛型类,方法,接口中
  • K,V
    :分别表示键(key)和值(value),常用于键值对中,如
    Map<K,V>
  • E
    :表示元素(Element),常用于集合中如
    List<E>
  • N
    :表示数字(Number),常用于数字类型
  • S, U, V
    等:用于表示第二、第三、第四个泛型类型参数,这些字母在代码中的使用已成为一种约定俗成的规范

集合

java中,集合框架是一组接口和类的集合,他们提供了一种数据储存和操作的方式.java的集合框架主要包括两大接口
Collection

Map

Collection接口

  • Collection
    是所有单列集合的根接口,其子接口包括
    List
    ,
    Set
    ,
    Queue

    java.util.Collection下的接口和继承类关系简易结构图:

java.util.Map下的接口和继承类关系简易结构图:

List接口

List集合也被称为序列,其允许有重复的元素.List接口的实现类主要有
ArrayList, LinkedList
Vector

ArrayList

底层使用数组实现,不是线程安全,查询速度块,但插入速度慢

 public static void main(String[] args) {
       //创建ArrayList对象
        List<String> list=new ArrayList<>();
        //使用add()方法向数组中添加元素
        list.add("张三");
        list.add("李四");
        list.add("王五");
        //使用get(index)方法获取数组下标为index的元素
        System.out.println(list.get(0));
        //list的增强for循环
        for (String s : list) {
            System.out.println(s);
        }
    }

LinkArray

底层使用双向链表实现,查询速度慢,但其增删速度快,使用方法与
ArrayList
基本一致

    public static void main(String[] args) {
        List<String> list=new LinkedList<>(); //创建LinkedList对象
        list.add("张三");//一下的使用方法与ArrayList一致
        list.add("李四");
        list.add("王五");
        System.out.println(list.get(0));
        for (String s : list) {
            System.out.println(s);
        }

Vector

底层与
ArrayList
一致都是使用数组实现的,线程安全性高,但效率较低

public static void main(String[] args) {
        List<String> list=new Vector<>();//创建Vector对象
        list.add("张三");
        list.add("李四");
        list.add("王五");
        System.out.println(list.get(0));
        for (String s : list) {
            System.out.println(s);
        }
    }

Set接口

其特点为无序集合,不允许有重复元素,包括主要实现类
HashSet
,
LinkedSet

TreeSet

HashSet

作为较为常用的Set集合,其底层是基于哈希表实现的,这就决定了其无法添加重复的元素和无序性

  • HashSet
    之所以能保证元素的唯一性是重写了其
    hashCode()
    方法和
    equals()
    方法,具体操作为:
  1. HashSet在每次储存元素的过程都会首先查看其
    hashCode()
    值,
    看其哈希值是否与以存入 HashSet 的元素的哈希值一致,若不一致则直接存入集合
    ,若一致则进行步骤2
  2. 如果其哈希值相同则继续调用元素的
    equals()
    方法与哈希值相同的元素进行依次比较,若返回值为ture,则说明重复则不添加,反之则添加
  • 无序性:
    HashSet
    是基于哈希表实现的,因此在添加元素时,不会按照其添加的顺序排放,而是根据哈希表原理,通过hash值存放.
  • 遍历无需性:
    当使用迭代器或者增强for循环时,HashSet的遍历也不是按照其元素插入的顺序执行的,也不是按照任何可预测的顺序执行的,
    而是基于哈希表的内部结构决定的,则意味着对于相同的 HashSet ,在不同的JVM和实现方法下其遍历顺序都是不同的
 HashSet<Integer> integerHashSet = new HashSet<>(); //创建HashSet对象
        integerHashSet.add(1);
        integerHashSet.add(1);//使用add方法向其插入元素
        integerHashSet.add(2);
        integerHashSet.add(-1);
        for (Integer integer : integerHashSet) {
            System.out.println(integer);
        } //打印结果为 -1 1 2

LinkedHashSet

作为
HashSet
的子类,继承了
HashSet
的所有特性,即不允许集合中有重复元素,
但与 HashSet 不同的是 LinkedHashSet 内部维护了一个双向链表,
用于实现按元素的插入的顺序实现遍历

  • 底层数据逻辑:
    LinkedHashSet
    底层的数据结构
    包括一个数组和一个双向链表
    (或者是红黑树),这个数组和双向链表(或者红黑树)共同构成了
    LinkedHashMap
    (本文将在下文讲解到),的实现基础,
    LinkedHashSet 就是通过封装 LinkedHashMap 来实现其功能,即底层是基于 LinkedHashMap 实现的
  • 具体实现:
    LinkedHashSet
    ,在添加元素时,都会调用
    LinkedHashMap

    put
    方法来实现
    .LinkedHashMap
    的put方法首先会计算插入元素的哈希值,并根据哈希值确定元素在数组中的位置,然后,
    会在双向链表(或红黑树)添加一个节点,保存元素值
    ,因此每次遍历
    *LinkedHashSet
    时实际上是遍历其双向链表(红黑树)*,从而保证了遍历顺序与元素插入顺序一致
 LinkedHashSet<Integer> integerLinkedHashSet = new LinkedHashSet<>();
 //创建一个LinkedHashSet对象
        integerLinkedHashSet.add(1);
        integerLinkedHashSet.add(1);//添加元素
        integerLinkedHashSet.add(2);
        integerLinkedHashSet.add(-1);
        for (Integer integer : integerLinkedHashSet) {
            System.out.println(integer);
        }//打印结果与插入顺序一致 1 2 -1

TreeSet

TreeSet
是Set的子类,因此也保留的
Set
接口的特性,特别的是
TreeSet
是基于红黑树实现的

  • 底层数据逻辑:
    TreeSet
    的底层实际上是基于
    TreeMap
    作为底层存储实现的,TreeSet内部维护了一个
    NavigableMap
    (实际上就是
    TreeMap
    的一个实例化对象),用于存储元素,在这个映射中,键(key)就是TreeSet中的元素,而值(value)是一个固定的关系共享的Object对象,(在TreeSet中,这个Object对象被命名为PRESENT),用于表现值的存在性,不储存特点的值.

以下是TreeSet内部代码结构:

  • TreeSet的排序机制:

TreeSet
元素默认是根据自然顺序或
根据指定的 Comparator 进行排序
,如果没有提供
Comparator
则,TreeSet会按照元素自然排序;如果提供了
Comparator
则使用
Comparator
来确定元素的顺序

public class NumComparator implements Comparator<Integer> {
//NumComparator类实现了Comparator接口
    @Override//重写了compare方法
    public int compare(Integer o1, Integer o2) {
        return Integer.compare(o1,o2);
    }
} 
   TreeSet<Integer> integerTreeSet = new TreeSet<>(new NumComparator());
   //传入NumComparator对象表明该TreeSet以该方式排序元素
        integerTreeSet.add(1);//添加元素
        integerTreeSet.add(-1);
        integerTreeSet.add(2);
        for (Integer integer : integerTreeSet) {
            System.out.println(integer);
        }打印结果为[-1,1,2]

List与Set的常用方法

返回值类型 方法和描述
boolean add(E e) 将指定的元素追加到此列表的末尾。
void add(int index, E element) 在此列表中的指定位置插入指定的元素。
boolean addAll(Collection<? extends E> c) 按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。
boolean addAll(int index, Collection<? extends E> c) 将指定集合中的所有元素插入到此列表中,从指定的位置开始。
void clear() 从列表中删除所有元素。
boolean contains(Object o) 如果此列表包含指定的元素,则返回 true 。
E get(int index) 返回此列表中指定位置的元素。
int indexOf(Object o) 返回此列表中指定元素的第一次出现的索引,如果此列表不包含元素,则返回-1。
boolean isEmpty() 如果此列表不包含元素,则返回 true 。
Iterator
iterator() 以正确的顺序返回该列表中的元素的迭代器。
int lastIndexOf(Object o) 返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1。
E remove(int index) 删除该列表中指定位置的元素。
boolean remove(Object o) 从列表中删除指定元素的第一个出现(如果存在)。
boolean removeAll(Collection<?> c) 从此列表中删除指定集合中包含的所有元素。
E set(int index, E element) 用指定的元素替换此列表中指定位置的元素。
int size() 返回此列表中的元素数。
Object[] toArray() 以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。

Map(字典)

Map是一种将键(key)映射到值(value)的对象,它提供了一种键值对的储存机制,
其中每个键都唯一映射到一个值,
这种结构有利于快速查找,插入和删除值

Map的存储结构:

HashMap

HashMap
是基于哈希表实现的,它允许使用
null
键和
null
值,HashMap不保证映射的顺序,即遍历Map时元素的顺序可能与插入顺序不同,HashMap底层主要维护一个数组和一个链表

  • HashMap
    的底层原理:
  1. HashMap
    底层维护了一个数组,被称为”桶”,用来储存多个键值对,没有指定初始量时,数组默认长度是16
  2. 当插入数据时两个不同的键产生了哈希冲突,
    这时就会通过HashMap底层维护的链表来解决哈希冲突
 HashMap<Integer, String> integerStringHashMap = new HashMap<>();//创建HashMap对象
        integerStringHashMap.put(1,"one");//Map使用put添加元素
        integerStringHashMap.put(-1,"-one");
        integerStringHashMap.put(2,"two");
        for (Map.Entry<Integer, String> entry : integerStringHashMap.entrySet()) {
            System.out.println(entry.getKey()+" "+entry.getValue());
        }//对于Map有特殊的遍历方式,本文将会在下文解析
        //输出[-1 -one,1 one,2 two]

TreeMap

TreeMap
是基于红黑树实现的Map接口,基于这种数据结构让
TreeMap
可以在log(n)时间复杂度完成
containsKey、get、put和remove
等操作.TreeMap是实现TreeSet的基础

  • 有序性:
    由于基于红黑树实现储存,则保证了TreeMap是有序的,这种有序可以是自然顺序(即插入顺序),或者可以根据指定
    Comparator
    实现
TreeMap<Integer, String> integerStringHashMap = new TreeMap<>();//创建TreeMap对象
        integerStringHashMap.put(1,"one");//Map使用put添加元素
        integerStringHashMap.put(-1,"-one");
        integerStringHashMap.put(2,"two");
        for (Map.Entry<Integer, String> entry : integerStringHashMap.entrySet()) {
            System.out.println(entry.getKey()+" "+entry.getValue());
        }//对于Map有特殊的遍历方式,本文将会在下文解析
        //输出[-1 -one,1 one,2 two]

HashTable

HashTable
底层原理与
HashMap
十分相似,但与
HashMap
相比HashTable的
put,get,remove
加上了同步块,和使用了
this
锁,则使得HashTable线程是安全的,但性能较低

  • 键和值的约束:
    HashTable是不允许使用null作为键和值的,否则会报出
    NullPointerException
    异常
      HashMap<Integer, String> integerStringHashMap = new HashMap<>();//创建HashMap对象
        integerStringHashMap.put(1,"one");//Map使用put添加元素
        integerStringHashMap.put(-1,"-one");
        integerStringHashMap.put(2,"two");
        for (Map.Entry<Integer, String> entry : integerStringHashMap.entrySet()) {
            System.out.println(entry.getKey()+" "+entry.getValue());
        }//对于Map有特殊的遍历方式,本文将会在下文解析
        //输出[-1 -one,1 one,2 two]

LinkedHashMap

LinkedHashMap
继承了
HashMap
,Linked的内部维护了一个双向链表用于保证元素的顺序

  • LinkedHashMap
    内部结构:

    其内部结合了
    哈希表和双向链表两种数据结构,
    哈希表用于快速检索元素,双向链表用于维护元素的顺序
  • 插入和访问
    :当元素被插入
    LinkedHashMap
    时,会在链表的尾部添加一个新的节点。如果设置了按访问顺序排列(通过构造函数或
    setAccessOrder
    方法),则每次访问元素时,会将该节点移动到链表的尾部,以保持访问顺序

        LinkedHashMap<Integer, String> integerStringHashMap = new LinkedHashMap<>();//创建LinkedHashMap对象
        integerStringHashMap.put(1,"one");//Map使用put添加元素
        integerStringHashMap.put(-1,"-one");
        integerStringHashMap.put(2,"two");
        for (Map.Entry<Integer, String> entry : integerStringHashMap.entrySet()) {
            System.out.println(entry.getKey()+" "+entry.getValue());
        }//对于Map有特殊的遍历方式,本文将会在下文解析
        //输出[1 one,-1 -one,2 two]

Map的遍历方式

由于Map数据结构的特性,(使用键值对),因此必须指定要遍历的条件,例如按键或按值遍历等等

  • 使用
    entrySet()
    和增强for循环:

    通过
    entrySet()
    方法,
    Map可以被转换为一个包含Map.Entry对象的Set集合,其中每个Map.Entry对象都代表Map中的一个键值对
    。然后,可以使用增强for循环来遍历这个Set集合

      LinkedHashMap<Integer, String> integerStringHashMap = new LinkedHashMap<>();//创建LinkedHashMap对象
            for (Map.Entry<Integer, String> entry : integerStringHashMap.entrySet()) {
                System.out.println(entry.getKey()+" "+entry.getValue());
            }
    
  • 使用
    KeySet()
    和增强for循环:

    如果只对Map的键感兴趣,可以使用
    keySet()
    方法获取一个包含Map中所有键的Set集合
    ,然后遍历这个集合。如果需要获取对应的值,可以通过键来从Map中获取。

     LinkedHashMap<Integer, String> integerStringHashMap = new LinkedHashMap<>();
            for (Integer integer : integerStringHashMap.keySet()) {//其中integer表示Map的键值
            //通过Map方法的get(key)方法返回的是通过key映射的value
                System.out.println(integer+integerStringHashMap.get(integer));
            }
    
    
  • 使用
    values()
    和增强for循环:


    KeySet()
    方法同理,如果只对Map的值感兴趣,可以使用
    values()
    方法获取一个包含Map中所有值的Collection集合,然后遍历这个集合。
    但请注意,这种方式无法直接获取到对应的键。只能获取其value值

     LinkedHashMap<Integer, String> integerStringHashMap = new LinkedHashMap<>();//创建LinkedHashMap对象
            for (String value : integerStringHashMap.values()) {
                System.out.println(value);
            }
    
  • 使用
    entrySet()

    Iterator
    迭代器

    使用
    entrySet()
    方法结合
    Iterator
    迭代器来遍历
    Map
    中的键值对是一种
    常见的做法
    ,尤其当需要同时访问键和值时,
    整体是通过while循环实现的


    • 在使用前必须使用
      interator()
      方法构建一个
      interator
      对象,并且需要通过
      Iterator

      hasNext()
      方法检查是否还有下一个元素。

    • 使用
      Iterator

      next()
      方法获取下一个
      Map.Entry
      对象,从
      Map.Entry
      对象中使用
      getKey()

      getValue()
      方法分别获取键和值。

              LinkedHashMap<Integer, String> integerStringHashMap = new LinkedHashMap<>();
        Iterator<Map.Entry<Integer, String>> iterator = integerStringHashMap.entrySet().iterator();
        //使用interator()创建一个intertor对象这步其实为联合方法可以分为一下两步
               while (iterator.hasNext())
               {
                   Map.Entry<Integer, String> entry = iterator.next();
                   //每次通过next()方法获取entries的下一个实体 储存再entry中
                   Integer key=entry.getKey();//使用迭代器的getKey()方法可以获取键
                   String value=entry.getValue();//getValue()方法可以获取值
                   System.out.println(key+value);
               }
      

      Iterator<Map.Entry<Integer, String>> iterator =integerStringHashMap.entrySet().iterator();
      //使用interator()创建一个intertor对象这步其实为联合方法可以分为一下两步


      1. 先使用
        entrySet()
        方法创建一个Set集合:

      Set<Map.Entry<Integer, String>> entries = integerStringHashMap.entrySet();

      其中
      Map.Entry<>
      表示Map中的一个实体


      1. 再使用
        interator()
        构造一个
        interator
        对象

      Iterator<Map.Entry<Integer, String>> iterator = entries. Iterator();

在Java中,Executors 类提供了多种静态工厂方法来创建不同类型的线程池。在学习线程池的过程中,一定避不开Executors类,掌握这个类的使用、原理、使用场景,对于实际项目开发时,运用自如,以下是一些常用的方法,V哥来一一细说:

  1. newCachedThreadPool()
    : 创建一个可缓存的线程池,如果线程池中的线程超过60秒没有被使用,它们将被终止并从缓存中移除。

  2. newFixedThreadPool(int nThreads)
    : 创建一个固定大小的线程池,其中 nThreads 指定了线程池中线程的数量。

  3. newSingleThreadExecutor()
    : 创建一个单线程的执行器,它创建单个工作线程来执行任务。

  4. newScheduledThreadPool(int corePoolSize)
    : 创建一个固定大小的线程池,它可以根据需要创建新线程,但会按照固定延迟执行具有给定初始延迟的任务。

  5. newWorkStealingPool(int parallelism)
    : 创建一个工作窃取线程池,它使用多个队列,每个线程都从自己的队列中窃取任务。

  6. newSingleThreadScheduledExecutor()
    : 创建一个单线程的调度执行器,它可以根据需要创建新线程来执行任务。

  7. privilegedThreadFactory()
    : 创建一个线程工厂,用于创建具有特权访问的线程。

  8. defaultThreadFactory()
    : 创建一个默认的线程工厂,用于创建具有非特权访问的线程。

  9. unconfigurableExecutorService(ExecutorService executor)
    : 将给定的 ExecutorService 转换为不可配置的版本,这样调用者就不能修改它的配置。

这些方法提供了灵活的方式来创建和管理线程池,以满足不同的并发需求,下面 V 哥来一一介绍一下9个方法的实现以及使用场景。

1. newCachedThreadPool()

newCachedThreadPool 方法是 Java java.util.concurrent 包中的 Executors 类的一个静态工厂方法。这个方法用于创建一个可缓存的线程池,它能够根据需要创建新线程,并且当线程空闲超过一定时间后,线程会被终止并从线程池中移除。

下面是 newCachedThreadPool 方法的大致实现原理和源代码分析:

实现原理

  • 线程创建: 当提交任务到线程池时,如果线程池中的线程数少于核心线程数,会创建新的线程来执行任务。
  • 线程复用: 如果线程池中的线程数已经达到核心线程数,新提交的任务会被放入任务队列中等待执行。
  • 线程回收: 如果线程池中的线程在一定时间内(默认是60秒)没有任务执行,它们会被终止,从而减少资源消耗。

源代码分析

在 Java 的 java.util.concurrent 包中,Executors 类并没有直接提供 newCachedThreadPool 的实现,而是通过调用 ThreadPoolExecutor 类的构造函数来实现的。以下是 ThreadPoolExecutor 构造函数的调用示例:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                   60L, TimeUnit.SECONDS,
                                   new SynchronousQueue<Runnable>());
}

参数解释:

  • corePoolSize: 核心线程数,这里设置为0,表示线程池不会保留任何核心线程。
  • maximumPoolSize: 最大线程数,这里设置为 Integer.MAX_VALUE,表示理论上可以创建无限多的线程。
  • keepAliveTime: 当线程数大于核心线程数时,多余的空闲线程能等待新任务的最长时间,这里设置为60秒。
  • unit: keepAliveTime 参数的时间单位,这里是秒。
  • workQueue: 一个任务队列,这里使用的是 SynchronousQueue,它是一个不存储元素的阻塞队列,每个插入操作必须等待一个相应的移除操作。

实现过程

  • 初始化: 当调用 newCachedThreadPool 时,会创建一个 ThreadPoolExecutor 实例。
  • 任务提交: 当任务提交给线程池时,线程池会检查是否有空闲线程可以立即执行任务。
  • 线程创建: 如果没有空闲线程,并且当前线程数小于 maximumPoolSize,则创建新线程执行任务。
  • 任务队列: 如果当前线程数已经达到 maximumPoolSize,则将任务放入 SynchronousQueue 中等待。
  • 线程复用: 当一个线程执行完任务后,它不会立即终止,而是尝试从 SynchronousQueue 中获取新任务。
  • 线程回收: 如果线程在 keepAliveTime 时间内没有获取到新任务,它将被终止。

这种设计使得 newCachedThreadPool 非常适合处理大量短生命周期的任务,因为它可以动态地调整线程数量以适应任务负载的变化。然而,由于它可以创建无限多的线程,如果没有适当的任务队列来控制任务的数量,可能会导致资源耗尽。因此,在使用 newCachedThreadPool 时,需要谨慎考虑任务的特性和系统的资源限制。

使用场景:

适用于执行大量短期异步任务,尤其是任务执行时间不确定的情况。例如,Web服务器处理大量并发请求,或者异步日志记录。

2. newFixedThreadPool(int nThreads)

newFixedThreadPool(int nThreads) 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法。这个方法用于创建一个固定大小的线程池,它能够确保线程池中始终有固定数量的线程在工作。

以下是 newFixedThreadPool 方法的实现原理、源代码分析以及实现过程:

实现原理

  • 固定线程数: 线程池中的线程数量始终保持为 nThreads。
  • 任务队列: 提交的任务首先由核心线程执行,如果核心线程都在忙碌状态,新任务将被放入一个阻塞队列中等待执行。
  • 线程复用: 线程池中的线程会重复利用,执行完一个任务后,会立即尝试从队列中获取下一个任务执行。

源代码分析

newFixedThreadPool 方法是通过调用 ThreadPoolExecutor 类的构造函数来实现的。以下是 ThreadPoolExecutor 构造函数的调用示例:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(
        nThreads, // 核心线程数
        nThreads, // 最大线程数
        0L,      // 线程空闲时间,这里设置为0,表示线程不会空闲
        TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>() // 使用阻塞队列来存储任务
    );
}

参数解释:

  • corePoolSize: 核心线程数,这里设置为 nThreads,表示线程池中始终有 nThreads 个线程。
  • maximumPoolSize: 最大线程数,这里也设置为 nThreads,表示线程池的线程数量不会超过 nThreads。
  • keepAliveTime: 当线程数大于核心线程数时,多余的空闲线程能等待新任务的最长时间,这里设置为0,表示如果线程池中的线程数超过核心线程数,这些线程将立即终止。
  • unit: keepAliveTime 参数的时间单位,这里是毫秒。
  • workQueue: 一个任务队列,这里使用的是 LinkedBlockingQueue,它是一个基于链表的阻塞队列,可以存储任意数量的任务。

实现过程

  • 初始化: 当调用 newFixedThreadPool 时,会创建一个 ThreadPoolExecutor 实例。
  • 任务提交: 当任务提交给线程池时,线程池会检查是否有空闲的核心线程可以立即执行任务。
  • 任务队列: 如果所有核心线程都在忙碌状态,新提交的任务将被放入 LinkedBlockingQueue 中等待。
  • 线程复用: 核心线程执行完一个任务后,会尝试从 LinkedBlockingQueue 中获取新任务继续执行。
  • 线程数量控制: 由于 keepAliveTime 设置为0,当线程池中的线程数超过核心线程数时,这些线程会立即终止,从而保证线程池中的线程数量不会超过 nThreads。

这种设计使得 newFixedThreadPool 非常适合处理大量且持续的任务,因为它可以保证任务以固定的线程数量并行执行,同时避免了线程数量的无限制增长。然而,由于线程池的大小是固定的,如果任务提交的速率超过了线程池的处理能力,可能会导致任务在队列中等待较长时间。因此,在使用 newFixedThreadPool 时,需要根据任务的特性和预期的负载来合理设置 nThreads 的值。

使用场景:

适用于执行大量长期运行的任务,其中线程数量需要固定。例如,同时运行多个数据加载或数据处理任务,且希望限制并发数以避免资源过载。

3. newSingleThreadExecutor()

newSingleThreadExecutor 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法,用于创建一个单线程的执行器。这个执行器确保所有任务都按照任务提交的顺序,在一个线程中顺序执行。

以下是 newSingleThreadExecutor 方法的实现原理、源代码分析以及实现过程:

实现原理

  • 单线程执行: 线程池中只有一个线程,所有任务都由这个线程顺序执行。
  • 任务队列: 如果这个线程在执行任务时有新任务提交,新任务会被放入一个阻塞队列中等待执行。
  • 线程复用: 这个线程会重复利用,执行完一个任务后,会立即尝试从队列中获取下一个任务执行。

源代码分析

newSingleThreadExecutor 方法同样是通过调用 ThreadPoolExecutor 类的构造函数来实现的。以下是 ThreadPoolExecutor 构造函数的调用示例:

public static ExecutorService newSingleThreadExecutor() {
    return new ThreadPoolExecutor(
        1, // 核心线程数
        1, // 最大线程数
        0L, TimeUnit.MILLISECONDS, // 线程空闲时间,这里设置为0,表示线程不会空闲
        new LinkedBlockingQueue<Runnable>() // 使用阻塞队列来存储任务
    );
}

参数解释:

  • corePoolSize: 核心线程数,这里设置为1,表示线程池中始终有一个核心线程。
  • maximumPoolSize: 最大线程数,这里也设置为1,表示线程池的线程数量不会超过1。
  • keepAliveTime: 线程空闲时间,这里设置为0,表示如果线程空闲,它将立即终止。
  • unit: keepAliveTime 参数的时间单位,这里是毫秒。
  • workQueue: 一个任务队列,这里使用的是 LinkedBlockingQueue,它是一个无界队列,可以存储任意数量的任务。

实现过程

  • 初始化: 当调用 newSingleThreadExecutor 时,会创建一个 ThreadPoolExecutor 实例。
  • 任务提交: 当任务提交给线程池时,如果核心线程空闲,则立即执行任务;如果核心线程忙碌,则将任务放入 LinkedBlockingQueue 中等待。
  • 顺序执行: 由于只有一个线程,所有任务都将按照提交的顺序被执行。
  • 任务队列: 如果核心线程在执行任务,新提交的任务将被放入 LinkedBlockingQueue 中排队等待。
  • 线程复用: 核心线程执行完一个任务后,会尝试从 LinkedBlockingQueue 中获取新任务继续执行。
  • 线程数量控制: 由于 keepAliveTime 设置为0,核心线程在没有任务执行时会立即终止。但由于 corePoolSize 和 maximumPoolSize 都为1,线程池会立即重新创建一个线程。

这种设计使得 newSingleThreadExecutor 非常适合处理需要保证任务顺序的场景,例如,当任务之间有依赖关系或者需要按照特定顺序执行时。同时,由于只有一个线程,这也避免了多线程环境下的并发问题。然而,由于只有一个线程执行任务,这也限制了并行处理的能力,如果任务执行时间较长,可能会导致后续任务等待较长时间。因此,在使用 newSingleThreadExecutor 时,需要根据任务的特性和对顺序的要求来决定是否适用。

使用场景:

适用于需要保证任务顺序执行的场景,例如,顺序处理队列中的消息或事件。也适用于需要单个后台线程持续处理周期性任务的情况。

4. newScheduledThreadPool(int corePoolSize)

newScheduledThreadPool 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法,用于创建一个固定大小的线程池,这个线程池支持定时以及周期性的任务执行。

以下是 newScheduledThreadPool 方法的实现原理、源代码分析以及实现过程:

实现原理

  • 定时任务: 线程池能够按照指定的延迟执行任务,或者以固定间隔周期性地执行任务。
  • 固定线程数: 线程池中的线程数量被限制为 corePoolSize 指定的大小。
  • 任务队列: 任务首先由核心线程执行,如果核心线程都在忙碌状态,新任务将被放入一个延迟任务队列中等待执行。

源代码分析

newScheduledThreadPool 方法是通过调用 ScheduledThreadPoolExecutor 类的构造函数来实现的。以下是 ScheduledThreadPoolExecutor 构造函数的调用示例:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

这里的 ScheduledThreadPoolExecutor 是 ThreadPoolExecutor 的一个子类,专门为执行定时任务设计。ScheduledThreadPoolExecutor 构造函数的参数 corePoolSize 定义了线程池中核心线程的数量。

ScheduledThreadPoolExecutor 内部使用了一个 DelayedWorkQueue 作为任务队列,这个队列能够按照任务的预定执行时间对任务进行排序。

实现过程

  • 初始化: 当调用 newScheduledThreadPool 时,会创建一个 ScheduledThreadPoolExecutor 实例。
  • 任务提交: 当任务提交给线程池时,线程池会根据任务的预定执行时间,将任务放入 DelayedWorkQueue 中。
  • 任务调度: 线程池中的线程会从 DelayedWorkQueue 中获取任务,如果任务的执行时间已经到达,线程将执行该任务。
  • 线程复用: 执行完一个任务的线程会再次尝试从 DelayedWorkQueue 中获取下一个任务。
  • 线程数量控制: 如果任务队列中的任务数量超过了核心线程能够处理的范围,ScheduledThreadPoolExecutor 会创建新的线程来帮助处理任务,直到达到 corePoolSize 指定的最大线程数。

特点

  • ScheduledThreadPoolExecutor 允许设置一个线程工厂,用于创建具有特定属性的线程。
  • 它还允许设置一个 RejectedExecutionHandler,当任务无法被接受时(例如,线程池关闭或任务队列已满),这个处理器会被调用。
  • 与 ThreadPoolExecutor 不同,ScheduledThreadPoolExecutor 的 shutdown 和 shutdownNow 方法不会等待延迟任务执行完成。

使用 newScheduledThreadPool 创建的线程池非常适合需要执行定时任务的场景,例如,定期执行的后台任务、定时检查等。然而,由于它是基于固定大小的线程池,所以在高负载情况下,任务可能会排队等待执行,这需要在设计时考虑适当的 corePoolSize 以满足性能要求。

使用场景:

适用于需要定期执行任务或在将来某个时间点执行任务的场景。例如,定时备份数据、定时发送提醒等。

5. newWorkStealingPool(int parallelism)

newWorkStealingPool 是 Java 8 中新增的 java.util.concurrent 包的 Executors 类的一个静态工厂方法。这个方法用于创建一个工作窃取(Work-Stealing)线程池,它能够提高并行任务的执行效率,特别是在多处理器系统上。

实现原理

  • 工作窃取: 在工作窃取线程池中,每个线程都有自己的任务队列。当一个线程完成自己的任务后,它会尝试从其他线程的任务队列中“窃取”任务来执行。
  • 并行级别: 线程池的大小由 parallelism 参数决定,这个参数通常等于主机上的处理器核心数。
  • 动态调整: 工作窃取线程池可以动态地添加或移除线程,以适应任务的负载和线程的利用率。

源代码分析

newWorkStealingPool 方法是通过调用 ForkJoinPool 类的静态工厂方法 commonPoolFor 来实现的。以下是 ForkJoinPool 构造函数的调用示例:

public static ExecutorService newWorkStealingPool(int parallelism) {
    return new ForkJoinPool(
        parallelism,
        ForkJoinPool.defaultForkJoinWorkerThreadFactory,
        null, // 没有未处理的异常处理器
        false // 不是一个异步任务
    );
}

参数解释:

  • parallelism: 线程池的并行级别,即线程池中的线程数量。
  • ForkJoinPool.defaultForkJoinWorkerThreadFactory: 默认的线程工厂,用于创建线程。
  • null: 未处理的异常处理器,这里没有指定,因此如果任务抛出未捕获的异常,它将被传播到 ForkJoinTask 的调用者。
  • false: 表示这不是一个异步任务。

ForkJoinPool 内部使用了 ForkJoinWorkerThread 来执行任务,并且每个线程都有一个 ForkJoinQueue 来存储任务。

实现过程

  • 初始化: 当调用 newWorkStealingPool 时,会创建一个 ForkJoinPool 实例。
  • 任务提交: 当任务提交给线程池时,它们会被放入调用线程的本地队列中。
  • 任务执行: 每个线程首先尝试执行其本地队列中的任务。
  • 工作窃取: 如果本地队列为空,线程会尝试从其他线程的队列中窃取任务来执行。
  • 动态调整: 线程池可以根据需要动态地添加或移除线程。

特点

  • 工作窃取线程池特别适合于工作量不均匀分布的任务,因为它可以减少空闲时间并提高资源利用率。
  • 它也适用于可分解为多个子任务的并行计算任务,因为可以将任务分解后,再将子任务提交给线程池。
  • 由于每个线程都有自己的队列,因此减少了锁的争用,提高了并发性能。

使用 newWorkStealingPool 创建的线程池非常适合于需要高并发和高吞吐量的场景,尤其是在多处理器系统上。然而,由于工作窃取机制,它可能不适用于任务执行时间非常短或者任务数量非常少的场景,因为窃取任务本身可能会引入额外的开销。

使用场景:

适用于工作量不均匀或可分解为多个小任务的并行计算任务。例如,图像处理、数据分析等,可以在多核处理器上有效利用所有核心。

6. newSingleThreadScheduledExecutor()

newSingleThreadScheduledExecutor 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法。这个方法用于创建一个单线程的调度执行器,它可以安排命令在给定的延迟后运行,或者定期地执行。

以下是 newSingleThreadScheduledExecutor 方法的实现原理、源代码分析以及实现过程:

实现原理

  • 单线程执行: 执行器确保所有任务都在单个线程中顺序执行,这保证了任务的执行顺序。
  • 定时任务: 支持延迟执行和周期性执行任务。
  • 任务队列: 所有任务首先被放入一个任务队列中,然后由单线程按顺序执行。

源代码分析

newSingleThreadScheduledExecutor 方法是通过调用 ScheduledThreadPoolExecutor 类的构造函数来实现的。以下是 ScheduledThreadPoolExecutor 构造函数的调用示例:

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
    return new ScheduledThreadPoolExecutor(1);
}

这里,ScheduledThreadPoolExecutor 是 ExecutorService 的一个实现,专门为执行定时任务设计。构造函数只有一个参数,即核心线程数,这里设置为1,表示这是一个单线程的执行器。

ScheduledThreadPoolExecutor 内部使用了一个 DelayedWorkQueue 作为任务队列,这个队列能够按照任务的预定执行时间对任务进行排序。

实现过程

  • 初始化: 当调用 newSingleThreadScheduledExecutor 时,会创建一个 ScheduledThreadPoolExecutor 实例,其核心线程数为1。
  • 任务提交: 当任务提交给执行器时,任务会被封装成 ScheduledFutureTask 或者 RunnableScheduledFuture,然后放入 DelayedWorkQueue 中。
  • 任务调度: 单线程会不断地从 DelayedWorkQueue 中获取任务,并按照预定的时间执行。如果任务的执行时间已经到达,任务将被执行;如果还没有到达,线程会等待直到执行时间到来。
  • 顺序执行: 由于只有一个线程,所有任务都将按照它们被提交的顺序被执行。
  • 周期性任务: 对于需要周期性执行的任务,执行器会在每次任务执行完毕后,重新计算下一次执行的时间,并再次将任务放入队列。

特点

  • newSingleThreadScheduledExecutor 创建的执行器非常适合需要保证任务顺序的场景,例如,需要按照特定顺序执行的任务或者具有依赖关系的任务。
  • 它也适合执行定时任务,如定期执行的维护任务或者后台任务。
  • 由于只有一个线程,这也避免了多线程环境下的并发问题,简化了任务同步和状态管理。

使用 newSingleThreadScheduledExecutor 创建的执行器可以提供强大的定时任务功能,同时保持任务执行的顺序性。然而,由于只有一个线程执行任务,这也限制了并行处理的能力,如果任务执行时间较长,可能会导致后续任务等待较长时间。因此,在使用 newSingleThreadScheduledExecutor 时,需要根据任务的特性和对顺序的要求来决定是否适用。

使用场景:

适用于需要单个后台线程按计划执行任务的场景。例如,定时检查系统状态、定时执行维护任务等。

7. privilegedThreadFactory()

privilegedThreadFactory 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法,用于创建一个线程工厂,该工厂能够产生具有特权访问的线程。这意味着这些线程可以加载系统属性和库,并且可以访问文件系统。

以下是 privilegedThreadFactory 方法的实现原理、源代码分析以及实现过程:

实现原理

  • 特权访问: 创建的线程将具有访问系统资源的权限,例如,加载系统属性和库。
  • 线程创建: 线程工厂将创建新的线程实例,这些线程实例将继承创建它们的线程的上下文。

源代码分析

在 Java 的标准库中,privilegedThreadFactory 方法的实现细节并未公开,因为它是一个私有方法。然而,我们可以分析其大致工作原理。privilegedThreadFactory 方法的调用示例如下:

public static ThreadFactory privilegedThreadFactory() {
    return new PrivilegedThreadFactory();
}

这里,PrivilegedThreadFactory 是 Executors 类的一个私有静态内部类,它实现了 ThreadFactory 接口。ThreadFactory 接口定义了一个 newThread(Runnable r) 方法,用于创建新的线程。

实现过程

  • 初始化: 当调用 privilegedThreadFactory 方法时,会返回一个新的 PrivilegedThreadFactory 实例。
  • 线程创建: 当使用这个工厂创建线程时,它会调用 newThread(Runnable r) 方法。
  • 特权访问: 在 newThread(Runnable r) 方法的实现中,会使用 AccessController.doPrivileged 方法来确保新创建的线程具有特权访问。
  • 上下文复制: 通常,新线程会复制创建它的线程的上下文,包括类加载器等。

示例代码

虽然我们不能查看 privilegedThreadFactory 的具体实现,但是我们可以提供一个示例实现,以展示如何创建具有特权访问的线程:

public class PrivilegedThreadFactory implements ThreadFactory {
    @Override
    public Thread newThread(Runnable r) {
        return AccessController.doPrivileged(new PrivilegedAction<>() {
            @Override
            public Thread run() {
                return new Thread(r);
            }
        });
    }
}

在这个示例中,PrivilegedAction 是一个实现了 PrivilegedAction
<T>
接口的匿名类,其 run 方法创建了一个新的线程。AccessController.doPrivileged 方法用于执行一个特权操作,这里是为了确保线程创建过程中具有必要的权限。

特点

  • 使用 privilegedThreadFactory 创建的线程可以在需要访问敏感系统资源的情况下使用。
  • 这种线程工厂通常用于需要执行特权操作的应用程序,例如,访问系统属性或者执行文件 I/O 操作。

使用 privilegedThreadFactory 可以确保线程在执行任务时具有适当的安全权限,从而避免安全异常。然而,需要注意的是,过度使用特权访问可能会带来安全风险,因此在设计应用程序时应谨慎使用。

使用场景:

适用于需要线程具有更高权限来访问系统资源的场景。例如,需要访问系统属性或执行文件I/O操作的应用程序。

8. defaultThreadFactory()

defaultThreadFactory 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法,用于创建一个默认的线程工厂。这个线程工厂生成的线程没有特殊的权限,它们是普通的线程,具有标准的访问权限。

以下是 defaultThreadFactory 方法的实现原理、源代码分析以及实现过程:

实现原理

  • 标准线程创建: 创建的线程工厂将生成具有默认属性的线程。
  • 线程名称: 生成的线程具有默认的线程名称前缀,通常是 "pool-x-thread-y",其中 x 和 y 是数字。
  • 线程优先级: 线程的优先级设置为 Thread.NORM_PRIORITY,这是 Java 线程的默认优先级。
  • 非守护线程: 创建的线程不是守护线程(daemon threads),它们的存在不会阻止 JVM 退出。

源代码分析

Java 的 defaultThreadFactory 方法的具体实现细节并未完全公开,因为它是 Executors 类的一个私有静态方法。但是,我们可以根据 Java 的 ThreadFactory 接口和一些公开的源代码片段来分析其大致实现。

以下是 defaultThreadFactory 方法的调用示例:

public static ThreadFactory defaultThreadFactory() {
    return new DefaultThreadFactory();
}

这里,DefaultThreadFactory 是 Executors 类的一个私有静态内部类,它实现了 ThreadFactory 接口。ThreadFactory 接口定义了一个 newThread(Runnable r) 方法,用于创建新的线程。

实现过程

  • 初始化: 当调用 defaultThreadFactory 方法时,会返回一个新的 DefaultThreadFactory 实例。
  • 线程创建: 使用这个工厂创建线程时,它会调用 newThread(Runnable r) 方法。
  • 设置线程名称: 在 newThread(Runnable r) 方法的实现中,会创建一个新的 Thread 对象,并设置一个默认的线程名称。
  • 设置线程组: 新线程会被分配到一个默认的线程组中。
  • 线程优先级和守护状态: 线程的优先级设置为默认值,且线程不是守护线程。

示例代码

虽然我们不能查看 defaultThreadFactory 的具体实现,但是我们可以提供一个示例实现,以展示如何创建具有默认属性的线程:

public class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                     poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

在这个示例中,DefaultThreadFactory 使用 AtomicInteger 来确保线程池和线程编号的唯一性。创建的线程名称具有前缀 "pool-x-thread-y",其中 x 和 y 是自增的数字。线程不是守护线程,且优先级设置为 Thread.NORM_PRIORITY。

特点

  • 使用 defaultThreadFactory 创建的线程工厂生成的线程具有标准的 Java 线程属性。
  • 这种线程工厂通常用于不需要特殊权限的应用程序。
  • 由于线程不是守护线程,它们的存在可以维持 JVM 的运行,直到所有非守护线程执行完毕。

使用 defaultThreadFactory 可以确保线程在执行任务时具有标准的安全和执行属性,适合大多数常规用途。然而,如果应用程序需要特殊的线程属性,如守护线程或不同的优先级,可能需要自定义线程工厂。

使用场景:

适用于大多数标准应用程序,需要创建具有默认属性的线程。这是大多数 ExecutorService 实现的默认选择。

9. unconfigurableExecutorService(ExecutorService executor)

unconfigurableExecutorService 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法。这个方法用于创建一个不可配置的 ExecutorService 包装器,这意味着一旦包装后的 ExecutorService 被创建,就不能更改其配置,比如不能修改其线程池大小或任务队列等。

以下是 unconfigurableExecutorService 方法的实现原理、源代码分析以及实现过程:

实现原理

  • 封装: 将现有的 ExecutorService 封装在一个不可配置的代理中。
  • 不可修改: 所有修改配置的方法调用,如 shutdown, shutdownNow, setCorePoolSize 等,都将抛出 UnsupportedOperationException。
  • 转发: 除了配置修改的方法外,其他方法调用将被转发到原始的 ExecutorService。

源代码分析

unconfigurableExecutorService 方法的具体实现细节并未完全公开,因为它是 Executors 类的一个私有静态方法。但是,我们可以根据 Java 的 ExecutorService 接口和代理机制来分析其大致实现。

以下是 unconfigurableExecutorService 方法的调用示例:

public static ExecutorService unconfigurableExecutorService(ExecutorService executor) {
    return new FinalizableDelegatedExecutorService(executor);
}

这里,FinalizableDelegatedExecutorService 是 Executors 类的一个私有静态内部类,它实现了 ExecutorService 接口,并代理了对另一个 ExecutorService 的调用。

实现过程

  • 初始化: 当调用 unconfigurableExecutorService 方法时,会返回一个新的 FinalizableDelegatedExecutorService 实例,它将原始的 ExecutorService 作为参数。
  • 方法调用拦截: 对 FinalizableDelegatedExecutorService 的方法调用将首先被拦截。
  • 配置修改拦截: 如果调用的方法是用于修改配置的,比如 shutdown 或 shutdownNow,将抛出 UnsupportedOperationException。
  • 转发其他调用: 对于其他不涉及配置修改的方法调用,比如 submit, execute, 将被转发到原始的 ExecutorService。

示例代码

下面V哥来模拟一个示例实现,以展示如何创建一个不可配置的 ExecutorService 代理:

public class UnconfigurableExecutorService implements ExecutorService {
    private final ExecutorService executor;

    public UnconfigurableExecutorService(ExecutorService executor) {
        this.executor = executor;
    }

    @Override
    public void shutdown() {
        throw new UnsupportedOperationException("Shutdown not allowed");
    }

    @Override
    public List<Runnable> shutdownNow() {
        throw new UnsupportedOperationException("Shutdown not allowed");
    }

    @Override
    public boolean isShutdown() {
        return executor.isShutdown();
    }

    @Override
    public boolean isTerminated() {
        return executor.isTerminated();
    }

    @Override
    public void execute(Runnable command) {
        executor.execute(command);
    }

    // 其他 ExecutorService 方法的实现,遵循相同的模式
}

在这个示例中,UnconfigurableExecutorService 拦截了 shutdown 和 shutdownNow 方法,并抛出了异常。其他方法则直接转发到原始的 ExecutorService。

特点

  • 使用 unconfigurableExecutorService 创建的 ExecutorService 代理确保了线程池的配置不能被外部修改。
  • 这可以用于防止意外地更改线程池的状态,提高线程池使用的安全性。
  • 除了配置修改的方法外,其他所有方法都保持了原有 ExecutorService 的行为。

使用 unconfigurableExecutorService 可以为现有的 ExecutorService 提供一个安全层,确保它们的状态不会被意外地更改。这对于在多线程环境中共享 ExecutorService 时特别有用。

使用场景:

适用于需要确保线程池配置在创建后不被更改的场景。例如,当多个组件共享同一个线程池时,可以防止一个组件意外修改配置。

最后

以上是V哥在授课过程中整理的关于Executors 9种创建线程池的方法及原理分析,分享给大家,希望对正在学习 Java 的你有所帮助,每天分享技术干货,欢迎关注威哥爱编程,你的鼓励是V哥技术创作路上的助推器,不喜勿喷,感谢。