스레드(1)
◎ 프로세스
-> 운영체제에서 실행 중인 하나의 애플리케이션
◎ 스레드
-> 프로세스 내부에서 코드의 실행 흐름
-> 한 프로세스 내에 스레드가 2개 존재하면 2개의 코드 실행 흐름이 생긴다.
◎ 멀티 스레드 vs 멀티 프로세스
-> 멀티 프로세스는 운영체제에서 할당 받은 자신의 메모리를 가지고 실행하기 때문에 각 프로세서는 서로 독립적이다. 따라서 하나의 프로세스에서 오류가 발생해도 다른 프로세스에 영향을 미치지 않는다.
-> 멀티 스레드는 하나의 프로세스 내부에서 동시에 생성되기 때문에 하나의 스레드에서 예외가 발생하면 다른 스레드에도 영향을 미친다.
☞ 멀티 프로세스는 워드 작업 동시에 메모장을 켜고 작성을 하는 것을 생각하면 되고, 멀티 스레드는 하나의 프로그램에서 채팅 기능과 동시에 파일 전송 기능을 제공하는 메신저를 생각하면 된다.
◎ 메인 스레드
-> 자바의 모든 애플리케이션은 메인 스레드가 main()메소드를 실행하면서 시작한다. 메인 스레드는 main()메소드의 첫 줄 부터 아래로 순서대로 실행하고, main()메소드의 마지막 코드를 실행하거나, return문을 만나면 종료된다.
-> 메인 스레드는 작업 스레드들을 만들어 병렬로 코드를 실행하는 멀티 스레드를 생성해서 멀티 태스킹 작업을 수행할 수 있다.
-> 싱글 스레드
-> 멀티 스레드
☞ 멀티 스레드로 실행하는 애플리케이션을 개발하려면 몇 개의 작업을 병렬로 실행할지 결정하고 각 작업별 스레드를 생성한다.
☞ 자바는 작업 스레드도 객체로 생성된다. 따라서 클래스가 필요하다. java.lang.Thread 클래스를 직접 객체화하여 사용하는 것도 가능하고, Thread클래스를 상속해서 하위 클래스를 만들어 생성하는 것도 가능하다.
☞ java.lang.Thread로 작업 스레드 객체를 생성하려면 Thread thread = new Thread(Runnable a);로 Runnable을 매개값으로 하는 생성자를 호출한다.
☞ Runnable은 작업 스레드가 실행할 수 있는 코드를 가지고 있는 객체다. 또한 인터페이스 타입이기 때문에 구현 객체를 만들어 대입한다. Runnable은 run()메소드가 정의되어 있기 때문에 구현 클래스에 run()을 재정의하여 작업 스레드를 작성해야 한다.
☞ thread.start()가 호출되면, 작업 스레드는 매개값으로 받은 Runnable의 run()메소드를 실행하면서 작업을 처리한다.
-> 메인 스레드만 이용
import java.awt.Toolkit;
public class ThreadTest1 {
public static void main(String[] args) {
Toolkit toolkit = Toolkit.getDefaultToolkit(); // Toolkit객체 얻기
for(int i = 0; i<4; i++) {
toolkit.beep();
try {
Thread.sleep(1000); // 1초간 일시 정지
}catch(Exception e) {}
}
String[] s = new String[4];
s[0] = "하나";
s[1] = "둘";
s[2] = "셋";
s[3] = "야";
for(int i = 0; i<4; i++) {
System.out.println(s[i]);
try {
Thread.sleep(1000);
}catch(Exception e) {}
}
}
}
☞ 위 코드는 1초 주기로 비프음을 발생시키면서 동시에 출력 작업을 하는 코드이다. 위와 같이 메인 스레드만 작성하면 비프음이 먼저 나오고, 출력을 한다.
-> 결과(영상)
☞ 비프음 발생과 동시에 출력을 하려면 하나의 작업은 메인 스레드가 아닌 작업 스레드에서 실행해야한다.
-> 비프음이 나오는 작업
import java.awt.Toolkit;
public class BeepTest implements Runnable{
@Override
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i = 0; i<4; i++) {
toolkit.beep();
try {
Thread.sleep(1000);
}catch(Exception e) {}
}
}
}
-> 메인 스레드와 작업 스레드 동시 실행
public class ThreadTest2 {
public static void main(String[] args) {
Runnable beepTest = new BeepTest();
Thread thread = new Thread(beepTest);
thread.start();
String[] s = new String[4];
s[0] = "하나";
s[1] = "둘";
s[2] = "셋";
s[3] = "야";
for(int i = 0; i<4; i++) {
System.out.println(s[i]);
try {
Thread.sleep(1000);
}catch(Exception e) {}
}
}
}
-> 결과(영상)
◎ Thread 하위 클래스로부터 생성
-> 작업 스레드가 실행할 작업을 Runnable로 만들지 않고, Thread의 하위 클래스로 정의하여 작업할 수 있다.
public class Thread extends Thread{
@Override
public void run() {
실행 코드
}
}
Thread thread = new Thread();
◎ 스레드 이름
-> 스레드는 디버깅할 때 어떤 스레드가 어떤 작업을 하는지 알아볼 목적으로 사용되는 이름이 있다.
-> 메인 스레드는 main이라는 이름을 가지고 있고, 직접 생성한 스레드는 Thread-n이라는 이름으로 생성된다.
-> thread.setName("스레드 이름")으로 이름을 변경할 수 있다.
-> thread.getName();으로 스레드의 이름을 알 수 있다.
-> setName()과 getName()은 Thread클래스의 인스턴스 메소드로 스레드 객체의 참조가 필요하다. 만약 스레드 객체의 참조를 가지고 있지 않다면, Thread클래스의 정적 메소드 currentThread()를 이용해 현재 스레드의 참조를 얻을 수 있다.
☞ Thread thread = Thread.currentThread();
-> 예제
public class ThreadName {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println("프로그램 시작 스레드 이름 : " + thread.getName());
ThreadA threadA = new ThreadA();
System.out.println("작업 스레드 이름 : " + threadA.getName());
threadA.start();
ThreadB threadB = new ThreadB();
System.out.println("작업 스레드 이름 : " + threadB.getName());
threadB.start();
}
}
-> ThreadA 클래스
public class ThreadA extends Thread {
public ThreadA() {
setName("ThreadA");
}
public void run() {
for(int i = 0; i<2; i++) {
System.out.println(getName() + "...");
}
}
}
-> ThreadB 클래스
public class ThreadB extends Thread {
public void run() {
for(int i = 0; i<2; i++) {
System.out.println(getName() + "...");
}
}
}
-> 결과
☞ ThreadA는 setName()으로 ThreadA라는 이름으로 변경하였고, ThreadB는 변경을 하지 않아 기본 이름인 Thread-1로 출력되었다.
◎ 공유 객체 사용과 문제점
-> 멀티 스레드 프로그램에서 스레드들이 객체를 공유해서 사용할 때 스레드A가 사용하던 객체를 스레드B가 사용하여 값이 변경될 수 있어 기대와 다른 결과가 나올 수 있다.
-> 공유 객체 사용 예제
public class MainThread {
public static void main(String[] args) {
Calculator cal = new Calculator();
UserA userA = new UserA();
userA.setCal(cal);
userA.start();
UserB userB = new UserB();
userB.setCal(cal);
userB.start();
}
}
-> 공유 객체
public class Calculator {
private int memory;
public int getMemory() {
return memory;
}
public void setMemory(int money) {
this.memory = money;
try {
Thread.sleep(2000);
}catch(Exception e) {}
System.out.println(Thread.currentThread().getName() + " : " + this.memory);
}
}
-> UserA 스레드
public class UserA extends Thread{
private Calculator cal;
public void setCal(Calculator cal) {
this.setName("UserA");
this.cal = cal;
}
public void run() {
cal.setMemory(100);
}
}
-> UserB 스레드
public class UserB extends Thread{
private Calculator cal;
public void setCal(Calculator cal) {
this.setName("UserB");
this.cal = cal;
}
public void run() {
cal.setMemory(50);
}
}
-> 결과
☞ 결과로 UserA : 100, userB : 50을 기대했지만 둘 다 50이 출력이 되었다. 이는 공유 객체를 사용하여 값이 변경이 되었기 때문이다.
◎ 동기화 메소드
-> 스레드가 사용 중인 객체를 다른 객체가 사용하지 못하도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸어 다른 스레드가 사용하지 못하게 한다.
-> 멀티 스레드 프로그램에서 하나의 스레드만 실행할 수 있는 코드 영역을 임계영역이라 한다. 자바는 임계 영역을 지정하기 위해 동기화 메소드를 사용한다. 스레드가 객체 내부의 동기화 메소드를 실행하면 객체에 잠금을 걸어 다른 스레드가 동기화 메소드를 사용하지 못하게 한다.
-> 메소드 선언할 때 synchronized를 붙이면 동기화 메소드가 된다.
public synchronized void setMemory(int money) {
...
}
-> 동기화 메소드 사용
public class Calculator {
private int memory;
public int getMemory() {
return memory;
}
public synchronized void setMemory(int money) {
this.memory = money;
try {
Thread.sleep(2000);
}catch(Exception e) {}
System.out.println(Thread.currentThread().getName() + " : " + this.memory);
}
}
-> 결과