百度高级Java面试真题

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

在Java中如何避免内存泄露?

在Java中,内存泄露通常指的是当对象不再被使用时,仍然被其他对象引用,因此无法被垃圾回收器(Garbage Collector, GC)回收的情况。避免内存泄露主要依赖于良好的编程实践和一些工具的辅助。以下是一些避免内存泄露的方法:

  1. 使用局部变量: 尽可能使用局部变量,这样当方法执行完毕后,这些局部变量就会自然脱离作用域,成为垃圾回收的候选对象。
  2. 释放资源: 对于需要手动管理的资源(如数据库连接、文件流等),确保在使用完毕后立即释放它们。通常可以使用try-with-resources语句来自动管理资源。
  3. 监听器和回调: 如果你注册了事件监听器或回调,确保在不需要它们时注销,否则它们可能会阻止垃圾回收。
  4. 使用弱引用: 对于不需要长期持有的对象,可以考虑使用WeakReference,这样一旦这些对象只被弱引用所引用,垃圾回收器就可以回收它们。
  5. 避免静态集合类: 静态生命周期的对象,如静态集合类,如果不正确管理,很容易造成内存泄露。确保静态集合中的对象在不再需要时被移除。
  6. 内存泄露检测工具: 使用内存分析工具(如Eclipse Memory Analyzer, VisualVM等)定期检查你的应用程序,以便发现和修复潜在的内存泄露问题。
  7. 避免循环引用: 在设计数据结构时,要注意避免创建不必要的循环引用,尤其是在使用缓存时。
  8. 缓存管理: 如果使用缓存,确保有一个合理的过期策略或大小限制,以防止缓存无限增长。
  9. 代码审查和测试: 定期进行代码审查可以帮助识别可能导致内存泄露的代码模式。同时,内存泄露往往在压力测试或长时间运行的测试中显现,因此这些测试也很重要。
  10. 使用最新的Java版本: 随着Java语言的发展,垃圾回收和内存管理也在不断改进。使用最新的Java版本可以帮助减少内存泄露的风险。

遵循这些最佳实践可以显著减少内存泄露的发生,但需要注意的是,即使是最谨慎的程序员也可能在复杂的应用程序中遇到内存泄露,因此定期的性能监测和分析是必不可少的。

请解释MySQL的执行计划以及如何根据它进行查询优化。

MySQL的执行计划是数据库在执行SQL查询前对如何访问数据所做的一系列优化选择。执行计划提供了关于MySQL是如何处理查询的详细信息,包括如何联接表、使用哪些索引、排序方式以及数据检索顺序等。了解执行计划可以帮助开发人员和数据库管理员优化查询,使其更加高效。

要获取MySQL中的查询执行计划,可以在查询前加上EXPLAIN关键字,或者使用EXPLAIN FORMAT=JSON获取更详细的JSON格式输出。例如:

EXPLAIN SELECT * FROM my_table WHERE my_column = 'my_value';

执行计划中的关键信息包括:

  • id: 查询的标识符,如果查询包含子查询,每个子查询或查询块都会有一个不同的id。
  • select_type: 查询的类型,如SIMPLE(简单查询,不包含子查询或联接)、PRIMARY(主查询)、SUBQUERY(子查询)等。
  • table: 显示这一行的数据是关于哪张表的。
  • partitions: 如果表进行了分区,显示查询涉及哪些分区。
  • type: 表示MySQL决定如何查找表中的行,例如:ALL(全表扫描)、index(索引扫描)、range(索引范围扫描)、ref(使用索引查找值)等。
  • possible_keys: 显示可能应用于这张表的索引。
  • key: 实际使用的索引。
  • key_len: 使用的索引的长度。
  • ref: 显示索引的哪一列被使用了,如果可能的话,是一个常数。
  • rows: 预估为了找到所需的行而需要读取的行数。
  • filtered: 表示返回结果的行数占开始查找行数的百分比。
  • Extra: 包含不适合在其他列中显示的额外信息,如“Using index”(表示相应的SELECT操作只用到了索引)。

根据执行计划进行查询优化的一般步骤包括:

  1. 查看type列:优先考虑避免ALL(全表扫描)。如果出现ALL,通常意味着需要添加或优化索引。
  2. 检查possible_keys和key:确保查询正使用最合适的索引。如果没有使用索引,考虑创建一个新的索引。
  3. 优化索引覆盖:如果Extra列中出现了”Using index”,这意味着查询能够仅通过索引来获取数据,这是最理想的情况之一。
  4. 减少rows的值:尽量减少查询中必须检查的行数,通过更有效的索引或查询条件来实现。
  5. 查看Extra列:这列信息可以告诉你是否进行了排序操作”Using filesort”,或是将数据从一个表合并到另一个表”Using temporary”。这些操作通常比较耗费资源,应当尽可能避免。
  6. 调整查询结构:有时候,通过重写查询逻辑或分解复杂查询,可以提高查询效率。
  7. 使用索引排序:如果查询需要排序,尽可能通过索引来完成排序,以避免额外的排序开销。
  8. 分析联接操作:对于涉及多表联接的查询,确保联接的顺序和方法(如STRAIGHT_JOIN)是最优的,并且每个联接操作都使用了索引。
  9. 调整服务器配置:有时候,优化查询也需要调整MySQL服务器的配置参数,如缓冲区大小等。
  10. 使用分区:对于非常大的表,可以考虑使用分区来提高查询性能。

通过对执行计划的分析和理解,你可以对查询进行优化,改进其性能。然而,需要注意的是,查询优化是一个迭代过程,可能需要多次调整和测试。

MySQL中的索引覆盖扫描是什么,如何使用它提高查询效率?

索引覆盖扫描(Index Covering Scan)是MySQL中的一种查询优化技术,指的是当一个查询可以完全通过索引来获取所需的数据,而无需读取数据行本身的情况。在这种情况下,查询操作只需要访问索引,而不是数据表的行。由于索引通常比完整的数据行小很多,且存储在连续的磁盘空间上,索引覆盖扫描可以显著提高查询效率,减少I/O操作。

如何实现索引覆盖扫描:

  1. 创建合适的索引:为了实现索引覆盖扫描,需要创建一个包含所有查询中所需字段的索引。这意味着,查询中涉及的所有列都必须包含在索引中。
  2. 查询中只使用索引列:确保SELECT语句中只包含索引中的列。如果查询中引用了索引之外的列,那么MySQL将不得不访问表中的实际数据行,从而无法实现索引覆盖扫描。
  3. 使用合适的查询语句:避免在查询中使用会导致无法使用索引覆盖扫描的操作,如使用函数处理索引列等。
  4. 检查执行计划:使用EXPLAIN关键字来检查查询的执行计划,确保Extra列中出现”Using index”。这表明查询正在使用索引覆盖扫描。

举个简单的例子:

假设有一个名为users的表,包含idusernameemail三个字段。如果你经常执行以下查询:

SELECT id, username FROM users WHERE username = 'some_user';

为了优化这个查询,你可以创建一个包含usernameid的复合索引:

CREATE INDEX idx_username_id ON users(username, id);

现在,当执行上述查询时,MySQL可以仅通过idx_username_id索引来检索数据,而无需访问数据表中的实际行。这种方式可以大大减少数据访问量,从而提高查询效率。

使用索引覆盖扫描的好处:

  • 减少磁盘I/O:由于数据可以直接从索引中获取,减少了对磁盘的访问次数。
  • 减少锁竞争:如果查询可以通过索引覆盖扫描完成,那么对数据行的锁请求会减少,这对于高并发环境尤其有利。
  • 提高缓存效率:索引条目通常比数据行小,因此更多的索引条目可以被缓存在内存中,从而提高缓存命中率。

