Java thread model Defects

Source: Internet
Author: User
Tags stack pop

The thread model of the Java programming language may be the weakest part of the language. It is not suitable for actual complex program requirements, and it is not object-oriented at all. We recommend that you modify and supplement the Java language to solve these problems.

The Java thread model is one of the most difficult and satisfying parts of the language. Although the Java language itself supports thread programming, it only applies to very small application environments with little support for thread syntax and class packages.

Most books on Java thread programming have repeatedly pointed out the defects of the Java thread model and provided a class library to solve these problems. I call these classes as first aid kits because the issues they can solve should be included in the Java language's own syntax. In the long run, the syntax rather than the class library method can produce more efficient code. This is because the compiler and Java Virtual Machine (JVM) can optimize program code together, and these optimizations are difficult or impossible to implement for the code in the class library.

Allen Holub pointed out that in my book "Taming Java Threads" (see references) and in this article, I further suggest modifying the Java programming language itself, so that it can truly solve these thread programming problems. The main difference between this article and my book is that I have made more thoughts when writing this article, so I have improved the proposals in this book. These suggestions are just tentative-my personal thoughts on these issues, and implementation of these ideas requires a lot of work and comments from colleagues. But this is, after all, the beginning. I have a dedicated working group for solving these problems. If you are interested, please send e-mail to the threading@holub.com. Once I start, I will send you a notification.

The suggestions made here are very bold. Some people suggest minor and minor modifications to the Java language specification (JLS) (see references) to solve the current fuzzy JVM behavior, but I want to improve it more thoroughly.

In actual drafts, many of my suggestions include introducing new keywords for this language. Although it is usually required not to break through the existing code of a language is correct, if the language does not need to remain unchanged so outdated, it must be able to introduce new keywords. To avoid conflict between the introduced keyword and the existing identifier, I will use a ($) character, which is invalid in the existing identifier. (For example, use $ task instead of task ). In this case, the command line switch of the compiler must be supported to use variants of these keywords instead of ignoring the dollar sign.

  Concept of task

The fundamental problem with the Java thread model is that it is not object-oriented at all. Object-oriented (OO) designers do not consider the issue from the thread perspective at all; they consider synchronous asynchronous information (synchronous information is processed immediately-the message handle is returned only after the information processing is complete; after receiving asynchronous information, it will be processed in the background for a period of time-and the message handle will be returned before the information processing is completed ). The Toolkit. getImage () method in Java programming language is a good example of asynchronous information. The message handle of getImage () is immediately returned without waiting for the entire image to be retrieved by the background thread.

This is an object-oriented (OO) processing method. However, as mentioned above, the Java thread model is not object-oriented. A Java programming language thread is actually a run () process, which calls other processes. There is no object, asynchronous or synchronous information, or other concepts here.

One solution I have discussed in depth in my book is to use an Active_object. An active object is an object that can receive asynchronous requests. It can be processed after receiving the request for a period of time. In Java programming language, a request can be encapsulated in an object. For example, you can transmit an instance implemented through the Runnable interface to this active object. The run () method of this interface encapsulates the work to be done. This runnable object is discharged into the queue by this active object. When it is its turn to execute it, the active object uses a background thread to execute it.

The asynchronous information running on an active object is actually synchronized, because they are retrieved and executed by a single service thread in order from the queue. Therefore, using an active object in a more procedural model can eliminate most synchronization problems.

In a sense, the entire Swing/AWT subsystem of the Java programming language is an active object. The only safe way to send a message to a Swing queue is to call a message similar to SwingUtilities. invokeLater () method, so that a runnable object is sent in the Swing event queue. When it is its turn to execute, the Swing event processing thread will process it.

