쓰레드 (Thread) - 구현

박영준·2023년 8월 18일
0

OS (운영체제)

목록 보기
6/6

1. 쓰레드 구현

  • Thread 클래스를 상속받을 경우, 다른 클래스를 상속받을 수 X

    • 따라서, Runnable 인터페이스를 구현하는 것이 일반적인 방법
  • "쓰레드를 구현하는 것"
    = 단지 쓰레드를 통해 run() 메서드의 { } 몸통안에 작업할 내용을 채우는 것일 뿐이다.

방법 1 : Thread 클래스를 상속받기

EX 클래스

class EX {
	public static void main(String args[]) {
    	ThreadEx t = new ThreadEx();		// 쓰레드 생성 : Thread의 자식 클래스의 인스턴스 생성
        
        t.start();			// 쓰레드 실행
    }
}

ThreadEx 클래스

class ThreadEx extends Thread {
	public void run() {				// Thread 클래스의 run() 메소드를 오버라이딩
    	for (int i = 0; i < 5; i++) {
        	System.out.println(getName());		// 부모인 Thread의 getName() 메서드 호출
        }
    }
}
  • getName() : 쓰레드의 이름을 반환

방법 2 : Runnable 인터페이스를 구현하기

EX 클래스

class EX {
	public static void main(String args[]) {
    	Runnable r = new ThreadEx();		// Runnable을 구현한 클래스의 인스턴스 생성
        Thread t = new Thread(r);			// 생성자 Thread : 위에서 생성한 인스턴스를 Thread 클래스의 생성자의 매개변수로 둔다
        
        /* 위 두 줄을 하나로 줄일 수 있다.
        Thread t = new Thread(new ThreadEx());
        */
        
        t.start();			// 쓰레드 실행
    }
}

ThreadEx 클래스

class ThreadEx implements Runnable {
	public void run() {					// Runnable 인터페이스의 run() 메소드를 구현
    	for (int i = 0; i < 5; i++) {
        	System.out.println(Thread.currentThread().getName());
        }
    }
}
  • currentThread() : 현재 실행중인 쓰레드의 참조를 반환

2. 쓰레드 실행

(1) start()

  • start() 를 호출하면, 쓰레드가 실행된다.

    • 실행대기 중 쓰레드가 없을 경우

      • 헤당 쓰레드는 바로 실행된다.
    • 실행대기 중 쓰레드가 있을 경우

      • 호출된다고 바로 실행되진 않고, 실행대기 상태가 된다.
      • 자신의 상태가 되면, 그때서야 실행된다.

(2) start() vs run()

run()

  • run()을 호출하는 것은
    • 생성된 쓰레드를 실행하는 것 X
    • 단순히 클래스에 선언된 메서드를 호출하는 것 O

start()

3. 쓰레드 종료

// 잘못된  방법
ThreadEx t = new ThreadEx();
t.start();
...
t.start();		// IllegalThreadStateException 발생

// 올바른 방법
ThreadEx t = new ThreadEx();
t.start();

t = new ThreadEx();		// 쓰레드를 다시 생성
t.start();				// start() 다시 호출
  • 한 번 실행되고 종료된 쓰레드는 다시 실행 X

    • 하나의 쓰레드에 대해 start()는 한 번만 호출 가능하다.
  • 쓰레드의 작업을 한번더 해야 할 경우

    • 다시 새로운 쓰레드 생성 후, start()를 호출해야 한다
  • 하나의 쓰레드에 대해 start() 를 두 번 호출하면, 예외 발생

4. 우선순위

1) 정의

  • 우선순위(작업 중요도)의 값에 따라, 쓰레드가 얻는 실행시간이 달라진다.

    • 작업 중요도가 높은 쓰레드에 더 많은 작업 시간을 갖도록 한다.
  • 예시1 : 메신저에서 파일 전송할 경우

    • 채팅 내용 전달하는 쓰레드의 우선순위 > 파일 다운로드를 처리하는 쓰레드의 우선순위
      • 다운로드 중에도 채팅에 불편함이 없음
      • 대신, 다운로드에 걸리는 시간은 더 길어질 것임
  • 예시 2

    • 실행시간 : 우선순위가 높은 t1 쓰레드 多 > 우선순위가 낲은 t2 쓰레드 小
      • 따라서, A 작업이 B 작업 보다 더 빨리 완료될 수 있다. (단, 모든 경우에서 그런 것은 아님)

