Advanced Java: Concurrency, Networking, and Reflection

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:

  1. Extending the Thread class: Define a class that inherits from Thread and overrides the run() method. Instantiate this class and call its start() 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();
        }
    }
    
  2. Implementing the Runnable interface: Define a class that implements Runnable and overrides the run() method. Create an instance of this class, then create a Thread object, passing the Runnable instance to its constructor. Call start() on the Thread object.

    // 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();
        }
    }
    
  3. Using Callable and Future: This approach allows threads to return a result. Implement the Callable interface, which has a call() method returning a value. Wrap the Callable in a FutureTask and then create a Thread with the FutureTask.

    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 like Thread-X if not explicitly named.
  • Thread.currentThread(): Returns the Thread object representing the currently executing thread.
  • Thread.sleep(long millis): Pauses the current thread for a specified duration in milliseconds. This method can throw InterruptedException.
  • 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 synchronized keyword 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 the synchronized block. 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 synchronized is 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);
        }
    }
    

    StringBuilder is not thread-safe; use StringBuffer for multithreaded environments.

  • java.util.concurrent.locks.Lock: Introduced in Java 5, the Lock interface provides more flexibility than synchronized. ReentrantLock is 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: The java.util.concurrent package provides BlockingQueue implementations (like ArrayBlockingQueue) 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.

  • Executors class: 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

  1. 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 (or localhost) refers to the local machine.
    • InetAddress class: Used to represent IP addresses. InetAddress.getByName("hostname") retrieves an InetAddress object.
  2. 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.

  3. Protocols: Rules governing data exchange.

    • UDP (User Datagram Protocol): Connectionless, unreliable, and faster. Uses DatagramSocket and DatagramPacket.

      • Sending: Create DatagramSocket, create DatagramPacket with 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 DatagramSocket bound to a port, create a DatagramPacket to receive data, call receive(), 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 using 255.255.255.255).
    • TCP (Transmission Control Protocol): Connection-oriented, reliable, and slower. Uses Socket (client) and ServerSocket (server).

      • Client (Sending): Create Socket connected to server IP and port, get OutputStream, 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 ServerSocket bound to a port, call accept() to get a Socket from a client, get InputStream, 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.

Reflection

Reflection allows a Java program to inspect and modify its own structure (classes, fields, methods, constructors) at runtime.

Obtaining Class Objects

  1. Class.forName("com.example.MyClass") (requires full class name).
  2. MyClass.class (when the class is known at compile time).
  3. 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:

    1. The real object (implementing an interface).
    2. The interface that defines the methods to be proxied.
    3. An InvocationHandler that 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);
        }
    }
    

Tags: java Concurrency multithreading networking tcp

Posted on Tue, 30 Jun 2026 16:03:21 +0000 by 182x