※본 내용은 <자바의 정석>을 기반으로 작성되었습니다.
BE 파트스터디를하면서 다중스레드 등의 내용이 가끔 나오고는 하는데 대략적으로 무슨 느낌인지만 알고 실제로는 잘 모르겠어서 간단하게 개념정도만 한 번 조사해보려한다.
스레드는 자원을 이용해 작업을 수행한다. 간단한 프로그램을 만들어서 돌려도 스레드가 작업을 돌리고, 엄청 복잡한 프로그램을 만들어도 스레드가 작업을 수행한다. 모든 프로세스, 프로그램은 하나 이상의 스레드가 존재하며, 여러 개의 스레드를 가지면 멀티스레드 프로세스라고 칭한다.
프로세스는 무수히 많은 스레드를 가질 수 있으나 메모리 한계에 따라 생성 가능한 스레드의 수가 정해진다.
멀티스레딩은 하나의 프로세스가 여러 작업을 수행하는 것이다. 멀티스레딩하면 나오는 대표적인 예시인 채팅. 채팅에는 UI를 구성하는 스레드와 메시지의 송수신을 담당하는 스레드로 나뉘어 프로그램을 실행하면 아주 짧은 시간 내에 스레드를 번갈아가며 작업을 수행하여 여러 작업들이 동시에 처리되는 것처럼 보이게한다.
자바에서 스레드를 구현하는 방법은 Thread 클래스를 상속하거나 Runnable 인터페이스를 구현하는 것 두 가지로 나뉜다. 다만 다중상속이 안되는 자바 특성 상 Runnable의 구현체를 만드는 것이 더 낫다.
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
Runnable 인터페이스는 간단하게 생겼다. run 메서드의 몸통을 채워 해당 스레드가 어떤 일을 해야할지 명시만하면 된다.
class myThread implements Runnable {
public void run() {
}
}
class myThread2 extends Thread {
public void run() {
}
}
public class Stack {
public static void main(String[] args) {
Runnable r = new myThread();
Thread t1 = new Thread(r);
Thread t2 = new myThread2();
t1.start();
t2.start();
}
}
Thread를 확장한 경우 그대로 start 메서드를 불러오면되며, Runnable의 구현체가 된 경우 Thread 객체의 생성자에 구현체를 전달하고 start 메서드를 불러오면 된다.
run 메서드를 사용하면 단순 메서드 호출로, 메인메서드 위에 호출스택에 run 이 올라가게된다. 즉, 멀티스레드 환경으로 넘어가는 것이 아니라 단일 스레드 환경에서 개발하게되는 것이기 때문에 start 메서드를 호출한다.
start 메서드는 메인메서드가 있는 호출스택이 아니라 새로운 스레드를 형성한 후, 거기서 run 을 호출한다.
멀티스레드 상태에서는 호출스택의 최상위에 있는 메서드일지라도 대기상태에 있을 수 있다. 이는 스케줄러가 스레드들의 우선순위를 고려하여 실행순서와 시간을 결정하기 때문에 본인의 차례가 아닌 스레드에 있는 메서드들은 호출스택에 최상위에 있어도 대기상태에 있어야한다.
멀티스레드는 무엇이 좋은가?
하나의 스레드에서 여러 작업을 처리하는 경우 호출스택의 구조에서 알 수 있듯이 하나의 작업이 끝나야 다음 작업이 시작된다. 하지만 멀티스레드 환경에서는 여러 스레드를 왔다갔다하며 동시에 두 작업이 처리된다.
다만 단일스레드 환경과 멀티스레드 환경에서 두 가지의 작업이 끝난 경우 처리시간은 비슷하다.
오히려 간단한 작업이 두 개 있다고 쳤을 때
public class Stack {
public static void main(String[] args) throws Exception {
int i = 0;
long start = System.currentTimeMillis();
for (int idx = 0; idx < 10000; idx++) {
System.out.print("l");
}
System.out.println("소요시간 : " + (System.currentTimeMillis() - start));
i = 0;
start = System.currentTimeMillis();
for (int idx = 0; idx < 10000; idx++) {
System.out.print("l");
}
System.out.println("소요시간 : " + (System.currentTimeMillis() - start));
}
}
class myThread1 extends Thread {
public void run() {
int i = 0;
long start = System.currentTimeMillis();
for (int idx = 0; idx < 10000; idx++) {
System.out.println("l");
}
System.out.println("소요시간 : " + (System.currentTimeMillis() - start));
}
}
class myThread2 extends Thread {
public void run() {
int i = 0;
long start = System.currentTimeMillis();
for (int idx = 0; idx < 10000; idx++) {
System.out.println("l");
}
System.out.println("소요시간 : " + (System.currentTimeMillis() - start));
}
public void Exception() {
}
}
public class Stack {
public static void main(String[] args) throws Exception {
Thread t2 = new myThread2();
Thread t1 = new myThread1();
t2.start();
t1.start();
}
}


