고래씌
[JAVA] 15. 스레드(Thread) 본문
1. 스레드(Thread)
▶ 프로그램
- 어떤 작업(프로세스)을 실행할 수 있는 파일(cpu로부터 메모리를 할당받지 못한 상태)
▶ 프로세스(Process)
- 간단한 의미로 실행 중인 프로그램.
- 프로세스는 프로그램이 실행될 때마다 개별적으로 생성.
- 하나의 프로세스는 프로그램을 수행함에 있어 필요한 데이터와 메모리 등의 할당 받은 자원, 그리고 하나 이상의 스레드로 구성됨.
- 현재 동작하고 있는 프로그램(작업을 위해 cpu로부터 메모리를 할당 받음)
▶ 스레드(Thread)
- 프로세스 내에서 할당된 자원을 이용해서 실제 작업을 수행하는 작업단위. 모든 프로세스는 하나 이상의 스레드를 가지며 각각 독립적인 작업단위를 가짐
- 한 개의 프로세스 내에서 실제로 주어진 자원과 메모리로 작업을 수행하는 최소단위
- 모든 프로세스에는 한개 이상의 스레드가 존재하여 작업을 수행
ex)
- 바닥청소(나, 1시간) + 빨래(세탁기, 2시간) + 설거지(나, 30분) + 화장실 청소(나, 30분)
- 2시간 걸림 → 무조건 있어야하는 중심이 되는 스레드(나) => 메인스레드
- 메인스레드가 실행하는 서브스레드(세탁기)
- 추가로 필요한 스레드 = 서브 스레드 = main에서 생성해서 실행한다.
- 하나의 프로세스를 안에서 다양한 작업을 담당하는 최소 실행 단위를 스레드라고 한다.
ex) 크롬 브라우저(=프로세스)에서 벨로그 작성하기(=스레드1) & 유튜브로 음악 듣기(=스레드2)
▶멀티스레드
- 한개의 프로그램을 실행하고 그 내부적으로 여러 작업을 처리하는 것
- 멀티 스레드는 CPU의 최대 활용을 위해 프로그램의 둘 이상을 동시에 실행하는 기술
- 멀티 스레드의 경우, 스레드 간의 자원을 공유하고 자원의 생성과 관리의 중복성을 최소화하여 수행 능력을 향상시킨다.
- 멀티스레드의 장점
① 자원을 효율적으로 사용
② 사용자 입장에서 일처리가 빠르게 보인다.
③ 작업이 분리되어서 코드가 간결해짐
=> 모든 자바어플리케이션 Main Thread가 Main 메소드를 실행하면서 시작됨.
2. 스레드 생성방법
1) 스레드 생성방법 ① - Thread 클래스를 상속받는 방법
- Thread클래스 상속 후 run()메소드 오버라이딩
- main에 Thread1객체 생성후 start()메소드 호출
start() 호출시 해당 스레드 내부의 run메소드가 실행이 됨(Thread1 클래스 내부의 run메소드)
즉, 스레드를 새롭게 가동시키려면 start()를 실행해야하고, 해당 스레드의 작업방식은 run 메소드 내부의 기술해야함
- getName() : 스레드의 이름 반환(스레드 이름 미지정시 Thread-0, Thread-1...순으로 이름 자동부여)
■ Thread 클래스를 상속받은 Thread1 클래스
package com.kh.chap01_thread.thread;
public class Thread1 extends Thread{
@Override
public void run() { // 실행하고자 하는 코드를 작성
for(int i = 1; i<=100; i++) {
System.out.println(getName()+"["+i+"]");
// getName() : 스레드의 이름 반환(스레드 이름 미지정시 Thread-0, Thread-1...순으로 이름 자동부여
}
}
}
■ main 메소드
package com.kh.chap01_thread.run;
import com.kh.chap01_thread.Thread2;
import com.kh.chap01_thread.thread.Thread1;
public class ThreadRun {
public static void main(String[] args) {
// 항상 앞 라인의 명령문이 다 끝난 후에 코드가 실행됨
for(int i = 1; i<= 100; i++) {
System.out.println("작업 1 ["+i+"]");
}
for(int i = 1; i<= 100; i++) {
System.out.println("작업 2 ["+i+"]");
}
// 작업 2를 수행하게 하기 위해서 새로운 스레드를 생성
// 스레드 생성방법 1 : Thread클래스 상속받기
Thread th1 = new Thread1();
// 스레드의 이름지정
th1.setName("스레드1");
// 스레드 시작시 start를 써야함
th1.start();
}
}
...
...
=> 순차적으로 스레드가 동작하는 것을 확인
2) 스레드 생성방법 ② - Runnable인터페이스를 구현하는 방법
▶ Runnable 인터페이스를 구현
- Runnable 인터페이스의 run()메소드 오버라이딩
■ Runnable 인터페이스를 구현한 Thread2 클래스
package com.kh.chap01_thread;
public class Thread2 implements Runnable{
@Override
public void run() {
for(int i=1; i<= 100; i++) {
System.out.println(Thread.currentThread().getName()+"["+i+"]");
}
}
}
☞ currentThread() : 현재 실행중인 스레드 객체를 가져옴
■ main 메소드 (Thread 생성방법 ①번)
package com.kh.chap01_thread.run;
import com.kh.chap01_thread.Thread2;
import com.kh.chap01_thread.thread.Thread1;
public class ThreadRun {
public static void main(String[] args) {
for(int i = 1; i<= 100; i++) {
System.out.println("작업 1 ["+i+"]");
}
for(int i = 1; i<= 100; i++) {
System.out.println("작업 2 ["+i+"]");
}
Thread th1 = new Thread1();
// 스레드의 이름지정
th1.setName("스레드1");
// 스레드 시작시 start를 써야함
th1.start();
■ main 메소드 (Thread 생성방법 ②번)
// 스레드 생성방법 2: Runnable인터페이스를 구현하는 방법
Thread th2 = new Thread(new Thread2(),"스레드2");
// Runnable 인터페이스를 구현한 구현클래스를 업캐스팅원리에 의해서 넣어줄수 있음
th2.setName("스레드2");
th2.start();
}
}
...
☞ Thread th2 = new Thread(new Thread2(),"스레드2");
Runnable 인터페이스를 구현한 구현클래스를 업캐스팅원리에 의해서 넣어줄수 있음
☞ 스레드 자체가 여러개 만들어졌기 때문에 1번 스레드 실행하고 2번 스레드 실행하고...섞이게 된 것임
3) 스레드 생성방법 ③ - 익명 인터페이스 (1회용 스레드가 필요할 때)
■ main 메소드 (Thread 생성방법 ①, ②번)
package com.kh.chap01_thread.run;
import com.kh.chap01_thread.Thread2;
import com.kh.chap01_thread.thread.Thread1;
public class ThreadRun {
public static void main(String[] args) {
// 스레드 생성방법 1 : Thread클래스 상속받기
Thread th1 = new Thread1();
th1.setName("스레드1");
th1.start();
// 스레드 생성방법 2: Runnable인터페이스를 구현하는 방법
Thread th2 = new Thread(new Thread2(),"스레드2");
th2.setName("스레드2");
th2.start(); // 스레드 자체가 여러개 만들어졌기 때문에 1번 스레드 실행하고 2번 스레드 실행하고...섞이게 된 것임
■ main 메소드 (Thread 생성방법 ③번)
Thread th3 = new Thread() {
@Override
public void run() {
for(int i=1; i<=100; i++) {
System.out.println(getName()+"["+i+"]");
}
}
};
th3.start();
System.out.println("메인스레드 종료");
}
}
...
☞ System.out.println("메인스레드 종료");
맨아래에 없음. 스레드 가동시키자마자 꺼진것을 볼수 있음
=> 서브스레드가 존재하면 메인스레드가 종료돼도 프로그램은 종료되지 않음
▶ 스레드의 특징
1) 메인 스레드가 종료되더라도 실행 중인 스레드가 하나라도 있다면 프로세스는 종료되지 않는다.
☞ 메인스레드 종료시 서브스레드도 종료시키게하려면, 메인과 서브스레드 간에 주종관계를 설정하면 됨
2) 매번 결과가 다르게 보임.
☞ 각 스레드의 실행시점은 스케줄러라는 것이 따로 결정해주기 때문에
3) 멀티스레드 환경에서는 모든 작업이 동시에 일어나는 것처럼 보이지만 사실 "한순간에 하나의 스레드만 실행"되고 있음
☞ (a스레드 실행 → a스레드 멈춤 → b스레드 실행 → b스레드 멈춤 → c스레드 실행 → c스레드 멈춤 ...)
☞ 이 과정들이 순식간에 발생하기 때문에 우리 눈에는 동시에 스레드가 작동하고 있는 것처럼 보임
Q. 왜 스레드는 이런방식으로 작업을 처리하는가?
① 하드웨어적인 한계(옛날 문제)
② 논리적 효율(요즘에는 이것때문에 사용)
=> 만약 용량이 엄청나게 큰 파일을 다운로드 받는다고 할 때 파일을 다운로드 받는동안 아무작업도 못한다면?
=> 그것보다는 기타 다른 작업도 실행하면서 동시에 다운로드도 받게해주는게 작업효율이 좋을 것이기 때문에 더 빠른 반응성을 가져다줌
▶ 컴퓨터의 특징
1) 프로그램을 실행시키기 위해서는 프로그램을 실행시킬수 있는 자원들을 CPU로부터 할당받아야 함
☞ 따라서 스레드는 프로그램을 실행하기 위해 CPU자원을 할당받았다가, 이 자원을 스케줄러에게 강탈당함.
☞ 그렇기 때문에 명령어들이 연속적으로 수행되지 못하고, 어느부분까지 명령을 수행했는지 스레드별로 개별적으로 기억할 필요가 생김
☞ 이 명령어들을 기억하는 저장공간이 Registrer이고 스레드별로 개별적인 Register를 가지고 있음
☞ 개별적인 실행흐름을 보장하기 위해 스렐드는 메모리 영역 중 statck을 각자 할당 받음
3. 스레드 스케줄링
1) 우선순위 변경 전
- 각 스레드는 기본값으로 5의 우선순위를 가지고 있다.
- 메모리가 모자랄때 우선순위를 파악해서 어떤 작업을 먼저 실행시킬지 정할 수 있다.
- 스레드의 우선순위를 정하지 않으면 스케줄러가 임의로 수행시킨다.
- 스레드는 CPU를 할당받았다가 스케줄러에 의해 다시 선점당한다.
- 그렇기 때문에 명령어의 실행이 연속적이지 못하고 어느부분까지 수행했는지 기억할 필요가 있다.
■ Runnable 인터페이스를 구현한 Car 클래스
package com.kh.chap02_scheduling.scheduling;
public class Car implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
for(int i=0; i<20; i++) {
System.out.println("Car is driving");
try {
Thread.sleep(100); // 스레드를 0.1초만큼 지연시키는 메소드
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
■ Runnable 인터페이스를 구현한 Tank 클래스
package com.kh.chap02_scheduling.scheduling;
public class Tank implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
for(int i=0; i<20; i++) {
System.out.println("Tank is shooting...");
try {
Thread.sleep(100); // 스레드를 0.1초만큼 지연시키는 메소드
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
■ Runnable 인터페이스를 구현한 Plane 클래스
package com.kh.chap02_scheduling.scheduling;
public class Plane implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
for(int i=0; i<20; i++) {
System.out.println("Plane flight...");
try {
Thread.sleep(100); // 스레드를 0.1초만큼 지연시키는 메소드
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
■ main 메소드
package com.kh.chap02_scheduling.run;
import com.kh.chap02_scheduling.scheduling.*;
public class ScheduleRun {
public static void main(String[] args) {
Thread t1 = new Thread(new Car(), "Car");
Thread t2 = new Thread(new Plane(), "Plane");
Thread t3 = new Thread(new Tank(), "Tank");
System.out.println("----- 우선순위 변경 전 -----");
System.out.println(Thread.currentThread().getName()+"의 우선순위 : " + Thread.currentThread().getPriority());
System.out.println(t1.getName()+"의 우선순위 "+ t1.getPriority());
System.out.println(t2.getName()+"의 우선순위 "+ t2.getPriority());
System.out.println(t3.getName()+"의 우선순위 "+ t3.getPriority());
t1.start();
t2.start();
t3.start();
}
}
...
☞ 무작위로 실행되는 것을 확인
2) 우선순위 변경 후
- setPrioity() : 우선순위를 지정해주는 메소드
- Thread.MAX_PRIORITY => 10
- Thread.MIN_PRIORITY => 1
// setPriority() : 우선순위를 지정해주는 메소드
t1.setPriority(Thread.MAX_PRIORITY); // 10
t3.setPriority(Thread.MIN_PRIORITY); // 1
System.out.println("----- 우선순위 변경 후 -----");
System.out.println(Thread.currentThread().getName()+"의 우선순위 : " + Thread.currentThread().getPriority());
System.out.println(t1.getName()+"의 우선순위 "+ t1.getPriority());
System.out.println(t2.getName()+"의 우선순위 "+ t2.getPriority());
System.out.println(t3.getName()+"의 우선순위 "+ t3.getPriority());
t1.start();
t2.start();
t3.start();
System.out.println("메인 스레드 종료");
}
}
...
...
☞ 변경 결과 Car 메소드가 항상 먼저 시작하고 끝나고, 그다음 Plane이 시작하고 끝나는 것을 볼수있지만,
☞ 가끔 순서가 바뀔때가 있음 → 우리가 스케줄러의 순서까지 건들 수 없으므로 어쩔 수 없다.
3) 주종관계 설정
▶ 메인 스레드가 종료되더라도 실행 중인 스레드가 하나라도 있으면 프로세스는 종료되지 않음.
이에 대한 해결책으로 스레드를 생성(start()) 하기전에 스레드간의 주종관계를 설정하면 된다.
▶ 주종관계 설정
main 스레드가 종료시 다른 스레드도 종료시키기 위해서는 setDaemon함수를 사용.
주의점으로는 스레드 중 한개라도 setDaemon을 설정했다면 모든 스레드에 setDaemon을 해주어야 정상작동한다.
t1.setDaemon(true);
t2.setDaemon(true);
t3.setDaemon(true);
// 3개 다 setDaemon을 설정해주어야 main 메소드가 종료되는 시점에 다른 스레드도 함께 종료된 것을 볼 수 있음
t1.start();
t2.start();
t3.start();
System.out.println("메인 스레드 종료");
☞ main 메소드가 종료되는 시점에 다른 스레드도 함께 종료된 것을 볼 수 있음
하지만!!!!!
멀티스레드를 잘 사용하면 프로그램적으로 좋은 성능을 낼 수 있지만, 멀티스레드 환경에서 반드시 고려해야할 점인 스레드간 동기화라는 문제는 꼭 해결해야한다. |
4. Synchronized
- 예를 들어 스레드간 서로 공유하고 수정할 수 있는 data가 있는데 스레드간 동기화가 되지 않은 상태에서
멀티스레드 프로그램을 돌리면, data의 안정성과 신뢰성을 보장할 수 없다.
- 따라서 data의 thread-safe 를 하기 위해 자바에서는 synchronized 키워드를 제공해 스레드간 동기화를 시켜 data의 thread-safe를 가능하게 함.
▶ Synchronized란?
: 한번에 한개의 스레드만 프로세스 공유 자원(객체)에 접근할 수 있도록 락(Lock)을 걸어 다른 스레드가 진행 중인 작업에 간섭하지 못하도록 하는 것
- 되도록 안쓰는게 좋지만, atm같은 중요한 것은 반드시 동기화하는 작업처리를 해야함!
- 멀티스레드 프로그램에서 스레드간의 공유자원에 대한 처리를 말함
- 공유자원의 사용순서를 정하여, 한번에 한개의 스레드만 공유자원에 접근하여 사용할 수 있도록 제어함
▶ 스레드의 문제점
- Heap 메모리 안에서 공유하면서 쓸 때 문제점이 있음. 만약 읽기만 할거라면 아무 문제가 없다. 하지만.....
- Heap 내부에 있는 자원에 int count 에 값을 추가하려고 한다? => 문제 발생
이 문제를 해결하기 위해서 "synchronized" 를 사용!
1) 동기화 처리 전
■ ATM 클래스
ATM에서 100~500 랜덤 함수를 돌려 Account 클래스에 있는 withdraw 메소드를 실행시켜 돈을 빼는 코드
package com.kh.chap03_sync.sync;
public class Atm implements Runnable{
private Account acc;
public Atm() {
}
public Atm(Account acc) {
this.acc = acc;
}
@Override
public void run() {
while(acc.getBalance() > 0) {
// 100, 200, 300, 400, 500
int money = (int)(Math.random() * 5 + 1) * 100;
acc.withdraw(money);
try {
Thread.sleep(500); // 0.5 초단위로 실행
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"종료");
}
}
■ Account 클래스
통장에 10000원 저장해놓고 현재의 스레드를 가져옴
withdraw 메소드에는 출금 및 출금시도를 출력하는 메소드
package com.kh.chap03_sync.sync;
// 계좌
// 2개의 ATM기에 동시 접속할 예정
public class Account {
private int balance = 10000;
public int getBalance() {
return balance;
}
public void withdraw(int money) {
String threadName = Thread.currentThread().getName();
System.out.println("현재 계좌에 남아있는 잔액? " + balance);
if(money <= balance) {
balance -= money;
System.out.printf("[%s] %d원 출금 >> 잔액 : %d원\n", threadName, money, balance);
}else {
System.out.printf("[%s] %d원 출금시도 >>> 현재 잔액 %d\n", threadName, money, balance);
}
}
}
■ main 메소드
package com.kh.chap03_sync.run;
import com.kh.chap03_sync.sync.Account;
import com.kh.chap03_sync.sync.Atm;
public class Run {
public static void main(String[] args) {
Account acc = new Account();
Thread atm1 = new Thread(new Atm(acc), "atm1");
Thread atm2 = new Thread(new Atm(acc), "atm2");
atm1.start();
atm2.start();
System.out.println("메인 스레드 종료");
}
}
...
☞ 두 스레드가 시작하면 잔액이 0이 될 때까지 두 스레드가 경쟁하며 출금시킬 것을 확인
☞ 여기서 멀티스레드의 문제점이 발견되는데, balance(잔액) thread-safe가 되지 않았기 때문에 atm1 스레드가 잔액을 삭감하기 전에 atm2가 balance(잔액)에 접근해 삭감을 해버리고 다시 atm1 이 sleep()에서 깨어나 balance(잔액) 을 삭감해버리기 때문에 잔액이 맞지 않게 됨!!!
☞ 하나의 공유자원에 동시에 여러개의 스레드가 달라붙어서 특정자원에 접근하는 상황을 "경쟁상태"라고 부름.
☞ 이를 방지하기 위해 하나의 공유자원에 하나의 스레드만 접근가능하도록 통제를 해야하는데, 그 통제가 필요한 영역을 임계영역이라고 부름.
☞ 임계영역을 통제하기 위하여 synchronized 키워드를 추가한다.
2) 동기화 처리 후
▶ synchronized(격리공간)
- 동기화 메소드, 동기화 블럭에 사용되는 키워드
- 동기화 메소드는 메소드 선언에 synchronized 키워드를 붙이고, 인스턴스 및 정적메소드에서도 사용이 가능함
- 동기화 메소드는 스레드가 메소드를 실행하면 메소드 전체에 즉시 락을 걸고 메소드가 종료되면 락이 풀린다.
- 메소드 전체가 아니라 일부 내용에만 락을 걸고 싶다면 동기화 블럭을 이용하면 됨.
- 락이 걸려있다면 락은 해소할 수 있는 키를 가진 스레드만 접근이 가능해짐
■ synchronized 를 추가한 Account클래스의 withdraw메소드
package com.kh.chap03_sync.sync;
// 계좌
// 2개의 ATM기에 동시 접속할 예정
public class Account {
private int balance = 10000;
public int getBalance() {
return balance;
}
public synchronized void withdraw(int money) {
String threadName = Thread.currentThread().getName();
System.out.println("현재 계좌에 남아있는 잔액? " + balance);
if(money <= balance) {
balance -= money;
System.out.printf("[%s] %d원 출금 >> 잔액 : %d원\n", threadName, money, balance);
}else {
System.out.printf("[%s] %d원 출금시도 >>> 현재 잔액 %d\n", threadName, money, balance);
}
}
}
☞ public synchronized void withdraw(int money) { }
메소드 전체에 synchronized 넣어 락을 걸고 이 락을 풀수있는 키를 가지고 있으면 접근이 가능해지지만 키를 가지고 있지 않다면 무시하고 넘어감
■ 메소드 전체에 동기화를 안걸고 일부 부분만 동기화를 하고싶다면
public void withdraw(int money) {
String threadName = Thread.currentThread().getName();
synchronized(this) { // 일부 부분만 동기화를 하고 싶다면
System.out.println("현재 계좌에 남아있는 잔액? " + balance);
if(money <= balance) {
balance -= money;
System.out.printf("[%s] %d원 출금 >> 잔액 : %d원\n", threadName, money, balance);
}else {
System.out.printf("[%s] %d원 출금시도 >>> 현재 잔액 %d\n", threadName, money, balance);
}
}
}
...
☞ synchronized 키워드를 사용함으로써 balance 공유데이터에 대한 락을 시켰기 때문에 데이터나 메서드 점유하고 있는 스레드가 온전히 자신의 작업을 마칠 수 있는 것을 볼 수 있음
'JAVA > JAVA 기초' 카테고리의 다른 글
[JAVA] 제너릭 - Farm 실습문제 (0) | 2023.10.28 |
---|---|
[JAVA] 16. 네트워크(Network) (1) | 2023.10.27 |
[JAVA] 14-4. 제네릭(Generics) 및 와일드카드 (0) | 2023.10.26 |
[JAVA] Map - Member 실습 문제 (0) | 2023.10.26 |
[JAVA] Set 실습 문제 - Lottery (0) | 2023.10.25 |