OOPS
OOPS
(Autonomous)
Unit -5
Thread, Generics and JDBC
Multithreading – Thread Class and Runnable Interface – Thread Life Cycle – Synchronization
– Introduction to Generics in Java, Java Database Connectivity
Multithreading is a Java feature that allows the concurrent execution of two or more parts of a
program (called threads) for maximum CPU utilization. Each thread runs independently but shares
Key Concepts
//Body
} //ending
Multithreaded Program
A unique property of the java is that it supports the multithreading. Java enables us the
multiple flows of control in developing the program. Each separate flow of control is thought
as tiny program known as "thread" that runs in parallel with other threads. In the following
example when the main thread is executing, it may call thread A, as the Thread A is in execution
again a call is mad for Thread B. Now the processor is switched from Thread A to Thread B.
After the task is finished the flow of control comes back to the Thread A. The ability of the
language that supports multiple threads is called "Concurrency". Since threads in the java are
small sub programs of the main program and share the same address space, they are called
"light weight processes".
Main
thread
Start
Start
Start
switch
Thread A
Thread B switch Thread C
static Thread.currentThread( )
This method returns a reference to the thread in which it is called. Once you have a reference
to the main thread, you can control it just like any other thread.
Let’s begin by reviewing the following example:
CurrentThreadDemo.java
Output
✅ Explanation:
Thread.currentThread() returns a reference to the currently executing thread (in this case, the
main thread).
The thread name is changed using setName("My Thread").
Thread.sleep(1000) pauses the main thread for 1 second between each print of the countdown.
If the thread is interrupted during sleep, an InterruptedException is caught and handled.
Creation of Thread
Creating the threads in the Java is simple. The threads can be implemented in the form of
object that contains a method "run()". The "run()" method is the heart and soul of any thread. It
makes up the entire body of the thread and is the only method in which the thread behavior can be
implemented.
There are two ways to create thread.
1. Declare a class that implements the Runnable interface which contains the run() method .
2. Declare a class that extends the Thread class and override the run() method.
Example program:
// STEP 1: Implement Runnable interface
class x implements Runnable {
Second Run: Produces different output in the second run, because of the processor switching
from one thread to other.
Creating Multiple Threads
So far, you have been using only two threads: the main thread and one child thread.
However, your program can spawn as many threads as it needs. For example, the following
program creates three child threads:
It is often very important to know which thread is ended. This helps to prevent the main from
terminating before the child Thread is terminating. To address this problem "Thread" class
provides two methods: 1) Thread.isAlive() 2) Thread.join().
This method returns the either "TRUE" or "FALSE" . It returns "TRUE" if the thread is alive,
returns "FALSE" otherwise.
While isAlive( ) is occasionally useful, the method that you will more commonly use to wait for
a thread to finish is called join( ), shown here:
Example Program:
Thread t;
NewThread(String threadname) {
name = threadname;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
class DemoJoin {
try {
ob1.t.join();
ob2.t.join();
ob3.t.join();
} catch (InterruptedException e) {
Output:
One: 5
Two: 5
Three: 5
...
Three exiting.
This program demonstrates multithreading in Java using the Runnable interface, where three threads
are created and executed concurrently. The join() method ensures that the main thread waits for all
other threads ("One", "Two", and "Three") to finish before proceeding. It effectively shows how to
manage thread execution order and monitor thread status using isAlive().
To set a thread’s priority, use the setPriority( ) method, which is a member of Thread.
This is its general form:
Here, level specifies the new priority setting for the calling thread. The value of level must be
within the range MIN_PRIORITY and MAX_PRIORITY. Currently, these values are 1 and
10, respectively. To return a thread to default priority, specify NORM_PRIORITY, which is
currently 5. These priorities are defined as static final variables within Thread.
You can obtain the current priority setting by calling the getPriority( ) method of Thread,
shown here:
// Setting and Getting Thread Priorities
// Start threads
pt1.start();
pt2.start();
pt3.start();
Output:
Child 1 is started
Child 2 is started
Child 3 is started
The pt1 thread priority is: 1
Explanation:
This program creates three threads and assigns different priorities using setPriority(). The start()
method begins their execution, and the getPriority() method is used to retrieve and display the priority
of one thread (pt1). Although priorities suggest execution order (higher runs first), actual scheduling is
JVM-dependent, so results may vary.
SYNCHRONIZATION
When two or more threads need access to a shared resource, they need some way to
ensure that the resource will be used by only one thread at a time. The process by which this is
achieved is called synchronization.
Let us try to understand the problem without synchronization. Here, in the following example to
threads are accessing the same resource (object) to print the Table. The Table class contains one
method, printTable(int ), which actually prints the table. We are creating two Threads, Thread1
and Thread2, which are using the same instance of the Table Resource (object), to print the table.
When one thread is using the resource, no other thread is allowed to access the same resource
Table to print the table.
class Table {
// Not synchronized: multiple threads can access simultaneously
void printTable(int n) {
for (int i = 1; i <= 5; i++) {
System.out.println(n * i);
try {
Thread.sleep(400);
} catch (InterruptedException ie) {
System.out.println("Exception: " + ie);
}
}
}
}
MyThread1(Table t) {
this.t = t;
}
MyThread2(Table t) {
this.t = t;
}
t1.start();
t2.start();
}
}
Output :
5
100
10
200
15
300
20
400
25
500
This Java program demonstrates what happens when multiple threads access a shared resource without
synchronization. The class Table contains a method printTable(int n) that prints the multiplication
table of a given number. Two thread classes, MyThread1 and MyThread2, are created. They both receive a
shared object of the Table class and call the printTable() method with different values: 5 and 100.
However, the printTable() method is not marked with the synchronized keyword, meaning there is no
control over how the threads access this method. When t1.start() and t2.start() are called, the threads
run independently and simultaneously. As a result, the JVM may switch between them at any moment,
leading to interleaved or mixed output on the console. This kind of output is unpredictable and varies with
each program run.
class Table {
// Synchronized method to avoid thread interference
synchronized void printTable(int n) {
for (int i = 1; i <= 5; i++) {
System.out.println(n * i);
try {
Thread.sleep(400); // delay to simulate thread execution
} catch (InterruptedException ie) {
System.out.println("The Exception is: " + ie);
}
}
} // end of printTable()
}
MyThread1(Table t) {
this.t = t;
}
MyThread2(Table t) {
this.t = t;
}
t1.start();
t2.start();
}
}
This The threads will now execute one after the other, not simultaneously, because of synchronization on the
printTable() method.
Java program demonstrates how to prevent thread interference by using the synchronized keyword
when multiple threads access a shared resource. The program defines a class Table with a method
printTable(int n) that prints the multiplication table of a given number. Two thread classes, MyThread1
and MyThread2, are created to call this method with different values (5 and 100).
In the original version, the printTable() method was not synchronized, so both threads could access and
execute it at the same time, leading to interleaved output. To fix this issue, the method is marked as
synchronized, which ensures that only one thread can execute the method at any given time on the
shared object.
In the main() method, a single instance of Table is created and shared by both threads. When the first
thread starts and enters the printTable() method, the second thread must wait until the first one
completes. This behavior guarantees clean and ordered output. The use of Thread.sleep(400) introduces
a small delay, making the thread switching and synchronization effects more visible.
Note:
1. This way of communications between the threads competing for same resource is
called implicit communication.
2. This has one disadvantage due to polling. The polling wastes the CPU time. To save the
CPU time, it is preferred to go to the inter-thread communication.
INTER-THREAD COMMUNICATION
If two or more Threads are communicating with each other, it is called "inter thread"
communication. Using the synchronized method, two or more threads can communicate
indirectly. Through, synchronized method, each thread always competes for the resource. This
way of competing is called polling. The polling wastes the much of the CPU valuable time. The
better solution to this problem is, just notify other threads for the resource, when the current
thread has finished its task. This is explicit communication between the threads.
Java addresses this polling problem, using via wait(), notify(), and notifyAll() methods. These
methods are implemented as final methods in Object, so all classes have them. All three
methods can be called only from within a synchronized context.
wait( ) tells the calling thread to give up the monitor and go to sleep until
some other thread enters the same monitor and calls notify( ).
notify( ) wakes up a thread that called wait( ) on the same object.
notifyAll( ) wakes up all the threads that called wait( ) on the same object. One
of the threads will be granted access.
class Q {
int n;
while (!valueSet) {
try {
} catch (InterruptedException e) {
System.out.println("InterruptedException
caught");
}
System.out.println("Got: " + n);
valueSet = false;
consumed
return n;
while (valueSet) {
try {
} catch (InterruptedException e) {
System.out.println("InterruptedException
caught");
this.n = n;
valueSet = true;
// Producer thread
class Producer implements Runnable {
Q q;
Producer(Q q) {
this.q = q;
int i = 0;
while (true) {
q.put(i++);
// Consumer thread
Q q;
Consumer(Q q) {
this.q = q;
while (true) {
q.get();
// Main class
thread
Output:
Put: 0
Got: 0
Put: 1
Got: 1
Put: 2
Got: 2
Both threads run in infinite loops, simulating a continuous production and consumption of values. The
synchronized keyword ensures that only one thread accesses put() or get() at a time, avoiding race
conditions. The wait() and notify() methods allow the threads to coordinate, ensuring that the Producer
doesn’t overwrite unconsumed data and the Consumer doesn’t read before data is available.
Whenever we want stop a thread we can stop from running using "stop()" method of thread
class. It's general form will be as follows:
Thread.stop();
This method causes a thread to move from running to dead state. A thread will also move to
dead state automatically when it reaches the end of its method.
Blocking Thread
A thread can be temporarily suspended or blocked from entering into the runnable and running
state by using the following methods:
sleep() —blocked for specified time
suspend() ----blocked until further orders
wait() --blocked until certain condition occurs
These methods cause the thread to go into the blocked state. The thread will return to the
runnable state when the specified time is elapsed in the case of sleep(), the resume() method is
invoked in the case of suspend(), and the notify() method is called in the case of wait().
Example program:
The following program demonstrates these methods:
// Using suspend() and resume().
Output:
New thread: Thread[One,5,main]
New thread: Thread[Two,5,main]
One: 15
Two: 15
One: 14
Two: 14
One: 13
Two: 13
Suspending thread One
Two: 12
Two: 11
Two: 10
Resuming thread One
Suspending thread Two
One: 12
One: 11
One: 10
Resuming thread Two
Two: 9
One: 9
Two: 8
One: 8
...
Main thread exiting.
Two threads One and Two are created from the NewThread class.
Both threads print a countdown from 15 to 1 with a 200ms delay.
Finally, the main() thread waits for both child threads to finish using join() and exits.
Start
suspend() resume()
sleep() notify()
wait()
Blocked State
Newborn State: When we create a thread it is said to be in the new born state. At this state we
can do the following:
schedule it for running using the start() method.
Kill it using stop() method.
Runnable State: A runnable state means that a thread is ready for execution and waiting for the
availability of the processor. That is the thread has joined the queue of the threads for execution.
If all the threads have equal priority, then they are given time slots for execution in the round
robin fashion, first-come, first-serve manner. The thread that relinquishes the control will join the
queue at the end and again waits for its turn. This is known as time slicing.
Running State:
Running state: Running state means that the processor has given its time to the thread for it
execution. The thread runs until it relinquishes the control or it is preempted by the other higher
priority thread. As shown in the fig. a running thread can be preempted using the suspend(), or
wait(), or sleep() methods.
Blocked state: A thread is said to be in the blocked state when it is prevented from entering into
runnable state and subsequently the running state.
Dead state: Every thread has a life cycle. A running thread ends its life when it has completed
execution. It is a natural death. However we also can kill the thread by sending the stop()
message to it at any time.
Thread Exceptions
Note that a call to the sleep() method is always enclosed in try/ catch block. This is necessary
because the sleep() method throws an exception, which should be caught. If we fail to catch the
exception the program will not compile.
try }
{
Th
rea
d.s
lee
p(
10
00
);
catch(Exception e)
{
}
Deadlock
Deadlock in java is a part of multithreading. Deadlock can occur in a situation when a thread
is waiting for an object lock, that is acquired by another thread and second thread is waiting for
an object lock that is acquired by first thread. Since, both threads are waiting for each other to
release the lock, the condition is called deadlock.
Thread1 Thread2
Y
Here, in the above figure, the resource X is held by Thread1, and at the same time the Thread1 is
trying to access the resource which is held by the Thread2. This is causing the circular
dependency between two Threads. This is called, Deadlock.
Example program:
TestDead.java
public class TestDead
{
public static void main(String[] args)
{
final String resource1 = "John Gardner";
final String resource2 = "James Gosling";
// t1 tries to lock resource1 then resource2
try {
Thread.sleep(100);
}
catch (Exception e)
{
System.out.println(e);
}
synchronized (resource2)
{
System.out.println("Thread 1: locked resource 2");
}
}
} //end of run()
}; //end of t1
synchronized (resource1)
{
System.out.println("Thread 2: locked resource 1");
}
}
}//end of run()
}; //end of t2
t1.start();
t2.start();
}
}
Output:
GENERICS IN JAVA
Generics means parameterized types. The idea is to allow a type (like Integer, String, etc., or user-defined
types) to be a parameter to methods, classes, and interfaces. Generics in Java allow us to create classes,
interfaces, and methods where the type of the data is specified as a parameter. If we use generics, we do not
need to write multiple versions of the same code for different data types.
1. Generic Method: A generic Java method takes a parameter and returns some value after performing a
task. It is exactly like a normal function, however, a generic method has type parameters that are cited by an
actual type. This allows the generic method to be used in a more general way. The compiler takes care of the
type of safety, which enables programmers to code easily since they do not have to perform long, individual
type castings.
2. Generic Classes: A generic class is implemented exactly like a non-generic class. The only difference is
that it contains a type parameter section. There can be more than one type of parameter, separated by a
comma. The classes that accept one or more parameters are known as parameterized classes or
parameterized types.
Generic Class
A generic class is a class that can operate on objects of different types using a type parameter. Like C++, we
use <> to specify parameter types in generic class creation. To create objects of a generic class, we use the
following syntax:
// To create an instance of generic class
Note: In Parameter type, we can not use primitives like "int", "char", or "double". Use wrapper classes
like Integer, Character, etc.
// Java program to show working of user defined
// Generic classes
Output
15
Java Programming
This program demonstrates the concept of Java Generics by defining a user-defined generic class
Test<T>. The type T is a type parameter which will be replaced with an actual data type when an object of
Test is created.
The class has one member: T obj, where T can be any data type (Integer, String, etc.).
The constructor Test(T obj) initializes the object.
The method getObject() returns the value of obj.
In the main() method of class Geeks, two instances of the generic class Test are created:
// constructor
Test(T obj1, U obj2)
{
this.obj1 = obj1;
this.obj2 = obj2;
}
obj.print();
}
}
Output
GfG
15
Generic Method
We can also write generic methods that can be called with different types of arguments based on the type of
arguments passed to the generic method. The compiler handles each method.
// Java program to show working of user defined
// Generic functions
class Geeks {
// Driver method
public static void main(String[] args)
{
// Calling generic method with Integer argument
genericDisplay(11);
Output
java.lang.Integer = 11
java.lang.String = GeeksForGeeks
java.lang.Double = 1.0
Limitations of Generics
1. Generics Work Only with Reference Types
When we declare an instance of a generic type, the type argument passed to the type parameter must be a
reference type. We cannot use primitive data types like int, char.
Test<int> obj = new Test<int>(20);
The above line results in a compile-time error that can be resolved using type wrappers to encapsulate a
primitive type.
But primitive type arrays can be passed to the type parameter because arrays are reference types.
ArrayList<int[]> a = new ArrayList<>();
2. Generic Types Differ Based on their Type Arguments
During compilation, generic type information is erased which is also known as type erasure.
Example:
// Java program to show working
// of user-defined Generic classes
Output:
error:
incompatible types:
Test cannot be converted to Test
Explanation: Even though iObj and sObj are of type Test, they are the references to different types because
their type parameters differ. Generics add type safety through this and prevent errors.
Benefits of Generics:
Programs that use Generics has got many benefits over non-generic code.
1. Code Reuse: We can write a method/class/interface once and use it for any type we want.
2. Type Safety: Generics make errors to appear compile time than at run time (It's always better to know
problems in your code at compile time rather than making your code fail at run time).
Suppose you want to create an ArrayList that store name of students, and if by mistake the programmer adds
an integer object instead of a string, the compiler allows it. But, when we retrieve this data from ArrayList,
it causes problems at runtime.
Example: Without Generics
// Java program to demonstrate that NOT using
// generics can cause run time exceptions
import java.util.*;
class Geeks
{
public static void main(String[] args)
{
// Creatinga an ArrayList without any type specified
ArrayList al = new ArrayList();
al.add("Sweta");
al.add("Gudly");
al.add(10); // Compiler allows this
String s1 = (String)al.get(0);
String s2 = (String)al.get(1);
Output :
Exception in thread "main" java.lang.ClassCastException:
java.lang.Integer cannot be cast to java.lang.String
at Test.main(Test.java:19)
class Geeks
{
public static void main(String[] args)
{
// Creating a an ArrayList with String specified
ArrayList <String> al = new ArrayList<String> ();
al.add("Sweta");
al.add("Gudly");
String s1 = (String)al.get(0);
String s2 = (String)al.get(1);
String s3 = (String)al.get(2);
}
}
Output:
15: error: no suitable method found for add(int)
al.add(10);
^
3. Individual Type Casting is not needed: If we do not use generics, then, in the above example, every
time we retrieve data from ArrayList, we have to typecast it. Typecasting at every retrieval operation is a big
headache. If we already know that our list only holds string data, we need not typecast it every time.
Example:
// We don't need to typecast individual members of ArrayList
import java.util.*;
class Geeks {
public static void main(String[] args)
{
// Creating a an ArrayList with String specified
ArrayList<String> al = new ArrayList<String>();
al.add("Sweta");
al.add("Gudly");
4. Generics Promotes Code Reusability: With the help of generics in Java, we can write code that will
work with different types of data. For example,
Let's say we want to Sort the array elements of various data types like int, char, String etc. Basically we will
be needing different functions for different data types. For simplicity, we will be using Bubble sort.
But by using Generics, we can achieve the code reusability feature.
swap(j, j + 1, a);
}
}
}
Output
Sorted Integer array: 6, 22, 41, 50, 58, 100,
Sorted Character array: a, c, d, g, t, v, x,
Sorted String array: Amiya, Gudly, Kandhei, Kuna, Mama, Rani, Sweta,
JDBC is an API that helps applications to communicate with databases, it allows Java programs to connect
to a database, run queries, retrieve, and manipulate data. Because of JDBC, Java applications can easily
work with different relational databases like MySQL, Oracle, PostgreSQL, and more.
JDBC Architecture
Explanation:
Application: It can be a Java application or servlet that communicates with a data source.
The JDBC API: It allows Java programs to execute SQL queries and get results from the database.
Some key components of JDBC API include
o Interfaces like Driver, ResultSet, RowSet, PreparedStatement, and Connection that helps
managing different database tasks.
o Classes like DriverManager, Types, Blob, and Clob that helps managing database
connections.
DriverManager: It plays an important role in the JDBC architecture. It uses some database-specific
drivers to effectively connect enterprise applications to databases.
JDBC drivers: These drivers handle interactions between the application and the database.
The JDBC architecture consists of two-tier and three-tier processing models to access a database. They are
as described below:
1. Two-Tier Architecture
A Java Application communicates directly with the database using a JDBC driver. It sends queries to the
database and then the result is sent back to the application. For example, in a client/server setup, the user's
system acts as a client that communicates with a remote database server.
Structure:
Client Application (Java) -> JDBC Driver -> Database
2. Three-Tier Architecture
In this, user queries are sent to a middle-tier services, which interacts with the database. The database results
are processed by the middle tier and then sent back to the user.
Structure:
Client Application -> Application Server -> JDBC Driver -> Database
JDBC Components
There are generally 4 main components of JDBC through which it can interact with a database. They are
as mentioned below:
1. JDBC API
It provides various methods and interfaces for easy communication with the database. It includes two key
packages
java.sql: This package, is the part of Java Standard Edition (Java SE) , which contains the core
interfaces and classes for accessing and processing data in relational databases. It also provides essential
functionalities like establishing connections, executing queries, and handling result sets
javax.sql: This package is the part of Java Enterprise Edition (Java EE) , which extends the
capabilities of java.sql by offering additional features like connection pooling, statement pooling, and
data source management.
It also provides a standard to connect a database to a client application.
2. JDBC Driver Manager
Driver manager is responsible for loading the correct database-specific driver to establish a connection with
the database. It manages the available drivers and ensures the right one is used to process user requests and
interact with the database.
3. JDBC Test Suite
It is used to test the operation (such as insertion, deletion, updating) being performed by JDBC Drivers.
4. JDBC Drivers
JDBC drivers are client-side adapters (installed on the client machine, not on the server) that convert
requests from Java programs to a protocol that the DBMS can understand. There are 4 types of JDBC
drivers:
1. Type-1 driver or JDBC-ODBC bridge driver
2. Type-2 driver or Native-API driver (partially java driver)
3. Type-3 driver or Network Protocol driver (fully java driver)
4. Type-4 driver or Thin driver (fully java driver) - It is a widely used driver. The older drivers like (JDBC-
ODBC) bridge driver have been deprecated and no longer supported in modern versions of Java.
JDBC Classes and Interfaces
Class/Interfaces Description
// Establish connection
Connection c = DriverManager.getConnection(
url, username, password);
// Create a statement
Statement st = c.createStatement();
Note: When the program runs successfully, a new record is added to the students table as shown below: