14-1 / 입출력 스트림

프로그램은 데이터를 읽고 출력하는 작업을 빈번하게 수행한다. 이런 수행에 있어 데이터를 읽고 출력하기 위해 사용되는 입출력 API에 대해 알아본다.

 

  • 스트림이란?

단일 방향으로 연속적으로 흘러가는 것을 말하는데 자바에서는 데이터는 스트림(stream)을 통해 입출력된다.

데이터가 출발지에서 도착지로 흘러간다.

 

- 프로그램이 출발지면 데이터를 출력하므로 출력 스트림을 사용

- 프로그램이 도착지면 데이터를 입력받아야하므로 입력 스트림을 사용

 


입출력 스트림의 종류

  • java.io 패키지에서 제공하는 스트림 클래스들을 크게 두 종류로 구분 (구분 방법은 접미사)

- 바이트(byte) 기반 스트림 : 그림, 멀티미디어 등의 바이너리 데이터를 읽고 출력 할 때 사용

- 문자(character) 기반 스트림 : 문자 데이터를 읽고 출력할 때 사용

 

구분 바이트 기반 스트림 문자 기반 스트림
입력 스트림 출력 스트림 입력 스트림 출력 스트림
최상위 클래스 InputStream OutputStream Reader Writer
하위 클래스 XXXInputStream XXXOutputStream XXXReader XXXWriter

 

 


바이트 출력 스트림 : OutputStream

모든 바이트 기반 출력 스트림 클래스가 OutputStream 클래스를 상속받아 만들어진다.

 

  • OutputStream 클래스의 주요 메소드
리턴 타입 메소드 설명
void write(int b) 1byte를 출력
void write(byte[] b) 매개값으로 주어진 배열 b의 모든 바이트 출력
void write(byte[] b, int off, int len) 매개값으로 주어진 배열 b[off] 부터 len개까지의 바이트 출력
void flush() 출력 버퍼에 잔류하는 모든 바이트 출력
void close() 출력 스트림 닫기

 

  • write(int b) 

이 메소드는 매개변수로 주어지는 int(4byte)에서 끝의 1byte만 출력 스트림 보낸다.

 

  • write(byte[] b)

매개값으로 주어진 배열 모든 바이트를 출력 스트림으로 보낸다

 

 

  • write(byte[] b, int off, int len)

매개값으로 주어진 배열b의 off 인덱스부터 len개의 바이트를 출력 스트림으로 보낸다

 

 


바이트 입력 스트림 : InputStream

모든 바이트 기반 입력 스트림 클래스가 InputStream 클래스를 상속받아 만들어진다.

 

  • InputStream 클래스의 주요 메소드
리턴 타입 메소드 설명
int read() 1byte를 읽고 읽은 바이트 리턴
int read(byte[] b) 읽은 바이트를 매개값으로 주어진 배열에 저장하고  읽은 바이트 수를 리턴
int read(byte[] b, int off, int len) len개의 바이트를 읽고 매개값으로 주어진 배열b의 off 인덱스부터 len개 까지 저장하며 읽은 바이트 수를 리턴한다.
void close() 입력 스트림을 닫기

 

  • read()

입력 스트림으로 부터 1byte씩을 읽어내 int(4byte) 타입으로 리턴하여 리턴된 int값엔 마지막 byte에만 데이터가 있다.

입력 스트림이 5개 byte가 들어오면 1byte씩 5번 읽는다. 더 이상 바이트를 읽을 수 없다면 -1을 리턴한다.

 

  • read(byte[] b)

매개값으로 주어진 배열 길이 만큼 byte를 읽고 해당 배열 저장한다. 실제로 읽은 byte수가 배열 길이보다 적다면 읽은 수만큼만 리턴한다. 그리고 최종적으로 더이상 byte를 읽어낼 수 없다면 -1를 리턴한다. 

 

  • read(byte[] b, int off, int len)

