[JAVA] 44. 스레드(thread)
프로그램(Program)
- 디스크에 설치되어 있는 실행되기 전 상태의 소프트웨어
프로세스(Process)
- 설치된 프로그램을 실행하여 메모리에 로딩된 상태(=실행중인 프로그램)
- 자원(resources)과 쓰레드로 구성
멀티태스킹(Multi Tasking)
- 프로세스가 여러 개일 때 해당 프로세스들이 동시에 수행되는 것 (정확히는 CPU가 빠른 속도로 프로세스들을 번갈아가면서 수행)
- 예를 들어 동영상을 재생하면서 웹페이지 표시, 음악 재생하는 것
스레드(Thread)
- 프로세스 내에서 작업의 최소 단위
- 프로세스 내에서 실제 작업의 수
- 하나의 프로세스 내에는 최소 한 개의 스레드가 동작함(=Single Thread)
-> 동시에 수행 가능한 작업은 단 한 개 뿐이다
- 하나의 프로세스 내에서 동시에 수행 가능한 작업을 늘리려면 멀티 스레드(Multi Thread)로 구현해야 한다!
ex) 메신저 내에서 파일 전송과 동시에 메시지, 송수신 하는 것
- 하나의 새로운 프로세스를 실행하는 것보다 하나의 새로운 스레드를 생성하는 것이 더 적은 비용이 듦
class NoThread {
String name;
int count;
public NoThread(String name, int count) {
super();
this.name = name;
this.count = count;
}
public void run() {
for(int i = 1; i <= count; i++) {
System.out.println(name + " : " + i);
}
}
}
NoThread nt1 = new NoThread("★A작업★", 1000000);
NoThread nt2 = new NoThread("○B작업○", 500000);
NoThread nt3 = new NoThread("→C작업←", 1000000);
nt1.run();
nt2.run();
nt3.run();
멀티 스레딩(Multi Threading)
- 하나의 프로세스 내에서 두 가지 이상의 작업(Thread)을 동시에 처리
- 실제로 두 가지 이상의 작업을 동시에 수행하는 것은 아니며 CPU가 빠른 속도로 여러 작업을 번갈아가면서 수행하므로 동시에 처리되는 것처럼 느껴짐 = Round Robin 방식이라고 함
- 멀티스레딩으로 처리되는 작업 순서는 고정이 아닌 변동이며 항상 실행 결과는 달라질 수 있다!
= 운영체제 스케줄링에 따라 처리되는 순서 및 횟수가 달라지기 때문
<멀티스레딩 구현 방법>
1. Thread 클래스를 상속받는 서브클래스를 정의하는 방법
1) 멀티스레딩 코드가 포함될 서브클래스에서 Thread 클래스 상속
2) Thread 클래스의 run() 메서드를 오버라이딩하여 멀티스레딩으로 처리할 코드를 기술
3) 멀티스레딩으로 구현된 클래스의 인스턴스 생성
4) 생성된 인스턴스의 start() 메서드를 호출하여 멀티스레딩 시작
2. Runnable 인터페이스를 구현하는 서브클래스를 정의하는 방법
3. 멀티스레딩으로 처리할 위치의 코드에 직접 Thread를 구현하는 방법
// 멀티스레딩을 구현한 서브클래스를 Thread 클래스 상속을 통해 정의
class MyThread extends Thread {
String name;
int count;
public MyThread(String name, int count) {
super();
this.name = name;
this.count = count;
}
// Thread 클래스의 run() 메서드를 오버라이딩하여 멀티스레딩으로 처리할 코드를 기술
@Override
public void run() {
for(int i = 1; i <= count; i++) {
System.out.println(name + " : " + i);
}
}
}
// 멀티스레딩 클래스 인스턴스 생성
MyThread mt1 = new MyThread("★A작업★", 1000000);
MyThread mt2 = new MyThread("○B작업○", 500000);
MyThread mt3 = new MyThread("→C작업←", 1000000);
// Thread t = new MyThread("→C작업←", 1000000); // 업캐스팅
// 멀티스레딩 코드 실행(스레드 시작)
// => run() 메서드를 직접 호출 시 싱글스레딩으로 처리되므로 주의!
// mt1.run();
// mt2.run();
// mt3.run();
// start() 메서드 실행 시 동작
// 1. main() 메서드에서 스레드의 start() 메서드를 호출
// 2. start() 메서드는 스레드가 작업을 수행하는데 사용될 새로운 호출 스택
// (Call Stack)을 생성
// 3. 생성된 호출 스택에 run() 메서드를 호출해서 스레드가 작업을 수행하도록 함
// 4. 호출 스택이 3개이기 때문에 스케줄러가 정한 순서에 의해서 번갈아 가면서 수행
// 반드시 run() 메서드가 아닌 start() 메서드를 호출해야 한다!
mt1.start();
mt2.start();
mt3.start();
TEST
public class Test2 {
public static void main(String[] args) {
// 메세지 전송과 파일 전송을 동시에 수행할 경우
// 1. 싱글쓰레드로 구현하는 경우
SendMessage sm = new SendMessage("안녕하세요", 100);
FileTransfer ft = new FileTransfer("a.java", 100000);
// 파일 전송을 먼저 시작한 후 메세지를 전송한다고 가정
// ft.run(); // 파일 전송이 끝난 후에야
// sm.run(); // 메세지 전송이 시작됨
// ----------------------------------------------------------
// 2. 멀티쓰레드로 구현하는 경우
SendMessageMultiThread smmt = new SendMessageMultiThread("안녕하세요", 100);
FileTrasnferMultiThread ftmt = new FileTrasnferMultiThread("a.java", 100);
ReceiveMessageMultiThred rmmt = new ReceiveMessageMultiThred("반갑습니다", 100);
ftmt.start();
smmt.start();
// rmmt.start(); // Runnable 구현체는 start() 메서드를 직접 호출 불가!
// => Thread 인스턴스에 Runnable 구현체를 파라미터값으로 전달해서 start() 호출
Thread t = new Thread(rmmt);
t.start();
}
}
/*
* 메시지 전송 클래스
* 1. 싱글쓰레드로 구현
* - class명 : SendMessage
* - 멤버변수 : 메시지(msg, 문자열), 카운트(count, 정수형)
* - 파라미터 생성자(String, int)
* - run() 메서드 : 리턴값 없음, 파라미터 없음, count 만큼 msg 출력
* ex) 메시지 전송 - 1 : 안녕하세요
* 메시지 전송 - 2 : 안녕하세요
*/
class SendMessage {
String msg; // 메세지 저장 변수
int count; // 카운트 저장 변수
public SendMessage(String msg, int count) {
super();
this.msg = msg;
this.count = count;
}
public void run() {
// count 횟수 만큼 msg 문자열을 화면에 출력하는 반복문 작성
// ex) 메시지 전송 - 1 : 안녕하세요
// 메시지 전송 - 2 : 안녕하세요
for(int i = 1; i <= count; i++) {
System.out.println("메세지 전송 - " + i + " : " + msg);
}
}
}
/*
* 멀티쓰레드로 구현
* - class명 : SendMessageMultiThread, Thread 클래스 상속
* - 멤버변수 : 메시지(msg, 문자열), 카운트(count, 정수형)
* - 파라미터 생성자(String, int)
* - run() 메서드 오버라이딩, 딜레이 시간 : 0.5초
*/
class SendMessageMultiThread extends Thread {
String msg;
int count;
public SendMessageMultiThread(String msg, int count) {
super();
this.msg = msg;
this.count = count;
}
@Override
public void run() {
// 멀티쓰레딩으로 처리할 작업을 기술
// count 횟수 만큼 msg 문자열을 화면에 출력하는 반복문 작성
// ex) 메시지 전송 - 1 : 안녕하세요
// 메시지 전송 - 2 : 안녕하세요
for(int i = 1; i <= count; i++) {
System.out.println("메세지 전송 - " + i + " : " + msg);
try {
// 현재 쓰레드를 잠시 딜레이시키는 기능(재우는 기능)
// => Thread 클래스의 static 메서드 sleep() 메서드 호출
Thread.sleep(500); // 500ms = 0.5초
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
// ------------------------------------------------------------
/*
* 파일 전송 클래스
* 1. 싱글쓰레드로 구현
* - class명 : FileTransfer
* - 멤버변수 : 파일명(fileName, 문자열), 카운트(count, 정수형)
* - 파라미터 생성자(String, int)
* - run()메서드 : 리턴값 없음, 파라미터 없음, count 만큼 fileName 출력
* ex) 파일 전송 - 1 : a.java
* 파일 전송 - 2 : a.java
*/
class FileTransfer {
String fileName;
int count;
public FileTransfer(String fileName, int count) {
super();
this.fileName = fileName;
this.count = count;
}
public void run() {
// count 횟수 만큼 fileName 문자열을 화면에 출력하는 반복문 작성
// ex) 파일 전송 - 1 : a.java
// 파일 전송 - 2 : a.aava
for(int i = 1; i <= count; i++) {
System.out.println("파일전송 - " + i + " : " + fileName);
}
}
}
/*
* 멀티쓰레드로 구현
* - class명 : FileTransferMultiThread, Thread 클래스 상속
* - 멤버변수 : 파일명(fileName, 문자열), 카운트(count, 정수형)
* - 파라미터 생성자(String, int)
* - run() 메서드 오버라이딩
*/
class FileTrasnferMultiThread extends Thread {
String fileName;
int count;
public FileTrasnferMultiThread(String fileName, int count) {
super();
this.fileName = fileName;
this.count = count;
}
@Override
public void run() {
for(int i = 1; i <= count; i++) {
System.out.println("파일전송 - " + i + " : " + fileName);
try {
// 현재 쓰레드를 잠시 딜레이시키는 기능(재우는 기능0
// => Thread 클래스의 static 메서드 sleep() 메서드 호출
Thread.sleep(500); // 500ms = 0.5초
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// --------------------------------------------------------------------
/*
* 메시지 수신 클래스
* - 멀티쓰레드로 구현
* - class명 : ReceiveMessageMultiThread
* => 단, Thread 클래스를 상속받지 않고 Runnable 인터페이스로 구현
* => Object 클래스를 상속받음
* - 멤버변수 : 메시지(msg, 문자열), 카운트(count, 정수형)
* - 파라미터 생성자(String, int)
* - run() 메서드 오버라이딩
*/
class ReceiveMessageMultiThred extends Object implements Runnable {
String msg;
int count;
public ReceiveMessageMultiThred(String msg, int count) {
super();
this.msg = msg;
this.count = count;
}
@Override
public void run() {
// 멀티쓰레딩으로 처리할 작업을 기술
// count 횟수 만큼 msg 문자열을 화면에 출력하는 반복문 작성
// ex) 메시지 전송 - 1 : 안녕하세요
// 메시지 전송 - 2 : 안녕하세요
for(int i = 1; i <= count; i++) {
System.out.println("메세지 수신 - " + i + " : " + msg);
try {
// 현재 쓰레드를 잠시 딜레이시키는 기능(재우는 기능)
// => Thread 클래스의 static 메서드 sleep() 메서드 호출
Thread.sleep(500); // 500ms = 0.5초
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
class A {}
// 이미 A 클래스를 상속받은 YourThread 클래스에 멀티쓰레딩을 구현해야 하는 경우
// => Thread 클래스를 상속받지 못함(= 다중 상속 불가!)
// => Runnable 인터페이스를 구현(implements) 해야함
class YourThread extends A implements Runnable {
// => The type YourThread must implement the inherited abstract method Runnable.run()
// => run() 메서드를 구현해야 한다!
String name;
int count;
public YourThread(String name, int count) {
super();
this.name = name;
this.count = count;
}
// run() 메서드를 오버라이딩하여 멀티쓰레딩으로 처리할 코드를 기술
@Override
public void run() {
for(int i = 1; i <= count; i++) {
System.out.println(name + " : " + i);
}
}
}
// Runnable 인터페이스 구현체 클래스의 인스턴스 생성
YourThread yt1 = new YourThread("★A작업★", 1000000);
YourThread yt2 = new YourThread("○B작업○", 500000);
YourThread yt3 = new YourThread("→C작업←", 1000000);
// Runnable r = new YourThread("→C작업←", 1000000); // 업캐스팅
// Runnable 구현체 내부에는 start() 메서드가 존재하지 않는다!
// => 애초에 Runnable 인터페이스 내에도 없는 메서드!
// yt1.start(); // 컴파일 에러 발생! 존재하지 않는 메서드
// Thread 클래스의 인스턴스 생성 시 생성자 파라미터에 Runnable 전달 가능
// => Thread 객체의 start() 메서드를 통해 간접적으로 멀티쓰레딩 수행
// Thread t1 = new Thread(yt1);
// Thread t2 = new Thread(yt2);
// Thread t3 = new Thread(yt3);
// -----------------------------------------------------------------
// YourThread 타입 변수(ytn)는 Thread 생성자에 전달 작업 외에 불필요
// => 1회성 변수를 선언하기 보다 생성자에 인스턴스를 직접 전달 가능
Thread t1 = new Thread(new YourThread("★A작업★", 1000000));
Thread t2 = new Thread(new YourThread("○B작업○", 500000));
Thread t3 = new Thread(new YourThread("→C작업←", 1000000));
t1.start();
t2.start();
t3.start();
멀티쓰레딩 구현 코드의 변형
- 실제 프로그래밍 과정에서 더 많이 사용하는 방식
- Thread 또는 Runnable의 구현체를 별도로 정의하지 않고 Thread 클래스의 생성자에 Runnable 인터페이스 객체 생성 코드를 바로 작성
=> Runnable 인터페이스의 임시 객체 형태를 Thread 생성자에 전달
< 기본 문법 >
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// 멀티쓰레딩으로 구현할 코드를 기술
}
});
t.start();
-----------------------------------------------------
// 위의 코드를 압축하여 Thread 객체도 임시 객체 형태로 정의할 경우
Thread 인스턴스 생성 코드 마지막에 .start() 메서드 호출 연결
new Thread(new Runnable() {
@Override
public void run() {
// 멀티쓰레딩으로 구현할 코드를 기술
}
}).start();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// 멀티쓰레딩으로 처리할 코드를 기술
for(int i = 1; i < 100000; i++) {
System.out.println(i + " : A작업");
}
}
});
// 또 다른 멀티쓰레딩 코드 구현
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// 멀티쓰레딩으로 처리할 코드를 기술
for(int i = 1; i < 100000; i++) {
System.out.println(i + " : B작업");
}
}
});
// start() 메서드를 호출하여 멀티쓰레딩 실행
// t.start();
// t2.start();
// ======================================================
// new Thread(new Runnable() {
//
// @Override
// public void run() {
// // 멀티쓰레딩으로 처리할 코드를 기술
// for(int i = 1; i < 100000; i++) {
// System.out.println(i + " : A작업");
// }
// }
// }).start();
//
// new Thread(new Runnable() {
//
// @Override
// public void run() {
// // 멀티쓰레딩으로 처리할 코드를 기술
// for(int i = 1; i < 100000; i++) {
// System.out.println(i + " : B작업");
// }
// }
// }).start();
// }
// =====================================================
// Runnable 인터페이스는 함수형 인터페이스이다
// 따라서, lambda 식으로 변형이 가능
new Thread( () -> {
// 멀티쓰레딩으로 처리할 코드를 기술
for(int i = 1; i < 100000; i++) {
System.out.println(i + " : A작업");
}
}
).start();
new Thread( () -> {
// 멀티쓰레딩으로 처리할 코드를 기술
for(int i = 1; i < 100000; i++) {
System.out.println(i + " : B작업");
}
}
).start();
}
}
쓰레드의 우선 순위
- 스케쥴러가 어떤 쓰레드를 실행하는데 있어서 우선순위에 따라 실행 (단, 절대적인 수치는 아니며 확률적으로 실행 비중을 높여줌)
- Thread 객체의 getPriority() 메서드: 우선순위 조회
Thread 객체의 setPriority() 메서드: 우선순위 설정
=> 우선순위는 1 ~ 10까지 범위의 정수 사용
=> 자주 사용되는 우선순위값을 Thread 클래스의 상수로 제공
MAX_PRIORITY : 10 (높음)
NORM_PRIORITY : 5 (보통) = > 기본값
MIN_PRIORITY : 1 (낮음)
쓰레드를 일시 정지 상태로 만드는 방법
- Thread 클래스의 static 메서드 sleep() 메서드를 호출하면 해당 쓰레드는 잠시 일시 정지 상태가 되어 Waiting Pool로 이동함
- sleep() 메서드 파라미터로 밀리초 단위 또는 나노초 단위의 시간을 설정하면 해당 시간이 만료된 후 다시 실행대기 상태로 변경됨
=> 단, 타이머 동작 중 interrupt() 메서드가 호출되면 타이머가 만료되지 않아도 강제로 쓰레드를 깨움
- sleep() 메서드 호출 시 반드시 try-catch 블록 처리 필수!
- 특정 쓰레드의 실행 시간에 대한 간격을 설정하거나, 우선순위에 따라 실행되지 못하는 쓰레드의 실행 권한을 부여하기 위해 특정 쓰레드들을 잠시 정지시킬 목적으로 사용
- 지정된 시간만큼 정확히 sleep 후에 복귀하는 것이 아니라 쓰레드 상태 저장 및 자원 교환에 필요한 시간이 추가되므로 지정된 시간보다 약간의 딜레이가 포함될 수 있다!
< 기본 문법 >
try {
Thread.sleep(밀리초);
catch(InterruptedException e) {
// interrupt() 메서드가 호출될 때(쓰레드가 깨어날 때)
// 실행할 작업을 기술...
}
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 1; i <= 100; i++) {
System.out.println("☆☆☆☆☆");
// 출력문 실행 후 현재 쓰레드를 0.1초 동안 재우기
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 1; i <= 100; i++) {
System.out.println("●●●●●");
// 출력문 실행 후 현재 쓰레드를 0.1초 동안 재우기
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
System.out.println("t1 쓰레드 우선순위 : " + t1.getPriority());
System.out.println("t2 쓰레드 우선순위 : " + t2.getPriority());
t1.setPriority(Thread.MIN_PRIORITY); // 우선순위를 1로 변경
t2.setPriority(Thread.MAX_PRIORITY); // 우선순위를 10으로 변경
System.out.println("t1 쓰레드 우선순위 : " + t1.getPriority());
System.out.println("t2 쓰레드 우선순위 : " + t2.getPriority());
// t1.start();
// t2.start();
for(int i = 1; i <= 10; i++) {
// 현재 시각 정보 가져오기
LocalTime now = LocalTime.now();
System.out.println(now);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
멀티쓰레드 환경에서의 문제점
- 복수개의 쓰레드에서 동일한 객체 데이터에 접근할 경우 각 쓰레드에서 사용되는 데이터의 일관성이 깨질 수 있다.
=> A 라는 쓰레드가 사용하는 데이터를 B 라는 쓰레드가 동시에 접근하여 변경하는 경우 A 가 의도한 데이터가 아니게 될 수 있다!
- 공유 데이터(객체)에 대한 일관성 문제를 해결하기 위해 데이터베이스의 Lock 개념과 유사한 동기화(Synchronize) 기능을 사용
=> 메서드 또는 특정 코드 블럭에 synchronized 키워드를 지정
< 기본 문법 >
[접근제한자] synchronized 리턴타입 메서드명([파라미터...]) {}
class Account {
int balance;
public Account(int balance) {
super();
this.balance = balance;
}
// 입금 기능
// => 동기화 메서드로 만들기 위해서 리턴타입 앞에 synchronized 키워드 사용
public synchronized int depisit(int amount) {
// public int deposit(int amount) {
// 입금된 금액을 누적 후 잔고 리턴
balance += amount;
return balance;
}
// 출금 기능
// => 동기화 메서드로 만들기 위해서 리턴타입 앞에 synchronized 키워드 사용
public synchronized int withdraw(int amount) {
// public int withdraw(int amount) {
String threadName = Thread.currentThread().getName();
// => 쓰레드명 저장
if(balance >= amount) { // 출금 가능
balance -= amount;
} else {
System.out.println(threadName + "잔액 부족으로 출금 불가!");
amount = 0;
}
System.out.println(threadName + " - 출금된 금액 : " + amount + ", 출금 후 잔액 : " + balance);
return amount;
}
}
// 출금 기능을 전담할 쓰레드 정의
class WithdrawThread extends Thread {
Account account; // 계좌를 저장할 변수
int amount; // 출금할 금액을 입력받아 저장할 변수
public WithdrawThread(String threadName, Account account, int amount) {
super(threadName);
this.account = account;
this.amount = amount;
}
// run() 메서드를 오버라이딩하여 출금 작업 수행
@Override
public void run() {
for(int i = 1; i <= 5; i++) {
try {
Thread.sleep(500); // 0.5초 동안 쓰레드 일시정지
} catch (InterruptedException e) {
e.printStackTrace();
}
// 전달받은 계좌(account)를 사용하여 출금 수행
int money = account.withdraw(amount);
}
}
}
// 은행 계좌 1개 생성 - 잔고 10000원
Account account = new Account(10000);
// 출금 작업을 수행하기 위한 쓰레드 구현 객체 2개 생성
WithdrawThread iBanking = new WithdrawThread("인터넷뱅킹", account, 3000);
WithdrawThread mBanking = new WithdrawThread("모바일뱅킹", account, 3000);
iBanking.start();
mBanking.start();