- 1. Critical section
- 1. Example
- 2. Interface Channel
- 1. Example
- 3. Events
- 1. Example
- 2. Conclusion
I propose to consider several ways to synchronize threads and processes that are most commonly used in Java. They differ in implementation and use cases. We will consider all methods on interesting examples.
Critical section
This method is suitable for you if:
- parallel threads work with a shared resource;
- requires synchronization between threads, not processes;
This method of synchronization is called resource synchronization ("open – close" synchronization). The idea of this method is that each object in Java has an associated monitor . A monitor is a kind of tool to control access to an object.
he synchronized statement is used to create a critical section. When code execution reaches the synchronized operator, the object monitor is blocked. At the time of its blocking, exclusive access to the code block has only one thread that made the blocking.After the code block finishes working, the object monitor is released and made available to other threads.
When the monitor is released, it is captured by another thread, and all other threads continue to wait for it to be released.
Example
Imagine how a retail online store works. After one of the buyers put the goods in the basket, the remaining goods should be counted. Only after that, another buyer will be able to put the desired amount of goods in the basket. After that, buy them.
public class Program { private static final Boolean key = true; //the object will be used for synchronization //it must be final private static Integer amountOfGoods = 20; public static class Buyer implements Runnable { @Override public void run() { final int goodsInTheShoppingCart = (int) (1 + Math.random() * amountOfGoods); //the number of goods that the buyer will put in the basket synchronized (key) { if (amountOfGoods - goodsInTheShoppingCart >= 0) { System.out.println("The " + Thread.currentThread().getName() + " placed " + goodsInTheShoppingCart + " items in the basket."); if ((amountOfGoods = amountOfGoods - goodsInTheShoppingCart) < 0) { System.out.println("Whoops! The product is out of stock, but the " + Thread.currentThread().getName() + " didn't know about it."); return; } else { System.out.println("There are " + amountOfGoods + " items left in the store."); } } else { System.out.println("The " + Thread.currentThread().getName() + " is notified that the goods are over."); return; } } System.out.println("The " + Thread.currentThread().getName() + " made a purchase."); } } public static void main(String[] args) { for (int i = 1; i <= 5; i++) { Thread t = new Thread(new Buyer()); // create five threads t.setName("buyer " + i); System.out.println("The " + t.getName() + " went to the store website."); t.start(); } } }
Work of the program before using the synchronization unit
We see that threads execute program code randomly. Because of this, "buyers" put the goods in baskets, but remain without goods.
Now we will add a synchronization block and see how the work of the program changes.
Now the program works exactly as we expected. Great!
Interface Channel
You can use Interface Channel in case you need to synchronize different processes . The channel is created at the JVM level and will be one in the system.
A channel represents an open connection to an entity such as a hardware device, a file, a network socket, or a program component that is capable of performing one or more distinct I/O operations, for example reading or writing.
A channel is either open or closed. A channel is open upon creation, and once closed it remains closed. Once a channel is closed, any attempt to invoke an I/O operation upon it will cause a ClosedChannelException to be thrown. Whether or not a channel is open may be tested by invoking its isOpen() and tryLock() methods. ).
Java package.nio.channels contains channels such as:
- AsynchronousChannel
- ByteChannel
- NetworkChannel
- FileChannel
- InterruptibleChannel
- MulticastChannel and other.
Example
Two processes write data to a file. They should record alternately, without interfering, in each other's work.
Let's create two projects that differ only in the fact that they will be written to a file - "Program 1" or "Program 2". We run them at the same time.
public class Program { public static void main(String[] args) throws InterruptedException, IOException { File file = new File("C:\\text.txt"); FileChannel fileChannel = new FileOutputStream(file, true).getChannel(); while(fileChannel.tryLock() == null){ // the process checks if the channel is free to write to the file Thread.sleep(10000); } try { String lineSeparator = System.getProperty("line.separator"); for (int i = 0; i < 10; i++) { String newData = "The program 2 is recording." + lineSeparator; ByteBuffer buf = ByteBuffer.allocate(200); buf.put(newData.getBytes()); buf.flip(); fileChannel.write(buf); Thread.sleep(1000); } } catch (IOException e) { e.printStackTrace(); } fileChannel.close(); } }
If we were to use the classic way of writing to a file with FileWriter, we would find something similar in the file.
Now let's see what will be in the file if we use channels.
This is exactly what we need! Processes take access to the file one by one.
Events
In addition to data synchronization, there is synchronization by events. When concurrently running threads are suspended until some event is triggered by another thread. The main operations for this type of synchronization are wait and notify.
This is a “wait – notify " synchronization”:
- parallel threads wait for some event;
- one of the threads is responsible for the occurrence of the event and notifies the rest of the threads(one using notify or all - notifyAll,) unlocking them:
- only suitable for synchronization of threads, not processes;
Example
Let's imagine how the automatic weapon reloading system works. For example, only has 15 cartridges in stock. The store can only hold 6. The shooter wants to use all the cartrisges.
public class Program { public static void main(String[] args) { WeaponStore weaponStore = new WeaponStore(); Arsenal arsenal = new Arsenal(weaponStore); Shooter shooter = new Shooter(weaponStore); new Thread(arsenal).start(); new Thread(shooter).start(); } } class WeaponStore { private int cartridges = 0; public synchronized void shot() { // synchronization of the method is required // because two threads will have access to it while (cartridges < 1) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //was thrown out notify by another thread, then the cartridge is received cartridges--; System.out.println("Produced 1 shot." + " Cartridges in the store: " + cartridges); notify(); } public synchronized void reload(int arsenalCartriges) { // synchronization of the method is required // because two threads will have access to it while (cartridges >= 6) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } cartridges++; System.out.println("1 cartridge has been added to the store." + " Cartridges in stock: " + arsenalCartriges); notify(); } } class Arsenal implements Runnable{ private int cartridges = 15; private WeaponStore weapon; Arsenal(WeaponStore weapon){ this.weapon = weapon; } public void run(){ for (int cartridges = 15; cartridges > 0; cartridges--) { weapon.reload(cartridges); } } } class Shooter implements Runnable{ private WeaponStore weapon; Shooter(WeaponStore weapon){ this.weapon = weapon; } public void run(){ for (int i = 1; i <= 15; i++) { weapon.shot(); } } }
The result of the program
1 cartridge has been added to the store. Cartridges in stock: 15 1 cartridge has been added to the store. Cartridges in stock: 14 1 cartridge has been added to the store. Cartridges in stock: 13 1 cartridge has been added to the store. Cartridges in stock: 12 1 cartridge has been added to the store. Cartridges in stock: 11 Produced 1 shot. Cartridges in the store: 4 Produced 1 shot. Cartridges in the store: 3 Produced 1 shot. Cartridges in the store: 2 Produced 1 shot. Cartridges in the store: 1 Produced 1 shot. Cartridges in the store: 0 1 cartridge has been added to the store. Cartridges in stock: 10 1 cartridge has been added to the store. Cartridges in stock: 9 1 cartridge has been added to the store. Cartridges in stock: 8 1 cartridge has been added to the store. Cartridges in stock: 7 1 cartridge has been added to the store. Cartridges in stock: 6 1 cartridge has been added to the store. Cartridges in stock: 5 Produced 1 shot. Cartridges in the store: 5 Produced 1 shot. Cartridges in the store: 4 Produced 1 shot. Cartridges in the store: 3 Produced 1 shot. Cartridges in the store: 2 Produced 1 shot. Cartridges in the store: 1 Produced 1 shot. Cartridges in the store: 0 1 cartridge has been added to the store. Cartridges in stock: 4 1 cartridge has been added to the store. Cartridges in stock: 3 1 cartridge has been added to the store. Cartridges in stock: 2 1 cartridge has been added to the store. Cartridges in stock: 1 Produced 1 shot. Cartridges in the store: 3 Produced 1 shot. Cartridges in the store: 2 Produced 1 shot. Cartridges in the store: 1 Produced 1 shot. Cartridges in the store: 0
Conclusion
As you noticed, the synchronization methods discussed above have different uses. Choose the right one and always synchronize your threads and processes so that they live peacefully and never interfere in each other’s work.
Subscribe to the Java section if you want more interesting articles. Write your opinion and ideas regarding this topic in the comments.