입력 스트림으로 부터 len개 byte만큼 읽고 매개갑승로 주어진 byte 배열b의 off 인덱스부터 len개 까지 저장한다. 그리고 읽은  byte수인 len개를 리턴한다. 실제로 읽은 byte수가 배열 길이보다 적다면 읽은 수만큼만 리턴한다. 바이트를 읽을 수 없다면 -1을 리턴한다.

 

 


문자 출력 스트림 : Writer

모든 문자 기반 출력 스트림 클래스가 Writer 클래스를 상속받아 만들어진다.

 

  • Writer클래스의 주요 메소드
리턴 타입 메소드 설명
void write(int c) 매개값으로 주어진 한 문자를 보낸다
void write(char[] cbuf) 매개값으로 주어진 배열의 모든 문자를 보낸다
void write(char[] cbuf, int off, int len) 매개값으로 주어진 배열 cbuf의 off 인덱스부터 len개까지 문자를 보낸다
void write(String str) 매개값으로 주어진 문자열을 보낸다
void write(String str, int off, int len) 매개값으로 주어진 문자열에서 off순번부터 len개까지의 문자를 보낸다
void flush() 버퍼에 잔류하는 모든 문자를 출력
void close() 출력 스트림을 닫기

 

  • write(int c)

매개 변수로 주어지는 int에서 끝에 2byte(1개 문자값)만 출력 스트림으로 보낸다.

 

  • write(char[] cbuf)

매개값으로 주어진 char[] 배열의 모든 문자를 출력 스트림으로 보낸다

 

  • write(char[] cbuf, int off, int len)

매개값으로 주어진 cbuf의 off 인덱스부터 len개의 문자를 출력 스트림으로 보낸다

 

  • write(String str)와 write(String str, int off, int len)

문자열을 쉽게 보내기 위해서 두 메소드를 제공한다.

- write(String str)는 문자열 전체를 출력 스트림으로 보냄

- write(String str, int off, int len)는 주어진 문자열 str에서 off순번부터 len개까지 출력 스트림으로 보냄

 

 


문자 입력 스트림 : Reader

모든 문자 기반 입력 스트림 클래스가 Reader 클래스를 상속받아 만들어진다.

 

  • Reader 클래스의 주요 메소드
리턴 타입  메소드 설명
int read() 1개의 문자를 읽고 리턴
int read(char[] cbuf) 읽은 문자들을 매개값으로 주어진 문자 배열에 저장하고 읽은 문자수 리턴
int read(char[] cbuf, int off, int len) len개의 문자를 읽고 매개값으로 주어진 문자 배열 cbuf의 off 인덱스부터 len개까지 저장. 그리고 읽은 문자 수 리턴
void close() 입력 스트림 닫기

 

  • read()

입력 스트림으로 부터 1개의 문자 (2byte)를 읽고 int(4byte)로 반환한다. 리턴된 4byte중 끝에 2byte만 문자 데이터가 들어가 있다. 더이상 입력 스트림으로 부터 문자를 읽을 수 없다면 -1을 리턴한다.

또한 read()가 리턴한 int값을 char로 변환하면 읽은 문자를 얻을 수 있다. 

 

  • read(char[] cbuf)

입력 스트림으로 부터 매개값으로 주어진 문자 배열의 길이만큼 문자를 읽고 배열에 저장한다. 그리고 읽은 문자 수를 리턴한다. 실제로 문자 수가 배열의 길이보다 적을 경우 읽은 수 만큼 리턴한다. 더 이상 입력 스트림에서 문자를 읽을 수 없다면 -1을 리턴한다.

 

 

  • read(char[] cbuf, int off, int len)

입력 스트림으로부터 len개의 문자 만큼 읽고 매개값으로 주어진 문자 배열 cbuf의 off 인덱스부터 len개까지 저장한다. 그리고 읽은 문자 수인 len개를 리턴한다. 입력 스트림으로부터 문자를 더이상 읽을 수 없다면 -1을 리턴한다. 

 

 

13-2 / LIFO와 FIFO 컬렉션

 

