|
SYS-CON.TV Webcasts
Comments
Did you read today's front page stories & breaking news?
SYS-CON.TV
|
Top Links You Must Click On
General Java Eiffel-Like Separate Classes
Eiffel-Like Separate Classes
Nov. 1, 2000 12:00 AM
To extend Java's concurrent behavior in a more natural way, in a more object-oriented point of view, we propose an extension to Java's concurrency model that will emulate Eiffel's separate statement. (Eiffel is an object-oriented language with a comprehensive approach to software construction.) The extension permits the attachment of nonphysical processors or threads to objects, thus allowing them to behave in an asynchronous and completely independent manner. This article briefly shows the concurrency tools of the Java programming language, points out their shortcomings, proposes solutions, and ends with the implementation of a solution. It's useful to evaluate Java's concurrent programming model by reviewing how this language implements the concepts explained by Bertrand Meyer and referred to as "The three forces of computation," which represent the statement: "To perform a computation is to use certain processors to apply certain actions to certain objects." Java classes and objects play the same role in Eiffel as they do in Java. In Java the notion of a nonphysical processor fits in with the concept of thread as represented by the Thread class. A processor would then represent an autonomous thread of control that's capable of supporting the sequential execution of instructions on one or more objects. Differences arise when we look at the possible assignment of processors to actions. In Java we have only the run() method, which belongs to the Runnable interface, for concurrent execution. This interface must be implemented by a class attached to a Thread instance in construction time and then, and only then, can we explicitly start the thread's execution, employing the technique known as delegation. The problem originates from the fact that threads and objects represent different entities - methods run on threads, objects do not - so the only way to get a method to run concurrently (in an independent thread) is to call it from inside that thread's run() method. This is a disadvantage of this model since the object technology's basic model for computation would somehow be broken, as we can easily conclude from the following statement: objRef.operID(args);
Here the execution of method operID(), called as part of an action performed on a client object, won't occur concurrently as long as it's not placed inside a thread's run() method. The model is said to be broken since there's no natural, object-oriented way in Java to provoke the attachment of different streams of execution to both the client object and objRef. The entire class design should be oriented to the implementation of the Runnable interface and the creation of Thread class instances.
The benefits of this mechanism are based on the fact that Java clearly separates the notion of data abstraction, implemented by classes, from the notion of control, represented by the framework that's composed of the Thread and ThreadGroup classes and the Runnable interface. Table 1 shows the main components of this framework. A Closer Look at Eiffel's Model We'll now analyze Eiffel by looking for features that are capable of improving the former concurrency model. According to Meyer: "... any object O2 is handled by a certain processor, its handler; the handler is responsible for executing all calls on O2 (all calls of the form x.f (a) where x is attached to O2)." From this statement we can conclude that the proposed model has two different call semantics:
In Java the default semantic is obviously synchronous. It doesn't provide any syntactic construction in its classes that will specify a different processor for its methods. So we need to define asynchronous execution through the use of Thread instances and the method run. This is the only way to define an asynchronous split of control.
Defining Separate Entities in Java:
A Straightforward Implementation To implement this difference in Java, we'll follow Eiffel's approach, which allows us to define separate entities in two ways. The first one permits the creation of an object's instance and attaches it as an independent processor. x: separate A Here we're declaring an object x, instance of class A, that you'd execute on a different processor. The second approach takes a static form and is applicable when all the instances of a class are intended to be separate entities: separate class A ... We cover both forms in this article. Our semantics require a new processor to handle all the messages for each of the instances of our separate classes or objects. Obviously, we need to modify the Java syntax, adding a new keyword as an optional modifier in class and field definitions. To accomplish this, a preprocessing approach is proposed. This means that a parser program should be developed for parsing the Java source file and eliminate, if included, the separate modifier and replace it with some standard code. The following is our first approach to the problem, but not the best one. We'll add some code before any invocation to a public method, and in separate-declared classes replace every method's body with code that's responsible for the creation of a new thread and the execution of its run() method. Inside this method we'll select the original code and invoke it. That's not the exact semantic of separate in Eiffel, but it provides us with a good place to start our evaluation. Since we need to intercept method invocation, we use the Java Reflection API.
Java Tools for the Solution The API defines the following elements:
Let's now proceed with our desired output definition on the basis of these elements.
Implementing the Solution
Two things must be taken into account:
The security police that are used could affect the way we obtain the original method references, the one we invoke later in method run(). In Listing 1 we defined the method m2() as public, since the getMethod() method just returns Method objects that reflect the specified public member of the class. To obtain a reference to members affected by private-, protected-, or default-access modifiers, like m1(), for example, we can take one of the two approaches listed below:
The second item to take into account is the methods' formal parameter treatment that must be done. The reflection API allows us to refer them at runtime through an array of Object instances, the __params variable used in Listing 2. Because of this, you shouldn't declare any parameter belonging to some of Java's primitive types. Instead we propose the use of the corresponding wrapper classes found in the java.lang package such as Integer, Float, and Double.
Separate as a Modifier for Class Fields To accomplish this, we've implemented the desired output in an inner class whose role is to permit the execution of each method's invocation in an object's thread. We use the starting code in Listing 3, then after preprocessing we should obtain the code in Listing 4. As you can see, the changes affect classes A and B. The _Aux class in A encapsulates the behavior to execute in the separate case. Each invocation of m1() in B should be replaced with a separate_m1(). Another detail to take into account is the use of the getDeclaredMethod() method, instead of the getMethod() mentioned earlier, for taking a reference to the method that will be invoked. The main reason behind this decision is that we're invoking methods of A from class B, so we can't make them all of public type. One more thing: the solution of encapsulating the separate behavior in an inner class is perfectly portable to the first analyzed case. The difference is just in the semantics of the problem.
Implementing the Preprocessing Parser
The first two tasks are frequently encompassed and performed by an auxiliary application named scanner. Its main objective consists of building high-level language units called tokens, while discarding constructs that don't represent any useful information, such as white spaces and comments. Figure 3 depicts this functionality. The actions mentioned in Step 3 try to match the tokens generated by the scanner against a language specification. If this match becomes successful, the parser then performs some action upon the construct. In our case it could generate the code that implements the separate behavior. Building a parser from the ground up is a highly complex and time-consuming task. We used a parser generator, namely Sun's Java Compiler Compiler version 1.0, or JavaCC, which can be freely downloaded from Sun's Web site or from Metamata.com. The last one offers the parser generator and a set of example grammars. A parser generator is usually a tool that's capable of generating parser and scanner programs starting from a grammar specification of the language to one we want to parse. The Java grammar we used was one of the samples that ships with JavaCC from Metamata.com, which covers Java's 1.1 language extensions. JavaCC is based on ANTLR technology; also, it is an LL(k) predicate-based parser generator. It is beyond the intention of this article to describe in depth the details of the parser generator's algorithms. The interested reader should note the references at the end of this article.
Changes to the Java Input Grammar
These changes are very simple since the specification language to build the input description is Java-based with a few extensions to specify grammars (see Listings 5 and 6). Note: There's a special method for analyzing a class field, instead of a general method for fields belonging to classes or interfaces. That's because the interface's fields may not be declared separate.
Output Generation To accomplish this generation, the only things to take into account are the method definitions, since separate behavior consists only of executing every method invocation in its own processor.
JVM Scheduling vs Eiffel's Concurrency Control Files Eventually we need to assign our physical resources to the processors of our program. In Eiffel we have the Concurrency Control File as a mechanism for defining this mapping. In Java it's impossible to define such things programmatically. Threads are created dynamically in runtime as any other object, and the scheduling is left to the Java Virtual Machine (JVM). We can use groups of threads and priorities to obtain some level of control over the execution of threads, but the core algorithm is embedded in the JVM and, in current versions of the JDK, it's not possible to customize or substitute. In fact, it's somewhat implementation-defined (as noted in earlier versions of JDK, Solaris and Windows implementations differ in that the first is not preemptive). In Java, distributed programming uses an additional API, called RMI, for communicating remote JVMs via remote interfaces. Alternatively, we can use IDL and CORBA. The JVM is an operating system process in current commercial OSs, so they're under control of the OS process control. We haven't heard about parallel implementations of the JVM, exploiting the runtime information about threads, but it's clear that it would be advantageous to obtain better performance in architectures that include multiprocessing.
Conclusion Our approach helped us to unify, in some way, the notion of objects and threads, giving a more consistent object-oriented view to the design and implementation of concurrent applications in Java. The newly introduced model keeps the conceptual architecture strictly separate from the physical one; the former assumes that there'll be as many resources as the application needs; the latter is responsible for the creation of threads and their assignation to separate objects. Another benefit to this approach is applicable to the academic field; we've taught Concurrent Java Design for three years and lacked a uniform object-oriented-based vision of the problem. With our extension, the design of concurrent Java applications is divided into two stages. The first is based on objects that are affected by the separate clause in which we apply the usual object-oriented execution mechanism of object.operation(args) and introduce the concurrency concepts in a more natural way. In the second stage we present Java's concurrency tools in more detail, going deeper into its mechanism and behavior. Through this division, new concepts become simpler and more natural for the student since the move from sequential to concurrent programming just represents, initially, a little change in processor assignation. One problem is the management of distributed resources. In Eiffel resources are mapped to processors through the Concurrency Control File, which enables you to manage the allocation of local and remote resources to processes. This unified view of mapping creates a better plan for running threads, allowing applications to control the physical allocation. The only ways for thread control in Java are, as mentioned previously, the Thread and ThreadGroup classes and the methods inherited from the Object class. These elements have to be combined with the functionality contained in the RMI package when we're developing distributed multithreaded applications. This makes this work error-prone and creates hard-to-predict client machine dependencies. Hence, we're preparing an extension of Java's thread control capabilities with support for distributed resources management.
References
Reader Feedback: Page 1 of 1
Enterprise Open Source Magazine Latest Stories . . .
Subscribe to the World's Most Powerful Newsletters
Subscribe to Our Rss Feeds & Get Your SYS-CON News Live!
|
SYS-CON Featured Whitepapers
Most Read This Week |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||