본문 바로가기
Programming/Java프로그래밍및실습

[자프실] 12. 자바 스레드 기초

by Lizardee 2024. 1. 15.
12.1 멀티태스킹
멀티태스킹이란?

: 다수의 작업을 동시에 처리하는 것

 

멀티태스킹 프로그램 사례

: 하나의 프로그램이 하나의 작업(태스크)만 하는 경우가 대부분이지만, 하나의 프로그램이 여러 작업(태스크)을 동시에 실행하는 경우가 있다.

 

스레드와 운영체제
  • 스레드(thread): 운영체제에 의해 관리되는 하나의 태스크
  • 멀티스레딩(multi-threading): 다수의 스레드를 동시에 실행시키도록 응용프로그램을 작성하는 기법

 

멀티태스킹과 멀티스레딩
  • 멀티프로세싱(multi-processing): 하나의 응용프로그램을 여러 개의 프로세스(process)로 구성하여 각 프로세스가 하나의 작업(태스크)을 처리하도록 하는 기법

 

자바 스레드와 JVM
  • 자바 스레드(java thread)
  • 자바 가상기계(JVM: Java Virtual Machine): 운영체제 역할

 


12.2 자바 스레드 만들기
Thread 클래스로 스레드 만들기
  • Thread 클래스를 상속받아 run() 오버라이딩
  • 스레드 객체 생성
  • 스레드 시작: start() 메소드 호출

/*
 * 예제12-1: Thread를 상속받아 1초 단위로 출력하는 타이머 스레드 만들기
 */
import javax.swing.*;
import java.awt.*;

class TimerThread extends Thread{
	private JLabel timerLabel;  // 타이머 값이 출력되는 레이블
	
	public TimerThread(JLabel timerLabel) {
		this.timerLabel = timerLabel;  // 타이머 카운트를 출력할 레이블
	}
	
	// 스레드 코드. run()이 종료하면 스레드 종료
	@Override
	public void run() {
		int n=0;  // 카이머 카운트 값
		while(true) {
			timerLabel.setText(Integer.toString(n));  // 레이블에 카운트 값 출력
			n++;  // 카운트 증가
			try {
				Thread.sleep(1000);   // 1초동안 잠을 잔다.
			}
			catch(InterruptedException e){
				return;  // 예외가 발생하면 스레드 종료
			}
		}
	}
}

public class TimerThreadEx extends JFrame {
	public TimerThreadEx() {
		setTitle("Thread를 상속받은 타이머 스레드 예제");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		Container c = getContentPane();
		c.setLayout(new FlowLayout());
		
		// 타이머 값을 출력할 레이블 생성
		JLabel timerLabel = new JLabel();
		timerLabel.setFont(new Font("Gothic", Font.ITALIC, 80));
		c.add(timerLabel);
		
		// 타이머 스레드 객체 생성. 타이머 값을 출력할 레이블을 생성자에 전달
		TimerThread th = new TimerThread(timerLabel);  // 스레드 객체
		
		setSize(400,400);
		setVisible(true);
		
		th.start();  // 타이머 스레드의 실행을 시작하게 한다.
	}
	
	public static void main(String[] args) {
		new TimerThreadEx();
	}
}

 

Runnable 인터페이스로 스레드 만들기
  • 스레드 클래스 선언: Runnable 인터페이스 구현
  • 스레드 객체 생성
  • 스레드 시작: start() 메소드 호출

/*
 * Runnable 인터페이스를 이용하여 1초 단위로 출력하는 타이머 스레드
 */
import javax.swing.*;
import java.awt.*;

class TimerRunnable implements Runnable{
	private JLabel timerLabel;  // 타이머 값이 출력된 레이블

	public TimerRunnable(JLabel timerLabel) {
		this.timerLabel = timerLabel;  // 초 카운트를 출력할 레이블
	}

	// 스레드 코드. run()이 종료하면 스레드 종료
	@Override
	public void run() {
		int n=0;  // 타이머 카운트 값
		while(true) {
			timerLabel.setText(Integer.toString(n));  // 레이블에 카운트 값 출력
			n++;
			try {
				Thread.sleep(1000);  // 1초동안 잠을 잔다.
			}
			catch(InterruptedException e) {
				return;
			}
		}
	}
}

