[21.07.28] Thread와 파일입출력

yed·2021년 7월 28일

Thread 클래스

쓰레드는 하나의 작업을 순차적으로 진행하는 실행코드를 의미합니다.
보통의 싱글스레드는 반복문같은 소스코드랑 비슷한 형태지만 이런 스레드가 두 개 이상인 멀티스레드는 확연한 성능차이를 보여줍니다!
멀티 쓰레드 프로그램은 하나의 프로그램에서 여러 개의 스레드를 동작하는 프로그램인데요 카카오톡같은 채팅프로그램은 채팅과 파일전송이 동시에 실행됩니다

Java에서 스레드를 생성하고 사용하는 방법 #1

  1. Thread 클래스를 상속받는 새로운 클래스 정의한다.
  2. 정의한 새로운 클래스안에서 run()메소드를 override한다.
    run() : 스레드가 해야하는 기능을 구현한 메소드.
  3. 정의한 클래스의 인스턴스를 생성한다.
  4. 생성된 인스턴스에서 start() 메소드를 호출한다.
    start() : 스레드가 가져야할 메모리 공간을 확보하고 스케쥴링을 위한 스레드 등록 및 초기화를 한다. 스레드의 run()메소드가 자동으로 실행된다.

메인스레드는 jvm이 관리하는데요 클래스가 Thread를 상속받으면 해당 클래스도 스레드가 되어 jvm과 소통을 할 수 있고 원하는 스레드의 형태를 쓸 수 있게 됩니다.

일반적인 프로그램의 형태는 메인함수에서 소스코드를 읽으면서 클래스의 객체 생성 후 함수를 호출해서 사용하는 방식이었죠? 이때 만약 호출한 함수가 엄청 길면 메인함수는 이 함수가 끝날때까지 아무것도 못하게됩니다. 그래서 스레드를 쓰는건데요

메인함수는 start()를 호출하면 다시 자신의 일을 하고 Thread는 run()을 실행하면서 Thread는 main한테 run()을 통해 결과값만 돌려주고 다시 Thread에서 일하는 겁니다. 즉, 스레드 따로 메인함수 따로 각자 소통은 하면서 동시에 각자의 일이 실행되는것이죠!
메인함수가 Thread클래스의 함수를 호출해서 쓴게 아니라 Thread클래스가 자체적으로 run()을 호출해서 쓰는겁니다. 이런 형태를 비동기식 프로그램이라고 해요. 메인함수는 스레드에게 해야할 일만 전달(start)하고 실행(run)은 스레드에서 하는 것입니다.

일반 프로그램 형태는 main이 호출자고 객체의 함수가 피호출자지만 비동기식은 피호출자(main)가 호출자(Thread)에게 기능만 전달받아 독립적으로 수행하는 방식이라는 점~

class MyThread extends Thread{
	private String msg;
	
	public MyThread(String msg) {
		this.msg=msg;
	}
	
	@Override
	public void run() {
		for(int i=0;i<100;i++) {
			System.out.println(i+" : "+msg);
		}
	}
}

Thread를 상속받아서 스레드를 정의하고 run()을 구현해요.

public static void main(String[] args) {
	MyThread th1=new MyThread("안녕");
	th1.start();
    
    Thread th2=new MyThread("Hello");
	th2.start();
    
    System.out.println("<메인스레드 종료>");
}//end main()

main은 각 스레드의 start()만 불러 할일을 전달하고 main은 다시 실행됩니다. 동시에 스레드도 실행됩니다.

<실행결과>

0 : 안녕
<메인스레드 종료>
0 : Hello
1 : Hello
1 : 안녕
2 : Hello
2 : 안녕
3 : Hello
3 : 안녕
4 : Hello

스레드 두 개가 동시에 실행중인게 보이시나요? 원래 소스코드는 main에서 순서대로 명령어가 실행되었는데 스레드 두 개가 동시에 돌아가면서 main도 따로 실행되고 있음을 알 수 있습니다. 결과를 보면 각 실행에 우선 순위도 따로 없어요
만약 멀티스레드가 같은 자원을 사용할 경우 데드락(무한정 대기상태)에 빠질 수 도 있으니 주의하세요!