So my first suggestion is to add the concept of a task to the Java programming language to integrate the active object into the language. (The concept of task is based on Intel's RMX operating system and Ada programming language. Most Real-time Operating Systems Support similar concepts .)

A task has a built-in active object distribution program that automatically manages all the mechanisms that process asynchronous information.

Defining a task is basically the same as defining a class. However, you only need to add an asynchronous modifier before the task method to instruct the allocating program of the active object to process these methods in the background.

All write requests are queued in the active-object Input Queue using a dispatch () process call. Any exception occurred when processing the asynchronous information in the background is handled by the Exception_handler object, which is transferred to the File_io_task constructor.

The main problem with this class-based processing method is that it is too complicated-for such a simple operation, the code is too complicated. After the $ task and $ asynchronous keywords are introduced to the Java language, you can rewrite the previous Code as follows:

Note that the asynchronous method does not specify the return value because its handle is returned immediately, instead of waiting for the request to be processed. Therefore, there is no reasonable return value. For the derived model, the $ task keyword is the same as the class Keyword: $ task can implement interfaces, inherit classes, and inherit other tasks. The method marked with the asynchronous keyword is processed by $ task in the background. Other methods will run synchronously, just like in the class.

The $ task keyword can be modified with an optional $ error clause (as shown above), which indicates that there will be a default handler for any exceptions that cannot be caught by the asynchronous method itself. I use $ to represent the thrown exception object. If the $ error clause is not specified, a reasonable error message (probably stack trace information) is printed ).

Note: To ensure thread security, the parameters of the asynchronous method must be immutable. The runtime system should use relevant semantics to ensure this immutability (simple replication is usually not enough ).

All task objects must support some pseudo information (pseudo-message ).

In addition to common modifiers (such as public), the task keyword should also accept a $ pooled (n) modifier, which causes the task to use a thread pool, instead of using a single thread to run asynchronous requests. N specifies the size of the required thread pool. If necessary, this thread pool can be increased, but it should be reduced to the original size when the thread pool is no longer needed. Pseudo-field $ pool_size returns the original n parameter value specified in $ pooled (n.

In chapter 8 of "Taming Java Threads", I provided a socket handler on the server side as an example of a thread pool. It is a good example of a job that uses a thread pool. The basic idea is to generate an independent object. Its job is to monitor a server socket. Every time a client connects to the server, the server object will capture a pre-created sleep thread from the pool and set this thread to serve the client connection. The socket server will generate an additional customer service thread, but when the connection is closed, these additional threads will be deleted.

The Socket_server object uses an independent background thread to process asynchronous listen () requests. It encapsulates the "accept" loop of the socket. When each client connects, listen () requests a Client_handler to process the request by calling handle. Each handle () request is executed in their own thread (because this is a $ pooled task ).

Note that each asynchronous message sent to $ pooled $ task actually uses its own thread for processing. In typical cases, a $ pooled $ task is used for autonomous operations. To solve potential synchronization problems related to access status variables, the best solution is to use this in the $ asynchronous Method as a unique copy of the object to which it points. This means that when an asynchronous request is sent to a $ pooled $ task, a clone () operation is executed and the this pointer of this method points to this cloned object. Inter-thread communication can be achieved through synchronous access to the static area.

 Improved synchronized

In most cases, $ task eliminates the synchronization operation requirements, but not all multithreading systems use tasks. Therefore, we also need to improve the existing thread module. The synchronized keyword has the following Disadvantages: a timeout value cannot be specified. A thread waiting for request lock cannot be interrupted. You cannot safely request multiple locks. (Multiple locks can only be obtained in order .)

To solve these problems, extend the synchronized syntax so that it supports multiple parameters and can accept a timeout description (specified in the following arc ). The following is my expected Syntax:


Synchronized (x & y & z) obtains the locks of x, y, and z objects.
Synchronized (x | y | z) obtains the lock of the x, y, or z object.
Synchronized (x & y) | z) provides some extensions for the previous code.
Synchronized (...) [1000] sets 1 second timeout to get a lock.
Synchronized [1000] f () {...} obtains this lock when entering the f () function, but can time out in 1 second.


