본문 바로가기

STUDY/Java

<자바의 신> 25장 쓰레드는 개발자라면 알아두는 것이 좋아요

25장 쓰레드는 개발자라면 알아두는 것이 좋아요

Thread

  • 하나의 프로세스 내에 여러 쓰레드가 수행
  • Thread = 경량 프로세스
  • 프로세스가 하나 시작하려면 많은 자원이 필요하기 때문에 보다 빠른 처리가 필요할 때 쓰레드를 사용

 

쓰레드 생성

  • 쓰레드가 수행되는 우리가 구현하는 메소드는 run() 메소드
  • 쓰레드를 시작하는 메소드는 start()
  • start() 메소드를 만들지 않아도 알아서 자바에서 run() 메소드를 수행하도록 되어있음

 

Runnable 인터페이스

  • java.lang
리턴 타입 메소드 이름 및 매개변수 설명
void run() 쓰레드가 시작되면 수행되는 메소드
public class RunnableSample implements Runnable{
	public void run(){
		System.out.println("Runnable Sample run() method");
	}
}
public class RunThreads{
	public static void main(String[] args){
		RunThreads threads = new RunThreads();
		threads.runBasic();
	}
	public void runBasic(){
		RunnableSample runnable = new RunnableSample();
		new Thread(runnable).start();
	}
}
  • Thread 클래스 생성자에 해당 객체를 추가하여 start()

 

Thread 클래스

public class ThreadSample extends Thread{
	public void run(){
		System.out.println("Thread Sample run() method");
	}
}
public class RunThreads{
	public static void main(String[] args){
		RunThreads threads = new RunThreads();
		threads.runBasic();
	}
	public void runBasic(){
		RunnableSample runnable = new RunnableSample();
		new Thread(runnable).start();

		ThreadSample thread = new ThreadSample();
		thread.start();
		System.out.println("RunThreads.runBasic() method is ended.");
	}
}
  • ThreadSample 클래스의 객체에 바로 start() 메소드 호출
// 결과 (다른 결과가 나올 수 있음)
Thread Sample run() method
RunThreads.runBasic() method is ended.
Runnable Sample run() method
💡 쓰레드를 start()를 통해 시작하면 프로세스가 아닌 하나의 쓰레드를 JVM에 추가하여 실행하기 때문에 순차적으로 실행되지 않는다

→ 쓰레드 클래스가 다른 클래스를 확장할 필요가 있을 경우: Runnable 인터페이스 구현, 그렇지 않은 경우 Thread 클래스 사용

 

Thread 클래스의 생성자

생성자 설명
Thread() 새로운 쓰레드 생성
Thread(Runnable target) target 객체의 run() 메소드를 수행하는 쓰레드 생성
Thread(Runnable target, String name) target 객체의 run() 메소드를 수행하고, name이라는 이름을 갖는 쓰레드 생성
Thread(String name) name이라는 이름을 갖는 쓰레드 생성
Thread(ThreadGroup group, Runnable target) group의 쓰레드 그룹에 속하는 target 객체의 run() 메소드를 수행하는 쓰레드 생성
Thread(ThreadGroup group, Runnable target, String name) group의 쓰레드 그룹에 속하는 target 객체의 run() 메소드를 수행하고, name이라는 이름을 갖는 쓰레드 생성
Thread(ThreadGroup group, Runnable target, String name, long stackSize) group의 쓰레드 그룹에 속하는 target 객체의 run() 메소드를 수행하고, name이라는 이름을 갖는 쓰레드를 생성. 단 해당 쓰레드의 스택의 크기는 stackSize만큼만 가능
Thread(ThreadGroup group, String name) group의 쓰레드 그룹에 속하는 name이라는 이름을 갖는 쓰레드 생성
  • 쓰레드 이름의 기본값 “Thread-n”
public class NameCalcThread extends Thread{
	private int calcNumber;
	public NameCalcThread(String name, int calcNumber){
		super(name); 
		this.calcNumber=calcNumber;
	}
	public void run(){
		calcNumber++; 
	}
}
  • 쓰레드를 시작할 때 숫자를 넘겨주고 그 숫자를 run() 안에서 계산 → 매개변수 사용

 

sleep()

리턴 타입 메소드 이름 및 매개 변수 설명
static void sleep(long millis) 매개 변수로 넘어온 시간(1/1000초)만큼 대기
static void sleep(long millis, int nanos) millis + nanos 만큼 대기
  • 쓰레드는 run() 메소드가 끝나지 않으면 애플리케이션이 끝나지 않음 (데몬 쓰레드 제외)
