0

I am trying to understand multi-threading in Java, using the features which were added as part of java.util.concurrent.* . To begin with, there is concept of lock which a thread can try to acquire; if a thread can't acquire, it can do some other tasks.

This I have read in online materials as well in some books, but never ever seen anything they have implemented in real. How is this possible that if a thread can't acquire a lock it can execute other tasks; isn't a thread supposed to do a single "piece of work"? How can it have multiple logic execution based on if it can/can't acquire a lock?

Is there any real implementation which I can refer to see to understand, to reinforce the concepts; else it seems too abstract, how to implement in real life.

Any explanations?

Kayaman
  • 72,141
  • 5
  • 83
  • 121
CuriousMind
  • 8,301
  • 22
  • 65
  • 134
  • 2
    While it may be possible to implement a thread like this, in practice it doesn't really happen. Ordinarily when a thread can't acquire a lock it will just wait (do nothing) until it can. – Michael Feb 15 '18 at 18:27
  • 1
    As for how this can be possible, it's no different from using standard conditional logic (i.e. if statements) – Michael Feb 15 '18 at 18:28
  • @Michael: Thanks for your reply. Have you seen any code which does "other" task if it can't acquire lock? Would "tomcat', "Jboss AS" which hides away multi-threading be doing something like that? – CuriousMind Feb 15 '18 at 18:29
  • 1
    https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/Lock.html#tryLock-- – Sotirios Delimanolis Feb 15 '18 at 18:30
  • 1
    If you search on SO for `tryLock()` you can find real life examples. – Kayaman Feb 15 '18 at 18:30
  • 1
    @CuriousMind, no, not really in JBOSS. Native OS threads can do that though, I'm pretty sure. Look at it like a queue of specialized tasks, each of which exposes a lock. Thread takes the lock from a task and tries to aquire it for, say, 2 seconds. If it fails with timeout, it takes next task, then after he's done with the next, he returns to the first one and waits on its lock some more. There can be back offs if wait on the same task failed more than once, etc. Real systems rarely do that because it requires very specific task implementation, and a lot of conditional logic. – M. Prokhorov Feb 15 '18 at 18:33
  • 2
    @CuriousMind not that I can remember. I can picture what it would look like but I'm struggling to think of a valid use case. If you have two distinct tasks that share a thread, one which requires a lock and one that doesn't, I can't see any advantage over using a thread for each task. The non-locking task would also have to be okay with potentially never getting executed which would be unusual as well. – Michael Feb 15 '18 at 18:33
  • 1
    @Michael, Java indeed doesn't really need this outside of very specific optimization cases, because threads are usually managed by OS in this sense, as are sync locks. – M. Prokhorov Feb 15 '18 at 18:35
  • @M.Prokhorov a relatively common (in all its rarity) use case for `tryLock()` is to avoid deadlocks, although this is more of a workaround for design flaws. I don't really see what "threads are usually managed by OS" has to do with anything. – Kayaman Feb 15 '18 at 18:40
  • @Kayaman, since threads are managed by OS, Java app has no reason for trying to keep all its threads busy, it's perfectly fine to have them wait. OS has some incentives for that, on the other hand, because it more often deals with actual hardware threads, which cannot be simply kept waiting on something when new messages for processing keep pumping in. So there's more of a case for hardware threads "giving up" on `tryLock()` than in normal Java apps. – M. Prokhorov Feb 15 '18 at 18:43
  • @M.Prokhorov There are plenty of reasons why a Java app would want to maximize its throughput and you never *want* threads to wait for locks. Waiting for signals is a different case. Besides Java threads are essentially hardware threads. The only reason `tryLock()` is not used very often is because there are easier designs that achieve the same thing. Or am I not understanding what you're trying to say? – Kayaman Feb 15 '18 at 18:50
  • @Kayaman, I was referring to my understanding that for `tryLock` to call allow the host thread to "do something else in the meantime", one needs to either make a special subtask type with the lock exposed, or still have runnable tasks, but then make them aware of their host `Executor`, so they can reschedule themselves. It's very non-common to have such rescheduling outside of actual hardware threads, where system can context-switch thread to do other tasks when `wait` command is issued from current task. That whole thing might be my big misunderstanding though. – M. Prokhorov Feb 15 '18 at 19:02
  • @M.Prokhorov it doesn't require anything special, just access to a `Lock`. No `Executor` or anything needed. It does require two flows of execution though, one that requires the lock to be acquired and one that doesn't. In "regular" programming this isn't very common since it's often easier to come up with a design where threads aren't competing for locks so much; threads have a single responsibility and message passing is used. – Kayaman Feb 15 '18 at 19:13
  • Yes, 'which a thread can try to acquire; if a thread can't acquire, it can do some other tasks' - trylock() is usually an excuse to write bad code. I'm sure that you can indeed find examples of trylock() on SO, but I'm betting they're all bad. – Martin James Feb 15 '18 at 20:41
  • @Kayaman, the example from your anwer is not what I was referring though. I'm not talking about the two paths. What I'm more or less talking about is "I receive a task which I has a protected state. I'm trying to lock that state so I can do the task. So I wait for some time to aquire a lock on it. If I fail, I've got the rest of the queue to work with, so I push that first task to the backlog". It's a form of message passing, I guess, but the one which doesn't lock the thread in if it can't aquire a lock, since he can go elsewhere looking for things to do until the backlog unlocks. – M. Prokhorov Feb 16 '18 at 12:11
  • @M.Prokhorov yeah my answer was basically hypothetical. Your situation is not impossible to imagine. On the other hand you could probably design that with separate threads and queues for the resources instead, so you'd just be dividing the tasks instead of trying to acquire locks. But yeah, it's a complicated issue (well, concurrency usually is) and there are multiple ways to skin a cat. – Kayaman Feb 16 '18 at 12:25