TimeoutException is a derived class of RuntimeException, Which is thrown after the timeout.

Timeout is required, but it is not enough to make the code strong. You also need to be able to suspend the request lock wait from outside. Therefore, when an interrupt () method is transferred to a lock-waiting thread, this method should throw a SynchronizationException object and interrupt the waiting thread. This exception should be a derived class of RuntimeException, so that it does not have to be specially processed.

The main problem with recommended methods for changing synchronized syntax is that they need to be modified at the binary code level. Currently, these codes use the enter-monitor and exit-monitor commands to implement synchronized. These commands have no parameters, so you need to extend the definition of binary code to support multiple lock requests. However, this modification is not easier than modifying the Java Virtual Machine in Java 2, but it is backward compatible with the existing Java code.

Another problem that can be solved is the most common deadlock. In this case, both threads are waiting for the other side to complete an operation.

Imagine that a thread calls a (), but is denied operation right after obtaining lock1 before obtaining lock2. The second thread enters the running state, calls B () and obtains lock2. However, because the first thread occupies lock1, it cannot obtain lock1, so it is in the waiting state. At this time, the first thread is awakened and it tries to obtain lock2, but it cannot be obtained because it is occupied by the second thread. A deadlock occurs.

The compiler (or virtual machine) will rearrange the order of request locks so that lock1 is always obtained first, which eliminates the deadlock.

However, this method is not always successful for multithreading, so you have to provide some methods to automatically break the deadlock. A simple method is to release the acquired lock while waiting for the second lock.

If each program waiting for the lock uses a different timeout value, it can break the deadlock and one thread can run. I suggest replacing the previous code with the following syntax:

The synchronized statement will always wait, but it often gives up the obtained lock to break the potential deadlock. Ideally, the timeout value for each repeated wait is a random value different from the previous one.
Improve wait () and Policy ()

The wait ()/notify () system also has some problems: it cannot detect whether wait () returns normally or because of timeout. Traditional condition variables cannot be used to achieve a "signal" status. Nested monitoring locks are too easy.

