스레드란 ?
프로세스 기반의 멀티태스킹은 여러 프로그램이 병핼 실행되는 것, 스레드 기반의 멀티태스킹은 하나의 프로그램에서 여러개의 작업을 병행하는 것
예를 들어 워드프로세서에서 문서를 편집하면서 동시에 프린트 작업을 하는 것은 문서편집과 문서출력 두 개의 스레드가 병행되는 것이다.
- 스레드 목적
보통은 메인 스레드 하나를 사용했고 메인 스레드는 자바 프로그램 시작 시 자동으로 생성되며 main() 메서드의 명령문 실행이 완료되면 메인 스레드는 종료되고 자바 프로그램도 종료된다.
멀티스레드로 구현해야 하는 상황
- 여러 사용자가 동시에 요청하는 상황
- 외부 데이터 처리
메인스레드만 실행되는 싱글 스레도 환경과 함께 실행되던 모든 스레드가 종료되어야 프로그램이 종료된다.
구현 및 실행
run()
메인 스레드가 아닌 독립적인 스레드에서 동작할 수 있도록 지원하는 객체는 java.lang.Thread 클래스이다. Thread 클래스는 실행 시 새로운 스레드를 생성한 후 자신의 run() 메서드를 찾아서 실행한다. 따라서 스레드에서 동작할 명령문들은 run() 메서드에 구현한다.class PrimeThread extends Thread{ public void run(){ // 명령문 } } class PrimeRun implements Runnable{ public void run(){ // 명령문 } }
start()
스레드에서 처리할 로직을 구현한 run() 메서드는 Thread 클래스의 start() 메서드를 이용해 실행한다. 그런데 Thread 클래스를 상속해 run() 메서드를 구현한 경우는 바로 start() 메서드를 호출할 수 있지만 Runnable 인터페이스에 구현한 경우는 Thread 객체를 생성해야 start() 메서드를 호출 할 수 있다. Thread 객체를 생성할 때 Runnable 객체를 인자로 지정한다.
//Thread 상속해 구현
PrimeThread pi = new PrimeThread();
p1.start();
// Runnable 상속해 구현
PrimeRun p2 = new PrimeRun();
new Thread(p2).start();
//Ruannble + lambda
Thread p3 = new Thread(()->{});
p3.start();
Thread
public class Test {
public static void main(String[] args) {
new Food().start();
new Phone().start();
for(int i=0;i<1000;i++) {
System.out.println("메인"+i);
}
}
}
class Food extends Thread{
@Override
public void run() {
for(int i=0;i<1000;i++) {
System.out.println("음식먹기"+i);
}
}
}
class Phone extends Thread{
@Override
public void run() {
for(int i=0;i<1000;i++) {
System.out.println("전화하기"+i);
}
}
}
Runnable
public class Test {
public static void main(String[] args) {
new Thread(new Food()).start();
new Thread(new Phone()).start();
for(int i=0;i<1000;i++) {
System.out.println("메인"+i);
}
}
}
class Food implements Runnable{
@Override
public void run() {
for(int i=0;i<1000;i++) {
System.out.println("음식먹기"+i);
}
}
}
class Phone implements Runnable{
@Override
public void run() {
for(int i=0;i<1000;i++) {
System.out.println("전화하기"+i);
}
}
}
- 이름 지정
Thread의 setName()으로 thread 이름 지정 가능 - 우선순위 지정
Thread.setPriority(int new Priority) 메서드로 1부터 10까지 정수를 전달해 thread간 우선순위를 지정할 수 있고 최소값은 1 최대값은 10이다.
동기화
하나의 자원을 여러개의 스레드에서 접근하면 중복접근이 발생할 수 있다. 코드를 보자
public class Test {
public static void main(String[] args) {
Account account = new Account();
DrawThread t1 = new DrawThread(account);
DrawThread t2 = new DrawThread(account);
t1.start();
t2.start();
}
}
class Account{
private long balance = 1000;
public void draw(long amount) {
balance -= amount;
}
public long getBalance() {
return balance;
}
}
class DrawThread extends Thread{
Account account;
DrawThread(Account account){
this.account = account;
}
public void run() {
for(int i=0;i<10;i++) {
account.draw(10);
System.out.println(this.getName() + " 출금 후 잔액 "+ account.getBalance());
}
}
}
동일한 Account.balance에 두개의 스레드가 접근해서 출금잔액이 기대했던 것과 다르게 출력될 것이다.
동기화처리
블록 동기화
객체명에 공유 객체명을 지정한다. 만일 객체 자신이 공유된다면 this를 지정한다.
Thread.run 메서드를 다음과 같이 수정한다public void run() { synchronized(account){ for(int i=0;i<10;i++) { account.draw(10); System.out.println(this.getName() + " 출금 후 잔액 "+ account.getBalance()); } } }
메서드 동기화
동기화가 필요한 메서드의 선언부에 synchronized를 선언한다. 선언된 메서드는 한 스레드에 의해 호출됬다면 실행이 완료될 대까지 다른 스레드는 실행할 수 없다. Account.draw를 다음과 같이 수정한다class Account{ private long balance = 1000; public synchronized void draw(long amount) { balance -= amount; } public long getBalance() { return balance; } }
스레드 제어
스레드 상태
상태값 | 상태 |
---|---|
NEW | 스레드 객체는 생성됬지만 아직 start() 메서드가 호출되지 않은 상태 |
RUNNABLE | start() 메서드가 호출되어 실행할 수 있는 상태 RUNNABLE 상태에서 JVM에 의해 선택되어 실행될 수 있음 |
BLOCKED | 실행 대기 상태 JVM에 의해 RUNNABLE로 변경됨 |
WAITING | 실행 대기 상태 다른 스레드에 의해 RUNNABLE로 변경됨 |
TIME_WAITING | 실행 대기 상태 일정시간이 지나면 RUNNABLE로 변경됨 |
TERMINATED | 스레드 실행 종료 상태 |
### 스레드 제어 | |
1. java.lang.Object .wait(), notify(), notifyAll() | |
하나의 자원을 대상으로 소비와 생산 작업이 동시에 실행될때 자원을 소비하는 스레드는 생산된 자원이 있을때만 실행할 수 있으므로 소비할 자원이 없을 때는 자원을 생산하는 스레드가 실행될때까지 기다려야 한다. | |
``` | |
public class Test { |
public static void main(String[] args) {
Pool pool = new Pool();
Thread productGet = new Thread(new ProductGet(pool));
Thread productAdd = new Thread(new ProductAdd(pool));
productGet.start();
productAdd.start();
}
}
class Pool{
List
public synchronized void get() throws InterruptedException{
if(products.size()==0) {
wait();
}
products.remove(0);
System.out.println("소비 / 재고 = " +products.size());
}
public synchronized void add(String value) {
products.add(value);
System.out.println("생산 / 재고 = "+products.size());
notifyAll();
}
}
class ProductGet implements Runnable{
Pool pool;
ProductGet(Pool pool){
this.pool = pool;
}
@Override
public void run() {
try{
for(int i=1;i<=10;i++) {
pool.get();
}
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
class ProductAdd implements Runnable{
Pool pool;
ProductAdd(Pool pool){
this.pool = pool;
}
@Override
public void run() {
for(int i=1;i<=10;i++) {
pool.add("상품" + i);
}
}
}
어떤 스레드가 wait() 메서드 호출 명령문을 실행하면 그 스레드는 WAITING 상태로 대기 된다. List Size가 0일때 get을 하면 waiting이 걸리고 List가 추가되고notifyAll() 메서드로 WAITING 대기 상태에 있던 모든 스레드들을 RUNNABLE 상태로 전환해준다.
2. join()
스레드간의 종속을 맺어준다. 예를 들어 A라는 스레드 작업이 완료되고 B라는 스레드 작업을 진행해야하는 경우에 join() 메서드를 사용한다.
public class Test{
public static void main(String[] args){
Phone calling = new Phone();
calling.start();
try{
calling.join();
}catch(InterruptedException e){
e.printStackTrace();
}
for(int i=0;i<=1000;i++){
System.out.println("밥먹기" +i);
}
}
}
calling 끝날때까지 기다린 이후 main 스레드 실행
3. sleep()
지정된 시간 동안 스레드를 TIME_WATING 대기 상태로 전환하는 메서드.
4. interrupt()
sleep, wait, joing 메서드가 실행되어 실행대기 상태에 있는 스레드들의 실행을 중지한다.
## 스레드풀
스레드를 많이 생설할 수록 Thread 객체가 많아지고 JVM이 스케쥴링해야하는 스레드가 많아진다. 따라서 스레드가 늘수록 메모리 사용량도 함께 늘며 스케줄링 작업 또한 복잡해지고 그만큼 개발자의 스레드 제어는 힘들어진다. 이 점을 보완하기 위한것이 스레드 풀이다. 스레드 풀은 처리 로직과 스레드를 일대일로 매핑하는 것이 아니라, 미리 스레드를 몇 개 생성해놓고 이 스레드를 재사용하는 방식이다. 스레드의 생성과 삭제 비용을 절감할 수 있고 제한된 개수로 사용하므로 스케쥴링에 많은 오버헤드가 발생하지 않게하고 간단한 스레드 제어도 가능하다.
### ExcecutorService 인터페이스
1. 스레드 풀 생성
ExecutorService threadPool1 = Executors.newFixedThreadPool(int nThreads);
ExecutorService threadPool2 = Executors.newCachedThreadPool();
2. 작업 실행
별도의 Thread 객체를 생성할 필요는 없고 run() 메서드만 구현한다. 그리고 실행하려면 execute() 메서드의 인자로 run()을 구현한 Runnable 객체를 전달하면 스레드가 실행된다.
class Task implements Runnable{
@Override
public void run(){
for(int i=0;i<10;i++){
System.out.println("스레드 작업1");
}
}
}
threadPool1.execute(new Task());
3. 스레드 풀 종료
threadPool1.shutdown();
threadPool2.shutdown();