Java Class Notes
Java Class Notes
● Java is a statically typed language. One of its key features is that it can be run on
many different platforms without modifying the code. This features is known as
“Write once, run anywhere”
● Java includes a built-in garbage collector that frees memory during runtime
● While Java supports multiple programming paradigms, it is primarily an object
oriented language: almost every part of the program can be considered an
object. The program itself can be considered as a set of interacting objects.
● When a vanilla Java application is run, a bunch of objects are instantiated
corresponding to the classes. These objects are related, and there is thus a
network of objects that are connected using object references. This network of
objects basically maps out dependencies between objects.
● Values such as numbers and strings are called ‘literals’. Java supports several
types of literals
● Public classes should only be declared in a .java file named after the class.
Therefore, you can only have one public class per file
● Java source code is written in .java files. A compiler (such as javac) compiles into
a much lower level form known as “bytecode”. Bytecode cannot be executed
natively by a computer, it is instead used by JVM (Java Virtual Machine). JVM,
which can be installed on many runtime environments, converts the bytecode
into native instructions and executes them on the computer.
● Bytecode has a .class file extension
● Bytecodes are generic and not specific to any runtime environment. Only the
JVM is specific to a system. Therefore, you can have generic Bytecode that can
run on Linux JVM, a mobile JVM, etc.
● Because a compiler converts Java source code to bytecodes, compilers also
exist that compile non-Java languages to bytecode. The JVM doesn’t know or
care what language or environment generated the bytecode. The following
illustration paints a picture of this:
● Obviously each platform has its own JVM, but they all behave identically with a
given bytecode, since they all function according to the JVM specification
document. Java HotSpot is one of the more popular JVMs
● Obviously the compiler runs natively on the system you’re using to write Java
code, so the compiler itself isn’t platform independent. However, all compilers
regardless of platform generate the same generic bytecode
● The JRE (Java runtime environment) is an execution environment for running
bytecode. It includes the JVM and the Java Class Library. Therefore, you almost
certainly need a JRE to run your bytecode it contains the class library
● A JDK (Java Development Kit) includes everything in the JRE plus a Java
compiler, debugger, etc.
● In practice, Java programs often consist of multiple .class files bundled into a
Java archive (JAR) file.
● The following diagram shows the relationship between JVM, JRE, and JDK:
Basics
● Java supports break and continue statements in loop just as you’d expect
● In a Switch statement, the break keyword is optional. If a case is satisfied and it
does not have a break keyword, the subsequent cases including the default case
will be executed as well, until a break statement is encountered. For this reason,
be careful about including the break statement. The following snippet illustrates
this concept which is known as a “switch statement fallthrough”:
switch (digit)
{
case 0:
result = ZERO_DIGIT;
break;
case 2:
case 4:
case 6:
case 8:
result = EVEN_DIGIT;
break;
}
● Java supports “for-each” loops, where we can access each element of an array,
string, or collection without having to use indexes. The following demonstrates
the syntax:
for (type var : array) {
//statements using var
}
● The key limitation of for-each loop is that the variable var will be a copy of the
array element, and doesn’t refer to the array element itself. Therefore, we cannot
modify the array in a for-each loop.
● Methods are declared by specifying access modifiers (if any), and use C like
syntax. See the image below:
● The name of the method combined with the types of it’s parameters comprise its
signature. In this example, the signature is countSeeds(int int)
● Here, we use the public “access modifier” to allow the method to be called from
other classes. We use the static “non-access modifier” to tell JVM that this
method can be called even if we haven’t instantiated an object of its class. It is
called a “static method”
● An access modifier is basically a keyword that allows us to choose who can
access a part of our code. Examples include public, package-private,
protected, and private
● Because Java forces the programmer to use classes, the application is
essentially a collection of objects that communicate by calling each other's
methods. Even a simple procedural style program needs to have at least one
class and its main method.
● The main method is typically declared as public static since it can be called
from anywhere and we do not need to instantiate the class it is declared in.
● Just like there’s a standard output, there’s also a standard input supported by the
operating system. It is a stream of data that can be read into the program
● By default, it obtains data from the keyboard but it can also be redirected to a file
● The simplest way to read from the standard input is using the standard class
Scanner, which can be imported by adding the following on top of your code:
import java.util.Scanner;
● We declare a new Scanner object by calling its constructor as follows: Scanner
scanner = new Scanner(System.in);
● Here, we pass System.in as the constructor parameter to indicate that we will
be reading from standard input from the keyboard
● We can use the Scanner object’s next() method to read a single word from a
string. This will always read one word as a string. If the input is numerical, it will
still read it as a string rather than an integer
● The method nextInt() works similarly as next() but it returns an integer.
nextLong() works in a similar manner.
● A similar method is nextLine(), which reads all data (including whitespace) until
a newline is encountered
● A ‘token’ refers to a sequence of characters surrounded by whitespace. You can
think of it as a ‘word’
● Methods hasNext() and hasNextLine() and hasNextInt() return a boolean
indicating whether there is anything left to read. See the snippet below, which
reads keeps reading and printing tokens until there is nothing left to read:
while(scanner.hasNext())
System.out.println(scanner.next());
● Java classes can have two types of methods, instance methods and static
methods. Instance methods require the class to be instantiated (i.e. an object
needs to be created) before the method can be called. Static methods can be
called from the actual class itself even if it hasn’t been instantiated to create an
object.
● Constructors have no return type (not even void), and have the same name as
the class.
● Instance methods have access to the fields of the object. Static methods
obviously do not since they can be called before instantiation (when the fields
don’t exist!). The following snippet exemplifies:
class Human {
String name;
int age;
public Robot() {
this.name = "Anonymous";
this.model = "Unknown";
}
● Sometimes, objects may all share a field or method with the same value. Such
fields, known as static members, may be declared with the static keyword.
● A static field is a class variable declared using the static keyword. It can store a
primitive or a reference type. It has the same value for all objects of the class.
Therefore, this field belongs to the class itself, not individual objects.
● If we want all instances of a class to have the same value for a field, it’s better to
make this field static since it can save us memory.
● Static fields can be accessed from the class even if we haven’t instantiated any
objects of the class, like so: ClassName.fieldName;
● Static fields can also be accessed from object instantiations of the class
● Here’s an interesting example where all instances of the class share a static field
storing the current date. This date is updated every time a new object is
instantiated:
public class SomeClass {
public SomeClass() {
lastCreated = new Date();
}
}
● Note that static fields are not necessarily final or constant. They can be updated
(either outside the class or by an instantiation)
● It is common for static fields, especially public static fields, to be constant. They
are declared by using static final keywords in the variable declaration.
● By convention, final fields should be named in all caps and underscores
● Java also supports static methods. These methods can be called from the class
name even if no object has been instantiated.
● For obvious reasons, static methods cannot access non-static fields (i.e. it
doesn’t have access to this keyword). Also, static methods cannot call
non-static methods. A static method can be called from the class without
instantiation like className.staticMethod(arg1, arg2), or from an object like
object1.staticMethod(arg1, arg2)
● This is why the main method is static. It is called even though the class it is
contained in isn't instantiated.
● In Java, an interface is a special type of class that cannot be instantiated. It is
implemented by child classes who implement its methods. If a class implements
an interface, it needs to implement all its declared abstract methods.
● An abstract method is a method with no body. It’s usually meant to be overridden
by child classes.
● Note that “extends” and “implements” are two separate keywords in Java
● The interface only declares an abstract method, it doesn’t implement it. The
implementation is done by its child classes. Here’s a snippet as an example:
interface DrawingTool {
void draw(Curve curve);
}
class Pencil implements DrawingTool {
...
public void draw(Curve curve) {...}
}
● This way, just a quick look at the classes Pencil and Brush inform us that it’s able
to implement all the functionalities of DrawingTool. The purpose of interfaces is to
declare functionality.
● Interfaces can be used as a type. See below:
DrawingTool pencil = new Pencil();
DrawingTool brush = new Brush();
● Now both pencil and brush are of the same type. Both of these objects can be
treated similarly. This is a form of polymorphism. It allows us to write methods
that can accept any of the interface implementations. See below, this function
can accept either pencil or brush:
void drawCurve(DrawingTool tool, Curve curve)
● In an interface, all variables must be public static final. You don’t need to specify
these access modifiers, the variables will be public static final by default.
● Methods are public abstract by default (these keywords are not required in an
interface). An abstract method is one where it is not implemented, only the
function signature is defined.
● You can implement default methods with implementation (default keyword
required)
● You can implement static methods (static keyword required), and private
methods (private keyword required). These must be implemented in the
interface.
● Static methods can be invoked directly from the interface (i.e. even if we haven’t
instantiated its child class).
● An interface cannot contain non-constant fields and it cannot have a constructor,
since interfaces cannot be instantiated. See the snippet below:
interface Interface {
int INT_CONSTANT = 0; //public static final by default
void instanceMethod1();
void instanceMethod2(); //These two functions are abstract
static void staticMethod() { //static method
System.out.println("Interface: static method");
}
default void defaultMethod() {
System.out.println("Interface: default method. It can be
overridden");
}
private void privateMethod() {
System.out.println("Interface private method");
}
}
@Override
public void instanceMethod2() {
System.out.println("Child: instance method2");
}
}
interface A { }
interface B { }
interface C { }
//We declare a new interface E which extends multiple interfaces
interface E extends A, B, C { }
● Furthermore, a class can both extend another class and implement an interface:
class A { }
interface B { }
interface C { }
class D extends A implements B, C { }
● Interfaces are useful because they allow polymorphism. Each class has its own
unique implementation of the abstract class defined in the interface. However,
the objects can have the reference type of their interface. This allows, among
other things, for a method to call an object’s method without knowing the exact
type of the object. For example:
void drawInstruments(DrawingTool[] instruments){
for(instrument: instruments){
instrument.draw();
}
}
//in main method
DrawingTool pencil = new Pencil();
DrawingTool brush = new Brush();
● It’s important to note that whenever a subclass’s constructor is called, it calls its
parent class’s no-args constructor by default. This occurs even if we don’t
explicitly call the parent’s constructor using super(). As the example shows, we
can call super() explicitly in the child constructor with parameters if we’d need
to.
● When we have superclasses and child classes, there are two ways to create a
child object. We can use a subclass reference or a superclass reference. See the
example below: (assume default no-args constructor is present)
class Person {
protected String name;
protected int yearOfBirth;
protected String address;
}
//Assume getters and setters exist for these classes
class Client extends Person {
protected String contractNumber;
protected boolean gold;
}
● We can always cast a subclass reference to its superclass. Then we can access
members only present in the superclass. If we have an object of superclass
reference but is an instance of a subclass, we can also cast it to a subclass
reference. See the example below. We first cast a superclass reference (Person)
to its subclass (Client). This is possible because the superclass reference
(client2) is an instance of the subclass Client. In the next line, we cast a subclass
(Client) to its superclass (Person), which is always allowed.
Client newClient = (Client) client2 //We have cast client2 to
subclass Client
Person newPerson = (Person) client //We have cast client to
superclass Person
● Two common use cases when we might want to use superclass references is
when we have an array of objects of different types within the hierarchy (i.e a mix
of superclass objects and subclass objects). Another common use case is when
we have a method that accepts the superclass type but also works with
subclasses. Here’s an example to demonstrate:
public static void printNames(Person[] persons) {
for (Person person : persons) {
System.out.println(person.getName());
}
}
● A subclass can have a method with the same function signature as one of its
superclass’s methods. This concept is known as method overriding (not the same
thing as method overloading!)
● It allows the subclass to have its own unique implementation of a superclass
method that’s more specific. This is only possible when the subclass inherits a
method from its superclass (i.e. you cannot override a superclass’s private
method, since the subclass doesn’t inherit it). Here’s an example:
class Mammal {
public String sayHello() {
return "ohlllalalalalalaoaoaoa";
}
}
class Cat extends Mammal {
@Override
public String sayHello() {
return "meow";
}
}
class Human extends Mammal {
@Override
public String sayHello() {
return "hello";
}
}
Mammal mammal = new Mammal();
System.out.println(mammal.sayHello()); // it prints
"ohlllalalalalalaoaoaoa"
Cat cat = new Cat();
System.out.println(cat.sayHello()); // it prints "meow"
Human human = new Human();
System.out.println(human.sayHello()); // it prints "hello"
● Here’s a code snippet that shows that Java correctly calls the subclass’s override
method even we use superclass references:
class Animal {
public void say() {
System.out.println("...An incomprehensible sound...");
}
}
● The Object class provides methods that all subclasses (i.e. literally every class
that exists) can access. It includes the following methods that can be handy in
multithreaded programming: wait, notify, notifyAll
● It has the following methods useful for object identity, hashCode and equals
● It has the following methods for object management: clone and getClass.
clone creates an identical copy of the object and returns it.
● It also contains a method called toString() which is used to return info on the
object in human readable form. This method is often overloaded in our classes.
● toString() allows us to print the contents of an object right from System.out.
For example, if human1 is the name of an object, we can print simply as
System.out.println(human1); This is also handy when overriding classes
that contain other classes as fields. However, be careful if two classes contain
each other as their fields. This can cause an infinite recursion when we call
toString, since the object will print its field, which will in turn print its field which
is the original object we called toString on!
●
Strings
● It's important to note that strings are immutable in Java. It’s value cannot be
changed once it is declared.
● The benefit of immutable data is that they are thread safe: they can be shared by
different threads safely
● Even though immutable objects cannot be changed, that doesn't mean the
variable holding the immutable object cannot be reassigned. For example, see
the snippet below:
String temp = "abc";
temp = temp + "def"; //this is allowed since we're assigning a new
value to variable temp instead of updating it’s existing value
● The string object has numerous methods that can come in handy. You can look
them up as necessary. One of these methods is String.format(), which allows
you to create strings with other variables using printf-like syntax. For example,
String sf1=String.format("name is %s",name);
● Strings can be concatenated both with other strings as well as different data
types. When concatenating with other data types, they are automatically
converted to the appropriate string value. See the snippet below:
String str = "str" + 10 + false; //str equals "str10false"
Advanced
● In Java, all data types are either “primitive types” or “reference types”. Primitive
types are stored in the stack. In reference types, the actual data will be stored in
the heap and a pointer to the heap address will be stored in the stack.
● Java has eight primitive types, which are lowercase. Reference types often begin
with an uppercase letter
● In most cases, reference types are created with the new keyword, which allocates
memory on the heap to store the object. This is called “instantiation”, since we
create an instance of the class. See the example below:
String language = new String("java");
String language2 = "java";
● In this example, the first line is the typical of instantiating a reference object. The
second method is String specific and equivalent to the first line.
● If you execute the new keyword twice on the same variable, a brand new object
is instantiated and assigned to the variable. The object that was previously
attached to the variable is lost unless its reference was assigned to another
variable before the reassignment. See the snippet below to demonstrate:
String temp = new String("Java");
temp = new String("Javascript"); //A new object is instantiated and
it's reference is stored in temp. The previous object containing
"Java" is now lost and will be picked up by the garbage collector
● That being said, because the String type is immutable, if we were to assign new
text to temp3 in the example above, temp4 would continue to hold “Example”
because a new address would be initialized when we reassign temp3.
● Because Strings are immutable, they behave much like primitive types in
practice.
● Whenever we reassign a String variable, a brand new object is instantiated and
its address is stored in the variable. The String object this variable held before is
either lost, or is held by any other variable it was assigned to.
● Because of the way reference types work, comparisons using == and != don’t
work the way they do for primitive types. The comparison operator compares the
reference addresses, not the contents of the data itself. The following example
illustrates:
String s1 = new String("java");
String s2 = new String("java"); //This string is different and has a
different address from s1
String s3 = s2; //Only the address is copied, not the actual data in
the heap
System.out.println(s1 == s2); // false because s1 and s2 point to
different addresses
System.out.println(s2 == s3); // true because they have the same
address
● With reference types, we can set it to null which can indicate that it hasn’t been
instantiated yet or doesn’t have a value. This does not work with primitive types.
For example, String s1 = null;
● In Java, arrays are a reference type and thus need to be instantiated using the
new keyword. Arrays hold a fixed size of the same datatype sequentially (the size
cannot be changed once the array has been instantiated)
● An array data type can be declared as follows: int[] array; but this does not
allocate any memory since we haven’t used the new keyword yet.
● We can declare and instantiate an array as follows: int[] numbers = new
int[n];
● Since we haven’t enumerated the contents of the array, they are initialized to the
default value of their datatype (0 in the case of int)
● Alternatively, the following syntax also instantiates an array even though we
haven’t used the new keyword: int[] numbers = { 1, 2, 3, 4 };
● The array object has a field called length, which returns the capacity of the array.
For example, numbers.length //returns 4
● We have access to some handy array methods if we import utility class Arrays.
We can import it using import java.util.Arrays;
● The following snippet demonstrates some of these functionalities:
int[] famousNumbers = { 0, 1, 2, 4, 8, 16, 32, 64 };
Arrays.sort(famousNumbers);
String arrayAsString = Arrays.toString(famousNumbers);
Arrays.equals(numbers1, numbers2); //returns true if numbers1 and
numbers2 are arrays of the same length containing the same value at
each index
Int size = 10;
char[] characters = new char[size];
Arrays.fill(characters, 0, size / 2, 'A'); //fill array starting at
index 0, up to and excluding size/2 with 'A'
● Because arrays are a reference type, when you pass an array to a method, any
changes you make to the array inside the method will reflect outside the method
as well.
● We can accept an unknown number of parameters in a method using varargs
(variable length arguments). We declare a vararg by typing three dots before the
type.
● When a method accepts two or more parameters, the varargs must be the last
one in the function signature. See the snippet below, where we declare a vararg
named varPam
public static void method(int a, double... varPam) { /* do something
*/ }
method(1, 2, 3); //a is 1, and varPam is [2,3]
method(1, new int[] { }); // a is 1, no arguments here for varPam
● As you can see, varargs can accept either separate integers in the method call
(separated by commas), or it can accept an array of integers.
● Java lets us declare final variables which cannot be modified once assigned.
These variables are called constants. It is standard practice to name constants
using all capital letters and underscores. See the snippet below to demonstrate
some concepts:
final int temp; //this is allowed, don't need to assign at the same
time as declaration
System.out.print(temp); //this causes an error, we cannot use a
constant before assigning it
temp = 30; //we now assign this constant. It cannot be modified going
forward
temp = temp + 1; //error, we cannot modify a constant
● When we use the final keyword with a reference type, it only prevents us from
reassigning the constant variable. It does not prevent us from changing the
internal state of the object. This is because at its core a reference variable only
stores an address to heap memory. With a final reference type, you cannot
change the address stored in the constant (via reassignment), but we can
change the data in the heap location it points to (changing the internal contents
of the object)
● Note that the final keyword is used in other contexts in Java in addition to
declaring constant variables (more research may be needed)
● Java has support for “annotations”, which provide metadata and mark classes,
variables, methods, etc.
● Annotations can be used to provide information to the compiler, to provide info to
the IDE (to generate code, etc), or to provide info to frameworks and libraries at
runtime
● The syntax for including an annotation is the @ symbol followed by the name of
the annotation. Java has three built-in annotations, @Deprecated which indicates
that the marked element (class, method, etc) is deprecated and should be
avoided.
● @SuppressWarnings tells the compiler to disable compile-time warnings.
@SuppressWarnings must be specified with annotation parameters to tell the
compiler what type of warnings to ignore.
● @Override is used to mark a method that overrides a superclass method. This
annotation can only be applied to methods.
● Some annotations accept elements. These elements have a name and type. For
example, @SuppressWarnings accepts an element called “value”. This
annotation has no default value, so the value element must always be specified.
See the following snippet:
@SuppressWarnings(value = "unused") //Suppress warnings about unused
variables
@SuppressWarnings("unused") //This is allowed when we have just one
element named "value"
@SuppressWarnings({"unused", "deprecation"}) //Passing array as value
● Packages help avoid conflicts in class names. Since even if two classes have the
same name, their full name (including the package) will be different. (Unless of
course there is also a conflict in the package name!)
● To avoid package name conflicts, it is good practice to begin the name of your
package with your domain name, in reverse. For example, org.hyperskill
● We use the import keyword to be able to use classes defined in another
package
● If we’re using both the package and import keywords in a file, the package must
come first.
● We can use a * to import all classes from a package, but you should avoid doing
this if you can help it. For example import java.util.*;
● We can use classes from other packages without using the import keyword if we
write out its full name, for example java.util.Scanner scanner = new
java.util.Scanner(System.in);
● We can import static fields and methods of a class by using the static keyword in
our import statement. Furthermore, if we use a * in this statement, we don’t need
to mention the class name before invoking its static methods. For example, here
we import all static methods from class Arrays:
import static java.util.Arrays.*;
//in main method, we have an array called numbers
sort(numbers); // instead of writing Arrays.sort(...)
● As you can see, the base class for all exceptions is java.lang.Throwable
● Its two direct subclasses are java.lang.Error and java.lang.Exception
● We have access to the following methods, getMessage which returns a String
with details about this exception object, getCause which returns a Throwable
object with the cause of this exception, and printStackTrace which prints a
stack trace on the standard error stream
● The java.lang.Error class represents low-level errors in JVM like stack
overflow
● As a developer, you will usually have to deal with the Exception class and its
subclasses, especially RuntimeException
● Checked exceptions are represented by the Exception class, excluding the
RuntimeException class.
● We handle exceptions using try catch blocks. We can also include a finally block,
which will always execute last whether or not an exception was encountered.
● In Java, each primitive type is accompanied with a dedicated class. These
classes are called “wrappers” and they are immutable, just like String.
● Here’s a table showing the wrapper classes are constructor arguments for all the
primitive types:
● The term “boxing” refers to converting a primitive type to an object of its wrapper
class. See the example below:
int primitive = 100;
Integer reference = Integer.valueOf(primitive); // boxing
int anotherPrimitive = reference.intValue(); //unboxing
● As you can see valueOf is a static method of the Integer class that returns an
object of its wrapper type. intValue() is an instance method that returns the
value of this wrapper object.
● “Autoboxing” and “auto-unboxing” is simpler syntax for doing the same thing.
They are automatic conversions handled by the compiler See the example
below:
double primitiveDouble = 10.8;
Double wrapperDouble = primitiveDouble; // autoboxing
double anotherPrimitiveDouble = wrapperDouble; // auto-unboxing
● Note that autoboxing only works when the left and right side of the assignment
are of the same type.
● When we want to create wrapper objects using other types, we can use their
constructor methods. See the example below:
Integer number = new Integer("10012"); // an Integer from "10012"
Float f = new Float("0.01"); // a Float from "0.01"
Long longNumber = new Long("100000000"); // a Long from "100000000"
Boolean boolVal = new Boolean("true");
● Beware that unboxing a wrapper object can cause a null pointer error if the object
is null. To prevent this, we can check to make sure the object is not null before
trying to unbox it.
● You can perform arithmetic operations on wrapper objects just like you would
with primitive types.
● A key reason to use wrapper objects is that they can be used in standard
collections (like list, set), while primitives cannot.
●
Threads
t.setName("my-thread");
System.out.println("New name: " + t.getName());
}
}
● isAlive() returns a boolean indicating whether the thread has been started and
hasn’t died yet.
● Threads with a higher priority are executed with higher preference.
● The main thread is our starting ground from where we can spawn new threads to
perform tasks.
● There are two ways to create our own thread: we can write our own class that
extends the Thread class and overwrites its run method:
class HelloThread extends Thread {
@Override
public void run() {
String helloMsg = String.format("Hello, i'm %s", getName());
System.out.println(helloMsg);
}
}
//inside main
Thread t1 = new HelloThread(); // a subclass of Thread
● Or, we can implement an already existing interface called Runnable, and pass its
implementation to the constructor of Thread:
class HelloRunnable implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
String helloMsg = String.format("Hello, i'm %s", threadName);
System.out.println(helloMsg);
}
}
//inside main
Thread t2 = new Thread(new HelloRunnable()); // passing runnable
@Override
public void run() {
while (true) {
int number = scanner.nextInt();
if (number == 0) {
break;
}
System.out.println(number * number);
}
System.out.println(String.format("%s finished", getName()));
}
}
● Our class that extends Thread, or our class that implements runnable, may have
private variables and a constructor to handle them accordingly. For example,
Runnable task = new PrintMessageTask("Hi, I'm good.");
Thread worker = new Thread(task);
● Here, we start a new thread called worker, and make the current thread wait until
it’s finished until we go on to print the final statement. Notice that we can pass a
float parameter, which will limit the amount of time the current thread will wait in
milliseconds. In this example, the current thread will wait until worker is done, or
it’ll resume after three seconds if the worker still isn’t done.
● If an error occurs in a thread that is not caught and handled by a method, the
thread will be terminated. If we are running a single threaded program, this
means the whole program will end. This is because JVM terminates a program
as soon as there are no more non-daemon threads active.
● This is the case even when the main thread has an error. The program will
continue until other threads finish.
● Keep in mind that even if a thread ended with an error, the other threads will
continue on. If thread A is waiting for thread B to finish using join(), and thread
B has an exception, thread A will resume its execution form its next statement
after join()
● Exceptions in threads are handled independently. It is good practice to write
exception handlers especially when dealing with multi-threaded programs.
● All threads in a process share the same heap memory. This can be used to
facilitate inter thread communication.
● If multiple threads are working on the same data concurrently, there are a few
points we ought to keep in mind:
● Not all operations are atomic. A non-atomic operation is an operation consisting
of multiple steps. If a thread operates on an intermediate value of a non-atomic
operation being performed by another thread, we run into a problem called
“thread interference”. It is where the sequence of steps for a non-atomic
operations may overlap between threads.
● Changes of a variable performed by one thread may be invisible to others
● If changes are visible, their order might not be (reordering)
● See the snippet below to demonstrate:
class Counter {
private int value = 0;
public void increment() {
value++;
}
public int getValue() {
return value;
}
}
● Here, we don’t have to deal with any issues since the main method waits for
thread1 to finish before starting thread2.
● Even though Counter’s method increment() only has one line of code, value++, it
is still not an atomic operation. It in fact has three distinct operations: read the
value, increment it, and write it back to the value variable.
● For this reason, it is prone to thread interference if two threads call increment()
on the same instance of counter around the same time.
● For example, thread A may read the value zero. It then increments it by one (but
it hasn’t written this new value back to the variable yet). Then, thread B reads the
value (still zero) and increments it by one. Now, thread A gets around to writing
the updated value to the variable (one). Finally, thread B writes the updated value
it calculated to the variable (also one!). Therefore, even though increment was
called twice, because of thread interference the value of the counter only
increased by one.
● Note that the reading and writing of primitive types (except long and double) are
guaranteed to be atomic. long and double types can also be made atomic by
declaring them with the volatile keyword.
● For various reasons such as compiler optimization, caching, etc, a change in a
value by one thread may not be visible to another thread. We can avoid this
problem by declaring the variable with the volatile keyword. This keyword may
be used in an instance variable or a static variable.
● Note that the volatile keyword still doesn’t make increment and decrement
operations atomic.
● Java provides ways to synchronize threads to avoid some of these issues.
● A “critical section” is code that accesses shared resources and should not be
executed by more than one thread at the same time. This resource could be a
variable, an external file, a database, or anything else. In the previous example,
the increment operation in the Counter is a critical section.
● A “monitor” is a mechanism in Java to control access to objects. Each class and
object has an implicit monitor. If a thread acquires a monitor for an object, then
other threads must wait until the owner (the thread that has the monitor) releases
it. Then the other thread can take ownership of the object by acquiring its
monitor.
● Thus, a thread can be locked out from an object through its monitor, until the
monitor is released. This allows programmers to protect critical sections from
being executed at the same time by different threads.
● The “classic” and simplest way to protect critical sections is by using the keyword
synchronized. We may have synchronized methods (either static or instance
method), or synchronized blocks or statements (inside a static or instance
method).
● When we declare a method or block as synchronized, the monitor for the object it
is in ensures that only one thread can access the synchronized method or block
(the critical section) at a time.
● When we declare a static method as synchronized, the monitor is the class itself.
Only one thread can execute the body of this method at a time.
● If an instance method is declared as synchronized, the monitor is the instance
(the object). In this case, only one thread can execute this particular object’s
method at a time. It does not stop threads from executing this method on other
objects at the same time. For example, if we have two objects of the same type
called A and B, and they have a method declared as public synchronized
void doSomething(), two threads can execute doSomething on A and B
simultaneously, since it is an instance method and A and B have their own
monitors. However, two threads cannot execute doSomething on A
simultaneously.
● You may also create a synchronized block, when a part of the body of a method
is the critical section. See the example below:
class SomeClass {
public static void staticMethod() {
// unsynchronized code ...
synchronized (SomeClass.class) { // synchronization on the
class
// synchronized code
}
}
public void instanceMethod() {
// unsynchronized code
synchronized (this) { // synchronization on this instance
// synchronized code
}
}
}
● As you can see, we need to specify the object that holds the monitor and can
lock the thread. In the case of a static method, it is the class itself. In an instance
method, it is the object (specified with the this keyword)
● Any changes made by a thread inside a synchronized method or block will be
visible to other threads after they acquire the monitor. This is one of the reasons
why it might be a good idea to synchronize getter methods as well, since if the
getter is not synchronized, a thread might need to acquire any monitor if it’s only
calling the getter. Therefore, it is not guaranteed that this thread will get the latest
value.
● Going back to the Counter example, we can avoid thread interference simply by
declaring the increment and getValue methods as synchronized. The getValue
method should be synchronized to ensure that we return the correct updated
value after it has been incremented.
● It is not necessary to synchronize methods that only read shared data (such as
by calling a getter method) if we only read after writer threads have finished
execution. We ensure that reading takes place after writer threads are done
using join(). This is the case in the Counter example above, where only the
main thread calls getValue() after the writer threads have finished. Therefore,
this example would work even if getValue wasn’t synchronized (increment still
needs to be synchronized, however).
● It is also not necessary to synchronize methods that only read shared data (such
as by calling a getter method), if the resource in question is a volatile variable.
● It’s important to note that when a class has multiple synchronized methods or
blocks, they all share the same monitor which is anchored to the object.
Therefore, when one synchronized instance method is being executed, other
threads cannot execute the other synchronized methods or blocks either. See the
snippet below:
class SomeClass {
public synchronized void method1() {
// do something useful
}
public synchronized void method2() {
// do something useful
}
public void method3() {
synchronized (this) {
// do something useful
}
}
}
● Here, all both methods as well as the block are synchronized using the same
monitor (the object itself). Therefore, only one thread can execute one of these
methods at a time per object. It is not possible, for example, for one thread to
execute method1 and another thread to execute method2 at the same time on
the same instance. Both methods and the block share the same monitor, so only
one of them can be executed at a time regardless of how many threads have
access to the object.
● Similar behavior as the above exists when we have multiple synchronized static
methods.
● A thread cannot acquire a lock held by another thread, but it can acquire a lock it
already owns, this is called “reentrant synchronization”. See the snippet below:
class SomeClass {
public static synchronized void method1() {
method2(); // legal invocation because a thread has acquired
monitor of SomeClass
}
public static synchronized void method2() {
// do something useful
}
}
● If we need multiple locks per object, we can instantiate new objects and use their
locks. This is useful when we have multiple fields that are independent, and can
thus safely be updated at the same time by different threads on the same object.
This technique is called “fine grained synchronization”. If we lock both these
fields using the same monitor, then they cannot be updated at the same by two
threads, thus reducing performance. See the example below to demonstrate:
class SomeClass {
private int numberOfCallingMethod1 = 0;
private int numberOfCallingMethod2 = 0;
final Object lock1 = new Object(); // an object for locking
final Object lock2 = new Object(); // another object for locking
public void method1() {
System.out.println("method1...");
synchronized (lock1) {
numberOfCallingMethod1++;
}
}
public void method2() {
System.out.println("method2...");
synchronized (lock2) {
numberOfCallingMethod2++;
}
}
}
● Here, we have two fields to count the number of times method1 and method2 are
called. Because these fields are totally independent, two threads can safely call
method1 and method2 at the same time on the same instance. We allow this by
synchronizing method1 and method2 using separate monitors. We create two
objects, lock1 and lock2, for this purpose.
● Because synchronization reduces parallelism and performance, minimize
synchronization when possible, synchronize blocks instead of whole methods
when appropriate, and use fine grained synchronization when appropriate
instead of using a single lock.