需要注意的是,并不是所有的索引都适合用于索引覆盖扫描。创建过多的索引会增加维护成本,并可能影响写操作的性能。因此,应当根据实际的查询模式来合理设计索引。

在SSM中,如何实现国际化和本地化?

SSM框架是指Spring、SpringMVC和MyBatis三个框架的整合,常用于Java Web应用开发。在SSM框架中实现国际化和本地化(i18n)通常涉及以下几个步骤:

  1. 资源文件:创建属性文件(.properties)存储不同语言的文本。这些文件通常按照语言和国家/地区来命名,例如:
messages.properties       // 默认资源文件
messages_en_US.properties // 美国英语资源文件
messages_zh_CN.properties // 简体中文资源文件
  1. 文件内容示例:
// messages.properties
welcome.message=Welcome
// messages_en_US.properties
welcome.message=Welcome
// messages_zh_CN.properties
welcome.message=欢迎
  1. Spring配置:在Spring的配置文件中配置MessageSource,指定资源文件的基本名称和默认编码。例如:
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"<property name="basename" value="classpath:messages" /><property name="defaultEncoding" value="UTF-8"/></bean
  1. SpringMVC配置:在SpringMVC配置文件中配置LocaleResolver(用于解析用户的Locale)和LocaleChangeInterceptor(拦截器用于切换Locale)。例如:
<!-- 配置LocaleResolver --><bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"<property name="defaultLocale" value="en"/></bean<!-- 配置LocaleChangeInterceptor --><mvc:interceptors<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"<property name="paramName" value="lang"/></bean</mvc:interceptors
  1. 这里的paramName是用于在请求中指定语言的参数名称。
  2. Controller中使用:在Controller中,可以使用MessageSource来获取本地化的消息。例如:
@Autowiredprivate MessageSource messageSource;

@RequestMapping("/welcome")public String welcome(Locale locale, Model model) {String welcomeMessage = messageSource.getMessage("welcome.message", null, locale);
    model.addAttribute("message", welcomeMessage);return "welcome";
}
  1. 视图中使用:在JSP或其他模板视图中,可以使用Spring的标签[spring:message](spring:message)来显示本地化的消息。例如:
<spring:message code="welcome.message"/>
  1. 语言切换:在页面上提供语言切换的选项,通常是链接或下拉菜单,点击后带上lang参数(与LocaleChangeInterceptor中配置的参数名相同)来切换语言。例如:
<a href="?lang=en"English</a<a href="?lang=zh_CN"中文</a

通过上述步骤,就可以在SSM框架中实现国际化和本地化。用户可以根据自己的需要切换不同的语言,而应用会根据用户的选择显示相应语言的内容。

在Spring Boot中,如何自定义错误处理逻辑?

查看更多

360高级Java面试真题

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

如何在Java中实现TCP粘包和拆包的处理?

在Java中实现TCP粘包和拆包的处理涉及到网络编程中的数据传输和解析问题。TCP粘包和拆包是由于TCP协议的特性,在传输过程中可能会导致多个数据包粘合在一起(粘包),或者一个数据包被拆分成多个部分(拆包)。下面我将介绍一些处理TCP粘包和拆包的常见方法。

  1. 使用固定长度的消息

一种常见的处理方法是在消息的开头定义一个固定长度的消息头,用来表示消息的长度,然后根据消息头指定的长度来截取完整的消息内容。这样就可以避免粘包和拆包的问题。

  1. 使用特殊分隔符

另一种常见的处理方法是在消息的末尾使用特殊的分隔符来标识消息的结束,比如换行符\n或者回车符\r。接收端可以根据分隔符来分割消息,从而得到完整的消息内容。

  1. 使用消息头表示消息长度

在消息的开头使用固定长度的消息头来表示消息的长度,然后根据消息头指定的长度来截取完整的消息内容。这种方法可以有效地避免粘包和拆包的问题。

示例代码

下面是一个简单的示例代码,演示了如何在Java中使用固定长度的消息头来处理TCP粘包和拆包的问题:

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;

public class TCPMessageHandler {private Socket socket;private DataOutputStream out;private DataInputStream in;
public void sendMessage(String message) throws IOException {bytebytes = message.getBytes();
        out.writeInt(bytes.length); // 使用固定长度的消息头表示消息长度
        out.write(bytes);
        out.flush();
    }
public String receiveMessage() throws IOException {int length = in.readInt(); // 读取消息长度bytebytes = new byte[length];
        in.readFully(bytes); // 读取指定长度的消息内容return new String(bytes);
    }
}

在上述示例代码中,TCPMessageHandler类封装了发送和接收消息的功能。在发送消息时,使用固定长度的消息头表示消息长度;在接收消息时,先读取消息头表示的长度,然后再读取指定长度的消息内容,从而避免了粘包和拆包的问题。

总之,在Java中处理TCP粘包和拆包的问题通常涉及到设计消息格式、消息长度的表示以及消息的解析等方面。合理地设计消息格式并使用合适的方法来解析消息,可以有效地避免TCP粘包和拆包导致的数据解析错误。

描述Java中的Selector机制及其在非阻塞IO中的应用。

在Java中,Selector(选择器)是Java NIO(New I/O)中的一个重要组件,用于实现非阻塞 I/O。Selector 提供了一种高效的方式来处理多个通道(Channel)的 I/O 事件,例如读、写和连接就绪等。下面我将详细描述 Java 中的 Selector 机制以及它在非阻塞 I/O 中的应用。

Selector 机制

Selector 是 Java NIO 中的一个关键组件,它允许单个线程处理多个 Channel 的 I/O 操作。Selector 通过轮询的方式检查注册在其上的多个 Channel,一旦某个 Channel 准备好进行 I/O 操作,就会通知程序进行相应的处理。这种方式可以大大提高 I/O 操作的效率,尤其适用于需要处理大量连接的服务器端程序。

在非阻塞 I/O 中的应用

在非阻塞 I/O 中,一个线程可以同时管理多个 Channel,而不需要为每个 Channel 创建一个单独的线程。这是通过 Selector 机制实现的。以下是在非阻塞 I/O 中使用 Selector 的一般步骤:

  1. 创建 Selector:通过调用 Selector.open() 方法创建一个 Selector 对象。
  2. 将 Channel 注册到 Selector:将需要进行 I/O 操作的 Channel 注册到 Selector 上,并指定感兴趣的 I/O 事件,比如读、写等。
  3. 轮询就绪的 Channel:通过调用 Selector 的 select() 方法来轮询已经准备好进行 I/O 操作的 Channel。
  4. 处理就绪的 Channel:一旦某个 Channel 准备好进行 I/O 操作,就可以通过遍历已选择的键集合(SelectionKey)来获取就绪的 Channel,并进行相应的 I/O 操作。
  5. 取消注册的 Channel:在完成了对某个 Channel 的 I/O 操作后,需要将其从 Selector 上取消注册,避免重复处理。

示例代码

下面是一个简单的示例代码,演示了如何在 Java 中使用 Selector 实现非阻塞 I/O:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NonBlockingServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式

        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 注册接受连接事件

        while (true) {
            selector.select(); // 轮询就绪的 Channel
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                if (key.isAcceptable()) {
                    ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                    SocketChannel clientChannel = serverChannel.accept();
                    clientChannel.configureBlocking(false);
                    clientChannel.register(selector, SelectionKey.OP_READ); // 注册读事件
                } else if (key.isReadable()) {
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int bytesRead = channel.read(buffer);
                    // 处理读取的数据
                    // ...
                }
                keyIterator.remove(); // 取消已处理的事件
            }
        }
    }
}

