20
Sat, Apr
5 New Articles

Java Journal: Optimize Your I/O with NIO

Java
Typography
  • Smaller Small Medium Big Bigger
  • Default Helvetica Segoe Georgia Times

The number one criticism that Java has faced since its inception is performance. We are in an industry that's obsessed, often to a fault, by performance. So, with each release of its Java JDK, Sun has not only introduced new features but also significantly improved performance.

In JDK 1.4, Sun addressed two areas of performance, one obviously critical and one not so obviously critical. The obviously critical need was to improve I/O performance, which is the focus of this month's Java Journal. The less obviously critical need was to improve the performance of Reflection. Reflection has become an integral part of most Java frameworks as well as most tools such as IDEs, so the bottlenecks that plagued the early implementation of the Reflection API were very costly--though most developers were blissfully unaware of them. But the improvement to the Java Reflection API is a topic for another article. So on to NIO, which stand for New I/O. I have heard it pronounced two ways. The first way sounds like "neo." The other way, each letter is pronounced, like en-eye-oh. The former is what I have most often heard, and being a Matrix fan, it's what I prefer.

Setting the Stage

Why is I/O performance so important? Well, its all about orders of magnitude. If we look at a typical computer system--whether it be from the past or present--we find a common ratio between CPU performance and I/O performance. Specifically, CPU performance is at least two orders of magnitude faster than I/O performance. Even a slight increase in I/O performance can have a drastic, positive effect on overall performance. Anybody who has used Java to do large amounts of I/O pre-JDK 1.4 knows that there was significant room for improvement. In my business, digital video, we are usually dealing with files in multi-gigabyte range, and processing batches of files of that order of magnitude can be painfully slow. So why were the pre-JKD 1.4 implementations of I/O so slow? It all ties back to one of my favorite topics, tradeoffs. The classic tradeoff in computing is time vs. space. We can almost always do something in less time if we have more space, and vice versa. Another tradeoff, which is less understood, is performance vs. understandability.

Let's examine an extreme example. Say we need to find all the prime numbers between 2 and 1,000,000. We could write two programs to do this, one in assembly and one in Java. Assuming we have two equally talented programmers in their own respective fields doing the work, the assembly language program is going to be faster. Why? Because layers of abstraction are expensive. The assembly programmer is dealing with one layer of abstraction at most (even assembly isn't quite machine code). The Java programmer is dealing with many layers of abstraction--Java code mapping to Java byte code mapping to machine code, and this still doesn't take into account the several layers of abstraction that the Java programmer is probably taking advantage of within the Java language itself. What these layers of abstraction buy us is understandability.

OK, some will argue that if they are equally talented, the assembly language programmer will be able to read the assembly code as easily as the Java programmer can read the Java code. I agree. However, there are two interesting scenarios to think about. First, the assembly programmer can probably read and understand the Java program, but the reverse will not be true and will probably just lead to the mass consumption of Advil and comments like "this is the exact reason why I program in Java." Second, let's say our application is something slightly more complicated than a prime number locator, something like income tax preparation software, which if written in assembly might be as incomprehensible as the actual tax law that it is based on.

All this said, what we want to do is look for tradeoffs where we give a little on one side but gain more on the other.

When we are dealing with I/O, there are two things we want to do or not do as the case may be. First, we want to actually move data as few times as possible. This sounds rather intuitive but can actually be difficult to implement. Second, when we do move data, we want to move it in the most efficient manner possible. To accomplish efficient moves, we want to move data that is in pieces that are multiples of page sizes. For NIO, this is exactly what Sun did. Without getting into all the gory details, I'll just say that Sun collapsed the number of abstraction layers between Java code and machine code.

Pre-JDK 1.4, the solution to I/O and other Java performance problems came in the form of the Java Native Interface (JNI), which allows you to call non-Java, platform-specific code from within a Java Virtual Machine. JNI is still a necessary part of the JDK 1.4, but great care should be exercised when using JNI. After all, JNI calls do tie Java applications to specific operating systems, which breaks the fundamental Java paradigm of Write Once Run Anywhere (WORA).

Java NIO API

NIO gives us many new technologies as well as new flavors of old favorites. This article focuses on buffers, but we will also dabble a little in channels, file locking, memory mapped files, sockets, selectors, and character sets. NIO was the end product of Java Specification Request (JSR) 51. In addition to the core I/O technologies listed above, JSR 51 also produced a powerful regular expression engine. However, since regular expressions aren't really an I/O topic, I will not go into details here.

Buffers