* 후입선출 (LIFO / Last In First Out) : 나중에 넣은 객체가 먼저 빠져나가는 자료구조

* 선입선출 (FIFO / First In First Out) : 먼저 넣은 객체가 먼저 빠져나가는 자료구조

 

컬렉션 프레임워크에는 LIFO 자료구조를 제공하는 Stack 클래스와 FIFO 자료구조 제공하는 Queue 인터페이스 제공

 

 


stack

LIFO 자료구조를 구현한 클래스

  • stack 클래스의 주요 메소드
리턴 타입 메소드 설명
E push(E item) 주어진 객체를 스택에 넣는다
E peek() 스택의 맨 위 객체를 가져온다. 객체를 스택에서 제거하지 않는다.
E pop() 스택의 맨 위 객체를 가져옵니다. 객체를 스택에서 제거한다.
Stack<E> stack = new Stack<E>();
Stack<E> stack = new Stack<>();

 

 


Queue

Queue 인터페이스는 FIFO 자료구조에서 사용되는 메소드를 정의하고 있다.

  • Queue 인터페이스에 정의된 메소드
리턴 타입 메소드 설명
boolean offer(E e) 주어진 객체를 넣는다.
E peek() 객체 하나를 가져온다. 객체를 큐에서 제거하지 않는다.
E poll() 객체를 하나 가져온다. 객체를 큐에서 제거한다.

 

Queue 인터페이스를 구현한 대표적인 클래스는 LinkedList이다. 

Queue<E> queue = new LinkedList<E>();
Queue<E> queue = new LinkedList<>();

 

 

 

13 / 컬렉션 프레임워크

 

자바는 객체들을 효율적으로 추가, 삭제, 검색할 수 있도록 인터페이스와 구현 클래스를 java.util 패키지에서 제공하는 데 이를 컬렉션 프레임워크라고 한다.

 

  • 컬렉션: 객체의 저장을 의미
  • 프레임워크: 사용법을 정해놓은 라이브러리, 짜여진 틀

List 컬렉션

* 객체를 배열처럼 인덱스로 관리하는 컬렉션으로 각 인덱스마다 객체 번지를 참조하고 있다.

* 배열과 달리 저장 용량이 자동으로 증가하고 객체 저장시 자동 인덱스가 부여된다.

* List 컬렉션에는 ArrayList, Vector, LinkedList 등이 있다

 

* List컬렉션에서 공통 사용 가능한 List 인터페이스의 메소드

기능 메소드 설명
객체 추가 boolean add(E element) 주어진 객체를 맨 끝에 추가
void add(int index, E element) 주어진 인덱스에 객체를 추가
E set(int index, E element) 주어진 인덱스에 저장된 객체를 주어진 객체로 바꾼다.
객체 검색 boolean contains(Object o) 주어진 객체가 저장되어 있는 지 조사
E get(int index) 주어진 인덱스에 저장된 객체를 리턴
boolean isEmpty() 컬렉션이 비어 있는지 조사
int size() 저장되어 있는 전체 객체 수를 리턴
객체 삭제 void clear() 저장된 모든 객체 삭제
E remove(int index) 주어진 인덱스에 저장된 객체 삭제
boolean remove(Object o) 주어진 객체 삭제

* E는 제네릭으로 그때 그때 맞는 클래스타입을 넣어주면 된다.

 

 

  • ArrayList
List<E> list = new ArrayList<E>();
List<E> list = new ArrayList<>();

* 기본 생성자로 객체 생성시 내부에 10개의 객체를 저장할 수 있는 초기 용량을 가지며, 이후 초과되면 자동 증가한다.

* 인덱스는 0번 부터 시작

* 중간 인덱스를 제거하면 그 뒤의 인덱스들은 1씩 당겨짐

* 중간 인덱스를 추가하면 그 뒤의 인덱스들은 1씩 밀려남

 

