자바
[JAVA] 스레드(Thread)
ChaeHing
2023. 3. 10. 19:22
스레드(Thread)
- 실행중인 애플리케이션을 프로세스(Process)
- 프로세스는 데이터, 컴퓨터 자원, 스레드로 구성
- 프로세스 내에서 실행되는 소스 코드의 실행 흐름을 스레드(Thread)
- 스레드는 데이터와 애플리케이션이 확보한 자원을 활용하여 소스 코드를 실행
- 단 하나의 스레드를 가지는 프로세스를 싱글 스레드 프로세스
- 여러 개의 스레드를 가지는 프로세스를 멀티 스레드 프로세스
메인 스레드(Main Thread)
- 자바 프로그램 실행시 메인이 되는 스레드
- 메인 메서드는 제일 처음 main 메서드를 실행시킨다.
- main 메서드의 코드 처음부터 끝까지 순차적으로 실행하여, main 메서드의 코드의 끝을 만나면 스레드가 종료된다.
- 중간에 다른 스레드를 생성하지 않고 메인 스레드로만 프로세스를 실행하는 경우 싱글 스레드 프로세스
멀티 스레드(Multi Thread)
- 하나의 프로세스는 여러개의 스레드를 가질 수 있다.
- 여러개의 스레드를 가지는 경우 멀티 스레드 프로세스 이다.
- 여러개의 스레드를 가진경우 여러 스레드가 동시의 작업을 할 수 있다. 이를 멀티 스레딩이라고 한다.
- 병렬 작업이 가능하다.
- 하나의 애플리케이션에서 여러개의 작업을 동시에 가능하게 해준다.
- 메신저 프로그램에서 음악을 들으며 메시지를 보낼수 있다.
작업 스레드의 생성과 실행
- 스레드를 생성한다는 것은 메인 스레드외 별도의 작업스레드를 생성한다는 것을 의미한다.
- 자바는 객체지향 언어이기 때문에 클래스 안에 run()이라는 메서드를 정의해야 한다.
작업 스레드 생성 방법 두가지
- Runnable 인터페이스를 구현한 객체에서 run()을 구현하여 스레드를 생성
- Thread 클래스를 상속받은 하위클래스에서 run()을 구현하여 스레드를 생성
public class ThreadEx {
public static void main(String[] args) {
// 1.
Runnable task1 = new ThreadTask1(); // Runnable 인터페이스 구현한 객체 생성
Thread thread1 = new Thread(task1); // 구현 객체를 Thread 클래스로 인스턴스화 (스레드 생성)
thread1.start(); // 작업 스레드 실행 -> run()을 실행 시킨다.
// 2.
ThreadTask thread2 = new ThreadTask(); // Thread를 상속받은 클래스 인스턴스화
thread2.start(); // 작업 스레드 실행 -> run()을 실행 시킨다.
for (int i=0; i<100; i++){
System.out.print("@"); // @ 100개 찍기
}
}
}
// 1. Runnable 인터페이스 구현 클래스
class ThreadTask1 implements Runnable{
@Override
public void run() { // run()메서드 생성
for (int i=0; i<100; i++){
System.out.print("*"); // * 100개 찍기
}
System.out.println(Thread.currentThread().getName());
}
}
// 2. Thread 클래스 상속 클래스
class ThreadTask2 extends Thread{
public void run(){ // run()메서드 생성
for (int i=0; i<100; i++){
System.out.print("#"); // # 100개 찍기
}
System.out.println(Thread.currentThread().getName());
}
}
/*
****************************************************************
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@******##################******
***********************##########################################
###########
*/
- 코드 내용상 순차적으로 실행됬다면 *, @, # 이 100개씩 순서대로 찍혀야 하지만 출력 내용을 보면 기호들이 뒤섞임
- 작업 스레드를 생성하여 프로그램을 실행 했기 때문에 작업들이 동시에 처리
- 메인스레드와 작업스레드가 병렬로 실행
익명 객체로 Thread 생성
Thread thread3 = new Thread(new Runnable() { // 익명 구현 객체
@Override
public void run() {
for (int i=0; i<100; i++){
System.out.print("*");
}
}
});
thread3.start();
Thread thread4 = new Thread() { // 익명 하위 객체
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
};
thread4.start();
스레드 이름
- main 스레드는 "main"
- 그외 추가적으로 생성하는 스레드 이름은 "Thread-n" 으로 생성 된다.
- 스레드들의 이름을 불러오고, 설정 할 수 있다.
스레드의 이름 조회
- 스레드참조명.getName()
Thread thread3 = new Thread(new Runnable() { // 익명 구현 객체
@Override
public void run() {
System.out.println("thread3");
}
});
thread3.start();
System.out.println("thread3 name : "+thread3.getName()); // thread3 이름 출력
Thread thread4 = new Thread() { // 익명 하위 객체
public void run() {
System.out.println("thread4");
}
};
thread4.start();
System.out.println("thread4 name : "+thread4.getName()); // tread4 이름출력
/*
thread3
thread3 name : Thread-0
thread4
thread4 name : Thread-1
*/
스레드 이름 설정
- 스레드참조명.setName()
Thread thread3 = new Thread(new Runnable() { // 익명 구현 객체
@Override
public void run() {
System.out.println("thread3");
}
});
thread3.setName("Thread3!!"); // thread3 이름 설정
thread3.start();
System.out.println("thread3 name : "+thread3.getName()); // thread3 이름 출력
Thread thread4 = new Thread() { // 익명 하위 객체
public void run() {
System.out.println("thread4");
}
};
thread4.setName("Thread4!!"); // thread4 이름 설정
thread4.start();
System.out.println("thread4 name : "+thread4.getName()); // tread4 이름출력
/*
thread3
thread3 name : Thread3!!
thread4
thread4 name : Thread4!!
*/
스레드 인스턴스의 주소값 얻기
- 인스턴스화된 스레드의 참조명이 아닌 현재 실행중인 스레드의 이름을 가져 오는방법
- Thread.currentThread() -> Thread 클래스의 정적메서드
Thread thread5 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()); // 현재 스레드 이름 출력
}
});
thread5.start();
System.out.println(Thread.currentThread().getName()); // 현재 스레드 이름 출력
/*
main
Thread-0
*/
//run() 블럭안에서는 main 스레드 상태이므로 main
//thread5.start() 이후에 작업스레드가 생성되므로 이후 스레드는 Thread-0이 출력 되었다.
스레드의 동기화
- 두 스레드가 동일한 데이터를 공유하게되는 문제 방지
package Thread;
// 버핏과 머스크가 깐부를 맺어 통장을 공유 하여 서로 돈을 인출하는 프로그램
public class ThreadSyn {
public static void main(String[] args) {
Runnable threadTask3 = new ThreadTask3(); // Runable 인터페이스 구현 객체
Thread thread3_1 = new Thread(threadTask3); // 스레드1 생성
Thread thread3_2 = new Thread(threadTask3); // 스레드2 생성
thread3_1.setName("버핏");
thread3_2.setName("머스크");
thread3_1.start();
thread3_2.start();
}
}
class Acoount { // 계좌 클래스
private int balance; //잔고
public Acoount(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public boolean withdraw(int money){ // 돈 인출 메서드
if (balance >= money) { // 인출하려는 돈보다 잔고가 더 많다면 실행
try {
Thread.sleep(1000); // 스레드를 1초가 멈춤, 문제 상황을 발생시키기 위해 사용
} catch (Exception error) {
}
balance -= money; // 인출하려는 돈
return true; // 인출후 true 반환
}
return false; // 인출이 불가하므로 false 반환
}
}
class ThreadTask3 implements Runnable{ // 쓰레드
Acoount account = new Acoount(100000); // 계좌 객체 생성, 십만달러 입금
public void run(){
while(account.getBalance() > 0){
int money = (int)(Math.random() * 3+1) *10000; // 10000~30000 달러 랜덤 생성
boolean denied = !account.withdraw(money); // 돈 인출 메서드 withdraw() 호출, 결과를 denied의 저장
System.out.println(String.format("Withdraw %d$ By %s. Balance %d$ %s",
money, Thread.currentThread().getName(), account.getBalance(), denied ? "-> DENIED" : "")
// 결과 출력
);
}
}
}
/*
Withdraw 20000$ By 머스크. Balance 70000$
Withdraw 10000$ By 버핏. Balance 70000$
Withdraw 10000$ By 머스크. Balance 40000$
Withdraw 20000$ By 버핏. Balance 40000$
Withdraw 20000$ By 머스크. Balance -10000$
Withdraw 30000$ By 버핏. Balance -10000$
*/
- 예제의 출력 결과를 보면 결과값이 이상하다.
- 처음 머스크가 20000$를 출금했는데 잔고가 70000$ 이다.
- 인출하려는 돈보다 잔고가 적으면 false가 반환되어 "-> DENIED"가 출력되어야하지만
- 출금되어 잔고가 -10000$이 된다.
- 이러한 이유는 Account 객체를 공유하고 있는 두개의 쓰레드가 실행 되어서 그렇다.
- 스레드는 동시작업이 가능하기 때문에 머스크가 출금하는 시점(20000$)에, 버핏도 출금(10000$) 하여서 출력 시점엔잔고가 이미 70000$이되어 출력결과가 70000$이 되었다.
- 같은 원리로 잔고가 -10000$ 된 이유는 먼저 실행된 스레드가 ' balance -= money'를 실행하기도 전에 'if (balance >= money)'에 다른 스레드가 통과를 하여 동시에 잔고를 소비해버렸기 때문이다.
- 이처럼 쓰레드는 동시 작업을 하기 때문에 공유 데이터를 사용하다보면 예기치 못한 문제가 생길 수 있다.
- 이러한 문제를 해결 하기 위해 임계영역과 락(Lock)을 사용한다.
임계영역(Critical Sectcion)과 락(Lock)
- 임계 영역은 오로지 하나의 스레드만 코드를 실행 할 수 있는 코드의 영역을 의미
- 락은 임계 영역을 포함하고 있는 객체에 접근할 수 있는 권한을 의미
- 임계 영역내 코드를 실행하려면 락이 필요하고 락은 한개의 스레드에게만 부여된다.
- 한개의 스레드가 작업을 끝내면 락을 반납하고 다른 스레드가 락을 획득하여 임계 영역 내 코드를 실행한다.
- 바톤 터치와 비슷하다.
임계영역 지정
- sychronized라는 키워드를 사용 한다.
- 메서드 전체를 임계영역으로 지정 할 수도 있고, 특정한 부분을 임계 영역으로 지정 할 수도 있다.
메서드 전체 임계 영역 지정
// 메서드 시그니처에 synchronized 키워드 추가
public synchronized boolean withdraw(int money){ // 메서드 전체의 임계 영역 지정
if (balance >= money) { // 인출하려는 돈보다 잔고가 더 많다면 실행
try {
Thread.sleep(1000); // 스레드를 1초가 멈춤, 문제 상황을 발생시키기 위해 사용
} catch (Exception error) {
}
balance -= money; // 인출하려는 돈
return true; // 인출후 true 반환
}
return false; // 인출이 불가하므로 false 반환
}
특정 부분 임계 영역 지정
// 지정할 영역을 synchronized (this){}로 블럭안에 지정
// this는 현재 객체를 의미함, 스레드 실행시점의 객체
public boolean withdraw(int money) { // 돈 인출 메서드
synchronized (this) { // 특정 부분 임계 영역 지정
if (balance >= money) { // 인출하려는 돈보다 잔고가 더 많다면 실행
try {
Thread.sleep(1000); // 스레드를 1초가 멈춤, 문제 상황을 발생시키기 위해 사용
} catch (Exception error) {
}
balance -= money; // 인출하려는 돈
return true; // 인출후 true 반환
}
return false; // 인출이 불가하므로 false 반환
}
}
임계 영역 지정시 결과
Withdraw 10000$ By 버핏. Balance 90000$
Withdraw 30000$ By 머스크. Balance 60000$
Withdraw 10000$ By 버핏. Balance 50000$
Withdraw 30000$ By 머스크. Balance 20000$
Withdraw 20000$ By 버핏. Balance 0$
Withdraw 10000$ By 머스크. Balance 0$ -> DENIED
- 정상적으로 출금한 만큼 잔고가 발생
- 잔고보다 많은 돈을 출금하려하면 '-> DENIED' 도 발생