Answer Compilation Code Compilation Thoughts Collection One

Answer Compilation Code Compilation Thoughts Collection One #

During the process of replying to the comments on the course “Java Common Business Development Errors - 100 Cases,” I noticed that some students were especially interested in seeing the answers to all the thinking questions in this course. Therefore, I have organized the thinking questions covered in this course, and provided detailed answers or solution approaches to the 67 questions, which I have compiled into a “Q&A” module.

I have divided these questions into 6 parts for separate updates, so you can study them according to your own time to ensure the learning effect. Through these answers, you can review the knowledge points and seek to consolidate your understanding. At the same time, you can compare your solution approaches with mine and see if there are any differences, and leave comments for me.

Today is the first lesson of the Q&A series, and let’s analyze the post-lesson thinking questions of the first 6 lessons in this course together. These questions cover 12 thinking questions related to concurrent tools, code locking, thread pools, connection pools, HTTP calls, and Spring declarative transactions.

Next, let’s analyze them one by one.

01 | Does using concurrent utility libraries guarantee thread safety? #

Question 1: ThreadLocalRandom is a class introduced in Java 7 for generating random numbers. Can you set its instance to a static variable for reuse in a multi-threaded environment?

Answer: No.

There is a statement in the ThreadLocalRandom documentation:

“Usages of this class should typically be of the form: ThreadLocalRandom.current().nextX(…) (where X is Int, Long, etc). When all usages are of this form, it is never possible to accidentally share a ThreadLocalRandom across multiple threads.”

Why is it specified to use ThreadLocalRandom.current().nextX(…) like this? Let me analyze the reasons.

When calling current(), it initializes an initial seed to the thread, and each call to nextseed generates a new seed using the previous seed:

UNSAFE.putLong(t = Thread.currentThread(), SEED, r = UNSAFE.getLong(t, SEED) + GAMMA);

If you call current once in the main thread to generate an instance of ThreadLocalRandom and save it, then when other threads try to get the seed, they will not be able to obtain the initial seed. Instead, each thread must initialize a seed to itself when using it. You can set a breakpoint in the nextSeed method to test this:

UNSAFE.getLong(Thread.currentThread(),SEED);

Question 2: ConcurrentHashMap also provides the putIfAbsent method. Based on the JDK documentation, can you explain the differences between the computeIfAbsent and putIfAbsent methods?

Answer: The computeIfAbsent and putIfAbsent methods are both atomic methods used to assign a value to a Map when the key does not exist. The specific differences between them are as follows:

When the key exists, if obtaining the value is expensive, the putIfAbsent method will waste time on obtaining this expensive value (pay special attention to this point). In contrast, computeIfAbsent will not have this problem because it uses a lambda expression instead of an actual value.

When the key does not exist, putIfAbsent returns null, so be careful about null pointers. On the other hand, computeIfAbsent returns the computed value without any null pointer issues.

When the key does not exist, putIfAbsent allows putting null into the map, while computeIfAbsent does not (of course, this only applies to HashMap; ConcurrentHashMap does not allow putting null values).

I wrote some code to demonstrate these three points. You can click on this GitHub link to view it.

02 | Locking Code: Don’t Let “Locks” Become Troublesome #

Question 1: In the example at the beginning of this lesson, we used the volatile keyword to modify variables a and b. Do you know the purpose of the volatile keyword? I have encountered a pitfall before: we started a thread in an infinite loop to run some tasks, and there was a boolean variable to control the loop and exit, with the default value of true for execution. After a period of time, the main thread set this variable to false. If this variable is not declared as volatile, can the sub-thread exit? Can you explain the reason behind it?

Answer: No, it cannot exit. For example, in the code below, another thread sets b to false after 3 seconds, but the main thread cannot exit:

private static boolean b = true;

public static void main(String[] args) throws InterruptedException {

    new Thread(()->{

        try {

            TimeUnit.SECONDS.sleep(3);

        } catch (InterruptedException e) { }

        b =false;

    }).start();
while (b) {

    TimeUnit.MILLISECONDS.sleep(0);

}

System.out.println("done");

}

Actually, this is a visibility problem.

Although another thread sets b to false, this field is still in the CPU cache, and the other thread (main thread) still cannot read the latest value. Using the volatile keyword can make the data refresh to the main memory. To be precise, there are two things that happen when the data is refreshed to the main memory:

  1. Write back the data of the current processor cache line to the system memory;
  2. This write-back operation will cause other CPUs that cache the memory address to become invalid.

Of course, you can also use keywords like AtomicBoolean to modify the variable b. But compared to volatile, AtomicBoolean and other keywords not only ensure visibility but also provide CAS methods and have more functions that are not needed in this example scenario.