2) 사용법

메서드

void setPriority(int newPriority) : 쓰레드의 우선순위를 지정한 값으로 변경

int getPriority() : 쓰레드의 우선순위를 반환

상수

public static fianl int MAX_PRIORITY = 10 : 최대 우선순위

public static fianl int NORM_PRIORITY = 5 : 보통 우선순위

public static fianl int MIN_PRIORITY = 1 : 최소 우선순위

  • 쓰레드의 우선순위의 범위 : 1 ~ 10

  • 숫자가 높을수록, 우선순위 높다.
    (10 大 <--- 1 小)

3) 예시

ThreadEx1 클래스

class ThreadEx1 extends Thread {
	publid void run() {
    	...
    }
}

ThreadEx2 클래스

class ThreadEx2 extends Thread {
	publid void run() {
    	...
    }
}

메인 클래스

class Ex {
	public static void main(String args[]) {
    	ThreadEx1 th1 = new ThreadEx1();
        ThreadEx2 th2 = new ThreadEx2();
        
        th2.setPriority(7);		// 쓰레드 우선순위 변경
        
        System.out.println(th1.getPriority());		// th1 쓰레드의 우선순위 반환
        System.out.println(th2.getPriority());		// th2 쓰레드의 우선순위 반환
        
        th1.start();		// th1 쓰레드 실행
        th2.start();		// th2 쓰레드 실행
    }
}
  • 주의!

    • 쓰레드 실행 전(start() 전)에만, 우선순위 변경(setPriority())이 가능하다
  • main메서드를 수행하는 쓰레드의 우선순위가 5

    • main메서드 내에서 th1 쓰레드를 생성했으므로, th1 쓰레드의 우선순위는 자동적으로 5
    • main메서드 내에서 th1 쓰레드를 생성했으므로, th2 쓰레드의 우선순위는 자동적으로 5

5. 쓰레드 그룹

1) 정의

  • "그룹" = "폴더" 라고 생각하면 된다

    • 서로 관련된 쓰레드를 다루기 위해 쓰레드를 폴더에 담아둔다
  • 보안상의 이유로 도입된 개념

    • 자신이 속한 쓰레드 그룹 & 하위 쓰레드 그룹 : 변경 가능
    • 다른 쓰레드 그룹 : 변경 불가능
  • 쓰레드 그룹을 지정하지 않고 생성한 경우

    • 자동적으로 자신을 생성한 쓰레드와 같은 쓰레드 그룹에 속하게 된다
  • 에시

    • main 쓰레드 그룹
      • 이 안에는 main메서드를 수행하는 main 쓰레드가 속해있다

2) 사용법

  • Thread의 생성자를 이용해서, 쓰레드를 쓰레드 그룹에 포함시킨다

메서드

(이 외에도 다른 메서드 있음)

ThreadGroup getThreadGroup() : 자기 쓰레드가 쓰레드 그룹을 반환

void uncaughtException(Thread t, Throwable e)
: 처리되지 않은 예외에 의해 쓰레드 그룹의 쓰레드가 실행 종료됐을 때, JVM에 의해 이 메서드가 자동 호출됨

6. 쓰레드 상태

(1) 상태

start() 메소드

  • 스레드 객체 생성 후, start() 메소드 호출하면 바로 실행되는 것이 아니라 실행 대기 상태가 된다.
    (실행 대기 상태 : 언제든지 실행할 준비가 되어 있는 상태)

실행 대기 상태

  • 실행 상태의 스레드는 run() 메소드를 모두 실행하기 전에는 다시 실행 대기 상태로 돌아갈 수 있다.
  • run() 메소드를 모두 실행하기 전에 실행 대기 상태의 다른 스레드를 선택하여 실행 상태로 만들기도 한다.

실행 상태 (running)

  • 운영체제(OS)는 실행 대기 상태에 있는 스레드 中 하나를 선택해서 실행 상태로 만든다.

종료 상태 (terminated)

  • run() 메소드가 종료되면, 더이상 실행할 코드가 없으므로 스레드의 실행이 멈추게 되는 상태

(2) 쓰레드 상태 제어

① 정의

동영상을 보다가 일시 정지 or 종료 할 수 있다.