在上述示例代码中,通过 Selector 实现了一个简单的非阻塞服务器。服务器使用 Selector 轮询就绪的 Channel,并根据就绪的事件进行相应的处理。这种方式可以让服务器在单个线程内高效地处理多个客户端的连接和数据传输。

总之,Selector 是 Java NIO 中非常重要的组件,它提供了一种高效的方式来处理多个 Channel 的 I/O 事件。在非阻塞 I/O 中,Selector 可以帮助我们实现高性能的网络编程,特别适用于需要处理大量连接的服务器端程序。

如何在Java中使用SSL/TLS建立安全的网络通信?

在Java中使用SSL/TLS(安全套接层/传输层安全)建立安全的网络通信涉及到使用Java的SSL相关类库和API来配置和管理安全通信的过程。下面我将介绍在Java中如何使用SSL/TLS建立安全的网络通信的一般步骤。

  1. 准备证书和密钥

首先,需要准备服务器端和客户端的数字证书以及相应的私钥。数字证书可以通过证书颁发机构(CA)获得,也可以自行创建自签名证书。私钥用于对证书进行签名和加密。

  1. 配置SSLContext

在Java中,可以通过SSLContext类来配置SSL/TLS的安全参数,包括使用的协议版本、证书和密钥等。可以使用KeyManagerFactory和TrustManagerFactory来加载服务器端和客户端的证书和密钥。

  1. 创建SSLServerSocket和SSLSocket(服务器端和客户端)

在服务器端,可以使用SSLServerSocket来监听和接受SSL连接;在客户端,可以使用SSLSocket来发起SSL连接。

  1. 进行安全通信

一旦SSL连接建立,服务器端和客户端就可以进行安全的通信,包括加密和身份验证等操作。

示例代码

下面是一个简单的示例代码,演示了如何在Java中使用SSL/TLS建立安全的网络通信:

import javax.net.ssl.*;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;

public class SSLServer {
    public static void main(String[] args) throws Exception {
        // 加载服务器端证书和私钥
        char[] serverPassword = "serverPassword".toCharArray();
        KeyStore serverKeyStore = KeyStore.getInstance("JKS");
        InputStream serverKeyStoreFile = new FileInputStream("server.jks");
        serverKeyStore.load(serverKeyStoreFile, serverPassword);
        KeyManagerFactory serverKeyManagerFactory = KeyManagerFactory.getInstance("SunX509");
        serverKeyManagerFactory.init(serverKeyStore, serverPassword);

        // 创建SSLContext
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(serverKeyManagerFactory.getKeyManagers(), null, null);

        // 创建SSLServerSocket
        SSLServerSocketFactory sslServerSocketFactory = sslContext.getServerSocketFactory();
        SSLServerSocket sslServerSocket = (SSLServerSocket) sslServerSocketFactory.createServerSocket(8080);

        // 监听并接受SSL连接
        SSLSocket sslSocket = (SSLSocket) sslServerSocket.accept();

        // 进行安全通信
        // ...
    }
}

在上述示例代码中,通过加载服务器端的证书和私钥,创建SSLContext,并使用SSLServerSocketFactory创建SSLServerSocket,最终实现了在服务器端建立安全的SSL连接。

在客户端,可以使用类似的方式创建SSLSocket,并使用它进行SSL连接。总之,在Java中使用SSL/TLS建立安全的网络通信需要仔细配置SSLContext,并确保正确加载和使用证书、密钥等安全材料。这样可以确保通信过程中的数据加密和安全性。

描述Java加密扩展(JCE)中的密钥管理和数字签名过程。

查看更多

字节高级Java面试真题

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

如何在Java中实现自定义的类加载器?

在Java中实现自定义类加载器,通常需要继承ClassLoader类,并重写findClass方法来指定你的类加载逻辑。以下是一个简单的自定义类加载器的示例:

import java.io.*;

public class MyClassLoader extends ClassLoader {

