스레드 with 자바 메모리 구조

Seo-Faper·2023년 2월 14일
0

이것이 자바다

목록 보기
20/20

용어 정리

Processor

프로세스를 실행시켜주는 하드웨어 유닛 (레지스터, 산술 논리 장치 등), 큰 의미로는 CPU 그 자체를 의미한다.
하나의 CPU를 지니면 싱글 Core, 여러개가 있으면 멀티 Core가 된다.
CPU가 하나이더라도 Multi-Threading이 가능한데, 이는 하나의 CPU가 Time Slicing 방식으로 여러개의 Thread를 번갈아가며 작업하는 형태로 진행된다. 이는 특별히 무거운 작업이 아닌 이상 눈치채지 못할 정도로 빠른 속도로 처리된다는 특징이 있다.

여기 보면 CPU가 하나뿐이지만 Time Slice 기술을 통해 여러개의 프로세스를 돌리는 것을 볼 수 있다.

Process

메모리에 적재되어 프로세서에 의해 실행중인 프로그램

Multi-Process

프로세스가 메모리에 여러개 올라가 있는 상태

Thread

어떤 프로세스 내에서 실행되는 흐름의 단위
Thread는 운영체제의 스케쥴러가 관리하는 최소 단위의 Instructuion이다.

Multi-Thread

하나의 프로세스에 둘 이상의 스레드가 동시에 진행되는 것

Thread Safety

멀티스레드 환경에서 충돌이 없는 구조.
하나의 함수가 한 스레드로 부터 호출되어 실행 중일 때, 다른 스레드가 그 함수를 호출하여 동시에 함께 실행되더라도 각 스레드에서의 함수의 수행 결과가 올바르게 나오는 것을 말한다. 멀티 스레드 환경에서 하나의 프로세스는 여러 스레드로 구성될 수 있는데, 프로세스 자원은 스레드간에 공유된다. 그 때 발생하는 문제가 여러 스레드 간에서 참조하고 활용하는 데이터 구조나 객체들이 서로 꼬이고 값이 중간중간에 변경되면서 엉킬 위험이 있다. 이러한 위험이 제거된 상태를 Thread Safe라고 한다.

Queue

CPU의 스케줄러는 큐를 이용해서 작업 순서를 정한다.
프로그램이 스레드를 구성하게 되면 실행코드를 담은 코드의 묶음인 Closure를 Queue에 싣고, 이 Queue를 Thread에 얹어서 실행(Dispatch) 하는 구조이다. CPU 스케줄러의 Queue에는 하나씩 순서대로 진행되는 Serial Queue와 동시에 여러 작업이 수행되는 Concurrent Queue가 있다.

Serial

먼저 수행한 작업이 완전히 끝나야만 다음 작업이 진행된다.
먼저 정의된 작업이 먼저 수행된다. (First In First Out, FIFO)

Concurrency

Serial과는 달리 여러 작업이 동시에 수행되는 것을 말한다.
시작을 동시에 하는 것도 아니고 끝나는 것도 동시에 끝나는게 아니다.
먼저 정의된 작업이 먼저 수행되기는 하지만 (FIFO) 먼저 수행되는 작업이 끝날 때까지 기다리는 것이 아니라 적절한 시점에서 동시에 실행된다.
Async, Multi Threading이 이에 해당된다.

Parallelism

여러 작업이 '끊이지 않고' 동시에 수행되는 것을 말한다.
Concurrent 방식이 Parallel하게 수행될 수도 있고 아닐 수도 있다. 즉, 싱글 Core에서 Time Sharing 방식으로 Multi Threading을 할 때 처럼, 동시에 작업이 진행되더라도 한 작업 조금 하다가 바로 다른 작업을 하고, 다시 원래 작업을 처리하는 방식으로 진행될 수 있다. 이 경우는 Concurrent 하지만 Parallel하지는 않은 예시 이다. Concurrent가 작업들이 동시에 병렬적으로 수행되느냐의 구조(Structure)를 따지는 것 이라면, Parallel은 어떻게 수행되느냐의 수행(Execution) 관련인 것이다.

즉, 멀티 프로세스는 연속성(Concurrency)와 병렬성(Parallelism)을 가진다는 특징이 있다.

멀티 프로세스 vs 멀티 스레드

멀티 스레드와 멀티 프로세스는 여러 흐름을 동시에 수행한다는 공통점이 있다.
그러나 멀티 프로세스는 각 프로세스가 독립적인 메모리를 별도로 가지지만 멀티 스레드는 각 스레드가 속한 프로세스 속에서 메모리를 공유한다는 차이점이 존재한다.

멀티 스레드는 쉽게 말해 프로그램의 로직에서 동시간에 다른 작업을 병행하는 기술이다.

멀티 스레드의 장점은 각 스레드가 자신이 속한 프로세스의 메모리를 공유하기 때문에 시스템의 자원 낭비가 적다는 것과 하나의 스레드가 작업할 때 다른 스레드가 별도의 작업을 할 수 있어서 사용자와의 응답성도 좋아진다.