public class EndlessThread extends Thread{
	public void run(){
		while(true){
			try{
				System.out.println(System.currentTimeMillis()); // 현재 시간을 밀리초 단위로 출력
				Thread.sleep(1000); // 1초간 대기
			}catch (InterruptedException e){ // sleep을 사용할 땐 try-catch 필수
				e.printStackTrace();
			}
		}
	}
}
  • 1초에 한번씩 현재 시간을 밀리초 단위로 계속 출력
  • main() 메소드의 수행이 끝나더라도 시작된 쓰레드가 종료하지 않으면 자바 프로세스는 끝나지 않음

 

 

Thread 클래스의 주요 메소드

1. 속성을 확인하고 지정하는 메소드

리턴 타입 메소드 이름 및 매개 변수 설명
void run() 쓰레드가 시작되면 수행되는 메소드
long getId() 쓰레드의 고유 id를 리턴 (JVM 자동 생성)
String getName() 쓰레드의 이름을 리턴
void setName(String name) 쓰레드의 이름을 지정
int getPriority() 쓰레드의 우선순위를 확인
void setPriority(int newPriority) 쓰레드의 우선순위를 지정
boolean isDaemon() 쓰레드가 데몬인지 확인
void setDaemon(boolean on) 쓰레드를 데몬으로 설정
StackTraceElement[] getStackTrace() 쓰레드의 스택 정보 확인
Thread.State getState() 쓰레드의 상태 확인
ThreadGroup getThreadGroup() 쓰레드의 그룹 확인
  • 우선순위: 대기하고 있는 상황에서 더 먼저 수행할 수 있는 순위 (기본값 사용 권장)
    • MAX_PRIORITY : 가장 높은 우선 순위 = 10
    • NORM_PRIORITY : 일반 쓰레드의 우선 순위 = 5
    • MIN_PRIORITY : 가장 낮은 우선 순위 = 1

 

데몬 쓰레드

: 어떤 쓰레드를 데몬으로 지정하면 그 쓰레드가 수행되고 있든 말든 상관없이 JVM이 끝날 수 있다

단, 해당 쓰레드가 시작하기(start()) 전에 데몬 쓰레드로 지정되어야 함

→ 해당 쓰레드가 종료되지 않아도 다른 실행중인 일반 쓰레드가 없다면 종료

 

 

Synchronized

  • 어떤 클래스나 메소드가 쓰레드에 안전하려면, synchronized를 사용해야만 한다
  • 메소드에서 인스턴스 변수를 수정하려고 할 때 synchronized 필요

1. synchronized methods : 메소드 자체를 synchronized로 선언하는 방법

public synchronized void plus(int value){
	amount += value;
}
  • 메소드 선언부에 synchronized가 있으면 동일한 객체의 이 메소드에 여러 개의 쓰레드가 접근해도 한 순간에는 하나의 쓰레드만 이 메소드를 수행 가능

 

2. synchronized statements : 메소드 내의 특정 문장만 synchronized로 감싸는 방법

Object lock = new Object(); // 잠금 처리를 하기 위한 객체 선언
public void plus(int value){
	synchronized(lock){
		amount += value;
	}
}
public void minus(int value){
	synchronized(lock){
		amount -= value;
	}
}
  • synchronized() 이후에 있는 중괄호 내에 있는 연산만 동시에 여러 쓰레드에서 처리하지 않겠다는 의미
  • lock: 한 명의 쓰레드만 일을 할 수 있도록 허용하고 쓰레드가 일을 다 처리하고 나오면 다른 쓰레드에게 기회를 주는 역할
💡 메소드를 synchronized 할 때는 같은 객체를 참조할 때만 유효
CommonCalculate calc = new CommonCalculate();
ModifyAmountThread thread1 = new ModifyAmountThread(calc, true);
ModifyAmountThread thread2 = new ModifyAmountThread(calc, true);

// 아래의 경우는 synchronized를 안한 것과 같음
CommonCalculate calc1 = new CommonCalculate();
ModifyAmountThread thread1 = new ModifyAmountThread(calc1, true);

CommonCalculate calc2 = new CommonCalculate();
ModifyAmountThread thread2 = new ModifyAmountThread(calc2, true);
💡 synchronized는 여러 쓰레드에서 하나의 객체에 있는 인스턴스 변수를 동시에 처리할 때 발생할 수 있는 문제를 해결하기 위해 필요한 것