이 당겨지고 밀러나는 동작때문에 객체수가 많거나 중간 인덱스의 삭제 추가가 빈번하게 일어난다면 LinkedList를 사용하는 것이 좋다. 하지만 인덱스를 이용해 객체를 찾거나 맨 마지막에 객체를 추가하는 경우라면 ArrayList가 더 성능이 좋다.

 

 

  • Vector
List<E> list = new Vector<E>();
List<E> list = new Vector<>();

ArrayList와 동일한 내부구조를 가지나 동기화(Synchronized)된 메소드로 구성되어 있어 멀티 스레드가 동시에 Vector의 메소드를 실행할 수 없어, 멀티 스레드 환경에서 안전하게 객체를 추가, 삭제 할 수 있다.

 

 

  • LinkedList
List<E> list = new LinkedList<E>();
List<E> list = new LinkedList<>();

LinkedList도 ArrayList와 사용법과 동일하나, 내부 구조가 다르다. 

ArrayList는 내부 배열에 객체를 저장해서 관리하지만, LinkedList는 인접 참조를 링크해 체인처럼 관리한다.

LinkedList에서 특정 인덱스를 삭제하거나 추가하면,  삭제되거나 추가된 인덱스 전후의 링크만 변경되고 나머지 링크는 변경되지 않는다.

만약 객체가 10000개인 ArrayList에 10번째 인덱스를 추가했다면 10번 부터 객체가 모두 밀리는 동작이 실행되지만 LinkedList라면 단 두개인 9번과 기존 10번 인덱스의 링크만 변경해주면 되므로 이런 부분에서는 LinkedList가 훨씬 효율적이다. 

 

구분 순차적으로 추가/삭제 중간에 추가/삭제 검색
ArrayList 빠르다 느리다 빠르다
LinkedList 느리다 빠르다 느리다

 

 


Set 컬렉션

* List컬렉션과 달리 Set컬렉션은 저장 순서가 유지되지 않는다. 즉, 순서 상관없다.

* 객체가 중복되지 않는다

* null도 단 한개만 저정할 수 있다.

* Set컬렉션에는 HashSet, LinkedHashSet, TreeSet 등이 있다

 

* Set컬렉션에서 공통 사용 가능한 Set 인터페이스 메소드

기능 메소드 설명
객체 추가 boolean add(E e) 주어진 객체를 저장. 저장 성공시 true리턴, 중복 개체면 false리턴
객체 검색 booleancontains(Object o) 주어진 객체가 저장되어 있는지 조사
boolean isEmpty() 컬렉션이 비어있는지 조사
Iterator<E> iterator() 저장된 객체를 한번씩 가져오는 반복자를 리턴
int szie() 저장되어 있는 전체 객체 수 리턴
객체 삭제 void clear() 저장된 모든 객체를 삭제
boolean remove(Object o) 주어진 객체 삭제

 

반복자는 Iterator 인터페이스를 구현한 객체를 말하는데 iterator 메소드를 호출하면 얻을 수 있다.

Set<String> set = ...;
Iterator<String> iterator = set.iterator();

//---- 사용 예제1
Set<String> set = ...;
Iterator<String> iterator = set.iterator();
while(iterator.hasNext()){
	String str = iterator.next();
}

//---- 사용 예제2
Set<String> set= ...;
for(String str : set){
}

//---- 사용 예제3
while(iterator.hasNext()){
	String str = iterator.next();
    if(str.equals("홍길동")){
        iterator.remove();
    }
}
리턴 타입 메소드 설명
boolean hasNext() 가져올 객체가 있으면 true리턴, false리턴
E next() 컬렉션에서 하나의 객체를 가져온다
void remove() Set 컬렉션에서 객체를 제거

 

  • HashSet

Set 인터페이스 구현 클래스

Set<E> set = new HashSet<E>();
Set<E> set = new HashSet<>();

* HashSet은 객체들을 순서없이 저장하고 동일한 객체는 중복 저장하지 않는다. 