메인스레드에서 작업을 두 번 실행하는 것과 멀티스레드 환경에서 작업을 수행하는데에는 시간의 차이가 나는데, 원인은 스레드를 왔다갔다하며 작업을 수행하여 작업 전환시간이 생기고, 한 스레드가 출력하는 동안 다른 스레드는 대기하고 있어야해서 대기시간 때문에 단일스레드 환경보다 멀티스레드 환경에서 시간이 더 걸린다.
하지만 스레드가 서로 다른 자원을 사용하는 작업의 경우 멀티스레드가 더 효율적이다.이를테면 데이터를 입력받거나 외부로부터 파일을 받는 등 외부기기와의 입출력이 필요한 경우가 이에 해당한다. 데이터를 받는 경우 사용자가 입력해주기 전까지는 무조건 대기하고 있어야하나 멀티스레드 환경에서는 사용자의 입력을 대기하는 동안 다른 스레드의 작업이 가능해지기 때문이다.
'외부기기'란 메인스레드가 실행 중인 CPU가 아니라 다른 입출력장치를 말한다. 서로다른 입출력 장치를 이용해야한다. 그래서 scanner와 print는 이에 해당하지 않는다.
스레드 우선순위
/**
* The minimum priority that a thread can have.
*/
public static final int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public static final int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public static final int MAX_PRIORITY = 10;
**/
스레드는 내부변수로 우선순위를 가진다. 가장 작은 것이 1, 가장 큰 것이 10, 기본값은 5다. 숫자가 높을수록 우선순위가 높으며, 작업 중요도에 따라 스레드의 우선순위를 다르게 지정하여 특정 스레드의 작업시간을 더 높일 수 있다.
참고로 main 메서드가 있는 스레드는 우선순위가 5다.
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
public final int getPriority() {
return priority;
}
이 우선순위들은 게터세터가 구현되어있어 위 메서드를 통해 접근할 수 있다.
class myThread1 extends Thread {
public void run() {
int i = 0;
for (int idx = 0; idx < 100; idx++) {
System.out.print("1");
}
}
}
class myThread2 extends Thread {
public void run() {
int i = 0;
for (int idx = 0; idx < 100; idx++) {
System.out.print("2");
}
}
}
public class Stack {
public static void main(String[] args) throws Exception {
Thread t1 = new myThread1();
Thread t2 = new myThread2();
t2.setPriority(2);
t1.start();
t2.start();
}
}
22222222111111111111111111111112222222111111112222222221111111112222222222222222222222222222222221111111111111111111111111111111111112222111111111111111111111111222222222222222222222222222222222222222
t1 스레드는 1을, t2 스레드는 2를 출력하는 멀티스레드 환경에서
우선순위가 동일할 경우에는 1과 2가 고루 출력되는 것으로 보인다.
class myThread1 extends Thread {
public void run() {
int i = 0;
for (int idx = 0; idx < 100; idx++) {
System.out.print("1");
}
}
}
class myThread2 extends Thread {
public void run() {
int i = 0;
for (int idx = 0; idx < 100; idx++) {
System.out.print("2");
}
}
}
public class Stack {
public static void main(String[] args) throws Exception {
Thread t1 = new myThread1();
Thread t2 = new myThread2();
t2.setPriority(2);
t1.start();
t2.start();
}
}
11111111111111111111222221111111111111111111111111111122222222211111111111111222221122222222222222222222222222222222222222221111111111111111111111111111111111122222222222222222222222222222222222222222
t2의 우선순위를 2로 설정해 t1보다 우선순위를 낮게 배정한 결과 t1이 우선적으로 실행되는 것을 볼 수 있다.
이렇게 우선순위를 조정할 수 있다. 가령 채팅에서 채팅 전송의 스레드 우선순위가 높게 설정되고 동영상 저장, 사진 전송 등의 부가기능은 낮게 설정하여 채팅전송이 주기능으로 작용할 수 있게..


