MultiThreading
package com.appsdeveloperblog.ws;
import java.util.ArrayList;
import java.util.Random;
public class ListWorker3 {
Random random = new Random();
ArrayList<Integer> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
public synchronized void addToList1() {
System.out.println(Thread.currentThread().getName() + " : addToList1");
try {
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
list1.add(random.nextInt(100));
}
public synchronized void addToList2() {
System.out.println(Thread.currentThread().getName() + " : addToList2");
try {
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
list2.add(random.nextInt(100));
}
public void assign() {
for (int i = 0; i < 1000; i++) {
addToList1();
addToList2();
}
}
public void doWork() {
Thread thread1 = new Thread(() -> assign());
Thread thread2 = new Thread(() -> assign());
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("List1 size: " + list1.size() + " List2 size: " + list2.size());
}
}
The behavior you're observing is due to how the synchronized
keyword works in Java and how it interacts with threads in your code.
Understanding synchronized
in Your Context:
-
Method-Level Synchronization: When you declare a method as
synchronized
, it locks on the instance (this
) for the duration of that method call. This means:- While one thread is executing a
synchronized
method on an object, no other thread can execute anysynchronized
method on the same object. - Once the method finishes execution, the lock is released immediately, allowing other threads to acquire the lock and execute
synchronized
methods.
- While one thread is executing a
-
Your Current Setup:
- Both
addToList1()
andaddToList2()
aresynchronized
methods. - However, each call to these methods is a separate lock acquisition and release. The lock is held only during the execution of the method.
- In your
assign()
method, which is not synchronized, you're calling these methods in a loop. Each iteration acquires and releases the lock separately.
- Both
Why Threads Are Interleaving:
- Since
assign()
is not synchronized, boththread1
andthread2
can executeassign()
concurrently. - During each iteration, when a thread calls
addToList1()
, it acquires the lock, executes the method, and then releases the lock. - The other thread can then acquire the lock and execute
addToList1()
oraddToList2()
. - This results in the threads interleaving their execution of
addToList1()
andaddToList2()
, but they are not blocking each other entirely.
Why synchronized
Doesn't Make One Thread Wait for Another to Finish All Work:
- The
synchronized
keyword at the method level only ensures mutual exclusion for that method call, not for a sequence of method calls or an entire operation that spans multiple methods or iterations. - If you want one thread to complete the entire
assign()
method before another thread starts it, you need to synchronize the entireassign()
method or manage thread execution order explicitly.
How to Make One Thread Complete Before Another Starts:
-
Synchronize the
assign()
Method:public synchronized void assign() { // method body }
- This will ensure that only one thread can execute
assign()
at a time. - However, this effectively serializes the execution of
assign()
across threads, negating the benefits of multithreading.
- This will ensure that only one thread can execute
-
Control Thread Execution Order:
- Instead of starting both threads simultaneously, you can start
thread1
, wait for it to finish usingjoin()
, and then startthread2
.
public void doWork() { Thread thread1 = new Thread(this::assign); Thread thread2 = new Thread(this::assign); thread1.start(); try { thread1.join(); // Wait for thread1 to finish } catch (InterruptedException e) { e.printStackTrace(); } thread2.start(); try { thread2.join(); // Wait for thread2 to finish } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("List1 size: " + list1.size() + " List2 size: " + list2.size()); }
- This ensures that
thread2
starts only afterthread1
has completed its execution.
- Instead of starting both threads simultaneously, you can start
Key Takeaways:
- Granularity of Locks: The scope of the
synchronized
lock is critical. Method-level synchronization locks only during the execution of that method. - Thread Interleaving: Without proper synchronization at the right level, threads can and will interleave, leading to concurrent execution.
- Synchronization Overhead: Over-synchronizing (e.g., synchronizing large methods or blocks) can reduce the benefits of multithreading due to increased waiting times.
Recommendation:
- Re-evaluate Synchronization Needs: Decide which parts of your code truly need to be synchronized to prevent data inconsistencies, and synchronize only those parts.
- Consider Thread Coordination Mechanisms: If the goal is to have one thread complete before another starts, using thread coordination mechanisms like
join()
or higher-level concurrency constructs (e.g.,CountDownLatch
,Semaphore
) might be more appropriate.
Example with Proper Synchronization:
If you aim to allow both threads to run concurrently but prevent simultaneous modifications to the lists, you can synchronize access to the lists themselves:
public void addToList1() {
synchronized (list1) {
System.out.println(Thread.currentThread().getName() + " : addToList1");
try {
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
list1.add(random.nextInt(100));
}
}
public void addToList2() {
synchronized (list2) {
System.out.println(Thread.currentThread().getName() + " : addToList2");
try {
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
list2.add(random.nextInt(100));
}
}
This allows both threads to add to different lists concurrently but ensures that only one thread can modify a list at a time.
In summary, the synchronized
keyword in your code ensures that only one thread can execute a synchronized
method at a time on the same object, but it doesn't prevent threads from interleaving their execution of different synchronized
methods or from executing non-synchronized code concurrently. To have thread1
finish all its work before thread2
starts, you need to synchronize at a higher level or control the thread execution order explicitly.
I appreciate your question, and it's a great observation. The difference in thread behavior you're noticing stems from the granularity of the locks and how the threads interact with these locks during execution.
Let's delve into why synchronizing on list1
and list2
results in more frequent thread switching compared to synchronizing on this
(method-level synchronization).
Understanding the Two Synchronization Approaches
-
Method-Level Synchronization (
synchronized
methods):- Lock Object: The lock is on
this
(the instance of the class). - Effect: Only one thread can execute any
synchronized
method on the same object at a time. - Lock Contention: High, because threads compete for the same lock (
this
), leading to longer wait times for threads.
- Lock Object: The lock is on
-
Block-Level Synchronization on
list1
andlist2
:- Lock Objects: Separate locks on
list1
andlist2
. - Effect: Threads can enter
addToList1()
andaddToList2()
concurrently as long as they are not accessing the same list simultaneously. - Lock Contention: Lower, because threads may be waiting on different locks, allowing more concurrency.
- Lock Objects: Separate locks on
Why Threads Switch More Frequently When Synchronizing on list1
and list2
1. Shorter Lock Duration:
-
Synchronizing on
list1
andlist2
:- The synchronized blocks are only around the critical sections where the lists are modified.
- Each time a thread enters
addToList1()
oraddToList2()
, it acquires the lock, performs the operation, and releases the lock immediately after. - The lock is held for a very short duration (just the time to add an element to the list).
-
Result:
- Threads frequently acquire and release locks.
- The scheduler has more opportunities to switch between threads because they spend less time holding the locks.
- Higher interleaving occurs, and threads "change between themselves every time."
2. Independent Locks Allow Concurrent Access:
- Since
list1
andlist2
are different objects, threads can hold locks onlist1
andlist2
simultaneously. - One thread can be working with
list1
while another works withlist2
, allowing parallel execution.
3. Reduced Lock Contention:
- Threads are less likely to be blocked waiting for a lock, leading to more frequent context switches and smoother multitasking.
Why Threads Switch Less Frequently with Method-Level Synchronization
1. Longer Lock Duration:
-
Synchronizing the Methods:
- The
synchronized
keyword on methods locks onthis
. - When a thread enters a synchronized method, it acquires the lock on
this
and holds it for the entire duration of the method execution. - In your
addToList1()
andaddToList2()
methods, even though they seem small, the lock is held for the whole method.
- The
-
Result:
- The thread holds the lock on
this
while inaddToList1()
, blocking other threads from entering any synchronized method. - Lock contention is higher, leading to threads waiting longer to acquire the lock.
- Fewer opportunities for the scheduler to switch threads during synchronized method execution.
- The thread holds the lock on
2. Single Lock for All Synchronized Methods:
- Both
addToList1()
andaddToList2()
are synchronized onthis
. - Only one thread can execute any synchronized method at a time, leading to more serialized execution.
- Threads spend more time blocked, waiting for the lock, resulting in less frequent thread switching.
Illustrative Example
Scenario with Synchronizing on list1
and list2
:
-
Thread 1 Execution:
- Enters
addToList1()
:- Acquires lock on
list1
. - Modifies
list1
. - Releases lock on
list1
.
- Acquires lock on
- Enters
addToList2()
:- Acquires lock on
list2
. - Modifies
list2
. - Releases lock on
list2
.
- Acquires lock on
- Enters
-
Thread 2 Execution:
- Can enter
addToList1()
oraddToList2()
whenever the locks are available. - Since locks are held briefly, it often finds the locks available.
- Can enter
-
Result:
- Threads frequently switch between each other.
- High concurrency and interleaving.
Scenario with Method-Level Synchronization (synchronizing on this
):
-
Thread 1 Execution:
- Enters
addToList1()
:- Acquires lock on
this
. - Modifies
list1
. - Releases lock on
this
.
- Acquires lock on
- Enters
addToList2()
:- Acquires lock on
this
(if not already held). - Modifies
list2
. - Releases lock on
this
.
- Acquires lock on
- Enters
-
Thread 2 Execution:
- Must wait for
thread1
to release the lock onthis
before entering any synchronized method. - Since
thread1
holds the lock for longer periods (entire method execution),thread2
waits more.
- Must wait for
-
Result:
- Threads switch less frequently.
- Lower concurrency and more sequential execution.
Impact of Sleep Duration
In your methods, you have:
try {
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
-
This introduces a small delay, simulating some work being done.
-
With synchronizing on
list1
andlist2
:- The sleep happens inside the synchronized block, but since the lock is held for a short duration, other threads don't have to wait long.
- Threads often find the locks available after a brief wait.
-
With method-level synchronization:
- The sleep causes the thread to hold the lock on
this
for slightly longer. - Other threads have to wait until the sleeping thread releases the lock.
- This increases the time a thread holds the lock, leading to longer periods where other threads are blocked.
- The sleep causes the thread to hold the lock on
Thread Scheduling and Context Switching
-
Context Switching:
- Occurs when the CPU switches from executing one thread to another.
- The operating system's scheduler decides when to switch based on thread states and priorities.
-
Factors Influencing Thread Switching Frequency:
-
Lock Contention:
- High contention (threads frequently waiting for locks) can lead to more context switches as the scheduler tries to keep the CPU busy.
-
Lock Hold Time:
- Short lock hold times allow threads to proceed without long waits, leading to more frequent switching.
-
Thread Blocking:
- When a thread is blocked (e.g., waiting for a lock), the scheduler may switch to another thread.
-
-
In Your Code:
-
Synchronizing on
list1
andlist2
:- Short lock hold times and lower contention lead to frequent context switches.
-
Method-Level Synchronization:
- Longer lock hold times and higher contention cause threads to wait longer, resulting in fewer context switches.
-
Visualization of Execution Flow
Synchronizing on list1
and list2
:
Iteration 1:
- Thread1 acquires lock on list1, adds to list1, releases lock.
- Thread2 acquires lock on list2, adds to list2, releases lock.
Iteration 2:
- Thread1 acquires lock on list2, adds to list2, releases lock.
- Thread2 acquires lock on list1, adds to list1, releases lock.
...
Threads frequently alternate, accessing different lists concurrently.
Method-Level Synchronization:
Iteration 1:
- Thread1 acquires lock on this, executes addToList1(), holds lock during method execution.
- Thread2 waits for lock on this.
Iteration 2:
- Thread1 continues, executes addToList2(), still holding lock on this.
- Thread2 still waiting.
...
Thread1 holds the lock longer, Thread2 waits longer, leading to less frequent alternation.
Implications for Concurrency and Performance
-
Synchronizing on
list1
andlist2
:-
Pros:
- Higher concurrency.
- Better CPU utilization.
- Threads spend less time waiting.
-
Cons:
- Increased complexity in ensuring thread safety if shared resources are accessed elsewhere.
- Potential for more context switches, which have overhead.
-
-
Method-Level Synchronization:
-
Pros:
- Simpler to reason about when all methods are synchronized on the same lock.
- Ensures that only one thread can execute any synchronized method at a time, providing strong mutual exclusion.
-
Cons:
- Reduced concurrency.
- Threads may spend more time blocked, leading to underutilization of CPU resources.
- Less responsive system if threads are waiting for long periods.
-
Recommendation
-
Choose Synchronization Strategy Based on Requirements:
- If you need high concurrency and can manage the complexity, synchronizing on separate locks (
list1
,list2
) is beneficial. - If you need strong mutual exclusion and simplicity, method-level synchronization on
this
is appropriate, but be aware of potential performance drawbacks.
- If you need high concurrency and can manage the complexity, synchronizing on separate locks (
-
Optimize Lock Granularity:
- Fine-grained locks (like synchronizing on individual resources) allow more concurrency but require careful design to avoid deadlocks and ensure thread safety.
- Coarse-grained locks (like synchronizing on
this
) are easier to implement but can lead to reduced performance due to increased contention.
Understanding the Observed Behavior
-
Your Observation:
- When synchronizing on
list1
andlist2
, threads switch between themselves every time. - When using method-level synchronization, threads also switch but not as quickly.
- When synchronizing on
-
Explanation:
- Synchronizing on
list1
andlist2
: Locks are held for shorter periods, allowing threads to alternate more frequently. - Method-Level Synchronization: Locks are held for longer periods (entire method execution), causing threads to wait longer before switching occurs.
- Synchronizing on
Final Thoughts
-
Thread Scheduling Is Non-Deterministic:
- The actual thread switching behavior can vary based on the JVM implementation and the underlying operating system's thread scheduler.
- While we can explain general patterns, exact behavior may differ in different runs.
-
Testing and Profiling:
- To understand the performance implications, consider profiling your application.
- Tools like Java VisualVM or thread dumps can help visualize thread states and lock contention.
-
Consider Higher-Level Concurrency Utilities:
- Java's
java.util.concurrent
package provides classes likeConcurrentHashMap
,CopyOnWriteArrayList
, andReentrantLock
that offer better concurrency control and performance. - These classes are designed to reduce contention and improve scalability.
- Java's
Summary
- Synchronizing on
list1
andlist2
leads to more frequent thread switching because locks are held for shorter durations and threads can access different resources concurrently. - Method-level synchronization results in less frequent thread switching due to longer lock hold times and higher contention on a single lock (
this
). - Understanding lock granularity and contention is key to designing concurrent applications that balance performance and thread safety.
By recognizing how different synchronization strategies affect thread behavior, you can make informed decisions to optimize your application's concurrency model.
===========================================================================
Multithreading in Java refers to the concurrent execution of multiple threads within a java program. A thread is a lightweight sub-process, and multithreading allows you to perform multiple tasks simultaneously, improving the overall efficiency of your program.
Thread basics:
package test;
public class Main {
public static void main(String[] args) {
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getClass().getName());
printThreadState(currentThread);
}
public static void printThreadState(Thread thread) {
System.out.println("--------------------------------------------");
System.out.println("Thread ID: " + thread.getId());
System.out.println("Thread Name: " + thread.getName());
System.out.println("Thread Priority:" + thread.getPriority());
System.out.println("Thread State: " + thread.getState());
System.out.println("Thread Group:" + thread.getThreadGroup());
System.out.println("Thread Is Alive: " + thread.isAlive());
System.out.println("--------------------------------------------");
}
}
package thread;
public class CustomThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.print(" 1 ");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package thread;
public class Main {
public static void main(String[] args) {
CustomThread thread = new CustomThread();
thread.run();
for(int i = 0; i < 5; i++) {
System.out.print(" 0 ");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Interacting with a running thread:
package thread;
public class Main {
public static void main(String[] args) {
System.out.println("Main Thread running");
try {
System.out.println("Main Thread paused for one second");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread = new Thread(() -> {
String tname = Thread.currentThread().getName();
System.out.println(tname + " should take 10 dots to run.");
for (int i = 0; i < 10; i++) {
System.out.print(". ");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("\nWhoops!! " + tname + " interrupted.");
return;
}
}
System.out.println("\n" + tname + " completed.");
});
System.out.println(thread.getName() + " starting");
thread.start();
System.out.println("Main Thread would continue here");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
}
package thread;
public class Main {
public static void main(String[] args) {
System.out.println("Main Thread running : " + Thread.currentThread().getName());
try {
System.out.println("Main Thread paused for one second");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread = new Thread(() -> {
String tname = Thread.currentThread().getName();
System.out.println(tname + " should take 10 dots to run.");
for (int i = 0; i < 10; i++) {
System.out.print(". ");
try {
Thread.sleep(500);
System.out.println("A. State = " + Thread.currentThread().getState());
} catch (InterruptedException e) {
System.out.println("\nWhoops!! " + tname + " interrupted.");
System.out.println("A1. State = " + Thread.currentThread().getState());
return;
}
}
System.out.println("\n" + tname + " completed.");
});
System.out.println(thread.getName() + " starting");
thread.start();
long now = System.currentTimeMillis();
while (thread.isAlive()) {
System.out.println("\nwaiting for thread to complete");
try {
Thread.sleep(1000);
System.out.println("B. State = " + thread.getState());
if (System.currentTimeMillis() - now > 2000) {
thread.interrupt();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("C. State = " + thread.getState());
// System.out.println("Main Thread would continue here");
// try {
// Thread.sleep(3000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// thread.interrupt();
}
}
package thread;
public class Main {
public static void main(String[] args) {
System.out.println("Main Thread running : " + Thread.currentThread().getName());
try {
System.out.println("Main Thread paused for one second");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread = new Thread(() -> {
String tname = Thread.currentThread().getName();
System.out.println(tname + " should take 10 dots to run.");
for (int i = 0; i < 10; i++) {
System.out.print(". ");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("\nWhoops!! " + tname + " interrupted.");
return;
}
}
System.out.println("\n" + tname + " completed.");
});
Thread installThread = new Thread(() -> {
try {
for (int i = 0; i < 3; i++) {
Thread.sleep(1000);
System.out.println("Installation Step " + (i + 1) + " is completed.");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "InstallThread");
Thread threadMonitor = new Thread(() -> {
long now = System.currentTimeMillis();
while (thread.isAlive()) {
try {
Thread.sleep(1000);
if (System.currentTimeMillis() - now > 2000) {
thread.interrupt();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println(thread.getName() + " starting");
thread.start();
threadMonitor.start();
try {
thread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (!thread.isInterrupted()) {
installThread.start();
} else {
System.out.println("Previous thread was interrupted, " +
installThread.getName() + " can't run.");
}
}
}
package thread;
public class Main {
public static void main(String[] args) {
System.out.println("Main Thread running : " + Thread.currentThread().getName());
try {
System.out.println("Main Thread paused for one second");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread = new Thread(() -> {
String tname = Thread.currentThread().getName();
System.out.println(tname + " should take 10 dots to run.");
for (int i = 0; i < 10; i++) {
System.out.print(". ");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("\nWhoops!! " + tname + " interrupted.");
Thread.currentThread().interrupt();
return;
}
}
System.out.println("\n" + tname + " completed.");
});
Thread installThread = new Thread(() -> {
try {
for (int i = 0; i < 3; i++) {
Thread.sleep(1000);
System.out.println("Installation Step " + (i + 1) + " is completed.");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "InstallThread");
Thread threadMonitor = new Thread(() -> {
long now = System.currentTimeMillis();
while (thread.isAlive()) {
try {
Thread.sleep(1000);
if (System.currentTimeMillis() - now > 2000) {
thread.interrupt();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println(thread.getName() + " starting");
thread.start();
threadMonitor.start();
try {
thread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (!thread.isInterrupted()) {
installThread.start();
} else {
System.out.println("Previous thread was interrupted, " +
installThread.getName() + " can't run.");
}
}
}
Concurrent Thread concepts:
package main;
import java.util.concurrent.TimeUnit;
public class CachedData {
private boolean flag = false;
public void toggleFlag() {
flag = !flag;
}
public boolean isReady() {
return flag;
}
public static void main(String[] args) {
CachedData example = new CachedData();
Thread writerThread = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
example.toggleFlag();
System.out.println("A. Flag set to " + example.isReady());
});
Thread readerThread = new Thread(() -> {
while (!example.isReady()) {
// Busy-wait until flag becomes true
}
System.out.println("B. Flag is " + example.isReady());
});
writerThread.start();
readerThread.start();
}
}
Volatile:
package main;
import java.util.concurrent.TimeUnit;
public class CachedData {
private volatile boolean flag = false;
public void toggleFlag() {
flag = !flag;
}
public boolean isReady() {
return flag;
}
public static void main(String[] args) {
CachedData example = new CachedData();
Thread writerThread = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
example.toggleFlag();
System.out.println("A. Flag set to " + example.isReady());
});
Thread readerThread = new Thread(() -> {
while (!example.isReady()) {
// Busy-wait until flag becomes true
}
System.out.println("B. Flag is " + example.isReady());
});
writerThread.start();
readerThread.start();
}
}
Synchronization:
package main;
public class BankAccount {
private volatile double balance;
public BankAccount(double balance) {
this.balance = balance;
}
public double getBalance() {
return balance;
}
public synchronized void deposit(double amount) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
double originalBalance = balance;
balance += amount;
System.out.printf("STARTING BALANCE: %.0f, DEPOSIT (%.0f)" +
" : NEW BALANCE = %.0f%n", originalBalance, amount, balance);
}
public synchronized void withdraw(double amount) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
double originalBalance = balance;
if (amount <= balance) {
balance -= amount;
System.out.printf("STARTING BALANCE: %.0f, WITHDRAWAL (%.0f)" +
" : NEW BALANCE = %.0f%n", originalBalance, amount, balance);
} else {
System.out.printf("STARTING BALANCE: %.0f, WITHDRAWAL (%.0f)" +
" : INSUFFICIENT FUNDS!", originalBalance, amount);
}
}
}
package main;
import java.util.concurrent.TimeUnit;
public class Task {
public static void main(String[] args) throws InterruptedException {
BankAccount bankAccount = new BankAccount(10000);
Thread thread1 = new Thread(() -> bankAccount.withdraw(2500));
Thread thread2 = new Thread(() -> bankAccount.deposit(5000));
Thread thread3 = new Thread(() -> bankAccount.withdraw(2500));
thread1.start();
thread2.start();
thread3.start();
try {
thread1.join();
thread2.join();
thread3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final balance: " + bankAccount.getBalance());
}
}
another example:
package main;
public class BankAccount {
private volatile double balance;
public BankAccount(double balance) {
this.balance = balance;
}
public double getBalance() {
return balance;
}
public void deposit(double amount) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
double originalBalance = balance;
balance += amount;
System.out.printf("STARTING BALANCE: %.0f, DEPOSIT (%.0f)" +
" : NEW BALANCE = %.0f%n", originalBalance, amount, balance);
}
}
public synchronized void withdraw(double amount) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
double originalBalance = balance;
if (amount <= balance) {
balance -= amount;
System.out.printf("STARTING BALANCE: %.0f, WITHDRAWAL (%.0f)" +
" : NEW BALANCE = %.0f%n", originalBalance, amount, balance);
} else {
System.out.printf("STARTING BALANCE: %.0f, WITHDRAWAL (%.0f)" +
" : INSUFFICIENT FUNDS!", originalBalance, amount);
}
}
}
Deadlock:
package main;
import java.util.Random;
class MessageRepository {
private String message;
private boolean hasMessage = false;
public synchronized String read() {
while (!hasMessage) {
}
hasMessage = false;
return message;
}
public synchronized void write(String message) {
while (hasMessage) {
}
hasMessage = true;
this.message = message;
}
}
class MessageWriter implements Runnable {
private MessageRepository outgoingMessage;
private final String text = """
Humpty Dumpty sat on wall,
Humpty Dumpty had a great fall,
All the king's horses and all the king's men,
Couldn't put Humpty together again.""";
public MessageWriter(MessageRepository outgoingMessage) {
this.outgoingMessage = outgoingMessage;
}
@Override
public void run() {
Random random = new Random();
String[] lines = text.split("\n");
for (int i = 0; i < lines.length; i++) {
outgoingMessage.write(lines[i]);
try {
Thread.sleep(random.nextInt(500, 2000));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
outgoingMessage.write("Finished");
}
}
class MessageReader implements Runnable {
private MessageRepository incomingMessage;
public MessageReader(MessageRepository outgoingMessage) {
this.incomingMessage = outgoingMessage;
}
@Override
public void run() {
Random random = new Random();
String lastMessage = "";
do {
try {
Thread.sleep(random.nextInt(500, 2000));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
lastMessage = incomingMessage.read();
System.out.println(lastMessage);
} while (!lastMessage.equals("Finished"));
}
}
public class Task {
public static void main(String[] args) throws InterruptedException {
MessageRepository repository = new MessageRepository();
Thread reader = new Thread(new MessageReader(repository));
Thread write = new Thread(new MessageWriter(repository));
reader.start();
write.start();
}
}
===========================================================================
Volatile:
package scheduler;
public class VolatileMain {
volatile static int i;
public static void main(String[] args) {
new MyThreadWrite().start();
new MyThreadRead().start();
}
static class MyThreadWrite extends Thread {
@Override
public void run() {
while (i < 5) {
System.out.println("increment i to " + (++i));
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
static class MyThreadRead extends Thread {
@Override
public void run() {
int localVar = i;
while (localVar < 5) {
if (localVar != i) {
System.out.println("localVar is " + localVar);
System.out.println("New value of i is " + i);
localVar = i;
}
}
}
}
}
Atomic:
package scheduler;
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileMain {
static AtomicInteger i = new AtomicInteger(0);
static int k;
public static void main(String[] args) throws InterruptedException {
for (int j = 0; j < 10_000; j++) {
new MyThread().start();
}
// Thread.sleep(2000);
System.out.println(i);
System.out.println(k);
}
static class MyThread extends Thread {
@Override
public void run() {
k++;
i.incrementAndGet();
}
}
}
Deadlock:
package threads;
public class Main {
public static void main(String[] args) {
ResourceA resourceA = new ResourceA();
ResourceB resourceB = new ResourceB();
resourceA.resourceB = resourceB;
resourceB.resourceA = resourceA;
Thread1 thread1 = new Thread1();
Thread2 thread2 = new Thread2();
thread1.resourceA = resourceA;
thread2.resourceB = resourceB;
thread1.start();
thread2.start();
}
}
class Thread1 extends Thread {
ResourceA resourceA;
@Override
public void run() {
System.out.println(resourceA.getI());
}
}
class Thread2 extends Thread {
ResourceB resourceB;
@Override
public void run() {
System.out.println(resourceB.getI());
}
}
class ResourceA {
ResourceB resourceB;
public synchronized int getI() {
return resourceB.returnI();
}
public synchronized int returnI() {
return 1;
}
}
class ResourceB {
ResourceA resourceA;
public synchronized int getI() {
return resourceA.returnI();
}
public synchronized int returnI() {
return 2;
}
}
Wait and notify:
package threads;
public class Main {
public static void main(String[] args) {
ResourceA resourceA = new ResourceA();
ResourceB resourceB = new ResourceB();
resourceA.resourceB = resourceB;
resourceB.resourceA = resourceA;
Thread1 thread1 = new Thread1();
Thread2 thread2 = new Thread2();
thread1.resourceA = resourceA;
thread2.resourceB = resourceB;
thread1.start();
thread2.start();
}
}
class Thread1 extends Thread {
ResourceA resourceA;
@Override
public void run() {
System.out.println(resourceA.getI());
}
}
class Thread2 extends Thread {
ResourceB resourceB;
@Override
public void run() {
System.out.println(resourceB.getI());
}
}
class ResourceA {
ResourceB resourceB;
public synchronized int getI() {
return resourceB.returnI();
}
public synchronized int returnI() {
return 1;
}
}
class ResourceB {
ResourceA resourceA;
public synchronized int getI() {
return resourceA.returnI();
}
public synchronized int returnI() {
return 2;
}
}
package com.appsdeveloperblog.ws.imran;
public class Main {
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Daemon thread is running...");
}
});
Thread t2 = new Thread(() -> {
{
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Daemon thread is running...");
}
});
t1.setDaemon(true);
t1.start();
t2.start();
}
}
Daemond thread esas thread iwini bitirene kimi iwleyir.
Daemon threads serve specific purposes in Java, primarily related to running background tasks that support the application. Here's why we need daemon threads and their typical use cases:
1. Background Tasks
Daemon threads are ideal for tasks that run in the background to support the application but are not critical to its primary functionality. These threads can terminate without disrupting the program’s main objectives.
Examples:
- Garbage Collection: The JVM uses daemon threads to run the garbage collector, cleaning up unused objects in memory.
- Monitoring Threads: Daemon threads can monitor system health, log statistics, or manage resource cleanup.
- Heartbeat/Watchdog Threads: Sending periodic pings or checking if a service is alive.
2. Non-Essential Tasks
Daemon threads are useful for tasks that are helpful but not essential to the application. If all user threads finish their work, the JVM can terminate the program without waiting for the daemon threads.
Why this is helpful:
- Ensures that the program doesn’t hang indefinitely because of long-running background tasks.
- Offloads minor tasks without affecting the main execution flow.
3. Resource Management
Daemon threads often handle resource management tasks that need to continue in the background but do not require blocking the application.
Examples:
- File System Cleanup: Automatically delete temporary files or perform periodic cleanups.
- Cache Maintenance: Periodically update or expire cache entries in the background.
4. JVM-Managed Lifecycle
Daemon threads are automatically terminated when the JVM exits, which simplifies their management. You don’t need to explicitly stop or manage them, unlike user threads.
Why this matters:
- Reduces complexity when managing threads.
- Prevents issues like forgetting to stop background threads, which could lead to resource leaks or application hangs.
5. Supporting Applications Without Blocking
Daemon threads allow applications to focus on their primary purpose while delegating auxiliary tasks to these background threads. They ensure the main application logic isn’t delayed or blocked by background processes.
Example Use Case:
- Web Servers: A web server might use a daemon thread to monitor inactive connections and close them, while user threads handle the actual request processing.
When NOT to Use Daemon Threads
- Critical Tasks: Tasks that must complete (e.g., saving user data to a database) should not be run on daemon threads because they are terminated when the JVM exits.
- Error-Prone Tasks: If a background task involves important error handling, use a user thread instead to ensure it completes.
Summary
We need daemon threads for:
- Running non-critical background tasks.
- Supporting the application without blocking its main functionality.
- Managing resources and monitoring tasks without manual intervention.
- Simplifying thread lifecycle management, as the JVM handles daemon threads automatically.
Daemon threads provide flexibility and efficiency, ensuring auxiliary tasks don’t interfere with or prolong the execution of an application unnecessarily.
Yes, that's correct! When the JVM exits, all daemon threads are killed immediately without finishing their work. Here's a detailed explanation of why and how this happens:
Key Concept: JVM Shutdown
The JVM shuts down when:
- All user threads (non-daemon threads) have finished executing.
- Someone explicitly calls
System.exit()
or an equivalent mechanism to terminate the JVM.
Daemon threads do not prevent the JVM from shutting down. Once all user threads finish (or System.exit()
is called), the JVM exits, and all daemon threads are forcibly terminated.
Why Are Daemon Threads Killed?
Daemon threads are considered non-critical. They run in the background to support the application but aren't required to complete for the application to function. When the JVM shuts down, it doesn’t wait for daemon threads to finish their work, as waiting for them could potentially delay or prevent the shutdown indefinitely.
Example Behavior:
public class DaemonThreadExample {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
System.out.println("Daemon thread working...");
} catch (InterruptedException e) {
System.out.println("Daemon thread interrupted.");
}
}
});
daemonThread.setDaemon(true);
daemonThread.start();
System.out.println("Main thread finished.");
// The JVM will shut down here, and the daemon thread will be terminated.
}
}
Output:
Main thread finished.
Daemon thread working... (may print once or twice before JVM exits)
As soon as the main
thread (a user thread) finishes, the JVM exits, and the daemon thread is killed mid-execution.
Difference Between User and Daemon Threads
Feature | User Thread | Daemon Thread |
---|---|---|
Prevents JVM Shutdown | Yes | No |
Lifecycle Dependency | Independent | Dependent on user threads |
Execution Guarantee | JVM waits for them to finish | JVM kills them immediately on shutdown |
Why Does This Matter?
Daemon threads being killed means:
- No graceful cleanup: If a daemon thread is performing file I/O, writing logs, or other operations, these tasks will be interrupted.
- Unfinished work: Any partially completed operation will remain incomplete.
When to Avoid Daemon Threads:
If a task must complete, it should run in a user thread, not a daemon thread. For example:
- Saving critical data to a database.
- Finalizing a transaction.
- Writing logs for debugging.
Analogy:
Imagine a JVM as a house and threads as people working inside:
- User threads are like important workers: the house (JVM) remains open until they finish their work.
- Daemon threads are like assistants doing background tasks. The house closes as soon as the main workers finish, and the assistants are sent home immediately, even if they haven't finished their jobs.
In Summary:
- Daemon threads are killed when the JVM exits.
- The JVM exits only after all user threads finish.
- Use daemon threads for non-critical background tasks that can be interrupted without affecting the application's core functionality.
Let’s dive deep into time slicing and understand its relationship with CPU cores, multitasking, and how it impacts thread execution.
1. What is Time Slicing?
Time slicing is a scheduling mechanism that allows multiple threads or processes to share the CPU in small time intervals (time slices). The Operating System (OS) scheduler ensures that no single thread/process monopolizes the CPU, creating the illusion that all threads/processes are running simultaneously.
2. Why Time Slicing is Needed
Modern systems run multitasking environments with multiple threads and processes. Since a CPU can only execute one instruction at a time per core, time slicing ensures:
- Fairness: Each thread or process gets a fair share of CPU time.
- Responsiveness: Shorter tasks get quick execution without waiting for long-running tasks to complete.
- Concurrency: It allows multiple threads to appear as if they are running in parallel, even if there's only one CPU core.
3. How Time Slicing Works
-
CPU Allocates Time Slices:
- Each thread gets a fixed amount of time on the CPU (e.g., 10ms or 20ms).
- This time slice is determined by the OS scheduler.
-
Thread Switching:
- When a thread’s time slice expires, the OS pauses the thread and saves its state (context switch).
- The CPU is then assigned to the next thread in the queue.
-
Preemptive Multitasking:
- If a thread does not finish within its time slice, it is preempted (paused and moved to the end of the queue).
- This ensures other threads also get CPU time.
-
Idle Threads:
- If a thread is waiting (e.g., for I/O operations), it releases its time slice, allowing other threads to use the CPU.
4. Time Slicing and CPU Cores
Let’s consider how time slicing interacts with CPU cores:
Single-Core CPUs
- Only one thread can execute at a time.
- The OS scheduler rapidly switches between threads, giving each a time slice.
- Example: If there are 5 threads and the time slice is 20ms, each thread will get the CPU for 20ms in a round-robin manner.
Multi-Core CPUs
- Multiple threads can execute simultaneously, with each core running one thread at a time.
- Time slicing occurs independently on each core.
- If there are 4 cores and 8 threads, the scheduler can assign 4 threads to the cores.
- After their time slices expire, the next 4 threads are scheduled.
Hyper-Threading (Simultaneous Multi-Threading)
- Some CPUs can run two threads per core simultaneously by efficiently utilizing idle parts of the CPU.
- Example: A 4-core CPU with hyper-threading can handle 8 threads concurrently.
5. Thread Scheduling in Java
In Java, the JVM thread scheduler relies on the OS scheduler for time slicing and thread execution.
- Threads are assigned time slices by the OS.
- Thread priority may influence how the OS scheduler assigns CPU time, but it’s not guaranteed.
6. What Happens Inside the CPU
When a thread is executing on the CPU:
- Fetch-Decode-Execute Cycle:
- The CPU fetches the thread's instructions from memory.
- Decodes the instructions and executes them.
- Registers and Cache:
- The thread’s context (e.g., variables, program counter) is stored in CPU registers.
- Frequently accessed data may reside in the CPU cache for speed.
- Context Switching:
- When a thread’s time slice ends, the CPU saves its state (e.g., registers, program counter) to memory.
- The next thread’s context is loaded from memory.
7. Visualizing Time Slicing
Here’s an example of time slicing on a single-core CPU with 3 threads (T1, T2, T3) and a 10ms time slice:
Time (ms): | 0-10 | 10-20 | 20-30 | 30-40 | 40-50 | ...
Thread: | T1 | T2 | T3 | T1 | T2 | ...
Observations:
- Each thread gets 10ms to execute before being paused.
- If a thread finishes its task early, the CPU moves to the next thread.
8. Pros and Cons of Time Slicing
Advantages:
- Fair CPU Allocation: Ensures all threads get CPU time.
- Concurrency: Allows multiple threads to run as if they’re executing simultaneously.
- Responsive Systems: Short tasks can complete quickly without waiting for long tasks.
Disadvantages:
- Context Switching Overhead: Saving/restoring thread states consumes CPU cycles.
- Non-Deterministic Execution: Threads may not execute in a predictable order.
- Starvation Risk: Low-priority threads may not get enough time slices if higher-priority threads dominate.
9. Time Slicing in Java Code
In Java, time slicing is managed by the OS, but we can observe its effects. Let’s simulate it:
public class TimeSlicingExample {
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " is running: " + i);
try {
Thread.sleep(500); // Simulating work for 500ms
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t1 = new Thread(task, "Thread-1");
Thread t2 = new Thread(task, "Thread-2");
Thread t3 = new Thread(task, "Thread-3");
t1.start();
t2.start();
t3.start();
}
}
Expected Output:
The output will show interleaved execution of Thread-1
, Thread-2
, and Thread-3
, depending on the OS time slicing.
10. Summary
- Time slicing divides CPU time into small chunks and schedules threads in a round-robin manner.
- On single-core CPUs, time slicing creates an illusion of concurrency by switching between threads.
- On multi-core CPUs, multiple threads can execute truly in parallel, but time slicing still applies within each core.
- The JVM relies on the OS to handle time slicing for Java threads.
Let me know if you'd like a deeper explanation of any part!
Parallel and concurrent programming are concepts used in computing, and while they seem similar, they have distinct meanings:
Concurrent Programming:
- Imagine you are a single person working on multiple tasks, like cooking and talking on the phone. You can switch between these tasks (stirring the pot, saying something on the call, then back to stirring). This is concurrent.
- In technical terms:
- Multiple tasks (threads or processes) are started and worked on.
- They don’t necessarily run at the same time but are progressing together.
- The focus is on managing multiple tasks that may interact with each other.
Parallel Programming:
- Now imagine there are two people, one cooking and the other talking on the phone at the same time. Both tasks are happening simultaneously. This is parallel.
- In technical terms:
- Multiple tasks run at the same time on different processors or cores.
- It requires hardware capable of true multitasking (like multi-core CPUs).
- The focus is on speeding up the execution by doing things simultaneously.
Key Difference:
- Concurrency is about dealing with many tasks at once, potentially taking turns.
- Parallelism is about doing many tasks at the same time.
You can think of concurrency as juggling tasks and parallelism as dividing tasks among team members to do them at the same time.
Mutual exclusion:
Mutual Exclusion is a fundamental concept in concurrent programming and threading. It refers to the principle of ensuring that only one thread can access a shared resource (e.g., variables, methods, files) or a critical section of code at any given time.
What is a Critical Section?
A critical section is a part of your code where shared resources are accessed or modified. If multiple threads enter the critical section at the same time, it can lead to issues such as:
- Race Conditions: Where the outcome depends on the order of thread execution.
- Data Inconsistency: Where shared data may be corrupted because threads interfere with each other.
Mutual exclusion ensures that only one thread can execute the critical section at a time.
How is Mutual Exclusion Enforced?
In Java, mutual exclusion is commonly enforced using:
-
synchronized
keyword: Ensures that only one thread can access a synchronized method or block at a time.- On an instance method or block, the lock is associated with the object instance.
- On a static method or block, the lock is associated with the class object.
-
Locks (
ReentrantLock
fromjava.util.concurrent.locks
): Provides more advanced and flexible locking mechanisms.
Example of Mutual Exclusion:
In your code:
public synchronized static void increment1() {
for (int i = 0; i < 10000; i++) {
count1++;
}
}
public synchronized static void increment2() {
for (int i = 0; i < 10000; i++) {
count2++;
}
}
Here:
- The
synchronized
keyword ensures that mutual exclusion is applied. - Both
increment1()
andincrement2()
methods are synchronized and static, meaning they both share the class-level lock (Main.class
). - If one thread acquires the lock (e.g., Thread
one
runningincrement1()
), no other thread (e.g., Threadtwo
) can access the critical section (increment2()
orincrement1()
) until the lock is released.
Why is Mutual Exclusion Important?
Mutual exclusion is crucial for:
- Thread Safety: Preventing data corruption or inconsistencies when multiple threads access shared resources.
- Avoiding Race Conditions: Ensuring a predictable and correct outcome, regardless of thread execution order.
- Consistency: Keeping shared data in a reliable state.
Without mutual exclusion, threads may:
- Overwrite each other's changes.
- Operate on inconsistent data.
- Cause unpredictable program behavior.
Simple Analogy:
Imagine a single bathroom in a house (shared resource). To ensure privacy:
- Lock the door when one person enters (acquire the lock).
- Others wait outside until the door is unlocked (release the lock).
- Only one person can use the bathroom at a time (mutual exclusion).
In your code, the synchronized
keyword acts like the bathroom lock, preventing simultaneous access.
Can a Thread Escape from wait()
Without notify()
?
Yes, a thread can escape from wait()
without being explicitly notified via notify()
or notifyAll()
under certain circumstances. This behavior is known as a spurious wakeup.
🚀 Key Points to Understand
-
Spurious Wakeups:
- Threads waiting on a monitor (
lock.wait()
) can occasionally wake up without any call tonotify()
ornotifyAll()
. - This is a documented behavior in Java and is typically rare, but you must code defensively to handle it.
- Threads waiting on a monitor (
-
Why Spurious Wakeups Happen:
- These wakeups are often an artifact of how operating systems and JVM thread schedulers handle thread management.
- They help improve system performance and avoid deadlocks in edge cases.
-
Best Practice:
- Always use a while-loop instead of an if-statement to check the condition before calling
wait()
. - This ensures that even if a thread wakes up spuriously, it will recheck the condition and avoid proceeding unexpectedly.
- Always use a while-loop instead of an if-statement to check the condition before calling
🛡️ Fixing Your Code
Replace the if
block with a while
block:
public void startAutoUpload() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " is working on uploading photos");
while (photosToUpload.isEmpty()) { // Use 'while' instead of 'if'
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Preserve interrupt status
e.printStackTrace();
}
}
uploadPhotos();
System.out.println(Thread.currentThread().getName() + " has finished");
}
}
✅ Why while
is Better Than if
- With
if
, if the thread wakes up spuriously, it would skip thewait()
block and proceed touploadPhotos()
without verifying ifphotosToUpload
is still empty. - With
while
, the condition is rechecked after every wake-up. IfphotosToUpload
is still empty, the thread goes back into thewait()
state.
🧠 Thread Safety Takeaway
- Always use
while
when waiting on a condition in multithreaded code. - Spurious wakeups are rare but possible, and
while
ensures your program behaves correctly even in those cases.
Комментарии
Отправить комментарий