public class RunnableTimerEx extends JFrame{
	public RunnableTimerEx() {
		setTitle("Runnable을 구현한 타이머 스레드 예제");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		Container c = getContentPane();
		c.setLayout(new FlowLayout());

		// 타이머 값을 출력할 레이블 생성
		JLabel timerLabel = new JLabel();
		timerLabel.setFont(new Font("Gothic", Font.ITALIC, 80));
		c.add(timerLabel);

		// 타이머 스레드로 활용할 Runnable 객체 생성. 타이머 값을 출력할 레이블을 생성자에 전달
		TimerRunnable runnable = new TimerRunnable(timerLabel);
		Thread th = new Thread(runnable);

		setSize(250,150);
		setVisible(true);

		th.start();  // 타이머 스레드가 실행을 시작하게 한다.
	}

	public static void main(String[] args) {
		new RunnableTimerEx();
	}
}

 

main 스레드

: JVM은 자바 응용프로그램을 실행하기 직전, 스레드를 하나 생성하고, 이 스레드로 하여금 main() 메소드를 실행하도록 한다. 이 스레드가 바로 메인 스레드(main 스레드)이고, 실행 시작 주소는 main() 메소드의 첫 코드가 된다.

/*
 * main 스레드 확인과 스레드 정보를 알아내는 코드
 */

public class ThreadMainEx {
	public static void main(String[] args) {
		long id = Thread.currentThread().getId();  // 스레드 ID 얻기
		String name = Thread.currentThread().getName();  // 스레드 이름 얻기
		int priority = Thread.currentThread().getPriority();  // 스레드 우선순위 값 얻기
		Thread.State s = Thread.currentThread().getState();  // 스레드 상태 값 얻기
		
		System.out.println("현재 스레드 이름 = " + name);
		System.out.println("현재 스레드 ID = " + id);
		System.out.println("현재 스레드 우선순위 값 = " + priority);
		System.out.println("현재 스레드 상태 = " + s);
	}
}

 


12.3 스레드 종료
스스로 종료
강제 종료

진동하는 스레드와 스레드의 강제 종료

/*
 * 예제 12-4: 진동하는 스레드와 스레드의 강제 종료
 */
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Random;

public class VibratingFrame extends JFrame implements Runnable {
	private Thread th;  // 진동하는 스레드
	
	public VibratingFrame() {
		setTitle("진동하는 프레임 만들기");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setSize(200,200);
		setLocation(300,300);
		setVisible(true);
		
		getContentPane().addMouseListener(new MouseAdapter() {
			public void mousePressed(MouseEvent e) {
				if(!th.isAlive())  // 이미 스레드가 종료했다면 그냥 리턴
					return;
				th.interrupt();  // 진동 스레드에게 InterruptedException을 보내 강제 종료
			}
		});
		
		th = new Thread(this);  // 진동 스레드 객체 생성
		th.start();  // 진동 시작
	}
	
	@Override
	public void run() {  // 프레임의 진동을 일으키기 위해 20ms마다 프레임의 위치를 랜덤하게 이동
		Random r = new Random();  // 진동할 위치를 랜덤하게 발생시킬 랜덤 객체 생성
		while(true) {
			try {
				Thread.sleep(20);  // 20ms 잠자기
			}
			catch(InterruptedException e) {
				return;  // 리턴하면 스레드 종료
			}
			int x = getX() + r.nextInt()%5;  // 새 위치 x
			int y = getY() + r.nextInt()%5;  // 새 위치 y
			setLocation(x,y);
		}
	}
	
	public static void main(String[] args) {
		new VibratingFrame();
	}
}

 


12.4 스레드 동기화
스레드 동기화의 필요성

: 멀티스레드 프로그램이 실행될 때, 다수의 스레드가 공유 데이터를 동시에 접근하는 경우가 발생한다. 특히 다수의 스레드가 동시에 공유 데이터 값을 변경시키는 경우, 공유 데이터 값이 정상적으로 변경되지 않게 된다.

  • 스레드 동기화(thread synchronization): 공유 데이터를 동시 접근하는 여러 스레드에 의해 공유데이터 값이 비정상적으로 유지되지 않도록 스레드의 실행을 제어하는 기술

 

자바 스레드 동기화를 위한 synchronized 블록

자바 스레드 동기화를 위한 synchronized 블록

 

synchronized 활용 사례

