사실 GC부터 할려고 했는데 갑자기 쓰레드가 땡겨서...
시작합니다.
쓰레드(Thread)는 프로세스 내에서 독립적으로 실행되는
작업의 단위입니다. 각각의 쓰레드는 프로세스 내의 자원을 공유하면서 동시에 여러 작업을 수행할 수 있습니다. 이를 통해 다중 작업(multi-tasking)이나 다중 처리(multi-processing)를 구현할 수 있습니다.
이게 무슨 뜻이냐하면

싱글스레드와 멀티스레드의 특징들부터 알아볼게요
싱글 쓰레드는 하나의 쓰레드만을 사용하여 작업을 처리하는 모델입니다. 프로그램이 순차적으로 실행되며, 한 번에 하나의 작업만 처리됩니다. 예를 들어, 웹 브라우저가 싱글 쓰레드로 동작하는 경우, 한 번에 하나의 페이지만 로드되고, 사용자가 다른 페이지를 요청할 때까지 이전 작업이 완료될 때까지 기다려야 합니다. 이러한 모델은 간단하고 예측 가능하지만, 여러 작업을 동시에 처리할 수 없으며, 작업이 느리게 진행될 수 있습니다.
멀티 쓰레드는 여러 쓰레드를 사용하여 작업을 동시에 처리하는 모델입니다. 각 쓰레드는 독립적으로 실행되며, 서로 다른 작업을 수행할 수 있습니다. 이를 통해 여러 작업을 동시에 처리하여 시스템의 성능을 향상시키고, 응답성을 향상시킬 수 있습니다. 예를 들어, 웹 서버가 멀티 쓰레드로 동작하는 경우, 여러 클라이언트 요청을 동시에 처리할 수 있으며, 한 클라이언트의 요청이 블로킹되더라도 다른 클라이언트의 요청을 처리할 수 있습니다.
이렇게만 보면 멀티 쓰레드가 Goat인데 물론 문제가 있겠죠?
쓰레드 간 동기화와 관련된 문제를 처리해야하고 이로 인해 프로그래밍이 복잡해진다는 단점이 있습니다.
동기화 예시 함 보러 가실게요.
두 개의 쓰레드가 동시에 같은 계좌에서 자금을 인출하려고 할 때.
class BankAccount {
private int balance = 1000; // 초기 계좌 잔액
public void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
System.out.println("Successfully withdrawn: " + amount);
} else {
System.out.println("Insufficient funds!");
}
}
}
public class BankExample {
public static void main(String[] args) {
BankAccount account = new BankAccount();
// 두 개의 쓰레드에서 동시에 계좌에서 자금을 인출하는 시도
Thread t1 = new Thread(() -> {
account.withdraw(800);
});
Thread t2 = new Thread(() -> {
account.withdraw(800);
});
t1.start();
t2.start();
}
}
이제 동기화를 사용해서 문제 해결하기.
동기화는 한 번에 하나의 쓰레드만이 특정 블록을 수행하도록 보장합니다.
class BankAccount {
private int balance = 1000; // 초기 계좌 잔액
public synchronized void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
System.out.println("Successfully withdrawn: " + amount);
} else {
System.out.println("Insufficient funds!");
}
}
}
동기화는 이렇게 여러 쓰레드 간의 상호작용을 조율하고, 데이터의 일관성과 무결성을 유지하는 데
중요한 역할을 합니다. 하지만 과도한 동기화는 성능에 부정적인 영향을 미칠 수 있으므로 적절하게 사용해야합니다.
뭔 얘기냐 하면...
예시를 또 들어볼게요(과도한 동기화)
import java.util.ArrayList;
import java.util.List;
class SharedResource {
private List<Integer> list = new ArrayList<>();
public synchronized void addItem(int item) {
list.add(item);
}
public synchronized void removeItem(int item) {
list.remove(Integer.valueOf(item));
}
public synchronized boolean containsItem(int item) {
return list.contains(item);
}
}
public class SynchronizationExample {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
Thread producerThread = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
resource.addItem(i);
}
});
Thread consumerThread = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
resource.removeItem(i);
}
});
producerThread.start();
consumerThread.start();
try {
producerThread.join();
consumerThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("List size: " + resource.getListSize());
}
}
쓰레드가 많지 않기 때문에 문제가 발생하진 않을 것 같지만 실제 상황에서는 많은 쓰레드가 동시에 실행 될 경우
성능 저하나 데드락이 발생할 수 있어요.
그래서 동기화 블록을 최소화하는 것이 좋고 Lock 객체를 사용해서 더 세밀한 제어를 한다거나,
동시성 컬렉션을 사용해서 동시에 안전하게 작업할 수 있어요.
Lock 객체 예시도 들어볼까요?
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
public class LockExample {
public static void main(String[] args) {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
Lock 인터페이스를 구현한 ReentrantLock 객체를 사용해서 동기화를 수행하는데
increment() 메서드와 getCount() 메서드에서는 먼저 lock을 획득하고(lock.lock()) 작업을 수행한 후
finally 블록에서 lock을 반납합니다.
이제 동시성 컬렉션도 알아봐야겠죠?
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class ConcurrentCollectionExample {
public static void main(String[] args) {
ConcurrentMap<String, Integer> map = new ConcurrentHashMap<>();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
map.put(Integer.toString(i), i);
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final map size: " + map.size());
}
}
컬렉션프레임워크 정리 마렵네...
여기서 ConcurrentMap은 내부적으로 세분화된 lock을 활용해서 동시에 안전하게 작업을 수행할 수 있도록 합니다.
결론은 동기화를 사용할 때는 신중하게 고려하고 필요한 경우에만 사용하도록 합시다.
이제.... 쓰레드를 봤으니 프로세스 안 나올 수가 없겠죠?
프로세스 갑니다.
프로세스는 운영체제에서 실행 중인 프로그램을 나타냅니다. 각 프로세스는 독립적인 메모리 공간을 할당받으며, 실행 중인 프로그램의 인스턴스입니다. 각 프로세스는 최소한 하나 이상의 쓰레드를 포함하고 있습니다.
IPC(Inter-Process Communication)를 사용해야 합니다. 반면에 쓰레드는 같은 프로세스 내에서 공유된 메모리를 사용하기 때문에, 데이터를 공유하기 위해 별도의 메커니즘이 필요하지 않습니다.오버헤드가 발생할 수 있지만, 쓰레드는 비교적 작은 오버헤드로 생성 및 종료할 수 있습니다.병렬 처리가 가능합니다. 반면에 쓰레드는 같은 프로세스 내에서 실행되기 때문에, 동시에 여러 쓰레드가 실행되더라도 실제로 동시에 실행되는 것은 아닙니다.예로는 웹 브라우저가 하나의 프로세스이며 각 탭은 다른 쓰레드입니다. 쓰레드를 사용해서 각 탭에서 동시에 여러 웹
페이지를 로드할 수 있습니다.
텍스트 편집기가 하나의 프로세스이며 각 문서 편집창은 다른 쓰레드입니다. 이를 통해 여러 문서를 동시에 편집할 수 있씁니다.
이제 IPC(Inter-Process Communication)이나 오버헤드를 궁금해하실 분들을 위해....(내가 궁금했음)
IPC(Inter-Process Communication)는 서로 다른 프로세스 간에 데이터를 교환하고 통신하기 위한 메커니즘을 의미합니다. 각각의 프로세스는 독립적인 메모리 공간을 가지고 있기 때문에, IPC를 사용하여 프로세스 간에 데이터를 전달하고 서로 통신할 수 있습니다.
파이프(Pipe): 단방향 통신을 위한 IPC 메커니즘으로, 두 프로세스 간에 바이트 스트림을 전달합니다. 부모 프로세스와 자식 프로세스 사이에서 주로 사용됩니다.
명명된 파이프(Named Pipe 또는 FIFO): 파일 시스템에서 이름을 가진 파이프로, 서로 다른 프로세스 간에 양방향 통신을 할 수 있습니다. 이름있는 파이프는 파일 시스템을 통해 생성되며, 독립적인 프로세스들 간에도 통신이 가능합니다.
메시지 큐(Message Queue): 메시지 큐는 메시지를 보내고 받는 프로세스 간의 통신을 제공합니다. 각 메시지는 특정한 형식을 가지고 있으며, 비동기식으로 전달될 수 있습니다.
공유 메모리(Shared Memory): 두 개 이상의 프로세스가 같은 메모리 공간을 공유하여 데이터를 주고받을 수 있습니다. 공유 메모리는 데이터를 직접 읽고 쓰기 때문에 빠르게 통신할 수 있지만, 동기화가 필요합니다.
소켓(Socket): 네트워크를 통해 프로세스 간에 통신하는 데 사용됩니다. TCP/IP나 UDP와 같은 프로토콜을 사용하여 데이터를 주고받을 수 있습니다. 소켓은 로컬 머신 뿐만 아니라 원격 머신과의 통신에도 사용됩니다.
오버헤드(Overhead)란?
"오버헤드(Overhead)"는 어떤 작업이나 프로세스를 수행하는 데 추가적으로 소비되는 자원 또는 비용을 의미합니다. 이는 주로 시스템의 성능을 저하시키는 요소로 작용할 수 있습니다.
발생 원인
알고리즘 복잡도: 일부 알고리즘이나 프로세스는 추가적인 계산이 필요하거나 더 많은 메모리를 사용하여 작업을 수행해야 할 수 있습니다.
데이터 구조: 데이터를 저장하고 처리하는 데 사용되는 구조는 오버헤드를 초래할 수 있습니다. 예를 들어, 배열의 크기를 동적으로 조정하는 경우에는 추가적인 메모리 할당 및 복사 작업이 필요합니다.
네트워크 통신: 데이터를 네트워크를 통해 전송하는 경우에는 데이터 패킷화, 프로토콜 오버헤드 등의 추가 작업이 발생할 수 있습니다.
이 둘은 나중에 깊게 다뤄볼게요~!