[OS / JAVA] 프로세스와 스레드의 차이점, 스레드간 메모리 공유와 동기화

hyng·2022년 3월 28일
0

os

목록 보기
1/5

잘못된 내용이 있다면 댓글로 알려주시면 감사하겠습니다. 🙇🏻‍♀️

프로세스와 스레드

  • 프로세스는 자원(코드, 전역변수, heap, 열린 파일, 소켓 등)의 소유주 역할을 합니다.
  • 코드, 전역변수, heap 등의 프로세스의 자원은 스레드의 개수와 상관없이 하나씩만 존재하고, 각 스레드는 이를 공유합니다.
  • 그래서 스레드들이 동시에 공유 자원 데이터에 접근할 경우 조심해야 합니다.
  • 스레드는 스케줄링의 대상이 되며 TCB(Thread Control Block)스택을 가집니다.
  • TCB에는 tid, 우선순위, 문맥 정보 등이 있고 문맥은 PC(Program Counter), SP(Stack Pointer), registers의 정보를 저장합니다.
    그래서 실행 중이던 스레드가 Blocking 되면 스레드의 실행 위치, 레지스터 값 등등은 TCB 문맥에 백업되기 때문에 스레드가 Blocking 상태에서 다시 Running 상태가 되더라도 안전하게 재실행될 수 있습니다.
  • 스레드 별로 스택을 별도로 가지기 때문에 함수 내 같은 지역변수를 두 스레드가 동시에 바꿔도 데이터가 깨지지 않습니다.
  • 반면 전역변수, heap 등 프로세스의 자원은 함께 공유하기 때문에 여러 스레드에서 하나의 데이터에 동시에 접근할 경우 race condition문제가 발생할 수 있습니다.

스레드간 메모리 공유

  • 위에서 스레드는 전역변수, heap 등의 프로세스 자원은 하나씩만 두고 서로 공유한다고 설명했습니다.
  • 그래서 공유 자원에 대해 여러 스레드가 동시에 접근하는 것을 조심해야 합니다.
  • 멀티스레딩 환경에서 나타낼 수 있는 문제로는 race condition이 있습니다.
  • race conditon은 스레드의 실행 순서에 따라 결과가 달라지는 문제를 말합니다.
  • race condition이 발생하는 상황을producer consumer process 가 존재할 때를 예시로 들어보겠습니다.
  • 아래와 같이 producer, consumer process를 가지는 실행 코드가 있습니다.
    Producer
R1 = counter;
R1 = R1 + 1;
counter = R1;

Consumer

R2 = counter;
R2 = R2 - 1;
counter = R2;
  • 두 개의 스레드를 생성하여 스레드A는 Producer 코드를, 스레드B는 Consumer 코드를 실행한다고 가정하겠습니다.
  • CPU Switch로 인해 코드가 실행되는 경우의 수는 여러 가지가 있습니다.
  • 스레드A가 R1 += 1까지 실행되고 스레드B가 끝까지 실행
  • 스레드A가 끝까지 실행되고 스레드B 실행
  • 등등
  • 즉 어느 스레드가 먼저 count에 값을 저장하냐에 따라 결과가 5 또는 6으로 달라지는 것을 확인할 수 있습니다.

동기화

  • 데이터의 신뢰성을 보장하기 위해서는 스레드 간 동기화 문제를 해결해야 합니다.
  • 이 문제를 해결하기 위해서 상호배제(각 스레드는 공유 자원의 손상을 방지하기 위해 혼자서만 공유자원을 사용해야 함, 즉 어떤 스레드가 임계 영역(공유 자원(공유 데이터, 공동으로 사용하는 I/O 디바이스들)를 읽거나 쓰는 코드영역)을 실행하고 있는 도중 다른 스레드가 동일한 임계 영역을 실행하게 해서는 안 됨) 하여야 합니다.
  • 즉 상호배제는 공유 자원에 접근하는 임계 영역을 lock 하고 다 사용한 다음 다른 스레드가 접근할 수 있도록 unlock 하는 것입니다.
  • java에서는 lock, unlock을 synchronized 키워드를 사용해서 처리할 수 있습니다.

synchronized

다음과 같은 두 가지의 방법으로 동기화를 할 수 있습니다.

  • synchronized 메서드를 사용하는 방법
  • synchronized 블록을 사용하는 방법



1. synchronized 메서드를 사용하는 방법

  • 인스턴스에 lock을 걸기 때문에 동일한 인스턴스를 통해서 메서드에 접근할 경우엔 하나의 스레드만 접근 가능합니다.
    • 따라서 아래 코드 실행 결과 "synchronization broken"은 출력되지 않습니다.

코드 참조

