Usage and depth of ThreadLocal class in Java multithreaded programming

ThreadLocal is literally translated as "thread local" or "local thread". If you really think so, you are wrong! In fact, it is a container for storing local variables of threads. I think it should be called
ThreadLocalVariable (thread local variable) is right. I really don't understand why the engineers of Sun company named it like this.

Back in the era of JDK 1.2, Java lang.ThreadLocal
It is designed to solve the problem of multithreading concurrency, but it is designed to be difficult to use, so it has not been widely used so far. In fact, it is still very useful. If you don't believe it, let's take a look at this example.

The program of a sequence number generator may have multiple threads accessing it concurrently. It is necessary to ensure that the sequence number obtained by each thread is self increasing and cannot interfere with each other.

Define an interface first:

    public interface Sequence {
    
      int getNumber();
    }
    
    

Each time you call the getNumber() method, you can get a serial number. When you call it again, the serial number will increase automatically.

Create another thread class:

    public class ClientThread extends Thread {
    
      private Sequence sequence;
    
      public ClientThread(Sequence sequence) {
        this.sequence = sequence;
      }
    
      @Override
      public void run() {
        for (int i = 0; i < 3; i++) {
          System.out.println(Thread.currentThread().getName() + " => " + sequence.getNumber());
        }
      }
    
    
    
    }
    
    

Output the thread name and its corresponding serial number three times in a row in the thread.

Let's make an implementation class instead of ThreadLocal.

    public class SequenceA implements Sequence {
    
      private static int number = 0;
    
      public int getNumber() {
        number = number + 1;
        return number;
      }
    
      public static void main(String[] args) {
        Sequence sequence = new SequenceA();
    
        ClientThread thread1 = new ClientThread(sequence);
        ClientThread thread2 = new ClientThread(sequence);
        ClientThread thread3 = new ClientThread(sequence);
    
        thread1.start();
        thread2.start();
        thread3.start();
      }
    }
    

The initial value of the serial number is 0. Three threads are simulated in the main() method. After running, the results are as follows:

    Thread-0 => 1
    Thread-0 => 2
    Thread-0 => 3
    Thread-2 => 4
    Thread-2 => 5
    Thread-2 => 6
    Thread-1 => 7
    Thread-1 => 8
    Thread-1 => 9

Since the thread startup sequence is random, it is not in the order of 0, 1 and 2. This is easy to understand. Why when Thread-0 outputs 1, 2 and 3, and Thread-2
But output 4, 5, 6? The static variable is shared between threads! This is the so-called "non thread safety" problem.

So how to ensure "thread safety"? Corresponding to this case, that is, different threads can have their own static variables. How to implement it? Let's take a look at another implementation.

    public class SequenceB implements Sequence {
    
      private static ThreadLocal<Integer> numberContainer = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
          return 0;
        }
      };
    
      public int getNumber() {
        numberContainer.set(numberContainer.get() + 1);
        return numberContainer.get();
      }
    
      public static void main(String[] args) {
        Sequence sequence = new SequenceB();
    
        ClientThread thread1 = new ClientThread(sequence);
        ClientThread thread2 = new ClientThread(sequence);
        ClientThread thread3 = new ClientThread(sequence);
    
        thread1.start();
        thread2.start();
        thread3.start();
      }
    }
    
    

A numberContainer static member variable of Integer type is encapsulated through ThreadLocal, and the initial value is 0. Look at getNumber()
Method, first get the current value from numberContainer, add 1, then set it to numberContainer, and finally
get the current value from numberContainer and return it.

Isn't it disgusting? But it's powerful! Indeed, we might as well regard ThreadLocal as a Container, which is simple to understand. Therefore, Container is deliberately used here
This word is used as a suffix to name the ThreadLocal variable.

What are the results? Take a look.

    Thread-0 => 1
    Thread-0 => 2
    Thread-0 => 3
    Thread-2 => 1
    Thread-2 => 2
    Thread-2 => 3
    Thread-1 => 1
    Thread-1 => 2
    Thread-1 => 3
    
    

