멀티 프로세싱
- 하나의 응용프로그램이 여러 개의 프로세스를 생성하고, 각 프로세스가 하나의 작업을 처리하는 기법입니다.
- 각 프로세스는 독립된 메모리 영역을 보유하고 실행합니다.
- 프로세스들 사이의 변수를 공유할 순 없습니다.
- 프로세스 사이의 문맥 교환에 따른 과도한 오버헤드와 시간 소모의 문제점이 있습니다. ( 브라우저의 중복 실행 )
멀티 스레딩
- 하나의 응용프로그램이 여러 개의 스레드를 생성하고, 각 스레드가 하나의 작업을 처리하는 기법입니다.
- 모든 스레드가 응용프로그램 내의 자원과 메모리를 공유하므로 통신 오버헤드가 크지 않고, 문맥 교환이 빠릅니다.
- 현재 대부분의 운영체제가 멀티스레딩을 기본으로 하고 있습니다.
이와 같이 웹 서버시스템에서 멀티 스레드를 사용할 수 있습니다.
자바 스레드와 JVM
JVM에 의해 스케쥴되는 실행단위의 코드 블럭을 자바 스레드라고 합니다. 스레드의 생명 주기는 JVM에 의해 관로됩니다.
또한 응용 프로그램은 하나 이상의 스레드로 구성 가능합니다.
자바에서 스레드는 java.lang.THread클래스를 상속받거나. java.lang.Runnable인터페이스를 구현하여 스레드를 작성할 수있습니다.
Java 실습
먼저 스레드 1개로 beep음을 들려주고 출력으로 "띵"이라는 문구를 출력하게 해 보겠습니다.
package com.company.Thread;
import javax.tools.Tool;
import java.awt.*;
public class Beef {
public static void main(String[] args) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i = 0; i < 5; i++) {
toolkit.beep();
try {
Thread.sleep(1000);
} catch(Exception e) {
}
}
for(int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(1000);
} catch(Exception e) {
}
}
}
}
당연하지만 소리가 1초 간격으로 5번 들린 후에 비로소, "띵"이 5번 1초 간격으로 출력되게 됩니다.
그 다음에는 스레드 2개로 작업을 분리해 보도록 하겠습니다.
package com.company.Thread;
import java.awt.*;
public class Beep2 {
public static void main(String[] args) {
Thread th = new BeepTask();
th.start();
for(int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(1000);
} catch(Exception e) {
}
}
}
}
class BeepTask extends Thread {
@Override
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i = 0; i < 5; i++) {
toolkit.beep();
System.out.println("beep");
try {
Thread.sleep(1000);
} catch(Exception e) {
}
}
}
}
다음과 같이 두개의 스레드가 병렬적으로 작업을 수행하는 것을 보실 수 있습니다.
이제 Java GUI와 결합해서 스레드를 활용해 보도록 하겠습니다.
TimerThread.java
package com.company.Thread;
import javax.swing.*;
public 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);
} catch (InterruptedException e) {
return;
}
}
}
}
ThreadTimerEx.java
package com.company.Thread;
import javax.swing.*;
import java.awt.*;
public class ThreadTimerEx extends JFrame {
public ThreadTimerEx() {
super("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(250, 150);
setVisible(true);
th.start();
}
public static void main(String[] args) {
new ThreadTimerEx();
}
}
이와 같이 main스레드는 죽었지만, 타이머 스레드는 살아 있어서 숫자가 계속 1초마다 1씩 증가하는 것을 보실 수 있을 겁니다.
또한 위와 같이 Thread클래스를 상속받아서 Thread를 구현할 수도 있지만 Runnable인터페이스를 구현해서 스레드를 만들 수도 있습니다.
TImerRunnable.java
package com.company.Thread;
import javax.swing.*;
public 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);
} catch (InterruptedException e) {
return;
}
}
}
}
main 스레드
JVM이 응용프로그램을 실행할 떄 피롤트로 생성되는 스레드로, main()메소드 실행을 시작합니다. 그리고 그후에는 main() 메소드가 종료하면 main스레드를 종료한 겁니다.
main() 메소드 내에서 현제 스레드 정보를 가진 Thread객체를 알아내어 현재 실행중인 스레드에 관한 다양한 정보를 출력해 보도록 하겠습니다.
ThreadMainEx.java
package com.company.Thread;
public class ThreadMainEx {
public static void main(String[] args) {
long id = Thread.currentThread().getId();
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);
}
}
스레드 종료와 타 스레드 강제 종료
interrupt()메소드를 실행해 주면 thread에서 exception이 발생하여 죽게 됩니다.
이를 활용하여 프레임이 심하게 진동하는 GUI프로그램을 만들고, 컨텐츠팬에 마우스를 클랙하면 지동 스레드를 종료시켜 진동이 멈추도록 하는 코드를 작성해 보도록 하겠습니다.
VibratingFrame.java
package com.company.Thread;
import javax.swing.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
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() {
@Override
public void mousePressed(MouseEvent e) {
if(!th.isAlive()) return;
else th.interrupt();
}
});
th = new Thread(this);
th.start();
}
@Override
public void run() { // 프레임의 진동을 일으키기 위해
// 20ms마다 프레임의 위치를 렌덤하게 이동
Random r = new Random();
while(true) {
try {
Thread.sleep(20);
} 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();
}
}
간단히 Runnable인터페이스를 구현해서 그 안에서 바로 Thread객체를 사용하도록 하겠습니다. 프레임의 진동은 위치를 20m마다 조금씩 바꿔서 구현하였고, mouseListener에 MouseAdapter를 달아줘서 만약 스레드가 죽었다면 리턴하고, 죽지 않았다면 interrupt()를 하여 진동하는 스레드를 죽이는 코드를 작성한 것입니다.
스레드 동기화
< 멀티스레드 프로그램 작성시 주의점 >
다수의 스레드가 공유 데이터에 동시에 접근하는 경우, 공유 데이터의 값에 예상치 못한 결과가 발생 가능합니다. 그래서 스레드 동기화하는 기능을 사용합니다.
스레드 동기화란 스레드 사이의 실행순서 제어, 공유데이터에 대한 접근을 원활하게 하는 기법입니다. 이를 위해서는 크게 두가지 방법을 생각해 볼 수 있습니다.
- 공유 데이터를 접근하는 모든 스레드의 한 줄 세우기
- 한 스레드가 공유 데이터에 대한 작업을 끝낼 떄까지 다른 스레드가 대기 하도록 함
전자의 경우 synchronized키워드로 동기화 블록을 지정해 줄 수 있습니다. 후자의 경우는 wait()-notify()메소드로 스레드의 실행 순서를 제어해 줄 수 있습니다.
왼쪽과 같은 상황을 오른쪽과 같이 바꾸고자 함이 목표가 되겠습니다.
synchronized 블록 지정
synchronized키워드는 스레드가 독점적으로 실행해야 하는 부분(동기화 코드)를 표시하는 키워드입니다. 메소드 전체나 코드 블록을 synchronized로 감싸서 사용 가능합니다.
synchronized블록이 실행될 때, 먼저 실행한 스레드가 모니터 소유가 됩니다. 여기서 모니터란 해당 객체를 독점적으로 사용할 수 있는 권한을 말하며, 모니터를 소유한 스레드가 모니터를 내놓을 때까지 다른 스레드는 대기하게 됩니다.
SynchronizedEx.java
package com.company.Thread;
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 = { "동해물과 백두산이 마르고 닳도록, ",
"하느님이 보우하사 우리 나라 만세",
"무궁화 삼천리 화려강산, ",
"대한 사람 대한으로 길이 보전하세",
"남산 위에 저 소나무, 철갑을 두른 듯",
"바람서리 불변함은 우리 기상일세.",
"무궁화 삼천리 화려강산, ",
"대한 사람 대한으로 길이 보전하세" };
Thread th1 = new WorkerThread(p, engText);
Thread th2 = new WorkerThread(p, korText);
// 두 스레드를 실행시킨다.
th1.start();
th2.start();
}
}
// 두 WorkerThread스레드에 의해 동시 접근되는 공유 프린터
class SharedPrinter {
// 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;
}
// 스레드는 반복적으로 공유 프린터에 10번 접근 text[]출력
@Override
public void run() {
for(int i = 0; i < text.length; i++) { // 한 줄씩 출력
p.print(text[i]); // 공유 프린터에 출력
}
}
}
다음과 같이 아무 처리 없이 하면 thread끼리 순서가 뒤죽박죽이 되어서 출력 순서가 엉망이 되버립니다. 하지만 print 메소드앞에 synchronized키워드를 붙여서 synchronized블록 지정을 해주면 다음과 같이 출력이 바뀌게 됩니다.
package com.company.Thread;
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 = { "동해물과 백두산이 마르고 닳도록, ",
"하느님이 보우하사 우리 나라 만세",
"무궁화 삼천리 화려강산, ",
"대한 사람 대한으로 길이 보전하세",
"남산 위에 저 소나무, 철갑을 두른 듯",
"바람서리 불변함은 우리 기상일세.",
"무궁화 삼천리 화려강산, ",
"대한 사람 대한으로 길이 보전하세" };
Thread th1 = new WorkerThread(p, engText);
Thread th2 = new WorkerThread(p, korText);
// 두 스레드를 실행시킨다.
th1.start();
th2.start();
}
}
// 두 WorkerThread스레드에 의해 동시 접근되는 공유 프린터
class SharedPrinter {
// 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;
}
// 스레드는 반복적으로 공유 프린터에 10번 접근 text[]출력
@Override
public void run() {
for(int i = 0; i < text.length; i++) { // 한 줄씩 출력
p.print(text[i]); // 공유 프린터에 출력
}
}
}
다음과 같이 동기화가 잘 수행된 것을 보실 수 있습니다.
또한 공유 겍체에 대한 문제에 대한 예시를 하나 더 들어보겠습니다.
다음과 같은 상황을 코딩해 보겠습니다.
Calculator.java
package com.company.Thread;
public class Calculator {
private int memory;
public int getMemory() {
return memory;
}
void setMemory(int memory) {
this.memory = memory;
try {
Thread.sleep(2000);
} catch(InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + ":" + this.memory);
}
public static void main(String[] args) {
Calculator calculator = new Calculator();
User1 user1 = new User1();
user1.setCalculator(calculator);
user1.start();
User2 user2 = new User2();
user2.setCalculator(calculator);
user2.start();
}
}
class User1 extends Thread {
private Calculator calculator;
public void setCalculator(Calculator calculator) {
this.setName("User1");
this.calculator = calculator;
}
@Override
public void run() {
calculator.setMemory(100);
}
}
class User2 extends Thread {
private Calculator calculator;
public void setCalculator(Calculator calculator) {
this.setName("User2");
this.calculator = calculator;
}
@Override
public void run() {
calculator.setMemory(50);
}
}
그럼 다음과 같이 2초 후에 멀티 스레드가 작동하여 공유 객체체에 모두 다 50이 저장되어 있으므로 둘 다 50이 나오는 불상사가 발생하게 됩니다. 저희가 원하는 상황은 다음 그림과 같습니다.
User1의 스레드가 실행한 메소드가 다 끝내고 User2의 스레드가 이 메소드에 접근할 수 있도록 말입니다. 이를 위해서 synchronized코드 블럭을 또 사용해 보도록 하겠습니다.
package com.company.Thread;
public class Calculator {
private int memory;
public int getMemory() {
return memory;
}
synchronized void setMemory(int memory) {
this.memory = memory;
try {
Thread.sleep(2000);
} catch(InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + ":" + this.memory);
}
public static void main(String[] args) {
Calculator calculator = new Calculator();
User1 user1 = new User1();
user1.setCalculator(calculator);
user1.start();
User2 user2 = new User2();
user2.setCalculator(calculator);
user2.start();
}
}
class User1 extends Thread {
private Calculator calculator;
public void setCalculator(Calculator calculator) {
this.setName("User1");
this.calculator = calculator;
}
@Override
public void run() {
calculator.setMemory(100);
}
}
class User2 extends Thread {
private Calculator calculator;
public void setCalculator(Calculator calculator) {
this.setName("User2");
this.calculator = calculator;
}
@Override
public void run() {
calculator.setMemory(50);
}
}
다음과 2초 후에 100이 나오고 2초 후에 50이 나와 4초만에 다음과 같은 결과가 나옴을 확인할 수 있을 겁니다.
'School > Java Programming' 카테고리의 다른 글
[ Java Programming ] IOStream && FileStream (0) | 2022.05.26 |
---|---|
Java Programming - Thread notify-wait() (0) | 2022.05.19 |
Java Programming - Collection Framework (0) | 2022.04.07 |
Java Programming - Basic API Class (0) | 2022.03.24 |
Java Programming - Exception (0) | 2022.03.17 |