NIO gives us a very robust hierarchy of Buffer classes. Buffers are the basic building blocks of I/O. The basic operations for buffers are filling and draining. The NIO Buffer class hierarchy consists of a single abstract base class fittingly enough called "Buffer." For each of the primitive Java types--byte, char, double, float, int, long, and short--there is a derived class, where the class name is of the form . Buffer, in addition to the ByteBuffer class, has a derived MappedByteBuffer class. Every Buffer class, regardless of type, has four essential properties: capacity, limit, position, and mark. Capacity is the number of elements that a buffer can hold; it is set at the time you create a buffer and cannot be changed. Limit is the number of elements currently contained in the buffer. Note that limit is not kept current automatically; instead, it's updated by making a call to the flip() method, which flips a buffer from a write state to a read state. Position is the current position in the buffer. Mark is like a bookmark in the buffer; you can use it as a convenient way to store a position that you may want to come back to. The relationship between these properties is always constant and transitive:

0 <= mark <= position <= limit <= capacity

Why must this always be true? Well, limit must be less than or equal to capacity because otherwise we would have broken the laws of time and space by putting something that is bigger than itself inside itself. Position must be less than or equal to the limit; that way, we are always dealing with valid elements. Next is the subtle one; mark must be less than or equal to position. The implementation of mark only allows it to be used for backing up in a buffer, as opposed to going forward. Since mark can only be set to the current position, the only way that this relationship can be violated is by explicitly setting position to be less than mark and then making a call to reset(), which sets position to the value of mark. In this case, reset() would throw a java.nio.InvalidMarkException.

Creating buffers poses a paradox. We want to be able to have our new Buffer classes manage the data, but we also want to copy the data as few times as possible. The Buffer classes solve this in an innovative way. All of the Buffer classes are abstract classes, meaning you can't create them directly using the standard constructor mechanism, but you can instantiate them by invoking any one of several static methods. If we don't already have data, we can instantiate a new buffer by calling allocate(), which uses a single int argument to set the capacity of the buffer. Note that this limits the size of a buffer to 2,147,483,648 elements, which sounds large, but most of the time when I'm using buffers, I'm using a ByteBuffer, and this puts the limit at 2GB of data, which sometimes is less than I need. As mentioned, sometimes we already have the data in an array and want to use this data in a buffer but don't want to take the hit for copying all the data into the buffer. No problem. The Buffer classes offer a solution for this situation: We can instantiate a new buffer by calling wrap() and supplying an array full of data as an argument. The Buffer class will then use this array as the storage for the buffer. Since the buffer truly is just wrapping the array, the underlying data can be manipulated through either the array interface or the buffer interface. This may not be the best academic solution, but it certainly makes things more efficient. There are also several methods for duplicating and slicing up buffers that are highly efficient and useful, but we will save these topics for a future article.

There is a hidden gem inside the NIO API for dealing with byte ordering. We touched on byte order briefly in "Java Journal: Fun with Parsing," where we needed to convert shorts and ints from little-endian notation to big-endian notation. Before NIO, when you needed to do endian conversion--either big to little or little to big--you had to roll your own like the little to big one below.

// EndianConverter.java  

 

public class EndianConverter 

{   

  public static int LEtoBE(short le)   

  {   

    return((le >> 8) & 0x00FF) | ((le << 8) & 0xFF00);

  }    

 

  public static int LEtoBE(int le)

  {     

    return (le >>> 24) | (le << 24) |

           ((le << 8) & 0x00FF0000) | ((le >> 8) & 0x0000FF00);

  } 

}

Maybe this kind of code would appeal to the assembly programmer, but to me it just looks like an opportunity for bugs to creep into my system. NIO provides a ByteOrder class that you can use two ways. The primary use is to query the current VM's OS for its native byte order. The secondary use is to define the constants BIG_ENDIAN and LITTLE_ENDIAN, which can be used as arguments to the ByteBuffer class's method order() to set the byte order of the Buffer. Note that ByteBuffer is the only Buffer class with the order() method.

All of these methods of dealing with Buffers are convenient and efficient to implement, as opposed to String classes and arrays. However, the true power of Buffers is derived from how they deal with Bulk Moves. Bulk Moves are optimized for moving large amounts of data at once. Instead of cycling through an iterator over an array and copying data one piece at a time, Bulk Moves allow us to copy everything to an array at once. This allows the underlying implementation of the Buffer class to take advantage of any hardware- or operating system-level I/O routines that might exist for the type of data move being performed.

Channels