이렇게 실행 중인 스레드의 상태를 변경하는 것을 '스레드 상태 제어'라고 한다.

② 메소드

sleep(long millis)

// 3초 동안 일시 정지 상태로 보내고, 3초 후 다시 실행 준비 상태로 돌아온다
public class SleepExample {
	public static void main(String[] args) {
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		for(int i=0; i<10; i++) {
        toolkit.beep();
        	try {
        		Thread.sleep(3000);		// 3초 동안 메인 스레드를 일시 정지 상태로 만든다
        	} catch(Interruptedexception e) {}		// 예외 처리
        }
	}
}
  • 주어진 시간 동안 스레드를 일시 정지 상태로 만든다.
  • 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다.
  • 밀리세컨드(1/1000초) 단위로 시간을 줄 수 있다.
  • 일시 정지 상태 中 interrupt() 메소드 호출할 경우, Interruptedexception 이 발생하므로 예외 처리가 필요

interrupt()

// 예시 1 (위의 코드와 동일)
public class SleepExample {
	public static void main(String[] args) {
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		for(int i=0; i<10; i++) {
        toolkit.beep();
        	try {
        		Thread.sleep(3000);		// 3초 동안 메인 스레드를 일시 정지 상태로 만든다
        	} catch(Interruptedexception e) {}		// 예외 처리
            
            thread.interrupt();		// 스레드를 종료하기 위해, interruptedException 을 발생시킴
        }
	}
}
public class PrintThread2 extends Thread {
	public void run() {
    	while(true) {
     		System.out.println("실행 중");
            if (Thread.interrupted()) {		// interrupted() 를 이용해서, PrintThread 의 interrupt()가 호출되었는지 확인
            	break;
            }
        }
        
        // while문을 빠져나옴
        System.out.println("자원 정리");
        System.out.println("실행 종료");
  	}
}
  • 일시 정지 상태의 스레드에서 InteruptedException 을 발생시켜,
    예외 처리 코드(catch)에서 실행 대기 상태/종료 상태로 갈 수 있도록 한다.
  • 둘 中 어느것을 사용해도 무방
    • interrupted()
      • 정적 메소드
      • 현재 스레드가 interrupted되었는지 확인
    • isInterrupted()
      • 인스턴스 메소드
      • 현재 스레드가 interrupted되었는지 확인

stop()

public class PrintThread1 extends Thread {
	private boolean stop;
    
    public void setStop(boolean stop) {
    	this.stop = stop;
    }
    
    public void run() {
    	while (!stop) {
        	System.out.println("실행 중");		// while문의 조건식(!stop)이 true 일 경우
        }
        
        System.out.println("자원 정리");		// while문의 조건식(!stop)이 false 일 경우, while문을 빠져나오게 된다
        System.out.println("실행 종료");
    }
}
  • 쓰레드를 족시 종료한다.
    → 불안전한 종료를 유발하므로, 사용하지 않을 것을 권장
    → run() 메소드 종료를 유도하면, 정상적으로 종료가 가능하다.

7. ThreadLocal

(1) 정의

  • 자바의 class
  • ThreadLocal class 는 오직 한 스레드에 의해서 읽고 쓰여질 수 있는 변수
  • 두 스레드가 같은 코드를 실행하고 이 코드가 하나의 ThreadLocal 변수를 참조 하더라도, 서로의 ThreadLocal 변수를 볼 수 없다.

(2) 활용

한 스레드에서 실행되는 코드가 동일한 객체를 사용할 수 있도록 해주기 때문에
관련된 코드에서 파라미터를 사용하지 않고, 객체를 각자가 가져다 쓸 때 사용된다.

  • 사용자 인증 정보 Spring Security 에서 사용자마다 다른 인증 정보 & Session정보를 사용할때
  • 쓰레드에 안전해야 하는 데이터를 보관 할때

(3) 주의 사항

Thread pool 환경에서 ThreadLocal을 사용하는 경우, 변수에 보관된 데이터 사용이 끝나면 반드시 해당 데이터를 삭제해야한다.
그렇지 않으면, 재사용 되는 스레드가 올바르지 않은 데이터를 참조할 가능성
(= 메모리 누수)

8. 쓰레드 종류

1) 사용자 쓰레드 (논 데몬 쓰레드)

2) 데몬 쓰레드

