본문 바로가기

컴퓨터학원(복습)(수료)

자바(JAVA)기반 안드로이드 웹&앱 개발 26일차(NIO2, Thread, 동기화)

드디어 JAVA 책 한권이 끝났습니다..

내용이 다 기억이 나진 않고..

아직 아무것도 안보고 코딩도 못합니다.. 취업할 수 있을런지 ㅠㅠㅠ

1. NIO 기반의 입출력

.1) NIO의 채널(Channel)과 버퍼(Buffer)

..(1) NIO에서는 스트림을 대신해서 ‘채널’이라는 것을 생성한다.

..(2) 스트림은 한 방향으로만 데이터가 이동하지만 채널은 양방향으로 데이터 이동이

.......가능하다.

..(3) 스트림은 입력스트림과 출력스트림이 구분되므로 쓰면서 동시에 읽는 것도

.......가능한 스트림을 생성할 수 없다. 그러나 채널은 하나의 채널을 대상으로 읽고 쓰는

.......것이 가능하다.

..(4) 채널은 반드시 버퍼에 연결해서 사용해야 한다.(제약사항)

...☞ 채널의 데이터 출력 경로(방향) : 데이터 > 버퍼 > 채널 > 파일

...☞ 파일을 대상으로 하는 채널의 데이터 입력 경로(방향) : 데이터 < 버퍼 < 채널 < 파일

..(5) NIO에서는 버퍼와 채널의 연결 과정을 거치지 않으며, 이 둘은 각각 독립적으로 존재한다.

※ NIO 기반의 파일 복사프로그램 예시

 

...☞ buf.clear(); // 버퍼를 완전히 비운다.

...☞ buf.compact(); // 버퍼에 저장된 내용 중에서 읽은 데이터만 지운다.

 

.2) 성능 향상 포인트는 어디에?

..(1) 기존 IO 모델을 기반으로 파일 복사 프로그램을 작성하려면 입력 스트림과 출력 스트림 각각에 버퍼 스트림을 연결해야만 했다. 즉 두개의 버퍼가 필요했다. 그리고 입력 버퍼에 저장된 데이터를 출력 버펑로 이동하는 ‘버퍼 사이의 데이터 이동 과정’을 반드시 거쳐야 했다. 그러나 이러한 작업을 NIO 모델에서는 생략할 수 있다.

..(2) Non-direct 버퍼를 생성하지 않고 Direct 버퍼를 생성하여 성능의 향상을 기대할 수 있다.

☞ ByteBuffer buf = ByteBuffer.allocate(1024); // Non-direct 버퍼

☞ ByteBuffer buf = ByteBuffer.allocateDirect(1024); // Direct 버퍼 생성

☞ Non-direct 버퍼는 가상머신이 생성하고 유지하는 버퍼이다. 따라서, 파일에 저장된 데이터를 읽어들일 때 다음의 흐름을 거쳐서, 실행 중인 프로그램으로 데이터가 전달된다.

..☞ 파일 > 운영체제 버퍼 > 가상머신 버퍼 > 실행 중인 자바 프로그램

☞ 반면, Direct 버퍼를 활용하면 이 과정이 다음과 같이 줄어든다.

..☞ 파일 > 운영체제 버퍼 > 실행 중인 자바 프로그램

..(3) Direct 버퍼의 할당과 해제에 드는 시간적 비용이 Non-direct 버퍼에 비해 다소 높기

.......때문에 입출력할 파일의 크기가 크지 않거나 버퍼를 빈번히 할당하고 해제해야 하는

.......상황이라면, 오히려 Non-direct 버퍼를 이용해서 입출력을 진행하는 것이 빠를 수 있다.

..(4) 다양한 데이터의 입출력을 위해 ByteBuffer 이외에도 기본자료형 별로 버퍼 클래스가

.......정의되어 있다.

 

.3) 파일 랜덤 접근(File Random Access)

..(1) 파일에 데이터를 쓰거나 읽을 때 원하는 위치에 쓰거나 읽는 것을 의미한다.

..(2) 버퍼에 파일의 데이터를 옮겨놓은 다음에, 버퍼에 저장된 데이터를 순서에 상관없이

......,원하는 순으로 읽어들이는 방식이다.

※ 파일 랜덤 접근(File Random Access) 예시

 

 

☞ 위 예제에서는 두 개의 버퍼와 하나의 채널을 생성하였다.

..☞ 데이터 > 버퍼1 > 채널 > 파일

..☞ 파일 > 채널 > 버퍼2 > 출력

☞ 목적은 다르지만 둘 다 데이터를 담았다가 내보내는 과정을 거치기 때문에 중간에 각 버퍼

.....를 대상으로 flip 메소드를 호출해야 한다.

☞ wb.putInt(120); : 메소드 호출 후 포지션 4 / wb.putInt(240); : 메소드 호출 후 포지션8

..(3) ‘포지션(Position)'

...☞ 그 대상이 채널이건 버퍼 건 어느 위치까지 데이터를 썼는지, 그리고 어느 위치까지 데이터를 읽었는지를 표시하기 위해서 ’포지션‘이라는 위치정보가 유지된다.

