Java Thin-Client Programming Jürgen Friedrichs, Henri Jubin, Martin Chilvers, Bernard Devaux, Mark Briggs
International Technical Support Organization http://www.redbooks.ibm.com
SG24-5118-00
SG24-5118-00
International Technical Support Organization Java Thin-Client Programming March 1999
Take Note! Before using this information and the product it supports, be sure to read the general information in Appendix B, “Special Notices” on page 211.
First Edition (March 1999) This edition applies to version 1.1.4 of the Java Development Kit for the use with the IBM Network Station 1000 and the IBM Network Station Manager 3.0. Comments may be addressed to: IBM Corporation, International Technical Support Organization Dept. DHHB Building 003 Internal Zip 2834 11400 Burnet Road Austin, Texas 78758-3493 When you send information to IBM, you grant IBM a non-exclusive right to use or distribute the information in any way it believes appropriate without incurring any obligation to you. © Copyright International Business Machines Corporation 1999. All rights reserved Note to U.S Government Users – Documentation related to restricted rights – Use, duplication or disclosure is subject to restrictions set forth in GSA ADP Schedule Contract with IBM Corp.
Contents Figures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vii Tables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .xi Preface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xiii The Team That Wrote This Redbook . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xiii Comments Welcome . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xiv Chapter 1. Introduction to Threading Issues . . . . . . . . . . . . . . . . . . . . . 1 1.1 What is in a Thread? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.1.1 Parallelism and Time-slicing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.1.2 What is Shared? What is Not? . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.1.3 Thread Safety . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.1.4 Creating and Running a Thread . . . . . . . . . . . . . . . . . . . . . . . . . . 5 1.1.5 Why Bother With Threads? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 1.2 Basic Synchronization Strategies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 1.2.1 Using the "synchronized" Keyword . . . . . . . . . . . . . . . . . . . . . . . . 7 1.2.2 Using Delegation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 1.2.3 A Simple Strategy: Don’t Synchronize . . . . . . . . . . . . . . . . . . . . . 9 1.3 Unpicking Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 1.3.1 Thread States . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 1.3.2 Synchronized Revisited . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 1.3.3 Priority . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 1.3.4 Thread Groups . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 1.4 Extending the Threading Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 1.4.1 Mutexes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 1.4.2 Condition Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 1.4.3 Read-Write Mutexes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 1.5 An Example Servlet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 1.5.1 The Traffic Report Servlet. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 1.5.2 First Version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 1.5.3 Second Version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 1.6 Asynchronous Method Invocation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 1.6.1 Sidebar: Making a Pseudo-Thread . . . . . . . . . . . . . . . . . . . . . . . 47 1.6.2 Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 1.6.3 Creating Threads to Invoke Callbacks . . . . . . . . . . . . . . . . . . . . 51 1.6.4 Thread Pools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 1.7 What is Missing? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 Chapter 2. Domino Go and IBM WebSphere Application Server . . . . . 55 2.1 Installation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
© Copyright IBM Corp. 1999
iii
2.1.1 2.1.2 2.1.3 2.1.4 2.1.5
Reading the Documentation . . . . . Getting Started . . . . . . . . . . . . . . . Adding a Servlet . . . . . . . . . . . . . . Configuring and Loading a Servlet Monitoring a Servlet . . . . . . . . . . .
.. .. .. .. ..
.. .. .. .. ..
. . . . .
. . . . . . . . . . . . . . . . . . 57 . . . . . . . . . . . . . . . . . . 57 . . . . . . . . . . . . . . . . . . 60 . . . . . . . . . . . . . . . . . . 61 . . . . . . . . . . . . . . . . . . 62
Chapter 3. Security Considerations . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 3.1 Language Features . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 3.1.1 Security of Address Space . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 3.1.2 Java Virtual Machine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 3.1.3 Loading Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 3.1.4 The Verifier. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 3.2 Custom Class Loaders . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 3.2.1 Example Class Loader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 3.3 The Security Manager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 3.3.1 Traps Into a Security Manager . . . . . . . . . . . . . . . . . . . . . . . . . . 74 3.3.2 Example Security Manager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 3.4 Authentication and Data Integrity . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 3.4.1 Message Digests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 3.4.2 Digital Signatures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 3.4.3 Using Digital Signatures In Java . . . . . . . . . . . . . . . . . . . . . . . . . 89 3.4.4 Authentication Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 3.4.5 Signed Applets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 Chapter 4. Using CORBA on the Network Computer . . . . . . . . . . . . . 121 4.1 What is CORBA? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 4.1.1 The Object Management Architecture (OMA) . . . . . . . . . . . . . . 121 4.1.2 The CORBA ORB Architecture . . . . . . . . . . . . . . . . . . . . . . . . . 123 4.1.3 CORBA versus Java RMI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124 4.2 A Simple Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126 4.2.1 Installing the ORB for NC Applications . . . . . . . . . . . . . . . . . . . 126 4.2.2 The IDL Interface Definition . . . . . . . . . . . . . . . . . . . . . . . . . . . 128 4.2.3 Writing the "Hello World" Client . . . . . . . . . . . . . . . . . . . . . . . . 129 4.2.4 Writing the "Hello World" Server . . . . . . . . . . . . . . . . . . . . . . . . 132 4.2.5 Generating the Stubs and Skeletons . . . . . . . . . . . . . . . . . . . . 135 4.2.6 Installing and Testing the "Hello World" Application . . . . . . . . . 135 4.2.7 Installing and Testing the C++ "Hello World" Server . . . . . . . . . 136 4.2.8 The "Hello World" Client as an Applet. . . . . . . . . . . . . . . . . . . . 137 4.2.9 Installing the ORB for Applets. . . . . . . . . . . . . . . . . . . . . . . . . . 137 4.2.10 Writing the "Hello World" Client Applet . . . . . . . . . . . . . . . . . . 138 4.2.11 Installing and Testing the "Hello World" Client Applet . . . . . . . 140 4.3 A CORBA Naming Service Example . . . . . . . . . . . . . . . . . . . . . . . . . 140 4.3.1 What is the CORBA Naming Service? . . . . . . . . . . . . . . . . . . . 140
iv
Java Thin-Client Programming
4.3.2 Installing the Naming Service for NC Applications . . . . . . . . . . 142 4.3.3 Writing the "Hello World" Client using the Naming Service . . . . 142 4.3.4 Writing the "Hello World" Server Using the Naming Service . . . 144 4.3.5 Installing and Testing the Naming Service Example . . . . . . . . . 146 4.3.6 Installing the Naming Service for Applets . . . . . . . . . . . . . . . . . 147 4.3.7 Writing the "Hello World" Client Applet . . . . . . . . . . . . . . . . . . . 148 4.3.8 Installing and Testing the Naming Service Client Applet . . . . . . 148 4.4 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149 Chapter 5. Object Recycling . 5.1 Why Recycle? . . . . . . . . . . 5.1.1 The Example Classes . 5.2 Recyclable Instances . . . . . 5.3 Instance Pools . . . . . . . . . . 5.4 Conclusions . . . . . . . . . . . . 5.5 Oddities . . . . . . . . . . . . . . .
.. .. .. .. .. .. ..
. . . . . . .
.. .. .. .. .. .. ..
. . . . . . .
.. .. .. .. .. .. ..
. . . . . . . . . . . . . . . . . . . . . . . 151 . . . . . . . . . . . . . . . . . . . . . . . 151 . . . . . . . . . . . . . . . . . . . . . . . 151 . . . . . . . . . . . . . . . . . . . . . . . 152 . . . . . . . . . . . . . . . . . . . . . . . 156 . . . . . . . . . . . . . . . . . . . . . . . 161 . . . . . . . . . . . . . . . . . . . . . . . 162
Chapter 6. Serialization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165 6.1 What Is Serialization?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165 6.2 Why Using Serialization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166 6.2.1 Serialization to Manage Persistent Objects. . . . . . . . . . . . . . . . 166 6.2.2 Serialization to Preinitialize a Set of Objects. . . . . . . . . . . . . . . 167 6.2.3 Serialization to Transfer Objects From The Server to The NC . 168 6.2.4 Serialization to Avoid Initialization on the NC . . . . . . . . . . . . . . 169 6.3 An Example of Serialization for a NC . . . . . . . . . . . . . . . . . . . . . . . . 171 6.3.1 Declaration of the Classes for the Detailed Description . . . . . . 172 6.3.2 Creating the Catalog of Descriptions on the Server . . . . . . . . . 173 6.3.3 A Reader to Deserialize Objects on the Client . . . . . . . . . . . . . 174 6.3.4 Using the SerializedObjectReader in an Applet . . . . . . . . . . . . 175 6.4 Using Serialization With Care . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176 6.4.1 Efficiency with Specialized Serialization . . . . . . . . . . . . . . . . . . 176 6.4.2 Only Serialize the Objects You Need . . . . . . . . . . . . . . . . . . . . 177 6.4.3 Using Different Versions of the Same Class . . . . . . . . . . . . . . . 177 6.4.4 Security and Integrity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182 6.5 Reasons Not to Use Serialization . . . . . . . . . . . . . . . . . . . . . . . . . . . 182 Chapter 7. Model View Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 7.1 Model View Definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 7.1.1 The Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 7.1.2 The View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 7.1.3 Model View Separation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186 7.2 Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186 7.2.1 The Model View Controller Pattern . . . . . . . . . . . . . . . . . . . . . . 186 7.2.2 The Model View Pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187 v
7.3 The MV Pattern in an NC Application . . . . . . . . . . . . . . . . . . . . . . . . 187 7.4 Using RMI in a Model View Pattern . . . . . . . . . . . . . . . . . . . . . . . . . 188 7.4.1 Definition of the Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188 7.4.2 The Method changeName() . . . . . . . . . . . . . . . . . . . . . . . . . . . 189 7.4.3 Creation of the Panel for the GUI . . . . . . . . . . . . . . . . . . . . . . . 189 7.4.4 Using the RMI tools for the PersonProxy Bean . . . . . . . . . . . . . 189 7.4.5 Visual Programming with the PersonProxy Bean . . . . . . . . . . . 190 7.4.6 Settings of the PersonProxy Bean . . . . . . . . . . . . . . . . . . . . . . 191 Chapter 8. Profiling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193 8.1 Profiling Pitfalls . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193 8.2 Sample Application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194 8.3 JInsight . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194 8.3.1 Installing JInsight . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195 8.3.2 Creating a Trace File . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196 8.3.3 Visualizing Trace Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196 8.3.4 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200 8.4 JProbe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200 8.4.1 Installing JProbe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200 8.4.2 Creating a Snapshot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201 8.4.3 Analyzing the Snapshot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202 8.4.4 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205 8.5 JavaSoft JDK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205 8.5.1 Creating the Profile Information File . . . . . . . . . . . . . . . . . . . . . 206 8.5.2 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206 8.6 Conclusions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206 Appendix A. Related Publications. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209 A.1 International Technical Support Organization Publications . . . . . . . . . . 209 A.2 Redbooks on CD-ROMs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209 A.3 Other Publications. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209 Appendix B. Special Notices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211 How to Get ITSO Redbooks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215 How IBM Employees Can Get ITSO Redbooks . . . . . . . . . . . . . . . . . . . . . . . 215 How Customers Can Get ITSO Redbooks. . . . . . . . . . . . . . . . . . . . . . . . . . . 216 IBM Redbook Order Form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217 Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219 ITSO Redbook Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
vi
Java Thin-Client Programming
Figures 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40.
Code Fragment of an Unsafe Object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Starting a Thread that Implements java.lang.runnable . . . . . . . . . . . . . . . . 6 Excerpt of Class java.util.Vector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Synchronizing on Particular Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Sample of Delegation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Sample for Access Modifiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 State-transition Diagram for Threads. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 Implementation of a Mutex. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Consumer-Producer Solution Without Condition Variables . . . . . . . . . . . . 21 Class CondVar. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 Output Sample of the Producer-Consumer Test . . . . . . . . . . . . . . . . . . . . 28 General Outline for Use of Condition Variables . . . . . . . . . . . . . . . . . . . . . 29 Constructor and Lock Methods of Class RWMutex . . . . . . . . . . . . . . . . . . 31 Method Unlock of Class RWMutex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 HTML Source for the Input Form for the Traffic Servlet. . . . . . . . . . . . . . . 36 Screen Shot of the HTML Form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 Sample Result of a Traffic Servlet Request . . . . . . . . . . . . . . . . . . . . . . . . 40 Critical Section for Update Operation of Class TrafficServlet . . . . . . . . . . 46 Critical Section for Read Operation of Class TrafficServlet . . . . . . . . . . . . 46 Sample Output From the Asynchronous Invocation Program . . . . . . . . . . 52 Application Server Components Window. . . . . . . . . . . . . . . . . . . . . . . . . . 56 Application Server Guide Introduction Page . . . . . . . . . . . . . . . . . . . . . . . 57 Application Server Manager Login Screen . . . . . . . . . . . . . . . . . . . . . . . . 58 View of Services . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 Servlet Configuration Window . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 Servlet Monitor View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 Install ClassLoader, Create Object, and Invoke a Method. . . . . . . . . . . . . 72 Class DoSomething for Testing the Class Loader . . . . . . . . . . . . . . . . . . . 72 Pseudo-Code Fragment for a Security Check . . . . . . . . . . . . . . . . . . . . . . 75 Output from the Example Security Manager Test . . . . . . . . . . . . . . . . . . . 80 Method checkExit() of OurSecurityManager . . . . . . . . . . . . . . . . . . . . . . . 81 Method checkWrite() of OurSecurityManager . . . . . . . . . . . . . . . . . . . . . . 81 Method checkRead() of OurSecurityManager . . . . . . . . . . . . . . . . . . . . . . 82 Program to Generate and View a Message Digest . . . . . . . . . . . . . . . . . . 87 Authenticated Exchange Using Digital Signatures. . . . . . . . . . . . . . . . . . . 90 Talker Applet in Browser Window . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 Spit-pane Server Dialog Window . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 Message Box Informing Client that the Session is Terminated . . . . . . . . . 92 Serializable Byte Buffer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 Serializable String Buffer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
© Copyright IBM Corp. 1999
vii
41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. 78. 79. 80. 81. 82. 83.
viii
Serializable Key Holder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 Method OurSecurity.Util.genKeys() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 Method OurSecurity.Util.genSig() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 Method OurSecurity.Util.verifySig() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 HTML Source for the Talker Applet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 Directives in the File cert.dir. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 Output Obtained by Using javakey to Display a Certificate . . . . . . . . . . . 111 Directives for Signing an Archive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 javakey Output Generated by Signing the Talker Archive . . . . . . . . . . . . 112 HTML File for the Signed Talker Applet. . . . . . . . . . . . . . . . . . . . . . . . . . 113 Directive File for Signing a Certificate of Another Identity . . . . . . . . . . . . 115 Directive File to Sign an Archive with a Second Certificate . . . . . . . . . . . 115 Signed Applet Error Message . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 Console Containing Applet Security Exception Chain . . . . . . . . . . . . . . . 116 HotJava Basic Applet Security Page . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 Dialog to Verify a Certificate’s Fingerprint . . . . . . . . . . . . . . . . . . . . . . . . 118 HotJava Advanced Security Settings Page . . . . . . . . . . . . . . . . . . . . . . . 119 HotJava Dialog Requests Confirmation of a Potentially Unsafe Action . . 120 The Object Management Architecture (OMA) . . . . . . . . . . . . . . . . . . . . . 122 The CORBA ORB Architecture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 Stub and Skeleton Generation in CORBA and Java RMI . . . . . . . . . . . . 125 The Structure of the "Hello World" Application . . . . . . . . . . . . . . . . . . . . 126 IDL for the "Hello World" Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128 "Hello World" Client Application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 Examples of CORBA Naming Contexts. . . . . . . . . . . . . . . . . . . . . . . . . . 141 The Serialization Process . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165 Example of the Serialization for a Persistence Management . . . . . . . . . 167 Using Serialization in Order to Save Default Preferences . . . . . . . . . . . . 168 Transfer of Objects from Server to NC using Serialization . . . . . . . . . . . 169 Factoring Process Produces Resulting Objects. . . . . . . . . . . . . . . . . . . . 170 Data on the Server, Processing on the NC . . . . . . . . . . . . . . . . . . . . . . . 170 Minimal Processing on the NC. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171 Exception When Class Gets Deserialized into a New Class Version . . . 178 Serialver With Option -show . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180 First Approach to Migrate a Class Stream . . . . . . . . . . . . . . . . . . . . . . . . 180 Second Approach to Migrate a Class Stream in Two Steps . . . . . . . . . . 181 ProfileMe Class Definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194 Histogram View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197 Invocation Browser View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198 Execution Pattern View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199 Reference Pattern View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200 JProbe Memory Usage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202 JProbe Call Graph . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
Java Thin-Client Programming
84. JProbe Method Detail . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204 85. JProbe Method List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205 86. Output from the JDK Profiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206
ix
x
Java Thin-Client Programming
Tables 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.
Network Station Manager Properties for the "Hello World" Application . . 136 NSM Properties for the Naming Service Example . . . . . . . . . . . . . . . . . . 147 NSM Properties for the "ProfileSimple" Test Programs . . . . . . . . . . . . . . 155 Results for ProfileSimple.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 Results for ProfilePool.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160 Profiling Results 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 Profiling Results 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164 Parameters Used for the Serialization Applet Sample. . . . . . . . . . . . . . . 175 Compatible Changes for Serialization . . . . . . . . . . . . . . . . . . . . . . . . . . . 179 Incompatible Changes for Serialization . . . . . . . . . . . . . . . . . . . . . . . . . . 179
© Copyright IBM Corp. 1999
xi
xii
Java Thin-Client Programming
Preface In an astonishingly short period of time, Java has emerged as a major force in the computing landscape, both as a programming language for the ’90s and as a new platform for the development of heterogeneous network-centric systems. Along with the rise of Java has come the idea of network computing, or the use of Java applications running on thin clients such as network computers. This redbook focuses primarily on topics such as threading, from a Java thin client programming perspective. Also, because these applications are in a multi-user environment, security issues will be covered. The use of CORBA on the network-computing environment is also introduced. Java programming issues, such as object recycling and serialization, are necessary topics to consider when dealing with client/server applications. When an application is developed under the object-oriented paradigm, an introduction to simple concepts of model/view programming techniques will help the reader. Last, but not least, the basics techniques and tools required for profile applications are presented.
The Team That Wrote This Redbook This redbook was produced by a team of specialists from around the world working at the International Technical Support Organization, Austin Center. Juergen Friedrichs is a project leader in the OO/AD group at the International Technical Support Organization (ITSO), Austin Center. Before joining the ITSO in 1997, Juergen worked in Technical Marketing Support for OS/2, Warp Server and TCP/IP in Germany. Henri Jubin is currently working for the International Technical Support Organization (ITSO) in Austin, where he covers the area of Object Oriented Technology. Henri has previously worked in various support and consulting positions within IBM France. He has dealt with topics such as Object Oriented Technology and OS/2. Mark Briggs is a Research and Development Engineer in the DSTC (Distributed Systems Technology Centre) in Brisbane, Australia.
© Copyright IBM Corp. 1999
xiii
Martin Chilvers is Research Scientist in the DSTC is Brisbane, Australia. He’s an OO analyst and design specialist and active as a CORBA consultant/instructor. His personal experience covers the implementation of a CORBA ORB and CORBA naming services. Bernard Devaux is an experienced OO development specialist. Bernard has worked for about 8 years on Smalltalk projects and is currently working on a project delivering a distributed and 100% pure Java application ready for network computing. The authors and project leaders would like to thank the following people for their invaluable contributions to this project: Gary Huber, IBM Network Computing Division, Austin, TX Lauren Kingman, IBM Network Computing Division, Raleigh
Comments Welcome Your comments are important to us! We want our redbooks to be as helpful as possible. Please send us your comments about this or other redbooks in one of the following ways: • Fax the evaluation form found in “ITSO Redbook Evaluation” on page 223 to the fax number shown on the form. • Use the electronic evaluation form found on the Redbooks Web sites: For Internet users For IBM Intranet users
http://www.redbooks.ibm.com http://w3.itso.ibm.com
• Send us a note at the following address:
[email protected]
xiv
Java Thin-Client Programming
Chapter 1. Introduction to Threading Issues Threading is an integral part of Java. Many other languages such as C++ have no intrinsic threading capability, relying instead on the underlying primitives of the operating system. When the threading model differs between computing platforms, portability is reduced. Java cannot use this non-portable approach, so lightweight threads of control are directly supported by the Java language. There is a reserved word, synchronized, that is intended to make working with threads easier for the programmer. There are two packages in the java.lang hierarchy specifically concerned with threads, java.lang.Thread and java.lang.ThreadGroup, together with an interface java.lang.runnable. In additional, certain methods of java.lang.Object exist exactly for the purpose of implementing multi-threaded applications. Threading is an inherently difficult subject. Many application programmers are not familiar with using threads. For most applications, a thorough understanding is not necessary. However, for certain types of applications, most notably servers , threading is essential to achieving an optimal throughput. This chapter explains some of the issues related to threading and outlines some techniques, for writing multi-threaded servers. This is not intended to be a comprehensive treatment of the subject. This chapter is not intended to be a comprehensive tutorial for using the java.lang.Threads package. Many aspects of working with the API at the most basic level, for example to create a thread and start it running, are glossed over. For reference, the reader should see the documentation at: http://www.javasoft.com/products/jdk1.1/docs/index.html
A tutorial on threading can be found at: http://java.sun.com/docs/books/tutorial/essential/threads/index.html
1.1 What is in a Thread? A thread is basically a single flow of control. Within a flow of control, a sequence of instructions are executed. In the Java Virtual Machine, this means that Java bytecodes are interpreted. This flow of control may contain branches and iterations. Those are simply part of the instruction sequence.
© Copyright IBM Corp. 1999
1
Most programs contain only one thread. In a Java application, the entry point of this thread is the method main(). As this method executes, objects are created, accessed, have their methods invoked and are destroyed. When main() returns, the flow of control is complete, and the thread ends. For an applet, things are not so obvious, because the applet’s methods are being invoked by an object maintained from outside its view, by the Web browser. However, the applet is simply an object within some thread of control. Threads can be interwoven to comprise an application, just as normal threads are interwoven to comprise cloth. When an application is multi-threaded, threads of control run in parallel. Something similar is seen by every user of modern computer systems. When several applications are running at the same time in a Windows system, or many users are logged on to an MVS or Unix system, the operating system is handling several concurrent processes. This is known as multi-tasking. Processes are executed within their own virtual address space. The operating system ensures that processes cannot interfere with the address space or with the execution flow of other processes. Of course, on many systems this does occasionally happen, because of some bug in the operating system kernel. The fundamental difference between processes and threads is that a thread executes within the address space of an application, along with other threads. For this reason, threads are often referred to as lightweight processes. Because there are several parallel sequences of execution, all with access to the same data, the potential for damage, in the form of data corruption, is considerable.
1.1.1 Parallelism and Time-slicing It may be noted that many multi-process systems actually execute on a single hardware processor. Processes are time-sliced, meaning that the operating system allows each process some time to execute before switching to another process. In single processor systems, this requires that the kernel runs at a high priority, and is re-entrant due to interrupts in the operating system. The kernel is responsible for storing and restoring process state, program counter and all machine registers, etc. A similar situation exists for threads in a single processor environment. However, the overhead of storing execution state for a thread is considerably less than for a process.
2
Java Thin-Client Programming
The situation known as pseudo-parallelism exists whenever there are less processors than processes and threads, that is, practically always. However, this is purely a technicality. It is usually easier to think of threads as running strictly in parallel. Time-slicing in almost all systems, is preemptive, which means that a process or thread may be interrupted at any time, without its knowledge. In the Java Virtual Machine, threads are indeed time-sliced on a preemptive basis.
1.1.2 What is Shared? What is Not? Not all of the data available to one thread is available to others. Every thread has it’s own stack and program counter. If this were not the case, there would be the overwhelming complexity of interleaving stack frames, and chaos would ensue whenever a thread terminated while executing a method at the end of a deep call chain. So local variables of base types such as int and double, are private to a thread. References to local objects and return values are kept on the stack, so in general they are not a cause for concern in a multi-threaded environment. In Java, all objects are dynamically created in heapspace, and they will be potentially shared among threads. However, the Java Virtual Machine ensures that no thread can access an object without a valid reference. One way to export a reference is to pass it as a method parameter to an object that can be manipulated by other threads. An object instance may also be shared if its reference can be assigned to a field of some globally visible object. In addition, class instances are available to all threads.
1.1.3 Thread Safety When an object is modified there is usually a period of time when it is in an inconsistent state. For example, consider the code of Figure 1 on page 4:
Introduction to Threading Issues
3
class AnObject { // definitions private int field1; private double field2; public AnObject(AnObject other) { field1 = other.field1; field2 = other.field2; } public void setFields(int i, double d) { field1 = i; field2 = d; } } Figure 1. Code Fragment of an Unsafe Object
There are two assignment operations in these methods, so they are not atomic. In fact it is usual that not even a single assignment will be atomic when more than one instruction is required to affect it. However, in multi-threaded environments such considerations may lead to confusion. It is best to think of threads as executing on their own processor, so that two instructions proceed at the same time, with no assumptions about relative speed. Suppose a thread is executing the code fragment below: AnObject o = new AnObject(anExistingObject);
At the same time another thread is executing: anExistingObject.setFields(4, 9.6);
It is likely that the newly constructed object is an invalid copy, because it was taken at a time when the copied object was in an inconsistent state. (If this object represented personal information, there could be ramifications beyond the life of the program.) In general, an object is not considered thread-safe if its methods modify or set any of its instance variables, if it uses class or global instances, and if it invokes any setter methods on object references passed as parameters. There are, however, many ways to do all of these things in a thread-safe manner.
4
Java Thin-Client Programming
1.1.4 Creating and Running a Thread In Java, threads are created by inheriting from the java.lang.Thread class or by implementing the java.lang.runnable interface. Note that a thread is not an object oriented concept. Most of the time, the problem is to design objects so that they can be executed in a multi-threaded environment. In languages such as C and C++, threads are created to run a particular function, and the address of this function is passed to the thread when it is created. In Java, methods may be accessed using the reflection mechanism, but this is not a preferred technique. In keeping with the object oriented model, objects can inherit from the thread class, but this is really only a convenience. The created object is not innately a thread in the same way that an Applet is innately a Component, or an elephant is innately a mammal. What it means is that one of the threads of control in its runtime environment is now executing in its own run() method. If the class defines other public methods, these methods can be (and will be) called by other threads. The essential property of a thread is the execute state. A run() method is therefore required. It is usually easier to encapsulate it within the class whose code it must execute, so that run() can directly access the instance variables. This is not always possible, for example when your class must inherit from another class (since Java does not support multiple inheritance.) In that case it is necessary to implement the runnable interface. Note that it is not sufficient to simply add a run() method to the classes that you create. This is because a thread does not start as soon as it is created, as is the case for many threads packages outside of the Java world. Further to this, the run() method should not be invoked directly. Instead, it is called in response to Thread.start(), so that a Thread object is necessary after all. The Java Virtual Machine ensures that the thread has been initialized before it has a chance to execute. Figure 2 on page 6 contains some code samples that demonstrate how to make this work.
Introduction to Threading Issues
5
public class MyClass extends Applet implements runnable { // class definitions ... public void run() { // run definition ... } } ... // kick start MyClass instance from the over-ridden // Applet.start method public void start() { new Thread(this).start(); } ... } Figure 2. Starting a Thread that Implements java.lang.runnable
This code uses the thread constructor whose single parameter is a runnable interface. Of course, you may want to save the thread object so you can use it later, for example to stop it, or ensure that it is still running.
1.1.5 Why Bother With Threads? There is one primary reason why programmers must be concerned with threads. The Java Virtual Machine provides a multi-threaded application environment where created objects for all threads exist within the same heapspace. Threads are an integral part of Java, supported at the most fundamental level, java.lang.Object. If you plan to make your classes available to third party developers, it is perfectly legitimate for those developers to create instances of your classes in a multi-threaded application. So you must know something about how to design for thread safety. In most cases, this is quite straightforward. However, for server applications, the design considerations may sometimes require complex schemes.
1.2 Basic Synchronization Strategies Synchronization applies to sections of code. A code section is synchronized if no more than one thread has a granted access to it. It is important to synchronize access to critical sections, code segments that place an object into a temporarily inconsistent state or that read an object when it may be in
6
Java Thin-Client Programming
such a state. Java makes synchronization very easy, through the use of the synchronized keyword. It is worthwhile noting that public instance variables can always be accessed by other threads. If they are not declared as final, they can be modified at any time, and thread safety will be automatically lost. As will be seen, thread safety is often a side-effect of sound object oriented design.
1.2.1 Using the "synchronized" Keyword There are two ways in which to employ synchronized. It is most frequently used as a modifier in the definition of methods. For example, refer to Figure 3 on page 7, which contains an excerpt of the java.util.Vector class:
public class Vector extends Object implements Clonable, Serializable { // definitions public final synchronized void addElement(Object obj); public final synchronized Object elementAt(int index); public final synchronized Object removeElementAt(int index); public final boolean size(); // more definitions } Figure 3. Excerpt of Class java.util.Vector
Notice that addElement() and elementAt() are both synchronized methods, but that size is not. The synchronized methods prevent the object being modified while it is being read or read while it is being modified. In fact they prevent any access by another method which is also declared as synchronized. However, the size operation does not constitute a critical section, so it may be called at any time. Of course as soon as the size is obtained it may be incorrect due to another thread having called addElement() or removeElementAt(). Alternatively, it may be correct only because there has been a call to each of those methods in other threads. Blocks of code may also be synchronized on particular objects. This gives a finer granularity of control, and can lead to shorter critical sections.
Introduction to Threading Issues
7
class DoubleHolder() { public Double value; } public class StatClass { private DoubleHolder lastStdDev; // definitions public double getLastStdDev() { synchronized(lastStdDev) { return lastStdDev.value.doubleValue(); } } double calculateStdDev() { double val = longCalculation(); synchronized(lastStdDev) { lastStdDev.value = new Double(val); } return val; } // etc Figure 4. Synchronizing on Particular Objects
Refer to Figure 4 shows a use of synchronizing on a particular object. In the example the object is of a class scoped at the package level so that it is not accessible outside of this package. In this case, synchronizing on an entire method would lead to unnecessary delays, because there is only one instance variable that potentially causes problems. The critical sections are therefore limited to blocks of code that access this instance variable. It is assumed that operation longCalculation() can be safely executed.
1.2.2 Using Delegation It is sometimes convenient to add a layer of protection. Where the methods of an object are not thread-safe, you can write a container class whose methods are thread-safe.
8
Java Thin-Client Programming
public class NotSafe { // definitions public void op(); } public class Safe { private NotSafe unsafe; // definitions public synchronized void op() { unsafe.op(); } } Figure 5. Sample of Delegation
Figure 5 is a simple example of delegation. A method is declared with the same signature in the containing class as in the original class. The only difference is that the method of the containing class is declared as synchronized. When it is called, it just delegates the functionality to the real method. Containment is a standard technique in object oriented programming, and is frequently employed to circumvent the limitations of single inheritance. It can also be used to provide thread-safety in an elegant way.
1.2.3 A Simple Strategy: Don’t Synchronize Not all objects require synchronization. Any object that doesn’t change its state does not require critical sections. Such an object is called an immutable object in object oriented terminology. An object that loads data from a database and performs transformations on that data is an example. Provided its instance variables are private or final, and there are methods other than the constructors that alter them, an object is thread-safe. This does not mean that such objects cannot provide a worthwhile service. An object may contain an invariant transformation matrix and apply transforms based on method parameters. The object would create new matrices, vectors, or scalars, and write the new references on the stack (as return values). These objects do not require synchronization, even if the objects they create do. In general, class methods, that is, methods declared as static, provide services that do not require synchronization, a java.lang.Character such as isWhiteSpace() is a well-known example.
Introduction to Threading Issues
9
Lastly, an object need not be concerned with synchronization if its instance variables are thread-safe. For example, if a class maintains an instance of class java.lang.Vector, there is no need to be concerned about synchronization, because the appropriate methods of Vector are themselves synchronized. It is still necessary to declare such instances as private, so that other objects cannot access them without the owner’s knowledge. Access modifiers are important in this context. Consider the following code fragment in Figure 6:
public class FIFOClass { // definitions private Vector vector; // public void enqueue(ObjType obj) { vector.addElement(obj); } public ObjType dequeue() { if (vector.size() != 0) return (ObjType)vector.removeElementAt(0); else return null; } } } Figure 6. Sample for Access Modifiers
If Vector were declared as public, or even protected or at package scope, other objects could bypass the enqueue() method and call vector.addElement() directly. This would be thread-safe, of course, but may insert a object into your vector of an illegal type. A consequent call to dequeue() would generate a ClassCastException. In addition, other methods of Vector could be called to violate the FIFO ordering. It should therefore be reiterated that thoughtful object oriented design is a step toward the goal of thread-safety.
1.3 Unpicking Threads In section 1.2, “Basic Synchronization Strategies” on page 6, some techniques were discussed for synchronizing threads. These techniques provide for what is known as mutual exclusion of critical sections of code.
10
Java Thin-Client Programming
While these techniques are quite simple, in practice they are insufficient for programming any but the simplest of multi-threaded applications. In order to program servers, the synchronization techniques must be expanded. Before doing that, it is necessary to understand some of the properties of threads and some of the methods of class java.lang.Thread. This section is intended to give you enough of a grounding to understand the techniques outlined in the following sections.
1.3.1 Thread States A thread has five possible stable states. They are: created, dead, runnable, running, and blocked. There is also a kind of half state, interrupted, however an interrupted thread is runnable, and you don’t need to be concerned about being interrupted unless you need to make your classes respond to extremely unusual situations. Consider the following statement: Thread aThread = new Thread(someRunnableObject);
Thread object aThread is now in the created state. Recall that a thread does not start executing (that is, its run() method is not invoked) until it is explicitly started. aThread.start();
Now, aThread is in the runnable state. After this statement, aThread will alternate between runnable and running, as it is time-sliced in and out by the scheduler. Because these transitions cannot be distinguished by the application, the states are often treated as one, runnable. As previously stated, it is probably less confusing to think of every thread as actually running in parallel. If a thread is runnable, assume it is running. Let the underlying threads’ package worry about the tricky aspects. A thread dies when its stop() method is called by itself or some other thread. More normally, a thread dies when its run() method terminates. A daemon thread dies when there are no more non-daemon threads. When the last non-demon thread terminates, so does the application. A demon thread is a thread that exists to serve others; it lives to serve, and must serve to live. If the stop() method is called a new ThreadDeath exception is thrown. This is caught by the Java Virtual Machine to ensure the finally clause executes if the thread was executing a try clause when it was stopped. It would not be expedient to leave an object in an inconsistent state, only because the thread that was executing its code is terminated.
Introduction to Threading Issues
11
Normally a dead thread cannot be resurrected, however there is an issue if the caller of stop catches the ThreadDeath exception. The only reason for doing so would be to take some remedial action that it is not possible for the stopped thread to take. If the caller does catch the exception, execution of any finally clause is delayed until the exception is re-thrown for the Java Virtual Machine. If it is not re-thrown, the thread cannot die, but exists as a sort of zombie, to use a Unix term. The topic of thread termination is actually quite large in its own right, but this document does contain any further treatment. For an overview of the mechanics, consult the documentation at: http://www.javasoft.com/products/jdk1.1/docs/api/java.lang.Thread.html
1.3.1.1 Runnable to Blocked During the lifetime of a thread, it will often become blocked. This simply means it no longer has the potential to run. Assume a thread is waiting on an I/O device to complete some operation, that thread cannot proceed until the operation completes, and the data is transferred to the address space of the program. However, you should regard blocking of such I/O operations as reading a database file or a socket as a technicalities. Although the scheduler cannot run this thread, this is usually irrelevant to the other threads. Java does allow some external control over the situation by providing the java.lang.Thread.interrupt() method. If a thread is interrupted while blocked, it will be awakened with an InterruptedException. Objects that call Thread.sleep() or Object.wait() (see below) are required to catch the exception. Interrupting a thread is usually a bad idea, and should not be done except in extraordinary circumstances. It is possible to design almost every application without resorting to interrupts. A thread will also pass into the blocked state when it calls the static method, Thread.sleep(). However, this is its own concern. No other thread should wake it up; leave that responsibility to the scheduler. Although it may sound appealing to interrupt a sleeping thread, there is usually a better way. Calling interrupt() is like opening a can of worms. The following statements which take a thread from runnable to blocked are of most concern to developers. 1. aThread.suspend(); called from another thread. 2. this.suspend(); called by the thread on itself. The this keyword in not necessary.
12
Java Thin-Client Programming
3. this.wait(); called by a thread while executing a critical section of an object. Again, the this keyword may be omitted. 4. obj.wait(); called by a thread while executing a critical section of object obj from a block defined as synchronized(obj). Object.wait() can only be called in a critical section of code, that is, a section that has been synchronized. If it is called outside of a critical section, an IllegalMonitorStateException will be thrown. See Part 1.3.2, “Synchronized Revisited” on page 14. 1.3.1.2 Blocked to Runnable There are matching methods to move a blocked thread into the runnable state again. They are listed below: 1. aThread.resume(); called by some other thread, since a blocked thread cannot call methods, of course. Thread.resume() is the complement of Thread.suspend(). In order to call resume(), you need a reference to the thread that called suspend(). 2. this.notify(); called by a thread executing a critical section of an object. If some other threads had previously called the object’s wait() method, notify() will move exactly one of those threads into the runnable state. That thread will just return from its wait() call and continue. 3. this.notifyAll(); This is exactly like notify() except that every thread that had previously called this object’s wait() method will become runnable. 4. obj.notify(); called from a thread executing a critical section of another object. It causes one thread to become runnable that had previously blocked on this.wait() while executing the object’s code, or obj.wait() while executing the code of another object. 5. obj.notifyAll(); This causes all threads blocked on obj.wait() to become runnable. Much of this document focuses on techniques based around the methods of java.lang.Object; wait(), notify(), and notifyAll(). These are more suited to general purposes than the Thread methods, especially from an object oriented point of view. Remember that threads are really a pseudo-objects, created to execute the code of real objects. Therefore, more generic designs focus more on objects; particular threads are not so important. Of course, if your object inherits from java.lang.Thread, the thread is the object and it doesn’t matter which set of methods you use, except that using Thread has possible security implications (not covered here).
Introduction to Threading Issues
13
It is important to be consistent. You cannot Thread.resume() a thread that is blocked on Object.wait() or expect to Object.notify() a thread that is blocked on Thread.suspend(). 1.3.1.3 Summary Figure 7 is a simplified view of the transitions for moving from one state to another. Those of you familiar with the famous state transition diagram of Unix processes will notice the similarities.
Figure 7. State-transition Diagram for Threads
1.3.2 Synchronized Revisited The Java threading model is based on the concept of a monitor, a mechanism originally proposed by Tony Hoare. An object implements a monitor if it can move threads from the runnable state to the blocked state, and vice versa. In order to block or resume a thread, it is necessary for a thread to obtain temporary ownership of the object’s monitor. Ownership of a monitor is exclusive by definition.
14
Java Thin-Client Programming
A thread obtains ownership of a class object’s monitor when it executes a static class method declared as synchronized. It obtains ownership of an instance’s monitor when it executes one of its synchronized methods, or a block of code that is synchronized on that object. If wait() or notify() are called without first obtaining the object’s monitor, an IllegalMonitorStateException is thrown. At a lower level, the Java Virtual Machine obtains a lock on the object and prevents other threads from executing the object’s code. It does not bother to check the lock if the executing code is not a critical section. Exactly how the lock is applied depends on the underlying operating system primitives, which must be utilized by the Java Virtual Machine. This is of no concern to the Java programmer; the behavior is the same on all platforms. As a model, the use of monitors is somewhat restrictive. However, it contains everything necessary to build powerful synchronization protocols between threads.
1.3.3 Priority Threads have a priority in Java. The basic idea is that threads of higher priority have precedence over threads of lower priority. If two threads are in the runnable state, the higher priority thread is selected by the scheduler to be run. Priority is one area where platform independence is not fully realized. The Java Virtual Machine uses the operating system threads packages, but not all supported platforms use prioritized threads. With the green threads package used by Java 1.1 on the Solaris operating system, a higher priority runs until it blocks. If there are lower priority threads in the runnable state, they will never get a change to run, in other words, they will experience starvation. However, this package will be soon be replaced by Solaris native threads, and genuine time-slicing will be implemented. Thread priority can lead to some unwanted side effects. In addition to certain threads being starved, there may be far more context-switching, that is, swapping threads in and out of the running state. This leads to reduced throughput, because too much work is being performed by the OS kernel. There is also the problem of priority inversion, where a low priority thread starves one of a high priority by holding a monitor it requires. Most of the time, using different thread priorities creates more problems than it solves. It is usually possible to design a prioritized system without actually changing thread priority. When a thread is created, it has a default priority,
Introduction to Threading Issues
15
and it is probably better to leave it as it is, so that all threads execute at the same priority.
1.3.4 Thread Groups Every thread executes as part of a thread group, defined in java.lang.ThreadGroup. Thread groups allow certain operations to be applied to every thread in the group, which is sometimes convenient, especially if you need to suspend and resume all your threads at the same time. Thread groups may contain other thread groups, allowing a tree of threads to be built up. Thread groups make it easy to keep tabs on the status of your threads, and are mostly used for administrative purposes. For more information consult the documentation at: http://www.javasoft.com/products/jdk1.1/docs/api/java.lang.ThreadGroup.html
1.4 Extending the Threading Model When designing servlets, or other kinds of servers, the monitor model can be limiting. The advantage of using monitors is that things are kept simple, but this simplicity is achieved at a cost in flexibility. Fortunately, more powerful models can be built from the basic Java constructs. This section builds up a package for use in multi-threaded servers, such as HTTP servlets. To use this package, the programmer must think in terms of a lower level threading model, one that is common in other multi-threaded environments, such as posix threads, and Windows NT. Although the package is not comprehensive, it can easily be enhanced once the basic premises are understood.
1.4.1 Mutexes A mutex is simply a object that provides for mutual exclusion. It is sometimes called a lock . The Java Virtual Machine typically uses a mutex to implement an object monitor. As we have already seen, mutual exclusion is achieved by using the synchronized keyword. Mutexes are therefore not required in Java programs. However, mutexes can be useful as objects in their own right, and can be used to build more general-purpose synchronization objects.
16
Java Thin-Client Programming
package com.ibm.austin.itsc.jalapeno.locks; import java.lang.Thread; public final class Mutex { private int cnt; public Mutex() { cnt = 0; } public synchronized void lock() { if (cnt++ != 0) { try { wait(); } catch (final InterruptedException ie) { } } } public synchronized void unlock() throws AlreadyUnlocked { if (cnt == 0) { throw new AlreadyUnlocked(); } if (--cnt != 0) { notify(); // wake up exactly one waiting thread } } } Figure 8. Implementation of a Mutex
Figure 8 contains the code for implementing a mutex. There are two methods only, lock() and unlock(). They have very little work to do, so they execute very quickly. A mutex object allows critical sections to be implemented without using the synchronized keyword. It is necessary to bracket critical sections with a lock() and unlock() pair. Note that this will ensure that every call to wait() will be matched by a call to notify(). When a mutex is constructed, its single member, a counter, is initialized to 0. This represents the number of waiting threads. On the first call to lock() the counter is incremented, but there is no need to block because the resource that this mutex is protecting is available. If it were not, the counter would be
Introduction to Threading Issues
17
non-zero. In the case that no other thread had called lock before the original thread called unlock(), there is no reason to call notify(), because there are no threads waiting on the mutex’s monitor. However, if another thread calls lock() and finds the counter is non-zero, it immediately blocks on wait(). When the controlling thread calls unlock(), notify() is invoked, and the second thread becomes runnable. Note that when many threads are blocked on the mutex, it is not important which is selected to become runnable. 1.4.1.1 Deadlock and the Mutex For simple mutual exclusion, mutexes are a poor alternative to using the normal synchronizing techniques, and are more subject to programming errors. Consider the following code fragment: // obtain Mutex mux from somewhere mux.lock(); if (protectedObj.op == value) // do something else // do something else mux.unlock();
This is correct, because the calls to lock() and unlock() are balanced. However, consider the following similar fragment: // obtain Mutex mux from somewhere mux.lock(); if (protectedObj.op == value) // do something else { // do something else return; } mux.unlock();
In this example, a return statement has by-passed the call to unlock(). Now all threads that call lock() will block immediately, without any chance of becoming runnable. This includes the thread that originally locked the mutex. Such a situation is known as deadlock . A synchronized statement would prevent this, because the Java Virtual Machine would make the monitor available upon exiting that statement’s scope. Note also that if a thread calls lock() twice, but without an intervening call to unlock, it will block, in effect on itself. Again, deadlock will result. However, a synchronized statement nested within another statement synchronized on the same object causes no problems. This is because the Java Virtual Machine
18
Java Thin-Client Programming
checks the identity of the thread that holds the monitor before it applies a lock. This is useful because it allows you to call a synchronized method from within another synchronized method. However, using synchronized is subject to errors when other objects come into play. Suppose one thread is executing the following code. synchronized (anObj) { // do things synchronized (anotherObj) { // do things } }
At the same time another thread is executing the following. synchronized (anotherObj) { // do things synchronized (anObj) { // do things } }
This is a genuine deadlock situation, where thread T1 holds resource R1 and needs resource R2, while thread T2 holds resource R2 and needs resource R1. Neither can proceed. The Java Virtual Machine does nothing to resolve these situations. Very few environments do, since deadlock detection is costly and complicated, with no universally applicable resolution strategy. It is simply up to the programmer to ensure deadlock does not occur, that is, to use deadlock avoidance steatites, some of which are covered in this document. For simple applications it is sufficient to ensure that all threads acquire resources in a predetermined order. This does not mean that if you need the first and third resource you must also acquire the second; you just need to obtain their monitors in order, first then third. 1.4.1.2 Extending the Mutex Although there is no obvious advantage in using the mutex as it stands, it can easily be extended to provide one. Suppose the following definition were to be added to the class. public synchronized boolean testlock() { if (cnt != 0) { return false; } else { lock();
Introduction to Threading Issues
19
return true; } }
This method returns false if the mutex is locked, but if not, it locks the mutex and returns true. Such conditional locking is useful when a server has a number of options to pursue before blocking. It is not possible using the usual synchronizing techniques. Below is an example of how to use it. if (mux.testlock()) { // manipulate resource mux.unlock(); } else // do something else }
1.4.2 Condition Variables Condition variables allow cooperating threads to signal each other that a condition has become true. Suppose a thread finds that it cannot proceed because the object it is executing is in an invalid state for the current operation, the thread can block until the condition becomes true. It is unblocked by another thread (performing a different operation) when the object state is such that the first thread can proceed. A classical example of this is the producer-consumer situation, covered in practically all texts on threading. In this situation, there is a common object which is continuously read and updated, and there are separate threads to read and update that object. Whenever the object changes, an operation must be applied exactly once. Many threads may be trying to perform the same operation, or a single thread may be trying to perform the operation continuously. Conversely, a thread may be trying to alter the object state before any operations have been performed. These threads must be coordinated. Requests are often placed into a common buffer, and it is an error to serve a request more than once or to erase a request before it was served. If the server is not properly designed, it could fall into a pattern of serving certain requests multiple times and/or ignoring other requests. These are examples of what are known as race conditions. 1.4.2.1 Producer-Consumer In the documentation at: http://java.sun.com/docs/books/tutorial/essential/threads/waitAndNotify.html
20
Java Thin-Client Programming
there is solution to a basic producer-consumer scenario. The code has reproduced in Figure 9:
public synchronized int get() { while (available == false) { try { // wait for Producer to put value wait(); } catch (InterruptedException e) { } available = false; // notify Producer that value has been retrieved notifyAll(); return contents; } public synchronized void put(int value) { while (available == true) { try { // wait for Consumer to get value wait(); } catch (InterruptedException e) { } } contents = value; available = true; // notify Consumer that value has been set notifyAll(); } Figure 9. Consumer-Producer Solution Without Condition Variables
Here, the algorithm works as follows: Accessor and modifier methods are synchronized, for mutual exclusion. The object has a flag to indicate whether it is available to be read. When a producer (a thread executing put()) finds the available flag unset, it overwrites the contents member with the new data and sets the flag. It then notifies all waiting threads. If, on the other hand, the available flag is set, the data has not been read, and the producer must wait until the situation changes. Consumers do the converse. They wait when the flag is unset, and read contents when the flag is set. They then unset the flag and notify all other threads waiting on this object.
Introduction to Threading Issues
21
Why is there a need for iterative testing of the condition, rather than a selection? Suppose there are two consumers, and one producer. The first consumer enters the get() method and calls wait(). Before the producer calls put(), the other consumer calls get(). Remember that the object is available because the first consumer is blocked. The second consumer is then blocked, as well. Both consumers will unblock when a producer calls notifyAll(), but one will immediately block again, because the method is synchronized, and only one thread at a time can obtain execution rights. When the second consumer is able to proceed, it cannot read the contents field without first checking the available flag, even though it has checked it previously. The flag is now unset, and contents must not be read with the object in this state. Failure to meet that condition results in a race condition. A simple selection would probably suffice if it were guaranteed that only one producer and one consumer thread existed. Even then, however, there may be problems, due the fact that notifyAll() and wait() are native methods. Some operating systems generate what is known as spurious wakeups, where a waiting thread is unblocked for no reason. This is rare, but defensive programming principles mandate that it should be protected against. When working with condition variables, it is important to test conditions in an iteration. See Part 1.4.2.3, “Improving the Producer-Consumer Algorithm” on page 25. Note the strategy for reading the object state: The object state is accessed by the caller as a return value, that is, a copy is made on the stack, and therefore is only available in the calling thread. Reading an object state is usually a fast operation. It would not be reasonable to expect clients to wait until the server cleared a backlog of preceding requests on a protected object, especially if the operations are long-running. In general, the best strategy is to take a thread-safe copy and work with that. 1.4.2.2 Condition Variable Class Although the algorithm in Figure 9 on page 21 is functionally correct, it is far from optimal. When working purely with the monitor model, this is fine but, if you want to write a fast server, there are better methods. The most serious drawback of the algorithm is that notifyAll() will unblock any thread waiting on the object. If a consumer makes the call, all other consumers will unblock and will immediately block again. Suppose there are a dozen consumers and only one producer, not necessarily a rare situation. Every consumer may run for no reason before
22
Java Thin-Client Programming
the producer gets an chance to update the object. This is a very inefficient use of threads, likely to cause very poor performance. It is better to unblock only those threads that have a reasonable chance to proceed. In this case we want to notify only the producers. However, the Java Virtual Machine does not discriminate between threads waiting on the same object’s monitor. A solution may be to have producers and consumers wait on different monitors, but there is then the problem of protecting the common resource. Condition variables address exactly this issue. Figure 10 on page 24 shows the implementation of a condition variable. Note that condition variables require a mutex as a member, and it is that mutex that is used to implement critical sections. This allows any object to be accessed in a critical section without the need to synchronize on the object itself. Using condition variables allows for great flexibility and control, at the cost of greater complexity for the application developer. In addition to the mutex, there is a count of waiting threads, and an object that is used to call wait() and notify(). Since every instance of a condition variable has its own synchronization object, you can now synchronize on conditions themselves, rather than on the objects where the conditions occur. There is only one constructor, taking a mutex as a parameter. A reference to this mutex is stored as its private member, and the wait count is set to zero. It is also necessary to create the synchronization object, sync; which exists solely for calls to wait() and notify(). Method waitcond() take the place of Object.wait(). When this method is called, the mutex must have been previously locked, or an exception is raised. waitcond() implements an unconditional wait; it is assumed that the condition was verified by the caller. See, for an example of how this works. The essential quality of this call is that it must be invoked with the mutex locked and when it returns the mutex will still be locked, that is, it will be safe to access the protected resource(s). Any thread calling waitcond() will be blocked. Prior to blocking, it will unlock the mutex that protects the critical resource, giving other threads an opportunity to set the condition that this thread requires to proceed.
Introduction to Threading Issues
23
package com.ibm.austin.itsc.jalapeno.locks; import java.lang.Thread; public final class CondVar { private Mutex mux; private Object sync; private int cnt; public CondVar(Mutex mux) { this.mux = mux; sync = new Object(); cnt = 0; } public void broadcast() { synchronized(sync) { if (cnt != 0) { sync.notifyAll(); } } } public synchronized void signal() { synchronized(sync) { if (cnt != 0) { sync.notify(); } } } public void waitcond() throws AlreadyUnlocked { synchronized(sync) { mux.unlock(); try { cnt++; sync.wait(); cnt--; } catch (final InterruptedException ie) { } } mux.lock(); } } Figure 10. Class CondVar
24
Java Thin-Client Programming
When another thread has set a condition, it calls either signal(), corresponding to notify(), or broadcast(), corresponding to notifyAll(). Which is used depends on what is appropriate for the application. Bear in mind that it is inefficient to call broadcast() unless every waiting thread has a reasonable chance to proceed. On the other hand, calling signal() may lead to deadlock in cases where a waiting thread can block for other reasons, or nested conditions apply. You must think carefully about the design of the application. Note that although this type of object is called a condition variable, there is actually no condition associated with its internal state. Such an association is made externally. In this respect, the use of conditional variables is not object-oriented. 1.4.2.3 Improving the Producer-Consumer Algorithm We are now in a position to make the producer-consumer algorithm more efficient. Below is the code of a test application to give the basic feel of using condition variables. import java.lang.*; import com.ibm.austin.itsc.jalapeno.locks.*;
public class Test1 { public static void main(String argv[]) { Unsafe uns = new Unsafe(); Mutex mux = new Mutex(); CondVar okRead = new CondVar(mux); CondVar okWrite = new CondVar(mux); Producer p1 Consumer c1 Consumer c2 Consumer c3 c1.start(); c2.start(); c3.start(); p1.start();
= = = =
new new new new
Producer(uns, Consumer(uns, Consumer(uns, Consumer(uns,
okWrite, okRead, okRead, okWrite, okRead, okWrite, okRead, okWrite,
mux, mux, mux, mux,
"p1"); 1); 2); 3);
} } class Unsafe { private String[] strs; private final int MAX = 8; private int idx, cnt, slot;
Introduction to Threading Issues
25
public Unsafe() { idx = cnt = slot = 0; strs = new String[MAX]; } public String getStr() { String ret = new String(strs[idx]); idx = (idx + 1) % MAX; cnt--; return ret; } public void putStr(String s) { strs[slot] = new String(s); slot = (slot + 1) % MAX; cnt++; } public boolean isEmpty() { return cnt == 0; } public boolean isFull() { return cnt == MAX; } } class Consumer extends Thread { private Unsafe uns; private Mutex mux; private CondVar proceed, informDone; private long num; public Consumer(Unsafe uns, CondVar proceed, CondVar informDone, Mutex mux, long num) { this.uns = uns; this.mux = mux; this.proceed = proceed; this.informDone = informDone; this.num = num; } public void run() { while (true) { mux.lock(); while(uns.isEmpty())
26
Java Thin-Client Programming
try {proceed.waitcond();} catch (AlreadyUnlocked au) {System.err.println("cond");} String str = uns.getStr(); try { mux.unlock(); } catch (AlreadyUnlocked au) {System.err.println("mux" + num);} informDone.broadcast(); System.out.println("Consumer " + num + " got: " + str); } } } class Producer extends Thread { private Unsafe uns; private Mutex mux; private CondVar proceed, informDone; private String name; private long num; public Producer(Unsafe uns, CondVar proceed, CondVar informDone, Mutex mux, String name) { this.uns = uns; this.mux = mux; this.proceed = proceed; this.informDone = informDone = informDone; this.name = name; num = 0; } public void run() { while (true) { mux.lock(); while(uns.isFull()) try {proceed.waitcond();} catch (AlreadyUnlocked au) {System.err.println("cond");} uns.putStr(name + ’:’ + ++num); try { mux.unlock(); } catch (AlreadyUnlocked au) {System.err.println("err");} informDone.broadcast(); } } }
In this program, class Unsafe implements a circular buffer, into which Strings are deposited and emptied in order, by a producer and three consumers. To show the basic principles, and because this document does not cover techniques concerning finalization of applications, this application will run until it is terminated externally.
Introduction to Threading Issues
27
Figure 11 contains a sample of the output. Notice that the consumer threads generally obtain more than one message before the next consumer has a chance to run, demonstrating the scheduling that underlies the Java threads package.
Consumer Consumer Consumer Consumer Consumer Consumer Consumer Consumer Consumer Consumer Consumer
3 1 2 1 1 1 3 3 2 2 2
got: got: got: got: got: got: got: got: got: got: got:
p1:48 p1:49 p1:50 p1:51 p1:52 p1:53 p1:54 p1:55 p1:56 p1:57 p1:58
Figure 11. Output Sample of the Producer-Consumer Test
Two condition variables are used to achieve the result. They are both constructed with the same mutex. When the producer has written a string into the buffer, all the readers are signalled. They may or may not be blocked; all consumers are runnable until the buffer becomes empty. The producer is not necessarily blocked, and can go on entering strings into the buffer until the buffer becomes full. As soon as it is full the producer will block. When a consumer is unblocked it reads a string and signals the producer that the buffer is available for another string. Again, the consumer is not necessarily blocked. We no longer unblock just any thread with an interest in the buffer, only those threads which are able to do work. Note that this may still not be the most effective implementation. It is possible for threads to be preempted when it would be better if they were allowed to execute for longer, or vice-versa. For example, why signal a producer if only one slot is available. It may be better to wait until the thread could work uninterrupted for longer periods. The topic of designing conditions for effective cooperation can be somewhat complex, and is not covered in this document.
28
Java Thin-Client Programming
// cooperating thread type one - alters a condition for type two mux.lock(); while(obj.condition) conditionCondVar.waitcond(); obj.doCorrespondingChangingOperation(); mux.unlock(); correspondingCondVar.broadcast(); // cooperating thread type two - alters a condition for type one mux.lock(); while(obj.corresponding) correspondingCondVar.waitcond(); obj.doConditionChangingOperation(); mux.unlock(); conditionCondVar.broadcast(); Figure 12. General Outline for Use of Condition Variables
Figure 12 contains a general outline of the code for threads that cooperate using condition variables. A common mutex is used to protect the resource, and for each of the conditions that apply to that resource, there is a separate condition variable, constructed with the common mutex. Prior to testing a condition, the resource must be protected by locking the mutex. The condition is then tested, and if it is safe to proceed, the operation(s) can be applied. These operations alter the state of the object so that other threads may apply corresponding operations. After the operations are complete, the mutex is unlocked, and any thread(s) that may be waiting on a corresponding condition is (are) signalled. If the condition does not hold, the thread calls waitcond() without unlocking the mutex. Since we know that our condition is not satisfied, the thread blocks with the mutex unlocked. Other threads can now obtain access to the object and perform operations that alter the object state to the extent that the desired condition now applies. At that point, the original thread will return from waitcond() (in response to a broadcast() or signal()) with the mutex locked, and its operations can be performed. Although the example shows a parity between two conditions, the model can be enhanced to allow communication between threads coordinating three or more conditions. Additionally, there are three points to note: 1. Testing of the condition is iteratively performed for much the same reasons as outlined in Part 1.4.2.1, “Producer-Consumer” on page 20. If the corresponding threads call broadcast(), many threads may be unblocked.
Introduction to Threading Issues
29
Only one of those will obtain access to the object. It may perform an operation leading to the reversal of the condition. So it cannot be assumed that the condition applies because it has been tested once. If signal() were to be used instead of broadcast() (and the calling thread can be relied on), there is still the threat in some systems of spurious wakeups. For general purposes, it is safer to always test conditions in a loop. 2. The mutex is unlocked prior to signalling another thread. When a thread is unblocked, the first thing it tries to do is lock the mutex. It will immediately block again if the mutex is still locked. Suppose that a thread signals another thread before unlocking the mutex. That thread will awake and block, wasting processor time. Now, if we set aside for a moment the ideal view that there is true parallelism and consider the possibilities in a time-sliced system, the situation is worse. Thread one signals thread two and a context switch occurs (due to thread one using up its time-slice) so that thread two starts running. Thread two blocks and a context switch occurs back to thread one. Eventually, thread one will use up its time-slice, triggering a context switch to thread two, which can now proceed. There have been three context switches where only one was required. 3. Each situation is different, so you can improvise on the basic example. Fine tuning can often lead to dramatic performance benefits. For example, if a thread knows in advance that it can do no meaningful work until other threads have an opportunity to run, it can call Thread.yield(), to voluntarily give up control of the processor. If it is known that only one thread can proceed after some operation, use signal() in preference to broadcast(). Unblocking every thread waiting on a condition may lead to thread avalanche, and result in all but one blocking again. Remember that using condition variables gives you a very fine granularity of control, allowing your system to be designed so that critical sections can be kept very short and threads can be unblocked in precisely the right circumstances.
1.4.3 Read-Write Mutexes Condition variables are used in the construction of read-write mutexes. A read-write mutex provides object safety with the more efficiency. They can vary in form, but the basic idea is that multiple read accesses are allowed in parallel, but modifications are serialized, and read access is prevented during modification. This section contains the implementation for a read-write mutex where the writer is given priority.
30
Java Thin-Client Programming
package com.ibm.austin.itsc.jalapeno.locks; public final class RWMutex { private Mutex mux; private CondVar read, write; boolean activeWriter; int nReaders, nWriters; public RWMutex() { mux = new Mutex(); read = new CondVar(mux); write = new CondVar(mux); nReaders = nWriters = 0; activeWriter = false; } public void rlock() { try { mux.lock(); while (nWriters != 0) { read.waitcond(); } nReaders++; mux.unlock(); } catch (final AlreadyUnlocked au) { } } public void wlock() { try { mux.lock(); nWriters++; while (nReaders != 0 || activeWriter) { write.waitcond(); } activeWriter = true; mux.unlock(); } catch (final AlreadyUnlocked au) { } } // incomplete Figure 13. Constructor and Lock Methods of Class RWMutex
Introduction to Threading Issues
31
Figure 13 on page 31 contains the definitions for the instance variables, constructor, and lock methods of class RWMutex. There are two ways to lock an object; readers use rlock() and writers use wlock(). The instance variables include a mutex and two condition variables, corresponding to the conditions that is safe to read or safe to write. There is also a count of the currently active readers, and the current writer plus the currently waiting writers. If the object is locked for writing the boolean activeWriter will be set to true. Note that mutex mux does not exist to protect the external object, but to protect the instance variables of the RWMutex. An instance of RWMutex will itself be used to guard the external object. In the constructor, the instance variables are initialized to reflect the fact that no access of any kind is in progress. When rlock() is invoked it checks whether there is a writer executing or any writers are waiting. This is the read condition. Only a single test is needed, because the count of writers includes the active writer, if one exists, as well as any that are waiting. If the count is non-zero, the calling thread blocks on the read condition. There are two points of interest. 1. Priority is given to writers, since readers are suspended when any thread wants to write, despite the possibility that it may be safe to read. This is an example of implementing a priority scheme without resorting to the vagaries or system dependence of prioritized threads. 2. It is not necessary to count waiting readers. It is sufficient to leave that task to the Java Virtual Machine, which in turn may leave it to the operating system. When the read condition is satisfied, the writing thread will broadcast all waiting readers. Method wlock() is similar, except that the count of writers is immediately incremented. This thread will eventually become the active writer although it may have to wait for the privilege. The method must check that there are no readers currently holding a lock, and that no writer is in the process of modifying the object. This is the write condition. Failure to verify that circumstance would result in either a read-write race condition or a write-write race condition. When the condition does not hold it blocks. Note that the count of writers is unconditionally incremented in order to implement the priority scheme. When any thread is blocked in wlock(), any thread that consequently calls rlock() will also be blocked.
32
Java Thin-Client Programming
// continuation of class RWMutex public void unlock() { mux.lock(); if (activeWriter) { activeWriter = false; --nWriters; } else { --nReaders; } boolean readOk = nWriters == 0; boolean writeOk = !readOk && nReaders == 0; try { mux.unlock(); } catch (final AlreadyUnlocked au) { } if (writeOk) { write.signal(); } else if (readOk) { read.broadcast(); } // else do nothing - last active reader // will eventually signal a writer } } // class definition complete Figure 14. Method Unlock of Class RWMutex
Figure 14 contains the implementation for RWMutex.unlock(). In order to call unlock(), the thread must hold either a read lock or a write lock, that is, rlock() or wlock() must have been previously called and completed. To determine whether a writer or reader is releasing a lock, it is sufficient to check the activeWriter boolean. If a writer is releasing the lock, activeWriter is set to false and the writer count is decremented. If a reader is releasing the lock, the count of active readers is decremented. Now it is necessary to determine which threads can proceed. If there are no waiting writers, it is permissible to read, and the all threads waiting on the read condition are notified through a broadcast(). If there are waiting writers, but no active readers, it is permissible to write, and exactly one writer is signalled. It is never permissible to write when it is permissible to read, these circumstances are mutually exclusive. However, their disjunction is not a tautology, because there is another possibility: when a reader releases the
Introduction to Threading Issues
33
lock at a moment when there are writers waiting, or other active readers. The intention is to give priority to writers, so all potential readers must remain blocked until the writers have completed. Neither can a writer be signalled, because there are currently executing readers. Fortunately, this situation can be resolved by taking no action. Active readers will progressively release the lock, until the count of readers falls to zero. At that time the condition exists such that it is permissible to write. A writer is signalled when the last active reader relinquishes the lock. It is important to note that such a read-write mutex is appropriate only when there are comparatively fewer writers than readers. In the worst situation, if write requests occur more frequently than they can be satisfied, readers will be prevented from executing at all. It is possible to modify the class to compensate for prolonged periods of write activity or to design a read-write mutex that implements a different priority scheme. Often, however, a read-write mutex of this type provides exactly the kind of synchronization that servers need.
1.5 An Example Servlet An example of a multi-threaded environment is in the Servlet technology of JavaSoft. Servlets were designed as a replacement for CGI or Common Gateway Interface scripts. A HTTP servlet is called upon by a HTTP server to handle requests that cannot be satisfied from a static HTML document. The documentation available for download from: http://java.sun.com/products/java-server/servlets/index.html includes the following statements: Servlets are modules that run inside request/response-oriented servers, such as Java-enabled Web servers, and extend them in some manner. For example, a servlet might be responsible for taking data in an HTML order-entry form and applying the business logic used to update a company’s order database. Servlets are to servers what applets are to browsers. The Servlet API, which you use to write servlets, assumes nothing about how a servlet is loaded, the server environment in which the servlet runs, or the protocol used to transmit data to and from the user. This allows servlets to be embedded in many different Web servers. Servlets are an effective substitute for CGI scripts: they provide a way to generate dynamic documents that are both easier to write and faster to run. They also address the problem of doing server-side programming
34
Java Thin-Client Programming
with platform-specific APIs. Servlets are developed with the Java Servlet API, a standard Java extension. While it is not part of the core Java framework, which must always be part of all products bearing the Java brand, it will be made available with such products by their vendors as an add-on package. It is already supported by many popular Web servers. Example Uses A few of the many applications for servlets include, Processing data POSTed over HTTPS using an HTML form, including purchase order or credit card data. A servlet like this could be part of an order-entry and processing system, working with product and inventory databases, and perhaps an on-line payment system. Allowing collaboration between people. A servlet can handle multiple requests concurrently; they can synchronize requests to support systems such as on-line conferencing. Forwarding requests. Servlets can forward requests to other servers and servlets. This allows them to be used to balance load among several servers that mirror the same content. It also allows them to be used to partition a single logical service over several servers, according to task type or organizational boundaries. Being a community of active agents. A servlet writer could define active agents that share work among each other. Each agent would be a servlet, and the agents could pass data among themselves. You write a servlet by extending class javax.servlet.http.HttpServlet. Unless you also implement the SingleThreadModel interface, the methods of your class will be called from multiple threads. Each thread corresponds to an HTTP request. It is your responsibility, therefore, to design the servlet to be thread-safe. This section contains an example of a very simple servlet. Two versions of the servlet are examined, each using different methods of synchronization. Note that the section does not contain a tutorial for the servlet API. The reader should consult the JavaSoft documentation for additional details.
1.5.1 The Traffic Report Servlet It is the task of this servlet to report on the current traffic situation in any particular city. Its input comes from a HTML pages containing a form that uses the HTTP post method. Fields of the form include the street name and vicinity for the desired report. It is possible to update the information if the
Introduction to Threading Issues
35
correct password is supplied. This is a trivial example of a server in which the frequency of reading far surpasses the frequency of modification.
Input Form Traffic Information Request
Figure 15. HTML Source for the Input Form for the Traffic Servlet
Figure 15 contains the HTML for the input form. The input form as it is displayed in the browser window can be seen in Figure 16 on page 37.
36
Java Thin-Client Programming
Figure 16. Screen Shot of the HTML Form
1.5.2 First Version In the first version of the traffic report servlet, the vicinity field of the form is ignored. When a user posts a request, the data is sent using HTTP to a servlet object, whose methods are invoked from separate threads. Below is the complete code to implement this: import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*;
public class TrafficServlet extends HttpServlet { private Hashtable trafficReports; private String password; public void init(ServletConfig config) throws ServletException { super.init(config); trafficReports = new Hashtable(); password = getInitParameter("password"); if (password == null) { password = new String("traffic_admin");
Introduction to Threading Issues
37
} }
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); PrintWriter textOut = response.getWriter(); String street = request.getParameter("street"); if (street == null || street.length() == 0) { reportError(textOut, "Require a street field"); return; } String opt = request.getParameter("opt"); if (opt == null || opt.equals("read")) { String desc = (String)trafficReports.get(street); if (desc == null) { reportError(textOut, street + " : not found"); return; } report(textOut, street, desc); } else { // traffic update String pwd = request.getParameter("password"); if (pwd == null || !pwd.equals(password)) { reportError(textOut, "Invalid password - access denied"); return; } String desc = request.getParameter("desc"); if (desc == null || desc.length() == 0) { reportError(textOut, "No description to update"); return; } trafficReports.put(street, desc); report(textOut, street, "description updated"); } }
protected void reportError(PrintWriter response, String err) { response.println(""); response.println("
Error!"); response.println(err); response.println(""); response.close(); }
38
Java Thin-Client Programming
protected void report(PrintWriter response, String street, String msg) { response.println(""); response.println("
Traffic Information"); response.println("
Traffic Information
"); response.print("
" + street + ": "); response.println(msg); response.println(""); response.close(); } }
It is important to note that the doPost() method is not synchronized. Because the HttpServletRequest and HttpServletResponse objects are parameters they are not available in any other thread. However, all of the servlets instance variables must be protected against concurrent access. There are only two of these, one of which is a password to check that modification requests come from a trusted source. (Those of you familiar with CGI will be aware the password field provides almost no security.) The instance is set during initialization, when concurrent access is not an issue, because the environment guarantees that the init() method will only be called once. Thereafter, this instance is immutable, and therefore safe to be accessed by multiple threads at the same time. Of primary concern is the member containing the information to be reported, trafficReports. It is an instance of class java.util.Hashtable. Unless you are familiar with that class, it is not apparent that the instance is being protected. The answer is simply that the get() and put() methods are synchronized. The return value of Hashtable.get() is a reference to the object it contains and such a reference may be simultaneously accessed by multiple threads. Since no modifications are performed on the object it represents, the code is thread-safe. It is also possible for the reference to be overwritten in the hash table. This presents no problem since the string associated with any particular key is never re-used. Instead, a new reference is simply inserted into the hash table. That reference is obtained from the request parameter of the doPost() method through a call to getParameter().
1.5.3 Second Version In the first version of the program, full functionality is not provided, as the vicinity field of the HTML form is ignored. The idea of this servlet is that the user can specify both a street and a vicinity, in which case a single traffic
Introduction to Threading Issues
39
report is returned in a HTML document. It is also desired that more than one report can be obtained. When the user does not supply the street field, all reports that match the requested vicinity are returned, including all associated streets. Similarly, when only the street is supplied, reports are returned for all associated vicinities. Sample output as it appears in a Web browser is reproduced in Figure 17.
Figure 17. Sample Result of a Traffic Servlet Request
This servlet is a good candidate for read-write mutexes. Whereas we could anticipate high usage around the rush hour, actual updates will be relatively infrequent. The complete servlet code is reproduced below. import java.io.*; import java.util.*; import java.text.*; import javax.servlet.*; import javax.servlet.http.*; import com.ibm.austin.itsc.jalapeno.locks.*; class TrafficReport { TrafficReport next; String street;
40
Java Thin-Client Programming
String vicinity; String desc; Date time; TrafficReport() { street = null; vicinity = null; time = null; desc = null; next = null; } TrafficReport(String street, String vicinity, String desc, Date time) { this.street = street; this.vicinity = vicinity; this.desc = desc; this.time = time; next = null; } TrafficReport(TrafficReport other) { this.street = other.street; this.vicinity = other.vicinity; this.desc = other.desc; this.time = other.time; next = null; } boolean matches(TrafficReport other) { boolean streetMatch = false; boolean vicinityMatch = false; if (street != null && other.street != null && street.length() != 0 && other.street.length() != 0) { if (!(streetMatch = street.equals(other.street))) { return false; } } if (vicinity != null && other.vicinity != null && vicinity.length() != 0 && other.vicinity.length() != 0) { if (!(vicinityMatch = vicinity.equals(other.vicinity))) { return false; } } return streetMatch || vicinityMatch; } }
Introduction to Threading Issues
41
public class TrafficServlet extends HttpServlet { private String password; private TrafficReport map; private RWMutex mux; public void init(ServletConfig config) throws ServletException { super.init(config); map = null; mux = new RWMutex(); password = getInitParameter("password"); if (password == null) { password = new String("traffic_admin"); } }
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); PrintWriter textOut = response.getWriter(); TrafficReport rpt = new TrafficReport(); rpt.vicinity = request.getParameter("vicinity"); rpt.street = request.getParameter("street"); String opt = request.getParameter("opt"); if (opt == null || opt.equals("read")) { if ((rpt.street == null || rpt.street.length() == 0) && (rpt.vicinity == null || rpt.vicinity.length() == 0)) { reportError(textOut, "Require street and/or vicinity field"); return; } mux.rlock(); rpt = getRpt(rpt); mux.unlock(); if (rpt == null) { reportError(textOut, "No matching record could be found"); } else { report(textOut, rpt); } } else { // traffic update if (rpt.street == null || rpt.street.length() == 0 || rpt.vicinity == null || rpt.vicinity.length() == 0) { reportError(textOut, "Require both street and vicinity fields for update");
42
Java Thin-Client Programming
return; } String pwd = request.getParameter("password"); if (pwd == null || !pwd.equals(password)) { reportError(textOut, "Invalid password - access denied"); return; } rpt.desc = request.getParameter("desc"); if (rpt.desc == null || rpt.desc.length() == 0) { reportError(textOut, "No description to update"); return; } rpt.time = new Date(); mux.wlock(); update(rpt); mux.unlock(); msg(textOut, rpt, "description updated"); } }
protected TrafficReport getRpt(TrafficReport rpt) { TrafficReport cursor = map; TrafficReport cur = null, ret = null; while (cursor != null) { if (rpt.matches(cursor)) { if (ret == null) { ret = new TrafficReport(cursor); cur = ret; } else { cur.next = new TrafficReport(cursor); cur = cur.next; } } cursor = cursor.next; } return ret; } protected void update(TrafficReport rpt) { TrafficReport cursor = map; if (map == null) { map = rpt; return; } TrafficReport prev = null; while (cursor != null && !rpt.matches(cursor)) {
Introduction to Threading Issues
43
prev = cursor; cursor = cursor.next; } if (cursor != null) { rpt.next = cursor.next; } prev.next = rpt; } protected void reportError(PrintWriter response, String err) { response.println(""); response.println("
Error!"); response.println(err); response.println(""); response.close(); } protected void report(PrintWriter response, TrafficReport rpt) { DateFormat fmt = new SimpleDateFormat(); response.println(""); response.println("
Traffic Information"); response.println("
Traffic Information
"); while (rpt != null) { response.print("
"); if (rpt.street != null && rpt.street.length() != 0) response.print(rpt.street); response.print(" in vicinity "); if (rpt.vicinity != null && rpt.vicinity.length() != 0) response.print(rpt.vicinity); response.print(" as at " + fmt.format(rpt.time)); response.println(":
"); response.println(rpt.desc); response.println("
"); rpt = rpt.next; } response.println("
"); response.println(""); response.close(); } protected void msg(PrintWriter response, TrafficReport rpt, String msg) { response.println(""); response.println("
Traffic Information"); response.println("
Traffic Information
"); response.print("
" + rpt.street + " in vicinity " + rpt.vicinity + " : ");
44
Java Thin-Client Programming
response.println(msg); response.println(""); response.close(); } }
Extra information is now required and it must be retrieved via different means. It is no longer appropriate to use an instance of Hashtable to store the reports. Instead, an individual report is stored in an instance of class TrafficReport. The report contains instance variables for the street, vicinity, time of creation, and description, that is, the report itself. As the TrafficReport instances are stored in a linked list, there is also a next field that refers to the next TrafficReport. Note that in a genuine application, required to contain a large number of reports while executing under a heavy load, a linked list implementation would be far too inefficient. It may be best to employ some data structure such as a map of maps. In this case, two such structures may produce more consistent response times. One structure could map vicinity strings to maps of streets, while the other could map street strings to maps of vicinities. The lower level maps would actually map a string to a TrafficReport reference. Hashtables would also be effective, but we do not want to use java.lang.Hashtable, or a class that implements synchronized methods. Not withstanding efficiency, the current implementation is sufficient to show the general principles. In addition to the report map, the servlet class has an extra instance variable, mux, of type RWMutex. This member is used to protect the map. TrafficServlet.update searches the map and links in a newly created TrafficReport object. Before this method is called, the thread must obtain a write lock on the TrafficServlet’s read-write mutex. There is a method, TrafficServlet.getRpt(), that returns a linked list of TrafficReports. Each TrafficReport in the list is a copy of those in the map instance variable. As copies, they are invisible to other threads. Their references are local to the methods executing within a single thread, which means they are written only to the local thread’s stack. Copies are required in order to link different instances; the use of other data structures should obviate the need to make copies of TrafficReport objects. However, some form of local object must be created to hold the references. Before getRpt() can be called the thread must obtain a read lock on the read-write mutex.
Introduction to Threading Issues
45
Although the principles are involved and some verification must be carried out so that references cannot leak to other threads, or be otherwise tampered with, the implementation in this case is trivial. All complexity is hidden in the functionality of class RWMutex. Figure 18 on page 46 contains the essential code to protect an update operation, and Figure 19 on page 46 contains the code to protect a read operation. // do lead in work in method doPost // executing in some thread mux.wlock(); update(rpt); mux.unlock(); // return HTML using local instances Figure 18. Critical Section for Update Operation of Class TrafficServlet
// do lead in work in method doPost // executing in some thread mux.rlock(); rpt = getRpt(rpt); // rpt is a local instance mux.unlock(); // return HTML report using local instances Figure 19. Critical Section for Read Operation of Class TrafficServlet
Although this is a simple example, we have achieved precisely what we intended. Reports can be generated with a great degree of parallelism, but updates will be given priority, such that they will not be pending for longer than is necessary.
1.6 Asynchronous Method Invocation In this section we briefly examine some issues other than synchronization. Often in a multi-threaded environment, we want to create instances whose methods will be invoked in response to some event. However, these objects may have none of the characteristics of thread objects, or they may inherit from other classes, and therefore cannot inherit from java.lang.Thread (since Java does not support multiple inheritance). The situation is somewhat analogous to the Java event model, where an event listener is registered with an instance of a class that generates events. The methods of the listener are invoked in response to the occurrence of events.
46
Java Thin-Client Programming
1.6.1 Sidebar: Making a Pseudo-Thread When an object cannot become a thread, the most obvious option is to implement the java.lang.Runnable interface. It is then possible to construct a thread that takes an instance of our object in its constructor, and can be used to apply thread operations such as a starting, stopping, or joining. Additionally, you can encapsulate a thread object and delegate calls. The following code shows the basic idea: public class NotAThread implements runnable { Thread thr; // definitions public NotAThread() { thr = new Thread(this); } public void start() { if (!started) { thr.start(); } } public void run() { // do stuff } public void join() { try { thr.join(); } catch (final InterruptedException ie) {} } // etc }
1.6.2 Example To emulate similar behavior to the event-listener model in a multi-threaded environment, we can use a similar technique. In languages that allow function parameters, the address of a method can be registered and then invoked when it is appropriate to do so. In Java, the same result can be elegantly achieved using interfaces. In this section a number of concepts are introduced and treated very summarily. Since we are primarily concerned with synchronization, the issues are not explored in detail. The following application integrates some useful techniques of multi-threading. However, it has no purpose other than to illustrate the possibilities.
Introduction to Threading Issues
47
import java.io.*; import java.util.*; import com.ibm.austin.itsc.jalapeno.locks.*; abstract class Data { public abstract int getInterval(); } class ReadData extends Data { private int interval; public ReadData(int interval) { this.interval = interval; } public ReadData(ReadData other) { this.interval = other.interval; }
public int getInterval() { return interval; } } class WriteData extends Data { private int interval; public WriteData(int interval) { this.interval = interval; } public WriteData(WriteData other) { this.interval = other.interval; } public int getInterval() { return interval; } } class DataHolder { public Data data; public boolean dirty;
48
Java Thin-Client Programming
DataHolder() { dirty = true; data = null; } } interface ThreadSafe { void readOp(ReadData data); void writeOp(WriteData data); } class Launcher extends Thread { private DataHolder buf; private CondVar proceed, done; private ThreadSafe invoke; private Mutex mux; Launcher(ThreadSafe invoke, DataHolder buf, CondVar proceed, CondVar done, Mutex mux, ThreadGroup group, String name) { super(group, name); this.buf = buf; this.proceed = proceed; this.done = done; this.invoke = invoke; this.mux = mux; } public void run() { while (true) { try { Data local = null; mux.lock(); while (buf.dirty) { proceed.waitcond(); } if (buf.data instanceof ReadData) { local = new ReadData((ReadData)buf.data); } else { local = new WriteData((WriteData)buf.data); } buf.dirty = true; mux.unlock(); done.signal(); if (local instanceof ReadData) { invoke.readOp((ReadData)local); } else {
Introduction to Threading Issues
49
invoke.writeOp((WriteData)local); } } catch (final AlreadyUnlocked au) { System.err.println("Mutex unlocked"); stop(); } } } } class DoSomething implements ThreadSafe { private RWMutex mux; private Object data; public DoSomething() { mux = new RWMutex(); data = new Object(); } public void readOp(ReadData rdata) { mux.rlock(); // do some accessing System.out.println("Accessing " + rdata.getInterval()); mux.unlock(); System.out.println("Done accessor operation"); } public void writeOp(WriteData wdata) { mux.wlock(); // do some modification System.out.println("Modifying " + wdata.getInterval()); mux.unlock(); System.out.println("Done modification operation"); } } public class Pool { public static void main(String argv[]) { ThreadGroup group = new ThreadGroup("pool"); Mutex mux = new Mutex(); CondVar put = new CondVar(mux); CondVar read = new CondVar(mux); DataHolder holder = new DataHolder(); DoSomething callee = new DoSomething(); for (int i = 0; i != 10; i ++) {
50
Java Thin-Client Programming
System.out.println("Starting thread " + i); Launcher thrd = new Launcher(callee, holder, put, read, mux, group, "" + i); thrd.start(); } group.setDaemon(true); for(int i = 0; i != 1024; i++) { Data dat = null; if ((i % 8) == 0) { dat = new WriteData(i); } else { dat = new ReadData(i); } try { mux.lock(); while (!holder.dirty) { read.waitcond(); } holder.data = dat; holder.dirty = false; mux.unlock(); put.signal(); } catch (final AlreadyUnlocked ae) { System.out.println("Something’s very, very wrong here"); System.exit(1); } } try { Thread.sleep(1000); } catch (final InterruptedException ie) { } group.stop(); return; } }
1.6.3 Creating Threads to Invoke Callbacks Thread objects can be designed to contain instances of classes that implement some particular interface. In the application of Part 1.6.2, “Example” on page 47, class Launcher exists to make calls to an object that implements interface ThreadSafe. This object is designated thread-safe because it should protect itself against concurrent asynchronous method invocations. Class DoSomething accomplishes this with a read-write mutex, obtaining a read lock for a method that reads, and a write lock for a method
Introduction to Threading Issues
51
that writes. In a real class this protection would apply to its own instance variables. Here, it simply illustrates the point. In some sense it does protect the reference of class Data that is passed as a parameter. That reference is obtained from a different thread, and may have its methods invoked from this object in many concurrent threads. In fact, the launcher ensures that this does not occur by making copies of the object and passing the copies to the operations it invokes. Naturally, you would need to synchronize on the object itself when other DoSomething instances or instances of other classes implementing ThreadSafe could obtain access. In method main(), only one instance is created to implements the ThreadSafe interface, of class DoSomething. That instance is passed to ten different launcher threads, each of which is executing the same run() loop. The operations of the DoSomething object are invoked asynchronously from these threads. Launchers participate in the familiar producer-consumer situation. There is a twist this time in that the launchers cooperate with the thread that is executing method main(). The main thread is the producer of data which may be used in one of two ways. Launcher threads dynamically determine the operation from the type of data. The data object itself is read from a holder class. After the data is read, a dirty flag is set to indicate that the producer can overwrite the buffer with a new object. You could alternatively set a reference to null when it is no longer required. However, in a situation such as this, using a holder can be more flexible than directly invalidating the object. Figure 20 contains a sample of the application output: Accessing 412 Done accessor operation Accessing 413 Done accessor operation Accessing 414 Done accessor operation Accessing 415 Done accessor operation Modifying 416 Done modification operation Accessing 417 Done accessor operation Figure 20. Sample Output From the Asynchronous Invocation Program
52
Java Thin-Client Programming
1.6.4 Thread Pools In the example, a java.lang.ThreadGroup is used for the launcher threads. Every thread in this group is a consumer of data, and does essentially the same thing. Thread groups can be used to make applications more efficient. Creating and running threads have high overheads, and it is often expedient to incur those overheads during initialization. During normal operation, it is usually desirable that no threads are created. If a thread is required, we recommend that you obtain a free thread from the pool. The application actually does this using notification through the condition variables.
1.7 What is Missing? This chapter has concentrated primarily on synchronization issues. Although other topics have been addressed, none have been treated in detail. In particular, the subject of server finalization has not been explored. This is a specialized synchronization topic. Because of its importance, especially for writing servlets, it really deserves separate treatment. Unfortunately, most texts on threading are not written with Java programmers in mind. For a detailed treatment of threading issues, refer to Programming With Posix Threads (Addison-Wesley Professional Computing Series) by David R. Butenhof, or Programming with Threads by Steve Kleiman, Devang Shah, Bart Smaalders, and Bart Smalders.
Introduction to Threading Issues
53
54
Java Thin-Client Programming
Chapter 2. Domino Go and IBM WebSphere Application Server IBM WebSphere Application Server Standard is a product for managing Java servlets. At its core is an engine that provides the framework in which servlets can execute. It also allows for servlet configuration and administration. As most of the Application Server is itself a Java application, it is highly portable. In addition, the servlet engine is not tightly coupled to any particular Web browser, and can be bound to several of the most widely used browsers through independent modules. Supported browsers include those listed below. • IBM Domino Go Web server running on Windows NT 4.0, AIX 4.1.5, and Solaris 2.5.1, using native threads. • Apache Server running on AIX 4.1.5, and Solaris 2.5.1, using native threads. Windows NT support is due in a future release. • Netscape Enterprise Server. • Netscape FastTrack Server. • Microsoft Internet Information Server. In this chapter we will examine installation and use of the servlet engine for IBM Domino Go Webserver in the Windows NT environment. Enough information is contained to enable first-time users to quickly get their servlets up and running.
2.1 Installation In this section, it is assumed that you already have Domino Go installed and servicing HTTP requests. It is also assumed that you have obtained the file ibmwebas.exe. This is a self-extracting archive that runs the Windows NT setup program. If you do not have the distribution CD, or a copy of this file, it can be downloaded from: http://www.software.ibm.com/webservers/appserv/download.html
Before you execute the program, you must have previously installed the Java Development Kit (JDK ), preferably version 1.1.6. or newer. Run the program from the TEMP directory or from your distribution CD. It is not necessary to be logged on as administrator. Any necessary components will be extracted and the setup program will execute. You will be prompted as to what components to install. Figure 21 on page 56 contains a representation of the window.
© Copyright IBM Corp. 1999
55
Figure 21. Application Server Components Window
You must ensure that the Application Server Base Function and Application Server Administration boxes are checked. These components provide core functionality, so it is not a good idea to omit them. For convenience, it is advisable to install the documentation. You can install the samples that are included, if you would like some example servlets, suitable to be emulated. See the documentation for details of these sample servlets. You should also check the Java Server Pages box to install the Web pages from which the administration applets will be run. Finally, install the CORBA components if you will be making CORBA connections from your servlets. The product incorporates its own Object Request Broker or ORB . See Chapter 4, “Using CORBA on the Network Computer” on page 121. When you are satisfied, click the Next button to proceed. When you are prompted for the location of the JDK, tell the installation which Web server you are running. You should check the box for Domino Go, version 4.6.1 or higher. The methods for integrating with different Web servers depend on those servers, and vary considerably. You will then be prompted for the location of the application on disk and for a program folder to contain the icons. It is probably easiest to accept the suggestions of the installation program. When all the necessary files have been copied, the system must be rebooted for the setup to complete. You can either reboot immediately or wait until later.
56
Java Thin-Client Programming
2.1.1 Reading the Documentation You should be able to access the documentation directly from the distribution CD. If you have installed the documentation you can read it from your fixed drive. The default location is: C:\WebSphere\AppServer\doc\index.html Documents are available in both HTML and PDF formats. Everything you need to know can be found in the documentation. The main document appears in Figure 22:
Figure 22. Application Server Guide Introduction Page
2.1.2 Getting Started In effect, administration is carried out from a HTTP server that runs on the host on which it was installed. The port number is 9090. The administration manager is a JDK1.1 applet, so you need a browser that supports the more recent class libraries. For the NC, you can use HotJava 1.1. If you are using a PC, Microsoft Internet Explorer 4.0 is suitable. For Netscape Communicator,
Domino Go and IBM WebSphere Application Server
57
you need to obtain version 4.05 or later. If you prefer, you can apply the JDK1.1 patch, available for versions 4.03 and 4.04. In order to start the manager, use an HTTP connection to the host where Domino Go is running, except port 9090. You should see the login screen as it appears in Figure 23:
Figure 23. Application Server Manager Login Screen
Since this is your first time logging in, type the word admin for both the user-name and password fields. The admin account is for the administrator. Now your Web browser should appear as in Figure 24 on page 59.
58
Java Thin-Client Programming
Figure 24. View of Services
There are three fields: Services, Status and Version. The first entry is WebSphere itself, and chained underneath are any Web servers with which it is cooperating. There should only be one: Domino Go. Both services should have a running status. You can change WebSphere’s port number and admin password by clicking on the Properties button in the upper right corner of the applet. You can obtain context sensitive help at any time by clicking the Help button. Highlight the Domino Go service and click the Manage button toward the bottom of the applet. A separate applet window appears, from which most of the administration is carried out. There are four buttons in the upper left corner of the window. They are Setup, Monitor, Security and Servlets. Click on each of these to see the various options.
Domino Go and IBM WebSphere Application Server
59
2.1.3 Adding a Servlet Before you can add a servlet to the Web server you must develop a class file or a JAR file. Usually, you would use include the JavaSoft Development Kit JAR file, jsdk.jar, in your CLASSPATH during development. If you prefer, you can include C:\WebSphere\AppServer\lib in your CLASSPATH, as this directory also contains jsdk.jar. If you do this, you can also take advantage of WebSphere’s other libraries. Assume that we have written a servlet and have generated all the necessary class files. We then copy the class files required by the servlet into the servlets directory. The default location is: C:\WebSphere\AppServer\servlets The next step is to make the servlet accessible through the Web server, in our case, Domino Go. Log in to the WebSphere manager at port 9090 as user admin. Click on the Servlets button. There is a list of options on the left; click on option Add, and fill in the servlet name and class. The servlet name is the name that will be used in the URL that will identify your servlet. Servlet requests will be made using this URL. In our example, we will configure the Traffic Report Server (See Chapter 1, “Introduction to Threading Issues” on page 1). It will be accessed through the URL, http://hostname/servlet/traffic, so we fill in the string traffic as the servlet name. The class name is TrafficServlet. Note that you may want to specify a full package name here to avoid conflicts. When you click the Add button, the servlet identified by the name traffic is associated with class TrafficServlet. At this point the applet should appear as it does in Figure 25 on page 61:
60
Java Thin-Client Programming
Figure 25. Servlet Configuration Window
2.1.4 Configuring and Loading a Servlet You should now configure the servlet. If the servlet has not been freshly added, you can get to the configuration window by first loading the servlets window, finding the name of your servlet under the Configure option, and clicking on that name, in our example, traffic. You will usually want to add some properties. The traffic servlet has only one Properties tab and then click the Add button. Now enter the name, password, and a value. You can now save the configuration and load the servlet. If you wish, you can specify that the servlet is automatically loaded by the servlet manager. If you change the servlet, all you need to do is overwrite the class files in the servlets directory. The manager will automatically reload the servlet. Suppose that a servlet has gone wrong, and entered a race condition or an infinite loop. All that is required is to click the Unload button. In practice however, unloading a servlet can cause problems in the communication
Domino Go and IBM WebSphere Application Server
61
between the applet and the manager process. If you do this, the applet may become unresponsive. Future releases should address this problem.
2.1.5 Monitoring a Servlet In order to check on the progress of your servlet, go to the WebSphere manager and click on the Monitor button. Then select the Loaded Servlets option from the list on the left hand side. Your view should be similar to that of Figure 26:
Figure 26. Servlet Monitor View
To periodically update the view, click the Refresh button. To have the view updated automatically, select an appropriate interval (the default is ten seconds) and click the Start button.
62
Java Thin-Client Programming
Note
You may have been wondering how an applet can provide such a powerful range of functionality. It is possible because the applet opens a TCP socket to the host for communication. Untrusted applets are allowed to open connections only to the host from which their classes were loaded. The listener at the host performs such tasks as undating the configuration files.
Domino Go and IBM WebSphere Application Server
63
64
Java Thin-Client Programming
Chapter 3. Security Considerations Perhaps the fundamental reason for the success of Java to date is its association with the World Wide Web. Java had been around for many years without generating much excitement before applets came along. They allowed for the creation of more visually appealing Web pages, and took the internet a step closer to the sophistication of the GUI interfaces that users had come to expect. In addition to this, Java applets promised platform independence; there is no need to port applets. As long as users have access to a Web browser that can launch a Java Virtual Machine, any user can retrieve and run an applet from any available Web site. In effect, the problem of distribution that many software vendors experience can be virtually eliminated. So far so good. However, users are downloading and running applets before they perceive that anything is happening. When we casually connect to a page from a search engine, we may not have a clue what data will be transferred or what program will start running on our computer. The internet community tolerates this state of affairs because they trust that nothing really unpleasant will happen. In other words, the success of Java depends to a very great extent on its inherent security. If it was relatively easy for applets to trash a hard drive, lock out a keyboard, replace a device driver in memory, or shut down the window manager, they would have been very short lived. Of course, applets can only execute within the sandbox. This model limits what can be performed to the narrow sandbox bounds. However, this is only the most well-known aspect of Java’s security features. There are others, even more fundamental than this. One problem with the sandbox model is its restrictiveness. Occasionally, the user knows and trusts the applet provider, and would like to allow more freedom so that some useful work can be done. In addition, Java was conceived as a general purpose programming language and environment, within which applets are only one facet. Security considerations must often be applied to applications as well. We would like Java to allow as much flexibility as possible in a way that does not compromise mechanisms. Security has been a concern of the computing industry since its inception. Remote commercial transactions are possible today because of advances that have nothing to do with Java. There are security practices, algorithms, and standards concerning authentication, authorization, and privacy. If Java is to become an environment for producing commercial quality applications, it
© Copyright IBM Corp. 1999
65
is imperative that there are libraries available that implement the known standards of security. Historically, Java has had more than its fair share of security problems. The internet community has uncovered and documented many errors in the basic security implementation of Version 1.0. Such disclosures have generally led to tightening of the underlying algorithms, sometimes in an ad-hoc manner. Sun actively encourages tracking security problems. In the long term, this practice will benefit the technology since it is essentially a protracted beta-testing process. Actually, security is one of better designed and integrated aspects of Java. This is not to say that there are no problems; like most aspects of Java, security is still immature in many areas of implementation. However the overall design is encompassing and consistent. The entire model is very extensive and covers a multitude of topics.
3.1 Language Features Basic language security was a principal design consideration, and dates from the JDK1.0. It involves a tightly controlled relationship between the Java Virtual Machine and the verification of the byte codes that it executes. In addition to the verifier, there is another check on what is sent to the Java Virtual Machine. Class loaders operate within the context of a security manager controlling what libraries an application can load.
3.1.1 Security of Address Space For dynamic memory management, languages such as C++ rely on directly manipulating the virtual address space through pointers. Although the address space of other applications is protected by the operating system, it is easy for C++ programs to alias addresses and cast away object type. In addition there are no bounds checks on arrays, allowing for the corruption of memory contiguous to the array. It is even possible for a malicious program to trap into the address space of a caller by manipulating the values written to the stack or by jumping to other locations. Libraries containing errors or deliberate attacks can cause considerable damage in unexpected ways. In Java, you cannot directly access a virtual address, modify the stack, jump over method calls, or manipulate machine registers. You cannot even make a system call. If making a system call is necessary, you must write a native method. Denying access to these features also makes for easier programming and is the basis for achieving platform independence. When you read or write a file or create an object, system calls must be made by the
66
Java Thin-Client Programming
Java interpreter, but this is not generally a concern of the programmer. It is implicit that the interpreter is trustworthy, since they are generally supplied by the vendors on whose platforms they execute, this is usually a fair assumption. Java also has runtime support for checking on array bounds and type conversions. If you attempt to overrun an array you will get an ArrayIndexOutOfBoundsException and if you try to cast an object to a class of which it is not an instance, you will get a ClassCastException. You can catch these exceptions, but you cannot force the operations to succeed.
3.1.2 Java Virtual Machine When you compile a Java program, the result is a class file. That file contains instructions called byte codes, readable by the Java Virtual Machine. The Java Virtual Machine is a zero address automation. That means that instructions have no parameters; any required values are obtained from a theoretical stack and results are written to that stack. Byte codes are an intermediate form. It is considerably easier to generate object code from byte code than directly from the Java source, and this is the job of the interpreter or the Just in Time (or JIT) compiler. These utilities provide concrete implementations of the abstract Java Virtual Machine. In future versions of Java Network Computers, the CPUs will directly implement the instruction set of the Java Virtual Machine.
3.1.3 Loading Classes Whenever a source file is compiled, a separate class file is generated for every class that it contains, including nested classes. The Java Virtual Machine knows nothing about nested classes; the compiler generates references to distinct classes that it creates. Now, these classes have to be loaded by the interpreter during program execution, but they may not all be required. Also, the program may refer to other classes such as java.lang.String, and these must be loaded as needed. Loading classes is a very dynamic process. To start, the specified application class is loaded, and the main() method is entered. As additional classes are referenced, they are loaded. For example, when a variable of type aType is declared in main, the class file, aType.class, is loaded. If aType contains a member of class anotherType, anotherType.class is loaded. If anotherType is derived from superOfAnotherType, then superOfAnotherType.class is loaded, and so on. The interpreter examines its CLASSPATH environment variable or command
Security Considerations
67
line arguments, extracting the classes it requires from default Java library locations, JAR files, directories, and/or ZIP files. This behavior provides for built-in language security. If you want to prevent certain classes being loaded, load classes from different sources and/or file names, or transform the content of class files in some way, you can write your own class loader. Because the byte codes of a class cannot be passed to the Java Virtual Machine before the class is loaded, you can augment the built-in language security. Details for writing your own class loader are given in Section 3.2, “Custom Class Loaders” on page 68.
3.1.4 The Verifier Whenever a class is loaded, the verifier is entered to check the byte codes for potentially damaging behavior. The verifier ensures that object types are correct across method calls, that private members cannot be accessed by external objects, that variables are initialized, and so on. If any of the tests fail, loading of the offending class is suppressed. If a class is generated using the java compiler, verification is not necessary. However, it is not difficult to create or alter a class file, in much the same way as executables can be edited to propagate a software virus. There are three levels of verification, verify, noverify, and verifyremote. The latter is employed to verify classes obtained from another host, and is the default setting of the interpreter, since it is these classes which are the greater security threat. It should never be necessary to verify the system classes on your own machine, but if you don’t trust some of your colleagues you can have all classes verified by using the switch, -verify.
3.2 Custom Class Loaders You can write your own class loader by inheriting from the abstract base class ClassLoader. When a particular class is loaded by a class loader, all classes referenced by the original class are loaded by the same instance. You only need to provide an implementation of the abstract method, with the following signature: public class loadClass(String className, bool resolve)
The method is transparently called by the Java Virtual Machine when some class that has been loaded by this class loader references another class for the first time. Note that such references may be circular, in which case loadClass() will be called with the className parameter corresponding to
68
Java Thin-Client Programming
the name of a class that has already been loaded. Therefore, the class loader needs to maintain a data structure mapping names to classes. JDK1.1 documentation recommends using a java.util.Hashtable for the purpose. When the resolve flag is set, loadClass() must invoke the resolveClass() method of its base class. This is a final method, and will result in loadClass() being called again if the class does indeed contain references to other classes. Most of the time resolve will be set, but occasionally the Java Virtual Machine will call loadClass() to check whether a class exists. In that case, resolve will be set false.
3.2.1 Example Class Loader The most obvious examples of class loaders are the applet class loaders used by the various browsers. They must load classes from a remote host using TCP, and ensure that the name space of every applet is strictly segregated. In the past, there have been several flaws and inconsistencies in the operation of an applet class. This is not so much of an issue now, however it remains to be seen if signed applets will present a problem in the future. See section 3.4.5, “Signed Applets” on page 108. As an example, a program is shown below containing a complete class loader. This loader can load classes from files with any extension or no extension, rather than class files alone. The extension to use is passed as a parameter to its constructor. import import import import
java.lang.*; java.util.*; java.io.*; java.lang.reflect.*;
class OurClassLoader extends ClassLoader { private String extension; private Hashtable map; public OurClassLoader(String fileExtension) { if (fileExtension.length() > 0) { extension = "." + fileExtension; } else { extension = ""; } map = new Hashtable(); } protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
Security Considerations
69
Class cl = (Class)map.get(name); if (cl == null) { // the class has not been previously loaded try { // check if this is a system class cl = findSystemClass(name); if (cl != null) { return cl; } } catch (final NoClassDefFoundError ncdfe) { // no problem - we’ll define the class } catch (final ClassNotFoundException cnfe) { // no problem - we’ll find the class } // load the byte codes for the class byte[] data = loadClassData(name); if (data == null) { throw new ClassNotFoundException(name); } // allow the JVM to turn the data into something usable // details do not concern the programmer cl = defineClass(name, data, 0, data.length); if (cl == null) { throw new ClassFormatError(name); } // ensure future lookups will immediately succeed map.put(name, cl); } // if resolve flag is true // class must be resolved before use if (resolve) { resolveClass(cl); } return cl; } private byte[] loadClassData(String name) { String fname = name.replace(’.’, ’/’) + extension; FileInputStream fin = null; try { fin = new FileInputStream(fname); ByteArrayOutputStream buf = new ByteArrayOutputStream(); for (int c; (c = fin.read()) != -1;) { buf.write(c); } fin.close();
70
Java Thin-Client Programming
return buf.toByteArray(); } catch (final IOException ioe) { try { if (fin != null) { fin.close(); } } catch (final IOException ioce) { } finally { return null; } } } } public class TestCL { public static void main(String[] argv) { if (argv.length != 1) { System.err.println("usage: java TestCL
"); return; } OurClassLoader ld = new OurClassLoader(argv[0]); try { Class c = ld.loadClass("DoSomething", true); Object o = c.newInstance(); Method m = c.getMethod("writeOut", null); m.invoke(o, null); } catch (final Exception e){ System.out.println(e); } } }
This program accepts the extension string as a command line parameter. It instantiates a class loader of class OurClassLoader and calls the loadClass() method to load a class whose name is hard-coded. In practice, the class name would have to be dynamically retrieved from another source. In order to invoke a method, an instance of class method from java.lang.reflect is employed. In this case, the method has no arguments, so the parameter list is not needed. Figure 27 contains the relevant section of code. After the first explicit call to loadClass(), the method will be transparently invoked by the Java Virtual Machine whenever necessary.
Security Considerations
71
Class c = ld.loadClass("DoSomething", true); Object o = c.newInstance(); Method m = c.getMethod("writeOut", null); m.invoke(o, null); Figure 27. Install ClassLoader, Create Object, and Invoke a Method
Figure 28 contains the Java source for the class used to test the class loader program. Note that the class loader itself is not limited to any particular class; it will load any class. For the purpose of illustration the test class is trivial. You can compile DoSomething.java and rename the class file to have any extension you like, or none at all. Then use the interpreter or JIT compiler to load class TestCL. The output will simply be: Loaded by a different class loader
as printed by method DoSomething.writeOut(). import java.lang.*; public class DoSomething { public DoSomething() {} public void writeOut() { System.out.println("Loaded by a different class loader"); } } Figure 28. Class DoSomething for Testing the Class Loader
Class loaders must generally behave like OurClassLoader. The differences between class loaders consists in reading the byte codes for the class to be loaded. In the example above, the class is read from an input file, in much the same way as the system class loader would work. All the work is done in OurClassLoader.loadClassData(), which returns an array of bytes. As previously stated, an applet class loader must obtain the byte codes from a remote host using TCP. The following sequence of steps must be performed by loadClass(): Class cl = (Class)map.get(name); if (cl == null) { try { cl = findSystemClass(name); if (cl != null) { return cl; } } catch (final NoClassDefFoundError ncdfe) {
72
Java Thin-Client Programming
} catch (final ClassNotFoundException cnfe) { }
First, try to retrieve the requested class from the map. If it is found, it has been previously loaded. You need to resolve the class if the resolve flag is set. If the class is found, it is returned. In this code example, the return statement is performed at the end of the if block. If the class has not been loaded, you must check whether it is a system class. Our class loader has nothing more to do in this case, other than return the class to the caller: byte[] data = loadClassData(name); if (data == null) { throw new ClassNotFoundException(name); }
Next comes the most important part of the process, loading the byte codes. You should implement this as a separate method. That way, you can largely separate the application-specific details from the code common to every class loader. Here, the separation is virtually complete, because the application information used by loadClassData(), in this case the file extension string, is stored in an instance variable: cl = defineClass(name, data, 0, data.length); if (cl == null) { throw new ClassFormatError(name); }
It is necessary to call method defineClass() of base class ClassLoader so that the byte codes are formatted into a usable form. If there is a problem with verification, a ClassFormatError is thrown. The check for a null return value is only a safeguard: map.put(name, cl); }
A loader may be called to return a class it has previously loaded. This could happen when there are circular references, for example. It is therefore very important that you map names to classes and reuse those classes that have an entry in the map: if (resolve) { resolveClass(cl); }
Security Considerations
73
You need to call method resolveClass() of base class ClassLoader to resolve all the classes that this class refers to. A loaded class does contain such references, whenever: • It is derived from a super class that has not been loaded. • It contains instance variables of a class that has not been loaded. • It refers to static members or methods of a class that has not been loaded. As a result of calling resolveClass(), the loadClass() method may be repeatedly invoked by the Java Virtual Machine: return cl;
Finally, return the class to the Java Virtual Machine.
3.3 The Security Manager A security manager allows Java applications to enforce a security policy. After a class has been loaded and verified, it may attempt to perform operations that the application chooses to restrict. A security manager distinguishes between classes and between threads, so considerable latitude is possible in implementing policies. Again, the obvious example is the security manager installed to guard against illegal applet operations. It is able to allow an operation such as reading a file for classes loaded from the local disk, while blocking similar operations for classes loaded by an installed class loader. Java applications usually run without a security manager. This is the responsibility of the application itself. Although it is possible to install several class loaders, only one security manager can be installed. This makes sense, because any security policy must be application-wide. It is not possible to create a chain of security managers, and this is not necessary in practice. If security checks need to be chained together, the usual inheritance mechanism can be utilized with explicit delegation.
3.3.1 Traps Into a Security Manager A security manager must inherit from the abstract class java.lang.SecurityManager. In effect, the class is a template for what is possible to implement, because each method defines a hook that can be called from various Java libraries. System libraries were designed and implemented with the security manager in mind, and the API is intimately bound to the library code. Consequently, the level of security implicit in the definition of SecurityManager cannot be extended.
74
Java Thin-Client Programming
Method names that are responsible for allowing or denying an operation typically begin with the word, check . When a library is about to carry out a potentially illegal action it obtains the security manager instance and calls one of its check methods. If the check fails, a SecurityException is thrown. Figure 29 shows how a library would make such a check: SecurityManager securityInstance = System.getSecurityManager(); if (securityInstance != null) { security.checkOperation(arg1, . . . ); } Figure 29. Pseudo-Code Fragment for a Security Check
Following is a list containing the method names with the actions that they are intended to monitor: checkAccept(String host, int portnum)
Checks whether the calling thread is permitted to accept a TCP connection from the specified host and port number. checkAccess(Thread)
Checks whether the calling thread is permitted to modify the thread argument with a call to Thread.stop(), Thread.suspend(), Thread.setPriority,() and so on. checkAccess(ThreadGroup)
Checks whether the calling thread is permitted to modify the thread group argument. checkAWTEventQueueAccess()
Checks whether the calling code is permitted to access the AWT event queue. checkConnect(String host, int portnum)
Checks whether the calling thread is permitted to open a TCP connection to the specified host and port number. checkConnect(String host, int portnum, Object context)
Checks whether the security context, context, is permitted to open a TCP connection to the specified host and port number. checkCreateClassLoader()
Checks whether the calling thread is permitted to instantiate a new class loader. checkDelete(String)
Security Considerations
75
Checks whether the calling thread is permitted to delete the file whose name is specified by the argument. checkExec(String command)
Checks whether the calling thread is permitted to launch a sub-process with the command specified in the argument. checkExit(int status)
Checks whether the calling thread is permitted to exit the Java Virtual Machine with the specified status. checkLink(String)
Checks whether the calling thread is permitted to dynamically load the library from the archives whose name is specified by the argument. checkListen(int portnum)
Checks whether the calling thread is permitted to listen for an incoming TCP connection on the specified port number. checkMemberAccess(Class class, int n)
Checks whether the calling code is permitted to access the nth instance variable in an instance of the specified class. checkMultiCast(InetAddress)
Checks whether the calling thread is permitted to use IP multicast for the specified group address. checkMultiCast(InetAddress, byte)
Checks whether the calling thread is permitted to use IP multicast for the specified group address with the specified value (Multicast send). checkPackageAccess(String)
Checks whether the calling thread is permitted to access the package whose name is specified by the argument. checkPackageDefinition(String)
Checks whether the calling thread is permitted to define classes in the package whose name is specified by the argument. This is only intended for use by certain class loaders. checkPrintJobAccess()
Checks whether the calling thread is permitted to queue a print request. checkPropertiesAccess()
Checks whether the calling thread is permitted to set system properties. checkPropertyAccess(String)
76
Java Thin-Client Programming
Checks whether the calling thread is permitted to set the system property whose name is specified by the argument. checkRead(FileDescriptor)
Checks whether the calling thread is permitted to read the file whose descriptor is specified by the argument. checkRead(String)
Checks whether the calling thread is permitted to read the file whose name is specified by the argument. checkRead(String filename, Object context)
Checks whether the security context, context, is permitted to read the file whose name is specified by the argument. checkSecurityAccess()
Checks access to particular operations to determine if a security API should be invoked. checkSetFactory()
Checks whether the calling thread is permitted to set the socket factory used by classes ServerSocket or Socket, or the stream handler factory used by class URL. checkSystemClipboardAccess()
Checks whether the calling code is permitted to access the system clipboard. checkTopLevelWindow(Object window)
An unusual method, that returns a boolean value indicating whether the calling thread is permitted to display the top-level window, window. If false is returned an exception is not thrown, but the window is expected to contain some warning text, for example, Untrusted Applet Window. checkWrite(FileDescriptor)
Checks whether the calling thread is permitted to modify the file whose descriptor is specified by the argument. checkWrite(String)
Checks whether the calling thread is permitted to modify the file whose name is specified by the argument.
3.3.2 Example Security Manager In the following program a custom security manager is installed to check for three possible security breaches. It ensures that System.exit() cannot be
Security Considerations
77
called by threads of a particular type, that Java class files cannot be altered, and that no file can be read if it contains a certain string. Here is the source. import import import import
java.lang.*; java.io.*; java.net.*; java.util.*;
class T1 extends Thread { public T1() {} public void run() { try{ System.exit(1); } catch (final SecurityException se) { System.out.println(se); } } } class OurSecurityManager extends SecurityManager { public void checkAccess(Thread t) {} public void checkAccess(ThreadGroup tg) {} public void checkExit(int status) { if (Thread.currentThread() instanceof T1) { throw new SecurityException("T1 can’t call exit"); } } public void checkRead(String filename) { Class[] contexts = getClassContext(); // check if this instance has the file open // the chain must be at least two deep // file opener and this instance for (int i = 1; i != contexts.length; i++) { if (contexts[0] == contexts[i]) { return; // we are in the context chain } } FileInputStream fi = null; String ms = "Microsoft"; try { fi = new FileInputStream(filename); int ch; int i = 0; while ((ch = fi.read()) != -1) { if (ms.charAt(i) == ch) i++;
78
Java Thin-Client Programming
else i = 0; if (i == ms.length()) { throw new SecurityException(filename + " contains " + ms); } } } catch (final IOException ioe) { // shouldn’t happen since file existence is // already checked and must exist System.out.println(ioe); } finally { try { if (fi != null) fi.close(); } catch (final IOException ioe) { } } } public void checkWrite(String filename) { if (filename.indexOf(".class") != -1) { throw new SecurityException(filename + ": attempt to corrupt class"); } } }
public class TestSecMgr { public static void main(String[] argv) { System.setSecurityManager(new OurSecurityManager()); new T1().start(); FileInputStream fin = null; try { fin = new FileInputStream("bill.txt"); fin.close(); } catch (final Exception e) { System.out.println(e); } finally { try { if (fin != null) fin.close(); } catch (final IOException ioe) { } } FileOutputStream fout = null; try {
Security Considerations
79
fout = new FileOutputStream("TestSecMgr.class"); } catch (final Exception e) { System.out.println(e); } finally { try { if (fout != null) fout.close(); } catch (final IOException ioe) { } } System.exit(0); } }
Figure 30 contains the output from the test program, when the file bill.txt contains the string Microsoft. java.lang.SecurityException: T1 can’t call exit java.lang.SecurityException: bill.txt contains Microsoft java.lang.SecurityException: TestSecMgr.class: attempt to corrupt class Figure 30. Output from the Example Security Manager Test
3.3.2.1 General Details Class OurSecurityManager extends SecurityManager and overrides five methods. It is necessary to override the checkAccess() methods for threads and thread groups because the inherited methods always throw a security exception, so that is impossible to modify threads. Since this program creates an instance of a thread, we ensure that neither method will throw an exception. To do that, it is sufficient to make the method bodies empty. To install an instance of the security manager the main method contains the following statement: System.setSecurityManager(new OurSecurityManager());
Once the security manager has been installed, it is active for the remainder of the application. It cannot be rescinded or replaced. 3.3.2.2 Checking System.exit First, we want to prevent threads of type T1 from exiting the Java Virtual Machine. Figure 31 contains the source:
80
Java Thin-Client Programming
public void checkExit(int status) { if (Thread.currentThread() instanceof T1) { throw new SecurityException("T1 can’t call exit"); } } Figure 31. Method checkExit() of OurSecurityManager
Now, whenever a thread of type T1 attempts to exit, an exception will be thrown and the action prevented. This is possible because System.exit() obtains the security manager through method System.getSecurityManager(). If there is no security manager installed, the call returns null and System.exit() invokes a private system method that actually exits the Java Virtual Machine. If there is a security manager, checkExit() is called. If an exception is thrown as a consequence, then the invocation of the private exit method is not reached. Note that our checkExit() does not even access the status argument. This is the original argument to System.exit(). When required, a determination on whether to throw an exception can be made from its value. 3.3.2.3 Checking Writes to Class Files The next thing we want to do is prevent Java class files from being modified. For this we override the checkWrite() method: public void checkWrite(String filename) { if (filename.indexOf(".class") != -1) { throw new SecurityException(filename + ": attempt to corrupt class"); } } Figure 32. Method checkWrite() of OurSecurityManager
Here we examine the name of the file that the calling thread is attempting to open for writing. If it is a Java class file (actually, if the string .class occurs anywhere in the name) an exception is thrown. 3.3.2.4 Checking File Content Both of the preceding methods are trivial. In order to check that a file cannot be read if contains a certain string, it is (paradoxically) necessary to read the file. Consequently, method checkRead() is a little trickier. Figure 33 contains the source:
Security Considerations
81
public void checkRead(String filename) { Class[] contexts = getClassContext(); // check if this instance has the file open // the chain must be at least two deep // file opener and this instance for (int i = 1; i != contexts.length; i++) { if (contexts[0] == contexts[i]) { return; // we are in the context chain } } FileInputStream fi = null; String ms = "Microsoft"; try { fi = new FileInputStream(filename); int ch; int i = 0; while ((ch = fi.read()) != -1) { if (ms.charAt(i) == ch) i++; else i = 0; if (i == ms.length()) { throw new SecurityException(filename + " contains " + ms); } } } catch (final IOException ioe) { // shouldn’t happen since file existence is // already checked and must exist System.out.println(ioe); } finally { try { if (fi != null) fi.close(); } catch (final IOException ioe) { } } } Figure 33. Method checkRead() of OurSecurityManager
Of course, the issue here is how to prevent an infinitely recursive call chain. When a calling thread attempts to open a file for reading, an I/O library method calls checkRead(), which immediately tries to open the file, leading to the I/O library method being called again, which in turn calls checkRead(). There is more than one way to prevent this situation. The checkRead() method could set the private member boolean, inCheck when it found that it was false. When it is re-entered it could access the member, either directly or
82
Java Thin-Client Programming
using the protected method getInCheck(). When it finds the boolean true, it simply returns without trying to open the file, and the I/O library call can make progress. However, this approach would be tedious in this case, because the check method can exit through many code paths, and each of those paths would require code to handle the setting of inCheck to false. Instead, we obtain an array of all the classes for instances that are currently executing methods on this thread’s runtime stack. This is possible with the method SecurityMethod.getClassContext(). Note that the granularity here is somewhat coarse. It would be more flexible if the security manager could obtain the objects and executing methods. However, this is not possible and for our purposes it does not matter. Only one instance of class SecurityManager can be executing, since only one security manager can be installed. There is no situation where the invocation of one of our methods leads to the invocation of another, apart from checkRead() leading to checkRead(). If this were not the case, we would have to analyze the possibilities and possibly keep a record of potential states. Because checkRead() is executing, class OurSecurityManager is on top of the stack, which also means that it is in the zero position of the returned array. It is safe to assume there is more than one class in the array. There will always be at least three: the caller, the I/O library, and the security manager. The strategy is to check the array of classes for the class at the top of the stack, that is, OurSecurityManager. When we find another occurrence, we know that this is a recursive call from the library, and we return. Now we can return from the call to the FileInputStream() constructor. We have an open file, which we can read, checking for the forbidden word, and then close. If the word is found we throw a security exception. 3.3.2.5 Other Protected Methods Apart from getClassContext() and getInCheck(), there are other protected methods to assist security managers. Briefly, they are as follows: int ClassDepth(String)
Returns the stack depth or position of the first instance of a class whose name is specified by the parameter. This is the position of its closest executing method. If no method of the specified class is executing, -1 is returned. boolean inClass(String)
Security Considerations
83
Checks the stack for an instance of a class whose name is specified by the parameter. If a method of this class is executing it returns true, if not it returns false. class currentLoadedClass()
Checks for the most recent method invocation on the stack of a class that was loaded by a class loader. This class is returned if it exists, otherwise null is returned. When the class exists, the security manager has been entered as a direct or indirect consequence of a method invocation of an instance (or class method) of the returned class. ClassLoader currentClassLoader()
Checks for the most recent method invocation on the stack of a class that was loaded by a class loader. If such a class exists, the class loader that loaded it is returned, otherwise it returns null. int classLoaderDepth()
Checks for the most recent method invocation on the stack of a class that was loaded by a class loader. If such a class exists, its position on the stack is returned, otherwise it returns -1. boolean inClassLoader()
Checks the stack for a class that was loaded by a class loader. If such a class exists it returns true, otherwise it returns false. If true is returned, this implies that the currently executing method of the security manager was invoked from an instance of a class loaded by a class loader. Of these support operations, the most frequently used is inClassLoader(). For example, applet security managers can use this method to determine if the potentially insecure operation is being attempted by the trusted local classes, or the untrusted remote classes, since the remote classes are loaded with the applet class loader.
3.4 Authentication and Data Integrity Basic data integrity is a concern of every transmission protocol. Data integrity at the network link and transmission levels means that messages are received in their entirety, without omission or duplication of the components that make up the message. This involves adding session and sequence information to the data blocks, together with checksums. When communication occurs over a network connection, you can never be guaranteed that a received message has integrity. However, when you use TCP, you can at least be assured that an error condition will be detected and
84
Java Thin-Client Programming
reported. When you use a datagram protocol such as UDP, there is no such guarantee. An issue that is closely related to integrity is authentication. Although we may be sure that a message has not been corrupted, we may not be sure of its point of origin. Transmission protocols operate at a low level, and are insufficient for application communications. They ensure a message is not garbled, but they do not and cannot ensure a message has not been deliberately altered in advance, because this is not a networking consideration. In any case, there are many well-known ways to spoof IP addresses, submit false information to protocols such as POP3 and exploit weaknesses in NFS. These facts ought to alert you to the vulnerabilities of running networked applications of any kind, including Java applications. In the following section, we will look at some of the mechanisms for achieving data integrity and authentication in Java. The libraries we will examine are new to JDK1.1. Java’s security model is not yet complete, but it has been designed to encompass a very wide set of features. This is possible because the theory that underlies digital security is well established, and has been well implemented in other environments. Absolute security is probably an unachievable goal. It has always been a concern of humanity, but only one revolutionary idea, public key cryptography, has emerged in the last hundred years. However, techniques are constantly being refined, aided by advances in computer processing power. At this point, it appears that Java will have a good basis for implementing secure systems with the release of Version 1.2.
3.4.1 Message Digests A message digest serves a similar purpose to that of a checksum. It is a value that is computed from a data block in order to validate the block. Because a checksum length is smaller than the length of the data block length from which it was calculated, there is a mathematical probability that the checksums for different blocks of data will be equal. A checksum algorithm used at the link level must produce values that have a comparatively small chance of being equal for blocks of data that are very similar. When an error occurs at the link level, it is unlikely to be of such a degree that the entire block is scrambled. Similar considerations apply to the algorithms used to produce message digests, except the probability that the same digest will be calculated from different blocks of data is much smaller than for conventional checksum algorithms.
Security Considerations
85
Java 1.1 supports two algorithms for computing message digests, the Secure Hash Algorithm #1 (SHA-1) and Message Digest #5 (MD5). SHA-1 was developed by the National Institute of Standards and Technology and MD5 was developed by Ronald Rivest of MIT, who also developed Rivest Cipher #4 (RC4) encryption and co-developed the RSA standard for public key cryptography. Both SHA-1 and MD5 are easy to implement and allow for relatively fast computation. Many mathematicians feel that SHA-1 is the slightly superior algorithm. However, there is no known way of changing a message to produce the same digest for either algorithm. As with good checksum algorithms, a slight change in the data block produces a radical change in the digest. SHA-1 produces a digest with a length of 20 bytes. The probability that the same digest will be produced for two different messages approaches 1/2160 . MD5 produces a digest with a length of 16 bytes. 3.4.1.1 Example Program Package java.security is fairly consistent in the API design of its classes. It is very easy to work with message digests. You need to obtain an instance that implements one of the algorithms. You do this with a statement such as: MessageDigest md = MessageDigest.getInstance("SHA-1"); Method getInstance() takes the name of the algorithm as its argument and throws a NoSuchAlgorithmException if there is no service installed for the named algorithm. Incidentally, you can implement your own service providers for algorithms other than MD5 and SHA-1. To do this you need to extend MessageDigest, override the protected Service Provider Interface (SPI) methods, and add the service to known service providers with an invocation of Security.addProvider(). As mentioned earlier, the security API is relatively consistent, and you can add providers for other elements of security, such as a public key encryption scheme to be used in generating digital signatures. However, we do not go into detail here, since we are concerned with using the API, rather than providing the services. The next thing you have to do is call one of the update() methods. There are three variants, taking an array of bytes, an array of bytes with start and end indices, and a single byte. All methods can be called repeatedly, using consecutive pieces of the input block. You can also mix the invocations, if that is appropriate. Finally, you call the digest() method, which returns an array of bytes. This method will pad the block as required by the algorithm, before
86
Java Thin-Client Programming
returning the digest. Figure 34 contains the source to generate and display digital signatures: import java.lang.*; import java.security.*; public class TestDigest { static char[] hex = { ’0’, ’1’, ’2’, ’3’, ’4’, ’5’, ’6’, ’7’, ’8’, ’9’, ’a’, ’b’, ’c’, ’d’, ’e’, ’f’ }; public static void main(String[] argv) { if (argv.length != 2) { System.err.println("usage: java TestDigest " + "[MD5 | SHA-1] "); return; } MessageDigest md = null; try { // get an instance implementing the algorithm md = MessageDigest.getInstance(argv[0]); // update the digest md.update(argv[1].getBytes()); // pad input and finalize byte[] digest = md.digest(); System.out.print("digest is: "); for(int i = 0; i != digest.length; i++) { System.out.print(hex[(digest[i] >> 4) & 0xf]); System.out.print(hex[digest[i] & 0xf]); System.out.print(’ ’); } System.out.println(); } catch (final NoSuchAlgorithmException nsae) { System.err.println(nsae); return; } } } Figure 34. Program to Generate and View a Message Digest
Running the program with the following command: C:\>java TestDigest SHA-1 "The dickens you say!"
produces the following result: digest is: 31 c4 bc 1f 9f 55 34 f1 f5 44 26 5f 0e 6d 05 bf 87 53 aa 46
Security Considerations
87
If you use MD5 with the same string, the following output is generated: digest is: 82 ca 10 92 66 f0 52 db 85 15 d3 ec 72 02 36 f3
3.4.2 Digital Signatures Message digests are used to ensure data integrity, but this is not enough. So long as the message and the digest are delivered from different sources, there is a reasonable chance that the message is authentic. However, as the preceding example has shown, generating a message digest is trivial. It is almost as trivial to forge a message and provide a genuine digest for the forgery. Authentication means being assured that the message has not been altered. It also means being assured that the message did not originate from a sender claiming to be someone else. For example, the message, "Transfer $1,000,000 from my account to the account of Mr. Y, from Ms. X.", with an appropriate digital signature, could have been sent by Mr. Y posing as Ms. X. A person is usually authenticated from something they know, like a password, from something they have , like a key, and/or from something they are, like a face or a fingerprint. The fingerprint analogy is often used for message digests. We refer to the digest as the fingerprint of a message. We are able to authenticate that a message did in fact come from Ms. X using something she has, a unique signature. Digital signatures authenticate electronically transmitted messages in much the same way as written signatures authenticate letters. It is possible to produce a digital signature that is exceptionally difficult to forge. In fact, the digital signature is based on the contents of the message, and therefore changes from message to message. The technology is based on public key cryptography. 3.4.2.1 Public Key Cryptography Public key cryptography is a relatively new concept. Other forms of encryption rely on a process of applying a cipher with a secret key value to a message. The same key is applied in a reversed process to decrypt. There are extremely good encryption algorithms. The biggest problem is not with the encryption process itself, but with the number of keys that are required. A secret key is needed by every two parties who wish to communicate privately. If A, B, and C wish to conduct two-way communications in private, three secret keys are necessary; one for A-B dialogs, one for A-C dialogs, and one for B-C dialogs. Exchanging the key value must be done using other keys, or through some out of band mechanism. Public key encryption involves making two keys, called a key pair. One of these keys is advertised for everybody to use in order to read your signature.
88
Java Thin-Client Programming
This is called the public key. The other key is used to generate the signature. This is called the private key. Both keys are data blocks, representing very large numbers. They must be generated at the same time by the same process. When you apply the public key to a message (the cleartext), you get another message called ciphertext, which to all intents and purposes, is gibberish. That message can only be deciphered by applying the private key. Additionally, the encryption process is reflexive. You can encrypt with the public key and decrypt with the private key, as well as vice versa. So public key cryptography can provide privacy as well as generate digital signatures. To send a secret message, you encrypt with the public key of the person or entity you wish to communicate with. To read the message, the recipient would decrypt using the private key. Public key cryptography has some obvious advantages over secret key cryptography. It is not necessary to generate so many keys, and the key distribution problem is (almost) eliminated since there is no need to distribute secret keys. A key pair is generated at any local host computer, as required. Entities can publish their public keys, through e-mail, a Web page, a database, corporate directories and so on. Only the key pair originator needs to know the secret key, as opposed to the situation with secret key cryptography, where both parties share the secret. However, secret key encryption has some advantages over public key encryption as well. Generating ciphertext is often faster, and the length of the ciphertext closely corresponds to the length of the cleartext. With public key cryptography, the length of the ciphertext may be very much longer. However, messages encrypted using algorithms such as RSA are subject to attacks using mathematical analysis. Although it may be prohibitively expensive to crack the private key, attacks can be mounted by examining carefully selected ciphertext or having a message encrypted with selected cleartext. Secret key encryption techniques such as Triple-DES (Data Encryption Standard) are fast and secure. Again, the major problem is key distribution. If someone can obtain the key, security is lost. For private communications, a combination of techniques is usually employed. RSA Data Security Inc. have developed a set of guidelines for using public key encryption, known as the Public Key Cryptography Standards (PKCS).
3.4.3 Using Digital Signatures In Java In this section we present an example of authenticated exchange using digital signatures. It is a client-server application where the client is an applet making an ordinary TCP connection to the server. When the server starts, it generates a key pair and listens for a TCP connection. When the applet starts
Security Considerations
89
it generates a key pair, connects to the server and transmits its public key. The server responds by transmitting its own public key. Public keys are exchanged over an ordinary link, with no encryption. This is relatively safe, because it is extremely difficult to deduce the private key from the public key. After the key exchange, a dialog is initiated by the client, followed by a server response, until the server decides to terminate the session. Every time the user transmits a block of data from the applet to the server, that block is signed using the client’s private key. The server process checks the signature before allowing the user at its end to respond. When the user hits the Enter key, the block is signed with the server’s own private key. At any time the server may disconnect, and the applet will inform the user by popping up a message box. Figure 35 illustrates the sequence of steps:
Figure 35. Authenticated Exchange Using Digital Signatures
With this sequence of events, both parties are assured that every transmitted block has been sent from the party with whom the connection was originally made. If the session is tapped, any attempt to impersonate either the client or server will fail when the signature of a fraudulent block is checked. A genuine
90
Java Thin-Client Programming
signature cannot be made by the spoofing process, because the private keys are in the address space of the remote client and server, and cannot be read. In the applet, the user types the messages to be transmitted into a text field. When he or she presses the Enter key, the input is transferred to a text area laid out underneath. This area is for display only. Incoming messages from the server are also copied to the text area. Every message going out is signed, and every message coming in is verified. Figure 36 shows how the applet appears during a dialog session:
Figure 36. Talker Applet in Browser Window
On the other side, the server creates a thread whenever a new connection is made, displaying a split window for the dialog. Actually this approach does
Security Considerations
91
not make sense since the server is a GUI application and only one thread can access the AWT event queue. However, it demonstrates the principles. Client messages are shown in the lower pane, and the user types into the upper text area. Figure 37 contains the window as it appears to the user at the server’s display:
Figure 37. Spit-pane Server Dialog Window
At the server, the user presses the Esc key to terminate a session. The client is rudely informed of session termination with the message box of Figure 38
Figure 38. Message Box Informing Client that the Session is Terminated
3.4.3.1 Package OurSecurity To make the exchange easier, we have created a package called OurSecurity that encapsulates the functionality for using digital signatures. It contains classes that can be serialized for transmission through object
92
Java Thin-Client Programming
streams. Figure 39 and Figure 40 contain the classes for serializable byte buffers and strings, respectively. package OurSecurity; import java.lang.*; import java.io.Serializable; public class ByteBuf implements Serializable { public byte[] bytes; public ByteBuf(byte[] bytes) { this.bytes = bytes; } } Figure 39. Serializable Byte Buffer
package OurSecurity; import java.lang.*; import java.io.Serializable; public class StringBuf implements Serializable { public String str; public StringBuf(String str) { this.str = str; } } Figure 40. Serializable String Buffer
Since the client and server also wish to exchange their public keys, we also need a serializable object transmit the key. We simply use another containing class as shown in Figure 41:
Security Considerations
93
package OurSecurity; import java.lang.*; import java.io.Serializable; import java.security.*; public class KeyBuf implements Serializable { public PublicKey key; public KeyBuf(PublicKey key) { this.key = key; } } Figure 41. Serializable Key Holder
Now we provide the support methods to generate key pairs, initialize, update and verify digital signatures. Class Util in package OurSecurity provides static methods to accomplish these purposes. There are three methods: Util.genKeys(), Util.genSig(), and Util.verifySig(). Firstly, let us examine the code for Util.genKeys(), shown in Figure 42: package OurSecurity; import java.lang.*; import java.security.*; import java.io.Serializable; public class Util { static public KeyPair genKeys() { try { KeyPairGenerator generator = KeyPairGenerator.getInstance("DSA"); // initialize from a self seeding secure random object // for better security the seed should be explicitly set generator.initialize(512, new SecureRandom()); return generator.generateKeyPair(); } catch (final Exception e) { System.out.println(e); // it would be better to throw the exception return null; } } // class Util definition incomplete Figure 42. Method OurSecurity.Util.genKeys()
94
Java Thin-Client Programming
First, we need an instance of class java.security.KeyGenerator, implementing a public key cryptography scheme. Currently, Java supports the Digital Signature Algorithm (DSA) , identified in the system provider database through the string DSA. We obtain an instance of a KeyGenerator with a call to the static method, KeyGenerator.getInstance(). Note the similarity with MessageDigest.getInstance(). Next we need to initialize the generator, by invoking initialize(), appropriately enough. We pass two values, the size of the modulus to use in generating the public key and an instance of a SecureRandom object. We have chosen a modulus of 512 bits, which is the minimum limit allowed by the DSA implementation. The maximum limit for the DSA implementation is 1024. Security is proportionate to the size of the modulus used to generate the keys. RSA is considered as safe as Triple-DES (using three keys of 56 bits each), when the public key size is 2880 bits. A key of this size is not necessary. PGP supports a public key size of 2047 bits, and this is considered absolutely secure in the presence of PKCS conformance. The KeyGenerator instance will use the SecureRandom instance to obtain the random numbers it requires to generate the key pair. Class java.security.SecureRandom provides better pseudo-random sequences than class java.util.Random, from which it is derived. It is necessary to generate numbers that are as random as possible, so that the key generation process cannot be reproduced. To provide the best sequence, you should seed the SecureRandom instance with a randomly generated twenty byte seed. When you do not specifically supply a seed, the instance seeds itself by measuring the exact times that threads awaken after calls to Thread.sleep(). Although the results are very good, it may be possible for analysts to determine non-random patterns if they know the architecture on which the SecureRandom instance was running. Now we generate a key pair using KeyGenerator.generateKeyPair(). If we wanted to, we could generate successive pairs with successive invocations. We then return the KeyPair to the caller, who can extract the public and private keys with calls to KeyPair.getPublic() and KeyPair.getPrivate(), respectively. KeyGenerator.getInstance() can throw a NoSuchAlgorithmException if the specified algorithm is unknown, or a NoSuchProviderException if no implementing class has been registered. Since a DSA implementation class is available with JDK1.1, no exception will be thrown. In a production class, it would be a better idea to propagate the exception back to the caller, rather than just catch and print it.
Security Considerations
95
Now that we have a key pair, we can use the private key to generate a signature for a block of data. To do this OurSecurity.Util.genSig() can be invoked. The source code appears in Figure 43: // class Util definition continues static public byte[] genSig(PrivateKey cryptKey, String msg) { try { Signature inst = Signature.getInstance("DSA"); inst.initSign(cryptKey); inst.update(msg.getBytes()); return inst.sign(); } catch (final Exception e) { System.out.println(e); // it would be better to throw the exception return null; } } // class Util definition incomplete Figure 43. Method OurSecurity.Util.genSig()
Util.genSig() method returns a digital signature as an array of bytes, consistent with method java.security.Signature.sign(). First, we must obtain an instance of class Signature, which we do by calling Signature.getInstance(), requesting a DSA implementation. We generate the signature by encrypting the message with a supplied PrivateKey instance, obtainable from the KeyPair returned by Util.genKeys(). To do this we call the variant of Signature.update() that takes an array of bytes as its argument. Finally, we call Signature.sign(), which pads the cleartext as required, and returns the array of bytes that represents the signature. Compare the procedure and API to what is used to generate message digests. The signature is not the encrypted text of the message itself. It is the ciphertext of the result of a hash function that is applied to the message. The signature also includes the hash type that was used. Finally, we need to verify a digital signature. We can do this with OurSecurity.Util.verifySig. Figure 44 contains the source code:
96
Java Thin-Client Programming
// class Util definition continues static public boolean verifySig(PublicKey decryptKey, String msg, byte[] sig) { try { Signature inst = Signature.getInstance("DSA"); inst.initVerify(decryptKey); inst.update(msg.getBytes()); return inst.verify(sig); } catch (final Exception e) { System.out.println(e); // it would be better to throw the exception return false; } } } // class Util definition complete Figure 44. Method OurSecurity.Util.verifySig()
As for generating of a signature, we first obtain an instance of Signature that implements DSA. This time, we must initialize for verification rather than signing, so we pass the public key whose matching private key was used to generate the signature. As before, we call Signature.update(), passing the received message we wish to validate. Finally, we call Signature.verify(), which returns true if the signature is authentic, and false otherwise. Remember that the update() method is actually applying a hash function. Method verify() decrypts the hash value and compares the result with the value computed from the data block. If these values match, the signature is authentic. Following sections contain the complete source code for the client, an applet, and the server. After every message, a digital signature is sent to be verified by the opposite party. 3.4.3.2 Source Code for the Talker Applet import import import import import import import import
java.lang.*; java.awt.*; java.awt.event.*; java.applet.*; java.net.*; java.io.*; java.security.*; OurSecurity.*;
Security Considerations
97
class ErrorPopup extends Frame { GridBagLayout layout; GridBagConstraints constraints; Label label; Button button; Component parent; public ErrorPopup(String text, Component parent) { super("Error"); this.parent = parent; layout = new GridBagLayout(); constraints = new GridBagConstraints(); constraints.insets = new Insets(3, 5, 5, 3); constraints.ipady = 3; label = new Label(text); button = new Button(" OK "); button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { dispose(); } }); setLayout(layout); layout.setConstraints(label, constraints); add(label); constraints.gridy = 1; layout.setConstraints(button, constraints); add(button); Dimension max = Toolkit.getDefaultToolkit().getScreenSize(); // display to an eight of screen height // and a sixth of screen width, centered setBounds(max.width / 2 - max.width / 12, max.height / 2 - max.height / 16, max.width / 6, max.height / 8); } public void setVisible(boolean b) { parent.setEnabled(!b); super.setVisible(b); if (b) Toolkit.getDefaultToolkit().beep(); } public void dispose() { parent.setEnabled(true); super.dispose();
98
Java Thin-Client Programming
} }
public class Talker extends Applet implements KeyListener { GridBagLayout layout; GridBagConstraints constraints; Label label; TextField input; TextArea dialog; String hostname; int port; Socket s; ObjectOutputStream outs; ObjectInputStream ins; KeyPair keys; PublicKey serverKey; // KeyListener methods public void keyPressed(KeyEvent k) {} public void keyTyped(KeyEvent k) {} public void keyReleased(KeyEvent k) { if (k.getKeyCode() == KeyEvent.VK_ENTER) { try { String msg = input.getText(); dialog.append("Input: "); dialog.append(msg + "\n"); input.setText(""); input.setEditable(false); // generate a signature byte[] sig = Util.genSig(keys.getPrivate(), msg); // send the message followed by the signature outs.writeObject(new StringBuf(msg)); outs.writeObject(new ByteBuf(sig)); // now read the response followed by the signature String s = ((StringBuf)ins.readObject()).str; byte[] responseSig = ((ByteBuf)ins.readObject()).bytes; // and check if the message is valid if (Util.verifySig(serverKey, s, responseSig)) { dialog.append("Response: "); dialog.append(s + "\n"); } else { new ErrorPopup("Security Alert - Bad Signature",
Security Considerations
99
this).setVisible(true); } } catch (final ClassNotFoundException cnfe) { // handle error condition } catch(final IOException ioe) { try { ins.close(); outs.close(); s.close(); } catch (final IOException ioe2) { } finally { new ErrorPopup("Connection Closed", this).setVisible(true); } } finally { input.setEditable(true); } } }
public void init() { // generate keys - in practice check for the error condition // i.e. a return value of null // or better - catch an exception keys = Util.genKeys(); layoutApplet(); hostname = getParameter("HOSTNAME"); port = Integer.parseInt(getParameter("PORTNUM")); try { s = new Socket(hostname, port); outs = new ObjectOutputStream(s.getOutputStream()); ins = new ObjectInputStream(s.getInputStream()); // now exchange keys // protocol is client submits key and server responds outs.writeObject(new KeyBuf(keys.getPublic())); serverKey = ((KeyBuf)ins.readObject()).key; } catch (final Exception e) { new ErrorPopup(e.getMessage(), this).setVisible(true); stop(); } } private void layoutApplet () { layout = new GridBagLayout(); constraints = new GridBagConstraints();
100
Java Thin-Client Programming
constraints.insets = new Insets(3, 5, 5, 3); constraints.ipady = 3; label = new Label("Type here:"); input = new TextField(35); dialog = new TextArea(8, 50); dialog.setEditable(false); input.addKeyListener(this); setLayout(layout); layout.setConstraints(label, constraints); add(label); constraints.gridx = 1; layout.setConstraints(input, constraints); add(input); constraints.gridy = 1; constraints.gridx = 0; constraints.gridwidth = GridBagConstraints.REMAINDER; layout.setConstraints(dialog, constraints); add(dialog); setSize(400, 400); } }
3.4.3.3 Source Code for the Talker Server import import import import import import import
java.lang.*; java.io.*; java.net.*; java.awt.*; java.awt.event.*; java.security.*; OurSecurity.*;
class Responder extends Frame implements Runnable { Socket sock; TextArea ta, textIn; int taIdx; ObjectInputStream ins; ObjectOutputStream outs; KeyPair keys; PublicKey clientKey; public class OurKeyAdapter extends KeyAdapter { public void keyReleased(KeyEvent k) { int code = k.getKeyCode(); if (code == KeyEvent.VK_ENTER) {
Security Considerations
101
sendSigned(new String(ta.getText().substring( taIdx, ta.getText().length() - 1))); // now get the response from the client getInput(); } else if (code == KeyEvent.VK_ESCAPE) { try { ins.close(); outs.close(); sock.close(); } catch (final IOException ioe) { } finally { dispose(); } } } }
public Responder(Socket s, KeyPair keys) { super("TalkerServer Dialog"); this.keys = keys; setLayout(new GridLayout(2, 1)); taIdx = 0; ta = new TextArea(6, 60); textIn = new TextArea(6, 60); textIn.setEditable(false); ta.addKeyListener(new OurKeyAdapter()); add(ta); add(textIn); setSize(400, 400); sock = s; try { ins = new ObjectInputStream(s.getInputStream()); outs = new ObjectOutputStream(s.getOutputStream()); // now exchange keys // protocol is client submits key and server responds clientKey = ((KeyBuf)ins.readObject()).key; outs.writeObject(new KeyBuf(keys.getPublic())); // enter the dialog cycle new Thread(this).start(); } catch (final ClassNotFoundException cnfe) { // handle error condition, etc // this doesn’t happen if a valid client is connected } catch (final IOException ioe) { dispose();
102
Java Thin-Client Programming
} }
public void run() { setVisible(true); getInput(); }
protected void getInput() { try { ta.setEditable(false); // read the response followed by the signature String s = ((StringBuf)ins.readObject()).str; byte[] responseSig = ((ByteBuf)ins.readObject()).bytes; // and check if the message is valid if (Util.verifySig(clientKey, s, responseSig)) { textIn.append(s + "\n"); } else { // log an error message to stderr // in practice - log tcp connection details System.err.println( "Security Alert - Bad Signature from client"); // disconnect rudely shutdown(); } } catch (final IOException ioe) { System.out.println(ioe); } catch (final ClassNotFoundException cnfe) { System.out.println(cnfe); } finally { ta.setEditable(true); taIdx = ta.getText().length(); ta.setCaretPosition(taIdx); } }
protected void sendSigned(String msg) { try { // generate a signature byte[] sig = Util.genSig(keys.getPrivate(), msg); // send the message followed by the signature outs.writeObject(new StringBuf(msg)); outs.writeObject(new ByteBuf(sig));
Security Considerations
103
} catch (final IOException ioe) { // log message and close window // in practice log tcp details should be logged System.err.println(ioe); shutdown(); } }
protected void shutdown() { try { ins.close(); outs.close(); sock.close(); } catch (final IOException ioe) { } finally { dispose(); } } } public class TalkerServer { public static void main(String argv[]) { if (argv.length != 1) { System.err.println("usage: java TalkerServer <port_number>"); return; } int port = Integer.parseInt(argv[0]); if (port < 1024) { System.out.println("Invalid port value: " + port); return; } ServerSocket s = null, accept = null; KeyPair keys = Util.genKeys(); try { s = new ServerSocket(port); while (true) { Socket ins = s.accept(); new Responder(ins, keys); } } catch(final Exception e) { System.out.println(e); } finally { try { if (s != null) s.close(); } catch (final IOException ioe) {
104
Java Thin-Client Programming
System.err.println(ioe); } } } }
3.4.4 Authentication Model Digital signatures are used as a building block to solve the problem of authentication. In the previous example, we saw how a dialog was carried out between two parties, where both parties were assured that their dialog could not be tampered with. However, because of the way that public keys were exchanged, neither party has the slightest idea who they are communicating with. It is an easy matter to generate key pairs and publish public keys, but unless you know their origin you cannot trust the content of messages. Authentication in Java is based around the idea of signed certificates, using the X.509 format of the standards body, CCITT, but it is possible to provide implementations that use other formats, such as the PGP format. This would be done in a similar way to providing other message digest or public key encryption algorithms. The most important standard for certificates is X.509. Unfortunately, Java support for X.509 is incomplete and inconsistent in JDK1.1. It includes the javakey.exe program to generate certificates and sign JAR files, however it is a fairly crude utility and still contains some bugs. With the release of 1.2, Java has taken some steps toward having a reliable authentication framework. Certificates are used to validate other certificates, using the real world model of chain of trust. Suppose you find some service you want, but it is offered by a service provider whom you do not know. Alternatively, suppose someone who is unknown to you wishes to initiate a dialog, inform you of something, sell you something, etc. It would be foolish to trust someone whom you know nothing about, but in the real world it is often possible to check his or her references. This is done either by verifying the unknown entity’s credentials through a mutual acquaintance or through a trusted authority. If this is not directly feasible, it may be possible using a chain of references. Using a chain of trust is very natural in day to day life, and the concepts are exactly the same in the Java authentication model. Just as in other mundane aspects of life, trust in the world of distributed computing systems is a complicated personal business. No framework can ever guarantee security. You may know and trust A and she may know and trust B, however it may turn out that A or B or both A and B are not trustworthy. Possibly, B was once a pillar of virtue, but then he lost everything in the crash of 98 and has taken
Security Considerations
105
to purse snatching. With such considerations in mind, certificates have expiry dates. In the authentication model, the burden of trust is placed on the user, where it belongs. The framework can only provide guarantees about identity and pedigree. 3.4.4.1 Certification Authorities There are certain organizations working toward becoming certification authorities. Included among these organizations is the US Postal Service, and the corporation, Verisign, Inc. Verisign is rather like a credit agency. A credit agency’s function is to assign ratings to potential borrowers, indicating the relative risk to potential creditors. Verisign generates certificates for entities of various types, for example, software developers or just users of e-mail. The certificate has a ranking that Verisign calls a class. Anyone can obtain a class one certificate for less than $US10 per year, but this only guarantees that the e-mail address is valid. For less than $US20 per year, you can obtain a class 2 certificate. It verifies that the name is actually correct. You can order such a certificate from the Verisign Web site at: http://www.verisign.com/.
3.4.4.2 Principals, Identities, Certificates A principal is a real world entity; a person or organization. In the java.security package, the Principal interface represents such an entity. An identity is a principal who has a public key, and is represented by class Identity, which implements Principal. Identities can be placed into a context, represented by class IdentityScope, and scopes can be placed within other scopes. For example, we could have identity John Jones, in scope ITSO, in scope IBM. In this way, identity scopes model real world organizations. An identity is resolved by scope, and a scope cannot contain duplicated identities. There is naturally a starting point, the system identity scope. It is available to the programmer using the static method, IdentityScope.getSystemScope(). We have already seen public and private keys. A certificate is represented by the interface Certificate. The certificate contains an identity, the entity being certified, and a guarantor, the entity doing the certification. It also contains the public key of the identity. The certificate is signed by the guarantor and its fingerprint can be calculated using the MD5 message digest algorithm. 106
Java Thin-Client Programming
A chain of trust is established by validating certificates with other certificates. Say that someone has a certificate that not validated. (Actually it will be self-validated, that is, the identity and guarantor are the same entity.) Another certificate may be made by a third party. The third party creates the new certificate by signing the certificate of the original identity with their private key. Doing so implies that the guarantor has verified that the first identity is indeed who he or she claims to be, rather than some other entity entirely. As stated earlier, a guarantee of identity is not necessarily a guarantee of trust. If you are ever called upon to sign a certificate, you must ensure that you know the entity, that you know the entity’s public key, and that the public key is correct. At this point there are no classes in the java.security package providing implementations for certificates. Sun have promised to include one in Version 1.2. There is a private implementation of the X.509 format in the sun.security.x509.X509Cert package, and that package is used by the HotJava browser. 3.4.4.3 X.509 Certificates CCITT have a series of standards aimed at specifying a directory service. They are know as the X.500 standards, of which the X.509 certificate format is a part. X.500 standards are written using Abstract Syntax Notation 1 (ASN1), which is based on the Backus-Naur Form (BNF) . An X.509 certificate consists of the following elements. • Certified entity identifier; • Public key of the certified entity, including the algorithm identifier, such as DSA, and the parameters, such as length of the modulus; • Certificate version number (X.509 is subject to revisions); • Serial number of the certificate; • Period of validity, that is, begin and end dates; • Signer (guarantor) identifier; • Signature algorithm identifier, such as DSA, including parameters such as hash function, and so on; • Signature. The signature is the digital signature obtained by hashing the contents of every other component and encrypting the result with the private key of the signer. Note that terminology of X.509 differs slightly from the terminology employed in package java.security. In particular, guarantors are referred to as signers.
Security Considerations
107
3.4.5 Signed Applets One of the primary uses of certificates is for code signing. When a distribution comes with a certificate, you can examine the certificate, determine if it is from a party that you trust and that it was signed with a signature that you trust. How do you know if you can trust the signature? There are two ways. If you know the public key of the signer you can verify the certificate with the appropriate algorithm, usually DSA. If you do not know the signer’s public key, you can contact the signer through an out of band means to ensure that the fingerprint of the signature is as it should be. The HotJava browser calculates the fingerprint using MD5. Java 1.1 provides a utility called javakey.exe that is used to update the system security database. You can use javakey.exe to sign applets. Unfortunately, at the time of writing, only Sun’s Hot Java browser is able to take advantage of signed applets, and it still has some problems. It is possible to use signed applets with Internet Explorer and Netscape Communicator through the Java plug-in, but even the examples in the JavaSoft documentation do work reliably. Both Microsoft and Netscape have beta versions of their browsers that recognize signed applets. By default, the system security database is kept in a file called identitydb.obj in the root directory on the drive where the JDK is installed. You can change this location by editing the file, java.security. For the JDK Version 1.1.6 this file is located in the directory: /jdk1.1.6/lib/security. This file is a configuration file for system security, and provider classes can be registered here. To change the default location of the system database, add the following line: identity.database=c:\identitydb.obj This is also the file that javakey.exe uses to determine the location of the security database. The security database itself contains the entities, signers, keys, and certificates. In practice, it is a good idea to protect the database file as closely as possible. Anyone who has access to the database has access to the private keys of its entities, and could forge certificates. 3.4.5.1 Making a Trusted Applet Consider the Talker applet of Section 3.4.3.2. One of the things that an applet is not permitted to do is to make a TCP connection to any node other than that from which it was loaded. Text for the HTML file that includes an appropriate applet tag is shown in Figure 45. Applet parameter HOSTNAME contains the address, jf0151c.itsc.austin.ibm.com. If this is not the address of the Web server which loaded the applet, the TCP connection will not be allowed by the applet security manager.
108
Java Thin-Client Programming
Talker Test page for Talker Class
Figure 45. HTML Source for the Talker Applet
Now suppose the author of the Web page wants the client to connect to a server running on a different host. This is possible with HotJava if the applet has been signed and the signer is trusted on the client. Firstly, we cover the simple case, where the applet signer is implicitly trusted by the host where the applet will execute. The first thing to do is sign the applet. On the development machine, you can use javakey.exe to create a signer, generate keys to sign certificates, generate a certificate, and finally attach the certificate to the applet distribution. The utility has a number of commands, and in this document we use the subset that we require. For more detail, consult the JDK1.1 documentation. To create a signer, use a command similar to the following: c:\> javakey -cs Mark true
Option -cs stands for create signer. Mark is the signer’s name, and the optional word, true, signifies that Mark is a trusted entity. javakey.exe responds with the message: Created identity [Signer]Mark[identitydb.obj][trusted]
You can also specify information about the new signer, if you wish. Next we need to generate a key pair, with the following command: c:\> javakey -gk Mark DSA 512
This generates a key pair for the signer Mark using a DSA implementation with a modulus of 512 bits. javakey.exe uses the java.security classes, so DSA is the only implementation that is available to it. The response is: Generated DSA keys for Mark (strength: 512).
Security Considerations
109
You can use the command, javakey -l, to see a brief listing of all entities, or javakey -li Mark for more details on the identity, Mark. Now we want to generate a certificate using the -gc option. Several input parameters are necessary for a certificate, and javakey.exe reads the input from a file, called a directive file. We create a file called cert.dir, whose contents are shown in Figure 46, and issue the command: C:\> javakey -gc cert.dir
and get the response: Generated certificate from directive file cert.dir. issuer.name=Mark subject.name=Mark subject.real.name=Mark Briggs subject.country=USA subject.org=IBM subject.org.unit=ITSO start.date=26 Aug 1998 end.date=31 Dec 1998 serial.number=0001 out.file=mark.x509 Figure 46. Directives in the File cert.dir
Notice that the directives correspond to the elements in a certificate, however the terminology is not consistent with that of X.509. The issuer corresponds to the signer, and the subject corresponds to the identity being certified. Both parameters are qualified by name, which is simply their designation in the security database. Note that this certificate is self-signed. We can add details about the subject with the various qualifications you can see in the figure. Start and end dates specify the range. Serial number is an identifier for the certificate, and it must be unique among every certificate created by the issuer. Finally the optional directive out.file specifies an output file for the certificate. The certificate will be added to the database and you can retrieve information about it later. You can also export the certificate to a file later by using the -ec option. We have called the output file mark.x509 because it contains an X.509 certificate. If you are ever asked to verify the fingerprint of
110
Java Thin-Client Programming
one of your certificates by a user of HotJava, you must calculate the message digest of the output file using the MD5 algorithm. You may want to look at the details of the certificate. Use the command: C:\> javakey -dc mark.509
Figure 47 contains an example of the output. You can look at details, including the public key of the subject and the digital signature. As you may notice, the expiry date is incorrect, because javakey.exe does not correctly convert to and from Universal Time: [ X.509v1 certificate, Subject is CN=Mark Briggs, OU=ITSO, O=IBM, C=USA Key: Sun DSA Public Key parameters: p: fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6 df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e17 q: 962eddcc369cba8ebb260ee6b6a126d9346e38c5 g: 678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b 71fd73d a179069b32e2935630e1c2062354d0da20a6c416e50be794ca4 y: db374fbee2463c316af13855f748b789a763f2be919c20538d550c093f4e228477c733 e52a217 ab7a354c45d4b31a491949f4a7949edb265700f07229dd6c4b1 Validity until <Wed Dec 30 18:00:00 CST 1998> Issuer is CN=Mark Briggs, OU=ITSO, O=IBM, C=USA Issuer signature used [SHA1withDSA] Serial number = 01 ]
Figure 47. Output Obtained by Using javakey to Display a Certificate
Now we have a certificate we can use to sign the applet. Assume that we have created a jar archive called talker.jar. We need to create another directive file in order to have javakey.exe generate a signature. Figure 48 shows the contents of that file, which we will call sig.dir.
signer=Mark cert=1 chain=0 signature.file=SIG Figure 48. Directives for Signing an Archive
There are four directives here, but we could have specified the output file with the directive, out.file=. First, we have the identifier or name of the signer, in this case Mark. Since a signer may have many certificates, it is necessary to supply the number of the certificate to use when signing the
Security Considerations
111
archive. Identity Mark has only one certificate. The chain directive is intended to specify the length of a chain of certificates that will be included. However, chaining is not supported yet. javakey.exe expects the value to be zero, but the directive is ignored. Finally the directive, signature.file, specifies the filename and block for the signature within the archive. It is allowable to sign an archive with more than one signature, so if you are signing a signed archive, the name cannot conflict with the existing signature. It is not particularly important what you specify for a name. Signatures are written to the META-INF directory in the archive, with a pair of entries. Now we can issue the following command: C:> javakey -gs sig.dir talker.jar
Figure 49 shows the resulting output. Because we did not specify an output file, the signed archive is written to the file, talker.jar.sig . Adding entry: META-INF/MANIFEST.MF Creating entry: META-INF\SIG.SF Creating entry: META-INF\SIG.DSA Adding entry: Talker.class Adding entry: ErrorPopup.class Adding entry: ErrorPopup$1.class Adding entry: OurSecurity/ByteBuf.class Adding entry: OurSecurity/KeyBuf.class Adding entry: OurSecurity/StringBuf.class Adding entry: OurSecurity/Util.class Signed JAR file talker.jar using directive file sig.dir. Figure 49. javakey Output Generated by Signing the Talker Archive
So a signed archive exists on the development machine, and we simplicity we will assume that the Web server where the archive resides is the same machine. However, we want to ensure that the applet is trusted on a remote machine, so it can make a connection to another host where the server is deployed. We need to copy the file, mark.x509, to the client machine. Recall that file contains the certificate used to sign the applet. Now we need to create the entity Mark on the client with the command: C:> javakey -c Mark true
and import the certificate with the command:
112
Java Thin-Client Programming
C:> javakey -ic Mark mark.x509
Mark is now a trusted entity on the client machine. If you start HotJava on the connected NC or the host itself, you can connect to a server running on a host other than the one from which the applet was loaded, demonstrating that the applet is trusted. In fact, the applet can do anything that an application could do with no security manager installed, because the applet signer, Mark, is trusted in the client’s security database. Figure 50 shows the HTML source to load the signed Talker applet from a machine other than host, jf0151c.itsc.austin.ibm.com, as specified by the HOSTNAME parameter. Provided a server is listening on that host (at port 12121), a TCP connection will be permitted, if: • The signer is a trusted entity at the client host, and, • The client security database contains a copy of the certificate used to do the signing. <meta name="generator" content="ModelWorks IDE"> Talker Test page for Talker Class
Figure 50. HTML File for the Signed Talker Applet
3.4.5.2 The Realistic Approach If a client had to create an entity in its database and import certificates for every signed archive that he or she may receive, the situation would be completely unworkable. Security is not a binary situation, trusted or untrusted. So the example in the previous section is unrealistic, and it is intended only to demonstrate some of the principles involved in authentication. As stated earlier, only one browser, Sun’s HotJava, has support for signed applets at the time of writing. If you load an applet that has been signed by an
Security Considerations
113
unknown or untrusted entity, HotJava will not trust it any more than it trusts an ordinary applet. It must be stressed again that the level of trust is a matter for the user to decide. Whenever you receive a signed applet, HotJava remembers the certificate and allows the user to make decisions about how to proceed. Certificates may be self signed, as in the previous example, but more frequently a certificate verifies the public key of an entity, as signed by another entity. To continue the example of the previous section, we will generate another certificate for the talker applet. First we need to create the identity, AnIdentity, with the command: C:> javakey -c AnIdentity
and a signer with the command: C:> javakey -cs ASigner
The only difference from the previous examples is that the identity and signer are not trusted. There is no need for the signer to be trusted. Also, there is a bug in javakey.exe, and a ClassCastException is often generated when you try to sign some certificate with the certificate of a trusted entity. Signer ASigner, whoever he or she may be, needs to import a certificate of AnIdentity, which can be obtained through e-mail, the Web, or some other source. ASigner must have already created a certificate of their own, to be used to certify that of AnIdentity. You have seen how to do this in the preceding example. Of course, ASigner should be careful to verify the fingerprint of the imported certificate. Now ASigner is in a position to sign the certificate, with the command: C:> javakey -gc cert_id.dir
where the contents of the file cert_id.dir are as shown in Figure 51:
114
Java Thin-Client Programming
issuer.name=ASigner issuer.cert=1 subject.name=AnIdentity subject.real.name=Someone Unknown subject.org.unit=ITSO subject.org=IBM subject.country=USA start.date=1 Sep 1998 end.date=31 Dec 1998 serial.number=10001 Figure 51. Directive File for Signing a Certificate of Another Identity
Because this is not a self-signed certificate, an additional directive, issuer.cert, is required. It is simply the number of ASigner’s certificate that will be used to do the signing. When the new certificate is created, it is added to the database, and AnIdentity will have two certificates, the original self-signed certificate, imported from the development host, and the new one. ASigner can export the certificate (or add an out.file directive), and AnIdentity can use it to sign archives at his or her host. The directive file to do so would be similar to that shown in Figure 52: signer=AnIdentity cert=2 chain=0 signature.file=SIG Figure 52. Directive File to Sign an Archive with a Second Certificate
3.4.5.3 HotJava and Signed Applets In this section we will briefly see how to use HotJava to give an applet some permission beyond what is allowed under the sandbox model. Assume we have a self-signed archive, signed by ASigner with the real name, It’s Me. It is the by-now familiar Talker applet. We load the HTML file with applet parameter HOSTNAME specifying a host other than that from where the applet was served, that is, the Web server host. This action is not allowed for an applet so we the applet pops up a message box as shown in Figure 53:
Security Considerations
115
Figure 53. Signed Applet Error Message
If we examine the HotJava console we will see the exception chain. It is shown in Figure 54:
Figure 54. Console Containing Applet Security Exception Chain
However, this is a signed applet. To view the basic settings for signed applets, pull down the Edit menu and choose Preferences->Applet Security, this is shown in Figure 55:
116
Java Thin-Client Programming
Figure 55. HotJava Basic Applet Security Page
There are four basic security settings for signed applets as set out below: Untrusted
Applets are not allowed to run.
High Security
A sandbox security manager guards against the usual unsafe actions.
Medium Security
HotJava prompts you with a dialog box that informs you of any attempted actions that may be unsafe, and allows them to proceed only if the user permits them.
Security Considerations
117
Low Security
Applets can run with no security management. We saw an example of this with an applet signed by a trusted entity.
These settings are for applets in general. However, before HotJava elevates a signed applet to medium security, you must verify the fingerprint of the certificate with which it was signed. To do this, click the button labelled Advance, and when the advanced settings window appears, click the button labelled Details. A dialog appears as shown in Figure 56. Details of the certificate are shown, including the fingerprint, which, as stated earlier, is the MD5 message digest of the signature. When you have verified the fingerprint by contacting the certificate’s owner, check the box labelled Fingerprint verified and click the OK button.
Figure 56. Dialog to Verify a Certificate’s Fingerprint
Note that if a certificate has been accepted and that certificate has been used to sign another certificate, this dialog would not appear. In that case, there is transient relationship, the chain of trust. When certificate A is trusted as valid, and it is used to certify certificate B, then certificate B must also be valid. We have determined that the applet has been signed with a valid certificate. The advanced applet security page now looks similar to that shown in Figure 57:
118
Java Thin-Client Programming
Figure 57. HotJava Advanced Security Settings Page
You need to select the appropriate certificate from the list, and check the radio box for the type of access that you wish to give. In our example, we are going to allow TCP connections to different hosts, but we will still be warned and given a chance to approve the action. This scheme corresponds to HotJava’s basic medium security setting. As you can see, you can customize the behavior for individual certificates.
Security Considerations
119
When this applet tries to connect to another host, the security manager intervenes and displays the dialog of Figure 58. It is up to the user to decide whether to let the connection proceed, or have the connection blocked.
Figure 58. HotJava Dialog Requests Confirmation of a Potentially Unsafe Action
120
Java Thin-Client Programming
Chapter 4. Using CORBA on the Network Computer This chapter provides an introduction to using CORBA (the Common Object Request Broker Architecture) in the Network Computer (NC) environment. CORBA is used to develop distributed, object oriented applications in a manner similar to the standard Java Remote Method Invocation (RMI) mechanism, but unlike RMI, CORBA allows the individual components of your application to be written in different programming languages. You can still use Java, but you can also use many other programming languages including C, C++, Smalltalk, COBOL, and Ada. The material covered in this chapter assumes that you are familar with the basic concepts of developing distributed (client/server) applications, and that you have some working knowledge of Java RMI.
4.1 What is CORBA? CORBA is actually just the specification of an infrastructure for developing distributed, object-oriented applications, and it is produced by a consortium of over 700 companies collectively known as the Object Management Group (OMG - http://www.omg.org). The OMG membership includes practically all of the major vendors including IBM, HP, Inprise (formerly Borland) and Microsoft, and it is therefore no surprise that CORBA is rapidly becoming the de-facto standard for distributed systems development. It is important to remember that the OMG does not actually produce any software itself, and instead it is left up to the individual members to produce implementations of the OMG’s specifications. Major commercial implementations of CORBA are available from IBM (http://www.ibm.com), Inprise (http://www.inprise.com), and Iona (http://www.iona.com), and there are also numerous free implementations including ORBacus from Object Oriented Concepts Inc (http://www.ooc.com/ob). We will be using ORBacus for the examples shown in this chapter, because it is freely available for non-commercial use.
4.1.1 The Object Management Architecture (OMA) At an abstract level, the CORBA specification describes a reference architecture known as the Object Management Architecture (OMA). The OMA has 5 main components as shown in Figure 59 on page 122:
© Copyright IBM Corp. 1999
121
Figure 59. The Object Management Architecture (OMA)
• The Object Request Broker (the ORB in CORBA) is the backbone of the OMA. It allows objects to communicate despite differences in location, platform, and implementation language. The ORB is covered in more detail in section 4.1.2, “The CORBA ORB Architecture” on page 123. • Application Interfaces are interfaces that are designed and implemented by developers outside of the OMG. While technically not a part of the OMG standardization activity, they still play an important role in the overall architecture as they provide a basis and an opportunity for component based software development. • Object Services provide fundamental facilities that are required by almost every distributed application. They include the Life-cycle Service for controlling object creation and destruction, the naming service for locating objects by name, and the Trader Service for locating objects by their properties. There is also a Security Service, an Event Service, a Policy Service - and the list goes on. For more information and an up-to-date list of services see the CORBA specification on the OMG Web site (http://www.omg.org). Once again the OMG does not produce implementations of any of the object services, and the set of services that are provided with each ORB varies from vendor to vendor.
122
Java Thin-Client Programming
• Common Facilities are similar to Object Services, although they are aimed more toward end-user applications. Examples include printing, database, document management and e-mail facilities. • Domain Interfaces, as the name suggests, define interfaces that are of interest to users in specific domains e.g. Banking, Telecommunications, etc. As mentioned previously, the best (and in fact the only) way to keep up to date with the currently adopted domain interfaces is to check the OMG Web site.
4.1.2 The CORBA ORB Architecture At the heart of the OMA is the ORB itself, and the ORB architecture is shown in detail in Figure 60:
Figure 60. The CORBA ORB Architecture
The components of the architecture are as follows: • The Object Implementation is the implementation of an Application Interface (see section 4.1.1, “The Object Management Architecture (OMA)” on page 121). The implementation can be written in any of the programming languages allowed by the CORBA specification and the particular ORB that you are using. In traditional client/server terminology,
Using CORBA on the Network Computer
123
object implementations are the servers, and we will use both terms interchangeably. • The Client is an entity that wishes to invoke an operation on an object implementation or server. One of the goals of CORBA is to try to make the invocation as simple as calling a method on a local object. • The IDL (Interface Definition Language) Stubs and Skeletons provide the glue that connects the client and server via the ORB. They are generated automatically by a tool called an IDL compiler that is provided by the ORB vendor (see 4.2.5, “Generating the Stubs and Skeletons” on page 135 for an example of using the ORBacus for Java IDL compiler). • The ORB Interface is used by both clients and servers to provide a standard way to communicate with the ORB irrespective of the ORB vendor. • The DII (Dynamic Invocation Interface) and DSI (Dynamic Skeleton Interface) allow the dynamic creation of operation requests without the need for compiled stubs and skeletons. They are an advanced CORBA topic, and we will not be discussing them further here. • The Object Adapter is used to connect servers to the ORB to allow it to deliver operation requests to the right place. Servers also use the object adapter to let the ORB know when they are willing to accept requests. • GIOP/IIOP (General Inter-ORB Protocol/Internet Inter-ORB Protocol) is the protocol used on-the-wire in CORBA. In theory, it allows objects in ORBs from any vendor to interoperate, although in practise we are still a little way from reaching this ideal state.
4.1.3 CORBA versus Java RMI As mentioned in the introduction to this chapter, one of the main advantages of CORBA over JAVA RMI is that it allows you to build your application using components that are written in different programming languages. This is not only useful for integrating legacy systems with new Java components, but also allows you to choose the programming language appropriate for the task at hand. As you would expect, this additional flexibility comes at the expense of a little extra work on the behalf of the developer. The languages currently adopted by the OMG include C, C++, Java, Smalltalk, COBOL, and Ada, with many others on the way, including scripting languages like Python, Perl and TCL. RMI is only concerned with Java to Java communication and therefore it makes sense that the interface between the client and server is also described in Java. All that the programmer has to do is provide a standard
124
Java Thin-Client Programming
Java interface definition, and the RMI stubs and skeletons are generated by the RMI compiler rmic.exe. In the CORBA world, life is a little more complicated. The client and the server can (and quite probably will) be written in different programming languages, and so the OMG have defined a new language called IDL (Interface Definition Language) that allows us to describe interfaces in a programming language independent way. In the same way as rmic.exe produces Java stubs and skeletons from Java interfaces, so each ORB vendor provides a tool known as an IDL compiler that translates IDL definitions into stubs and skeletons in the target programming language (the actual name of the IDL compiler is not mandated by the CORBA specification, and varies from vendor to vendor - in ORBacus for Java it is simply jidl.exe). Figure 61 shows the process of stub and skeleton generation in both CORBA and Java RMI: .
Figure 61. Stub and Skeleton Generation in CORBA and Java RMI
IDL is, by necessity, a reasonably complex language, and we do not propose to describe it in detail here. Similarly, we will not attempt to describe the mapping from IDL into either of the languages that we will be using in our
Using CORBA on the Network Computer
125
examples (Java and C++). For detailed information on both of these topics please consult the OMG Web site (http://www.omg.org). Note
IDL is not a programming language - it is only used to describe interfaces, not to implement them.
4.2 A Simple Example As dictated by tradition, we will start with the old faithful "Hello World" application. In this case it will (hopefully) be a bit more interesting than usual, as we will be building it as a CORBA system with the client running on the NC (as both an application and an applet) and the server running on the NC’s host as shown in Figure 62:
Figure 62. The Structure of the "Hello World" Application
Furthermore, to demonstrate the multi-lingual capabilities of CORBA, we will implement the system in two ways: 1. With the client and server both written in Java. 2. With the client written in Java, and the server written in C++ (although we provide the complete C++ source code for the server, we will not discuss its implementation here, and instead we will focus on the NC and Java aspects of the application).
4.2.1 Installing the ORB for NC Applications Before we start looking at the actual development of the client and server code, we will first download and install ORBacus for Java such that it is easily accessible from the NC. At the time of writing the latest version of ORBacus for Java is 3.0.1, so be aware that by the time you are reading this, some of
126
Java Thin-Client Programming
the steps describing the download and installation maybe out of date. For the definitive instructions see the Web page at http://www.ooc.com/ob. What you will need
To build ORBacus for Java you will need access to the ’nmake utility that comes with Microsoft Visual C++. Failing that you can try any other make variant, but be aware that you will have to manually edit the make files. The steps required to download ORBacus for Java are as follows: 1. Go to the Object Oriented Concepts Inc. homepage: http://www.ooc.com 2. Follow the link to the ORBacus page. 3. Follow the link to the Download page. 4. Download the following files: • The source code for ORBacus for Java:
JOB-3.0.1.zip • The precompiled IDL preprocessor and compiler:
jidl-3.0.1-win32.zip • The documentation (includes information for both Java and C++ in PDF format):
OB-3.0.pdf.zip First of all we need to consider where we are going to install the ORB classes. Obviously, we cannot store them on our NC as there is no permanent storage space, and so we must put them on a part of the host’s file system that is accessible from the NC. In our configuration, the directory C:\nstation on the NC’s host is mounted (or mapped) as /netstation in the NC’s file system. Bearing in mind that your configuration may be different, the steps to install and build ORBacus are as follows: 1. Create the directory: C:\nstation\prodbase\jalapeno 2. Unzip the source code (JOB-3.0.1.zip) into the directory created in step 1. 3. Unzip the IDL preprocessor and compiler (jidl-3.0.1-win32.zip) into a directory that is on your PATH (this is only required for development and neither the pre-processor or the IDL compiler are required at runtime). 4. Unzip the documentation ( OB-3.0.pdf.zip) and read it. 5. Read the build instructions in the file: C:\nstation\prodbase\jalapeno\JOB-3.0.1\INSTALL
Using CORBA on the Network Computer
127
Assuming that you built ORBacus successfully, the packages and class files that implement the ORB are now in the directory C:\nstation\prodbase\jalapeno\JOB-3.0.1\ob\lib. When we configure our application using the IBM Network Station Manager (see section 4.2.6, “Installing and Testing the "Hello World" Application” on page 135), we add this directory to the Java CLASSPATH so that our client on the NC can find and load the ORB classes. JAR Files
The packages and classes for ORBacus for Java can be installed as a single Java archive (jar) file. We chose not to install it in this way simply to avoid compatability issues with different Web browsers and Java Virtual Machine implementations. Now that we have installed the ORB, let’s move on and see how we go about building the "Hello World" application and getting it up and running in the NC environment.
4.2.2 The IDL Interface Definition Figure 63 on page 128 shows the IDL for the "Hello World" example (contained in the file ...\com\ibm\austin\itsc\jalapeno\corba\Example.idl ). As you can see it is reasonably trivial as we have a single interface containing a single operation. However, it is worth noting the use of the #pragma prefix directive, and the definition of the Example module that contains the HelloWorld interface. These mechanisms ensure that we do not pollute the global namespace and should be adopted by every good CORBA citizen, see the Interface Repository section in the CORBA specifications. #pragma prefix "jalapeno.itsc.austin.ibm.com" module Example { interface HelloWorld { // The IDL version of the old faithful, "Hello World!" example. // This operation takes no parameters and returns a string. string hello_world(); }; }; Figure 63. IDL for the "Hello World" Example
128
Java Thin-Client Programming
4.2.3 Writing the "Hello World" Client Initially, we will build the client as an application (as opposed to an applet) with the simple AWT GUI shown in Figure 64. The entire code for the client is found in the file ...\com\ibm\austin\itsc\jalapeno\corba\simple\Client.java .
Figure 64. "Hello World" Client Application
In typical Java fashion, we start by importing the required classes, and all Java CORBA programs (regardless of the ORB vendor) must import the classes found in the package org.omg.CORBA. This package is provided by the ORB vendor and contains the classes that implement the actual ORB itself. The client also needs to import the stub classes generated by the IDL compiler (see section 4.2.5, “Generating the Stubs and Skeletons” on page 135) from the package com.ibm.austin.itsc.jalapeno.corba.Example. package com.ibm.austin.itsc.jalapeno.corba.simple; // Java. import java.awt.*; import java.awt.event.*; import java.io.*; // CORBA. import org.omg.CORBA.*; // Jalapeno utilities. import com.ibm.austin.itsc.jalapeno.util.*; // Stubs generated by the IDL compiler. import com.ibm.austin.itsc.jalapeno.corba.Example.*;
The first thing that every CORBA client must do is initialize the ORB. This is achieved by calling the static method ORB.init() found in the ORB class.
Using CORBA on the Network Computer
129
ORB.init() can be called as many times as you like and always returns a reference to the currently initialized ORB. public Client(String[] args) { // Initialize the ORB. ORB orb = ORB.init(args, new java.util.Properties()); ... ...
Having initialized the ORB, the client must now locate the server object which in the CORBA world is stringiness achieved using an object reference. Object References
Object references can be thought of as the distributed version of Java references or C++ instance pointers. However, instead of containing a simple memory address, they contain information to locate a server object even if it is running in a different process on a different machine. On the client-side, object references are opaque, and you do not need (or want) to know what is inside them. To allow object references to be transmitted easily and to be stored in databases, CORBA defines a way to convert them to and from a standardized string representation. In our case when the server starts up it will write this stringified version of its object reference to the file Simple.ref, and we will pass the name of this file to the client on the command line (passing the name of the file as a parameter rather than hard-coding it makes things a little easier when we come to implement the client as an applet in section 4.2.10, “Writing the "Hello World" Client Applet” on page 138). Obviously, this is not an ideal solution as it requires that the client and server share a common file system, and in section 4.3, “A CORBA Naming Service Example” on page 140 we will see a more robust and scalable solution. // The server’s (stringified) object reference is read from a file, the // name of which is passed as the first argument on the command line. if (args.length != 1) { System.err.println("Usage: Filename"); System.exit(1); } String stringified_ior = null; try { FileInputStream file = new FileInputStream(args[0]);
130
Java Thin-Client Programming
BufferedReader in = new BufferedReader(new InputStreamReader(file)); stringified_ior = in.readLine(); file.close(); } catch(IOException ex) { System.err.println("Can’t read from: " + ex.getMessage()); System.exit(1); }
Unfortunately, the stringified version of the object reference is not much use other than for passing around to your friends via e-mail, and writing to databases, so the client’s next call is to the ORB method string_to_object(). This method creates a stub (also called proxy) object that has the same methods as the server object. When methods are invoked on the stub, it forwards the request onto the server object in whatever process, on whatever machine it happens to be running at the time. In this way the client is insulated from the details of where the server object is physically located, and how operation requests are delivered to it. // Convert the stringified object reference into a ’live’ object reference. org.omg.CORBA.Object obj = orb.string_to_object(stringified_ior); if(obj == null) { System.err.println("NULL object reference."); System.exit(1); }
Before we can use the stub we have to make sure that it is of the correct type. This is achieved by calling the narrow () method that is found on the helper class that the IDL compiler generates for every IDL interface. Narrowing an object reference is analogous to casting a Java reference or C++ instance pointer, and it allows us to access the methods specific to the interface that we are narrowing (casting) to. If the object reference does not refer to an object that implements the appropriate interface, the narrow method will return null. // Narrow the object reference to make sure that it is of the correct type. server = HelloWorldHelper.narrow(obj); if(server == null) { System.err.println("Object does not implement the correct interface."); System.exit(1); }
In this example, the client calls the server whenever the Push Me button is pressed. Note that at this level, the method itself looks the same as if the
Using CORBA on the Network Computer
131
server was just another local Java instance, with some added exception handling: public void actionPerformed(ActionEvent event) { try { // Call the server. System.out.println(server.hello_world()); } catch(SystemException ex) { System.err.println("Error calling the server."); System.exit(1); } }
4.2.4 Writing the "Hello World" Server First of all we need to write a class that actually implements the HelloWorld interface as described in the IDL file. The code for this class is found in the file ...\com\ibm\austin\itsc\jalapeno\corba\HelloWorld_impl.java package com.ibm.austin.itsc.jalapeno.corba; // Skeletons generated by the IDL compiler. import com.ibm.austin.itsc.jalapeno.corba.Example.*; public class HelloWorld_impl extends _HelloWorldImplBase { public String hello_world() { System.out.println("Hello CORBA World!"); return "Hello CORBA World!"; } }
Notice that the class HelloWorld_impl inherits from the skeleton class _HelloWorldImplBase that was generated for us by the IDL compiler (this is the CORBA equivalent of inheriting from a class such as UnicastRemoteObject in RMI). Apart from that, you can see that the implementation looks suspiciously like a normal non-CORBA Java class. Now let’s take a look at the server application that will create an instance of our implementation class. The code for the server is found in the file ...\com\ibm\austin\itsc\jalapeno\corba\simple\Server.java .
132
Java Thin-Client Programming
As mentioned in section 4.2.3, “Writing the "Hello World" Client” on page 129, all CORBA programs, both clients and servers, must import the classes in the org.omg.CORBA package. The server also imports the skeleton classes generated by the IDL compiler in the package com.ibm.austin.itsc.jalapeno.corba.Example. package com.ibm.austin.itsc.jalapeno.corba.simple; // Java. import java.io.*; // CORBA. import org.omg.CORBA.*; // Skeletons generated by the IDL compiler. import com.ibm.austin.itsc.jalapeno.corba.Example.*; // Implementation of the "Hello World" interface. import com.ibm.austin.itsc.jalapeno.corba.*;
As in the client, the first thing that the server must do is initialize the ORB using the ORB_init() method. Next comes an additional step only required by CORBA servers - the initialization of the BOA (Basic Object Adaptor). The BOA is the object adaptor that is used to connect interface implementations to the ORB. The BOA initialization method is called BOA_init(), and just like the ORB_init() method it can be called multiple times with the second and subsequent calls simply returning a reference to the initial BOA instance. public static void main(String args[]) { // Initialize the ORB and the BOA. ORB orb = ORB.init(args, new java.util.Properties()); BOA boa = orb.BOA_init(args, new java.util.Properties()); ...
Next we create an instance of our implementation class. // Create the implementation. HelloWorld_impl impl = new HelloWorld_impl();
As mentioned in section 4.2.3, “Writing the "Hello World" Client” on page 129, objects in the CORBA world are identified by opaque data structures known as object references. To advertise the location of our object we stringify the object reference and write it into the files Simple.ref for use by programs, and Simple.html for use by applets. // Stringify the implementation’s object reference. String stringified_ior = orb.object_to_string(impl);
Using CORBA on the Network Computer
133
// Advertise the (stringified) object reference in the file // ’Simple.ref’ for use by applications. try { FileOutputStream file = new FileOutputStream("Simple.ref"); PrintWriter out = new PrintWriter(file); out.println(stringified_ior); out.flush(); file.close(); } catch(IOException ex) { System.err.println("Can’t write to: " + ex.getMessage()); System.exit(1); } // Advertise the (stringified) object reference as HTML in the file // ’Simple.html’ for use by applets try { FileOutputStream file = new FileOutputStream("Simple.html"); PrintWriter out = new PrintWriter(file); out.print("\n"); out.print("Simple CORBA Example\n"); out.print("\n"); out.print("\n"); out.flush(); file.close(); } catch(IOException ex) { System.err.println("Can’t write to: " + ex.getMessage()); System.exit(1); }
134
Java Thin-Client Programming
Finally, to activate the implementation we connect it to the BOA by calling the method impl_is_ready(). This call starts the ORB’s event loop, and under normal conditions, never returns. // Start the implementation. try { System.out.println("Server accepting requests..."); boa.impl_is_ready(null); } catch(SystemException ex) { System.err.println(ex.getMessage()); ex.printStackTrace(); System.exit(1); }
4.2.5 Generating the Stubs and Skeletons Before we can use the client and server that we have just written we must first generate the stubs and skeletons using the IDL compiler which in ORBacus for Java is called jidl.exe: C:\> cd ...\com\ibm\austin\itsc\jalapeno\corba C:\> jidl --package com.ibm.austin.itsc.jalapeno.corba \ --output-dir ..\..\..\..\..\.. \ Example.idl
This command tells the IDL compiler to generate package statements in each of the java files that it creates, and to put those classes in the current directory (we are 6 levels down the package hierarchy). When the command completes, you will see that a package has been generated with the same name as the module that we defined in the IDL file, namely, Example. This package contains all of the stub and skeleton code required to write clients and servers that communicate using the HelloWorld interface.
4.2.6 Installing and Testing the "Hello World" Application The steps to install and test the application are as follows: 1. Generate all of the Java class files for the application: C:\> cd ...\com\ibm\austin\itsc\jalapeno\corba C:\> javac simple\*.java Example\*.java *.java
2. Install the class files so that they are accesible from the NC: C:\> copy ...\com C:\nstation\prodbase\jalapeno
Using CORBA on the Network Computer
135
3. Set your CLASSPATH to include both the ORB and our application packages: C:\set CLASSPATH=%CLASSPATH%;C:\nstation\prodbase\jalapeno\JOB-3.0.1\ob\lib C:\set CLASSPATH=%CLASSPATH%;C:\nstation\prodbase\jalapeno
4. Using the IBM Network Station manager, install a new Java application with the properties shown in Table 1. For more information on using the Network Station Manager see the Redbook Java Thin-Client Programming for a Network Computing Environment. Table 1. Network Station Manager Properties for the "Hello World" Application
Property
Value
Menu item label
Hello World
Application name
com.ibm.austin.itsc.jalapeno.corba.simple.Client
Arguments
/netstation/prodbase/jalapeno/corba/Simple.ref
Classpath
/netstation/prodbase/jalapeno/JOB-3.0.1/ob/lib: /netstation/prodbase/jalapeno
5. Start the server: C:\> cd C:\nstation\prodbase\jalapeno\corba C:\> java com.ibm.austin.itsc.jalapeno.corba.simple.Server Server accepting requests...
6. Start the client by rebooting the NC and clicking on the menu item labelled Hello World. The client GUI appears and every time you click on the Push Me button, you will see the message "Hello CORBA World!" displayed both at the server and at the NC console.
4.2.7 Installing and Testing the C++ "Hello World" Server As mentioned in the introduction to this chapter, one of the strengths of CORBA is that it allows us to implement the components of our application in different programming languages. Let us now assume that for performance reasons (there could be many other reasons too) we have had to implement the server in C++. As long we use the same IDL file to generate both the Java and C++ stubs and skeletons, the Java client can communicate with the C++ server in exactly the same way as before. In fact there is no way that a client can find out what programming language was used to implement the server objects that it uses. The entire code for the C++ version of the "Hello World" server can be found in the directory ...\jalapeno\corba . We will not cover its development here, and instead we will assume that it has been precompiled for us into the executable file Server.exe.
136
Java Thin-Client Programming
To install and start the server type the following at the command prompt: C:\> copy ...\jalapeno\corba\Server.exe C:\nstation\prodbase\jalapeno\corba C:\> cd C:\nstation\prodbase\jalapeno\corba C:\> .\Server.exe Server accepting requests...
The client is started exactly the same way as in section 4.2.6, “Installing and Testing the "Hello World" Application” on page 135.
4.2.8 The "Hello World" Client as an Applet As with RMI, there are a number of special concerns that arise when using CORBA with applets. Because of restrictions enforced by the browser’s security manager, applets can only open connections back to the host that they were loaded from. This means that the host that contains the Web page that loads the applet must also contain: • The classes that implement the client applet (including the stub classes generated by the IDL compiler). • The active server object. We will see in section 4.2.9, “Installing the ORB for Applets” on page 137 that in the NC environment there is a little more flexibility in where we install the classes for the ORB itself. For our testing, we used the Lotus DominoGo Web server installed in the directory: C:\WWW, and configured to use the default directories: • C:\WWW\HTML - for HTML files • C:\WWW\Applets - the root of the applet package hierarchy
4.2.9 Installing the ORB for Applets This section assumes that you have downloaded and built ORBacus for Java as described in 4.2.1, “Installing the ORB for NC Applications” on page 126. There are basically two choices when deciding where to locate the ORB classes: • Install them on the NC’s host machine in the same way as we did for the "Hello World" application. If we do this then we must use the IBM Network Station Manager to set the CLASSPATH of our Web browser to include the directory: C:\nstation\prodbase\jalapeno\JOB-3.0.1\ob\lib. • Install them so that they are always accessed via the Web server in the directory: C:\WWW\Applets\jalapeno.
Using CORBA on the Network Computer
137
The advantage of the first option is speed - the ORB classes are always loaded from the NC’s host file system, rather than via the Web server using the HTTP protocol. The disadvantage is that the applet will only run on the NC’s we have configured to use the host machine. The second option overcomes this, and at the price of speed, allows our client application to run on any device with a Web browser. For this example, we will access the ORB via the Web server, and therefore all we have to do is to copy the ORBacus packages into the directory C:\WWW\Applets\jalapeno directory. C:\> copy C:\nstation\prodbase\jalapeno\JOB-3.0.1\ob\lib\com C:\WWW\Applets\jalapeno C:\> copy C:\nstation\prodbase\jalapeno\JOB-3.0.1\ob\lib\org C:\WWW\Applets\jalapeno
4.2.10 Writing the "Hello World" Client Applet The entire code for the client applet is shown below. The only real differences between the applet and the application are: 1. The applet uses a different version of the ORB_init() version which takes the applet itself as its first parameter. This allows ORB configuration options to be passed using the standard applet parameter mechanism. 2. Instead of using a file, the client receives the server’s object reference through the ior parameter. package com.ibm.austin.itsc.jalapeno.corba.simple; // Java. import java.applet.*; import java.awt.*; import java.awt.event.*; import java.io.*; // CORBA. import org.omg.CORBA.*; // Stubs generated by the IDL compiler. import com.ibm.austin.itsc.jalapeno.corba.Example.*;
public class ClientApplet extends Applet implements ActionListener { public void init() { // The server’s (stringified) object reference is passed as a // parameter to the applet. String stringified_ior = getParameter("ior");
138
Java Thin-Client Programming
// Initialize the ORB. ORB orb = ORB.init(this, null); // Convert the stringified object reference into a ’live’ object // reference. org.omg.CORBA.Object obj = orb.string_to_object(stringified_ior); if(obj == null) { System.err.println("NULL object reference."); throw new RuntimeException(); } // Narrow the object reference to make sure that it is of the // correct type. server = HelloWorldHelper.narrow(obj); if(server == null) { System.err.println("Object does not implement the correct interface.");
throw new RuntimeException(); } // Add a single button to the frame. Button button = new Button("Push Me"); button.addActionListener(this); add(button); } public void actionPerformed(ActionEvent event) { try { // Call the server. System.out.println(server.hello_world()); } catch(SystemException ex) { System.err.println("Error calling the server."); throw new RuntimeException(); } } private HelloWorld server; }
Using CORBA on the Network Computer
139
4.2.11 Installing and Testing the "Hello World" Client Applet The steps to install and test the application using the client applet are as follows: 1. Copy the application’s package hierarchy into the directory: C:\WWW\Applets\jalapeno. C:\>
copy C:\nstation\prodbase\jalapeno\com\ibm C:\WWW\Applets\jalapeno\com
2. Set your CLASSPATH to include both the ORB and our application packages: set CLASSPATH=%CLASSPATH%;C:\nstation\prodbase\jalapeno\JOB-3.0.1\ob\lib set CLASSPATH=%CLASSPATH%;C:\nstation\prodbase\jalapeno
3. Start the server (if it is not still running from a previous example): C:\> cd C:\nstation\prodbase\jalapeno\corba C:\> java com.ibm.austin.itsc.jalapeno.corba.Server Server accepting requests...
4. Make the HTML file generated by the server available on the Web server: C:\> copy Simple.html C:\WWW\HTML\jalapeno\corba
5. Boot the NC and start a Web browser (HotJava or NCBrowser - in our experience HotJava is a lot faster): 6. Load the Web page that starts the applet using the following URL: http://jf0151g.itsc.austin.ibm.com/jalapeno/corba/Simple.html
The applet should now be displayed, and once again, every time you click on the Push Me button, you should see the message "Hello CORBA World!" displayed both at the server and at the browser’s Java console.
4.3 A CORBA Naming Service Example The example application presented in section 4.2, “A Simple Example” on page 126 relies on the client locating the server using a stringified object reference. The client obtains the reference either from a file (in the case of the application) or a via a parameter (in the case of the applet). Obviously, this mechanism is extremely cumbersome and a much more robust and scalable solution is to let the client refer to the server by a more meaningful name. This section introduces the CORBA naming service which allows us to do just that.
4.3.1 What is the CORBA Naming Service? The CORBA naming service is a naming system that allows us to attach humanly readable names to CORBA objects instead of having to distribute
140
Java Thin-Client Programming
stringified object references. In many ways it is very similar to a file system that allows us to attach names to files and directories stored on a disk. In CORBA terms, directories are known as naming contexts (or just contexts) and instead of containing the names of files and directories, they contain the names of CORBA objects and other contexts. Similarly, just as a filename refers to the location of some data on a storage device, so a name in a naming context refers to the location of a CORBA object, in other words, its object reference. In CORBA terminology, the association of a name with an object reference is called a binding. Figure 65 shows an example of a naming graph consisting of three contexts.
Figure 65. Examples of CORBA Naming Contexts
The naming service is in many ways the CORBA equivalent of the Java RMI registry. Both allow us to decouple clients and servers and to provide a way for clients to locate server objects using a meaningful name. The main difference between the CORBA naming service and the RMI registry is that CORBA allows us to define a complex naming graph, while the RMI registry has a simple flat namespace.
Using CORBA on the Network Computer
141
The IDL for the naming service is defined in the file CosNaming.idl which is available from the OMG. The bulk of the IDL describes the NamingContext interface which contains operations for creating and destroying contexts, and for binding, unbinding and resolving (looking up) names. The NamingContext interface is reasonably complex, and we don’t intend to describe it in detail here. Instead, you are referred to the ORBacus documentation that we downloaded in section 4.2.1, “Installing the ORB for NC Applications” on page 126. For the OMG specification of the naming service including the complete IDL see the OMG Web site at http://www.omg.org.
4.3.2 Installing the Naming Service for NC Applications This section assumes that you have downloaded and built ORBacus for Java as described in 4.2.1, “Installing the ORB for NC Applications” on page 126. The class files that implement the naming service are found in the directory C:\nstation\prodbase\jalapeno\JOB-3.0.1\naming\lib , and when we configure our application using the IBM Network Station Manager (see section 4.3.5, “Installing and Testing the Naming Service Example” on page 146), we add this directory to the Java CLASSPATH so that our client on the NC can find and load the naming service classes. ORBacus for Java provides us with a convenient way to initialize the ORB and its associated services via a configuration file. Below is the configuration file ...\com\ibm\austin\itsc\jalapeno\corba\orb.cfg that we use for the naming service example application: # ORB configuration file. [services] NameService iiop://jf0151g.itsc.austin.ibm.com:10000/DefaultNamingContext
This file tells the ORB to contact port 10000 on the NC’s host machine to find the naming service. To use the configuration file we simply use the -ORBconfig option specifying the name of the file. By explicitly stating the port number that we want the naming service to use, we ensure that even if the service closes down and restarts, clients will always know how to contact it. To make the configuration file available to NC applications we install it in the C:\nstation\prodbase\jalapeno\corba directory.
4.3.3 Writing the "Hello World" Client using the Naming Service In this section we will extend the client application given in section 4.2.3, “Writing the "Hello World" Client” on page 129, to retrieve the server’s object reference from the naming service rather than via a stringified reference in a
142
Java Thin-Client Programming
file. The code for this extended client is found in the file ...\com\ibm\austin\itsc\jalapeno\corba\naming\Client.java The first change is to import the classes for the naming service from the package org.omg.CosNaming. // Java. import java.awt.*; import java.awt.event.*; // CORBA. import org.omg.CORBA.*; import org.omg.CosNaming.*; // Jalapeno utilities. import com.ibm.austin.itsc.jalapeno.util.*; // Stubs generated by the IDL compiler. import com.ibm.austin.itsc.jalapeno.corba.Example.*;
After initializing the ORB as usual, the client now has to locate the initial naming context (think of it as the root of the file system). This is done using the resolve_initial_references method on the ORB. Note
The string that represents the naming service in the call to resolve_initial_references() is NameService and not, as in the rest of the OMG documentation, NamingService. public Client(String[] args) { // Initialize the ORB. ORB orb = ORB.init(args, new java.util.Properties()); // Get the object reference of the initial naming context. org.omg.CORBA.Object obj = null; try { obj = orb.resolve_initial_references("NameService"); if(obj == null) { System.err.println("NULL object reference."); System.exit(1); } } catch (org.omg.CORBA.ORBPackage.InvalidName ex) { System.err.println("Naming Service not available."); System.exit(1); throw new RuntimeException(); }
Using CORBA on the Network Computer
143
// Narrow the object reference to make sure that it is of the // correct type. NamingContext ctx = NamingContextHelper.narrow(obj); if(ctx == null) { System.err.println("Object does not implement the correct interface."); System.exit(1); throw new RuntimeException(); }
To find the server’s object reference we create a CORBA name representing Fred, and then look it up using the resolve () method on the naming context. // Create the naming service name that the server advertises under. NameComponent[] serverName = new NameComponent[1]; serverName[0] = new NameComponent(); serverName[0].id = "Fred"; serverName[0].kind = ""; // Lookup the server’s object reference using the name. try { obj = ctx.resolve(serverName); if(obj == null) { System.err.println("NULL object reference."); System.exit(1); } } catch (org.omg.CosNaming.NamingContextPackage.InvalidName ex) { System.err.println("Got InvalidName exception."); System.exit(1); } catch (org.omg.CosNaming.NamingContextPackage.NotFound ex) { System.err.println("Got NotFound exception."); System.exit(1); } catch (org.omg.CosNaming.NamingContextPackage.CannotProceed ex) { System.err.println("Got CannotProceed exception."); System.exit(1); }
From here on in, the code for the client is exactly the same as in the example in section 4.2.3, “Writing the "Hello World" Client” on page 129.
4.3.4 Writing the "Hello World" Server Using the Naming Service In this section we will extend the server application given in section 4.2.4, “Writing the "Hello World" Server” on page 132 to make the server advertise its location using the Name Service. The code for this extended server is found in the file ...\com\ibm\austin\itsc\jalapeno\corba\naming\Server.java As for the client, the server must import the naming service classes from the package omg.org.CosNaming.
144
Java Thin-Client Programming
// CORBA. import org.omg.CORBA.*; import org.omg.CosNaming.*; // Skeletons generated by the IDL compiler. import com.ibm.austin.itsc.jalapeno.corba.Example.*; // The implementation of the "Hello World" interface. import com.ibm.austin.itsc.jalapeno.corba.*;
After initializing the ORB and the BOA as usual, the server obtains a reference to the initial naming context in exactly the same way as the client. // Get the object reference of the initial naming context. org.omg.CORBA.Object obj = null; try { obj = orb.resolve_initial_references("NameService"); if(obj == null) { System.err.println("NULL object reference."); System.exit(1); } } catch (org.omg.CORBA.ORBPackage.InvalidName ex) { System.err.println("Naming Service not available."); System.exit(1); } // Narrow the object reference to make sure that it is of the // correct type. NamingContext ctx = NamingContextHelper.narrow(obj); if(ctx == null) { System.err.println("Reference is not a Naming Context"); System.exit(1); }
The server creates the implementation as before, and then advertises its location using the rebind() method on the naming context. We use rebind() here instead of bind so that we can start and stop the server without having to restart the naming service (see the naming service specification for more details). // Create the implementation. HelloWorld_impl impl = new HelloWorld_impl();
Using CORBA on the Network Computer
145
// Create the name that the server will advertise under. NameComponent[] serverName = new NameComponent[1]; serverName[0] = new NameComponent(); serverName[0].id = "Fred"; serverName[0].kind = ""; // Advertise the object reference using the name. try { ctx.rebind(serverName, impl); } catch (org.omg.CosNaming.NamingContextPackage.InvalidName ex) { System.err.println("Got InvalidName exception."); System.exit(1); } catch (org.omg.CosNaming.NamingContextPackage.NotFound ex) { System.err.println("Got NotFound exception."); System.exit(1); } catch (org.omg.CosNaming.NamingContextPackage.CannotProceed ex) { System.err.println("Got CannotProceed exception."); System.exit(1); }
Once again, from here on, the code for the server is exactly the same as in section 4.2.4, “Writing the "Hello World" Server” on page 132.
4.3.5 Installing and Testing the Naming Service Example The steps to install and test the example are as follows: 1. Generate all of the Java class files for the application: C:\> cd ...\com\ibm\austin\itsc\jalapeno\corba C:\> javac naming\*.java Example\*.java *.java
2. Install the class files so that they are accessible from the NC: C:\> copy ...\com C:\nstation\prodbase\jalapeno
3. Set your CLASSPATH to include the ORB, the naming service, and our application packages: C:\set CLASSPATH=%CLASSPATH%;C:\nstation\prodbase\jalapeno\JOB-3.0.1\ob\lib C:\set CLASSPATH=%CLASSPATH%;C:\nstation\prodbase\jalapeno\JOB-3.0.1\naming\lib C:\set CLASSPATH=%CLASSPATH%;C:\nstation\prodbase\jalapeno
146
Java Thin-Client Programming
4. Using the IBM Network Station manager, install a new Java application with the properties shown in Table 2. For more information on using the Network Station Manager see the Redbook Java Thin-Client Programming for a Network Computing Environment. Table 2. NSM Properties for the Naming Service Example
Property
Value
Menu item label
Naming Service Example
Application name
com.ibm.austin.itsc.jalapeno.corba.naming.Client
Arguments
-ORBconfig /netstation/prodbase/jalapeno/corba/orb.cfg
Classpath
/netstation/prodbase/jalapeno/JOB-3.0.1/ob/lib: /netstation/prodbase/jalapeno/JOB-3.0.1/naming/lib: /netstation/prodbase/jalapeno
5. Start the naming service: C:\> java com.ooc.CosNaming.Server -OAport 10000
6. Start the server: C:\> cd C:\nstation\prodbase\jalapeno\corba C:\> java com.ibm.austin.itsc.jalapeno.corba.Server -ORBconfig org.cfg Server accepting requests...
7. Start the client by rebooting the NC and clicking on the menu item labelled Naming Service Example. The client GUI appears and every time you click on the Push Me button, you will see the message "Hello CORBA World!" displayed at the server and at the NC console.
4.3.6 Installing the Naming Service for Applets This section assumes that you have downloaded and built ORBacus for Java as described in 4.2.1, “Installing the ORB for NC Applications” on page 126. All we have do now is install the naming service packages in the C:\WWW\Applets\jalapeno hierarchy as follows: 1. Copy the package: C:\nstation\prodbase\jalapeno\JOB-3.0.1\naming\lib\com\ooc\CosNaming
to the directory: C:\www\Applets\jalapeno\com\ooc
2. Copy the package: C:\nstation\prodbase\jalapeno\JOB-3.0.1\naming\lib\org\omg\CosNa ming
Using CORBA on the Network Computer
147
to the directory: C:\www\Applets\jalapeno\org\omg
As described in section 4.3.2, “Installing the Naming Service for NC Applications” on page 142 ORBacus for Java allows us to specify a configuration file to tell the ORB how to find services such has the naming service. To make this file available to applets we simply copy it onto the Web server as follows: C:\> copy C:\nstation\prodbase\jalapeno\corba\org.cfg C:\WWW\HTML\jalapeno\corba
4.3.7 Writing the "Hello World" Client Applet The complete Java source code for the client applet is found in the file ...\com\ibm\austin\itsc\jalapeno\corba\naming\ClientApplet.java. Because we are using the naming service to locate the server the only real difference between the applet and application versions of the client is that the applet again calls the version of ORB.init() that takes the applet as its first parameter. In this case this allows the ORB to extract the -ORBconfig parameter and hence to locate the ORB configuration file on the NC’s host.
4.3.8 Installing and Testing the Naming Service Client Applet The steps to install and test the client applet are as follows: 1. Copy the applet’s package hierarchy into the directory C:\WWW\Applets\jalapeno. C:\> copy C:\nstation\prodbase\jalapeno\com\ibm C:\WWW\Applets\jalapeno\com
2. If you haven’t already done so, set your CLASSPATH to include the ORB, the naming service, and our application packages. C:\set CLASSPATH=%CLASSPATH%;C:\nstation\prodbase\jalapeno\JOB-3.0.1\ob\lib C:\set CLASSPATH=%CLASSPATH%;C:\nstation\prodbase\jalapeno\JOB-3.0.1\naming\lib C:\set CLASSPATH=%CLASSPATH%;C:\nstation\prodbase\jalapeno
3. Start the naming service (if it isn’t running already): C:\>
java com.ooc.CosNaming.Server -OAport 10000
4. Start the server: C:\>
cd C:\nstation\prodbase\jalapeno\corba java com.ibm.austin.itsc.jalapeno.corba.Server -ORBconfig org.cfg Server accepting requests...
C:\>
5. Boot the NC and start a Web browser (HotJava or NC Browser - in our experience HotJava is a lot faster). 6. Load the Web page that starts the applet using the following URL:
148
Java Thin-Client Programming
http://jf0151g.itsc.austin.ibm.com/jalapeno/corba/Naming.html
The applet should now be displayed, and once again, every time you click on the Push Me button, you should see the message "Hello CORBA World!" displayed both at the server and at the browser’s Java console.
4.4 Summary In summary, we have seen that developing CORBA applications for the NC is similar to developing standard Java RMI applications. CORBA goes one step further than RMI in that it allows us to integrate components developed in languages other than Java, which is extremely useful for legacy code, third party products, and even new components that are simply best implemented in a language other than Java.
Using CORBA on the Network Computer
149
150
Java Thin-Client Programming
Chapter 5. Object Recycling In Java programs the runtime cost of instance creation and garbage collection is significant, and in this chapter we investigate ways to minimize these costs by recycling Java instances. While these techniques are applicable to Java on any platform, they are especially relevant to developing applications destined for the resource-hungry environment of the Network Computer (NC).
5.1 Why Recycle? You may well be asking, "Why doesn’t the VM look after this for me?". This is a valid question and it is certainly true that by using techniques such as object recycling we are doing work that Java, as a garbage-collected, byte-compiled language, is meant to save us from. Unfortunately, in the NC environment, we are forced to work with limited resources, and instance creation and garbage collection can cost us heavily in terms of both memory and machine performance. Remember that there is no secondary storage for the NC to use as swap space, and our applications need to minimize their memory requirements in order to coexist peacefully. The following sections investigate a number of ways in which object recycling can be implemented, starting with some simple modifications that can be applied to individual classes, through to an attempt at a generic instance pooling mechanism. All of the code for the examples in this chapter can be found in the directory ...\com\ibm\austin\itsc\jalapeno\recycling.
5.1.1 The Example Classes The examples in the following sections use two classes to represent those that may be part of a typical Java application. The class CreateMe is intended to represent a class whose instances have a small memory footprint, and CreateMeBig is a class whose instances are (not surprisingly) significantly larger (~10k bytes). The classes can be found in the files CreateMe.java and CreateMeBig.java respectively, and are defined as follows: class CreateMe { public CreateMe() { id = -1; } public CreateMe (int i)
© Copyright IBM Corp. 1999
151
{ id = i + 1; } public void reinitialize(int i) { id = i + 1; } private int id; } class CreateMeBig { public CreateMeBig() { id = -1; stuff = new byte[10000]; } public CreateMeBig(int i) { id = i + 1; stuff = new byte[10000]; } public void reinitialize(int i) { id = i + 1; } private int id; private byte stuff[]; }
5.2 Recyclable Instances This section introduces a simple technique that can be used when Java instances have a short life-span in a scope that is created and destroyed multiple times. The example we use here is a tightly nested iteration, but be aware that the principles apply to cases where the repetition may not be quite so obvious. Here is a portion of the code taken from ProfileSimple.java: public static void smallWithoutRecycling(int n) { long start = System.currentTimeMillis();
152
Java Thin-Client Programming
for (int i = 0; i < n; i++) { CreateMe instance = new CreateMe(i); // Obviously, in a normal situation, we would actually *do* // something with ’instance’ here. } long end = System.currentTimeMillis(); System.out.println("Time taken SMALL withOUT Recycling: " + (end - start));
} public static void bigWithoutRecycling(int n) { long start = System.currentTimeMillis(); for (int i = 0; i < n; i++) { CreateMeBig instance = new CreateMeBig(i); // Obviously, in a normal situation, we would actually *do* // something with ’instance’ here. } long end = System.currentTimeMillis(); System.out.println("Time taken BIG withOUT Recycling: " + (end - start));
}
In these examples, the constructors for the classes CreateMe and CreateMeBig are called n times, and n instances are (eventually) garbage collected. A simple way to alleviate this problem is to write the classes as shown in section 5.1.1, “The Example Classes” on page 151, and to give each class a default constructor, and a reinitialize() method. The reinitialize() method acts as a pseudo-constructor and sets the state of the instance as if it had been created normally. Using the default constructor and reinitialize(), we can rewrite the methods as follows: public static void smallWithRecycling(int n) { long start = System.currentTimeMillis(); CreateMe recycledInstance = new CreateMe(); for (int i = 0; i < n; i++) { recycledInstance.reinitialize(i); // Obviously, in a normal situation, we would actually *do* // something with ’recycledInstance’ here.
Object Recycling
153
} long end = System.currentTimeMillis(); System.out.println("Time taken SMALL WITH Recycling: " + (end - start));
} public static void bigWithRecycling(int n) { long start = System.currentTimeMillis(); CreateMeBig recycledInstance = new CreateMeBig(); for (int i = 0; i < n; i++) { recycledInstance.reinitialize(i); // Obviously, in a normal situation, we would actually *do* // something with ’recycledInstance’ here. } long end = System.currentTimeMillis(); System.out.println("Time taken BIG WITH Recycling: " + (end - start));
}
Now there is a single call to each constructor, n calls to the reinitialize() method, and a single instance to be garbage collected. Obviously, in this case the CreateMe and CreateMeBig classes are trivial, and the feasibility of using this approach largely depends on how easy it is to reinitialize the state of an instance. In practise most classes will be considerably more complex, and you will have to take care to ensure that a reinitialized instance behaves exactly as if it had been created via a constructor. To get some measure of the efficacy of the recycling approach we ran ProfileSimple.java on the PC and on the NC, varying the number of iterations from 10 to 1,000,000. On the PC the program was started as follows: C:\> C:\> C:\> C:\> ... ... C:\> C:\>
java java java java
com.ibm.austin.itsc.jalapeno.recycling.ProfileSimple com.ibm.austin.itsc.jalapeno.recycling.ProfileSimple com.ibm.austin.itsc.jalapeno.recycling.ProfileSimple com.ibm.austin.itsc.jalapeno.recycling.ProfileSimple
10 10 10 10
Small Small Recycle Big Big Recycle
java com.ibm.austin.itsc.jalapeno.recycling.ProfileSimple 1000000 Big java com.ibm.austin.itsc.jalapeno.recycling.ProfileSimple 1000000 Big Recycle
On the NC, we used the IBM Network Station Manager to configure 6 new Java applications with menu labels "10", "100", "1000", "10,000", "100,000" and "1,000,000". The parameters to each application were then changed to reflect each set of tests. The basic properties of the 6 applications are as shown in Table 3. For more information on using the IBM Network Station
154
Java Thin-Client Programming
Manager see the Redbook Java Thin-Client Programming for a Network Computing Environment. Table 3. NSM Properties for the "ProfileSimple" Test Programs
Property
Value
Menu item label
"10", "100", ...
Application name
com.ibm.austin.itsc.jalapeno.recycling.ProfileSimple
Arguments
10 Small 10 Small Recycle ... ... 1000000 Big 1000000 Big Recycle
Classpath
/netstation/prodbase/Recycle
The results are shown in Table 4 where each timing result represents the average of 3 runs using the same configuration: Table 4. Results for ProfileSimple.java
On the PC Iterations
WithOUT Recycling (ms)
On the NC WITH Recycling (ms)
WithOUT Recycling (ms)
WITH Recycling (ms)
Creating instances of CreateMe 10
0
0
57
57
100
0
0
57
57
1000
0
0
57
57
10,000
20
0
86
57
100,000
301
10
443
180
1,000,000
3035
70
4315
1314
Creating instances of CreateMeBig 10
0
0
57
57
100
20
0
115
57
1000
230
0
457
58
10,000
2313
0
4057
58
Object Recycling
155
On the PC
On the NC
Iterations
WithOUT Recycling (ms)
WITH Recycling (ms)
WithOUT Recycling (ms)
WITH Recycling (ms)
100,000
23023
10
39971
171
1,000,000
231894
80
398871
1315
Analyzing the results, we can see that the recycling approach provides a substantial improvement over the traditional constructor approach, especially as the size of the instance footprint increases (this is not surprising as the memory allocation takes place in the constructor). However, before you rush out and start using recycling for all of your instances, bear in mind that this technique is really only useful when: • Instances are short-lived and are used multiple times. • The cost of initializing and reinitializing each instance is not significant.
5.3 Instance Pools The example in section 5.2, “Recyclable Instances” on page 152 is useful when a single instance of a particular class is required within the current scope (i.e. the scope of an iteration). What if you need more than one instance in a scope? One solution would be to have a recycled instance for every instance required: public static void smallWithRecycling(int n) { long start = System.currentTimeMillis(); CreateMe recycledInstance1 = new CreateMe(); CreateMe recycledInstance2 = new CreateMe(); for (int i = 0; i < n; i++) { recycledInstance1.reinitialize(i); recycledInstance2.reinitialize(i); // Obviously, in a normal situation, we would actually *do* // something with ’recycledInstance1’ and ’recycledInstance2’ // here. } long end = System.currentTimeMillis(); System.out.println("Time taken BIG WITH Recycling: " + (end - start));
}
156
Java Thin-Client Programming
This approach is fine for two or three instances, but rapidly becomes unwieldy as the number increases. A more generic solution to this problem is to use a pool of shared instances for each class and to get instances from the pool rather than creating them using the new keyword. Once again, bear in mind that this approach is only useful when instances of a class are short-lived and are repeatedly being created and garbage collected. If instances are long-lived then stick with the standard Java construction mechanism. Before we take a look at the implementation of our generic instance pool, we must first consider some assumptions that were made in its design: 1. The maximum number of instances that you require is fixed: If you need an instance pool that grows dynamically then the solution is not clear. Implementing a pool using the Java Vector class is not a good idea as the class is synchronized, and the performance penalty of acquiring locks on the data structure far outweighs any gains provided by the pool. If a growable pool is required then you will need to implement a dynamic array class of your own. 2. The instance pool is not thread-safe: If you want a thread-safe instance pool you will again encounter the costs of synchronizing access. This does not rule out the use of the pool in threaded programs of course, it just means that it is up to the programmer to control access to the pool in a thread-safe way. The advantage of this approach is that the programmer can easily control the granularity of the thread locking, something which the designers of the Java data structures such has Vector should have thought more deeply about. The implementation of the instance pool show below can be found in the file .../com/ibm/austin/itsc/jalapeno/recycling/InstancePool.java . public class InstancePool { public InstancePool(Class klass, int poolSize) throws IllegalAccessException, InstantiationException { this.poolSize = poolSize; // Initialise the instances in the pool. pool = new Object[poolSize]; for (int i = 0; i < poolSize; i++) pool[i] = klass.newInstance(); nextAvailable = 0;
Object Recycling
157
nextReturn = 0; usedCount = 0; } public InstancePool(String klassName, int poolSize) throws ClassNotFoundException, IllegalAccessException, InstantiationException { this(Class.forName(klassName), poolSize); } public Object newInstance() throws PoolEmptyException { Object instance; if (usedCount == poolSize) throw new PoolEmptyException(); instance = pool[nextAvailable]; nextAvailable = (nextAvailable + 1) % poolSize; usedCount++; return instance; } public void freeInstance(Object instance) throws PoolFullException { pool[nextReturn] = instance; nextReturn = (nextReturn + 1) % poolSize; usedCount--; } private private private private private
int poolSize; // Size of the pool. Object[] pool; // The pool itself. int nextAvailable;// Index of the next unused reference. int nextReturn;// Index of the next reference returned to pool. int usedCount;// Number of references in use.
}
The InstancePool class is designed to be as efficient as possible and it works by maintaining a sliding window onto the array of instance references. Note that it is up to the programmer to make sure that instances are not returned to the pool more than once, otherwise the pool will become
158
Java Thin-Client Programming
corrupted. The following code, taken from ProfilePool.java, shows how the instance pool is created and then used: To create an instance pool we simply specify the name of a class (or an instance of the Class class), and the size of the pool: String klassName = "com.ibm.austin.itsc.jalapeno.recycling.CreateMe"; InstancePool ip = new InstancePool(klassName, 10);
The instance pool is then used as follows: • To get a new instance, the newInstance() method is invoked on the instance pool. • The programmer reinitializes the instance manually (this is difficult to do generically and efficiently as the reinitialize() method will need to take all of the parameters required to initialize the instance). • When the instance is finished, it must be returned to the pool using the freeInstance() method. static void smallWithInstancePool(InstancePool ip, int n) throws IllegalAccessException, InstantiationException, PoolEmptyException, PoolFullException { long start = System.currentTimeMillis(); for (int i = 0; i < n; i++) { CreateMe i1 = (CreateMe) ip.newInstance(); CreateMe i2 = (CreateMe) ip.newInstance(); CreateMe i3 = (CreateMe) ip.newInstance(); CreateMe i4 = (CreateMe) ip.newInstance(); CreateMe i5 = (CreateMe) ip.newInstance(); CreateMe i6 = (CreateMe) ip.newInstance(); CreateMe i7 = (CreateMe) ip.newInstance(); CreateMe i8 = (CreateMe) ip.newInstance(); CreateMe i9 = (CreateMe) ip.newInstance(); CreateMe i10 = (CreateMe) ip.newInstance(); i1.reinitialize(i); i2.reinitialize(i); i3.reinitialize(i); i4.reinitialize(i); i5.reinitialize(i); i6.reinitialize(i); i7.reinitialize(i); i8.reinitialize(i);
Object Recycling
159
i9.reinitialize(i); i10.reinitialize(i); ip.freeInstance(i1); ip.freeInstance(i2); ip.freeInstance(i3); ip.freeInstance(i4); ip.freeInstance(i5); ip.freeInstance(i6); ip.freeInstance(i7); ip.freeInstance(i8); ip.freeInstance(i9); ip.freeInstance(i10); } long end = System.currentTimeMillis(); System.out.println("Time taken SMALL with Instance Pool: " + (end start)); }
As in section 5.2, “Recyclable Instances” on page 152, we tested ProfilePool.java on both the PC and the NC, and the results are shown in Table 5. Each timing result represents the average of 3 runs of the same configuration. This time we compared 3 situations: 1. Using the constructor as usual 2. Using the hand-coded recycling technique as before 3. Using the instance pool Table 5. Results for ProfilePool.java
On the PC Iterations
WithOUT (ms)
On the NC WITH (ms)
Instance Pool
WithOUT (ms)
WITH (ms)
Instance Pool
Creating instances of CreateMe
160
10
0
0
20
57
57
57
100
0
0
25
57
57
57
1000
30
0
30
100
57
57
10,000
310
10
160
440
180
600
100,000
3075
80
1392
4114
1130
6143
1,000,000
30744
761
13670
40729
10900
61529
Java Thin-Client Programming
On the PC Iterations
WithOUT (ms)
On the NC WITH (ms)
Instance Pool
WithOUT (ms)
WITH (ms)
Instance Pool
Creating instances of CreateMeBig 10
20
0
20
100
57
57
100
230
0
25
450
71
57
1000
2315
0
30
4020
86
57
10,000
23140
10
160
39800
171
610
100,000
232734
80
1392
397486
1170
6257
1,000,000
fixme
765
13670
3994743
10830
62700
In most cases the results are not surprising - the instance pool fares better than the traditional constructor approach and slightly worse than the hand-coded recycling approach. However, in the case of allocating small instances on the NC, the instance pool performs spectacularly worse. At the time of writing the cause of the problem remains unknown, but it is an excellent reminder that the only way to see whether an optimization actually works on a specific platform is to test it.
5.4 Conclusions As we have seen, object recycling can be a useful technique but we have to be wary of drawing firm conclusions from the experiments that we have performed so far. Some important things to consider before deciding whether or not to use this technique are: • The cost of reinitializing the instances that you want to recycle: If initialization is costly, then the benefit of avoiding the constructor calls will be minimized. • The memory footprint of those instances: If instances have a big footprint and are used often then they are definite candidates for recycling. • The version of the JDK that you are using: Optimizations made for one version of the JDK may well cause problems in another. • The stack/heap size of the VM configuration on the NC:
Object Recycling
161
In order to be sure that you will get the performance you require you need to experiment with the configuration of the NC for your specific application. There are no silver-bullet solutions. • The VM implementation: Different VMs will allocate memory differently, garbage collect differently and implement different optimizations at the compiler and VM level. Write once - profile everywhere. Our results also served as an excellent reminder of some general points of optimization philosophy: • Make sure that you really need to optimize in the first place: Profile, profile, profile. • Optimization often comes at the expense of portability: An improvement on one platform can degrade performance on another. This is still the case in the Java world where differences in the implementation of the VM can be as critical as differences in the hardware architecture. • Optimizations generally make programs more complex and therefore harder (and costlier) to debug and maintain: There is no such thing as a clever optimization - only a necessary one, and the code should be littered with good comments (good commenting is surprisingly rare).
5.5 Oddities As an aside, it is worth mentioning a little oddity that occurred during our initial investigations using the following version of the CreateMe class: class CreateMe { public CreateMe() { id = -1; } public CreateMe(int i) { id = i; } public void reinitialize(int i)
162
Java Thin-Client Programming
{ id = i; } private int id; }
In an attempt to demonstrate the performance improvement gained by adopting the recycling approach we profiled ProfileSimple.java using the standard tool java_g.exe supplied with the JDK: C:\> java_g -prof:without.log com.ibm.austin.itsc.jalapeno.ProfileSimple 1000000 Small C:\> java_g -prof:with.log com.ibm.austin.itsc.jalapeno.ProfileSimple 1000000 Small Recycle
Unfortunately, as you can see in Table 6, the results weren’t quite what we expected, and the method without the recycling actually ran faster. Table 6. Profiling Results 1
Count
Callee
Time
1
.../ProfileSimple.withoutRecycling(I)V
11637
1
.../CreateMe.(I)V
0
1
.../ProfileSimple.withRecycling(I)V
15701
1000000
.../CreateMe.reinitialize(I)V
3717
On closer inspection we discovered that in the case without recycling the constructor was only being called once, and not, as expected, a million times. In turns out that this apparently strange behaviour is caused by an optimization in the Java VM when there are no other operations in a constructor other than simple assignments to instance variables. Obviously, most constructors will do some real work and therefore this will not be a problem, but it is definitely worth bearing in mind. By changing the definition of CreateMe as shown below and re-running the program, we came up with the results shown in Table 7. Note that the only change was to make the assignment to the id instance variable a more complex expression (in this case + 1.), and that was enough to prevent the Java VM from making the optimization. class CreateMe { public CreateMe() { id = -1; }
Object Recycling
163
public CreateMe(int i) { id = i + 1; } public void reinitialize(int i) { id = i + 1; } private int id; } Table 7. Profiling Results 2
Count
Callee
Time
1
.../ProfileSimple.withoutRecycling(I)V
32672
1000000
.../CreateMe.(I)V
8565
1
.../ProfileSimple.withRecycling(I)V
17320
1000000
.../CreateMe.reinitialize(I)V
4981
This time, the constructor was indeed called a million times, and the use of this simple recycling technique almost cuts the overall execution time in half. Obviously, most real constructors will do more than just assign to their instance variables, but it is definitely worth bearing in mind.
164
Java Thin-Client Programming
Chapter 6. Serialization The reader of this chapter should be familiar with the implementation and use of serialization in Java. However, if this information is required, this documentation can be found at the JavaSoft site at the following URL: http://java.sun.com/products/jdk/1.1/docs/guide/serialization/index.html
The purpose of this chapter is to show how you can use serialization particularly for the NC environment (very thin clients). Of course, because Java’s platform independency, these techniques can be applied to other platforms as well.
6.1 What Is Serialization? Serialization allows objects to be transformed into a stream. This stream can then either be used to store the object in some persistent storage (such as a file or a database) or it can be directed to any other target stream, such as the response stream of a servlet. In the other direction, serialization can be used to read the stream from any source and create an instance of this previously serialized instance. Figure 66 on page 165 illustrates the serialization process:
Figure 66. The Serialization Process
An object can be serialized if its class implements the Serializable interface defined in the java.io package. This is, for example, the case for all objects of the java.awt package. The default implementation of the Serializable interface is sufficient for most cases, therefore there is no need to implement
© Copyright IBM Corp. 1999
165
any of its methods. For classes that require a special treatment during the serialization of deserialization process you must implement the following two methods: private void writeObject(java.io.ObjectOutputStream out) throws IOException; private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
6.2 Why Using Serialization Serialization allows an elementary persistence of objects. Since serialization does not manage any concurrent writing. This technique should only been used for delimited classes. The following sections discuss four useful techniques where serialization can be used: • • • •
To To To To
give objects persistence. preinitialize objects. transmit objects from the server to the client. avoid running the creation code for the serialized objects on the client.
An example of implementation is given in section 6.3 on page 171.
6.2.1 Serialization to Manage Persistent Objects Serialization is an elementary way to manage persistence. Because there is no concurrent access management, the use of serialization should be reserved for elementary cases only. This is the case when writing is managed only by the server not by all the NCs. It is possible to conceive and build a system in order to allow many NCs to read and write to a common pool of serialized objects. But there are many problems to solve, such as security, concurrent access, data integrity, no update and more. In this case, serialization is not a good choice. However, there are some cases where persistent objects are stored on the server and these objects are read by many NCs. For example, let’s take a shop that sells furniture over the Internet. One of the company’s Web pages proposes a list of available products along with a short description. In addition it is possible for customers to consult the detailed description of the products. An example for an implementation when using Java applets on the client side could be that the server has a set of files containing the serialized instances
166
Java Thin-Client Programming
of those classes that keep and manage the detailed information for the products. The NC could load those files through an URL and create the instances with the detailed description of the client. The following Figure 67 illustrates this example:
Figure 67. Example of the Serialization for a Persistence Management
Implementation requires managing many files on the server. This might raise an important problem that cannot be neglected. For example, if the classes of the serialized objects change, this could involve conflicts between versions of classes (see section 6.4.3, “Using Different Versions of the Same Class” on page 177).
6.2.2 Serialization to Preinitialize a Set of Objects Serialization can be a fast way to get a set of initialized objects. Once initialized, an instance can be serialized to a persistent storage and when a new instance of this object is needed, it can be deserialized to a new and already initialized instance. This technique can be used to initialize an application or applet, and to begin a process, as well as for many more cases. It can be helpful for distributing and managing preferences, because it allows us to include them in the stream and make that stream available for a groups of client objects (or users). However, this way to manage preferences should be used with care. The problem can often be solved differently and sometimes much more efficiently. For example, using parameters for an applet in order to set the preferences can require less administrative overhead and in some cases be even faster that using serialization to retrieve preinitialized instances of objects. Serialization needs classes to load from a remote site if they are not available
Serialization
167
locally. Serialization only speeds up the initialization, if the initialization on the client takes longer than loading the serialized objects. If you chose to use serialization to solve this type of task, you should be aware about all impacts this technique can have. These could be lower performance as well as unsuspected guests caused by the use of serialization. Section 6.4, “Using Serialization With Care” on page 176 is discussing possible side effects when serialization is used.
Figure 68. Using Serialization in Order to Save Default Preferences
Figure 68 on page 168 shows an example on how to use serialization to receive preinitialized instances. In this sample, a workspace has been populated and laid out with several other object instances. Once this has been done in a satisfactory way, the workspace objects must be serialized and stored to a persistent storage on the server. This has to be done only once for each different configuration. All NCs that need this workspace initialized in the same way as it has been serialized before, can obtain and deserialize the appropriate stream to generate all preinitialized instances.
6.2.3 Serialization to Transfer Objects From The Server to The NC There are two different ways to use serialization in order to transfer objects from the server to the client. Both ways are not specific to the NC, but for NCs without a persistent storage like the IBM Network Station, transferring objects will be a typical requirement. However, keep in mind that there are other techniques than serialization to achieve this goal. If the object is static (the opposite of dynamically created), it can be serialized and stored in a file on the server before the application or applet is started. When the NC requires this object, it can request the file containing the serialized object through a URL and deserialize it from the resulting stream. This sample has been discussed in section 6.2.1, “Serialization to Manage
168
Java Thin-Client Programming
Persistent Objects” on page 166. In case the object has to be build dynamically on request, for example out of different data sources, the object can still be requested through an URL from the server. A Java Servlet can be used to build the requested object on the server side and to serialize this object, directing the resulting stream to the HTTP request response stream. On the client, the object can be deserialized as before from the resulting stream (see Figure 69 on page 169). For example, a database on the server is used to store a catalog detailed description for each item that is for sale. Instead of storing the serialized Java object, the database is more generic and stores just the attributes of the items. If the describing object for an item is requested, the servlet requests the necessary information from the database and creates and initializes the requested object, serializes it and sends it to the client. The client then receives the response stream and deserializes it to the resulting object.
Figure 69. Transfer of Objects from Server to NC using Serialization
6.2.4 Serialization to Avoid Initialization on the NC In some cases, the creation and initialization of an object or a set of objects is complex and requires many other factoring objects that are simply used to create and initialize the resulting objects. Figure 70 on page 170 illustrates the initialization process (which can require a lot of CPU time and network delays when gathering information from a remote database).
Serialization
169
Figure 70. Factoring Process Produces Resulting Objects
The architecture of an NC always involves a client and a server. Transforming the prior process leads to an architecture shown in Figure 71 on page 170: The client gets the required data to create and initialize the objects from the server (for example using JDBC). The object factoring is done on the client, all objects necessary to create the resulting objects must be available on the client (in our case for example the JDBC classes).
Figure 71. Data on the Server, Processing on the NC
Because in general, an NC has limited resources available, it is preferable to use a different architecture and implementation in order to save those resources. Figure 72 shows how the object factoring process is entirely done on the server; then the resulting objects must be transferred from the server to the client. If servlets are used on the server side, this can be done as described before, by serializing the object to be transferred and directing the stream to the response stream from the HTTP request. On the client side, the resulting object can be deserialized out of this response stream.
170
Java Thin-Client Programming
Figure 72. Minimal Processing on the NC
This architecture has two advantages: • It saves memory on the NC, because it does not require loading and instantiating the factoring classes. • It moves the used processing power from the NC to the server(s). Since the NC might only have limited resources available and only limited scalability because of its architecture, this gives a possibility of better management and correctly calculating the processing power needed on the server. Using scalable servers allows us to provide the demanded computing power. Note
In order to reach this separation between resulting objects that are needed on the NC and those objects that are used to build them, the design of the application must clearly and completely separate them.
6.3 An Example of Serialization for a NC Suppose that a furniture store has a Web application that allows a user to order items. One of the Web pages contains a java applet with a list of items, together with a short description. From this applet the user is also able to look at a detailed description for each item. Because the detailed description might be large or complex and the number of items rather numerous, the detailed information for each item was not downloaded in the first place. The detailed description for each item has been isolated in order to load the detailed description of each item separately and on demand. On the server, the objects for each of the detailed descriptions has been serialized, each in its own file.
Serialization
171
When the applet requests the detailed description, it will load the appropriate file from the server, using a URL. From the resulting stream, the applet deserializes the object for the requested description.
6.3.1 Declaration of the Classes for the Detailed Description The detailed description has been defined as: // Detailed description of a piece of furniture sold import java.io.Serializable; public class PieceOfFurnitureDescription implements Serializable { private String description; // A descriptive text of the item private Dimensions dimensions; // The dimensions of the item private Boolean available; // Is this item in stock? transient private PieceOfFurnitureShortDescription shortDescription; // The corresponding short description }
Note that the class implements the interface Serializable. This allows us to use the default implementation of serialization in order to serialize an instance of PieceOfFurnitureDescription. One of the attributes is the items dimension. This attribute must be either one of Java basic types or implement the Serializable interface in order to get serialized with the instance of PieceOfFurnitureDescription. Sometimes it is useful not to serialize an attribute, even if it implements the Serializable interface. This can be accomplished by defining this attribute as transient. So for example if Dimensions should not be serialized, it could be declared as: transient private Dimensions dimensions; // The dimensions of the item But for our example, Dimensions has to be serialized. So its definition is: // The dimensions of a piece of furniture import java.io.Serializable; public class Dimensions implements Serializable { private double width; private double height; private double depth; }
172
Java Thin-Client Programming
Note
Note that the class implements the interface Serializable. Back to the declaration of the PieceOfFurnitureDescription class. The declaration of one of its attributes is: transient private PieceOfFurnitureShortDescription shortDescription; This attribute has been defined as transient because in our application, the detailed description is only requested from the view of the short description. So storing this information with the detailed description is not useful. Note
This is not a generic reason for this design. In other cases other than this sample, this attribute might not be transient. Also, the class PieceOfFurnitureDescription inherits directly from the object. This choice has been made in order to simplify the presentation of the example. A better conception is to define a hierarchy of descriptions in order to manage the different types of items.
6.3.2 Creating the Catalog of Descriptions on the Server On the server, the detailed description of each item must be serialized in a different file. For the example, we created a runnable class CreateSerializeFiles having no user interaction in order to create some examples of serialized descriptions. The following method has two input parameters: a file name and an object that implements the interface Serializable. This method serializes the object and stores it in a local file: // Serialize in the current local directory an item // in a file named ’fileName’. static public void serialize (String fileName, Serializable item) throws IOException { FileOutputStream fout = new FileOutputStream(fileName); ObjectOutputStream objectOutStream = new ObjectOutputStream (fout); objectOutStream.writeObject(item); objectOutStream.close(); return; }
Serialization
173
Note
The object that gets serialized must just implement the Serializable interface. The method serialize() does not check if the object to serialize is of type PieceOfFurnitureDescription. This is possible because we just use the standard serialization. This method could work for any object that implements the interface Serializable and not only for aPieceOfFurnitureDescription.
6.3.3 A Reader to Deserialize Objects on the Client The class SerializedObjectReader has the responsibility to deserialized a description on the client. A description is identified by the file name where the description is stored. All the files are stored in the same directory. A SerializedObjectReader needs this information and other. This information is passed to the constructor of SerializedObjectReader when the instance gets initialized. The definition of this class is: // // // //
This class contains the sample code that read a serialized Object. Each object is serialized in its own file. All the files are on the same path in the same host and the same URL protocol will be used to retrieve the file.
import import import import import import
java.io.IOException; java.io.Serializable; java.io.InputStream; java.io.ObjectInputStream; java.net.URL; java.net.MalformedURLException;
public class SerializedObjectReader { private String protocol; // The URL protocol used to retrieve the files private String host; // The URL host where are the files. In some cases could be null. private String catalogPath; // The path where are stored all the files. }
A constructor allows to set the initial values of the instance fields: // Initialize a reader of serializedObject public SerializedObjectReader (String aProtocol, String aHost, String aCatalogPath) { protocol = aProtocol; host = aHost; catalogPath = aCatalogPath; }
The SerializedObjectReader also has a service that allows for the retrieval of a serialized object. This service has the file name of a the serialized object as an input parameter.
174
Java Thin-Client Programming
// Read a PieceOfFurnitureDescription serialized in the file named fileName // public Object readSerializedObject (String aFileName) throws MalformedURLException, IOException, ClassNotFoundException { URL anUrl = new URL(protocol, host ,catalogPath+aFileName); InputStream anInputStream = (InputStream)anUrl.getContent(); ObjectInputStream anObjectInputStream = new ObjectInputStream(anInputStream); return anObjectInputStream.readObject(); }
Note
This service is based on the generic services that come with serialization returns an object. The type of this object is not necessary PieceOfFurnitureDescription. It is up to the implementation of this service to check the type of the received object.
6.3.4 Using the SerializedObjectReader in an Applet To show the use of the classes defined in the earlier sections of this chapter, here is a short example how to use them in an applet. The applet reads parameters to get the values required by the SerializedObjectReader. This allows us to create an instance of a reader, which is then used to retrieve and deserialize the object representing the detailed description. Following to this paragraph is the applets instance method that reads the parameters and initializes the SerializedObjectReader with the given value. // // // // // //
Initialize the parameters of the applet. This method must be called in the init() method of the applet. The parameters of the applet are: catalogProtocol : The protocol to use to access the serialized files. catalogPath : The path where are located the serialized files. catalogHost : It’s the host where are the files, it could be omitted
public void initAppletParameters ( ) { descriptionReader = new SerializedObjectReader (getParameter("catalogProtocol") , getParameter("catalogHost") , getParameter("catalogPath")); return; } Table 8. Parameters Used for the Serialization Applet Sample
Parameter
Description
catalogProtocol
Protocol used to access the serialized files.
catalogHost
Path where the serialized files are located on the server.
Serialization
175
Parameter
Description
catalogPath
Server host name, can be omitted.
The applet requires three parameters shown in Table 8. Here is an example how the HTML code embedding the applet could look like for our example: Note
In this example, the parameter catalogHost is not specified, because it is not needed. theApplet.getParameter("catalogHost") will return null. In our case, this value for the host name is ok for the SerializedObjectReader. The following method in the applet is used to retrieve and deserialize the object containing the detailed description. As a parameter, it gets the filename of the serialized object. This method uses the descriptionReader instance created at the initialization of the applet (see the source code shown on page 175.) // by the filename of the file which includes the serialized description. public PieceOfFurnitureDescription retrieveDescription (String fileName) throws MalformedURLException, IOException, ClassNotFoundException { return (PieceOfFurnitureDescription) descriptionReader.readSerializedObject(fileName); }
6.4 Using Serialization With Care Serialization is an easy and useful way to store objects in a stream, if you know the side effects and how to deal with them. This section discusses some of these side effects in order to give you a feeling of how you should use serialization to solve persistence problems.
6.4.1 Efficiency with Specialized Serialization The serialization implementation that comes with Java 1.1 is generic. So for example, it stores information about the attributes class and also the version
176
Java Thin-Client Programming
of the used classes. For some cases, serialization could be implemented more efficiently by not storing and handling this kind of information. A way to get this higher efficiency is to specialize the implementation of serialization for one or more classes of objects. The easiest way to do this is to use the generic serialization first and replace it later, if necessary, with a more specific one.
6.4.2 Only Serialize the Objects You Need The default implementation of serialization is to store each object attribute that is not defined as transient. If an attribute has not been defined as transient and is referring to another object, it can result in a serialization of a whole tree of objects, needed or not. In order not to serialize the complete tree of aggregated objects, it might be worthwhile to isolate smaller objects and make them transient. The alternative approach to solve this behavior is not to use a specialized extension of the default serialization. In the specialized version, it is possible to decide what to serialize and what not.
6.4.3 Using Different Versions of the Same Class There are different scenarios where the serialized data stream of an object is tries to get loaded into a different version of the same class. This can happen in the following two ways: • Two applications (for example a client and a server) are running with two different versions of the same class in different JVMs and are trying to move an instance of this class from one environment to the other, using serialization (for example using RMI). • Objects have been stored consistently into a file or database using serialization and are now trying to get loaded, using a newer version of the same class.
Serialization
177
Figure 73. Exception When Class Gets Deserialized into a New Class Version
Java differentiates versions of a class by a serial UID. The UID is a 64-bit hash value that gets generated out of: • The class name • Implemented interface names • Method names • Field names Even if two versions of a class have the same fully qualified name, they will differ from each other in the serial UID. For classes that implement the Externalizable interface, where the developer has to keep track of where and how object data in the class is to be saved, the only data in the stream that has to be changed is the serial UID, in order to be able to read the older version of the stream. For classes that implement the Serializable interface, Java provides a way to allow newer versions of a class to read the stream of an older class and also to write back the stream in a way to stay compatible with the old version. Because the newer class has the responsibility to keep compatibility to the
178
Java Thin-Client Programming
older version, it has to follow some rules that are described in the following Table 9 and Table 10: Table 9. Compatible Changes for Serialization
Compatible Change
Description
Appending additional data to the stream
All the data in the stream after the data that a class is expecting is ignored.
Adding fields
Extra fields in the stream are ignored and missing fields are initialized to the default value.
Changing the access modifiers of a field (public, private, protected, default)
Access modifiers have no influence on the ability to serialize a class
Changing a static field to non-static
Same as adding a field.
Changing a transient field to non-transient
Same as adding a field.
Table 10. Incompatible Changes for Serialization
Incompatible Changes
Description
Removing fields
Missing fields will be initialized to the default value, which may cause the serialization to fail.
Changing the position of the class in the class hierarchy
The class name in the beginning of the stream contains the full identifier of the class. The class ID changes, making it incompatible for the previous class.
Changing a non-static field to static
Same as removing a field.
Changing a non-transient field to transient
Same as removing a field.
Changing the type of a field
The loading class will not be able to read the data for the field and will initialize it to the default value.
Removing data from the stream
Missing fields will be initialized to the default value, and missing data on readObject() will be raise an exception.
Newer versions of a class must also retain the same serial UID as the older version they wish to remain compatible with. The default generation of the serial UID can be suppressed, if serialVersionUID is already declared within the class: static final long serialVersionUID = 3554689432406741995L;
Serialization
179
To find out the hash code of the old class version, you can use the serialver tool that comes with the JDK:
C:\serial>serialver MyApp MyApp: static final long serialVersionUID = 3554689432406741995L
This tool can be either used on the command line as just shown or with a graphical user interface that is started when the -show option is specified as shown in Figure 74: c:\serial>serialver -show
Figure 74. Serialver With Option -show
In some cases where the evolution of a class does not allow to keep the compatibility with older versions, it might be considerable to migrate older versions of persistently stored object streams into the newer version of the class. The first approach that comes in mind to migrate the class streams as shown in Figure 75:
Figure 75. First Approach to Migrate a Class Stream
180
Java Thin-Client Programming
On the first look, this approach might appear as a good one, but on the second it is not. The reason for this lays in the fact that the application that is moving the data from the old object stream to the new one has to have the new and the old version of the class loaded at the same time. This is only possible, if the new class A’ is occupying a different place in the JVMs name space, meaning that the fully qualified name of the new class must be a different one that the older one. This results in an unacceptable situation where a lot of code in other classes referring to this class in the name space has to be modified manually.
Figure 76. Second Approach to Migrate a Class Stream in Two Steps
A second approach shown in Figure 76 is a work-around the fact, that a JVM can only have one version of a class active at the time. The migration is done in two steps, each step running a different JVM. For the first step, the JVM loads the older version of the class in order to be able to load the original
Serialization
181
object stream (1). The old object data is then moved into a temporary object (2). The temporary class can have a different name than the original one. Then this class has to be moved from the first JVM into the second, where the newer version of the original class is loaded. This JVM has to use the same version of the temporary class than the first one. Moving the temporary object can be done by serializing this object and moving its stream either through the file system (3 and 4), or direct using interprocess communication mechanisms (such as RMI or socket connections) to the other JVM. The second JVM is then performing the second step, moving the object data from the transferred temporary object to an instance of the final new version of the original class (5) and serializing it back to the persistent storage (6). Note
In the sample above, we ignored the fact that the actual implementation to copy data from one object to another rises some additional problems, especially if some data that has to be moved is defined as private. For those cases, the class has to implement a mechanism that allows to copy all its own data to (and from) another class.
6.4.4 Security and Integrity Be aware that an objects serialized stream is neither protected, nor save against modifications. This means that if a stream is stored into a file, everybody with read access to this file can read its contents. Fields that were declared as private in order not to expose their contents are readable as well. In order not to expose sensible data such as passwords, those fields should be declared as transient, so that their data does not get serialized. Also, everybody with write access to the file can search for certain data in the stream and change it. In order to protect this data integrity and content, you have either the possibility to control and restrict the access to those files, or you can implement a mechanism to encrypt and authenticate this data as discussed in Chapter 3, “Security Considerations” on page 65.
6.5 Reasons Not to Use Serialization Serialization is a very powerful mechanism in Java. However, it can have some significant impacts, so that you should carefully consider if you are going to use this mechanism to solve your specific problem. As a kind of
182
Java Thin-Client Programming
conclusion, this section is giving some reasons when you should think about alternative ways instead of using serialization. In general, serialization is not a magic solution to increase performance. In the example discussed earlier in this chapter, serialization might reduce initialization time, but on the other hand it increases the amount of data that has to be transferred to the client. If the network is not designed for those amounts of network traffic, these delays can easily outbid the time we saved for the initialization. Serialization is not necessarily a powerful way to implement object persistence. As mentioned earlier in this chapter, it lacks on management of concurrent write access — this has to be solved by the application designer. Serialization also requires writing to an instance of ObjectOutputStream. For some persistence, this can already be too specific. In those cases, serialization is not the best way to achieve object persistence; Java offers another interface that is extending the Serializable interface: Externalizable. This interface has the advantage that the way objects are stored or loaded have a less restrictive definition as they just need to implement the ObjectOutput or ObjectInput interface. This is another good way to define object persistence. When using the default implementation of serialization, it can result that the complete tree of aggregated objects get serialized, needed or not. As mentioned before in section 6.4, it is possible to adapt the conception in order to change this behavior. But for those cases where you cannot change the serialization of some objects, it should not be used to avoid that unnecessary and unused objects get stored. Of course, there are many other ways than serialization to transfer objects from the server to the client and vice versa. If you are aware of the pitfalls that come with serialization, you should be able to decide, if this technique fits to your problem or if you should look for one of many other different solutions.
Serialization
183
184
Java Thin-Client Programming
Chapter 7. Model View Patterns The purpose of this chapter is to introduce and understand the use of the model/view patterns in the context of an application made for an NC. We will first introduce the basic concepts, such as models, views and different patterns. Their use in the NC environment will be presented and then an example using RMI will illustrate the theory.
7.1 Model View Definition The model and View components are the main elements to be implemented when a developer wishes to split the data/operations objects part with the presentation objects.
7.1.1 The Model The definition of the model is the object or set of object directly related to the functionality of the domain. For example if the domain is relative to the Bank, an object of the domain could be an Account and one service could be to open the Account. Note that this functionality could be provided by another object.
7.1.2 The View The view definition used here will be the one related with the user interface. The view is a visual presentation to a user of the model data and functionality. It will use the Graphical User Interface (GUI). The view shows the data to the user. With a framework like the Java AWT classes, this means: • To build the view with a set of basic or other composite views. • To use values given by the model in order to adapt the drawing of the views to this model: For example, to draw a string field of the model in a text field of the view. • To update the views when the values of the model are changed: The way to do this without requiring the model to know the view is to have the model generate events when it knows that the view has changed. • To allow the user to interact with this model. The actions of the user involve events generated by the basic views. The events involve different actions in order to modify the data, release a
© Copyright IBM Corp. 1999
185
functionality of the model and not at least to browse through the links between the objects of the model. A set of views may use the same model using and querying different part of the object model.
7.1.3 Model View Separation The main goal of the model/view pattern is to split the business design in order to get the best reusability in the model and the best mobility and extensibility at the view level. The model will be specific to the domain containing the relevant data and methods acting on it. The views will interact back and forth with the model object using just what it needs. The coherence and unity is concentrated at the model level.
7.2 Patterns The pattern separation in different responsibility groups is done in two or three parts. The Model View Controller (MVC) will allow tree group objects functionality. The model view pattern is a reduction of the MVC pattern.
7.2.1 The Model View Controller Pattern This is a pattern that separates in three the tasks and responsabilities of an application. The view, the model and the controller will have different responsabilities. 7.2.1.1 The View of the MVC Pattern In the MVC pattern the view is only responsible for drawing a representation of the model. Two things are taken care by the view: 1. The view knows the model and how to get from it the texts or other data that will allow the view to draw the representation of this model: This view could be composed of subviews. Often, the responsibility to know what must be drawn is delegated to the subviews. For example a view could be a text field or a panel that includes a set of text fields. In this case, the panel could delegate to each subviews the task to know how to draw the model. So each subview will draw a part of the model. 2. In order to manage the changes of the model, there is a special mechanism that allows any object to signal to another object that it has changed: This mechanism is used by the view in order to know when the model has changed and then to update the drawing of this model.
186
Java Thin-Client Programming
More information can be found on this mechanism at the URL:
http://st-www.cs.uiuc.edu/users/smarch/st-docs/mvc.html 7.2.1.2 The Model of the MVC Pattern In the MVC pattern, the model will have the responsibility to provide services that provide the data required by the view in order to draw its representation. The model will use the generic mechanism in order to warn the interested objects such as the controllers that it has changed his state. It will provide services to the controller in order to execute the actions released by the user. 7.2.1.3 The Controller in the MVC Pattern The controller is responsible for managing the action of the user. It is typically mouse events and keyboard events. The controller knows the view to be able to control it. For example if a key is hit, the controller will know that and will ask its text field view to add the corresponding character. The controller knows the model in order to ask it services that must be released on some user actions. For example the controller of a menu will ask the model to execute the action that corresponds to the menu item clicked.
7.2.2 The Model View Pattern In the MVC pattern the controller plays a big part as a graphical and event controller. These functions are integrated in the GUI and, in fact, the MVC pattern is similar to the model view pattern. The view will interact directly with the model, the GUI and the operating system acting has a partial controller. In most cases the view is a collection of basics or composite views. The final view will have an additional layer of logic in order to route the different requests to the internal subsets views. The developer has to use her common sense and find a middle point between a monolithic, huge view and very granular set of aggregated views.
7.3 The MV Pattern in an NC Application We are looking at an NC application split between the client and the server. In this case, the MV pattern demonstrates its worth. The developer can have different strategies. We are assuming the application is sliced properly in a set of views and models. The views and the models can be aggregated or not. The developer will decide which part of the model should be in the server or in the client side. Complications arise when pieces of the model stay in the client side because it is necessary to ensure a multiple communication between the views in the client and the models in the server and also between the models in the client and the models in the server. In this case the models in the client side are acting also as a kind of view for the models in the
Model View Patterns
187
server side and at the same time are acting as a model for the views in the client.
7.4 Using RMI in a Model View Pattern This section will illustrate a simple example of the use of RMI so that an object on the server can be used on the client.
7.4.1 Definition of the Model The following model is of a person. The implementing bean consists of: • Two properties name() and age). • One method changeName() with a string as parameter, the new name. The bean has been defined using the tools of VisualAge Java that help to define a bean. So the definition of the class is: // // // // //
This is a sample Model, a person. Note that the fields has been generated by the VisualAge Java tools that help to create a bean. The comments and default values have been added manually
public class Person { String fieldName = "Jack"; // The name with a default value protected transient java.beans.PropertyChangeSupport propertyChange = new java.beans.PropertyChangeSupport(this); int fieldAge = 15; // The age with a default value }
The tools have generated the methods that correspond to the properties. The two properties generate events when their values change. The tools have also generated the code and methods that manage these events. Take care that in the window where you create the properties name and age, you check the checkbox with the label bound (firePropertyChange). This will enable the management of the change events of the values of these. properties.
188
Java Thin-Client Programming
7.4.2 The Method changeName() The method changeName() has been generated but with an empty body. So it must be defined. Just to illustrate the example, this method sets the property name with the new value. This will be executed on the server and could be a complex service executed. Then still on the server the property is set with a new value. This will trigger an event which will be retransmitted to the proxy on the client. This proxy will trigger on the client the corresponding event. The definition of the method is: /** * Performs the changeName method. * @param newName java.lang.String */ public void changeName(String newName) { /* Perform the changeName method. */ // Some computing... setName(newName); return; }
7.4.3 Creation of the Panel for the GUI The purpose is to create a Panel which will have: • Two Labels to show the age and the name of a person proxy. • A TextField to set a new name for Person. • A Button with the label Transmit New Name to execute the method setName() of Person .
7.4.4 Using the RMI tools for the PersonProxy Bean The goal is to generate the classes that will allow a person on the server and its proxy on the client. If one of the properties of the bean change the corresponding event on the client will be released for the proxy. The tools for RMI included in VisualAge allow to do that easily. In fact the proxy on the client will be a bean too. In order to have this proxy and the other classes, the process is simple using VisualAge Java: 1. Open the Workbench window: Menu Windows, Item Workbench
Model View Patterns
189
2. In the Projects page of the Workbench window, find your class, which is a bean. For the example this is the class Person. 3. Select this class (Person). 4. Open the popup menu. 5. In this popup menu select the submenu Tools 6. In this submenu Select the submenu Remote Bean Access 7. In this submenu click the item Create Proxy Beans...’ 8. A window will ask a name for the proxy class. This will be the name of the class of the proxy. Many other classes are generated, their names are deduced from this name. For the example the name chosen is PersonProxy. 9. Fill the other fields of this window as the package where the classes will be generated. 10.In this window, there is a checkbox with the label, Create RMI stub and skeleton for generated classes, check this checkbox if it isn’t checked. 11.Then click Finish. This will generate many classes. Some are required on the server, others on the client. The class PersonProxy has been generated too. This class is the bean to use on the client.
7.4.5 Visual Programming with the PersonProxy Bean The goal is to use the visual programming in order to program the Panel which will run on the client. A PersonProxy will be used, this will be the proxy of the person object on the server. In fact, this programming, by using the PersonProxy, has the same effect as using Person: 1. Add the PersonProxy bean, select Class and not Variable or Serialize in the window of creation of the bean. To select class will create automatically an instance of the proxy on the client. 2. Add two connections from the properties name and age of the PersonProxy to the property text of each label. This will set the values of the labels and these values will be updated when the event change of the properties is released.
190
Java Thin-Client Programming
3. Add a connection from the event actionPerformed() of the button Transmit New Name to the method changeName() of the PersonProxy bean, changeName. 4. Connect the property text of the TextField to the parameter arg1 of the previous connection. 5. Connect the property parentComponent of the PersonProxy to the property this of the Panel. Then the visual programming is done.
7.4.6 Settings of the PersonProxy Bean The settings of the bean must be filled in order to set the IP name of the server, which port will be used and so on. In fact the default setting could be used for this example. For the example, the port 1099 is used. The following example will use the same machine as the client, but the principles are the same. To run the example locally, you have to start the RMI registry and then create an instance of Person: 1. Select the Options... menu item from the Workspace pulldown menu and go to the RMI page. 2. There you must use the same port number as the one used for the settings of the PersonProxy: 1099. 3. Run the registry by clicking on the start button. Then you must run the Remote Instance Manager and create an instance of Person. 4. In the Workbench, select the class PersonProxy which is the server-side server proxy. 5. From the pop-up menu, select Tools, then Remote Bean Access and Instantiate Bean in Server. The RMI registry and RemoteObject instance Manager are started, and the instance Person of the server bean are created.
Model View Patterns
191
192
Java Thin-Client Programming
Chapter 8. Profiling Profiling helps programmers optimize their applications by identifying performance bottlenecks and hotspots. In this chapter we take a look at some of the tools that are currently available for Java. Unfortunately, at the time of writing, there are no tools that allow profiling to take place on the IBM Network Station, so we have to make do with producing results in the development environment and then assuming that the architecture of the NC will not have a significant impact.
8.1 Profiling Pitfalls Before we look at the profiling tools, here are a few words of warning about profiling Java programs, and in particular, Java programs destined for the NC. • The NC has its own Java VM implementation with its own unique performance characteristics: An optimization implemented on a different VM could well cause problems on the NC. As there are currently no profiling tools that run directly on the NC, the only way to have some measure of confidence that an optimization is having the desired effect is to manually insert profiling code into your application. • Java profiling tools often have their own implementation of the Java VM: This means that we are never measuring the performance of our applications in the actual environment in which they will be deployed. • Some profiling tools (especially java_g.exe that comes with the JDK) seem to affect the things that we are trying to measure. • Profiling results may differ from one version of the JDK to another: Once again this is beyond the control of the programmer, and it serves to highlight the basic Java profiling philosophy which is, "Write once - profile everywhere". • Optimizations generally make programs more complex and therefore harder (and costlier) to debug and maintain. Given the above points it is still worth profiling our programs. We will still find bottlenecks and hotspots caused by weaknesses in our program designs, and at the very least, we will gain a greater understanding of how our program executes.
© Copyright IBM Corp. 1999
193
8.2 Sample Application The sample Java application that we will use to demonstrate the profiling tools is found in ...\com\ibm\austin\itsc\jalapeno\profiling\Profile.java. This program is simply a test harness that creates and tests instances of the ProfileMe class in ...\com\ibm\austin\itsc\jalapeno\profiling\ProfileMe.java which is shown in its entirety in Figure 77: package com.ibm.austin.itsc.jalapeno.profiling; import java.util.*; class ProfileMe { ProfileMe(int n) { // Initialize the hash table. h = new Hashtable(n); for (int i = 0; i < n; i++) { Integer I = new Integer(i); h.put(I, I); } } public void doSomething(int n) { for (int i = 0; i < n; i++) { // Do a lookup. h.get(new Integer(i)); } } private Hashtable h; } Figure 77. ProfileMe Class Definition
8.3 JInsight JInsight is profiling tool from IBM research that provides a visualization of Java programs. It shows object population, messages, garbage collection, CPU and memory bottlenecks, thread interactions, and deadlocks. Using
194
Java Thin-Client Programming
JInsight program behavior can be monitored from several perspectives, and it can also help to reveal memory leaks and their causes. JInsight consists of two distinct components: 1. An instrumented VM (distributed as a Windows executable) that produces trace files containing information about program execution. JInsight cannot use any other VM to produce JInsight visualizations. Lies, More Liesand Profilers
Because JInsight uses its own specialized VM we have to be cautious when drawing any conclusions from the results that it produces. In most cases JInsight is a useful tool that can help improve program performance, but it is only by testing on the target VM that you can be totally sure that any optimizations are really worthwhile. 2. A visualizer (written in 100% Pure Java) gives a graphical replay of a program execution using the data in a trace file.
8.3.1 Installing JInsight Follow the steps below to download and install JInsight. At the time of writing the latest version is 1.0. 1. Go to the IBM Alphaworks Web site and select the JInsight link: http://www.alphaworks.ibm.com/alphapreview_tools
2. Download and run the installation program:
jinsight1.0-install.exe 3. If you choose an installation directory other than the default one: (C:\jinsight) make sure that there are no spaces in the directory name (for example if you want to install JInsight into C:\Program Files\.JInsight then enter C:\Progra~1\JInsight in the installation dialog box). 4. JInsight will overwrite the following files in your JDK installation, but it keeps copies of the original versions:
• java_g.exe • javai_g.dll • javai_g.lib • javaw_g.exe
Profiling
195
5. Add the JInsight classes (contained in the file jinsight.zip ) to your class path: set CLASSPATH=%CLASSPATH%;C:\Progra~1\JInsight\jinsight.zip
6. Try starting JInsight and loading one of the sample trace files: C:\> jinsight C:\Progra~1\JInsight\sampleTraces\TwoThreadsTest.trc
8.3.2 Creating a Trace File In this section we will create a trace file for the sample Java program called ProfileMe (See Figure 77 on page 194). To run the instrumented VM, type the following at a command prompt: C:\> cd ...\com\ibm\austin\itsc\jalapeno\profiling C:\> set JINSIGHT=YES C:\> java_g -tm com.ibm.austin.itsc.jalapeno.profiling.Profile SimpleTest.dat
The trace file that is created is called: com_ibm_austin_itsc_jalapeno_profiling_Profile.trc
8.3.3 Visualizing Trace Files The Jinsight visualizer is started as follows: C:\> jinsight com_ibm_austin_itsc_jalapeno_profiling_Profile.trc
There are six views that you can choose within Jinsight depending on what it is that you are attempting to measure. Most of the views have multiple configuration options and for detailed information on how to use them please see the HTML documentation that is part of the JInsight distribution. 1. Histogram View This view is really two subviews that show instances and methods grouped by class, and indicates their level of activity. Menu options allow you to color the views by cumulative time, number of calls and number of threads, and the right mouse button gives you access to cross-reference information about the instances and methods. Figure 78 on page 197 shows the instance subview for the trace file produced in section 8.3.2. Because the histogram view displays instances and methods so clearly, it is often the best place to select methods and instances to be examined in other views.
196
Java Thin-Client Programming
Figure 78. Histogram View
2. Execution View This view shows an overview of communication among objects per thread as a function of time. It gives a birds-eye view of the entire execution and when fully zoomed out shows at a glance which threads are active, whe,n and how much garbage collection is going on, and any hotspots of activity. This view is best seen in action and so we do not reproduce it here. 3. Invocation Browser View This view allows you to browse every call of a selected method or every message to a selected object, showing all subsequent communication as a function of time. To use this view, you first have to select an instance or method using the histogram view (Figure 78 above). Figure 79 on page 198 shows the Invocation browser view for the trace file produced in section 8.3.2 on page 196.
Profiling
197
Figure 79. Invocation Browser View
4. Execution Pattern View This view summarizes the information in the Invocation browser view and shows recurring patterns of communication arising from a selected method, each displayed as a function of time. To use this view, you first have to select a method in another view such as the histogram view shown in Figure 78 on page 197. Figure 80 on page 199, shows the execution pattern view for the trace file produced in section 8.3.2 on page 196.
198
Java Thin-Client Programming
Figure 80. Execution Pattern View
5. Call Stack View This is another view that is best seen in action. It displays the call stack for each thread of execution showing the name of each method in the stack. 6. Reference Pattern View This view shows patterns of references between objects in varying detail, which is useful for studying data structures and finding memory leaks.
Profiling
199
Figure 81 shows the reference pattern view with the ProfileMe instance selected for the trace file produced in section 8.3.2 on page 196.
Figure 81. Reference Pattern View
8.3.4 Summary JInsight is a useful tool that gives clear visual representations to many aspects of program execution. It is quite a complex tool and the best way to become familiar with is to test it out on programs that you already understand. As mentioned previously, be aware that JInsight uses its own version of the Java VM, and hence compatibility issues between it and your target VM may occur.
8.4 JProbe JProbe is a commercial profiler produced by KL Group Inc. JProbe is based internally onJDK1.1.5 and it can be downloaded free for a 15 day evaluation period. JProbe is similar to JInsight in that it allows you to produce trace information for individual invocations of your program, and then later inspect the data from a number of different view points.
8.4.1 Installing JProbe Follow the steps below to download and install JProbe. At the time of writing the latest version is 1.1.1.
200
Java Thin-Client Programming
1. Go to the JProbe homepage at: http://www.klg.com/jprobe/
2. Follow the links to the evaluation download. 3. Download and run the installation program. JProbeProfiler-v1.1.1.exe
4. Download the documentation in PDF format. JProbe.pdf
5. To make sure that JProbe was installed successfully, start JProbe (the installation creates a shortcut on your desktop) and load one of the example snapshots.
8.4.2 Creating a Snapshot In this section we will create a JProbe snapshot for the sample Java program introduced in section 8.2 on page 194. 1. Start JProbe and choose the option to run an application or applet. 2. Browse for the class file and select: ...\com\ibm\austin\itsc\jalapeno\profiling\Profile 3. For the application arguments enter: SimpleTest.dat
4. JProbe will fill in other values in the dialog box automatically, and you can leave these items unchanged. 5. Hit the OK button to run the program and produce a snapshot. 6. While the program is executing JProbe displays the memory usage information shown in Figure 82 on page 202 (this figure is an example only - the memory usage of our test application barely registers).
Profiling
201
Figure 82. JProbe Memory Usage
8.4.3 Analyzing the Snapshot When your program finishes JProbe will automatically display the call graph window shown in Figure 83 on page 203. This gives a graphical representation of your application and the color coding allows you to quickly see where time is being spent.
202
Java Thin-Client Programming
Figure 83. JProbe Call Graph
The Call Graph window has many options and we will not attempt to describe them in detail here. For example, the graph can be colored by cumulative time, method time, number of calls, and many others. Double-clicking on a method in this window brings up the method detail window shown in Figure 84 on page 204.
Profiling
203
Figure 84. JProbe Method Detail
Using the view menu in the call graph window you can also select the Method List which is a tabular list of methods that includes information such as package and file location, number of calls, cumulative time and more. The Method List is shown in Figure 85 on page 205.
204
Java Thin-Client Programming
Figure 85. JProbe Method List
8.4.4 Summary JProbe is a commercial product and, as you would expect, has a well-designed user interface and presents profiling information in a clear manner. It is not obvious however, how the GUI approach will scale to profiling more complex applications.
8.5 JavaSoft JDK The JavaSoft JDK comes complete with its own profiling tool in the form of the java_g.exe executable. This tool produces profiling information for every method, giving you: 1. The name of the method 2. How many times the method was called
Profiling
205
3. The name of the caller 4. The total time spent in the method in milliseconds
8.5.1 Creating the Profile Information File In this section we will create profiling information for the sample Java program introduced in section 8.2, “Sample Application” on page 194. To run the JDK profiler, type the following at a command prompt: C:\> cd ...\com\ibm\austin\itsc\jalapeno\profiling C:\> java_g -prof:prof.dat com.ibm.austin.itsc.jalapeno.profiling.Profile SimpleTest.dat
A sample of the output file prof.dat is shown in Figure 86:
count callee caller time 101 java/lang/Integer.hashCode()I java/util/Hashtable.put(...)Ljava/lang/Object; 1 100 java/util/Hashtable.get; com/ibm/.../profiling/ProfileMe.doSomething(I)V 3 100 java/lang/Integer.hashCode()I java/util/Hashtable.getLjava/lang/Object; 0 ... ...
Figure 86. Output from the JDK Profiler
As you can see, the profiler does not produce the most beautifully formatted output in the world but the information is all there if you look hard enough.
8.5.2 Summary The JDK profiler provides a fairly coarse level of profiling information, and the output it produces is a little hard to read. However, it can be used to obtain rough estimates of the time your program spends in each method. Once again, be aware that the profiler VM is different from the deployment VM, and that the act of measuring performance can at times have a significant effect on the profiled application.
8.6 Conclusions The tools covered in this chapter provide the Java developer with a number of ways to analyze the performance of their applications. JInsight and JProbe both provide good graphical simulations of many aspects of program execution, while the JDK profiler provides a more coarse and terse level of timing information. As mentioned before, profiling and optimization are complex issues that depend greatly on the target VM, and this should not be underestimated. The
206
Java Thin-Client Programming
Java language may be cross-platform, but the VMs that implement the language differ significantly in many ways. The golden rule, as always, is write once and profile everywhere.
Profiling
207
208
Java Thin-Client Programming
Appendix A. Related Publications The publications listed in this section are considered particularly suitable for a more detailed discussion of the topics covered in this redbook.
A.1 International Technical Support Organization Publications For information on ordering these ITSO publications see “How to Get ITSO Redbooks” on page 215. • Java Thin-Client Programming for a Network Computing Environment, SG24-5115
• IBM Network Station: RS/6000 Notebook, SG24-2016
A.2 Redbooks on CD-ROMs Redbooks are also available on CD-ROMs. Order a subscription and receive updates 2-4 times a year at significant savings.
CD-ROM Title System/390 Redbooks Collection Networking and Systems Management Redbooks Collection Transaction Processing and Data Management Redbook Lotus Redbooks Collection Tivoli Redbooks Collection AS/400 Redbooks Collection RS/6000 Redbooks Collection (HTML, BkMgr) RS/6000 Redbooks Collection (PostScript) RS/6000 Redbooks Collection (PDF Format) Application Development Redbooks Collection
Subscription Number SBOF-7201 SBOF-7370 SBOF-7240 SBOF-6899 SBOF-6898 SBOF-7270 SBOF-7230 SBOF-7205 SBOF-8700 SBOF-7290
Collection Kit Number SK2T-2177 SK2T-6022 SK2T-8038 SK2T-8039 SK2T-8044 SK2T-2849 SK2T-8040 SK2T-8041 SK2T-8043 SK2T-8037
A.3 Other Publications These publications are also relevant as further information sources: • Java in a Nutshell. O’Reilly. 1997. • Java Examples in a Nutshell. O’Reilly. 1997. • Java Distributed Computing. O’Reilly. 1998. • Core Java 1.1: Fundamentals - Volume 1 . Prentice Hall. 1997.
© Copyright IBM Corp. 1999
209
• Core Java 1.1: Advanced Features - Volume 2. Prentice Hall. 1997. • Java Thin-Client Programming for a Network Computing Environment . Prentice Hall. 1998. • Design Patterns. Addison-Wesley Publishing Company. 1995 • Applying UML and Patterns. Prentice Hall. 1998. • Internet Cryptography. Addison-Wesley Publishing Company. 1997. • Java Security. Wiley Computer Publishing. 1997. • Secure Commerce on the Internet . Academic Press. 1997. • Network & Internet Security. Academic Press. 1996. • Programming with Posix Threads. Addison-Wesley Publishing Company.1997. • Programming with Threads . Prentice Hall. 1995.
210
Java Thin-Client Programming
Appendix B. Special Notices This publication is intended to help program designers develop applications for thin Java clients such as the IBM Network Station 1000. The information in this publication is not intended as the specification of any programming interfaces that are provided byJavasoft. See the PUBLICATIONS section of the IBM Programming Announcement for the IBM Network Station and Sun’s Java for more information about what publications are considered to be product documentation. References in this publication to IBM products, programs or services do not imply that IBM intends to make these available in all countries in which IBM operates. Any reference to an IBM product, program, or service is not intended to state or imply that only IBM's product, program, or service may be used. Any functionally equivalent program that does not infringe any of IBM's intellectual property rights may be used instead of the IBM product, program or service. Information in this book was developed in conjunction with use of the equipment specified, and is limited in application to those specific hardware and software products and levels. IBM may have patents or pending patent applications covering subject matter in this document. The furnishing of this document does not give you any license to these patents. You can send license inquiries, in writing, to the IBM Director of Licensing, IBM Corporation, 500 Columbus Avenue, Thornwood, NY 10594 USA. Licensees of this program who wish to have information about it for the purpose of enabling: (i) the exchange of information between independently created programs and other programs (including this one) and (ii) the mutual use of the information which has been exchanged, should contact IBM Corporation, Dept. 600A, Mail Drop 1329, Somers, NY 10589 USA. Such information may be available, subject to appropriate terms and conditions, including in some cases, payment of a fee. The information contained in this document has not been submitted to any formal IBM test and is distributed AS IS. The information about non-IBM ("vendor") products in this manual has been supplied by the vendor and IBM assumes no responsibility for its accuracy or completeness. The use of this information or the implementation of any of these techniques is a customer responsibility and depends on the customer's ability to evaluate and integrate them into the customer's operational environment. While each item may have
© Copyright IBM Corp. 1999
211
been reviewed by IBM for accuracy in a specific situation, there is no guarantee that the same or similar results will be obtained elsewhere. Customers attempting to adapt these techniques to their own environments do so at their own risk. Any pointers in this publication to external Web sites are provided for convenience only and do not in any manner serve as an endorsement of these Web sites. Any performance data contained in this document was determined in a controlled environment, and therefore, the results that may be obtained in other operating environments may vary significantly. Users of this document should verify the applicable data for their specific environment. The following document contains examples of data and reports used in daily business operations. To illustrate them as completely as possible, the examples contain the names of individuals, companies, brands, and products. All of these names are fictitious and any similarity to the names and addresses used by an actual business enterprise is entirely coincidental. Reference to PTF numbers that have not been released through the normal distribution process does not imply general availability. The purpose of including these reference numbers is to alert IBM customers to specific information relative to the implementation of the PTF when it becomes available to each customer according to the normal IBM PTF distribution process. The following terms are trademarks of the International Business Machines Corporation in the United States and/or other countries: AS/400 BookManager CICS DB2 IBM S/390 SanFrancisco
RS/6000 PROFS AIX OS/2 OS/400 VisualAge 400
The following terms are trademarks of other companies: C-bus is a trademark of Corollary, Inc. in the United States and/or other countries. Java and all Java-based trademarks and logos are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and/or other countries.
212
Java Thin-Client Programming
Microsoft, Windows, Windows NT, and the Windows logo are trademarks of Microsoft Corporation in the United States and/or other countries. PC Direct is a trademark of Ziff Communications Company in the United States and/or other countries and is used by IBM Corporation under license. ActionMedia, LANDesk, MMX, Pentium and ProShare are trademarks of Intel Corporation in the United States and/or other countries. (For a complete list of Intel trademarks see www.intel.com/dradmarx.htm) UNIX is a registered trademark in the United States and/or other countries licensed exclusively through X/Open Company Limited. Other company, product, and service names may be trademarks or service marks of others.
Special Notices
213
214
Java Thin-Client Programming
How to Get ITSO Redbooks This section explains how both customers and IBM employees can find out about ITSO redbooks, CD-ROMs, workshops, and residencies. A form for ordering books and CD-ROMs is also provided. This information was current at the time of publication, but is continually subject to change. The latest information may be found at http://www.redbooks.ibm.com/.
How IBM Employees Can Get ITSO Redbooks Employees may request ITSO deliverables (redbooks, BookManager BOOKs, and CD-ROMs) and information about redbooks, workshops, and residencies in the following ways: • Redbooks Web Site on the World Wide Web http://w3.itso.ibm.com/
• PUBORDER – to order hardcopies in the United States • Tools Disks To get LIST3820s of redbooks, type one of the following commands: TOOLCAT REDPRINT TOOLS SENDTO EHONE4 TOOLS2 REDPRINT GET SG24xxxx PACKAGE TOOLS SENDTO CANVM2 TOOLS REDPRINT GET SG24xxxx PACKAGE (Canadian users only)
To get BookManager BOOKs of redbooks, type the following command: TOOLCAT REDBOOKS
To get lists of redbooks, type the following command: TOOLS SENDTO USDIST MKTTOOLS MKTTOOLS GET ITSOCAT TXT
To register for information on workshops, residencies, and redbooks, type the following command: TOOLS SENDTO WTSCPOK TOOLS ZDISK GET ITSOREGI 1998
• REDBOOKS Category on INEWS • Online – send orders to: USIB6FPL at IBMMAIL or DKIBMBSH at IBMMAIL Redpieces For information so current it is still in the process of being written, look at "Redpieces" on the Redbooks Web Site (http://www.redbooks.ibm.com/redpieces.html). Redpieces are redbooks in progress; not all redbooks become redpieces, and sometimes just a few chapters will be published this way. The intent is to get the information out much quicker than the formal publishing process allows.
© Copyright IBM Corp. 1999
215
How Customers Can Get ITSO Redbooks Customers may request ITSO deliverables (redbooks, BookManager BOOKs, and CD-ROMs) and information about redbooks, workshops, and residencies in the following ways: • Online Orders – send orders to: In United States In Canada Outside North America
IBMMAIL usib6fpl at ibmmail caibmbkz at ibmmail dkibmbsh at ibmmail
Internet [email protected] [email protected] [email protected]
• Telephone Orders United States (toll free) Canada (toll free)
1-800-879-2755 1-800-IBM-4YOU
Outside North America (+45) 4810-1320 - Danish (+45) 4810-1420 - Dutch (+45) 4810-1540 - English (+45) 4810-1670 - Finnish (+45) 4810-1220 - French
(long distance charges apply) (+45) 4810-1020 - German (+45) 4810-1620 - Italian (+45) 4810-1270 - Norwegian (+45) 4810-1120 - Spanish (+45) 4810-1170 - Swedish
• Mail Orders – send orders to: IBM Publications Publications Customer Support P.O. Box 29570 Raleigh, NC 27626-0570 USA
IBM Publications 144-4th Avenue, S.W. Calgary, Alberta T2P 3N5 Canada
IBM Direct Services Sortemosevej 21 DK-3450 Allerød Denmark
• Fax – send orders to: United States (toll free) Canada Outside North America
1-800-445-9269 1-800-267-4455 (+45) 48 14 2207
(long distance charge)
• 1-800-IBM-4FAX (United States) or (+1) 408 256 5422 (Outside USA) – ask for: Index # 4421 Abstracts of new redbooks Index # 4422 IBM redbooks Index # 4420 Redbooks for last six months • On the World Wide Web Redbooks Web Site IBM Direct Publications Catalog
http://www.redbooks.ibm.com http://www.elink.ibmlink.ibm.com/pbl/pbl
Redpieces For information so current it is still in the process of being written, look at "Redpieces" on the Redbooks Web Site (http://www.redbooks.ibm.com/redpieces.html). Redpieces are redbooks in progress; not all redbooks become redpieces, and sometimes just a few chapters will be published this way. The intent is to get the information out much quicker than the formal publishing process allows.
216
Java Thin-Client Programming
IBM Redbook Order Form Please send me the following: Title
First name
Order Number
Quantity
Last name
Company Address City
Postal code
Country
Telephone number
Telefax number
VAT number
Card issued to
Signature
Invoice to customer number Credit card number
Credit card expiration date
We accept American Express, Diners, Eurocard, Master Card, and Visa. Payment by credit card not available in all countries. Signature mandatory for credit card payment.
217
218
Java Thin-Client Programming
Index GIOP 124 IDL 124, 125 IIOP 124 Interface Definition Language 124, 125 Internet Inter-ORB Protocol 124 Naming Service 140 Object Adapter 124 object references 130 Object Service 122 ORB 123 ORB Interface 124 ORBacus 125, 126 Skeletons 135 skeletons 124 Stubs 135 stubs 124
A Abstract Syntax Notation 1 107 Ada 124 Application Interfaces 122 Application Server 55 ASN1 107 Authentication 105 Authors xiii
B Basic Object Adaptor 133 BOA 133 bottlenecks 194
C C 124 C++ 124 CCITT 105, 107 cert.dir 110 cert_id.dir 114 Certificates 106 certificates 105 Certification Authorities 106 CGI 34 chain of trust 105 ciphertext 89 CLASSPATH 60 cleartext 89 COBOL 124 Common Facilities 123 Common Gateway Interface 34 Common Object Request Broker Architecture 121 CORBA 121 Application Interface 122 Basic Object Adaptor 133 BOA 133 C++ 126 Client 124 Common Facilities 123 DII 124 Domain Interfaces 123 DSI 124 Dynamic Invocation Interface 124 Dynamic Skeleton Interface 124 General Inter-ORB Protocol 124
© Copyright IBM Corp. 1999
D Data Encryption Standard 89 Deadlock 18 Delegation 8 Digital Signature Algorithm 95 Digital Signatures 89 DII 124 Domain Interfaces 123 Domino Go 55 DSA 95, 108 DSI 124 Dynamic Invocation Interface 124 Dynamic Skeleton Interface 124
F Feedback xiv
G General Inter-ORB Protocol 124 GIOP 124
H HotJava 107, 115
I IBM WebSphere Application Server 55 Identities 106 identity 106
219
identitydb.obj 108 IDL 124, 125 IIOP 124 Instance Pools 156 Interface Definition Language 124, 125 Internet Inter-ORB Protocol 124 issuer 115 issuer.cert 115
J java_g javakey JInsight JProbe
193, 205 105, 108 194 200
M MD5 106, 108 Model/View 185 Mutex 19 Mutexe 16
N nmake 127
O Object Adapter 124 Object Management Architecture 121 Object Management Group 121 Object Recycling 151 Instance Pools 156 pooling 156 Object Services 122 OMA 121 OMG 121 ORB 123 ORB Interface 124 ORBacus 125
P Parallelism 2 pedigree 106 Perl 124 PGP 95, 105 PKCS 89 Preface xiii Pretty-Good-Privacy 95 Principals 106
220
Java Thin-Client Programming
Priority 15 private key 89 Profiling 193 bottlenecks 194 Call Graph 203 Call Stack View 199 Execution Pattern View 198 Execution View 197 Histogram View 196 Invocation Browser View 197 java_g 193, 205 JInsight 194 JProbe 200 Pitfalls 193 Reference Pattern View 199 Trace File 196 public key 89 Public Key Cryptography Standard 89 Python 124
R race condition 22 Read-Write Mutex 30 Remote Method Invocation 121 RMI 121, 124 rmic 125 skeletons 125 stubs 125 rmic 125 RSA 95
S Security Abstract Syntax Notation 1 107 ASN1 107 Authentication 105 CCITT 105, 107 cert.dir 110 cert_id.dir 114 certificates 105, 106 certification authorities 106 chain of trust 105 ciphertext 89 cleartext 89 Data Encryption Standard 89 digital signature algorithm 95 digital signatures 89 DSA 95, 108
guarantor 106 HotJava 107, 115 identities 106 identity 106 identitydb.obj 108 issuer.cert 115 javakey 105, 108 key distribution 89 MD5 106, 108 modulus 95 pedigree 106 PGP 95, 105 PKCS 89 Pretty-Good-Privacy 95 principals 106 public key 89 Public Key Cryptography Standard 89 RSA 95 signature.file 112 signed applets 108, 115 Triple-DES 89, 95 trusted applet 108 Verisign 106 X.500 107 X.509 105, 107 X.509 certificate 107 Serializable 165 Serialization Compatible Changes 179 efficiency 176 Integrity 182 persistency 166 pre-initialization 166 Security 182 Serializable interface 165 serialver 180 serialVersionUID 179 Versioning 177 serialver 180 serialVersionUID 179 Servlet Configuration 61 Monitoring 62 Servlets 34 servlets 55 signature.file 112 Signed Applets 108, 115 SingleThreadModel 35 Skeletons 135
Smalltalk 124 stubs 135 Synchronization 6 synchronized 19
T TCL 124 Thread 1 Thread Groups 16 Thread Pools 53 Thread Safety 3 Thread states 11 Thread.notify() 13 Thread.notifyAll() 13 Thread.resume() 13 Thread.sleep() 12 Thread.start 5 Thread.suspend() 12 Thread.wait() 13 Threading 1 applet 2 application 2 Asynchronous Method Invocation 46 blocked 11, 12 Callbacks 51 circular buffer 27 Condition Variables 20 created 11 dead 11 deadlock 18 Delegation 8 groups 16 heapspace 3 interrupted 11 java.lang.runnable 1 java.lang.Thread 1 java.lang.ThreadGroup 1, 16 lock 17 monitor 14 mutex 16, 19 mutual exclusion 10 ownership 15 priority 15 Pseudo-thread 47 race condition 22 read-write mutex 30 run() method 5 runnable 11
221
running 11 servlets 16 spurious wakeups 22 Synchronization 6 synchronized 1, 15, 19 synchronized keyword 7 Thread Pools 53 ThreadDeath exception 11 Tutorial 1 unlock 17 zombie 12 Threads java.lang.runnable 5 java.lang.Thread 5 Time-slicing 2 Triple-DES 89, 95 Trusted Applet 108
V Verisign 106
W WebSphere 55 administration 57 CLASSPATH 60 Documentation 57 Installation 55
X X.500 107 X.509 105, 107 X.509 Certificates 107
222
Java Thin-Client Programming
ITSO Redbook Evaluation Java Thin-Client Programming SG24-5118-00 Your feedback is very important to help us maintain the quality of ITSO redbooks. Please complete this questionnaire and return it using one of the following methods: • Use the online evaluation form found at http://www.redbooks.ibm.com • Fax this form to: USA International Access Code + 1 914 432 8264 • Send your comments in an Internet note to [email protected] Which of the following best describes you? _ Customer _ Business Partner _ Solution Developer _ None of the above
_ IBM employee
Please rate your overall satisfaction with this book using the scale: (1 = very good, 2 = good, 3 = average, 4 = poor, 5 = very poor) Overall Satisfaction
__________
Please answer the following questions: Was this redbook published in time for your needs?
Yes___ No___
If no, please explain:
What other redbooks would you like to see published?
Comments/Suggestions:
© Copyright IBM Corp. 1999
(THANK YOU FOR YOUR FEEDBACK!)
223
Java Thin-Client Programming
SG24-5118-00 Printed in the U.S.A.
SG24-5118-00