Question 2: There are two pitfalls regarding code locking: mismatched locking and unlocking, and the problem of repeated logic execution caused by the automatic release of distributed locks. Do you have any methods to discover and solve these two problems?

Answer: To address the issue of mismatched locking and unlocking, we can use code quality tools or code scanning tools (such as Sonar) to assist in debugging. This problem can be detected during the coding phase.

Regarding the problem of automatic release of distributed locks causing timeout, you can refer to the lock extension mechanism of RedissonLock in Redisson. The lock extension period is renewed every certain period of time, such as every 30 seconds, and extended every 10 seconds. Although it is renewed an infinite number of times, even if the client crashes, it does not matter as the lock will not be occupied indefinitely because it is unable to automatically extend after a crash and will eventually timeout.

03 | ThreadPoolExecutor: The Most Commonly Used and Most Error-prone Component in Business Code #

Question 1: When discussing the management strategy of ThreadPoolExecutor, we mentioned that perhaps an elastic thread pool that aggressively creates threads would be more suitable for our needs. Can you provide the relevant implementation? After implementing it, test whether all tasks can be processed correctly.

Answer: Let’s implement an aggressive thread pool according to the two ideas mentioned in the article:

  1. As the thread pool expands when the work queue is full and cannot enqueue, we can override the offer method of the queue to create the illusion that the queue is already full;
  2. Since we have hacked the queue and reaching the maximum number of threads will trigger the rejection policy, we need to implement a custom rejection policy handler and insert the tasks into the queue at this point.

Here is the complete implementation code and corresponding test code:

