실행하기 위해 메모리에 로딩된 프로그램을 프로세스라 하고,
프로세스 내부에 쓰레드가 존재한다.
쓰레드는 'CPU를 사용하는 최소 단위'라고 할 수 있다.
하나의 프로세스에서 여러 개의 쓰레드가 동작하는 것을 '멀티 쓰레드'라고 한다.
쓰레드의 생성 및 실행방법 2가지
쓰레드에서 작업할 내용은 run() 메서드 안에 작성한다. 이후 Thread 클래스의 생성자를 이용해 객체를 생성한다. 마지막으로 Thread 객체의 start() 메서드를 호출해 실행한다.
start() = 새로운 쓰레드 생성 / 추가하기 위한 모든 준비 + 새로운 쓰레드 위에 run() 실행
방법 1. Thread 클래스 상속받아 run() 메서드 오버라이딩
class MyThread extends Thread{
@Override
public void run() {
//내용 생략
...
}
}
public class Thread_1 {
public static void main(String[] args) {
// 객체생성 및 start()
Thread mythread = new MyThread();
mythread.start();
Thread를 상속받은 클래스를 작성하고 쓰레드가 실행할 기능을 run() 메서드 안에 오버라이딩한다.
메인에서 객체를 생성해주고 start() 메서드를 실행하면 된다.
하지만 만약 이미 상속받고 있는 클래스가 존재한다면 Runnable 인터페이스를 상속받아 구현해야 한다.
자바는 단일 상속이 원칙이기 때문이다.
방법 2. Runnable 인터페이스 구현 객체 생성한 후 Thread 생성자로 Runnable 객체 전달
class MyThread implements Runnable{
@Override
public void run() {
//내용 생략
...
}
}
public class Thread_1 {
public static void main(String[] args) {
// Runnable 방법
MyThread myth = new MyThread();
Thread th = new Thread(myth, "쓰레드이름");
th.start();
Thread 클래스의 생성자로 Runnable 객체를 전달할 수 있다.
따라서 Runnable 인터페이스를 상속받은 객체를 생성한 후 넘겨주면 된다.
*Thread 생성자
Thread() | 쓰레드의 이름은 'Thread-' + n |
Thread(Runnable r) | 인터페이스 객체 |
Thread(Runnable r, String name) | 인터페이스 객체, 쓰레드 이름 |
Thread(String name) | 쓰레드 이름 지정 |
쓰레드의 상태 (생명주기)
스레드는 Thread 객체가 생성되면 생명주기를 갖게 되는데 크게 5가지로 나누게 된다.
1. new
스레드가 만들어진 상태
2. Runnable
스레드 객체가 생성된 후에 start() 메서드를 호출하면 Runnable 상태로 이동하게 된다.
3. Running
Runnable 상태에서 쓰레드 스케줄러에 의해 Running 상태로 이동하게 된다.
4. Blocked
스레드가 다른 특정한 이유로 Running 상태에서 Blocked 상태로 이동하게 된다.
동기화 메서드 또는 동기화 블록을 실행하기 위해 먼저 실행 중인 쓰레드의 실행 완료를 기다리는 상태다.
5. Dead
쓰레드가 종료되면 그 쓰레드는 다시 시작할 수 없게 된다.
쓰레드 우선순위 ( Priority )
Thread 클래스에서 쓰레드의 우선순위를 지정할 수 있는 setPriority(int newPriority) 메서드를 제공한다.
// 우선순위 직접지정
for(int i=0; i<5; i++) {
Thread thread = new MyThread();
thread.setName(i+"번째 쓰레드");
if(i==4) {
thread.setPriority(Thread.MAX_PRIORITY);
}
thread.start();
}
setPriority() 메서드의 매개변수로 1, 5, 10을 넣을 수 있다. Thread는 상수도 제공한다.
MAX_PRIORITY = 10
NORM_PRIORITY = 5
MIN_PRIORITY = 1
동기화
멀티 쓰레드 프로그래밍에서 동기화 처리가 필수다. 쓰레드 동기화란 여러 쓰레드가 동일한 리소스를
공유하여 사용하게 되면 서로의 결과에 영향을 주기 때문에 방지하는 기법이다.
쓰레드 동기화를 하기 위해서는 임계영역(critical section)과 락(lock)을 사용한다.
임계영역으로 설정한 구역은 동시에 리소스를 사용할 수 없는 구역이고, 락을 획득한 쓰레드에 대해서만
리소스를 사용하도록 하는 방식이다.
임계영역 : 멀티 스레드에 의해 공유자원이 참조될 수 있는 코드의 범위를 말한다.
락(lock) : 동기화를 처리하기 위해 모든 객체에 락(lock)을 포함 시켰다.
공유 객체에 여러 스레드가 동시에 접근하지 못하도록 하기 위함이다.
모든 객체가 힙 영역에 생성될 때 자동으로 생성된다.
//공유객체
class MyData{
int data = 3;
//동기화 처리 X
public void plusData() {
int mydata = data;
try {Thread.sleep(2000);} catch(InterruptedException e) {}
data = mydata + 1;
}
}
//공유객체를 사용하는 쓰레드
class PlusThread extends Thread{
MyData myData;
public PlusThread(MyData myData) {
this.myData = myData;
}
@Override
public void run() {
myData.plusData();
System.out.println(getName() + " 실행 결과: " + myData.data);
}
}
public class TheNeedsForSynchronized {
public static void main(String[] args) {
//공유객체 생성
MyData myData = new MyData();
Thread thread1 = new PlusThread(myData);
thread1.start();
try {Thread.sleep(1000);} catch(InterruptedException e) {} // 1초 기다림
Thread thread2 = new PlusThread(myData);
thread2.start();
// 2개의 쓰레드가 각각 +1을 해주었으니 5가 나올 것 같지만 4가 나왔다.
// 이유는 두 개의 쓰레드가 data필드를 증가시키는 시점에 아직 첫 번째 쓰레드의 실행이 끝나지 않았기 때문이다.
// 따라서 동기화 처리를 해주어야 잘 동작한다.
}
}
위 예제 코드를 보면 두 개의 쓰레드가 MyData를 공유 객체로 사용하고 있다.
두 쓰레드가 한 번씩 동작해서 3이었던 data가 5가 될 것 같지만, 실행 시점이 꼬여서 결국
data는 4가 출력되었다. 이렇게 동기화 처리를 해주지 않으면 서로의 결과에 영향을 미치므로 꼭 임계 영역을 작성해야 한다.
동기화 방법 ( synchronized )
자바에서는 synchronized 키워드를 사용하여 임계영역을 지정하여 동시에 공유 객체를 차지하지 않도록
강제한다. synchronized를 사용하는 방법은 2가지가 있다.
// 메서드 동기화 ( 한 객체를 두 쓰레드가 동시에 사용할 수 없도록 설정 )
public synchronized void plusData() {
int mydata = data;
try {Thread.sleep(2000);} catch(InterruptedException e) {}
data = mydata + 1;
}
// 블록 동기화
public void plusData() {
synchronized(this) { // (임의의 객체, 주로 this)
int mydata = data;
try {Thread.sleep(2000);} catch(InterruptedException e) {}
data = mydata + 1;
}
}
1. 메서드 동기화
여러 개의 쓰레드가 동시에 메서드를 실행할 수 없다.
동기화 하고자 하는 메서드의 리턴 타입 앞에 synchronized 키워드만 넣으면 된다.
2. 블록 동기화
여러 개의 쓰레드가 동시에 해당 블록을 실행할 수 없다.
동기화 영역에서는 하나의 쓰레드만 실행할 수 있기 때문에 성능 면에서는 많은 손해가 있다.
따라서 동기화 영역은 꼭 필요한 부분에 한정해 적용하는 것이 좋다.
만일 메서드 전체 중에 동기화가 필요한 부분이 일부분이라면 굳이 전체 메서드를 동기화 하는 것보다
특정 영역만 블록 동기화를 사용하는 것이 좋다.
wait( ), notify( ), notifyAll( ) 메서드
synchronized를 사용하여 동기화된 임계영역에서는 다른 쓰레드에게 제어권을 넘기지 못한다.
이와 같은 임계영역에서 쓰레드 간의 통신을 하기 위해서 Object 클래스의 wait(), notify(), notifyAll() 메서드를
사용해야 한다.
*주의점 : synchronized 영역 내에서 사용했을 때만 의미가 있다.
(java.lang.IllegalMonitorStateException 에러발생)
class ATM implements Runnable{
private long depositeMoney = 10000;
public void run() {
synchronized(this) { //동기화처리
for(int i=0; i<10; i++) {
try {
notify(); // 깨우기 (제어권을 먼저 넘겨야 한다.)
Thread.sleep(1000); //돈빼기
wait(); // 재우기
} catch(InterruptedException e) {
e.printStackTrace(); //?
}
if (getDepositeMoney()<=0) {
break;
}
withDraw(1000);
}
}
}
public void withDraw(long howMuch) {
if(getDepositeMoney()>0) {
depositeMoney -= howMuch;
System.out.println(Thread.currentThread().getName()+" , "); //사용중인 쓰레드 이름 출력
System.out.printf("pay : %d %n",getDepositeMoney());
} else {
System.out.println(Thread.currentThread().getName());
System.out.println("End .");
}
}
public long getDepositeMoney() {
return depositeMoney;
}
}
public class Synch {
public static void main(String[] args) {
ATM atm = new ATM();
Thread mother = new Thread(atm, "mother"); //인터페이스 객체, 쓰레드 이름
Thread son = new Thread(atm, "son");
son.start();
mother.start();
}
}
참고 문헌 : Do it! 자바 완전정복
'Java > 자바 이론' 카테고리의 다른 글
[Java] 자바 제네릭(Generic)이란? (Feat. 오토박싱) (0) | 2022.07.09 |
---|---|
[Java] 자바 컬렉션(Collection) 프레임워크란? (0) | 2022.07.06 |
[Java] 자바 내부 클래스 4종류 (Inner Class) (0) | 2022.07.05 |
[Java] 자바 인터페이스(Interface)란? (0) | 2022.07.05 |
[Java] 추상 클래스와 추상 메서드 (abstract) (0) | 2022.07.04 |