阅读视图

LangChain4j Prompt对话机器人

未完待续

引言

之前,使用Spring AI对接大模型实现了对话机器人的功能:Spring AI实现一个简单的对话机器人,spring-boot与langchain4j整合可以实现同样的功能。

spring-boot与langchain4j整合,可以采用集成底层API(popular integrations)的方式,也有集成高层API(declarative AI Services)的方式,这里先后使用底层和高层API进行集成和测试。

1.底层API实现对话

引入spring-boot 3.5.4,langchain4j-bom。截至目前,官网上langchain4j-bom的最高版本是1.8.0,均需要jdk17+

<parent>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-parent</artifactId>    <version>3.5.4</version></parent><properties>    <maven.compiler.source>21</maven.compiler.source>    <maven.compiler.target>21</maven.compiler.target>    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencyManagement>    <dependencies>        <dependency>            <groupId>dev.langchain4j</groupId>            <artifactId>langchain4j-bom</artifactId>            <version>1.8.0</version>            <type>pom</type>            <scope>import</scope>        </dependency>    </dependencies></dependencyManagement><repositories>    <repository>        <name>Central Portal Snapshots</name>        <id>central-portal-snapshots</id>        <url>https://central.sonatype.com/repository/maven-snapshots/</url>        <releases>            <enabled>false</enabled>        </releases>        <snapshots>            <enabled>true</enabled>        </snapshots>    </repository></repositories>

以对接OpenAI及支持该协议的大模型为例,添加底层API依赖langchain4j-open-ai-spring-boot-starter

<dependencies>    <dependency>        <groupId>dev.langchain4j</groupId>        <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>    </dependency>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>    </dependency>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-test</artifactId>        <scope>test</scope>    </dependency>    <dependency>        <groupId>org.projectlombok</groupId>        <artifactId>lombok</artifactId>        <scope>provided</scope>    </dependency></dependencies>

1.1 阻塞式ChatModel

使用OpenAI协议对接DeepSeek大模型,更多详细的模型参数介绍见:https://docs.langchain4j.dev/tutorials/model-parameters

langchain4j:  open-ai:    chat-model:      base-url: https://api.deepseek.com      api-key: ${OPEN_API_KEY}      model-name: deepseek-reasoner      log-requests: true      log-responses: true      return-thinking: trueserver:  port: 8080logging:  level:    dev.langchain4j: debug #需要设置日志级别

有些配置项不支持填写在配置文件,因此还可以通过配置类进行配置

package org.example.config;import dev.langchain4j.model.chat.ChatModel;import dev.langchain4j.model.openai.OpenAiChatModel;import org.springframework.context.annotation.Configuration;@Configurationpublic class LangChainConfig {    public ChatModel chatModel() {              return OpenAiChatModel.builder()                .baseUrl("https://api.deepseek.com")                .apiKey(System.getProperty("OPEN_API_KEY"))                .modelName("deepseek-reasoner")                .maxRetries(3)                .logRequests(true)                .logResponses(true)                .returnThinking(true)                .build();    }}

然后可以直接使用ChatModel实现Prompt对话,并返回消耗的Token数,ChatModel是一种阻塞式的API,需要等待大模型回复完成将结果一次性返回

package org.example.controller;import dev.langchain4j.data.message.ChatMessage;import dev.langchain4j.data.message.SystemMessage;import dev.langchain4j.data.message.UserMessage;import dev.langchain4j.model.chat.ChatModel;import dev.langchain4j.model.chat.response.ChatResponse;import dev.langchain4j.model.output.TokenUsage;import jakarta.annotation.Resource;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import reactor.core.publisher.Flux;import java.util.Arrays;import java.util.List;@RestController@RequestMapping("chat")@Slf4jpublic class ChatController {    @Resource    private ChatModel chatModel;    @GetMapping("chat")    public String chat(String msg) {        List<ChatMessage> messages = Arrays.asList(                SystemMessage.from("你是一个数学老师,用简单易懂的方式解释数学概念。"),                UserMessage.from(msg)        );        ChatResponse chatResponse = chatModel.chat(messages);        TokenUsage tokenUsage = chatResponse.tokenUsage();        log.info("token usage: {}", tokenUsage);        return chatResponse.aiMessage().text();    }}

1.2 流式StreamingChatModel

StreamingChatModel是一种非阻塞式的API,不需要等待大模型回复完成将结果一次性返回,而是实时返回大模型生成的片段,直到全部返回。

pom.xml中新增支持流式返回的依赖

<dependency>    <groupId>dev.langchain4j</groupId>    <artifactId>langchain4j-reactor</artifactId></dependency>

配置文件application.yml需要新增流式的streaming-chat-model配置

langchain4j:  open-ai:    streaming-chat-model:      base-url: https://api.deepseek.com      api-key: ${OPEN_API_KEY}      model-name: deepseek-reasoner      log-requests: true      log-responses: true      return-thinking: true

同样可以通过配置类进行配置

package org.example.config;import dev.langchain4j.model.openai.OpenAiStreamingChatModel;import dev.langchain4j.model.chat.StreamingChatModel;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class LangChainConfig {    @Bean    public StreamingChatModel chatModel() {        return OpenAiStreamingChatModel.builder()                .baseUrl("https://api.deepseek.com")                .apiKey(System.getProperty("OPEN_API_KEY"))                .modelName("deepseek-reasoner")                .logRequests(true)                .logResponses(true)                .returnThinking(true)                .build();    }}

流式API是由StreamingChatModel类来实现,在web环境下,需要配合Spring的Flux来使用,在下面方法回调触发时调用相应的Flux的方法,像Spring AI那样将Flux对象返回。

  • onPartialResponse 实时返回大模型生成的片段,调用sink.next()实时输出到浏览器
  • onPartialThinking 实时返回大模型推理过程,调用sink.next()实时输出到浏览器
  • onCompleteResponse 大模型生成完成,调用sink.complete()结束流的输出,还可以对消耗的token进行统计
  • onError 出错,记录错误信息,调用sink.complete()结束流的输出
package org.example.controller;import dev.langchain4j.data.message.ChatMessage;import dev.langchain4j.data.message.SystemMessage;import dev.langchain4j.data.message.UserMessage;import dev.langchain4j.model.chat.StreamingChatModel;import dev.langchain4j.model.chat.response.*;import dev.langchain4j.model.output.TokenUsage;import jakarta.annotation.Resource;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import reactor.core.publisher.Flux;import java.util.Arrays;import java.util.List;@RestController@RequestMapping("chat")@Slf4jpublic class StreamController {    @Resource    private StreamingChatModel streamingChatModel;    @GetMapping(value = "streaming", produces = "text/html; charset=utf-8")    public Flux<String> streaming(String msg) {        List<ChatMessage> messages = Arrays.asList(                SystemMessage.from("你是一个数学老师,用简单易懂的方式解释数学概念。"),                UserMessage.from(msg)        );        return Flux.create(sink -> {            streamingChatModel.chat(messages, new StreamingChatResponseHandler() {                @Override                public void onPartialResponse(PartialResponse partialResponse, PartialResponseContext context) {                    sink.next(partialResponse.text());                }                @Override                public void onPartialThinking(PartialThinking partialThinking) {                    sink.next("<span style='color:red;'>" + partialThinking.text() + "</span>");                }                @Override                public void onCompleteResponse(ChatResponse completeResponse) {                    TokenUsage tokenUsage = completeResponse.tokenUsage();                    log.info("token usage: {}", tokenUsage);                    sink.complete();                }                @Override                public void onError(Throwable error) {                    error.printStackTrace();                    sink.complete();                }            });        });    }}

2.高层API实现对话

使用高层API,需要在底层API基础上,额外引入这个依赖

<dependency>    <groupId>dev.langchain4j</groupId>    <artifactId>langchain4j-spring-boot-starter</artifactId></dependency>

2.1 阻塞式对话

新建一个接口,将调用大模型的方法声明在里面,方法的第一个参数默认就是UserMessage

package org.example.ai;public interface AiAssistant {    String chat(String prompt);}

langchain4j提供了一些消息注解对高级API接口内方法进行设定

  • @SystemMessage 指明系统提示词,可以从类路径下读取文本文件
  • @UserMessage 预先指明用户提示词的固定部分,也可以从类路径下读取文本文件,会和后续调用方法时传入的用户提示词进行拼接替换,因此需要通过{{it}}的固定写法对用户传入的提示词进行占位,如果不想写成{{it}},则需要@V注解更换展位的字符