@GetMapping("better")
public int better() throws InterruptedException {

    // Here starts the implementation of an aggressive thread pool

    BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>(10) {

        @Override
        public boolean offer(Runnable e) {
            // Return false first to create the illusion that the queue is full and let the thread pool expand first
            return false;
        }
    };

    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
            2, 5,
            5, TimeUnit.SECONDS,
            queue, new ThreadFactoryBuilder().setNameFormat("demo-threadpool-%d").get(), (r, executor) -> {
        try {
config.setMaxWaitMillis(10000);
JedisPool jedisPool = new JedisPool(config, "localhost", 6379, 5000);

针对 Apache HttpClient,是设置连接池管理器的连接超时和请求连接超时:

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setValidateAfterInactivity(10000);
RequestConfig config = RequestConfig.custom()
        .setConnectTimeout(5000)
        .setConnectionRequestTimeout(10000)
        .build();
CloseableHttpClient httpClient = HttpClients.custom()
        .setConnectionManager(connectionManager)
        .setDefaultRequestConfig(config)
        .build();

05 | 资源泄露:永远别抛异常时去关闭连接和释放资源 #

问题 1:资源清理应该放在 finally 块中,用 try-with-resource 方式关闭。请看下面一段代码:

try (Statement statement = connection.createStatement()) {
    // 执行SQL操作
} catch (SQLException e) {
    // 异常处理
}

在这段代码中,Statement 的 close 方法被调用了吗?为什么? 答:在这段代码中,Statement 的 close 方法被自动调用了。这是因为在 try-with-resources 中,会自动调用实现了AutoCloseable接口的资源对象的 close 方法来关闭资源。 Statement 类实现了 AutoCloseable 接口,所以在 try 块结束时会自动关闭该资源。这样可以确保资源在任何情况下都能被正确关闭,而不会出现资源泄露的问题。

问题 2:现在有一个需求,打开 20 个文件,每个文件都要读取它的内容,但是只有 1 个文件读取失败就不能继续处理。你会怎么实现? 答:我会使用 try-with-resources 的方式打开文件,并在读取文件内容时捕获异常。一旦发生异常,会直接跳过后续的文件处理。

伪代码如下:

List<File> fileList = new ArrayList<>();
try {
    for (File file : fileList) {
        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
            // 读取文件内容
        }
    }
} catch (IOException e) {
    // 处理异常
}
        <artifactId>spring-aspects</artifactId>
    
    </dependency>
    
    

第二步配置 aspectj-maven-plugin 插件
    
    
    <plugin>
    
        <groupId>org.codehaus.mojo</groupId>
    
        <artifactId>aspectj-maven-plugin</artifactId>
    
        <version>${aspectj.version}</version>
    
        <configuration>
    
            <complianceLevel>1.8</complianceLevel>
    
            <source>1.8</source>
    
            <target>1.8</target>
    
            <aspectLibraries>
    
                <aspectLibrary>
    
                    <groupId>org.springframework</groupId>
    
                    <artifactId>spring-aspects</artifactId>
    
                </aspectLibrary>
    
            </aspectLibraries>
    
        </configuration>
    
        <executions>
    
            <execution>
    
                <goals>
    
                    <goal>compile</goal>
    
                </goals>
    
            </execution>
    
        </executions>
    
    </plugin>
    
    

第三步配置 spring 配置文件开启 AspectJ 事务管理模式
    
    
    <aop:aspectj-autoproxy/>
    
    

第四步使用 @Transactional 注解标注 private 方法
    
    
    @Transactional
    
    private void createUser(UserDO user) {
    
        userDao.insert(user);
    
    }
    
  

现在我们就可以在 private 方法上使用 @Transactional 注解了而且是 AspectJ 方式的事务管理

值得一提的是如果项目使用了 lombok可能会出现未生效的情况解决办法是在 IDEA 的 settings.gradle 中加入一行配置

lombok.appendExcludes = '.apt_generated'

spring-aspects

第二步,加入 lombok 和 aspectj 插件:

<groupId>org.projectlombok</groupId>

<artifactId>lombok-maven-plugin</artifactId>

<version>1.18.0.0</version>

<executions>

    <execution>

        <phase>generate-sources</phase>

        <goals>

            <goal>delombok</goal>

        </goals>

    </execution>

</executions>

<configuration>

    <addOutputDirectory>false</addOutputDirectory>

    <sourceDirectory>src/main/java</sourceDirectory>

</configuration>
<groupId>org.codehaus.mojo</groupId>

<artifactId>aspectj-maven-plugin</artifactId>

<version>1.10</version>

<configuration>

    <complianceLevel>1.8</complianceLevel>

    <source>1.8</source>

    <aspectLibraries>

        <aspectLibrary>

            <groupId>org.springframework</groupId>

            <artifactId>spring-aspects</artifactId>

        </aspectLibrary>

    </aspectLibraries>

</configuration>

<executions>

    <execution>

        <goals>

            <goal>compile</goal>

            <goal>test-compile</goal>

        </goals>

    </execution>

</executions>

使用 delombok 插件的目的是,把代码中的 Lombok 注解先编译为代码,这样 AspectJ 编译不会有问题,同时需要设置中的 sourceDirectory 为 delombok 目录:

${project.build.directory}/generated-sources/delombok

第三步,设置 @EnableTransactionManagement 注解,开启事务管理走 AspectJ 模式:

@SpringBootApplication

@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)

public class CommonMistakesApplication {

第四步,使用 Maven 编译项目,编译后查看 createUserPrivate 方法的源码,可以发现 AspectJ 帮我们做编译时织入(Compile Time Weaving):

img

运行程序,观察日志可以发现 createUserPrivate(私有)方法同样应用了事务,出异常后事务回滚:

[14:21:39.155] [http-nio-45678-exec-2] [DEBUG] [o.s.orm.jpa.JpaTransactionManager:370 ] - Creating new transaction with name [org.geekbang.time.commonmistakes.transaction.transactionproxyfailed.UserService.createUserPrivate]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT

[14:21:39.155] [http-nio-45678-exec-2] [DEBUG] [o.s.orm.jpa.JpaTransactionManager:393 ] - Opened new EntityManager [SessionImpl(1087443072)] for JPA transaction

[14:21:39.158] [http-nio-45678-exec-2] [DEBUG] [o.s.orm.jpa.JpaTransactionManager:421 ] - Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@4e16e6ea]

[14:21:39.159] [http-nio-45678-exec-2] [DEBUG] [o.s.orm.jpa.JpaTransactionManager:356 ] - Found thread-bound EntityManager [SessionImpl(1087443072)] for JPA transaction

[14:21:39.159] [http-nio-45678-exec-2] [DEBUG] [o.s.orm.jpa.JpaTransactionManager:471 ] - Participating in existing transaction

[14:21:39.173] [http-nio-45678-exec-2] [DEBUG] [o.s.orm.jpa.JpaTransactionManager:834 ] - Initiating transaction rollback

[14:21:39.173] [http-nio-45678-exec-2] [DEBUG] [o.s.orm.jpa.JpaTransactionManager:555 ] - Rolling back JPA transaction on EntityManager [SessionImpl(1087443072)]

[14:21:39.176] [http-nio-45678-exec-2] [DEBUG] [o.s.orm.jpa.JpaTransactionManager:620 ] - Closing JPA EntityManager [SessionImpl(1087443072)] after transaction

[14:21:39.176] [http-nio-45678-exec-2] [ERROR] [o.g.t.c.t.t.UserService:28 ] - create user failed because invalid username!

[14:21:39.177] [http-nio-45678-exec-2] [DEBUG] [o.s.o.j.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler:305 ] - Creating new EntityManager for shared EntityManager invocation

以上,就是咱们这门课前 6 讲的思考题答案了。

关于这些题目,以及背后涉及的知识点,如果你还有哪里感觉不清楚的,欢迎在评论区与我留言,也欢迎你把今天的内容分享给你的朋友或同事,一起交流。