    private String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    private byte[] loadClassData(String name) {
        // 将包路径中的"."替换为文件系统的路径分隔符"/"
        name = name.replace(".", "/");
        String filePath = classPath + "/" + name + ".class";
        InputStream is = null;
        ByteArrayOutputStream baos = null;
        try {
            is = new FileInputStream(filePath);
            baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = is.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) is.close();
                if (baos != null) baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] data = loadClassData(name);
        if (data == null) {
            throw new ClassNotFoundException();
        }
        return defineClass(name, data, 0, data.length);
    }

    public static void main(String[] args) {
        MyClassLoader classLoader = new MyClassLoader("path_to_classes");
        try {
            Class<?> clazz = classLoader.loadClass("com.example.MyClass");
            Object obj = clazz.newInstance();
            System.out.println("Class loaded by: " + obj.getClass().getClassLoader());
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,MyClassLoader重写了findClass方法,它使用loadClassData方法从文件系统中读取类的字节码。loadClassData方法将类的全限定名转换为文件系统路径,并从指定路径读取.class文件,将其转换为字节数组。

main方法中,我们创建了一个MyClassLoader实例,并尝试加载一个名为com.example.MyClass的类。如果类文件位于path_to_classes/com/example/MyClass.class路径下,类加载器将能够找到并加载它。

自定义类加载器可以用于许多高级场景,例如加载网络上的类,实现热部署,或者加载加密的类文件等。在实现自定义类加载器时,应该注意类加载的委托机制和安全性问题。

解释Java内存模型,并讨论它对并发编程的影响。

Java内存模型(Java Memory Model,JMM)是一种抽象的概念,它描述了Java虚拟机(JVM)在计算机内存中如何存储数据,以及线程如何通过内存与其他线程交互。JMM解决了多线程环境中的可见性、原子性、有序性问题,并定义了线程如何以及何时可以看到其他线程写入的值。

JMM的主要组件和概念包括:

  1. 主内存与工作内存:JMM区分了主内存(所有线程共享的内存区域,用于存储实例字段、静态字段和构成数组的元素)和工作内存(每个线程私有的内存缓冲区,包含了线程使用的变量的副本)。
  2. 内存操作:包括读取(read)、加载(load)、使用(use)、赋值(assign)、存储(store)和写入(write)操作。
  3. 内存屏障:JMM使用内存屏障来插入指令,以防止某些代码的执行顺序被重排序,从而保证特定的内存可见性和有序性。
  4. 原子性:JMM保证了基本读写操作的原子性,例如对volatile变量的读/写,以及对final变量的写入和构造函数退出后的读取。
  5. 可见性:JMM通过volatile关键字、锁(synchronized blocks)、final域等机制提供了内存可见性保证,确保一个线程对共享变量的修改能够及时地被其他线程看到。
  6. 有序性:JMM禁止编译器和处理器对代码执行顺序进行重排序,以保证在单线程环境下代码的执行顺序不会影响最终结果。但在多线程环境下,JMM允许重排序,只要不违反happens-before原则。

Happens-before原则是JMM中最核心的概念之一,它定义了一个全局的顺序,规定了在没有其他同步手段的情况下,一个操作的结果必须对另一个操作可见。以下是一些基本的happens-before规则:

  • 程序顺序规则:一个线程内,按照代码顺序,前面的操作happens-before于后续的操作。
  • 锁定规则:一个unlock操作happens-before于后面对同一个锁的lock操作。
  • volatile变量规则:对一个volatile字段的写操作happens-before于后续对这个volatile字段的读操作。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

JMM对并发编程的影响是深远的,它为开发者提供了一套规则和保证,使得并发程序的编写变得可预测,并且可以在不同的JVM实现和硬件平台上保持一致的行为。然而,正确理解和使用JMM也是并发编程中的一个挑战,开发者需要确保对共享变量的访问和修改是安全的,并且要意识到潜在的竞争条件和内存一致性错误。

如何优化Java程序的CPU和内存使用?

优化Java程序的CPU和内存使用是一个复杂的过程,涉及到代码层面的优化、算法改进、数据结构选择以及运行时的JVM调优。以下是一些通用的策略:

代码层面的优化:

  1. 避免不必要的对象创建:尽量重用对象,避免频繁创建和销毁对象,特别是在循环和高频调用的方法中。
  2. 使用高效的算法和数据结构:选择合适的算法和数据结构可以大幅提高程序性能,例如使用HashMap而不是List来进行快速查找。
  3. 减少冗余计算:缓存计算结果,避免在每次调用时都重新计算。
  4. 延迟初始化:仅在实际需要时才初始化对象,可以减少内存的使用。
  5. 优化循环:减少循环内部的计算量,移除不必要的循环。
  6. 使用基本类型而非包装类:尽量使用int等基本类型,而不是Integer这样的包装类型,以减少内存消耗和避免自动装箱拆箱的开销。
  7. 并发和多线程优化:合理使用并发和多线程可以提高CPU的利用率,但需要注意线程安全和避免线程竞争。

JVM调优:

  1. 垃圾收集器选择和调优:根据应用的特点选择合适的垃圾收集器(如G1, CMS, ZGC等),并调整相关参数以优化GC行为。
  2. 堆内存分配:合理分配JVM堆内存的大小,避免频繁的垃圾回收或内存溢出。
  3. 调整线程栈大小:可以通过-Xss参数调整线程栈的大小,避免不必要的内存占用。
  4. JVM内联和编译优化:JVM会对热点代码进行内联和即时编译优化,确保这些优化正常进行。
  5. 使用JVM性能监控工具:如JProfiler, VisualVM等工具可以帮助识别性能瓶颈。

代码分析和性能监控:

  1. 分析CPU使用情况:使用工具(如JProfiler, Java Mission Control)来分析哪些方法或线程占用了过多CPU。
  2. 内存泄漏检测:使用内存分析工具(如Eclipse Memory Analyzer)来检测内存泄漏。
  3. 代码剖析:使用剖析工具来分析代码的运行时间和资源消耗,识别瓶颈。
  4. 日志记录和监控:合理的日志记录可以帮助在问题发生时快速定位问题。
  5. 性能测试和基准测试:定期进行性能测试和基准测试,确保优化的效果符合预期。

最佳实践:

  1. 代码审查:定期进行代码审查,可以发现并修正潜在的性能问题。
  2. 文档和指南:遵循Java性能优化的最佳实践和指南。
  3. 持续集成和持续部署(CI/CD):在CI/CD流程中集成性能测试,确保代码变更不会引入新的性能问题。

优化Java程序的CPU和内存使用是一个持续的过程,需要不断地监控、分析和调整。通过上述策略,你可以显著提高Java程序的性能和资源利用效率。

Java中的finalize()方法有哪些缺陷?

在Java中,finalize()方法是Object类的一个方法,它被设计为在垃圾收集器决定回收对象内存之前给对象一个清理资源的机会。然而,finalize()方法存在多个缺陷,导致它在实际开发中被不推荐使用甚至在Java 9中被标记为废弃(Deprecated)。

以下是finalize()方法的一些主要缺陷:

  1. 不确定性finalize()方法的调用时机是不确定的,因为它依赖于垃圾收集器的运行,而垃圾收集器的执行时机是不可预测的。这意味着你无法知道资源什么时候会被释放。
  2. 性能开销:对象有finalize()方法会给垃圾收集带来额外的负担。这些对象会被放在一个叫做finalization queue的队列中,需要单独处理,这会延迟它们的回收过程,并增加垃圾收集的复杂性。
  3. 可能导致内存泄漏:如果在finalize()方法中对象被重新引用(比如被赋值给某个类变量),那么这个对象可能不会被垃圾收集器回收,从而导致内存泄漏。
  4. 无法保证被调用:如果JVM提前退出,那么finalize()方法可能根本不会被执行。因此,依赖finalize()来释放资源是不可靠的。
  5. 异常问题:如果finalize()方法抛出异常,并且没有被捕获,那么垃圾收集器将忽略这个异常,而且不会再次调用该对象的finalize()方法。这可能会导致资源无法正确清理。
  6. 安全问题finalize()方法可能会被恶意子类覆盖,用于对象复活(resurrection)或者资源窃取。

鉴于上述缺陷,Java开发者应该避免使用finalize()方法来清理资源。取而代之,可以使用以下替代方案:

  • try-with-resources语句:自Java 7起,用于自动管理实现了AutoCloseable或Closeable接口的资源对象。
  • 显式清理:提供一个显式的清理方法(如close()dispose()),并在使用对象的地方确保调用这个方法。
  • 清理器(Cleaner)和PhantomReference:Java 9引入了java.lang.ref.Cleaner类,它提供了一种更灵活和可靠的方式来清理资源,而不需要依赖于垃圾收集器的不确定性。

总之,finalize()方法由于其不可预测性和潜在的风险,不应该被用作清理资源的主要手段。开发者应该寻求更稳定和可控的资源管理方式。

如何优化Java垃圾收集器的性能?

优化Java垃圾收集器(GC)的性能通常涉及到选择合适的垃圾收集器、调整GC相关参数以及优化应用程序的内存使用。以下是一些具体的步骤和策略:

选择合适的垃圾收集器:

  1. 了解不同垃圾收集器:Java提供了多种垃圾收集器,如Serial GC、Parallel GC、Concurrent Mark Sweep (CMS) GC、G1 GC、ZGC、Shenandoah GC等,每种收集器都有其适用场景和特点。
  2. 根据应用需求选择:选择垃圾收集器时,需要考虑应用的需求,如吞吐量、延迟、内存占用等。例如,对于延迟敏感的应用,可能更适合使用G1 GC、ZGC或Shenandoah GC。

调整GC参数:

  1. 堆大小(-Xms和-Xmx):适当地设置JVM堆的初始大小(-Xms)和最大大小(-Xmx)可以减少垃圾收集的频率,但设置得过大可能会导致长时间的GC停顿。
  2. 新生代大小(-Xmn):调整新生代的大小可以影响对象晋升到老年代的速度,以及新生代和老年代之间的垃圾收集频率。
  3. Eden与Survivor区比例:调整Eden区和Survivor区的比例可以优化对象在新生代的存活周期。
  4. 垃圾收集器特定参数:各个垃圾收集器都有自己的特定参数,可以调整以优化性能,如G1 GC的-XX:MaxGCPauseMillis参数可以设置目标停顿时间。
  5. 并行GC线程数(-XX:ParallelGCThreads):对于并行垃圾收集器,可以调整并行GC线程数以匹配系统的CPU核心数。

应用程序优化:

  1. 减少内存分配速率:减少对象的创建和短生命周期对象的数量可以减轻垃圾收集器的压力。
  2. 优化数据结构:选择更合适的数据结构可以减少内存占用和提高效率。
  3. 避免内存泄漏:确保及时释放不再使用的对象引用,避免内存泄漏。
  4. 使用对象池:对于频繁创建和销毁的对象,使用对象池可以减少垃圾收集的负担。
  5. 减少大对象的分配:大对象(如大数组)直接分配在老年代,频繁分配可能导致早期晋升或大型对象的GC停顿。

监控和调试:

  1. 使用监控工具:使用JVM监控和分析工具(如JConsole、VisualVM、JProfiler等)来监控GC活动和内存使用情况。
  2. GC日志:开启GC日志(-Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps等)可以帮助分析GC行为和性能。
  3. 分析GC日志:使用GC日志分析工具(如GCViewer、GCEasy等)来分析GC日志,找出GC性能瓶颈。
  4. 测试和调整:在实际的生产环境中进行测试,根据应用的实际表现调整GC参数。

优化GC性能是一个迭代过程,需要不断地监控、分析和调整。通过上述方法,可以显著改善Java应用程序的GC性能和整体性能。

如何在MySQL中优化大表的查询性能?

查看更多

阿里高级java面试真题

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

大的灵活性,但它也可能带来一些性能上的影响:

  1. 性能开销:由于反射是在运行时进行的,因此它通常比直接调用代码要慢。例如,通过反射调用方法会比直接调用方法的性能开销要大。
  2. 编译器优化限制:由于反射操作是在运行时动态确定的,因此编译器无法进行一些优化。这可能会导致一些性能上的损失。
  3. 安全性问题:反射机制可以绕过访问控制,可以访问私有成员,这可能会导致安全性问题。

因此,在使用反射时需要权衡灵活性和性能之间的关系。通常情况下,如果不是必须使用反射,最好避免使用它来提高性能。如果需要频繁使用反射,可以考虑使用缓存机制来减少性能开销。

请描述Java中的弱引用、软引用、幻象引用的区别和用途。

在Java中,除了普通的强引用外,还存在着弱引用、软引用和幻象引用,它们在内存管理和对象生命周期控制方面发挥着重要作用。下面我将分别介绍它们的区别和用途:

  1. 强引用(Strong Reference):
  2. 强引用是最常见的引用类型,当一个对象被强引用关联时,即使内存不足,垃圾回收器也不会回收该对象。例如:Object obj = new Object(); // obj是一个强引用

2. 弱引用(Weak Reference):

弱引用是一种比较弱的引用类型,当一个对象只被弱引用关联时,垃圾回收器在下一次回收时就会回收这个对象。弱引用通常用于实现缓存,当缓存中的对象不再被强引用时,可以被及时回收。例如:

WeakReference<Object> weakRef = new WeakReference<>(new Object());

3. 软引用(Soft Reference):

软引用是介于弱引用和强引用之间的一种引用类型,当内存不足时,垃圾回收器会尝试回收被软引用关联的对象,但只有在内存不足的情况下才会回收。软引用通常用于实现缓存,可以在内存不足时释放缓存对象,避免OutOfMemoryError的发生。例如:

SoftReference<Object> softRef = new SoftReference<>(new Object());

4. 幻象引用(Phantom Reference):

幻象引用是最弱的一种引用类型,它主要用于跟踪对象被垃圾回收器回收的活动。幻象引用在被回收时会被放入一个ReferenceQueue中,通过监控ReferenceQueue可以知道对象何时被垃圾回收器回收。例如:

ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);

总结:

  • 强引用是最常见的引用类型,对象只要被强引用关联,就不会被回收。
  • 弱引用、软引用和幻象引用都是通过java.lang.ref包中的类来实现的,它们在内存管理和对象生命周期控制方面提供了灵活性。
  • 弱引用和软引用通常用于实现缓存,幻象引用主要用于对象回收跟踪。

如何在Java中实现自定义注解处理器?

在Java中,注解(Annotation)是一种用于类、方法、变量、参数等元素的元数据形式。注解本身不直接影响程序的操作,但可以被注解处理器(Annotation Processor)在编译时或运行时读取和处理,来实现特定的功能。

要实现一个自定义注解处理器,你需要完成以下几个步骤:

1. 定义注解

首先,你需要定义一个或多个注解类型。注解的定义使用@interface关键字,可以指定一些元素作为注解的属性。例如:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE) // 表明这个注解只在源码级别保留,不会编译到字节码中
@Target(ElementType.TYPE) // 表明这个注解可以用在类上
public @interface CustomAnnotation {
    String value() default ""; // 注解的一个属性
}