* HashSet이 판단하는 동일한 객체란 꼭 같은 인스턴스를 뜻하지 않는다. 객체를 저장하기전 HashCode() 메소드를 호출해서 해시코드를 얻고, 이미 저장되어 있는 해시코드와 비교하며, 혹 같은 해시코드가 있다면 이를 다시 equals()메소드로 객체 비교를 하여 true가 나오면 최종적으로 같은 객체로 판단. 중복 저장을 하지않는다. 

 * String 같은 경우는 같은 문자열을 갖으면 같은 객체로 간주된다.

 

 

 


Map 컬렉션

* Map 컬렉션으 키(Key)와 값(Value)로 구성된 Map.Entry 객체를 저장하는 구조를 가지고 있다.

* Entry는 Map 인터페이스 내부에 선언된 중첩 인터페이스로 키와 값은 모두 객체이다.

* 키는 중복될 수 없으나 값은 중복 될 수 있다.

(만일 저장된 키와 동일한 키로 값을 저장하면 기존 값이 없어지고 새로운 값으로 대체)

* Map 컬렉션에는 HashMap, Hashtable, LinkedHashMap, Properties, TreeMap 등이 있다.

 

* Map 컬렉션에서 공통적으로 사용 가능한 Map 인터페이스의 메소드

기능 메소드 설명
객체 추가 V put (K key, V value) 주어진 키로 값을 저장, 새로운 키 경우 null 리턴, 동일한 키 있을 경우 값 대체하고 이전 값 리턴
객체 검색 boolean containsKey(Object key) 주어진 키가 있는지 여부 확인
boolean containsValue(Object value) 주어진 값이 있는지 여부 확인
Set<Map.Entry<K, V>> entrySet() 키와 값의 쌍으로 구성된 모든 Map.Entry 객체를 Set에 담아서 리턴
V get(Object Key) 주어진 키가 있는 값을 리턴
boolean isEmpty() 컬렉션이 비어 있는지 여부 확인
Set<K> KeySet 모든 키를 Set객체에 담아서 리턴
int size() 저장된 키의 총 수를 리턴
Collection<V> values() 저장된 모든 값을 Collection에 담아서 리턴
객체 삭제 void clear() 모든 Map.Entry(키와 값)을 삭제
V remove(Object key) 주어진 키와 일치하는 Map.Entry를 삭제하고 값을 리턴

 

 

  • HashMap

* HashMap의 키로 사용할 객체는 hashCode()와 equals()메소드를 재정의해서 동등 객체가 될 조건을 정해야한다.

동등 객체의 조건은 hashCode()의 리턴값이 같아야하고, equals() 메소드가 true를 리턴해야 한다.

Map<K, V> map = new HashMap<K, V>();

예시)

Map<String, Integer> map = new HashMap<String, Integer>();
Map<String, Integer> map = new HashMap<>();

 

 

  • Hashtable

Hashtable은 HashMap과 동일한 내부 구조를 가지고 있다. Hashtable도 키로 사용할 객체는 hashCode()와 equals()메소드를 재정의해서 동등 객체가 될 조건을 정해야 한다.

차이점은 Hashtable안은 동기화(synchronized) 메소드로 구성되어 있기 때문에 멀티 스레드가 동시에 Hashtable의 메소드를 실행할 수 없고, 하나의 스레드가 실행을 완료해야만 다른 스레드를 실행 할 수 있다. 그래서 Hashtable은 스레드에 안전 (thread safe) 하다.

 

Map<K, V> map = new Hashtable<K, V>();

예시)

Map<String, Integer> map = new Hashtable<String, Integer>();
Map<String, Integer> map = new Hashtable<>();

 

 

 

12-2 / 스레드 제어

스레드 객체 생성하고 start() 메소드를 호출하면 실행 대기 상태가 된다.

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

 

실행 상태의 스레드는 run() 메소드를 모두 실행하기 "전"에 다시 실행 대기 상태로 돌아갈 수 있다.

실행 대기 상태에 있는 다른 스레드가 선택되어 실행 상태가 되기도 한다.

 

