프로세스는 실행중인 애플리케이션을 의미한다.
프로세스는 데이터, 컴퓨터 자원, 그리고 스레드로 구성되는데, 스레드는 데이터와 애플리케이션이 확보한 자원을 활용하여 소스코드를 실행한다.
스레드는 하나의 코드 실행 흐름이라고 볼 수 있다.
자바 애플리케이션을 실행하면 가장 먼저 실행되는 메서드는 main
메서드이다. 메인 스레드가 main
메서드를 실행시켜준다.
하나의 프로세스는 여러 개의 스레드를 가질 수 있으며, 이를 멀티 스레드 프로세스라고 한다.
여러개의 스레드를 가진다는 것은 여러 스레드가 동시에 작업을 수행할 수 있음을 의미한다.
이르 멀티 스레딩이라고 한다.
작업 스레드가 수행할 코드를 작성하고, 작업 스레드를 생헝하여 실행시키는 것을 의미한다.
run()
을 구현하여 스레드를 생성하고 실행하는 방법run()
을 구현하여 스레드를 생성하고 실행하는 방법run()
을 구현하여 스레드를 생성하고 실행하는 방법public class ThreadExample1 {
public static void main(String[] args) {
}
}
// Runnable 인터페이스를 구현하는 클래스
class ThreadTask1 implements Runnable {
public void run() {
}
}
Runnable 에는 run()
이 정의되어 있기 때문에 반드시 run()을 구현해주어야한다.
그 후
public class ThreadExample1 {
public static void main(String[] args) {
}
}
class ThreadTask1 implements Runnable {
// run() 메서드 바디에 스레드가 수행할 작업 내용 작성
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
}
run()
메서드 바디에 새롭게 생성된 작업 스레드가 수행할 코드를 적어주면 된다.
이제 스레드를 생성한다.
public class ThreadExample1 {
public static void main(String[] args) {
// Runnable 인터페이스를 구현한 객체 생성
Runnable task1 = new ThreadTask1();
// Runnable 구현 객체를 인자로 전달하면서 Thread 클래스를 인스턴스화하여 스레드를 생성
Thread thread1 = new Thread(task1);
// 위의 두 줄을 아래와 같이 한 줄로 축약할 수도 있다.
// Thread thread1 = new Thread(new ThreadTask1());
}
}
class ThreadTask1 implements Runnable {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
}
이와 같이 스레드를 생성할 때는 Runnable 구현 객체를 인자로 전달하면서 Thread 클래스를 인스턴스화 한다.
하지만 스레드를 생성하고 실행하려면 start()
메서드를 아래와 같이 호출해 실행시켜야한다.
public class ThreadExample1 {
public static void main(String[] args) {
Runnable task1 = new ThreadTask1();
Thread thread1 = new Thread(task1);
// 작업 스레드를 실행시켜, run() 내부의 코드를 처리하도록 한다.
thread1.start();
}
}
class ThreadTask1 implements Runnable {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
}
마지막으로 main
메서드에 반복문을 추가한 후 코드를 실행하면
public class ThreadExample1 {
public static void main(String[] args) {
Runnable task1 = new ThreadTask1();
Thread thread1 = new Thread(task1);
thread1.start();
// 반복문 추가
for (int i = 0; i < 100; i++) {
System.out.print("@");
}
}
}
class ThreadTask1 implements Runnable {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
}
이렇게 나온다.
결과 값
@@@@@@@@@@@######@@@@@############################ @#########@@@@@@@@@@@@@@@@############@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@##@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@########################################### Process finished with exit code 0 // 가독성을 위해 50개 마다 개행을 임의로 추가함.
run()
을 구현하여 스레드를 생성하고 실행하는 방법Thread 클래스에 run()
메서드가 정의되어져 있으며, 따라서 run()
메서드를 오버라이딩 해줘야 한다.
public class ThreadExample2 {
public static void main(String[] args) {
}
}
// Thread 클래스를 상속받는 클래스 작성
class ThreadTask2 extends Thread {
public void run() {
}
}
run()
메서드 바디에 새롭게 생성될 스레드가 수행할 작업을 작성한다.
public class ThreadExample2 {
public static void main(String[] args) {
}
}
class ThreadTask2 extends Thread {
// run() 메서드 바디에 스레드가 수행할 작업 내용 작성
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
}
이제 스레드를 생성해보자. 첫 번째 방법과의 차이점은, Thread 클래스를 직접 인스턴스화 하지 않는다는 점이다.
public class ThreadExample2 {
public static void main(String[]args) {
// Thread 클래스를 상속받은 클래스를 인스턴스화하여 스레드를 생성
ThreadTask2 thread2 = new ThreadTask2();
}
}
class ThreadTask2 extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
}
마찬가지로 start()
메서드를 실행시켜주고, main
메서드에 반복문을 추가한 후, 코드를 실행시켜준다.
public class ThreadExample2 {
public static void main(String[] args) {
ThreadTask2 thread2 = new ThreadTask2();
// 작업 스레드를 실행시켜, run() 내부의 코드를 처리하도록 합니다.
thread2.start();
// 반복문 추가
for (int i = 0; i < 100; i++) {
System.out.print("@");
}
}
}
class ThreadTask2 extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
}
그럼 첫 번째 방법과 유사한 결과를 얻을 수 있다.
Runnable 익명 구현 객체를 활용한 스레드 생성 및 실행
public class ThreadExample1 {
public static void main(String[] args) {
// 익명 Runnable 구현 객체를 활용하여 스레드 생성
Thread thread1 = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
});
thread1.start();
for (int i = 0; i < 100; i++) {
System.out.print("@");
}
}
}
Thread 익명 하위 객체를 활용한 스레드 생성 및 실행
public class ThreadExample2 {
public static void main(String[] args) {
// 익명 Thread 하위 객체를 활용한 스레드 생성
Thread thread2 = new Thread() {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
};
thread2.start();
for (int i = 0; i < 100; i++) {
System.out.print("@");
}
}
}
메인 스레드는 main
이라는 이름을 가지며, 그 외 추가적으로 생성한 스레드는 기본적으로 Thread-n 이라는 이름을 가진다.
스레드의_참조값.getName()
으로 조회할 수 있다.
public class ThreadExample3 {
public static void main(String[] args) {
Thread thread3 = new Thread(new Runnable() {
public void run() {
System.out.println("Get Thread Name");
}
});
thread3.start();
System.out.println("thread3.getName() = " + thread3.getName());
}
}
출력결과
Get Thread Name
thread3.getName() = Thread-0
Process finished with exit code 0
스레드의_참조값.setName()
으로 설정할 수 있다.
public class ThreadExample4 {
public static void main(String[] args) {
Thread thread4 = new Thread(new Runnable() {
public void run() {
System.out.println("Set And Get Thread Name");
}
});
thread4.start();
System.out.println("thread4.getName() = " + thread4.getName());
thread4.setName("Code States");
System.out.println("thread4.getName() = " + thread4.getName());
}
}
출력값
Set And Get Thread Name
thread4.getName() = Thread-0
thread4.getName() = Code States
Process finished with exit code 0
currentThread()
를 사용하면 된다.
public class ThreadExample1 {
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
thread1.start();
System.out.println(Thread.currentThread().getName());
}
}
출력값
main
Thread-0
Process finished with exit code 0
멀티 스레드 프로세스의 경우, 두 스레드가 동일한 데이터를 공유하게 되어 문제가 발생할 수 있다.
//예제
public class ThreadExample3 {
public static void main(String[] args) {
Runnable threadTask3 = new ThreadTask3();
Thread thread3_1 = new Thread(threadTask3);
Thread thread3_2 = new Thread(threadTask3);
thread3_1.setName("김코딩");
thread3_2.setName("박자바");
thread3_1.start();
thread3_2.start();
}
}
class Account {
// 잔액을 나타내는 변수
private int balance = 1000;
public int getBalance() {
return balance;
}
// 인출 성공 시 true, 실패 시 false 반환
public boolean withdraw(int money) {
// 인출 가능 여부 판단 : 잔액이 인출하고자 하는 금액보다 같거나 많아야 합니다.
if (balance >= money) {
// if문의 실행부에 진입하자마자 해당 스레드를 일시 정지 시키고,
// 다른 스레드에게 제어권을 강제로 넘깁니다.
// 일부러 문제 상황을 발생시키기 위해 추가한 코드입니다.
try { Thread.sleep(1000); } catch (Exception error) {}
// 잔액에서 인출금을 깎아 새로운 잔액을 기록합니다.
balance -= money;
return true;
}
return false;
}
}
class ThreadTask3 implements Runnable {
Account account = new Account();
public void run() {
while (account.getBalance() > 0) {
// 100 ~ 300원의 인출금을 랜덤으로 정합니다.
int money = (int)(Math.random() * 3 + 1) * 100;
// withdraw를 실행시키는 동시에 인출 성공 여부를 변수에 할당합니다.
boolean denied = !account.withdraw(money);
// 인출 결과 확인
// 만약, withraw가 false를 리턴하였다면, 즉 인출에 실패했다면,
// 해당 내역에 -> DENIED를 출력합니다.
System.out.println(String.format("Withdraw %d₩ By %s. Balance : %d %s",
money, Thread.currentThread().getName(), account.getBalance(), denied ? "-> DENIED" : "")
);
}
}
}
라는 코드를 실행시켜보면
Withdraw 100₩ By 김코딩. Balance : 600
Withdraw 300₩ By 박자바. Balance : 600
Withdraw 200₩ By 김코딩. Balance : 400
Withdraw 200₩ By 박자바. Balance : 200
Withdraw 200₩ By 김코딩. Balance : -100
Withdraw 100₩ By 박자바. Balance : -100
Process finished with exit code 0
라고 코드가 나온다.
첫 번째 줄에서 100원을 사용했지만 잔여금액이 600원이 나왔다. (오류)
그 이유는 thread.sleep을 통해 1000ms 가 멈춰있는 동안 다른 코드가 와서 돈을 사용했기 때문에이다.
그래서 이런 상황이 발생하지 않게 하는 것이 바로 스레드 동기화이다.
임계 영역은 오로지 하나의 스레드만 코드를 실행할 수 있는 코드 영역이다.
락은 임계 영역을 포함하고 있는 객체에 접근할 수 있는 권한을 의미한다.
synchronized
키워드를 작성하면 된다.
class Account {
...
public synchronized boolean withdraw(int money) {
if (balance >= money) {
try { Thread.sleep(1000); } catch (Exception error) {}
balance -= money;
return true;
}
return false;
}
}
class Account {
...
public boolean withdraw(int money) {
synchronized (this) {
if (balance >= money) {
try { Thread.sleep(1000); } catch (Exception error) {}
balance -= money;
return true;
}
return false;
}
}
}
그러면 다시 기존 코드로 와서 수정을 하면
public class ThreadExample3 {
public static void main(String[]args) {
Runnable threadTask3 = new ThreadTask3();
Thread thread3_1 = new Thread(threadTask3);
Thread thread3_2 = new Thread(threadTask3);
thread3_1.setName("김코딩");
thread3_2.setName("박자바");
thread3_1.start();
thread3_2.start();
}
}
class Account {
private int balance = 1000;
public int getBalance() {
return balance;
}
public synchronized boolean withdraw(int money) {
if (balance >= money) {
try {
Thread.sleep(1000);
} catch (Exception error) {
}
balance -= money;
return true;
}
return false;
}
}
class ThreadTask3 implements Runnable {
Account account = new Account();
public void run() {
while (account.getBalance() > 0) {
int money = (int)(Math.random() * 3 + 1) * 100;
boolean denied = !account.withdraw(money);
System.out.println(String.format("Withdraw %d₩ By %s. Balance : %d %s",
money, Thread.currentThread().getName(), account.getBalance(), denied ? "-> DENIED" : "")
);
}
}
}
출력값
Withdraw 100₩ By 김코딩. Balance : 900
Withdraw 100₩ By 박자바. Balance : 800
Withdraw 200₩ By 김코딩. Balance : 600
Withdraw 300₩ By 박자바. Balance : 300
Withdraw 300₩ By 김코딩. Balance : 0
Withdraw 100₩ By 박자바. Balance : 0 -> DENIED
이렇게 정상출력이 된다.