그래서 우선순위가 더 높은 스레드의 작업이 빨리 종료된다.
단, CPU가 멀티코어인 경우에는 스레드의 우선순위에 따른 차이가 별 없다고한다.
동기화
스레드와 동기화가 파면 한도끝도없이 내용이 나오는터라 synchronized와 volite만 간단하게 조사해보려고한다. 오늘은 스레드 개념만 좀 알아보는걸로하고..
멀티스레드 프로세스의 경우 여러 스레드가 자원을 공유할 경우 서로 영향을 끼칠 수 있다. 그래서 특정 스레드가 작업을 마치기 전까지 그 영역에는 다른 스레드가 접근할 수 없도록해야하는데, 이를 '임계 영역', '잠금' 개념이 도입되었다.
synchronized
public synchronized void test() { }
메서드 선언부에 synchronized를 붙이면 메서드가 임계영역으로 지정된다. 스레드는 저 메서드가 호출되면 메서드가 포함된 객체의 lock을 얻어 그 스레드만 접근할 수 있게하다가 메서드가 종료되면 lock을 반환한다.
synchronized (Stack.class) {
try {
t1.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.start();
t2.start();
}
아니면 synchronized(참조변수){ } 를 통하는 방법도 있다. 메서드 내의 일부 코드를 블럭으로 감싸며, 참조변수는 락을 걸고자하는 객체의 참조다. 방식은 메서드에 synchronized 선언과 같다.
임계영역은 멀티스레드 성능을 좌우하기 때문에 최소한으로 설정해야한다.
volite()

멀티코어 프로세서의 경우 메모리에서 값을 가져와 CPU 코어마다 캐시를 저장해두는데, 만약 메모리의 값이 변경되었음에도 캐시의 값이 변하지 않으면 메모리의 값과 실제 사용되는 값에 차이가 생긴다. 따라서 이를 방지하기 위해 volatile을 사용한다. 이것이 붙은 변수는 메모리에서 값을 읽어오기 때문에 값의 불일치가 해결된다.
synchronized 를 사용해도되는데, synchronized로 정의된 영역에 들어올 때와 나올 때 캐시-메모리 동기화가 이뤄지기 때문이다.
'언어공부 > Java | Kotlin' 카테고리의 다른 글
| [CS] 자바의 정석 독서 #14 - 예외처리 (0) | 2025.11.11 |
|---|---|
| [CS] 이펙티브 자바 독서 #3 - 아이템85. 직렬화를 피하라! (0) | 2025.10.23 |
| [CS] 이펙티브자바 독서 #2 - 아이템18 (0) | 2025.10.14 |
| [CS] 이펙티브자바 독서 #1 - 아이템15 ~ 17 (0) | 2025.10.11 |
| [CS] 자바의 정석 독서 #13 - 내부클래스 (0) | 2025.10.10 |