/*
 * 예제12-5: 두 스레드가 공유 프린터 객체를 통해 동시에 출력하는 경우, synchronized 블록 지정
 */

public class SynchronizedEx {
	public static void main(String[] args) {
		SharedPrinter p = new SharedPrinter();  // 공유데이터 생성
		
		String [] engText = {"Wise men say, ",
				"only fools rush in",
				"But I can't help, ",
				"falling in love with you",
				"Shall I stay? ",
				"Would it be a sin?",
				"If I can't help, ",
				"falling in love with you"
		};
		String [] korText = {"동해물과 백두산이 마르고 닳도록, ",
				"하느님이 보우하사 우리나라 만세",
				"무궁화 삼천리 화려강산, ",
				"대한 사람 대한으로 길이 보전하세",
				"남산 위에 저 소나무, 철갑을 두른 듯",
				"바람서리 불편함은 우리 기상일세.",
				"무궁화 삼천리 화려강산, ",
				"대한 사람 대한으로 길이 보전하세"
		};
		
		// 스레드 생성시 공유 프린터의 주소를 알려준다. 두 스레드는 공유 프린터 p에 동시에 접근한다.
		Thread th1 = new WorkerThread(p, engText);  // 영문 출력 스레드
		Thread th2 = new WorkerThread(p, korText);  // 국문 출력 스레드
		
		// 두 스레드를 실행시킨다.
		th1.start();
		th2.start();
	}
}

class SharedPrinter{  // 두 WorkerThread 스레드에 의해 동시 접근되는 공유 프린터
	// synchronized를 생략하면 한글과 영어가 한 줄에 섞여 출력되는 경우가 발생한다.
	synchronized void print(String text) {
		for(int i=0; i<text.length(); i++)
			System.out.print(text.charAt(i));
		System.out.println();
	}
}

class WorkerThread extends Thread{  // 스레드 클래스
	private SharedPrinter p;  // 공유프린터 주소
	private String [] text;
	
	public WorkerThread(SharedPrinter p, String [] text) {  // 공유 프린터 주소와 텍스트 전달받음
		this.p = p;
		this.text = text;
	}
	
	@Override
	public void run() {
		for(int i=0; i<text.length; i++)  // 한 줄씩 출력
			p.print(text[i]);  // 공유 프린터에 출력
	}
}

 

wait()-notify()를 이용한 스레드 동기화

wait()-notify()를 이용한 스레드 동기화

/*
 * 예제12-6: wait(), notify()를 이용하여 키 입력으로 바 채우기
 */

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

class MyLabel extends JLabel {
    private int barSize = 0;
    private int maxBarSize;

    public MyLabel(int maxBarSize) {
        this.maxBarSize = maxBarSize;
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.setColor(Color.MAGENTA);
        int width = (int) (((double) (getWidth())) / maxBarSize * barSize);
        if (width == 0)
            return;
        g.fillRect(0, 0, width, this.getHeight());
    }

    synchronized void fill() {
        if (barSize == maxBarSize) {
            try {
                wait();
            } catch (InterruptedException e) {
                return;
            }
        }
        barSize++;
        repaint();
        notify();
    }

    synchronized void consume() {
        if (barSize == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                return;
            }
        }
        barSize--;
        repaint();
        notify();
    }
}

class ConsumerThread extends Thread {
    private MyLabel bar;

    public ConsumerThread(MyLabel bar) {
        this.bar = bar;
    }

    @Override
    public void run() {
        while (true) {
            try {
                sleep(100);
                bar.consume();
            } catch (InterruptedException e) {
                return;
            }
        }
    }
}

public class TabAndThreadEx extends JFrame {
    private MyLabel bar = new MyLabel(100);

    public TabAndThreadEx(String title) {
        super(title);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Container c = getContentPane();
        c.setLayout(null);
        bar.setBackground(Color.ORANGE);
        bar.setOpaque(true);
        bar.setLocation(20, 50);
        bar.setSize(300, 20);
        c.add(bar);

        c.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                bar.fill();
            }
        });
        setSize(350, 200);
        setVisible(true);

        c.setFocusable(true);
        c.requestFocus();
        ConsumerThread th = new ConsumerThread(bar);
        th.start();
    }

    public static void main(String[] args) {
        new TabAndThreadEx("아무키나 빨리 눌러 바 채우기");
    }
}