(1) 정의

  • 일반 쓰레드의 작업을 돕는 보조 역할을 수행하는 쓰레드
    (일반 쓰레드 : 데몬 쓰레드가 아닌 쓰레드)

  • 일반 쓰레드가 종료되면, 데몬 쓰레드도 강제 종료된다.
    (데몬 쓰레드가 일반 쓰레드를 보조하는 역할이기 때문)

  • 일반 쓰레드와 데몬 쓰레드 공통점

    • 작성 방법
    • 실행 방법
  • 일반 쓰레드와 데몬 쓰레드 차이점

    • 실행 전, setDemon(true)를 호출
    • 데몬 쓰레드가 생성한 쓰레드는 자동으로 데몬 쓰레드가 됨
  • 예시

    • 가비지 컬렉터
    • 워드프로세서의 자동 저장
    • 화면 자동 갱신

(2) 사용법

메서드

boolean isDaemon() : 해당 쓰레드가 데몬 쓰레드인지 확인 (true/false)

void setDaemon(boolean on)
: 해당 쓰레드를 데몬 쓰레드/사용자 쓰레드로 변경
: 매개변수 on의 값을 true로 지정할 경우, 데몬 쓰레드로 변경됨

(3) 예시

class Ex implements Runnable {
	static boolean autoSave = false;
    
    public static void main(String args[]) {
    	Thread t = new Thread(new Ex());
        t.setDaemon(true);		// 데몬 쓰레드로 지정 -> 이 부분이 없으면, 절대 종료되지 않음
        t.start();		// 쓰레드 실행
        
        for (...) {
        	...
        }    
        ...
    }
}
  • 주의!
    • 쓰레드 실행 전(start() 전)에만, 데몬 쓰레드로 변경(setDaemon(true))이 가능하다

9. 싱글쓰레드

1) 정의

  • 파일 다운로드 받는 중 + 메신저로 채팅 X

    • 사용자의 요청마다 새로운 프로세스를 생성
      • 프로세스 생성 : (쓰레드 생성에 비해) 시간多 메모리 공간多 필요
  • 메인 스레드가 종료될 시, 싱글 스레드의 프로세스도 같이 종료된다.

  • 멀티 스레드 어플리케이션에서 실행 중인 스레드가 하나라도 있을 경우, 싱글 스레드의 프로세스는 종료되지 않는다.

  • (멀티 쓰레드에서와 달리) main 쓰레드가 종료되면 프로세스도 종료된다.

10. 멀티쓰레드 (Multi Thread)

1) 정의

  • 파일 다운로드 받으면서 + 메신저로 채팅하면서 + 음성 대화를 나눌 수 있다.
    • 하나의 서버 프로세스가 여러 개의 쓰레드를 생성해서,
      쓰레드와 사용자의 요청이 일대일로 처리되도록 프로그래밍

메신져 프로세스 같은 경우, 채팅 기능을 제공하면서 동시에 파일 업로드 기능을 수행할 수 있다.

이렇게 한 프로세스에서 멀티 태스킹(두 가지 이상의 작업을 동시에 처리하는 것)이 가능한 이유는 멀티 스레드 덕분이다.

즉, 멀티태스킹을 하는 방식 中 한 코어에서 여러 스레드를 이용해서 번갈아 작업을 처리하는 방식

2) 장단점

장점

  1. CPU 사용률 향상

  2. 자원을 보다 효율적으로 사용

    • 공유하는 영역이 많아, 프로세스방식 보다 context switcing(작업전환) 오버헤드가 작아져서 메모리 리소스가 상대적으로 적다.
  3. 사용자에 대한 응답성 향상

  4. 작업이 분리되어, 코드 간결함

단점

  1. 동시성(concurrency) 이슈

    • 여러 스레드가 동시에 하나의 자원을 공유하고 있기 때문에, 같은 자원을 두고 경쟁 상태(raceCondition) 같은 문제가 발생
      • 즉, 하나만 있는 인스턴스의 필드에 여러 쓰레드가 동시에 접근하면서 동시성 문제가 발생
  2. 교착상태(deadlock)

3) 구성

(1) main 쓰레드

