-
TIL #23 - 스레드, I/O, 파일프로그래밍/TIL(국비과정) 2020. 4. 28. 20:34
# 스레드
다시 한 번 더 스레드가 시작하고 끝나는 시점을 상기시켜본다.
스레드가 생성되고 start() 로 인해 운영체제에 스레드가 들어가면 run()의 상태가 된다.
run()의 상태가 된 스레드는 오롯이 CPU 마음대로 운영체제(JVM)에 들어갔다 나왔다를 차례로 수행한다.
class JoinTest implements Runnable{ @Override public void run(){ for(int i = 1; i <= 5; i++){ System.out.println(i); try{ Thread.sleep(500); }catch(InterruptedException e){ e.printStackTrace(); } } } }
스레드는 extends로 Thread를 직접 상속받을 수도 있고 위와 같이 Runnable 인터페이스를 implements 받을 수도 있다.
만약 extends로 Thread를 상속받았다면 JoinTest는 Thread 그 자체가 된다.
하지만 Runnable 인터페이스를 implements 받을 시엔 Thread가 되는 것이 아니고 Thread의 기능을 흉내내는 꼴이 된다.
run() 메소드를 오버라이드해 스레드가 어떻게 돌아갈지 알려준다.
5번 스레드를 돌리며 sleep으로 속도를 조절해주었다.
sleep()으로 인해 Thread는 일시정지 상태가 되고 지정 시간이 다 되는 등의 이유로 InterruptedException이 발생하므로 try catch로 예외 처리를 해주었다.
굳이 try catch로 예외처리를 해준 것은 자신이 만든 메서드가 아닌 Java 자체에서 만들어진 메서드를 Override한 메서드는 throws 로 예외처리 해줄 수 없기 때문이다.
class JoinMain { public static void main(String[] args) throws InterruptedException{ JoinTest jt = new JoinTest(); Thread t = new Thread(jt); System.out.println("스레드 시작"); t.start(); // 스레드 시작 t.join(); // 스레드를 해지 (동시처리 불가) System.out.println("스레드 종료"); } }
JoinMain에서 Thread를 생성해서 JoinTest를 스레드로 만들어준다.
Thread를 생성할 때엔 Thread 생성이 되는 클래스가 Thread가 되지 않는 이상은 괄호 안에 어떤 클래스가 Thread가 될 것인지 명시해주어야한다.
지금은 JoinTest가 Thread가 되고자 함으로 JoinTest 객체를 생성해서 Thread에 넣어준다.
그런 다음 스레드를 시작한다.
스레드가 JoinTest에 있는 run() 메서드로 인해 1부터 5까지 카운팅하는 동안
스레드는 join을 만난다.
join을 만나게 되면 스레드는 해지된다.
동시처리가 불가해지는 것이다.
즉, 하나의 스레드의 일이 끝날 때까지 기다리고 그 스레드의 일이 끝나면 다른 스레드가 시작된다.
그렇기 때문에 1부터 5까지 다 카운팅이 된 후에야 스레드 종료가 print된다
# 스레드를 이용한 타이머 만들기
위와 같은 형태의 타이머를 만든다.
시간이 20초까지 흐르며 시작버튼과 멈춤 버튼에 따라 타이머가 시작하고 멈추는 기능을 수행한다.
단, 시작버튼을 누르면 0으로 초기화되며 시작된다.
import java.awt.Font; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; class TimerTestAgain extends JFrame implements ActionListener, Runnable{ private JLabel label; private JButton startBtn, stopBtn; private boolean bb = true; TimerTestAgain(){ setLayout(null);
먼저 버튼에 따른 이벤트를 구현하기 위해 ActionListener를 implements하고 스레드를 사용할 것이기 때문에 Runnable도 implements 한다.
필드의 경우엔 시간 초를 올려놓을 label, 각 버튼 두개.
실행 상태인지 멈춤 상태인지 구분하기 위해 boolean 타입으로 bb를 두었다.
setLayout은 null로 주어 버튼과 라벨들의 자리를 하나하나 주도록 한다.
label = new JLabel("0"); label.setFont(new Font("Serif", Font.BOLD, 70)); startBtn = new JButton("시작"); stopBtn = new JButton("멈춤"); label.setBounds(70, 30, 100, 100); startBtn.setBounds(200, 40, 60, 30); stopBtn.setBounds(200, 80, 60, 30);
label은 0이 항상 출력되게 해놓는다.
setFont를 통해 label의 폰트 스타일을 변경했다.
시작 버튼과 멈춤 버튼을 만들고 setBounds를 통해 각자 위치를 지정해준다.
add(label); add(startBtn); add(stopBtn);
마지막으로 프레임에 라벨과 버튼들을 추가한다.
setBounds(500, 500, 300, 200); setVisible(true); setDefaultCloseOperation(EXIT_ON_CLOSE);
프레임의 설정이다.
버튼 이벤트를 통해 ActionListener를 implements 했으므로 그에 따른 메서드들을 Override 해주어야한다.
각 버튼을 눌렀을 때 actionListener가 실행되도록 아래와 같이 해준다
startBtn.addActionListener(this); stopBtn.addActionListener(this);
아래는 ActionListener에 따른 actionPerformed 메서드를 오버라이드 한 것이다.
먼저 각 버튼들의 actionPerformed이다.
@Override public void actionPerformed(ActionEvent e){ // 스레드 생성 Thread t; if(e.getSource() == startBtn){ t = new Thread(this); // 스레드 시작 t.start();
스레드를 생성해주고 startBtn이 눌러졌을 때 스레드가 되고자 하는 TimerTest(this)를 스레드로 만들어준 후 start() 한다.
또, 시작버튼이 눌러져 스레드가 시작되었을 때에는 위에서 선언해준 boolean 타입의 bb가 true가 된다.
bb = true;
만약 시작버튼이 눌러져 타이머의 시간이 흐르기 시작했다면 시작 버튼을 비활성화 시키고 멈춤버튼을 활성화 시킨다.
startBtn.setEnabled(false); stopBtn.setEnabled(true);
만약, 사용자가 멈춤을 눌렀을 때에는 아래와 같다.
}else if(e.getSource() == stopBtn){ t = null; // 스레드를 없앤다 bb = false;
스레드를 null로 만들어 스레드를 없앤 후 bb를 false로 한다.
startBtn.setEnabled(true); stopBtn.setEnabled(false); } }
멈춤버튼을 눌렀을 시엔 시작버튼을 활성화, 멈춤 버튼을 비활성화 시킨다.
여기까지가 actionPerformed 메서드이다.
위의 메서드에서 버튼에 따라 스레드가 어떻게 상황에 따라 시작하고 끌지 지정해 주었다.
이제는 run() 메서드에서 스레드가 실행할 코드를 지정해준다.
타이머는 0부터 20까지 카운트 된다.
public void run(){ for(int i = 1; i <= 20; i++){ label.setText(new Integer(i).toString());
그래서 for문으로 20까지 label의 text를 리턴해준다.
하지만 label이 받는 값은 i인데 i는 int값이므로 string 타입으로 바꿔준다
원래는 아래의 코드도 가능하다
label.setText(i + "");
위의 코드는 toString으로 string 타입으로 바꿔주는 형태인데 toString은 Integer라는 wrapper 클래스에는 실행되지만
자료형인 int형에는 실행되지 않는다.
따라서 Integer 객체를 생성해 i를 Integer로 만들어 준 후에 toString()을 적용시킨다.
if(!bb) break;
for문이 돌아가는 동안에도 (카운터 시간이 흘러가는 중에도 )
bb가 true가 아닌 경우가 되면 (멈춤 버튼이 눌려서 false가 되면) for문을 나갈 수 있게 한다.
try{ Thread.sleep(100); // 스레드 속도 늦춘다. }catch(InterruptedException e){ e.printStackTrace(); } }// for
마지막으로 sleep ()을 통해 속도를 조절해 주었다.
for문이 다 돌면 카운터가 끝났다는 뜻이므로 타이머가 다시 시작할 수 있도록 start를 활성화 해준다.
startBtn.setEnabled(true); stopBtn.setEnabled(false); }
public static void main(String[] args) { new TimerTestAgain(); } }
# 스레드로 공장 돌리기
스레드 코드를 좀 더 보도록 한다.
공장을 하나 만들고 한 메서드는 생산만 하는 쪽, 다른 한 메서드는 소비만 하는 쪽으로 만든다.
이 둘은 스레드를 통해 돌아가고 만약 생산이 충분히 되면 소비를 촉진시키고
소비가 많으면 생산을 재촉한다.
단, 이 두 메서드는 각각 동기화를 시켜줘 동시에 돌아갈 수 없게 만든다.
class Factory{ private int product;
상품을 product로 지정한다.
아래는 생산 만하는 메서드이다.
public synchronized void produce(){
아래는 소비만 하는 메서드이다.
public synchronized void sell(){
먼저 생산 만 하는 메서드를 본다.
생산이 많은 기준은 product가 4개를 초과했을 때부터이다.
만약 product가 4개를 초과했으면 sell()을 불러 소비를 촉진시킨다.
if(product > 4){ System.out.println("생산 중단 :" + product); try{ wait(); }catch(InterruptedException e){ e.printStackTrace(); } }
생산이 중단되고 sell메서드가 실행되면
wait()를 통해 스레드를 일시 정지상태로 만든다.
만약 product가 4개를 넘지 않으면 물건을 계속 만든다.
product++; System.out.println("생산 : " + product); notifyAll();
물건을 만들면서 notifyAll()을 통해 sell 메서드를 깨운다.
이번엔 소비만 하는 sell() 메서드이다.
소비할 물건이 없는 기준은 product가 1개 미만일 경우이다.
그런 경우엔 소비를 중단하고 wait() 메서드가 걸린다.
ublic synchronized void sell(){ if(product < 1){ System.out.println("소비 중단 :" + product); try{ wait(); }catch(InterruptedException e){ e.printStackTrace(); } }
만약 그렇지 않다면 product를 계속 소모시키며 소비를 한다.
그럼과 동시에 notifyAll을 통해 상대 스레드를 깨운다.
product--; System.out.println("소비 : " + product); notifyAll(); } }
이제 생산 라인 소비 라인(?) 을 구축했으니 물건을 생산할 사람과 소비할 사람을 만들어본다.
이 둘은 class로 만드는데 Thread를 직접 extends한다.
또한 위의 생산과 소비 메서드가 들어있는 Factory class의 값을 받아와야하기 때문에 필드값에 factory를 지정한다.
먼저 물건을 생산할 사람 클래스인 Worker 이다
class Worker extends Thread{ private Factory factory;
생성자를 통해 값을 가져온다.
그리고 produce() 메서드를 호출한다
public Worker(Factory factory){ this.factory = factory; }
public void run(){ for(int i = 0; i < 10; i++){ factory.produce(); } } }
소비를 할 사람도 생산을 할 사람처럼 factory의 값을 받아오고 sell 메서드를 호출한다.
class Buyer extends Thread{ private Factory factory; public Buyer(Factory factory){ this.factory = factory; } public void run(){ for(int i = 0; i < 12; i++){ factory.sell(); } } }
마지막으로 메인메서드에서 worker와 buyer를 호출해 스레드를 각각 실행한다.
class FactoryMain { public static void main(String[] args) { Factory f = new Factory(); // 스레드 생성 Worker t = new Worker(f); Buyer b = new Buyer(f); // 스레드 시작 t.start(); b.start(); } }
worker와 buyer모두 Thread를 직접 상속을 받아 그 자체로 thread가 되었기 때문에 위와 같이 생성이 가능하다.
단, factory에서 값을 받고 있기 때문에 factory의 값을 넣어준다.
정리하면 이렇다.
Worker와 Buyer 클래스가 정해진 횟수에 따라 run() 메서드를 통해 Factory 내부에 있는 produce()와 sell()을 호출한다.
Factory 클래스 안에 있는 produce()와 sell() 메서드는 정해진 기준에 따라 스레드를 wait()시키고
다음 스레드를 깨운다.
이 둘은 동기화 시켰기 때문에 동시에 실행될 수 없고 차례차례 실행된다.
메인메서드는 Thread를 상속받아 스레드 그 자체가 된 Worker와 Buyer로 스레드를 생성해
스레드를 시작한다.
위의 예제에서 알 수 있는 것은 어디로 어떻게 돌아갈지 모를 스레드이지만 일정한 조건을 통해 번갈아 가면서 움직이게 컨트롤 할 수 있다는 것이다.
# IO 입출력
프로그램에서 데이터가 입출력되는 흐름을 스트림(Stream)이라고 한다.
입력을 받을 때는 입력스트림(InputStream) / 출력을 받을 때에는 출력스트림(OutputStream)
import java.io.DataOutputStream; import java.io.FileOutputStream; import java.io.DataInputStream; import java.io.FileInputStream; import java.io.IOException; class DataStream { public static void main(String[] args) throws IOException{ //DataOutputStream dos = new DataOutputStream(new FileOutputStream("result.txt")); FileOutputStream fos = new FileOutputStream("result.txt"); DataOutputStream dos = new DataOutputStream(fos);
위의 코드는 DataOutputStream으로 들어와서 FileOutputStream으로 나가라는 의미이다.
FileOutputStream으로 나가서 도달할 최종목적지는 result.txt이다.
위의 코드는 한줄로 적어줄 수도 있고 두줄로 나누어줄 수도 있다.
단, 두줄로 생성될 때는 나가는 쪽(안쪽)이 먼저 생성되어야 한다.
이제 result.txt에 데이터를 넣어본다.
dos.writeUTF("양아무개"); dos.writeInt(25); dos.writeDouble(185.3); dos.close();
데이터르르 넣고 dos를 닫아준다.
그런데 result.txt는 디렉토리(폴더)에 생성한 적이 없는 폴더이다.
출력 때는 없으면 자동 생성이지만, 입력 시엔 파일이 없으면 error가 뜬다.
현재는 출력이기 때문에 result.txt라는 파일을 따로 찾는 것이 아닌, 자동 생성이 이루어진다.
위의 과정까지 완료해서 확인해보면 파일이 byte단위로 들어가기 때문에 한글의 경우엔 다 깨져있을 것이다.
이제 파일 내용을 꺼내와본다.
FileInputStream fis = new FileInputStream("result.txt"); DataInputStream dis = new DataInputStream(fis);
위의 경우엔 없는 파일을 꺼내오면 error가 뜬다.
System.out.println("이름 = "+ dis.readUTF()); System.out.println("나이 = "+ dis.readInt()); System.out.println("키 = " + dis.readDouble()); } }
파일을 직접 확인해 보면 내용은 다 깨져있지만 직접 꺼내보면 제대로 출력된다.
FileNotFound와 IOException은 상속관계이다.
IOException 쪽이 부모이므로 따라서 IOException 에러를 잡으면 FileNotFound 에러도 같이 잡힌다.
'프로그래밍 > TIL(국비과정)' 카테고리의 다른 글
TIL #25 - 네트워크, I/O (0) 2020.05.01 TIL #24 - I/O (0) 2020.04.29 TIL #22 - Swing (0) 2020.04.27 TIL #21 - SingleTon, Synchronized (0) 2020.04.24 TIL #20 - Comparable, Exception, 스레드 (0) 2020.04.22