실행 상태에서 run() 메소드 내용이 모두 실행되면 스레드의 실행이 멈추고 종료 상태가 된다.

 


스레드 상태

  • 실행 대기 상태 : 스레드는 start() 메소드를 호출하면 실행을 기다리고 있는 실행 대기 상태가 된다. 
  • 실행 상태 (running) : 실행 대기 상태에 있는 스레드 중 운영체제가 하나의 스레드를 선택하고 CPU가 run()메소드를 실행한 상태
  • 대기 상태와 실행 상태의 반복 : 실행 상태의 스레드는 run()메소드가 모두 실행하기 전 다시 실행 상태로 돌아갈 수 있고, 실행 대기 상태의 다른 스레드가 선택되어 실행 상태가 되기도 한다. 이런 과정을 반복하여 스레드는 자신의 run() 메소드를 조금씩 실행한다. 
  • 일시정지 : 경우 따라 일시정지 상태로 가기도 하며, 일시정지 상태가 풀리면 바로 실행이 아닌 실행 대기 상태로 돌아가 실행을 기다리게 된다.

  • 종료 상태 (terminated) : 위과정을 반복하다가 더이상 실행할 코드가 없게 되면 run()메소드가 종료되고 스레드 실행은 멈추어 종료 상태가 된다.

 


스레드 상태 제어

동영상을 재생하다 일시정지를 하는 등 실행되고 있는 스레드의 행동을 하려면 스레드의 상태를 변경해야 하는데 이를 스레드 상태 제어라고 한다. 특히 멀티 스레드 프로그램에서는 더더욱 정교한 스레드 상태 제어가 필요하다.

 

메소드 설명
interrupt() 일시 정지 상태의 스레드에서 InterruptedException을 발생시켜, 예외 처리코드(catch)에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있게 한다.
sleep(long millis) 주어진 시간 동안 스레드를 일시정지. 시간이 지나면 자동적으로 실행 대기 상태가 된다.
stop() 스레드 즉시 종료. (불안전한 종료가 되므로 안정성에 문제가 있어 Deprecated 되었다.)

 

  • 주어진 시간 동안 일시정지
try {
	Thread.sleep(1000);
} catch (InterruptedException e) {
	// interrupt() 메소드가 호출되면 실행
}

위의 코드는 1000 밀리세컨드  즉 1초만 스레드를 일시정지 상태로 만든다. 그러나 1초 안에 interrupt() 메소드가 호출되면 InterruptedException이 발생하게 되고 catch문이 이걸 잡아 해당 실행 블록을 실행한다.

 

  • 스레드의 안전한 종료

스레드는 자신의 run() 메소드가 모두 실행되면 자동 종료된다. 하지만 경우에 따라 중간에 즉시 종료해야할 경우가 있는데 stop() 메소드는 종료시 사용하던 자원들을 불안전한 상태로 남겨 안정성 문제로 deprecated(중요도가 떨어져 이제 사용X) 되었다. 그렇다면 안전하게 종료할 수 있는 방법은 무엇이 있을까

 

① stop 플래그 이용하여 run()메소드의 종료를 유도한다.

public class XXXThread extends Thread {
    private boolean stop; 	// stop 플래그 필드
    
    public void run(){
		while( !stop ) { 	// stop이 true가 되면 run() 종료
        	스레드가 반복 실행하는 코드
        }
    }
}

stop이 true가 되면 run을 종료하고 스레드가 사용한 자원을 정리하며 run()메소드가 끝나 안전하게 종료한다.

 

 

② interrupt() 메소드 이용하는 방법

public class InterruptExample {
    public static void main(String[] args){
        Thread thread = new PrintThread2();
        thread.start();
        
        try { 
        	Thread.sleep(1000); 
        } catch (InterruptedException e){ }
        
        thread.interrupt(); // 스레드 종료를 위해 InterruptedException 발생
    }
}
public class PrintTread2 extends Thread {
    public void run(){
    	try {
            while(true){
            	System.out.println("실행 중");
                Thread.sleep(1);	// InterruptedException 발생
            }
        } catch (InterruptedException e) { }
        
        System.out.println("자원 정리");
        System.out.println("실행 종료");
    }
}

