image
Threads and Processes Part 2
Start course
Difficulty
Intermediate
Duration
2h
Students
32
Description

In this course, we will learn the concepts of Java EE 7 with a focus on Concurrency Utilities with Threads, Semaphore, Phaser and other methods, and Transactions.

Learning Objectives

  • Concurrency Utilities 

Intended Audience

  • Anyone looking to get Oracle Java Certification
  • Those who want to improve Java 7 EE knowledge
  • Java developers

Prerequisites

  • Have at least 2 years of Java development experience 
Transcript

Hello there. In this lesson, we will continue to talk about threads and multi-threads in Java. So, let's start. The structure of producer and consumer is a structure that we can encounter frequently, both in daily life and while programming. The producer adds something to the queue while the consumer gets thing in that queue in a fairly orderly fashion and does what it's supposed to do. If we give an example from daily life: in a bank, the device that generates sequence numbers for customers can be considered as producer. And as long as there are customers with unprocessed sequence numbers, the teller officer who calls them via lead sequence indicators and processes them can be considered as consumer. As you may have noticed, producer and consumer can be implemented as two separate threads. However, let's not lose sight of the fact that we have a common data source here. Queue. Again, the thread-safe helper class, BlockingQueue from the java.util.concurrent.*package comes to our aid. BlockingQueue is actually an interface and we will use the ArrayBlockingQueue class, which is one of its implementations. When creating a queue of type ArrayBlockingQueue, we specify a capacity. If this capacity is full, the thread that wants to add a new element to the queue is suspended. Similarly, if there is no element in the queue, the thread that wants to get an element from the queue is waited until there are elements in the queue.

Now let's examine our program that will illustrate the producer consumer structure and the use of the ArrayBlockingQueue class. Let's move on to the Eclipse. We'll create a Javaclass in this project. Thread producer. Thread consumer. producerThreadstart();, consumerThread.start();, producerThread.join(); , consumerThread.join();, Thread.sleep. First, we create a queue with a capacity of 10 elements and whose elements will be integers. Then we create and start two threads in the main method; one of which will call the produce and other the consume(); method. The produce method generates random numbers in an infinite loop and adds them to the queue. The consume(); method, on the other hand, takes a number from the queue in an endless loop waiting 100 milliseconds for us to observe it comfortably, and prints the current size of the queue with this number. When we run the program, the output will be like this. Since the consumer takes members from the queue every 100 milliseconds, the producer has already filled the queue by the time that the 100 milliseconds has passed. As the consumer receives an element from the queue, the producer which works simultaneously may sometimes add a new one before the consumer has not yet printed the size of the queue.

For this reason, the size of the queue is usually printed as nine or 10. If we remove the Thread.sleep(100) call that we put in order to observe the program's operation comfortably. We can see that the queue size differs from time to time and sometimes even resets. Let's talk about Wait and Notify. In Java, every class automatically inherits from the object class. The wait and notify methods of the object super-class are used to provide the above-mentioned wait and resume functionality. In this section, we will introduce the Wait and Notify methods with a simple example of what they do and how they are used. Let's say, our processor class which has producer and consumer methods namely produce and consume methods looks like this. Thread.sleep(2000); synchronized(this){. ("Press

'Enter' to continue:);. ("Consumer continued for 5 more seconds..") Our main method will do the same thing as in the previous section. Create and start two threads; one of which will run the produce and the other consume() method. Thread producerThread. Thread consumerThread. Now that we have our entire sample program in front of us, let's start examining our processor class. The produce method creates a synchronized block using the current object as the lock object. In a sense, it uses the integral locking mechanism. In the block, it calls the wait(); method of that object. After it prints an output, indicating that it has started working. With this call, the execution of the current thread is put on hold to continue later. And the lock specified for the block is released. Since the lock is released, other threads will be able to enter blocks with the same lock. First, we made a call to Thread.sleep(2000); at the beginning of the consume() method to make sure that the synchronized block in the produce method is entered. We did this just to make the example understandable of course. During this time a wait call was made in the block with the produce() method and the relevant thread was put on hold, and the specified lock for the block was released.

With the lock released, it was possible to enter the block with the same lock object in the consume() method although the block in the produce() method did not terminate. When this block is entered, an output is printed stating that the block was entered first. Then the user is expected to press 'Enter'. Finally, after the user presses 'Enter', a call to notify is made to inform the other thread that has the lock of this block and is waiting that it can now continue. If we run the program and press 'Enter' when prompted, the output will be as follows. Attention: There is an important detail in the output. As soon as the notify() method is called, the thread that was suspended with wait does not continue. Because the block in the consume() method has not been completed yet and therefore, the corresponding lock has not been released. As can be seen from the printout, after five seconds the lock is released and the manufacturer can continue to work. Attention: The wait() method can only be called from within synchronized block codes or synchronized methods. And the objects whose wait() method is called can only be the lock object associated with that synchronized block. Let's say, the lock object specified for the synchronized block is an object named lock object. In this case, the wait() method to be called in this block can only be the method belonging to the lock object. In our example, since we specified the lock object as the current object, that is this.

