Java Concurrency -- Thread Safety

Keywords: Java Lambda JDK

We are no strangers to the term concurrency, which usually refers to the simultaneous execution of multiple tasks. In fact, this is not entirely true, "parallel" is the real sense of simultaneous execution, while "concurrent" is more focused on Multi-task alternate execution. Sometimes we see people chewing and talking, which is parallel; of course, the more civilized and polite way is to swallow the things in their mouth before speaking, which is concurrent. Early concurrency was used to improve the performance of single processors, such as I/O blocking. Today, when multicore processors are widely used, the concepts of parallelism and concurrency have been blurred. Perhaps we need not dwell too much on the subtle differences between them.

Java concurrency is achieved through multi-threading. If there are multiple processors, the thread scheduling mechanism will automatically assign threads to each processor. Threads are different from processes in that they are at a lower level. A process can spawn multiple threads. Modern operating systems are multi-process, different programs belong to different processes, and each process will not share the same memory space. Because there is no intersection between the processes, so the processes can run smoothly, which is like different households in the same building. Everyone closes their doors and has nothing to do with you. Every program running in the computer belongs to different processes. When you use IDE, you don't have to worry about the player modifying your code, nor about the impact of communication software on IDE. But when it comes to multi-threading, everything becomes complicated. Different households have to move together to share the rent. The bathroom and kitchen have become public. Each thread shares the resources of its own process. The difficulty of multithreading is to coordinate the use of shared resources among tasks driven by different threads.

Since multithreading is so difficult, why not use multiprocesses directly? One reason is that processes are expensive and the operating system limits the number of processes. Another reason comes from the remote barbaric era, when some medieval systems did not support multi-process. In order to achieve portability, java used multi-threading to achieve concurrency.

Java multithreading is ubiquitous, but the reality is that very few people have actually written concurrent code, in fact, quite a number of technicians have never written real concurrency. The reason is that some frameworks such as Servlets help us deal with concurrency problems.

Tasks and Threads

Java threads are implemented through the Runnable interface, so that a thread can be implemented:

 1 class Task implements Runnable {
 2     private int n = 1;
 3     private String tName = "";
 4     
 5     public Task(String tName) {
 6         this.tName = tName;
 7     }
 8     
 9     @Override
10     public void run() {
11         while(n <= 10) {
12             System.out.print(this.tName + "#" + n + "  ");
13             n++;
14         }
15         System.out.println(this.tName + " is over.");
16     }
17 }
18 
19 public class C_1 {
20     public static void main(String[] args) {
21         Task A = new Task("A");
22         Task B = new Task("B");
23         A.run();
24         B.run();
25         System.out.println("main is over.");
26     }
27 }

Running results are no different from sequential execution:

This shows that the class that implements Runnable is actually no different from the ordinary class. It is only a task at best. To achieve concurrency, the task must be attached to a thread:

1 public class C_1 {
2     public static void main(String[] args) {
3         Thread t1 = new Thread(new Task("A"));
4         Thread t2 = new Thread(new Task("B"));
5         t1.start();
6         t2.start();
7         System.out.println("main is over.");
8     }
9 }

This is the real concurrency:

 

start() makes the necessary preparations for thread startup, and then calls the run() method of the task to make the task run on the thread. After JDK 1.5, a thread manager is added, which can attach tasks to threads without displaying them. At the same time, the thread manager automatically manages the lifecycle of threads.

1 public class C_1 {
2     public static void main(String[] args) {        
3         ExecutorService es = Executors.newCachedThreadPool();
4         es.execute(new Task("A"));
5         es.execute(new Task("B"));
6         es.shutdown();
7         System.out.println("main is over.");
8     }
9 }

The shutdown() method is used to prevent new threads from being submitted to ExecutorService. If a new thread is still submitted when es.shutdown(), java.util.concurrent.RejectedExecutionException will be thrown.

After JDK8, lambda expression is added. For some short tasks that do not need to be reused, it is not necessary to write a single class:

 1 public class C_1 {
 2     public static void main(String[] args) {
 3         ExecutorService es = Executors.newCachedThreadPool();
 4         es.execute(new Task("A"));
 5         es.execute(new Task("B"));
 6         es.execute(new Runnable() {
 7             @Override
 8             public void run() {
 9                 System.out.println("I am in lambda.");
10             }
11         });
12         es.shutdown();
13         System.out.println("main is over.");
14     }
15 }

Since the initialization of each lambda expression takes a little time, this method often fails to see the effect when executing a short, fast multithreaded program, and the program is more sequential.

Thread Safety