실행 대기 또는 실행 상태에서 interrupt() 메소드가 실행되면 즉시 InterruptedException가 발생하지 않고 "미래에" 일시 정지 상태가 되면 InterruptedException 발생한다. 따라서 interrupt()메소드는 일시정지 상태가 아니면 메소드 호출의 의미가 없다. 그래서 강제로 나마 짧은 일시 정지를 만들어 InterruptedException 발생시키기 위해 중간에 2번째 코드에 Thread.sleep(1); 이 들어간 것이다.

 

일시 정지를 하지 않고 interrupt의 호출 여부를 알려면 interrupted() 메소드와 isInterrupted() 메소드를 사용하면 되며 interrupt() 호출시 이 두 메소드는 true를 리턴한다. 

- interrupted()  :  정적 메소드로 현재 스레드가 interrupted 되었는지 확인

- isInterrupted() :  인스턴스 메소드로 현재 스레드가 interrupted 되었는지 확인

 

이 함수를 이용해 if문의 조건으로 사용해 while문을 빠져나오게 하여 스레드가 자원을 정리하게 유도한다.

 

 


데몬 스레드 

스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드. 주 스레드의 보조 역할을 수행하기 때문에 주 스레드가 종료되면 데몬 스레드는 강제적으로 자동 종료된다. 이 점을 제외하면 데몬 스레드는 일반 스레드와 큰 차이가 없다.

 

스레드를 데몬으로 만들기 위해 주 스레드가 데몬이 될 스레드의 setDaemeon(true)를 호출해주면 된다. 

 

주의할 점은 start()메소드가 호출되고 나서 setDaemeon(true)을 호출하면 IllegalThreadStateException이 발생하기 때문에 start() 메소드 호출전에 setDaemeon(true)를 호출해야 한다.

 

12-1 / 멀티 스레드

📌 프로세스(process) 

어플리케이션 실행시 운영체제로부터 실행에 필요한 메모리를 할당 받아 실행하는 것

 

📌 스레드(thread)

한가닥의 실. 한가지 작업을 싱해하기 위해 순차적으로 실행할 코드를 실처럼 이어놓았다고 해서 유래된 이름으로 프로세스 내부에서 코드의 실행 흐름을 말한다.

 

 


스레드

운영체제는 두가지 이상의 작업을 동시에 처리하는 멀티 태스킹을 할 수 있도로고 CPU 및 메모리 자원을 프로세스마다 적절히 할당하고 병렬로 실행 시킨다. 

이런 멀티태스킹은 하나의 프로세스 안에서도 이루어질 수 있는데 이것을 멀티 스레드라고 한다.

 

멀티 프로세스는 운영체제에서 할당받은 자신의 메모리를 가지고 실행해 프로세스끼리 독립적이다. 따라서 하나의 프로세스가 오류가 발생해도 다른데에 영향을 주지 않는다.

하지만 멀티 스레드는 하나의 프로세스 내부에 생성되어 있어 하나의 스레드가 예외를 발생시키면 프로세스 자체가 종료될 수 있어 다른 스레드에 영향을 미칠 수 있다. 그렇기에 스레드에서는 예외 처리가 중요하다.

 

 

 


메인 스레드

자바의 모든 어플리케이션은 메인 스레드가 main() 메소드를 실행하면서 시작된다.

첫줄 부터 시작해 아래로 순차적으로 실행하며, main() 메소드의 마지막 코드를 실행하거나 return문을 만나면 실행이 종료되는 데 이 흐름이 메인 스레드다.

메인 스레드는 필요따라 스레드를 더 만들어 멀티 태스킹을 수행할 수 있다. (멀티 스레드)

 

