Ch5 - Parallelism and Distributed Objects
Ch5 - Parallelism and Distributed Objects
CHAPTER V
PARALLELISM AND DISTRIBUTED OBJECTS
2024/2025 Prof. Abdelhakim HANNOUSSE [email protected]
Outline
2
Parallelism involves executing multiple tasks at the same time to improve efficiency
and reduce the overall execution time.
Two main types of parallelisms exist:
True parallelism: case of multiple processors/cores + dedicated OS.
Pseudo-parallelism: case where tasks take turns (interleaving).
Parallelism isn’t just for speed or comfort — it’s essential for making complex programs
feasible within realistic time limits.
Modern applications (e.g., AI, big data, simulations) involve computations that are too large
or time-sensitive to be handled sequentially.
Systems like web servers, cloud services, and databases rely on parallelism to serve
thousands/millions of users concurrently.
Applications like autonomous vehicles and robotics must process multiple streams of data
simultaneously to meet real-time constraints.
Multithreading
5
Multithreading is based on the concept of having multiple execution parts within the
same program, known as threads, which can run independently.
A thread is an execution unit that represents an independent execution path inside a
program. A thread runs autonomously and in parallel with other threads.
Multithreading can be true or pseudo-parallelism depending on hardware and OS:
On multi-core processors, threads may run simultaneously ➤ True parallelism
On single-core processors or when there are not enough cores, threads are
interleaved by the CPU scheduler ➤ Pseudo-parallelism
In every Java application, the JVM starts a main thread that runs the code within the
main method ➤ every Java application has at least one thread.
Each thread has a priority. Threads with higher priority are executed preferentially
over threads with lower priority.
Multithreading vs. Concurrent Process Execution (1/2)
6
myThread.start() notify(),
notifyAll()
myThread.wait()
timeout
elapsed run()
returns
myThread.sleep(timeout)
myThread.wait(timeout)
myThread.join(timeout)
Methods of the Thread class
12
Method Description
currentThread() Returns the thread currently being executed.
getName()/setName() Returns/Updates the thread’s name. Default names form: Thread-N
isAlive() Returns true if the thread is not in the NEW or TERMINATED state.
start() Transitions a thread from NEW to RUNNABLE state.
run() Defines the code that the thread executes. It is invoked automatically
when the thread reaches the RUNNING state after start() is called.
sleep(n) Stops the execution of a thread for n milliseconds.
join() Waits for the end of the thread to proceed to the next statement.
getState() Returns the state of the thread (NEW, RUNNABLE, BLOCKED, WAITING,
TIMED_WAITING, TERMINATED).
getPriority()/setPriority(int) Returns/Updates the thread's priority.
setDaemon(boolean) Marks the thread as a daemon thread or a regular or user thread.
Priorities of Threads
13
import java.util.Random;
class NumberGenerator implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
number = rand.nextInt(100);
System.out.println("Thread Producer - Generated number: " + number);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Multithreading in Java – Example (2/4)
15
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread Handler - Square of generated number : " + (i * i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Multithreading in Java – Example (3/4)
16
try {
// Ensure the main thread waits for both threads to finish before exiting
thread1.join();
thread2.join();
} catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("Execution finished.");
}
}
Multithreading in Java – Example (4/4)
17
A daemon thread is a special type of thread that runs in the background and does
not keep the main program alive if it is the only active thread remaining.
Regular threads handle the main tasks of the application such as data processing or
user input, while daemon threads run background services such as garbage collection,
monitoring, or logging.
The JVM continues running as long as at least one regular thread is active. It can
terminate even if one or more daemon threads are still running.
By default, all threads are regular. To create a daemon thread, you must explicitly
call setDaemon(true) before starting the thread.
Daemon Threads (2/4)
19
import java.util.Scanner;
class UserInputGetter implements Runnable {
@Override
public void run() {
Scanner scanner = new Scanner(System.in);
System.out.print("Enter your name: ");
String name = scanner.nextLine();
System.out.println("Hello " + name);
scanner.close();
}
}
class Timer implements Runnable {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("Time's up!");
System.exit(0);
}
}
Daemon Threads (3/4)
20
At the end of the synchronized method call, any waiting thread can be selected by
the JVM thread scheduler, which may not follow a strict order. The choice depends on:
Thread priorities, JVM scheduling policy, and OS-level thread management
Thread Synchronization – Monitors (2/3)
24
When a block of code is marked as synchronized, it means that access to this block
is regulated by the monitor (the current object or any other object) associated with
the block. synchronized(this) {
// Critical section
}
To ensure mutual exclusion to a method or block across all instances of a class, we use
class-level synchronization. This is essential when threads must coordinate access to
shared resources. Two main approaches are used:
Using static synchronized methods: Public static synchronized TYPE method(…) {
// Critical section
}
Using synchronized blocks with a shared monitor:
private static ClassObject obj = new ClassObject(); synchronized (MyClass.class) {
... // Critical section
synchronized (obj) { // Critical section } }
Thread Synchronization – Monitors (3/3)
25
The wait() and notify() methods allow threads to cooperate. wait() puts a thread
into a waiting state until another thread calls notify() to wake it up.
When a thread is inside a synchronized method or block and calls the wait() method,
it releases the monitor of the object it is synchronized on, allowing other threads to
access synchronized methods on the same object while this thread is waiting.
When a thread is awakened after a call to notify() or notifyAll(), it waits to obtain
the monitor of the object again before it can resume execution.
notify(): wakes up only one thread waiting on the object's monitor. The choice of
which thread to wake up is not predictable — it's up to the JVM scheduler.
notifyAll(): wakes up all threads waiting on the object's monitor. Only one thread
will acquire the lock and proceed — others go back to waiting state.
Using notify() when multiple threads are involved can lead to missed signals or
deadlocks if the awakened thread isn't the one that can proceed.
Thread Synchronization – Monitors – Example (1/4)
26
import java.util.Random;
class NumberGenerator implements Runnable {
private Random rand = new Random();
private int number;
private boolean numberProduced = false;
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
synchronized(this) {
number = rand.nextInt(100);
System.out.println("Thread Producer - Generated number: " + number);
numberProduced = true;
this.notify();
}
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
public synchronized int getGeneratedNumber() throws InterruptedException {
while (!numberProduced) this.wait();
numberProduced = false;
return number;
}
}
Thread Synchronization – Monitors – Example (2/4)
27
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
int number = generator.getGeneratedNumber();
System.out.println("Thread Handler - Square of generated number: " + (number * number));
} catch (InterruptedException e) { e.printStackTrace(); }
}
}
}
Thread Synchronization – Monitors – Example (3/4)
28
generatorThread.start();
processorThread.start();
try {
generatorThread.join();
processorThread.join();
} catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("Execution finished.");
}
}
Thread Synchronization – Monitors – Example (4/4)
29
Monitors are high-level synchronization structures that allow for mutual exclusion,
passive waiting, etc., without the need for mutexes or semaphores.
Semaphores can be used to regulate access to a set of shared resources and are
useful for controlling concurrent access in scenarios where a limited number of threads
can simultaneously access certain resources.
In Java, the Semaphore class is part of the java.util.concurrent package and
provides an implementation of a semaphore.
With semaphores, threads attempt to acquire the semaphore using the acquire()
method, perform critical operations, and then release the semaphore using the
release() method. This allows for controlling concurrent access to shared resources.
Thread Synchronization – Semaphores – Example
31
(1/3)
import java.util.Random;
import java.util.concurrent.Semaphore;
class NumberGenerator implements Runnable {
private Random rand = new Random();
private int number;
private final Semaphore production = new Semaphore(1);
private final Semaphore consumption = new Semaphore(0);
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try { production.acquire();
number = rand.nextInt(100);
System.out.println("Thread Producer - Generated number: " + number);
consumption.release();
} catch (InterruptedException e) { e.printStackTrace(); }
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
public int getGeneratedNumber() throws InterruptedException {
consumption.acquire();
int result = this.number;
production.release();
return result;
}
}
Thread Synchronization – Semaphores – Example
32
(2/3)
class NumberProcessor implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
int number = generator.getGeneratedNumber();
System.out.println("Thread Handler - Square of generated number: " + (number * number));
} catch (InterruptedException e) { e.printStackTrace(); }
}
}
}
Thread Synchronization – Semaphores – Example
33
(3/3)
public class SemaphoreSyncExample {
public static void main(String[] args) {
NumberGenerator generator = new NumberGenerator();
Thread generatorThread = new Thread(generator);
Thread processorThread = new Thread(new NumberProcessor(generator));
generatorThread.start();
processorThread.start(); Thread Producer - Generated number: 97
Thread Handler - Square of generated number: 9409
try { Thread Producer - Generated number: 19
generatorThread.join(); Thread Handler - Square of generated number: 361
processorThread.join(); Thread Producer - Generated number: 84
Thread Handler - Square of generated number: 7056
} catch (InterruptedException e) { Thread Producer - Generated number: 2
e.printStackTrace(); Thread Handler - Square of generated number: 4
} Thread Producer - Generated number: 54
Thread Handler - Square of generated number: 2916
System.out.println("Execution finished."); Execution finished.
}
}
34 Distributed Objects
Introduction to Object Distribution
35
Uses serialization and marshalling for data transfer and proxies for communication.
Sockets and RMI are two fundamental techniques for enabling object distribution.
36 Distributed Objects: Sockets
Low-level Communication
Sockets Mechanism (1/3)
37
Using sockets in UDP mode involves some differences compared to using TCP sockets:
UDP is simpler and faster than TCP — no handshake and minimal overhead.
Unlike TCP, UDP does not ensure packet delivery or ordering nor data completeness
but reliability isn't ignored — it's offloaded to developers.
UDP is ideal for applications when timeliness matters more than reliability:
◼ VoIP (e.g., Skype, Zoom) where missing a word is better than delayed audio
◼ Live video streaming where a dropped frame is OK — delay isn't
◼ Online gaming where real-time reaction matters more than every update
There are two main types of sockets:
Server-side sockets: listens for incoming connections.
Once a connection is established between the client and the server, data streams can
be created on both sides to allow reading and writing data over the connection.
The lifecycle of communication via sockets typically involves:
1. Create sockets – The server creates a server socket to listen for connections,
while the client creates a socket to initiate a connection.
2. Establishing a connection – The server accepts incoming connections, and the
client connects to the server.
4. Data exchange – Client and server communicate through streams of their sockets.
5. Closing the connection – Once communication is complete, both sides close their
streams and sockets to release resources.
Sockets Types
40
Sockets in Java
41
Using sockets in UDP mode in java involves some differences compared to using TCP
sockets.
Sockets in Java – TCP Mode (1/3)
42
The range of ports in Java for TCP and UDP sockets is from 0 to 65535.
◼ Ports 0 to 1023 (reserved ports) are typically reserved for system services.
◼ Ports 1024 to 49151 (registered ports) can be used by user applications.
◼ Ports 49152 to 65535 (private ports) are generally used for temporary
connections.
If no port is specified when creating a socket, the operating system assigns a
temporary (private) port as the source port for the outgoing connection.
Sockets in Java – TCP Mode (2/3)
43
To accept incoming connections on the server side, the accept() method of the
ServerSocket object is used. This method returns a Socket object representing the
connection established with a client.
Socket clientSocket = serverSocket.accept();
Once a connection is established, data streams associated with the socket can be
obtained for communication. InputStream and OutputStream are commonly used to
read and write data.
InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream();
Sockets in Java – TCP Mode (3/3)
44
We use the input and output stream methods to exchange data between the client and
the server.
// Reading from the server side
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String clientMessage = reader.readLine();
// Closing streams
reader.close(); writer.close(); socket.close();
} catch (IOException e) { e.printStackTrace(); }
}
}
Sockets in Java – Mode TCP – Example (2/2)
46
DatagramSocket is used to create a socket that will be utilized for sending UDP
datagrams (packets) to the server.
DatagramSocket socket = new DatagramSocket();
The data to be sent must first be converted into bytes before being encapsulated by a
DatagramPacket.
byte[] data = message_to_be_sent.getBytes();
InetAddress serverAddress = InetAddress.getByName(server_ip);
DatagramPacket packet = new DatagramPacket(data, data.length, serverAddress, server_port);
Once the datagram is received, you can extract the data from the DatagramPacket
similarly to how you read/write using streams in TCP.
String message = new String(packet.getData(), 0, packet.getLength());
import java.net.*;
class UDPClient {
public static void main(String[] args) {
try {
// Create a DatagramSocket
DatagramSocket socket = new DatagramSocket();
// Specify the server address and port
InetAddress serverAddress = InetAddress.getByName("localhost");
int serverPort = 9876;
// Create the message to send
String message = "Hello, Server!";
// Convert the message into a byte array
byte[] buffer = message.getBytes();
// Create a DatagramPacket to send the data
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, serverAddress, serverPort);
// Send the packet
socket.send(packet);
// Close the DatagramSocket
socket.close();
} catch (Exception e) { e.printStackTrace(); }
}
}
Sockets in Java – UDP Mode – Example (2/2)
50
import java.net.*;
class UDPServer {
public static void main(String[] args) {
try {
// Create a DatagramSocket to listen on port 9876
DatagramSocket socket = new DatagramSocket(9876);
System.out.println("Server is listening on port 9876...");
// Create a buffer for receiving data
byte[] buffer = new byte[1024];
// Create a DatagramPacket to hold incoming data
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
// Receive a packet
socket.receive(packet);
System.out.println("Packet received!");
// Extract the data and display it
String receivedMessage = new String(packet.getData(), 0, packet.getLength());
System.out.println("Message from client: " + receivedMessage);
// Close the DatagramSocket
socket.close();
} catch (Exception e) { e.printStackTrace(); }
}
}
51 Distributed Objects : RMI
RMI = Remote Method Invocation
High-level Abstraction
RMI Mechanism (1/2)
52
First, you need to define a Java interface that exposes the methods you want to be
called remotely.
This interface must extend java.rmi.Remote, and each method must declare throws
RemoteException.
import java.rmi.Remote;
import java.rmi.RemoteException;
Implement the remote interface on the server. The objects on the server side that
implement this interface are the remote objects.
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
@Override
public TYPE remoteMethod() throws RemoteException {
//Do somthing and return the intended result;
}
}
RMI in Java – Implementation of the Remote Object
(2/2)
57
If the remoteObjectClass needs to inherit from another user-defined class, you can
overcome Java’s single inheritance limitation by using delegation — manually export
the object using UnicastRemoteObject.exportObject() instead of extending
UnicastRemoteObject.
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
@Override
public TYPE remoteMethod() throws RemoteException {
//Do somthing and return the intended result;
}
}
RMI in Java – Registering the Remote Object (Server)
58
The remote object must be registered in the RMI registry so that it can be found by
clients.
import java.rmi.Naming;
} catch (Exception e) {
e.printStackTrace();
}
}
}
RMI in Java – Calling the Remote Object (Client)
59
The client can search for and call methods on the remote object using the automatically
generated stub.
import java.rmi.Naming;
Use the cd command to navigate to the directory where your compiled class files
(.class) are located.
Run the rmiregistry command.
import java.rmi.Remote;
import java.rmi.RemoteException;
// Remote Interface
public interface Calculator extends Remote {
int add(int a, int b) throws RemoteException;
}
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
@Override
public int add(int a, int b) throws RemoteException {
return a + b;
}
}
RMI in Java – Example (2/2)
62
import java.rmi.Naming;
public class CalculatorServer {
public static void main(String[] args) {
try {
Calculator calculator = new CalculatorImpl();
Naming.rebind("CalculatorService", calculator);
System.out.println("Calculator server is running...");
} catch (Exception e) { e.printStackTrace(); }
}
}