Java.next: Contrasting concurrency

See how concurrency options differ in the Java.next languages

Perhaps the starkest difference among the Java.next languages lies in threading, concurrency, and parallelism. This installment shows easy ways to make existing functional code in Scala, Groovy, and Clojure parallel. Then it investigates the actor currency model in Scala.

Neal Ford, Director / Software Architect / Meme Wrangler, ThoughtWorks

Photo of Neal fordNeal Ford is Director, Software Architect, and Meme Wrangler at ThoughtWorks, a global IT consultancy. He is also the designer and developer of applications, instructional materials, magazine articles, courseware, and video/DVD presentations, and he is the author or editor of books spanning a variety of technologies, including the most recent Presentation Patterns. He focuses on designing and building large-scale enterprise applications. He is also an internationally acclaimed speaker at developer conferences worldwide. Check out his website.



31 March 2014

Also available in Chinese Japanese

About this series

The Java legacy will be the platform, not the language. More than 200 languages run on the JVM, and it's inevitable that one of them will eventually supplant the Java language as the best way to program the JVM. This series explores three next-generation JVM languages: Groovy, Scala, and Clojure, comparing and contrasting new capabilities and paradigms, to provide Java developers a glimpse into their own near future.

The Java™ engineers struggled to make concurrency easy for developers to use. But despite improvements, concurrency it is still a complex and error-prone part of the platform. Part of the difficulty lies with understanding the low-level abstractions for concurrency in the language itself, which riddle your code with synchronized blocks. Another complexity comes from newer libraries such as fork/join, which work spectacularly well in certain scenarios but less well in others. Understanding the bewildering array of low-level options requires expertise and time.

One of the benefits of breaking away from the Java language is the ability to improve and simplify areas such as concurrency. Each of the Java.next languages has a unique answer to this problem that leverages the default programming style for the language. In this installment, I first cover a benefit of the functional programming style: easy parallelization. I delve into specifics for Scala and Groovy (and will complete Clojure coverage in the next installment). Then I introduce Scala actors.

Perfect numbers

The mathematician Nicomachus (born c. 60 A.D.) classified natural numbers as uniquely perfect, abundant, or deficient. A perfect number equals the sum of its positive factors, excluding the number itself. For example, 6 is a perfect number because its factors are 1, 2, 3, and 6, as is 28 (= 1 + 2 + 4 + 7 + 14). The sum of an abundant number's factors is greater than the number, and the sum of a deficient number's factors is less than the number.

I use perfect-number-classifier examples here for convenience. Unless you're working with enormous numbers, finding factors is too small a problem to benefit from parallelization. You derive benefit from using more threads, but the switching overhead between them is expensive for fine-grained jobs.

Making existing code parallel

In the "Functional coding styles" installment, I encourage the use of higher-level abstractions such as reduce, map, and filter rather than iteration. One of the several advantages of this approach is easy parallelization.

Readers of my Functional thinking series are familiar with the number-classification scheme that features perfect numbers (see the Perfect numbers sidebar). None of the solutions I show in that series leverage concurrency. But because the solutions use transformation functions such as map, I can create a parallelized version in each Java.net language with surprisingly little effort.

Listing 1 is a Scala example of a perfect-number classifier.

Listing 1. Parallel perfect-number classifier in Scala
object NumberClassifier {
  def isFactor(factor: Int, number: Int) =
    number % factor == 0

  def factors(number: Int) = {
    val factorsBelowSqrt = (1 to Math.sqrt(number).toInt).par.filter (isFactor(_, number))
    val factorsAboveSqrt = factorsBelowSqrt.par.map(number / _)
    (factorsBelowSqrt ++ factorsAboveSqrt).toList.distinct
  }

  def sum(factors: Seq[Int]) =
    factors.par.foldLeft(0)(_ + _)

  def isPerfect(number: Int) =
    sum(factors(number)) - number == number
}

Edge case