싱글 스레드는 하나의 스레드가 종료되면 프로세스도 종료되지만 멀티 스레드는 하나의 스레드라도 남아 있다면 프로세스가 종료되지 않는다.

 

 

 


작업 스레드 생성과 실행

자바에서는 작업 슬드도 객체로 생성되기 때문에 클래스가 필요하다. java.lang.Thread 클래스를 직접 객체화 하거나 이를 상속해 하위 클래스를 만들어 생성 할 수도 있다.

 

  • Thread 클래스로부터 직접 생성

스레드 객체를 직접 생성하려면 Runnable(실행할수 있는) 매개값을 갖는 생성자를 호출해야 한다.

Thread thread = new Thread(Runnable target);

 

Runnable은 인터페이스 타입이기에 구현 객체를 만들어야 대입해야 하며 그 안의 run() 메소드를 재정의해 작업 스레드가 할 코드를 작성해야 한다.

Runnable은 작업 내용을 가진 객체일 뿐 스레드는 아니기에 이를 매개값으로 해 Thread 생성자를 호출해야 비로소 작업 스레드가 생성된다.

// 1.Runnable 구현 객체
class Task implements Runnable {
    pulic void run(){
    	스레드가 실행할 코드;
    }
}

// 2.스레드 생성
Runnable task = new Task();
Thread thread = new Thread(task);

// 1+2 방법인 익명객체 사용법 (더 많이 사용되는 방법)
Thread thread = new Thread(){
    pulic void run(){
    	스레드가 실행할 코드;
    }
}

// 스레드 시작
thread.start();

 

 

  • Thread 하위 클래스로부터 생성

Thread의 하위 클래스로 작업 스레드를 정의하면서 작업 내용을 포함 시킬수 있다.

그리고 코드 절약위해 이 또한 익명 객체로 생성할 수 있다.

public class WorkerThread extends Thread {
    @Override
    public void run() {
		스레드가 실행할 코드;
	}
}

Thread thread = new WorkerThread();

// 익명 객체 스레드 생성
Thread thread = new Thread(){
    public void run(){
		스레드가 실행할 코드;
	}
}

 

 

  • 스레드의 이름

스레드 또한 이름을 가지고 있다. 이 이름은 디버깅할 대 어떤 스레드가 어떤 작업을 하는 지 조사할 목적으로 사용된다.

 

 


동기화 메소드

  • 공유 객체를 사용할 때의 주의할 점

멀티스레드 프로그램에서 스레드는 같은 객체를 공유해 작업해야하는 경우 A가 사용하던 객체를 B가 사용해 의도치 않은 결과가 나올 수 있다.

 

  • 동기화 메소드

위와 같은 문제가 일어나지 않도록 하나의 스레드가 사용중인 객체에 다른 스레드가 접근 할 수 없도록 해야했다. 그래서 존재하는 것이 단 하나의 스레드만 실행할 수 있는 코드 영역 "임계영역 (Critical Section)"이다.

자바는 임계영역을 지정하기 위해 동기화 메소드를 제공한다.

 

스레드가 객체 내부의 동기화 메소드를 실행하면 즉시 객체에 잠금을 걸어 드란 스레드가 동기화 메소드를 실행 못하게 하는 것인데 이는 메소드 선언시 synchronized 키워드를 붙인다. (인스턴스, 정적 상관없이)

public synchronized void method() {
	임계 영역;
}

 

키워드를 선언해주면 해당 실행 블록 전부가 임계 영역이 되어 스레드가 메소드 실행시 안의 객체에 모두 잠금이 일어나고, 스레드가 해당 메소드를 종료하면 잠금이 풀린다.

 

'Programming > Java' 카테고리의 다른 글

혼공자바] 13-1 컬렉션 프레임  (1) 2022.12.27
혼공자바] 12-2 스레드 제어  (0) 2022.12.19
혼공자바] 11 기본 API 클래스  (0) 2022.12.14
혼공자바] 10-2 예외처리  (0) 2022.12.13
제네릭(Generic)  (0) 2022.12.13

+ Recent posts