2. 实现注解处理器

注解处理器是一种特殊的工具,它在Java编译器编译代码的过程中运行。你需要创建一个类来实现javax.annotation.processing.Processor接口或者继承javax.annotation.processing.AbstractProcessor类。

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.Element;
import javax.tools.Diagnostic;

import com.google.auto.service.AutoService;
import java.util.Set;

@AutoService(Processor.class) // 使用Google的auto-service库来自动生成配置信息
public class CustomAnnotationProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        // 初始化处理器,可以获取到一些有用的工具类
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(CustomAnnotation.class)) {
            // 处理被@CustomAnnotation注解的元素
            String message = "Found @CustomAnnotation at " + element;
            processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message);
        }
        return true; // 表示注解已经被处理,不需要后续处理器再处理
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Set.of("your.package.name.CustomAnnotation"); // 支持的注解类型
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported(); // 支持的源码版本
    }
}

3. 注册注解处理器

你需要在你的项目中创建META-INF/services/javax.annotation.processing.Processor文件,然后在文件中指定你的注解处理器的全限定名。如果你使用了auto-service库,这一步可以自动完成。

your.package.name.CustomAnnotationProcessor

4. 使用注解和编译

最后,你可以在你的代码中使用自定义的注解,并通过Java编译器编译代码。如果你正确实现了注解处理器,编译器在编译过程中会自动调用你的处理器。

注意事项

  • 注解处理器在编译时运行,不会影响运行时性能。
  • 注解处理器通常用于生成额外的源代码、资源文件或者编译时校验。
  • 如果你使用了构建工具(如Maven或Gradle),确保你的注解处理器在编译路径上正确配置。

通过上述步骤,你可以实现自定义的注解处理器,在编译时对注解进行处理,以实现强大的代码生成和校验功能。

如何在MySQL中实现和优化分区表?

查看更多

大厂面试题集

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

阿里高级java面试真题

字节高级Java面试真题

哔哩哔哩高级Java面试真题

百度高级java面试真题

360高级java面试真题

京东高级Java面试真题

华为高级Java面试真题

滴滴高级Java面试真题

酷狗高级Java面试真题

快手高级Java面试真题

美团高级Java面试真题

蚂蚁金服高级Java面试真题

顺丰高级Java面试真题

三七互娱高级Java面试真题

腾讯高级Java面试真题

网易高级Java面试真题

携程高级Java面试真题

小红书高级Java面试真题

新浪微博高级Java面试真题

小米高级Java面试真题

Java程序员如何写简历

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

一、优秀简历撰写思路


⼀份优秀的简历,需要具备两个特点:
1、 让招聘者看了很舒服,不会看了就想丢垃圾桶⾥。
2、 让招聘者能很快看到⾃⼰想要看到的东⻄。

招聘者可能每天都需要看上百份,⼀份简历的查看时间可能是按秒计算,没那么多时间去从⼀堆垃圾信息中寻找亮点,总结下来就两个词:简洁⼯整、突出重点。

1)简洁⼯整
简历整体排版上要⼯整、结构清晰,字体尽量保持⼀致。
⻚数不超过2⻚,经验较少的1⻚,较多的2⻚,超过了,说明你肯定说废话了。
写完检查⼏遍,不要有错别字,这个会让招聘者觉得这个⼈不细⼼或者不重视。
⽩底⿊字,不要有太多花⾥胡哨的颜⾊,在标题部分可以有⼀些⾊彩,但是不要太多,不要太刺眼。
保存时使⽤PDF,这个细节很多⼈不知道,WORD⽂档在不同设备上的格式排版可能会乱掉,导致根本没法看,所以记得⼀定要⽤PDF。

