자바의 멀티스레드 프로그래밍에 대해 정리합니다.
학습할 내용은 다음과 같습니다.
- Thread 클래스와 Runnable 인터페이스
- 스레드의 상태
- 스레드의 우선순위
- Main 스레드
- 동기화
- 데드락
Reference
자바에서 스레드의 사용은 Thread Class의 인스턴스와 연관이 깊습니다. 기본적으로 이 스레드 오브젝트를 사용하는 전략은 크게 두가지가 있습니다.
Thread 클래스 인스턴스화를 통한 직접 제어하는 방법
스레드 관리를 Executors를 통한 추상적인 제어방법이 있습니다.
여기서는 Thread Object를 위주로 다루고 Executors는 이후에 간단하게 다뤄보겠습니다.
어플리케이션에서 스레드 인스턴스를 만들고 실행하는 방법은 두가지가 있습니다.
Runnable interface를 Thread 클래스에 제공해서 실행하는 방법이 있습니다.
Runnable interface는 단 하나의 싱글 메소드를 가지고 있는데 이걸 정의한 후 Thread Instance를 만들 때 생성자에 넣어주면 됩니다.
public class HelloRunnable implements Runnable {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
(new Thread(new HelloRunnable())).start();
}
}
Thread 클래스 자체적으로 Runnable interface를 상속받고 있습니다. 이 Thread 클래스를 상속받아서 Runnable interface에서 제공해주는 메소드를 오버라이딩 하면 됩니다.
public class HelloThread extends Thread {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
(new HelloThread()).start();
}
}
둘 방식에서는 Runnable Interface를 구현하는게 더 선호됩니다. 자바에선 하나의 클래스만 상속받을 수 있으므로 상속의 이점을 누리기 위해서도 있지만 인터페이스를 이용하는 측면이 더 유연합니다. Thread 클래스를 상속받으면 항상 그 오브젝트는 특정한 행동만을 할 수 있는 반면에 Runnable interface는 그렇게 하지 않아도 됩니다.
현재 스레드의 스레드 그룹과 하위 그룹에 있는 활성화 된 스레드 개수를 반환합니다.
public class ThreadDemo {
public static void main(String[] args) {
Thread t = Thread.currentThread();
t.setName("Admin Thread");
System.out.println("Thread = " + t);
int count = Thread.activeCount();
System.out.println("currently active threads = " + count);
Thread th[] = new Thread[count];
Thread.enumerate(th);
for (int i = 0; i < count; i++) {
System.out.println(i + ": " + th[i]);
}
}
}
// expected output
Thread = Thread[Admin Thread,1,main]
currently active threads = 1
0: Thread[Admin Thread,1,main]
현재 실행중인 스레드의 참조를 반환합니다.
public class ThreadDemo implements Runnable{
ThreadDemo() {
// main thread
Thread currThread = Thread.currentThread();
// 만들어진 thread
Thread t = new Thread(this, "Admin Thread");
System.out.println("current thread = " + currThread);
System.out.println("thread created = " + t);
t.start();
}
@Override
public void run() {
System.out.println("This is run() method");
}
public static void main(String[] args) {
new ThreadDemo();
}
}
// expected output
current thread = Thread[main,5,main]
thread created = Thread[Admin Thread,5,main]
This is run() method
현재 스레드의 stack trace를 출력합니다. 이 메소드는 디버깅에만 사용합니다.
public class ThreadDemo {
public static void main(String[] args) {
Thread t = Thread.currentThread();
t.setName("Admin Thread");
// 현재 실행중이 스레드를 출력합니다.
System.out.println("Thread = " + t);
int count = Thread.activeCount();
System.out.println("currently active threads = " + count);
// stack trace를 출력합니다.
Thread.dumpStack();
}
}
// expect output
Thread = Thread[Admin Thread,5,main]
currently active threads = 2
java.lang.Exception: Stack trace
at java.base/java.lang.Thread.dumpStack(Thread.java:1388)
at me.jeongmin.solution.thread.ThreadDemo.main(ThreadDemo.java:18)
모든 살아있는 스레드에 대한 stack tract Map을 반환합니다.
각 Map의 Key는 Thread이며 Value는 StackTraceElement의 Array 입니다.
이 메소드를 실행하는 동안 다른 스레드가 실행될 수 있으므로 각 스레드의 stack trace는 스냅샷입니다.
public class ThreadDemo implements Runnable{
public void run() {
System.out.println("This is run() method");
}
public static void main(String args[]) {
ThreadDemo trace = new ThreadDemo();
Thread t = new Thread(trace);
// this will call run() method
t.start();
// returns a map of stack traces
Map m = Thread.getAllStackTraces();
System.out.println(m);
}
}
// expect output
This is run() method
{Thread[Finalizer,8,system]=[Ljava.lang.StackTraceElement;@71be98f5, Thread[main,5,main]=[Ljava.lang.StackTraceElement;@6fadae5d, Thread[Reference Handler,10,system]=[Ljava.lang.StackTraceElement;@17f6480, Thread[Monitor Ctrl-Break,5,main]=[Ljava.lang.StackTraceElement;@2d6e8792, Thread[Signal Dispatcher,9,system]=[Ljava.lang.StackTraceElement;@2812cbfa, Thread[Thread-0,5,main]=[Ljava.lang.StackTraceElement;@2acf57e3, Thread[Common-Cleaner,8,InnocuousThreadGroup]=[Ljava.lang.StackTraceElement;@506e6d5e}
스레드가 캡처되지 않은 예외로 인해 갑자기 종료된 경우 호출할 기본 Exception Handler를 반환합니다.
반환된 값이 null이면 기본값은 없습니다.
public class ThreadDemo implements Runnable {
Thread t;
public ThreadDemo() {
t = new Thread(this);
t.start();
}
public void run() {
// 스레드 이름을 출력합니다.
System.out.println("Thread = " + t.getName());
Thread.UncaughtExceptionHandler handler = Thread
.getDefaultUncaughtExceptionHandler();
// 기본 스레드 예외 핸들러를 리턴합니다.
System.out.println(handler);
}
public static void main(String[] args) {
new ThreadDemo();
}
}
// expect output
Thread = Thread-0
null
현재 스레드가 obj에 대한 the monitor lock을 가지고 있으면 true를 리턴합니다.
이 메소드는 현재 스레드가 이미 lock을 보유하고 있다고 주장할 수 있도록 설계되었습니다.
assert Thread.holdsLock(obj);
public class ThreadDemo implements Runnable {
Thread th;
public ThreadDemo() {
th = new Thread(this);
th.start();
}
public void run() {
// 현재 lock을 가지고 있지 않으므로 false를 리턴합니다.
System.out.println("Holds Lock = " + Thread.holdsLock(this));
// 현재 이 오브젝트에 대한 lock을 가질 것이므로 true를 리턴합니다.
synchronized (this) {
System.out.println("Holds Lock = " + Thread.holdsLock(this));
}
}
public static void main(String[] args) {
new ThreadDemo();
}
}
// expected output
Holds Lock = false
Holds Lock = true
현제 스레드가 interrupted 되었는지 테스트 합니다.
이 메소드에 의해 스레드의 interrupted 상태가 지워집니다. 즉 interrupted 된 스레드에서 이 메소드를 두번 호출한다면 첫번째에는 interrupted 상태를 지우고 두번째에는 false가 리턴됩니다.
public class ThreadDemo implements Runnable {
Thread t;
ThreadDemo() {
t = new Thread(this);
System.out.println("Executing " + t.getName());
// this will call run() fucntion
t.start();
// interrupt the threads
if (!Thread.interrupted()) {
t.interrupt();
}
// block until other threads finish
try {
t.join();
} catch(InterruptedException e) {}
}
public void run() {
try {
while (true) {
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.print(t.getName() + " interrupted:");
System.out.println(e.toString());
}
}
public static void main(String args[]) {
new ThreadDemo();
new ThreadDemo();
}
}
// expected output
Executing Thread-0
Thread-0 interrupted:java.lang.InterruptedException: sleep interrupted
Executing Thread-1
Thread-1 interrupted:java.lang.InterruptedException: sleep interrupted
Thread.onSpinWait() 메서드는 Java 9에 도입되었습니다.
Thread Class의 static method이며 대기 중인 루프에서 선택적으로 호출할 수 있습니다.
JVM이 일부 시스템 아키텍처에서 프로세서 명령을 실행하여 스핀-대기 루프에서 반응 시간을 개선하고 코어 스레드에서 소비되는 파워도 줄일 수 있습니다.
캡처되지 않은 예외로 인해 스레드가 갑자기 종료되고 해당 스레드에 대해 정의된 다른 처리기가 없을 때 호출되는 기본 처리기를 설정합니다.
캡처되지 않은 예외 처리는 먼저 스레드에 의해 제어된 다음 스레드의 스레드 그룹 객체와 마지막으로 캡처되지 않은 기본 예외 처리기에 의해 제어됩니다.
스레드에 명시적으로 캐시되지 않은 예외 처리기 집합이 없고 스레드의 스레드 그룹(상위 스레드 그룹 포함)이 uncatchedException 메서드를 specialize 않으면 기본 처리기의 uncatchedException 메서드가 호출됩니다.
public class ThreadDemo implements Runnable {
public void run()
{
throw new RuntimeException();
}
public static void main(String[] args)
{
Thread thread = new Thread(new ThreadDemo());
thread.setDefaultUncaughtExceptionHandler(new Thread.
UncaughtExceptionHandler()
{
public void uncaughtException(Thread thread, Throwable e)
{
System.out.println("Exception caught: " + e);
}
});
// call run() function
thread.start();
}
}
// expected output
Exception caught: java.lang.RuntimeException
시스템 타이머에 따라 지정된 시간(밀리초) 동안 현재 실행 중인 스레드가 절전 모드로 전환되도록 합니다.
스레드는 monitors의 소유권을 잃지 않습니다.
public class ThreadDemo implements Runnable {
public void run() {
for (int i = 10; i < 13; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
try {
Thread.sleep(1000);
} catch (Exception e) {
System.out.println(e);
}
}
}
public static void main(String[] args) throws Exception {
Thread t = new Thread(new ThreadDemo());
t.start();
Thread t2 = new Thread(new ThreadDemo());
t2.start();
}
}
// expected output
Thread-1 10
Thread-0 10
Thread-0 11
Thread-1 11
Thread-1 12
Thread-0 12
현재 스레드가 CPU를 과도하게 사용하는 걸 막기위한 개선을 목적으로 현재 사용하고 있는 프로세서를 양보하는데 사용합니다.
이 메소드의 용도는 상세한 프로파일링 및 벤치마킹과 결합하여 실제로 원하는 효과가 나오도록 해야합니다.
거의 이 메소드를 사용하는 건 적합하지 않고 race condition 상태에서 디버깅과 버그를 재현하는데 도움이 될 수 있습니다.
public class ThreadDemo extends Thread {
public void run()
{
for (int i=0; i<3 ; i++)
System.out.println(Thread.currentThread().getName() + " in control");
}
public static void main(String[]args)
{
ThreadDemo t1 = new ThreadDemo();
ThreadDemo t2 = new ThreadDemo();
t1.start();
t2.start();
for (int i=0; i<3; i++)
{
t1.yield();
System.out.println(Thread.currentThread().getName() + " in control");
}
}
}
// expected output
main in control
Thread-0 in control
Thread-1 in control
Thread-0 in control
main in control
Thread-0 in control
Thread-1 in control
main in control
Thread-1 in control
현재 실행중인 스레드가 해당 스레드를 수정할 권한이 있는지 확인합니다.
public class ThreadDemo {
public static void main(String args[]) {
new ThreadClass("A");
Thread t = Thread.currentThread();
try {
// 현제 스레드 이 스레드를 수정할 권한이 있는지 확인합니다.
t.checkAccess();
System.out.println("You have permission to modify");
}
// 만약 수정할 권한이 없다면 에러로 빠집니다.
catch(Exception e) {
System.out.println(e);
}
}
}
class ThreadClass implements Runnable {
Thread t;
String str;
ThreadClass(String str) {
this.str = str;
t = new Thread(this);
t.start();
}
public void run() {
System.out.println("This is run() function");
}
}
// expected output
You have permission to modify
This is run() function
이 스레드에 대한 Context ClassLoader를 반환합니다.
ClassLoader는 클래스와 리소스를 로딩할떼 스레드에서 사용할 수 있도록 스레드 작성자에게서 제공됩니다.
설정되지 않은경우 기본값은 상위 스레드의 ClassLoder입니다.
public class ThreadDemo extends Thread {
Thread t;
ThreadDemo() {
t = new Thread(this);
// this will call run() function
t.start();
}
public void run() {
ClassLoader c = t.getContextClassLoader();
System.out.println("Class = " + c.getClass());
System.out.println("Parent = " + c.getParent());
}
public static void main(String args[]) {
new ThreadDemo();
}
}
// expected output
Class = class jdk.internal.loader.ClassLoaders$AppClassLoader
Parent = jdk.internal.loader.ClassLoaders$PlatformClassLoader@62f6d394
스레드의 식별자를 리턴합니다. 스레드 ID는 Positive long number이며 스레드가 만들어질 때 ID도 만들어집니다.
이 스레드 ID는 Unique하며 Lifetime 동안 변하지 않습니다. 스레드가 종료된다면 이 ID는 재사용될 수 있습니다.
public class ThreadDemo extends Thread {
Thread t;
ThreadDemo() {
t = new Thread(this, "Admin Thread");
System.out.println("thread = " + t);
t.start();
}
public void run() {
System.out.println("Name = " + t.getName());
System.out.print("Id = " + t.getId());
}
public static void main(String args[]) {
new ThreadDemo();
}
}
// expected output
thread = Thread[Admin Thread,5,main]
Name = Admin Thread
Id = 22
스레드의 이름을 리턴합니다.
public class ThreadDemo extends Thread {
Thread t;
ThreadDemo() {
t = new Thread(this, "Admin Thread");
System.out.println("thread = " + t);
t.start();
}
public void run() {
System.out.println("Name = " + t.getName());
System.out.print("Id = " + t.getId());
}
public static void main(String args[]) {
new ThreadDemo();
}
}
// expected output
thread = Thread[Admin Thread,5,main]
Name = Admin Thread
Id = 22
스레드의 우선순위를 리턴합니다.
자바 런타임은 고정 우선순위 스케쥴링 알고리즘인 deterministic scheduling algorithm을 사용합니다.
이 알고리즘은 실행 가능한 다른 스레드와 우선순위를 비교해 스레드 실행을 예약합니다.
Java 스레드가 작성되면, Java 쓰레드는 작성된 스레드에서 우선순위를 이어받습니다.
우선순위는 setPriority()를 통해 언제든지 바꿀 수 있습니다.
스레드 우선순위는 Thread.MIN_PRIORITY(1)부터 Thread.MAX_PRIORITY(10)까지 정할 수 있고 숫자가 높을수록 우선순위가 높습니다.
스레드 우선순위가 같다면 두 스레드 중 하나를 라운드 로빈 방식으로 실행하도록 선택합니다.
public class ThreadDemo extends Thread {
public void run() {
for (int i = 0; i < 100000 ; i++) {
System.out.println("Priority = " + this.getPriority() + " i = " + i);
}
}
public static void main(String args[]) {
ThreadDemo threadDemo1 = new ThreadDemo();
ThreadDemo threadDemo2 = new ThreadDemo();
threadDemo1.setPriority(1);
threadDemo2.setPriority(10);
threadDemo1.start();
threadDemo2.start();
}
}
// expected output
...
Priority = 10 i = 41
Priority = 10 i = 42
Priority = 10 i = 43
Priority = 10 i = 44
Priority = 1 i = 5
Priority = 10 i = 45
Priority = 10 i = 46
Priority = 10 i = 47
Priority = 1 i = 6
이 스레드가 속한 스레드 그룹을 반환합니다. 스레드가 중지되거나 죽은 경우 null을 반환합니다.
public class ThreadDemo implements Runnable {
Thread t;
ThreadGroup tgrp;
ThreadDemo() {
tgrp = new ThreadGroup("Thread Group");
t = new Thread(tgrp, this);
t.start();
}
public void run() {
System.out.println(t.getThreadGroup());
}
public static void main(String[] args) {
new ThreadDemo();
}
}
// expected output
java.lang.ThreadGroup[name=Thread Group,maxpri=10]
이 스레드의 상태를 반환합니다. 이 방법은 동기화 제어가 아닌 시스템 상태 모니터링에 사용하도록 설계되었습니다.
public class ThreadDemo implements Runnable {
public void run() {
Thread.State state = Thread.currentThread().getState();
System.out.println(Thread.currentThread().getName());
System.out.println("state = " + state);
}
public static void main(String args[]) {
Thread t = new Thread(new ThreadDemo());
t.start();
}
}
// expected output
Thread-0
state = RUNNABLE
이 스레드를 interrupt 합니다.
만약 승인받은 현재 스레드가 interrupt되지 않는다면 checkAccess 메소드를 호출한 후 SecurityException이 발생할 수 있습니다.
스레드가 join(), wait(), sleep() 이 메소드가 불러져있던 상태였다면 인터럽트 상태는 지워지고 InterruptedException이 발생합니다.
스레드가 InterruptibleChannel에서 I/O operation 작업중에 스레드가 차단된다면 스레드는 인터럽트 상태가되고 ClosedByInterruptException이 발생합니다.
이와같은 조건이 아니라면 인터럽트 상태가 설정됩니다.
살아있지 않은 스레드에 인터럽트를 거는건 어떠한 영향을 끼치지 않습니다.
public class ThreadDemo extends Thread {
Thread t;
ThreadDemo() {
t = new Thread(this);
System.out.println("Executing " + t.getName());
t.start();
if (!t.interrupted()) {
t.interrupt();
}
try {
t.join();
} catch(InterruptedException e) {}
}
public void run() {
try {
while (true) {
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.print(t.getName() + " interrupted:");
System.out.println(e.toString());
}
}
public static void main(String args[]) {
new ThreadDemo();
new ThreadDemo();
}
}
// expected output
Executing Thread-1
Thread-1 interrupted:java.lang.InterruptedException: sleep interrupted
Executing Thread-3
Thread-3 interrupted:java.lang.InterruptedException: sleep interrupted
이 스레드가 살아있는지 확인합니다.
public class ThreadDemo extends Thread {
public void run() {
Thread t = Thread.currentThread();
// tests if this thread is alive
System.out.println("status = " + t.isAlive());
}
public static void main(String args[]) throws Exception {
Thread t = new Thread(new ThreadDemo());
t.start();
t.join();
System.out.println("status = " + t.isAlive());
}
}
// expected output
status = true
status = false
스레드가 데몬 스레드인지 확인합니다.
public class ThreadDemo extends Thread {
public ThreadDemo(String name){
super(name);
}
public void run()
{
// Checking whether the thread is Daemon or not
if(Thread.currentThread().isDaemon())
{
System.out.println(getName() + " is Daemon thread");
}
else
{
System.out.println(getName() + " is User thread");
}
}
public static void main(String[] args)
{
ThreadDemo t1 = new ThreadDemo("t1");
ThreadDemo t2 = new ThreadDemo("t2");
ThreadDemo t3 = new ThreadDemo("t3");
t1.setDaemon(true);
t1.start();
t2.start();
t3.setDaemon(true);
t3.start();
}
}
// expected output
t1 is Daemon thread
t3 is Daemon thread
t2 is User thread
스레드가 죽을 때까지 기다립니다.
join(long millis) 메소드의 경우 최대 millis초 만큼 기다립니다. 0을 넣으면 영원히 기다립니다.
join(long millis, int nanos)의 경우 millis + nanos 초 만큼 기다립니다.
public class ThreadDemo extends Thread {
public void run() {
for(int i=1;i<=5;i++){
try{
Thread.sleep(500);
}catch(Exception e){System.out.println(e);}
System.out.println("Thread name = " + this.getName() + " i = " +i);
}
}
public static void main(String[] args) {
ThreadDemo t1=new ThreadDemo();
ThreadDemo t2=new ThreadDemo();
ThreadDemo t3=new ThreadDemo();
t1.setName("t1");
t2.setName("t2");
t3.setName("t3");
t1.start();
try{
t1.join();
}catch(Exception e){System.out.println(e);}
t2.start();
t3.start();
}
}
// expected output
Thread name = t1 i = 1
Thread name = t1 i = 2
Thread name = t1 i = 3
Thread name = t1 i = 4
Thread name = t1 i = 5
Thread name = t3 i = 1
Thread name = t2 i = 1
Thread name = t3 i = 2
Thread name = t2 i = 2
Thread name = t2 i = 3
Thread name = t3 i = 3
Thread name = t2 i = 4
Thread name = t3 i = 4
Thread name = t3 i = 5
Thread name = t2 i = 5
스레드가 별도의 Runnable run 객체를 사용하여 구성된 경우 Runnable 객체의 run 메서드가 호출되고, 그렇지 않으면 이 메서드는 아무 작업도 수행하지 않고 반환됩니다.
이 스레드가 실행을 시작합니다. Java Virtual Machine은 이 스레드의 실행 메서드를 호출합니다.
그 결과 현재 스레드(호출에서 시작 메서드로 반환)와 다른 스레드(실행 메서드를 실행하는) 두 스레드가 동시에 실행됩니다.
start() 메소드를 두번이상 하는건 올바르지 않습니다.
스레드는 다음 상태 중 하나입니다.
아직 시작하지 않은 스레드의 상태입니다.
실행 가능한 스레드의 상태입니다.
JVM에서 실행 중인 스레드지만 프로세서와 같은 다른 운영체제의 리소스를 기다리는 상태일 수 있습니다.
monitor lock을 기다리는 동안 차단된 스레드의 상태입니다.
synchronized block/method를 만난 후 Object.wait을 통해 다시 재진입을 기다리는 상태입니다.
대기중인 스레드의 상태입니다.
다음과 같은 메소드를 호출해서 기다리는 상태가 될 수 있습니다.
대기 상태의 스레드는 다른 스레드가 특정 작업을 수행하기를 기다리고 있습니다.
예를 들어 개체에서 Object.wait()를 호출한 스레드는 해당 개체의 Object.notify() 또는 Object.notifyAll()을 호출할 다른 스레드를 기다리고 있습니다. 스레드.join()이라고 하는 스레드가 지정된 스레드가 종료될 때까지 대기하고 있습니다.
대기 시간이 지정된 대기 스레드의 스레드 상태입니다.
지정한 양의 대기 시간으로 다음 메서드 중 하나를 호출하여 스레드가 시간 대기 상태에 있습니다.
종료된 스레드의 스레드 상태입니다. 스레드가 실행을 완료했습니다.
Java Virtual Machine을 사용하면 응용 프로그램에 여러 실행 스레드가 동시에 실행될 수 있습니다.
우선순위가 높은 스레드는 우선순위가 낮은 스레드를 선호하여 실행됩니다.
스레드에 허용되는 우선 순위 값은 1에서 10 사이의 범위입니다. 우선 순위에 대한 스레드 클래스에 정의된 static variable는 3개입니다.
java.lang.Thread.getPriority() 메소드로 인해 스레드 우선순위 값을 가지고 올 수 있습니다.
java.lang.Thread.setPriority() 메소드로 인해 스레드 우선순위 값으 변경할 수 있습니다.
두 스레드의 우선 순위가 같으면 어떤 스레드가 먼저 실행될지 예상할 수 없습니다. 스레드 스케줄러의 알고리즘(Round-Robin, First Come First Server 등)에 따라 다릅니다.
Java 프로그램이 시작되면 하나의 스레드가 즉시 실행되기 시작합니다. 이것은 프로그램이 시작될 때 실행되는 것이기 때문에 보통 프로그램의 Main 스레드로 불립니다.
특징으로는 Main 스레드로부터 하위 스레드가 생겨날 수 있습니다. 그리고 다양한 종료 작업을 수행하므로 실행을 마치는 마지막 스레드여야 하는 경우가 많습니다.
Main 스레드의 참조를 얻기 위해서는 Main 메소드에서 Thread Class에 있는 currentThread 메소드를 통해 얻을 수 있습니다.
JVM은 프로그램 실행을 시작할 때 Main 스레드를 자동으로 생성합니다.
스레드를 작성하든 작성하지 않든 Main 스레드는 프로그램에 존재합니다.
그리고 JVM은 각 스레드에 스택을 할당합니다. 각 스레드는 런타임 데이터를 저장하는 데 사용됩니다.
JVM이 Main 메소드에서 실행을 시작하면 Main 스레드에 대한 스택이 생성되고 특정 스택에 데이터가 저장됩니다.
모든 메서드 호출은 스택에 저장되며 각 항목은 스택 프레임으로 알려져 있습니다. 스택에는 스택 크기에 따라 완전히 달라지는 여러 스택 프레임이 있을 수 있습니다.
그리고 스레드가 스택 크기보다 많은 항목을 스택에 저장하려고 하면 스레드가 스택 오버플로 오류를 발생시킵니다
응용 프로그램에는 스레드 수가 얼마든지 있을 수 있고 JVM은 프로그램의 시작으로 항상 Main 스레드를 먼저 실행합니다.
다른 모든 스레드는 Main 스레드의 일부입니다. 즉, 각 스레드가 Main 스레드 이후에 실행을 시작한다는 의미입니다. 프로그램에 여러 스레드가 있다고 가정하면 JVM은 런타임에 여러 스택을 생성합니다.
예전에 Oracle Java Document를 보고 쓴 글인 Java Synchronization에서 이 내용을 다룬 적이 있습니다.