① 정의

  • 우리가 사용하던 main메서드 또한 쓰레드다.

    • 모든 자바 애플리케이션은 메인 스레드가 main() 메소드를 실행하면서 시작된다.
    • main 쓰레드 흐름 안에서 멀티 스레드 어플리케이션은 필요에 따라 작업 쓰레드를 만들어 병렬로 코드를 실행할 수 있다.
  • 실행 중인 사용자 쓰레드가 하나도 없을 경우에만, 프로그램이 종료됨

    1. main메서드 수행 마쳤으나, 다른 쓰레드가 아직 작업 마치지 않음
    2. 프로그램 종료되지 않음

② 생성 방법

public static void main(String[] args) {
	String data = null;
    if (...) {
    }
    
    while (...) {
    }
    
    System.out.println("...");
}

main() 메소드를 실행하면서 메인 스레드가 시작되고,
main() 메소드의 마지막 코드를 실행 or return문을 만날 경우, 실행이 종료된다.

③ 이름

문법

  1. 이름 설정하는 방법
    1) 기본 이름

    • 메인 스레드 : 'main' 라는 이름을 가진다.
    • 우리가 직접 생성한 스레드 : 'Thread-n' 이라는 이름을 가진다. (n 은 스레드의 번호)

    2) Thread-n 대신, 이름을 직접 설정하고 싶을 경우

    threa.setName("스레드 이름");
  2. 스레드의 이름을 알고 싶을 경우

    thread.getName();

예시
ThreadA

public class ThreadA extends Thread {
	public ThreadA() {
    	setName("ThreadA");		// 스레드의 이름 설정
    }
    
    public void run() {
    	// ThreadA 실행 내용
    	for(int i=0; i<2; i++) {
        	System.out.println(getName() + "가 출력한 내용");		// 스레드의 이름 얻기
        }
    }
}
  • setName("ThreadA") 로 스레드의 이름 직접 설정

ThreadB

public class ThreadB extends Thread {
    public void run() {
    	// ThreadB 실행 내용
    	for(int i=0; i<2; i++) {
        	System.out.println(getName() + "가 출력한 내용");		// 스레드의 이름 얻기
        }
    }
}
  • 스레드의 이름을 직접 설정해주지 않음
    → 따라서, 디폴트로 Thread-1 이라는 이름이 설정되어 있음

메인 스레드 이름 출력, UserThread 생성/시작

public class ThreadNameExample {
	public static void main(String[] args) {
    	Thread mnainThread = Thread.currentThread();		// 이 코드를 실행하는 스레드 객체 얻기
    	Systelm.out.println("프로그램 시작 스레드 이름: " + mainThread.getName();		// 스레드의 이름 얻기
        
        ThreadA threadA = new ThreadA();		// ThreadA 생성
        System.out.println("작업 스레드 이름: " + ThreadA.getName());		// 스레드의 이름 얻기
        threadA.start();		// ThreadA 시작 (ThreadA 내부에서 getName() 메소드를 출력하게 됨)
        
        ThreadB threadB = new ThreadB();		// ThreadB 생성
        System.out.println("작업 스레드 이름: " + ThreadB.getName());		// 스레드의 이름 얻기
        threadB.start();		// ThreadB 시작 (ThreadB 내부에서 getName() 메소드를 출력하게 됨)
	}
}

/* 실행 결과
프로그램 시작 스레드 이름: main		// ThreadNameExample 클래스
작업 스레드 이름: ThreadA		// ThreadNameExample 클래스
ThreadA가 출력한 내용		// ThreadNameExample 클래스
ThreadA가 출력한 내용 	// ThreadA 클래스 
작업 스레드 이름: Thread-1		// ThreadNameExample 클래스
Thread-1가 출력한 내용	// ThreadNameExample 클래스
Thread-1가 출력한 내용 	// threadB 클래스 */

스레드 참조
setName(), getName() 은 Thread 클래스의 인스턴스 메소드이므로, 스레드 객체의 참조가 필요하다.

그러나 만약, 스레드 객체의 참조를 가지고 있지 않다면
Thread 클래스의 정적 메소드인 currentThread() 를 이용해서, 현재 스레드의 참조를 얻을 수 있다.

(2) 작업 쓰레드

① 정의

어떤 자바 애플리케이션이든 메인 스레드는 반드시 존재하므로,
메인 작업 외의 추가적인 작업의 수 만큼 스레드를 생성하면 된다.

또한, 자바에서는 작업 스레드도 객체로 생성되기 때문에 클래스가 필요하다.

② 설계

멀티 스레드로 실행하는 어플리케이션 개발을 위해서는
몇 개의 작업을 병렬로 실행할지 결정하고, 각 작업별로 스레드를 생성한다.

③ 생성 방법 (2가지)