Each thread is independent of each other. It is also a static variable. For different threads, it is not shared, but one for each thread, which ensures thread safety.
In other words, TheadLocal provides an independent copy for each thread!

After understanding the principle of ThreadLocal, it is necessary to summarize the API of ThreadLocal. In fact, it is very simple.

  • public void set(T value): put the value into the thread local variable
  • public T get(): get value from thread local variable
  • public void remove(): remove values from thread local variables (helps JVM garbage collection)
  • protected T initialValue(): returns the initial value in the thread local variable (null by default)

Why is the initialValue() method protected? To remind programmers that this method is for you to implement, please give an initial value to the thread local variable.

After understanding the principle and these API s, in fact, think ThreadLocal encapsulates a Map? You can write a ThreadLocal yourself. Give it a try.

    public class MyThreadLocal<T> {
    
      private Map<Thread, T> container = Collections.synchronizedMap(new HashMap<Thread, T>());
    
      public void set(T value) {
        container.put(Thread.currentThread(), value);
      }
    
      public T get() {
        Thread thread = Thread.currentThread();
        T value = container.get(thread);
        if (value == null && !container.containsKey(thread)) {
          value = initialValue();
          container.put(thread, value);
        }
        return value;
      }
    
      public void remove() {
        container.remove(Thread.currentThread());
      }
    
      protected T initialValue() {
        return null;
      }
    }
    

The above completely imitates a ThreadLocal, in which a synchronous Map is defined (why? Please think for yourself). The code should be very easy to understand.
Let's use MyThreadLocal to implement it again.

    public class SequenceC implements Sequence {
    
      private static MyThreadLocal<Integer> numberContainer = new MyThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
          return 0;
        }
      };
    
      public int getNumber() {
        numberContainer.set(numberContainer.get() + 1);
        return numberContainer.get();
      }
    
      public static void main(String[] args) {
        Sequence sequence = new SequenceC();
    
        ClientThread thread1 = new ClientThread(sequence);
        ClientThread thread2 = new ClientThread(sequence);
        ClientThread thread3 = new ClientThread(sequence);
    
        thread1.start();
        thread2.start();
        thread3.start();
      }
    }
    
    

The above code actually replaces ThreadLocal with MyThreadLocal. That's all. The running effect is the same as before and is correct.

In fact, ThreadLocal can be a design pattern alone, depending on what you think.

What are the specific use cases of ThreadLocal?

The first thing I want to say is: store JDBC Connection through ThreadLocal to achieve the ability of transaction control.

I'd better keep my consistent Style and talk with a Demo. Users put forward a demand: when modifying the product price, they need to record the operation log and what they did when.

Presumably, this case should be encountered by all the partners who have done the application system? There are only two tables in the database: product and log. Two SQL statements should solve the problem:

    update product set price = ? where id = ?
    insert into log (created, description) values (?, ?)
    
    

But! To ensure that these two SQL statements must be committed in the same transaction, otherwise it is possible that update is committed but insert
But not submitted. If such a thing really happens, we will certainly be scolded by users pointing to their noses: "why did the product price change, but we can't see when it was changed?".

Smart, I did this after receiving this demand:

First, I write a tool class of DBUtil, which encapsulates the common operations of the database:

    public class DBUtil {
      // Database configuration
      private static final String driver = "com.mysql.jdbc.Driver";
      private static final String url = "jdbc:mysql://localhost:3306/demo";
      private static final String username = "root";
      private static final String password = "root";
    
      // Define a database connection
      private static Connection conn = null;
    
      // Get connection
      public static Connection getConnection() {
        try {
          Class.forName(driver);
          conn = DriverManager.getConnection(url, username, password);
        } catch (Exception e) {
          e.printStackTrace();
        }
        return conn;
      }
    
      // Close connection
      public static void closeConnection() {
        try {
          if (conn != null) {
            conn.close();
          }
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }
    
    

There is a static Connection in it. Now the database Connection is easy to operate. It's awesome!

Then, I defined an interface to call the logical layer:

    public interface ProductService {
    
      void updateProductPrice(long productId, int price);
    }
    
    

According to the user's requirements, I think this interface is fully sufficient. Update the price of the corresponding Product according to the productId, and then insert a piece of data into the log table.

In fact, the business logic is not too complex, so I quickly completed the implementation class of ProductService interface:

    public class ProductServiceImpl implements ProductService {
    
      private static final String UPDATE_PRODUCT_SQL = "update product set price = ? where id = ?";
      private static final String INSERT_LOG_SQL = "insert into log (created, description) values (?, ?)";
    
      public void updateProductPrice(long productId, int price) {
        try {
          // Get connection
          Connection conn = DBUtil.getConnection();
          conn.setAutoCommit(false); // Turn off auto commit transactions (turn on transactions)
    
          // Perform operations
          updateProduct(conn, UPDATE_PRODUCT_SQL, productId, price); // Update products
          insertLog(conn, INSERT_LOG_SQL, "Create product."); // Insert log
    
          // Commit transaction
          conn.commit();
        } catch (Exception e) {
          e.printStackTrace();
        } finally {
          // Close connection
          DBUtil.closeConnection();
        }
      }
    
      private void updateProduct(Connection conn, String updateProductSQL, long productId, int productPrice) throws Exception {
        PreparedStatement pstmt = conn.prepareStatement(updateProductSQL);
        pstmt.setInt(1, productPrice);
        pstmt.setLong(2, productId);
        int rows = pstmt.executeUpdate();
        if (rows != 0) {
          System.out.println("Update product success!");
        }
      }
    
      private void insertLog(Connection conn, String insertLogSQL, String logDescription) throws Exception {
        PreparedStatement pstmt = conn.prepareStatement(insertLogSQL);
        pstmt.setString(1, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date()));
        pstmt.setString(2, logDescription);
        int rows = pstmt.executeUpdate();
        if (rows != 0) {
          System.out.println("Insert log success!");
        }
      }
    }
    
    

Is the code readable? Here I use the advanced feature Transaction of JDBC
Yes. After secretly congratulating myself, I wonder if it is necessary to write a client to test whether the execution result is what I want? So I was lazy and went directly to ProductServiceImpl
A main() method is added in the:

    public static void main(String[] args) {
      ProductService productService = new ProductServiceImpl();
      productService.updateProductPrice(1, 3000);
    }
    

I want to change the price of the product with productId 1 to 3000. So I ran the program again and the console output:

    Update product success!
    Insert log success!
    
    

You should be right. As a professional programmer, in order to be safe, I must go to the database and have a look. you 're right! The record corresponding to the product table is updated, and a record is inserted into the log table. So you can
The ProductService interface is delivered to others to call.

A few hours later, my QA sister began to scold me: "shit! I only simulated 10 requests. Why did your interface hang up? It said that the database connection was closed!".

Hearing such a cry made me tremble. I immediately interrupted my little video, quickly opened the IDE and found the implementation class of ProductServiceImpl. There seems to be no Bug
Right? But now I dare not give her any response. I'm really a little afraid of her.

It suddenly occurred to me that she simulated with tools, that is, simulating multiple threads! Then I can simulate myself, so I wrote a thread class:

    public class ClientThread extends Thread {
    
      private ProductService productService;
    
      public ClientThread(ProductService productService) {
        this.productService = productService;
      }
    
      @Override
      public void run() {
        System.out.println(Thread.currentThread().getName());
        productService.updateProductPrice(1, 3000);
      }
    }
    
    

I use this thread to call the method of ProduceService to see if there is a problem. At this point, I will also modify the main() method:

    // public static void main(String[] args) {
    //   ProductService productService = new ProductServiceImpl();
    //   productService.updateProductPrice(1, 3000);
    // }
      
    public static void main(String[] args) {
      for (int i = 0; i < 10; i++) {
        ProductService productService = new ProductServiceImpl();
        ClientThread thread = new ClientThread(productService);
        thread.start();
      }
    }
    

I also simulate 10 threads, so I don't believe that evil!

The running result really made me dizzy:

    Thread-1
    Thread-3
    Thread-5
    Thread-7
    Thread-9
    Thread-0
    Thread-2
    Thread-4
    Thread-6
    Thread-8
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after connection closed.
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
    at com.mysql.jdbc.Util.handleNewInstance(Util.java:411)
    at com.mysql.jdbc.Util.getInstance(Util.java:386)
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1015)
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:989)
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:975)
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:920)
    at com.mysql.jdbc.ConnectionImpl.throwConnectionClosedException(ConnectionImpl.java:1304)
    at com.mysql.jdbc.ConnectionImpl.checkClosed(ConnectionImpl.java:1296)
    at com.mysql.jdbc.ConnectionImpl.commit(ConnectionImpl.java:1699)
    at com.smart.sample.test.transaction.solution1.ProductServiceImpl.updateProductPrice(ProductServiceImpl.java:25)
    at com.smart.sample.test.transaction.ClientThread.run(ClientThread.java:18)
    
    