→ 인스턴스 변수가 선언되어 있는 객체를 다른 쓰레드에서 공유할 일이 없으면 synchronized를 쓸 필요가 없음

*StringBuffer는 synchronized 블록으로 주요 데이터 처리 부분을 감싸두었기 때문에 Thread safe

→ 하나의 문자열 객체를 여러 쓰레드에서 공유해야 하는 경우 사용

*StringBuilder는 synchronized가 사용되지 않아 Thread unsafe

→ 여러 쓰레드에서 공유할 일이 없을 때 사용

 

2. 쓰레드를 통제하는 메소드

리턴 타입 메소드 이름 및 매개 변수 설명
Thread.State getState() 쓰레드의 상태 확인
void join() 수행중인 쓰레드가 중지할 때까지 대기
void join(long millis) millis만큼 대기
void join(long millis, int nanos) millis + nanos만큼 대기
void interrupt() 수행중인 쓰레드에 중지 요청
  • Thread.State
상태 의미
NEW 쓰레드 객체는 생성되었지만 시작 전 상태
RUNNABLE 쓰레드가 실행중인 상태
BLOCKED 쓰레드가 실행 중지 상태(모니터 락이 풀리기 기다리는 상태)
WAITING 쓰레드가 대기 중인 상태
TIMED_WAITING 특정 시간만큼 쓰레드가 대기중인 상태
TERMINATED 쓰레드가 종료된 상태

NEW → 상태 → TERMINATED의 라이프 사이클을 가짐

  • join() 해당 쓰레드가 끝날 때까지 대기
thread.join(60000); // 1분 대기
  • interrupt() : InterruptedException을 발생시키며 쓰레드를 중지

 

 

3. 상태 확인을 위한 메소드

리턴 타입 메소드 이름 및 매개 변수 설명
void checkAccess() 현재 수행중인 쓰레드가 해당 쓰레드를 수정할 수 있는 권한인지 확인, 권한이 없다면 SecurityException 예외 발생
boolean isAlive() 해당 쓰레드의 run() 메소드가 종료되었는지 확인
boolean isInterrupted() run() 메소드가 정상적으로 종료되지 않고, interrupt() 메소드 호출을 통해 종료되었는지 확인
static boolean interrupted() 현재 쓰레드가 중지되었는지 확인

 

 

4. static 메소드

  • JVM에서 사용되는 쓰레드의 상태들을 확인하는 메소드
리턴 타입 메소드 이름 및 매개 변수 설명
static int activeCount() 현재 쓰레드가 속한 그룹의 쓰레드 중 살아있는 쓰레드의 개수를 리턴
static Thread currentThread() 현재 수행중인 쓰레드의 객체를 리턴
static void dumpStack() 콘솔 창에 현재 쓰레드의 스택 정보를 출력

 

 

5. Object 클래스에 선언된 쓰레드와 관련있는 메소드

리턴 타입 메소드 이름 및 매개 변수 설명
void wait() 다른 쓰레드가 Object 객체에 대한 notify() 메소드나 notifyAll() 메소드를 호출할 때까지 현재 쓰레드가 대기
void wait(long timeout) 밀리초 단위로 지정한 시간만큼 대기
void wait(long timeout, int nanos) 밀리초+나노초 단위로 지정한 시간만큼 대기
void notify() Object 객체의 모니터에 대기하고 있는 단일 쓰레드를 깨운다
void notifyAll() Object 객체의 모니터에 대기하고 있는 모든 쓰레드를 깨운다
public class StateThread extends Thread{
	private Object monitor; // 인스턴스 변수
	public StateThread(Object monitor){
		this.monitor = monitor;
	}
	
	public void run(){
		try{
			for(int loop = 0; loop < 10000; loop++){
				String a = "A";
			}
			synchronized(monitor){
				monitor.wait(); // 쓰레드 대기 
			}
			System.out.println(getName() + " is notified.");
			Thread.sleep(1000); // wait()이 끝나면 1초간 대기 후 쓰레드 종료
		}catch(InterruptedException ie){
			ie.printStackTrace();
		}
	}
}
public class RunObjectThreads{
	public static void main(String args[]){
		RunObjectThreads sample = new RunObjectThreads();
		sample.checkThreadState();
	}