  1. Thread 클래스로부터 직접 생성

    • java.lang.Thread 클래스로부터 작업 스레드 객체를 직접 생성하는 방법
    • new를 통해 Thread 클래스 객체 생성 후, start 메서드를 통해 다른 스레드에서 할 작업을 할당하는 방법

    1) Runnable 인터페이스 타입의 매개값을 갖는 생성자를 호출해야 한다

    Thread thread = new Thread(Runnable target);

    2) Runnable 에는 run() 메소드 하나가 정의되어 있는데,
    구현 클래스는 run()을 재정의 해서, 작업 스레드가 실행할 코드를 작성해야 한다.

    public class Task implements Runnable{
        @Override
        public void run() {
        // 여기에 작업 스레드가 실행할 코드 작성
        }
    }

    3-1) Runnable 구현 객체(task)를 생성 후,
    이것을 매개값으로 해서 Thread 생성자를 호출해야 비로소 작업 스레드가 생성된다.

    public class MultiThread {
        Runnable task = new Task();
        Thread thread = new Thread(task); // 구현 객체를 매개값으로 해서 Thread 생성자를 호출 -> 작업 스레드 생성.
    }

    3-2) 3-1 방법 보다는, Thread 생성자를 호출할 때
    Runnable 익명 객체를 매개값으로 사용하는 방법을 더 많이 사용한다.(코드 절약)

    4) 이렇게 작업 스레드를 생성 후, start() 메소드를 호출해야만 비로소 실행된다.
    (작업 스레드는 생성되는 즉시 실행되는 것이 아니다.)

    thread.start();

    5) start()메소드가 호출되면,
    작업 스레드는 매개값으로 받은 Runnable의 run() 메소드를 실행하면서 자신의 작업을 처리한다.

    Runnable

    • 작업 스레드가 실행할 수 있는 코드를 가지고 있는 객체 라고 해서 붙여진 이름
    • 인터페이스 타입이기 때문에, 구현 객체를 만들어서 대입해야 한다.
    • Runnable은 작업 내용을 가지고 있는 객체이지, 실제 스레드는 아니다.
  2. Thread 하위 클래스로부터 생성

    • 작업 스레드가 실행할 작업을 Runnable 로 만들지 않고,
      Thread 의 하위 클래스로 작업 스레드를 정의하면서 작업 내용을 포함시키는 방법

    1-1) Thread 클래스를 상속한 후 , run() 메소드를 재정의해서 스레드가 실행할 코드를 작성.

    public class WorkerThread extends Thread{
    
    	  // run() 메소드 재정의
        @Override
        public void run() {
        	스레드가 실행할 코드;
        }
    }
    
    Thread thread = new WorkerThread();

    1-2) Thread 익명 객체로 작업 스레드 객체를 생성할 수도 있다.(코드 절약)

    2) 이렇게 생성된 작업 스레드 객체에서 start() 메소드를 호출하면,
    작업 스레드는 자신의 run() 메소드를 실행하게 된다.

방법1 VS 방법2

  • 방법1
    • Runnable 인터페이스의 구현 객체를 만들 때, 우선 클래스를 작성하고 implements Runnable 해준 다음 Runnable 에 있는 run() 메소드를 클래스에서 재정의.
    • 작업 스레드가 매개값으로 받은 Runnable의 run() 메소드를 실행하면서 자신의 작업을 처리.
  • 방법2 : extends Thread 를 한 다음 run() 메소드를 재정의

4) 동시성 문제

1) 정의

  • 멀티 스레드 프로그램에서 스레드들이 객체를 고유해서 작업할 경우,
    스레드A가 사용하던 객체를 스레드B가 A의 상태를 변경할 수 있기 때문에
    스레드A가 의도했던 것과는 다른 결과를 산출할 수도 있다.

  • '공유 객체 사용의 문제' 이다.

2) 예시 : 여러 사람이 하나의 계산기를 나눠 사용하는 상황**

사람A가 계산기로 작업하다가 계산 결과를 메모리에 저장한 뒤 잠시 자리를 비운다.
이때 사람B가 계산기를 만져서 사람A가 메모리에 저장한 값을 다른 값으로 변경해버린다.
사람A가 다시 돌아와 계산기에 저장된 값을 이용해서 이후 작업을 진행하게 된다.

