프로세스는 실행중인 애플리케이션을 의미한다.
프로세스는 데이터, 컴퓨터 자원, 그리고 스레드로 구성되는데, 스레드는 데이터와 애플리케이션이 확보한 자원을 활용하여 소스코드를 실행한다.
스레드는 하나의 코드 실행 흐름이라고 볼 수 있다.
자바 애플리케이션을 실행하면 가장 먼저 실행되는 메서드는 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
이렇게 정상출력이 된다.