Channels are the abstraction for how data gets from place to place. We pack data into buffers and then pack the buffers into channels. There are several species and subspecies of channels. They can be readable, writeable, socket-based, file-based, interruptible, and selectable as well as several other possibilities (some of which are rather esoteric). Most channels are specified as interfaces rather than abstract classes, which allow for implementations to be written in native code and also allow for the easy implementation of multiple interfaces. Channels are a completely new concept in Java I/O.

File Locking

File locking allows us to specify that we want either an exclusive or a shared lock on a file. Since not all operating systems support shared locks, requests for shared locks on operating systems that don't support them are treated as exclusive locks. If your operating system doesn't support exclusive locks, you should look for a new operating system. File locking is also a completely new concept in Java I/O.

Memory Mapped Files

Think of memory mapped files as doing for file I/O what virtual memory does for physical and user memory at the operating system level. With memory mapped files, we can treat the entire file as if it were in a ByteBuffer. Even if the file is much larger than the amount of memory we have available to us for creating our ByteBuffer, the memory mapped file class handles all the swapping of data to and from the file system for us--and probably much more efficiently than we would have. This is a perfect example of how NIO works; it gives us an abstraction that is both easy to use and more efficient than a hand-coded solution.

Sockets and Selectors

Sockets (or more accurately, socket channels) are not as new to Java I/O as some of the other concepts. Sockets were included in previous Java versions in the java.net package. However, in NIO, we combine sockets with the concept of selectors, which are a way of monitoring multiple channels at once. The result is that we get the abilities to do non-blocking I/O and to manage many simultaneous connections within a single thread, solving the traditional problem of server-side Java I/O scalability.

Character Sets (Charsets)

Although Java has been a pioneer in the area of internationalization and is one of the only languages to support Unicode natively, there was still a void to fill in translations between various character set encodings. NIO provides an API for standard character set transcodings as well as a standardized interface for creating your own encodings. Why you would ever want to do that I can't guess, but at least now you have the flexibility to do it.

Wrapping It All Up

NIO provides a much-needed boost to Java application performance by providing new and improved classes and interfaces for managing various types of I/O operations. The Buffer classes are a convenient way of handling data, and we examined the relationships and nuances of its attributes. The goal of I/O is to move data as few times as possible, which often conflicts with layers of abstraction. NIO provides new abstractions rather than layers to create an interface that is both efficient and easy to use. The hidden gem ByteOrder class allows us to easily deal with the complexities of byte ordering without having to write our own solution. The Bulk Moves are the main motivation behind using Buffer classes because they allow us to take advantage of hardware- and operating system-level I/O accelerations. Channels, sockets, and selectors all collaborate to give us scalable I/O, and there have been some nice enhancements to file I/O in the areas of file locking and memory mapped files. Overall, the best thing about NIO is that Sun has been able to pull off the difficult task of coupling a good abstraction with higher performance and thus has been able to at least partially address Java's number one criticism.

For more information, check out Java NIO by Ron Hitchens.

Michael J. Floyd is the Software Engineering Manager for DivXNetworks. He is also a consultant for San Diego State University and can be reached at This email address is being protected from spambots. You need JavaScript enabled to view it..

Michael Floyd

Michael J. Floyd is the Vice President of Engineering for DivX, Inc.

BLOG COMMENTS POWERED BY DISQUS

LATEST COMMENTS

Support MC Press Online

$0.00 Raised:
$

Book Reviews

Resource Center

  • SB Profound WC 5536 Have you been wondering about Node.js? Our free Node.js Webinar Series takes you from total beginner to creating a fully-functional IBM i Node.js business application. You can find Part 1 here. In Part 2 of our free Node.js Webinar Series, Brian May teaches you the different tooling options available for writing code, debugging, and using Git for version control. Brian will briefly discuss the different tools available, and demonstrate his preferred setup for Node development on IBM i or any platform. Attend this webinar to learn:

  • SB Profound WP 5539More than ever, there is a demand for IT to deliver innovation. Your IBM i has been an essential part of your business operations for years. However, your organization may struggle to maintain the current system and implement new projects. The thousands of customers we've worked with and surveyed state that expectations regarding the digital footprint and vision of the company are not aligned with the current IT environment.

  • SB HelpSystems ROBOT Generic IBM announced the E1080 servers using the latest Power10 processor in September 2021. The most powerful processor from IBM to date, Power10 is designed to handle the demands of doing business in today’s high-tech atmosphere, including running cloud applications, supporting big data, and managing AI workloads. But what does Power10 mean for your data center? In this recorded webinar, IBMers Dan Sundt and Dylan Boday join IBM Power Champion Tom Huntington for a discussion on why Power10 technology is the right strategic investment if you run IBM i, AIX, or Linux. In this action-packed hour, Tom will share trends from the IBM i and AIX user communities while Dan and Dylan dive into the tech specs for key hardware, including:

  • Magic MarkTRY the one package that solves all your document design and printing challenges on all your platforms. Produce bar code labels, electronic forms, ad hoc reports, and RFID tags – without programming! MarkMagic is the only document design and print solution that combines report writing, WYSIWYG label and forms design, and conditional printing in one integrated product. Make sure your data survives when catastrophe hits. Request your trial now!  Request Now.

  • SB HelpSystems ROBOT GenericForms of ransomware has been around for over 30 years, and with more and more organizations suffering attacks each year, it continues to endure. What has made ransomware such a durable threat and what is the best way to combat it? In order to prevent ransomware, organizations must first understand how it works.

  • SB HelpSystems ROBOT GenericIT security is a top priority for businesses around the world, but most IBM i pros don’t know where to begin—and most cybersecurity experts don’t know IBM i. In this session, Robin Tatam explores the business impact of lax IBM i security, the top vulnerabilities putting IBM i at risk, and the steps you can take to protect your organization. If you’re looking to avoid unexpected downtime or corrupted data, you don’t want to miss this session.

  • SB HelpSystems ROBOT GenericCan you trust all of your users all of the time? A typical end user receives 16 malicious emails each month, but only 17 percent of these phishing campaigns are reported to IT. Once an attack is underway, most organizations won’t discover the breach until six months later. A staggering amount of damage can occur in that time. Despite these risks, 93 percent of organizations are leaving their IBM i systems vulnerable to cybercrime. In this on-demand webinar, IBM i security experts Robin Tatam and Sandi Moore will reveal:

  • FORTRA Disaster protection is vital to every business. Yet, it often consists of patched together procedures that are prone to error. From automatic backups to data encryption to media management, Robot automates the routine (yet often complex) tasks of iSeries backup and recovery, saving you time and money and making the process safer and more reliable. Automate your backups with the Robot Backup and Recovery Solution. Key features include:

  • FORTRAManaging messages on your IBM i can be more than a full-time job if you have to do it manually. Messages need a response and resources must be monitored—often over multiple systems and across platforms. How can you be sure you won’t miss important system events? Automate your message center with the Robot Message Management Solution. Key features include:

  • FORTRAThe thought of printing, distributing, and storing iSeries reports manually may reduce you to tears. Paper and labor costs associated with report generation can spiral out of control. Mountains of paper threaten to swamp your files. Robot automates report bursting, distribution, bundling, and archiving, and offers secure, selective online report viewing. Manage your reports with the Robot Report Management Solution. Key features include:

  • FORTRAFor over 30 years, Robot has been a leader in systems management for IBM i. With batch job creation and scheduling at its core, the Robot Job Scheduling Solution reduces the opportunity for human error and helps you maintain service levels, automating even the biggest, most complex runbooks. Manage your job schedule with the Robot Job Scheduling Solution. Key features include:

  • LANSA Business users want new applications now. Market and regulatory pressures require faster application updates and delivery into production. Your IBM i developers may be approaching retirement, and you see no sure way to fill their positions with experienced developers. In addition, you may be caught between maintaining your existing applications and the uncertainty of moving to something new.

  • LANSAWhen it comes to creating your business applications, there are hundreds of coding platforms and programming languages to choose from. These options range from very complex traditional programming languages to Low-Code platforms where sometimes no traditional coding experience is needed. Download our whitepaper, The Power of Writing Code in a Low-Code Solution, and:

  • LANSASupply Chain is becoming increasingly complex and unpredictable. From raw materials for manufacturing to food supply chains, the journey from source to production to delivery to consumers is marred with inefficiencies, manual processes, shortages, recalls, counterfeits, and scandals. In this webinar, we discuss how:

  • The MC Resource Centers bring you the widest selection of white papers, trial software, and on-demand webcasts for you to choose from. >> Review the list of White Papers, Trial Software or On-Demand Webcast at the MC Press Resource Center. >> Add the items to yru Cart and complet he checkout process and submit

  • Profound Logic Have you been wondering about Node.js? Our free Node.js Webinar Series takes you from total beginner to creating a fully-functional IBM i Node.js business application.

  • SB Profound WC 5536Join us for this hour-long webcast that will explore:

  • Fortra IT managers hoping to find new IBM i talent are discovering that the pool of experienced RPG programmers and operators or administrators with intimate knowledge of the operating system and the applications that run on it is small. This begs the question: How will you manage the platform that supports such a big part of your business? This guide offers strategies and software suggestions to help you plan IT staffing and resources and smooth the transition after your AS/400 talent retires. Read on to learn: