Continuing with the lock comparison code here, I've modified it to test the timings for all the locks for different permutations of readers and writers and compared the results
I've tested the different locks with 1 to 16 readers and 1 to 16 writers on a 16 core (with hyper-threading) E5-2687W chip. With the generated matrix, I used R to graph the results (code at the bottom of the post).
Note, duration is measured in milliseconds and is scaled by log 10.
Synchronized blocks
> showPlot("SYNCHRONIZED")
Perhaps surprisingly, with synchronized blocks, more writers mean the target number is reached more quickly. Reader threads (which are also synchronized) only make a huge difference to the overall duration when there are few writers. Then they can make a difference of an order of magnitude.
JDK 8 Adders
> showPlot("ADDER")
As you might expect from the way adders work, the threads don't seem to interfere with each other. The more threads that write, the faster the target number is reached, irrespective of reader threads. But even when they're slow, they're fast (relative to other locks).
Atomics
> showPlot("ATOMIC")
Atomic times are all tightly within an order of magnitude of each other. Like synchronized, with smaller number of writer threads, a smaller number of reader threads makes a difference. However, the effect is by no means as pronounced.
Java Read-Write Locks
> showPlot("RWLOCK")
Read-write locks are also sensitive to the number of reader threads at low number of write threads. It can make a difference of an order of magnitude. Best and worse times are also similar to Synchronized blocks.
JDK 8 StampedLocks with an Optimistic Approach
> showPlot("OPTIMISTIC_CORRECT")
I've modified the original code to retry optimistic locks in the event it was not attained, which I think makes for a more fair comparison.
This strategy is relatively insensitive to the number of threads when compared to other locking techniques but it's not the fastest strategy.
Volatile
> showPlot("VOLATILE")
NB: this is not to be trusted too much. That adding a value to a value is not an atomic operation in Java so this metric is somewhat misleading. However, if we just care for speed, knock yourself out.
Having said that, this technique is fastest when there is only one writer - in this particular case, you'll get the correct result. Fastest of all is one reader, one writer. This is not that much slower than a dirty read/write.
Having said that, this technique is fastest when there is only one writer - in this particular case, you'll get the correct result. Fastest of all is one reader, one writer. This is not that much slower than a dirty read/write.
Dirty
> showPlot("DIRTY")
I've just just added this for grins. As long as you don't mind dirty data, it's fine.
StampedLock
> showPlot("STAMPED")
StampedLock again but this time using a pessimistic locking strategy. I've actually got some data missing at a low number of writer threads and high reader threads as the test decided it took too long and aborted! Suffice to say, it's fast for anything but a permutation of low number of writer threads with a large number of reader threads.
R Scripting
These graphics were generated with R. The code is below for review.
The raw results are a CSV file that looks like:
Lock_type, Number_of_Readers, Number_of_Writers, Duration_in_ms
allLocksMatrix.R:
.libPaths("/home/henryp/R/x86_64-unknown-linux-gnu-library/3.1/")
require("rgl")
timingsData <- read.csv("/home/henryp/Code/R/JavaLocks/all.csv", header = T)
names <- levels(timingsData$name)
toMatrix <- function (counterName) {
counter.data <- toRaw(counterName)
counter.dim <- sqrt(length(counter.data[,1]))
counter.matrix <- matrix(0, nrow=counter.dim, ncol=counter.dim, byrow=T)
for (i in 1:counter.dim) {
for (j in 1:counter.dim) {
counter.matrix[i,j] = log10(counter.data[((i - 1)*counter.dim) + j, 4])
}
}
return (counter.matrix)
}
toRaw <- function(counterName) {
counter.data <- subset(timingsData, name == counterName)
return (counter.data)
}
rotate <- function(x) t(apply(x, 2, rev))
showPlot <- function (counterName) {
myMatrix <- toMatrix(counterName) # rotate(rotate(rotate(toMatrix(counterName))))
nbcol = 100
color = rev(rainbow(nbcol, start = 0/6, end = 4/6))
zcol = cut(myMatrix, nbcol)
open3d()
persp3d(seq(1, length(myMatrix[,1])),
seq(1, length(myMatrix[1,])),
myMatrix,
col=color[zcol],
xlab="",
ylab="",
zlab="",
axes=FALSE
)
axes3d(c('x++','z++', 'y++'))
box3d()
mtext3d("Writers",edge="x++",line=3,las=2,cex=1.1)
mtext3d("Readers",edge="y++",line=3,las=2,cex=1.1)
mtext3d("duration", edge="z++",line=2,las=2,cex=1.1)
title3d(counterName, col='red', line=4)
}
The raw results are a CSV file that looks like:
Lock_type, Number_of_Readers, Number_of_Writers, Duration_in_ms
allLocksMatrix.R:
.libPaths("/home/henryp/R/x86_64-unknown-linux-gnu-library/3.1/")
require("rgl")
timingsData <- read.csv("/home/henryp/Code/R/JavaLocks/all.csv", header = T)
names <- levels(timingsData$name)
toMatrix <- function (counterName) {
counter.data <- toRaw(counterName)
counter.dim <- sqrt(length(counter.data[,1]))
counter.matrix <- matrix(0, nrow=counter.dim, ncol=counter.dim, byrow=T)
for (i in 1:counter.dim) {
for (j in 1:counter.dim) {
counter.matrix[i,j] = log10(counter.data[((i - 1)*counter.dim) + j, 4])
}
}
return (counter.matrix)
}
toRaw <- function(counterName) {
counter.data <- subset(timingsData, name == counterName)
return (counter.data)
}
rotate <- function(x) t(apply(x, 2, rev))
showPlot <- function (counterName) {
myMatrix <- toMatrix(counterName) # rotate(rotate(rotate(toMatrix(counterName))))
nbcol = 100
color = rev(rainbow(nbcol, start = 0/6, end = 4/6))
zcol = cut(myMatrix, nbcol)
open3d()
persp3d(seq(1, length(myMatrix[,1])),
seq(1, length(myMatrix[1,])),
myMatrix,
col=color[zcol],
xlab="",
ylab="",
zlab="",
axes=FALSE
)
axes3d(c('x++','z++', 'y++'))
box3d()
mtext3d("Writers",edge="x++",line=3,las=2,cex=1.1)
mtext3d("Readers",edge="y++",line=3,las=2,cex=1.1)
mtext3d("duration", edge="z++",line=2,las=2,cex=1.1)
title3d(counterName, col='red', line=4)
}