Basic knowledge
The Java language provides good support for threads, and the implementation method is small and elegant. For method re-entry protection, the semaphores (semaphore) and critical sections (critical section) mechanisms are all very simple. It is easy to implement synchronization between multiple threads to protect the consistency of key data. These features make Java a leader in supporting multithreading in object-oriented languages (C ++ is trying to include thread support in the boost library into language standards ).
Java has built-in support for concurrent object access. Each object has a monitor, and only one thread can hold the monitor to access the object, the threads that do not obtain the monitor must wait until the threads that hold the monitor release the monitor. The object uses the synchronized keyword to declare that the thread must obtain the monitor to access itself.
The synchronized statement is only effective for some simple inter-thread synchronization issues. Java provides an additional solution for complex synchronization problems, such as synchronization problems with conditions, wait/notify/notifyAll.
The thread that obtains the object monitor can actively release the Monitor by calling the wait method of the object, waiting for the thread of the object to wait for the queue. In this case, other threads can obtain the monitor to access the object, then, you can call the Y/notifyAll method to wake up the thread waiting for calling the wait method.
Generally, the wait/notify/notifyAll method is called based on certain conditions, such as the null or full queue determination in typical producer/consumer problems. Readers familiar with POSIX will find that using wait/notify/policyall can easily implement an advanced synchronization technology between threads in POSIX: conditional variables.
Simple example
This article will focus on a simple example, so that we can more easily highlight the ideas and methods for solving the problem. This article is intended to show readers exactly these ideas and methods. These ideas and methods are more suitable for solving concurrency problems in large-scale and complex applications. Consider a simple example. We have a service provider that provides external services through an interface. The service content is very simple, that is, printing Hello World on the standard output. The class structure is as follows:
The code is as follows:
1. interface Service
2 .{
3. public void sayHello ();
4 .}
5. class ServiceImp implements Service
6 .{
7. public void sayHello (){
8. System. out. println ("Hello World! ");
9 .}
10 .}
11. class Client
12 .{
13. public Client (Service s ){
14. _ service = s;
15 .}
16. public void requestService (){
17. _ service. sayHello ();
18 .}
19. private Service _ service;
20 .}
If there are new requirements, the service must support concurrent access from the Client. A simple method is to add the synchronized statement before each method in the ServicImp class to ensure internal data consistency (of course, this example is not necessary at present, because ServiceImp does not have data to be protected, but it may be available in the future as the demand changes ). However, at least the following problems may occur:
1. now we need to maintain two versions of ServiceImp: multi-threaded version and single-threaded version (in some places, such as other projects, there may be no concurrency issues), which may lead to problems of simultaneous updates and correct version selection, it brings trouble to maintenance.
2. If multiple concurrent clients frequently call the service, the Client will be blocked and the service quality will be reduced because the service is directly called synchronously.
3. It is difficult to implement flexible control, such as queuing based on the Client priority.
4. These problems are especially prominent for large multi-threaded application servers. Some simple applications (such as the example in this article) may not be considered at all. This article discusses the solutions to these problems. The simple examples in this article only provide a platform to illustrate the problem and demonstrate the ideas and methods.
5. How can we better solve these problems? Is there a reusable solution? Let's take a look at these issues first. Let's talk about some framework-related issues first.
Framework Overview
Readers familiar with object-oriented systems must know that one of the biggest advantages of object-oriented systems is software reuse. Reuse can reduce a lot of work and improve the productivity of software development. Reuse itself is also hierarchical, code-level reuse and design architecture reuse.
You may be very familiar with some standard libraries in C language. They provide some common functions for your program to use. However, these standard libraries do not affect your program structure and design philosophy. They only provide some functions to help your program complete the work. They enable you not to rewrite general functions (such as printf). They emphasize the reusability of the program code, rather than the reusability of the design architecture.
So what is the framework? The so-called framework, which is different from the general standard library, refers to a group of closely related (class) classes, emphasizing mutual cooperation to complete a design concept that can be reused. These classes work together in a specific way and are indispensable to each other. They have a considerable impact on the morphology of your program. The framework itself plans the backbone of the application, so that the program follows a certain process and line to show a certain style and function. In this way, programmers do not have to bother with the complexity of universal functions and concentrate on the professional field.
It must be emphasized that a universal framework does not exist and is the least useful. The framework is usually applicable to a specific application field. It abstracts the conceptual model of the application based on a deep understanding of the application field, A model built on these abstract concepts is a tangible framework. Different specific applications implement the abstract concepts in the framework based on their own characteristics, so as to give the framework life and complete the functions of the application.
Framework-based applications are composed of two parts: Framework and specific application. To achieve the goal of frame reuse, the framework and specific application must be isolated. This can be achieved using a powerful object-oriented feature: polymorphism. In the framework, the interaction and association between abstract concepts are completed, and specific implementations are handed over to specific applications. The Template Method design mode is usually used in large quantities. Collection Framework in Java and Microsoft's MFC are good examples of frameworks. Interested readers can study it on their own.
Build a framework
How to build a Java concurrency model framework? Let's go back to the original problem and analyze the cause first. The reason for maintaining the multi-thread and single-thread versions is that the application logic and concurrency logic are mixed together. If the application logic and concurrency model can be well isolated, the application logic itself can be reused, and it is easy to add the concurrency logic without affecting the application logic. The cause of Client blocking, performance reduction, and the inability to perform additional control is that all service calls are synchronized, and the solution is simple. Instead, the Asynchronous call method is used, separate service calls from service execution.
First, we will introduce the concept of an Active Object ). The so-called active object is relative to the passive object. The method call and execution of the passive object are in the same thread, passive object method calls are synchronous and congested. Generally, objects belong to passive objects. The call and execution of active object methods are separated, the active object has its own independent execution thread. The method call of the active object is initiated by other threads, but the method is executed in its own thread, the call of the active object method is asynchronous and non-blocking.
The core of this framework is to use active objects to encapsulate concurrency logic, and then forward Client requests to actual service providers (application logic ), in this way, both the Client and the actual service provider do not need to care about the existence of concurrency, and do not need to consider the data consistency problems caused by concurrency. In this way, application logic and concurrency logic are isolated, and service calls and service execution are isolated. The following describes the key implementation details.
The framework consists of the following parts:
1. An ActiveObject class, inherited from Thread, encapsulates the activity object of the concurrency logic;
2. An ActiveQueue class, mainly used to store caller requests;
3. A MethodRequest interface is mainly used to encapsulate caller requests. It is an implementation method of the Command design mode. A simple implementation is as follows:
1. // MethodRequest Interface Definition
2. interface MethodRequest
3 .{
4. public void call ();
5 .}
6. // ActiveQueue is defined as a producer/consumer queue.
7. class ActiveQueue
8 .{
9. public ActiveQueue (){
10. _ queue = new Stack ();
11 .}
12. public synchronized void enqueue (MethodRequest mr ){
13. while (_ queue. size ()> QUEUE_SIZE ){
14. try {
15. wait ();
16.} catch (InterruptedException e ){
17. e. printStackTrace ();
18 .}
19 .}
20.
21. _ queue. push (mr );
22. Policyall ();
23. System. out. println ("Leave Queue ");
24 .}
25. public synchronized MethodRequest dequeue (){
26. MethodRequest mr;
27.
28. while (_ queue. empty ()){
29. try {
30. wait ();
31.} catch (InterruptedException e ){
32. e. printStackTrace ();
33 .}
34 .}
35. mr = (MethodRequest) _ queu. pop ();
36. Policyall ();
37.
38. return mr;
39 .}
40. private Stack _ queue;
41. private final static int QUEUE_SIZE = 20;
42 .}
43. // ActiveObject definition
44. class ActiveObject extends Thread
45 .{
46. public ActiveObject (){
47. _ queue = new ActiveQueue ();
48. start ();
49 .}
50. public void enqueue (MethodRequest mr ){
51. _ queue. enqueue (mr );
52 .}
53. public void run (){
54. while (true ){
55. MethodRequest mr = _ queue. dequeue ();
56. mr. call ();
57 .}
58 .}
59. private ActiveQueue _ queue;
60 .}
From the code above, we can see that these classes work together to complete the encapsulation of the concurrency logic. Developers only need to implement the MethodRequest interface as needed, and define a service proxy class for the user. In the service proxy class, convert the request of the service caller to the MethodRequest implementation and hand it over to the activity object.
By using this framework, the application logic and concurrency model can be well separated, so that developers can concentrate on the application field and then smoothly integrate with the concurrency model, you can also customize the queuing mechanism for ActiveQueue, such as priority-based.
Framework-based solutions
This section uses the above framework to implement the preceding example and provide support for concurrency. The first step is to complete the implementation of MethodRequest. For our example, the implementation is as follows:
1. class SayHello implements MethodRequest
2 .{
3. public SayHello (Service s ){
4. _ service = s;
5 .}
6. public void call (){
7. _ service. sayHello ();
8 .}
9. private Service _ service;
10 .}
This class encapsulates the sayHello method provided by the service. Next, define a Service proxy class to encapsulate and queue requests. Of course, this class must implement the Service interface to make the request transparent to the Client. Definition:
11. class ServiceProxy implements Service
12 .{
13. public ServiceProxy (){
14. _ service = new ServiceImp ();
15. _ active_object = new ActiveObject ();
16 .}
17.
18. public void sayHello (){
19. MethodRequest mr = new SayHello (_ service );
20. _ active_object.enqueue (mr );
21 .}
22. private Service _ service;
23. private ActiveObject _ active_object;
24 .}
The definition of other classes and interfaces remains unchanged. Next we will compare the changes in service calls before and after the concurrency logic increases. Before the concurrency logic increases, we will call the sayHello service:
25. Service s = new ServiceImp ();
26. Client c = new Client (s );
27. c. requestService ();
After the concurrency logic is increased, the method for calling the sayHello service is as follows:
28. Service s = new ServiceProxy ();
29. Client c = new Client (s );
30. c. requestService ();
We can see that there is no need to change the ServiceImp of the Client before and after the concurrency logic is added, and the usage is also very consistent. ServiceImp can also be reused independently. The class structure is as follows:
Readers can easily see that the use of the framework also adds some complexity. For some simple applications, it may not be necessary to use the framework at all. We hope that readers can make judgments based on their actual conditions.
Conclusion
This article describes how to build a Java concurrency model framework with a simple example. Some common technologies are used to build the framework. Of course, the framework is compared with some mature commercial frameworks, it seems very immature. For example, if the returned value of a service call is not considered, the thought methods are the same. I hope readers can understand it more deeply, in this way, it is helpful to build your own framework or understand some other frameworks. You can expand the framework in this article and apply it directly to your work.