1 Answers1

3

It's difficult to find real life examples because normally you wouldn't design your software to use tryLock(). The example given in the javadoc is as follows:

Lock lock = ...;
if (lock.tryLock()) {
  try {
    // manipulate protected state
  } finally {
    lock.unlock();
  }
} else {
  // perform alternative actions
}

But you wouldn't design your software like that, would you? What if the lock is never (or almost never) available, how will that affect your program? What if it's always available? You have a method that does one of two things depending on pure chance. That's not good design, it increases randomness and complexity.

Okay, so it's not something you decide to use because it's elegant. What is it good for?

Imagine you've inherited a legacy project designed by an insane programmer and it has severe issues with deadlocks. It has synchronized methods peppered all around and needs to be booted at least once every week because it locks up. First you convert all the synchronized methods to use Lock instead. Now you no longer block forever on synchronized, but can use tryLock(long, TimeUnit) to timeout and prevent deadlocks.

Now you've solved the reboot causing deadlocks, but it's still not optimal since you're spending time waiting. With additional refactoring you manage to reduce the locks, but unfortunately you can't do proper lock ordering just yet. Your end code looks like this, where inner locks are acquired with tryLock() or outerlock is released to prevent deadlock:

Lock outerLock = ...;
outerLock.lock();  // Here we block freely
try {
   Lock innerLock = ...;
   if (innerLock.tryLock()) {  // Here we risk deadlock, we'd rather "fail-fast"
     try {
       doSomethingProtectedByLocks();
     } finally {
       innerLock.unlock();
     }
   } else {
     throw new OperationFailedException(); // Signal the calling code to retry
   }   
} finally {
   outerLock.unlock();
}

I think the problem is mainly with wording. The Javadoc talks about "actions" (like unlocking an outer lock) being performed based on whether the lock was acquired or not, but it's easy to read it as if the thread would have 2 separate responsibilities determined by the lock state.

Kayaman
  • 72,141
  • 5
  • 83
  • 121
  • 1
    Well it was the least convoluted situation I could think of. You don't always have enough pull to say "let's rewrite the whole thing", so the conversion from `synchronized` to `Lock` to get access to additional functionality is a valid tool in refactoring. – Kayaman Feb 15 '18 at 20:56