In Listing 1's implementation of the factors() method, whole-number square roots (for example, 16's square root: 4) appear in both lists. Thus, the last line of the factors() method's return is a call to the distinct function, which removes duplicates from the list. Alternatively, I could use Sets everywhere rather than lists, but often List has useful functions that are absent from Set.

The factors() method in Listing 1 returns a list of the factors of a number, using the isFactor() method to filter all possible values. The factors() method uses an optimization I describe in more detail in "Functional thinking: Transformations and optimizations." In brief, it's inefficient to filter every possible number looking for factors because, by definition, a factor is one of two numbers whose product equals the target number. Instead, I filter only the numbers up to and including the target number's square root, then generate the list of symmetrical factors by dividing the target number by each factor that's less than the square root. In Listing 1, the factorsBelowSqrt variable holds the result of the filtering operation. The factorsAboveSqrt value is the mapping of the existing list to generate the symmetrical values. Finally, the return value of factors() is the concatenated list, converted from a parallel List back to a regular List.

Notice the addition of the par modifier in Listing 1. This modifier causes the filter, map, and foldLeft to run in parallel, using multiple threads to handle the request. The par method, which is consistent throughout the Scala collections libraries, converts the sequence into a parallel sequence. Because both types of sequences mirror their signatures, the par function becomes a drop-in way to parallelize an operation.

The ease with which you can parallelize common problems in Scala is a testament to both the language design and the functional paradigm. Functional programming encourages the use of common functions such as map, filter, and reduce, which in turn can be optimized by the runtime in invisible ways. The Scala language designers had these optimizations in mind, which led to the design of the collections API.

Groovy also allows for easy modification to existing functional code to make it parallel through the GPars library, which is bundled with Groovy distributions. The GPars framework creates useful abstractions atop built-in Java concurrency primitives, often wrapping them in syntactic sugar. GPars offers an astounding variety of concurrency mechanisms, one of which you can use to allocate thread pools, then spread operations across those pools. A perfect-number classifier written in Groovy that uses GPars thread pools appears in Listing 2.

Listing 2. Parallel perfect-number classifier in Groovy
class NumberClassifierPar {

  static def factors(number) {
    GParsPool.withPool {
      def factors = (1..round(sqrt(number) + 1)).findAllParallel { number % it == 0 }
      (factors + factors.collectParallel { number / it }).unique()
    }
  }

  static def sumFactors(number) {
    factors(number).inject(0, { i, j -> i + j })
  }

  static def isPerfect(number) {
    sumFactors(number) - number == number
  }

}

The factors() method in Listing 2 uses the same algorithm as Listing 1: It gathers all the factors up to the square root of the target number, then generates the remaining factors and returns the concatenated collection. As in Listing 1, I use the unique() method to ensure that whole-number square roots don't generate duplicates.

Rather than augment collections to create symmetrical parallel versions as in Scala, Groovy's designers created xxxParallel() versions of the language's transformation methods (for example, findAllParallel() and collectParallel()). But these methods won't work unless they are wrapped in a GPars thread-pool block. In Listing 2, I create a thread pool by creating a code block via the call to GParsPool.withPool, enabling the use of the xxxParallel() methods within the block. Other variants of the withPool method exist. For example, you can specify the number of threads in the pool.

Clojure has a similar drop-in parallelization mechanism via the reducers library. You use the reducers version of the transformation functions for automatic parallelization — for example, r/map instead of map. (The r/ is the reducers namespace.) The implementation of reducers is a fascinating case study in Clojure's syntactic flexibility, which makes powerful additions possible with minimal change.


Actors in Scala

Scala includes a wide variety of concurrency and parallelization mechanisms. One of the more popular is the actor model, which provides the benefits of allocating work across threads without synchronization complexity. Conceptually, an actor has the ability to do work, then send a non-blocking result to a coordinator. To create an actor, you subclass the Actor class and implement the act() method. By using Scala's syntactic sugar, you can bypass much of the definitional formality and define actors within a code block.