3) 동시성 제어 방법

스레드가 사용 중인 객체를 다른 스레드가 변경할 수 없게 하려면,
스레드 작업이 끝날 때까지 해당 객체에 잠금(Lock 락)을 걸어서, 다른 스레드가 해당 객체를 사용할 수 없게 만들면 된다.

  1. 암시적 Lock (synchronized)
    1) 동시성 해결에 가장 간단하면서 쉬운 방법
    2) 문제가 된 메서드, 변수에 각각 synchronized 키워드를 넣는다
    3) Java 는 임계 영역(critical section, 멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역)을 지정하기 위해,
    동기화 메소드를 제공한다.
    4) 스레드가 객체 내부의 동기화 메소드를 실행할 경우, 즉시 객체에 잠금을 걸어 다른 스레드가 동기화 메소드를 실행하지 못하게 한다.
    5) synchronized 키워드를 붙이면, 동기화 메소드를 만들 수 있다.

    // 예시 1
    public class Calculator {
        private int memory;
    
        public int getMemory() {
            return memory;
        }
    
        // synchronized 키워드로 인해, Calculator 객체가 잠금 처리됨
        public synchronized void setMemory(int memory) {
            this.memory = memory;
            try {
                Thread.sleep(2000);
            } catch(InterruptedException e) {}
            System.out.println(Thread. currentThread().getName() + this.memory);
        }
    }
    
    /* 
    User1 스레드는 Calculaor 객체의 동기화 메소드인 setMemory()를 실행하는 순간 Calculator 객체를 잡금 처리한다.
    메인 스레드가 User2 스레드를 실행하지만, 동기화 메소드인 seMemory()를 실행하지는 못하고 User1 이 setMemor()를 모두 실행할 동안 대기해야 한다.
    Userl 스레드가 setMemory() 메소드를 모두 실행하고 나면, User2 스레드가 setMemory()메소드를 실행할 수 있게 된다.
    */
    // 예시 2
    class Count {
        private int count;
        public synchronized int view() {	 // 문제가 된 메서드
        	return count++;
        }
    }
    
    class Count {
        private Integer count = 0;
        public int view() {
            synchronized (this.count) {	    // 문제가 된 변수
                return count++;
            }
        }
    }
  2. 명시적 Lock
    1) synchronized 키워드 없이, 명시적으로 ReentrantLock 키워드 를 사용하는 방법
    2) when? 해당 Lock의 범위를 메서드 내부에서 한정하기 어렵거나 or 동시에 여러 Lock을 사용하고 싶을 때
    3) 직접적으로 Lock 객체를 생성하여 사용
    (Reentrant: 재진입)

    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 < 1000; j++) {
                            count.getLock().lock();
                            System.out.println(count.view());
                            count.getLock().unlock();
                        }
                    }
                }.start();
            }
        }
    }
    class Count {
        private int count = 0;
        private Lock lock = new ReentrantLock();
        public int view() {
                return count++;
        }
        
        public Lock getLock(){
            return lock;
        };
    }
  3. 스레드 안전한 객체 사용
    1) Concurrent 패키지
    - concurrent 패키지에 존재하는 컬랙션들은 락을 사용할 때 발생하는 성능 저하를 최소한으로 만든다.
    - Lock Striping 기법(락을 여러 개로 분할하여 사용)을 사용하여,
    동시에 여러 스레드가 하나의 자원에 접근하더라도, 동시성 이슈가 발생하지 않도록 돕는다.

    class Count {
        private AtomicInteger count = new AtomicInteger(0);
        public int view() {
                return count.getAndIncrement();
        }
    }

    2) ConcurrentHashMap
    - 내부적으로 여러개의 락을 가지고 해시값을 이용해, 이러한 락을 분할하여 사용
    - 분할 락을 사용하여, 병렬성과 성능을 모두 잡은 컬랙션
    - 일반적인 map을 사용할 때처럼 구현하면, 내부적으로 알아서 락을 자동으로 사용

    ```java
    int binCount = 0;
           for (Node<K,V>[] tab = table;;) {
               Node<K,V> f; int n, i, fh;
               if (tab == null || (n = tab.length) == 0)
                   tab = initTable();
               else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                   if (casTabAt(tab, i, null,
                                new Node<K,V>(hash, key, value, null)))
                       break;                   // no lock when adding to empty bin
               }
               else if ((fh = f.hash) == MOVED)
                   tab = helpTransfer(tab, f);
               else {
                   V oldVal = null;
                   synchronized (f) {
                       if (tabAt(tab, i) == f) {
  4. 불변 객체 (Immutable Instance)
    1) 스레드 안전한 프로그래밍을 하는 방법 中 효과적인 방법

    2) 예시: String

    3) 불변 객체는 락을 걸 필요가 없는데,
    내부적인 상태가 변하지 않으니 여러 스레드에서 동시에 참조해도 동시성 이슈가 발생하지 않기 때문
    (즉, 불변 객체는 언제나 '스레드 안전(Thread-safe)하다.')

    4) 불변 객체는 생성자로 모든 상태 값을 생성할 때 세팅하고,
    객체의 상태를 변화시킬 수 있는 부분을 모두 제거해야 한다.
    --> 방법 1: 세터(setter)를 만들지 않기
    --> 방법 2: 내부 상태가 변화 없도록 모든 변수를 final로 선언(단, final 을 쓰면 무조건 초기화 해야함)
    --> 방법 3: 데이터 자체를 Stream()안에서 캡슐화해서 결과를 도출하기(함수형 프로그래밍을 사용하는 이유)

스레드 안전성(Thread safe)
여러 스레드가 작동하는 환경에서도 문제 없이 동작하는 것을 '스레드 안전하다'고 말한다.
즉, 동시성 이슈를 해결하고 일어나지 않는다면, 'Thread safe하다'고 하는 것

11. 싱글쓰레드 vs 멀티쓰레드

1) 싱글쓰레드

(1) 정의

  • '하나'의 쓰레드로 처리하는 경우

(2) 작업 방식

  • 하나의 작업 마친 후, 다른 작업 수행

(3) 사용 상황

  • 단순히 CPU만을 사용하는 계산 작업일 경우 (멀티쓰레드 보다 시간 소요 ↓)

(4) I/O 블락킹(blocking)

  • I/O 블락킹(blocking)

    • 쓰레드가 입출력(I/O)처리를 위해 기다리는 것
  • 입력받는 작업과 화면에 출력하는 작업을 하나의 쓰레드로 할 경우, I/0 블락킹이 나타나게 된다.

    • 사용자가 입력을 마치기 전까지는 화면에 숫자가 출력 X
    • 사용자가 입력을 마치고 나서야 화면에 숫자가 출력 O

2) 멀티쓰레드

(1) 정의

  • '두 개'의 쓰레드로 처리하는 경우

(2) 작업 방식

  • 두 개의 쓰레드가 번갈아 가면서 작업 수행 (즉, 동시에 작업 수행)

(3) 사용 상황

  • 외부 기기와의 입출력이 필요한 작업일 경우

    • 사용자로부터 데이터 입력받는 작업
    • 네트워크로 파일 주고받는 작업
    • 프린터로 파일 출력하는 작업

    즉, 두 쓰레드가 서로 다른 자원을 사용하는 경우

(4) (싱글쓰레드에 비해) 작업 수행에 시간 소요 多 이유?

  1. 쓰레드 간의 작업 전환 시간

    • 두 개의 쓰레드가 번걸아 가면서 작업을 수행하기 때문
  2. 대기 시간

    • 한 쓰레드가 화면에 출력하는 동안,
      다른 쓰레드는 출력이 끝나는 것을 기다리기 때문

(5) 싱글 코어 vs 멀티 코어

싱글 코어

  • 하나의 코어가 번갈아 가면서 작업 수행
    • 따라서, 두 작업은 겹치지 않음

멀티 코어

  • 동시에 두 쓰레드의 작업 수행
    • 작업이 겹치는 부분이 발생 → 하나의 자원을 두고, 두 쓰레드가 경쟁하게 됨

(6) I/O 블락킹(blocking)

  • 싱글쓰레드와 달리, I/0 블락킹이 나타나지 않는다.
    • 사용자가 입력을 마치지 않았어도, 화면이 숫자가 출력 O
profile
개발자로 거듭나기!

1개의 댓글

comment-user-thumbnail
2023년 8월 18일

글 잘 봤습니다.

답글 달기