sleep()

java.lang.Thread.sleep(long millis)

Thread의 메소드인 sleep()은 millis seconds단위의 시간을 인식하면서 지정한 시간만큼 출력을 정지하는 메소드입니다.

try {
	sleep(1000);
} catch (InterruptedException e) {
	e.printStackTrace();
}

예외처리를 꼭 해야해요!

join()

아까 스레드 출력에는 우선순위가 없었잖아요 만약 순서를 만들고싶다면 스레드에 join()을 설정해주세요. 그렇다면 해당 쓰레드가 종료될때까지 메인스레드가 기다리게 됩니다.

try {
	th1.join();
} catch (InterruptedException e) {
	e.printStackTrace();
}

Runnable 인터페이스

Java에서는 다중 상속을 허지않기때문에 다른 클래스를 이미 상속받고 있는 클래스의 경우, Thread 클래스를 상속받지못하는데요. 그러면 Thread를 만들 수 없는걸까요? 대신 Runnable 인터페이스를 구현하여 Thread를 생성할수있는 방법이 있습니다.

Java에서 스레드를 생성하고 사용하는 방법 #2

  1. Runnable 인터페이스를 구현하는 클래스를 정의한다.
  2. 정의한 클래스에서 run()메소드를 override한다.
  3. 정의한 클래스의 인스턴스를 생성한다.
  4. Runnable 인스턴스를 매개변수로 갖는 Thread 인스턴스를 생성한다.
  5. Thread 인스턴스에서 start() 메소드를 호출한다.

Runnable을 구현한 클래스는 Thread에 접근할 수 있게 됩니다. Thread의 메소드를 쓰고싶다면 Thread.sleep() 처럼 쓰면돼요

@FunctionalInterface인 Runnable 인터페이스는 람다표현식을 사용할 수 있는데요