package org.example.ai;import dev.langchain4j.service.SystemMessage;import dev.langchain4j.service.UserMessage;import dev.langchain4j.service.V;import reactor.core.publisher.Flux;public interface AiAssistant {    // 系统提示词    @SystemMessage("你是一个数学老师,用简单易懂的方式解释数学概念。")    // @SystemMessage(fromResource = "1.txt") 基于工程类路径查找    Flux<String> teacher(String prompt);    // 用户提示词    @UserMessage("你是一个数学老师,用简单易懂的方式解释数学概念。{{it}}")    //@UserMessage(fromResource = "1.txt") 基于工程类路径查找    Flux<String> check(String prompt);    @UserMessage("你是一个数学老师,用简单易懂的方式解释数学概念。{{msg}}")    Flux<String> chat3(@V("msg") String prompt);}

配置类中,通过AiServices类将刚刚定义的AiAssistant注入容器,并注入之前定义好的ChatModel对象到AiAssistant

package org.example.config;import dev.langchain4j.model.openai.OpenAiChatModel;import dev.langchain4j.model.chat.ChatModel;import dev.langchain4j.service.AiServices;import org.example.ai.AiAssistant;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class LangChainConfig {    @Bean    public AiAssistant aiAssistant(ChatModel chatModel) {        return AiServices.builder(AiAssistant.class)                .chatModel(chatModel)                .build();    }}

然后直接注入AiAssistant到对应类,并调用方法即可

package org.example.controller;import jakarta.annotation.Resource;import org.example.ai.AiAssistant;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("high-chat")public class HighChatController {    @Resource    private AiAssistant aiAssistant;    @GetMapping("chat")    public String chat(String msg) {        return aiAssistant.chat(msg);    }}

实际上,高层API可以使用接口类加注解的方式进行配置,通过@AiService注解标注为操作大模型的接口类,会直接被实例化,无需在配置类中再去通过AiServices.builder进行实例化

package org.example.ai;import dev.langchain4j.data.message.ChatMessage;import dev.langchain4j.service.SystemMessage;import dev.langchain4j.service.spring.AiService;import dev.langchain4j.service.spring.AiServiceWiringMode;@AiService(        //如需手动配置模型,需要设置属性:AiServiceWiringMode.EXPLICIT        wiringMode = AiServiceWiringMode.EXPLICIT,        //如需手动配置模型,要指定具体使用哪个模型,例如:chatModel = "deepseek"        chatModel = "chatModel")public interface AiAssistant {        String chat(String prompt);}

2.2 流式对话

  1. 同底层API的流式一样,也要引入langchain4j-reactor依赖
  2. 同样需要先将一个StreamingChatModel的对象注入容器
  3. @AiService注解中大模型属性名使用streamingChatModel,然后调用StreamAssistant的方法即可,Controller中可以直接将Flux对象返回
package org.example.ai;import dev.langchain4j.service.spring.AiService;import dev.langchain4j.service.spring.AiServiceWiringMode;import reactor.core.publisher.Flux;@AiService(        wiringMode = AiServiceWiringMode.EXPLICIT,        streamingChatModel = "streamingChatModel")public interface StreamAssistant {        Flux<String> chat(String prompt);}
@Resourceprivate StreamAssistant streamAssistant;@GetMapping(value = "chat", produces = "text/html; charset=utf-8")public Flux<String> chat(String msg) {    return streamAssistant.chat(msg);}

3.对话记忆ChatMemory

关于会话记忆的概念等,已经在:Spring AI实现一个简单的对话机器人一文中讲到。

先明确langchain4j中的两个概念,记忆和历史

  • 历史(History) 历史记录会完整保存用户与人工智能之间的所有消息。历史记录就是用户在用户界面中看到的内容,它代表了实际发生过的所有对话。

  • 记忆(Memory) 保留一些信息,这些信息会呈现给LLM,使其表现得好像“记住”了对话。记忆与历史记录截然不同。根据所使用的内存算法,它可以以各种方式修改历史记录:例如,删除一些消息、汇总多条消息、汇总单个消息、移除消息中不重要的细节、向消息中注入额外信息(用于RAG算法)或指令(用于结构化输出)等等。

langchain4j目前仅提供记忆管理,不提供历史记录管理。如需要保留完整的历史记录,要手动操作。

langchain4j通过ChatMemory实现记忆缓存,因为一段长对话含有的信息很多,如果不加以修剪,会产生很多冗余,甚至超过一次对话的Token大小限制,因此langchain4j对ChatMemory设计了两种实现:

  • MessageWindowChatMemory 一个比较简单的实现,作为一个滑动窗口,只保留最近的N多个记录
  • TokenWindowChatMemory 保留最近的N多个Token,通过TokenCountEstimator计算会话的令牌数

3.1 底层API实现对话记忆

这里以MessageWindowChatMemory为例,配置类中新增配置

package org.example.config;import dev.langchain4j.memory.ChatMemory;import dev.langchain4j.memory.chat.ChatMemoryProvider;import dev.langchain4j.memory.chat.MessageWindowChatMemory;import dev.langchain4j.store.memory.chat.ChatMemoryStore;import dev.langchain4j.store.memory.chat.InMemoryChatMemoryStore;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class LangChainConfig {    /**     * 采用内存存储     */    @Bean    public ChatMemoryStore chatMemoryStore() {        return new InMemoryChatMemoryStore();    }    /**     * ChatMemoryProvider类,每次根据不同对话ID生成专属的ChatMemory对象     */    @Bean    public ChatMemoryProvider chatMemoryProvider () {        return new ChatMemoryProvider() {            @Override            public ChatMemory get(Object id) {                return MessageWindowChatMemory.builder()                        .id(id)                        .maxMessages(1000)                        .chatMemoryStore( chatMemoryStore() )                        .build();            }        };    }}

Controller中,注入ChatMemoryProvider对象,将和大模型的对话改造升级为支持记忆的

每次对话,将用户提问和大模型回答都进行保存,关联到同一个会话ID

package org.example.controller;import dev.langchain4j.data.message.AiMessage;import dev.langchain4j.data.message.ChatMessage;import dev.langchain4j.data.message.SystemMessage;import dev.langchain4j.data.message.UserMessage;import dev.langchain4j.memory.ChatMemory;import dev.langchain4j.memory.chat.ChatMemoryProvider;import dev.langchain4j.model.chat.StreamingChatModel;import dev.langchain4j.model.chat.response.*;import dev.langchain4j.model.output.TokenUsage;import jakarta.annotation.Resource;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import reactor.core.publisher.Flux;import java.util.Arrays;import java.util.List;@RestController@RequestMapping("memory-chat")@Slf4jpublic class MemoryController {    @Resource    private StreamingChatModel streamingChatModel;    @Resource    private ChatMemoryProvider chatMemoryProvider;    @GetMapping(value = "streaming", produces = "text/html; charset=utf-8")    public Flux<String> streaming(String msg, String msgId) {        // 将问题保存到当前对话记忆        ChatMemory chatMemory = chatMemoryProvider.get(msgId);        chatMemory.add(UserMessage.from(msg));        return Flux.create(sink -> {            streamingChatModel.chat(chatMemory.messages(), new StreamingChatResponseHandler() {                @Override                public void onPartialResponse(PartialResponse partialResponse, PartialResponseContext context) {                    sink.next(partialResponse.text());                }                @Override                public void onPartialThinking(PartialThinking partialThinking) {                    sink.next("<span style='color:red;'>" + partialThinking.text() + "</span>");                }                @Override                public void onCompleteResponse(ChatResponse completeResponse) {                    TokenUsage tokenUsage = completeResponse.tokenUsage();                    log.info("token usage: {}", tokenUsage);                    // 大模型回答完毕,将大模型的回答也添加进当前对话记忆                    AiMessage aiMessage = completeResponse.aiMessage();                    chatMemory.add(aiMessage);                    sink.complete();                }                @Override                public void onError(Throwable error) {                    error.printStackTrace();                    sink.complete();                }            });        });    }}

3.2 高层API实现对话记忆

高层API实现对话记忆,首先接口类的方法要标注一个消息ID@MemoryId String msgId,其次接口方法如果不止一个参数则需要将用户提示词通过@UserMessage注解标注。然后@AiService注解通过属性chatMemoryProvider = "chatMemoryProvider"关联我们之前在配置类声明的chatMemoryProvider对象

package org.example.ai;import dev.langchain4j.service.MemoryId;import dev.langchain4j.service.SystemMessage;import dev.langchain4j.service.UserMessage;import dev.langchain4j.service.spring.AiService;import dev.langchain4j.service.spring.AiServiceWiringMode;import reactor.core.publisher.Flux;@AiService(        wiringMode = AiServiceWiringMode.EXPLICIT,        streamingChatModel = "streamingChatModel",        chatMemoryProvider = "chatMemoryProvider")public interface StreamAssistant {    @SystemMessage("你是一个数学老师,用简单易懂的方式解释数学概念。")    Flux<String> chat(@UserMessage String prompt, @MemoryId String msgId);}

总结

本文简述了langchain4j提示词工程和对应会话记忆的两种API实现。

  •  

LangChain4j开篇

系列未完待续

1.概述

LangChain4j(https://docs.langchain4j.dev/),由Python AI框架LangChain而来,同时也吸纳了Haystack, LlamaIndex的特性,是一款基于Java语言开发大模型应用的工具,提供统一调用AI大模型以及向量存储的API,类似这样的框架还有Spring AI

LangChain4j开发于2023年初,截至目前它支持:

  • 大语言模型LLM 20+
  • 嵌入(向量)模型 20+
  • 嵌入(向量)数据库 30+
  • 多模态
  • 会话记忆存储实现Chat Memory Stores 7个
  • 文档解析Document Parsers:Tika,MD,PDF…
  • RAG
  • Tools(Function calling)
  • Model Context Protocol (MCP),但是SSE模式未来将不受支持
  • 联网搜索Web Search Engines:SearXNG…

LangChain4j在两个抽象层次上运行:

  • 底层API,访问所有底层组件,例如 ChatModel、UserMessage……、AiMessage…… EmbeddingStore、Embedding……等等

  • 高层API,使用高级API(例如AI Service)与LLM进行交互,可以灵活地调整和微调。

2.快速开始

引入langchain4j-bom,截至目前,官网上langchain4j-bom的最高版本是1.8.0,均需要jdk17+

<dependencyManagement>    <dependencies>        <dependency>            <groupId>dev.langchain4j</groupId>            <artifactId>langchain4j-bom</artifactId>            <version>1.8.0</version>            <type>pom</type>            <scope>import</scope>        </dependency>    </dependencies></dependencyManagement><repositories>    <repository>        <name>Central Portal Snapshots</name>        <id>central-portal-snapshots</id>        <url>https://central.sonatype.com/repository/maven-snapshots/</url>        <releases>            <enabled>false</enabled>        </releases>        <snapshots>            <enabled>true</enabled>        </snapshots>    </repository></repositories><build>    <plugins>        <plugin>            <groupId>org.apache.maven.plugins</groupId>            <artifactId>maven-compiler-plugin</artifactId>            <configuration>                <source>21</source>                <target>21</target>                <encoding>UTF-8</encoding>            </configuration>        </plugin>    </plugins></build>

以对接OpenAI大模型为例,添加依赖langchain4j-open-ai,原生使用langchain4j

<dependencies>    <dependency>        <groupId>dev.langchain4j</groupId>        <artifactId>langchain4j-open-ai</artifactId>    </dependency></dependencies>

新建测试类,通过URL,API-KEY以及模型名称构造ChatModel对象,传入system和user提示词,测试调用大模型

package org.example;import dev.langchain4j.data.message.ChatMessage;import dev.langchain4j.data.message.SystemMessage;import dev.langchain4j.data.message.UserMessage;import dev.langchain4j.model.chat.ChatModel;import dev.langchain4j.model.chat.response.ChatResponse;import dev.langchain4j.model.openai.OpenAiChatModel;import java.util.Arrays;import java.util.List;public class Main {    public static void main(String[] args) {        ChatModel chatModel = OpenAiChatModel.builder()                .baseUrl("https://api.gptsapi.net/v1")                .apiKey(System.getProperty("OPEN_API_KEY"))                .modelName("gpt-4.1")                .build();        List<ChatMessage> messages = Arrays.asList(                new SystemMessage("你是一个数学老师,用简单易懂的方式解释数学概念。"),                new UserMessage("什么是微积分?")        );        ChatResponse chatResponse = chatModel.chat(messages);        System.out.println(chatResponse);    }}

得到大模型的回答,原生方式使用langchain4j调用大模型测试通过。

LangChain4j支持和Quarkus, Spring Boot, Helidon和Micronaut进行整合,后面都会集成到Spring Boot中进行测试

3.使用LangChain4j

序号文章名概述
1LangChain4j Prompt对话机器人LangChain4j实现Prompt对话
  •  

一个解析Excel2007的POI工具类

通过apache-poi解析读取excel2007表格中的文字和图片,数字按照字符形式读取,表格中的图片和文字都按照行和列顺序读取到二维数组中相应的位置上。

package com.util;import org.apache.poi.hssf.usermodel.HSSFSheet;import org.apache.poi.hssf.usermodel.HSSFWorkbook;import org.apache.poi.ooxml.POIXMLDocumentPart;import org.apache.poi.ss.usermodel.*;import org.apache.poi.xssf.usermodel.*;import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker;import java.io.ByteArrayInputStream;import java.io.IOException;import java.io.InputStream;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;public class POIUtil {    /**     * 读入excel2007文件     *     * @param file     * @throws IOException     */    public static List<String[]> readExcel(String fileName, byte[] bytes, int sheetNum) throws IOException {        // 获取excel文件的io流        InputStream is = new ByteArrayInputStream(bytes);        // 根据文件后缀名不同(xls和xlsx)获得不同的Workbook实现类对象        Workbook workbook =  new XSSFWorkbook(is);;        // 创建返回对象,把每行中的值作为一个数组,所有行作为一个集合返回        List<String[]> list = new ArrayList<String[]>();        if (workbook != null) {            // for (int sheetNum = 0; sheetNum < workbook.getNumberOfSheets(); sheetNum++) {            // 获得当前sheet工作表            Sheet sheet = workbook.getSheetAt(sheetNum);            // if (sheet == null) {            // continue;            // }            // 获得当前sheet的开始行            int firstRowNum = sheet.getFirstRowNum();            // 获得当前sheet的结束行            int lastRowNum = sheet.getLastRowNum();            // 循环除了第一行的所有行            for (int rowNum = firstRowNum + 0; rowNum <= lastRowNum; rowNum++) {                // 获得当前行                Row row = sheet.getRow(rowNum);                if (row == null || row.getPhysicalNumberOfCells()==0) {                    continue;                }                // 获得当前行的开始列                int firstCellNum = row.getFirstCellNum();                // 获得当前行的列数                int lastCellNum = row.getPhysicalNumberOfCells();                String[] cells = new String[row.getPhysicalNumberOfCells()];                // 循环当前行                for (int cellNum = firstCellNum; cellNum < lastCellNum; cellNum++) {                    Cell cell = row.getCell(cellNum);                    cells[cellNum] = getCellValue(cell);                }                list.add(cells);            }            // }            workbook.close();        }        return list;    }    private static String getCellValue(Cell cell) {        String cellValue = "";        if (cell == null) {            return cellValue;        }        // 把数字当成String来读,避免出现1读成1.0的情况        if (cell.getCellType() == CellType.NUMERIC) {            cell.setCellType(CellType.STRING);        }        // 判断数据的类型        switch (cell.getCellType()) {            case NUMERIC: // 数字                cellValue = String.valueOf(cell.getNumericCellValue());                break;            case STRING: // 字符串                cellValue = String.valueOf(cell.getStringCellValue());                break;            case BOOLEAN: // Boolean                cellValue = String.valueOf(cell.getBooleanCellValue());                break;            case FORMULA: // 公式                cellValue = String.valueOf(cell.getCellFormula());                break;            case BLANK: // 空值                cellValue = "";                break;            case ERROR: // 故障                cellValue = "非法字符";                break;            default:                cellValue = "未知类型";                break;        }        return cellValue;    }    public static Map<String, byte[]> getExcelPictures(String fileName, byte[] bytes, int sheetNum) throws IOException {        Map<String, byte[]> map = new HashMap<String, byte[]>();        // 获取excel文件的io流        InputStream is = new ByteArrayInputStream(bytes);        // 获得Workbook工作薄对象        Workbook workbook =  new XSSFWorkbook(is);;        XSSFSheet sheet = (XSSFSheet) workbook.getSheetAt(sheetNum);        List<POIXMLDocumentPart> list = sheet.getRelations();        for (POIXMLDocumentPart part : list) {            if (part instanceof XSSFDrawing) {                XSSFDrawing drawing = (XSSFDrawing) part;                List<XSSFShape> shapes = drawing.getShapes();                for (XSSFShape shape : shapes) {                    XSSFPicture picture = (XSSFPicture) shape;                    XSSFClientAnchor anchor = picture.getPreferredSize();                    CTMarker marker = anchor.getFrom();                    String key = marker.getRow() + "-" + marker.getCol();                    byte[] data = picture.getPictureData().getData();                    map.put(key, data);                }            }        }        return map;    }}
  •  

Java实现LDAP登录

LDAP的全称是Lightweight Directory Access Protocol(轻量级目录访问协议),是一种用于访问和管理分布式目录信息服务的应用协议。LDAP通常用于存储用户、组和其他组织信息,提供对这些信息的快速查询和管理。

LDAP是基于X.500标准的一个简化版本,使用更简单的网络协议(如 TCP/IP)来实现,定义了客户端如何与目录服务交互,如添加、删除、修改或查询目录信息。

LDAP使用SSL加密(ldaps://)时,如果服务端是自签证书,需提前安装证书到jdk的信任证书库内,我采用的open-jdk8的证书库位于/etc/pki/ca-trust/extracted/java/cacerts,将自签发的证书certificate.pem导入

keytool -importcert -keystore $JAVA_HOME/jre/lib/security/cacerts -storepass changeit -trustcacerts -file certificate.pem -alias uua01

Java原生支持LDAP协议,通过管理员账户adminDnadminPassword连接LDAP服务器,并搜索用户的DN,验证用户凭据,再检查输入的密码是否正确

import javax.naming.Context;import javax.naming.NamingEnumeration;import javax.naming.NamingException;import javax.naming.directory.SearchControls;import javax.naming.directory.SearchResult;import javax.naming.ldap.Control;import javax.naming.ldap.InitialLdapContext;import javax.naming.ldap.LdapContext;import java.io.InputStream;import java.util.Hashtable;import java.util.Map;import lombok.extern.slf4j.Slf4j;@Slf4jpublic class LdapVerify {    public boolean connehct(String username, String password) {        String ip = "";        String port = "";        String timeOut = "";        String adminDn = "";        String adminPassword = "";        String url = String.format("ldaps://%s:%s", ip, port);        // 1. 建立与 LDAP 的连接        Hashtable<String, String> env = new Hashtable<>();        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");        env.put(Context.PROVIDER_URL, url);        env.put(Context.SECURITY_AUTHENTICATION, "simple");        env.put(Context.SECURITY_PRINCIPAL, adminDn);        env.put(Context.SECURITY_CREDENTIALS, adminPassword);        env.put(Context.SECURITY_PROTOCOL, "ssl"); // 启用 LDAPS        env.put("com.sun.jndi.ldap.connect.timeout", "3000");        try {            LdapContext ldapContext = new InitialLdapContext(env, null);            // 2. 查找用户的完整 DN            String searchBase = "OU=All Users,DC=demo,DC=com"; // 搜索起点            String searchFilter = "(sAMAccountName=" + username + ")"; // 根据用户名查找            SearchControls searchControls = new SearchControls();            searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);            NamingEnumeration<SearchResult> results = ldapContext.search(searchBase, searchFilter, searchControls);            if (results.hasMore()) {                SearchResult result = results.next();                String userDn = result.getNameInNamespace();                log.info("LDAP登录, 找到用户 DN: " + userDn);                // 3. 验证用户密码                Hashtable<String, String> userEnv = new Hashtable<>();                userEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");                userEnv.put(Context.PROVIDER_URL, url);                userEnv.put(Context.SECURITY_AUTHENTICATION, "simple");                userEnv.put(Context.SECURITY_PRINCIPAL, userDn);                userEnv.put(Context.SECURITY_CREDENTIALS, password);                userEnv.put(Context.SECURITY_PROTOCOL, "ssl");                userEnv.put("com.sun.jndi.ldap.connect.timeout", "3000");                try {                    new InitialLdapContext(userEnv, null).close();                    log.info("LDAP登录, 用户验证成功 {}", username);                    return true;                }                catch (Exception e) {                    log.error("LDAP登录, 用户验证失败", username);                    return false;                }            }            log.error("LDAP登录, 找不到用户 DN {} ", username);            return false;        }        catch (NamingException e) {            log.error("LDAP登录, 找用户异常 DN {} {} ", username, e.getMessage(), e);            return false;        }    }}
  •  

Java线程的状态

JDK 1.5 前线程状态

线程状态中文名称描述
New新建刚创建的线程,还未启动。
Runnable可运行线程可以运行,可能在等待 CPU 调度。
Blocked阻塞线程被阻塞,正在等待锁的释放。
Dead终止线程执行完成或异常终止,已进入结束状态。

jdk1.5之前

JDK 1.5 后线程状态

java.lang.Thread.State

public enum State {    /**     * Thread state for a thread which has not yet started.     */    NEW,    /**     * Thread state for a runnable thread.  A thread in the runnable     * state is executing in the Java virtual machine but it may     * be waiting for other resources from the operating system     * such as processor.     */    RUNNABLE,    /**     * Thread state for a thread blocked waiting for a monitor lock.     * A thread in the blocked state is waiting for a monitor lock     * to enter a synchronized block/method or     * reenter a synchronized block/method after calling     * {@link Object#wait() Object.wait}.     */    BLOCKED,    /**     * Thread state for a waiting thread.     * A thread is in the waiting state due to calling one of the     * following methods:     * <ul>     *   <li>{@link Object#wait() Object.wait} with no timeout</li>     *   <li>{@link #join() Thread.join} with no timeout</li>     *   <li>{@link LockSupport#park() LockSupport.park}</li>     * </ul>     *     * <p>A thread in the waiting state is waiting for another thread to     * perform a particular action.     *     * For example, a thread that has called <tt>Object.wait()</tt>     * on an object is waiting for another thread to call     * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on     * that object. A thread that has called <tt>Thread.join()</tt>     * is waiting for a specified thread to terminate.     */    WAITING,    /**     * Thread state for a waiting thread with a specified waiting time.     * A thread is in the timed waiting state due to calling one of     * the following methods with a specified positive waiting time:     * <ul>     *   <li>{@link #sleep Thread.sleep}</li>     *   <li>{@link Object#wait(long) Object.wait} with timeout</li>     *   <li>{@link #join(long) Thread.join} with timeout</li>     *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>     *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>     * </ul>     */    TIMED_WAITING,    /**     * Thread state for a terminated thread.     * The thread has completed execution.     */    TERMINATED;}
线程状态中文名称描述
New新建刚创建的线程,还未启动。
Runnable可运行线程可以运行,可能在等待 CPU 调度。
Blocked阻塞线程尝试获取锁失败,被阻塞,等待锁释放。
Waiting等待线程进入等待状态,等待其他线程显式唤醒,通常由 Object.wait() 引起。
Timed Waiting计时等待线程等待指定时间后自动唤醒,由 Thread.sleep()wait(time) 引起。
Terminated终止线程执行完成或异常终止,已进入结束状态。

jdk1.5之后

  •  

一个通用的CloseableHttpClient工厂类

一个CloseableHttpClient工厂,基于java知名开源库apache-httpclient,能够忽略SSL,并且超时和状态异常时可以重试

<dependencies>    <dependency>        <groupId>org.apache.httpcomponents</groupId>        <artifactId>httpclient</artifactId>        <version>4.5.9</version>    </dependency>    <dependency>        <groupId>org.apache.httpcomponents</groupId>        <artifactId>httpmime</artifactId>        <version>4.5.9</version>    </dependency></dependencies>
package util;import lombok.extern.slf4j.Slf4j;import org.apache.http.ConnectionClosedException;import org.apache.http.HttpResponse;import org.apache.http.NoHttpResponseException;import org.apache.http.client.CookieStore;import org.apache.http.client.ServiceUnavailableRetryStrategy;import org.apache.http.conn.ConnectTimeoutException;import org.apache.http.conn.ssl.NoopHostnameVerifier;import org.apache.http.conn.ssl.SSLConnectionSocketFactory;import org.apache.http.impl.client.BasicCookieStore;import org.apache.http.impl.client.CloseableHttpClient;import org.apache.http.impl.client.HttpClients;import org.apache.http.protocol.HttpContext;import org.apache.http.ssl.SSLContextBuilder;import javax.net.ssl.SSLContext;import javax.net.ssl.SSLHandshakeException;import java.net.SocketTimeoutException;@Slf4jpublic class HttpClientUtil {    public static CloseableHttpClient createSSLClientDefault() {        try {            SSLContextBuilder sslContextBuilder = new SSLContextBuilder();            SSLContext sslContext = sslContextBuilder.loadTrustMaterial(null, (chain, authType) -> true).build();            SSLConnectionSocketFactory ssl = new SSLConnectionSocketFactory(                    sslContext,                    new String[]{"TLSv1", "TLSv1.1", "TLSv1.2"},                    null,                    SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);            return HttpClients.custom()                    .setSSLSocketFactory(ssl)                    .setRetryHandler((e, executionCount, context) -> {                        if (executionCount <= 20) {                            if (e instanceof NoHttpResponseException                                    || e instanceof ConnectTimeoutException                                    || e instanceof SocketTimeoutException                                    || e instanceof SSLHandshakeException) {                                log.info("{} 异常, 重试 {}", e.getMessage(), executionCount);                                return true;                            }                        }                        return false;                    })                    .setServiceUnavailableRetryStrategy(new ServiceUnavailableRetryStrategy() {                        @Override                        public boolean retryRequest(HttpResponse response, int executionCount, HttpContext context) {                            int statusCode = response.getStatusLine().getStatusCode();                            if (statusCode != 200) {                                if (executionCount <= 20) {                                    log.info("{} 状态码异常, 重试 {}", statusCode, executionCount);                                    return true;                                }                            }                            return false;                        }                        @Override                        public long getRetryInterval() {                            return 0;                        }                    })                    .build();        }        catch (Exception e) {            throw new RuntimeException(e);        }    }}
  •  

JUC可重入锁ReentrantLock

1.概述

java.util.concurrent.locks.ReentrantLock,即可重入锁,是Lock接口的一个实现类,可以实现和synchronized一样的功能

以往我们都是使用synchronized解决线程的安全问题,在JUC中,java.util.concurrent.locks.Lock接口提供了比synchronized更多的功能,可以使用Lock接口实现手动上锁和释放锁,并且可以更加精准的设置条件(Condition)控制等待唤醒,它的特点以及和synchronized的区别是:

  1. synchronized是Java中的关键字,而Lock则是Java中的一个类。
  2. synchronized的上锁和释放锁都是自动完成,而使用Lock时必须手动释放锁,否则会造成死锁现象。
  3. Lock可以让等待锁的线程中断,而synchronized却不能,使用synchronized时,未得到锁的线程会一直等待下去不会中断。
  4. 通过Lock可以知道是否成功获取到锁,而synchronized却不能。
  5. Lock可以提高多个线程进行读操作的效率,在大量线程同时竞争时,效率远胜于synchronized。

2.获取锁和释放锁

使用lock.lock();方法实现上锁,用lock.unlock();方法释放锁,一般情况下,lock.unlock();的代码总是要写在finally中,以保证方法出现异常后也能释放锁,避免死锁。

例:10个线程卖票,每次只能有一个线程对票数进行修改,JUC中所有的例子都可以写成“线程操作资源类”的形式,本例创建10个线程共同操作资源类TicketTask,调用ticket()方法卖票

package example.juc.test2;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class TestLock {    public static void main(String[] args) {        TicketTask ticketTask = new TicketTask();        for (int i = 0; i < 10; i++) {            new Thread(() -> {                ticketTask.ticket();            }, "线程" + i).start();        }    }}class TicketTask {    private int number = 500;    private Lock lock = new ReentrantLock();    public void ticket() {        while (true) {            lock.lock();            try {                if (number > 0) {                    try {                        Thread.sleep(10);                    } catch (InterruptedException e) {                        throw new RuntimeException(e);                    }                    number--;                    System.out.println(Thread.currentThread().getName() + "完成售票:" + number);                } else {                    break;                }            } finally {                lock.unlock();            }        }    }}

3.公平与非公平

synchronized默认是非公平的,ReentrantLock也是默认非公平锁,比如以下例子运行结果会出现卖的票被同一个线程大量抢占的情况

package example.juc.test2;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class TestLock {    public static void main(String[] args) {        TicketTask ticketTask = new TicketTask();        new Thread(() -> {            for (int i = 0; i < 1000; i++) {                ticketTask.ticket();            }        }, "线程A").start();        new Thread(() -> {            for (int i = 0; i < 1000; i++) {                ticketTask.ticket();            }        }, "线程B").start();        new Thread(() -> {            for (int i = 0; i < 1000; i++) {                ticketTask.ticket();            }        }, "线程C").start();    }}class TicketTask {    private int number = 30;    private Lock lock = new ReentrantLock();    public void ticket() {        lock.lock();        try {            if (number > 0) {                try {                    Thread.sleep(10);                }                catch (InterruptedException e) {                    throw new RuntimeException(e);                }                number--;                System.out.println(Thread.currentThread().getName() + "完成售票:" + number);            }        }         finally {            lock.unlock();        }    }}

出现这种情况的原因就在于,ReentrantLock默认是非公平的

java.util.concurrent.locks.ReentrantLock

public ReentrantLock() {    sync = new NonfairSync();}......public ReentrantLock(boolean fair) {    sync = fair ? new FairSync() : new NonfairSync();}

如果想要实现公平的效果,创建对象时需要加上一个初始化参数

private Lock lock = new ReentrantLock(true);

公平锁的优点在于“公平”,缺点在于效率较低

4.可重入

可重入锁指的是,一个线程在一个方法外层获取了锁 ,进入方法内层会自动获取锁。

和synchronized一样,ReentrantLock也是可重入的,但是需要注意加锁和释放次数要匹配,否则其他线程无法获得锁,例如:

package example.juc3;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class TestReentrantLock {    public static void main(String[] args) {        Lock lock = new ReentrantLock();        new Thread(() -> {            lock.lock();            try {                System.out.println("外层");                lock.lock();                try {                    System.out.println("内层");                } finally {                    lock.unlock();                }            } finally {                lock.unlock();            }        },"t1").start();        new Thread(() -> {            lock.lock();            try {                System.out.println("2");            } finally {                lock.unlock();            }        },"t2").start();    }}

5.响应中断

synchronized是无法支持响应中断的,一个线程获取不到锁,就会一直等着,程序无法结束,造成死锁。

而ReentrantLock支持通过tryLock()设置一个时间参数,到时间后自动放弃获取锁,如果不设置时间参数代表立即返回获取锁的结果,通过这个可以避免死锁现象

例1:派发1000个线程,每个为变量加1,结果抢不到的直接放弃,所以变量被加的远远不到1000

package example.juc2.test;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class TestTryLock {        private static final Lock lock = new ReentrantLock();    private static int sum = 0;    public static void main(String[] args) {        for (int i = 0; i < 1000; i++) {            new Thread(() -> {                if (lock.tryLock()) {                    try {                        sum ++;                        System.out.println(Thread.currentThread().getName() +" - "+ sum);                    }                    catch (Exception e) {                        throw new RuntimeException(e);                    }                    finally {                        lock.unlock();                    }                }            }).start();        }    }}

例2:每个抢到锁的线程1秒钟才能执行完成,每个抢不到锁的线程最多等待5秒否则直接放弃争抢锁,因此程序只能打印5次左右

package example.juc2.test;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class TestTryLock {        private static final Lock lock = new ReentrantLock();    private static int sum = 0;    public static void main(String[] args) {        for (int i = 0; i < 1000; i++) {            new Thread(() -> {                try {                    if (lock.tryLock(5, TimeUnit.SECONDS)) {                        try {                            sum ++;                            System.out.println(Thread.currentThread().getName() +" - "+ sum);                            TimeUnit.SECONDS.sleep(1);                        }                        catch (Exception e) {                            throw new RuntimeException(e);                        }                        finally {                            lock.unlock();                        }                    }                }                catch (InterruptedException e) {                    throw new RuntimeException(e);                }            }).start();        }    }}

6.精准唤醒

如果说Lock代替了synchronized的使用,Condition则是替代了Object的wait()notify()notifyAll()方法,java.util.concurrent.locks.Condition是一个接口,通过调用Lock对象的newCondition()方法获取具体Condition对象,将Condition绑定在Lock上,通过Condition的await()signal()signalAll()实现线程之间的通信,与传统Object作为同步监视器一样,Condition的await()也要总是出现在循环中,实现二次条件判断。

例:实现一个资源类AirConditioner,然后新建4个线程,每个线程对AirConditioner中的变量number交替改成0和1

package example.juc2.test;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class TestConditionWaitNotify {        public static class AirConditioner {                private int number = 0;                private Lock lock = new ReentrantLock();        private Condition condition = lock.newCondition();        public void increment() {            lock.lock();            try {                while (number != 0) {                    condition.await();                }                number ++;                System.out.println(Thread.currentThread().getName()+" 修改为 "+number);                condition.signalAll();            } catch (InterruptedException e) {                throw new RuntimeException(e);            } finally {                lock.unlock();            }                    }        public void decrement() {            lock.lock();            try {                while (number == 0) {                    condition.await();                }                number --;                System.out.println(Thread.currentThread().getName()+" 修改为 "+number);                condition.signalAll();            }             catch (InterruptedException e) {                throw new RuntimeException(e);            }             finally {                lock.unlock();            }        }    }    public static void main(String[] args) {        AirConditioner airConditioner = new AirConditioner();        new Thread(() -> {            for (int i=0;i<10;i++) {                airConditioner.increment();            }        }, "T1").start();        new Thread(() -> {            for (int i=0;i<10;i++) {                airConditioner.decrement();            }        }, "T2").start();        new Thread(() -> {            for (int i=0;i<10;i++) {                airConditioner.increment();            }        }, "T3").start();        new Thread(() -> {            for (int i=0;i<10;i++) {                airConditioner.decrement();            }        }, "T4").start();    }}

运行结果:

T1 修改为 1T2 修改为 0T1 修改为 1T2 修改为 0T3 修改为 1T2 修改为 0T3 修改为 1T4 修改为 0T3 修改为 1T4 修改为 0T3 修改为 1T4 修改为 0T3 修改为 1T4 修改为 0T3 修改为 1T4 修改为 0T3 修改为 1T4 修改为 0T3 修改为 1T4 修改为 0T3 修改为 1T4 修改为 0T3 修改为 1T4 修改为 0T1 修改为 1T4 修改为 0T1 修改为 1T2 修改为 0T1 修改为 1T2 修改为 0T1 修改为 1T2 修改为 0T1 修改为 1T2 修改为 0T1 修改为 1T2 修改为 0T1 修改为 1T2 修改为 0T1 修改为 1T2 修改为 0

上面的例子中,线程对资源类的操作是无序的,和加了synchronized的效果一样,而Condition有一个比synchronized更强大的地方,那就是能精确的控制等待唤醒。如果希望几个线程有序的交替执行,就可以获取多个监视器Condition绑定同一个Lock,实现精确的定制化通信。

例:三个线程ABC,按A-B-C顺序执行资源类ShareResource中的打印任务,并循环交替3轮(ABC-ABC-ABC),A执行时打印5次,B10次,C15次,通过lock.newCondition()为三个线程分别创建3个同步监视器:conditionA,conditionB, conditionC,再通过标志位变量flag判断当前该谁执行了,开始默认为A。

程序执行,A线程先获得执行权,执行第一轮,执行完成唤醒B,执行第一轮,B线程执行完成唤醒C,C也执行第一轮,完成再唤醒A,A开始执行第二轮,以此类推直到三轮任务全部完成,实现每一轮都是按照A-B-C的顺序执行。。

package example.juc2.test;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class TestThreadOrderAccess {        public static class ShareResource {                private char flag = 'A';                private final Lock lock = new ReentrantLock();                private final Condition conditionA = lock.newCondition();        private final Condition conditionB = lock.newCondition();        private final Condition conditionC = lock.newCondition();                public void doA() {            lock.lock();            try {                while (flag != 'A') {                    conditionA.await();                }                for (int i = 0; i < 5; i++) {                    System.out.println(Thread.currentThread().getName() + "\t" +i);                }                                flag = 'B';                conditionB.signal();            }             catch (InterruptedException e) {                throw new RuntimeException(e);            }             finally {               lock.unlock();             }        }        public void doB() {            lock.lock();            try {                while (flag != 'B') {                    conditionB.await();                }                for (int i = 0; i < 10; i++) {                    System.out.println(Thread.currentThread().getName() + "\t" +i);                }                flag = 'C';                conditionC.signal();            }             catch (InterruptedException e) {                throw new RuntimeException(e);            }             finally {                lock.unlock();            }        }        public void doC() {            lock.lock();            try {                while (flag != 'C') {                    conditionC.await();                }                for (int i = 0; i < 15; i++) {                    System.out.println(Thread.currentThread().getName() + "\t" +i);                }                System.out.println("*********");                flag = 'A';                conditionA.signal();            }             catch (InterruptedException e) {                throw new RuntimeException(e);            }             finally {                lock.unlock();            }        }            }    public static void main(String[] args) {        ShareResource shareResource = new ShareResource();                new Thread(() -> {            for (int i = 0; i < 3; i++) {                shareResource.doA();            }        }, "A").start();                new Thread(() -> {            for (int i = 0; i < 3; i++) {                shareResource.doB();            }        }, "B").start();                new Thread(() -> {            for (int i = 0; i < 3; i++) {                shareResource.doC();            }        }, "C").start();    }}

最终运行结果:

A0A1A2A3A4B0B1B2B3B4B5B6B7B8B9C0C1C2C3C4C5C6C7C8C9C10C11C12C13C14*********A0A1A2A3A4B0B1B2B3B4B5B6B7B8B9C0C1C2C3C4C5C6C7C8C9C10C11C12C13C14*********A0A1A2A3A4B0B1B2B3B4B5B6B7B8B9C0C1C2C3C4C5C6C7C8C9C10C11C12C13C14*********

7.总结

Lock和synchronized都是独占锁,可重入锁,但是synchronized获取/释放锁是JVM自动完成,Lock是需要开发者手动完成。synchronized不能响应中断,Lock可以响应中断,synchronized无法精准唤醒,Lock可以实现精准唤醒。

  •  

JUC读写锁ReadWriteLock

1.概述

在一些业务场景中,如果大部分是读数据,写数据的很少,此时如果使用独占锁(synchronized,Lock)会导致效率低下,因此JUC提供了读写锁java.util.concurrent.locks.ReadWriteLock来解决这个问题

2.ReentrantReadWriteLock

可重入读写锁java.util.concurrent.locks.ReentrantReadWriteLock是读写锁的一个实现类,允许同一时刻多个读线程同时访问,但是在写线程访问时,所有其他写线程和读线程都不可访问

特点:

  • 写写不可并发
  • 读写不可并发
  • 读读允许并发

例子:

package example.juc2.test;import java.util.HashMap;import java.util.Map;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;/** * 读写锁 */public class TestReadWriteLock {        public static class MyCache {        private volatile Map<String, String> map = new HashMap<>();        private ReadWriteLock lock = new ReentrantReadWriteLock();                public void write(String key, String valle) {            lock.writeLock().lock();            try {                System.out.println(Thread.currentThread().getName() + "准备写入数据");                TimeUnit.SECONDS.sleep(2);                map.put(key, valle);                System.out.println(Thread.currentThread().getName() + "写入数据完成");            } catch (Exception e) {                e.printStackTrace();            } finally {                lock.writeLock().unlock();            }        }                public void read(String key) {            lock.readLock().lock();            try {                System.out.println(Thread.currentThread().getName() + "准备读取数据");                TimeUnit.SECONDS.sleep(2);                String s = map.get(key);                System.out.println(Thread.currentThread().getName() + " 读取数据 " + s);            } catch (Exception e) {                            } finally {                lock.readLock().unlock();            }        }    }                public static void main(String[] args) {        MyCache myCache = new MyCache();                for (int i = 0; i < 5; i++) {            int finalI = i;            new Thread(() -> {                myCache.write(finalI +"", finalI+"");            }).start();        }        for (int i = 0; i < 5; i++) {            int finalI = i;            new Thread(() -> {                myCache.read(finalI +"");            }).start();        }    }}
  •  

Java的单例

1. 懒汉式(Lazy Initialization)

这种方式在第一次调用时创建实例,延迟实例的创建。

public class LazySingleton {    private static LazySingleton instance;    private LazySingleton() {        // 私有构造函数    }    public static LazySingleton getInstance() {        if (instance == null) {            instance = new LazySingleton();        }        return instance;    }}

2. 线程安全的懒汉式

通过同步方法来保证线程安全。

public class ThreadSafeLazySingleton {    private static ThreadSafeLazySingleton instance;    private ThreadSafeLazySingleton() {        // 私有构造函数    }    public static synchronized ThreadSafeLazySingleton getInstance() {        if (instance == null) {            instance = new ThreadSafeLazySingleton();        }        return instance;    }}

3. 双重检查锁定(Double-Checked Locking)

通过双重检查来减少同步的开销。

public class DoubleCheckedLockingSingleton {    private static volatile DoubleCheckedLockingSingleton instance;    private DoubleCheckedLockingSingleton() {        // 私有构造函数    }    public static DoubleCheckedLockingSingleton getInstance() {        if (instance == null) {            synchronized (DoubleCheckedLockingSingleton.class) {                if (instance == null) {                    instance = new DoubleCheckedLockingSingleton();                }            }        }        return instance;    }}

4. 饿汉式(Eager Initialization)

在类加载时就创建实例,简单且线程安全。

public class EagerSingleton {    private static final EagerSingleton instance = new EagerSingleton();    private EagerSingleton() {        // 私有构造函数    }    public static EagerSingleton getInstance() {        return instance;    }}

5. 静态内部类(Static Inner Class)

利用类加载机制确保单例的延迟初始化和线程安全。

public class StaticInnerClassSingleton {    private StaticInnerClassSingleton() {        // 私有构造函数    }    private static class SingletonHolder {        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();    }    public static StaticInnerClassSingleton getInstance() {        return SingletonHolder.INSTANCE;    }}

6. 枚举单例

使用枚举来实现单例,简洁且天然支持序列化。

public enum EnumSingleton {    INSTANCE;    public void someMethod() {        // 示例方法    }}
  •  

Java泛型

一、引言

泛型(Generics)和面向对象、函数式编程一样,也是一种程序设计的范式,泛型允许程序员在定义类、接口和方法时使用引用类型的类型形参代表一些以后才能确定下来的类型,在声明变量、创建对象、调用方法时像调用函数传参一样将具体类型作为实参传入来动态指明类型。

Java的泛型,是在jdk1.5中引入的一个特性,最主要应用是在jdk1.5的新集合框架中。作为Java语法层面的东西,本博客原本不打算介绍,但考虑到泛型理解和使用起来有一定的难度,应用的还很普遍,再加上自己工作多年好像也没有能够完全理解和灵活的运用泛型,因此还是决定看一些相关的书籍中与泛型有关的内容,并用一些篇幅总结下学习成果,介绍下我理解的泛型。

二、泛型类(接口)

2.1 创建泛型类

先来看两个类

public class StringPrinter {    private String thingsToPrint;    public StringPrinter() {    }    public StringPrinter(String thing) {        thingsToPrint = thing;    }    public void setThingsToPrint(String thing) {        thingsToPrint = thing;    }    public void print() {        System.out.println(thingsToPrint);    }}
public class IntegerPrinter {    private String thingsToPrint;    public IntegerPrinter() {    }    public IntegerPrinter(String thing) {        thingsToPrint = thing;    }    public void setThingsToPrint(String thing) {        thingsToPrint = thing;    }    public void print() {        System.out.println(thingsToPrint);    }}

两个类的作用相当,都是将传进来的参数进行打印,类的功能几乎完全相同,唯一的不同是参数的类型不一样,假如要为很多类型实现这个打印功能,就会编写很多的Printer类,如果要实现一个类统一实现这个功能,就可以采用泛型。

先来讲讲泛型语法,泛型用一个“菱形”<>声明,<>中是类型形参列表,如有多个类型形参,使用英文逗号,隔开。

下面程序定义了一个带有泛型声明的Printer类,有一个类型形参T(Type),声明了类的泛型参数后,就可以在类内部使用此泛型参数,构造函数名仍然是类名本身不需要加泛型

public class Printer<T> {    private T thingsToPrint;    public Printer() {            }    public void setThingsToPrint(T thing) {        thingsToPrint = thing;    }    public Printer(T thing) {        thingsToPrint = thing;    }    public void print() {        System.out.println(thingsToPrint);    }}

Java还能在定义类型参数时设置限制条件,如下例定义了一个NumberPrinter类,通过extends指定T的类型上限只能是Number。

⚠️注意:类型参数和第四章提到的类型通配符是不一样的,类型参数上的限制不能用super关键字,因为会造成不确定,使用extends指定T的类型上限,编译器至少知道T是个Number,如果是super关键字,编译器根本不知道T有哪些属性和方法。

public class NumberPrinter<T extends Number> {    private T thingsToPrint;    public NumberPrinter() {    }    public void setThingsToPrint(T thing) {        thingsToPrint = thing;    }    public NumberPrinter(T thing) {        thingsToPrint = thing;    }    public void print() {        System.out.println(thingsToPrint);    }    public T get() {        return thingsToPrint;    }}

还可以设置多个限制条件,extends后面只能有一个类但是可以有多个接口:

public class NumberPrinter<T extends Number & Comparable<T>> {    private T thingsToPrint;}

创建泛型接口同理,例如jdk中的List实际上就是一个接口。

public interface List<E> extends Collection<E> {    }

并非任何类都能声明为泛型类,Java规定:异常类(java.lang.Throwable)不得带有泛型

public class MyException<T> extends Exception { //编译出错❌,Generic class may not extend 'java.lang.Throwable'    T msg;}
public class MyException<T> extends RuntimeException { //编译出错❌,Generic class may not extend 'java.lang.Throwable'    T msg;}
public class MyException<T> extends Throwable { //编译出错❌,Generic class may not extend 'java.lang.Throwable'    T msg;}

2.2 实例化泛型类

使用泛型类创建对象时就可以为类型形参T传入具体类型,就可以生成类似Printer<String>Printer<Double>的类型

public static void main(String[] args) {    // 构造器T形参是String,只能用String初始化    Printer<String> printer1 = new Printer<String>("apple");    printer1.print(); //apple    // 构造器T形参是Double,只能用Double初始化    Printer<Double> printer2 = new Printer<Double>(3.8);    printer2.print(); //3.8}

jdk1.7以后,支持泛型类型推断,可以简写为:

Printer<String> printer1 = new Printer<>("apple");Printer<Double> printer2 = new Printer<>(3.8);

如不指定类型实参默认为Object类型,因为所有引用类型都能被Object代表,int、double、char等基本数据类型不能被Object代表,这就是类型实参必须是引用类型的原因,不过注意如果定义类型形参时通过entends指定了上限例如NumberPrinter<T extends Number>,则不传递类型实参时默认为上限类型Number

public static void main(String[] args) {    Printer printer1 = new Printer("apple");    printer1 = new Printer(12);    printer1 = new Printer(new Date());    NumberPrinter numberPrinter1 = new NumberPrinter(5);    NumberPrinter numberPrinter2 = new NumberPrinter(5.8);    NumberPrinter numberPrinter3 = new NumberPrinter(""); //编译出错❌}

2.3 派生泛型类

派生该类时,需要指定类型实参

public class HPPrinter extends Printer<Integer> {}

通过entends指定了上限的类型需不超过上限类型,以下同理

public class SuperNumberPrinter extends NumberPrinter<Double> {}public class SuperNumberPrinter extends NumberPrinter<Date> { //编译出错❌}

如不使用泛型,不指定类型实参,则泛型转换为Object类型或上限类型

public class HPPrinter extends Printer {    public static void main(String[] args) {        HPPrinter hpPrinter = new HPPrinter();        hpPrinter.setThingsToPrint(new Object());        hpPrinter.setThingsToPrint("hello");        hpPrinter.setThingsToPrint(12);    }}

还可以子类和父类声明同一个类型形参,子类中也不确定具体的类型,需要子类被实例化时将类型间接传递给父类,同时子类还可以一同定义自己的泛型

public class HPPrinter<T> extends Printer<T> {}
public class HPPrinter<T, E> extends Printer<T> {}

子类确定父类泛型类型的同时,又可以有自己的泛型

public class HPPrinter<E> extends Printer<Integer> {}

使用泛型又不指定类型的写法是错误的

public class HPPrinter extends Printer<T> { //编译出错❌}

三、泛型方法和泛型构造器

有时候,在类和接口上不需定义类型形参,只是具体方法中的某个类型不确定,需要在方法上面定义类型形参,这个也是支持的,jdk1.5提供了对于泛型方法的支持。

3.1 泛型方法

声明方法时,在返回值前指明泛型的类型形参列表<>,类型形参仅作用于方法内,这个方法就声明为了泛型方法。类型形参可以出现在参数和返回值中,调用方法时指定具体类型。泛型方法可以根据需要声明为静态。任何类中都可以存在泛型方法,而不是只有泛型类中才能声明泛型方法。

💡在返回值前面指明泛型的类型形参列表<>是泛型方法的特征,没有这个特征的都不是泛型方法,泛型类中使用类<>里面声明的类型作为方法参数或返回值类型的方法,不属于泛型方法,例如2.1中Printer类中的任何方法都不是泛型方法。

public class Demo {    public <T> E fun1(T e) {        return null;    }    public <T> void fun2(T e) {            }    public static <T> List<T> copyArray(T[] arr) {        List<T> list = new ArrayList<>();                for (int i = 0; i < arr.length; i++) {            list.add(arr[i]);        }                return list;    }    public <T> T getMiddle(T... a) {        return a[a.length / 2];    }    public static  <T> T getMiddleStatic(T... a) {        return a[a.length / 2];    }}

与类、接口中使用泛型参数不同的是,方法中的泛型参数无须显式传入实际类型参数,当程序调用copyArray()方法时,无须在调用该方法前传入String、Obiect等类型,但系统依然可以知道类型形参的数据类型,因为编译器根据实参推断类型形参的值,它通常推断出最直接的类型参数。例如,下面调用代码:

public static void main(String[] args) {    List<Dog> dogs = copyArray(new Dog[]{});}

如果要显示指明类型实参,则需要和实际类型一致,而且必须在对象名.this.类名.之后指定,否则语法报错。

public static void main(String[] args) {    Demo demo = new Demo();    Integer middle1 = getMiddleStatic(1, 2, 3);        String middle2 = Demo.<String>getMiddleStatic("a", "b", "c");    // 指定的和传入的类型不匹配,编译出错❌    Integer middle3 = Demo.<String> getMiddleStatic(1, 2, 3);    Double d = demo.<Double>getMiddleStatic(1.0, 2.0, 3.0);}

当使用自动推断时,如果涉及多个类型进行自动推断则取多个类型的共同父类(接口)

public static void main(String[] args) {    Number n1 = getMiddleStatic(1, 2, 3.9);    Serializable n2 = getMiddleStatic(1, 2, "", new Date());    Serializable n3 = getMiddleStatic( 2, "", null);}

3.2 泛型构造器

构造器也可能成为泛型方法,Java也允许在构造器签名中声明类型形参,这样就产生了所谓的泛型构造器。例如:

public class Demo {    public <T> Demo(T obj) {        System.out.println(obj.getClass());    }    public static void main(String[] args) {        new Demo("hello"); //class java.lang.String        new Demo(12); //java.lang.Integer        new <String> Demo("hello"); //class java.lang.String        new <Integer> Demo(12); //java.lang.Integer        new <String> Demo(12); // 编译出错❌    }}

如果泛型构造器上指明了类型形参,则不可以在new后使用“菱形”语法又手动指定,否则会导致类型无法推断。不过这种奇怪的写法应该不常碰见

public class Demo<T> {    public <T> Demo(T obj) {        System.out.println(obj.getClass());    }    public static void main(String[] args) {                // 类指定String 方法显示指定Integer        Demo<String> demo1 =  new <Integer> Demo<String>(4);        //类指定Integer 方法隐式指定String        Demo<Integer> demo2 = new Demo<Integer>("4");                //类型推断为Integer        Demo<Integer> demo3 = new Demo<>(3);                // 编译出错❌,不能既要求自动类型推断,又要手动指定        Demo<Integer> demo4 = new <String> Demo<>("4");    }}

四、不存在泛型类

包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态地生成无数多个逻辑上的子类,但这种子类在物理上并不存在。

即使加了不同泛型,运行时仍然是同一种类,并不会因为类型参数的不同,产生新的类

public static void main(String[] args) {    Fruit<String> fruit = new Fruit<>("apple");    Fruit<Double> fruit2 = new Fruit<>(3.8);    System.out.println(fruit2.getClass() == fruit.getClass()); //true}

因此在泛型类中的静态的代码块、静态变量和静态方法上,不能使用类型形参

public class Demo<T> {        public static T st; //编译出错❌        static {        T a = st;  //编译出错❌    }        public static void fun1(T obj) { //编译出错❌        st = obj;    }}

由于并不存在真正的泛型类,因此instanceof关键字后不能接泛型类

if (new ArrayList<>() instanceof List<String>) {  //编译出错❌            }if (new ArrayList<>() instanceof List) { //正确写法✅            }

事实上,泛型在编译后会被擦除,运行时Java虚拟机中没有泛型,只有普通类和普通方法,<T>会变为Object类型,<T extends Serializable>会变成Serializable类型

例如下面例子通过反射忽略了泛型,从而在运行时将一个List<String>中添加进去了一个Integer和Date类型的对象。

public class TestReflection {    public static void main(String[] args) {        List<String> strs = new ArrayList<>();        strs.add("hello");        strs.add("world");        Class clazz = strs.getClass();        try {            Method method = clazz.getMethod("add", Object.class);            method.invoke(strs, 1);            method.invoke(strs, new Date());        }        catch (Exception e) {            e.printStackTrace();        }        finally {            for (Object obj : strs) {                System.out.println(obj);            }        }    }}

运行结果

helloworld1Fri Apr 25 22:30:10 CST 2025

之前提到Java规定异常类不得带有泛型,原因就是异常在运行时是存在的,而泛型在运行时不存在,进行捕获处理时根本不能区分出来。

因为泛型运行时被擦除,因此泛型会影响方法的重载。例如下例由于List的泛型被擦除,导致两个方法不能重载。

public class Demo {    //编译出错❌ 'test(List<String>)' clashes with 'test(List<Integer>)'; both methods have same erasure    void test(List<String> list) {            }    void test(List<Integer> list) {    }}

五、类型通配符

因为泛型被大面积应用于Java集合,因此以List集合为例进行分析。

有时,如要实现一个遍历打印list的方法,list中是哪一种元素都有可能,于是我们将泛型实参指定为Object类型,看似解决了问题,但是调用时却会编译报错:无法将List<Object>用于List<String>

public class Demo {    public static void main(String[] args) {        List<String> strings = new ArrayList<>();        test(strings); //编译出错❌    }    public static void test(List<Object> list) {        for (int i = 0; i < list.size(); i++) {            System.out.println(list.get(i));        }    }}

在Java中,两个类通过继承和实现接口可以具有父子关系,但不能认为使用了父子类型的两个泛型类具有父子关系,例如上面程序出现了编译错误,说明List<String>不能被当成List<Object>的子类来用。

💡泛型与数组不同,如果是两个有父子关系的类各声明一个数组,例如:Object[]String[]String[]Object[]的子类型,是可以将String[]类型的变量赋值给Object[]的,这是一种Java语言早期不安全的设计,操作不当会引发ArrayStoreException,因此jdk1.5设计泛型时避免了这种设计。

为了表示任意类型,可以使用类型通配符,类型通配符是一个问号?,例如将一个问号作为类型实参传给List集合,写作List<?>,意思是元素类型未知的List。这个问号?被称为通配符,它的元素类型可以匹配任何类型。

现在使用任何类型的List来调用它,程序依然可以访问集合中的元素,其类型是Obiect,这永远是安全的,因为不管List的真实类型是什么,它包含的都是Obiect。

public static void test(List<?> list) {    Object obj = list.get(i);}

但是,如果调用add()方法向其中添加非null元素,又会发现编译出错

public static void test(List<?> list) {        list.add(new Date()); //编译出错❌    list.add(null); //能通过编译✅}

List.java

E get(int index);boolean add(E e);

通过分析List的get()add()两个方法的源码,可知get()方法是对泛型的读取,返回为EE类型虽然不确定,但肯定是一个Object类型,而add()方法需要为E类型赋值一个参数,是对泛型的写入,而传进来的?不能确定是什么类型,假如传进来的List是个List<String>,在方法中又add(new Date())写入Date类型,就会导致类型混乱,因此无法处理,但是null除外,它是任何引用类型的实例。

说白了,Java的泛型系统是类型安全优先的,不确定类型的泛型可读不可写。

类型通配符还能进行类型范围的限制,例如如果不希望List<?>可以传入任意一种类型,只希望传入某一类具体的类型,在设置类型通配符时,可以添加extendssuper限制条件,叫做受限制的通配符,extends代表某种类型及子类,super代表某种类型及父类。

例如有这样的一些类:

/** * 动物 */public class Animal {}/** * 猫 */public class Cat extends Animal {}/** * 狗 */public class Dog extends Animal {}/** * 英短猫 */public class YingDuan extends Cat {    }/** * 布偶猫 */public class BuOu extends Cat {    }

首先看extends,extends设置的是类型的上限,保证传入的类型不能超过某个类型,在使用时,如果只希望泛型参数类型是某个类型及其子类,List的类型参数就可以写成? extends,例如:List<? extends Animal>就是只允许传入的泛型类型是Animal及其子类,这样修饰的泛型可读,读出为父类Animal类型,但不可写

为什么不能写入?因为允许写入会导致类型混乱,只要泛型中限制某个类及其子类,那随着类的不断继承就一定会出现更小的子类,当更小的子类作为类型参数时,比这个子类大一些的父类祖父类对象就不能写到泛型修饰的变量中,因为没有子类引用指向父类的道理。再者假如两个兄弟类AB继承自同一父类,AB甚至没有父子关系,当A类作为类型形参,B类的对象更不能写到A类的泛型中。

例如传进来的list是个List<Dog>,方法中又去add(new Cat())会导致类型混乱,因为虽然Cat和Dog都继承自Animal但是Cat不是Dog的子类,会破坏List<Dog>的类型一致性。再例如传进来的是个List<Dog>,方法中又去add(new Animal())也会导致类型混乱,所以extends修饰的泛型禁止写入任何一个非null实例

public class Demo {    public static void test(List<? extends Animal> list) {        Animal animal = list.get(1); //获取返回值时,由父类Animal接收        list.add(new Cat()); //编译出错❌                list.add(new YingDuan()); //编译出错❌                list.add(new Dog()); //编译出错❌        list.add(new Animal()); //同样编译出错❌    }}

再来看super,super和extends的情况会略有不同,super代表限制类型为某种类型及父类,设置的是类型的下限,保证传入的泛型类型不能低于某个类型,super修饰的泛型可读,但只能读出为Object,可写,但只能写入对应类型及其子类,例如? super Cat修饰的变量只允许赋值Cat类及其子类的对象,因为Cat类及其子类的对象肯定可以被Cat类及其父类的引用指向

public class Demo{    public static void test(List<? super Cat> list) {        Object object = list.get(1);        list.add(new Animal()); //编译出错❌        list.add(new Cat()); //编译通过✅        list.add(new YingDuan()); //编译通过✅        list.add(new BuOu()); //编译通过✅        list.add(null); //编译通过✅        list.add(new Dog()); //编译出错❌    }}

举例来讲的话,可以传给List<? super Cat> list的不是“List<猫>”就是“List<动物>”,所以首先不能add“动物”进去,因为有可能传进来的是“List<猫>”,只有“动物”包含“猫”,没有“猫”包含“动物”。同理,不能add“狗”进去,因为“狗”属于“动物”但不属于“猫”(废话)。所以只有“猫”和“英短”以及“布偶”能add进去,因为无论传进来的是“List<猫>”还是“List<动物>”,“英短”和“布偶”既直接继承自“猫”,也间接继承自“动物”,而“猫”本身就能添加进去

所以只有类及其子类(猫、英短、布偶)可写入super修饰的泛型是因为这样符合程序里面类的继承关系,不会导致泛型中类型混乱。说白了就是:存放“动物”的List,存一只“猫”进去也行,存放“猫”的List,存进去“英短”以及“布偶”逻辑上都是正确的,都可以实现父类引用指向子类对象而不是颠倒过来。

上面例子中,List是一个带有泛型,但是泛型参数没有类型限制的类,如果定义一个泛型类,并限制类型参数的范围,该怎样和类型通配符搭配使用呢

此处定义一个限制类型参数的泛型类Pet,指定类型上限是Animal

public class Pet<T extends Animal> {    private T thing;    public T get() {        return thing;    }    public void set(T t) {        thing = t;    }}

使用类型通配符?时,类型参数需严格按照定义泛型时指定的上限,除了读取时返回的都是上限Animal类型,其他的和不加类型限制的泛型类没有区别,都是可读不可写,extends和super修饰的类型通配符也类似,可直接看结论:

public static void main(String[] args) {    Pet<?> pet = new Pet<Object>(); //编译出错❌    Pet<?> pet2 = new Pet<>();    pet2.set(new Animal()); //编译出错❌    Animal animal = pet2.get();}
public static void main(String[] args) {    Pet<? extends Cat> pet = new Pet<YingDuan>();    pet.set(new Cat()); //编译出错❌    Cat cat = pet.get();}
public static void main(String[] args) {    Pet<? super Cat> pet1 = new Pet<Animal>();        Pet<? super Dog> pet2 = new Pet<>();    Pet<? super Cat> pet3 = new Pet<Object>(); //编译出错❌    Animal animal1 = pet1.get();    Animal animal2 = pet2.get();}

💡可直接记住结论:
? extends T → “只能读(读出是T或类型上限)不能写”
? super T → “能写(T和T的子类),读出来只能是Object或者类型上限”

类型通配符和类型参数的区别:

用法位置意义是否允许
? extends Cat通配符接受某类或子类(只读)✅ 允许
? super Cat通配符接受某类或父类(只写)✅ 允许
T extends Cat类型参数限制上界,T至少是某类✅ 允许
T super Cat类型参数企图限制下界(但 Java 不支持)❌ 不允许
  •  

Java8的新特性

一、Lambda表达式

  lambda表达式是Java8的重要更新,lambda表达式可以用更简洁的代码来创建一个只有一个抽象方法的接口(函数式接口)的实例,从而更简单的创建匿名内部类的对象。

1.1 语法和使用

  lambda表达式的基本语法是形参列表(可以省略类型)箭头,以及代码块,例如() -> {},或者(x, y) -> {},如果只有一个参数,那么小括号()可以省略,如果代码块只有一条语句,那么代码块的花括号{}可一并省略,如果代码块内只有一处return,那么return也可一并省略。

例: TreeSet类的构造器需要传进去一个Comparator的匿名类对象进去,来进行排序,所以程序实现了一个匿名内部类来封装处理行为,而且不得不用匿名内部类的语法来封装对象。

@Testpublic void test() {    TreeSet<Integer> treeSet = new TreeSet<>(new Comparator<Integer>() {        @Override        public int compare(Integer o1, Integer o2) {            return Integer.compare(o1, o2);        }    });    treeSet.add(20);    treeSet.add(78);    treeSet.add(-98);    System.out.println(treeSet);}

Comparator接口是一个函数式接口,因此完全可以使用lambda表达式来简化创建匿名内部类对象,因此上面代码可以修改成这样

@Testpublic void test() {    TreeSet<Integer> treeSet = new TreeSet<>((Integer x, Integer y) -> {        return x.compareTo(y);    });    treeSet.add(20);    treeSet.add(78);    treeSet.add(-98);    System.out.println(treeSet);}

进一步简化: 参数类型可以省略,如果代码块只有一条语句,那么代码块的花括号{}可一并省略,如果代码块内只有一处return,那么return也可一并省略

@Testpublic void test() {    TreeSet<Integer> treeSet = new TreeSet<>((x, y) -> x.compareTo(y));    treeSet.add(20);    treeSet.add(78);    treeSet.add(-98);    System.out.println(treeSet);}

  逻辑与上面代码是完全相同的,只是不再需要new Xxx() {}这种繁琐的语法,不需要指出重写方法的名字,也不需要给出重写方法的返回值类型,只需要给出重写方法的括号以及括号内的形参变量即可,用lambda表达式的代码块代替掉匿名内部类抽象方法的方法体,lambda表达式在这里就像是一个匿名方法。

1.2 方法引用和构造器引用

  前面说过如果花括号只有一条代码,便可以省略花括号,不仅如此,还可以使用方法引用和构造器引用,使得lambda表达式变得再简洁一些,方法引用和构造器引用的语法是两个英文冒号::,支持以下使用方式

种类语法说明lambda表达式写法
类方法类名::类方法抽象方法全部参数传给该类某个方法作为参数(a,b,…) -> 类名.类方法(a,b,…)
特定对象实例方法特定对象::实例方法抽象方法全部参数传给该方法作为参数(a,b,…) -> 特定对象.实例方法(a,b,…)
某类对象实例方法类名::实例方法抽象方法第一个参数作为调用者,后面的参数全部传给该方法作为参数(a,b,c,…) -> a.实例方法(b,c,…)
构造器类名::new抽象方法全部参数传给该构造器作为参数(a,b,…) -> new 类名(a,b,…)

例: 类名::类方法

@FunctionalInterfaceinterface Convert {    Integer fun(String s);}@Testpublic void test8() {    Convert convert =  from -> Integer.valueOf(from);    System.out.println(convert.fun("150") + 1);}@Testpublic void test9() {    Convert convert = Integer::valueOf;    System.out.println(convert.fun("150") + 1);}

例: 特定对象::实例方法

@FunctionalInterfaceinterface Convert {    Integer fun(String s);}@Testpublic void test8() {    Convert convert = from -> "liuzijian.com".indexOf(from);    System.out.println(convert.fun("zi"));}@Testpublic void test9() {    Convert convert = "liuzijian.com"::indexOf;    System.out.println(convert.fun("zi"));}

例: 类名::实例方法

@FunctionalInterfaceinterface Fun {    String test(String a, int b, int c);}@Testpublic void test8() {    Fun fun = (a, b, c) -> a.substring(b, c);    String s = fun.test("abcdefghi", 3, 5);    System.out.println(s);}@Testpublic void test9() {    Fun fun = String::substring;    String s = fun.test("abcdefghi", 3, 5);    System.out.println(s);}

例: 类名::new

@FunctionalInterfaceinterface Fun {    BigDecimal test(String n);}@Testpublic void test8() {    Fun fun = (n) -> new BigDecimal(n);    BigDecimal b = fun.test("45.64");    System.out.println(b);}@Testpublic void test9() {    Fun fun = BigDecimal::new;    BigDecimal b = fun.test("45.64");    System.out.println(b);}

二、函数式接口

  在Java8中,引入了函数式接口的概念,函数式接口是一个只有一个抽象方法的接口,通常用于Lambda表达式和方法引用,函数式接口可以有多个默认方法静态方法,但是必须只有一个抽象方法

2.1 定义

@FunctionalInterfacepublic interface MyPredicate<T> {        boolean fun(T obj);        default void other() {        System.out.println("hello world");    }    static void staticMethod() {        System.out.println("static method");    }}

  @FunctionalInterface注解:这是一个可选的注解,它可以帮助编译器在编译时检查接口是否符合函数式接口的要求,即是否只有一个抽象方法,如不符合还加这个注解,会导致编译器报错。

2.2 使用

编写一个实体类Employee

@Data@AllArgsConstructor@NoArgsConstructor@ToStringpublic class Employee {    private String name;    private Double salary;    private Integer age;    public Employee(Integer age) {        this.age = age;    }    public Employee(Integer age, String name) {        this.age = age;        this.name = name;    }}

新增一个按条件过滤的方法filter,将List<Employee>作为第一个参数,函数式接口MyPredicate<Employee>作为第二个参数传进filter()方法,方法体内循环将每个Employee对象一一作为参数传入接口的抽象方法fun()中,并调用,根据抽象方法运行后得到的布尔值判断是否过滤掉。

private List<Employee> filter(List<Employee>employees, MyPredicate<Employee> predicate) {    List<Employee>list = new ArrayList<>();    for (Employee e : employees) {        if (predicate.fun(e)) {            list.add(e);        }    }    return list;}

声明一个员工集合employees,插入5个对象,然后调用filter()方法,将employees作为第一个参数传入,然后直接new一个实现MyPredicate接口抽象方法的匿名内部类作为第二个参数传入,这样一来,调用时既告诉了目标方法filter()要处理的数据是employees,也一并将数据的具体处理规则obj.getAge() > 16告诉了目标方法,调用同一个方法可以有无数种处理数据的策略,这个实际上就是一种典型的策略模式,实际上Java8已经为我们写好了一种策略模式的函数式接口。

private List<Employee> employees = Arrays.asList(    new Employee("soo", 8547.322, 17),    new Employee("lili", 1000D, 15),    new Employee("王萌", 2154D, 16),    new Employee("张帆", 8547.322, 22),    new Employee("goog", 353D, 12));@Testpublic void test3() {    List<Employee>list = filter(employees, new MyPredicate<Employee>() {        @Override        public boolean fun(Employee obj) {            return obj.getAge() > 16;        }    });    System.out.println(list);}

Java8中,通过将策略接口实现简写为Lambda表达式的方式,可以使得语法显得更加简洁

List<Employee>list2 = filter(employees, (e) -> e.getAge() < 16);

2.3 内置的函数式接口

Java8提供了一些预定义的函数式接口,位于java.util.function包中

  • java.util.function.Consumer 消费
  • java.util.function.Supplier 供给
  • java.util.function.Function 函数
  • java.util.function.Predicate 断言
  • java.util.function.BinaryOperator 不常用
  • java.util.function.UnaryOperator 不常用

编写4个将函数式接口作为参数的方法

private void testConsumer(String str, Consumer<String>consumer) {    consumer.accept(str);}private String testSupplier(Supplier<String>supplier) {    return supplier.get();}private Integer testFunction(String str, Function<String, Integer>function) {    return function.apply(str);}private boolean testPredicate(String str, Predicate<String>predicate) {     return predicate.test(str); }

分别调用这些方法,按照业务逻辑通过匿名内部类的lambda表达式写法实现函数式接口的抽象方法,作为参数传入

@Testpublic void test4() {    testConsumer("hello lambda", (x) -> System.out.println(x));    String str = testSupplier(() -> { return "hello world"; });    System.out.println(str);    Integer integer = testFunction("66", (x) -> Integer.valueOf(x));    System.out.println(integer);    boolean b = testPredicate("hello", (e) -> e.equals("hello"));    System.out.println(b);}

得到运行结果

hello lambdahello world66true

还可以通过lambda表达式的方法引用和构造器引用将调用修改的更简洁一些

@Testpublic void test2() {    testConsumer("hello lambda", System.out::println);    Integer integer = testFunction("66", Integer::valueOf);}

三、Stream API

  Stream是Java8引入的一个新特性,是一个数据流,它提供了一种声明性的方法来处理集合、数组等数据源中的数据,可以更简洁、函数式的方式进行数据处理,它不会改变数据源本身,而是返回一个新的Stream或者是最终的结果。

Java8中引进的常见流式API包括:

  • java.util.stream.Stream
  • java.util.stream.LongStream
  • java.util.stream.IntStream
  • java.util.stream.DoubleStream

其中java.util.stream.Stream是个通用的流接口,以外的几种则代表流的元素类型为longintdouble

  Stream操作是延迟执行的,这意味着它们会等到需要结果时在执行,Stream操作可以被链式调用,并且一般分为两类操作:中间操作和终止操作

3.1 创建Stream

从集合类型的stream()方法创建

List<String> list = new ArrayList<>();Stream<String> stream = list.stream();

从数组创建

Employee[] employees = new Employee[10];Stream<Employee> employeeStream = Arrays.stream(employees);

通过Stream的静态方法创建流

Employee[] employees = new Employee[10];Stream<Employee> employeeStream1 = Stream.of(employees);

迭代创建无限流,根据种子和消费接口

Stream.iterate(10, (x) -> x + 2)      .limit(10)      .forEach(System.out::println);

随机数

Stream.generate(Math::random)       .limit(20)       .forEach(System.out::println);

通过builder()创建一个int流

@Testpublic void test5() {        IntStream intStream = IntStream.builder()            .add(1)            .add(2)            .add(3)            .add(4).build();    // 下面的聚集方法每次只能执行一行    System.out.println(intStream.max().getAsInt());    //System.out.println(intStream.min().getAsInt());    //System.out.println(intStream.sum());    //System.out.println(intStream.count());    //System.out.println(intStream.average());}

3.2 Stream的操作

  Stream的操作包含中间操作终止操作,在《疯狂Java讲义》一书中,李刚老师也将其称为中间方法和末端方法,中间操作允许流保持打开状态,并允许直接调用后续方法,中间方法返回值是另一个流。终止方法是对流的最终操作,在对某个流执行终止操作后,整个流将不再可用。

常见中间操作

  • filter(Predicate predicate) 过滤流中不符合predicate的元素
  • mapToXxx(ToXxxFunction mapper) 使用ToXxxFunction对流中的元素进行一对一转换。返回的新流中包含ToXxxFunction转换生成的所有元素
  • peek(Consumer action) 依次对每个元素执行了一些操作,返回的流与原有的流包含相同的元素(多用于调试)
  • distinct() 排除流中所有重复元素,判断标准是equals()返回true
  • sorted() 该方法用于排序
  • sorted(Comparator comparator) 该方法用于根据自定义规则排序
  • limit(long maxSize) 截取流中的前maxSize个元素
  • skip(long n) 跳过流中的前n个元素
  • map(Function mapper) 映射每个元素为其他形式
  • flatMap(Function mapper) 将每个元素转换为一个流,然后将多个流合并成一个流

常见终止操作

  • collect(Collector collector) 将流中的元素收集到一个容器中(如集合、列表、映射等)
  • count() 返回流中的元素个数
  • forEach(Consumer action) 遍历流中所有元素,对每个元素执行action
  • toArray() 将流中所有元素转换为一个数组
  • reduce() 通过某种操作来合并流中的元素
  • min() 返回流中元素的最小值
  • max() 返回流中元素的最大值
  • anyMatch(Predicate predicate) 如果流中任一元素匹配给定条件,返回 true
  • allMatch(Predicate predicate) 如果流中所有元素都匹配给定条件,返回 true
  • noneMatch(Predicate predicate) 如果流中没有任何元素匹配给定条件,返回 true
  • findFirst() 返回流中的第一个元素
  • findAny() 返回流中的任意一个元素

中间操作返回的是一个新的Stream,并且中间操作是惰性执行的,直到终止操作才触发计算

下面是用例:

数据:

private List<Employee> employees = Arrays.asList(    new Employee("soo", 8547.322, 17),    new Employee("lili", 1000D, 18),    new Employee("王萌", 2154D, 16),    new Employee("张帆", 8547.322, 22),    new Employee("张帆", 8547.322, 22),    new Employee("张帆", 8547.322, 22),    new Employee("goog", 353D, 12));private List<String> list = Arrays.asList("aaa", "bbb", "ccc", "ddd", "eee");

例: stream+limit筛选切片,满足e.getAge() > 16条件的对象达到两个时就停止迭代,而不是迭代一遍后返回前两个,提高效率。终止操作forEach()不触发,中间操作filter()limit()也不会得到执行。

@Testpublic void test() {    employees.stream()    .filter((e) -> {        // 中间操作        System.out.println("中间操作");         return e.getAge() > 16;    })    .limit(2) //中间操作    .forEach(System.out::println); //终止操作    }

运行结果:

中间操作Employee(name=soo, salary=8547.322, age=17)中间操作Employee(name=lili, salary=1000.0, age=18)

例: 跳过流中的前n个元素,与limit相反

@Testpublic void test2() {    employees.stream().skip(2).forEach(System.out::println); }

运行结果

Employee(name=王萌, salary=2154.0, age=16)Employee(name=张帆, salary=8547.322, age=22)Employee(name=张帆, salary=8547.322, age=22)Employee(name=张帆, salary=8547.322, age=22)Employee(name=goog, salary=353.0, age=12)

例: 去重,根据equalshashCode,本例去重成功的前提是Employee类需要重写equalshashCode

//Employee类重写equals hashCode@Overridepublic boolean equals(Object o) {    if (this == o) return true;    if (o == null || getClass() != o.getClass()) return false;    Employee employee = (Employee) o;    if (!Objects.equals(name, employee.name)) return false;    if (!Objects.equals(salary, employee.salary)) return false;    return Objects.equals(age, employee.age);}@Overridepublic int hashCode() {    int result = name != null ? name.hashCode() : 0;    result = 31 * result + (salary != null ? salary.hashCode() : 0);    result = 31 * result + (age != null ? age.hashCode() : 0);    return result;}
@Testpublic void test3() {    employees.stream().distinct().forEach(System.out::println);}

运行结果

Employee(name=soo, salary=8547.322, age=17)Employee(name=lili, salary=1000.0, age=18)Employee(name=王萌, salary=2154.0, age=16)Employee(name=张帆, salary=8547.322, age=22)Employee(name=goog, salary=353.0, age=12)

例: flatMap将流中每个值,都转换成另一个流,然后把所有流连接成一个

下面程序先将"aaa"转换成由3个'a'构成的List<Character>,再将List<Character>转换为Stream<Character>"bbb""ccc"同理,最后将转换成的三个Stream<Character>合并为含有9个元素的Stream<Character>,再调用结束方法collect()将其变为含有9个元素的List<Character>,依次打印输出。

@Testpublic void test5() {    List<String> list = Arrays.asList("aaa", "bbb", "ccc");        Function<String, Stream<Character>> function = (e) -> {        List<Character> characters = new ArrayList<>();        for (char c : e.toCharArray()) {            characters.add(c);        }        return characters.stream();    };    List<Character> collect = list.stream()                .flatMap(function)                .collect(Collectors.toList());    collect.forEach(System.out::println);}

运行结果

aaabbbccc

例: map映射,得到流中的一个元素,处理组成新的流

@Testpublic void test4() {    employees.stream().map((e) -> e.getName()).forEach(System.out::println);}

运行结果

soolili王萌张帆张帆张帆goog

例: sorted()自然排序

@Testpublic void test() {    list.stream().sorted().forEach(System.out::println);}

运行结果

aaabbbcccdddeee

例: sorted(Comparator c)定制排序

@Testpublic void test2() {    employees.stream()            .sorted((e1, e2) -> e1.getAge() - e2.getAge())            .forEach(System.out::println);}

运行结果

Employee(name=goog, salary=353.0, age=12)Employee(name=王萌, salary=2154.0, age=16)Employee(name=soo, salary=8547.322, age=17)Employee(name=lili, salary=1000.0, age=18)Employee(name=张帆, salary=8547.322, age=22)Employee(name=张帆, salary=8547.322, age=22)Employee(name=张帆, salary=8547.322, age=22)

例: xxxMatchfindXXXcount()max()min()

@Testpublic void test3() {    boolean b = employees.stream().allMatch((e) -> e.getAge() > 10);    System.out.println(b);    b = employees.stream().anyMatch((e) -> e.getAge() > 100);    System.out.println(b);    b = employees.stream().noneMatch((e) -> e.getAge() > 100);    System.out.println(b);    Optional<Employee> first = employees.stream().findFirst();    System.out.println(first.get());    Optional<Employee> any = employees.stream().findAny();    System.out.println(any.get());    long count = employees.stream().count();    System.out.println(count);    Optional<Employee> max = employees.stream()            .max(Comparator.comparingInt(Employee::getAge));    System.out.println(max.get());    Optional<Integer> maxAge = employees.stream()            .map(Employee::getAge)            .max(Integer::compare);    System.out.println(maxAge.get());}

运行结果

truefalsetrueEmployee(name=soo, salary=8547.322, age=17)Employee(name=soo, salary=8547.322, age=17)7Employee(name=张帆, salary=8547.322, age=22)22

例: reduce() 将流中元素反复结合,得到新值,先将起始值作为x,从流中取出一个值作为y

@Testpublic void test() {    List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9);    Integer sum = list.stream().reduce(0, Integer::sum);    System.out.println(sum);        Optional<Double> reduce = employees.stream().map(Employee::getSalary)            .reduce(Double::sum);    System.out.println(reduce.get());}

运行结果

4537696.288

例: .collect(Collectors.toList()) .collect(Collectors.toCollection()) 收集为集合

@Testpublic void test2() {    List<String> names = employees.stream()            .map(Employee::getName)            .collect(Collectors.toList());            //.collect(Collectors.toCollection(LinkedList::new))        names.forEach(System.out::println);}

运行结果

soolili王萌张帆张帆张帆goog

例: collect(Collectors.averagingDouble()) 求平均值

@Testpublic void test5() {    Double avg = employees.stream()            .collect(Collectors.averagingDouble(Employee::getSalary));    System.out.println(avg);}

例: collect(Collectors.joining()) 用相同的内容连接多个字符串,非常适合SQL等参数拼接场景

@Testpublic void test() {    String collect = list.stream().collect(Collectors.joining(","));    System.out.println(collect);}

运行结果

aaa,bbb,ccc,ddd,eee

例: 收集为Map Collectors.groupingBy()

将相同航司和票号的票和行李的价格加在一起

public class TestGroupBy {        private List<Detail> details = new ArrayList<>();        @Before    public void mock() {        details.add(new Detail(1, "001", "123456789", new BigDecimal("120.00")));        details.add(new Detail(2, "001", "123456789", new BigDecimal("99.32")));                details.add(new Detail(3, "003", "333222111", new BigDecimal("27.32")));        details.add(new Detail(4, "003", "333222111", new BigDecimal("36.00")));        details.add(new Detail(5, "003", "123456789", new BigDecimal("48.32")));                details.add(new Detail(6, "101", "123456789", new BigDecimal("53.32")));        details.add(new Detail(7, "101", "123456789", new BigDecimal("10.32")));        details.add(new Detail(8, "102", "333222111", new BigDecimal("3.32")));        details.add(new Detail(9, "103", "123456789", new BigDecimal("9.00")));        details.add(new Detail(10, "103", "123456789", new BigDecimal("12.12")));            }        @Test    public void test() {        Map<String, List<Detail>> groupByAir = details.parallelStream().collect(Collectors.groupingBy(Detail::getAir));        groupByAir.forEach((air, sameAirs) -> {            Map<String, List<Detail>> groupByDoc = sameAirs.parallelStream().collect(Collectors.groupingBy(Detail::getDocument));            groupByDoc.forEach((doc, sameDocs) -> {                Optional<BigDecimal> reduce = sameDocs.parallelStream().map(Detail::getPrice).reduce(BigDecimal::add);                reduce.ifPresent(e -> {                    System.out.println(air + " "+ doc + " " + e);                });            });        });    }        @Data    @AllArgsConstructor    public static class Detail {        /**         * ID         */        private Integer id;        /**         *航司编码         */        private String air;        /**         *票号         */        private String document;        /**         *机票价格         */        private BigDecimal price;    }    }

运行结果

001 123456789 219.32101 123456789 63.64102 333222111 3.32003 333222111 63.32003 123456789 48.32103 123456789 21.12

例: peek() 实时打印调试看流处理的每一步里面的元素是什么样的

@Testpublic void test6() {    List<String> names = Arrays.asList("liuzijian", "liutongtong", "zhaoying", "wangwendi");    names.stream()            .filter(name -> name.startsWith("liu"))            .peek(name -> System.out.println("过滤后: " + name))            .map(String::toUpperCase)            .peek(name -> System.out.println("变成大写后: " + name))            .collect(Collectors.toList());}

运行结果

过滤后: liuzijian变成大写后: LIUZIJIAN过滤后: liutongtong变成大写后: LIUTONGTONG

3.3 并行流和串行流

  在Java8中,流可以分为并行流和串行流,这两者的主要区别在于数据处理的方式。

  Java8的stream()默认是串行流,即数据按顺序一个一个处理,可以通过parallel()方法将串行流转换为并行流,或者直接在流创建时使用parallelStream()

  并行流底层是基于Java的ForkJoinPool实现的,这个池管理多个线程来并行处理数据,流的元素会被拆分成多个子任务并分配到不同的线程中处理,最后将结果合并。

  并行流本身并不保证顺序。但是,在某些操作中,比如Collectors.joining(),它会保证合并结果的顺序,这通过收集器的设计来实现。

例: 并行流遍历打印

@Testpublic void test() {    list.parallelStream().forEach(System.out::println);}

运行结果

ccceeedddbbbaaa

例: 并行流多线程将0加到100

LongStream.rangeClosed(0, 100000000000L)创建了从0100000000000L之间所有整数的流,然后reduce()会先将流分成多个子流,每个子流计算局部的和,在不同的线程中进行,每个线程分别计算一部分和,计算完成后,再将各个子任务计算的结果合并,得到计算结果932356074711512064

public static void main(String[] args) {    long reduce = LongStream.rangeClosed(0, 100000000000L)            .parallel() // 转换为并行流,底层是fork-join            .reduce(0, Long::sum);    System.out.println(reduce);}

以上就是Java8 StreamAPI的全部内容。

四、接口的默认方法

  Java8前的接口,只能有两个成员,全局静态常量和抽象方法,Java8引入了接口的默认方法和静态方法作为新特性,它们的引入是为了增强接口的功能,特别是在接口的扩展性和灵活性方面。

  接口中的默认方法,使用default修饰符修饰,可以带有实现,实现类可以直接继承使用,实现类可以选择重写默认方法,也可以直接使用。

  接口中的静态方法只能通过接口名调用,不能通过接口的实现类或实例调用,为接口提供相关的工具性功能,而不需要依赖具体的实现类,静态方法不会被实现类继承,也不能被实现类重写。

4.1 接口的默认方法和静态方法

编写一个接口test.testinterface.MyInterface,拥有两个默认方法test()hello()和一个静态方法helloworld()

package test.testinterface;public interface MyInterface {        default String test() {        System.out.println("default");        return "default";    }    default void hello() {        System.out.println("my interface");    }        static void helloworld() {        System.out.println("hello java8!!!");    }}

编写一个类test.testinterface.SubClass,实现接口MyInterface

package test.testinterface;public class SubClass  implements MyInterface {    public static void main(String[] args) {        SubClass subClass = new SubClass();          subClass.hello();        MyInterface.helloworld();    }}

不实现接口里面的hello()方法也可以直接调用默认方法hello(),而且可以通过接口名直接调用接口的静态方法helloworld(),程序输出:

my interfacehello java8!!!

4.2 方法冲突

编写另一个接口test.testinterface.OtherInterface,并实现一个默认方法hello

package test.testinterface;public interface OtherInterface {    default void hello() {        System.out.println("other interface");    }}

令类test.testinterface.SubClass再实现一个接口OtherInterface,该接口含有和接口MyInterface一样定义的default方法hello(),就产生了接口冲突,当实现的多个接口中有相同签名的默认方法时,子类必须显式重写冲突的方法hello(),最终程序输出结果:”sub hello!”

package test.testinterface;public class SubClass implements MyInterface, OtherInterface {    /**     * 多实现方法冲突,实现类必须实现     **/    @Override    public void hello() {        System.out.println("sub hello!");    }    public static void main(String[] args) {        SubClass subClass = new SubClass();        subClass.hello();    }}

4.3 类优先

编写一个类test.testinterface.MyClass,里面有一个方法String test(),并让SubClass类继承它,并执行subClass.test();,得到输出结果:”class”,但是SubClass实现的接口MyInterface里面也有个方法String test(),却没有被执行,而是执行了类里面的方法,说明类优先,如果类或其父类中已经提供了方法实现,则优先使用类的实现,而不是接口的默认方法。

package test.testinterface;public class MyClass  {    public String test() {        System.out.println("class");        return "class";    }}
package test.testinterface;public class SubClass extends MyClass implements MyInterface, OtherInterface {    // 多实现方法冲突,实现类必须实现    @Override    public void hello() {        System.out.println("sub hello!");    }    public static void main(String[] args) {        SubClass subClass = new SubClass();        // 类优先原则, 继承类的方法        subClass.test();    }}

五、新的日期和时间API (java.time)

5.1 旧API的线程安全问题

  旧的日期时间工具类java.text.SimpleDateFormat存在线程安全问题,例如SimpleDateFormat线程不安全,内部依赖一个Calendar实例来解析和格式化日期,而Calendar是线程不安全的,多线程格式化会并发更新Calendar状态会导致出现异常。

以下代码使用100个线程并发调用一个format对象进行日期解析操作,会导致出现错误。

package test.time;import java.text.SimpleDateFormat;public class Test1 {    public static void main(String[] args)  {                SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd");        Runnable r = new Runnable() {            @Override            public void run() {                try {                    System.out.println(format.parse("20191231"));                } catch (Exception e) {                    throw new RuntimeException(e);                }            }        };        for (int i=0; i<100; i++) {            new Thread(r, "t"+i).start();        }    }}

可以采取同步块,线程单独持有format对象,以及线程池内使用ThreadLocal的办法解决。采用同步代码块时,只能有一个线程执行parse方法,可以避免线程安全问题。

package test.time;import java.text.SimpleDateFormat;public class Test1 {    public static void main(String[] args)  {                SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd");        Runnable r = new Runnable() {            @Override            public void run() {                synchronized (format) {                    try {                        System.out.println(format.parse("20191231"));                    } catch (Exception e) {                        throw new RuntimeException(e);                    }                }            }        };        for (int i=0; i<100; i++) {            new Thread(r, "t"+i).start();        }    }}

采用线程独自持有format对象的方法解决,每个线程执行时创建一个format对象,每个线程单独持有,防止线程安全问题。

package test.time;import java.text.SimpleDateFormat;public class Test1 {    public static void main(String[] args)  {        Runnable r = new Runnable() {            @Override            public void run() {                try {                    System.out.println(new SimpleDateFormat("yyyyMMdd").parse("20191231"));                } catch (Exception e) {                    throw new RuntimeException(e);                }            }        };        for (int i=0; i<100; i++) {            new Thread(r, "t"+i).start();        }    }}

线程池+ThreadLocal,10个线程同时派发100个格式化任务,可以为每个线程绑定一个format对象,各自使用,也可以避免线程安全问题。

package test.time;import java.text.SimpleDateFormat;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class Test1 {        public static void main(String[] args)  {        ExecutorService executorService = Executors.newFixedThreadPool(10);        ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));        Runnable runnable = new Runnable() {            @Override            public void run() {                try {                    System.out.println(threadLocal.get().parse("20191231"));                }                catch (Exception e) {                    throw new RuntimeException(e);                }            }        };        for (int i=0; i<100; i++) {            executorService.submit(runnable, "t"+i);        }        executorService.shutdown();    }}

5.2 新的日期时间API

  Java 8通过发布新的Date-TimeAPI(JSR310)进一步加强了对日期与时间的处理。

  首先,在旧版的Java中,日期时间API存在诸多问题,首先java.util.Date是非线程安全的,所有的日期类都是可变的。

  其次,Java的日期/时间类的定义并不一致,在java.utiljava.sql的包中都有日期类,负责格式化和解析的类又在java.text包中定义。java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,而且放进sql包下并不合理。

  而且,无法更好的处理时区,日期类不能国际化,没有时区支持,因此Java引入了java.util.Calendarjava.util.TimeZone,但是它们仍然存在一样的问题。

于是Java8引入了新的日期时间API,位于java.time包下,该包下有几个重要的类:

  • java.time.Instant 时间戳
  • java.time.Duration 时间差
  • java.time.LocalDate 只包含日期,例如2011-07-11
  • java.time.LocalTime 只包含时间,例如09:00:01
  • java.time.LocalDateTime 同时包含日期和时间,例如2024-11-30 04:09:45
  • java.time.Period 时间段
  • java.time.OffsetDateTime 带有时区偏移量的日期和时间,是LocalDateTime和ZoneOffset的结合体,更适用于需要精确到时间和偏移量的场景,尤其当你关心的只是某个时间点相对于 UTC 的偏移。例如,在处理需要表示时间差(例如时间戳、系统日志等)时,OffsetDateTime 比较合适。
  • java.time.ZoneOffset 时区偏移量,比如+8:00
  • java.time.ZonedDateTime 带有时区的日期和时间,是LocalDateTimeZoneId的组合,ZonedDateTime更适用于需要考虑时区历史和夏令时等复杂问题的场景。例如,如果你需要表示某个特定时区(如America/New_York)的时间,并且要处理夏令时,ZonedDateTime会更加准确
  • java.time.Clock 时钟
package test.time;import org.junit.Test;import java.time.*;import java.time.format.DateTimeFormatter;import java.time.format.FormatStyle;import java.time.temporal.*;import java.util.Date;public class Test4 {    /**     * java8 API获取当前时间     */    @Test    public void current() {        Instant instant = Instant.now();        LocalDate localDate = LocalDate.now();        LocalTime localTime = LocalTime.now();        LocalDateTime localDateTime = LocalDateTime.now();        ZonedDateTime zonedDateTime = ZonedDateTime.now();        System.out.println(instant);        System.out.println(localDate);        System.out.println(localTime);        System.out.println(localDateTime);        System.out.println(zonedDateTime);    }        /**     * Instant的常见方法     */    @Test    public void testInstant() {        //通过Instant获取当前时间戳,格林威治时间        Instant now = Instant.now();        System.out.println(now);        //添加时区,转换为带时区的时间:OffsetDateTime        OffsetDateTime us = now.atOffset(ZoneOffset.ofHours(-4));        System.out.println(us);//US        //设置偏移量        OffsetDateTime offsetDateTime = now.atOffset(ZoneOffset.ofHours(+8));        System.out.println(offsetDateTime);//CN        System.out.println(now.atOffset(ZoneOffset.ofHours(+9)));//JP        System.out.println(now.atOffset(ZoneOffset.ofHours(+10)));//AU        //根据给定的Unix时间戳(即自1970年1月1日00:00:00 UTC起的秒数)创建一个Instant对象        Instant instant = Instant.ofEpochSecond(1);//开始于1970        System.out.println(instant);        //设置时区        ZonedDateTime zonedDateTime = now.atZone(ZoneId.of("GMT+9"));        LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();        System.out.println(localDateTime);    }    /**     * LocalDateTime LocalDate LocalTime 的常见方法和使用     */    @Test    public void testLocalDateTime() {        // 获取当前时间        LocalDateTime now = LocalDateTime.now();        System.out.println(now);        //构造时间        LocalDateTime localDateTime = LocalDateTime.of(2019,8,8,12,23,50);        System.out.println(localDateTime);        //从LocalDate和LocalTime构造时间        System.out.println(LocalDateTime.of(LocalDate.now(), LocalTime.now()));        // 获取年月日时分秒        System.out.println(localDateTime.getYear());        System.out.println(localDateTime.getDayOfYear());        System.out.println(localDateTime.getDayOfMonth());        //星期        DayOfWeek dayOfWeek = localDateTime.getDayOfWeek();        System.out.println(dayOfWeek);        //当前时间的纳秒部分,表示这个时间点内的精细时间        System.out.println(localDateTime.getNano());        //时间计算        System.out.println(LocalDateTime.now().plusMonths(2));        System.out.println(LocalDateTime.now().minusYears(2));        System.out.println(LocalDateTime.now().plusHours(24));        System.out.println(LocalDateTime.now().plusNanos(500));        System.out.println(LocalDateTime.now().plusYears(2).plusMonths(8).plusDays(9));        // Period.of 用于创建一个表示特定时间间隔的Period对象        System.out.println(LocalDateTime.now().plus(Period.of(3, 5, 20))); ;        // ChronoUnit.DECADES代表十年        System.out.println(LocalDateTime.now().plus(3, ChronoUnit.DECADES)) ;        // 时间修改        System.out.println(LocalDateTime.now().withMonth(2));        System.out.println(LocalDateTime.now().withDayOfMonth(25));        System.out.println(LocalDateTime.now().withSecond(22));        System.out.println(LocalDateTime.now().with(ChronoField.DAY_OF_MONTH, 2));        System.out.println(LocalDateTime.now().with(ChronoField.MONTH_OF_YEAR, 8));        // LocalDate LocalTime        System.out.println(LocalDate.of(2020, 1, 19));        System.out.println(LocalDate.of(2020, Month.AUGUST, 19));        System.out.println(LocalDate.of(2020, Month.of(12), 19));        System.out.println(LocalTime.of(20, 0));        System.out.println(LocalDate.now().withMonth(8));        System.out.println(LocalDate.of(2020, Month.AUGUST, 19).plusDays(5));        System.out.println(LocalDate.of(2020, Month.of(12), 19));        System.out.println( LocalTime.of(20, 0).plusHours(8) );        // LocalDate的方法,判断当前年份是否为闰年        System.out.println(LocalDate.now().isLeapYear());    }    /**     * TemporalAdjusters 时间校正器     */    @Test    public void testTemporalAdjusters() {        // 下一个周四        LocalDateTime dateTime = LocalDateTime.now();        dateTime.with(TemporalAdjusters.next(DayOfWeek.THURSDAY));        System.out.println(dateTime);        dateTime.with(TemporalAdjusters.previous(DayOfWeek.THURSDAY));        System.out.println(dateTime);        dateTime.with(TemporalAdjusters.nextOrSame(DayOfWeek.THURSDAY));        System.out.println(dateTime);        dateTime.with(TemporalAdjusters.previousOrSame(DayOfWeek.THURSDAY));        System.out.println(dateTime);        System.out.println(LocalDate.now().with(TemporalAdjusters.nextOrSame(DayOfWeek.SATURDAY)));        // 获取月份第一天        System.out.println(LocalDate.now().with(TemporalAdjusters.firstDayOfMonth()));        System.out.println(LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth()));        // 自定义 计算下一个工作日        LocalDateTime nextWorkDay = LocalDateTime.now().with((e) -> {            LocalDateTime temp = LocalDateTime.from(e);            DayOfWeek dayOfWeek = temp.getDayOfWeek();            if (dayOfWeek.equals(DayOfWeek.FRIDAY)) {                return temp.plusDays(3);            } else if (dayOfWeek.equals(DayOfWeek.SATURDAY)) {                return temp.plusDays(2);            } else {                return temp.plusDays(1);            }        });        System.out.println(nextWorkDay);    }    public void test() {        System.out.println(Year.now());        System.out.println(YearMonth.now());        System.out.println(MonthDay.now());    }    /**     * 计算时间间隔:武汉封了多少天,多少小时     */    @Test    public void testChronoUnit() {        LocalDateTime from = LocalDateTime.of(2020, Month.JANUARY, 23, 10, 0,0);        LocalDateTime to = LocalDateTime.of(2020, Month.APRIL, 8, 0, 0,0);        long days = ChronoUnit.DAYS.between(from, to);        long hours = ChronoUnit.HOURS.between(from, to);        System.out.println( days );        System.out.println( hours );    }    /**     * 使用 TemporalQuery 来计算当前时间与一个指定时间点(2020年1月19日10:00:00)之间的小时差,     * 并将其作为 long 类型的值返回     */    @Test    public void testTemporalQuery() {        long l = LocalDateTime.now().query(new TemporalQuery<Long>() {            @Override            public Long queryFrom(TemporalAccessor temporal) {                LocalDateTime now = LocalDateTime.from(temporal);                LocalDateTime from = LocalDateTime.of(2020, Month.JANUARY, 19, 10, 0,0);                return ChronoUnit.HOURS.between(from, now);            }        });        System.out.println(l);    }    /**     * Duration类,只能计算时间差异     */    @Test    public void testDurationPeriod() {        LocalTime start = LocalTime.of(20, 0);        LocalTime end = LocalTime.of(21, 30);        // 时间间隔        Duration between = Duration.between(start, end);        System.out.println(between.toHours());        System.out.println(between.toMinutes());    }    /**     * 格式化 DateTimeFormatter     */    @Test    public void testDateTimeFormatter() {        DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME;        System.out.println(LocalDateTime.now().format(formatter));        LocalDate localDate = LocalDate.parse("2009-12-31", DateTimeFormatter.ofPattern("yyyy-MM-dd"));        System.out.println(localDate);        LocalDateTime localDateTime = LocalDateTime.parse("2009-12-31 01:01:02", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));        System.out.println(localDateTime);        // 2024年12月1日 星期日        System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)));        // 2024年12月1日        System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)));        // 24-12-1        System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)));        // 2024-12-1        System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)));    }    @Test    public void getAvailableZoneIds() {        // 当前系统时区        System.out.println(ZoneId.systemDefault());        // 打印java8中所有支持时区        ZoneId.getAvailableZoneIds().forEach(System.out::println);    }    /**     * OffsetDateTime     */    @Test    public void testOffsetDateTime() {        OffsetDateTime offsetDateTime = new Date().toInstant().atOffset(ZoneOffset.of("-4"));        System.out.println(offsetDateTime);        System.out.println(offsetDateTime.toLocalDateTime());        OffsetDateTime of = OffsetDateTime.of(LocalDateTime.now(), ZoneOffset.of("-4"));        System.out.println(of);    }    /**     * ZonedDateTime     */    @Test    public void testZonedDateTime() {        // 当前时间转换为东京时间是几时        ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));        System.out.println(zonedDateTime);        System.out.println(zonedDateTime.toLocalDateTime());        ZonedDateTime of = ZonedDateTime.of(LocalDateTime.now(), ZoneId.of("Asia/Tokyo"));        System.out.println(of);        // 为当前时间带上时区        ZonedDateTime tokyo = LocalDateTime.now().atZone(ZoneId.of("Asia/Tokyo"));        System.out.println(tokyo);        System.out.println(tokyo.toLocalDateTime());        // 将一个时区时间转换为同一时刻另一个时区时间        ZonedDateTime beijing = tokyo.withZoneSameInstant(ZoneId.of("GMT+8"));        System.out.println(beijing);        ZonedDateTime usa = LocalDateTime.now()                .atZone(ZoneId.systemDefault())                .withZoneSameInstant(ZoneId.of("GMT-4"));        System.out.println(usa);    }}

新API和旧的Date之前的互转

package test.time;import org.junit.Test;import java.sql.Timestamp;import java.time.*;import java.util.Calendar;import java.util.Date;public class Test5 {    /**     * 将 LocalDateTime 和系统默认时区结合,转换为 ZonedDateTime     * 再将 ZonedDateTime 转换为 Instant,这是一个包含 UTC 时间戳的对象。     * Date.from():将 Instant 转换为 java.util.Date 对象     */    @Test    public void toDate() {        LocalDateTime localDateTime = LocalDateTime.now();        Instant instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();        Date date = Date.from(instant);        System.out.println(date);    }    @Test    public void toLocalDateTime() {        Date date = new Date();        LocalDateTime dateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());        System.out.println(dateTime);    }    /**     * java.sql.Date 转换 LocalDateTime     */    @Test    public void sqlDate() {        java.sql.Date date = new java.sql.Date(System.currentTimeMillis());        LocalDate localDate = date.toLocalDate();        System.out.println(localDate);        Timestamp timestamp = new Timestamp(System.currentTimeMillis());        LocalDateTime localDateTime = timestamp.toLocalDateTime();        System.out.println(localDateTime);    }    /**     * Calendar 转换 LocalDateTime     */    @Test    public void calendarToLocalDateTime() {        Calendar calendar = Calendar.getInstance();        ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId());        System.out.println(zonedDateTime.toLocalDateTime());    }}

六、Optional

  java.util.Optional是一个容器类,用来表示可能包含或者不包含值的对象。它提供了一种优雅的方式来避免出现空指针,从而帮助开发者更安全、更清晰地处理可能为NULL的值。

6.1 创建

包装一个非空的值,如果传入的变量为null会直接抛出空指针异常,如果直接写死null进去,IDEA可能直接编译出错

Optional<String> optional = Optional.of("Hello, World!");

ofNullable方法允许填充一个可能为空的值进去

Optional<String> optional = Optional.ofNullable(null);

空的Optional对象

Optional<String> optional = Optional.empty();

6.2 检查

可以使用isPresent()方法判断

Optional<String> optional = Optional.empty();if (optional.isPresent()) {    System.out.println("Value: " + optional.get());} else {    System.out.println("No value present");}

还可以采用ifPresent()避免if显式调用

optional.ifPresent(value -> System.out.println("Value: " + value));

6.3 默认值

如果为空,提供一个默认值

Optional<String> optional = Optional.empty();String value = optional.orElse("Default Value");

还可以通过提供的Supplier函数式接口生成默认值

Optional<String> optional = Optional.empty();String value = optional.orElseGet(() -> "Generated Default Value");// optional.orElseGet(String::new);

如果值不存在,可以抛出自定义异常

Optional<String> optional = Optional.empty();String value = optional.orElseThrow(() -> new RuntimeException("Value is missing!"));

6.4 转换

map() 如果有值进行处理,并返回处理后的Optional对象,否则返回Optional.empty()

空值,不执行输出

Optional<String> optional = Optional.empty();Optional<String> upperCase = optional.map(String::toUpperCase);upperCase.ifPresent(System.out::println); 

非空,处理后返回新的Optional,输出:HELLO WORLD

Optional<String> optional = Optional.ofNullable("hello world");Optional<String> upperCase = optional.map(String::toUpperCase);upperCase.ifPresent(System.out::println); 

使用flatMap()进一步防止空指针异常,如果optional中的值为null,flatMap()直接返回Optional.empty(),否则,它返回一个包含e.getName()的Optional对象

Employee employee = new Employee();employee.setName("XXX");Optional<Employee> optional = Optional.ofNullable(employee);Optional<String> s = optional.flatMap((e) -> Optional.of(e.getName()));s.ifPresent(System.out::println);

七、重复注解 (Repeating Annotations)

1.首先创建一个容器注解,这个注解类型包含一个注解数组,存储多个相同类型的注解

package test.anno;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;import static java.lang.annotation.ElementType.CONSTRUCTOR;import static java.lang.annotation.ElementType.FIELD;import static java.lang.annotation.ElementType.LOCAL_VARIABLE;import static java.lang.annotation.ElementType.METHOD;import static java.lang.annotation.ElementType.PARAMETER;import static java.lang.annotation.ElementType.TYPE;@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})@Retention(RetentionPolicy.RUNTIME)public @interface MyAnnotations {    MyAnnotation[] value();}

2.定义一个重复注解,并使用@Repeatable标记

package test.anno;import java.lang.annotation.Repeatable;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;import static java.lang.annotation.ElementType.CONSTRUCTOR;import static java.lang.annotation.ElementType.FIELD;import static java.lang.annotation.ElementType.LOCAL_VARIABLE;import static java.lang.annotation.ElementType.METHOD;import static java.lang.annotation.ElementType.PARAMETER;import static java.lang.annotation.ElementType.TYPE;import static java.lang.annotation.ElementType.TYPE_PARAMETER; // 类型注解@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, TYPE_PARAMETER})@Retention(RetentionPolicy.RUNTIME)@Repeatable(MyAnnotations.class)public @interface MyAnnotation {    String value() default "hello world";}

3.测试,通过反射访问方法上的注解,由于MyAnnotation是重复注解,所以一个方法加上多个也不会语法报错,然后提取其中的多个MyAnnotation注解。

package test.anno;import java.lang.reflect.Method;import java.util.Arrays;public class TestAnnotation {        @MyAnnotation("hello")    @MyAnnotation("world")    public void test(String s) {            }    public static void main(String[] args) {        Class<TestAnnotation> clazz = TestAnnotation.class;        try {            Method method = clazz.getMethod("test", String.class);            MyAnnotation[] annotations = method.getAnnotationsByType(MyAnnotation.class);            Arrays.stream(annotations).map(MyAnnotation::value).forEach(System.out::println);        }         catch (Exception e) {            e.printStackTrace();        }                    }}
  •  

volatile作用分析

Java 内存模型 (JMM) 中的一个核心问题是线程对共享变量的可见性。在多线程环境中,每个线程都有自己的工作内存(即 CPU 缓存)。当一个线程修改了某个变量,其他线程并不能立即看到这个修改,因为每个线程可能都在使用自己工作内存中的缓存值。

volatile 的可见性机制:

  • 强制刷新主内存:当一个线程对 volatile 修饰的变量进行写操作时,修改后的值会立即被刷新到主内存中,而不是缓存在该线程的工作内存中。

  • 强制读取主内存:当一个线程对 volatile 修饰的变量进行读操作时,会直接从主内存中读取最新值,而不会从线程的工作内存中读取缓存的值。

  • 示例代码

public class TestVolatile {        static volatile boolean flag = false;        public static void main(String[] args) {                new Thread(() -> {            try {                Thread.sleep(2000);            } catch (InterruptedException e) {                e.printStackTrace();            }            flag = true;                        System.out.println("子线程修改flag的值为:  " + flag);                    }).start();                        // while(true) 调用底层代码,效率极高,不会从主存中再次获取被其他线程修改过的数据        while (true) {            if (flag) {                System.out.println("flag is true 主线程结束循环!");                break;            }        }    }}

因此,volatile 保证了变量的可见性,即当一个线程修改了 volatile 变量后,其他线程能够立即看到最新的值。

volatile 与 synchronized 的对比

特性volatilesynchronized
可见性保证可见性保证可见性
原子性不保证原子性保证原子性
重排序禁止指令重排序保证顺序执行
性能开销较低(无锁机制)较高(加锁/解锁开销)
使用场景适用于简单状态标志位或单次读写适用于复杂的临界区保护
  •  

Java的线程和常见方法

在学习和使用Java的多线程前,需要了解一些关于计算机中进程,线程的基础知识。

1.线程,进程和管程

1.1 线程(Thread)

  • 定义:线程是操作系统中能够独立运行的最小单位,是进程的一个执行分支。一个进程可以包含多个线程,它们共享同一进程的资源(如内存和文件句柄)。
  • 特点
    • 线程之间的创建和销毁开销较小。
    • 线程间共享内存,通信较为高效,但也容易引发竞争条件和数据不一致问题。

1.2 进程(Process)

  • 定义:进程是程序在计算机上运行的实例,它拥有自己的内存空间和资源。进程之间是相互独立的,通常通过进程间通信(IPC)进行数据交换。
  • 特点
    • 进程有自己的地址空间,线程间不共享内存。
    • 进程的创建和销毁开销较大,但提供更好的隔离性和稳定性。

1.3 管程(Monitor)

  • 定义:管程是一种高层次的同步机制,用于控制对共享资源的访问。它将共享资源的访问和管理封装在一个对象中,并提供互斥访问。
  • 特点
    • 管程通常包括一个互斥锁和一些条件变量。
    • 通过管程,可以避免线程间的竞争条件,简化线程同步的复杂性。

2.串行、并行和并发

2.1 串行(Serial)

  • 定义:派发多个任务,所有任务都按照顺序先后执行。
  • 特点:顺序执行,执行总时长几乎等于每个任务执行的时间相加。

2.2 并行(Parallelism)

  • 定义:并行是指派发多个任务在同一时刻同时执行直到全部完成。通常是在多核处理器上,多个任务可以同时在不同的核心上运行。
  • 特点:同一时间分别执行,每个任务执行都不被打断,执行总时长约等于耗时最长的那个任务需要的时间。

2.3 并发(Concurrency)

  • 定义:派发多个任务在同一时间段内进行,不一定是同时执行的。任务可能在共享的时间片上交替运行。
  • 特点:同一时间交替的执行,任务有被其他任务抢走时间片后中断和抢占其他任务的时间片的可能,执行总时长可能小于以串行或并行来执行这些任务的总时长。

3.Java多线程的实现和常见方法

3.1 继承Thread类创建子线程

通过继承java.lang.Thread,重写其run()方法来创建自定义线程类,然后实例化该类并调用start()方法启动线程,jvm自动调用run()方法,run()方法运行的就是子线程。

start()方法执行后,线程不一定立即创建,因为线程是操作系统的资源,需要等待操作系统分配

1.可以通过重写带参(线程名)构造函数,或调用setName()设置线程名。

2.每个Thread只能执行一次run()方法否则会出现IllegalThreadStateException异常,如果我们自己直接运行线程的run()方法等同于对象调用方法,仍然是单线程。

3.通过Thread.currentThread()可以获取当前运行的线程。

class MyThread extends Thread {    public MyThread(String name) {        super(name);    }    @Override    public void run() {        System.out.println("Thread is running.");        System.out.println(Thread.currentThread().getName());    }}MyThread thread = new MyThread("t1");thread.start();
class MyThread extends Thread {    @Override    public void run() {        System.out.println("Thread is running.");        System.out.println(Thread.currentThread().getName());    }}MyThread thread = new MyThread();thread.setName("t1");thread.start();

通过匿名内部类写法

new Thread("t1") {   @Override   public void run() {      System.out.println("Thread is running.");      System.out.println(Thread.currentThread().getName());   }}.start();

3.2 实现Runnable接口创建子线程

通过实现java.lang.Runnable接口并重写其run()方法实现一个Target对象,然后将该Target传递给Thread,同时可以选择指定一个线程名,再调用Thread的start()方法启动线程,jvm自动调用run()方法,在run()方法开启子线程。

实现Runnable接口创建子线程,既可以避免单继承的局限,又能使得代码更加清晰,把子线程的任务与执行任务的Thread对象分开,可以用一个任务创建出多个线程同时执行。

class MyRunnable implements Runnable {    @Override    public void run() {        System.out.println("Thread is running.");    }}Thread thread = new Thread(new MyRunnable());thread.start();Thread thread2 = new Thread(new MyRunnable(), "t2");thread2.start();

无论哪种方式创建线程,都不能自己手动调用Thread的run()方法,必须调用start()start()方法最终调用C++实现的native方法start0(),由JVM调用run()方法,所以自己调用run()方法无法实现多线程。
java.lang.Thread

public synchronized void start() {  /**  * This method is not invoked for the main method thread or "system"  * group threads created/set up by the VM. Any new functionality added  * to this method in the future may have to also be added to the VM.  *  * A zero status value corresponds to state "NEW".  */  if (threadStatus != 0)     throw new IllegalThreadStateException();  /* Notify the group that this thread is about to be started  * so that it can be added to the group's list of threads  * and the group's unstarted count can be decremented. */  group.add(this);  boolean started = false;  try {     start0();     started = true;  } finally {     try {           if (!started) {              group.threadStartFailed(this);           }     } catch (Throwable ignore) {           /* do nothing. If start0 threw a Throwable then           it will be passed up the call stack */     }  }}private native void start0();

3.3 创建守护线程

JVM的线程分为用户线程和守护线程

用户线程:系统的工作线程,会完成这个程序需要完成的业务操作。

守护线程:服务线程,没有服务对象就没有必要继续运行下去了。

Java中,通过设置setDaemon(true)来实现一个守护线程,需要在调用start()方法之前设置,当主线程结束后,守护线程即使还有任务未完成,JVM进程也会退出。如果子线程没有设置为守护线程,即使主线程完成,子线程仍然继续执行未完成的任务,JVM不会退出。

public static void main(String[] args) {   Thread daemon = new Thread(() -> {      System.out.println(Thread.currentThread().getName());      while (true) {      }   }, "daemon");   daemon.setDaemon(true);   daemon.start();   System.out.println(Thread.currentThread().getName());}

3.4 线程的暂停:sleep()

暂停执行(睡眠)多少毫秒

Thread.sleep(2000);

3.5 检查线程是否存活:isAlive()

通过Thread.sleep();使线程休眠,观测线程开启和结束后的状态。

public static void main(String[] args) {   Thread t1 = new Thread(() -> {      System.out.println(Thread.currentThread().getName());      try {         Thread.sleep(2000);      } catch (InterruptedException e) {         throw new RuntimeException(e);      }   }, "t1");   t1.start();   System.out.println(t1.isAlive()); //true   try {      Thread.sleep(4000);   } catch (InterruptedException e) {      throw new RuntimeException(e);   }   System.out.println(t1.isAlive()); //false}

3.6 让出CPU:yield()

静态方法,当前线程让出CPU,给同级或更高优先级线程机会

Thread.yield();

还有一些线程的方法涉及到线程间的通信,见:Java线程间的通信机制

  •  

Java线程间的通信机制

当需要多个线程共同完成一件任务,而且需要有规律的执行,那么多个线程之间需要一定的通信机制,可以协调他们的工作,以此实现多线程共同操作一份数据。

1.等待唤醒机制

这是一种线程间的协作机制,与争夺锁的竞争机制相对应,当一个线程满足某个条件时,就进入等待状态( wait/wait(m) ),等到其他线程执行完指定的代码后,再将其唤醒,或者可以指定时间,到时间了自动唤醒,有多个线程等待时,如果有需要,可以notifyAll()唤醒所有等待的线程,wait/notify就是一种线程间的协助机制。

wait()notify()notifyAll()都是java.lang.Object中的方法,这说明在Java语言的设计中,任何对象都能充当同步监视器(锁)

1.1 wait

作用:使当前线程等待,直到其他线程调用相同对象的notify()notifyAll()方法,或者线程被中断。
使用场景:当一个线程需要等待某个条件发生时,比如资源可用、状态改变等。
条件:调用wait()方法时,当前线程必须持有该对象的监视器锁(synchronized),否则会抛出 IllegalMonitorStateException 异常。

synchronized (lock) {    while (condition) {        lock.wait(); // 释放锁并等待    }    // 条件满足后的操作}

1.2 notify

作用:唤醒在该对象上等待的一个线程。如果有多个线程在等待,则随机选择一个线程唤醒。
使用场景:在某个条件被满足时,通知一个等待的线程继续执行。

synchronized (lock) {    // 修改条件    lock.notify(); // 唤醒一个等待线程}

1.3 notifyAll

作用:唤醒在该对象上等待的所有线程。
使用场景:当条件变化可能影响所有等待线程时,使用 notifyAll() 确保所有线程都有机会重新检查条件。

synchronized (lock) {    // 修改条件    lock.notifyAll(); // 唤醒所有等待线程}

1.4 例:两个线程交替累加

第1次执行:假设线程1先抢到锁,进去后会先唤醒了线程2,但是锁还是线程1持有的,线程2只能等待,线程1将变量i加1变成1,wait释放锁进入等待唤醒状态。

第2次执行:线程2拿到锁后同样先唤醒了线程1,但是现在锁是线程2持有,线程1无法执行,线程2将变量i加1变成2后,wait释放锁进入等待唤醒状态。

第3次执行:线程1拿到锁还是先唤醒线程2然后将变量i加1变成3,然后wait释放锁。

……

第99次执行:线程1拿到锁还是先唤醒线程2然后执行加1将变量i变成99,然后wait释放锁。

第100次执行:线程2进来先唤醒了线程1,然后将i加到100后wait释放锁,此时循环加到100就已经完成,但是线程的执行还没有结束,被唤醒的线程1继续执行,先唤醒了线程2然后进入if判断,但是这次已经不能再加了,所以线程1没有wait就退出了while循环,离开synchronized块后自动失去锁,线程2随后自然得到锁进入synchronized块,同样判断已经不能再加了后随即也跳出while循环,线程1和线程2就都在就绪状态自然结束了,随后JVM进程退出。

public class TestAdd {    static int i = 0;    public static void main(String[] args) {        Runnable runnable = new Runnable() {            @Override            public void run() {                while (true) {                    synchronized (this){                        this.notify();                        if (i < 100) {                            System.out.println(Thread.currentThread().getName() + "---" + ++i);                        }                        else {                            break;                        }                        try {                            this.wait();                        } catch (InterruptedException e) {                            throw new RuntimeException(e);                        }                    }                }            }        };        new Thread(runnable).start();        new Thread(runnable).start();    }    }

1.5 wait退出时检查

lock.wait()阻塞了的线程,一旦被唤醒,或超时时,会从wait()方法下面的代码继续向下执行,即直接跳出if执行下面的代码,需要注意的是此时已经醒来的线程并不会再次判断if中的条件是否满足而是跳出if直接向下执行,这样就会遇到一个问题,线程阻塞期间,其他线程进行的一些操作可能造成条件改变,不能满足if中的条件了,如果不加以二次判断就继续执行,就可能导致程序出错。

synchronized (lock) {    if (条件不满足) {        lock.wait();    }    //继续执行后面操作}

Java给出的解决办法是:让wait总是出现在循环中,使用while去判断wait的条件而不是if,当使用while时,线程被唤醒后,不会继续跳出while块向下执行,而是会再判断一次while中的逻辑,如果条件不满足会继续wait,直到条件满足跳出while。

synchronized (lock) {    while (条件不满足) {        lock.wait();    }    //继续执行后面操作}

例1:如果使用if,wait中的线程被唤醒时,不会再次判断if中的条件

public class SimpleWakeupDemo {    private volatile static boolean flag = true;     private static final Object lock = new Object();    public static boolean condition() {        System.out.println(Thread.currentThread().getName() + "判断flag = " + flag);        return flag;    }    public static void main(String[] args) {        Thread t1 = new Thread() {            @Override            public void run() {                synchronized (lock) {                    if (condition()) {                        try {                            System.out.println(Thread.currentThread().getName() +" before wait");                            lock.wait();                            System.out.println(Thread.currentThread().getName() +" after wait");                        } catch (InterruptedException e) {                            throw new RuntimeException(e);                        }                    }                    System.out.println(Thread.currentThread().getName() + "打印flag = " + flag);                }            }        };        Thread t2 = new Thread() {            @Override            public void run() {                synchronized (lock) {                    try {                        Thread.sleep(900);                        lock.notifyAll();                    } catch (InterruptedException e) {                        throw new RuntimeException(e);                    }                }            }        };        t1.start();        t2.start();    }}
Thread-0判断flag = trueThread-0 before waitThread-0 after waitThread-0打印flag = true

如果改成while,可以看到wait结束后,会再次判断条件是否成立

while (condition()) {    try {        System.out.println(Thread.currentThread().getName() +" before wait");        lock.wait();        System.out.println(Thread.currentThread().getName() +" after wait");    } catch (InterruptedException e) {        throw new RuntimeException(e);    }}
Thread-0判断flag = trueThread-0 before waitThread-0 after waitThread-0判断flag = trueThread-0 before wait

例2:生产消费模型:两个线程操作同一变量,一个判断变量为0就加1变成1,另一个判断变量为1就减1变成0

public class SimpleWakeupDemo {    private volatile static int product = 0;     private static final Object lock = new Object();    public static void main(String[] args) {        Runnable consumer = new Runnable() {            @Override            public void run() {                synchronized (lock) {                    while (product < 1) {                        try {                            System.out.println(Thread.currentThread().getName() + "不足");                            System.out.println("before wait");                            lock.wait();                            System.out.println("after wait");                        } catch (InterruptedException e) {                            throw new RuntimeException(e);                        }                    }                    product --;                    System.out.println(Thread.currentThread().getName() + " = " + product);                    lock.notifyAll();                }            }        };        Runnable productor = new Runnable() {            @Override            public void run() {                synchronized (lock) {                    try {                        Thread.sleep(900);                    } catch (InterruptedException e) {                        throw new RuntimeException(e);                    }                    while (!(product < 1)) {                        try {                            System.out.println(Thread.currentThread().getName() + "已满");                            System.out.println("before wait");                            lock.wait();                            System.out.println("after wait");                        } catch (InterruptedException e) {                            throw new RuntimeException(e);                        }                    }                    product ++;                    System.out.println(Thread.currentThread().getName() + " = " + product);                    lock.notifyAll();                }            }        };        for (int i = 0; i < 10; i++) {            new Thread(productor, "productor-"+i).start();            new Thread(consumer, "consumer-"+i).start();        }    }}

当使用while时,操作后的数总是0或1

productor-0 = 1productor-3已满before waitconsumer-8 = 0productor-9 = 1productor-8已满before waitconsumer-1 = 0productor-7 = 1productor-6已满before waitconsumer-6 = 0consumer-5不足before waitproductor-1 = 1consumer-4 = 0productor-4 = 1consumer-3 = 0productor-2 = 1consumer-9 = 0consumer-2不足before waitconsumer-7不足before waitproductor-5 = 1consumer-0 = 0after waitconsumer-7不足before waitafter waitconsumer-2不足before waitafter waitconsumer-5不足before waitafter waitproductor-6 = 1after waitproductor-8已满before waitafter waitproductor-3已满before waitafter waitconsumer-5 = 0after waitconsumer-2不足before waitafter waitconsumer-7不足before waitafter waitproductor-3 = 1after waitproductor-8已满before waitafter waitconsumer-7 = 0after waitconsumer-2不足before waitafter waitproductor-8 = 1after waitconsumer-2 = 0

如果换成if,就会错误的出现其他的数字

productor-0 = 1productor-2已满before waitproductor-9已满before waitconsumer-8 = 0productor-7 = 1productor-8已满before waitconsumer-7 = 0consumer-6不足before waitconsumer-4不足before waitproductor-6 = 1consumer-5 = 0productor-5 = 1productor-4已满before waitconsumer-3 = 0productor-1 = 1productor-3已满before waitconsumer-2 = 0consumer-9不足before waitconsumer-1不足before waitconsumer-0不足before waitafter waitproductor-3 = 1after waitproductor-4 = 2after waitconsumer-4 = 1after waitconsumer-6 = 0after waitproductor-8 = 1after waitproductor-9 = 2after waitproductor-2 = 3after waitconsumer-0 = 2after waitconsumer-1 = 1after waitconsumer-9 = 0

1.6 小总结

线程互相交替执行的过程可以简记为:1判断,2干活,3通知

synchronized (Object) {    while (....) { //判断        try {            Object.wait();        } catch (InterruptedException e) {            throw new RuntimeException(e);        }    }    //干活    //.........    Object.notifyAll(); //通知}   

2.线程连接:join方法

主执行中插入其他线程m1,主线程立刻被阻塞,直到插入的线程m1执行完成。

public static void main(String s[]) {    Thread m1 = new Thread() {        @Override        public void run() {            for (int i = 0; i < 100; i++) {                try {                    Thread.sleep(100);                } catch (InterruptedException e) {                    e.printStackTrace();                }                System.out.println(this.getName() +" = " + i);            }        }    };    m1.setName("m1");    m1.start();    for (int i = 0; i < 100; i++) {        /**i加到20的时候,插入子线程,子线程执行完了(消亡),主线程再继续*/        if (i == 20) {            try {                m1.join();            } catch (InterruptedException e) {                e.printStackTrace();            }        }        System.out.println(Thread.currentThread().getName() +" = " + i);    }}

3.volatile

volatile是一种轻量级的线程通信机制,具体见:volatile作用分析

  •  

Java线程安全和同步机制

当多个线程同时访问同一资源(变量,文件,记录),如果只有读操作,则不会有线程安全问题,如果有读和写操作,则会产生线程安全问题,必须保证共享数据同一时刻只能有同一个线程操作。Java采取的办法是synchronized同步代码块或同步方法。同步代码块或同步方法解决了线程安全问题,但是操作共享数据时,线程时串行执行的,意味着效率较低。

1.多线程安全问题

经典卖票案例:

两个线程一块卖票,没有加同步代码块,程序运行结果不正确,存在超卖重卖

public class Ticket {    public static void main(String[] args) {        TicketTask t1 = new TicketTask();        TicketTask t2 = new TicketTask();        t1.start();        t2.start();    }    static class TicketTask extends Thread {        static int ticket = 200;        @Override        public void run() {            while (true) {                if (ticket > 0) {                    System.out.println(Thread.currentThread().getName() +" " + ticket);                    ticket--;                }                else {                    break;                }            }        }    }}

2 同步代码块和同步方法解决线程安全问题

2.1 同步代码块

需要被同步的代码,即为操作共享数据的代码。共享数据,即为多个线程都需要操作的数据。同步监视器可以由任何类的对象担任,但是多个线程必须共用同一个同步监视器。

同步代码块的语法

synchronized (同步监视器/) {  //需要被同步的代码}

加入同步代码块,程序运行结果正确,当有线程操作共享数据,其他线程需要等待。lock作为同步监视器,锁住代码块中的操作,谁获得同步监视器,谁运行同步代码块中的代码。

public class Ticket {    public static void main(String[] args) {        TicketTask t1 = new TicketTask();        TicketTask t2 = new TicketTask();        t1.start();        t2.start();    }    static class TicketTask extends Thread {        static int ticket = 200;        static final Object lock = new Object();        @Override        public void run() {            while (true) {                synchronized (lock) {                    if (ticket > 0) {                        System.out.println(Thread.currentThread().getName() +" " + ticket);                        ticket--;                    }                    else {                        break;                    }                }            }        }    }}

2.2 同步方法

如果需要同步执行的代码恰好在一个方法中,可以使用同步方法保证线程安全,在方法声明上使用 synchronized 关键字,此时锁为对象实例(this)。同一时刻,只有一个线程能够执行该实例的方法。

public synchronized void test() {}

3 同步代码块和同步方法的使用

3.1 Runnable创建线程时使用同步代码块

使用synchronized使得实例方法加锁变为同步方法,因为Runnable对象只有一个,所以锁可以直接使用当前调用者this

public class TestRunnable {    public static void main(String[] args) {        Target target = new Target();        for (int i = 0; i < 10; i++) {            new Thread(target, "T"+i).start();        }    }}class Target implements Runnable {    private Integer i = 1000;    @Override    public void run() {        while (true) {            try {                Thread.sleep(200);            } catch (InterruptedException e) {                e.printStackTrace();            }            synchronized (this) {                if (i > 0) {                    i--;                    System.out.println(Thread.currentThread().getName() + "->" + i);                } else {                    break;                }            }        }    }}

3.2 Thread类创建线程时,使用同步代码块

Thread类创建线程时,使用同步代码块加锁,因为Thread对象是多个,所以需要静态的监视器对象object,如果还用this就出现了多个锁

public class TestThread {    public static void main(String[] args) {        for (int i = 0; i < 10; i++) {            new MyThread("T"+i).start();        }    }}class MyThread extends Thread {    private static Integer i = 1000;    //同步监视器,锁    private static Object object = new Object();    public MyThread(String name) {        super(name);    }    @Override    public void run() {        while(true) {             synchronized(object) {                if (i > 0) {                    i--;                    System.out.println(Thread.currentThread().getName() +"->" + i);                } else {                    break;                }            }        }    }}

3.3 Runnable创建线程时,使用同步方法加锁

public class TestRunnable {    public static void main(String[] args) {        Runnable runnable = new Runnable() {            private int num = 1000;            @Override            public void run() {                while (true) {                    this.show();                }            }             public synchronized void show() {                try {                    Thread.sleep(20);                } catch (InterruptedException e) {                    e.printStackTrace();                }                if (num > 0) {                    num--;                    System.out.println(Thread.currentThread().getName() + "->" + num);                }            }        };        for (int i = 0; i < 10; i++) {            new Thread(runnable, "T" + i).start();        }    }}

3.4 Thread创建线程时,使用同步方法加锁

public class TestThread {    public static void main(String[] args) {        TicketTest ticketTest = new TicketTest();        for (int i = 0; i < 10; i++) {            new Thread("T"+i) {                @Override                public void run() {                    while (true) {                        ticketTest.test();                    }                }            }.start();        }    }    static class TicketTest  {        private int num = 1000;        private synchronized void test() {            if (num > 0) {                num--;                System.out.println(Thread.currentThread().getName() + "->" + num);            }        }    }}

3.5 静态方法加锁和使用.class对象做锁

在静态方法上使用 synchronized,锁住的是类的.class对象,每个类的class对象只有一个,所以同时只能有一个线程进入方法。

public static synchronized void staticMethod() {    // 方法体}

同步块上使用.class对象做锁,因为每个类.class对象只有一个,故也能用于保证线程安全

synchronized (A.class) {}

4 死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

写程序时,要避免出现死锁。

示例代码1

死锁原因的分析:这个例子是个比较明显的死锁,线程t1t2几乎同时启动,在一秒钟的等待时间里,t1获得了锁lock1t2获得了锁lock2,一秒钟后t1又想去获得lock2,但是现在lock2t2持有,需要一直等直到t2释放lock2,与此同时,t2也想去获得lock1,但是lock1现在被t1持有,需要一直等待,直到t1释放lock1,两个线程都在争抢在对方持有的锁,且都在等待对方先释放各自持有的锁,不然就一直等待,线程都一直处在阻塞状态无法继续运行,造成死锁。

public class TestDeadLock {    public static void main(String[] args) {        Object lock1 = new Object();        Object lock2 = new Object();        Thread t1 = new Thread() {            @Override            public void run() {                synchronized (lock1) {                    try {                        Thread.sleep(100);                    } catch (InterruptedException e) {                        Thread.currentThread().interrupt();                    }                    synchronized (lock2) {                        System.out.println(lock1);                        System.out.println(lock2);                    }                }            }        };        Thread t2 = new Thread(){            @Override            public void run() {                synchronized (lock2) {                    try {                        Thread.sleep(100);                    } catch (InterruptedException e) {                        Thread.currentThread().interrupt();                    }                    synchronized (lock1) {                        System.out.println(lock1);                        System.out.println(lock2);                    }                }            }        };        t1.start();        t2.start();    }}

示例代码2

这是一个不是非常明显的死锁的例子,线程thread1thread2几乎同时开始执行,thread1执行a.fun(b)时,由于A类的fun方法是个同步方法,故锁是当前调用者this对象,即a,调用fun方法,thread1便持有了锁a,与此同时,thread2同理的持有了锁b,这些都在一秒钟前完成了,1秒钟后,thread1执行blast同步方法,同理需要先获得锁b,但是锁b目前被thread2持有,同时thread2也开始执行alast方法,需要先持有锁a,但是锁athread1持有,双方都在等待对方先释放自己需要的锁,否则就一直阻塞无法继续运行,造成死锁。

public class TestDeadLock2  {    public static void main(String[] args) {        A a = new A();        B b = new B();        Thread thread1 = new Thread() {            @Override            public void run() {                a.fun(b);            }        };        Thread thread2 = new Thread() {            @Override            public void run() {                b.fun(a);            }        };        thread1.start();        thread2.start();    }    public static class A extends Thread {        public synchronized void fun(B b) {            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                throw new RuntimeException(e);            }            b.last();        }        public synchronized void last() {        }    }    public static class B extends Thread {        public synchronized void fun(A a) {            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                throw new RuntimeException(e);            }            a.last();        }        public synchronized void last() {        }    }}
  •  

Java开篇

栏目持续更新中

一、引言

Java基础是全站的开篇,但是这个系列只会整理Java基础类库的使用和最新特性以及一些底层原理和源码解读,不会赘述JDK的安装配置和面向对象等最基础的内容。

二、Java版本发展

  • 1990年末, Sun公司成立了一个由James Gosling领导的”Green 计划”,准备为下一代智能家电 (如电视机、微波炉、电话)编写一个通用控制系统,在尝试了使用C++和改造C++未果后,决定创造一种全新的语言: Oak
  • 1992年夏,Green计划己经完成了新平台的部分功能,包括Green操作系统、Oak的程序设计语言、类库等
  • 1994年,互联网和浏览器的出现,开发组意识到Oak非常适合于互联网,对Oak进行了小规模的改造运行在浏览器,并更名为Java
  • 1995年初,Sun推出了Java语言
  • 1996年初,发布JDK1.0,这个版本包括两部分: 运行环境(JRE)和开发环境(JDK)
  • 1997年2月,发布JDK1.1,增加了JIT(即时编译)编译器
  • 1998年12月,Sun发布了Java历史上最重要的JDK版本:JDK1.2,伴随JDK1.2一同发布的还有JSP/Servlet、EJB等规范,并将Java分成了J2EE、J2SE和J2ME三个版本
  • 2002年2月,Sun发布了JDK历史上最为成熟的版本: JDK1.4
  • 2004年10月,发布里程式板本:JDK1.5,为突出此版本的重要性,更名为JDK5.0(Java5),同时,Sun将J2EE、J2ME也相应地改名为Java EE和Java ME,增加了诸如泛型、增强的for语句、可变数量的形参、注释 (Annotations)、自动拆箱和装箱等功能
  • 2006年12月,Sun公司发布了JDK1.6(Java6)
  • 2009年4月20日,Oracle宣布将以每股9.5美元的价格收购Sun
  • 2011年,发布JDK1.7(Java7),是Oracle来发布的第一个Java版本,引入了二进制整数、支持字符串的switch语句、菱形语法、多异常捕捉、自动关闭资源的try语句等新特性。
  • 2014年,发布Java8,是继Java5以来变化最大的版本,带来了全新的Lambda表达式、流式编程等大量新特性,具体见:Java8的新特性
  • 2017年9月,发布Java9,提供了超过150项新特性,主要包括模块化系统,jshell交互工具,jdk编译工具,java公共API以及安全增强,而且采用了更高效、更智能的G1垃圾回收器,完全对Java体系进行了改变,让庞大的Java语言更轻量化。从Java9开始,Java版本更迭从特性驱动改为时间驱动,每个版本之间的更迭周期调整为6个月,但是LTS版本的更迭时间为3年。同时将Oracle JDK的原商业特性进行开源。
  • 2018年3月,发布Java10
  • 2018年9月,发布Java11

三、Java核心类库

3.1 JDK基础类库

待续

3.2 数据结构 (Collection,Map,Set)

待续

3.3 输入输出 (IO/NIO)

在Java中,和IO有关的操作封装在java.iojava.nio中,传统IO(java.io)是以流的形式实现输入输出操作的,还有基于通道和缓冲区的NIO(java.nio

I/O操作,有阻塞和非阻塞之分:

  • 阻塞:发起读取数据的线程是被阻塞的
  • 非阻塞:发起读取数据的线程不被阻塞,直接返回

也有同步和异步之分:

  • 同步:数据读取完成后,直接在接收到数据的线程上,紧接着进行拷贝操作
  • 异步:数据读取完成后,通过一个回调函数,在新的线程处理数据的拷贝

阻塞非阻塞和同步异步,描述的阶段和描述的事情是不同的,因此可以自由组合,Java语言将其组合为分为BIO,NIO,和AIO三种不同的IO,与Linux的IO模型(详见:浅谈Linux(Unix)的I/O模型)对应的话,BIO对应的是Blocking I/O

  • BIO

    同步的,阻塞的IO,位于java.io包下,是Java的传统IO,基于流,IO流的分类方式有很多,按照方向分为输入流和输出流,按照数据传输单位又能分为字节流和字符流

  • NIO

    同步的,非阻塞的IO,位于java.nio包下

  • AIO

    异步的,非阻塞的IO,也位于java.nio包下

3.4 网络 (Socket)

3.5 线程 (Thread)

本部分主要讲述了使用Java语言来实现多线程的程序,包括线程的创建方式,线程基本方法的使用,多线程操作共享数据导致的安全问题以及死锁现象的原因和避免死锁,多线程运行过程中的协作和通信机制以及调用各种线程的方法时线程状态和生命周期的改变等。

序号文章名概述
1Java的线程和常见方法进程和线程,并发和并行,线程的创建和常见方法
2Java线程安全和同步机制多线程导致的问题,线程的同步机制,死锁
3Java线程间的通信机制线程的通信,等待唤醒机制wait/notify
4Java线程的状态不同线程状态之间的转换过程
5volatile作用分析volatile关键字的作用,可见性、原子性和重排序

3.6 并发编程 (JUC)

JUC就是Java在并发编程中使用的工具包java.util.concurrent的简称,包括java.util.concurrentjava.util.concurrent.atomicjava.util.concurrent.locks等,起始于JDK1.5,是Java语言中增强版的多线程和高并发工具,拥有更加强大的多线程实现,本章内容会介绍些JUC中常用的并发编程工具类及其实现原理,需要在理解了第3.5小节的线程一章的基础上学习

主要涉及:

  • 锁,可重入锁,公平/非公平锁,读写锁相关:java.util.concurrent.locks.Lockjava.util.concurrent.locks.Conditionjava.util.concurrent.locks.ReadWriteLock
  • 异步计算 java.util.concurrent.CompletableFuturejava.util.concurrent.FutureTaskjava.util.concurrent.Callable
  • 阻塞队列 java.util.concurrent.BlockingQueue
  • 线程池 java.util.concurrent.ExecutorService
  • 任务拆分合并工具 java.util.concurrent.ForkJoinPooljava.util.concurrent.ForkJoinTask
  • 线程安全容器 java.util.concurrent.CopyOnWriteArrayListjava.util.concurrent.ConcurrentHashMapjava.util.concurrent.CopyOnWriteArraySet
  • 并发控制工具 java.util.concurrent.CountDownLatchjava.util.concurrent.CyclicBarrierjava.util.concurrent.Semaphore
  • 各种原子类,例如java.util.concurrent.atomic.AtomicIntegerjava.util.concurrent.atomic.AtomicReference
  • 多线程编程的一些问题:缓存行对齐、锁的粗化,消除等
序号文章名概述
1JUC可重入锁ReentrantLockReentrantLock实现更精准的锁操作
2JUC读写锁ReadWriteLockReadWriteLock读写锁实现读写分离

3.7 反射 (Reflect)

待续

3.8 JDK其他工具和类

序号文章名概述
1Java实现LDAP登录使用Java与LDAP进行交互

四、Java的设计模式

序号文章名概述
1Java单例Java中单例模式的几种实现形式

五、参考

  1. 《疯狂Java讲义》,作者:李刚,电子工业出版社,2018年1月
  2. 《深入理解Java核心技术》,作者:张洪亮,电子工业出版社,2022年5月
  3. 《Effective Java》,作者:Joshua Bloch,机械工业出版社,2009年1月
  •  

疲于奔命

最近这几天,的确是没什么时间来写点东西,每天都在与代码拼命抗争。另外就是之前处理的服务器被入侵貌似远没结束,还有一些乱七八糟的东西需要处理。

上周对象把博越送去保养,汽修厂的说减震没有问题,不过刹车片、火花塞需要换了。于是保养完又换了这两项,加起来又一千多。这乱七八糟的钱真的是不禁花。每天都有这些乱七八糟的事情,最近粉皮的保险也快到期了,月初各种电话,反而是这几天真的快到期反而消停了,也不知道到底是什么逻辑。

前几天  陈沩亮博客 提示说我的友联检测不到:

然而,自己看了一下没什么问题。但是他却一致纠结这件事情。其实不想管这事,主要还是最近太忙了。项目用的国产时序数据库,数据写入和查询一直有问题,让 cursor 给修复,结果这小众的玩意儿她也很无奈,甚至一度直接给干废了。

对于这种结果也是真的无奈了。但是还是要继续啊。既然有人坚持,那就去看下到底什么问题吧,结果不看不知道,一看吓一跳。真的 tmd 全部的链接在百度爬虫访问的时候都变成了 moban 什么狗屁玩意儿:

这 tmd 就很艹了。直接搜索moban 最后在 class-wp 中找到了这个东西:

真 tm 防不胜防,直接下载 wp 的原始安装包,替换这个文件一切恢复正常。

感谢这哥们的坚持。

昨天中午,实在不知道吃什么想着汤姆家的牛排还有余额,于是跑去万达吃牛排。结果回来的时候,一 jio 油门下去发动机故障灯亮了:

下午开去四儿子店,一个小时的检测之后告知氧传感器坏了,给索赔了一个,让等电话,到了之后过去换。问在哪里加的油,感觉油品有问题。

既然油品有问题,那就去中石化吧,问怎么加油优惠,说买优惠券。看了一下感觉不合适,就放弃了。让那个大哥直接加。

刚来的时候就说了加 95,结果那个傻屌直接用 92 的油枪就开始加,好在自己看了一眼,问不是该加 95 吗?!

那大哥都懵逼了,我当时 tm 脸都绿了肯定。这 tm 脑子是装了些屎吗?艹!

重新给换到 95 继续加,说不好意思,给送了瓶玻璃水。我拿了东西也没搭理他,真 tm智障。好在 tm 就加了 200。

最后也没注意92 是加了 3 升还是 5 升,艹!开到公司后,总感觉不对劲,于是跑到中石油重新加满了 98,算是标号能够中和一下。

不过这 98 是真 tm 贵。

最近晚上休息也休息不好,做梦都 tm 在改代码。半夜热醒了,忽然想到对象说的,加油站那么多的 666,888 去加油应该没问题。于是翻了下加油记录,发现最后实在老家的壳牌加的。

这忽然又有种似曾相识的感觉,最开始开小六子回老家去小加油站加的油,回来就让维修发动机。从此再也没去过路边的小加油站,这次传感器坏了,这尼玛壳牌的 95 也不能加了?

对象说,肯定是忽悠你呢,本来就是要坏,非得说油不行。对于这个问题,我是没办法求证的,我也不知道是不是他最终要坏啊。坑爹!

不过不得不说,这破 java 真是写的够够的,太 tm 麻烦了。

题外话,这几天还遇到一个大哥也 tm 神奇:

这尼玛想屁吃呢?CSB!!

  •