We called the wait(); method of the current object in the block. Otherwise, the exception named illegal monitor state exception would be thrown. Let's talk about Reentrant locks. The working logic of Reentrant locks is as follows. A Reentrant lock of type Reentrant lock class under the java.util.concurrent.locks.*package is created. The code block that we want only one thread to enter at a time is re-entered and surrounded by the locks; lock and unlock methods. Now, let's continue our review through our sample program. lock = new ReentrantLock();. First thread. Second thread. Our runner class has two methods to be executed simultaneously in two separate threads;  first thread and second thread. Our main(); method will do the same thing as the previous sections. It will create and start the two simultaneous threads. And when the threads are terminated, it will print the last state of the counter with the printCount(); method of the runner class. Therefore, I do not give the main(); method here. When a thread obtains that lock by calling the lock() method, other threads that want to obtain the lock with the lock called are suspended. In the thread that has obtained the lock, it is re-entered and kept on hold until the unlock method of the lock is called. Thus, the functionality of the increment method calls is provided as if they were in a synchronized block. When we run the program, the output will be like this each time.

In order for other threads to obtain the lock, the thread that has obtained the lock must release the lock. It is now possible for another thread that has access to the Reentrant lock object to release the lock. In such a case, you will see that the illegal monitor state exception exception is thrown. Therefore, the thread that has obtained the lock must do this for sure. What if an exception is thrown during its execution and that thread terminates before it can call unlock? For this reason, it would be a good practice to include the code block between the lock and unlock calls in the try and the unlock call finally. Thus, even if an exception is thrown, the lock is released. Finally. Try. Finally. We've learned how Reentrance Locks can be used as an alternative to synchronized code locks. So, what is the equivalent of the wait() and notify() methods which we examined in the previous section, and which give us the functionality to wait and resume a thread according to the condition in this alternative method?

For this, we will need a Java.util.concurrent.locks.condition object, which will be obtained from the lock.new condition call. The await method of the condition class corresponds to the wait() method and the signal method to the notify() method. Now, using the condition object and these methods, let's make our runner class a structure similar to the processor class. FirstThread throws interrupted exception. SecondThread throws interrupted exception. First, we hold the secondThread for two seconds to make sure that the firstThread gets the lock.

Now, our last subject in Thread topic, deadlock. Now, in our example program, we will sample a money transfer between two bank accounts. The account class that defines a bank account. We set the starting balance of the accounts to 10,000. The deposit method will be used to deposit funds into an account, while the withdrawal method will be used to withdraw funds from that account. The transfer helper method will be used to withdraw the specified amount of money specified by the amount parameter from the source account and deposit into the target account. Public void deposit. Withdraw the amount. Balance - =amount. Now, let's examine our runner class that will perform the money transfer. Public class runner. Account1 = new Account. Account2 = new Account. First Thread. Account.transfer. SecondThread. Account.transfer. The Runner class has two methods, first thread and second thread methods that the main method will run simultaneously in a good separate thread. The print balance method which will be called by the main method when these two threads are terminated and print the latest status of the account balances. And finally, it has two account objects of type account. The first thread method transfers a certain amount of money, a random amount, from the first account to the second account in the loop. The second thread method, on the other hand, performs a similar money transfer in the opposite direction, that is from the second account to the first account. Our output is like this. In some cases, you can see the total balance as 20,000, but in most cases you will see such irrelevant results. The reason for this is, of course threads running concurrently, because both threads are processing the first and second account balance values at the same time. It should use a locking mechanism

to resolve this faulty situation. Let's use ReentrantLock for this. Since only one action is required on an account at a time, we have created one ReentrantLock per account. In addition, since transactions are made on two accounts at the same time during a transfer transaction by withdrawing from one and depositing to the other, both of the locks for the relevant account should be locked before the transfer and both locks should be released after the transaction. Thanks to the locking mechanism, we will now always see the output with the total balance of 20,000. The problem has been solved. Since there were only two accounts, we could easily solve it by creating a lock and making lock calls in the correct order. What have we got the order of the lock calls wrong? In such a case, the first thread would acquire the first lock. The second thread, the second lock. And since both locks were obtained at the same time, deadlock would occur. One of the methods that can be used to prevent such a situation is to use the instead of lock.

The tryLock method returns true if it obtains the lock. However, if it still hasn't gotten the lock after a certain amount of time, it will throw an exception. Thus, we are aware of the situation and can take as retrying after a certain period of time. Let's remove first and second lock. Replace acquire locks. firstLock.unlock. secondLock.unlock. We have removed the lock calls in the first thread and the second thread methods, and replaced them with the acquire locks method call. The acquire lock method uses the retry capability provided by the tryLock method and continues to try to obtain the locks  until both locks are obtained at the same time. If only one of the locks is obtained, it releases that lock as to not cause a deadlock. So, that's it. Hope to see you in our next lesson. Have a nice day.

 

About the Author
Students
3906
Courses
64
Learning Paths
5

OAK Academy is made up of tech experts who have been in the sector for years and years and are deeply rooted in the tech world. They specialize in critical areas like cybersecurity, coding, IT, game development, app monetization, and mobile development.

Covered Topics