new Thread(()->{ 
	for(int i=0;i<100;i++) {
		System.out.println("람다!");
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}).start();

인터페이스 변수선언 부분을 생략하고 인터페이스가 들어갈 자리에 바로 람다식을 적용해서 시작할 수도 있습니다~


데이터 입출력

프로그램은 입력장치(마우스,키보드,파일 등)로 데이터를 입력받고 출력장치(모니터,프린터,파일 등)를 통해서 데이터가 출력됩니다.

입력과 출력을 동시에 하는 것도 있는데요 하드웨어로는 스마트폰이 있고 우리가 자주 쓰던 클래스에서도 볼수있습니다. Scanner 인데요!

Scanner(file) < file의 데이터를 가져올때 사용하는 Scanner
Scanner(InputStream) < 데이터를 입력할 때 사용하는 Scanner

제가 자주 접한건 InputStream을 매개변수로 하는 scanner입니다.
new Scanner(System.in); 여기서 in은 inputStream 클래스의 인스턴스입니다. 데이터를 입력받아서 프로그램으로 데이터를 넣으려면 InputStream이 필요해요. InputStream은 데이터를 프로그램이 인식할 수 있도록 변환해주는 역할을 하며 외부 입력장치로부터 데이터를 읽어올수있는 통로가 돼요

출력의 경우 OutputStream이 있는데요 이것도 제가 자주 사용한게 있죠?
System.out.println(); 여기서 out은 PrintStream 클래스의 인스턴스인데요. PrintStream은 OutputStream의 하위클래스입니다! 콘솔화면으로 데이터를 출력하는 통로가 되어줍니다

File 입출력

파일도 데이터를 입력하고 출력할때 사용될 수 있어요!

  • 파일 ⏩ java.io.InputStream ⏩ 프로그램
    FileInputStream 클래스의 read() 메소드를 사용해 파일의 데이터를 읽어옵니다.

  • 파일 ⏪ java.io.OutputStream ⏪ 프로그램
    FileOutputStream 클래스의 write() 메소드를 사용해 데이터를 파일에 씁니다.

두 클래스의 매개변수로는 파일에서 데이터를 읽어오고 내보내는 경로를 지정해줍니다.

A파일에서 데이터를 1바이트씩 읽어오고 B파일에 데이터를 넣으려면 어떻게 해야하는지 코드를 통해 봅시다.

InputStream  in=null;
OutputStream out=null;
		
try { 
	in = new FileInputStream("temp/original.txt");
	out= new FileOutputStream("temp/copy.txt");
			
	int data=0; //read()메소드가 리턴하는 값 저장
	int byteCopied=0; //복사된 파일의 용량
			
	while(true) {
		data=in.read();
		if(data==-1) {
			break;
		}
        out.write(data);
		byteCopied++;
	}
	System.out.println(byteCopied+"바이트 복사됨");
			
} catch (FileNotFoundException e) {
	e.printStackTrace();
} catch (IOException e) {
	e.printStackTrace();
} finally {
	try {
		in.close();
		out.close();
	} catch (IOException e) {
		e.printStackTrace();
	}
}

보시면 예외처리가 엄청많은데요 데이터 입출력 시에는 데이터가 유실될 경우가 발생할 수 있어요. 그래서 꼭 예외처리를 해야 되게끔 설정되어있습니다
그리고 파일도 읽어오는 통로를 열었다면 다시 닫아야해요! finally를 사용해 Stream을 꼭 닫아주세요. 모든 부분에서 열고닫기는 필수!!

read()

파일에서 1바이트씩 데이터를 읽어옵니다. 파일 끝에 도달했을 때 -1을 리턴해요

read(byte[] b)

파일에서 읽은 데이터를 매개변수 배열 b에 저장합니다. 실제로 읽은 바이트 수를 리턴하고 파일 끝을 만나면 -1 리턴합니다.

write()

입력받은 값을 1바이트씩 경로설정한 파일에 씁니다.

write(byte[] b)

매개변수 배열 b의 내용을 한번에 파일에 씁니다

write(byte[] b, index, length)

배열 b의 index번째부터 length 길이까지만 파일에 씁니다.

InputStream in=null;
OutputStream out=null;
		
try {
	in = new FileInputStream("temp/big_text.txt");
	out=new FileOutputStream("temp/big2.txt");
			
	byte[] buffer=new byte[1024*1024];
	//배열 크기 : 1MB = 1024*1024 byte
	int byteCopied=0;
	long startTime=System.currentTimeMillis();
			
	while(true) {
		int result=in.read(buffer);
		System.out.println("result:"+result);
		if(result==-1) {
			break;
		}
		out.write(buffer, 0, result);
		byteCopied += result;
	}
	long endTime=System.currentTimeMillis();
	System.out.println("복사 경과시간:"+(endTime-startTime));
	System.out.println("복사된 바이트 :"+byteCopied);	
    
} catch (Exception e) {
	e.printStackTrace();
}

Buffered[InputStream/OutputStream]

  • 프로그램 ⏪ FileInputStream ⏪ 파일(하드디스크, HDD)
    FileInputStream의 read() 메소드는 HDD에서 직접 접근하기 때문에 속도가 느려요
  • 프로그램 ⏪ BufferedInputStream ⏪ FileInputStream ⏪ 파일(HDD)
    BufferedInputStream의 read() 메소드는 메모리 버퍼에서 읽기 때문에 속도가 빠릅니다 오히려 경유하는게 더 많아졌는데 속도가 빠르다니 신기하져

마찬가지로 BufferedInputStream도 메모리 버퍼를 접근해서 속도가 빨라요! 기능은 똑같다는 점

위에 있는 코드를 버퍼접근으로만 바꿔도 경과시간이 반이나 주는것을 확인할 수 있었습니다.


Tip💡

  • // TODO : 메모. Task에서 확인가능
  • System.currentTimeMillis() : 시스템의 현재시간을 milli-second 단위로 리턴함
profile
6개월 국비과정 기록하기

0개의 댓글