public class Main {
    private String mHero;
    public static void main(String[] agrs) {
        Main main1 = new Main();
        Main main2 = new Main();
        System.out.println("Test start!");
        new Thread(() -> {
            for (int i = 0; i<1000000; i++) {
                main1.batman();}
        }).start();
        new Thread(() -> {
            for (int i = 0; i<1000000; i++) {
                main2.batman();
            }
        }).start();
        System.out.println("Test end!");
    }
    public synchronized void batman() {
        mHero= "batman";

        try {
            long sleep = (long) (Math.random()*100);
            Thread.sleep(sleep);
            if ("batman".equals(mHero) == false) {
                System.out.println("synchronization broken");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    } public void superman() {
        mHero = "superman";

        try {
            long sleep = (long) (Math.random()*100);
            Thread.sleep(sleep);
            if ("superman".equals(mHero) == false) {
                System.out.println("synchronization broken");
            }
        } catch (InterruptedException e) {
            e.printStackTrace(); }
    }
}
  • 인스턴스 자체를 lock 하기 때문에 synchronized키워드가 없는 다른 메서드를 호출해도 "synchronization broken"가 출력되지 않습니다.
public class Main {
    private String mHero;
    public static void main(String[] agrs) {
        Main main = new Main();
        System.out.println("Test start!");
        new Thread(() -> {
            for (int i = 0; i<1000000; i++) {
                main.superman();}
        }).start();
        new Thread(() -> {
            for (int i = 0; i<1000000; i++) {
                main.superman();
            }
        }).start();
        System.out.println("Test end!");
    }
    public synchronized void batman() {
        mHero= "batman";

        try {
            long sleep = (long) (Math.random()*100);
            Thread.sleep(sleep);
            if ("batman".equals(mHero) == false) {
                System.out.println("synchronization broken");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    } public void superman() {
        mHero = "superman";

        try {
            long sleep = (long) (Math.random()*100);
            Thread.sleep(sleep);
            if ("superman".equals(mHero) == false) {
                System.out.println("synchronization broken");
            }
        } catch (InterruptedException e) {
            e.printStackTrace(); }
    }
}
  • static키워드와 함께 사용한다면 클래스에 lock 합니다.
  • 코드 실행 결과 "synchronization broken"은 출력되지 않습니다.
public class Main {
    private static String mHero;
    public static void main(String[] agrs) {
        System.out.println("Test start!");
        new Thread(() -> {
            for (int i = 0; i<1000000; i++) {
                Main.batman();}
        }).start();
        new Thread(() -> {
            for (int i = 0; i<1000000; i++) {
                Main.batman();
            }
        }).start();
        System.out.println("Test end!");
    }
    public static synchronized void batman() {
        mHero= "batman";

        try {
            long sleep = (long) (Math.random()*100);
            Thread.sleep(sleep);
            if ("batman".equals(mHero) == false) {
                System.out.println("synchronization broken");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    } public void superman() {
        mHero = "superman";

        try {
            long sleep = (long) (Math.random()*100);
            Thread.sleep(sleep);
            if ("superman".equals(mHero) == false) {
                System.out.println("synchronization broken");
            }
        } catch (InterruptedException e) {
            e.printStackTrace(); }
    }
}
  • 물론 클래스 자체를 lock하기 때문에 synchrozied키워드가 없는 클래스 내 static 메서드를 호출해도 실행 결과 "synchronization broken"은 출력되지 않습니다.
public class Main {
    private static String mHero;
    public static void main(String[] agrs) {
        System.out.println("Test start!");
        new Thread(() -> {
            for (int i = 0; i<1000000; i++) {
                Main.superman();}
        }).start();
        new Thread(() -> {
            for (int i = 0; i<1000000; i++) {
                Main.superman();
            }
        }).start();
        System.out.println("Test end!");
    }
    public static synchronized void batman() {
        mHero= "batman";

        try {
            long sleep = (long) (Math.random()*100);
            Thread.sleep(sleep);
            if ("batman".equals(mHero) == false) {
                System.out.println("synchronization broken");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    } public static void superman() {
        mHero = "superman";

        try {
            long sleep = (long) (Math.random()*100);
            Thread.sleep(sleep);
            if ("superman".equals(mHero) == false) {
                System.out.println("synchronization broken");
            }
        } catch (InterruptedException e) {
            e.printStackTrace(); }
    }
}
  • 이 방법은 인스턴스 또는 클래스 자체를 무식하게 lock하는 방법입니다.

2. synchronized 블록을 사용하는 방법

  • 필요한 부분에만 lock 하는 방법
  • synchronized 파라미터로 lock이 걸려야 하는 부분을 전달해 줄 수 있습니다.
  • public static synchrozied void methodA(){ ... }
    과 public static void methodA() {synchronized(TargetClass.class){...}} 동일
  • public synchronozied void methodA(){...}는
    public void methodA() {synchronized(this){...}}과 동일

코드 참조

public class Main {
    private Map<String, String> map1 = new HashMap<>();
    private Map<String, String> map2 = new HashMap<>();

    public static void main(String[] agrs) {
        Main main1 = new Main();
        System.out.println("Test start!");
        new Thread(() -> {
            for (int i = 0; i < 1; i++) {
                main1.batman("asdf");
            }
        }).start();
        new Thread(() -> {
                for (int i = 0; i < 1; i++) {
                main1.superman("asdfa");
            }
        }).start();
        System.out.println("Test end!");
    }

    public void batman(String s) {
        synchronized (map1){
            map1.put("s", s);
        }
    }

    public void superman(String s) {
        synchronized (map2) {
            map2.put("sb", s);

        }
    }

}


static

  • static은 java에서 메서드, 필드 등에 붙일 수 있는 키워드입니다.
  • static은 스레드끼리 공유되는 자원으로, 클래스당 하나만 생성됩니다.
  • jvm 내에서 staticheap 영역이 아닌 클래스 코드가 저장되는 static공간에 메모리가 할당됩니다. static 공간은 GC가 관리하지 않는 영역이기 때문에 프로그램 종료 시 까지 할당받은 메모리가 유지됩니다.
    참고로 jvm 내에 heap 영역은 클래스를 new 해서 인스턴스를 생성할 경우 사용되는 영역입니다. 이 영역은 GC 가 수시로 관리합니다.

참고

https://ooeunz.tistory.com/110
https://yeonyeon.tistory.com/113
https://tourspace.tistory.com/54?category=788398
https://ohgyun.com/5

profile
공부하고 알게 된 내용을 기록하는 블로그

0개의 댓글