2)突出重点
关键的信息可以放在显眼的位置、加粗、⽤较⼤的字号来突出体现等等,让招聘者能更容易看到。
⽆关紧要的内容不⽤写,简历不是⽐谁字数更多。⼀个⽐较简单的判断标准是:这个内容会让你看起来变得⽜吗,如果⼀点也不会,那就删了吧。
尽量使⽤总结性的话语概括⽽不是冗⻓的描述,可以参考STAR法则。简单来说就是:遇到了什么问题、怎么解决的、带来了多少价值。


二、简历应该包含哪些模块


⼀份标准的简历通常应该有以下⼏个模块:
基本信息、教育背景、专业技能、⼯作和项⽬经历、⾃我评价。如果以前有获得过⼀些荣誉,还有可以有个曾获荣誉。


1、基本信息
这⼀栏没啥好说的,放在简历第⼀栏,然后整⻬、完整的写好必要的信息就⾏了。必要的⼏项:姓名、性别、出⽣年⽉、电话、邮箱、现居住地、期望⼯作地、⼯作年限。
BTW:写上籍贯:说不定碰到⽼乡就放⽔了;这⼀模块有⼀个容易有分歧的问题是:要不要放个⼈照⽚。因为这⼀项并不是必须的,当⼀个选项不是必须的时候,你不放就不会给别⼈扣分的机会,如果你放了就给机会了。当然,如果有⽼弟对⾃⼰的相貌⾮常⾃信的话,hr⼜通常是妹⼦,可以尝试放⼀下,说不定就加分了。


2、教育背景
通常放在基本信息下⾯,每⼀段经历⼀⾏,也是整⻬、完整的写好必要的信息就⾏了。从⼤学的经历写起,通常包含:时间、学校、专业即可。如果成绩优秀的可以将平均学分绩点啥的加上。有些⼈会罗列⾃⼰学过的学科,什么⾼等数学、离散数学,这些其实就不⽤了,基本不会有⼈关⼼这个。


3、专业技能
专业技能是简历⾥⽐较关键的⼀项,这⼀栏的⼏个关键点:
1) 罗列出你会的技能,例如HashMap、MySQL、JVM、Redis等等。
2) 你对该技能的掌握程度:了解、熟悉、熟练(掌握)、精通。这边精通最好慎⽤,否则⾯试官会很友好的像你“请教”⼀下你所精通的技能。
3) 技能不是随便罗列,要分组,例如:IO、集合、多线程、反射这些可以放⼀⾏,都是属于Java基础。常⻅的分组有:计算机基础、Java基础、SSM框架、JVM、SQL、分布式和中间件、设计模式等。
4) 全部内容最好控制在⼗⾏或者以内,太⻓了就容易没有重点。
5) 突出亮点。对于亮点不多的同学来说,可以写的更细⼀点,看过哪些源码,可以直接写上去,例如:深⼊学习过HashMap、ArrayList、LinkedList等常⽤集合的源码。对于经验丰富亮点较多的同学来说,可以写的总结⼀点,例如:对常⽤的JDK源码有深⼊的研究。
6)⾮本岗位的相关技能,例如很常⻅的前端相关知识:Html、js、ajax等。我觉得如果内容实在不多的话,可以写⼀下,但是如果内容已经很多了,那就可以不写了。以下是Java常⽤的技术栈简单列了⼀个模板,如下:


4、⼯作(项⽬)经历
简历⾥另外⼀个⽐较关键的⼀项,⼯作经历和项⽬经历可以写⼀起,按时间倒序,按公司分组。主要包含⼏个信息:⼯作时间、公司名称、项⽬名称、担任的⻆⾊、项⽬背景、主要职责。


1)⼯作时间:正常写即可。
2)公司名称:这边有个重点,有的同学⽐较⽼实,会写合同上的公司名称,例如:上海拉扎斯信息科技有限公司、北京三快在线科技有限公司、上海寻梦科技有限公司。这边其实没有必要,直接写公司更⼴为⼈知的名字即可,例如:饿了么、美团、拼多多,不然万⼀有招聘者不知道被刷就亏⼤了。
3)项⽬名称:也是正常写就ok。
4)担任的⻆⾊:如果只是普通的开发者,可以写核⼼开发,如果是主要负责,则可以写XX模块负责⼈、XX域负责⼈、项⽬负责⼈。
5)项⽬背景:⽐较关键的⼀项信息,项⽬背景要尽量简短,最好不超过2⾏,并且⼜要能让招聘者看出你究竟做了个什么东⻄,同时还要尽量看起来⽜X点。这个⽐较考验总结能⼒,可以多花点时间想想,然后可以让其他没有参与过该项⽬的同学模拟看⼀看是否能看得懂。
6)主要职责:本栏最重要的内容,内容可以是:做过的⽜X需求、解决过的复杂问题等等。关键词还是:尽量简短、突出重点、STAR法则。
⼏个常⻅的样例如下,招聘者喜欢看到的点:
样例1:参与XX项⽬从0到1的搭建,负责项⽬的架构设计、技术选型和整体环境搭建。
样例2:引⼊了XX技术/框架,解决了XX问题,提升了XX指标50%。
样例3:搭建XX⼯具,实现了XX功能的可视化、配置化、⾃动化(智能化)。
样例4:沉淀了XX技术,复⽤到XX个部⻔/⼩组,提升了开发效率50%。
样例5:负责系统整体的稳定性保障,使⽤隔离、熔断、降级、限流等,保障了系统可⽤性99.99%以上、XX指标99.99%以上。
样例6:负责X个⼈的⼩团队,团队同学在部⻔表现突出,XX指标排名第1,获得XX荣誉。

查看更多

分布式事务

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

简介

# 事务

事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。事务应该具有 4 个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为 ACID 特性。

# 分布式事务

分布式事务是指事务的参与者,支持事务的服务器,资源服务器以及事务管理器分别位于分布式系统的不同节点之上。通常一个分布式事务中会涉及对多个数据源或业务系统的操作。分布式事务也可以被定义为一种嵌套型的事务,同时也就具有了ACID事务的特性。

# 强一致性、弱一致性、最终一致性

强一致性

任何一次读都能读到某个数据的最近一次写的数据。系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致。简言之,在任意时刻,所有节点中的数据是一样的。

弱一致性

数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。

最终一致性

不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。简单说,就是在一段时间后,节点间的数据会最终达到一致状态。

由于分布式事务方案,无法做到完全的ACID的保证,没有一种完美的方案,能够解决掉所有业务问题。因此在实际应用中,会根据业务的不同特性,选择最适合的分布式事务方案。

# 分布式事务的基础

# CAP理论

Consistency(一致性):数据一致更新,所有数据变动都是同步的(强一致性)。

Availability(可用性):好的响应性能。

Partition tolerance(分区容错性) :可靠性。

定理:任何分布式系统只可同时满足二点,没法三者兼顾。

CA系统(放弃P):指将所有数据(或者仅仅是那些与事务相关的数据)都放在一个分布式节点上,就不会存在网络分区。所以强一致性以及可用性得到满足。

CP系统(放弃A):如果要求数据在各个服务器上是强一致的,然而网络分区会导致同步时间无限延长,那么如此一来可用性就得不到保障了。坚持事务ACID(原子性、一致性、隔离性和持久性)的传统数据库以及对结果一致性非常敏感的应用通常会做出这样的选择。

AP系统(放弃C):这里所说的放弃一致性,并不是完全放弃数据一致性,而是放弃数据的强一致性,而保留数据的最终一致性。如果即要求系统高可用又要求分区容错,那么就要放弃一致性了。因为一旦发生网络分区,节点之间将无法通信,为了满足高可用,每个节点只能用本地数据提供服务,这样就会导致数据不一致。一些遵守BASE原则数据库,(如:Cassandra、CouchDB等)往往会放宽对一致性的要求(满足最终一致性即可),一次来获取基本的可用性。