holy crap An error was reported in a multithreaded environment. As expected, the database connection was closed. What's going on? I was lost in thought. So I copied the error message in Baidu, Google and
I've looked in OSC, and the answers are really strange.

It suddenly occurred to me that since it is related to Connection, I will focus on checking the code related to Connection. Is Connection not supposed to be
What about static? I originally designed static to make the static method of DBUtil more convenient to access and store it with static variables
Connection also improves performance. What's the matter?

So I saw a very popular article "ThreadLocal" on OSC
That thing "finally made me understand! Originally, each thread should have its own connection instead of sharing the same connection. Otherwise, thread 1 may close the connection of thread 2, so thread 2 will report an error. It must be!

I quickly refactored DBUtil:

    public class DBUtil {
      // Database configuration
      private static final String driver = "com.mysql.jdbc.Driver";
      private static final String url = "jdbc:mysql://localhost:3306/demo";
      private static final String username = "root";
      private static final String password = "root";
    
      // Define a local thread variable for placing database connections (so that each thread has its own connection)
      private static ThreadLocal<Connection> connContainer = new ThreadLocal<Connection>();
    
      // Get connection
      public static Connection getConnection() {
        Connection conn = connContainer.get();
        try {
          if (conn == null) {
            Class.forName(driver);
            conn = DriverManager.getConnection(url, username, password);
          }
        } catch (Exception e) {
          e.printStackTrace();
        } finally {
          connContainer.set(conn);
        }
        return conn;
      }
    
      // Close connection
      public static void closeConnection() {
        Connection conn = connContainer.get();
        try {
          if (conn != null) {
            conn.close();
          }
        } catch (Exception e) {
          e.printStackTrace();
        } finally {
          connContainer.remove();
        }
      }
    }
    
    

I put the Connection into ThreadLocal, so that each thread is isolated from each other and will not interfere with each other.

In addition, in the getConnection() method, first get from ThreadLocal (that is, connContainer)
Connection. If not, create a connection through JDBC, and finally put the created connection into the ThreadLocal. ThreadLocal
As a container, it's true.

Similarly, I also refactor the closeConnection() method. First, get the Connection from the container, close it when you get it, and finally delete it from the container
remove to keep the container clean.

Should it work now? I run the main() method again:

    Thread-0
    Thread-2
    Thread-4
    Thread-6
    Thread-8
    Thread-1
    Thread-3
    Thread-5
    Thread-7
    Thread-9
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    Update product success!
    Insert log success!
    
    

It's finally solved

Keywords: Python Java

Added by nanban on Sun, 30 Jan 2022 17:19:50 +0200