...☞ ByteBuffer wb = ByteBuffer.allocate(1024); // 포지션 0

...☞ wb.flip(); // 메소드 호출 후 포지션 0

※ 포지션(Position) 예시

 

 

쓰레드 그리고 동기화

1. 쓰레드의 이해와 쓰레드의 생성

.1) 쓰레드의 이해와 쓰레드의 생성 방법

..(1) 쓰레드는 실행 중인 프로그램 내에서 ’또 다른 실행의 흐름을 형성하는 주체‘를 의미

※ main 쓰레드 표시 예시

 

 

☞ Thread ct = Thread.currentThread();

...☞main 메소드를 실행하는 쓰레드의 정보를 담고 있는 인스턴스의 참조

☞ String name = ct.getName(); // 참조하는 쓰레드의 이름을 표시

.2) 쓰레드를 생성하는 방법

※ 쓰레드 생성 예시

 

 

☞ Runnable 인터페이스를 구현하는 클래스의 인스턴스를 생성해야 한다.

☞ Runnable은 void run() 추상메소드 하나만 존재하는 함수형 인터페이스이다.(람다식)

..(1) main 쓰레드가 일을 마쳤다고 해서 프로그램이 종료되지는 않는다. 모든 쓰레드가 일을 마치고 소멸되어야 프로그램이 종료된다. 따라서 위 예제에서 생성된 쓰레드는 자신의 일을 마칠 충분할 시간을 갖는다.

..(2) 생성된 쓰레드는 자신의 일을 마치면 자동으로 소멸된다.(쓰레드의 소멸은 스레드의 생성을 위해 할당했던 모든 자원의 해제를 의미한다.)

..(3) Thread의 별도의 이름을 붙여주고 싶다면 다음 생성자를 통해 Thread 인스턴스를 생성하면 된다.

...☞ public Thread(Runnable target, String name)

 

※ sleep 메소드를 활용한 쓰레드의 실행속도를 늦추기

 

☞ 보통은 쓰레드 하나에 CPU의 코어 하나가 할당되어 동시에 실행이 이뤄진다. 보급형 스마트폰의 CPU에도 코어가 8개 들어가는 시대이므로 쓰레드 별로 코어가 하나씩 할당되는 상황은 이제 일반적인 경우이다.

☞ 따라서 동시에 실행이 이뤄지는 쓰레드를 대상으로 위 예제에서 보이는 수준으로 실행 흐름을 조절하거나 예측하는 것은 잘못된 결과로 이어지기 쉽다.

☞ 즉, 수백 번 실행을 해서 동일한 결과를 얻더라도 그 결과를 항상 보장할 수 는 없다.

 

.3) 쓰레드를 생성하는 두 번째 방법

..(1) 앞에서 설명한 쓰레드의 생성과정을 세 단계로 정리하면 다음과 같다.

☞ 1단계 Runnable을 구현한 인스턴스 생성

☞ 2단계 Thread 인스턴스 생성

☞ 3단계 start 메소드 호출

..(2) 이와 달리, 두 단계를 거쳐서 쓰레드를 생성하는 방법도 있다.

☞ 1단계 Thread를 상속하는 클래스의 정의와 인스턴스 생성

☞ 2단계 start 메소드 호출

※ Thread를 상속하는 클래스의 인스턴스 생성 예시

 

 

☞ 위 예제에서 보이듯이 Thread를 상속하는 클래스는 public void run()을 오버라이딩 해야 한다. 그러면 start 메소드 호출을 통해 쓰레드가 생성되었을 때, 이 쓰레드는 오버라이딩 된 run 메소드를 실행하게 된다.

 

2. 쓰레드의 동기화

.1) 쓰레드의 메모리 접근 방식과 그에 따른 문제점

※ 둘 이상의 쓰레드가 하나의 메모리 공간에(하나의 변수에) 접근했을 때 문제 예시

 

 

☞ 실행할 때마다 출력결과가 다르다.

☞ 둘 이상의 쓰레드가 동일한 변수에 접근하는 것은 문제를 일으킬 수 있다는 것을 확인하였다. 따라서, 문제가 발생하지 않도록 ’동기화(synchronization)'라는 것을 해야 한다.

 

.2) 동기화(Synchronization) 메소드

..(1) ‘동기화 메소드’

...- 해당 클래스의 메소드에 synchronized 선언을 추가하면 된다.

.....☞ synchronized public void increment() {

...- ‘한 클래스의 두 메소드’에 synchronized 선언이 되면, 두 메소드는 둘 이상의 쓰레드에

.....의해 동시에 실행될 수 없도록 동기화된다. 예를 들어서 한 쓰레드가 increment 메소드를

.....실행하는 중간에 다른 쓰레드가 decrement 메소드를 호출하면, 이 쓰레드는

.....increment의 호출이 완료될 때까지 대기하게 된다.

※ 동기화 적용 예시

 

 

.3) 동기화(Synchronization) 블록