Timeout detection can be solved by redefining wait () so that it returns a boolean variable (instead of void. A true return value indicates a normal return, and a false value indicates a return due to timeout.

The concept of State-based conditional variables is very important. If this variable is set to false, the waiting thread will be blocked until the variable enters the true state. Any waiting thread for the true condition variable will be automatically released. (In this case, wait () calls will not be blocked .).

Nested monitoring locking is very troublesome and I have no simple solution. Nested monitoring locks are deadlocks. When the occupying thread of a lock does not release the lock before it suspends itself, this nested monitoring locks occur.

In this example, two locks are involved in get () and put () Operations: one on the Stack object and the other on the shortlist object. Next we will consider the situation when a thread tries to call an empty stack pop () operation. This thread acquires the two locks and then calls wait () to release the lock on the Stack object, but does not release the lock on the list. If the second thread tries to press an object into the stack at this time, it will always be suspended on the synchronized (list) statement and will never be allowed to press an object. Because the first thread is waiting for a non-empty stack, a deadlock will occur. This means that the first thread will never be able to return data from wait () because it occupies the lock, and the second thread will never be able to run the notify () statement.

In this example, there are many obvious ways to solve the problem: for example, use synchronization for any method. But in the real world, the solution is usually not that simple.

One feasible method is to release all locks obtained by the current thread in reverse order in wait (), and then obtain them again in the original order after the waiting conditions are met. However, I can imagine that code using this method is simply incomprehensible to people, so I don't think it is a really feasible method. If you have a good method, please send me an email.

I also hope to wait until the following complex conditions are met one day. For example:

A, B, and c are arbitrary objects.

Modify Thread class

At the same time, the ability to support preemptible and collaborative threads is a basic requirement in some server applications, especially when you want to achieve the highest performance of the system. I think the Java programming language is too far away from simplifying the thread model, and the Java programming language should support Posix/Solaris's "green" and "lightweight) process concept (discussed in chapter 1 of "(Taming Java Threads ). This means that some Java virtual machines (such as Java virtual machines on NT) should simulate collaborative processes within them, while other Java virtual machines should simulate preemptible threads. In addition, it is easy to add these extensions to Java virtual machines.

A Java Thread should always be preemptible. That is to say, a Java programming language thread should work like a Solaris Lightweight Process. The Runnable interface can be used to define a Solaris-type "green thread", which must forward control to other green threads running in the same Lightweight Process.

It can effectively generate a green Thread for the Runnable object and bind it to a lightweight process represented by the Thread object. This implementation is transparent to the existing code, because it is exactly the same as the existing code.

To turn a Runnable object into a green Thread, you only need to pass several Runnable objects to the Thread constructor to extend the existing syntax of the Java programming language, to support multiple green threads in a single lightweight thread. (Green threads can collaborate with each other, but they can be preemptible by the Green Processes (Runnable objects) running on other lightweight processes (Thread objects .). For example, the following code creates a green Thread for each runnable object, which shares the lightweight process represented by the Thread object.

The existing override Thread object and the habit of implementing run () continue to be valid, but it should be mapped to a green Thread bound to a lightweight process. (The default run () method in the Thread () class will effectively create the second Runnable object internally .)

Collaboration between threads

More functions should be added to the language to support inter-thread communication. Currently, the PipedInputStream and PipedOutputStream classes can be used for this purpose. But for most applications, they are too weak. I suggest adding the following function to the Thread class: Add a wait_for_start () method, which is usually in the blocking state until the run () method of a Thread starts. (If the waiting thread is released before calling run, there is no problem ). In this way, a thread can create one or more auxiliary threads, and ensure that these auxiliary threads are running before the creation thread continues to execute operations. Add the $ send (Object o) and Object = $ receive () Methods to the Object class. They use an internal blocking queue to send objects between threads. The blocking queue should be automatically created as the by-product of the first $ send () call. $ Send () calls will add the object to the queue. $ Receive () calls are usually blocked until an object is added to the queue and then it returns this object. The variables in this method should support setting the operation timeout capabilities of the queue and queue: $ send (Object o, long timeout) and $ receive (long timeout ).

   Internal Support for read/write locks

The concept of read/write locks should be built into the Java programming language. The reader lock is discussed in detail in "Taming Java Threads" (and other places). In summary, a read/write lock supports multiple Threads to access an object at the same time, however, at the same time, only one thread can modify this object, and this object cannot be modified during access.

For an object, multiple threads can enter the $ reading block only when there is no thread in the $ writing block. During read operations, a thread attempting to enter the $ writing block is blocked until the read thread exits the $ reading block. When other threads are in the $ writing block, the thread attempting to enter the $ reading or $ writing block will be blocked until the write thread exits the $ writing block.

If both the read and write threads are waiting, by default, the read thread starts. However, you can use the $ writer_priority attribute to modify the class definition.

Access part of the created object should be illegal

Currently, JLS allows access to some created objects. For example, a thread created in a constructor can access the object being created, even if the object is not completely created.

The thread with x as-1 can be set at the same time as the thread with x as 0. Therefore, the value of x cannot be predicted.

One solution to this problem is that, before the constructor returns a result, the priority of the thread created in this constructor is higher than that of the new thread, you must also disable the run () method for running it.

This means that the start () request must be postponed before the constructor returns.

In addition, the Java programming language should allow constructor synchronization. In other words, the following code (which is illegal in the current situation) will work as expected:

I think the first method is more concise than the second method, but it is more difficult to implement it.

Volatile keywords should work as expected

JLS requires that requests for volatile operations be retained. Most Java virtual machines simply ignore this part of content, which is not appropriate. In the case of multi-processor, many hosts have this problem, but it should have been solved by JLS. If you are interested in this, the University of Maryland's Bill put in the work (see references ).

  Access problems

The lack of good access control will make thread programming very difficult. In most cases, if the thread can be called only from the synchronization subsystem, the thread safety issue does not need to be considered. I recommend that you set the following limits on the access permission concept of the Java programming language. You should use the package keyword precisely to restrict package access. I think that the existence of the default behavior is a flaw in any computer language, and I am confused about the existence of this default permission (and this default is "package) "level rather than" private )"). In other aspects, the Java programming language does not provide equivalent default keywords. Although the use of explicit package restrictions can damage existing code, it will make the code more readable and eliminate potential errors of the entire class (for example, if the access permission is ignored due to an error, rather than being ignored intentionally ). Introduce private protected again. Its function should be the same as the current protected, but it should not allow package-level access. Allow the private syntax to specify "implemented access". It is private to all external objects, and even the current object is of the same class. The only reference (implicitly or explicitly) on the left of "." should be this. Extends the public syntax to authorize it to create access to specific classes. For example, the following code should allow objects of the Fred class to call some_method (), but for objects of other classes, this method should be private.

This suggestion is different from the "friend" mechanism of C ++. In the "friend" mechanism, it authorizes one class to access all the private parts of the other class. Here, I suggest strictly controlling access to a limited set of methods. In this way, a class can define an interface for another class, which is invisible to other classes of the system.

Unless the domain references an object that is immutable or basic type of static final, the definition of all domains should be private. Direct access to a domain in a class violates two basic rules designed by OO: Abstraction and encapsulation. From the thread's point of view, allowing direct access to the domain only makes it easier to perform non-synchronous access to it.

Add the $ property keyword. Objects with this keyword can be accessed by a "bean box" application, which uses the introspection API defined in the Class, otherwise it will be the same as private. $ Property is available in fields and methods, so that the existing JavaBean getter/setter method can be easily defined as an attribute.

 Immutability)

Because the access to unchanged objects does not need to be synchronized, the immutable concept (the value of an object cannot be changed after it is created) in the multi-threaded environment is priceless. In Java programming language, immutability is not strictly implemented for two reasons: For an immutable object, it can be accessed before it is completely created. Such access may produce incorrect values for some domains. The definition of a constant (all fields of the class are final) is too loose. For objects specified by final reference, although the reference itself cannot be changed, the object itself can be changed.

The first problem can be solved. The thread cannot start execution in the constructor (or the start request cannot be executed before the constructor returns ).

The second problem can be solved by specifying the final modifier to point to a constant object. That is to say, for an object, only when all the fields are final, and all the referenced object fields are also final, this object is truly constant. In order not to break the existing code, this definition can be enhanced using the compiler, that is, this class is the same class only when a class is explicitly labeled unchanged.

With the $ immutable modifier, the final modifier in the domain definition is optional.

Finally, when an inner class is used, an error in the Java compiler makes it unable to reliably create unchanged objects.

This error message is returned even if the empty final is initialized in each constructor. This error has always occurred in the compiler since internal classes were introduced in version 1.1. In this version (three years later), this error still exists. Now it is time to correct this error.

Instance-level access to a class-level domain

In addition to access permissions, there is also a problem: both the class-level (static) method and the instance (non-static) method can directly access the class-level (static) domain. This access is very dangerous because the synchronization of instance methods does not obtain class-level locks, so a synchronized static method and a synchronized method can still merge class fields at the same time. An obvious way to correct this problem is to require that only static access methods can be used in the instance method to access static domains of non-unchanged classes. Of course, this requirement requires the compiler and runtime check.

Since f () and g () can run in parallel, they can change the value of x at the same time (resulting in an indefinite result ). Remember, there are two locks: the static method requires the lock of the Class object, and the non-static method requires the lock of the Class instance.

Or, the compiler should obtain the use of the read/write lock:

Another method is (this is also an ideal method)-the compiler should automatically use a read/write lock to synchronously access non-unchanged static domains, so that programmers do not have to worry about this problem.

End of background thread

When all non-Background threads are terminated, the background threads are suddenly terminated. When the background thread creates some global resources (such as a database connection or a temporary file) and the background thread ends, these resources are not closed or deleted, which may cause problems.

For this problem, we recommend that you set rules so that the Java Virtual Machine does not close the application in the following cases: any non-Background thread is running, or: any background thread is executing a synchronized method or a synchronized code block.

The background thread can be immediately shut down after it executes the synchronized block or the synchronized method.

Re-introduce the stop (), suspend (), and resume () keywords.

This may not work for practical reasons, but I want not to abolish stop () (in Thread and ThreadGroup ). However, I will change the meaning of stop () so that the existing code will not be destroyed when calling it. However, for the stop () issue, remember that when the thread is terminated, stop () will release all locks, this may potentially bring the thread working on this object into an unstable (local modification) state. Since the stopped thread has released all the locks on this object, these objects cannot be accessed again.

To solve this problem, you can redefine the stop () action so that the thread can be terminated immediately only when no lock is occupied. If it occupies the lock, I suggest terminating it only after this thread releases the last lock. You can use a mechanism similar to throwing an exception to implement this behavior. The stopped thread should set a flag and immediately test it when exiting all synchronization blocks. If this flag is set, an implicit exception is thrown, but this exception should no longer be captured and no output will be generated when the thread ends. Note that Microsoft's NT operating system cannot handle a sudden stop (abrupt) of an external indication ). (It does not notify the dynamic Connection Library of the stop message, which may cause system-level resource vulnerabilities .) This is why I recommend that you use a method similar to an exception to simply result in the return of run.

The actual problem with handling methods similar to this exception is that you must insert code after each synchronized block to test the "stopped" flag. In addition, this additional code will reduce system performance and increase the code length. Another method I have come up with is to make stop () implement a "lazy" stop. In this case, the next call to wait () or yield (). I also want to add an isStopped () and stopped () method to the Thread (at this time, the Thread will work like isInterrupted () and interrupted, but the "stop-requested" status will be detected ). This method is not as common as the first one, but is feasible and does not produce overload.

The suspend () and resume () methods should be put back in the Java programming language. They are very useful and I don't want to be treated as kindergarten children. It is unreasonable to remove them because they may cause potential risks (when suspended, a thread can occupy a lock. Please let me decide whether to use them. If the received threads are occupying the lock, Sun should treat them as a run-time exception (run-time exception) that calls suspend (); or a better way is, delay the actual suspension process until the thread releases all locks.

The blocked I/O should work properly

You should be able to interrupt any blocked operations, instead of simply making them wait () and sleep (). I have discussed this issue in the socket section in chapter 2 of "Taming Java Threads. But now, for an I/O operation on a blocked socket, the only way to interrupt it is to close this socket, and there is no way to interrupt a blocked file I/O operation. For example, once a Read Request starts and enters the blocking status, the thread remains in the blocking status unless it actually reads something. The read operation cannot be interrupted even if the file handle is disabled.

In addition, the program should support I/O operation timeout. All objects (such as InputStream objects) that may block operations should also support this method.

This is equivalent to the setSoTimeout (time) method of the Socket class. Similarly, timeout must be passed as a parameter to a blocked call.

 ThreadGroup class

ThreadGroup should implement all the methods in Thread that can change the Thread state. I especially want it to implement the join () method, so that I can wait for the termination of all threads in the group.

Summary

The above is my suggestion. As I said in the title, if I am a king... (AH ). I hope these changes (or other equivalent methods) will eventually be introduced into the Java language. I do think that Java is a great programming language, but I also think that the Java thread model is not well designed. This is a pity. However, the Java programming language is evolving, so there are still prospects for improvement.

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.