이렇게 main 함수에서 시작된 코드 흐름에서 동시에 어러개의 작업을 처리하는 방식이다.
즉 프로그램에서 병렬로 실행할 작업을 결정한 후, 각 작업별로 스레드를 나누어 실행해 주는 것이다.

멀티 스레드의 메모리 관리 (예제 코드)

앞서 말했듯 멀티 스레드는 하나의 프로세스 속에서 공통된 메모리를 공유한다.
자바에서 Thread와 Ruunable는 Thread Safey한 상태를 유지하게끔 도와주는 여러 메소드들이 있다.

우선 자바 프로그램에서 스레드의 안정성이 깨지는 상황에 대해 알아보자.

스레드의 안정성이 깨지는 순간

이것은 조회수 계산 프로그램이다.
특정 글을 조회하면 원래 조회수에 1을 더할 것이고, 여러 사용자가 동시에 접속할 수도 있기 때문에 멀티 스레드 환경에서 동작하게 만들었다.
이것은 100명의 사람이 100번 클릭했을 때를 가정한 코드이다.

public class CountingTest {
    public static void main(String[] args) {
        Count count = new Count();
        for (int i = 0; i < 100; i++) {
            new Thread(){
                public void run(){
                    for (int j = 0; j < 100; j++) {
                        System.out.println(count.view());
                    }
                }
            }.start();
        }
    }
}
class Count {
    private int count;
    public int view() {return count++;}
    public int getCount() {return count;}
}

이렇게 되면 100 * 100이라서 10,000이 나올 것 같지만 실행해 보면 10,000이 되지 않는 모습을 볼 수 있다. 그 이유는 멀티스레드의 동시성(Concurrency) 때문이다.
view() 메소드는 count++을 수행하는데, count++는 사실 count = count + 1 과 같으므로
아래와 같은 로직을 거친다.

  1. count 변수의 값을 조회한다.
  2. 조회한 count 변수 값에 1을 더한 값을 저장한다.

이러한 로직 사이에서 여러 스레드가 순서를 지키지 않고 count 변수에 접근을 하게 되면 그림과 같이 동시성 이슈가 발생한다.

멀티 스레드가 과정 1을 동시에 실행하게 되면 실행된 스레드는 2개지만 최종적으로 저장되는 값은 101이 된다.

그래서 synchronized 키워드를 사용해 lock을 걸어 메소드에 스레드가 동시에 접근하는 것을 막는다. synchronized 키워드를 통해 동시에 접근하는 것을 막고 개발자가 의도한 대로 스케줄링되어 Queue에 의해 순차적으로 실행시킬 수 있다. 그러나, 여러 스레드가 하나의 자원에 동시에 read & write를 할 때 항상 메모리에 접근하지 않는다.

자원의 가시성 (volatile)

운영체제는 여러 스레드가 하나의 자원에 동시에 read & write를 할 때, 성능 향상을 위해 메모리가 아닌 CPU 캐시에 저장된 값을 사용한다. 그렇기 때문에 해당 데이터가 메모리에 저장된 실제 데이터와 항상 일치하는지 보장할 수 없다. 변수에 저장한 데이터를 읽었는데 이 데이터가 실제 데이터와 차이가 있을 수 있다는 것이다. 메인 메모리에 저장된 실제 자원의 값을 볼수 있는 개념을 자원의 가시성이라고 부르는데, 이 가시성을 확보하지 못한 경우, 앞서 말한 문제가 발생한다.

volatile 키워드는 이러한 CPU 캐시의 사용을 막는다. 해당 변수에 volatie를 붙여주면 이 변수는 캐시에 저장되는 대상에서 제외된다. 그러므로 매 번 메모리에 접근해서 실제 값을 읽어오도록 설정되어 캐시 사용으로 인한 데이터 불일치를 막아준다.

불변 객체 (Immutable Instance)

스레드에 안전한 프로그래밍을 하는 방법 중 가장 안전한 방법은 바로 불변 객체를 만드는 것이다.
내부적인 상태가 변하지 않으니 여러 스레드가 동시에 참조해도 동시성 이슈가 발생하지 않는다.

아니면 그냥 내부 상태가 바뀌지 않도록 모든 변수를 final로 선언하면 된다.

레퍼런스

http://www.tcpschool.com/java/java_thread_concept
https://blog.naver.com/jdub7138/220936452048
https://ko.wikipedia.org/wiki/%EB%A9%80%ED%8B%B0%EC%8A%A4%EB%A0%88%EB%94%A9
https://webcache.googleusercontent.com/search?q=cache:dRhr1EFFiLIJ:https://imbf.github.io/computer-science(cs)/2020/10/18/CPU-Scheduling.html&cd=4&hl=ko&ct=clnk&gl=kr
https://deveric.tistory.com/104

profile
gotta go fast

0개의 댓글