Multithreading in Java:
Multithreading is the concurrent execution of more than one sequential process, or thread. In
Java, a thread is a lightweight process that runs independently but shares resources like memory
with other threads. Multithreading is used to perform multiple operations at the same time, which
is useful for tasks like GUI updates, server communication, and data processing
1. The Main Thread:
Every Java application has a main thread that starts execution when the program runs. This
thread is created by the JVM when the main method is invoked.
The main thread typically initializes the application and may spawn additional threads for
concurrent tasks.
Example:
static void main(String[] args) {
System.out.println("Main thread started");
// Main thread executes this code
}
}
2. Java Thread Model:
In Java, the thread model is based on the Thread class and the Runnable interface.
Thread Class:
Java provides the Thread class, which represents a thread of execution.
It provides various methods like start(), run(), sleep(), join(), and getId().
Example:
public class MyThread extends Thread {
public void run() {
System.out.println("Thread is running");
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // This invokes the run() method
}
}
Runnable Interface:
Instead of extending the Thread class, you can implement the Runnable interface.
The Runnable interface has a single method run(), which defines the code to be executed by
the thread.
Example:
public class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread is running using Runnable");
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable);
t1.start();
}
}
Thread Lifecycle:
Threads in Java follow a lifecycle with states like:
New: Thread is created but not yet started.
Runnable: Thread is ready to run and waiting for CPU time.
Blocked: Thread is waiting to acquire a lock.
Waiting: Thread is waiting indefinitely for another thread to perform a particular action.
Terminated: Thread has finished execution.
3. Thread Priorities:
Java allows assigning priorities to threads. By default, all threads have a priority of 5, but you can set it
between Thread.MIN_PRIORITY (1) and Thread.MAX_PRIORITY (10).
Thread Priority:
o Priority helps the JVM decide when each thread should be executed relative to others.
o Higher priority threads are typically given preference by the CPU over lower-priority
threads.
Example:
public class ThreadPriorityExample {
public static void main(String[] args) {
Thread t1 = new Thread(() -> System.out.println("Thread 1"));
Thread t2 = new Thread(() -> System.out.println("Thread 2"));
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
}
}
4. Synchronization in Java:
When multiple threads access shared resources (like memory or files) concurrently, the possibility of
data inconsistency arises. Synchronization in Java ensures that only one thread can access a shared
resource at a time, preventing data corruption.
Types of Synchronization:
Method-level Synchronization: You can synchronize a method by using the synchronized
keyword. This ensures that only one thread can execute the method at any given time.
Example:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
Block-level Synchronization: You can synchronize a block of code inside a method to limit the
scope of synchronization.
Example:
class Counter {
private int count = 0;
public void increment() {
synchronized(this) {
count++;
}
}
}
Static Method Synchronization: Synchronizing a static method locks on the class object rather
than an instance of the class.
Example:
public static synchronized void staticMethod() {
// synchronized code
}
Deadlock:
A deadlock occurs when two or more threads are blocked forever, waiting for each other to release
resources. It's crucial to avoid circular dependencies between synchronized blocks.
5. Inter-thread Communication:
Java provides methods for threads to communicate and coordinate with each other:
wait(): Causes the current thread to release the lock and wait until another thread sends a
notification.
notify(): Wakes up one thread that is waiting on the object’s monitor.
notifyAll(): Wakes up all threads waiting on the object’s monitor.
These methods are typically used inside synchronized blocks.
Example of Inter-thread Communication:
class SharedResource {
private int number = 0;
public synchronized void produce() throws InterruptedException {
while (number != 0) {
wait(); // Wait if number is not 0
}
number = 1; // Produce an item
System.out.println("Produced: " + number);
notify(); // Notify consumer
}
public synchronized void consume() throws InterruptedException {
while (number == 0) {
wait(); // Wait if number is 0
}
number = 0; // Consume the item
System.out.println("Consumed: " + number);
notify(); // Notify producer
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
Thread producer = new Thread(() -> {
try {
resource.produce();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
resource.consume();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
In the above code:
The produce() method produces an item and notifies the consumer.
The consume() method consumes the item and notifies the producer.
I/O in Java:
In Java, I/O (Input/Output) operations are crucial for interacting with external systems, like files,
network connections, and devices. Java provides a robust and flexible I/O framework to handle
these operations efficiently.
1. I/O Basics:
I/O in Java is handled through the java.io package, which provides a rich set of classes to perform
input and output operations. These operations can be done using:
Streams: Represent a flow of data (either from input or output).
Readers and Writers: Handle character-based I/O, i.e., for textual data.
InputStream and OutputStream: Handle byte-based I/O, i.e., for binary data.
I/O Types in Java:
Byte Stream: For handling raw binary data (e.g., FileInputStream, FileOutputStream).
Character Stream: For handling character data, which includes automatic encoding/decoding
(e.g., FileReader, FileWriter).
2. Streams and Stream Classes:
Streams in Java are abstract representations of input and output devices, allowing you to read and write
data. The two main categories of streams are:
Byte Streams:
These handle I/O of raw binary data.
InputStream: Abstract class for reading byte data.
o FileInputStream: Reads data from a file.
o BufferedInputStream: Buffers input data for more efficient reading.
OutputStream: Abstract class for writing byte data.
o FileOutputStream: Writes data to a file.
o BufferedOutputStream: Buffers output data to increase performance.
Character Streams:
These handle I/O of character data, ensuring proper encoding and decoding.
Reader: Abstract class for reading character data.
o FileReader: Reads character data from a file.
o BufferedReader: Buffers characters to read data more efficiently (e.g., line-by-line
reading).
Writer: Abstract class for writing character data.
o FileWriter: Writes character data to a file.
o BufferedWriter: Buffers characters to write data more efficiently (e.g., to improve
performance when writing large data).
3. The Predefined Streams:
Java provides three predefined streams that are automatically available when the program starts:
1. Standard Input Stream (System.in): Used to read data from the console.
2. Standard Output Stream (System.out): Used to write data to the console.
3. Standard Error Stream (System.err): Used to write error messages to the console.
These streams are instances of InputStream and OutputStream and are often used for basic console
interaction.
Example of using System.in (reading from console):
import java.io.*;
import java.util.Scanner;
public class ConsoleInputExample {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("Enter your name:");
String name = scanner.nextLine();
System.out.println("Hello, " + name);
scanner.close();
}
}
4. Reading from and Writing to the Console:
In Java, console input and output can be handled using the System.in and System.out streams.
Reading from Console:
You can read from the console using the Scanner class, or directly using System.in with
BufferedReader.
Using Scanner (easy and popular):
import java.util.Scanner;
public class ConsoleReadExample {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter a number: ");
int num = sc.nextInt();
System.out.println("You entered: " + num);
sc.close();
}
}
Using BufferedReader:
import java.io.*;
public class ConsoleReadBufferedReader {
public static void main(String[] args) throws IOException {
BufferedReader reader = new BufferedReader(new
InputStreamReader(System.in));
System.out.print("Enter a line: ");
String line = reader.readLine();
System.out.println("You entered: " + line);
}
}
Writing to Console:
For writing output to the console, you can use System.out.println() or System.out.print():
public class ConsoleWriteExample {
public static void main(String[] args) {
System.out.println("This is a message to the console.");
}
}
5. Reading and Writing Files:
In Java, you can use various stream classes to read from and write to files. Here are examples of reading
and writing text files.
Reading from a File (Character Stream):
import java.io.*;
public class FileReaderExample {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new
FileReader("example.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Writing to a File (Character Stream):
import java.io.*;
public class FileWriterExample {
public static void main(String[] args) {
try (BufferedWriter writer = new BufferedWriter(new
FileWriter("example.txt"))) {
writer.write("Hello, this is a line of text.");
writer.newLine();
writer.write("Writing to file is easy with BufferedWriter.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
Reading from a File (Byte Stream):
import java.io.*;
public class FileInputStreamExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("example.txt")) {
int byteData;
while ((byteData = fis.read()) != -1) {
System.out.print((char) byteData); // Print byte as a
character
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Writing to a File (Byte Stream):
import java.io.*;
public class FileOutputStreamExample {
public static void main(String[] args) {
try (FileOutputStream fos = new FileOutputStream("example.txt")) {
String data = "This is some binary data.";
fos.write(data.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
6. The Transient and Volatile Modifiers:
transient Keyword:
The transient keyword is used to indicate that a variable should not be serialized. When an object is
serialized, transient variables are not included in the serialization process. This is useful when certain
data should not be part of the object's serialized state.
Example:
import java.io.*;
class Person implements Serializable {
String name;
transient int age; // This field will not be serialized
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
public class TransientExample {
public static void main(String[] args) {
try (ObjectOutputStream oos = new ObjectOutputStream(new
FileOutputStream("person.ser"))) {
Person p = new Person("John", 30);
oos.writeObject(p);
} catch (IOException e) {
e.printStackTrace();
}
}
}
volatile Keyword:
The volatile keyword ensures that a variable’s value is always read from the main memory and not
from a thread's local cache. It is used for variables that are shared between multiple threads and need to
be accessed consistently.
Example:
class SharedResource {
volatile boolean flag = false; // Ensure that changes are visible across
threads
public void toggleFlag() {
flag = !flag;
}
}
7. Using Instance of Native Methods:
In Java, native methods are methods that are implemented in another programming language, typically
C or C++, to optimize performance or to interface with the hardware. The native keyword indicates
that the method's implementation is not written in Java.
To use a native method, Java uses the Java Native Interface (JNI).
Example:
public class NativeExample {
static {
System.loadLibrary("nativeLib"); // Load the native library
}
public native void displayMessage(); // Declaration of a native method
public static void main(String[] args) {
NativeExample obj = new NativeExample();
obj.displayMessage(); // Calls the native method
}
}
In this example, displayMessage() is implemented in a native C/C++ library nativeLib.
STRINGS AND CHARACTERS IN JAVA
In Java, strings and characters are fundamental elements used for handling textual data. Java
provides several built-in classes and methods to manage and manipulate strings and characters
efficiently.
1. Fundamentals of Characters and Strings:
Characters:
A character in Java is represented using the char data type, which is a 16-bit Unicode character.
This allows Java to support internationalization, as Unicode can represent characters from
virtually all human languages.
The char type stores a single character (e.g., 'A', '1', '$').
Example:
char letter = 'A';
char digit = '9';
char symbol = '$';
Strings:
A string is a sequence of characters. In Java, strings are represented using the String class,
which is part of the java.lang package.
Unlike char, strings are objects, and they are immutable, meaning once a string is created, its
value cannot be changed. However, new strings can be created from the original string.
Example:
String greeting = "Hello, World!";
2. The String Class:
The String class is used to create, manipulate, and work with strings. It is one of the most commonly
used classes in Java.
String Methods:
length(): Returns the length of the string (number of characters).
charAt(int index): Returns the character at the specified index.
substring(int start, int end): Returns a substring from the start index to the end index.
concat(String str): Concatenates the specified string to the end of the current string.
equals(Object obj): Compares the string to the specified object for equality.
toUpperCase(): Converts the string to uppercase.
toLowerCase(): Converts the string to lowercase.
indexOf(String str): Returns the index of the first occurrence of the specified substring.
replace(char oldChar, char newChar): Replaces all occurrences of the specified character in the
string.
Example:
public class StringExample {
public static void main(String[] args) {
String str = "Java Programming";
System.out.println("Length: " + str.length()); // 16
System.out.println("Character at index 5: " + str.charAt(5)); // P
System.out.println("Substring: " + str.substring(5, 16)); //
Programming
System.out.println("Uppercase: " + str.toUpperCase()); // JAVA
PROGRAMMING
System.out.println("Lowercase: " + str.toLowerCase()); // java
programming
System.out.println("Replace 'a' with 'o': " + str.replace('a', 'o'));
// Jovo Progromming
}
}
3. String Operations:
Java provides several powerful string operations that allow you to manipulate strings easily. Below are
some common operations:
Concatenation:
You can concatenate strings using the + operator or the concat() method of the String class.
Example:
String firstName = "John";
String lastName = "Doe";
String fullName = firstName + " " + lastName; // Using + operator
System.out.println(fullName); // John Doe
String Comparison:
equals(): Checks if two strings are equal.
equalsIgnoreCase(): Compares two strings, ignoring case differences.
compareTo(): Compares two strings lexicographically.
Example:
String str1 = "Hello";
String str2 = "hello";
System.out.println(str1.equals(str2)); // false
System.out.println(str1.equalsIgnoreCase(str2)); // true
System.out.println(str1.compareTo(str2)); // Negative value (because 'H' <
'h')
4. Data Conversion Using valueOf() Methods:
Java provides the String.valueOf() method, which is used to convert different types of data (e.g.,
primitive types, objects) to strings.
Common Usages of valueOf():
Convert primitive types to string: You can convert primitive data types (like int, double, etc.)
to their string representation.
Convert Objects to string: You can also use valueOf() to convert objects to their string
representation. If the object is null, the method returns the string "null".
Example:
int num = 42;
double price = 19.99;
char letter = 'A';
String strNum = String.valueOf(num); // "42"
String strPrice = String.valueOf(price); // "19.99"
String strLetter = String.valueOf(letter); // "A"
System.out.println(strNum);
System.out.println(strPrice);
System.out.println(strLetter);
In this example:
The integer 42 is converted to the string "42".
The double value 19.99 is converted to the string "19.99".
The char 'A' is converted to the string "A".
5. StringBuffer Class and Methods:
In addition to the String class, Java also provides the StringBuffer class, which is used for creating
and manipulating mutable strings. Unlike String, a StringBuffer object allows modifications to the
string without creating a new object each time, making it more efficient for situations where you need to
perform frequent string manipulations.
StringBuffer Methods:
append(): Adds text to the end of the current string buffer.
insert(int offset, String str): Inserts a string at the specified position.
delete(int start, int end): Removes characters between the specified start and end indices.
reverse(): Reverses the sequence of characters in the buffer.
capacity(): Returns the current capacity of the buffer.
setLength(): Sets the length of the buffer to the specified value.
Example:
public class StringBufferExample {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer("Hello");
// Append a string
sb.append(" World!");
System.out.println(sb); // Hello World!
// Insert a string at a specified index
sb.insert(6, "Java ");
System.out.println(sb); // Hello Java World!
// Delete a portion of the string
sb.delete(6, 10); // Removes "Java"
System.out.println(sb); // Hello World!
// Reverse the string
sb.reverse();
System.out.println(sb); // !dlroW olleH
// Set a specific length (shortens the string)
sb.setLength(5);
System.out.println(sb); // !dlro
}
}
In this example:
append() adds " World!" to the end of the string.
insert() adds "Java " at index 6.
delete() removes characters from index 6 to 10.
reverse() reverses the entire string.
setLength() truncates the string to 5 characters.