One of the optimizations that I didn't perform for the number classifier in Listing 1 is the use of threads to partition the factors-finding portion of the job. If I have four processors on my computer, I can create a thread for each and split the work. For example, if I'm trying to find the sum of factors for the number 16, I can assign processor 1 to find (and sum) the factors from 1 to 4, processor 2 can work on 5 through 8, and so on. Using actors is a natural fit: I create an actor for each range, execute each actor independently (either implicitly via syntactic sugar or explicitly by a call to its act() method), then gather the results, as shown in Listing 3.

Listing 3. Identifying perfect numbers using actors in Scala
object NumberClassifier extends App {

  def isPerfect(candidate: Int) = {
    val RANGE = 10000
    val numberOFPartitions = (candidate.toDouble / RANGE).ceil.toInt
    val coordinator = self

    for (i <- 0 until numberOFPartitions) {
      val lower = i * RANGE + 1
      val upper = candidate.min((i + 1) * RANGE)
      actor {
        var partialSum = 0
        for (j <- lower to upper)
          if (candidate % j == 0) partialSum += j

        coordinator ! partialSum
      }
    }
    var responsesExpected = numberOFPartitions
    var sum = 0
    while (responsesExpected > 0) {
      receive {
        case partialSum : Int =>
          responsesExpected -= 1
          sum += partialSum
      }
    }

    sum == 2 * candidate
  }
}

To keep this example simple, I wrote isPerfect() as a single monolithic function. I first create a number of partitions based on a constant RANGE. Second, I need a way to gather the messages that the actors generate. In the coordinator variable, I hold a reference that actors can send messages to, where self is a member of Actor and represents the reliable way in Scala to get the thread identifier.

I then create a loop for the number of partitions and use the RANGE offset to generate the lower and upper bounds of the range. Next, I create an actor for that range, using Scala's syntactic sugar to avoid the formal class definition. Within the actor, I create a temporary holder for the partialSum, then investigate the range, gathering factors into partialSum as I find them. After I gather the partial sum (the sum of all the factors within this range), I send a message back to the coordinator, using the exclamation-mark operator (coordinator ! partialSum). (This message-passing syntax was inspired by the Erlang language as a way to make a non-blocking call to another thread.)

Next, I start a loop and wait for all the actors to finish processing. While I'm still waiting, I enter a receive block. Within the block, I expect an Int message, which I locally assign to partialSum, then decrement the number of responses expected and add the partial sum to the whole sum. After all the actors have finished and reported their results, the last line of the method compares the sum against twice the candidate. If the comparison yields true, then my candidate is a perfect number, and the return of the function is true.

One nice benefit of actors is the partitioning of ownership. Each actor has a partialSum local variable, but they never interfere with one another. When you receive messages via the coordinator, the underlying mechanism is invisible: You create a receive block, and the other implementation details are invisible.

The actor mechanism in Scala is a great example of how Java.next languages encapsulate existing facilities of the JVM and extend them with consistent abstractions. Writing similar code in the Java language, using low-level concurrency primitives, would require much more sophistication to coordinate the multiple threads. Actors in Scala hide all that complexity, leaving an easy-to-understand abstraction.


Conclusion

The Java.next languages all have answers to the concurrency headaches in the Java language, and each approaches the problems differently. In this installment, I illustrated how all three Java.next languages enable drop-in parallelization. I also illustrated the actor model in Scala, building a number classifier that computes the sum of factors in parallel.

In the next installment, I investigate the most radical of the Java.next approaches to concurrency, in Clojure.

Resources

Learn

Get products and technologies

Discuss

  • Get involved in the developerWorks community. Connect with other developerWorks users as you explore the developer-driven blogs, forums, groups, and wikis.

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=966965
ArticleTitle=Java.next: Contrasting concurrency
publish-date=03312014