📘 PART 5: THREADS, MULTITHREADING AND JDBC
🎯 General Objectives
- Understand how to handle data input/output using Streams.
- Get familiar with Multithreading programming.
- Connect and manipulate data with databases using JDBC.
🧑🏫 Lesson 1: JAVA I/O Streams
Concept of Streams in Java
- A Stream is a continuous sequence of data, read from a source or written to a destination. In Java, streams are managed through classes in the
java.iopackage.
Basic Classification
Byte Streams: Handle 8-bit byte data.
- Base classes:
InputStreamandOutputStream - Suitable for binary data like images, audio, video
- Base classes:
Character Streams: Handle character data (Unicode).
- Base classes:
ReaderandWriter - Suitable for text, configuration files
- Base classes:
Commonly Used Classes
Byte Streams
FileInputStream/FileOutputStream: Read/write data from/to filesBufferedInputStream/BufferedOutputStream: Optimize performance using buffersDataInputStream/DataOutputStream: Read/write primitive data typesObjectInputStream/ObjectOutputStream: Read/write objects (Serialization)
Character Streams
FileReader/FileWriter: Read/write text from/to filesBufferedReader/BufferedWriter: Optimize performance for character streamsInputStreamReader/OutputStreamWriter: Convert between bytes and charactersPrintWriter: Output formatted data
Reading Files with InputStream and Reader
Reading Binary Files with FileInputStream
public static void readBinaryFile(String filePath) {
try (FileInputStream fis = new FileInputStream(filePath)) {
int data;
System.out.println("Reading binary data from file: " + filePath);
// Read each byte until end of file (-1)
while ((data = fis.read()) != -1) {
System.out.print(data + " ");
}
} catch (FileNotFoundException e) {
System.err.println("File not found: " + e.getMessage());
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
}Reading Text Files with BufferedReader
public static void readTextFile(String filePath) {
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
String line;
System.out.println("Reading text from file: " + filePath);
// Read each line until end of file (null)
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (FileNotFoundException e) {
System.err.println("File not found: " + e.getMessage());
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
}Writing Files with OutputStream and Writer
Writing Binary Files with FileOutputStream
public static void writeBinaryFile(String filePath, byte[] data) {
try (FileOutputStream fos = new FileOutputStream(filePath)) {
fos.write(data);
System.out.println("Written " + data.length + " bytes to file: " + filePath);
} catch (FileNotFoundException e) {
System.err.println("Cannot create file: " + e.getMessage());
} catch (IOException e) {
System.err.println("Error writing file: " + e.getMessage());
}
}Writing Text Files with BufferedWriter
public static void writeTextFile(String filePath, List<String> lines) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) {
for (String line : lines) {
writer.write(line);
writer.newLine(); // Add newline character
}
System.out.println("Written " + lines.size() + " lines to file: " + filePath);
} catch (IOException e) {
System.err.println("Error writing file: " + e.getMessage());
}
}Real-world Application: Copying Files with Buffer
public static void copyFile(String sourceFile, String destinationFile) {
try (FileInputStream fis = new FileInputStream(sourceFile);
BufferedInputStream bis = new BufferedInputStream(fis);
FileOutputStream fos = new FileOutputStream(destinationFile);
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
byte[] buffer = new byte[4096]; // 4KB Buffer
int bytesRead;
// Read and write in buffer chunks
while ((bytesRead = bis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
System.out.println("File copied successfully!");
System.out.println("From: " + sourceFile);
System.out.println("To: " + destinationFile);
} catch (IOException e) {
System.err.println("Error copying file: " + e.getMessage());
}
}Serialization and Deserialization
Serialization is the process of converting an object into a byte stream for storage or transmission:
// Student class must implement Serializable
public static void serializeObject(String filePath, Student student) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath))) {
oos.writeObject(student);
System.out.println("Saved Student object to file: " + filePath);
} catch (IOException e) {
System.err.println("Error saving object: " + e.getMessage());
}
}
public static Student deserializeObject(String filePath) {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath))) {
Student student = (Student) ois.readObject();
System.out.println("Read Student object from file: " + filePath);
return student;
} catch (IOException | ClassNotFoundException e) {
System.err.println("Error reading object: " + e.getMessage());
return null;
}
}Handling Different Encodings
public static void readFileWithEncoding(String filePath, String encoding) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(filePath), encoding))) {
String line;
System.out.println("Reading file with encoding " + encoding + ":");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (UnsupportedEncodingException e) {
System.err.println("Encoding not supported: " + encoding);
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
}Practice: Creating a Logger Application
public class SimpleLogger {
private static final String LOG_FILE = "application.log";
private static PrintWriter writer;
static {
try {
// Create writer for log file, append mode (true)
writer = new PrintWriter(new BufferedWriter(new FileWriter(LOG_FILE, true)));
// Add header on startup
writer.println("=== Log started at: " + new Date() + " ===");
writer.flush();
} catch (IOException e) {
System.err.println("Cannot initialize logger: " + e.getMessage());
}
}
public static void log(String message) {
String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
writer.println(timestamp + " - " + message);
writer.flush();
}
public static void close() {
if (writer != null) {
writer.println("=== Log closed at: " + new Date() + " ===");
writer.close();
}
}
}🧑🏫 Lesson 2: Multithreading in JAVA
Thread Concept and Benefits of Multithreading
Thread is the smallest unit of processing that can be scheduled by the operating system. A Java program runs in a separate process, but can have multiple threads executing concurrently within that process.
Benefits of Multithreading
- CPU Resource Utilization: On multi-core systems, multiple threads can run in parallel.
- Increased Performance: Perform multiple tasks simultaneously.
- Improved Responsiveness: Keep user interface responsive while performing heavy tasks in background.
- Optimized Wait Time: While one thread waits for I/O, other threads can continue execution.
Creating Threads in Java
There are two main ways to create threads in Java:
1. Extending Thread Class
class MyThread extends Thread {
@Override
public void run() {
// Code to be executed in new thread
for (int i = 1; i <= 5; i++) {
System.out.println("Thread [" + getName() + "]: Number " + i);
try {
// Pause thread for 1 second
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Thread interrupted");
return;
}
}
System.out.println("Thread [" + getName() + "] finished.");
}
}
// Usage
public static void main(String[] args) {
MyThread thread1 = new MyThread();
thread1.setName("MyThread-1");
thread1.start(); // Start new thread, calls run() method
// Create and start another thread
MyThread thread2 = new MyThread();
thread2.setName("MyThread-2");
thread2.start();
System.out.println("Main thread continues execution...");
}2. Implementing Runnable Interface
class MyRunnable implements Runnable {
@Override
public void run() {
// Code to be executed in new thread
String threadName = Thread.currentThread().getName();
for (int i = 1; i <= 5; i++) {
System.out.println("Thread [" + threadName + "]: Number " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Thread interrupted");
return;
}
}
System.out.println("Thread [" + threadName + "] finished.");
}
}
// Usage
public static void main(String[] args) {
// Create Runnable object
MyRunnable myRunnable = new MyRunnable();
// Create thread with Runnable
Thread thread1 = new Thread(myRunnable, "Thread-A");
Thread thread2 = new Thread(myRunnable, "Thread-B");
// Start threads
thread1.start();
thread2.start();
System.out.println("Main thread continues execution...");
}3. Using Lambda Expressions (Java 8+)
public static void main(String[] args) {
// Create thread with lambda expression
Thread thread = new Thread(() -> {
String name = Thread.currentThread().getName();
for (int i = 1; i <= 5; i++) {
System.out.println("Thread [" + name + "]: Number " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Thread interrupted");
return;
}
}
System.out.println("Thread [" + name + "] finished.");
}, "Lambda-Thread");
thread.start();
System.out.println("Main thread continues execution...");
}Thread Management
Starting a Thread (start)
When thread.start() is called, the JVM allocates resources, schedules, and calls the run() method. The new thread runs in parallel with other threads.
Thread thread = new Thread(() -> System.out.println("New thread running"));
thread.start(); // Start new threadWaiting for Thread Completion (join)
The join() method causes the current thread to wait until another thread completes.
Thread worker = new Thread(() -> {
System.out.println("Worker started...");
try {
Thread.sleep(2000); // Simulate work taking 2 seconds
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Worker finished!");
});
worker.start();
System.out.println("Main thread waiting for worker...");
try {
worker.join(); // Main thread waits here until worker finishes
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread continues after worker finished");Pausing a Thread (sleep)
The Thread.sleep(milliseconds) method pauses the current thread for a specified duration.
try {
System.out.println("Starting pause...");
Thread.sleep(3000); // Pause 3 seconds
System.out.println("Continuing after pause!");
} catch (InterruptedException e) {
System.out.println("Interrupted during sleep!");
}Interrupting a Thread (interrupt)
The interrupt() method marks a thread as "interrupted" and is often used to request a thread to terminate early.
Thread workerThread = new Thread(() -> {
try {
System.out.println("Worker thread started...");
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Processing...");
Thread.sleep(500);
}
} catch (InterruptedException e) {
// Thread.sleep() throws InterruptedException when thread is interrupted
System.out.println("Worker thread interrupted during sleep");
return; // Exit run() method
}
System.out.println("Worker thread finished normally");
});
workerThread.start();
// Let thread run for 2 seconds
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Interrupt thread
System.out.println("Main thread requesting worker to stop...");
workerThread.interrupt();Thread Priority
You can set priority for threads (1-10, default is 5):
Thread highPriorityThread = new Thread(() -> {
// code...
});
highPriorityThread.setPriority(Thread.MAX_PRIORITY); // 10
Thread lowPriorityThread = new Thread(() -> {
// code...
});
lowPriorityThread.setPriority(Thread.MIN_PRIORITY); // 1Synchronization
When multiple threads access a shared resource, data inconsistency (race condition) can occur. Synchronization ensures data integrity.
Race Condition Problem
class Counter {
private int count = 0;
public void increment() {
count++; // Not thread-safe!
}
public int getCount() {
return count;
}
}
// Usage with multithreading
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
// Wait for both threads to finish
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Result might be less than 20000 due to race condition
System.out.println("Final value: " + counter.getCount());Synchronization using synchronized keyword
class SynchronizedCounter {
private int count = 0;
// Synchronized method - only one thread can enter at a time
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
// Or using synchronized block
class SynchronizedCounter2 {
private int count = 0;
private final Object lock = new Object(); // Lock object
public void increment() {
// Only synchronize critical section
synchronized(lock) {
count++;
}
}
public int getCount() {
synchronized(lock) {
return count;
}
}
}Real-world Example: File Access Synchronization
class FileLogger {
private final String fileName;
private final Object fileLock = new Object();
public FileLogger(String fileName) {
this.fileName = fileName;
}
public void logMessage(String message) {
String threadName = Thread.currentThread().getName();
String logEntry = new java.util.Date() + " - " + threadName + ": " + message;
synchronized(fileLock) {
try (java.io.PrintWriter writer = new java.io.PrintWriter(
new java.io.FileWriter(fileName, true))) {
writer.println(logEntry);
} catch (java.io.IOException e) {
System.err.println("Error writing log: " + e.getMessage());
}
}
}
}
// Usage
FileLogger logger = new FileLogger("application.log");
Runnable logTask = () -> {
for (int i = 1; i <= 5; i++) {
logger.logMessage("Record #" + i);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// Create multiple threads logging together
new Thread(logTask, "Logger-A").start();
new Thread(logTask, "Logger-B").start();
new Thread(logTask, "Logger-C").start();Issues in Multithreading
Deadlock
Deadlock occurs when two or more threads wait for each other indefinitely.
public static void demonstrateDeadlock() {
final Object resource1 = new Object();
final Object resource2 = new Object();
// Thread 1: Acquires resource1, then tries to acquire resource2
Thread t1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: Acquired resource1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 1: Trying to acquire resource2");
synchronized (resource2) {
System.out.println("Thread 1: Acquired resource2");
}
}
});
// Thread 2: Acquires resource2, then tries to acquire resource1
Thread t2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: Acquired resource2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 2: Trying to acquire resource1");
synchronized (resource1) {
System.out.println("Thread 2: Acquired resource1");
}
}
});
t1.start();
t2.start();
}Avoiding Deadlock
One way to avoid deadlock is to always acquire resources in a specific order:
public static void avoidDeadlock() {
final Object resource1 = new Object();
final Object resource2 = new Object();
// Both threads acquire resource1 first, then resource2
Thread t1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: Acquired resource1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (resource2) {
System.out.println("Thread 1: Acquired resource2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 2: Acquired resource1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (resource2) {
System.out.println("Thread 2: Acquired resource2");
}
}
});
t1.start();
t2.start();
}Real-world Example: Restaurant Simulation
class Restaurant {
private final Object kitchenLock = new Object();
private int pendingOrders = 0;
private final int MAX_PENDING_ORDERS = 5;
// Chef cooks meals
class Chef implements Runnable {
@Override
public void run() {
while (true) {
cookMeal();
try {
// Time to cook a meal
Thread.sleep((int)(Math.random() * 1000) + 500);
} catch (InterruptedException e) {
return;
}
}
}
private void cookMeal() {
synchronized (kitchenLock) {
if (pendingOrders == 0) {
try {
System.out.println("Chef: No orders, waiting...");
kitchenLock.wait(); // Wait for new order
} catch (InterruptedException e) {
return;
}
}
pendingOrders--;
System.out.println("Chef: Finished a meal, remaining orders: " + pendingOrders);
kitchenLock.notifyAll(); // Notify Waiter that meal is ready (conceptually)
}
}
}
// Waiter takes orders
class Waiter implements Runnable {
@Override
public void run() {
while (true) {
takeOrder();
try {
// Time to take an order
Thread.sleep((int)(Math.random() * 2000) + 1000);
} catch (InterruptedException e) {
return;
}
}
}
private void takeOrder() {
synchronized (kitchenLock) {
if (pendingOrders >= MAX_PENDING_ORDERS) {
try {
System.out.println("Waiter: Kitchen overloaded, waiting...");
kitchenLock.wait(); // Wait until kitchen is less busy
} catch (InterruptedException e) {
return;
}
}
pendingOrders++;
System.out.println("Waiter: Taken new order, current pending: " + pendingOrders);
kitchenLock.notify(); // Notify Chef about new order
}
}
}
public void startSimulation() {
Thread chefThread = new Thread(new Chef(), "Chef");
Thread waiterThread = new Thread(new Waiter(), "Waiter");
chefThread.start();
waiterThread.start();
// Run simulation for 10 seconds
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// End threads
chefThread.interrupt();
waiterThread.interrupt();
System.out.println("Restaurant simulation finished!");
}
}Practice: Parallel File Downloader
import java.io.*;
import java.net.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class ParallelDownloader {
public static void main(String[] args) {
String[] urls = {
"https://example.com/file1.zip",
"https://example.com/file2.pdf",
"https://example.com/file3.jpg",
"https://example.com/file4.mp4"
};
// Create destination directory
File downloadDir = new File("downloads");
if (!downloadDir.exists()) {
downloadDir.mkdir();
}
System.out.println("Starting parallel download of " + urls.length + " files...");
long startTime = System.currentTimeMillis();
// Create ExecutorService with thread pool
ExecutorService executor = Executors.newFixedThreadPool(3);
// Create list of download tasks
List<Future<DownloadResult>> results = new ArrayList<>();
for (String url : urls) {
// Create download task for each URL
Callable<DownloadResult> downloadTask = new DownloadTask(url, downloadDir);
// Submit task to executor and save Future to track
Future<DownloadResult> future = executor.submit(downloadTask);
results.add(future);
}
// Collect results
for (Future<DownloadResult> future : results) {
try {
DownloadResult result = future.get(); // Wait for task to complete
System.out.println("Downloaded: " + result.getFileName() +
" (" + result.getFileSize() + " bytes) in " +
result.getElapsedTime() + "ms");
} catch (InterruptedException | ExecutionException e) {
System.err.println("Error downloading file: " + e.getMessage());
}
}
// Shutdown executor
executor.shutdown();
long endTime = System.currentTimeMillis();
System.out.println("Completed! Total time: " + (endTime - startTime) + "ms");
}
static class DownloadTask implements Callable<DownloadResult> {
private final String urlString;
private final File downloadDir;
public DownloadTask(String urlString, File downloadDir) {
this.urlString = urlString;
this.downloadDir = downloadDir;
}
@Override
public DownloadResult call() throws Exception {
long startTime = System.currentTimeMillis();
URL url = new URL(urlString);
String fileName = new File(url.getPath()).getName();
File outputFile = new File(downloadDir, fileName);
System.out.println("Thread [" + Thread.currentThread().getName() +
"] downloading: " + fileName);
long fileSize = 0;
try (BufferedInputStream in = new BufferedInputStream(url.openStream());
FileOutputStream fos = new FileOutputStream(outputFile)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
fileSize += bytesRead;
// Simulate slow network
Thread.sleep(10);
}
}
long endTime = System.currentTimeMillis();
return new DownloadResult(fileName, fileSize, endTime - startTime);
}
}
static class DownloadResult {
private final String fileName;
private final long fileSize;
private final long elapsedTime;
public DownloadResult(String fileName, long fileSize, long elapsedTime) {
this.fileName = fileName;
this.fileSize = fileSize;
this.elapsedTime = elapsedTime;
}
public String getFileName() {
return fileName;
}
public long getFileSize() {
return fileSize;
}
public long getElapsedTime() {
return elapsedTime;
}
}
}🧑🏫 Lesson 3: Concurrency
Concurrency is a technique that allows multiple tasks to be executed simultaneously, increasing performance and optimizing resource usage. Java provides powerful APIs to build efficient and safe concurrent applications.
Concurrency API in Java
Java SE 5 introduced the java.util.concurrent package with classes and interfaces to manage threads more efficiently and safely than traditional methods.
ExecutorService and Thread Pool
ExecutorService is a service that helps manage threads and tasks. ThreadPool is a group of pre-created threads ready to perform work, helping to:
- Reuse threads, reducing initialization overhead
- Manage the number of concurrent threads
- Improve performance with appropriate thread count
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorServiceExample {
public static void main(String[] args) {
// Create thread pool with fixed size of 5 threads
ExecutorService executor = Executors.newFixedThreadPool(5);
// Submit 10 tasks to executor
for (int i = 1; i <= 10; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("Executing task " + taskId +
" by thread " + Thread.currentThread().getName());
try {
// Simulate time-consuming work
Thread.sleep((long)(Math.random() * 1000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Task " + taskId + " completed");
});
}
// Shutdown executor after completion
executor.shutdown();
// Wait until all tasks are finished
while (!executor.isTerminated()) {
// Wait
}
System.out.println("All tasks completed");
}
}Common Executor Types
Fixed Thread Pool: Fixed number of threads
javaExecutorService fixedPool = Executors.newFixedThreadPool(5);Cached Thread Pool: Automatically creates new threads as needed, reuses idle threads
javaExecutorService cachedPool = Executors.newCachedThreadPool();Single Thread Executor: Uses only 1 single thread
javaExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();Scheduled Thread Pool: Allows scheduling tasks execution
javaScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);
Callable and Future
Callable is similar to Runnable but can return a result and throw exceptions. Future is an object representing an asynchronous result, allowing to check status and retrieve result when completed.
import java.util.concurrent.*;
import java.util.*;
public class CallableFutureExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
// List of Futures to store results
List<Future<Integer>> resultList = new ArrayList<>();
// Create and submit 5 Callable tasks
for (int i = 1; i <= 5; i++) {
final int taskId = i;
Callable<Integer> task = () -> {
System.out.println("Calculating task " + taskId);
Thread.sleep(1000); // Simulate processing
// Suppose calculating sum from 1 to taskId
int sum = 0;
for (int j = 1; j <= taskId; j++) {
sum += j;
}
return sum;
};
// Submit task and save Future
Future<Integer> future = executor.submit(task);
resultList.add(future);
}
// Process results
for (int i = 0; i < resultList.size(); i++) {
try {
// get() will wait until task completes
Integer result = resultList.get(i).get();
System.out.println("Task " + (i + 1) + " result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
executor.shutdown();
}
}CompletableFuture in Java 8+
CompletableFuture is a new class in Java 8, extending Future with many useful methods for asynchronous programming:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class CompletableFutureExample {
public static void main(String[] args) {
// Create asynchronous CompletableFuture
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
System.out.println("Processing task...");
TimeUnit.SECONDS.sleep(2);
return "Result from async task";
} catch (InterruptedException e) {
return "Task interrupted";
}
});
// Add callback to handle result when completed
future.thenAccept(result -> {
System.out.println("Received result: " + result);
});
// Add error handling
future.exceptionally(ex -> {
System.out.println("Error occurred: " + ex.getMessage());
return "Default value due to error";
});
// Combine two async tasks
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
return "Data from source 1";
} catch (InterruptedException e) {
return "Error from source 1";
}
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
return "Data from source 2";
} catch (InterruptedException e) {
return "Error from source 2";
}
});
// Combine results from both sources
CompletableFuture<String> combinedFuture = future1.thenCombine(future2,
(result1, result2) -> result1 + " + " + result2);
combinedFuture.thenAccept(System.out::println);
// Ensure program doesn't exit before tasks complete
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}Advanced Synchronization with Lock Interface
Java provides the Lock interface in java.util.concurrent.locks package as an advanced solution for synchronized keyword:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BankAccount {
private double balance;
private final Lock lock = new ReentrantLock();
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public void deposit(double amount) {
lock.lock(); // Lock resource
try {
if (amount > 0) {
double newBalance = balance + amount;
// Simulate delay to expose race condition
Thread.sleep(100);
balance = newBalance;
System.out.println("Deposited: " + amount + ", New balance: " + balance);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // Always ensure unlock in finally
}
}
public boolean withdraw(double amount) {
lock.lock();
try {
if (amount <= balance) {
double newBalance = balance - amount;
Thread.sleep(100);
balance = newBalance;
System.out.println("Withdrew: " + amount + ", New balance: " + balance);
return true;
}
return false;
} catch (InterruptedException e) {
e.printStackTrace();
return false;
} finally {
lock.unlock();
}
}
public double getBalance() {
lock.lock();
try {
return balance;
} finally {
lock.unlock();
}
}
// Use tryLock to avoid deadlock
public boolean transfer(BankAccount target, double amount) {
// Try to lock source account
if (lock.tryLock()) {
try {
// Try to lock target account
if (target.lock.tryLock()) {
try {
if (amount <= balance) {
withdraw(amount);
target.deposit(amount);
return true;
}
return false;
} finally {
target.lock.unlock();
}
}
} finally {
lock.unlock();
}
}
// If cannot lock both accounts
System.out.println("Cannot transfer at this time, please try again later");
return false;
}
}Semaphore and Latch
Semaphore controls the number of threads that can access a resource simultaneously:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
// Simulate a connection pool with 3 connections
Semaphore connectionPool = new Semaphore(3);
// Create 10 threads accessing connection pool
for (int i = 1; i <= 10; i++) {
final int clientId = i;
new Thread(() -> {
try {
System.out.println("Client " + clientId + " waiting for connection...");
connectionPool.acquire(); // Acquire 1 permit (connection)
System.out.println("Client " + clientId + " acquired connection!");
// Simulate connection usage time
Thread.sleep((long)(Math.random() * 2000) + 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// Release connection when done
connectionPool.release();
System.out.println("Client " + clientId + " released connection!");
}
}).start();
}
}
}CountDownLatch is a synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
// Create latch with count of 3
CountDownLatch latch = new CountDownLatch(3);
System.out.println("Application starting...");
// Start 3 services
for (int i = 1; i <= 3; i++) {
final int serviceId = i;
new Thread(() -> {
try {
System.out.println("Service " + serviceId + " starting...");
// Simulate startup time
Thread.sleep((long)(Math.random() * 2000) + 1000);
System.out.println("Service " + serviceId + " ready!");
// Mark this service as completed
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
// Wait until all 3 services are ready
latch.await();
System.out.println("All services ready! Application starting to process requests...");
}
}Atomic Variables
Atomic variables in java.util.concurrent.atomic package provide atomic operations without locking:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicVariableExample {
// Traditional counter (not thread-safe)
private static int normalCounter = 0;
// Atomic counter (thread-safe)
private static AtomicInteger atomicCounter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
// Create multiple threads incrementing counter
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
normalCounter++; // Not safe, data loss possible
atomicCounter.incrementAndGet(); // Safe, atomic
}
});
threads[i].start();
}
// Wait for all threads to complete
for (Thread thread : threads) {
thread.join();
}
System.out.println("Normal counter value: " + normalCounter);
System.out.println("Atomic counter value: " + atomicCounter.get());
}
}ConcurrentHashMap and CopyOnWriteArrayList
Concurrent Collections are designed specifically to work efficiently in multithreaded environments:
import java.util.*;
import java.util.concurrent.*;
public class ConcurrentCollectionsExample {
public static void main(String[] args) {
// Normal HashMap (not thread-safe)
Map<String, Integer> normalMap = new HashMap<>();
// ConcurrentHashMap (thread-safe)
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
// Normal ArrayList (not thread-safe)
List<String> normalList = new ArrayList<>();
// CopyOnWriteArrayList (thread-safe)
List<String> concurrentList = new CopyOnWriteArrayList<>();
// Create and run multiple threads accessing collections
Runnable task = () -> {
// Add element to list
String threadName = Thread.currentThread().getName();
concurrentList.add(threadName);
// Update value in map
concurrentMap.put(threadName, concurrentList.size());
// Read all elements from list
for (String item : concurrentList) {
// Read operation is not blocked
System.out.println(threadName + " read: " + item);
}
// Retrieve and update value in map
concurrentMap.computeIfPresent(threadName, (key, oldValue) -> oldValue + 1);
};
// Start 10 threads
List<Thread> threads = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
Thread t = new Thread(task, "Thread-" + i);
threads.add(t);
t.start();
}
// Wait for all threads to complete
for (Thread t : threads) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// Display results
System.out.println("\nFinal Result:");
System.out.println("ConcurrentList size: " + concurrentList.size());
System.out.println("ConcurrentMap entries: " + concurrentMap);
}
}Real-world Example: Parallel Data Processing
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
public class ParallelDataProcessingExample {
// Simulate data to process
private static List<String> generateData(int size) {
List<String> data = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
data.add("Item-" + i);
}
return data;
}
// Simulate time-consuming processing function
private static String processItem(String item) {
try {
// Simulate processing time
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return item.toUpperCase() + "-PROCESSED";
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
// Create sample data
List<String> rawData = generateData(100);
System.out.println("Starting processing of " + rawData.size() + " items");
// 1. Sequential processing
long startTime = System.currentTimeMillis();
List<String> sequentialResult = processSequentially(rawData);
long sequentialTime = System.currentTimeMillis() - startTime;
System.out.println("Sequential processing completed in " + sequentialTime + " ms");
// 2. Processing with ExecutorService
startTime = System.currentTimeMillis();
List<String> executorResult = processWithExecutor(rawData);
long executorTime = System.currentTimeMillis() - startTime;
System.out.println("Processing with ExecutorService completed in " + executorTime + " ms");
// 3. Processing with CompletableFuture
startTime = System.currentTimeMillis();
List<String> completableFutureResult = processWithCompletableFuture(rawData);
long completableFutureTime = System.currentTimeMillis() - startTime;
System.out.println("Processing with CompletableFuture completed in " +
completableFutureTime + " ms");
// 4. Processing with Parallel Streams (Java 8+)
startTime = System.currentTimeMillis();
List<String> parallelStreamResult = processWithParallelStream(rawData);
long parallelStreamTime = System.currentTimeMillis() - startTime;
System.out.println("Processing with Parallel Streams completed in " +
parallelStreamTime + " ms");
// Compare results
boolean allResultsMatch = sequentialResult.equals(executorResult) &&
executorResult.equals(completableFutureResult) &&
completableFutureResult.equals(parallelStreamResult);
System.out.println("All results match: " + allResultsMatch);
System.out.println("\nSpeed improvement:");
double executorSpeedup = (double) sequentialTime / executorTime;
double completableFutureSpeedup = (double) sequentialTime / completableFutureTime;
double parallelStreamSpeedup = (double) sequentialTime / parallelStreamTime;
System.out.printf("- ExecutorService: %.2fx faster\n", executorSpeedup);
System.out.printf("- CompletableFuture: %.2fx faster\n", completableFutureSpeedup);
System.out.printf("- Parallel Streams: %.2fx faster\n", parallelStreamSpeedup);
}
// 1. Sequential processing
private static List<String> processSequentially(List<String> data) {
List<String> results = new ArrayList<>(data.size());
for (String item : data) {
results.add(processItem(item));
}
return results;
}
// 2. Processing with ExecutorService
private static List<String> processWithExecutor(List<String> data)
throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors());
List<Future<String>> futures = new ArrayList<>(data.size());
for (String item : data) {
Future<String> future = executor.submit(() -> processItem(item));
futures.add(future);
}
List<String> results = new ArrayList<>(data.size());
for (Future<String> future : futures) {
results.add(future.get());
}
executor.shutdown();
return results;
}
// 3. Processing with CompletableFuture
private static List<String> processWithCompletableFuture(List<String> data) {
List<CompletableFuture<String>> futures = data.stream()
.map(item -> CompletableFuture.supplyAsync(() -> processItem(item)))
.collect(Collectors.toList());
return futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
// 4. Processing with Parallel Streams
private static List<String> processWithParallelStream(List<String> data) {
return data.parallelStream()
.map(ParallelDataProcessingExample::processItem)
.collect(Collectors.toList());
}
}Best Practices
Use ThreadPool instead of creating threads directly
- To efficiently manage and optimize thread usage in the application.
Always release resources
- Always call
shutdown()onExecutorServicewhen no longer needed. - Use try-with-resources or finally to ensure locks are unlocked.
- Always call
Avoid deadlock
- Always acquire locks in the same order.
- Use tryLock() with timeout to avoid indefinite waiting.
- Don't hold locks while performing blocking or long-running operations.
Minimize shared state
- Design to minimize shared data.
- Use local thread variables when possible.
- Use
ThreadLocalwhen separate state is needed for threads.
Exception Handling
- Do not ignore exceptions in threads.
- Log or propagate exceptions to higher levels.
- Check InterruptedException and restore interrupted status.
Understand Memory Consistency
- Use volatile variables for simple flags.
- Use synchronized or Lock for complex state.
- Prefer Atomic classes and Concurrent Collections.
Consider Synchronization Cost
- Synchronization is only truly necessary when multiple threads write data.
- Read-only operations do not need complex synchronization.
- Use structures like ReadWriteLock when appropriate.
Multithreading Testing
- Apply static analysis tools.
- Write multithreaded tests with contention scenarios.
- Perform stress testing to find performance issues.
Practice Exercise: Building Parallel Document Indexer
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;
public class ParallelDocumentIndexer {
// Store index: keyword -> list of documents
private final ConcurrentMap<String, Set<String>> index = new ConcurrentHashMap<>();
// Directory containing documents to index
private final Path documentsDir;
public ParallelDocumentIndexer(String documentsPath) {
this.documentsDir = Paths.get(documentsPath);
}
public void buildIndex() throws IOException, InterruptedException {
System.out.println("Starting indexing from directory: " + documentsDir);
// Get list of all .txt files
List<Path> textFiles = Files.walk(documentsDir)
.filter(p -> p.toString().endsWith(".txt"))
.collect(Collectors.toList());
System.out.println("Found " + textFiles.size() + " documents");
ExecutorService executor = Executors.newWorkStealingPool();
// Create futures to track progress
List<Future<?>> futures = new ArrayList<>();
for (Path file : textFiles) {
// Submit index task for each file
futures.add(executor.submit(() -> {
try {
indexFile(file);
} catch (IOException e) {
System.err.println("Error indexing file " + file + ": " + e.getMessage());
}
}));
}
// Wait for all tasks to complete
for (Future<?> future : futures) {
try {
future.get();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
executor.shutdown();
System.out.println("Completed! Indexed " + index.size() + " keywords.");
}
private void indexFile(Path file) throws IOException {
System.out.println("Indexing file: " + file.getFileName());
String fileContent = Files.readString(file);
String fileName = file.getFileName().toString();
// Split content into words and process
String[] words = fileContent.toLowerCase()
.replaceAll("[^a-z0-9\\s]", " ")
.split("\\s+");
// Add each word to index
Arrays.stream(words)
.filter(word -> word.length() > 2) // Ignore short words
.forEach(word -> {
// putIfAbsent ensures atomic, then we update the set
index.computeIfAbsent(word, k -> ConcurrentHashMap.newKeySet())
.add(fileName);
});
}
public Set<String> search(String keyword) {
String normalizedKeyword = keyword.toLowerCase().trim();
Set<String> result = index.getOrDefault(normalizedKeyword, Collections.emptySet());
return new HashSet<>(result); // Return copy to avoid direct modification
}
public void searchMultipleKeywords(String[] keywords) {
System.out.println("\nSearch Results:");
Arrays.stream(keywords)
.map(String::toLowerCase)
.forEach(keyword -> {
Set<String> documents = search(keyword);
System.out.println("\"" + keyword + "\" appears in " +
documents.size() + " documents:");
documents.forEach(doc -> System.out.println(" - " + doc));
});
}
public static void main(String[] args) {
try {
// Path to directory containing documents to index
String documentsPath = "documents";
// Create directory if not exists
Files.createDirectories(Paths.get(documentsPath));
// Create some sample text files for testing
createSampleDocuments(documentsPath);
// Initialize and run indexer
ParallelDocumentIndexer indexer = new ParallelDocumentIndexer(documentsPath);
indexer.buildIndex();
// Search for some keywords
String[] searchTerms = {"java", "programming", "concurrent", "concurrency", "thread"};
indexer.searchMultipleKeywords(searchTerms);
} catch (IOException | InterruptedException e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
}
}
// Create sample documents for testing
private static void createSampleDocuments(String documentsPath) throws IOException {
Map<String, String> sampleDocs = new HashMap<>();
sampleDocs.put("java_basics.txt",
"Java is a popular object-oriented programming language. " +
"It is designed to have as few implementation dependencies as possible. " +
"Java applications are typically compiled to bytecode.");
sampleDocs.put("concurrency_intro.txt",
"Concurrency in Java allows executing multiple threads simultaneously. " +
"This helps maximize resources of modern multi-core computers.");
sampleDocs.put("threading_models.txt",
"Java supports multithreading through Thread class and Runnable interface. " +
"Since Java 5, concurrency API (java.util.concurrent) was introduced with many powerful features.");
sampleDocs.put("memory_model.txt",
"Java memory model specifies how threads interact through memory. " +
"It defines rules to ensure values are read consistently across different threads.");
sampleDocs.put("performance_tips.txt",
"When programming multithreading in Java, need to avoid race conditions and deadlocks. " +
"Using thread pool instead of creating threads directly will help improve application performance.");
// Write sample files
for (Map.Entry<String, String> entry : sampleDocs.entrySet()) {
Path filePath = Paths.get(documentsPath, entry.getKey());
Files.writeString(filePath, entry.getValue());
System.out.println("Created sample file: " + filePath);
}
}
}With knowledge of concurrent programming in Java, you can build efficient applications, make good use of hardware resources, and achieve higher performance compared to traditional sequential programming.
🧑🏫 Lesson 4: Database Connection with JDBC
Loading Driver and Creating Connection
// Load driver (only need to do once)
try {
Class.forName("com.mysql.cj.jdbc.Driver");
// Or with SQLite: Class.forName("org.sqlite.JDBC");
} catch (ClassNotFoundException e) {
System.out.println("JDBC Driver not found");
e.printStackTrace();
}
// Create connection
String url = "jdbc:mysql://localhost:3306/mydatabase";
String username = "root";
String password = "password";
try (Connection connection = DriverManager.getConnection(url, username, password)) {
System.out.println("Connected to database successfully!");
// Database operations
} catch (SQLException e) {
System.out.println("Connection failed!");
e.printStackTrace();
}Using Statement to Execute Queries
public static void executeSimpleQuery(Connection conn) throws SQLException {
// Create Statement
try (Statement stmt = conn.createStatement()) {
// Execute SQL query
String sql = "SELECT id, name, email FROM users";
ResultSet rs = stmt.executeQuery(sql);
// Process results
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
String email = rs.getString("email");
System.out.println("ID: " + id + ", Name: " + name + ", Email: " + email);
}
}
}Using PreparedStatement for Safe Queries
public static void findUserById(Connection conn, int userId) throws SQLException {
String sql = "SELECT id, name, email FROM users WHERE id = ?";
// Create PreparedStatement
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
// Set parameter
pstmt.setInt(1, userId);
// Execute query
ResultSet rs = pstmt.executeQuery();
// Process results
if (rs.next()) {
String name = rs.getString("name");
String email = rs.getString("email");
System.out.println("User found:");
System.out.println("ID: " + userId + ", Name: " + name + ", Email: " + email);
} else {
System.out.println("User not found with ID: " + userId);
}
}
}Using try-with-resources with JDBC
public static void safeDatabaseOperation() {
String url = "jdbc:mysql://localhost:3306/mydatabase";
String username = "root";
String password = "password";
// try-with-resources automatically closes Connection, Statement and ResultSet
try (
Connection conn = DriverManager.getConnection(url, username, password);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT COUNT(*) AS user_count FROM users")
) {
if (rs.next()) {
int count = rs.getInt("user_count");
System.out.println("Total users: " + count);
}
} catch (SQLException e) {
System.out.println("Database operation error:");
e.printStackTrace();
}
}Real-world Example - Connection and Query
import java.sql.*;
public class JDBCExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/library_db";
String username = "root";
String password = "password";
try (Connection conn = DriverManager.getConnection(url, username, password)) {
System.out.println("Connected to database successfully!");
// Find all books published after 2020
findBooksByYear(conn, 2020);
// Find books by author
findBooksByAuthor(conn, "J.K. Rowling");
} catch (SQLException e) {
System.out.println("Connection error: " + e.getMessage());
e.printStackTrace();
}
}
public static void findBooksByYear(Connection conn, int year) throws SQLException {
String sql = "SELECT id, title, author, published_year FROM books WHERE published_year > ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, year);
ResultSet rs = pstmt.executeQuery();
System.out.println("\nBooks published after " + year + ":");
while (rs.next()) {
System.out.printf("ID: %d, Title: %s, Author: %s, Year: %d\n",
rs.getInt("id"),
rs.getString("title"),
rs.getString("author"),
rs.getInt("published_year"));
}
}
}
public static void findBooksByAuthor(Connection conn, String authorName) throws SQLException {
String sql = "SELECT id, title, published_year FROM books WHERE author LIKE ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, "%" + authorName + "%"); // Fuzzy search
ResultSet rs = pstmt.executeQuery();
System.out.println("\nBooks by author '" + authorName + "':");
boolean found = false;
while (rs.next()) {
found = true;
System.out.printf("ID: %d, Title: %s, Published Year: %d\n",
rs.getInt("id"),
rs.getString("title"),
rs.getInt("published_year"));
}
if (!found) {
System.out.println("No books found for this author.");
}
}
}
}🧑🏫 Lesson 5: CRUD Operations with JDBC
Creating Table in Database
public static void createTable(Connection conn) throws SQLException {
String sql = "CREATE TABLE IF NOT EXISTS students (" +
"id INT AUTO_INCREMENT PRIMARY KEY," +
"name VARCHAR(100) NOT NULL," +
"email VARCHAR(100) UNIQUE," +
"age INT," +
"gpa DOUBLE" +
")";
try (Statement stmt = conn.createStatement()) {
stmt.execute(sql);
System.out.println("Table 'students' created or already exists");
}
}Adding Data (Create)
public static void addStudent(Connection conn, String name, String email, int age, double gpa)
throws SQLException {
String sql = "INSERT INTO students (name, email, age, gpa) VALUES (?, ?, ?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, name);
pstmt.setString(2, email);
pstmt.setInt(3, age);
pstmt.setDouble(4, gpa);
int rowsInserted = pstmt.executeUpdate();
if (rowsInserted > 0) {
System.out.println("Successfully added student: " + name);
}
}
}Querying Data (Read)
public static void getAllStudents(Connection conn) throws SQLException {
String sql = "SELECT id, name, email, age, gpa FROM students";
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
if (!rs.isBeforeFirst()) {
System.out.println("No students in database");
return;
}
System.out.println("\n----- STUDENT LIST -----");
System.out.printf("%-5s %-20s %-25s %-5s %-5s\n",
"ID", "Name", "Email", "Age", "GPA");
System.out.println("-----------------------------------------------------------");
while (rs.next()) {
System.out.printf("%-5d %-20s %-25s %-5d %-5.2f\n",
rs.getInt("id"),
rs.getString("name"),
rs.getString("email"),
rs.getInt("age"),
rs.getDouble("gpa"));
}
}
}
public static void findStudentByName(Connection conn, String searchName) throws SQLException {
String sql = "SELECT id, name, email, age, gpa FROM students WHERE name LIKE ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, "%" + searchName + "%");
try (ResultSet rs = pstmt.executeQuery()) {
boolean found = false;
System.out.println("\n----- SEARCH STUDENT -----");
System.out.printf("%-5s %-20s %-25s %-5s %-5s\n",
"ID", "Name", "Email", "Age", "GPA");
System.out.println("-----------------------------------------------------------");
while (rs.next()) {
found = true;
System.out.printf("%-5d %-20s %-25s %-5d %-5.2f\n",
rs.getInt("id"),
rs.getString("name"),
rs.getString("email"),
rs.getInt("age"),
rs.getDouble("gpa"));
}
if (!found) {
System.out.println("No student found with name \"" + searchName + "\"");
}
}
}
}Updating Data (Update)
public static void updateStudentGPA(Connection conn, int studentId, double newGPA)
throws SQLException {
String sql = "UPDATE students SET gpa = ? WHERE id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setDouble(1, newGPA);
pstmt.setInt(2, studentId);
int rowsUpdated = pstmt.executeUpdate();
if (rowsUpdated > 0) {
System.out.println("Updated GPA for student ID = " + studentId);
} else {
System.out.println("Student not found with ID = " + studentId);
}
}
}Deleting Data (Delete)
public static void deleteStudent(Connection conn, int studentId) throws SQLException {
// First, check if student exists
String checkSql = "SELECT name FROM students WHERE id = ?";
try (PreparedStatement checkStmt = conn.prepareStatement(checkSql)) {
checkStmt.setInt(1, studentId);
try (ResultSet rs = checkStmt.executeQuery()) {
if (rs.next()) {
String studentName = rs.getString("name");
// Student exists, proceed to delete
String deleteSql = "DELETE FROM students WHERE id = ?";
try (PreparedStatement deleteStmt = conn.prepareStatement(deleteSql)) {
deleteStmt.setInt(1, studentId);
int rowsDeleted = deleteStmt.executeUpdate();
System.out.println("Deleted student: " + studentName);
}
} else {
System.out.println("Student not found with ID = " + studentId);
}
}
}
}Real-world Example - Complete CRUD Student Management
import java.sql.*;
import java.util.Scanner;
public class StudentManagementSystem {
// Database connection info
private static final String URL = "jdbc:mysql://localhost:3306/school_db";
private static final String USERNAME = "root";
private static final String PASSWORD = "password";
private static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) {
try {
// Load driver
Class.forName("com.mysql.cj.jdbc.Driver");
// Create connection
try (Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD)) {
// Create table if not exists
createTable(conn);
boolean running = true;
while (running) {
displayMenu();
int choice = scanner.nextInt();
scanner.nextLine(); // Read newline
switch (choice) {
case 1:
addNewStudent(conn);
break;
case 2:
viewAllStudents(conn);
break;
case 3:
searchStudent(conn);
break;
case 4:
updateStudent(conn);
break;
case 5:
deleteStudentRecord(conn);
break;
case 0:
running = false;
System.out.println("Thank you for using the program!");
break;
default:
System.out.println("Invalid choice!");
}
}
}
} catch (ClassNotFoundException e) {
System.out.println("JDBC Driver not found: " + e.getMessage());
} catch (SQLException e) {
System.out.println("SQL Error: " + e.getMessage());
e.printStackTrace();
}
}
private static void displayMenu() {
System.out.println("\n----- STUDENT MANAGEMENT SYSTEM -----");
System.out.println("1. Add new student");
System.out.println("2. View all students");
System.out.println("3. Search student by name");
System.out.println("4. Update student GPA");
System.out.println("5. Delete student");
System.out.println("0. Exit");
System.out.print("Select function: ");
}
private static void addNewStudent(Connection conn) throws SQLException {
System.out.println("\n----- ADD NEW STUDENT -----");
System.out.print("Enter student name: ");
String name = scanner.nextLine();
System.out.print("Enter email: ");
String email = scanner.nextLine();
System.out.print("Enter age: ");
int age = scanner.nextInt();
System.out.print("Enter GPA: ");
double gpa = scanner.nextDouble();
scanner.nextLine(); // Read newline
addStudent(conn, name, email, age, gpa);
}
private static void viewAllStudents(Connection conn) throws SQLException {
getAllStudents(conn);
}
private static void searchStudent(Connection conn) throws SQLException {
System.out.print("\nEnter student name to search: ");
String searchName = scanner.nextLine();
findStudentByName(conn, searchName);
}
private static void updateStudent(Connection conn) throws SQLException {
System.out.print("\nEnter student ID to update: ");
int id = scanner.nextInt();
System.out.print("Enter new GPA: ");
double newGPA = scanner.nextDouble();
scanner.nextLine(); // Read newline
updateStudentGPA(conn, id, newGPA);
}
private static void deleteStudentRecord(Connection conn) throws SQLException {
System.out.print("\nEnter student ID to delete: ");
int id = scanner.nextInt();
scanner.nextLine(); // Read newline
System.out.print("Are you sure you want to delete this student? (y/n): ");
String confirm = scanner.nextLine();
if (confirm.equalsIgnoreCase("y")) {
deleteStudent(conn, id);
} else {
System.out.println("Delete operation cancelled");
}
}
// Other CRUD methods defined above...
}Error Handling and Transactions
public static void registerStudentWithCourses(Connection conn, String studentName,
String email, int[] courseIds) throws SQLException {
// Disable auto-commit to use transaction
boolean autoCommit = conn.getAutoCommit();
conn.setAutoCommit(false);
try {
// 1. Add new student
String insertStudentSql = "INSERT INTO students (name, email) VALUES (?, ?)";
int studentId;
try (PreparedStatement pstmt = conn.prepareStatement(insertStudentSql,
Statement.RETURN_GENERATED_KEYS)) {
pstmt.setString(1, studentName);
pstmt.setString(2, email);
pstmt.executeUpdate();
// Get ID of newly added student
try (ResultSet rs = pstmt.getGeneratedKeys()) {
if (rs.next()) {
studentId = rs.getInt(1);
} else {
throw new SQLException("Cannot get ID of newly added student");
}
}
}
// 2. Register student for courses
String registerCourseSql = "INSERT INTO student_courses (student_id, course_id) VALUES (?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(registerCourseSql)) {
for (int courseId : courseIds) {
pstmt.setInt(1, studentId);
pstmt.setInt(2, courseId);
pstmt.executeUpdate();
}
}
// If everything OK, commit transaction
conn.commit();
System.out.println("Registered student " + studentName + " with " +
courseIds.length + " courses");
} catch (SQLException e) {
// If error, rollback
try {
System.out.println("Transaction failed, rolling back...");
conn.rollback();
} catch (SQLException ex) {
System.out.println("Error rolling back: " + ex.getMessage());
}
throw e;
} finally {
// Restore auto-commit state
conn.setAutoCommit(autoCommit);
}
}🧑🏫 Lesson 6: Practice - Building Application with JDBC
Simple Database Design
CREATE TABLE students (
id INT AUTO_INCREMENT PRIMARY KEY,
student_id VARCHAR(10) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
birth_date DATE,
email VARCHAR(100) UNIQUE,
phone VARCHAR(15)
);
CREATE TABLE courses (
id INT AUTO_INCREMENT PRIMARY KEY,
course_code VARCHAR(10) UNIQUE NOT NULL,
course_name VARCHAR(100) NOT NULL,
credits INT
);
CREATE TABLE enrollments (
id INT AUTO_INCREMENT PRIMARY KEY,
student_id INT,
course_id INT,
enrollment_date DATE,
grade DOUBLE,
FOREIGN KEY (student_id) REFERENCES students(id),
FOREIGN KEY (course_id) REFERENCES courses(id),
UNIQUE (student_id, course_id)
);Database Connection Class
public class DatabaseConnection {
private static final String URL = "jdbc:mysql://localhost:3306/school_management";
private static final String USERNAME = "root";
private static final String PASSWORD = "password";
private static Connection connection = null;
public static Connection getConnection() throws SQLException {
if (connection == null || connection.isClosed()) {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
} catch (ClassNotFoundException e) {
throw new SQLException("JDBC Driver not found", e);
}
}
return connection;
}
public static void closeConnection() {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
System.out.println("Error closing connection: " + e.getMessage());
}
}
}
}Student DAO (Data Access Object) Class
public class StudentDAO {
private Connection conn;
public StudentDAO() throws SQLException {
this.conn = DatabaseConnection.getConnection();
}
// Add new student
public boolean addStudent(Student student) throws SQLException {
String sql = "INSERT INTO students (student_id, name, birth_date, email, phone) " +
"VALUES (?, ?, ?, ?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, student.getStudentId());
pstmt.setString(2, student.getName());
pstmt.setDate(3, new java.sql.Date(student.getBirthDate().getTime()));
pstmt.setString(4, student.getEmail());
pstmt.setString(5, student.getPhone());
int rowsInserted = pstmt.executeUpdate();
return rowsInserted > 0;
}
}
// Get all students
public List<Student> getAllStudents() throws SQLException {
List<Student> students = new ArrayList<>();
String sql = "SELECT id, student_id, name, birth_date, email, phone FROM students";
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
Student student = new Student();
student.setId(rs.getInt("id"));
student.setStudentId(rs.getString("student_id"));
student.setName(rs.getString("name"));
student.setBirthDate(rs.getDate("birth_date"));
student.setEmail(rs.getString("email"));
student.setPhone(rs.getString("phone"));
students.add(student);
}
}
return students;
}
// Find student by ID
public Student findByStudentId(String studentId) throws SQLException {
String sql = "SELECT id, student_id, name, birth_date, email, phone " +
"FROM students WHERE student_id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, studentId);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
Student student = new Student();
student.setId(rs.getInt("id"));
student.setStudentId(rs.getString("student_id"));
student.setName(rs.getString("name"));
student.setBirthDate(rs.getDate("birth_date"));
student.setEmail(rs.getString("email"));
student.setPhone(rs.getString("phone"));
return student;
}
}
}
return null; // Not found
}
// Update student info
public boolean updateStudent(Student student) throws SQLException {
String sql = "UPDATE students SET name = ?, birth_date = ?, email = ?, phone = ? " +
"WHERE student_id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, student.getName());
pstmt.setDate(2, new java.sql.Date(student.getBirthDate().getTime()));
pstmt.setString(3, student.getEmail());
pstmt.setString(4, student.getPhone());
pstmt.setString(5, student.getStudentId());
int rowsUpdated = pstmt.executeUpdate();
return rowsUpdated > 0;
}
}
// Delete student
public boolean deleteStudent(String studentId) throws SQLException {
String sql = "DELETE FROM students WHERE student_id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, studentId);
int rowsDeleted = pstmt.executeUpdate();
return rowsDeleted > 0;
}
}
}Student Class
import java.util.Date;
public class Student {
private int id;
private String studentId; // Student ID code
private String name;
private Date birthDate;
private String email;
private String phone;
// Default constructor
public Student() {
}
// Parameterized constructor
public Student(String studentId, String name, Date birthDate, String email, String phone) {
this.studentId = studentId;
this.name = name;
this.birthDate = birthDate;
this.email = email;
this.phone = phone;
}
// Getters and Setters
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getStudentId() {
return studentId;
}
public void setStudentId(String studentId) {
this.studentId = studentId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getBirthDate() {
return birthDate;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
@Override
public String toString() {
return "Student [id=" + id + ", studentId=" + studentId + ", name=" + name +
", birthDate=" + birthDate + ", email=" + email + ", phone=" + phone + "]";
}
}Complete Application with Multithreading
import java.sql.SQLException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class StudentManagementApp {
private static Scanner scanner = new Scanner(System.in);
private static StudentDAO studentDAO;
private static ExecutorService executor = Executors.newFixedThreadPool(3);
private static SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
public static void main(String[] args) {
try {
// Initialize DAO
studentDAO = new StudentDAO();
boolean running = true;
while (running) {
displayMenu();
int choice = scanner.nextInt();
scanner.nextLine(); // Read newline
switch (choice) {
case 1:
addNewStudent();
break;
case 2:
displayAllStudents();
break;
case 3:
findStudentById();
break;
case 4:
updateStudentInfo();
break;
case 5:
deleteStudentRecord();
break;
case 6:
backupDataToFile();
break;
case 0:
running = false;
executor.shutdown();
DatabaseConnection.closeConnection();
System.out.println("Thank you for using the program!");
break;
default:
System.out.println("Invalid choice!");
}
}
} catch (SQLException e) {
System.out.println("Database connection error: " + e.getMessage());
} finally {
if (executor != null && !executor.isShutdown()) {
executor.shutdown();
}
}
}
private static void displayMenu() {
System.out.println("\n=== STUDENT MANAGEMENT SYSTEM ===");
System.out.println("1. Add new student");
System.out.println("2. View all students");
System.out.println("3. Find student by ID");
System.out.println("4. Update student info");
System.out.println("5. Delete student");
System.out.println("6. Backup data to file");
System.out.println("0. Exit");
System.out.print("Select function: ");
}
private static void addNewStudent() {
System.out.println("\n=== ADD NEW STUDENT ===");
try {
System.out.print("Enter student ID: ");
String studentId = scanner.nextLine();
System.out.print("Enter name: ");
String name = scanner.nextLine();
System.out.print("Enter birth date (dd/MM/yyyy): ");
String birthDateStr = scanner.nextLine();
Date birthDate = dateFormat.parse(birthDateStr);
System.out.print("Enter email: ");
String email = scanner.nextLine();
System.out.print("Enter phone number: ");
String phone = scanner.nextLine();
Student student = new Student(studentId, name, birthDate, email, phone);
// Execute adding student in separate thread
executor.submit(() -> {
try {
boolean success = studentDAO.addStudent(student);
if (success) {
System.out.println("Student added successfully!");
} else {
System.out.println("Failed to add student!");
}
} catch (SQLException e) {
System.out.println("Error: " + e.getMessage());
}
});
} catch (ParseException e) {
System.out.println("Invalid date format. Please use dd/MM/yyyy");
}
}
private static void displayAllStudents() {
System.out.println("\n=== STUDENT LIST ===");
executor.submit(() -> {
try {
List<Student> students = studentDAO.getAllStudents();
if (students.isEmpty()) {
System.out.println("No students in database");
return;
}
System.out.printf("%-10s %-30s %-15s %-25s %-15s\n",
"ID", "Full Name", "Birth Date", "Email", "Phone");
System.out.println("--------------------------------------------------------------------------------------------------------");
for (Student student : students) {
System.out.printf("%-10s %-30s %-15s %-25s %-15s\n",
student.getStudentId(),
student.getName(),
dateFormat.format(student.getBirthDate()),
student.getEmail(),
student.getPhone());
}
} catch (SQLException e) {
System.out.println("Error retrieving student list: " + e.getMessage());
}
});
}
private static void findStudentById() {
System.out.print("\nEnter student ID to find: ");
String studentId = scanner.nextLine();
executor.submit(() -> {
try {
Student student = studentDAO.findByStudentId(studentId);
if (student != null) {
System.out.println("\n=== STUDENT INFO ===");
System.out.println("Student ID: " + student.getStudentId());
System.out.println("Full Name: " + student.getName());
System.out.println("Birth Date: " + dateFormat.format(student.getBirthDate()));
System.out.println("Email: " + student.getEmail());
System.out.println("Phone: " + student.getPhone());
} else {
System.out.println("Student not found with ID " + studentId);
}
} catch (SQLException e) {
System.out.println("Error finding student: " + e.getMessage());
}
});
}
private static void updateStudentInfo() {
System.out.print("\nEnter student ID to update: ");
String studentId = scanner.nextLine();
executor.submit(() -> {
try {
Student student = studentDAO.findByStudentId(studentId);
if (student != null) {
System.out.println("\n=== UPDATE STUDENT INFO ===");
System.out.println("Current student: " + student.getName());
System.out.print("Enter new name (Enter to keep): ");
String name = scanner.nextLine();
if (!name.isEmpty()) {
student.setName(name);
}
System.out.print("Enter new birth date (dd/MM/yyyy) (Enter to keep): ");
String birthDateStr = scanner.nextLine();
if (!birthDateStr.isEmpty()) {
try {
Date birthDate = dateFormat.parse(birthDateStr);
student.setBirthDate(birthDate);
} catch (ParseException e) {
System.out.println("Invalid date format, keeping old birth date");
}
}
System.out.print("Enter new email (Enter to keep): ");
String email = scanner.nextLine();
if (!email.isEmpty()) {
student.setEmail(email);
}
System.out.print("Enter new phone (Enter to keep): ");
String phone = scanner.nextLine();
if (!phone.isEmpty()) {
student.setPhone(phone);
}
boolean success = studentDAO.updateStudent(student);
if (success) {
System.out.println("Student info updated successfully!");
} else {
System.out.println("Failed to update student info!");
}
} else {
System.out.println("Student not found with ID " + studentId);
}
} catch (SQLException e) {
System.out.println("Error updating student info: " + e.getMessage());
}
});
}
private static void deleteStudentRecord() {
System.out.print("\nEnter student ID to delete: ");
String studentId = scanner.nextLine();
System.out.print("Are you sure you want to delete this student? (y/n): ");
String confirm = scanner.nextLine();
if (confirm.equalsIgnoreCase("y")) {
executor.submit(() -> {
try {
boolean success = studentDAO.deleteStudent(studentId);
if (success) {
System.out.println("Student deleted successfully!");
} else {
System.out.println("Student not found with ID " + studentId);
}
} catch (SQLException e) {
System.out.println("Error deleting student: " + e.getMessage());
}
});
} else {
System.out.println("Delete operation cancelled");
}
}
private static void backupDataToFile() {
System.out.println("\n=== BACKUP DATA ===");
System.out.print("Enter file path to save: ");
String filePath = scanner.nextLine();
executor.submit(() -> {
try {
List<Student> students = studentDAO.getAllStudents();
// Create another thread to write file
Runnable backupTask = () -> {
try (java.io.PrintWriter writer = new java.io.PrintWriter(new java.io.FileWriter(filePath))) {
writer.println("Student ID,Full Name,Birth Date,Email,Phone");
for (Student student : students) {
writer.printf("%s,%s,%s,%s,%s\n",
student.getStudentId(),
student.getName(),
dateFormat.format(student.getBirthDate()),
student.getEmail(),
student.getPhone());
}
System.out.println("Data backed up successfully to file: " + filePath);
} catch (java.io.IOException e) {
System.out.println("Error writing file: " + e.getMessage());
}
};
// Use executor to perform backup task
executor.submit(backupTask);
} catch (SQLException e) {
System.out.println("Error retrieving data for backup: " + e.getMessage());
}
});
}
}🧪 FINAL PROJECT: Student Management System with Database
Project Description
Build a JAVA application with the following functions:
- Connect to a database (MySQL or SQLite).
- Allow:
- Add student (ID, name, birth date, email).
- View student list.
- Edit, delete student.
- Find student by name or ID.
- Command line interface, menu options.
Requirements
- Use JDBC to manipulate data.
- Handle multithreading when reading/writing backup files in parallel with user operations.
- Ensure data is not conflicted when there are multiple concurrent operations.
Here, we will temporarily pause the journey with JAVA to enter the world of SQL - the most widely used database query language today. This will not only expand your knowledge but also help you understand deeper how applications interact with databases - an essential skill in most real-world software development projects.