# BASE理论

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。是对CAP中AP的一个扩展。

  1. 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。
  2. 软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是CAP中的不一致。
  3. 最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。

BASE解决了CAP中理论没有网络延迟,在BASE中用软状态和最终一致,保证了延迟后的一致性。BASE和 ACID 是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。

# 分布式事务解决方案

分布式事务的实现主要有以下 6 种方案:

  • 2PC 方案
  • TCC 方案
  • 本地消息表
  • MQ事务
  • Saga事务
  • 最大努力通知方案

# 2PC方案

2PC方案分为两阶段:

第一阶段:事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交.

第二阶段:事务协调器要求每个数据库提交数据,或者回滚数据。

优点: 尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于MySQL是从5.5开始支持。

缺点:

  • 单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。
  • 同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。
  • 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。

总的来说,2PC方案比较简单,成本较低,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其最大的弱点。

# TCC

TCC 的全称是:TryConfirmCancel

  • Try 阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行 锁定或者预留
  • Confirm 阶段:这个阶段说的是在各个服务中执行实际的操作。
  • Cancel 阶段:如果任何一个服务的业务方法执行出错,那么这里就需要 进行补偿,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚)

举个简单的例子如果你用100元买了一瓶水, Try阶段:你需要向你的钱包检查是否够100元并锁住这100元,水也是一样的。

如果有一个失败,则进行cancel(释放这100元和这一瓶水),如果cancel失败不论什么失败都进行重试cancel,所以需要保持幂等。

如果都成功,则进行confirm,确认这100元扣,和这一瓶水被卖,如果confirm失败无论什么失败则重试(会依靠活动日志进行重试)。

这种方案说实话几乎很少人使用,但是也有使用的场景。因为这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大。

# 本地消息表

查看更多

Git面试题

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

版本控制

什么是版本控制

版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。 除了项目源代码,你可以对任何类型的文件进行版本控制。

为什么要版本控制

有了它你就可以将某个文件回溯到之前的状态,甚至将整个项目都回退到过去某个时间点的状态,你可以比较文件的变化细节,查出最后是谁修改了哪个地方,从而找出导致怪异问题出现的原因,又是谁在何时报告了某个功能缺陷等等。

本地版本控制系统

许多人习惯用复制整个项目目录的方式来保存不同的版本,或许还会改名加上备份时间以示区别。 这么做唯一的好处就是简单,但是特别容易犯错。 有时候会混淆所在的工作目录,一不小心会写错文件或者覆盖意想外的文件。

为了解决这个问题,人们很久以前就开发了许多种本地版本控制系统,大多都是采用某种简单的数据库来记录文件的历次更新差异。

本地版本控制系统

集中化的版本控制系统

接下来人们又遇到一个问题,如何让在不同系统上的开发者协同工作? 于是,集中化的版本控制系统(Centralized Version Control Systems,简称 CVCS)应运而生。

集中化的版本控制系统都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。

集中化的版本控制系统

这么做虽然解决了本地版本控制系统无法让在不同系统上的开发者协同工作的诟病,但也还是存在下面的问题:

  • 单点故障: 中央服务器宕机,则其他人无法使用;如果中心数据库磁盘损坏又没有进行备份,你将丢失所有数据。本地版本控制系统也存在类似问题,只要整个项目的历史记录被保存在单一位置,就有丢失所有历史更新记录的风险。
  • 必须联网才能工作: 受网络状况、带宽影响。

分布式版本控制系统

于是分布式版本控制系统(Distributed Version Control System,简称 DVCS)面世了。 Git 就是一个典型的分布式版本控制系统。

这类系统,客户端并不只提取最新版本的文件快照,而是把代码仓库完整地镜像下来。 这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。 因为每一次的克隆操作,实际上都是一次对代码仓库的完整备份。

分布式版本控制系统

分布式版本控制系统可以不用联网就可以工作,因为每个人的电脑上都是完整的版本库,当你修改了某个文件后,你只需要将自己的修改推送给别人就可以了。但是,在实际使用分布式版本控制系统的时候,很少会直接进行推送修改,而是使用一台充当“中央服务器”的东西。这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。

分布式版本控制系统的优势不单是不必联网这么简单,后面我们还会看到 Git 极其强大的分支管理等功能。

认识 Git

Git 简史

Linux 内核项目组当时使用分布式版本控制系统 BitKeeper 来管理和维护代码。但是,后来开发 BitKeeper 的商业公司同 Linux 内核开源社区的合作关系结束,他们收回了 Linux 内核社区免费使用 BitKeeper 的权力。 Linux 开源社区(特别是 Linux 的缔造者 Linus Torvalds)基于使用 BitKeeper 时的经验教训,开发出自己的版本系统,而且对新的版本控制系统做了很多改进。

Git 与其他版本管理系统的主要区别

查看更多

Maven面试题

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

Maven 是强大的构建工具,能够帮我们自动化构建过程–清理、编译、测试、打包和部署。比如测试,我们无需告诉 maven 如何去测试,只需遵循 maven 的约定编写好测试用例,当我们运行构建的时候,这些测试就会自动运行。

Maven 不仅是构建工具,还是一个依赖管理工具和项目信息管理工具。它提供了中央仓库,能帮助我们自动下载构件。

# 配置

配置用户范围 settings.xml。M2_HOME/conf/settings.xml 是全局范围的,而~/.m2/settings.xml 是用户范围的。配置成用户范围便于 Maven 升级。若直接修改 conf 目录下的 settings.xml,每次 Maven 升级时,都需要直接 settings.xml 文件。

# 入门

# 编写测试代码

# 添加 junit 依赖

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.7</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

maven 会自动访问中央仓库,下载 junit 依赖。scope 为 test 表明依赖只对测试有效,即测试代码中的 import JUnit 代码没有问题,而主代码中使用 import JUnit 代码,则会产生编译错误。

# 编译

主类 HelloWorld

public class HelloWorld {
    public String sayHello() {
        return "Hello world";
    }
    public static void main(String[] args) {
        System.out.println(new HelloWorld().sayHello());
    }
}

编译代码:mvn clean compile

clean 清理输出目录/target,compile 编译项目主代码。

# 测试代码

public class HelloWorldTest {
    @Test
    public void testHelloWord() {
        HelloWorld hw = new HelloWorld();
        hw.sayHello();
    }
}

# 执行测试

mvn clean test

测试结果:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.tyson.test.HelloWorldTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.117 sec

# 打包和安装

打包:mvn clean package 将项目代码打包成 jar 包,位于/target 目录。

安装:mvn clean install,将项目输出的 jar 包安装到 maven 本地库,这样其他 maven 项目就可以直接引用这个 jar 包。

默认打包生成的 jar 不能直接运行,为了生成可运行的 jar 包,需要借助 maven-shade-plugin。

    <build>
        <finalName>HelloWorld</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>1.2.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.tyson.maven.HelloWorld</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

执行顺序:compile->test->package->install

# 依赖

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.7</version>
    <scope>test</scope>
</dependency>

  • scope:依赖的范围
  • type:依赖的类型,jar 或者 war
  • exclusions:用来排除传递性依赖

# 依赖范围 scope