We often say that a method is thread-safe. I don't think "thread safety" is an easy word to understand. Simply put, if a method is "thread-safe", then the results of this method in a multi-threaded environment will also be expected.

 1 import java.util.concurrent.ExecutorService;
 2 import java.util.concurrent.Executors;
 3 
 4 class Task2 implements Runnable {
 5     String tName = "";
 6 
 7     public Task2(String tName) {
 8         this.tName = tName;
 9     }
10 
11     @Override
12     public void run() {
13         for(int i = 1; i <= 10; i++) {
14             System.out.print(tName + "#" + i + " ");
15         }
16     }
17 }
18 
19 public class C_2 {
20     public static void main(String[] args) {
21         ExecutorService es = Executors.newCachedThreadPool();
22         es.execute(new Task2("A"));
23         es.execute(new Task2("B"));
24         es.shutdown();
25     }
26 }

The results may be:

As a task, Task 2 prints out 10 numbers each time it runs. Although the order of each printing may vary, we still think that Task 2 is predictable and thread-safe.

Task2 is secure because it has no shared state. If you add state, it's easy to turn a thread-safe approach into insecurity.

 1 class Task2 implements Runnable {
 2     String tName = "";
 3     static int no = 1;
 4     
 5     public Task2(String tName) {
 6         this.tName = tName;
 7     }
 8 
 9     @Override
10     public void run() {
11         for(int i = 1; i <= 10; i++) {
12             System.out.print(tName + "#" + i + " ");
13             no++;
14         }
15     }
16 }

This is just a slight modification to Task 2 so that two tasks share the same serial number, adding 1 to no each time a loop is executed. The expected effect is to print out different no values each time. However, the actual operation results may be:

A#9 and B#9 appeared. The reason is that two threads compete for no at the same time, and no++ is accomplished through multiple instructions. At no=9, thread A prints it out, and then executes the + + operation. Half of the execution, B comes in. Because the + + operation is not over, B still sees the previous state.

Stateless programs must be thread-safe. HTTP is stateless, and the servlet handling HTTP requests is stateless, so the servlet is thread-safe. Nevertheless, you need to be vigilant all the time because there are no constraints that prevent you from turning an otherwise stateless method into a stateful one.

1 public class MyServlet extends HttpServlet {
2 
3     private static int no = 1; 
4     
5     @Override
6     protected void service(HttpServletRequest arg0, HttpServletResponse arg1) throws ServletException, IOException {
7         arg0.setAttribute("no", no++);
8     }
9 }

With sharing, there will be competition, and then the original thread security will become insecure.

Singleton pattern

I have interviewed many programmers and asked them what design patterns they know commonly. Many people's first answer is the singleton pattern, which shows that the singleton pattern is deeply rooted in people's hearts. Here is a typical example.

 1 public class Singleton {
 2     private static Singleton sl = null;
 3     
 4     private Singleton() {
 5         System.out.println("OK");
 6     }
 7     
 8     public static Singleton getInstance() {
 9         if(sl == null)
10             sl = new Singleton();
11         return sl;
12     }
13     
14     public static void main(String[] args) {
15         Singleton.getInstance();
16         Singleton.getInstance();
17         Singleton.getInstance();
18 }

Singleton prints OK after initialization, and since Singleton only performs initialization once, the program eventually prints OK only once. But everything is different in multithreading. Put singletons in threads:

 1 class Task3 implements Runnable {
 2 
 3     @Override
 4     public void run() {
 5         Singleton.getInstance();
 6     }
 7 }
 8 
 9 public class Singleton {
10     private static Singleton sl = null;
11     
12     private Singleton() {
13         System.out.println("OK");
14     }
15     
16     public static Singleton getInstance() {
17         if(sl == null)
18             sl = new Singleton();
19         return sl;
20     }
21     
22     public static void main(String[] args) {
23         ExecutorService es = Executors.newCachedThreadPool();
24         es.execute(new Task3());
25         es.execute(new Task3());
26         es.execute(new Task3());
27         es.shutdown();
28     }
29 }

Three threads found sl==null at the same time. At this time, three initializations and three OK prints may be performed. This is also the reason why the singleton pattern has been criticized. Although the above situation can be solved by double checking lock and volatile keyword, the code is more complex and the performance is urgent. A good way is to use active initialization instead of singletons:

 1 public class Singleton_better {
 2 
 3     private static Singleton_better sl = new Singleton_better();
 4     
 5     public static Singleton_better getInstance() {
 6         return sl;
 7     }
 8     
 9     public Singleton_better() {
10         System.out.println("OK");
11     }
12 }

Another way is lazy initialization, which solves thread security while retaining the advantages of singletons:

 1 public class Single_lazy {
 2         
 3     private static class Handle {
 4         public static Single_lazy sl = new Single_lazy();
 5     }
 6     
 7     public static Single_lazy getInstance() {
 8         return Handle.sl;
 9     }
10 }

Author: I am eight.

Origin: http://www.cnblogs.com/bigmonkey

This paper focuses on learning, research and sharing. If you need to reproduce, please contact me, indicating the author and origin, non-commercial use!  

Scanning 2-D Code Focuses on the Public Author's Public Number "I'm 8-bit"

Posted by pakmannen on Sun, 25 Aug 2019 23:29:52 -0700