mirror of
https://github.com/dholerobin/Lecture_Notes.git
synced 2025-09-13 05:42:12 +00:00
added sql and lld1 notes to master
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
# Introduction to Object Oriented Programming
|
||||
---
|
||||
## Programming Paradigms
|
||||
Programming paradigms are different ways or styles in which a given program or programming language can be organized. Each paradigm consists of certain structures, features, and opinions about how common programming problems should be tackled.
|
||||
|
||||
The question of why are there many different programming paradigms is similar to why are there many programming languages. Certain paradigms are better suited for certain types of problems, so it makes sense to use different paradigms for different kinds of projects. They're more like a set of ideals and guidelines that many people have agreed on, followed, and expanded upon.
|
||||
|
||||
Programming languages aren't always tied to a specific paradigm. There are languages that have been built with a certain paradigm in mind and have features that facilitate that kind of programming more than others (Haskel and functional programming is a good example).
|
||||
But there are also "multi-paradigm" languages, meaning you can adapt your code to fit a certain paradigm or another (JavaScript and Python are good examples).
|
||||
|
||||
Broadly speaking, the paradigms can be classified into two major types of programming paradigms:
|
||||
|
||||
**Imperative** - an imperative program consists of commands for the computer to perform to change state e.g. C, Java, Python, etc.
|
||||
|
||||
**Declarative** - focuses on what the program should accomplish without specifying all the details of how the program should achieve the result e.g. SQL, Lisp, Java etc.
|
||||
|
||||
|
||||
### Popular Programming Paradigms
|
||||
Now that we have introduced what programming paradigms are and are not, let's go through the most popular ones, explain their main characteristics, and compare them.
|
||||
|
||||
- Imperative Programming
|
||||
- Procedural Programming
|
||||
- Object Oriented Programming
|
||||
- Functional Programming
|
||||
- Reactive Programming
|
||||
|
||||
### 1. Imperative Programming
|
||||
Imperative programming consists of sets of detailed instructions that are given to the computer to execute in a given order. It's called "imperative" because as programmers we dictate exactly what the computer has to do, in a very specific way.
|
||||
Imperative programming focuses on describing how a program operates, step by step.
|
||||
Say you want to bake a cake. Your imperative program to do this might look like this
|
||||
```
|
||||
1- Pour flour in a bowl
|
||||
2- Pour a couple eggs in the same bowl
|
||||
3- Pour some milk in the same bowl
|
||||
4- Mix the ingredients
|
||||
5- Pour the mix in a mold
|
||||
6- Cook for 35 minutes
|
||||
7- Let chill
|
||||
```
|
||||
|
||||
Using an actual code example, let's say we want to filter an array of numbers to only keep the elements bigger than 5. Our imperative code might look like this:
|
||||
|
||||
```java
|
||||
int nums[] = [1,4,3,6,7,8,9,2]
|
||||
Arraylist<Integer> result = new Arraylist<>();
|
||||
|
||||
for (int i = 0; i < nums.length; i++) {
|
||||
if (nums[i] > 5) result.push_back(nums[i])
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Procedural Programming
|
||||
Procedural programming is a derivation of imperative programming, adding to it the feature of functions (also known as "procedures" or "subroutines").
|
||||
|
||||
In procedural programming, the user is encouraged to subdivide the program execution into functions, as a way of improving modularity and organization.
|
||||
|
||||
Following our cake example, procedural programming may look like this:
|
||||
```java
|
||||
function pourIngredients() {
|
||||
- Pour flour in a bowl
|
||||
- Pour a couple eggs in the same bowl
|
||||
- Pour some milk in the same bowl
|
||||
}
|
||||
|
||||
function mixAndTransferToMold() {
|
||||
- Mix the ingredients
|
||||
- Pour the mix in a mold
|
||||
}
|
||||
|
||||
function cookAndLetChill() {
|
||||
- Cook for 35 minutes
|
||||
- Let chill
|
||||
}
|
||||
|
||||
pourIngredients()
|
||||
mixAndTransferToMold()
|
||||
cookAndLetChill()
|
||||
```
|
||||
You can see that, thanks to the implementation of functions, we could just read the three function calls at the end of the file and get a good idea of what our program does.
|
||||
|
||||
That simplification and abstraction is one of the benefits of procedural programming. But
|
||||
within the functions, we still got same old imperative code.
|
||||
|
||||
### 3. Object-Oriented Programming
|
||||
One of the most popular programming paradigms is object-oriented programming (OOP).
|
||||
|
||||
The core concept of OOP is to separate concerns into entities which are coded as objects. Each entity will group a given set of information (properties) and actions (methods) that can be performed by the entity.
|
||||
|
||||
OOP makes heavy usage of classes (which are a way of creating new objects starting out from a blueprint or boilerplate that the programmer sets). Objects that are created from a class are called instances.
|
||||
|
||||
Following our pseudo-code cooking example, now let's say in our bakery we have a main cook (called Frank) and an assistant cook (called Anthony) and each of them will have certain responsibilities in the baking process. If we used OOP, our program might look like this.
|
||||
|
||||
```java
|
||||
// Create the two classes corresponding to each entity
|
||||
class Cook {
|
||||
String name;
|
||||
Cook (String name) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
void mixAndBake() {
|
||||
- Mix the ingredients
|
||||
- Pour the mix in a mold
|
||||
- Cook for 35 minutes
|
||||
}
|
||||
}
|
||||
|
||||
class AssistantCook {
|
||||
String name;
|
||||
AssistantCook (String name) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
void pourIngredients() {
|
||||
- Pour flour in a bowl
|
||||
- Pour a couple eggs in the same bowl
|
||||
- Pour some milk in the same bowl
|
||||
}
|
||||
|
||||
void chillTheCake() {
|
||||
- Let chill
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate an object from each class
|
||||
Cook Frank = new Cook('Frank')
|
||||
AssistantCook Anthony = new AssistantCook('Anthony')
|
||||
|
||||
// Call the corresponding methods from each instance
|
||||
Anthony.pourIngredients()
|
||||
Frank.mixAndBake()
|
||||
Anthony.chillTheCake()
|
||||
```
|
||||
|
||||
What's nice about OOP is that it facilitates the understanding of a program, by the clear separation of concerns and responsibilities. Languages like C++, Java, Python etc. support Object Oriented Programming.
|
||||
|
||||
### 4.Declarative programming / Functional Programming
|
||||
Declarative Programming is all about hiding away complexity and bringing programming languages closer to human language and thinking. It's the direct opposite of imperative programming in the sense that the programmer doesn't give instructions about how the computer should execute the task, but rather on what result is needed.
|
||||
|
||||
The basic objective of this style of programming is to make code more concise, less complex, more predictable, and easier to test compared to the legacy style of coding.
|
||||
|
||||
So far Java was supporting the imperative style of programming and object-oriented style of programming. The next big thing what java has been added is that Java has started supporting the functional style of programming with its Java 8 release.
|
||||
|
||||
The functional style of programming is declarative programming. In the imperative style of coding, we define what to do a task and how to do it. Whereas, in the declarative style of coding, we only specify what to do. Let’s understand this with an example. Given a list of number let’s find out the sum of double of even numbers from the list using an imperative and declarative style of coding.
|
||||
|
||||
```java
|
||||
// Java program to find the sum
|
||||
// using imperative style of coding
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
public class TestImperative {
|
||||
public static void main(String[] args)
|
||||
{
|
||||
List<Integer> numbers
|
||||
= Arrays.asList(11, 22, 33, 44,
|
||||
55, 66, 77, 88,
|
||||
99, 100);
|
||||
|
||||
int result = 0;
|
||||
for (Integer n : numbers) {
|
||||
if (n % 2 == 0) {
|
||||
result += n * 2;
|
||||
}
|
||||
}
|
||||
System.out.println(result);
|
||||
}
|
||||
}
|
||||
```
|
||||
The first issue with the above code is that we are mutating the variable result again and again. So mutability is one of the biggest issues in an imperative style of coding. The second issue with the imperative style is that we spend our effort telling not only what to do but also how to do the processing. Now let’s re-write above code in a declarative style.
|
||||
```java
|
||||
// Java program to find the sum
|
||||
// using declarative style of coding
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
public class GFG {
|
||||
public static void main(String[] args)
|
||||
{
|
||||
List<Integer> numbers
|
||||
= Arrays.asList(11, 22, 33, 44,
|
||||
55, 66, 77, 88,
|
||||
99, 100);
|
||||
|
||||
System.out.println(
|
||||
numbers.stream()
|
||||
.filter(number -> number % 2 == 0)
|
||||
.mapToInt(e -> e * 2)
|
||||
.sum());
|
||||
}
|
||||
}
|
||||
```
|
||||
The above example uses Java 8 Stream API. We will learn about Java 8 Streams, in later part of this LLD module. Language like Scala, Haskell, C# and Java also supports this paradigm as seen in above example.
|
||||
|
||||
### 5. Reactive Programming
|
||||
Reactive programming is a programming paradigm that focuses on constructing responsive and robust software applications that can handle asynchronous data streams and change propagation, allowing developers to create scalable and more easily maintainable applications that can adapt to a dynamic environment. In a reactive system, data flows through streams, which can be thought of as sequences of events over time.
|
||||
Reactive programming revolves around the following principles - Data Streams, Observables, Observers and Operators. Java also supports this paradigm.
|
||||
|
||||
-----
|
||||
## Motivation - Object Oriented Programming
|
||||
|
||||
**Problem Statement** Once upon a time in a software shop, two programmers were given the same spec and told to “build it”. The Project Manager forced the two coders - to compete. The problem statement is as follows: There will be shapes on GUI, a square, a circle and a triangle. When the user clicks the shape, it will rotate clockwise 360 degrees and play a .mp3 sound corresponding to that shape.
|
||||
|
||||
**Coder 1**
|
||||
Focusses on writing procedures. He wrote the `rotate()` and `playSound()` procedure in no time.
|
||||
|
||||
```java
|
||||
void rotate(int shapeNum){
|
||||
//code to rotate the shape about center
|
||||
}
|
||||
|
||||
void playSound(int shapeNum){
|
||||
if(shapeNum==1){
|
||||
//play the square sound
|
||||
}
|
||||
else if(shapeNum==2){
|
||||
//play the circle sound
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
**Coder 2**
|
||||
Coder2 wrote a class for each of the three shapes.
|
||||
|
||||

|
||||
|
||||
**Another Requirement comes...**
|
||||
Now manager, added another requirement : There will be an amoeba shape on the screen, with the others. When the user clicks on the amoeba, it will rotate like the others, and play a .hif sound file.
|
||||
|
||||
**Coder1:** has to make changes in playSound method, rotate would still work!
|
||||
```java
|
||||
void playSound(shapeNum) {
|
||||
// if the shape is not an amoeba,
|
||||
// use shapeNum to lookup which
|
||||
// AIF sound to play, and play it
|
||||
// else
|
||||
// play amoeba .hif sound
|
||||
}
|
||||
```
|
||||
It turned out not to be such a big deal, but it still made him queasy to touch previously-tested code. Of all people, he should know that no matter what the project manager says, the spec always changes.
|
||||
|
||||
**Coder 2**: smiled, sipped his Coffee, and wrote one new class. Sometimes the thing he loved most about OO was that he didn’t have to touch code he’d already tested and delivered. “Flexibility, extensibility,...” he mused, reflecting on the benefits of Object Oriented Programming.
|
||||
|
||||
```java
|
||||
class Amoeba{
|
||||
void playSound(){
|
||||
...
|
||||
}
|
||||
void rotate(){
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
But even that's not the best design! We’ve got duplicated code! The rotate method is in all four Shape things. We didn’t see the final design. Let us come back to it when we discuss inheritance in upcoming class.
|
||||
|
||||
---
|
||||
## What is Object Oriented Programming?
|
||||
Object-oriented programming (OOP) is a programming paradigm that uses objects to model real-world things and aims to implement state and behavior using objects.
|
||||
|
||||
Object-Oriented Programming is based on implementing the state and behaviour concepts together. State and behaviour are combined into one new concept: an Object. An OO application can therefore produce some output by calling an Object, without needing to pass data structures.
|
||||
|
||||
**Procedural vs OOPS World**
|
||||
The focus of procedural programming is to break down a programming task into a collection of variables, data structures, and subroutines, whereas in object-oriented programming it is to break down a programming task into objects that expose behavior (methods) and data (members or attributes). The most important distinction is that while procedural programming uses procedures to operate on data structures, object-oriented programming bundles the two together, so an "object", which is an instance of a class, operates on its "own" data structure.
|
||||
## Classes and Objects
|
||||
Object-oriented programming bundles the the data + behaviour related to one entity inside a class. Lets understand classes and objects followed by pillars of OOPS - Abstraction, Encapsulation, Inheritance and Polymorphism in more detail.
|
||||
|
||||
### Classes
|
||||
Classes are the starting point of all objects, and we may consider them as the template for creating objects. A class would typically contain member fields, member methods, and a special constructor method.
|
||||
|
||||
```java
|
||||
public class Player {
|
||||
//Data Members
|
||||
String name;
|
||||
int guess;
|
||||
|
||||
|
||||
//Member Methods
|
||||
void makeGuess() {
|
||||
this.guess = (int)(Math.random() * 9.0) + 1;
|
||||
System.out.println(this.name + " guessed " + this.guess);
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
### Objects
|
||||
Objects are created from classes and are called instances of the class. We create objects from classes using their constructors.
|
||||
```java
|
||||
Player p1 = new Player();
|
||||
Player p2 = new Player();
|
||||
p1.name = "Prateek";
|
||||
p2.name = "Naman";
|
||||
p1.makeGuess(); //Prateek guessed 9
|
||||
p2.makeGuess(); //Naman guessed 6
|
||||
```
|
||||
Now, we’ve created different Player objects, all from a single class. These objects are created from Player class at runtime. This is the point of it all, to define the blueprint in one place, and then, to reuse it many times in many places.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### Pillars of Object Oriented Programming
|
||||
We have 4 pillars foundational concepts in OOPS also called Pillars of Object Oriented Programming, these are Abstraction, Encapsulation, Inheritance & Polymorphism.
|
||||
#### Abstraction
|
||||
Abstraction is hiding complexities of implementation and exposing simpler interfaces.
|
||||
If we think about a typical computer, one can only see the external interface, which is most essential for interacting with it, while internal chips and circuits are hidden from the user. In OOP, abstraction means hiding the complex implementation details of a program, exposing only the API required to use the implementation. In Java, we achieve abstraction by using interfaces and abstract classes.
|
||||
|
||||
#### Encapsulation
|
||||
Encapsulation is hiding the state or internal representation of an object from the end-user and providing publicly accessible methods bound to the object for read-write access. This allows for hiding specific information and controlling access to internal implementation.
|
||||
|
||||
Encapsulation is used to hide the values or state of a structured data object inside a class, preventing direct access to them by clients in a way that could expose hidden implementation details or violate state invariance maintained by the methods.
|
||||
|
||||
```java
|
||||
public class Player {
|
||||
//Data Members
|
||||
String name;
|
||||
private int guess;
|
||||
|
||||
//Provide a method to read guess
|
||||
int getGuess(){
|
||||
return guess;
|
||||
}
|
||||
|
||||
//Member Methods
|
||||
void makeGuess() {
|
||||
//Randomly generated from 1-9
|
||||
this.guess = (int)(Math.random() * 9.0) + 1;
|
||||
System.out.println(this.name + " guessed " + this.guess);
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
For example, private member fields like ```guess``` in the class are hidden from other classes, and they can be accessed using the member methods ```getGuess()```. This provides a way to read the value of Guess but prevents write access from other classes.
|
||||
|
||||
#### Inheritance
|
||||
Inheritance is the mechanism that allows one class to acquire all the properties from another class by inheriting the class. We call the inheriting class a child class and the inherited class as the superclass or parent class.
|
||||
|
||||
In Java, we do this by extending the parent class. Thus, the child class gets all the properties from the parent. We will talk about inheritance in detail later.
|
||||
|
||||
```java
|
||||
public class User {
|
||||
String username;
|
||||
String email;
|
||||
};
|
||||
```
|
||||
Student inherits all properties and methods from User.
|
||||
```java
|
||||
public class Student extends User{
|
||||
int marks;
|
||||
}
|
||||
```
|
||||
|
||||
#### Polymorphism
|
||||
Polymorphism is the ability of an OOP language to process data differently depending on their types of inputs. In Java, this can be the same method name having different method signatures and performing different functions.
|
||||
```java
|
||||
public class TextFile {
|
||||
//...
|
||||
|
||||
public String read() {
|
||||
return this.getContent()
|
||||
.toString();
|
||||
}
|
||||
|
||||
public String read(int limit) {
|
||||
return this.getContent()
|
||||
.toString()
|
||||
.substring(0, limit);
|
||||
}
|
||||
|
||||
public String read(int start, int stop) {
|
||||
return this.getContent()
|
||||
.toString()
|
||||
.substring(start, stop);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
In this example, we can see that the method ```read()``` has three different forms with different functionalities. This type of polymorphism is static or compile-time polymorphism and is also called method overloading. There is also runtime or dynamic polymorphism, where the child class overrides the parent’s method.We will study runtime polymorphism in later classes.
|
||||
|
||||
---
|
||||
## Advantages & Disadvantages of OOPS
|
||||
|
||||
### Advantages
|
||||
**1. Reusability**: Through classes and objects, and inheritance of common attributes and functions.
|
||||
|
||||
**2. Security**: Hiding and protecting information through encapsulation.
|
||||
|
||||
**3. Maintenance**: Easy to make changes without affecting existing objects much.
|
||||
|
||||
**4. Inheritance**: Easy to import required functionality from libraries and customize them, thanks to inheritance.
|
||||
|
||||
### Disadvantages
|
||||
|
||||
1. Beforehand planning of entities is required that should be modeled as classes.
|
||||
|
||||
2. OOPS programs are usually larger than those of other paradigms.
|
||||
|
||||
3. Banana-gorilla problem - You wanted a banana but what you got was a gorilla holding the banana and the entire jungle. [Read More](https://dev.to/efpage/what-s-wrong-with-the-gorilla-2l4j#:~:text=Joe%20Armstrong%2C%20the%20principal%20inventor,and%20the%20entire%20jungle.%22.)
|
||||
|
||||
----- End ----
|
||||
|
||||
|
||||
|
@@ -0,0 +1,372 @@
|
||||
# OOPS II - Access Modifiers & Constructors
|
||||
---
|
||||
In this tutorial, we will learn about
|
||||
- Access Modifiers
|
||||
- Getters & Setters
|
||||
- Constructors
|
||||
- Shallow & Deep Copy
|
||||
- Java Memory Model - Objects & References
|
||||
- Life & Death on Objects on Heap
|
||||
- Project : Guess Game using OOPS Concepts
|
||||
|
||||
## Access Modifiers
|
||||
The access modifiers in Java specifies the accessibility or scope of a field, method, constructor, or class. We can change the access level of fields, constructors, methods, and class by applying the access modifier on it. Let's understand it through an example.
|
||||
```java
|
||||
public class Player {
|
||||
//Data Members
|
||||
String name;
|
||||
private int guess;
|
||||
public String handle;
|
||||
|
||||
//Example of private method
|
||||
// can't be called from outside the class
|
||||
private void assignTeam(){
|
||||
...
|
||||
}
|
||||
//example of a public method
|
||||
//can be called from anywhere
|
||||
public void setTeam(int teamId){
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
In the above class, `guess` is private, `name` is default, `handle` is public. What does't it mean? Let understand the meanings of above acess modifiers.
|
||||
|
||||
There are four types of access modifiers in Java:
|
||||
|
||||
```public``` - The access level of a public modifier is everywhere. It can be accessed from within the class, outside the class, within the package and outside the package
|
||||
|
||||
```protected``` - The access level of a protected modifier is within the package and outside the package through child class. If you do not make the child class, it cannot be accessed from outside the package.
|
||||
|
||||
```private``` - The access level of a private modifier is only within the class. It cannot be accessed from outside the class.
|
||||
|
||||
```default``` - The access level of a default modifier is only within the package. It cannot be accessed from outside the package. If you do not specify any access level, it will be the default.
|
||||
|
||||

|
||||
|
||||
**Getters & Setters**
|
||||
|
||||
If you try to read or write a private data member, outside the class, you will get a compile error. In order to work with private data members, you might need to create special public methods called `getters()` and `setters()` in the class for specific data members as shown below.
|
||||
|
||||
```java
|
||||
public class Player {
|
||||
//Data Members
|
||||
String name;
|
||||
private int guess;
|
||||
public String handle;
|
||||
|
||||
//Setter Method
|
||||
public int setGuess(int guess){
|
||||
//Setters can have their validation logic before updating class member
|
||||
if(guess>=0){
|
||||
this.guess = guess;
|
||||
}
|
||||
}
|
||||
// Getter Method
|
||||
public int getGuess(){
|
||||
return this.guess;
|
||||
}
|
||||
}
|
||||
```
|
||||
The advantage of this approach is you can set the value if it satisfies the class specific validation logic.
|
||||
|
||||
|
||||
### Constructors
|
||||
A constructor is a special method that is called when an object is created. It is used to initialize the object. It is called automatically when the object is created. It can be used to set initial values for object attributes.
|
||||
|
||||
Constructors are the gatekeepers of object-oriented design. Let us create a class for students:
|
||||
|
||||
**Student.java**
|
||||
```java
|
||||
public class Student {
|
||||
|
||||
private String name;
|
||||
private String email;
|
||||
private Integer age;
|
||||
private String address;
|
||||
private String batchName;
|
||||
private Integer psp;
|
||||
|
||||
public void changeBatch(String batchName) {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
The above class can be used to create objects of type Student. This is done by using the new keyword:
|
||||
```java
|
||||
Student student = new Student();
|
||||
student.name = "Eklavya";
|
||||
```
|
||||
You can notice that we did not define a constructor for the Student class. This brings us to our first type of constructor
|
||||
|
||||
### Default constructor
|
||||
A default constructor is a constructor created by the compiler if we do not define any constructor(s) for a class.
|
||||
|
||||
A default constructor is a constructor that either has no parameters, or if it has parameters, all the parameters have default values. If no user-defined constructor exists for a class and one is needed, the compiler implicitly declares a default parameterless constructor.
|
||||
|
||||
A default constructor is also known as a no-argument constructor or a nullary constructor. All fields are left at their initial value of 0 (integer types), 0.0 (floating-point types), false (boolean type), or null (reference types) An example of a no-argument constructor is:
|
||||
|
||||
```java
|
||||
public class Student {
|
||||
private String name;
|
||||
private String email;
|
||||
private Integer age;
|
||||
private String address;
|
||||
private String batchName;
|
||||
private Integer psp;
|
||||
|
||||
public Student() {
|
||||
// no-argument constructor
|
||||
}
|
||||
}
|
||||
```
|
||||
Notice a few things about the constructor which we just wrote. First, it's a method, but it has no return type. That's because a constructor implicitly returns the type of the object that it creates. Calling new Student() now will call the constructor above. Secondly, it takes no arguments. This particular kind of constructor is called a no-argument constructor.
|
||||
|
||||
**Syntax of a constructor**
|
||||
In Java, every class must have a constructor. Its structure looks similar to a method, but it has different purposes. A constructor has the following format `<Constructor Modifiers> <Constructor Declarator> <Constructor Body>`
|
||||
|
||||
Constructor declarations begin with access modifiers: They can be public, private, protected, or package access, based on other access modifiers. Unlike methods, a constructor can't be abstract, static, final, native, or synchronized.
|
||||
|
||||
The declarator is the name of the class, followed by a parameter list. The parameter list is a comma-separated list of parameters enclosed in parentheses. The body is a block of code that defines the constructor's behavior.
|
||||
|
||||
```Constructor Name (Parameter List)```
|
||||
|
||||
### Parameterised Constructor
|
||||
Now, a real benefit of constructors is that they help us maintain encapsulation when injecting state into the object. The constructor above is a no-argument constructor and hence value have to be set after the instance is created.
|
||||
|
||||
```java
|
||||
Student student = new Student();
|
||||
student.name = "prateek";
|
||||
student.email = "prateek@gmail.in";
|
||||
...
|
||||
```
|
||||
The above approach works but requires setting the values of all the fields after the instance is created. Also, we won't be able to validate or sanitize the values. We can add the validation and sanitization logic in the getters and setters but we wont be able to fail instance creation. Hence, we need to add a parameterised constructor. A parameterised constructor has the same syntax as the constructors before, the onl change is that it has a parameter list.
|
||||
|
||||
```java
|
||||
public class Student {
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
public Student(String name, String email) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
Student s1 = new Student("prateek", "prateek@gmail.in");
|
||||
Student s2 = new Student("rahul", "Rahul@gmail.in");
|
||||
```
|
||||
In Java, constructors differ from other methods in that:
|
||||
|
||||
- Constructors never have an explicit return type.
|
||||
- Constructors cannot be directly invoked (the keyword “new” invokes them).
|
||||
- Constructors should not have non-access modifiers.
|
||||
|
||||
### Copy constructor
|
||||
A copy constructor is a member function that initializes an object using another object of the same class. A copy constructor has the following general function prototype:
|
||||
|
||||
```java
|
||||
class Student {
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
public Student(String name, String email) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
//Copy Constructor
|
||||
public Student(Student student) {
|
||||
this.name = student.name;
|
||||
this.email = student.email;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Shallow & Deep Copy
|
||||
When we do a copy of some entity to create two or more than two entities such that changes in one entity are reflected in the other entities as well, then we can say we have done a shallow copy. In shallow copy, new memory allocation never happens for the other entities, and the only reference is copied to the other entities. The following example demonstrates the same.
|
||||
|
||||
```java
|
||||
public class Test {
|
||||
public int n;
|
||||
String name;
|
||||
public int arr[];
|
||||
|
||||
Test(int n,String name){
|
||||
this.n = n;
|
||||
this.name = name;
|
||||
this.arr = new int[n];
|
||||
for(int i=0;i<n;i++){
|
||||
arr[i] = i+1;
|
||||
}
|
||||
}
|
||||
|
||||
//Copy Constructor
|
||||
Test(Test X){
|
||||
//Copying the references (Shallow Copy)
|
||||
this.n = X.n;
|
||||
this.name = X.name;
|
||||
this.arr = X.arr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
|
||||
//Parametrised Constructor
|
||||
Test t1 = new Test(6,"Test1");
|
||||
//Copy Constructor Call
|
||||
Test t2 = new Test(t1);
|
||||
|
||||
t2.n = 7;
|
||||
t2.name = "Test2";
|
||||
t2.arr[0] = 56;
|
||||
|
||||
//Changes in T2's array are also reflect in T1's Array
|
||||
System.out.println(t1.arr); // 56, 2,3,4,5,6
|
||||
System.out.println(t2.arr); // 56,2,3,4,5,6
|
||||
}
|
||||
}
|
||||
```
|
||||
In the above example, we see we are creating a shallow copy in Line Copy Constructor of Test Class. Both the array references point to the same memory, the ideal way to do it would be to create a new array inside the copy constructor.
|
||||
|
||||
```java
|
||||
class Test{
|
||||
...
|
||||
Test(Test X){
|
||||
this.n = X.n;
|
||||
this.name = X.name;
|
||||
|
||||
// allocate a new memory - Deep Copy
|
||||
this.arr = new int[this.n];
|
||||
for(int i=0;i<n;i++){
|
||||
this.arr[i] = X.arr[i];
|
||||
}
|
||||
}
|
||||
```
|
||||
You might be wondering why didn't we create a deep copy for `int` and `String` data-types. This is because of the way java memory model works, for primitive data types like int, float etc Java always create new memory for different objects, and for strings because of immutability whenever you try to update the value of a String object, a new String object is automatically created in the string pool and the reference starts pointing to it. Hence for `int' and 'string' even if they are changed in T2 object, the changes won't be reflected in T1.
|
||||
|
||||
## Java Memory Model
|
||||
Let's try to understand how are objects stored in the memory. Calling a constructor with the command `new` causes several things to happen. First, space is reserved in the heap memory for storing object variables. Then default or initial values are set to object variables (e.g. an int type variable receives an initial value of 0). Lastly, the source code in the constructor is executed.
|
||||
|
||||
A constructor call returns a reference to an object. A reference is information about the location of object data.
|
||||
|
||||
|
||||
```java
|
||||
Player p1 = new Player("Prateek");
|
||||
```
|
||||
Here `p1` stores the location of object on the heap, and hence `p1` in a reference to the newly created player object.
|
||||
|
||||
### Objects & Object References
|
||||

|
||||
|
||||
### Life and Death on Heap
|
||||

|
||||
|
||||
|
||||
---
|
||||
# Project - Guess Game using OOPS Concepts
|
||||
**Problem Statement** - Create a 3 player game, in which a computer generates a random integer between 1-9. Each player has to make a random guess, guessing the number. The player takes turn in order to make a guess, the player who guesses the number correctly wins the game, if all three players make a wrong guess, the game starts again with computer making a new guess. Think about the entities, and their attributes, and the actions they can perform. Design & execute the game using OOPS principles.
|
||||
|
||||
|
||||
**Player.java**
|
||||
```java
|
||||
package GuessGame;
|
||||
|
||||
public class Player {
|
||||
//Attributes
|
||||
String name;
|
||||
private int guess;
|
||||
|
||||
//Methods
|
||||
Player(String name){
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
int getGuess(){
|
||||
return guess;
|
||||
}
|
||||
|
||||
void makeGuess(){
|
||||
this.guess = (int)(Math.random()*9) + 1;
|
||||
System.out.println(this.name + " guessed "+this.guess);
|
||||
}
|
||||
}
|
||||
```
|
||||
**Game.java**
|
||||
```java
|
||||
package GuessGame;
|
||||
|
||||
public class Game {
|
||||
int computerGuess;
|
||||
Player p1,p2,p3;
|
||||
|
||||
Game(String n1,String n2,String n3){
|
||||
//init players
|
||||
p1 = new Player(n1);
|
||||
p2 = new Player(n2);
|
||||
p3 = new Player(n3);
|
||||
}
|
||||
|
||||
private boolean checkWinner(){
|
||||
if(p1.getGuess()==computerGuess){
|
||||
System.out.println(p1.name + " wins");
|
||||
return true;
|
||||
}
|
||||
else if(p2.getGuess()==computerGuess){
|
||||
System.out.println(p2.name + " wins");
|
||||
return true;
|
||||
}
|
||||
else if(p3.getGuess()==computerGuess){
|
||||
System.out.println(p3.name + " wins");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void launch(){
|
||||
//update the computer Guess
|
||||
System.out.println("Welcome to Game");
|
||||
this.computerGuess = (int)(Math.random()*9) + 1;
|
||||
|
||||
while(true){
|
||||
System.out.println("Computer Guessed" + this.computerGuess);
|
||||
p1.makeGuess();
|
||||
p2.makeGuess();
|
||||
p3.makeGuess();
|
||||
|
||||
if(checkWinner()){
|
||||
System.out.println("Game Over");
|
||||
break;
|
||||
}
|
||||
else{
|
||||
this.computerGuess = (int)(Math.random()*9) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Launcher.java**
|
||||
```java
|
||||
package GuessGame;
|
||||
|
||||
public class Launcher {
|
||||
public static void main(String[] args) {
|
||||
Game firstGame = new Game("Sachin","Harsh","Suraj");
|
||||
firstGame.launch();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
Diagram References: [Head First Java Book](https://www.amazon.in/Head-First-Java-Brain-Friendly-Grayscale/dp/9355420900)
|
||||
|
||||
|
||||
|
298
Non-DSA Notes/LLD1 Notes/03 OOPS - Inheritance & Polymorphism.md
Normal file
298
Non-DSA Notes/LLD1 Notes/03 OOPS - Inheritance & Polymorphism.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# OOPS III - Inheritance & Polymorphism
|
||||
---
|
||||
In this tutorial, we will learn about
|
||||
- Static Variables & Methods
|
||||
- [Inheritance and polymorphism](#inheritance-and-polymorphism)
|
||||
- [Inheritance](#inheritance)
|
||||
- [Types of inheritance](#types-of-inheritance)
|
||||
- [Diamond problem](#diamond-problem)
|
||||
- [What is Polymorphism?](#what-is-polymorphism)
|
||||
- [Subtyping](#subtyping)
|
||||
- [Method Overloading](#method-overloading)
|
||||
- [Method Overriding](#method-overriding)
|
||||
- [Advantages of Polymorphism](#advantages-of-polymorphism)
|
||||
- [Problems with Polymorphism](#problems-with-polymorphism)
|
||||
- [Reading List](#reading-list)
|
||||
|
||||
|
||||
## Static Variables & Methods
|
||||
|
||||
The `static` keyword in Java is used for memory management mainly. We can apply static keyword with variables, methods, blocks and nested classes. The static keyword belongs to the class than an instance of the class.
|
||||
|
||||

|
||||
|
||||
When you add the static keyword to a variable, it means that the variable is no longer tied to an instance of the class. This means that only one instance of that static member is created which is shared across all instances of the class. Static variables are stored in heap memory just like non-static variables. The difference is that static variables are stored in a separate static segment of the heap memory.
|
||||
|
||||
```java
|
||||
public class StaticExample {
|
||||
public static String myStaticVariable = "Hello World";
|
||||
public String myNonStaticVariable = "Hello World";
|
||||
}
|
||||
```
|
||||
|
||||
Static variables are initialized when the class is loaded and can be accessed without creating an instance of the class.
|
||||
|
||||
```java
|
||||
System.out.println(StaticExample.myStaticVariable);
|
||||
```
|
||||
|
||||
Static methods are accessed using the class name and the dot operator. You don’t need to create an instance of the class to access the static method.
|
||||
|
||||
```java
|
||||
public class StaticExample {
|
||||
public static String myStaticVariable = "Hello World";
|
||||
public String myNonStaticVariable = "Hello World";
|
||||
|
||||
public static void myStaticMethod() {
|
||||
System.out.println("I am a static method");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
StaticExample.myStaticMethod();
|
||||
```
|
||||
|
||||
Static methods can only access static variables and other static methods. They can not access non-static variables and methods by default. If you try to access a non-static variable or method from a static method, the compiler will throw an error.
|
||||
|
||||
---
|
||||
|
||||
## Inheritance
|
||||
|
||||
Inheritance is the mechanism that allows one class to acquire all the properties from another class by inheriting the class. We call the inheriting class a **child class** and the inherited class as the **superclass or parent class**.
|
||||
|
||||
The idea behind inheritance in Java is that you can create new classes that are built upon existing classes. When you inherit from an existing class, you can reuse methods and fields of the parent class. Moreover, you can add new methods and fields in your current class also.
|
||||
|
||||
Inheritance represents the IS-A relationship which is also known as a parent-child relationship.
|
||||
|
||||
Imagine, as a car manufacturer, you offer multiple car models to your customers. Even though different car models might offer different features like a sunroof or bulletproof windows, they would all include common components and features, like engine and wheels.
|
||||
|
||||
It makes sense to create a basic design and extend it to create their specialized versions, rather than designing each car model separately, from scratch.
|
||||
|
||||
Similarly, with inheritance, we can create a class with basic features and behavior and create its specialized versions, by creating classes, that inherit this base class. In the same way, interfaces can extend existing interfaces.
|
||||
|
||||
|
||||
Let us create a new class `User` which should be the parent class of `Student`:
|
||||
|
||||
```java
|
||||
public class User {
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
public User(String name, String email) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now, let us create a new class `Student` which should be the child class of `User`. Let us add some methods specific to the student class:
|
||||
|
||||
```java
|
||||
public class Student {
|
||||
private String batchName;
|
||||
private Integer psp;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Now in order to inherit the methods and fields of the parent class, we need to use the keyword `extends`:
|
||||
|
||||
```java
|
||||
public class Student extends User {
|
||||
private String batchName;
|
||||
private Integer psp;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
To pass the values to the parent class, we need to create a constructor and use the keyword `super`:
|
||||
|
||||
```java
|
||||
public class Student extends User {
|
||||
private String batchName;
|
||||
private Integer psp;
|
||||
|
||||
public Student(String name, String email, String batchName, Integer psp) {
|
||||
super(name, email);
|
||||
this.batchName = batchName;
|
||||
this.psp = psp;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Types of inheritance
|
||||
|
||||
There are four types of inheritance:
|
||||
* `Single` - A single inheritance is when a class can have only one parent class.
|
||||
* `Multilevel` - A multilevel inheritance is when a class can have multiple parent classes at different levels.
|
||||
* `Hierarchical` - When two or more classes inherits a single class, it is known as hierarchical inheritance.
|
||||
* `Multiple` - When a class can have multiple parent classes, it is known as multiple inheritance.
|
||||
|
||||

|
||||
|
||||
#### Diamond problem
|
||||
|
||||
In multiple inheritance one class inherits the properties of multiple classes. In other words, in multiple inheritance we can have one child class and n number of parent classes. Java does not support multiple inheritance (with classes).
|
||||
|
||||

|
||||
|
||||
The "diamond problem" (sometimes referred to as the "Deadly Diamond of Death") is an ambiguity that arises when two classes B and C inherit from A, and class D inherits from both B and C. If there is a method in A that B and C have overridden, and D does not override it, then which version of the method does D inherit: that of B, or that of C.
|
||||
|
||||
---
|
||||
|
||||
## What is Polymorphism?
|
||||
|
||||
Polymorphism is one of the main aspects of Object-Oriented Programming(OOP). The word polymorphism can be broken down into “Poly” and “morphs”, as “Poly” means many and “Morphs” means forms. In simple words, we can say that ability of a message to be represented in many forms.
|
||||
|
||||
Polymorphism is often referred to as the third pillar of object-oriented programming, after encapsulation and inheritance. Polymorphism is a Greek word that means "many-shaped" and it has two distinct aspects:
|
||||
|
||||
* At run time, objects of a derived class may be treated as objects of a base class in places such as method parameters and collections or arrays. When this polymorphism occurs, the object's declared type is no longer identical to its run-time type
|
||||
* Base classes may define methods, and derived classes can override them, which means they provide their own definition and implementation. At run-time, when client code calls the method, the compiler looks up the run-time type of the object, and invokes that override of the virtual method. In your source code you can call a method on a base class, and cause a derived class's version of the method to be executed.
|
||||
|
||||
Polymorphism in Java can be achieved in two ways i.e., method overloading and method overriding.
|
||||
|
||||
Polymorphism in Java is mainly divided into two types.
|
||||
|
||||
* Compile-time polymorphism
|
||||
* Runtime polymorphism
|
||||
|
||||
Compile-time polymorphism can be achieved by method overloading, and Runtime polymorphism can be achieved by method overriding.
|
||||
|
||||
### Subtyping
|
||||
|
||||
Subtyping is a concept in object-oriented programming that allows a variable of a base class to reference a derived class object. This is called polymorphism, because the variable can take on many forms. The variable can be used to call methods that are defined in the base class, but the actual implementation of the method is defined in the derived class.
|
||||
|
||||
For example, the following is our User class:
|
||||
|
||||
```java
|
||||
public class User {
|
||||
private String name;
|
||||
private String email;
|
||||
}
|
||||
```
|
||||
|
||||
The user class is extended by the Student class:
|
||||
|
||||
```java
|
||||
public class Student extends User {
|
||||
private String batchName;
|
||||
private Integer psp;
|
||||
}
|
||||
```
|
||||
|
||||
The Student class inherits the name and email properties from the User class. The Student class also has its own properties batchName and psp. The Student class can be used in place of the User class, because the Student class is a subtype of the User class. The following is an example of how this works:
|
||||
|
||||
```java
|
||||
User user = new Student();
|
||||
```
|
||||
The advantage of sub-typing is that if you have a method that can accept any kind of User - then you can easily pass User or any of its subtypes while calling that method. This concept is heavily used in the Collections Framework.
|
||||
```
|
||||
class Main(){
|
||||
public void makePayment(User u){
|
||||
...
|
||||
}
|
||||
public static void Main(){
|
||||
User u = new User();
|
||||
Student s = new User();
|
||||
makePayment(u);
|
||||
makePayment(s);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Method Overloading
|
||||
|
||||
Method overloading is a feature that allows a class to have more than one method having the same name, if their argument lists are different. It is similar to constructor overloading in Java, that allows a class to have more than one constructor having different argument lists.
|
||||
|
||||
Let's take an example of a class that has two methods having the same name but different in parameters.
|
||||
|
||||
```java
|
||||
public class User {
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
public void printUser() {
|
||||
System.out.println("Name: " + name + ", Email: " + email);
|
||||
}
|
||||
|
||||
public void printUser(String name, String email) {
|
||||
System.out.println("Name: " + name + ", Email: " + email);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, the class has two methods with the same name printUser but different in parameters. The first method has no parameters, and the second method has two parameters. This is called method overloading.
|
||||
|
||||
**The compiler distinguishes these two methods by the number of parameters in the list and their data types. The return type of the method does not matter.**
|
||||
|
||||
### Method Overriding
|
||||
|
||||
Runtime polymorphism is also called Dynamic method dispatch. Instead of resolving the overridden method at compile-time, it is resolved at runtime.
|
||||
|
||||
Here, an overridden child class method is called through its parent's reference. Then the method is evoked according to the type of object. In runtime, JVM figures out the object type and the method belonging to that object.
|
||||
|
||||
Runtime polymorphism in Java occurs when we have two or more classes, and all are interrelated through inheritance. To achieve runtime polymorphism, we must build an "IS-A" relationship between classes and override a method.
|
||||
|
||||
If a child class has a method as its parent class, it is called method overriding.
|
||||
|
||||
If the derived class has a specific implementation of the method that has been declared in its parent class is known as method overriding.
|
||||
|
||||
Rules for overriding a method in Java
|
||||
* There must be inheritance between classes.
|
||||
* The method between the classes must be the same(name of the class, number, and type of arguments must be the same).
|
||||
|
||||
Let's add a method to our User class:
|
||||
|
||||
```java
|
||||
public class User {
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
public void printUser() {
|
||||
System.out.println("Name: " + name + ", Email: " + email);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now, let's add a method to our Student class:
|
||||
|
||||
```java
|
||||
public class Student extends User {
|
||||
private String batchName;
|
||||
private Integer psp;
|
||||
|
||||
@Override
|
||||
public void printUser() {
|
||||
System.out.println("Name: " + name + ", Email: " + email + ", Batch: " + batchName + ", PSP: " + psp);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, we have added a method to the Student class that overrides the method in the User class. The Student class has a method with the same name and parameters as the User class. The Student class method has an additional print statement that prints the batchName and psp properties.
|
||||
|
||||
The @Override annotation is optional, but it is a good practice to use it. It is used to ensure that the method is actually being overridden. If the method is not being overridden, the compiler will throw an error.
|
||||
|
||||
|
||||
|
||||
### Advantages of Polymorphism
|
||||
* Code reusability is the main advantage of polymorphism; once a class is defined, it can be used multiple times to create an object.
|
||||
* In compile-time polymorphism, the readability of code increases, as nearly similar functions can have the same name, so it becomes easy to understand the functions.
|
||||
* The same method can be created in the child class as in the parent class in runtime polymorphism.
|
||||
* Easy to debug the code. You might have intermediate results stored in arbitrary memory locations while executing code, which might get misused by other parts of the program. Polymorphism adds necessary structure and regularity to computation, so it is easier to debug.
|
||||
|
||||
|
||||
### Problems with Polymorphism
|
||||
* Implementing code is complex because understanding the hierarchy of classes and its overridden method is quite difficult.
|
||||
* Problems during downcasting because implicitly downcasting is not possible. Casting to a child type or casting a common type to an individual type is known as downcasting.
|
||||
* Sometimes, when the parent class design is not built correctly, subclasses of a superclass use superclass in unexpected ways. This leads to broken code.
|
||||
* Runtime polymorphism can lead to the real-time performance issue (during the process), it basically degrades the performances as decisions are taken at run time because, machine needs to decide which method or variable to invoke
|
||||
|
||||
## Reading List
|
||||
* [Deadly Diamond of Death](https://medium.com/@harshamw/deadly-diamond-of-death-e8bb4355c343)
|
||||
* [Detailed explanation of the diamond problem](https://www.cs.cornell.edu/courses/JavaAndDS/abstractInterface/05diamond.pdf)
|
||||
* [Duck Typing](https://realpython.com/lessons/duck-typing/#:~:text=Duck%20typing%20is%20a%20concept,a%20given%20method%20or%20attribute.)
|
||||
* [OOP in Python](https://gist.github.com/kanmaytacker/e6ed49131970c67588fba9164fbc45d4)
|
||||
|
@@ -0,0 +1,302 @@
|
||||
# OOPS IV - Abstract Classes & Interfaces
|
||||
---
|
||||
|
||||
- [Interfaces](#interfaces)
|
||||
- [How to create an interface?](#how-to-create-an-interface)
|
||||
- [Why use an interface?](#why-use-an-interface)
|
||||
- [Abstract Classes](#abstract-classes)
|
||||
- [Why use an abstract class?](#why-use-an-abstract-class)
|
||||
- [How to create an abstract class?](#how-to-create-an-abstract-class)
|
||||
- [Reading List](#reading-list)
|
||||
|
||||
## Interfaces
|
||||
|
||||
In Java, an interface is an abstract type that contains a collection of methods and constant variables.It can be thought of as a blueprint of behavior. It is one of the core concepts in Java and is used to achieve abstraction, polymorphism and multiple inheritances. An interface is a reference type in Java. It is similar to a class, but it cannot be instantiated. It can contain only constants, method signatures, default methods, static methods, and nested types. Method bodies exist only for default methods and static methods.
|
||||
|
||||
|
||||
### How to create an interface?
|
||||
|
||||
Let us create an interface for a Person
|
||||
|
||||
```java
|
||||
public interface Person {
|
||||
String getName();
|
||||
String getEmail();
|
||||
}
|
||||
```
|
||||
|
||||
Now let's create a class that implements the Person interface:
|
||||
|
||||
```java
|
||||
public class User implements Person {
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
public User(String name, String email) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In an interface, we’re allowed to use:
|
||||
|
||||
- constants variables
|
||||
- abstract methods
|
||||
- static methods
|
||||
- default methods
|
||||
|
||||
We also should remember that:
|
||||
- we can’t instantiate interfaces directly
|
||||
- an interface can be empty, with no methods or variables in it
|
||||
- we can’t use the final word in the interface definition, as it will result in a compiler error
|
||||
- all interface declarations should have the public or default access modifier; the abstract modifier will be added automatically by the compiler
|
||||
- an interface method can’t be protected or final
|
||||
- up until Java 9, interface methods could not be private; however, Java 9 introduced the possibility to define private methods in interfaces
|
||||
- interface variables are public, static, and final by definition; we’re not allowed to change their visibility
|
||||
|
||||
### Why use an interface?
|
||||
|
||||
#### 1. Behavioral Functionality
|
||||
We use interfaces to add certain behavioral functionality that can be used by unrelated classes. For instance, Comparable, Comparator, and Cloneable are Java interfaces that can be implemented by unrelated classes. Below is an example of the Comparator interface that is used to compare two instances of the Employee class:
|
||||
|
||||
```java
|
||||
public class Player {
|
||||
private int ranking;
|
||||
private String name;
|
||||
private int age;
|
||||
|
||||
// constructor, getters, setters
|
||||
}
|
||||
```
|
||||
Comparable is an interface defining a strategy of comparing an object with other objects of the same type. This is called the class’s “natural ordering.”
|
||||
|
||||
In order to be able to sort, we must define our Player object as comparable by implementing the Comparable interface.
|
||||
```java
|
||||
public class Player implements Comparable<Player> {
|
||||
|
||||
// same as before
|
||||
@Override
|
||||
public int compareTo(Player otherPlayer) {
|
||||
return Integer.compare(getRanking(), otherPlayer.getRanking());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
List<Player> footballTeam = new ArrayList<>();
|
||||
Player player1 = new Player(59, "John", 20);
|
||||
Player player2 = new Player(67, "Roger", 22);
|
||||
Player player3 = new Player(45, "Steven", 24);
|
||||
footballTeam.add(player1);
|
||||
footballTeam.add(player2);
|
||||
footballTeam.add(player3);
|
||||
|
||||
System.out.println("Before Sorting : " + footballTeam);
|
||||
Collections.sort(footballTeam);
|
||||
System.out.println("After Sorting : " + footballTeam);
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Multiple Inheritances
|
||||
Java classes support singular inheritance. However, by using interfaces, we’re also able to implement multiple inheritances.
|
||||
For instance, in the example below, we notice that the Car class implements the Fly and Transform interfaces. By doing so, it inherits the methods fly and transform:
|
||||
|
||||
```java
|
||||
public interface Transform {
|
||||
void transform();
|
||||
}
|
||||
|
||||
public interface Fly {
|
||||
void fly();
|
||||
}
|
||||
|
||||
public class Car implements Fly, Transform {
|
||||
|
||||
@Override
|
||||
public void fly() {
|
||||
System.out.println("I can Fly!!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transform() {
|
||||
System.out.println("I can Transform!!");
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### 3. Polymorphism
|
||||
Polymorphism is the ability for an object to take different forms during runtime. To be more specific it’s the execution of the override method that is related to a specific object type at runtime.
|
||||
|
||||
In Java, we can achieve polymorphism using interfaces. For example, the Shape interface can take different forms — it can be a Circle or a Square.
|
||||
|
||||
Let’s start by defining the Shape interface:
|
||||
|
||||
```java
|
||||
public interface Shape {
|
||||
String name();
|
||||
}
|
||||
```
|
||||
Now let’s also create the Circle class:
|
||||
```java
|
||||
public class Circle implements Shape {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "Circle";
|
||||
}
|
||||
}
|
||||
```
|
||||
And also the Square class:
|
||||
```java
|
||||
public class Square implements Shape {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "Square";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Finally, it’s time to see polymorphism in action using our Shape interface and its implementations. Let’s instantiate some Shape objects, add them to a List, and, finally, print their names in a loop:
|
||||
```java
|
||||
List<Shape> shapes = new ArrayList<>();
|
||||
Shape circleShape = new Circle();
|
||||
Shape squareShape = new Square();
|
||||
|
||||
shapes.add(circleShape);
|
||||
shapes.add(squareShape);
|
||||
|
||||
for (Shape shape : shapes) {
|
||||
System.out.println(shape.name());
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Default Methods in Interfaces
|
||||
Traditional interfaces in Java 7 and below don’t offer backward compatibility.
|
||||
What this means is that if you have legacy code written in Java 7 or earlier, and you decide to add an abstract method to an existing interface, then all the classes that implement that interface must override the new abstract method. Otherwise, the code will break.
|
||||
|
||||
Java 8 solved this problem by introducing the default method that is optional and can be implemented at the interface level.
|
||||
|
||||
```java
|
||||
public interface Shape {
|
||||
default void draw() {
|
||||
System.out.println("Drawing a Shape");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Summmary
|
||||
Interfaces are used in the following scenarios:
|
||||
* It is used to achieve abstraction.
|
||||
* Due to multiple inheritance, it can achieve loose coupling.
|
||||
* Define a common behavior for unrelated classes.
|
||||
|
||||
|
||||
|
||||
## Abstract Classes
|
||||
There are many cases when implementing a contract where we want to postpone some parts of the implementation to be completed later. We can easily accomplish this in Java through abstract classes.
|
||||
Before diving into when to use an abstract class, let’s look at their most relevant characteristics:
|
||||
|
||||
- We define an abstract class with the abstract modifier preceding the class keyword
|
||||
- An abstract class can be subclassed, but it can’t be instantiated
|
||||
- If a class defines one or more abstract methods, then the class itself must be declared abstract
|
||||
- An abstract class can declare both abstract and concrete methods
|
||||
- A subclass derived from an abstract class must either implement all the base class’s abstract methods or be abstract itself
|
||||
To better understand these concepts, we’ll create a simple example.
|
||||
|
||||
### How to create an abstract class?
|
||||
|
||||
Let us create an abstract class for a Person
|
||||
You can create an abstract class by using the abstract keyword.
|
||||
Similarly, you can create an abstract method by using the abstract keyword.
|
||||
|
||||
```java
|
||||
public abstract Person {
|
||||
|
||||
public abstract String getName();
|
||||
public abstract String getEmail();
|
||||
}
|
||||
```
|
||||
|
||||
Now let's create a class that extends the Person abstract class:
|
||||
|
||||
```java
|
||||
public class User extends Person {
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
public User(String name, String email) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Why use an abstract class?
|
||||
|
||||
Now, let’s analyze a few typical scenarios where we should prefer abstract classes over interfaces and concrete classes:
|
||||
|
||||
* It is used to achieve abstraction.
|
||||
* It can have abstract methods and non-abstract methods.
|
||||
* When you don't want to provide the implementation of a method, you can make it abstract.
|
||||
* When you don't want to allow the instantiation of a class, you can make it abstract.
|
||||
* We want to encapsulate some common functionality in one place (code reuse) that multiple, related subclasses will share
|
||||
* We need to partially define an API that our subclasses can easily extend and refine
|
||||
The subclasses need to inherit one or more common methods or fields with protected access modifiers
|
||||
* Moreover, since the use of abstract classes implicitly deals with base types and subtypes, we’re also taking advantage of Polymorphism.
|
||||
|
||||
**Sample Code**
|
||||
Let’s have our base abstract class define the abstract API of a board game:
|
||||
|
||||
```java
|
||||
public abstract class BoardGame {
|
||||
|
||||
//... field declarations, constructors
|
||||
|
||||
public abstract void play();
|
||||
|
||||
//... concrete methods
|
||||
}
|
||||
```
|
||||
Then, we can create a subclass that implements the play method:
|
||||
```java
|
||||
public class Checkers extends BoardGame {
|
||||
|
||||
public void play() {
|
||||
//... implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Reading List
|
||||
* [Comparators & Comparable Interface](https://www.baeldung.com/java-comparator-comparable)
|
||||
* [Interface Inheritance](https://www.baeldung.com/java-8-functional-interfaces)
|
||||
* [Functional Interfaces in Java 8](https://www.baeldung.com/java-8-functional-interfaces)
|
||||
* [Sample Java Collections Interface - Queue Interface](https://docs.oracle.com/javase/8/docs/api/java/util/Queue.html)
|
||||
* [Duck Typing](https://realpython.com/lessons/duck-typing/#:~:text=Duck%20typing%20is%20a%20concept,a%20given%20method%20or%20attribute.)
|
||||
* [OOP in Python](https://gist.github.com/kanmaytacker/e6ed49131970c67588fba9164fbc45d4)
|
||||
|
252
Non-DSA Notes/LLD1 Notes/Advanced Java 01 - Generics.md
Normal file
252
Non-DSA Notes/LLD1 Notes/Advanced Java 01 - Generics.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# Adv Java 01 - Generics
|
||||
----
|
||||
## Agenda
|
||||
- Intro to Generics
|
||||
- Generic Classes
|
||||
- Generic Methods
|
||||
- Wildcards in Generics
|
||||
- Bounded Generics
|
||||
- Generic Interfaces
|
||||
- Additional Concepts
|
||||
- Type Erasure
|
||||
|
||||
## Introduction
|
||||
Generics in Java provide a way to create classes, interfaces, and methods with a type parameter. This allows you to write code that can work with different types while providing compile-time type safety. In this beginner-friendly tutorial, we'll explore the basics of Java generics.
|
||||
|
||||
Generics offer several benefits:
|
||||
|
||||
1. Type Safety: Generics provide compile-time type checking, reducing the chances of runtime errors.
|
||||
|
||||
2. Code Reusability: You can write code that works with different types without duplicating it.
|
||||
|
||||
3. Elimination of Type Casting: Generics eliminate the need for explicit type casting, making the code cleaner.
|
||||
|
||||
## Generic Classes
|
||||
A generic class is a class that has one or more type parameters. Here's a simple example of a generic class.
|
||||
```java
|
||||
public class Box<T> {
|
||||
private T content;
|
||||
|
||||
public void addContent(T content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public T getContent() {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
```
|
||||
In this example, T is a type parameter. You can create instances of Box for different types:
|
||||
```java
|
||||
Box<Integer> intBox = new Box<>();
|
||||
intBox.addContent(42);
|
||||
System.out.println("Box Content: " + intBox.getContent()); // Output: 42
|
||||
|
||||
Box<String> stringBox = new Box<>();
|
||||
stringBox.addContent("Hello, Generics!");
|
||||
System.out.println("Box Content: " + stringBox.getContent()); // Output: Hello, Generics!
|
||||
|
||||
```
|
||||
## Generic Methods
|
||||
You can also create generic methods within non-generic classes. Here's an example:
|
||||
```java
|
||||
public class Util {
|
||||
public <E> void printArray(E[] array) {
|
||||
for (E element : array) {
|
||||
System.out.print(element + " ");
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
```
|
||||
You can use this method with different types:
|
||||
```java
|
||||
Integer[] intArray = {1, 2, 3, 4, 5};
|
||||
String[] stringArray = {"apple", "banana", "orange"};
|
||||
|
||||
Util util = new Util();
|
||||
util.printArray(intArray); // Output: 1 2 3 4 5
|
||||
util.printArray(stringArray); // Output: apple banana orange
|
||||
```
|
||||
|
||||
## Wildcard in Generics
|
||||
The wildcard (?) is used to represent an unknown type. Let's see an example.
|
||||
```java
|
||||
public class Printer {
|
||||
public static void printList(List<?> list) {
|
||||
for (Object item : list) {
|
||||
System.out.print(item + " ");
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
```
|
||||
You can use this method with lists of different types:
|
||||
```java
|
||||
List<Integer> intList = Arrays.asList(1, 2, 3);
|
||||
List<String> stringList = Arrays.asList("apple", "banana", "orange");
|
||||
|
||||
Printer.printList(intList); // Output: 1 2 3
|
||||
Printer.printList(stringList); // Output: apple banana orange
|
||||
```
|
||||
|
||||
## Bounded Generics
|
||||
Remember that type parameters can be bounded. Bounded means “restricted,” and we can restrict the types that a method accepts.
|
||||
|
||||
For example, we can specify that a method accepts a type and all its subclasses (upper bound) or a type and all its superclasses (lower bound).
|
||||
|
||||
Type bounds restrict the types that can be used as arguments in a generic class or method. You can use extends or super to set upper or lower bounds.
|
||||
|
||||
```java
|
||||
public class NumberBox<T extends Number> {
|
||||
private T content;
|
||||
|
||||
public void addContent(T content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public T getContent() {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
```
|
||||
In this example, T must be a subclass of Number.
|
||||
|
||||
|
||||
To declare an upper-bounded type, we use the keyword extends after the type, followed by the upper bound that we want to use:
|
||||
```java
|
||||
public <T extends Number> List<T> fromArrayToList(T[] a) {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
We use the keyword extends here to mean that the type T extends the upper bound in case of a class or implements an upper bound in case of an interface.
|
||||
|
||||
There are two types of wildcards: `? extends T` and `? super T`. The former is for upper-bounded wildcards, and the latter is for lower-bounded wildcards.
|
||||
|
||||
Consider this example:
|
||||
```java
|
||||
public static void paintAllBuildings(List<Building> buildings) {
|
||||
buildings.forEach(Building::paint);
|
||||
}
|
||||
```
|
||||
|
||||
If we imagine a subtype of Building, such as a House, we can’t use this method with a list of House, even though House is a subtype of Building.
|
||||
|
||||
If we need to use this method with type Building and all its subtypes, the bounded wildcard can do the magic:
|
||||
```java
|
||||
public static void paintAllBuildings(List<? extends Building> buildings) {
|
||||
...
|
||||
}
|
||||
```
|
||||
Now this method will work with type Building and all its subtypes. This is called an upper-bounded wildcard, where type Building is the upper bound.
|
||||
|
||||
We can also specify wildcards with a lower bound, where the unknown type has to be a supertype of the specified type. Lower bounds can be specified using the super keyword followed by the specific type. For example, <? super T> means unknown type that is a superclass of T (= T and all its parents).
|
||||
|
||||
|
||||
## Generic Interfaces
|
||||
|
||||
Interfaces can also be generic. For example:
|
||||
|
||||
```java
|
||||
public interface Pair<K, V> {
|
||||
K getKey();
|
||||
V getValue();
|
||||
}
|
||||
```
|
||||
You can implement this interface with different types:
|
||||
|
||||
```java
|
||||
public class OrderedPair<K, V> implements Pair<K, V> {
|
||||
private K key;
|
||||
private V value;
|
||||
|
||||
public OrderedPair(K key, V value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public K getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Additional Concepts
|
||||
### Type Erasure
|
||||
Type erasure is a feature in Java generics where the type parameters used in generic code are removed (or erased) during compilation. This means that the generic type information is not available at runtime, and the generic types are replaced with their upper bounds or Object type.
|
||||
|
||||
#### How Type Erasure Works:
|
||||
##### 1. Compilation Phase:
|
||||
|
||||
During the compilation phase, Java generics are type-checked to ensure type safety.
|
||||
The compiler replaces all generic types with their upper bounds or with Object if no bound is specified.
|
||||
|
||||
##### 2. Type Erasure:
|
||||
|
||||
The compiler removes all generic type information and replaces it with casting or Object.
|
||||
This process is known as type erasure, and it allows Java to maintain backward compatibility with non-generic code.
|
||||
Example:
|
||||
Consider the following generic class:
|
||||
|
||||
```java
|
||||
public class Box<T> {
|
||||
private T content;
|
||||
|
||||
public void setContent(T content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public T getContent() {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
```
|
||||
After compilation, the generic type T is replaced with Object:
|
||||
```java
|
||||
public class Box {
|
||||
private Object content;
|
||||
|
||||
public void setContent(Object content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public Object getContent() {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
```
|
||||
#### Implications of Type Erasure:
|
||||
1. Loss of Type Information at Runtime:
|
||||
|
||||
Type information about generic types is not available at runtime due to type erasure.
|
||||
For example, you can't determine the actual type parameter used for a generic class or method at runtime.
|
||||
|
||||
2. Bridge Methods:
|
||||
|
||||
When dealing with generic methods in classes or interfaces, the compiler generates bridge methods to maintain compatibility with pre-generics code.
|
||||
3. Arrays and Generics:
|
||||
|
||||
Due to type erasure, arrays of generic types are not allowed. You can't create an array of a generic type like T[] array = new T[5];.
|
||||
|
||||
4. Casting and Unchecked Warnings:
|
||||
|
||||
Type casts may be necessary when working with generic types, and this can lead to unchecked warnings. For example, when casting to a generic type, the compiler issues a warning because it can't verify the type at runtime.
|
||||
```java
|
||||
Box<Integer> integerBox = new Box<>();
|
||||
integerBox.setContent(42);
|
||||
|
||||
// Warning: Unchecked cast
|
||||
int value = (Integer) integerBox.getContent();
|
||||
```
|
||||
#### Summary
|
||||
Type erasure is a mechanism in Java generics that removes generic type information during compilation to maintain compatibility with non-generic code. While this approach allows for seamless integration with existing code, it also means that certain generic type information is not available at runtime. Developers need to be aware of the implications of type erasure, such as potential unchecked warnings and limitations on working with arrays of generic types.
|
||||
-- End --
|
||||
|
||||
|
287
Non-DSA Notes/LLD1 Notes/Advanced Java 02 - Collections.md
Normal file
287
Non-DSA Notes/LLD1 Notes/Advanced Java 02 - Collections.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# Adv Java 02 - Collections
|
||||
----
|
||||
## Agenda
|
||||
- Intro to Collections
|
||||
- Common Collection Interfaces
|
||||
- List Interface
|
||||
- Queue Interface
|
||||
- Set Interface
|
||||
- Map Interface
|
||||
|
||||
- Intro to Iterators
|
||||
- Using Iterators
|
||||
- Iterator Methods
|
||||
|
||||
- Additonal Concepts
|
||||
- Using Custom Object as Key with Hashmap etc
|
||||
|
||||
## Introduction
|
||||
Java Collections Framework provides a set of interfaces and classes to store and manipulate groups of objects. Collections make it easier to work with groups of objects, such as lists, sets, and maps. In this beginner-friendly tutorial, we'll explore the basics of Java Collections and how to use iterators to traverse through them.
|
||||
|
||||
### 1. Introduction to Java Collections
|
||||
Java Collections provide a unified architecture for representing and manipulating groups of objects. The Collections Framework includes interfaces, implementations, and algorithms that simplify the handling of groups of objects.
|
||||
|
||||
[Collection PlayList - Video Tutorial](https://drive.google.com/drive/folders/1lLcfZzmKSa0bq_1--OOya0Xk7-y5A9SN?usp=drive_link)
|
||||
|
||||
### 2. Common Collection Interfaces
|
||||
There are several core interfaces in the Collections Framework:
|
||||
|
||||

|
||||
|
||||
**Collection:** The root interface for all collections. It represents a group of objects, and its subinterfaces include List, Set, and Queue.
|
||||
|
||||
**List:** An ordered collection that allows duplicate elements. Implementations include ArrayList, LinkedList, and Vector.
|
||||
|
||||
**Queue:**: The Queue interface in Java is part of the Java Collections Framework and extends the Collection interface. Queues typically, but do not necessarily, order elements in a FIFO (first-in-first-out) manner. Among the exceptions are priority queues, which order elements according to a supplied comparator, or the elements' natural ordering. Implementations include ArrayDeque, LinkedList, PriorityQueue etc.
|
||||
|
||||
**Set:** An unordered collection that does not allow duplicate elements. Implementations include HashSet, LinkedHashSet, and TreeSet.
|
||||
|
||||
**Map:** A collection that maps keys to values. Implementations include HashMap, LinkedHashMap, TreeMap, and Hashtable.
|
||||
|
||||
### Example-1 List Interface and ArrayList
|
||||
The List interface extends the Collection interface and represents an ordered collection of elements. One of the common implementations is ArrayList. Let's see a simple example:
|
||||
|
||||
```java
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ListExample {
|
||||
public static void main(String[] args) {
|
||||
List<String> myList = new ArrayList<>();
|
||||
myList.add("Java");
|
||||
myList.add("Python");
|
||||
myList.add("C++");
|
||||
|
||||
System.out.println("List elements: " + myList);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example-2 Set Interface and HashSet
|
||||
The Set interface represents an unordered collection of unique elements. One of the common implementations is HashSet. Here's a simple example:
|
||||
|
||||
```java
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class SetExample {
|
||||
public static void main(String[] args) {
|
||||
Set<String> mySet = new HashSet<>();
|
||||
mySet.add("Apple");
|
||||
mySet.add("Banana");
|
||||
mySet.add("Orange");
|
||||
|
||||
System.out.println("Set elements: " + mySet);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example-3 Map Interface and HashMap
|
||||
The Map interface represents a collection of key-value pairs. One of the common implementations is HashMap. Let's see an example:
|
||||
|
||||
```java
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class MapExample {
|
||||
public static void main(String[] args) {
|
||||
Map<String, Integer> myMap = new HashMap<>();
|
||||
myMap.put("Java", 20);
|
||||
myMap.put("Python", 15);
|
||||
myMap.put("C++", 10);
|
||||
|
||||
System.out.println("Map elements: " + myMap);
|
||||
}
|
||||
}
|
||||
```
|
||||
## Introduction to Iterators
|
||||
An iterator is an interface that provides a way to access elements of a collection one at a time. The Iterator interface includes methods for iterating over a collection and retrieving elements.
|
||||
|
||||
Let's see how to use iterators with a simple example using a List:
|
||||
### Example - 1 List Iterator
|
||||
```java
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class IteratorExample {
|
||||
public static void main(String[] args) {
|
||||
List<String> myList = new ArrayList<>();
|
||||
myList.add("Java");
|
||||
myList.add("Python");
|
||||
myList.add("C++");
|
||||
|
||||
// Getting an iterator
|
||||
Iterator<String> iterator = myList.iterator();
|
||||
|
||||
// Iterating through the elements
|
||||
while (iterator.hasNext()) {
|
||||
String element = iterator.next();
|
||||
System.out.println("Element: " + element);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example-2 Iterating Over Priority Queue
|
||||
```java
|
||||
public class PriorityQueueIteratorExample {
|
||||
public static void main(String[] args) {
|
||||
// Creating a PriorityQueue with Integer elements
|
||||
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
|
||||
|
||||
// Adding elements to the PriorityQueue
|
||||
priorityQueue.offer(30);
|
||||
priorityQueue.offer(10);
|
||||
priorityQueue.offer(20);
|
||||
|
||||
// Using Iterator to iterate over elements in PriorityQueue
|
||||
System.out.println("Elements in PriorityQueue using Iterator:");
|
||||
|
||||
Iterator<Integer> iterator = priorityQueue.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
System.out.println(iterator.next());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
n this example, we create a PriorityQueue of integers and add three elements to it. We then use an Iterator to iterate over the elements and print them.
|
||||
|
||||
Keep in mind that when using a PriorityQueue, the order of retrieval is based on the natural order (if the elements are comparable) or a provided comparator. The element with the highest priority comes out first.
|
||||
|
||||
It's important to note that the iterator does not guarantee any specific order when iterating over the elements of a PriorityQueue.
|
||||
|
||||
#### Iterator Methods
|
||||
The Iterator interface provides several methods, including:
|
||||
|
||||
- hasNext(): Returns true if the iteration has more elements.
|
||||
- next(): Returns the next element in the iteration.
|
||||
- remove(): Removes the last element returned by next() from the underlying collection (optional operation).
|
||||
|
||||
Here's an example demonstrating the use of these methods:
|
||||
|
||||
```java
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class IteratorMethodsExample {
|
||||
public static void main(String[] args) {
|
||||
List<Integer> numbers = new ArrayList<>();
|
||||
numbers.add(1);
|
||||
numbers.add(2);
|
||||
numbers.add(3);
|
||||
|
||||
Iterator<Integer> iterator = numbers.iterator();
|
||||
|
||||
// Using hasNext() and next() methods
|
||||
while (iterator.hasNext()) {
|
||||
Integer number = iterator.next();
|
||||
System.out.println("Number: " + number);
|
||||
|
||||
// Using remove() method (optional operation)
|
||||
iterator.remove();
|
||||
}
|
||||
|
||||
System.out.println("Updated List: " + numbers);
|
||||
}
|
||||
}
|
||||
```
|
||||
## Additional Concepts
|
||||
|
||||
### Hashmap with Custom Objects
|
||||
Using a HashMap with custom objects in Java involves a few steps. Let's go through the process step by step. Suppose you have a custom object called Person with attributes like id, name, and age.
|
||||
|
||||
- Step 1: Create the Custom Object
|
||||
```java
|
||||
public class Person {
|
||||
private int id;
|
||||
private String name;
|
||||
private int age;
|
||||
|
||||
public Person(int id, String name, int age) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
// Getters and setters (not shown for brevity)
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Person{id=" + id + ", name='" + name + "', age=" + age + '}';
|
||||
}
|
||||
}
|
||||
```
|
||||
- Step 2: Use Person as a Key in HashMap
|
||||
Now, you can use Person objects as keys in a HashMap. For example:
|
||||
|
||||
```java
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class HashMapExample {
|
||||
public static void main(String[] args) {
|
||||
// Create a HashMap with Person objects as keys
|
||||
Map<Person, String> personMap = new HashMap<>();
|
||||
|
||||
// Add entries
|
||||
Person person1 = new Person(1, "Alice", 25);
|
||||
Person person2 = new Person(2, "Bob", 30);
|
||||
|
||||
personMap.put(person1, "Employee");
|
||||
personMap.put(person2, "Manager");
|
||||
|
||||
// Retrieve values using Person objects as keys
|
||||
Person keyToLookup = new Person(1, "Alice", 25);
|
||||
String position = personMap.get(keyToLookup);
|
||||
|
||||
System.out.println("Position for " + keyToLookup + ": " + position);
|
||||
}
|
||||
}
|
||||
```
|
||||
In this example, Person objects are used as keys, and the associated values represent their positions. Note that for keys to work correctly in a HashMap, the custom class (Person in this case) should override the hashCode() and equals() methods.
|
||||
|
||||
- Step 3: Override hashCode() and equals()
|
||||
|
||||
```java
|
||||
public class Person {
|
||||
// ... existing code
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, name, age);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null || getClass() != obj.getClass()) return false;
|
||||
|
||||
Person person = (Person) obj;
|
||||
|
||||
return id == person.id && age == person.age && Objects.equals(name, person.name);
|
||||
}
|
||||
}
|
||||
```
|
||||
By overriding these methods, you ensure that the HashMap correctly handles collisions and identifies when two Person objects are considered equal.
|
||||
|
||||
**Important Considerations**
|
||||
|
||||
**Immutability:**
|
||||
It's often a good practice to make the custom objects used as keys immutable. This helps in maintaining the integrity of the HashMap because the keys should not be modified after being used.
|
||||
|
||||
**Consistent hashCode():**
|
||||
|
||||
Ensure that the hashCode() method returns the same value for two objects that are considered equal according to the equals() method. This ensures proper functioning of the HashMap.
|
||||
|
||||
**Performance:**
|
||||
Consider the performance implications when using complex objects as keys. If the hashCode() and equals() methods are computationally expensive, it might affect the performance of the HashMap.
|
||||
By following these steps and considerations, you can effectively use custom objects as keys in a HashMap in Java.
|
||||
|
||||
### Summary
|
||||
Java Collections and Iterators are fundamental concepts for handling groups of objects efficiently. Understanding the different collection interfaces, implementing classes, and utilizing iterators will empower you to work with collections effectively in your Java applications. Practice and explore the various methods available in the Collections Framework to enhance your programming skills.
|
||||
|
||||
-- End --
|
||||
|
||||
|
587
Non-DSA Notes/LLD1 Notes/Advanced Java 03- Lambdas & Streams.md
Normal file
587
Non-DSA Notes/LLD1 Notes/Advanced Java 03- Lambdas & Streams.md
Normal file
@@ -0,0 +1,587 @@
|
||||
# Adv Java 03 - Lambdas & Streams
|
||||
----
|
||||
## Agenda
|
||||
- Key Terms
|
||||
- Lambdas
|
||||
- Streams
|
||||
- Functional Interfaces
|
||||
|
||||
- Lambdas Expressions
|
||||
- Motivation
|
||||
- Examples
|
||||
- Runnable
|
||||
- Addition
|
||||
- Passing Lambdas as Arguments
|
||||
- Lambdas in Collections
|
||||
- Sorting Example
|
||||
|
||||
- Streams
|
||||
- Basics
|
||||
- Creation
|
||||
- Intermediate Operations
|
||||
- Filtering, Mapping, Sorting etc.
|
||||
- Terminal Operations
|
||||
- Iterating, Reducing, Collecting etc
|
||||
- Examples
|
||||
- Advantages of Streams
|
||||
- Sequential Streams & Parallel Streams
|
||||
|
||||
|
||||
- Additional Reading
|
||||
- Collect Method()
|
||||
- Collectors Interface
|
||||
|
||||
---
|
||||
## Key Terms
|
||||
**Lambdas**
|
||||
```A lambda expression is a block of code that gets passed around, like an anonymous method. It is a way to pass behavior as an argument to a method invocation and to define a method without a name.```
|
||||
|
||||
**Streams**
|
||||
```A stream is a sequence of data. It is a way to write code that is more declarative and less imperative to process collections of objects.```
|
||||
|
||||
**Functional Interfaces**
|
||||
```A functional interface is an interface that contains one and only one abstract method. It is a way to define a contract for behavior as an argument to a method invocation```
|
||||
---
|
||||
|
||||
|
||||
## Lambda Expressions
|
||||
Lambda expressions, also known as anonymous functions, provide a way to create concise and expressive code by allowing the definition of a function in a more compact form.
|
||||
|
||||
The basic syntax of a lambda expression consists of the parameter list, the arrow (->), and the body. The body can be either an expression or a block of statements.
|
||||
```java
|
||||
(parameters) -> expression
|
||||
(parameters) -> { statements }
|
||||
```
|
||||
|
||||
**Parameter List:** This represents the parameters passed to the lambda expression. It can be empty or contain one or more parameters enclosed in parentheses. If there's only one parameter and its type is inferred, you can omit the parentheses.
|
||||
|
||||
**Arrow Operator (->):** This separates the parameter list from the body of the lambda expression.
|
||||
|
||||
**Lambda Body:** This contains the code that makes up the implementation of the abstract method of the functional interface. The body can be a single expression or a block of code enclosed in curly braces.
|
||||
|
||||
Lambda expressions are most commonly used with functional interfaces, which are interfaces containing only one abstract method. Java 8 introduced the @FunctionalInterface annotation to mark such interfaces.
|
||||
|
||||
```
|
||||
@FunctionalInterface
|
||||
interface MyFunctionalInterface {
|
||||
void myMethod();
|
||||
}
|
||||
```
|
||||
|
||||
### Examples
|
||||
Let's start with some simple examples to illustrate the basic syntax:
|
||||
|
||||
#### 1. Hello World Runnable
|
||||
To understand, the motivation behind lambdas, remember how we create a thread in Java. We create a class that implements the Runnable interface and override the run() method. Then we create a new instance of the class and pass it to the Thread constructor.
|
||||
```java
|
||||
// Traditional approach
|
||||
Runnable traditionalRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println("Hello, World!");
|
||||
}
|
||||
};
|
||||
```
|
||||
This is a lot of code to write just to print a simple message. Here, the Runnable interface is a functional interface. It contains only one abstract method, run(). An interface with a single abstract method (SAM) is called a functional interface. Such interfaces can be implemented using lambdas.
|
||||
|
||||
Using Lambda Expression.
|
||||
```java
|
||||
// Lambda expression
|
||||
Runnable lambdaRunnable = () -> System.out.println("Hello, World!");
|
||||
```
|
||||
|
||||
#### 2. Add Numbers
|
||||
```java
|
||||
// Traditional approach
|
||||
MathOperation traditionalAddition = new MathOperation() {
|
||||
@Override
|
||||
public int operate(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Using Lambda expression
|
||||
```java
|
||||
MathOperation lambdaAddition = (a, b) -> a + b;
|
||||
```
|
||||
#### 3. Lambda Expressions with Parameters
|
||||
Lambda expressions can take parameters, making them versatile for various use cases.
|
||||
```java
|
||||
NumberChecker traditionalChecker = new NumberChecker() {
|
||||
@Override
|
||||
public boolean check(int number) {
|
||||
return number % 2 == 0;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
Using Lambda expression
|
||||
```java
|
||||
NumberChecker lambdaChecker = number -> number % 2 == 0;
|
||||
```
|
||||
|
||||
|
||||
### 4. Lambda Expressions in Collections
|
||||
Lambda expressions are commonly used with collections for concise iteration and processing.
|
||||
|
||||
Filtering a List Example (Traditonal Approach)
|
||||
```java
|
||||
List<String> fruits = Arrays.asList("Apple", "Banana", "Orange", "Mango");
|
||||
|
||||
// Traditional approach
|
||||
List<String> filteredTraditional = new ArrayList<>();
|
||||
for (String fruit : fruits) {
|
||||
if (fruit.startsWith("A")) {
|
||||
filteredTraditional.add(fruit);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Using Lambda expression & Java Stream API
|
||||
```java
|
||||
List<String> filteredLambda = fruits.stream()
|
||||
.filter(fruit -> fruit.startsWith("A"))
|
||||
.collect(Collectors.toList());
|
||||
```
|
||||
|
||||
### 4. Sorting Example
|
||||
Method references provide a shorthand notation for lambda expressions, making the code even more concise.
|
||||
```java
|
||||
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
|
||||
|
||||
// Lambda expression for sorting
|
||||
Collections.sort(names, (a, b) -> a.compareTo(b));
|
||||
|
||||
// Method reference for sorting
|
||||
Collections.sort(names, String::compareTo);
|
||||
```
|
||||
|
||||
## Java 8 Streams
|
||||
### Streams
|
||||
A stream in Java is simply a wrapper around a data source, allowing us to perform bulk operations on the data in a convenient way. The Java Stream API, introduced in Java 8, is a powerful abstraction for processing sequences of elements, such as collections or arrays, in a functional and declarative way.
|
||||
|
||||
Streams are designed to be used in a chain of operations, allowing you to create complex data processing pipelines.
|
||||
|
||||
In this tutorial we will learn about Sequential Streams, Parallel Streams and Collect() Method of stream.
|
||||
|
||||
## 1. Creating Streams
|
||||
**Example 1: Creating a Stream from a Collection**
|
||||
```java
|
||||
List<String> fruits = Arrays.asList("Apple", "Banana", "Orange", "Mango");
|
||||
|
||||
// Creating a stream from a collection
|
||||
Stream<String> fruitStream = fruits.stream();
|
||||
```
|
||||
|
||||
**Example 2: Creating a Stream from an Array**
|
||||
```java
|
||||
String[] cities = {"New York", "London", "Tokyo", "Paris"};
|
||||
|
||||
// Creating a stream from an array
|
||||
Stream<String> cityStream = Arrays.stream(cities);
|
||||
```
|
||||
|
||||
|
||||
**Example 3: Creating a Stream of Integers**
|
||||
```java
|
||||
IntStream intStream = IntStream.rangeClosed(1, 5);
|
||||
// Creating a stream of integers
|
||||
intStream.forEach(System.out::println); // Output: 1 2 3 4 5
|
||||
```
|
||||
### 2. Intermediate Operations
|
||||
Intermediate operations are operations that transform a stream into another stream. They are lazy, meaning they don't execute until a terminal operation is invoked. There are two types of operations that you can perform on a stream:
|
||||
Some examples of intermediate operations are filter(), map(), sorted(), distinct(), limit(), and skip().
|
||||
|
||||
Filtering and Mapping Example:
|
||||
```java
|
||||
List<String> fruits = Arrays.asList("Apple", "Banana", "Orange", "Mango");
|
||||
|
||||
// Filtering fruits starting with 'A' and converting to uppercase
|
||||
List<String> result = fruits.stream()
|
||||
.filter(fruit -> fruit.startsWith("A"))
|
||||
.map(String::toUpperCase)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
System.out.println(result); // Output: [APPLE]
|
||||
```
|
||||
**Filtering**
|
||||
The filter() method is used to filter elements from a stream based on a predicate. It takes a predicate as an argument and returns a stream that contains only those elements that match the predicate. For example, let's filter out the even numbers from a stream of numbers:
|
||||
|
||||
```java
|
||||
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
|
||||
Stream<Integer> evenNumbers = stream.filter(number -> number % 2 == 0);
|
||||
```
|
||||
Here, we have created a stream of numbers and filtered out the even numbers from the stream. The `filter()` method takes a predicate as an argument. A predicate is a functional interface that takes an argument and returns a boolean result. It is defined in the java.util.function package. It contains the test() method that takes an argument of type T and returns a boolean result. For example, let's create a
|
||||
predicate that checks if a number is even:
|
||||
```java
|
||||
Predicate<Integer> isEven = number -> number % 2 == 0;
|
||||
```
|
||||
Here, we have created a predicate called isEven that checks if a number is even. We can use this predicate to filter out the even numbers from a stream of numbers as follows:
|
||||
```java
|
||||
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
|
||||
Stream<Integer> evenNumbers = stream.filter(isEven);
|
||||
```
|
||||
**Mapping**
|
||||
The map() method is used to transform elements in a stream. It takes a function as an argument and returns a stream that contains the results of applying the function to each element in the stream. For
|
||||
example, let's convert a stream of numbers to a stream of their squares:
|
||||
```java
|
||||
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
|
||||
Stream<Integer> squares = stream.map(number -> number * number);
|
||||
```
|
||||
Here, we have created a stream of numbers and converted it to a stream of their squares. The map() method takes a function as an argument. A function is a functional interface that takes an argument and returns a result. It is defined in the java.util.function package. It contains the apply() method that takes an argument of type T and returns a result of type R. For example, let's create a function that converts
|
||||
a number to its square:
|
||||
```java
|
||||
Function<Integer, Integer> square = number -> number * number;
|
||||
```
|
||||
Here, we have created a function called square that converts a number to its square. We can use this function to convert a stream of numbers to a stream of their squares as follows:
|
||||
```java
|
||||
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
|
||||
Stream<Integer> squares = stream.map(square);
|
||||
```
|
||||
|
||||
**Sorting**
|
||||
The sorted() method is used to sort elements in a stream. It takes a comparator as an argument and returns a stream that contains the elements sorted according to the comparator. For example, let's sort a stream of numbers in ascending order:
|
||||
```java
|
||||
Stream<Integer> stream = Stream.of(5, 3, 1, 4, 2);
|
||||
Stream<Integer> sortedNumbers = stream.sorted();
|
||||
```
|
||||
Here, we have created a stream of numbers and sorted it in ascending order. The sorted() method takes a comparator as an argument. A comparator is a functional interface that compares two objects of the same type. It is defined in the `java.util.function package`. It contains the compare() method that takes two arguments of type T and returns an integer result. For example, let's create a comparator that
|
||||
compares two numbers:
|
||||
```java
|
||||
Comparator<Integer> comparator = (number1, number2) -> number1 - number2;
|
||||
```
|
||||
Here, we have created a comparator called comparator that compares two numbers. We can use this comparator to sort a stream of numbers in ascending order as follows:
|
||||
```java
|
||||
Stream<Integer> stream = Stream.of(5, 3, 1, 4, 2);
|
||||
Stream<Integer> sortedNumbers = stream.sorted(comparator);
|
||||
```
|
||||
|
||||
### 3. Terminal operations
|
||||
Terminal operations trigger the processing of elements and produce a result or a side effect. They are the final step in a stream pipeline. They are eager, which means that they are executed immediately. Some examples of terminal operations are
|
||||
forEach(), count(), collect(), reduce(), min(), max(), anyMatch(), allMatch(), and
|
||||
noneMatch().
|
||||
|
||||
**Iterating**
|
||||
The forEach() method is used to iterate over the elements in a stream. It takes a consumer as an argument and invokes the consumer for each element in the stream. For example, let's iterate over a stream of numbers and print each number:
|
||||
```java
|
||||
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
|
||||
stream.forEach(number -> System.out.println(number))
|
||||
```
|
||||
|
||||
**Reducing**
|
||||
The reduce() method is used to reduce the elements in a stream to a single value. It takes an identity value and a binary operator as arguments and returns the result of applying the binary operator to the identity value and the elements in the stream. For example, let's find the sum of all the numbers in a stream:
|
||||
```java
|
||||
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
|
||||
int sum = stream.reduce(0, (number1, number2) -> number1 + number2);
|
||||
```
|
||||
Here, we have created a stream of numbers and found the sum of all the numbers in the stream. The reduce() method takes an identity value and a binary operator as arguments. A binary operator is a functional interface that takes two arguments of the same type and returns a result of the same type. It is defined in the java.util.function package. It contains the apply() method that takes two arguments of type T and returns a result of type T. For example, let's create a binary operator that adds two numbers:
|
||||
```java
|
||||
BinaryOperator<Integer> add = (number1, number2) -> number1 + number2;
|
||||
```
|
||||
Here, we have created a binary operator called add that adds two numbers. We can use this binary operator to find the sum of all the numbers in a stream as follows:
|
||||
```java
|
||||
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
|
||||
int sum = stream.reduce(0, add);
|
||||
```
|
||||
|
||||
**Collecting**
|
||||
The collect() method is used to collect the elements in a stream into a collection. It takes a collector as an argument and returns the result of applying the collector to the elements in the stream. For example,let's collect the elements in a stream into a list:
|
||||
```java
|
||||
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
|
||||
List<Integer> numbers = stream.collect(Collectors.toList());
|
||||
```
|
||||
You can now use the toList() method on streams to collect the elements in a stream into a list.
|
||||
```
|
||||
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
|
||||
List<Integer> numbers = stream.toList();
|
||||
```
|
||||
Similarly, you can use the toSet() method on streams to collect the elements in a stream into a set.
|
||||
```java
|
||||
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
|
||||
Set<Integer> numbers = stream.toSet();
|
||||
```
|
||||
**Finding the first element**
|
||||
The findFirst() method is used to find the first element in a stream. It returns an Optional that contains the first element in the stream. For example, let's find the first even number in a stream of numbers:
|
||||
```java
|
||||
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
|
||||
Optional<Integer> firstEvenNumber = stream.filter(number -> number % 2 ==
|
||||
0).findFirst();
|
||||
```
|
||||
|
||||
## More Examples
|
||||
|
||||
**Example1: Collecting into a List**
|
||||
```java
|
||||
List<String> fruits = Arrays.asList("Apple", "Banana", "Orange", "Mango");
|
||||
|
||||
// Collecting filtered fruits into a new list
|
||||
List<String> result = fruits.stream()
|
||||
.filter(fruit -> fruit.length() > 5)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
System.out.println(result); // Output: [Banana, Orange]
|
||||
```
|
||||
**Example2: Counting Elements**
|
||||
```java
|
||||
List<String> fruits = Arrays.asList("Apple", "Banana", "Orange", "Mango");
|
||||
|
||||
// Counting the number of fruits
|
||||
long count = fruits.stream()
|
||||
.filter(fruit -> fruit.length() > 5)
|
||||
.count();
|
||||
|
||||
System.out.println("Number of fruits: " + count); // Output: Number of fruits: 2
|
||||
```
|
||||
|
||||
**Example3: Joining Strings**
|
||||
```java
|
||||
List<String> words = Arrays.asList("Hello", " ", "Stream", " ", "API");
|
||||
|
||||
// Concatenating strings
|
||||
String result = words.stream()
|
||||
.collect(Collectors.joining());
|
||||
|
||||
System.out.println("Concatenated String: " + result); // Output: Concatenated String: Hello
|
||||
```
|
||||
|
||||
### Advantages of Streams
|
||||
The motivation for introducing streams in Java was to provide a more concise, readable, and expressive way to process sequences of data elements, such as collections or arrays. Streams were designed to address several challenges and limitations that traditional imperative programming with loops and conditionals
|
||||
presented:
|
||||
**Readability and Expressiveness:** Traditional loops often involve low-level details like index manipulation and explicit iteration, which can make the code harder to read and understand. Streams provide a higher-level, declarative approach that focuses on expressing the operations you want to perform on the data rather than the mechanics of how to perform them.
|
||||
|
||||
**Code Reduction:** Streams allow you to perform complex operations on data elements in a more concise and compact manner compared to traditional loops. This leads to fewer lines of code and improved code maintainability.
|
||||
|
||||
**Parallelism:** Streams can be easily converted to parallel streams, allowing you to take advantage of multi-core processors and perform operations concurrently. This can lead to improved performance for certain types of data processing tasks.
|
||||
|
||||
**Separation of Concerns:** With traditional loops, you often mix the concerns of iterating over elements, filtering, mapping, and aggregation within a single loop. Streams encourage a separation of concerns by providing distinct operations that can be chained together in a more modular way.
|
||||
|
||||
**Lazy Evaluation:** Streams introduce lazy evaluation, which means that operations are only performed when the results are actually needed. This can lead to improved performance by avoiding unnecessary computations.
|
||||
|
||||
**Functional Programming:** Streams embrace functional programming concepts by providing
|
||||
operations that transform data in a functional and immutable manner. This makes it easier to reason about the behavior of your code and reduces the potential for side effects.
|
||||
|
||||
**Data Abstraction:** Streams abstract away the underlying data source, allowing you to work with different data sources (collections, arrays, I/O channels) in a consistent way. This makes your code more flexible and reusable.
|
||||
|
||||
In summary, the motivation behind introducing streams in Java was to provide a modern, expressive, and functional programming paradigm for processing data elements, enabling developers to write more readable, maintainable, and efficient code. Streams simplify complex data manipulations, encourage separation of concerns, and support parallel processing, contributing to improved code quality and developer productivity.
|
||||
|
||||
|
||||
## Multithreading using Java Streams
|
||||
### Sequential Streams
|
||||
By default, any stream operation in Java is processed sequentially, unless explicitly specified as parallel.
|
||||
|
||||
Sequential streams use a single thread to process the pipeline:
|
||||
```java
|
||||
List<Integer> listOfNumbers = Arrays.asList(1, 2, 3, 4);
|
||||
listOfNumbers.stream().forEach(number ->
|
||||
System.out.println(number + " " + Thread.currentThread().getName())
|
||||
);
|
||||
```
|
||||
The output of this sequential stream is predictable. The list elements will always be printed in an ordered sequence:
|
||||
|
||||
```
|
||||
1 main
|
||||
2 main
|
||||
3 main
|
||||
4 main
|
||||
```
|
||||
|
||||
### Parallel Streams
|
||||
Stream API also simplifies multithreading by providing the `parallelStream()` method that runs operations over stream’s elements in parallel mode. Any stream in Java can easily be transformed from sequential to parallel.
|
||||
|
||||
We can achieve this by adding the parallel method to a sequential stream or by creating a stream using the parallelStream method of a collection:
|
||||
|
||||
The code below allows to run method doWork() in parallel for every element of the stream:
|
||||
```java
|
||||
list.parallelStream().forEach(element -> doWork(element));
|
||||
```
|
||||
For the above sequential example, the code will looks like this -
|
||||
|
||||
```java
|
||||
List<Integer> listOfNumbers = Arrays.asList(1, 2, 3, 4);
|
||||
listOfNumbers.parallelStream().forEach(number ->
|
||||
System.out.println(number + " " + Thread.currentThread().getName())
|
||||
);
|
||||
```
|
||||
Parallel streams enable us to execute code in parallel on separate cores. The final result is the combination of each individual outcome.
|
||||
|
||||
However, the order of execution is out of our control. It may change every time we run the program:
|
||||
```
|
||||
4 ForkJoinPool.commonPool-worker-3
|
||||
2 ForkJoinPool.commonPool-worker-5
|
||||
1 ForkJoinPool.commonPool-worker-7
|
||||
3 main
|
||||
```
|
||||
Parallel streams make use of the fork-join framework and its common pool of worker threads. Parallel processing may be beneficial to fully utilize multiple cores. But we also need to consider the overhead of managing multiple threads, memory locality, splitting the source and merging the results.
|
||||
Refer this [Article](https://www.baeldung.com/java-when-to-use-parallel-stream) to learn more about when to use parallel streams.
|
||||
|
||||
## Additonal Topics
|
||||
### Collect() Method
|
||||
A stream represents a sequence of elements and supports different kinds of operations that lead to the desired result. The source of a stream is usually a Collection or an Array, from which data is streamed from.
|
||||
|
||||
Streams differ from collections in several ways; most notably in that the streams are not a data structure that stores elements. They're functional in nature, and it's worth noting that operations on a stream produce a result and typically return another stream, but do not modify its source.
|
||||
|
||||
To "solidify" the changes, you **collect** the elements of a stream back into a Collection.
|
||||
|
||||
The `stream.collect()` method is used to perform a mutable reduction operation on the elements of a stream. It returns a new mutable object containing the results of the reduction operation.
|
||||
|
||||
This method can be used to perform several different types of reduction operations, such as:
|
||||
|
||||
- Computing the sum of numeric values in a stream.
|
||||
- Finding the minimum or maximum value in a stream.
|
||||
- Constructing a new String by concatenating the contents of a stream.
|
||||
- Collecting elements into a new List or Set.
|
||||
|
||||
```java
|
||||
public class CollectExample {
|
||||
public static void main(String[] args) {
|
||||
Integer[] intArray = {1, 2, 3, 4, 5};
|
||||
|
||||
// Creating a List from an array of elements
|
||||
// using Arrays.asList() method
|
||||
List<Integer> list = Arrays.asList(intArray);
|
||||
|
||||
// Demo1: Collecting all elements of the list into a new
|
||||
// list using collect() method
|
||||
List<Integer> evenNumbersList = list.stream()
|
||||
.filter(i -> i%2 == 0)
|
||||
.collect(toList());
|
||||
System.out.println(evenNumbersList);
|
||||
|
||||
// Demo2: finding the sum of all the values
|
||||
// in the stream
|
||||
Integer sum = list.stream()
|
||||
.collect(summingInt(i -> i));
|
||||
System.out.println(sum);
|
||||
|
||||
// Demo3: finding the maximum of all the values
|
||||
// in the stream
|
||||
Integer max = list.stream()
|
||||
.collect(maxBy(Integer::compare)).get();
|
||||
System.out.println(max);
|
||||
|
||||
// Demo4: finding the minimum of all the values
|
||||
// in the stream
|
||||
Integer min = list.stream()
|
||||
.collect(minBy(Integer::compare)).get();
|
||||
System.out.println(min);
|
||||
|
||||
// Demo5: counting the values in the stream
|
||||
Long count = list.stream()
|
||||
.collect(counting());
|
||||
System.out.println(count);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In Demo1: We use the stream() method to get a stream from the list. We filter the even elements and collect them into a new list using the collect() method.
|
||||
|
||||
In Demo2: We use the collect() method summingInt(ToIntFunction) as an argument. The summingInt() method returns a collector that sums the integer values extracted from the stream elements by applying an int producing mapping function to each element.
|
||||
|
||||
In Demo 3: We use the collect() method with maxBy(Comparator) as an argument. The maxBy() accepts a Comparator and returns a collector that extracts the maximum element from the stream according to the given Comparator.
|
||||
|
||||
Lets learn more about Collectors.
|
||||
|
||||
|
||||
### Collectors Class
|
||||
|
||||
Collectors represent implementations of the Collector interface, which implements various useful reduction operations, such as accumulating elements into collections, summarizing elements based on a specific parameter, etc.
|
||||
|
||||
All predefined implementations can be found within the [Collectors](https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html) class.
|
||||
|
||||
|
||||
Within the Collectors class itself, we find an abundance of unique methods that deliver on the different needs of a user. One such group is made of summing methods - `summingInt()`, `summingDouble()` and `summingLong()`.
|
||||
|
||||
|
||||
|
||||
Let's start off with a basic example with a List of Integers:
|
||||
|
||||
```java
|
||||
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
|
||||
Integer sum = numbers.stream().collect(Collectors.summingInt(Integer::intValue));
|
||||
System.out.println("Sum: " + sum);
|
||||
```
|
||||
We apply the .stream() method to create a stream of Integer instances, after which we use the previously discussed `.collect()` method to collect the elements using `summingInt()`. The method itself, again, accepts the `ToIntFunction`, which can be used to reduce instances to an integer that can be summed.
|
||||
|
||||
Since we're using Integers already, we can simply pass in a method reference denoting their `intValue`, as no further reduction is needed.
|
||||
|
||||
More often than not - you'll be working with lists of custom objects and would like to sum some of their fields. For instance, we can sum the quantities of each product in the productList, denoting the total inventory we have.
|
||||
|
||||
Let us try to understand one of these methods using a custom class example.
|
||||
``` java
|
||||
public class Product {
|
||||
private String name;
|
||||
private Integer quantity;
|
||||
private Double price;
|
||||
private Long productNumber;
|
||||
|
||||
// Constructor, getters and setters
|
||||
...
|
||||
}
|
||||
...
|
||||
List<Product> products = Arrays.asList(
|
||||
new Product("Milk", 37, 3.60, 12345600L),
|
||||
new Product("Carton of Eggs", 50, 1.20, 12378300L),
|
||||
new Product("Olive oil", 28, 37.0, 13412300L),
|
||||
new Product("Peanut butter", 33, 4.19, 15121200L),
|
||||
new Product("Bag of rice", 26, 1.70, 21401265L)
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
In such a case, the we can use a method reference, such as `Product::getQuantity` as our `ToIntFunction`, to reduce the objects into a single integer each, and then sum these integers:
|
||||
|
||||
```java
|
||||
Integer sumOfQuantities = products.stream().collect(Collectors.summingInt(Product::getQuantity));
|
||||
System.out.println("Total number of products: " + sumOfQuantities);
|
||||
```
|
||||
This results in:
|
||||
|
||||
```
|
||||
Total number of products: 174
|
||||
```
|
||||
|
||||
You can also very easily implement your own collector and use it instead of the predefined ones, though - you can get pretty far with the built-in collectors, as they cover the vast majority of cases in which you might want to use them.
|
||||
|
||||
The following are examples of using the predefined collectors to perform common mutable reduction tasks:
|
||||
```java
|
||||
|
||||
// Accumulate names into a List
|
||||
List<String> list = people.stream().map(Person::getName).collect(Collectors.toList());
|
||||
|
||||
// Accumulate names into a TreeSet
|
||||
Set<String> set = people.stream().map(Person::getName).collect(Collectors.toCollection(TreeSet::new));
|
||||
|
||||
// Convert elements to strings and concatenate them, separated by commas
|
||||
String joined = things.stream()
|
||||
.map(Object::toString)
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
// Compute sum of salaries of employee
|
||||
int total = employees.stream()
|
||||
.collect(Collectors.summingInt(Employee::getSalary)));
|
||||
|
||||
// Group employees by department
|
||||
Map<Department, List<Employee>> byDept
|
||||
= employees.stream()
|
||||
.collect(Collectors.groupingBy(Employee::getDepartment));
|
||||
|
||||
// Compute sum of salaries by department
|
||||
Map<Department, Integer> totalByDept
|
||||
= employees.stream()
|
||||
.collect(Collectors.groupingBy(Employee::getDepartment,
|
||||
Collectors.summingInt(Employee::getSalary)));
|
||||
|
||||
// Partition students into passing and failing
|
||||
Map<Boolean, List<Student>> passingFailing =
|
||||
students.stream()
|
||||
.collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
|
||||
|
||||
```
|
||||
You can look at the offical documentation for more details on these methods.
|
||||
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html
|
||||
|
||||
--- End ---
|
||||
|
||||
|
@@ -0,0 +1,296 @@
|
||||
# Adv Java 04 - Exception Handling
|
||||
----
|
||||
Exception handling is a critical aspect of programming in Java. It allows developers to manage and respond to unexpected errors that may occur during program execution. In this tutorial, we'll cover the basics of exception handling in Java for beginners.
|
||||
|
||||
## Agenda
|
||||
- Introduction to Exceptions
|
||||
- Types of Exceptions
|
||||
- Checked Exceptions
|
||||
- Unchecked Exceptions
|
||||
- Handling Exceptions
|
||||
- The try-catch Block
|
||||
- Multiple catch Blocks
|
||||
- The finally Block
|
||||
|
||||
- Throwing Exceptions
|
||||
- Custom Exceptions
|
||||
- Best Practices
|
||||
- for Checked Exceptions
|
||||
- Unchecked Exceptions
|
||||
- Additional Reading
|
||||
- More on Checked & Unchecked Exceptions
|
||||
- Exception Hierarchy in Java
|
||||
|
||||
## 1. Introduction to Exceptions
|
||||
An exception is an event that disrupts the normal flow of a program. When an exceptional situation occurs, an object representing the exception is thrown. Exception handling allows you to catch and handle these exceptions, preventing your program from crashing.
|
||||
|
||||
## 2. Types of Exceptions
|
||||
In Java, exceptions are broadly categorized into two types: checked exceptions and unchecked exceptions.
|
||||
|
||||
### Checked Exceptions
|
||||
These are checked at compile-time, and the programmer is required to handle them explicitly using try-catch blocks or declare them in the method signature using the throws keyword.
|
||||
|
||||
Checked exceptions extend the Exception class (directly or indirectly) but do not extend RuntimeException. They are subject to the compile-time checking by the Java compiler, meaning the compiler ensures that these exceptions are either caught or declared.
|
||||
|
||||
Some common examples of checked exceptions include:
|
||||
|
||||
- IOException
|
||||
- SQLException
|
||||
- ClassNotFoundException
|
||||
- InterruptedException
|
||||
|
||||
Handling checked exceptions involves taking appropriate actions to address the exceptional conditions that may arise during program execution. There are two primary ways to handle checked exceptions: using the try-catch block and the throws clause.
|
||||
|
||||
The try-catch block is used to catch and handle exceptions. When a block of code is placed inside a try block, any exceptions that occur within that block are caught and processed by the corresponding catch block.
|
||||
|
||||
Example
|
||||
```java
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileReader;
|
||||
|
||||
public class FileReaderMethodExample {
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
readFile("example.txt");
|
||||
} catch (FileNotFoundException e) {
|
||||
System.err.println("FileNotFoundException: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Method with a throws clause
|
||||
static void readFile(String fileName) throws FileNotFoundException {
|
||||
FileReader fileReader = new FileReader(fileName);
|
||||
// Code to read from the file
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Unchecked Exceptions
|
||||
These are not checked at compile-time, and they are subclasses of RuntimeException. They usually indicate programming errors, and it's not mandatory to handle them explicitly.
|
||||
Unchecked exceptions also known as runtime exceptions, are exceptions that occur during the execution of a program.
|
||||
|
||||
Unchecked exceptions can occur at runtime due to unexpected conditions, such as division by zero, accessing an array index out of bounds, or trying to cast an object to an incompatible type.
|
||||
|
||||
Some common examples of unchecked exceptions include:
|
||||
|
||||
`ArithmeticException`: Occurs when an arithmetic operation encounters an exceptional condition, such as division by zero.
|
||||
|
||||
`NullPointerException`: Occurs when trying to access a member (field or method) on an object that is null.
|
||||
|
||||
`ArrayIndexOutOfBoundsException`: Occurs when trying to access an array element with an index that is outside the bounds of the array.
|
||||
|
||||
`ClassCastException`: Occurs when attempting to cast an object to a type that is not compatible with its actual type.
|
||||
|
||||
```java
|
||||
public class UncheckedExceptionExample {
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
int result = divide(10, 0); // This may throw an ArithmeticException
|
||||
System.out.println("Result: " + result);
|
||||
} catch (ArithmeticException e) {
|
||||
System.out.println("Error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
static int divide(int a, int b) {
|
||||
return a / b;
|
||||
}
|
||||
}
|
||||
```
|
||||
In this example, the divide method may throw an ArithmeticException if the divisor b is zero. The try-catch block catches the exception and handles it, preventing the program from terminating abruptly.
|
||||
|
||||
|
||||
|
||||
## 3. Handling Exceptions
|
||||
### The try-catch Block
|
||||
The try-catch block is used to handle exceptions. The code that might throw an exception is placed inside the try block, and the code to handle the exception is placed inside the catch block.
|
||||
|
||||
|
||||
```java
|
||||
try {
|
||||
// Code that might throw an exception
|
||||
// ...
|
||||
} catch (ExceptionType e) {
|
||||
// Code to handle the exception
|
||||
// ...
|
||||
}
|
||||
```
|
||||
Example:
|
||||
```java
|
||||
public class ExceptionHandlingExample {
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
int result = divide(10, 0); // This may throw an ArithmeticException
|
||||
System.out.println("Result: " + result);
|
||||
} catch (ArithmeticException e) {
|
||||
System.out.println("Error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
static int divide(int a, int b) {
|
||||
return a / b;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple catch Blocks
|
||||
You can have multiple catch blocks to handle different types of exceptions that may occur within the try block.
|
||||
|
||||
```java
|
||||
try {
|
||||
// Code that might throw an exception
|
||||
// ...
|
||||
} catch (ExceptionType1 e1) {
|
||||
// Code to handle ExceptionType1
|
||||
// ...
|
||||
} catch (ExceptionType2 e2) {
|
||||
// Code to handle ExceptionType2
|
||||
// ...
|
||||
}
|
||||
```
|
||||
Example:
|
||||
```java
|
||||
public class MultipleCatchExample {
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
String str = null;
|
||||
System.out.println(str.length()); // This may throw a NullPointerException
|
||||
} catch (ArithmeticException e) {
|
||||
System.out.println("ArithmeticException: " + e.getMessage());
|
||||
} catch (NullPointerException e) {
|
||||
System.out.println("NullPointerException: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
System.out.println("Generic Exception: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### The finally Block
|
||||
The finally block contains code that will be executed regardless of whether an exception is thrown or not. It is often used for cleanup operations, such as closing resources.
|
||||
|
||||
|
||||
```java
|
||||
try {
|
||||
// Code that might throw an exception
|
||||
// ...
|
||||
} catch (ExceptionType e) {
|
||||
// Code to handle the exception
|
||||
// ...
|
||||
} finally {
|
||||
// Code that will be executed regardless of exceptions
|
||||
// ...
|
||||
}
|
||||
```
|
||||
Example:
|
||||
```java
|
||||
public class FinallyBlockExample {
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
System.out.println("Inside try block");
|
||||
int result = divide(10, 2);
|
||||
System.out.println("Result: " + result);
|
||||
} catch (ArithmeticException e) {
|
||||
System.out.println("ArithmeticException: " + e.getMessage());
|
||||
} finally {
|
||||
System.out.println("Inside finally block");
|
||||
}
|
||||
}
|
||||
|
||||
static int divide(int a, int b) {
|
||||
return a / b;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Throwing Exceptions
|
||||
You can use the throw keyword to explicitly throw an exception in your code. This is useful when you want to signal an exceptional condition.
|
||||
```java
|
||||
public ReturnType methodName() throws ExceptionType1, ExceptionType2 {
|
||||
// Method implementation
|
||||
...
|
||||
if(condition){
|
||||
throw new ExceptionType1("Error message");
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
The `throws` clause is used in a method signature to declare that the method may throw checked exceptions. It informs the caller that the method might encounter certain exceptional conditions, and the caller is responsible for handling these exceptions.
|
||||
|
||||
Example
|
||||
```java
|
||||
public class ThrowExample {
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
validateAge(15); // This may throw an InvalidAgeException
|
||||
} catch (InvalidAgeException e) {
|
||||
System.out.println("Error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
static void validateAge(int age) throws InvalidAgeException {
|
||||
if (age < 18) {
|
||||
throw new InvalidAgeException("Age must be 18 or older");
|
||||
}
|
||||
System.out.println("Valid age");
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidAgeException extends Exception {
|
||||
public InvalidAgeException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
```
|
||||
## 5. Custom Exceptions
|
||||
You can create your own custom exceptions by extending the Exception class or one of its subclasses.
|
||||
|
||||
Example:
|
||||
```java
|
||||
public class CustomExceptionExample {
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
throw new CustomException("Custom exception message");
|
||||
} catch (CustomException e) {
|
||||
System.out.println("Caught custom exception: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CustomException extends Exception {
|
||||
public CustomException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Best Practices
|
||||
- Catch specific exceptions rather than using a generic catch (Exception e) block whenever possible.
|
||||
- Handle exceptions at an appropriate level in your application. Don't catch exceptions if you can't handle them effectively.
|
||||
- Clean up resources (e.g., closing files or database connections) in the finally block.
|
||||
- Log exceptions or relevant information to aid in debugging.
|
||||
|
||||
|
||||
### Best Practices for Checked Exceptions
|
||||
|
||||
- Handle or Declare: Always handle checked exceptions using the try-catch block or declare them in the method signature using the throws clause.
|
||||
|
||||
- Provide Meaningful Messages: When catching or throwing checked exceptions, include meaningful messages to aid in debugging.
|
||||
|
||||
- Close Resources in a finally Block: If a method opens resources (e.g., files or database connections), close them in a finally block to ensure proper resource management.
|
||||
|
||||
### Best Practices for Handling Unchecked Exceptions
|
||||
- Use Defensive Programming: Validate inputs and conditions to avoid common causes of unchecked exceptions.
|
||||
|
||||
- Catch Specific Exceptions: When using a try-catch block, catch specific exceptions rather than using a generic catch (RuntimeException e) block. This allows for more targeted handling.
|
||||
|
||||
- Avoid Suppressing Exceptions: Avoid using empty catch blocks that suppress exceptions without any meaningful action. Log or handle exceptions appropriately.
|
||||
|
||||
- Logging: Consider logging exceptions using logging frameworks (e.g., SLF4J) to record information that can aid in debugging.
|
||||
|
||||
### Conclusion
|
||||
Exception handling is a crucial aspect of Java programming, allowing developers to gracefully handle unexpected errors and improve the robustness of their applications. By understanding the basics of exception handling and following best practices, you can write more resilient and reliable Java code. As you gain experience, you'll become proficient in anticipating and addressing potential issues in your programs
|
||||
|
||||
--- End ---
|
||||
|
||||
|
721
Non-DSA Notes/LLD1 Notes/Concurrency 01 - Processes & Threads.md
Normal file
721
Non-DSA Notes/LLD1 Notes/Concurrency 01 - Processes & Threads.md
Normal file
@@ -0,0 +1,721 @@
|
||||
# Concurrency-1 Introduction to Processes and Threads
|
||||
---
|
||||
In this tutorial, we will cover the following concepts.
|
||||
|
||||
- How Computer Applications Run
|
||||
- Concurrency: Real-World Applications of Threads
|
||||
- Google Docs
|
||||
- Music Player
|
||||
- Adobe Lightroom
|
||||
|
||||
- Processes and Threads
|
||||
- Benefits of Multithreading
|
||||
- Challenges of Multithreading
|
||||
- Concurrent vs Parallel Execution
|
||||
- Multithreading in Java
|
||||
- Thread Creation
|
||||
- Subclass of Thread Class
|
||||
- Using Runnable
|
||||
- Starting a Thread
|
||||
- Problem Statement 1 : Number Printer
|
||||
- Additional Concepts
|
||||
- Commonly used Methods on Threads
|
||||
- Problem Statement 2: Factorial Computation Task
|
||||
- Thread Lifecycle & States
|
||||
|
||||
## Understanding How Computer Applications Run
|
||||
Computer applications are complex systems that run on a computer's operating system, interacting with hardware and software components to perform various tasks. To comprehend how these applications operate efficiently, it's essential to delve into fundamental concepts like processes, threads, CPU scheduling, multithreading, and parallel execution. Let us understand how a program runs.
|
||||
|
||||
### 1. Programs / Processes
|
||||
Programs: These are sets of instructions for the computer. Each application you use, such as a web browser or word processor, is a program.
|
||||
Processes: When you open a program, it becomes a process. A process is an instance of a program in execution.
|
||||
|
||||
### 2. Memory Allocation
|
||||
When you start a program, the operating system (OS) allocates memory to it. This memory contains the program's code, data, and other necessary information.
|
||||
|
||||
### 3. Processor (CPU) Execution
|
||||
The Central Processing Unit (CPU) is the brain of the computer. It fetches instructions from memory and executes them.
|
||||
Each process takes turns using the CPU. The OS manages this by employing a technique called CPU Scheduling.
|
||||
|
||||
### 4. Context Switching
|
||||
The CPU rapidly switches between different processes. This is known as context switching.
|
||||
The OS saves the current state of a process, loads the state of the next process, and hands control to it.
|
||||
|
||||
### 5. Multitasking
|
||||
The ability of a computer to execute multiple processes concurrently is called multitasking.
|
||||
While it may seem like everything is happening at once, the CPU is actually rapidly switching between processes.
|
||||
|
||||
### 6. Parallel Execution
|
||||
In some systems, especially those with multiple processors or cores, true parallel execution can occur. This means multiple processes genuinely run simultaneously.
|
||||
|
||||
### 7. Threads
|
||||
A process can be further divided into threads. Threads within a process share the same resources but can execute independently. Multithreading allows for parallel execution within a single process.
|
||||
|
||||
### 8. Synchronization
|
||||
When multiple processes or threads share resources (like data), synchronization mechanisms are employed to avoid conflicts. This ensures that data remains consistent.We will discuss synchronization in great detail in coming lectures.
|
||||
|
||||
### 9. Task Management by the Operating System
|
||||
The OS keeps track of all running processes and manages their execution.
|
||||
It assigns priorities, allocates resources, and ensures fair access to the CPU. This is done by scheduling algorithms. Some of the popular scheduling algorithms are as follows -
|
||||
|
||||
- First-Come-First-Serve (FCFS): Processes are executed in the order they arrive.
|
||||
- Shortest Job Next (SJN): The process with the shortest execution time is selected.
|
||||
- Round Robin (RR): Each process gets a fixed time slice, then moves to the back of the queue.
|
||||
- Priority Scheduling: Processes are assigned priorities, and the highest priority process is executed first.
|
||||
|
||||
Modern CPUs often employ a mix of static and dynamic scheduling strategies.
|
||||
Advanced techniques, like predictive algorithms, may be used to anticipate the next process to run.
|
||||
|
||||
### 10. Interrupts
|
||||
The CPU can be interrupted to handle external events, like input from a user or data arriving from a network.Interrupts are crucial for maintaining responsiveness in a multitasking environment.
|
||||
### 11. Termination of Processes
|
||||
When a program finishes its task or is closed by the user, the associated process is terminated. The OS reclaims the allocated resources and frees up memory.
|
||||
|
||||
### 12. Efficient Resource Utilization
|
||||
The goal is to efficiently utilize the available resources, ensuring that each running application gets its fair share of CPU time. In summary, the execution of multiple applications or processes involves careful management by the operating system, with the CPU rapidly switching between tasks, allocating resources, and ensuring that everything runs smoothly. Multitasking and, in some cases, parallel execution contribute to the efficiency and responsiveness of modern computer systems.
|
||||
|
||||
|
||||
### Conclusion
|
||||
Understanding how computer applications run involves grasping the intricacies of processes, threads, CPU scheduling, multithreading, and parallel execution. As technology evolves, mastering these concepts becomes increasingly important for developing efficient and responsive applications. Experimenting with these concepts in programming languages and frameworks will deepen your understanding and proficiency in building robust and high-performance software.
|
||||
|
||||
----
|
||||
## Concurrency: Real-World Applications of Threads
|
||||
Concurrent programming, which involves the execution of multiple tasks simultaneously, is a fundamental concept in modern software development. One powerful mechanism for achieving concurrency is the use of threads. Let's explore how threads are employed in real-world applications, focusing on Google Docs, Music Players, and Adobe Lightroom.
|
||||
|
||||
Concurrent programming enables multiple operations to progress in overlapping time intervals. Threads, the smallest units of execution within a process, are instrumental in achieving concurrency. They allow different parts of a program to run concurrently, enhancing efficiency and responsiveness.
|
||||
|
||||
### 1. Google Docs: Collaborative Editing
|
||||
Google Docs exemplifies the power of concurrency through its collaborative editing feature. When multiple users are editing a document simultaneously, threads come into play. Each user's edits are handled by a separate thread, ensuring that changes made by one user do not disrupt the editing experience of others.
|
||||
|
||||
**Threads in Google Docs**
|
||||
- Thread per User: Each user's editing actions are processed by an individual thread.
|
||||
- Conflict Resolution: Threads synchronize to resolve conflicts and merge edits seamlessly.
|
||||
- Auto-Suggest/Auto-complete: A separate thread can run spell check for the words you write.
|
||||
- UI Thread: A separate thread can continuously update UI for the users.
|
||||
|
||||
### 2. Music Players: Smooth Playback and User Interaction
|
||||
In music players like Spotify or iTunes, threads are crucial for delivering a smooth user experience during playback while allowing users to interact with the application concurrently.
|
||||
|
||||
**How Threads Work in Music Players**
|
||||
- Playback Thread: A dedicated thread manages audio playback, ensuring uninterrupted streaming.
|
||||
- User Interface Thread: Another thread handles user interactions, such as browsing playlists or adjusting settings.
|
||||
- Parallel Execution: Threads allow simultaneous playback and user interactions without one affecting the other.
|
||||
|
||||
### 3. Adobe Lightroom: Image Processing
|
||||
In photo editing applications like Adobe Lightroom, where resource-intensive tasks like image processing are common, threads are employed to maintain responsiveness and reduce processing times.
|
||||
|
||||
**How Threads Work in Lightroom**
|
||||
- Image Processing Threads: Multiple threads handle the processing of different parts of an image concurrently.
|
||||
- Background Tasks: Threads enable background tasks like importing photos while allowing users to continue editing.
|
||||
- Responsive UI: Threads ensure that the user interface remains responsive even during computationally intensive operations.
|
||||
|
||||
---
|
||||
|
||||
### Processes and Threads - Deep Dive
|
||||
A **process** is an independent program in execution. It has its own memory space called heap, code, data, and system resources. The heap isn't shared between two applications or two processes, they each have their own. The terms process and application are often used interchangeably. Processes enable multiple tasks to run concurrently, offering isolation and independence.
|
||||
|
||||
|
||||
**Process Lifecycle**
|
||||
- Creation: When a program is launched, it is loaded into memory, a process is created.
|
||||
- Execution: The process runs its instructions.
|
||||
- Termination: The process completes its execution or is terminated.
|
||||
|
||||
|
||||
A **thread** is the smallest unit of execution within a process. Multiple threads can exist within a single process, sharing the same resources like memory but executing independently.
|
||||
|
||||

|
||||
|
||||
|
||||
**Benefits of Multithreading - Why use multiple threads?**
|
||||
- Performance: Threads can execute concurrently, enhancing performance.
|
||||
- Responsiveness: Multithreading allows a program to remain responsive during time-consuming tasks. This is especially helpful in applications with user interfaces.
|
||||
- Efficiency: Exploiting parallelism improves overall system performance.
|
||||
- One of the most common reasons, is to offload long running tasks.
|
||||
Instead of tying up the main thread, we can create additional threads, to execute tasks that might take a long time. This frees up the main thread so that it can continue working, and executing, and being responsive to the user.
|
||||
- You also might use multiple threads to process large amounts of data, which can improve performance, of data intensive operations.
|
||||
- A web server, is another use case for many threads, allowing multiple connections and requests to be handled, simultaneously.
|
||||
- Resource Sharing: Threads within a process share resources, reducing overhead.
|
||||
|
||||
|
||||
**Challenges**
|
||||
- Data Synchronization: Threads may need to synchronize access to shared data to prevent conflicts.
|
||||
- Deadlocks: Concurrent threads might lead to situations where each is waiting for the other to release a resource.
|
||||
|
||||
We will address these challenges in the up-coming classes.
|
||||
|
||||
---
|
||||
## Concurrent Execution vs Parallel Execution
|
||||
- Concurrent execution refers to the ability of a system to execute multiple tasks or processes at the same time, appearing to overlap in time. Concurrent Execution can happen on single core as well.
|
||||
|
||||
- Parallel execution involves the simultaneous execution of multiple tasks or processes using multiple processors or cores. Multiple cores are must for truly parallel execution.
|
||||
|
||||

|
||||
Data Parallelism: Dividing a task into subtasks processed concurrently.
|
||||
Task Parallelism: Assigning multiple independent tasks to separate processors/cores.
|
||||
|
||||
#### Key Differences
|
||||
**Concurrent Execution** - Tasks may overlap in time but not necessarily execute simultaneously.
|
||||
**Parallel Execution** - Tasks are actively running at the same time on separate processors.
|
||||
|
||||
|
||||
#### Resource Utilization
|
||||
**Concurrent Execution** - Utilizes a single processor by interleaving tasks.
|
||||
**Parallel Execution** - Utilizes multiple processors, ensuring more tasks are completed in the same time frame.
|
||||
|
||||
#### Hardware Requirement
|
||||
**Concurrent Execution** - Can occur on a system with a single processor.
|
||||
**Parallel Execution** - Requires multiple processors or cores.
|
||||
|
||||
#### Example
|
||||
**Concurrent Execution** - Multiple applications running on a single-core processor.
|
||||
**Parallel Execution** - Image processing tasks being performed simultaneously on different cores of a multi-core processor.
|
||||
|
||||
---
|
||||
|
||||
## Multithreading in Java
|
||||
|
||||
In the Java, multithreading is driven by the core concept of a Thread. There are two ways to create Threads in Java.
|
||||
|
||||
**Thread Class**: Java provides the Thread class, which serves as the foundation for creating and managing threads.
|
||||
|
||||
**Runnable Interface**: The Runnable interface is often implemented to define the code that a thread will execute.
|
||||
|
||||
Lets write some logic that runs in a parallel thread by using the Thread framework. In the below code example we are creating two threads and running them concurrently.
|
||||
|
||||
**Way-1 : Subclassing a Thread Class**
|
||||
```java
|
||||
public class NewThread extends Thread {
|
||||
public void run() {
|
||||
// business logic
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Class to initialize and start our thread.
|
||||
|
||||
```java
|
||||
public class MultipleThreadsExample {
|
||||
public static void main(String[] args) {
|
||||
NewThread t1 = new NewThread();
|
||||
t1.setName("MyThread-1");
|
||||
NewThread t2 = new NewThread();
|
||||
t2.setName("MyThread-2");
|
||||
t1.start();
|
||||
t2.start();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Way -2 : Using Runnable (Preferred Way)**
|
||||
```java
|
||||
class SimpleRunnable implements Runnable {
|
||||
public void run() {
|
||||
// business logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
Thread t = new Thread(new SimpleRunnable());
|
||||
t.start();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The above SimpleRunnable is just a task which we want to run in a separate thread.
|
||||
There’re various approaches we can use for running it; one of them is to use the Thread class.
|
||||
|
||||
Simply put, we generally encourage the use of Runnable over Thread:
|
||||
When extending the Thread class, we’re not overriding any of its methods. Instead, we override the method of Runnable (which Thread happens to implement).
|
||||
- This is a clear violation of IS-A Thread principle
|
||||
- Creating an implementation of Runnable and passing it to the Thread class utilizes composition and not inheritance – which is more flexible
|
||||
- After extending the Thread class, we can’t extend any other class
|
||||
- From Java 8 onwards, Runnables can be represented as lambda expressions
|
||||
|
||||
```java
|
||||
public class ThreadWithLambdaExample {
|
||||
public static void main(String[] args) {
|
||||
// Creating a thread with a Runnable implemented as a lambda expression
|
||||
Thread myThread = new Thread(() -> {
|
||||
System.out.println(Thread.currentThread().getName());
|
||||
...
|
||||
}
|
||||
});
|
||||
|
||||
// Starting the thread
|
||||
myThread.start();
|
||||
}
|
||||
}
|
||||
```
|
||||
We create a new Thread and pass a Runnable as a lambda expression directly to its constructor. The lambda expression defines the code to be executed in the new thread. In this case, it's a simple prints message that prints the name of Thread.
|
||||
|
||||
|
||||
## Starting a Thread - Behind the Scenes
|
||||
When you call `thread.start()` in Java, it initiates the execution of the thread and invokes the run method of the thread. Here's a step-by-step explanation of what happens:
|
||||
|
||||
### 1. Thread Initialization
|
||||
|
||||
If you have a class that extends the Thread class, or if you have a class that implements the Runnable interface, you create an instance of that class, which represents the thread.
|
||||
```java
|
||||
Thread myThread = new MyThread(); // or Thread myThread = new Thread(new MyRunnable());
|
||||
```
|
||||
|
||||
The thread is in the "new" state after initialization. When you call start(), the thread transitions to the "runnable" state. It is ready to run but is waiting for its turn to be scheduled by the Java Virtual Machine (JVM).
|
||||
```
|
||||
myThread.start();
|
||||
```
|
||||
### 2. Thread Scheduling:
|
||||
|
||||
The JVM's scheduler determines when the thread gets CPU time for execution. The actual timing is managed by the operating system, and it may vary.
|
||||
|
||||
### 3. run() Method Execution:
|
||||
|
||||
Once the thread is scheduled, the JVM calls the run method of the thread. The run method contains the code that will be executed in the new thread.
|
||||
```java
|
||||
class MyThread extends Thread {
|
||||
public void run() {
|
||||
// Code to be executed by the thread
|
||||
}
|
||||
}
|
||||
```
|
||||
If you implemented Runnable instead:
|
||||
|
||||
```java
|
||||
class MyRunnable implements Runnable {
|
||||
public void run() {
|
||||
// Code to be executed by the thread
|
||||
}
|
||||
}
|
||||
```
|
||||
### 4. Concurrent Execution:
|
||||
|
||||
If there are multiple threads in the program, they may execute concurrently, with each thread running independently, potentially interleaving their execution.
|
||||
### 5. Thread Termination:
|
||||
The run method completes its execution, and the thread transitions to the "terminated" state. The thread is no longer active.
|
||||
|
||||
|
||||
### Important Notes
|
||||
- Direct run Method Invocation: Calling the run method directly (myThread.run()) will not start a new thread; it will execute the run method in the current thread (ie main thread).
|
||||
|
||||
- One-Time Execution: The start method can only be called once for a thread. Subsequent calls will result in an IllegalThreadStateException.
|
||||
|
||||
- In summary, calling thread.start() initiates the execution of a new thread, and the JVM takes care of the thread scheduling and execution of the run method in a separate concurrent context.
|
||||
----
|
||||
## Problem Statement - 1 Number Printer
|
||||
Write a program to print numbers from 1 to 100 using 100 different threads. Since you can't control the order of execution of threads, it is okay to get these numbers in any order.
|
||||
Hint: Create a Runnable Task, which prints a single number.
|
||||
### Solution
|
||||
|
||||
**NumberPrinter.java**
|
||||
```java
|
||||
public class NumberPrinter implements Runnable {
|
||||
int number;
|
||||
NumberPrinter(int number){
|
||||
this.number = number;
|
||||
}
|
||||
@Override
|
||||
public void run(){
|
||||
System.out.println("Printing "+number + " from "+Thread.currentThread().getName());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Main.java**
|
||||
```java
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
for(int i=0; i<100;i++){
|
||||
Thread t = new Thread(new NumberPrinter(i));
|
||||
t.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
Sample Output
|
||||
```
|
||||
Printing 3 from Thread-3
|
||||
Printing 19 from Thread-19
|
||||
Printing 14 from Thread-14
|
||||
Printing 6 from Thread-6
|
||||
Printing 21 from Thread-21
|
||||
Printing 22 from Thread-22
|
||||
Printing 0 from Thread-0
|
||||
Printing 10 from Thread-10
|
||||
...
|
||||
Printing 94 from Thread-94
|
||||
Printing 95 from Thread-95
|
||||
Printing 96 from Thread-96
|
||||
Printing 97 from Thread-97
|
||||
Printing 98 from Thread-98
|
||||
Printing 99 from Thread-99
|
||||
```
|
||||
---
|
||||
## Additonal Concepts (Optional)
|
||||
Lets cover some more advanced concepts related to Threads.
|
||||
|
||||
|
||||
### Commonly used Methods on Threads
|
||||
|
||||
In Java, the Thread class provides several commonly used methods for managing and controlling threads. Here are some of the key methods:
|
||||
|
||||
#### 1. start()
|
||||
Initiates the execution of the thread, causing the run method to be called.
|
||||
Usage ```myThread.start();```
|
||||
|
||||
#### 2. run()
|
||||
Contains the code that will be executed by the thread. This method needs to be overridden when extending the Thread class or implementing the Runnable interface.
|
||||
Usage: Defined by the user based on the specific task.
|
||||
|
||||
#### 3. sleep(long milliseconds)
|
||||
Description: Causes the thread to sleep for the specified number of milliseconds, pausing its execution.
|
||||
Usage:```Thread.sleep(1000);```
|
||||
|
||||
#### 4. join()
|
||||
Waits for the thread to complete its execution before the current thread continues. It is often used for synchronization between threads.
|
||||
Usage: ```myThread.join();```
|
||||
|
||||
#### 5. interrupt()
|
||||
Interrupts the thread, causing it to stop or throw an InterruptedException. The thread must handle interruptions appropriately.
|
||||
Usage:
|
||||
```myThread.interrupt();```
|
||||
#### 6. isAlive():
|
||||
Returns true if the thread has been started and has not yet completed its execution, otherwise returns false.
|
||||
Usage: `boolean alive = myThread.isAlive();`
|
||||
|
||||
#### 7. setName(String name)
|
||||
Sets the name of the thread.
|
||||
Usage: `myThread.setName("MyThread");`
|
||||
|
||||
#### 8. getName()
|
||||
Returns the name of the thread.
|
||||
Usage: `String threadName = myThread.getName();`
|
||||
|
||||
#### 9. setPriority(int priority)
|
||||
Sets the priority of the thread. Priorities range from Thread.MIN_PRIORITY to Thread.MAX_PRIORITY.
|
||||
Usage: ```myThread.setPriority(Thread.MAX_PRIORITY);```
|
||||
|
||||
#### 10. getPriority()
|
||||
Returns the priority of the thread.
|
||||
Usage: ```int priority = myThread.getPriority();```
|
||||
|
||||
#### 11. currentThread()
|
||||
Returns a reference to the currently executing thread object.
|
||||
Usage: `Thread currentThread = Thread.currentThread();`
|
||||
|
||||
These methods provide essential functionality for managing thread execution, synchronization, and interaction. When working with threads, it's crucial to understand and use these methods effectively to create robust and efficient concurrent programs.
|
||||
|
||||
---
|
||||
## Problem Statement - 2 Factorial Computation Task
|
||||
Write a program that computes Factorial of a list of numbers. Each factorial should be computed on a separate thread. For each factorial calculation, do not wait for more than 2 seconds.
|
||||
Hint: Use the join() method on each factorial thread, before main starts executing again.
|
||||
|
||||
### Solution
|
||||
**FactorialThread.java**
|
||||
```java
|
||||
import java.math.BigInteger;
|
||||
|
||||
public class FactorialThread extends Thread {
|
||||
private long number;
|
||||
private BigInteger result;
|
||||
private boolean isFinished;
|
||||
|
||||
FactorialThread(long number){
|
||||
this.number = number;
|
||||
result = BigInteger.valueOf(0); //Or BigInteger.ZERO;
|
||||
isFinished = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
//Business Logic
|
||||
result = factorial(number);
|
||||
isFinished = true;
|
||||
}
|
||||
BigInteger factorial(long n){
|
||||
BigInteger ans = BigInteger.ONE;
|
||||
for(long i=2; i<=n; i++){
|
||||
ans = ans.multiply(BigInteger.valueOf(i));
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
BigInteger getResult(){
|
||||
return result;
|
||||
}
|
||||
boolean isFinished(){
|
||||
return isFinished;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class Main {
|
||||
|
||||
// Task calculate Factorial of List of Numbers
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
|
||||
List<Long> inputNumbers = Arrays.asList(100000000L, 3435L, 35435L, 2324L, 4656L, 23L, 5556L);
|
||||
List<FactorialThread> threads = new ArrayList<>();
|
||||
for(long number:inputNumbers){
|
||||
FactorialThread t = new FactorialThread(number);
|
||||
//System.out.println(t.getState());
|
||||
threads.add(t);
|
||||
}
|
||||
|
||||
for(Thread t:threads){
|
||||
t.start();
|
||||
}
|
||||
|
||||
for(Thread t:threads){
|
||||
t.join(2000);
|
||||
}
|
||||
|
||||
//--------------------//
|
||||
for(int i=0;i<inputNumbers.size();i++){
|
||||
FactorialThread t = threads.get(i); //ith Thread Object
|
||||
if(t.isFinished()){
|
||||
System.out.println(t.getResult());
|
||||
}
|
||||
else{
|
||||
System.out.println("Couldn't complete calc in 2s");
|
||||
}
|
||||
}
|
||||
System.out.println("Main is completed!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
### Thread Life Cycle
|
||||
During thread lifecycle, threads go through various states. The `java.lang.Thread` class contains a static State enum – which defines its potential states. During any given point of time, the thread can only be in one of these states:
|
||||
|
||||
- **NEW** – a newly created thread that has not yet started the execution
|
||||
- **RUNNABLE** – either running or ready for execution but it’s waiting for resource allocation
|
||||
- **BLOCKED** – waiting to acquire a monitor lock to enter or re-enter a synchronized block/method
|
||||
- **WAITING** – waiting for some other thread to perform a particular action without any time limit
|
||||
- **TIMED_WAITING** – waiting for some other thread to perform a specific action for a specified period
|
||||
- **TERMINATED** – has completed its execution
|
||||
|
||||
#### 1.NEW
|
||||
A NEW Thread (or a Born Thread) is a thread that’s been created but not yet started. It remains in this state until we start it using the start() method.
|
||||
|
||||
The following code snippet shows a newly created thread that’s in the NEW state:
|
||||
|
||||
```java
|
||||
Runnable runnable = new NewState();
|
||||
Thread t = new Thread(runnable);
|
||||
System.out.println(t.getState());
|
||||
```
|
||||
|
||||
Since we’ve not started the mentioned thread, the method `t.getState()` prints:
|
||||
```
|
||||
NEW
|
||||
```
|
||||
|
||||
### 2. Runnable
|
||||
When we’ve created a new thread and called the start() method on that, it’s moved from NEW to RUNNABLE state. Threads in this state are either running or ready to run, but they’re waiting for resource allocation from the system.
|
||||
|
||||
In a multi-threaded environment, the Thread-Scheduler (which is part of JVM) allocates a fixed amount of time to each thread. So it runs for a particular amount of time, then leaves the control to other RUNNABLE threads.
|
||||
|
||||
For example, let’s add `t.start()` method to our previous code and try to access its current state:
|
||||
|
||||
```java
|
||||
Runnable runnable = new NewState();
|
||||
Thread t = new Thread(runnable);
|
||||
t.start();
|
||||
System.out.println(t.getState());
|
||||
```
|
||||
|
||||
This code is most likely to return the output as:
|
||||
```
|
||||
RUNNABLE
|
||||
```
|
||||
Note that in this example, it’s not always guaranteed that by the time our control reaches `t.getState()`, it will be still in the RUNNABLE state.
|
||||
|
||||
It may happen that it was immediately scheduled by the Thread-Scheduler and may finish execution. In such cases, we may get a different output.
|
||||
|
||||
### 3. BLOCKED
|
||||
A thread is in the BLOCKED state when it’s currently not eligible to run. It enters this state when it is waiting for a monitor lock and is trying to access a section of code that is locked by some other thread.
|
||||
|
||||
Let’s try to reproduce this state:
|
||||
```java
|
||||
public class BlockedState {
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
Thread t1 = new Thread(new DemoBlockedRunnable());
|
||||
Thread t2 = new Thread(new DemoBlockedRunnable());
|
||||
|
||||
t1.start();
|
||||
t2.start();
|
||||
|
||||
Thread.sleep(1000); //pause so that t2 states changes during this time
|
||||
System.out.println(t2.getState());
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
class DemoBlockedRunnable implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
commonResource();
|
||||
}
|
||||
|
||||
public static synchronized void commonResource() {
|
||||
while(true) {
|
||||
// Infinite loop to mimic heavy processing
|
||||
// 't1' won't leave this method
|
||||
// when 't2' try to enter this
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
In this code:
|
||||
|
||||
We’ve created two different threads – t1 and t2, t1 starts and enters the synchronized commonResource() method; this means that only one thread can access it; all other subsequent threads that try to access this method will be blocked from the further execution until the current one will finish the processing.
|
||||
|
||||
When t1 enters this method, it is kept in an infinite while loop; this is just to imitate heavy processing so that all other threads cannot enter this method
|
||||
|
||||
Now when we start t2, it tries to enter the commonResource() method, which is already being accessed by t1, thus, t2 will be kept in the BLOCKED state.
|
||||
Being in this state, we call `t2.getState()` and get the output as:
|
||||
```
|
||||
BLOCKED
|
||||
```
|
||||
|
||||
### 4. WAITING
|
||||
A thread is in WAITING state when it’s waiting for some other thread to perform a particular action. According to JavaDocs, any thread can enter this state by calling any one of the following three methods:
|
||||
|
||||
- object.wait()
|
||||
- thread.join() or
|
||||
- LockSupport.park()
|
||||
|
||||
Note that in wait() and join() – we do not define any timeout period as that scenario is covered in the next section.
|
||||
|
||||
|
||||
In this example, thread-1 starts thread 2 and waits for thread-2 to finish using `thread.join()` method. During this time t1 is in `WAITING` state.
|
||||
|
||||
**Simple Runnable.java - Thread 1**
|
||||
```java
|
||||
public class SimpleRunnable implements Runnable{
|
||||
|
||||
@Override
|
||||
public void run(){
|
||||
Thread t2 = new Thread(new SimpleRunnableTwo());
|
||||
t2.start();
|
||||
try {
|
||||
t2.join();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**Simple Runnable 2 - Thread 2**
|
||||
```java
|
||||
public class SimpleRunnableTwo implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
try{
|
||||
Thread.sleep(5000);
|
||||
}
|
||||
catch(InterruptedException e){
|
||||
Thread.currentThread().interrupt();
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
**Main**
|
||||
```java
|
||||
public class Main {
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
Thread t1 = new Thread(new SimpleRunnable());
|
||||
t1.start();
|
||||
|
||||
Thread.sleep(1000); //1ms pause
|
||||
System.out.println("T1 :"+ t1.getState()); //T1 is waiting state
|
||||
System.out.println("Main :" + Thread.currentThread().getState());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. TIMED WAITING
|
||||
A thread is in `TIMED_WAITING` state when it’s waiting for another thread to perform a particular action within a stipulated amount of time.
|
||||
|
||||
According to JavaDocs, there are five ways to put a thread on TIMED_WAITING state:
|
||||
|
||||
- thread.sleep(long millis)
|
||||
- wait(int timeout) or wait(int timeout, int nanos)
|
||||
- thread.join(long millis)
|
||||
- LockSupport.parkNanos
|
||||
- LockSupport.parkUntil
|
||||
|
||||
Here, we’ve created and started a thread t1 which is entered into the sleep state with a timeout period of 5 seconds; the output will be `TIMED_WAITING`.
|
||||
|
||||
```java
|
||||
public class SimpleRunnable implements Runnable{
|
||||
@Override
|
||||
public void run() {
|
||||
try{
|
||||
Thread.sleep(5000);
|
||||
}
|
||||
catch(InterruptedException e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
In Main, if you check the state of T1 after 2s it will be `TIMED WAITING`
|
||||
```java
|
||||
public class Main {
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
Thread t1 = new Thread(new SimpleRunnable());
|
||||
t1.start();
|
||||
|
||||
Thread.sleep(2000);
|
||||
System.out.println(t1.getState());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 6. TERMINATED
|
||||
This is the state of a dead thread. It’s in the `TERMINATED` state when it has either finished execution or was terminated abnormally. There are different ways of terminating a thread.
|
||||
|
||||
Let’s try to achieve this state in the following example:
|
||||
```java
|
||||
public class TerminatedState implements Runnable {
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
Thread t1 = new Thread(new TerminatedState());
|
||||
t1.start();
|
||||
|
||||
// The following sleep method will give enough time for
|
||||
// thread t1 to complete
|
||||
Thread.sleep(1000);
|
||||
System.out.println(t1.getState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// No processing in this block
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
Here, while we’ve started thread t1, the very next statement Thread.sleep(1000) gives enough time for t1 to complete and so this program gives us the output as:
|
||||
```
|
||||
TERMINATED
|
||||
```
|
||||
--End---
|
||||
|
@@ -0,0 +1,626 @@
|
||||
# Concurrency-2 Executors and Callables
|
||||
---
|
||||
In this tutorial, we will cover the following concepts.
|
||||
- Executor Framework
|
||||
- Overview
|
||||
- Using Executor Framework
|
||||
- Thread Pools
|
||||
- Types of Thread Pool
|
||||
- Benefits of Executor Framework
|
||||
|
||||
- Callables & Future
|
||||
- How threads can return data?
|
||||
- Coding Problems
|
||||
- Multi-threaded Merge Sort
|
||||
- Download Manager
|
||||
- Image Processing App
|
||||
- Scheduled Executor
|
||||
|
||||
- Synchronization Problems Introduction
|
||||
- Adder - Subtractor
|
||||
|
||||
|
||||
# Overview
|
||||
Imagine you have a computer program that needs to do several tasks at the same time. For example, your program might need to download files, process data, and update a user interface simultaneously. In the traditional way of programming, you might use threads to handle these tasks. However, managing threads manually can be complex and error-prone.
|
||||
Java ExecutorService implementations let you stay focused on tasks that need to be run, rather than thread creation and management.
|
||||
|
||||
Think of a chef in a kitchen as your program. The chef has multiple tasks like chopping vegetables, cooking pasta, and baking a cake. Instead of the chef doing each task one by one, the chef hires sous-chefs (threads) to help. The chef (Executor Framework) can assign tasks to sous-chefs efficiently, ensuring that multiple tasks are happening simultaneously, and the kitchen operates smoothly.
|
||||
|
||||
In Java, the Executor Framework provides a convenient way to implement this idea in your code, making it more readable, maintainable, and efficient. It simplifies the process of managing tasks concurrently, so you can focus on solving the problems your program is designed to address.
|
||||
|
||||
|
||||
|
||||
## Using Executor Framework
|
||||
The Executor Framework in Java provides a high-level and flexible framework for managing and controlling the execution of tasks in concurrent programming. It is part of the `java.util.concurrent` package and was introduced in Java 5 to simplify the development of concurrent applications. The primary motivation behind using the Executor Framework is to abstract away the complexities of thread management, providing a clean and efficient way to execute tasks asynchronously.
|
||||
|
||||
Now, let's look at a real-world example to illustrate the use of the Executor Framework. Consider a scenario where you have a set of tasks that need to be executed concurrently to improve performance. We'll use a ThreadPoolExecutor for this example:
|
||||
|
||||
```java
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class ExecutorDemo {
|
||||
public static void main(String[] args) {
|
||||
ExecutorService executor = Executors.newFixedThreadPool(10);
|
||||
for(int i=0; i<100;i++){
|
||||
executor.execute(new NumberPrinter(i));
|
||||
}
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
```
|
||||
Here `NumberPrinter()` is runnable task as created earlier. The `Executor` interface is used to execute tasks. It is a generic interface that can be used to execute any kind of task. The `Executor` interface has only one method:
|
||||
|
||||
```java
|
||||
public interface Executor {
|
||||
void execute(Runnable command);
|
||||
}
|
||||
```
|
||||
|
||||
The `execute` method takes a `Runnable` object as a parameter. The `Runnable` interface is a functional interface that has only one method. Executors internally use a thread pool to execute the tasks. The `execute` method is non-blocking. It returns immediately after submitting the task to the thread pool. The `execute` method is used to execute tasks that do not return a result. A thread pool is a collection of threads that are used to execute tasks. Instead of creating a new thread for each task, a thread pool reuses the existing threads to execute the tasks. This improves the performance of the application.
|
||||
|
||||
### Thread Pool - Deep Dive
|
||||
- Creating threads, destroying threads, and then creating them again can be expensive.
|
||||
- A thread pool mitigates the cost, by keeping a set of threads around, in a pool, for current and future work.
|
||||
- Threads, once they complete one task, can then be reassigned to another task, without the expense of destroying that thread and creating a new one.
|
||||
|
||||
A thread pool consists of three components.
|
||||
1. **Worker Threads** are available in a pool to execute tasks. They're pre-created and kept alive, throughout the lifetime of the application.
|
||||
|
||||
2. Submitted Tasks are placed in a **First-In First-Out queue**. Threads pop tasks from the queue, and execute them, so they're executed in the order they're submitted.
|
||||
|
||||
3. The **Thread Pool Manager** allocates tasks to threads, and ensures proper thread synchronization.
|
||||
|
||||
### Types of Thread Pool
|
||||
In Java, the `ExecutorService` interface, along with the `ThreadPoolExecutor` class, provides a flexible thread pool framework. Here are five types of thread pools in Java, each with different characteristics:
|
||||
|
||||
## 1. FixedThreadPool
|
||||
|
||||
- **Description:** A thread pool with a fixed number of threads.
|
||||
- **Characteristics:**
|
||||
- Reuses a fixed number of threads for all submitted tasks.
|
||||
- If a thread is idle, it will be reused for a new task.
|
||||
- If all threads are busy, tasks are queued until a thread becomes available.
|
||||
- **Creation:**
|
||||
```java
|
||||
ExecutorService executor = Executors.newFixedThreadPool(nThreads);
|
||||
```
|
||||
|
||||
## 2. CachedThreadPool
|
||||
|
||||
- **Description:** A thread pool that dynamically adjusts the number of threads based on demand.
|
||||
- **Characteristics**
|
||||
- Creates new threads as needed, but reuses idle threads if available.
|
||||
- Threads that are idle for a certain duration are terminated.
|
||||
- Suitable for handling a large number of short-lived tasks.
|
||||
|
||||
```java
|
||||
ExecutorService executor = Executors.newCachedThreadPool();
|
||||
```
|
||||
## 3. SingleThreadExecutor
|
||||
|
||||
- **Description:** A thread pool with only one thread.
|
||||
- **Characteristics:**
|
||||
- Executes tasks sequentially in the order they are submitted.
|
||||
- Useful for tasks that need to be executed in a specific order or when a single thread is sufficient.
|
||||
```java
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
```
|
||||
|
||||
## 4.ScheduledThreadPool
|
||||
|
||||
- **Description**: A thread pool that supports scheduling of tasks.
|
||||
- **Characteristics**:
|
||||
- Similar to FixedThreadPool but with added support for scheduling tasks at fixed rates or delays.
|
||||
- Suitable for periodic tasks or tasks that need to be executed after a certain delay.
|
||||
```java
|
||||
ScheduledExecutorService executor = Executors.newScheduledThreadPool(nThreads);
|
||||
```
|
||||
|
||||
## 5. WorkStealingPool
|
||||
|
||||
- **Description**: Introduced in Java 8, it's a parallelism-friendly thread pool.
|
||||
- **Characteristics:**
|
||||
- Creates a pool of worker threads that dynamically adapt to the number of available processors.
|
||||
- Each worker thread has its own task queue.
|
||||
- Suitable for parallel processing tasks.
|
||||
|
||||
```java
|
||||
ExecutorService executor = Executors.newWorkStealingPool();
|
||||
```
|
||||
These different types of thread pools cater to various scenarios and workloads. The choice of a thread pool depends on factors such as task characteristics, execution requirements, and resource constraints in your application.
|
||||
|
||||
# Benefits of Executor Framework
|
||||
|
||||
#### 1. Simplifies Task Execution
|
||||
The Executor Framework makes it easier to execute tasks concurrently. It abstracts away the low-level details of managing threads, so you don't have to worry about creating and controlling them yourself.
|
||||
|
||||
#### 2. Efficient Resource Utilization:
|
||||
When you have many tasks to perform, creating a new thread for each task can be inefficient. The Executor Framework provides a pool of threads that can be reused for multiple tasks. This reuse of threads reduces the overhead of creating and destroying threads for every task.
|
||||
|
||||
#### 3. Better Control and Flexibility
|
||||
With the Executor Framework, you can control how many tasks can run simultaneously, manage the lifecycle of threads, and specify different policies for task execution. This level of control is important for optimizing the performance of your program.
|
||||
|
||||
#### 4. Enhanced Scalability
|
||||
When your program needs to handle more tasks, the Executor Framework makes it easier to scale. You can adjust the size of the thread pool to accommodate more tasks without rewriting a lot of code.
|
||||
|
||||
#### 5. Task Scheduling
|
||||
The framework allows you to schedule tasks to run at specific times or after certain intervals. This is useful for scenarios where you want to automate repetitive tasks or execute tasks at specific points in time.
|
||||
|
||||
### Summary
|
||||
- Managing threads manually can be complex and error-prone.
|
||||
- It can lead to complex issues like resource contention, thread creation overhead, and scalability challenges.
|
||||
- For these reasons, you'll want to use an ExecutorService, even when working with a single thread.
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
## Callable and Future
|
||||
|
||||
Runnables do not return a result. If we want to execute a task that returns a result, we can use the `Callable` interface. The `Callable` interface is a functional interface that has only one method:
|
||||
|
||||
```java
|
||||
public interface Callable<V> {
|
||||
V call() throws Exception;
|
||||
}
|
||||
```
|
||||
|
||||
The `call` method returns a result of type `V`. The `call` method can throw an exception. The `Callable` interface is used to execute tasks that return a result.
|
||||
For instance we can use the `Callable` interface to execute a task that returns the sum of two numbers:
|
||||
|
||||
```java
|
||||
Callable<Integer> sumTask = () -> 2 + 3;
|
||||
```
|
||||
|
||||
In order to execute a task that returns a result, we can use the `submit` method of the `ExecutorService` interface. The `submit` method takes a `Callable` object as a parameter. The `submit` method returns a `Future` object. The `Future` interface has a method called `get` that returns the result of the task. The `get` method is a blocking method. It waits until the task is completed and then returns the result of the task.
|
||||
|
||||
```java
|
||||
ExecutorService executorService = Executors.newCachedThreadPool();
|
||||
Future<Integer> future = executorService.submit(() -> 2 + 3);
|
||||
Integer result = future.get();
|
||||
```
|
||||
|
||||
Futures can be used to cancel tasks. The `Future` interface has a method called `cancel` that can be used to cancel a task. The `cancel` method takes a boolean parameter. If the boolean parameter is `true`, the task is cancelled even if the task is already running. If the boolean parameter is `false`, the task is cancelled only if the task is not running.
|
||||
|
||||
```java
|
||||
ExecutorService executorService = Executors.newCachedThreadPool();
|
||||
Future<Integer> future = executorService.submit(() -> 2 + 3);
|
||||
future.cancel(false);
|
||||
```
|
||||
|
||||
---
|
||||
## Coding Problem 1 : Merge Sort
|
||||
Implement multi-threaded merge sort.
|
||||
|
||||
**Solution**
|
||||
**Sorter.java**
|
||||
```java
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public class Sorter implements Callable<List<Integer>> {
|
||||
private List<Integer> arr;
|
||||
private ExecutorService executor;
|
||||
Sorter(List<Integer> arr,ExecutorService executor){
|
||||
this.arr = arr;
|
||||
this.executor = executor;
|
||||
|
||||
}
|
||||
@Override
|
||||
public List<Integer> call() throws Exception {
|
||||
//Business Logic
|
||||
//base case
|
||||
if(arr.size()<=1){
|
||||
return arr;
|
||||
}
|
||||
|
||||
//recursive case
|
||||
int n = arr.size();
|
||||
int mid = n/2;
|
||||
|
||||
List<Integer> leftArr = new ArrayList<>();
|
||||
List<Integer> rightArr = new ArrayList<>();
|
||||
|
||||
//Division of array into 2 parts
|
||||
for(int i=0;i<n;i++){
|
||||
if(i<mid){
|
||||
leftArr.add(arr.get(i));
|
||||
}
|
||||
else{
|
||||
rightArr.add(arr.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
//Recursively Sort the 2 array
|
||||
Sorter leftSorter = new Sorter(leftArr,executor);
|
||||
Sorter rightSorter = new Sorter(rightArr,executor);
|
||||
|
||||
Future<List<Integer>> leftFuture = executor.submit(leftSorter);
|
||||
Future<List<Integer>> rightFuture = executor.submit(rightSorter);
|
||||
|
||||
leftArr = leftFuture.get();
|
||||
rightArr = rightFuture.get();
|
||||
|
||||
|
||||
//Merge
|
||||
List<Integer> output = new ArrayList<>();
|
||||
int i=0;
|
||||
int j=0;
|
||||
while(i<leftArr.size() && j<rightArr.size()){
|
||||
if(leftArr.get(i)<rightArr.get(j)){
|
||||
output.add(leftArr.get(i));
|
||||
i++;
|
||||
}
|
||||
else{
|
||||
output.add(rightArr.get(j));
|
||||
j++;
|
||||
}
|
||||
}
|
||||
// copy the remaining elements
|
||||
while(i<leftArr.size()){
|
||||
output.add(leftArr.get(i));
|
||||
i++;
|
||||
}
|
||||
while(j<rightArr.size()){
|
||||
output.add(rightArr.get(j));
|
||||
j++;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Main.java**
|
||||
```java
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) throws Exception {
|
||||
List<Integer> l = List.of(7,3,1,2,4,6,17,12);
|
||||
ExecutorService executorService = Executors.newCachedThreadPool();
|
||||
|
||||
Sorter sorter = new Sorter(l,executorService);
|
||||
Future<List<Integer>> output = executorService.submit(sorter);
|
||||
System.out.println(output.get()); //Blocking Code
|
||||
executorService.shutdown();
|
||||
}
|
||||
}
|
||||
```
|
||||
## Coding Problem 2 : Download Manager (Homework)
|
||||
Consider a simple download manager application that needs to download multiple files concurrently. Implement the download manager using the Java Executor Framework.
|
||||
Requirements:
|
||||
- The download manager should be able to download multiple files simultaneously.
|
||||
- Each file download is an independent task that can be executed concurrently.
|
||||
- The download manager should use a thread pool from the Executor Framework to manage and execute the download tasks.
|
||||
- Implement a mechanism to track the progress of each download task and display it to the user.
|
||||
|
||||
```java
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
class DownloadManager {
|
||||
private ExecutorService executorService;
|
||||
|
||||
public DownloadManager(int threadPoolSize) {
|
||||
// TODO: Initialize the ExecutorService with a fixed-size thread pool.
|
||||
}
|
||||
|
||||
public void downloadFiles(List<String> fileUrls) {
|
||||
// TODO: Implement a method to submit download tasks for each file URL.
|
||||
}
|
||||
|
||||
// TODO: Implement a method to track and display the progress of each download task.
|
||||
|
||||
public void shutdown() {
|
||||
// TODO: Shutdown the ExecutorService when the download manager is done.
|
||||
}
|
||||
}
|
||||
```
|
||||
```java
|
||||
public class DownloadManagerApp {
|
||||
public static void main(String[] args) {
|
||||
// TODO: Create a DownloadManager instance with an appropriate thread pool size.
|
||||
// TODO: Test the download manager by downloading multiple files concurrently.
|
||||
// TODO: Display the progress of each download task.
|
||||
// TODO: Shutdown the download manager after completing the downloads.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Tasks for Implementation**
|
||||
- Initialize the ExecutorService in the DownloadManager constructor.
|
||||
- Implement the downloadFiles method to submit download tasks for each file URL using the ExecutorService.
|
||||
- Implement a mechanism to track and display the progress of each download task.
|
||||
- Test the download manager in the DownloadManagerApp by downloading multiple files concurrently.
|
||||
- Shutdown the ExecutorService when the download manager is done.
|
||||
- Feel free to adapt and extend the code as needed. This example focuses on using the Executor Framework for concurrent file downloads in a download manager application.
|
||||
|
||||
**Solution**
|
||||
```java
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
class DownloadTask implements Runnable {
|
||||
private String fileUrl;
|
||||
|
||||
public DownloadTask(String fileUrl) {
|
||||
this.fileUrl = fileUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// Simulate file download
|
||||
System.out.println("Downloading file from: " + fileUrl);
|
||||
|
||||
// Simulate download progress
|
||||
for (int progress = 0; progress <= 100; progress += 10) {
|
||||
System.out.println("Progress for " + fileUrl + ": " + progress + "%");
|
||||
try {
|
||||
Thread.sleep(500); // Simulate download time
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("Download complete for: " + fileUrl);
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadManager {
|
||||
private ExecutorService executorService;
|
||||
|
||||
public DownloadManager(int threadPoolSize) {
|
||||
executorService = Executors.newFixedThreadPool(threadPoolSize);
|
||||
}
|
||||
|
||||
public void downloadFiles(List<String> fileUrls) {
|
||||
for (String fileUrl : fileUrls) {
|
||||
DownloadTask downloadTask = new DownloadTask(fileUrl);
|
||||
executorService.submit(downloadTask);
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
executorService.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
public class DownloadManagerApp {
|
||||
public static void main(String[] args) {
|
||||
DownloadManager downloadManager = new DownloadManager(3); // Use a thread pool size of 3
|
||||
|
||||
List<String> filesToDownload = List.of("file1", "file2", "file3", "file4", "file5");
|
||||
|
||||
downloadManager.downloadFiles(filesToDownload);
|
||||
|
||||
// Display progress (simulated)
|
||||
// Note: In a real-world scenario, you might need to implement a more sophisticated progress tracking mechanism.
|
||||
for (int i = 0; i < 10; i++) {
|
||||
System.out.println("Main thread is doing some work...");
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
downloadManager.shutdown();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Coding Problem 3 : Image Processing
|
||||
Many image processing applications like Lightroom & Photoshop use multiple threads to process an image quickly. In this problem, you will build a simplified image repainting task using multiple threads, the repainting task here simply doubles the value of every pixel stored in the form of a 2D array. Take Input a NXN matrix and repaint it by using 4 threads, one for each quadrant.
|
||||
|
||||
**Solution**
|
||||
Repainting a 2D array using four threads can be achieved by dividing the array into quadrants, and assigning each quadrant to a separate thread for repainting.
|
||||
|
||||
This example divides the 2D array into four quadrants and assigns each quadrant to a separate thread for repainting. The ArrayRepainterTask class represents the task for repainting a specific quadrant. The program then uses an ExecutorService with a fixed thread pool to concurrently execute the tasks. Finally, it prints the repainted 2D array.
|
||||
|
||||
Below is an example code using the Java Executor Framework
|
||||
|
||||
```java
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
class ArrayRepainterTask implements Runnable {
|
||||
private final int[][] array;
|
||||
private final int startRow;
|
||||
private final int endRow;
|
||||
private final int startCol;
|
||||
private final int endCol;
|
||||
|
||||
public ArrayRepainterTask(int[][] array, int startRow, int endRow, int startCol, int endCol) {
|
||||
this.array = array;
|
||||
this.startRow = startRow;
|
||||
this.endRow = endRow;
|
||||
this.startCol = startCol;
|
||||
this.endCol = endCol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// Simulate repainting for the specified quadrant
|
||||
for (int i = startRow; i <= endRow; i++) {
|
||||
for (int j = startCol; j <= endCol; j++) {
|
||||
array[i][j] = array[i][j] * 2; // Repaint by doubling the values (simulated)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ArrayRepaintingExample {
|
||||
public static void main(String[] args) {
|
||||
int[][] originalArray = {
|
||||
{1, 2, 3, 4},
|
||||
{5, 6, 7, 8},
|
||||
{9, 10, 11, 12},
|
||||
{13, 14, 15, 16}
|
||||
};
|
||||
|
||||
int rows = originalArray.length;
|
||||
int cols = originalArray[0].length;
|
||||
|
||||
ExecutorService executorService = Executors.newFixedThreadPool(4);
|
||||
|
||||
// Divide the array into four quadrants
|
||||
int midRow = rows / 2;
|
||||
int midCol = cols / 2;
|
||||
|
||||
// Create tasks for each quadrant
|
||||
ArrayRepainterTask task1 = new ArrayRepainterTask(originalArray, 0, midRow - 1, 0, midCol - 1);
|
||||
ArrayRepainterTask task2 = new ArrayRepainterTask(originalArray, 0, midRow - 1, midCol, cols - 1);
|
||||
ArrayRepainterTask task3 = new ArrayRepainterTask(originalArray, midRow, rows - 1, 0, midCol - 1);
|
||||
ArrayRepainterTask task4 = new ArrayRepainterTask(originalArray, midRow, rows - 1, midCol, cols - 1);
|
||||
|
||||
// Submit tasks to the ExecutorService
|
||||
executorService.submit(task1);
|
||||
executorService.submit(task2);
|
||||
executorService.submit(task3);
|
||||
executorService.submit(task4);
|
||||
|
||||
// Shutdown the ExecutorService
|
||||
executorService.shutdown();
|
||||
|
||||
// Wait for all tasks to complete
|
||||
while (!executorService.isTerminated()) {
|
||||
// Wait
|
||||
}
|
||||
|
||||
// Print the repainted array
|
||||
for (int[] row : originalArray) {
|
||||
for (int value : row) {
|
||||
System.out.print(value + " ");
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
## Coding Problem 4: Scheduled Executor
|
||||
Write a Java program that uses ScheduledExecutorService to schedule a task to run periodically. Implement a task that prints a message “Hello” at fixed intervals of 5s.
|
||||
```java
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ScheduledExecutorExample {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// Create a ScheduledExecutorService with a single thread
|
||||
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
// Schedule the task to run periodically every 5 seconds
|
||||
scheduledExecutorService.scheduleAtFixedRate(() -> {
|
||||
System.out.println("Hello");
|
||||
}, 0, 5, TimeUnit.SECONDS);
|
||||
|
||||
// Sleep for a while to allow the task to run multiple times
|
||||
try {
|
||||
Thread.sleep(20000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// Shutdown the ScheduledExecutorService
|
||||
scheduledExecutorService.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Synchronisation
|
||||
|
||||
Whenever we have multiple threads that access the same resource, we need to make sure that the threads do not interfere with each other. This is called synchronisation.
|
||||
|
||||
Synchronisation can be seen in the adder and subtractor example. The adder and subtractor threads access the same counter variable. If the adder and subtractor threads do not synchronise, the counter variable can be in an inconsistent state.
|
||||
|
||||
* Create a count class that has a count variable.
|
||||
* Create two different classes `Adder` and `Subtractor`.
|
||||
* Accept a count object in the constructor of both the classes.
|
||||
* In `Adder`, iterate from 1 to 10000 and increment the count variable by 1 on each iteration.
|
||||
* In `Subtractor`, iterate from 1 to 10000 and decrement the count variable by 1 on each iteration.
|
||||
* Print the final value of the count variable.
|
||||
* What would the ideal value of the count variable be?
|
||||
* What is the actual value of the count variable?
|
||||
* Try to add some delay in the `Adder` and `Subtractor` classes using inspiration from the code below. What is the value of the count variable now?
|
||||
|
||||
|
||||
**Steps to implement**
|
||||
- Implement Adder & Subtractor
|
||||
- Shared counter via constructor
|
||||
- Create a package called addersubtractor
|
||||
- Create two tasks under adder and subtractor
|
||||
|
||||
Adder
|
||||
```java
|
||||
package addersubtractor;
|
||||
public class Adder implements Runnable {
|
||||
private Count count;
|
||||
public Adder (Count count) {
|
||||
this.count = count;
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
for (int i = 1 ; i <= 100; ++ 1) {
|
||||
count.value += i;
|
||||
}
|
||||
}
|
||||
```
|
||||
Subtracter
|
||||
```java
|
||||
package addersubtractor;
|
||||
public class Subtractor implements Runnable {
|
||||
private Count count;
|
||||
public Subtractor (Count count) {
|
||||
this.count = count
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
for (int i = 1 ; i <= 100; ++ 1) {
|
||||
count.value -= i;
|
||||
}
|
||||
}
|
||||
```
|
||||
Count class
|
||||
```java
|
||||
package addersubtractor;
|
||||
public class Count {
|
||||
int value = 0;
|
||||
}
|
||||
```
|
||||
Now, let’s make our client class here:
|
||||
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
Count count = new Count;
|
||||
Adder adder = new Adder (count);
|
||||
Subtractor subtractor = new Subtractor (count);
|
||||
Thread t1 = new Thread (adder);
|
||||
Thread t2 = new Thread (subtractor);
|
||||
t1.start();
|
||||
t2.start();
|
||||
t1.join();
|
||||
t2.join();
|
||||
|
||||
system.out.println(count.value);
|
||||
```
|
||||
Output is some random number every time we run the code.
|
||||
|
||||
Now, this particular problem is known to be a data synchronization problem.
|
||||
This happens because the same data object is shared among various multi-threads, and they both are trying to modify the same data. This is an unexpected result that we have seen, but we will continue this in the next tutorial.
|
||||
|
@@ -0,0 +1,883 @@
|
||||
# Concurrency-3 Introduction to Synchronisation, Mutex, Synchronized, Atomic Data-types
|
||||
----
|
||||
|
||||
### Agenda
|
||||
- Synchronisation Problem
|
||||
- Adder Subtracter Recap
|
||||
- Conditions for Synchronisation Problem
|
||||
- Properties for a Good Solution
|
||||
- Solutions for Synchronisation
|
||||
- Mutex Locks
|
||||
- Synchronised Keyword
|
||||
- Semaphores(Next Class)
|
||||
- Coding Problems
|
||||
- Thread Safe Counter
|
||||
- ReentrantLock Basics
|
||||
|
||||
- Addtional Topics
|
||||
- Atomic Datatypes
|
||||
- Volatile Keyword
|
||||
- Concurrent Hashmap (Interviews)
|
||||
|
||||
- Coding Projects (Homework)
|
||||
- Ticket Booking System (Project)
|
||||
- Thread Safe Bank Transactions
|
||||
- Additional Reading
|
||||
|
||||
# Synchronisation Problem
|
||||
|
||||
## Adder Subtracter Recap
|
||||
|
||||
The adder and subtractor problem is a sample problem that is used to demonstrate the need for synchronisation in a system. The problem is as follows:
|
||||
|
||||
- Create a count class that has a count variable.
|
||||
- Create two different classes Adder and Subtractor.
|
||||
- Accept a count object in the constructor of both the classes.
|
||||
- In Adder, iterate from 1 to 100 and increment the count variable by 1 on each iteration.
|
||||
- In Subtractor, iterate from 1 to 100 and decrement the count variable by 1 on each iteration.
|
||||
- Print the final value of the count variable.
|
||||
|
||||
**What would the ideal value of the count variable be?**
|
||||
**What is the actual value of the count variable?**
|
||||
Try to add some delay in the Adder and Subtractor classes using inspiration from the code below. What is the value of the count variable now?
|
||||
|
||||
**Adder.java**
|
||||
```java
|
||||
public class Adder implements Runnable {
|
||||
private Count count;
|
||||
|
||||
public Adder(Count count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
count.increment();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Subtracter.java**
|
||||
```java
|
||||
public class Subtractor implements Runnable {
|
||||
private Count count;
|
||||
|
||||
public Subtractor(Count count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
count.decrement();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
**Runner.java**
|
||||
```java
|
||||
public class Runner {
|
||||
public static void main(String[] args) {
|
||||
Count count = new Count();
|
||||
Adder adder = new Adder(count);
|
||||
Subtractor subtractor = new Subtractor(count);
|
||||
|
||||
Thread adderThread = new Thread(adder);
|
||||
Thread subtractorThread = new Thread(subtractor);
|
||||
|
||||
adderThread.start();
|
||||
subtractorThread.start();
|
||||
|
||||
adderThread.join();
|
||||
subtractorThread.join();
|
||||
|
||||
System.out.println(count.getCount());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Synchronisation Problem
|
||||
In multithreaded environments, synchronization problems can arise due to concurrent execution of multiple threads, leading to unpredictable and undesirable behavior. There are certain conditions that can lead to synchronization problems. These Conditions are:
|
||||
|
||||
**1. Critical Section**
|
||||
- A critical section is a part of the code that must be executed by only one thread at a time to avoid data inconsistency or corruption.
|
||||
- If multiple threads access and modify shared data simultaneously within a critical section, it can lead to unpredictable results.
|
||||
- Ensuring mutual exclusion by using synchronization mechanisms like locks or semaphores helps prevent multiple threads from entering the critical section simultaneously.
|
||||
Race Condition:
|
||||
|
||||
**2. Race Conditions**
|
||||
- A race condition occurs when the final outcome of a program depends on the relative timing of events, such as the order in which threads are scheduled to run.
|
||||
- In a race condition, the correctness of the program depends on the timing of the thread executions, and different outcomes may occur depending on the interleaving of thread execution.
|
||||
- Proper synchronization mechanisms, like locks or atomic operations, are needed to prevent race conditions by enforcing a specific order of execution for critical sections.
|
||||
Preemption:
|
||||
|
||||
**3. Preemption**
|
||||
- Preemption refers to the interrupting of a currently executing thread to start or resume the execution of another thread.
|
||||
- In multithreaded environments, preemption can lead to issues if not handled carefully. For example, a thread might be preempted while in the middle of updating shared data, leading to inconsistent or corrupted state.
|
||||
- To avoid issues related to preemption, critical sections should be protected using mechanisms like locks or disabling interrupts temporarily to ensure that a thread completes its operation without being interrupted.
|
||||
|
||||
|
||||
To address these synchronization problems, various synchronization mechanisms are employed, such as locks, semaphores, and atomic operations. These tools help ensure that only one thread can access critical sections at a time, preventing race conditions and mitigating the impact of preemption on shared data. Additionally, proper design practices, like minimizing the use of shared mutable data and using thread-safe data structures, can contribute to reducing synchronization issues in multithreaded environments.
|
||||
|
||||
|
||||
### Properties of a Good Synchronization Solution
|
||||
|
||||
1. **Mutual Exclusion:**
|
||||
- *Definition:* Only one thread should be allowed to execute its critical section at any given time. Suppose there are three threads, and they are waiting to enter the critical sections of the Adder, Subtractor, and Multiplier. But a blocker should be there to allow only one thread in a critical section at a time.
|
||||
|
||||
- *Importance:* Ensures that conflicting operations on shared resources do not occur simultaneously, preventing data corruption or inconsistency.
|
||||
|
||||
2. **Progress:**
|
||||
- *Definition:* The overall system should keep moving and making progress. It should not stop at any stage and be waiting for a long period. If no thread is in its critical section and some threads are waiting to enter the critical section, then the selection of the next thread to enter the critical section should be definite.
|
||||
|
||||
|
||||
- *Importance:* Guarantees that the system makes progress and avoids deadlock situations where threads are unable to proceed.
|
||||
|
||||
3. **Bounded Waiting:**
|
||||
- *Definition:* There exists a limit on the number of times other threads are allowed to enter their critical sections after a thread has requested entry into its critical section and before that request is granted. No thread should be waiting infinitely. There should be a bound on how long they have to wait before they are allowed to enter the critical section.
|
||||
|
||||
- *Importance:* Prevents the problem of starvation, where a thread is repeatedly delayed in entering its critical section by other threads.
|
||||
|
||||
4. **No Deadlock:**
|
||||
- *Definition:* A deadlock is a state where two or more threads are blocked forever, each waiting for the other to release a resource.
|
||||
- *Importance:* A good synchronization solution should avoid deadlocks, as they can lead to a complete system halt and result in unresponsive behavior.
|
||||
|
||||
5. **Efficiency:**
|
||||
- *Definition:* The synchronization solution should introduce minimal overhead and allow non-conflicting threads to execute concurrently.
|
||||
- *Importance:* Ensures that the system performs well and doesn't suffer from unnecessary delays or resource contention.
|
||||
|
||||
6. **Adaptability:**
|
||||
- *Definition:* The synchronization solution should be adaptable to different system configurations and workloads.
|
||||
- *Importance:* Facilitates the use of the synchronization mechanism in a variety of scenarios without requiring significant modifications.
|
||||
|
||||
7. **Low Busy-Waiting:**
|
||||
- *Definition:* Minimizes the use of busy-waiting (spinning in a loop while waiting for a condition to be satisfied) to conserve CPU resources. When a thread has to continuously check if they can now enter the critical section. Checking if a thread can enter the critical section is not a productive use of time.
|
||||
The ideal solution should have some kind of notification system.
|
||||
For example if you have to check if a person is available or not:
|
||||
In way 1, you go and knock on the person’s door every 2 minutes to check if they are free. This is busy waiting
|
||||
In way 2, you go and tell the person that I am here. Please let me know when you are free. This is called a notification. This provides better usage of the time.
|
||||
|
||||
- *Importance:* Reduces unnecessary CPU consumption, making the system more efficient and avoiding the negative impact of busy-waiting on power consumption.
|
||||
|
||||
|
||||
|
||||
8. **Fairness:**
|
||||
- *Definition:* All threads should have a fair chance to enter their critical sections. No thread should be unfairly delayed or granted preferential access.
|
||||
- *Importance:* Ensures that the synchronization solution treats all threads fairly, preventing situations where some threads consistently get better access to shared resources.
|
||||
|
||||
9. **Scalability:**
|
||||
- *Definition:* The synchronization solution should scale well with an increasing number of threads and resources.
|
||||
- *Importance:* Allows the system to efficiently handle a growing number of threads without a significant degradation in performance.
|
||||
|
||||
10. **Portability:**
|
||||
- *Definition:* The synchronization solution should be portable across different platforms and operating systems.
|
||||
- *Importance:* Enables the synchronization mechanism to be used in diverse computing environments without requiring extensive modifications.
|
||||
|
||||
-----
|
||||
## Solutions to Synchronisation Problem
|
||||
|
||||
### 1. Mutex Lock
|
||||
Mutex means Mutual Exclusion. Mutex Lock is a lock that enables mutual exclusion. Mutex locks are a way to solve the synchronisation problem. Mutex locks are a way to ensure that only one thread can access a critical section at a time. Mutex locks are also known as mutual exclusion locks.
|
||||
|
||||
A thread can only access the critical section if it has the lock. If a thread does not have the lock, it cannot access the critical section. If a thread has the lock, it can access the critical section. If a thread has the lock, it can release the lock and allow another thread to access the critical section.
|
||||
|
||||
Suppose if we take the example of Adder and Subtractor here,
|
||||
Adder:
|
||||
```java
|
||||
print('Hi')
|
||||
x <- read(count)
|
||||
x = x + 1
|
||||
print('Bye')
|
||||
```
|
||||
|
||||
Subtractor:
|
||||
```java
|
||||
print('Hello')
|
||||
x <- read(count)
|
||||
x = x - 1
|
||||
print('Bye')
|
||||
```
|
||||
|
||||
The above adder-substracter if executed concurrently, can lead to wrong results due to interleaving of instructions.
|
||||
So, MUTEX SAYS:
|
||||
- A thread must take a lock before it enters its critical section.
|
||||
- They must remove the lock as soon as they leave the critical section.
|
||||
|
||||
Think of a room with a lock. Only one person can enter the room at a time. If a person has the key, they can enter the room. If a person does not have the key, they cannot enter the room. If a person has the key, they can leave the room and give the key to another person. This is the same as a mutex lock.
|
||||
|
||||
- So, A thread(person) must take a lock(have a key) before they enter their critical section(Room).
|
||||
- They must remove the lock(key) as soon as they leave the (Room)critical section.
|
||||
- By default, the program can not enforce synchronization. The developer has to do it.
|
||||
|
||||
**So what do we have to do?**
|
||||
Before entering a critical section, lock the thread and, at exit, unlock it.
|
||||
Example:
|
||||
|
||||
Adder:
|
||||
```java
|
||||
print('Hi')
|
||||
lock.lock()
|
||||
x <- read(count)
|
||||
x = x + 1
|
||||
count = x
|
||||
lock.unlock()
|
||||
print('Bye')
|
||||
```
|
||||
Subtractor:
|
||||
```java
|
||||
print('Hello')
|
||||
lock.lock()
|
||||
x <- read(count)
|
||||
x = x - 1
|
||||
count = x
|
||||
lock.unlock()
|
||||
print('Bye')
|
||||
```
|
||||
Now let’s discuss about the Properties of lock:
|
||||
- Only one thread can unlock a thread at one time; other threads have to wait till that thread unlocks.
|
||||
- Lock will automatically notify the second thread to run when the first one exits.
|
||||
- It has no busy waiting
|
||||
- It has mutual exclusion
|
||||
- Bounded waiting
|
||||
- The system is having overall progress
|
||||
|
||||
Code for reference:
|
||||
```java
|
||||
Client class (main)
|
||||
|
||||
public static void main(String[] args) {
|
||||
Count count = new Count;
|
||||
Lock lock = new ReentranttLock();
|
||||
Adder adder = new Adder (count, lock);
|
||||
Subtractor subtractor = new Subtractor (count, lock);
|
||||
Thread t1 = new Thread (adder);
|
||||
Thread t2 = new Thread (subtractor);
|
||||
t1.start();
|
||||
t2.start();
|
||||
t1.join();
|
||||
t2.join();
|
||||
|
||||
system.out.println(count.value);
|
||||
```
|
||||
Now, let’s change the constructor of adder and subtractor
|
||||
Adder:
|
||||
```java
|
||||
package addersubtractor;
|
||||
public class Adder implements Runnable {
|
||||
private Count count;
|
||||
private Lock lock;
|
||||
public Adder (Count count, Lock lock) {
|
||||
this.count = count;
|
||||
this.lock = lock
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
for (int i = 1 ; i <= 100; ++ 1) {
|
||||
lock.lock()
|
||||
count.value += i;
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
```
|
||||
Subtractor:
|
||||
```java
|
||||
package addersubtractor;
|
||||
public class Subtractor implements Runnable {
|
||||
private Count count;
|
||||
private Lock lock;
|
||||
public Subtractor (Count count, Lock lock) {
|
||||
this.count = count;
|
||||
this.lock = lock;
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
for (int i = 1 ; i <= 100; ++ 1) {
|
||||
lock.lock();
|
||||
count.value -= i;
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
```
|
||||
Now, when we run this, we will always get 0 as the answer, which was not the case earlier.
|
||||
|
||||
#### Properties of a mutex lock
|
||||
- A thread can only access the critical section if it has the lock.
|
||||
- Only one thread can have the lock at a time.
|
||||
- Other threads cannot access the critical section if a thread has the lock and thus have to wait.
|
||||
- Lock will automatically be released when the thread exits the critical section.
|
||||
|
||||
### 2. Synchronised keyword
|
||||
The synchronized keyword is a way to solve the synchronisation problem. The synchronized keyword is a way to ensure that only one thread can access a critical section at a time.
|
||||
|
||||
A synchronized method or block can only be accessed by one thread at a time. If a thread is accessing a synchronized method or block, other threads cannot access the synchronized method or block. If a thread is accessing a synchronized method or block, other threads have to wait until the thread exits the synchronized method or block.
|
||||
|
||||
Following is an example of a synchronized method:
|
||||
```java
|
||||
public class Count {
|
||||
private int count = 0;
|
||||
|
||||
public synchronized void increment() {
|
||||
count++;
|
||||
}
|
||||
|
||||
public synchronized void decrement() {
|
||||
count--;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
}
|
||||
```
|
||||
In the above example, the increment() and decrement() methods are synchronized. This means that only one thread can access the increment() and decrement() methods at a time. If a thread is accessing the increment() method, other threads cannot access the increment() method. If a thread is accessing the decrement() method, other threads cannot access the decrement() method. If a thread is accessing the increment() method, other threads have to wait until the thread exits the increment() method. If a thread is accessing the decrement() method, other threads have to wait until the thread exits the decrement() method.
|
||||
|
||||
Similarly, the **synchronized keyword** can be used to synchronize a block of code. Following is an example of a synchronized block:
|
||||
```java
|
||||
public class Count {
|
||||
private int count = 0;
|
||||
|
||||
public void increment() {
|
||||
synchronized (this) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
public void decrement() {
|
||||
synchronized (this) {
|
||||
count--;
|
||||
}
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
}
|
||||
```
|
||||
If you declare a method as synchronized, only one thread will be able to access any synchronized method in the class. This is because the synchronized keyword is associated with the object.
|
||||
|
||||
---
|
||||
### Coding Problem 1 - Thread Safe Counter (Homework)
|
||||
// Implement a class that represents a counter and is accessed by multiple threads.
|
||||
// Ensure that the counter is updated in a thread-safe manner without using the synchronized keyword.
|
||||
|
||||
```java
|
||||
public class ThreadSafeCounter {
|
||||
private int count = 0;
|
||||
|
||||
// TODO: Implement a thread-safe method to increment the counter.
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: Create multiple threads that concurrently increment the counter.
|
||||
// Ensure that the counter is updated in a thread-safe manner without using the synchronized keyword.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Coding Problem 2 - Reentrantlock Basics (Homework)
|
||||
Implement a program that uses ReentrantLock to achieve thread safety.
|
||||
|
||||
```java
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public class ReentrantLockExample {
|
||||
private int value = 0;
|
||||
private final Lock lock = new ReentrantLock();
|
||||
|
||||
// TODO: Implement a method to update the value using ReentrantLock.
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: Create multiple threads that concurrently update the value using ReentrantLock.
|
||||
// Ensure that the value is updated in a thread-safe manner.
|
||||
}
|
||||
}
|
||||
```
|
||||
---
|
||||
## Additonal Topics
|
||||
### 1. Atomic Datatypes in java
|
||||
In Java, the java.util.concurrent.atomic package provides a set of classes that support atomic operations on variables. These classes are designed to be thread-safe and eliminate the need for explicit synchronization in certain scenarios. One commonly used class is AtomicInteger. In this tutorial, we'll explore AtomicInteger and provide a simple code example.
|
||||
|
||||
**Atomic Integer Basics**
|
||||
The AtomicInteger class provides atomic operations on an integer variable. These operations are performed in a way that ensures atomicity, making them thread-safe without the need for explicit synchronization.
|
||||
|
||||
Example Usage:
|
||||
|
||||
```java
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class AtomicIntegerExample {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// Create an AtomicInteger with an initial value
|
||||
AtomicInteger atomicInteger = new AtomicInteger(0);
|
||||
|
||||
// Perform atomic increment
|
||||
int newValue = atomicInteger.incrementAndGet();
|
||||
System.out.println("Incremented Value: " + newValue);
|
||||
|
||||
// Perform atomic decrement
|
||||
newValue = atomicInteger.decrementAndGet();
|
||||
System.out.println("Decremented Value: " + newValue);
|
||||
|
||||
// Perform atomic add
|
||||
int addValue = 5;
|
||||
newValue = atomicInteger.addAndGet(addValue);
|
||||
System.out.println("After Adding " + addValue + ": " + newValue);
|
||||
|
||||
// Perform compare-and-set operation
|
||||
int expectedValue = 5;
|
||||
int updateValue = 10;
|
||||
boolean success = atomicInteger.compareAndSet(expectedValue, updateValue);
|
||||
if (success) {
|
||||
System.out.println("Value updated successfully. New Value: " + atomicInteger.get());
|
||||
} else {
|
||||
System.out.println("Value was not updated. Current Value: " + atomicInteger.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits of Atomic Datatypes:**
|
||||
- Thread Safety: Operations on AtomicInteger are atomic, eliminating the need for explicit synchronization.
|
||||
- Performance: Atomic operations are more efficient than using locks for simple operations on shared variables.
|
||||
- Simplicity: Simplifies the development of thread-safe code in scenarios where simple atomic operations suffice.
|
||||
|
||||
Lets use Atomic Integers in Adder-Subtracter Example.
|
||||
**InventoryCounter.java**
|
||||
```java
|
||||
public class InventorCounter {
|
||||
AtomicInteger counter = new AtomicInteger(0);
|
||||
}
|
||||
```
|
||||
**Adder.java**
|
||||
```java
|
||||
public class Adder implements Runnable{
|
||||
private InventorCounter ic;
|
||||
|
||||
Adder(InventorCounter ic){
|
||||
this.ic = ic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
for(int i=0;i<=10000;i++){
|
||||
ic.counter.addAndGet(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
**Subtracter.java**
|
||||
```java
|
||||
public class Subtracter implements Runnable{
|
||||
private InventorCounter ic;
|
||||
|
||||
Subtracter(InventorCounter ic){
|
||||
this.ic = ic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
for(int i=0;i<=10000;i++){
|
||||
ic.counter.addAndGet(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
**Main.java**
|
||||
```java
|
||||
public class Main {
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
|
||||
InventorCounter ic = new InventorCounter();
|
||||
Thread t1 = new Thread(new Adder(ic));
|
||||
Thread t2 = new Thread(new Subtracter(ic));
|
||||
t1.start();
|
||||
t2.start();
|
||||
|
||||
t1.join();
|
||||
t2.join();
|
||||
|
||||
System.out.println(ic.counter.get());
|
||||
}
|
||||
}
|
||||
```
|
||||
In summary, the AtomicInteger class in Java provides a convenient and efficient way to perform atomic operations on integer variables, making it a valuable tool for concurrent programming. Similar classes, such as AtomicLong and AtomicBoolean, exist for other primitive types.
|
||||
|
||||
### 2. Volatile Keyword
|
||||
[Video Tutorial on Volatile](https://drive.google.com/drive/folders/1NpxU-yvk-sgiRrsmIz05vjy2Iz8gYMED?usp=sharing)
|
||||
|
||||
Volatile Keyword solves for problems like Memory Inconsistency Errors & Data Races. Let's understand this in more detail.
|
||||
|
||||
|
||||
The Operating system may read from heap variables, and make a copy of the value in each thread's own storage. Each threads has its own small and fast memory storage, that holds its own copy of shared resource's value.
|
||||
|
||||
Once thread can modify a shared variable, but this change might not be immediately reflected or visible. Instead it is first update in thread's local cache. The operating system may not flush the first thread's changes to the heap, until the thread has finished executing, causing memory inconsistency errors.
|
||||
|
||||
Lets see it through this code in action:
|
||||
**SharedResourced.java**
|
||||
```java
|
||||
public class SharedResource {
|
||||
volatile private boolean flag;
|
||||
SharedResource(){
|
||||
flag = false;
|
||||
}
|
||||
//Two More Methods
|
||||
public void toggleFlag(){
|
||||
flag = !flag;
|
||||
}
|
||||
public boolean getFlag(){
|
||||
return flag;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Main.java**
|
||||
```java
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
SharedResource sharedResource = new SharedResource();
|
||||
System.out.println("Shared Resource Created, Flag Value " + sharedResource.getFlag());
|
||||
|
||||
Thread A = new Thread(()->{
|
||||
//After 2S, toggle the value
|
||||
try{
|
||||
Thread.sleep(2000);
|
||||
}
|
||||
catch(InterruptedException e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
sharedResource.toggleFlag();
|
||||
System.out.println("Thread A is finished, Flag is "+sharedResource.getFlag());
|
||||
});
|
||||
|
||||
Thread B = new Thread(()->{
|
||||
while(!sharedResource.getFlag()){
|
||||
//...busy-wait...
|
||||
// System.out.println("Inside Loop " + sharedResource.getFlag());
|
||||
|
||||
}
|
||||
System.out.println("In Thread B, Flag is "+sharedResource.getFlag());
|
||||
});
|
||||
|
||||
A.start();
|
||||
B.start();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
**Solution - Volatile Keyword**
|
||||
- The volatile keyword is used as modifier for class variables.
|
||||
- It's an indicator that this variable's value may be changed by multiple threads.
|
||||
- This modifier ensures that the variable is always read from, and written to the main memory, rather than from any thread-specific cache.
|
||||
- This provides memory consistency for this variables value across threads.
|
||||
Volatile doesn't gurantee atomicicty.
|
||||
|
||||
However, volatile does not provide atomicity or synchronization, so additional synchronization mechanisms should be used in conjunction with it when necessary.
|
||||
|
||||
**When to use volatile**
|
||||
- When a variable is used to track the state of a shared resource, such as counter or a flag.
|
||||
- When a varaible is used to communicate between threads.
|
||||
|
||||
**When not use volatile**
|
||||
- When the variable is used by single thread.
|
||||
- When a variable is used to store a large amount of data.
|
||||
### 3. Concurrent Data Structures
|
||||
There are data structures designed in Collections Framework which support Concurrency but we will limit our discussions to one of the widely asked data structures - Concurrent Hashmap.
|
||||
Java Collections provides various data structures for working with **key-value pairs**. The commonly used ones are -
|
||||
- **Hashmap** (Non-Synchronised, Not Thread Safe)
|
||||
- discuss the Synchronized Hashmap method
|
||||
|
||||
- **Hashtable** (Synchronised, Thread Safe)
|
||||
- locking over entire table
|
||||
|
||||
- **Concurrent Hashmap** (Synchronised, Thread Safe, Higher Level of Concurrency, Faster)
|
||||
- locking at bucket level, fine grained locking
|
||||
|
||||
**Hashmap and Synchronised Hashmap Method**
|
||||
Synchronization is the process of establishing coordination and ensuring proper communication between two or more activities. Since a HashMap is not synchronized which may cause data inconsistency, therefore, we need to synchronize it. The in-built method ‘Collections.synchronizedMap()’ is a more convenient way of performing this task.
|
||||
|
||||
A synchronized map is a map that can be safely accessed by multiple threads without causing concurrency issues. On the other hand, a Hash Map is not synchronized which means when we implement it in a multi-threading environment, multiple threads can access and modify it at the same time without any coordination. This can lead to data inconsistency and unexpected behavior of elements. It may also affect the results of an operation.
|
||||
|
||||
Therefore, we need to synchronize the access to the elements of Hash Map using ‘synchronizedMap()’. This method creates a wrapper around the original HashMap and locks it whenever a thread tries to access or modify it.
|
||||
|
||||
```java
|
||||
Collections.synchronizedMap(instanceOfHashMap);
|
||||
```
|
||||
|
||||
The `synchronizedMap()` is a static method of the Collections class that takes an instance of HashMap collection as a parameter and returns a synchronized Map from it. However,it is important to note that only the map itself is synchronized, not its views such as keyset and entrySet. Therefore, if we want to iterate over the synchronized map, we need to use a synchronized block or a lock to ensure exclusive access.
|
||||
|
||||
```java
|
||||
import java.util.*;
|
||||
public class Maps {
|
||||
public static void main(String[] args) {
|
||||
HashMap<String, Integer> cart = new HashMap<>();
|
||||
// Adding elements in the cart map
|
||||
cart.put("Butter", 5);
|
||||
cart.put("Milk", 10);
|
||||
cart.put("Rice", 20);
|
||||
cart.put("Bread", 2);
|
||||
cart.put("Peanut", 2);
|
||||
// printing synchronized map from HashMap
|
||||
Map mapSynched = Collections.synchronizedMap(cart);
|
||||
System.out.println("Synchronized Map from HashMap: " + mapSynched);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Hashtable vs Concurrent Hashmap**
|
||||
HashMap is generally suitable for single threaded applications and is faster than Hashtable, however in multithreading environments we have you use **Hashtable** or **Concurrent Hashmap**. So let us talk about them.
|
||||
|
||||
While both Hashtable and Concurrent Hashmap collections offer the advantage of thread safety, their underlying architectures and capabilities significantly differ. Whether we’re building a legacy system or working on modern, microservices-based cloud applications, understanding these nuances is critical for making the right choice.
|
||||
|
||||
Let's see the differences between Hashtable and ConcurrentHashMap, delving into their performance metrics, synchronization features, and various other aspects to help us make an informed decision.
|
||||
|
||||
**1. Hashtable**
|
||||
Hashtable is one of the oldest collection classes in Java and has been present since JDK 1.0. It provides key-value storage and retrieval APIs:
|
||||
|
||||
```java
|
||||
Hashtable<String, String> hashtable = new Hashtable<>();
|
||||
hashtable.put("Key1", "1");
|
||||
hashtable.put("Key2", "2");
|
||||
hashtable.putIfAbsent("Key3", "3");
|
||||
String value = hashtable.get("Key2");
|
||||
```
|
||||
**The primary selling point of Hashtable is thread safety, which is achieved through method-level synchronization**.
|
||||
|
||||
Methods like put(), putIfAbsent(), get(), and remove() are synchronized. Only one thread can execute any of these methods at a given time on a Hashtable instance, ensuring data consistency.
|
||||
|
||||
**2. Concurrent Hashmap**
|
||||
ConcurrentHashMap is a more modern alternative, introduced with the Java Collections Framework as part of Java 5.
|
||||
|
||||
Both Hashtable and ConcurrentHashMap implement the Map interface, which accounts for the similarity in method signatures:
|
||||
```java
|
||||
ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>();
|
||||
concurrentHashMap.put("Key1", "1");
|
||||
concurrentHashMap.put("Key2", "2");
|
||||
concurrentHashMap.putIfAbsent("Key3", "3");
|
||||
String value = concurrentHashMap.get("Key2");
|
||||
```
|
||||
|
||||
ConcurrentHashMap, on the other hand, provides thread safety with a higher level of concurrency. It allows multiple threads to read and perform limited writes simultaneously **without locking the entire data structure**. This is especially useful in applications that have more read operations than write operations.
|
||||
|
||||
**Performance Comparison**
|
||||
Hashtable locks the entire table during a write operation, thereby preventing other reads or writes. This could be a bottleneck in a high-concurrency environment.
|
||||
|
||||
ConcurrentHashMap, however, allows concurrent reads and limited concurrent writes, making it more scalable and often faster in practice.
|
||||
|
||||
|
||||
---
|
||||
## Coding Projects on Synchronisatoin
|
||||
### Coding Problem 3 - Ticket Booking System
|
||||
|
||||
Consider an online reservation system for booking tickets to various events. The system needs to handle concurrent requests from multiple users trying to reserve seats. To ensure thread safety and prevent race conditions, a Reentrant Lock can be employed.
|
||||
Requirements:
|
||||
- The reservation system manages the availability of seats for different events.
|
||||
- Multiple users can attempt to reserve seats concurrently.
|
||||
- A user should be able to reserve multiple seats for the same event.
|
||||
- The system should prevent overbooking and ensure the integrity of seat reservations.
|
||||
|
||||
|
||||
```java
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
class ReservationSystem {
|
||||
private int availableSeats;
|
||||
private final Lock lock = new ReentrantLock();
|
||||
|
||||
public ReservationSystem(int totalSeats) {
|
||||
this.availableSeats = totalSeats;
|
||||
}
|
||||
|
||||
public void reserveSeats(String user, int numSeats) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (numSeats > 0 && numSeats <= availableSeats) {
|
||||
// Simulate the reservation process
|
||||
System.out.println(user + " is reserving " + numSeats + " seats.");
|
||||
|
||||
// Update available seats
|
||||
availableSeats -= numSeats;
|
||||
|
||||
// Simulate the ticket issuance
|
||||
System.out.println(user + " reserved seats successfully.");
|
||||
} else {
|
||||
System.out.println(user + " could not reserve seats. Not enough available seats.");
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public int getAvailableSeats() {
|
||||
return availableSeats;
|
||||
}
|
||||
}
|
||||
|
||||
public class OnlineReservationSystem {
|
||||
public static void main(String[] args) {
|
||||
ReservationSystem reservationSystem = new ReservationSystem(50);
|
||||
|
||||
// Simulate multiple users trying to reserve seats concurrently
|
||||
Thread user1 = new Thread(() -> reservationSystem.reserveSeats("User1", 5));
|
||||
Thread user2 = new Thread(() -> reservationSystem.reserveSeats("User2", 10));
|
||||
Thread user3 = new Thread(() -> reservationSystem.reserveSeats("User3", 8));
|
||||
|
||||
user1.start();
|
||||
user2.start();
|
||||
user3.start();
|
||||
|
||||
try {
|
||||
user1.join();
|
||||
user2.join();
|
||||
user3.join();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
System.out.println("Remaining available seats: " + reservationSystem.getAvailableSeats());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example, the ReservationSystem class utilizes a Reentrant Lock (lock) to ensure that the reservation process is thread-safe. The reserveSeats method is enclosed in a try-finally block to ensure that the lock is always released, even if an exception occurs.
|
||||
This real-world problem demonstrates how Reentrant Locks can be used to synchronize access to shared resources in a multi-threaded environment, ensuring data consistency and preventing race conditions in a scenario like an online reservation system.
|
||||
|
||||
|
||||
### Coding Problem 4 - Thread-safe Bank Transactions
|
||||
**Problem Statement:**
|
||||
You are tasked with implementing a simple bank system that supports concurrent transactions. The bank has multiple accounts, and customers can deposit and withdraw money from their accounts concurrently. Implement a program that ensures the integrity of bank transactions by using threads.
|
||||
**Requirements:**
|
||||
- Each account has a unique account number and an initial balance.
|
||||
- Customers can concurrently deposit and withdraw money from their accounts.
|
||||
- The bank should ensure that the account balance remains consistent and does not go below zero during concurrent transactions.
|
||||
- Use threads to simulate multiple customers performing transactions simultaneously.
|
||||
|
||||
**Solution**
|
||||
|
||||
```java
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
class BankAccount {
|
||||
private final int accountNumber;
|
||||
private int balance;
|
||||
private final Lock lock = new ReentrantLock();
|
||||
|
||||
|
||||
public BankAccount(int accountNumber, int initialBalance) {
|
||||
this.accountNumber = accountNumber;
|
||||
this.balance = initialBalance;
|
||||
}
|
||||
|
||||
|
||||
public int getAccountNumber() {
|
||||
return accountNumber;
|
||||
}
|
||||
|
||||
|
||||
public int getBalance() {
|
||||
return balance;
|
||||
}
|
||||
|
||||
|
||||
public void deposit(int amount) {
|
||||
lock.lock();
|
||||
try {
|
||||
balance += amount;
|
||||
System.out.println("Deposited $" + amount + " to account " + accountNumber + ". New balance: $" + balance);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void withdraw(int amount) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (amount <= balance) {
|
||||
balance -= amount;
|
||||
System.out.println("Withdrawn $" + amount + " from account " + accountNumber + ". New balance: $" + balance);
|
||||
} else {
|
||||
System.out.println("Insufficient funds for withdrawal from account " + accountNumber);
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class BankTransaction implements Runnable {
|
||||
private final BankAccount account;
|
||||
private final int transactionAmount;
|
||||
|
||||
|
||||
public BankTransaction(BankAccount account, int transactionAmount) {
|
||||
this.account = account;
|
||||
this.transactionAmount = transactionAmount;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// Simulate a bank transaction (deposit or withdrawal)
|
||||
if (transactionAmount >= 0) {
|
||||
account.deposit(transactionAmount);
|
||||
} else {
|
||||
account.withdraw(Math.abs(transactionAmount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class BankSimulation {
|
||||
public static void main(String[] args) {
|
||||
BankAccount account1 = new BankAccount(101, 1000);
|
||||
BankAccount account2 = new BankAccount(102, 1500);
|
||||
|
||||
|
||||
// Simulate concurrent bank transactions using threads
|
||||
Thread thread1 = new Thread(new BankTransaction(account1, 200));
|
||||
Thread thread2 = new Thread(new BankTransaction(account1, -300));
|
||||
Thread thread3 = new Thread(new BankTransaction(account2, 500));
|
||||
|
||||
|
||||
thread1.start();
|
||||
thread2.start();
|
||||
thread3.start();
|
||||
|
||||
|
||||
try {
|
||||
thread1.join();
|
||||
thread2.join();
|
||||
thread3.join();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
|
||||
// Display final account balances
|
||||
System.out.println("Final balance for account " + account1.getAccountNumber() + ": Rs" + account1.getBalance());
|
||||
System.out.println("Final balance for account " + account2.getAccountNumber() + ": Rs" + account2.getBalance());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example, BankAccount represents a bank account with deposit and withdraw methods protected by a ReentrantLock. The BankTransaction class simulates a bank transaction (deposit or withdrawal), and the BankSimulation class demonstrates how threads can be used to perform concurrent transactions on multiple accounts. The use of locks ensures the thread safety of the bank transactions
|
||||
|
||||
## Additonal Reading
|
||||
- [Reentrant Locks](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantLock.html)
|
||||
- [Concurrent DataStructures](https://docs.oracle.com/javase/tutorial/essential/concurrency/collections.html)
|
||||
- [Fairness of Re-entrant Locks](https://docs.oracle.com/javase%2F7%2Fdocs%2Fapi%2F%2F/java/util/concurrent/locks/ReentrantLock.html)
|
||||
|
||||
|
||||
-- End --
|
||||
|
||||
|
@@ -0,0 +1,865 @@
|
||||
# Concurrency-4 Synchronization with Semaphores
|
||||
----
|
||||
## Agenda
|
||||
- Synchronisation using Semaphores
|
||||
- Producer Consumer Problem using Semaphores
|
||||
- Producer Consumer Problem using Concurrent Data Structure (Queue)
|
||||
- Print In Order LeetCode Problem
|
||||
- Deadlocks
|
||||
- Additional Topics
|
||||
- wait(), notify() methods
|
||||
- Producer Consumer Using wait() & notify()
|
||||
- Coding Projects (Optional)
|
||||
- Traffic Intersection Control
|
||||
- Resource Pooling in Library
|
||||
- LeetCode Problems
|
||||
- Additional Resources
|
||||
|
||||
## Synchronisation using Semaphores
|
||||
### Producer Consumer Problem : A T-Shirt Store Example
|
||||
|
||||
The Producer-Consumer problem is a classic synchronization problem where two processes, the producer and the consumer, share a common, fixed-size buffer or store. The producer produces items and adds them to the buffer, while the consumer consumes items from the buffer. Semaphores are synchronization primitives that can be used to solve this problem efficiently.
|
||||
|
||||
**Problem Description**
|
||||
Let's use a T-shirt store as an analogy for the Producer-Consumer problem. The T-shirt store has a limited capacity to store T-shirts. Producers can create T-shirts and add them to the store, and consumers can buy T-shirts from the store. The challenge is to ensure that the store doesn't overflow with T-shirts or run out of stock.
|
||||
|
||||
|
||||
|
||||
**Java Implementation-1 Using Semaphores**
|
||||
(simplified implementation than what is covered in class, here we don't maintain an actual queue for T-Shirts, just the count)
|
||||
|
||||
```java
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
public class TShirtStore {
|
||||
private static final int STORE_CAPACITY = 5;
|
||||
private static Semaphore mutex = new Semaphore(1); // Controls access to critical sections
|
||||
private static Semaphore empty = new Semaphore(STORE_CAPACITY); // Represents empty slots in the store
|
||||
private static Semaphore full = new Semaphore(0); // Represents filled slots in the store
|
||||
private static int tShirtCount = 0;
|
||||
|
||||
static class Producer implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
while (true) {
|
||||
empty.acquire(); // Wait for an empty slot
|
||||
mutex.acquire(); // Enter critical section
|
||||
|
||||
// Produce a T-shirt
|
||||
System.out.println("Producer produces a T-shirt. Total T-shirts: " + ++tShirtCount);
|
||||
|
||||
mutex.release(); // Exit critical section
|
||||
full.release(); // Signal that a T-shirt is ready to be consumed
|
||||
Thread.sleep(1000); // Simulate production time
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class Consumer implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
while (true) {
|
||||
full.acquire(); // Wait for a T-shirt to be available
|
||||
mutex.acquire(); // Enter critical section
|
||||
|
||||
// Consume a T-shirt
|
||||
System.out.println("Consumer buys a T-shirt. Total T-shirts: " + --tShirtCount);
|
||||
|
||||
mutex.release(); // Exit critical section
|
||||
empty.release(); // Signal that a slot is available for production
|
||||
Thread.sleep(1500); // Simulate consumption time
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Thread producerThread = new Thread(new Producer());
|
||||
Thread consumerThread = new Thread(new Consumer());
|
||||
|
||||
producerThread.start();
|
||||
consumerThread.start();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Semaphores
|
||||
|
||||
- **mutex**: Controls access to the critical sections (mutex stands for mutual exclusion).
|
||||
- **empty**: Represents the number of empty slots in the store, initially set to the store's capacity.
|
||||
- **full**: Represents the number of filled slots in the store, initially set to 0.
|
||||
|
||||
|
||||
**Producer:**
|
||||
|
||||
- The producer acquires an empty slot using empty.acquire() and enters the critical section with mutex.acquire().
|
||||
|
||||
- It produces a T-shirt, increments the count, releases the mutex, and signals that a T-shirt is ready for consumption using full.release().
|
||||
|
||||
**Consumer:**
|
||||
|
||||
- The consumer acquires a filled slot using full.acquire() and enters the critical section with mutex.acquire().
|
||||
- It consumes a T-shirt, decrements the count, releases the mutex, and signals that an empty slot is available for production using empty.release().
|
||||
|
||||
**Simulated Production and Consumption:**
|
||||
`Thread.sleep()` is used to simulate the time it takes to produce and consume T-shirts.
|
||||
**Execution**: When you run this program, you will observe the producer producing T-shirts and the consumer buying T-shirts. The store's capacity is maintained, and semaphores ensure proper synchronization between the producer and the consumer.
|
||||
|
||||
This example demonstrates how semaphores can be used to solve the Producer-Consumer problem efficiently, preventing issues such as overproduction or stockouts.
|
||||
|
||||
**Java Implementation -2 using Semaphores**
|
||||
**Producer.java**
|
||||
```java
|
||||
public class Producer implements Runnable{
|
||||
private Queue<Object> queue;
|
||||
private int maxSize;
|
||||
private String name;
|
||||
private Semaphore producerSemaphore;
|
||||
private Semaphore consumerSemaphore;
|
||||
|
||||
Producer(Queue<Object> queue, int maxSize, String name, Semaphore ps, Semaphore cs){
|
||||
this.queue = queue;
|
||||
this.maxSize = maxSize;
|
||||
this.name = name;
|
||||
this.producerSemaphore = ps;
|
||||
this.consumerSemaphore = cs;
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
while(true){
|
||||
try {
|
||||
producerSemaphore.acquire();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if(queue.size()<this.maxSize){
|
||||
System.out.println(this.name + " adding to queue, Size " + queue.size());
|
||||
queue.add(new Object());
|
||||
}
|
||||
consumerSemaphore.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
**Consumer.java**
|
||||
```java
|
||||
package Multithreading.ProducerConsumer;
|
||||
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
public class Consumer implements Runnable{
|
||||
private Queue<Object> queue;
|
||||
private int maxSize;
|
||||
private String name;
|
||||
private Semaphore producerSemaphore;
|
||||
private Semaphore consumerSemaphore;
|
||||
|
||||
Consumer(Queue<Object> queue, int maxSize, String name, Semaphore ps, Semaphore cs){
|
||||
this.queue = queue;
|
||||
this.maxSize = maxSize;
|
||||
this.name = name;
|
||||
this.producerSemaphore = ps;
|
||||
this.consumerSemaphore = cs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while(true){
|
||||
try {
|
||||
consumerSemaphore.acquire();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if(queue.size()>0){
|
||||
System.out.println(this.name + " removing from queue, Size " + queue.size());
|
||||
queue.remove();
|
||||
}
|
||||
producerSemaphore.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
**Client.java**
|
||||
|
||||
Here we create multiple producers and multiple consumers.
|
||||
```java
|
||||
public class Client {
|
||||
public static void main(String[] args) {
|
||||
Queue<Object> objects = new ConcurrentLinkedQueue<>();
|
||||
int maxSize = 6;
|
||||
Semaphore producerSemaphore = new Semaphore(maxSize);
|
||||
Semaphore consumerSemaphore = new Semaphore(0);
|
||||
|
||||
Producer p1 = new Producer(objects,6,"p1",producerSemaphore,consumerSemaphore);
|
||||
Producer p2 = new Producer(objects,6,"p2",producerSemaphore,consumerSemaphore);
|
||||
Producer p3 = new Producer(objects,6,"p3",producerSemaphore,consumerSemaphore);
|
||||
|
||||
Consumer c1 = new Consumer(objects,6,"c1",producerSemaphore,consumerSemaphore);
|
||||
Consumer c2 = new Consumer(objects,6,"c2",producerSemaphore,consumerSemaphore);
|
||||
Consumer c3 = new Consumer(objects,6,"c3",producerSemaphore,consumerSemaphore);
|
||||
Consumer c4 = new Consumer(objects,6,"c4",producerSemaphore,consumerSemaphore);
|
||||
Consumer c5 = new Consumer(objects,6,"c5",producerSemaphore,consumerSemaphore);
|
||||
|
||||
Thread t1 = new Thread(p1);
|
||||
Thread t2 = new Thread(p2);
|
||||
Thread t3 = new Thread(p3);
|
||||
Thread t4 = new Thread(c1);
|
||||
Thread t5 = new Thread(c2);
|
||||
Thread t6 = new Thread(c3);
|
||||
Thread t7 = new Thread(c4);
|
||||
Thread t8 = new Thread(c5);
|
||||
|
||||
t1.start();
|
||||
t2.start();
|
||||
t3.start();
|
||||
t4.start();
|
||||
t5.start();
|
||||
t6.start();
|
||||
t7.start();
|
||||
}
|
||||
}
|
||||
```
|
||||
**Java Implementation -3 using Concurrent Data Structure**
|
||||
Here is another implementation in which you can use a ConcurrentLinkedQueue in place of semaphores to ensure concurrency is handled well.
|
||||
**Producer.java**
|
||||
```java
|
||||
import java.util.Queue;
|
||||
|
||||
public class Producer implements Runnable{
|
||||
private Queue<Object> queue;
|
||||
int maxSize;
|
||||
String name;
|
||||
|
||||
public Producer(Queue<Object> queue,int maxSize, String name){
|
||||
this.queue = queue;
|
||||
this.maxSize = maxSize;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
//Each producer wants to continuously produces
|
||||
// T-Shirts and add them to the queue if there is space available
|
||||
|
||||
while(true){
|
||||
synchronized (queue){
|
||||
if(queue.size()<this.maxSize){
|
||||
System.out.println("Adding - "+ queue.size());
|
||||
queue.add(new Object());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
**Consumer.java**
|
||||
```java
|
||||
import java.util.Queue;
|
||||
|
||||
public class Consumer implements Runnable{
|
||||
private Queue<Object> queue;
|
||||
int maxSize;
|
||||
String name;
|
||||
|
||||
public Consumer(Queue<Object> queue,int maxSize, String name){
|
||||
this.queue = queue;
|
||||
this.maxSize = maxSize;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
//Each producer wants to continuously produces
|
||||
// T-Shirts and add them to the queue if there is space available
|
||||
while(true){
|
||||
synchronized (queue) {
|
||||
if (queue.size() > 0) {
|
||||
System.out.println("Removing - "+ queue.size());
|
||||
queue.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Main.java**
|
||||
```java
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
//Shared Object
|
||||
Queue<Object> q = new ConcurrentLinkedQueue<>();
|
||||
int maxSize = 6;
|
||||
|
||||
Producer p1 = new Producer(q,maxSize,"p1");
|
||||
Producer p2= new Producer(q,maxSize,"p2");
|
||||
Producer p3 = new Producer(q,maxSize,"p3");
|
||||
|
||||
Consumer c1 = new Consumer(q,maxSize,"c1");
|
||||
Consumer c2 = new Consumer(q,maxSize,"c2");
|
||||
Consumer c3 = new Consumer(q,maxSize,"c3");
|
||||
Consumer c4 = new Consumer(q,maxSize,"c4");
|
||||
Consumer c5 = new Consumer(q,maxSize,"c5");
|
||||
|
||||
Thread t1 = new Thread(p1);
|
||||
Thread t2 = new Thread(p2);
|
||||
Thread t3 = new Thread(p3);
|
||||
Thread t4 = new Thread(c1);
|
||||
Thread t5 = new Thread(c2);
|
||||
Thread t6 = new Thread(c3);
|
||||
Thread t7 = new Thread(c4);
|
||||
Thread t8 = new Thread(c5);
|
||||
|
||||
t1.start();
|
||||
t2.start();
|
||||
t3.start();
|
||||
t4.start();
|
||||
t5.start();
|
||||
t6.start();
|
||||
t7.start();
|
||||
t8.start();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Time To Try - Print In Order (LeetCode)
|
||||
Try to solve the following problem using Semaphores Concept.
|
||||
- [Print In Order - LeetCode](https://leetcode.com/problems/print-in-order/description/)
|
||||
|
||||
**Solution**
|
||||
```java
|
||||
class Foo {
|
||||
Semaphore semaSecond = new Semaphore(0);
|
||||
Semaphore semaThird = new Semaphore(0);
|
||||
public Foo() {
|
||||
|
||||
}
|
||||
public void first(Runnable printFirst) throws InterruptedException {
|
||||
printFirst.run();
|
||||
semaSecond.release();
|
||||
}
|
||||
public void second(Runnable printSecond) throws InterruptedException {
|
||||
semaSecond.acquire();
|
||||
printSecond.run();
|
||||
semaThird.release();
|
||||
}
|
||||
public void third(Runnable printThird) throws InterruptedException {
|
||||
semaThird.acquire();
|
||||
printThird.run();
|
||||
}
|
||||
}
|
||||
```
|
||||
## DeadLocks
|
||||
|
||||
A deadlock in OS is a situation in which more than one process is blocked because it is holding a resource and also requires some resource that is acquired by some other process.
|
||||
|
||||
### Conditions for a deadlock
|
||||
* `Mutual exclusion` - The resource is held by only one process at a time and cannot be acquired by another process.
|
||||
* `Hold and wait` - A process is **holding** a resource and **waiting** for another resource to be released by another a process.
|
||||
* `No preemption` - The resource can only be released once the execution of the process is complete.
|
||||
* `Circular wait` - A set of processes are waiting for each other circularly. Process `P1` is waiting for process `P2` and process `P2` is waiting for process `P1`.
|
||||
|
||||

|
||||
|
||||
Process P1 and P2 are in a deadlock because:
|
||||
* Resources are non-shareable. (Mutual exclusion)
|
||||
* Process 1 holds "Resource 1" and is waiting for "Resource 2" to be released by process 2. (Hold and wait)
|
||||
* None of the processes can be preempted. (No preemption)
|
||||
* "Resource 1" and needs "Resource 2" from Process 2 while Process 2 holds "Resource 2" and requires "Resource 1" from Process 1. (Circular wait)
|
||||
|
||||
### Tackling deadlocks
|
||||
|
||||
There are three ways to tackle deadlocks:
|
||||
* Prevention - Implementing a mechanism to prevent the deadlock.
|
||||
* Avoidance - Avoiding deadlocks by not allocating resources when deadlocks are possible.
|
||||
* Detecting and recovering - Detecting deadlocks and recovering from them.
|
||||
* Ignorance - Ignore deadlocks as they do not happen frequently.
|
||||
|
||||
#### 1. Prevention and avoidance
|
||||
|
||||
Deadlock prevention means to block at least one of the four conditions required for deadlock to occur. If we are able to block any one of them then deadlock can be prevented. Spooling and non-blocking synchronization algorithms are used to prevent the above conditions. In deadlock prevention all the requests are granted in a finite amount of time.
|
||||
|
||||
In Deadlock avoidance we have to anticipate deadlock before it really occurs and ensure that the system does not go in unsafe state.It is possible to avoid deadlock if resources are allocated carefully. For deadlock avoidance we use Banker’s and Safety algorithm for resource allocation purpose. In deadlock avoidance the maximum number of resources of each type that will be needed are stated at the beginning of the process.
|
||||
|
||||
#### 2. Detecting and recovering from deadlocks
|
||||
|
||||
We let the system fall into a deadlock and if it happens, we detect it using a detection algorithm and try to recover.
|
||||
|
||||
Some ways of recovery are as follows:
|
||||
|
||||
* Aborting all the deadlocked processes.
|
||||
* Abort one process at a time until the system recovers from the deadlock.
|
||||
* Resource Preemption: Resources are taken one by one from a process and assigned to higher priority processes until the deadlock is resolved.
|
||||
|
||||
#### 3. Ignorance
|
||||
|
||||
The system assumes that deadlock never occurs. Since the problem of deadlock situation is not frequent, some systems simply ignore it. Operating systems such as UNIX and Windows follow this approach. However, if a deadlock occurs we can reboot our system and the deadlock is resolved automatically.
|
||||
|
||||
#### 4. Tackling deadlocks at an application level
|
||||
* Set timeouts for all the processes. If a process does not respond within the timeout period, it is killed.
|
||||
* Implementing with caution: Use interfaces that handle or provide callbacks if locks are held by other processes.
|
||||
* Add timeout to locks: If a process requests a lock, and it is held by another process, it will wait for the lock to be released until the timeout expires.
|
||||
|
||||
## Additonal Topics
|
||||
### Inter-thread Communication using wait() and notify()
|
||||
[wait() & notify() - Recording Link](https://www.scaler.com/meetings/i/backend-lld-concurrency-callables-continued-3/archive)
|
||||
Certainly! In Java, the wait() and notify() methods are part of the built-in mechanism for inter-thread communication and synchronization. These methods are used to coordinate the activities of multiple threads, allowing them to work together effectively. Let's break down these concepts for beginners:
|
||||
|
||||
|
||||
#### wait() Method:
|
||||
- The wait() method is called on an object within a synchronized context (i.e., within a method or block synchronized on that object).
|
||||
- It causes the current thread to release the lock on the object and enter a state of waiting.
|
||||
Purpose:
|
||||
- wait() is used when a thread needs to wait for a certain condition to be met before proceeding.
|
||||
|
||||
For example, if a thread is waiting for a shared resource to be available, it can call wait() until another thread notifies it that the resource is ready.
|
||||
Example:
|
||||
|
||||
```java
|
||||
synchronized (sharedObject) {
|
||||
while (!conditionMet) {
|
||||
try {
|
||||
sharedObject.wait(); // Releases the lock and waits for notification
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
// Continue with the critical section
|
||||
}
|
||||
```
|
||||
#### notify() Method:
|
||||
- The notify() method is called on an object within a synchronized context.
|
||||
- It wakes up one of the threads that are currently waiting on that object.
|
||||
Purpose:
|
||||
|
||||
- notify() is used to signal that a condition (for which threads are waiting) has been met and that one of the waiting threads can proceed.
|
||||
- It is essential to note that notify() only wakes up one waiting thread. If there are multiple waiting threads, it is not determined which one will be awakened.
|
||||
Example:
|
||||
|
||||
```java
|
||||
synchronized (sharedObject) {
|
||||
// Perform some operations and change the condition
|
||||
conditionMet = true;
|
||||
|
||||
// Notify one of the waiting threads
|
||||
sharedObject.notify();
|
||||
}
|
||||
```
|
||||
|
||||
**Important Points:**
|
||||
- Both wait() and notify() must be called within a synchronized context to avoid illegal monitor state exceptions.
|
||||
- The calling thread must hold the lock on the object on which it is calling wait() or notify().
|
||||
- The wait() method releases the lock, allowing other threads to access the synchronized block or method.
|
||||
- The notify() method signals a waiting thread to wake up, allowing it to reacquire the lock and continue execution.
|
||||
|
||||
**Example Scenario:**
|
||||
Consider a scenario where multiple threads are working on a shared resource. If a thread finds that the resource is not yet available (e.g., a buffer is empty), it can call wait() to release the lock and wait until another thread populates the buffer and calls notify() to signal that the resource is ready for consumption.
|
||||
|
||||
In summary, wait() and notify() are fundamental methods for thread synchronization in Java, enabling threads to communicate and coordinate their activities efficiently.
|
||||
|
||||
**Producer Consumer using Wait() and Notify()**
|
||||
[Recording Link](https://www.scaler.com/meetings/i/backend-lld-concurrency-callables-continued-3/archive)
|
||||
|
||||
**Implementation -1 (Simplified)**
|
||||
Here is a simplifed version of Producer Consumer as discussed in above LIVE Class.
|
||||
**ProducerConsumer.java**
|
||||
```java
|
||||
public class ProducerConsumer {
|
||||
|
||||
public void produce() throws InterruptedException {
|
||||
synchronized (this){
|
||||
System.out.println("Produced - T-shirt");
|
||||
//release the lock on the shared resource and wait till some other invokes this
|
||||
wait();
|
||||
System.out.println("Going to produce another T-Shirt");
|
||||
}
|
||||
}
|
||||
|
||||
public void consume() throws InterruptedException {
|
||||
Thread.sleep(1000);
|
||||
Scanner sc = new Scanner(System.in);
|
||||
|
||||
synchronized (this) {
|
||||
System.out.println("Take t-shirt? ");
|
||||
sc.nextLine();
|
||||
System.out.println("Recieved T-shirt");
|
||||
notify();
|
||||
Thread.sleep(3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**PCDemo.java**
|
||||
```java
|
||||
public class PCDemo {
|
||||
public static void main(String[] args) {
|
||||
ProducerConsumer pc = new ProducerConsumer();
|
||||
|
||||
Thread t1 = new Thread(()->{
|
||||
try {
|
||||
pc.produce();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
Thread t2 = new Thread(()->{
|
||||
try {
|
||||
pc.consume();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
|
||||
t1.start();
|
||||
t2.start();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation-2**
|
||||
A more robust Implementation is as follows:
|
||||
```java
|
||||
import java.util.LinkedList;
|
||||
|
||||
class SharedBuffer {
|
||||
private final LinkedList<Integer> buffer = new LinkedList<>();
|
||||
private final int capacity;
|
||||
|
||||
public SharedBuffer(int capacity) {
|
||||
this.capacity = capacity;
|
||||
}
|
||||
|
||||
public void produce() throws InterruptedException {
|
||||
synchronized (this) {
|
||||
while (buffer.size() == capacity) {
|
||||
// Buffer is full, wait for consumer to consume
|
||||
wait();
|
||||
}
|
||||
|
||||
// Produce an item and add to the buffer
|
||||
int newItem = (int) (Math.random() * 100);
|
||||
buffer.add(newItem);
|
||||
System.out.println("Produced: " + newItem);
|
||||
|
||||
// Notify the consumer that an item is available
|
||||
notify();
|
||||
}
|
||||
}
|
||||
|
||||
public void consume() throws InterruptedException {
|
||||
synchronized (this) {
|
||||
while (buffer.isEmpty()) {
|
||||
// Buffer is empty, wait for producer to produce
|
||||
wait();
|
||||
}
|
||||
|
||||
// Consume an item from the buffer
|
||||
int consumedItem = buffer.removeFirst();
|
||||
System.out.println("Consumed: " + consumedItem);
|
||||
|
||||
// Notify the producer that a slot is available in the buffer
|
||||
notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Producer implements Runnable {
|
||||
private final SharedBuffer sharedBuffer;
|
||||
|
||||
public Producer(SharedBuffer sharedBuffer) {
|
||||
this.sharedBuffer = sharedBuffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
while (true) {
|
||||
sharedBuffer.produce();
|
||||
Thread.sleep(1000); // Simulate production time
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Consumer implements Runnable {
|
||||
private final SharedBuffer sharedBuffer;
|
||||
|
||||
public Consumer(SharedBuffer sharedBuffer) {
|
||||
this.sharedBuffer = sharedBuffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
while (true) {
|
||||
sharedBuffer.consume();
|
||||
Thread.sleep(1500); // Simulate consumption time
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ProducerConsumerExample {
|
||||
public static void main(String[] args) {
|
||||
SharedBuffer sharedBuffer = new SharedBuffer(5);
|
||||
|
||||
Thread producerThread = new Thread(new Producer(sharedBuffer));
|
||||
Thread consumerThread = new Thread(new Consumer(sharedBuffer));
|
||||
|
||||
producerThread.start();
|
||||
consumerThread.start();
|
||||
}
|
||||
}
|
||||
```
|
||||
In this example:
|
||||
|
||||
- `SharedBuffer` is the shared buffer where the producer produces and the consumer consumes items.
|
||||
- The Producer class produces items and adds them to the buffer.
|
||||
- The Consumer class consumes items from the buffer.
|
||||
- The main method creates instances of the shared buffer, producer, and consumer, and starts their respective threads.
|
||||
- This solution uses the `wait()` and `notify()` methods to ensure that the producer waits when the buffer is full and the consumer waits when the buffer is empty, allowing for proper coordination and synchronization between the two threads.
|
||||
|
||||
## Coding Projects
|
||||
### Coding Problem 1 : Traffic Intersection Control
|
||||
Implement a program that simulates a traffic intersection control system using Java Semaphores. The intersection has two roads, each with its own traffic signal. The traffic lights control the flow of traffic through the intersection.
|
||||
**Requirements:**
|
||||
- There are two roads, Road A and Road B, crossing at the intersection.
|
||||
- Each road has its own traffic signal (Semaphore) controlling the traffic flow.
|
||||
- The traffic lights for Road A and Road B have a cycle of Green, Yellow, and Red signals.
|
||||
Only one road should have a green light at a time, while the other road has a red light.
|
||||
The intersection should allow a smooth transition between green lights for both roads.
|
||||
|
||||
**Constraints:**
|
||||
- The time duration for each signal (Green, Yellow, Red) can be adjusted based on the program's design.
|
||||
- Use Semaphores to control access to the traffic signals and ensure a safe transition.
|
||||
Each road signal should run in a separate thread.
|
||||
- Implement a way to visually represent the current state of the traffic signals and indicate which road has the green light.
|
||||
|
||||
|
||||
Sample Output:
|
||||
|
||||
Road A: Green
|
||||
Road B: Red
|
||||
|
||||
[... Some time passes ...]
|
||||
|
||||
Road A: Yellow
|
||||
Road B: Red
|
||||
|
||||
[... Some time passes ...]
|
||||
|
||||
Road A: Red
|
||||
Road B: Green
|
||||
|
||||
|
||||
**Notes:**
|
||||
- The program should demonstrate proper synchronization to ensure that only one road has a green light at any given time.
|
||||
- You may choose to implement additional features such as a pedestrian signal or a button for road switching.
|
||||
- Consider the safety and efficiency of the intersection control system.
|
||||
- This problem statement reflects a real-world scenario where semaphores can be used to control access to shared resources (in this case, the green light for each road) in a concurrent environment. Students can implement the solution to gain hands-on experience with semaphore-based synchronization in a practical setting.
|
||||
|
||||
|
||||
**Solution:**
|
||||
|
||||
```java
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
class TrafficIntersectionControl {
|
||||
private Semaphore roadASemaphore = new Semaphore(1); // Semaphore for Road A's traffic signal
|
||||
private Semaphore roadBSemaphore = new Semaphore(0); // Semaphore for Road B's traffic signal
|
||||
|
||||
// Simulate time passing
|
||||
private void sleep(int seconds) {
|
||||
try {
|
||||
Thread.sleep(seconds * 1000);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
// Switch the traffic lights for Road A and Road B
|
||||
private void switchLights() {
|
||||
System.out.println("Switching lights...");
|
||||
|
||||
try {
|
||||
roadASemaphore.acquire(); // Acquire the semaphore for Road A
|
||||
roadBSemaphore.release(); // Release the semaphore for Road B
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate traffic on Road A
|
||||
private void trafficOnRoadA() {
|
||||
while (true) {
|
||||
System.out.println("Road A: Green");
|
||||
sleep(5); // Green light duration
|
||||
|
||||
System.out.println("Road A: Yellow");
|
||||
sleep(2); // Yellow light duration
|
||||
|
||||
System.out.println("Road A: Red");
|
||||
switchLights(); // Switch to Road B
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate traffic on Road B
|
||||
private void trafficOnRoadB() {
|
||||
while (true) {
|
||||
try {
|
||||
roadBSemaphore.acquire(); // Acquire the semaphore for Road B
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
System.out.println("Road B: Green");
|
||||
sleep(5); // Green light duration
|
||||
|
||||
System.out.println("Road B: Yellow");
|
||||
sleep(2); // Yellow light duration
|
||||
|
||||
System.out.println("Road B: Red");
|
||||
switchLights(); // Switch to Road A
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
TrafficIntersectionControl control = new TrafficIntersectionControl();
|
||||
|
||||
// Create and start threads for traffic on Road A and Road B
|
||||
Thread roadAThread = new Thread(control::trafficOnRoadA);
|
||||
Thread roadBThread = new Thread(control::trafficOnRoadB);
|
||||
|
||||
roadAThread.start();
|
||||
roadBThread.start();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Coding Problem - 2 Real-Life Problem Statement: Resource Pooling in a Library
|
||||
Imagine a library that has a collection of books, and multiple students want to borrow and return books from the library. Implement a program that uses Java Semaphores to control access to the books, ensuring that the available resources (books) are used efficiently.
|
||||
|
||||
**Requirements:**
|
||||
The library has a fixed number of books (resources) available for borrowing.
|
||||
Students can borrow books from the library and return them after reading.
|
||||
The library enforces a limit on the maximum number of students who can borrow books simultaneously.
|
||||
When a student returns a book, another student can borrow it if there is an available slot.
|
||||
|
||||
**Constraints:**
|
||||
Use Semaphores to control access to the shared resource (books).
|
||||
Each student should run in a separate thread.
|
||||
The program should handle the borrowing and returning of books concurrently.
|
||||
Implement a way to visually represent the current state of the library, indicating which books are borrowed and available.
|
||||
|
||||
**Example Output:**
|
||||
|
||||
Student 1 borrows Book A
|
||||
Library: [Book A is borrowed, Book B is available, Book C is available]
|
||||
|
||||
Student 2 borrows Book B
|
||||
Library: [Book A is borrowed, Book B is borrowed, Book C is available]
|
||||
|
||||
Student 1 returns Book A
|
||||
Library: [Book A is available, Book B is borrowed, Book C is available]
|
||||
|
||||
|
||||
…
|
||||
**Notes:**
|
||||
Ensure that the library's state is properly synchronized to avoid race conditions.
|
||||
You may choose to implement additional features, such as a waitlist for students.
|
||||
Consider scenarios where a student may need to wait if all books are currently borrowed.
|
||||
This problem statement reflects a real-world scenario where semaphores can be used to control access to a limited set of resources, ensuring that they are utilized efficiently and concurrently by multiple entities. Students can implement the solution to gain practical experience with semaphores in a resource pooling scenario.
|
||||
|
||||
**Sample Code**
|
||||
```java
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
class Library {
|
||||
private static final int MAX_STUDENTS = 2;
|
||||
private static final int MAX_BOOKS = 3;
|
||||
|
||||
private Semaphore availableBooks = new Semaphore(MAX_BOOKS, true);
|
||||
private Semaphore studentSlots = new Semaphore(MAX_STUDENTS, true);
|
||||
|
||||
// Simulate time passing
|
||||
private void sleep(int seconds) {
|
||||
try {
|
||||
Thread.sleep(seconds * 1000);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
// Borrow a book from the library
|
||||
private void borrowBook(String book, int studentId) {
|
||||
try {
|
||||
availableBooks.acquire(); // Acquire a book
|
||||
System.out.println("Student " + studentId + " borrows " + book);
|
||||
sleep(2); // Simulate reading time
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
// Return a book to the library
|
||||
private void returnBook(String book, int studentId) {
|
||||
System.out.println("Student " + studentId + " returns " + book);
|
||||
availableBooks.release(); // Release the returned book
|
||||
}
|
||||
|
||||
// Simulate a student using the library
|
||||
private void student(int studentId) {
|
||||
while (true) {
|
||||
try {
|
||||
studentSlots.acquire(); // Acquire a student slot
|
||||
String bookToBorrow = "Book " + (studentId % MAX_BOOKS + 1);
|
||||
borrowBook(bookToBorrow, studentId);
|
||||
returnBook(bookToBorrow, studentId);
|
||||
studentSlots.release(); // Release the student slot
|
||||
sleep(1); // Wait before the next operation
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Library library = new Library();
|
||||
|
||||
// Create and start multiple threads for students using the library
|
||||
for (int i = 1; i <= MAX_STUDENTS; i++) {
|
||||
int finalI = i;
|
||||
new Thread(() -> library.student(finalI)).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## LeetCode Multithreading Problems (HomeWork)
|
||||
- [Dining Philoshpers - LeetCode](https://leetcode.com/problems/the-dining-philosophers/)
|
||||
- [Fizz Buzz Multithreaded - LeetCode](https://leetcode.com/problems/fizz-buzz-multithreaded/)
|
||||
- [Building H20 - LeetCode]( https://leetcode.com/problems/building-h2o/)
|
||||
- [Traffic Light Control](https://leetcode.com/problems/traffic-light-controlled-intersection/description/)
|
||||
|
||||
## Additonal Resources
|
||||
- [Udemy - Concurrency, Multithreading and Parallel Computing in Java](https://www.udemy.com/course/multithreading-and-parallel-computing-in-java/)
|
||||
- [Memory Management in Operating Systems - Thrasing, Paging etc Interview Topics](https://github.com/kanmaytacker/fundamentals/blob/master/os/notes/04-memory-management.md)
|
||||
|
||||
-- End --
|
||||
|
||||
|
@@ -0,0 +1,172 @@
|
||||
# Asynchronous Programming vs Multithreading
|
||||
---
|
||||
|
||||
|
||||
## Asynchronous Programming
|
||||
Asynchronous programming is a programming paradigm that allows tasks to be executed independently without blocking the main thread. It focuses on managing the flow of the program by handling tasks concurrently and efficiently. It's commonly used to improve the responsiveness of applications by avoiding long-running operations that might otherwise cause the user interface to freeze. Java provides several mechanisms for asynchronous programming, and in this tutorial, we'll cover the basics using - Threads, CompletableFuture and the ExecutorService.
|
||||
|
||||
### 1. Threads
|
||||
We can create a new thread to perform any operation asynchronously. With the release of lambda expressions in Java 8, it’s cleaner and more readable.
|
||||
|
||||
Let’s create a new thread that computes and prints the factorial of a number:
|
||||
```java
|
||||
public class ThreadExample {
|
||||
public static int factorial(int n){
|
||||
|
||||
System.out.println(Thread.currentThread().getName() + "is running");
|
||||
|
||||
try{
|
||||
Thread.sleep(2000);
|
||||
}
|
||||
catch(InterruptedException e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
int ans=1;
|
||||
for(int i=1;i<n;i++){
|
||||
ans = ans*i;
|
||||
}
|
||||
System.out.println(Thread.currentThread().getName() + "is finished");
|
||||
return ans;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
int number = 5;
|
||||
Thread newThread = new Thread(()->{
|
||||
System.out.println("Factorial of 5 " + factorial(number));
|
||||
});
|
||||
newThread.start();
|
||||
System.out.println("Main is still running-1");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. FutureTask
|
||||
Since Java 5, the Future interface provides a way to perform asynchronous operations using the FutureTask. We can use the submit method of the ExecutorService to perform the task asynchronously and return the instance of the FutureTask.
|
||||
```java
|
||||
ExecutorService threadpool = Executors.newCachedThreadPool();
|
||||
Future<Long> futureTask = threadpool.submit(() -> factorial(number));
|
||||
|
||||
while (!futureTask.isDone()) {
|
||||
System.out.println("FutureTask is not finished yet...");
|
||||
}
|
||||
long result = futureTask.get(); //Blocking Code
|
||||
threadpool.shutdown();
|
||||
```
|
||||
Here we’ve used the `isDone` method provided by the Future interface to check if the task is completed. Once finished, we can retrieve the result using the get method.
|
||||
|
||||
### 3. CompletableFuture
|
||||
CompletableFuture is a class introduced in Java 8 that provides a way to perform asynchronous operations and handle their results using a fluent API. Java 8 introduced CompletableFuture with a combination of a Future and CompletionStage. It provides various methods like supplyAsync, runAsync, and thenApplyAsync for asynchronous programming.
|
||||
|
||||
**Example-1**
|
||||
```java
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class CompletableFutureExample {
|
||||
public static void main(String[] args) {
|
||||
// Create a CompletableFuture
|
||||
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
|
||||
// Simulate a time-consuming task
|
||||
try {
|
||||
Thread.sleep(2000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return "Hello, CompletableFuture!";
|
||||
});
|
||||
|
||||
// Attach a callback to handle the result
|
||||
future.thenAccept(result -> System.out.println("Result: " + result));
|
||||
|
||||
// Wait for the CompletableFuture to complete (not recommended in real applications)
|
||||
try {
|
||||
future.get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
In this example, we use `CompletableFuture.supplyAsync` to perform a task asynchronously. The thenAccept method is used to attach a callback that will be executed when the asynchronous task completes.
|
||||
|
||||
**Example-2**
|
||||
```
|
||||
CompletableFuture<Long> completableFuture = CompletableFuture.supplyAsync(() -> factorial(number));
|
||||
while (!completableFuture.isDone()) {
|
||||
System.out.println("CompletableFuture is not finished yet...");
|
||||
}
|
||||
long result = completableFuture.get();
|
||||
```
|
||||
We don’t need to use the ExecutorService explicitly. The CompletableFuture internally uses ForkJoinPool to handle the task asynchronously. Thus, it makes our code a lot cleaner.
|
||||
|
||||
|
||||
### Uses of Asynchronous Programming:
|
||||
|
||||
- IO-Intensive Operations: Asynchronous programming is often used for tasks that involve waiting for external resources, such as reading from or writing to files, making network requests, or interacting with databases.
|
||||
- Responsive UI: In GUI applications, asynchronous programming helps in maintaining a responsive user interface by executing time-consuming tasks in the background.
|
||||
|
||||
- Callback Mechanism: Asynchronous programming often uses callbacks or combinators to specify what should happen once a task is complete.
|
||||
|
||||
- Composability: It emphasizes composability, allowing developers to chain together multiple asynchronous operations.
|
||||
|
||||
```java
|
||||
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello, CompletableFuture!");
|
||||
|
||||
// Attach a callback to handle the result
|
||||
future.thenAccept(result -> System.out.println("Result: " + result));
|
||||
```
|
||||
|
||||
### Multi-threading
|
||||
Multithreading involves the concurrent execution of two or more threads to achieve parallelism. It is a fundamental concept for optimizing CPU-bound tasks and improving overall system performance. Key Components for achieving multithreading in Java are thread class and Executor Framework.
|
||||
|
||||
- Thread Class: Java provides the Thread class for creating and managing threads.
|
||||
- Executor Framework: The ExecutorService and related interfaces offer a higher-level abstraction for managing thread pools.
|
||||
|
||||
### Use Case of Multithreading:
|
||||
|
||||
- CPU-Intensive Operations: Multithreading is suitable for tasks that are CPU-bound and can benefit from parallel execution, such as mathematical computations.
|
||||
|
||||
- Parallel Processing: Multithreading can be used to perform multiple tasks simultaneously, making efficient use of available CPU cores.
|
||||
|
||||
### Shared State and Synchronization:
|
||||
|
||||
- Shared State: In multithreading, threads may share data, leading to potential issues like race conditions and data corruption.
|
||||
|
||||
- Synchronization: Techniques like synchronization, locks, and atomic operations are used to ensure proper coordination between threads.
|
||||
|
||||
Example using ExecutorService:
|
||||
```java
|
||||
ExecutorService executorService = Executors.newFixedThreadPool(2);
|
||||
|
||||
// Submit a task for execution
|
||||
Future<String> future = executorService.submit(() -> "Hello, ExecutorService!");
|
||||
|
||||
// Retrieve the result when ready
|
||||
try {
|
||||
String result = future.get(); // This will block until the result is available
|
||||
System.out.println("Result: " + result);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
// Shutdown the ExecutorService
|
||||
executorService.shutdown();
|
||||
```
|
||||
|
||||
### Summary
|
||||
### Asynchronous Programming:
|
||||
- Focuses on non-blocking execution.
|
||||
- Primarily used for IO-bound tasks and maintaining responsive applications.
|
||||
- Utilizes higher-level abstractions like CompletableFuture.
|
||||
- Emphasizes composability and chaining of asynchronous operations.
|
||||
|
||||
### Multithreading:
|
||||
- Focuses on parallelism for CPU-bound tasks.
|
||||
- Suitable for tasks that can be executed concurrently.
|
||||
- Utilizes threads and thread pools, managed by the Thread class and ExecutorService.
|
||||
- Requires attention to synchronization and shared state management.
|
||||
|
||||
In some scenarios, asynchronous programming and multithreading can be used together to achieve both parallelism and non-blocking execution, depending on the nature of the tasks in an application.
|
@@ -0,0 +1,82 @@
|
||||
# Concurrent Hashmap
|
||||
|
||||
Java Collections provides various data structures for working with key-value pairs. The commonly used ones are -
|
||||
- **Hashmap** (Non-Synchronised, Not Thread Safe)
|
||||
- discuss the Synchronized Hashmap method
|
||||
|
||||
- **Hashtable** (Synchronised, Thread Safe)
|
||||
- locking over entire table
|
||||
|
||||
- **Concurrent Hashmap** (Synchronised, Thread Safe, Higher Level of Concurrency, Faster)
|
||||
- locking at bucket level, fine grained locking
|
||||
|
||||
**Hashmap and Synchronised Hashmap Method**
|
||||
Synchronization is the process of establishing coordination and ensuring proper communication between two or more activities. Since a HashMap is not synchronized which may cause data inconsistency, therefore, we need to synchronize it. The in-built method ‘Collections.synchronizedMap()’ is a more convenient way of performing this task.
|
||||
|
||||
A synchronized map is a map that can be safely accessed by multiple threads without causing concurrency issues. On the other hand, a Hash Map is not synchronized which means when we implement it in a multi-threading environment, multiple threads can access and modify it at the same time without any coordination. This can lead to data inconsistency and unexpected behavior of elements. It may also affect the results of an operation.
|
||||
|
||||
Therefore, we need to synchronize the access to the elements of Hash Map using ‘synchronizedMap()’. This method creates a wrapper around the original HashMap and locks it whenever a thread tries to access or modify it.
|
||||
|
||||
```java
|
||||
Collections.synchronizedMap(instanceOfHashMap);
|
||||
```
|
||||
|
||||
The `synchronizedMap()` is a static method of the Collections class that takes an instance of HashMap collection as a parameter and returns a synchronized Map from it. However,it is important to note that only the map itself is synchronized, not its views such as keyset and entrySet. Therefore, if we want to iterate over the synchronized map, we need to use a synchronized block or a lock to ensure exclusive access.
|
||||
|
||||
```java
|
||||
import java.util.*;
|
||||
public class Maps {
|
||||
public static void main(String[] args) {
|
||||
HashMap<String, Integer> cart = new HashMap<>();
|
||||
// Adding elements in the cart map
|
||||
cart.put("Butter", 5);
|
||||
cart.put("Milk", 10);
|
||||
cart.put("Rice", 20);
|
||||
cart.put("Bread", 2);
|
||||
cart.put("Peanut", 2);
|
||||
// printing synchronized map from HashMap
|
||||
Map mapSynched = Collections.synchronizedMap(cart);
|
||||
System.out.println("Synchronized Map from HashMap: " + mapSynched);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Hashtable vs Concurrent Hashmap**
|
||||
HashMap is generally suitable for single threaded applications and is faster than Hashtable, however in multithreading environments we have you use **Hashtable** or **Concurrent Hashmap**. So let us talk about them.
|
||||
|
||||
While both Hashtable and Concurrent Hashmap collections offer the advantage of thread safety, their underlying architectures and capabilities significantly differ. Whether we’re building a legacy system or working on modern, microservices-based cloud applications, understanding these nuances is critical for making the right choice.
|
||||
|
||||
Let's see the differences between Hashtable and ConcurrentHashMap, delving into their performance metrics, synchronization features, and various other aspects to help us make an informed decision.
|
||||
|
||||
**1. Hashtable**
|
||||
Hashtable is one of the oldest collection classes in Java and has been present since JDK 1.0. It provides key-value storage and retrieval APIs:
|
||||
|
||||
```java
|
||||
Hashtable<String, String> hashtable = new Hashtable<>();
|
||||
hashtable.put("Key1", "1");
|
||||
hashtable.put("Key2", "2");
|
||||
hashtable.putIfAbsent("Key3", "3");
|
||||
String value = hashtable.get("Key2");
|
||||
```
|
||||
**The primary selling point of Hashtable is thread safety, which is achieved through method-level synchronization**.
|
||||
|
||||
Methods like put(), putIfAbsent(), get(), and remove() are synchronized. Only one thread can execute any of these methods at a given time on a Hashtable instance, ensuring data consistency.
|
||||
|
||||
**2. Concurrent Hashmap**
|
||||
ConcurrentHashMap is a more modern alternative, introduced with the Java Collections Framework as part of Java 5.
|
||||
|
||||
Both Hashtable and ConcurrentHashMap implement the Map interface, which accounts for the similarity in method signatures:
|
||||
```java
|
||||
ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>();
|
||||
concurrentHashMap.put("Key1", "1");
|
||||
concurrentHashMap.put("Key2", "2");
|
||||
concurrentHashMap.putIfAbsent("Key3", "3");
|
||||
String value = concurrentHashMap.get("Key2");
|
||||
```
|
||||
|
||||
ConcurrentHashMap, on the other hand, provides thread safety with a higher level of concurrency. It allows multiple threads to read and perform limited writes simultaneously **without locking the entire data structure**. This is especially useful in applications that have more read operations than write operations.
|
||||
|
||||
**Performance Comparison**
|
||||
Hashtable locks the entire table during a write operation, thereby preventing other reads or writes. This could be a bottleneck in a high-concurrency environment.
|
||||
|
||||
ConcurrentHashMap, however, allows concurrent reads and limited concurrent writes, making it more scalable and often faster in practice.
|
@@ -0,0 +1,136 @@
|
||||
# Functional Programming in Java
|
||||
---
|
||||
Functional Programming (FP) is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. In Java, functional programming features were introduced in Java 8 with the addition of lambda expressions, the `java.util.function` package, and the Stream API. Here are the key concepts of Functional Programming in Java:
|
||||
- Lambda Expressions
|
||||
- Functional Interfaces
|
||||
- Stream API
|
||||
- Immutabilitity
|
||||
- Higher Order Functions
|
||||
- Parallelism
|
||||
|
||||
### 1. Lambda Expressions:
|
||||
Lambda expressions are a concise way to represent anonymous functions. They provide a clear and concise syntax for writing functional interfaces (interfaces with a single abstract method). Lambda expressions are the cornerstone of functional programming in Java.
|
||||
|
||||
```java
|
||||
// Traditional anonymous class
|
||||
Runnable runnable1 = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println("Hello, world!");
|
||||
}
|
||||
};
|
||||
|
||||
// Lambda expression
|
||||
Runnable runnable2 = () -> System.out.println("Hello, world!");
|
||||
```
|
||||
|
||||
### 2. Functional Interfaces:
|
||||
Functional interfaces are interfaces with a single abstract method, often referred to as functional methods. They can have multiple default or static methods, but they must have only one abstract method.
|
||||
```java
|
||||
@FunctionalInterface
|
||||
interface MyFunctionalInterface {
|
||||
void myMethod();
|
||||
}
|
||||
```
|
||||
Lambda expressions can be used to instantiate functional interfaces:
|
||||
```java
|
||||
MyFunctionalInterface myFunc = () -> System.out.println("My method implementation");
|
||||
```
|
||||
|
||||
In Java, the `java.util.function` package provides several functional interfaces that represent different types of functions. These functional interfaces are part of the functional programming support introduced in Java 8 and are commonly used with lambda expressions. Here's an explanation of some commonly used functional interfaces in Java:
|
||||
|
||||
##### Function<T, R>
|
||||
Represents a function that takes one argument of type T and produces a result of type R.
|
||||
The method `apply(T t)` is used to apply the function.
|
||||
```java
|
||||
Function<String, Integer> stringLengthFunction = s -> s.length();
|
||||
int length = stringLengthFunction.apply("Java");
|
||||
```
|
||||
|
||||
##### Consumer<T>
|
||||
Represents an operation that accepts a single input argument of type T and returns no result. The method `accept(T t)` is used to perform the operation.
|
||||
```java
|
||||
Consumer<String> printUpperCase = s -> System.out.println(s.toUpperCase());
|
||||
printUpperCase.accept("Java");
|
||||
```
|
||||
|
||||
##### BiFunction<T,U,R>
|
||||
Represents a function that takes two arguments of types T and U and produces a result of type R. The method `apply(T t, U u)` is used to apply the function.
|
||||
```java
|
||||
BiFunction<Integer, Integer, Integer> sumFunction = (a, b) -> a + b;
|
||||
int sum = sumFunction.apply(3, 5);
|
||||
```
|
||||
##### Predicate<T>
|
||||
Represents a predicate (boolean-valued function) that takes one argument of type T.
|
||||
The method `test(T t)` is used to test the predicate
|
||||
```java
|
||||
Predicate<Integer> isEven = n -> n % 2 == 0;
|
||||
boolean result = isEven.test(4); // true
|
||||
```
|
||||
##### Supplier<T>
|
||||
Represents a supplier of results.
|
||||
The method get() is used to get the result.
|
||||
```java
|
||||
Supplier<Double> randomNumberSupplier = () -> Math.random();
|
||||
double randomValue = randomNumberSupplier.get();
|
||||
```
|
||||
|
||||
These functional interfaces facilitate the use of lambda expressions and support the functional programming paradigm in Java. They can be used in various contexts, such as with the Stream API, to represent transformations, filters, and other operations on collections of data. The introduction of these functional interfaces in Java 8 enhances code readability and expressiveness.
|
||||
|
||||
### 3. Streams
|
||||
Streams provide a functional approach to processing sequences of elements. They allow you to express complex data manipulations using a pipeline of operations, such as map, filter, and reduce. Streams are part of the `java.util.stream` package.
|
||||
|
||||
```java
|
||||
List<String> strings = Arrays.asList("abc", "def", "ghi", "jkl");
|
||||
|
||||
// Filter strings starting with 'a' and concatenate them
|
||||
String result = strings.stream()
|
||||
.filter(s -> s.startsWith("a"))
|
||||
.map(String::toUpperCase)
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
System.out.println(result); // Output: ABC
|
||||
```
|
||||
### 4. Immutablility
|
||||
Functional programming encourages immutability, where objects once created cannot be changed. In Java, you can use the final keyword to create immutable variables.
|
||||
|
||||
The immutability is a big thing in a multithreaded application. It allows a thread to act on an immutable object without worrying about the other threads because it knows that no one is modifying the object. So the immutable objects are more thread safe than the mutable objects. If you are into concurrent programming, you know that the immutability makes your life simple.
|
||||
|
||||
### 5. Higher-Order Functions:
|
||||
Functional programming supports higher-order functions, which are functions that can take other functions as parameters or return functions as results. Higher-order functions are a key concept in functional programming, enabling a more expressive and modular coding style. Java, starting from version 8, introduced support for higher-order functions with the introduction of lambda expressions and the `java.util.function` package.
|
||||
|
||||
|
||||
```java
|
||||
// Function that takes a function as a parameter
|
||||
public static void processNumbers(List<Integer> numbers, Function<Integer, Integer> processor) {
|
||||
for (int i = 0; i < numbers.size(); i++) {
|
||||
numbers.set(i, processor.apply(numbers.get(i)));
|
||||
}
|
||||
}
|
||||
|
||||
// Usage of higher-order function
|
||||
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
|
||||
processNumbers(numbers, x -> x * 2);
|
||||
System.out.println(numbers); // Output: [2, 4, 6, 8, 10]
|
||||
```
|
||||
|
||||
### 7. Parallelism:
|
||||
Functional programming encourages writing code that can easily be parallelized. The Stream API provides methods for parallel execution of operations on streams.
|
||||
```java
|
||||
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
|
||||
|
||||
// Parallel stream processing
|
||||
int sum = numbers.parallelStream()
|
||||
.mapToInt(Integer::intValue)
|
||||
.sum();
|
||||
System.out.println(sum); // Output: 15
|
||||
```
|
||||
---
|
||||
## Benefits of Functional Programming in Java
|
||||
- **Conciseness**: Lambda expressions make code more concise and readable.
|
||||
- **Parallelism**: Easier to parallelize code due to immutability and statelessness.
|
||||
- **Predictability**: Immutability reduces side effects and makes code more predictable.
|
||||
- **Testability**: Functions with no side effects are easier to test.
|
||||
- **Modularity**: Encourages modular and reusable code.
|
||||
|
||||
Functional programming in Java complements the existing object-oriented programming paradigm and provides developers with powerful tools to write more expressive, modular, and maintainable code. It promotes the use of pure functions, immutability, and higher-order functions, leading to code that is often more concise and easier to reason about.
|
@@ -0,0 +1,276 @@
|
||||
|
||||
# Chapter - Garbage Collection in Java
|
||||
-----
|
||||
|
||||
|
||||
### Introduction
|
||||
|
||||
One of the reasons that make Java as a robust programming language is its memory management. Memory management can be a difficult, tedious task in traditional programming environments. For example, in C/C++, the programmer will often manually allocate and free dynamic memory. This sometimes leads to problems, because programmers will either forget to free memory that has been previously allocated or, worse, try to free some memory that another part of their code is still using. Java virtually eliminates these problems by managing memory allocation and deallocation for you. In fact, deallocation is completely automatic, because Java provides garbage collection for unused objects. In this tutorial, we will study the following topics.
|
||||
|
||||
**Part-I**
|
||||
- Java Memory Model (Stack and Heap)
|
||||
- Need for Garbage Collection (Operations, Benefits, Disadvantanges)
|
||||
- Benefits and Disadvantages of GC
|
||||
|
||||
**Part-II**
|
||||
- Memory Allocation, Defragmentation and Garbage Collection
|
||||
- Conditions for Garbage Collector to run
|
||||
- Garbage Collection for Java Objects
|
||||
- Handling unmanaged resources
|
||||
|
||||
**Part-III**
|
||||
- Choosing a Garbage Collection Algorithm
|
||||
- Understanding Mark and Sweep
|
||||
- Garbage Collectors in Java 17
|
||||
|
||||
### Java Memory Model - Stack vs Heap
|
||||
Applications need memory to run, because they need to create objects in the memory and perform computational tasks. These can be created on Stack and Heap Memory. Lets us quickly discuss the features of stack and heap memory.
|
||||
|
||||
Local primitive variables and reference variables to objects data types are created on stack memory and cleared automatically when the stack frame is popped after the function call gets over. Hence, everything associated when stack memory gets cleared off automatically following the LIFO order in the call stack. There is no garbage collection involved in stack memory. Because of simplicity in memory allocation (LIFO), stack memory is very fast when compared to heap memory.
|
||||
|
||||
```java
|
||||
func(){
|
||||
int a = 10; //here 'a' is created on stack
|
||||
int arr[] = new int[10];
|
||||
// here arr reference is created on stack but actual allocation is on heap
|
||||
...
|
||||
}
|
||||
```
|
||||
However, you need heap memory when you need to allocate any kind of objects like arrays, user defined objects, dynamic data structures such as arraylist, strings, trees etc
|
||||
|
||||
Whenever an object is created, it’s always stored in the Heap space and stack memory contains the reference to it. Objects stored in the heap are globally accessible whereas stack memory can’t be accessed by other threads. Creating objects on heap also allows passing large objects by reference across different functions, thus avoiding the need to create a copy of the object. For such objects on heap de-allocation is required for unused objects, which can be performed explicitly by invoking `delete` in langages like C++. But language like Java, Python provide support for automatic garbage collection.
|
||||
|
||||
When stack memory is full, Java runtime throws `java.lang.StackOverFlowError` whereas if heap memory is full, it throws `java.lang.OutOfMemoryError: Java Heap Space error`. Stack memory size is very less when compared to Heap memory. We can use `-Xms` and `-Xmx` JVM option to define the startup size and maximum size of heap memory. We can use `-Xss` to define the stack memory size.
|
||||
|
||||
|
||||
### Need for Garbage Collection
|
||||
|
||||
The garbage collector manages the allocation and release of memory for an application. Therefore, developers working with managed code don't have to write code to perform memory management tasks. Automatic memory management can eliminate common problems such as forgetting to free an object and causing a memory leak or attempting to access freed memory for an object that's already been freed.
|
||||
|
||||
**Operations performed by a Garbage Collector**
|
||||
- Allocates from and gives back memory to the operating system.
|
||||
- Hands out that memory to the application as it requests it.
|
||||
- Determines which parts of that memory is still in use by the application.
|
||||
- Reclaims the unused memory for reuse by the application.
|
||||
- Running memory defragmentation.
|
||||
|
||||
**Benefits of Garbage Collector**
|
||||
- Frees developers from having to manually release memory.
|
||||
- Allocates objects on the managed heap efficiently.
|
||||
- Reclaims objects that are no longer being used, clears their memory, and keeps the memory available for future allocations.
|
||||
- Provides memory safety by making sure that an object can't use for itself the memory allocated for another object.
|
||||
- No overhead of handling Dangling Pointer
|
||||
|
||||
**Disadvantages of Garbage Collector**
|
||||
- Java garbage collection helps your Java environments and applications perform more efficiently. However, you can still potentially run into issues with automatic garbage collection, including degraded application performance.
|
||||
- Since JVM has to keep track of object reference creation/deletion, this activity requires more CPU power than the original application. It may affect the performance of requests which require large memory.
|
||||
- Programmers have no control over the scheduling of CPU time dedicated to freeing objects that are no longer needed.
|
||||
- Using some GC implementations might result in the application stopping unpredictably.
|
||||
|
||||
|
||||
While you can’t manually override automatic garbage collection, there are things you can do to optimize garbage collection in your application environment, such as changing the garbage collector you use, removing all references to unused Java objects, tuning the parameters of Garbage collector etc.
|
||||
|
||||
**Memory Allocation, Defragmentation & Garbage Collection**
|
||||
We’ve seen how heap memory can provide a flexible way of allocation chunks of memory on-the-go. The chunks aren’t planned ahead of time; it’s a real-time thing: when the program, for whatever reason, needs more memory, then the operating system finds an available chunk and allocates that chunk to the program.The program can use it until it’s done with that chunk, at which time it releases the chunk for later use by the same or a different program.
|
||||

|
||||
|
||||
After some time, the memory might look like this.
|
||||

|
||||
|
||||
Now what? There’s enough free memory for the new allocation, but the problem is that it’s all broken up all over the place. Said another way, it’s fragmented. We really don’t want to break up the allocation and spread it over multiple holes. That would use more memory (for managing where all the pieces are), and it would slow things down.
|
||||
|
||||
So we’re left with the problem: how do we allocate that new chunk? We first need to reorganize the memory and move things around to get all of those holes together into a larger chunk of available memory. That means closing up the holes and “pushing” the holes to the end of the memory where they can be reused.
|
||||
|
||||
That process of moving things around to bring the free memory chunks together is called **defragmentation**.The process of defragmenting memory by moving multiple free “holes” in memory together so that they can be allocated more effectively.And yeah, it takes some time to do. It’s also hard to predict when it will be needed, since it all depends on who needs memory and releases memory at what time. The process is fast enough to where you may not notice it, but it can make a difference.
|
||||
|
||||

|
||||
|
||||
**Pseudocode for New()**
|
||||
```java
|
||||
def new():
|
||||
obj = allocate() //request for memory
|
||||
if obj == NULL:
|
||||
GC.collect() //trigger garbage collector
|
||||
obj = allocate() //re-try to allocate memory
|
||||
if obj == NULL: //no garbage was collected or not sufficient memory
|
||||
raise OutOfMemoryError
|
||||
|
||||
return obj
|
||||
```
|
||||
|
||||
**Important Note**
|
||||
Garbage collection only occurs sporadically (if at all) during the execution of your program. It will not occur simply because one or more objects exist that are no longer used. Furthermore, different Java run-time implementations will take varying approaches to garbage collection, but for the most developers, you should not have to think about it while writing your programs. The classes in the **java.lang.ref** package provide more flexible control over the garbage collection process.
|
||||
|
||||
There are various ways in which the references to an object can be released to make it a candidate for Garbage Collection. Some of them are:
|
||||
|
||||
**By making a reference null**
|
||||
```java
|
||||
Student student = new Student();
|
||||
student = null;
|
||||
```
|
||||
|
||||
**By assigning a reference to another**
|
||||
```java
|
||||
Student studentOne = new Student();
|
||||
Student studentTwo = new Student();
|
||||
studentOne = studentTwo;
|
||||
```
|
||||
**Conditions for a Garbage Collector to run**
|
||||
Garbage collection occurs when one of the following conditions is true:
|
||||
|
||||
- The system has low physical memory. The memory size is detected by either the low memory notification from the operating system or low memory as indicated by the host.
|
||||
|
||||
- The memory that's used by allocated objects on the managed heap surpasses an acceptable threshold. This threshold is continuously adjusted as the process runs.
|
||||
|
||||
- The GC.Collect() method is called. In almost all cases, you don't have to call this method because the garbage collector runs continuously. This method is primarily used for unique situations and testing.
|
||||
|
||||
**Handling unmanaged resources and finalize() Method**
|
||||
For most of the objects your application creates, you can rely on garbage collection to perform the necessary memory management tasks automatically. However, unmanaged resources require explicit cleanup. The most common type of unmanaged resource is an object that wraps an operating system resource, such as a file handle, window handle, or network connection. Although the garbage collector can track the lifetime of a managed object that encapsulates an unmanaged resource, it doesn't have specific knowledge about how to clean up the resource. finalize() method in Java is a method of the Object class that is used to perform cleanup activity before destroying any object. It is called by Garbage collector before destroying the objects from memory. You can either use a safe handle to wrap the unmanaged resource, or override the Object.Finalize() method. `finalize()` method is called by default for every object before its deletion. This method helps Garbage Collector to close all the resources used by the object and helps JVM in-memory optimization.
|
||||
|
||||
-----
|
||||
### Choice of a Garbage Collector Algorithm
|
||||
|
||||
Any garbage collection algorithm must perform 2 basic operations. One, it should be able to detect all the unreachable objects and secondly, it must reclaim the heap space used by the garbage objects and make the space available again to the program.
|
||||
|
||||
When does the choice of a garbage collector matter? For some applications, the answer is never. That is, the application can perform well in the presence of garbage collection with pauses of modest frequency and duration. However, this isn't the case for a large class of applications, particularly those with large amounts of data (multiple gigabytes), many threads, and high transaction rates. Garbage collectors make assumptions about the way applications use objects, and these are reflected in tunable parameters that can be adjusted for improved performance.
|
||||
|
||||
|
||||
Here are few desirable properties of a Garbage Collector.
|
||||
|
||||
##### 1. Safety
|
||||
A garbage collector is safe when it never reclaims the space of a LIVE object and always cleans up only the dead objects.
|
||||
Although this looks like an obvious requirement, some GC algorithms claim space of LIVE objects just to gain that extra ounce of performance.
|
||||
|
||||
##### 2.Throughput
|
||||
A garbage collector should be as little time cleaning up the garbage as possible; this way it would ensure that the CPU is spent on doing actual work and not just cleaning up the mess.
|
||||
Most garbage collectors hence run small cycles frequently and a major cycle does deep cleaning once a while. This way they maximize the overall throughput and ensure we spend more time doing actual work.
|
||||
|
||||
|
||||
##### 3.Completeness
|
||||
A garbage collector is said to be complete when it eventually reclaims all the garbage from the heap.
|
||||
It is not desirable to do a complete clean-up every time the GC is executed, but eventually, a GC should guarantee that the garbage is cleaned up ensuring zero memory leaks.
|
||||
|
||||
##### 4.Pause Time
|
||||
Some garbage collectors pause the program execution during the cleanup and this induces a "pause". Long pauses affect the throughput of the system and may lead to unpredictable outcomes; so a GC is designed and tuned to minimize the pause time.
|
||||
The garbage collector needs to pause the execution because it needs to either run defragmentation where the heap objects are shuffled freeing up larger contiguous memory segments.
|
||||
|
||||
|
||||
##### 5.Space overhead
|
||||
Garbage collectors require auxiliary data structures to track objects efficiently and the memory required to do so is pure overhead. An efficient GC should have this space overhead as low as possible allowing sufficient memory for the program execution.
|
||||
|
||||
|
||||
##### 6.Language Specific Optimizations
|
||||
Most GC algorithms are generic but when bundled with the programing language the GC can exploit the language patterns and object allocation nuances. So, it is important to pick the GC that can leverage these details and make its execution as efficient as possible.
|
||||
For example, in some programming languages, GC runs in constant time by exploiting how objects are allocated on the heap.
|
||||
|
||||
##### 7.Scalability
|
||||
Most GC are efficient in cleaning up a small chunk of memory, but a scalable GC would run efficiently even on a server with large RAM. Similarly, a GC should be able to leverage multiple CPU cores, if available, to speed up the execution.
|
||||
|
||||
|
||||
Amdahl's law (parallel speedup in a given problem is limited by the sequential portion of the problem) implies that most workloads can't be perfectly parallelized; some portion is always sequential and doesn't benefit from parallelism. In the Java platform, there are currently four supported garbage collection alternatives and all but one of them, the serial GC, parallelize the work to improve performance. It's very important to keep the overhead of doing garbage collection as low as possible.
|
||||
|
||||
# Garbage Collection Algorithm
|
||||
|
||||
A theoretical, most straightforward garbage collection algorithm iterates over every reachable object every time it runs. Any leftover objects are considered garbage. The time this approach takes is proportional to the number of live objects, which is prohibitive for large applications maintaining lots of live data.
|
||||
|
||||
|
||||
## Mark-and-sweep Algorithm
|
||||
Over the lifetime of a Java application, new objects are created and released. Eventually, some objects are no longer needed. You can say that at any point in time, the heap memory consists of two types of objects:
|
||||
|
||||
- Live - these objects are being used and referenced from somewhere else
|
||||
|
||||
- Dead - these objects are no longer used or referenced from anywhere and can be deleted.
|
||||
|
||||
|
||||
The Java garbage collection process uses a mark-and-sweep algorithm. Here’s how that works
|
||||
There are two phases in this algorithm: *mark followed by sweep*.
|
||||
|
||||
- During the mark phase, the garbage collector traverses object trees starting at their roots. When an object is reachable from the root, the mark bit is set to 1 (true). Meanwhile, the mark bits for unreachable objects is unchanged (false).
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
- During the sweep phase, the garbage collector traverses the heap, reclaiming memory from all items with a mark bit of 0 (false).
|
||||
|
||||
**What are Garbage Collection Roots?**
|
||||
Garbage collectors work on the concept of Garbage Collection Roots (GC Roots) to identify live and dead objects. The garbage collector traverses the whole object graph in memory, starting from those Garbage Collection Roots and following references from the roots to other objects.
|
||||
|
||||
*Object graph* is basically a dependency graph between objects.In this graph, the nodes are Java objects, and the edges are the explicit or implied references that allow a running program to "reach" other objects from a given one. It is used to determine which objects are reachable and which not, so that all unreachable objects could be made eligible for garbage collection.
|
||||
|
||||
----
|
||||
#### Garbage Collectors in Java 17
|
||||
Java 17 supports several types of garbage collectors, including the Serial GC, Parallel GC, Concurrent Mark Sweep (CMS) GC, G1 GC, and the newly-introduced Z Garbage Collector (ZGC) and Shenandoah GC. Each of these garbage collectors has unique characteristics and can be chosen based on the requirements of your Java application. The Java garbage collectors employ various techniques to improve the efficiency of these operations:
|
||||
|
||||
- Java Garbage Collectors implement a generational garbage collection strategy that categorizes objects by age. Having to mark and compact all the objects in a JVM is inefficient. As more and more objects are allocated, the list of objects grows, leading to longer garbage collection times.
|
||||

|
||||
|
||||
- Use multiple threads to aggressively make operations parallel, or perform some long-running operations in the background concurrent to the application.
|
||||
|
||||
- Try to recover larger contiguous free memory by compacting live objects.
|
||||
|
||||
|
||||
**1. Serial Garbage Collector**
|
||||
|
||||
The Serial GC, also known as the ‘single-threaded’ GC, is the simplest form of garbage collection in Java. It uses just one CPU thread for garbage collection, which means it can be efficient for applications with a small heap size (up to approximately 100MB). However, during the garbage collection process, user threads are paused, which can lead to latency issues in larger applications. All garbage collection events are conducted serially in one thread. Compaction is executed after each garbage collection.
|
||||
|
||||

|
||||
|
||||
|
||||
Compacting describes the act of moving objects in a way that there are no holes between objects. After a garbage collection sweep, there may be holes left between live objects. Compacting moves objects so that there are no remaining holes.
|
||||
To enable Serial Garbage Collector, we can use the following argument:
|
||||
|
||||
```java -XX:+UseSerialGC -jar Application.java```
|
||||
|
||||
**2. Parallel Garbage Collector**
|
||||
Unlike Serial Garbage Collector, it uses multiple threads for managing heap space, but it also freezes other application threads while performing GC. The parallel collector is intended for applications with medium-sized to large-sized data sets that are run on multiprocessor or multithreaded hardware. This is the default implementation of GC in the JVM and is also known as Throughput Collector. Running the Parallel GC also causes a "stop the world event" and the application freezes. Since it is more suitable in a multi-threaded environment, it can be used when a lot of work needs to be done and long pauses are acceptable, for example running a batch job.
|
||||
|
||||
Multiple threads are used for minor garbage collection in the Young Generation. A single thread is used for major garbage collection in the Old Generation.
|
||||
If we use this GC, we can specify maximum garbage collection threads and pause time, throughput, and footprint (heap size) using command line arguments.
|
||||
|
||||
```java -XX:+UseParallelGC -jar Application.java```
|
||||
|
||||

|
||||
|
||||
**3. Concurrent Mark and Sweep**
|
||||
This is also known as the concurrent low pause collector. Multiple threads are used for minor garbage collection using the same algorithm as Parallel. Major garbage collection is multi-threaded, like Parallel Old GC, but CMS runs concurrently alongside application processes to minimize “stop the world” events. Because of this, the CMS collector uses more CPU than other GCs. If you can allocate more CPU for better performance, then the CMS garbage collector is a better choice than the parallel collector. No compaction is performed in CMS GC.
|
||||
|
||||

|
||||
|
||||
|
||||
The JVM argument to use Concurrent Mark Sweep Garbage Collector is ```java -XX:+UseConcMarkSweepGC```
|
||||
|
||||
**4. G1 Garbage Collector**
|
||||
G1 (Garbage First) Garbage Collector is designed for applications running on multi-processor machines with large memory space. It’s available from the JDK7 Update 4 and in later releases.
|
||||
|
||||
When performing garbage collections, G1 shows a concurrent global marking phase (i.e. phase 1, known as Marking) to determine the liveness of objects throughout the heap.
|
||||
|
||||
After the mark phase is complete, G1 knows which regions are mostly empty. It collects in these areas first, which usually yields a significant amount of free space (i.e. phase 2, known as Sweeping).
|
||||
|
||||
```java -XX:+UseG1GC -jar Application.java```
|
||||
|
||||
|
||||
**5. Z Garbage Collector**
|
||||
The Z Garbage Collector (ZGC) is a scalable low latency garbage collector. ZGC performs all expensive work concurrently, without stopping the execution of application threads for more than 10ms, which makes is suitable for applications which require low latency and/or use a very large heap (multi-terabytes).
|
||||
The Z Garbage Collector is available as an experimental feature, and is enabled with the command-line options
|
||||
```java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC```
|
||||
|
||||
#### Conclusion
|
||||
Remember, there’s no one-size-fits-all when it comes to choosing a garbage collector. A GC that works great for one application might not be the best choice for another. As with most aspects of system tuning, the best strategy often involves a mix of knowledge, experimentation, and a thorough understanding of your specific use case.
|
||||
|
||||
If your application doesn't have strict pause-time requirements, you should just run your application and allow the JVM to select the right collector.
|
||||
|
||||
Most of the time, the default settings should work just fine. If necessary, you can adjust the heap size to improve performance. If the performance still doesn't meet your goals, you can modify the collector as per your application requirements:
|
||||
|
||||
**Serial** - If the application has a small data set (up to approximately 100 MB) and/or it will be run on a single processor with no pause-time requirements
|
||||
|
||||
**Parallel** - If peak application performance is the priority and there are no pause-time requirements or pauses of one second or longer are acceptable
|
||||
|
||||
**CMS/G1** - If response time is more important than overall throughput and garbage collection pauses must be kept shorter than approximately one second
|
||||
|
||||
**ZGC** - If response time is a high priority, and/or you are using a very large heap
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -0,0 +1,248 @@
|
||||
## Java Collections - Hashmap, Linked Hashmap & Tree Map
|
||||
|
||||
- Hashmap
|
||||
- Linked Hashmap
|
||||
- TreeMap
|
||||
|
||||
|
||||
A **hash map** is good as a general-purpose map implementation that provides rapid storage and retrieval operations. However, it falls short because of its chaotic and unorderly arrangement of entries.
|
||||
|
||||
A **linked hash map** possesses the good attributes of hash maps and adds order to the entries. It performs better where there is a lot of iteration because only the number of entries is taken into account regardless of capacity.
|
||||
|
||||
A **tree map** takes ordering to the next level by providing complete control over how the keys should be sorted. On the flip side, it offers worse general performance than the other two alternatives.
|
||||
|
||||
### 1. Hashmap
|
||||
Let’s first look at what it means that HashMap is a map. A map is a key-value mapping, which means that every key is mapped to exactly one value and that we can use the key to retrieve the corresponding value from a map.
|
||||
|
||||
The advantage of a HashMap is that the time complexity to insert and retrieve a value is O(1) on average. We have covered the internal workings in the video lectures already.
|
||||
Before we proceed Let’s summarize how the put and get operations work.
|
||||
|
||||
**Put()**
|
||||
When we add an element to the map, HashMap calculates the bucket. If the bucket already contains a value, the value is added to the list (or tree) belonging to that bucket. If the load factor becomes bigger than the maximum load factor of the map, the capacity is doubled.
|
||||
|
||||
**Get()**
|
||||
When we want to get a value from the map, HashMap calculates the bucket and gets the value with the same key from the list (or tree).
|
||||
|
||||
**Example Code**
|
||||
Lets try to create a hashmap of products. We will create a Product class first.
|
||||
```java
|
||||
public class Product {
|
||||
|
||||
private String name;
|
||||
private String description;
|
||||
private List<String> tags;
|
||||
|
||||
// standard getters/setters/constructors
|
||||
|
||||
public Product addTagsOfOtherProduct(Product product) {
|
||||
this.tags.addAll(product.getTags());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
```
|
||||
We can now create a HashMap with the key of type String and elements of type Product:
|
||||
|
||||
```java
|
||||
Map<String, Product> productsByName = new HashMap<>();
|
||||
```
|
||||
**1. Put Method**
|
||||
Adding to hashmap.
|
||||
```java
|
||||
Product eBike = new Product("E-Bike", "A bike with a battery");
|
||||
Product roadBike = new Product("Road bike", "A bike for competition");
|
||||
//using the Put Method
|
||||
productsByName.put(eBike.getName(), eBike);
|
||||
productsByName.put(roadBike.getName(), roadBike);
|
||||
```
|
||||
|
||||
**2. Get Method**
|
||||
We can retrieve a value from the map by its key:
|
||||
```java
|
||||
Product nextPurchase = productsByName.get("E-Bike");
|
||||
assertEquals("A bike with a battery", nextPurchase.getDescription());
|
||||
```
|
||||
|
||||
If we try to find a value for a key that doesn’t exist in the map, we’ll get a null value:
|
||||
|
||||
**3. Remove**
|
||||
|
||||
We can remove a key-value mapping from the HashMap:
|
||||
```java
|
||||
productsByName.remove("E-Bike");
|
||||
assertNull(productsByName.get("E-Bike"));
|
||||
```
|
||||
|
||||
**4. Contains Key**
|
||||
To check if a key is present in the map, we can use the containsKey() method:
|
||||
```java
|
||||
productsByName.containsKey("E-Bike");
|
||||
```
|
||||
|
||||
-----
|
||||
**Hashmap with Custom Key Class**
|
||||
We can use any class as the key in our HashMap. However, for the map to work properly, we need to provide an implementation for equals() and hashCode().
|
||||
In most cases, we should use **immutable keys**. Or at least, we must be aware of the consequences of using mutable keys. If key changes after insertion, HashMap will be searching in the wrong bucket and leading to inconsistent behaviour.
|
||||
|
||||
|
||||
Let’s say we want to have a map with the product as the key and the price as the value:
|
||||
|
||||
```java
|
||||
HashMap<Product, Integer> priceByProduct = new HashMap<>();
|
||||
priceByProduct.put(eBike, 900);
|
||||
```
|
||||
Let’s implement the equals() and hashCode() methods:
|
||||
|
||||
|
||||
```java
|
||||
//Override these methods in the Product Class
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Product product = (Product) o;
|
||||
return Objects.equals(name, product.name) &&
|
||||
Objects.equals(description, product.description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, description);
|
||||
}
|
||||
```
|
||||
Note that `hashCode()` and `equals()` need to be overridden only for classes that we want to use as map keys, not for classes that are only used as values in a map.
|
||||
|
||||
-----
|
||||
|
||||
### 2. Linked Hashmap
|
||||
The LinkedHashMap class is very similar to HashMap in most aspects. However, the linked hash map is based on both hash table and linked list to enhance the functionality of hash map.
|
||||
|
||||
It maintains a doubly-linked list running through all its entries in addition to an underlying array of default size 16.
|
||||
|
||||
This linked list defines the order of iteration, which by default is the order of insertion of elements (insertion-order).
|
||||
|
||||
Let’s have a look at a linked hash map instance which orders its entries according to how they’re inserted into the map. It also guarantees that this order will be maintained throughout the life cycle of the map:
|
||||
|
||||
```java
|
||||
public void givenLinkedHashMap_whenGetsOrderedKeyset_thenCorrect() {
|
||||
LinkedHashMap<Integer, String> map = new LinkedHashMap<>();
|
||||
map.put(1, null);
|
||||
map.put(2, null);
|
||||
map.put(3, null);
|
||||
map.put(4, null);
|
||||
map.put(5, null);
|
||||
|
||||
Set<Integer> keys = map.keySet();
|
||||
Integer[] arr = keys.toArray(new Integer[0]);
|
||||
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
assertEquals(new Integer(i + 1), arr[i]);
|
||||
}
|
||||
}
|
||||
```
|
||||
We can guarantee that this test will always pass as the insertion order will always be maintained. We cannot make the same guarantee for a HashMap.
|
||||
|
||||
**Access Order Linked Hashmap**
|
||||
LinkedHashMap provides a special constructor which enables us to specify, among custom load factor (LF) and initial capacity, a different ordering mechanism/strategy called access-order:
|
||||
```java
|
||||
LinkedHashMap<Integer, String> map = new LinkedHashMap<>(16, .75f, true);
|
||||
```
|
||||
The first parameter is the initial capacity, followed by the load factor and the last param is the ordering mode. So, by passing in true, we turned on access-order, whereas the default was insertion-order.
|
||||
|
||||
This mechanism ensures that the order of iteration of elements is the order in which the elements were last accessed, from least-recently accessed to most-recently accessed.
|
||||
|
||||
**LRU using LinkedHashmap**
|
||||
And so, building a Least Recently Used (LRU) cache is quite easy and practical with this kind of map. A successful put or get operation results in an access for the entry:
|
||||
```java
|
||||
public void givenLinkedHashMap_whenAccessOrderWorks_thenCorrect() {
|
||||
LinkedHashMap<Integer, String> map
|
||||
= new LinkedHashMap<>(16, .75f, true);
|
||||
map.put(1, null);
|
||||
map.put(2, null);
|
||||
map.put(3, null);
|
||||
map.put(4, null);
|
||||
map.put(5, null);
|
||||
|
||||
Set<Integer> keys = map.keySet();
|
||||
assertEquals("[1, 2, 3, 4, 5]", keys.toString());
|
||||
|
||||
map.get(4);
|
||||
assertEquals("[1, 2, 3, 5, 4]", keys.toString());
|
||||
|
||||
map.get(1);
|
||||
assertEquals("[2, 3, 5, 4, 1]", keys.toString());
|
||||
|
||||
map.get(3);
|
||||
assertEquals("[2, 5, 4, 1, 3]", keys.toString());
|
||||
}
|
||||
|
||||
```
|
||||
Just like HashMap, LinkedHashMap implementation is not synchronized. So if you are going to access it from multiple threads and at least one of these threads is likely to change it structurally, then it must be externally synchronized.
|
||||
```java
|
||||
Map m = Collections.synchronizedMap(new LinkedHashMap());
|
||||
```
|
||||
We will learn more about concurrency in a separate tutorial.
|
||||
|
||||
-----
|
||||
### 3. TreeMap ###
|
||||
TreeMap is a map implementation that keeps its entries sorted according to the natural ordering of its keys or better still using a comparator if provided by the user at construction time.
|
||||
|
||||
By default, TreeMap sorts all its entries according to their natural ordering. For an integer, this would mean ascending order and for strings, alphabetical order.A hash map does not guarantee the order of keys stored and specifically does not guarantee that this order will remain the same over time, but a tree map guarantees that the keys will always be sorted according to the specified order.
|
||||
|
||||
|
||||
TreeMap, unlike a hash map and linked hash map, does not employ the hashing principle anywhere since it does not use an array to store its entries but uses a self-balanancing tree such as **Red Black Tree** data structure to store the entries.
|
||||
A red-black tree is a self-balancing binary search tree. This attribute and the above guarantee that basic operations like search, get, put and remove take logarithmic time O(log n) as For every insertion and deletion, the maximum height of the tree on any edge is maintained at O(log n) i.e. the tree balances itself continuously.
|
||||
|
||||
Just like hash map and linked hash map, a tree map is not synchronized and therefore the rules for using it in a multi-threaded environment are similar to those in the other two map implementations.
|
||||
|
||||
|
||||
A Tree Map example with Comparator
|
||||
```java
|
||||
public void givenTreeMap_whenOrdersEntriesByComparator_thenCorrect() {
|
||||
TreeMap<Integer, String> map =
|
||||
new TreeMap<>(Comparator.reverseOrder());
|
||||
map.put(3, "val");
|
||||
map.put(2, "val");
|
||||
map.put(1, "val");
|
||||
map.put(5, "val");
|
||||
map.put(4, "val");
|
||||
|
||||
assertEquals("[5, 4, 3, 2, 1]", map.keySet().toString());
|
||||
}
|
||||
```
|
||||
Notice that we placed the integer keys in a non-orderly manner but on retrieving the key set, we confirm that they are indeed maintained in ascending order. This is the natural ordering of integers.
|
||||
|
||||
We now know that TreeMap stores all its entries in sorted order. Because of this attribute of tree maps, we can perform queries like; find “largest”, find “smallest”, find all keys less than or greater than a certain value, etc.
|
||||
```java
|
||||
public void givenTreeMap_whenPerformsQueries_thenCorrect() {
|
||||
TreeMap<Integer, String> map = new TreeMap<>();
|
||||
map.put(3, "val");
|
||||
map.put(2, "val");
|
||||
map.put(1, "val");
|
||||
map.put(5, "val");
|
||||
map.put(4, "val");
|
||||
|
||||
Integer highestKey = map.lastKey();
|
||||
Integer lowestKey = map.firstKey();
|
||||
Set<Integer> keysLessThan3 = map.headMap(3).keySet();
|
||||
Set<Integer> keysGreaterThanEqTo3 = map.tailMap(3).keySet();
|
||||
|
||||
assertEquals(new Integer(5), highestKey);
|
||||
assertEquals(new Integer(1), lowestKey);
|
||||
assertEquals("[1, 2]", keysLessThan3.toString());
|
||||
assertEquals("[3, 4, 5]", keysGreaterThanEqTo3.toString());
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -0,0 +1,101 @@
|
||||
## Java Architecture
|
||||
|
||||
Java is a platform-independent language. For that we need to understand the steps of compilation and execution of code.
|
||||
|
||||
- The code written in Java, is converted into byte codes which is done by the Java Compiler
|
||||
- The byte code, is converted into machine code by the JVM.
|
||||
- The Machine code is executed directly by the machine.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
Bytecodes are effectively *platform-independent*. The java virtual machine takes care of the differences between the bytecodes for the different platforms. This makes the Java Compiled Code platform independent.
|
||||
|
||||

|
||||
|
||||
There are three main components of Java architechure: JVM, JRE, and JDK.
|
||||
Java Virtual Machine, Java Runtime Environment and Java Development Kit respectively. Lets understand them one by one.
|
||||
|
||||
# JVM
|
||||
JVM (Java Virtual Machine) is an abstract machine(software) that enables your computer to run a Java program.
|
||||
|
||||
When you run the Java program, Java compiler -`javac` first compiles your Java code to bytecode. Then, the JVM translates bytecode into native machine code (set of instructions that a computer's CPU executes directly). JVM comes with **JIT(Just-in-Time) compiler** that converts Java source code into low-level machine language. Hence, it runs more faster as a regular application.
|
||||
|
||||
|
||||
# JRE
|
||||
JRE (Java Runtime Environment) is a software package that provides Java class libraries, Java Virtual Machine (JVM), and other components that are required to run Java applications. JRE is the superset of JVM.
|
||||
|
||||
When our software tends to execute a particular program, it requires some environment to run in. Usually, it’s any operating system for example, Unix, Linux, Microsoft Windows, or the MacOS. Here our JRE acts as a translater and also a facilitator between the java program and the operating system.
|
||||
|
||||

|
||||
|
||||
### JDK
|
||||
JDK (Java Development Kit) is a software development kit required to develop applications in Java. When you download JDK, JRE is also downloaded with it.
|
||||
|
||||
In addition to JRE, JDK also contains a number of development tools (compilers, JavaDocs, Java Debugger, etc).
|
||||
|
||||

|
||||
|
||||
----
|
||||
### JVM Deep Dive
|
||||
Java applications are platform independent - write once, run anywhere. This is because of JVM which performs the following tasks -
|
||||
- Loads the code
|
||||
- Verifies the code
|
||||
- Executes the code
|
||||
- Provides runtime environment
|
||||
|
||||
Here are the important components of JVM architecture:
|
||||
|
||||
**1. Class Loader**
|
||||
The class loader is a subsystem used for loading class files. It performs three major functions viz. Loading, Linking, and Initialization.Whenever we run the java program, class loader loads it first.
|
||||
|
||||
**2. Method Area**
|
||||
It is one of the Data Area in JVM, in which Class data will be stored. Static Variables, Static Blocks, Static Methods, Instance Methods are stored in this area.
|
||||
JVM Method Area stores structure of class like metadata, the code for Java methods, and the constant runtime pool.
|
||||
|
||||
**3. Heap**
|
||||
A heap is created when the JVM starts up. It may increase or decrease in size while the application runs. All the Objects, arrays, and instance variables are stored in a heap. This memory is shared across multiple threads.
|
||||
|
||||
**4. JVM language Stacks**
|
||||
Java language Stacks store local variables, and its partial results. Each and every thread has its own JVM language stack, created concurrently as the thread is created. A new stack frame is created when method is invoked, and it is removed when method invocation process is complete. JVM stack is known as a thread stack.
|
||||
|
||||
|
||||
**5. PC Registers**
|
||||
PC registers store the address of the Java virtual machine instruction, which is currently executing. In Java, each thread has its separate PC register.
|
||||
|
||||
**6. Native Method Stacks**
|
||||
|
||||
Native method stacks hold the instruction of native code depends on the native library. It allocates memory on native heaps or uses any type of stack.
|
||||
|
||||
**7) Execution Engine**
|
||||
Execution Engine is the brain of JVM. It has two components.
|
||||
- JIT compiler
|
||||
- Garbage collector
|
||||
|
||||
**JIT compiler**: The Just-In-Time (JIT) compiler is a part of the runtime environment. It helps in improving the performance of Java applications by compiling bytecodes to machine code at run time. The JIT compiler is enabled by default. When a method is compiled, the JVM calls the compiled code of that method directly. The JIT compiler compiles the bytecode of that method into machine code, compiling it “just in time” to run.
|
||||
|
||||
**Garbage collector**: As the name explains that Garbage Collector means to collect the unused material. Well, in JVM this work is done by Garbage collection. It tracks each and every object available in the JVM heap space and removes unwanted ones.
|
||||
Garbage collector works in two simple steps known as Mark and Sweep:
|
||||
|
||||
Mark – it is where the garbage collector identifies which piece of memory is in use and which are not
|
||||
|
||||
|
||||
Sweep – it removes objects identified during the “mark” phase.
|
||||
|
||||
**8) Native Method interface**
|
||||
|
||||
The Native Method Interface is a programming framework. It allows Java code, which is running in a JVM to call by libraries and native applications.
|
||||
|
||||
**9) Native Method Libraries**
|
||||
|
||||
Native Libraries is a collection of the Native Libraries (C, C++), which are needed by the Execution Engine.
|
||||
|
||||

|
||||
|
||||
----
|
||||
|
||||
|
||||
|
@@ -0,0 +1,263 @@
|
||||

|
||||
------
|
||||
# Understanding Static and Final Keyword in Java
|
||||
|
||||
## Static Keyword
|
||||
Java uses static keyword at 4 different places. Lets learn about each of them.
|
||||
- Static Instance Variables
|
||||
- Static Methods
|
||||
- Static Block
|
||||
- Static Classes.
|
||||
|
||||
**Static Members and Methods**
|
||||
There will be times when you will want to define a class member that will be used independently of any object of that class. Normally, a class member must be accessed only in conjunction with an object of its class. However, it is possible to create a member that can be used by itself, without reference to a specific instance.
|
||||
|
||||
To create such a member, precede its declaration with the keyword static. When a member is declared `static`, it can be accessed before any objects of its class are created, and without reference to any object. You can declare both methods and variables to be static.
|
||||
|
||||
The most common example of a static member is main( ). main( ) is declared as static because it must be called before any objects exist.
|
||||
|
||||
Instance variables declared as static are, essentially, global variables. When objects of its class are declared, no copy of a static variable is made. Instead, all instances of the class share the same static variable.
|
||||
|
||||
**Math.java**
|
||||
```java
|
||||
public class Math {
|
||||
static String author = "Prateek";
|
||||
|
||||
static int area(int l,int b){
|
||||
return l*b;
|
||||
}
|
||||
}
|
||||
```
|
||||
In the above example the static keywod has been used to create a static data member and a static method. We don't need create a Math Class objects to acess the methods and data members, instead we can directly refer `Math.author` and `Math.area(v1,v2)` from main.
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
//static block will execute when the class is loaded
|
||||
|
||||
System.out.println("Area of Rectangle " + Math.area(10,20));
|
||||
System.out.println(Math.author);
|
||||
}
|
||||
```
|
||||
|
||||
**Restrictions on Static Methods**:
|
||||
|
||||
• They can only directly call other static methods of their class.
|
||||
|
||||
• They can only directly access static variables of their class.
|
||||
|
||||
• They cannot refer to `this` or `super` in any way. (Super keyword is used in inheritance)
|
||||
|
||||
|
||||
**Static Block**
|
||||
If you need to do computation in order to initialize your static variables, you can declare a static block that gets executed exactly once, when the class is first loaded.
|
||||
As soon as the class is loaded, all of the static statements are run.
|
||||
|
||||
```java
|
||||
public class StaticDemoExample {
|
||||
static int a = 3;
|
||||
static int b;
|
||||
|
||||
static{
|
||||
System.out.println("Inside Static Block");
|
||||
b = a*4;
|
||||
printData();
|
||||
}
|
||||
|
||||
static void printData(){
|
||||
System.out.println(a);
|
||||
System.out.println(b);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
//static block will execute when the class is loaded
|
||||
// it is used to init static variables
|
||||
}
|
||||
}
|
||||
```
|
||||
In the above code, static block will automatically as you launch the program, the class is loaded and static block is executed. Despite the main being empty, the code will output the following as static block is still executed.
|
||||
|
||||
*Code Output*
|
||||
```
|
||||
Inside Static Block
|
||||
3
|
||||
12
|
||||
```
|
||||
|
||||
## Nested Classes and 'Static' Modifier
|
||||
It is possible to define a class within another class; such classes are known as nested classes. There are two types of nested classes: static and non-static. Lets learn about them.
|
||||
|
||||
**Static Nested Class (Static Inner Class)**:
|
||||
- A static nested class is a nested class that is declared as static.
|
||||
- It does not have access to the instance-specific members of the outer class.Because it is static, it must access the non-static members of its enclosing class through an object.
|
||||
- You can create an instance of a static nested class without creating an instance of the outer class.
|
||||
- Static nested classes are often used for grouping related utility methods or encapsulating code within a class.
|
||||
- Note: In Java, only nested classes are allowed to be static.
|
||||
|
||||
|
||||
```java
|
||||
public class OuterClass {
|
||||
// Outer class members
|
||||
|
||||
static class StaticNestedClass {
|
||||
// Static nested class members
|
||||
}
|
||||
}
|
||||
```
|
||||
**Inner Class (Non-static Nested Class)**
|
||||
|
||||
- An inner class is a nested class that is not declared as static.
|
||||
- It can access both static and instance-specific members of the outer class.
|
||||
- An instance of an inner class can only be created within an instance of the outer class.
|
||||
- Inner classes are often used for implementing complex data structures or for achieving better encapsulation.
|
||||
|
||||
```java
|
||||
public class OuterClass {
|
||||
// Outer class members
|
||||
|
||||
class InnerClass {
|
||||
// Inner class members
|
||||
}
|
||||
}
|
||||
```
|
||||
**Nested Static Classes in Builder Design Pattern**
|
||||
This kind of class design is particularly useful in Builder Design Pattern which you will study later as a part of Low Level Design Course. In short, The Builder Design Pattern is a creational design pattern that allows you to create complex objects step by step. It's especially useful when you have an object with many optional parameters or configurations. Here's an example of how you can implement the Builder pattern in Java:
|
||||
|
||||
Suppose you want to create a Person class with optional attributes like name, age, address, and phone number using the Builder pattern:
|
||||
|
||||
```java
|
||||
public class Person {
|
||||
private String name;
|
||||
private int age;
|
||||
private String address;
|
||||
private String phoneNumber;
|
||||
|
||||
// Private constructor to prevent direct instantiation
|
||||
private Person() {
|
||||
}
|
||||
|
||||
// Nested Builder class
|
||||
public static class Builder {
|
||||
private String name;
|
||||
private int age;
|
||||
private String address;
|
||||
private String phoneNumber;
|
||||
|
||||
public Builder(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Builder age(int age) {
|
||||
this.age = age;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder address(String address) {
|
||||
this.address = address;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder phoneNumber(String phoneNumber) {
|
||||
this.phoneNumber = phoneNumber;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Person build() {
|
||||
Person person = new Person();
|
||||
person.name = this.name;
|
||||
person.age = this.age;
|
||||
person.address = this.address;
|
||||
person.phoneNumber = this.phoneNumber;
|
||||
return person;
|
||||
}
|
||||
}
|
||||
|
||||
// Getter methods for Person class
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return age;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public String getPhoneNumber() {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Name: " + name + ", Age: " + age + ", Address: " + address + ", Phone: " + phoneNumber;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
__PersonDemo.java__
|
||||
```java
|
||||
public class PersonDemo {
|
||||
public static void main(String[] args) {
|
||||
Person person1 = new Person.Builder("John")
|
||||
.age(30)
|
||||
.address("123 Main St")
|
||||
.phoneNumber("555-1234")
|
||||
.build();
|
||||
|
||||
Person person2 = new Person.Builder("Alice")
|
||||
.age(25)
|
||||
.phoneNumber("555-5678")
|
||||
.build();
|
||||
|
||||
System.out.println(person1);
|
||||
System.out.println(person2);
|
||||
}
|
||||
}
|
||||
```
|
||||
This allows you to create Person objects with various combinations of attributes while keeping the code clean and readable.
|
||||
|
||||
|
||||
|
||||
------------------
|
||||
### Final Keyword
|
||||
The keyword final has three uses. First, it can be used to create the equivalent of a named constant. The other two uses of final apply to inheritance as discussed below.
|
||||
|
||||
**1. Final in Variables**
|
||||
A field can be declared as final. Doing so prevents its contents from being modified, making it, essentially, a constant. This means that you must initialize a final field when it is declared. You can do this in one of two ways: First, you can give it a value when it is declared. Second, you can assign it a value within a constructor. The first approach is probably the most common. Here is an example:
|
||||
```
|
||||
final int FILE_NEW = 1;
|
||||
final int FILE_OPEN = 2;
|
||||
final int FILE_SAVE = 3;
|
||||
final int FILE_SAVEAS = 4;
|
||||
final int FILE_QUIT = 5;
|
||||
```
|
||||
|
||||
Subsequent parts of your program can now use FILE_OPEN, etc., as if they were constants, without fear that a value has been changed. It is a common coding convention to choose all **uppercase identifiers** for final fields, as this example shows.
|
||||
|
||||
In addition to fields, both method parameters and local variables can be declared final. Declaring a parameter final prevents it from being changed within the method. Declaring a local variable final prevents it from being assigned a value more than once.
|
||||
|
||||
The keyword final can also be applied to methods, but its meaning is substantially different than when it is applied to variables.
|
||||
|
||||
|
||||
**2. Using final to Prevent Method Overriding**
|
||||
While method overriding is one of Java’s most powerful features, there will be times when you will want to prevent it from occurring. To disallow a method from being overridden, specify final as a modifier at the start of its declaration. Methods declared as final cannot be overridden. The following fragment illustrates final.
|
||||
|
||||

|
||||
Because meth( ) is declared as final, it cannot be overridden in B. If you attempt to do so, a compile-time error will result.
|
||||
|
||||
Methods declared as final can sometimes provide a performance enhancement: The compiler is free to inline calls to them because it “knows” they will not be overridden by a subclass. When a small final method is called, often the Java compiler can copy the bytecode for the subroutine directly inline with the compiled code of the calling method, thus eliminating the costly overhead associated with a method call. Inlining is an option only with final methods. Normally, Java resolves calls to methods dynamically, at run time. This is called late binding. However, since final methods cannot be overridden, a call to one can be resolved at compile time. This is called early binding.
|
||||
|
||||
**3. Using final to Prevent Inheritance**
|
||||
Sometimes you will want to prevent a class from being inherited. To do this, precede the class declaration with final. Declaring a class as final implicitly declares all of its methods as final, too. As you might expect, it is illegal to declare a class as both abstract and final since an abstract class is incomplete by itself and relies upon its subclasses to provide complete implementations.
|
||||
|
||||
Here is an example of a final class:
|
||||

|
||||
As the comments imply, it is illegal for B to inherit A since A is declared as final.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -0,0 +1,226 @@
|
||||
### Java Streams
|
||||
A stream in Java is simply a wrapper around a data source, allowing us to perform bulk operations on the data in a convenient way.
|
||||
|
||||
It doesn’t store data or make any changes to the underlying data source. Rather, it adds support for functional-style operations on data pipelines.
|
||||
|
||||
In this tutorial we will learn about Sequential Streams, Parallel Streams and Collect() Method of stream.
|
||||
|
||||
### Sequential Streams
|
||||
By default, any stream operation in Java is processed sequentially, unless explicitly specified as parallel.
|
||||
|
||||
Sequential streams use a single thread to process the pipeline:
|
||||
```java
|
||||
List<Integer> listOfNumbers = Arrays.asList(1, 2, 3, 4);
|
||||
listOfNumbers.stream().forEach(number ->
|
||||
System.out.println(number + " " + Thread.currentThread().getName())
|
||||
);
|
||||
```
|
||||
The output of this sequential stream is predictable. The list elements will always be printed in an ordered sequence:
|
||||
|
||||
```
|
||||
1 main
|
||||
2 main
|
||||
3 main
|
||||
4 main
|
||||
```
|
||||
|
||||
### Multithreading using Parallel Streams
|
||||
Stream API also simplifies multithreading by providing the `parallelStream()` method that runs operations over stream’s elements in parallel mode. Any stream in Java can easily be transformed from sequential to parallel.
|
||||
|
||||
We can achieve this by adding the parallel method to a sequential stream or by creating a stream using the parallelStream method of a collection:
|
||||
|
||||
The code below allows to run method doWork() in parallel for every element of the stream:
|
||||
```java
|
||||
list.parallelStream().forEach(element -> doWork(element));
|
||||
```
|
||||
For the above sequential example, the code will looks like this -
|
||||
|
||||
```java
|
||||
List<Integer> listOfNumbers = Arrays.asList(1, 2, 3, 4);
|
||||
listOfNumbers.parallelStream().forEach(number ->
|
||||
System.out.println(number + " " + Thread.currentThread().getName())
|
||||
);
|
||||
```
|
||||
Parallel streams enable us to execute code in parallel on separate cores. The final result is the combination of each individual outcome.
|
||||
|
||||
However, the order of execution is out of our control. It may change every time we run the program:
|
||||
```
|
||||
4 ForkJoinPool.commonPool-worker-3
|
||||
2 ForkJoinPool.commonPool-worker-5
|
||||
1 ForkJoinPool.commonPool-worker-7
|
||||
3 main
|
||||
```
|
||||
Parallel streams make use of the fork-join framework and its common pool of worker threads. Parallel processing may be beneficial to fully utilize multiple cores. But we also need to consider the overhead of managing multiple threads, memory locality, splitting the source and merging the results.
|
||||
Refer this [Article](https://www.baeldung.com/java-when-to-use-parallel-stream) to learn more about when to use parallel streams.
|
||||
|
||||
`
|
||||
|
||||
### Collect() Method
|
||||
|
||||
A stream represents a sequence of elements and supports different kinds of operations that lead to the desired result. The source of a stream is usually a Collection or an Array, from which data is streamed from.
|
||||
|
||||
Streams differ from collections in several ways; most notably in that the streams are not a data structure that stores elements. They're functional in nature, and it's worth noting that operations on a stream produce a result and typically return another stream, but do not modify its source.
|
||||
|
||||
To "solidify" the changes, you **collect** the elements of a stream back into a Collection.
|
||||
|
||||
The `stream.collect()` method is used to perform a mutable reduction operation on the elements of a stream. It returns a new mutable object containing the results of the reduction operation.
|
||||
|
||||
This method can be used to perform several different types of reduction operations, such as:
|
||||
|
||||
- Computing the sum of numeric values in a stream.
|
||||
- Finding the minimum or maximum value in a stream.
|
||||
- Constructing a new String by concatenating the contents of a stream.
|
||||
- Collecting elements into a new List or Set.
|
||||
|
||||
```java
|
||||
public class CollectExample {
|
||||
public static void main(String[] args) {
|
||||
Integer[] intArray = {1, 2, 3, 4, 5};
|
||||
|
||||
// Creating a List from an array of elements
|
||||
// using Arrays.asList() method
|
||||
List<Integer> list = Arrays.asList(intArray);
|
||||
|
||||
// Demo1: Collecting all elements of the list into a new
|
||||
// list using collect() method
|
||||
List<Integer> evenNumbersList = list.stream()
|
||||
.filter(i -> i%2 == 0)
|
||||
.collect(toList());
|
||||
System.out.println(evenNumbersList);
|
||||
|
||||
// Demo2: finding the sum of all the values
|
||||
// in the stream
|
||||
Integer sum = list.stream()
|
||||
.collect(summingInt(i -> i));
|
||||
System.out.println(sum);
|
||||
|
||||
// Demo3: finding the maximum of all the values
|
||||
// in the stream
|
||||
Integer max = list.stream()
|
||||
.collect(maxBy(Integer::compare)).get();
|
||||
System.out.println(max);
|
||||
|
||||
// Demo4: finding the minimum of all the values
|
||||
// in the stream
|
||||
Integer min = list.stream()
|
||||
.collect(minBy(Integer::compare)).get();
|
||||
System.out.println(min);
|
||||
|
||||
// Demo5: counting the values in the stream
|
||||
Long count = list.stream()
|
||||
.collect(counting());
|
||||
System.out.println(count);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In Demo1: We use the stream() method to get a stream from the list. We filter the even elements and collect them into a new list using the collect() method.
|
||||
|
||||
In Demo2: We use the collect() method summingInt(ToIntFunction) as an argument. The summingInt() method returns a collector that sums the integer values extracted from the stream elements by applying an int producing mapping function to each element.
|
||||
|
||||
In Demo 3: We use the collect() method with maxBy(Comparator) as an argument. The maxBy() accepts a Comparator and returns a collector that extracts the maximum element from the stream according to the given Comparator.
|
||||
|
||||
Lets learn more about Collectors.
|
||||
|
||||
|
||||
### Collectors and Stream.Collect()
|
||||
|
||||
Collectors represent implementations of the Collector interface, which implements various useful reduction operations, such as accumulating elements into collections, summarizing elements based on a specific parameter, etc.
|
||||
|
||||
All predefined implementations can be found within the [Collectors](https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html) class.
|
||||
|
||||
|
||||
Within the Collectors class itself, we find an abundance of unique methods that deliver on the different needs of a user. One such group is made of summing methods - `summingInt()`, `summingDouble()` and `summingLong()`.
|
||||
|
||||
|
||||
|
||||
Let's start off with a basic example with a List of Integers:
|
||||
|
||||
```java
|
||||
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
|
||||
Integer sum = numbers.stream().collect(Collectors.summingInt(Integer::intValue));
|
||||
System.out.println("Sum: " + sum);
|
||||
```
|
||||
We apply the .stream() method to create a stream of Integer instances, after which we use the previously discussed `.collect()` method to collect the elements using `summingInt()`. The method itself, again, accepts the `ToIntFunction`, which can be used to reduce instances to an integer that can be summed.
|
||||
|
||||
Since we're using Integers already, we can simply pass in a method reference denoting their `intValue`, as no further reduction is needed.
|
||||
|
||||
More often than not - you'll be working with lists of custom objects and would like to sum some of their fields. For instance, we can sum the quantities of each product in the productList, denoting the total inventory we have.
|
||||
|
||||
Let us try to understand one of these methods using a custom class example.
|
||||
``` java
|
||||
public class Product {
|
||||
private String name;
|
||||
private Integer quantity;
|
||||
private Double price;
|
||||
private Long productNumber;
|
||||
|
||||
// Constructor, getters and setters
|
||||
...
|
||||
}
|
||||
...
|
||||
List<Product> products = Arrays.asList(
|
||||
new Product("Milk", 37, 3.60, 12345600L),
|
||||
new Product("Carton of Eggs", 50, 1.20, 12378300L),
|
||||
new Product("Olive oil", 28, 37.0, 13412300L),
|
||||
new Product("Peanut butter", 33, 4.19, 15121200L),
|
||||
new Product("Bag of rice", 26, 1.70, 21401265L)
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
In such a case, the we can use a method reference, such as `Product::getQuantity` as our `ToIntFunction`, to reduce the objects into a single integer each, and then sum these integers:
|
||||
|
||||
```java
|
||||
Integer sumOfQuantities = products.stream().collect(Collectors.summingInt(Product::getQuantity));
|
||||
System.out.println("Total number of products: " + sumOfQuantities);
|
||||
```
|
||||
This results in:
|
||||
|
||||
```
|
||||
Total number of products: 174
|
||||
```
|
||||
|
||||
You can also very easily implement your own collector and use it instead of the predefined ones, though - you can get pretty far with the built-in collectors, as they cover the vast majority of cases in which you might want to use them.
|
||||
|
||||
The following are examples of using the predefined collectors to perform common mutable reduction tasks:
|
||||
```java
|
||||
|
||||
// Accumulate names into a List
|
||||
List<String> list = people.stream().map(Person::getName).collect(Collectors.toList());
|
||||
|
||||
// Accumulate names into a TreeSet
|
||||
Set<String> set = people.stream().map(Person::getName).collect(Collectors.toCollection(TreeSet::new));
|
||||
|
||||
// Convert elements to strings and concatenate them, separated by commas
|
||||
String joined = things.stream()
|
||||
.map(Object::toString)
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
// Compute sum of salaries of employee
|
||||
int total = employees.stream()
|
||||
.collect(Collectors.summingInt(Employee::getSalary)));
|
||||
|
||||
// Group employees by department
|
||||
Map<Department, List<Employee>> byDept
|
||||
= employees.stream()
|
||||
.collect(Collectors.groupingBy(Employee::getDepartment));
|
||||
|
||||
// Compute sum of salaries by department
|
||||
Map<Department, Integer> totalByDept
|
||||
= employees.stream()
|
||||
.collect(Collectors.groupingBy(Employee::getDepartment,
|
||||
Collectors.summingInt(Employee::getSalary)));
|
||||
|
||||
// Partition students into passing and failing
|
||||
Map<Boolean, List<Student>> passingFailing =
|
||||
students.stream()
|
||||
.collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
|
||||
|
||||
```
|
||||
You can look at the offical documentation for more details on these methods.
|
||||
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html
|
||||
|
||||
|
||||
|
||||
|
@@ -0,0 +1,173 @@
|
||||
# Java Strings - Advanced Concepts
|
||||
In this tutorial we discuss 3 important concepts related to Strings.
|
||||
- String Pool
|
||||
- String Immutability
|
||||
- String Builder
|
||||
|
||||
|
||||
String are handled differently in Java. There are two ways to store strings - one as string literals stored in String Pool and as string objects stored in regular heap space. Lets discuss about them.
|
||||
|
||||
### 1. Strings in String Pool
|
||||
Each time you create a string literal, the JVM checks the "string constant pool" first. If the string already exists in the pool, a reference to the pooled instance is returned. If the string doesn't exist in the pool, a new string instance is created and placed in the pool. For example:
|
||||
|
||||
**String Literal Syntax**
|
||||
```java
|
||||
String s1 = "Hello World";
|
||||
String s2 = "Hello World";//It doesn't create a new instance
|
||||
```
|
||||

|
||||
|
||||
In the above example, only one object will be created. Firstly, JVM will not find any string object with the value "Hello World" in string constant pool that is why it will create a new object. After that it will find the string with the value "Hello World" in the pool, it will not create a new object but will return the reference to the same instance.
|
||||
|
||||
**Java String Pool** is the special memory region where Strings are stored by the JVM. Since Strings are immutable in Java, the JVM optimizes the amount of memory allocated for them by storing only one copy of each literal String in the pool. This process is called interning
|
||||
|
||||
### 2. String Allocated Using the Constructor
|
||||
When we create a String via the new operator, the Java compiler will create a new object and store it in the heap space reserved for the JVM.
|
||||
|
||||
Every String created like this will point to a different memory region with its own address.
|
||||
|
||||
Let’s see how this is different from the previous case:
|
||||
|
||||
```java
|
||||
String s1 = new String("Welcome");
|
||||
String s2 = new String("Welcome");
|
||||
//creates two objects and two reference variables point to different addresses
|
||||
```
|
||||
---
|
||||
### Big Question - String Literal vs String Object?
|
||||
We have just seen that when we create a String object using the `new()` operator, it always creates a new object in heap memory. On the other hand, if we create an object using String literal syntax e.g. “Hello World”, it may return an existing object from the String pool, if it already exists. Otherwise, it will create a new String object and put in the string pool for future re-use.
|
||||
|
||||
At a high level, both are the String objects, but the main difference comes from the point that new() operator always creates a new String object. Also, when we create a String using literal – it is interned.
|
||||
|
||||
In general, we should use the String literal notation when possible. It is easier to read and it gives the compiler a chance to optimize our code.
|
||||
|
||||
----
|
||||
|
||||
## Immutablibity of Java Strings
|
||||
|
||||
*Immutable* simply means unmodifiable or unchangeable. This means that once the object has been assigned to a variable, we can neither update the reference nor change the internal state by any means.
|
||||
|
||||
In Java, Strings are immutable. An obvious question that is quite prevalent in interviews is “Why Strings are designed as immutable in Java?” The key benefits of keeping this class as immutable are caching, security, synchronization, and performance.
|
||||
|
||||
Let’s discuss how these things work.
|
||||
|
||||
#### Why String objects are immutable in Java?
|
||||
As Java uses the concept of String literal. Suppose there are 5 reference variables, all refer to one object "Sachin". If one reference variable changes the value of the object, it will be affected by all the reference variables. That is why String objects are immutable in Java.
|
||||
|
||||
Following are some more features of String which makes String objects immutable.
|
||||
|
||||
**1. Heap Space**
|
||||
The immutability of String helps to minimize the usage in the heap memory. When we try to declare a new String object, the JVM checks whether the value already exists in the String pool or not. If it exists, the same value is assigned to the new object. This feature allows Java to use the heap space efficiently.
|
||||
|
||||
Java String Pool is the special memory region where Strings are stored by the JVM. Since Strings are immutable in Java, the JVM optimizes the amount of memory allocated for them by storing only one copy of each literal String in the pool. This process is called **interning**
|
||||
|
||||
**2. Security**
|
||||
The String is widely used in Java applications to store sensitive pieces of information like usernames, passwords, connection URLs, network connections, etc. It’s also used extensively by JVM class loaders while loading classes.
|
||||
|
||||
Hence securing String class is crucial regarding the security of the whole application in general. For example, consider this simple code snippet:
|
||||
|
||||
```java
|
||||
void criticalMethod(String userName) {
|
||||
// perform security checks
|
||||
if (!isAlphaNumeric(userName)) {
|
||||
throw new SecurityException();
|
||||
}
|
||||
|
||||
// do some secondary tasks
|
||||
initializeDatabase();
|
||||
|
||||
// critical task
|
||||
connection.executeUpdate("UPDATE Customers SET Status = 'Active' " +
|
||||
" WHERE UserName = '" + userName + "'");
|
||||
}
|
||||
```
|
||||
|
||||
In the above code snippet, let’s say that we received a String object from an untrustworthy source. We’re doing all necessary security checks initially to check if the String is only alphanumeric, followed by some more operations.
|
||||
|
||||
Remember that our unreliable source caller method still has reference to this userName object.
|
||||
|
||||
If Strings were mutable, then by the time we execute the update, we can’t be sure that the String we received, even after performing security checks, would be safe. The untrustworthy caller method still has the reference and can change the String between integrity checks. Thus making our query prone to SQL injections in this case. So mutable Strings could lead to degradation of security over time.
|
||||
|
||||
It could also happen that the String userName is visible to another thread, which could then change its value after the integrity check.
|
||||
|
||||
**3. Synchronization**
|
||||
Being immutable automatically makes the String thread safe since they won’t be changed when accessed from multiple threads.
|
||||
|
||||
Hence immutable objects, in general, can be shared across multiple threads running simultaneously. They’re also thread-safe because if a thread changes the value, then instead of modifying the same, a new String would be created in the String pool. Hence, Strings are safe for multi-threading.
|
||||
|
||||
**4. Hashcode Caching**
|
||||
Since String objects are abundantly used as a data structure, they are also widely used in hash implementations like HashMap, HashTable, HashSet, etc. When operating upon these hash implementations, hashCode() method is called quite frequently for bucketing.
|
||||
|
||||
The immutability guarantees Strings that their value won’t change. So the hashCode() method is overridden in String class to facilitate caching, such that the hash is calculated and cached during the first hashCode() call and the same value is returned ever since.
|
||||
|
||||
This, in turn, improves the performance of collections that uses hash implementations when operated with String objects.
|
||||
|
||||
On the other hand, mutable Strings would produce two different hashcodes at the time of insertion and retrieval if contents of String was modified after the operation, potentially losing the value object in the Map.
|
||||
|
||||
-----
|
||||
|
||||
# String Builder Class
|
||||
|
||||
String builder is a class that represents a *mutable sequence* of characters.
|
||||
Both StringBuilder and StringBuffer create objects that hold a mutable sequence of characters. Let’s see how this works, and how it compares to an immutable String class:
|
||||
|
||||
```java
|
||||
String immutable = "abc";
|
||||
immutable = immutable + "def";
|
||||
```
|
||||
Even though it may look like that we’re modifying the same object by appending “def”, we are creating a new one because String instances can’t be modified.
|
||||
|
||||
When using either StringBuffer or StringBuilder, we can use the append() method:
|
||||
```java
|
||||
StringBuffer sb = new StringBuffer("abc");
|
||||
sb.append("def");
|
||||
```
|
||||
In this case, there was no new object created. We have called the append() method on sb instance and modified its content. StringBuffer and StringBuilder are mutable objects.
|
||||
|
||||
You can look at more methods available in string buffer at [official documentation](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/StringBuilder.html).
|
||||
|
||||
Some of the commonly used methods are `toString()`, `insert()`, 'delete()',`append()`, `getChars()` etc.
|
||||
|
||||
|
||||
**String Builder Demo**
|
||||
```java
|
||||
public class StringBuilderExample {
|
||||
|
||||
static void generateString(){
|
||||
String s = "";
|
||||
//Adding to String Object
|
||||
// Inffecient Runs in O(n*n)
|
||||
for(int i=0; i<100000;i++){
|
||||
s = s + (char)('A' + i); //inefficient
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
static void generateStringUsingSB(){
|
||||
StringBuilder sb = new StringBuilder();
|
||||
//Efficient
|
||||
//Runs in O(N)
|
||||
for(int i=0; i<100000;i++){
|
||||
sb.append((char)('A' + i)); //efficient
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
//you can do a time comparison for both
|
||||
long start = System.currentTimeMillis();
|
||||
generateStringUsingSB();
|
||||
long end = System.currentTimeMillis();
|
||||
System.out.println(end-start);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
**String Buffer vs String Builder**
|
||||
StringBuffer is synchronized and therefore thread-safe. StringBuilder is compatible with StringBuffer API but with no guarantee of synchronization.Because it’s not a thread-safe implementation, it is faster and it is recommended to use it in places where there’s no need for thread safety
|
||||
|
||||
|
||||
Simply put, the StringBuffer is a thread-safe implementation and therefore slower than the StringBuilder. In single-threaded programs, we can take of the StringBuilder. Yet, the performance gain of StringBuilder over StringBuffer may be too small to justify replacing it everywhere.
|
||||
|
||||
|
@@ -0,0 +1,283 @@
|
||||
### Threads in Java Recap
|
||||
In the Java, multithreading is driven by the core concept of a Thread. Lets recap some logic that runs in a parallel thread by using the Thread framework. In the below code example we are creating two threads and running them in parallel.
|
||||
|
||||
**Using Thread Class**
|
||||
```java
|
||||
public class NewThread extends Thread {
|
||||
public void run() {
|
||||
// business logic
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Class to initialize and start our thread.
|
||||
|
||||
```java
|
||||
public class MultipleThreadsExample {
|
||||
public static void main(String[] args) {
|
||||
NewThread t1 = new NewThread();
|
||||
t1.setName("MyThread-1");
|
||||
NewThread t2 = new NewThread();
|
||||
t2.setName("MyThread-2");
|
||||
t1.start();
|
||||
t2.start();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Using Runnable**
|
||||
```java
|
||||
class SimpleRunnable implements Runnable {
|
||||
public void run() {
|
||||
// business logic
|
||||
}
|
||||
}
|
||||
```
|
||||
The above SimpleRunnable is just a task which we want to run in a separate thread.
|
||||
There’re various approaches we can use for running it; one of them is to use the Thread class:
|
||||
|
||||
```java
|
||||
public void test(){
|
||||
Thread thread = new Thread(new SimpleRunnable());
|
||||
thread.start();
|
||||
thread.join();
|
||||
}
|
||||
```
|
||||
|
||||
Simply put, we generally encourage the use of Runnable over Thread:
|
||||
|
||||
When extending the Thread class, we’re not overriding any of its methods. Instead, we override the method of Runnable (which Thread happens to implement).
|
||||
- This is a clear violation of IS-A Thread principle
|
||||
- Creating an implementation of Runnable and passing it to the Thread class utilizes composition and not inheritance – which is more flexible
|
||||
- After extending the Thread class, we can’t extend any other class
|
||||
- From Java 8 onwards, Runnables can be represented as lambda expressions
|
||||
|
||||
### Thread Life Cycle
|
||||
During thread lifecycle, threads go through various states. The `java.lang.Thread` class contains a static State enum – which defines its potential states. During any given point of time, the thread can only be in one of these states:
|
||||
|
||||
- **NEW** – a newly created thread that has not yet started the execution
|
||||
- **RUNNABLE** – either running or ready for execution but it’s waiting for resource allocation
|
||||
- **BLOCKED** – waiting to acquire a monitor lock to enter or re-enter a synchronized block/method
|
||||
- **WAITING** – waiting for some other thread to perform a particular action without any time limit
|
||||
- **TIMED_WAITING** – waiting for some other thread to perform a specific action for a specified period
|
||||
- **TERMINATED** – has completed its execution
|
||||
|
||||
#### 1.NEW
|
||||
A NEW Thread (or a Born Thread) is a thread that’s been created but not yet started. It remains in this state until we start it using the start() method.
|
||||
|
||||
The following code snippet shows a newly created thread that’s in the NEW state:
|
||||
|
||||
```java
|
||||
Runnable runnable = new NewState();
|
||||
Thread t = new Thread(runnable);
|
||||
System.out.println(t.getState());
|
||||
```
|
||||
|
||||
Since we’ve not started the mentioned thread, the method `t.getState()` prints:
|
||||
```
|
||||
NEW
|
||||
```
|
||||
|
||||
### 2. Runnable
|
||||
When we’ve created a new thread and called the start() method on that, it’s moved from NEW to RUNNABLE state. Threads in this state are either running or ready to run, but they’re waiting for resource allocation from the system.
|
||||
|
||||
In a multi-threaded environment, the Thread-Scheduler (which is part of JVM) allocates a fixed amount of time to each thread. So it runs for a particular amount of time, then leaves the control to other RUNNABLE threads.
|
||||
|
||||
For example, let’s add `t.start()` method to our previous code and try to access its current state:
|
||||
|
||||
```java
|
||||
Runnable runnable = new NewState();
|
||||
Thread t = new Thread(runnable);
|
||||
t.start();
|
||||
System.out.println(t.getState());
|
||||
```
|
||||
|
||||
This code is most likely to return the output as:
|
||||
```
|
||||
RUNNABLE
|
||||
```
|
||||
Note that in this example, it’s not always guaranteed that by the time our control reaches `t.getState()`, it will be still in the RUNNABLE state.
|
||||
|
||||
It may happen that it was immediately scheduled by the Thread-Scheduler and may finish execution. In such cases, we may get a different output.
|
||||
|
||||
### 3. BLOCKED
|
||||
A thread is in the BLOCKED state when it’s currently not eligible to run. It enters this state when it is waiting for a monitor lock and is trying to access a section of code that is locked by some other thread.
|
||||
|
||||
Let’s try to reproduce this state:
|
||||
```java
|
||||
public class BlockedState {
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
Thread t1 = new Thread(new DemoBlockedRunnable());
|
||||
Thread t2 = new Thread(new DemoBlockedRunnable());
|
||||
|
||||
t1.start();
|
||||
t2.start();
|
||||
|
||||
Thread.sleep(1000); //pause so that t2 states changes during this time
|
||||
System.out.println(t2.getState());
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
class DemoBlockedRunnable implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
commonResource();
|
||||
}
|
||||
|
||||
public static synchronized void commonResource() {
|
||||
while(true) {
|
||||
// Infinite loop to mimic heavy processing
|
||||
// 't1' won't leave this method
|
||||
// when 't2' try to enter this
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
In this code:
|
||||
|
||||
We’ve created two different threads – t1 and t2, t1 starts and enters the synchronized commonResource() method; this means that only one thread can access it; all other subsequent threads that try to access this method will be blocked from the further execution until the current one will finish the processing.
|
||||
|
||||
When t1 enters this method, it is kept in an infinite while loop; this is just to imitate heavy processing so that all other threads cannot enter this method
|
||||
|
||||
Now when we start t2, it tries to enter the commonResource() method, which is already being accessed by t1, thus, t2 will be kept in the BLOCKED state.
|
||||
Being in this state, we call `t2.getState()` and get the output as:
|
||||
```
|
||||
BLOCKED
|
||||
```
|
||||
|
||||
### 4. WAITING
|
||||
A thread is in WAITING state when it’s waiting for some other thread to perform a particular action. According to JavaDocs, any thread can enter this state by calling any one of the following three methods:
|
||||
|
||||
- object.wait()
|
||||
- thread.join() or
|
||||
- LockSupport.park()
|
||||
|
||||
Note that in wait() and join() – we do not define any timeout period as that scenario is covered in the next section.
|
||||
|
||||
|
||||
In this example, thread-1 starts thread 2 and waits for thread-2 to finish using `thread.join()` method. During this time t1 is in `WAITING` state.
|
||||
|
||||
**Simple Runnable.java - Thread 1**
|
||||
```java
|
||||
public class SimpleRunnable implements Runnable{
|
||||
|
||||
@Override
|
||||
public void run(){
|
||||
Thread t2 = new Thread(new SimpleRunnableTwo());
|
||||
t2.start();
|
||||
try {
|
||||
t2.join();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**Simple Runnable 2 - Thread 2**
|
||||
```java
|
||||
public class SimpleRunnableTwo implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
try{
|
||||
Thread.sleep(5000);
|
||||
}
|
||||
catch(InterruptedException e){
|
||||
Thread.currentThread().interrupt();
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
**Main**
|
||||
```java
|
||||
public class Main {
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
Thread t1 = new Thread(new SimpleRunnable());
|
||||
t1.start();
|
||||
|
||||
Thread.sleep(1000); //1ms pause
|
||||
System.out.println("T1 :"+ t1.getState()); //T1 is waiting state
|
||||
System.out.println("Main :" + Thread.currentThread().getState());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. TIMED WAITING
|
||||
A thread is in `TIMED_WAITING` state when it’s waiting for another thread to perform a particular action within a stipulated amount of time.
|
||||
|
||||
According to JavaDocs, there are five ways to put a thread on TIMED_WAITING state:
|
||||
|
||||
- thread.sleep(long millis)
|
||||
- wait(int timeout) or wait(int timeout, int nanos)
|
||||
- thread.join(long millis)
|
||||
- LockSupport.parkNanos
|
||||
- LockSupport.parkUntil
|
||||
|
||||
Here, we’ve created and started a thread t1 which is entered into the sleep state with a timeout period of 5 seconds; the output will be `TIMED_WAITING`.
|
||||
|
||||
```java
|
||||
public class SimpleRunnable implements Runnable{
|
||||
@Override
|
||||
public void run() {
|
||||
try{
|
||||
Thread.sleep(5000);
|
||||
}
|
||||
catch(InterruptedException e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
In Main, if you check the state of T1 after 2s it will be `TIMED WAITING`
|
||||
```java
|
||||
public class Main {
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
Thread t1 = new Thread(new SimpleRunnable());
|
||||
t1.start();
|
||||
|
||||
Thread.sleep(2000);
|
||||
System.out.println(t1.getState());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 6. TERMINATED
|
||||
This is the state of a dead thread. It’s in the `TERMINATED` state when it has either finished execution or was terminated abnormally. There are different ways of terminating a thread.
|
||||
|
||||
Let’s try to achieve this state in the following example:
|
||||
```java
|
||||
public class TerminatedState implements Runnable {
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
Thread t1 = new Thread(new TerminatedState());
|
||||
t1.start();
|
||||
|
||||
// The following sleep method will give enough time for
|
||||
// thread t1 to complete
|
||||
Thread.sleep(1000);
|
||||
System.out.println(t1.getState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// No processing in this block
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
Here, while we’ve started thread t1, the very next statement Thread.sleep(1000) gives enough time for t1 to complete and so this program gives us the output as:
|
||||
```
|
||||
TERMINATED
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
54
Non-DSA Notes/LLD1 Notes/Miscellaneous Topics/Volatile.md
Normal file
54
Non-DSA Notes/LLD1 Notes/Miscellaneous Topics/Volatile.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Volatile Keyword
|
||||
---
|
||||
|
||||
## Recap of Basics
|
||||
- Interleaving: When threads start and pause, in the same blocks as other threads, this is called interleaving.
|
||||
- The execution of multiple threads happens in arbitrary order. The order in which threads execute can't be guranteed.
|
||||
|
||||
## Atomic Action
|
||||
An action that effectively happens all at once - either it happens completely or doesn't happen at all.
|
||||
|
||||
Even increments and decrements aren't atomic, nor are all primitive assignments.
|
||||
Example - Long and double assignments may not be atomic on all virtual machines.
|
||||
|
||||
## Thread Safe Code
|
||||
An object or a block of code is thread-safe, if it is not comprised by execution of concurrent threads.
|
||||
This means the correctness and consistency of program's output or its visible state, is unaffected by other threads.
|
||||
Atomic Operations and immutable objects are examples of thread-safe code.
|
||||
|
||||
In real life, there are shared resources which are available to multiple concurrent threads in real time. We have techniques, to control acess to the resources to prevent affects of interleaving threads. These technicques are -
|
||||
1) Synchronisation/Locking
|
||||
2) Volatile Keyword
|
||||
|
||||
### Problem 1 - Atomicity/Synchronization
|
||||
... alreaedy seen ...
|
||||
|
||||
|
||||
### Problem 2 - Memory Inconsistency Errors, Data Races
|
||||
The Operating system may read from heap variables, and make a copy of the value in each thread's own storage. Each threads has its own small and fast memory storage, that holds its own copy of shared resource's value.
|
||||
|
||||
Once thread can modify a shared variable, but this change might not be immediately reflected or visible. Instead it is first update in thread's local cache. The operating system may not flush the first thread's changes to the heap, until the thread has finished executing, causing memory inconsistency errors.
|
||||
|
||||
### Solution - Volatile Keyword
|
||||
- The volatile keyword is used as modifier for class variables.
|
||||
- It's an indicator that this variable's value may be changed by multiple threads.
|
||||
- This modifier ensures that the variable is always read from, and written to the main memory, rather than from any thread-specific cache.
|
||||
- This provides memory consistency for this variables value across threads.
|
||||
Volatile doesn't gurantee atomicicty.
|
||||
|
||||
However, volatile does not provide atomicity or synchronization, so additional synchronization mechanisms should be used in conjunction with it when necessary.
|
||||
|
||||
**When to use volatile**
|
||||
- When a variable is used to track the state of a shared resource, such as counter or a flag.
|
||||
- When a varaible is used to communicate between threads.
|
||||
|
||||
**When not use volatile**
|
||||
- When the variable is used by single thread.
|
||||
- When a variable is used to store a large amount of data.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user