Concurrency in Java allows multiple threads to execute parts of a program. Concurrency involves multiple instructions interleaving on a single CPU, while parallelism involves multiple instructions executing simultaneously on multiple CPUs.
Thread Implementation
There are several ways to implement multithreading in Java:
-
Extending the
Threadclass: Define a class that inherits fromThreadand overrides therun()method. Instantiate this class and call itsstart()method.// MyThread.java public class MyThread extends Thread { @Override public void run() { // Thread's task } } // Main program public class Main { public static void main(String[] args) { MyThread thread1 = new MyThread(); MyThread thread2 = new MyThread(); thread1.setName("Thread-A"); thread2.setName("Thread-B"); thread1.start(); thread2.start(); } } -
Implementing the
Runnableinterface: Define a class that implementsRunnableand overrides therun()method. Create an instance of this class, then create aThreadobject, passing theRunnableinstance to its constructor. Callstart()on theThreadobject.// MyTask.java public class MyTask implements Runnable { @Override public void run() { // Task logic here Thread currentThread = Thread.currentThread(); System.out.println(currentThread.getName() + " executing."); } } // Main program public class Main { public static void main(String[] args) { MyTask task = new MyTask(); Thread thread1 = new Thread(task, "Worker-1"); Thread thread2 = new Thread(task, "Worker-2"); thread1.start(); thread2.start(); } } -
Using
CallableandFuture: This approach allows threads to return a result. Implement theCallableinterface, which has acall()method returning a value. Wrap theCallablein aFutureTaskand then create aThreadwith theFutureTask.import java.util.concurrent.*; // MyCallable.java public class MyCallable implements Callable<Integer> { @Override public Integer call() throws Exception { // Computation logic return 42; } } // Main program public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException { MyCallable callableTask = new MyCallable(); FutureTask<Integer> futureTask = new FutureTask<>(callableTask); Thread thread = new Thread(futureTask); thread.start(); // Get the result Integer result = futureTask.get(); System.out.println("Task result: " + result); } }
Common Thread Methods
getName()/setName(String name): Retrieves or sets the name of a thread. Threads have default names likeThread-Xif not explicitly named.Thread.currentThread(): Returns theThreadobject representing the currently executing thread.Thread.sleep(long millis): Pauses the current thread for a specified duration in milliseconds. This method can throwInterruptedException.setPriority(int newPriority)/getPriority(): Sets or gets the thread's priority (1-10). Higher priority threads have a higher chance of being scheduled but don't guarantee execution.setDaemon(boolean on): Marks a thread as a daemon thread. Daemon threads are background threads that exit when all non-daemon threads terminate. They are useful for services like logging or monitoring.yield(): Suggests that the currently running thread give up its CPU time to other threads that are ready to run. It's a hint and not guaranteed to be honored.join(): Waits for the thread on which it's called to complete its execution. The calling thread blocks until the joined thread terminates.
Thread Safety and Synchronization
When multiple threads access shared mutable data, race conditions can occur, leading to unpredictable results. Synchronization mechanisms prevent these issues.
-
Synchronized Code Blocks: The
synchronizedkeyword can be used to create a block of code that only one thread can execute at a time. A lock object (monitor) is associated with thesynchronizedblock. Threads must acquire the lock before entreing the block and release it upon exiting.public class SharedResource { private Object lock = new Object(); private int counter = 0; public void increment() { synchronized (lock) { counter++; // Critical section System.out.println(Thread.currentThread().getName() + ": Counter is " + counter); } } } -
Synchronized Methods: When
synchronizedis applied to a method, the lock is implicitly acquired on the object instance (for instance methods) or the class (for static methods) before the method executes and released afterward.public class SafeCounter { private int count = 0; public synchronized void increment() { count++; System.out.println(Thread.currentThread().getName() + ": Count = " + count); } }StringBuilderis not thread-safe; useStringBufferfor multithreaded environments. -
java.util.concurrent.locks.Lock: Introduced in Java 5, theLockinterface provides more flexibility thansynchronized.ReentrantLockis a common implementation.import java.util.concurrent.locks.ReentrantLock; public class TicketSeller implements Runnable { private int tickets = 100; private final ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true) { lock.lock(); // Acquire the lock try { if (tickets <= 0) { break; } Thread.sleep(50); // Simulate ticket selling time tickets--; System.out.println(Thread.currentThread().getName() + " sold ticket. " + tickets + " remaining."); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lock.unlock(); // Release the lock } } } } // Main program public class TicketDemo { public static void main(String[] args) { TicketSeller seller = new TicketSeller(); new Thread(seller, "Window 1").start(); new Thread(seller, "Window 2").start(); } }
Deadlocks
A deadlock occurs when two or more threads are blocked indefinitely, each waiting for a resource held by another thread in the group.
Producer-Consumer Problem
This classic concurrency problem involves two types of threads: producers that create data and consumers that process it. They often communicate via a shared buffer.
-
Using
wait(),notify(),notifyAll(): Threads can wait for a condition to be met and be notified when it changes.// Represents the shared buffer (desk) public class SharedDesk { private boolean hasFood = false; private int foodCount = 10; private final Object lock = new Object(); public void putFood() { synchronized (lock) { while (hasFood) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } if (foodCount <= 0) return; // No more food to produce System.out.println("Cook is preparing food."); hasFood = true; foodCount--; lock.notifyAll(); // Notify consumers } } public void takeFood() { synchronized (lock) { while (!hasFood) { if (foodCount <= 0 && !hasFood) return; // All produced and consumed try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } System.out.println("Customer is eating food."); hasFood = false; lock.notifyAll(); // Notify cook } } public int getFoodCount() { return foodCount; } } // Cook thread public class Cook extends Thread { private SharedDesk desk; public Cook(SharedDesk desk) { this.desk = desk; } @Override public void run() { while (desk.getFoodCount() > 0) { desk.putFood(); } System.out.println("Cook finished."); } } // Customer thread public class Customer extends Thread { private SharedDesk desk; public Customer(SharedDesk desk) { this.desk = desk; } @Override public void run() { while (true) { desk.takeFood(); if (desk.getFoodCount() <= 0 && !desk.hasFood) break; // Exit condition } System.out.println("Customer finished."); } } // Main program public class ProducerConsumerDemo { public static void main(String[] args) { SharedDesk desk = new SharedDesk(); new Cook(desk).start(); new Customer(desk).start(); // Add more customers if needed } } -
Using
BlockingQueue: Thejava.util.concurrentpackage providesBlockingQueueimplementations (likeArrayBlockingQueue) that handle the waiting and notification automatically.import java.util.concurrent.*; // Cook thread public class Cook implements Runnable { private final ArrayBlockingQueue<String> queue; public Cook(ArrayBlockingQueue<String> queue) { this.queue = queue; } @Override public void run() { try { queue.put("Burger"); System.out.println("Cook added a burger."); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } // Customer thread public class Customer implements Runnable { private final ArrayBlockingQueue<String> queue; public Customer(ArrayBlockingQueue<String> queue) { this.queue = queue; } @Override public void run() { try { String food = queue.take(); System.out.println("Customer ate " + food); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } // Main program public class BlockingQueueDemo { public static void main(String[] args) { ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1); new Thread(new Cook(queue)).start(); new Thread(new Customer(queue)).start(); } }
Thread States
Threads transition through various states: New, Runnable, Blocked, Waiting, Timed Waiting, and Terminated.
Thread Pools
Thread pools manage a collection of worker threads to execute tasks. This avoids the overhead of creating and destroying threads for each task.
-
Executorsclass: Provides factory methods for creating thread pools.newCachedThreadPool(): Creates a pool that reuses existing threads or creates new ones as needed. It can grow unbounded.newFixedThreadPool(int nThreads): Creates a pool with a fixed number of threads.
import java.util.concurrent.*; public class ThreadPoolDemo { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); for (int i = 0; i < 5; i++) { executor.submit(() -> { System.out.println(Thread.currentThread().getName() + " executing task."); }); } executor.shutdown(); // Initiate shutdown, prevents new tasks } } -
Custom Thread Pools: For fine-grained control, use
ThreadPoolExecutor.import java.util.concurrent.*; public class CustomThreadPool { public static void main(String[] args) { ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, // corePoolSize 4, // maximumPoolSize 60, TimeUnit.SECONDS, // keepAliveTime new ArrayBlockingQueue<>(5), // workQueue Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy() // Reject Policy ); for (int i = 0; i < 10; i++) { executor.submit(() -> { System.out.println(Thread.currentThread().getName() + " performing task."); try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } executor.shutdown(); } } -
Optimal Pool Size: The ideal number of threads often depends on the number of available processors.
Runtime.getRuntime().availableProcessors()returns this value.
Network Programming
Network programming enables communication between programs on different computers. The two main models are Client/Server (C/S) and Browser/Server (B/S).
Network Communication Essentials
-
IP Address: A unique identifier for a device on a network.
- IPv4: 32-bit addresses, e.g.,
192.168.1.1. - IPv6: 128-bit addresses, e.g.,
2001:0db8:85a3:0000:0000:8a2e:0370:7334. - Loopback Address:
127.0.0.1(orlocalhost) refers to the local machine. InetAddressclass: Used to represent IP addresses.InetAddress.getByName("hostname")retrieves anInetAddressobject.
- IPv4: 32-bit addresses, e.g.,
-
Port Number: Identifies a specific process or service on a host. Ranges from 0 to 65535. Ports 0-1023 are typically reserved for well-known services.
-
Protocols: Rules governing data exchange.
-
UDP (User Datagram Protocol): Connectionless, unreliable, and faster. Uses
DatagramSocketandDatagramPacket.- Sending: Create
DatagramSocket, createDatagramPacketwith data, destination IP, and port, then send.import java.net.*; import java.io.*; public class UDPSender { public static void main(String[] args) throws IOException { DatagramSocket socket = new DatagramSocket(); String message = "Hello UDP!"; byte[] data = message.getBytes(); InetAddress receiverAddress = InetAddress.getByName("127.0.0.1"); int port = 9999; DatagramPacket packet = new DatagramPacket(data, data.length, receiverAddress, port); socket.send(packet); socket.close(); } } - Receiving: Create
DatagramSocketbound to a port, create aDatagramPacketto receive data, callreceive(), then extract data.import java.net.*; import java.io.*; public class UDPReceiver { public static void main(String[] args) throws IOException { DatagramSocket socket = new DatagramSocket(9999); byte[] buffer = new byte[1024]; DatagramPacket packet = new DatagramPacket(buffer, buffer.length); socket.receive(packet); String receivedMessage = new String(packet.getData(), 0, packet.getLength()); System.out.println("Received: " + receivedMessage); socket.close(); } } - Types of UDP communication: Unicast (one-to-one), Multicast (one-to-many within a group using addresses like
224.0.0.1), Broadcast (one-to-all on the network using255.255.255.255).
- Sending: Create
-
TCP (Transmission Control Protocol): Connection-oriented, reliable, and slower. Uses
Socket(client) andServerSocket(server).- Client (Sending): Create
Socketconnected to server IP and port, getOutputStream, write data, close resources.import java.net.*; import java.io.*; public class TCPClient { public static void main(String[] args) throws IOException { Socket socket = new Socket("127.0.0.1", 12345); OutputStream outputStream = socket.getOutputStream(); outputStream.write("Hello TCP!".getBytes()); outputStream.close(); socket.close(); } } - Server (Receiving): Create
ServerSocketbound to a port, callaccept()to get aSocketfrom a client, getInputStream, read data, close resources.import java.net.*; import java.io.*; public class TCPServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(12345); System.out.println("Server listening on port 12345..."); Socket clientSocket = serverSocket.accept(); // Blocks until client connects System.out.println("Client connected: " + clientSocket.getInetAddress()); InputStream inputStream = clientSocket.getInputStream(); byte[] buffer = new byte[1024]; int bytesRead = inputStream.read(buffer); if (bytesRead != -1) { System.out.println("Received: " + new String(buffer, 0, bytesRead)); } inputStream.close(); clientSocket.close(); serverSocket.close(); } }
TCP uses a three-way handshake for connection establishment and a four-way handshake for connection termination.
- Client (Sending): Create
-
Reflection
Reflection allows a Java program to inspect and modify its own structure (classes, fields, methods, constructors) at runtime.
Obtaining Class Objects
Class.forName("com.example.MyClass")(requires full class name).MyClass.class(when the class is known at compile time).object.getClass()(when an instance of the class is available).
Accessing Constructors, Fields, and Methods
Reflection provides methods to get Constructor, Field, and Method objects. The getConstructors(), getDeclaredConstructors(), getFields(), getDeclaredFields(), getMethods(), and getDeclaredMethods() methods retrieve these components. You can also get specific ones using their names and parameter types.
setAccessible(true): Bypasses access control checks (e.g., for private members).newInstance(...): Creates a new instance using a constructor.get(...)/set(...): Get or set the value of a field on an object.invoke(...): Invokes a method on an object.
import java.lang.reflect.*;
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
// Get Class object
Class<?> studentClass = Class.forName("com.example.Student");
// Get and invoke a constructor
Constructor<?> constructor = studentClass.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
Object studentInstance = constructor.newInstance("Alice", 25);
// Access and modify a field
Field nameField = studentClass.getDeclaredField("name");
nameField.setAccessible(true);
String currentName = (String) nameField.get(studentInstance);
System.out.println("Initial name: " + currentName);
nameField.set(studentInstance, "Bob");
// Invoke a method
Method sayHelloMethod = studentClass.getMethod("sayHello");
sayHelloMethod.invoke(studentInstance);
// Invoke a private method
Method studyMethod = studentClass.getDeclaredMethod("study", String.class);
studyMethod.setAccessible(true);
studyMethod.invoke(studentInstance, "Java Reflection");
}
}
Use Cases
Reflection is powerful for frameworks (e.g., dependency injection, ORMs), unit testing, and debugging tools. It enables dynamic object creation and method invocation based on configuration or runtime information.
Dynamic Proxies
Dynamic proxies allow intercepting method calls on an object without modifying its original code. This is useful for adding cross-cutting concerns like logging, security, or transaction management.
-
Key Components:
- The real object (implementing an interface).
- The interface that defines the methods to be proxied.
- An
InvocationHandlerthat defines the interception logic.
-
Proxy.newProxyInstance(...): Creates the proxy object.import java.lang.reflect.*; import java.util.Arrays; // Interface interface RemoteService { String performAction(String input); } // Real implementation class RealService implements RemoteService { @Override public String performAction(String input) { System.out.println("RealService processing: " + input); return "Processed: " + input; } } // Invocation Handler class ServiceProxyHandler implements InvocationHandler { private final Object target; public ServiceProxyHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Proxy: Intercepting call to method " + method.getName() + " with args: " + Arrays.toString(args)); // Call the original method on the target object Object result = method.invoke(target, args); System.out.println("Proxy: Method " + method.getName() + " finished."); return result; } } // Main program public class DynamicProxyDemo { public static void main(String[] args) { RemoteService realService = new RealService(); InvocationHandler handler = new ServiceProxyHandler(realService); RemoteService proxyService = (RemoteService) Proxy.newProxyInstance( RemoteService.class.getClassLoader(), new Class[]{RemoteService.class}, handler ); String output = proxyService.performAction("test input"); System.out.println("Final output: " + output); } }