maven 在编译、测试和运行项目时会使用不同的 classpath(编译classpath、测试 classpath、运行 classpath)。依赖范围就是用来控制依赖和这三种 classpath 的关系。maven 中有以下几种依赖范围:

  • compile:默认值,使用该依赖范围的 maven 依赖,在编译、测试和运行时都需要使用该依赖
  • test:只对测试 classpath 有效,在编译主代码和运行项目时无法使用此类依赖。如 JUnit 只在编译测试代码和运行测试的时候才需要此类依赖
  • provided:已提供依赖范围。对于编译和测试 classpath 有效,但在运行时无效。如 servlet-api,编译和测试时需要该依赖,但在运行项目时,由于容器已经提供此依赖,故不需要 maven重复引入
  • runtime:运行时依赖范围。对于测试和运行 classpath 有效,但在编译主代码时无效。如 JDBC 驱动实现,项目主代码的编译只需要 JDK 提供的 JDBC接口,只有在测试和运行时才需要实现 JDBC 接口的具体实现
  • system:系统依赖范围
  • import:导入依赖范围 import 导入依赖管理

# 传递性依赖

假如项目 account 有一个 compile 范围的 spring-core 依赖,而 spring-core 有一个 compile 范围的 common-logging 依赖,那么 common-logging 就会成为 account 的 compile 范围依赖,common-logging 是 account 的一个传递性依赖。maven 会直接解析各个直接依赖的 POM,将那些必要的间接依赖,以传递性依赖的形式引入到项目中。

spring-core 是 account 的第一直接依赖,common-logging 是 spring-core 的第二直接依赖,common-logging 是 account 的传递性依赖。第一直接依赖的范围和第二直接依赖的范围共同决定了传递性依赖的范围。下表左边是第一直接依赖的范围,上面一行是第二直接依赖的范围,中间部分是传递依赖的范围

# 排除依赖

查看更多

全局唯一ID生成方案

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

传统的单体架构的时候,我们基本是单库然后业务单表的结构。每个业务表的ID一般我们都是从1增,通过AUTO_INCREMENT=1设置自增起始值,但是在分布式服务架构模式下分库分表的设计,使得多个库或多个表存储相同的业务数据。这种情况根据数据库的自增ID就会产生相同ID的情况,不能保证主键的唯一性。

如上图,如果第一个订单存储在 DB1 上则订单 ID 为1,当一个新订单又入库了存储在 DB2 上订单 ID 也为1。我们系统的架构虽然是分布式的,但是在用户层应是无感知的,重复的订单主键显而易见是不被允许的。那么针对分布式系统如何做到主键唯一性呢?

# UUID

UUID (Universally Unique Identifier),通用唯一识别码的缩写。UUID是由一组32位数的16进制数字所构成,所以UUID理论上的总数为 1632=2128,约等于 3.4 x 10^38。也就是说若每纳秒产生1兆个UUID,要花100亿年才会将所有UUID用完。

生成的UUID是由 8-4-4-4-12格式的数据组成,其中32个字符和4个连字符’ – ‘,一般我们使用的时候会将连字符删除 uuid.toString().replaceAll("-","")

目前UUID的产生方式有5种版本,每个版本的算法不同,应用范围也不同。

  • 基于时间的UUID – 版本1: 这个一般是通过当前时间,随机数,和本地Mac地址来计算出来,可以通过 org.apache.logging.log4j.core.util包中的 UuidUtil.getTimeBasedUuid()来使用或者其他包中工具。由于使用了MAC地址,因此能够确保唯一性,但是同时也暴露了MAC地址,私密性不够好。
  • DCE安全的UUID – 版本2 DCE(Distributed Computing Environment)安全的UUID和基于时间的UUID算法相同,但会把时间戳的前4位置换为POSIX的UID或GID。这个版本的UUID在实际中较少用到。
  • 基于名字的UUID(MD5)- 版本3 基于名字的UUID通过计算名字和名字空间的MD5散列值得到。这个版本的UUID保证了:相同名字空间中不同名字生成的UUID的唯一性;不同名字空间中的UUID的唯一性;相同名字空间中相同名字的UUID重复生成是相同的。
  • 随机UUID – 版本4 根据随机数,或者伪随机数生成UUID。这种UUID产生重复的概率是可以计算出来的,但是重复的可能性可以忽略不计,因此该版本也是被经常使用的版本。JDK中使用的就是这个版本。
  • 基于名字的UUID(SHA1) – 版本5 和基于名字的UUID算法类似,只是散列值计算使用SHA1(Secure Hash Algorithm 1)算法。

我们 Java中 JDK自带的 UUID产生方式就是版本4根据随机数生成的 UUID 和版本3基于名字的 UUID,有兴趣的可以去看看它的源码。

public static void main(String[] args) {

    //获取一个版本4根据随机字节数组的UUID。
    UUID uuid = UUID.randomUUID();
    System.out.println(uuid.toString().replaceAll("-",""));

    //获取一个版本3(基于名称)根据指定的字节数组的UUID。
    byte[] nbyte = {10, 20, 30};
    UUID uuidFromBytes = UUID.nameUUIDFromBytes(nbyte);
    System.out.println(uuidFromBytes.toString().replaceAll("-",""));
}

得到的UUID结果,

59f51e7ea5ca453bbfaf2c1579f09f1d
7f49b84d0bbc38e9a493718013baace6

虽然 UUID 生成方便,本地生成没有网络消耗,但是使用起来也有一些缺点,

  • 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
  • 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,暴露使用者的位置。
  • 对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能,可以查阅 Mysql 索引原理 B+树的知识。

# 数据库生成

是不是一定要基于外界的条件才能满足分布式唯一ID的需求呢,我们能不能在我们分布式数据库的基础上获取我们需要的ID?

由于分布式数据库的起始自增值一样所以才会有冲突的情况发生,那么我们将分布式系统中数据库的同一个业务表的自增ID设计成不一样的起始值,然后设置固定的步长,步长的值即为分库的数量或分表的数量。

以MySQL举例,利用给字段设置auto_increment_incrementauto_increment_offset来保证ID自增。

  • auto_increment_offset:表示自增长字段从那个数开始,他的取值范围是1 .. 65535。
  • auto_increment_increment:表示自增长字段每次递增的量,其默认值是1,取值范围是1 .. 65535。

假设有三台机器,则DB1中order表的起始ID值为1,DB2中order表的起始值为2,DB3中order表的起始值为3,它们自增的步长都为3,则它们的ID生成范围如下图所示:

通过这种方式明显的优势就是依赖于数据库自身不需要其他资源,并且ID号单调自增,可以实现一些对ID有特殊要求的业务。

但是缺点也很明显,首先它强依赖DB,当DB异常时整个系统不可用。虽然配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能会导致重复发号。还有就是ID发号性能瓶颈限制在单台MySQL的读写性能。

# 使用redis实现

Redis实现分布式唯一ID主要是通过提供像 INCRINCRBY 这样的自增原子命令,由于Redis自身的单线程的特点所以能保证生成的 ID 肯定是唯一有序的。

但是单机存在性能瓶颈,无法满足高并发的业务需求,所以可以采用集群的方式来实现。集群的方式又会涉及到和数据库集群同样的问题,所以也需要设置分段和步长来实现。

为了避免长期自增后数字过大可以通过与当前时间戳组合起来使用,另外为了保证并发和业务多线程的问题可以采用 Redis + Lua的方式进行编码,保证安全。

Redis 实现分布式全局唯一ID,它的性能比较高,生成的数据是有序的,对排序业务有利,但是同样它依赖于redis,需要系统引进redis组件,增加了系统的配置复杂性。

当然现在Redis的使用性很普遍,所以如果其他业务已经引进了Redis集群,则可以资源利用考虑使用Redis来实现。

# 雪花算法-Snowflake

查看更多

error: Content is protected !!
滚动至顶部