	public void checkThreadState(){
		Object monitor = new Object();  
		StateThread thread = new StateThread(monitor);
		try{
			System.out.println("thread state = " + thread.getState());
			thread.start(); // thread 객체 생성 후 시작
			System.out.println("after start = " + thread.getState());
			
			Thread.sleep(100);
			System.out.println("after 0.1 sec = " + thread.getState());
		
			synchronized(monitor){
				monitor.notify(); // 쓰레드 깨우기
			}
			Thread.sleep(100);
			System.out.println("after notify = " + thread.getState());
			
				thread.join(); // 쓰레드가 종료될 때까지 기다린 후 상태를 출력
				System.out.println("after join = " + thread.getState());
			}catch (InterruptedException ie){
				ie.printStackTrace();
			}
	}
}
// 결과
thread state = NEW
after start = RUNNABLE
after 0.1 sec = WAITING
Thread-0 is notified
after notify = TIMED_WAITING
after join = TERMINATED

 

 

6. ThreadGroup에서 제공하는 메소드

  • ThreadGroup: 쓰레드의 관리를 용이하게 하기 위한 클래스
  • 쓰레드 그룹은 tree 구조를 가짐
리턴 타입 메소드 이름 및 매개 변수 설명
int activeCount() 실행 중인 쓰레드의 개수를 리턴
int activeGroupCount() 실행 중인 쓰레드 그룹의 개수를 리턴
int enumerate(Thread[] list) 현재 쓰레드 그룹에 있는 모든 쓰레드를 list에 저장
int enumerate(Thread[] list, boolean recurse) 현재 쓰레드 그룹에 있는 모든 쓰레드를 list에 저장하고 true이면 하위에 있는 쓰레드 그룹에 있는 쓰레드 목록도 포함
int enumerate(ThreadGroup[] list) 현재 쓰레드 그룹에 있는 모든 쓰레드 그룹을 list에 저장
int enumerate(ThreadGroup[] list, boolean recurse) 현재 쓰레드 그룹에 있는 모든 쓰레드 그룹을 list에 저장하고 true이면 하위에 있는 쓰레드 그룹 목록도 포함
String getName() 쓰레드 그룹의 이름을 리턴
ThreadGroup getParent() 부모 쓰레드 그룹을 리턴
void list() 쓰레드 그룹의 상세 정보를 출력
void setDaemon(boolean daemon) 지금 쓰레드 그룹에 속한 쓰레드들을 데몬으로 지정

 

 


 

27장 Serializable과 NIO도 살펴 봅시다

Serializable 인터페이스

  • 파일을 읽거나 쓸 수 있도록 하거나, 다른 서버로 보내거나 받을 수 있도록 하려면 Serializable 인터페이스를 구현해야 함
  • Serializable 인터페이스를 구현 후 serialVersionUID 값을 지정해 주는 것을 권장
    • 해당 객체의 버전을 명시하는 데 사용
    • 무조건 static final long serialVersionUID 로 선언해야 함
static final long serialVersionUID = 1L;

 

transient

: transient로 선언한 변수는 Serializable 대상에서 제외

→ 보안상 중요한 변수나 꼭 저장해야 할 필요가 없는 변수에 사용

transient private int bookOrder;

 

NIO

  • 채널과 버퍼를 사용해 속도를 높임
  • ByteBuffer라는 버퍼와 FileChannel이라는 채널을 사용해 파일 데이터를 처리

 

NIO Buffer 클래스

리턴 타입 메소드 설명
int capacity() 버퍼에 담을 수 있는 크기를 리턴
int limit() 버퍼에서 읽거나 쓸 수 없는 첫 위치 리턴
int position() 현재 버퍼의 위치 리턴
  • java.nio.Buffer 클래스를 확장
  • 현재의 “위치”를 나타내는 메소드 position()
  • 읽거나 쓸 수 없는 “위치”를 나타내는 메소드 limit()
  • 버퍼의 크기를 나타내는 메소드 capacity()
  • 0 ≤ mark ≤ position ≤ limit ≤ capacity

 

리턴 타입 메소드 설명
Buffer flip() limit 값을 현재 position으로 지정한 후 position을 0으로 이동
Buffer mark() 현재 position을 mark
Buffer reset() 버퍼의 posotion을 mark한 곳으로 이동
Buffer rewind() 현재 버퍼의 position을 0으로 이동
int remaining() limit-position 계산 결과를 리턴
boolean hasRemaining() position과 limit 값에 차이가 있을 경우 true 리턴
Buffer clear() 버퍼를 지우고 현재 position을 0으로 이동하고 limit 값을 버퍼의 크기로 변경
  • flip()은 limit값을 변경하지만 rewind()는 limit값을 변경하지 않는다
  • remaining()과 hasRemaining()은 limit까지만 데이터를 읽을 수 있다