..(1) ‘동기화 메소드’기반의 동기화는 사용하기는 편하지만 메소드 전체에 동기화를 걸어야

......한다는 단점이 있다.

..(2) ‘동기화 블록’이라는 것을 통해 문장 단위로 동기화 선언을 하는 것이 효율적이다.

public void increment() {

..synchronized(this) { // 동기화 블록

.....count++; // 동기화 필요한 문장

..}

..System.out.println("카운터의 값이 1 증가하였습니다.“); // 동기화 불필요한 문장

}

...- 동기화 블록의 선언에 포함된 this의 의미는 다음과 같다.

....☞ 이 인스턴스의 다른 동기화 블록과 더불어 동기화하겠다.

※ synchronized(this) 예시

 

 

3. 쓰레드를 생성하는 더 좋은 방법

.1) 쓰레드 풀(Thread Pool)

..(1) 쓰레드의 생성과 소멸은 그 자체로 시스템에 부담을 주는 일이다. 따라서 처리해야 할

.....일이 있을 때마다 쓰레드를 생성하는 것은 성능의 저하로 이어질 수 있다. 그래서 ‘쓰레드

....풀(Thread Pool)이라는 것을 만들고 그 안에 미리 제한된 수의 쓰레드를 생성해 두고

....이를 재활용 한다.

..(2) concurrent 패키지를 활용하면 간단히 쓰레드 풀을 생성할 수 있다.

※ concurrent 패키지를 활용한 쓰레드 풀 사용 예시

 

..(3) Executors의 클래스의 메소드

☞ newSingleThreadExecutor : 풀 안에 하나의 쓰레드만 생성하고 유지한다.

...☞ SingleThread가 생성하는 쓰레드 풀 안에는 하나의 쓰레드만 생성해두고 이 쓰레드가

.....모든 작업을 처리하게 한다. 따라서 하나의 코어를 기준으로 코어의 활용도를 매우 높인

.,....풀이라 할 수 있다. 그러나 이는 마지막에 전달된 작업은 가장 늦게 처리된다는 단점이

.....있다.

☞ newFixedThreadPool : 풀 안에 인자로 전달된 수의 쓰레드를 생성하고 유지한다.

☞ newCachedThreadPool : 풀 안의 쓰레드의 수를 작업의 수에 맞게 유동적으로 관리한다.

...☞ CachedThread가 생성하는 풀은 효율적으로 쓰레드를 관리하는 것처럼 보이지만 전달된 작업의 수에 비례하여 쓰레드가 생성될 수 있는 관계로 앞서 언급한, 빈번한 쓰레드의 생성과 소멸로 이어질 수 있어 주의가 필요하다.

..(4) 생성된 쓰레드의 풀과 그 안에 존재하는 쓰레드를 소멸하기 위해서는 다음 메소드를 호출해야 한다.

void shutdown()

...☞ 위 메소드가 호출되어도 이미 전달된 작업은 진행이 된다.(추가로 작업전달x)

.......그러나 전달된 모든 작업이 처리가 되면 해당 풀은 종료가 된다.

 

.2) Callable & Future

..(1) Runnable에 위치한 추상 메소드 run의 반환형이 void이기 때문에 작업의 결과를

.....return문을 통해 반환하는 것은 불가능하다.

..(2) 그러나 다음 인터페이스를 기반으로 작업을 구성하면 값을 반환하는 것이 가능하다.

@FunctionalInterface

public interface Callable<V> {

V call() throws Exception;

}

..(3) 메소드의 반환 값을 Future<V> 형 참조변수에 저장해야 한다.(Callable의 타입인자와

.....동일해야 한다.)

※ Callable & Future 예시

 

 

.3) synchronized를 대신하는 ReentrantLock

..(1) 한 쓰레드가 lock 메소드를 호출하고, 이어서 다음 문장을 실행하기 시작한 상태에서,

......다른 쓰레드가 lock 메소드를 호출하면 이 쓰레드는 lock 메소드 호출을 반환하지 않고

......그 자리에서 대기하게 된다. 먼저 lock 메소드를 호출한 쓰레드가 unlock 메소드를 호

......출할 때까지 대기하게 된다.

※ ReentrantLock 예시

 

☞ shutdown 메소드는 바로 반환이 되므로 작업의 최종결과를 확인하기 위해서는

....exr.awaitTermination(100, TimeUnit.SECONDS); 가 필요하다.

 

.4) 컬렉션 인스턴스 동기화

public static <T> Set<T> synchronizedSet(Set<T> s)

public static <T> List<T> synchronizedList(List<T> list)

public static <K, V> Map<K, V> synchronizedMap(Map<K, V> m)

public static <T> Collection<T> synchronizedCollection(Collection<T> c)

..(1) 컬렉션 인스턴스가 동기화되었다고해도 이를 기반으로 생성된 반복자까지 동기화가 이

......루어지는 것은 아니다. 그러므로 반복자를 통해 접근할 때에는 이에 대한 동기화도

......추가해야 한다.

※ 컬렉션 인스턴스 동기화 예시