멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게 된다.
이러한 일이 발생하는 것을 방지하기 위해서 한 쓰레드가 특정 작업을 끝마치기 전까지
다른 쓰레드에 의해 방해받지 않도록 하는 것이 필요하다. 그래서 도입된 개념이 바로 ‘임
계 영역(critical section)’과 ‘잠금(락,lock)’이다
공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정해놓고, 공유 데이터(객체)가
가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 한
다. 그리고 해당 쓰레드가 임계 영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납해
야만 다른 쓰레드가 반납된 lock을 획득하여 임계 영역의 코드를 수행할 수 있게 된다
이처럼 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 ‘쓰레드의 동기화(synchronization)’라고 한다.
동기화하려면 간섭받지 않을 문장들을 하나의 영역, 즉 임계 영역으로 묶으면 된다.
이 묶는 행위는 synchronized 라는 키워드로 할 수 있다.
임계영역은 락이 걸려 하나의 영역에 하나의 쓰레드만 출입 가능하게 된다.
synchronized 를 이용한 동기화
synchronized 로 임계영역을 설정하는 방법 두가지는
1. 메서드 전체를 임계영역으로 지정 (voide 앞에 synchronized 키워드 넣기)
2. 특정 영역을 임계영역으로 지정하는 방법이 있다. synchronized(참조변수)
첫 번째 방법은 메서드 앞에 synchronized를 붙이는 것인데, synchronized를 붙이면 메서드 전체가 임계 영역으로 설정된다
두 번째 방법은 메서드 내의 코드 일부를 블럭{} 으로 감싸고 블럭 앞에 ‘synchronized (참조변수)’를 붙이는 것인데, 이때 참조변수는 탁을 걸고자하는 객체를 참조하는 것이어 야 한다.
블럭을 synchronized 블럭이라고 부르며, 이 블럭의 영역 안으로 들어가면서 부터 쓰레드는 지정된 객체의 lock을 얻게 되고, 이 블럭을 벗어나면 lock을 반납한다.
임계 영역은 멀티쓰레드 프로그램의 성능을 좌우하기 때문에 가능하면 메서드 전체에 락을 거는 것보다 synchronized블럭으로 임계 영역을 최소화해서 보다 효율적인 프로그 램이 되도록 노력해야한다.
즉, 임계영역은 한번에 한 쓰레드만 들어갈 수 있는 만큼 그 영역 최소화 될 수록 좋다.
두 방법 모두 lock의 획득과 반납이 모두 자동적으로 이루어지므로 우리가 해야 할 일은
그저 임계 영역만 설정해주는 것뿐이다.
synchronized 하지않았을때는 한 쓰레드에 다른 쓰레드가 영향을 미쳐 마이너스 잔고가 뜨게 된다.
그 이유는 한 쓰레드가 if문의 조건식을 통과하고 출금하기 바로 직전에 다른 쓰레드가 끼어들 어서 출금을 먼저 했기 때문이다.
즉, synchronized키워드가 없을 때 이런 결과가 출력될 수 있다.
withdraw메서드에 synchronized키워드를 붙이기만 하면 간단히 동기화가 된다.
그러나 synchronized 키워드를 적어 해당 메소드를 임계영역으로 만들어주면
잔고가 마이너스로 출력되는 결과가 해결된다.
예제에서도 synchronized 키워드가 없으면 한쓰레드가 다른 쓰레드에 영향을 미쳐 잔고가 마이너스로 출력된다.
① 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 것이 아니라 실행대기열에 저장되어 자신의 차례가 될 때까지 기다려야 한다. 실행대기열은 큐 (queue)와 같은 구조로 먼저 실행대기 열에 들어온 쓰레드가 먼저 실행된다.
② 실행대기상태에 있다가 자신의 차례가 되면 실행상태가 된다.
③ 주어진 실행시간이 다되거나 yield()를 만나면 다시 실행대기상태가 되고 다음 차례의 쓰레드가 실행상태가 된다.
④ 실행 중에 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 될 수 있다. I/O block은 입출력작업에서 발생하는 지연상태를 말한다. 사용자의 입력을 기다리는 경우를 예로 들 수 있는데,이런 경우 일시정지 상태에 있다가 사용자가 입력을 마치면 다시 실행대기 상태가 된다.
⑤ 지정된 일시정지시간이 다되거나(time-out), notify(), resume(), in te rrup t)가 호출되면 일시 정지상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다리게 된다.
⑥ 실행을 모두 마치거나 stop( )이 호출되면 쓰레드는 소멸된다.
쓰레드의 실행제어 메서드 종류
유일하게 static 메서드인 sleep() 과 yield() : 쓰레드 자기 자신에게만 호출이 가능하다.
이는 자기 자신에게만 호출이 가능함을 의미한다.
즉, 자거나 양보하는건 자기 스스로에게만 적용되는 것
쓰레드의 스케줄링을 잘하기 위해서는 쓰레드의 상태와 관련 메서드를 잘 알아야 한다.
sleep(long millis)
일정시간동안 쓰레드를 엄추게 한다.
sleep()에 의해 일시정지 상태가 된 쓰레드는 지정된 시간이 다 되거나 interrupt()가 호출되면(InterruptedException발생), 잠에서 깨어나 실행대기 상태가 된다.
그래서 sleepO을 호출할 때는 항상 try-catch문으로 예외를 처리해줘야 한다.
static 메서드로 현재 쓰레드에서만 동작한다.
interrupt()와 interrupted()
쓰레드의 작업을 취소한다.
진행 중인 쓰레드의 작업이 끝나기 전에 취소시켜야할 때 사용한다.
interrupt()는 쓰레드에게 작업을 멈추라고 요청한다.
단지 멈추라고 요청 만 하는 것일 뿐 쓰레드를 강제로 종료시키지는 못한다.
interrupt()는 그저 쓰레드의 interrupted상태(인스턴스 변수)를 바꾸는 것일 뿐이다.
suspend( ), resume( ), stop ()
suspend()는 sleep()처럼 쓰레드를 멈추게 한다.
suspend()에 의해 정지된 쓰레드는 resume()을 호출해야 다시 실행대기 상태가 된다.
stop()은 호출되는 즉시 쓰레드가 종 료된다.
이 메서드들은 교착상태 (deadlock)를 일으키기 쉽게 작성되어있으므로 사용이 권장되지 않는다.
그래서 이 메서드들은 모두 ‘deprecated’되었다. (권장이지 강제는 아님)
yield()
다른 쓰레드에게 양보한다.
yield()는 쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보(yield)한다.
static 메서드로 현재 쓰레드에서만 동작한다.
join()
다른 쓰레드의 작업을 기다린다.
쓰레드 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도 록할 때 join()을 사용한다
시간을 지정하지 않으면, 해당 쓰레드가 작업을 모두 마칠 때까지 기다리게 된다.
join()도 sleep()처럼 interrupt()에 의해 대기상태에서 벗어날 수 있으며, join()이 호출 되는 부분을 try-catch문으로 감싸야 한다.
join( )은 여러모로 sleep( )과 유사한 점이 많다.
sleep()과 다른 점은 join()은 현재 쓰레드가 아닌 특정 쓰레드에 대해 동작하므로 static메서드가 아니라는 것이다.
쓰레드를 구현하는 방법은 1. Tread 클래스를 상속하는 것과 2. Runnable 인터페이스를 구현하는 방법이 있는데
Tread 클래스를 상속할 경우 다른 클래스의 상속을 못 받기 때문에 Runnable 인터페이스를 구현하는 방법을 일반적으로 사용한다.
알다시피 자바는 단일 상속만을 지원하기 때문이다.
main 메서드를 채워주듯이 쓰래드 내에서 run() 메서드를 구현해주면 된다.
1번 방법은 첫번째 박스와같이 스레드를 생성 > 곧바로 start() 를 호출하여 실행하면 된다.
2번 방법은 두번째 박스와 같이 Runnable 인터페이스를 구현 후 start() 메서드를 호출한다.
Runnable인터페이스를 구현하기 위해서 해야 할 일은 추상메서드인 run()의 몸통{ }을 만들어 주는 것뿐이다.
쓰레드를 구현한다는 것은, 위의 두 방법 중 어떤 것을 선택하든지, 그저 쓰레드를 통해 작업하고자 하는 내용으로 run()의 몸통({ })을 채우는 것일 뿐이다.
Runnable인터페이스를 구현한 경우,Runnable인터페이스를 구현한 클래스의 인스턴스 를 생성한 다음,이 인스턴스를 Thread클래스의 생성자의 매개변수로 제공해야 한다.
예제에서 두가지 방법으로 쓰레드를 구현하였다.
방법이 달라도 run() 메소드 내에 쓰레드가 수행할 작업을 작성하는 것은 동일하다.
run() 메소드 대신 start() 메소드를 실행하는 것에 유의한다.
Tread 메소드를 상속하여 작성한 쓰래드는 현재 실행중인 쓰래드의 이름을 알고싶을때 this.getName() 만으로 손쉽게 호출할 수 있다.
Runnable 인터페이스를 구현하여 작성한 쓰래드는 Tread.currentTread().getName 과 같이 현재 실행중인 스레드 객체를 불러와 이름을 호출해야하는 것을 알 수 있다.
Thread클래스를 상속받으면, 자손 클래스에서 조상인 Thread클래스의 메서드를 직접 호출할 수 있지만, Runnable을 구현하면 Thread클래스의 static메서드인 current ThreadO를 호출하여 쓰레드에 대한 참조를 얻어 와야만 호출이 가능하다.
Thread를 상속받은 ThreadExl_1에서는 간단히 getName()을 호출하면 되지만, Runnable을 구현한 ThreadExl_2에는 멤버라고는 run()밖에 없기 때문에 Thread클래 스의 getName()을 호출하려면, ‘Thread.currentThread().getName()' 와 같이 해야한다.
쓰레드의 이름을 지정하지 않으면 Tliread-번호’ 의 형식으로 이름이 정해진다.
쓰레드를 생성했다고 해서 자동으로 실행되는 것은 아니며 startO를 호출해야만 쓰레드가 실행된다.
사실은 startO가 호출되었다고 해서 바로 실행되는 것이 아니라, 일단 실행대기 상태에 있다가 자신의 차례가 되어야 실행된다. 물론 실행대기중인 쓰레드가 하나도 없으면 곧바 로 실행상태가 된다.
작성한 쓰레드는 멀티 스레드로서 두가지 스레드가 동시에 실행되는데 데이터를 넓혀보면 다음과같은결과를 확인할 수 있다.
작성순서와 길이 등 우선순위가 실행시마다 다르게 출력됨을 알 수 있다.
이는 OS 의 스케줄러가 어떤 스래드를 실행할지 정하기 때문이다.
만약 콘솔이 한줄로 나온다면 이미지속 버튼을 클릭하여 줄바꿈 한다.
한 가지 더 알아 두어야 하는 것은 한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다는 것이다.
즉, 하나의 쓰레드에 대해 start( )가 한 번만 호출될 수 있다는 뜻이다.
그래서 만일 쓰레드의 작업을 한 번 더 수행해야 한다면 새로운 쓰레드를 생성한 다음에 start()를 호출해야 한다.
멀티스레드로 작성된 내용을 주석처리 후 싱글 스레드 작성로 변경 시 출력결과를 보자
하나의 작업(0)이 끝나야 다음작업(1)이 실행되는 것을 확인할 수 있다.
그러나 멀티스레드로 작성시 두 작업이 번갈아 실행된다.
어떤 작업이 먼저, 얼마나 실행될지는 앞서 기술한대로 OS 스케줄러에 따라 결정된다.
쓰레드의 실행 - start()
main메서드에서 run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아니라 단순 히 클래스에 선언된 메서드를 호출하는 것일 뿐이다
반면에 start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택(call stack)을 생 성한 다음에 run()을 호출해서, 생성된 호출스택에 run()이 첫 번째로 올라가게 한다.
모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택을 필요로 하기 때문 에,새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출스택이 생성되고 쓰레드가 종 료되면 작업에 사용된 호출스택은 소멸된다.
쓰래드를 구현한 후 작성한 메소드는 run()이었지만 다음과같이 호출시엔 start() 메서드를 사용한다.
쓰래드의 실행전 다음과같은 두가 사항을 기억해야 한다.
1. 이 start() 메서드를 호출하는 것이 즉시 실행을 의미하는 것은 아니라는 것
2. 또한 먼저 작성한 메서드가 먼저 실행되는 것도 아니라는 것
예제 속 콘솔창을 보자.
두번째 줄에 작성한 t2.start() 가 먼저 실행되어 0보다 1이 먼저 출력되었으며
0과 1을 얼마나 출력할지, 얼마나 지속할지 일정하게 정해진 것이 아니라는 점을 확인할 수 있다.
우리는 알게모르게 쓰레드를 사용해왔다. 바로 main 메서드이다.
start() 메소드는 쓰레드의 즉시 실행을 의미하지 않는다고 했다.
두번째 그림 2.Call stack 에서 확인할 수 있듯 '새로운 호출스택을 생성' 해 줄 뿐이다.
이는 해당 스택에서 run() 메소드가 실행될 수 있음을 의미한다.
main 메서드가 기본 쓰레드를 생성후 start() 메서드로 새로운 호출스택을 생성한다.
이렇게 start() 로 인해 생성된 스택에서 또다른 쓰레드(run 쓰레드)는 이전의 (main 쓰레드)와는 별개로 독립적인 작업을 수행할 수 있는 것이다.
위의 그림에서와 같이 쓰레드가 둘 이상일 때는 호출스택의 최상위에 있는 메서드일지라도 대기상태에 있을 수 있다.
스케줄러는 실행대기중인 쓰레드들의 우선순위를 고려하여 실행순서와 실행시간을 결정하고,
각 쓰레드들은 작성된 스케줄에 따라 자신의 순서가 되면 지정된 시간동안 작업 을 수행한다.
지금까지는 main메서드가 수행을 마치면 프로그램이 종료되었으나, 위의 그림에서와 같 이 main메서드가 수행을 마쳤다하더라도 다른 쓰레드가 아직 작업을 마치지 않은 상태라면 프로그램이 종료되지 않는다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>3_includeDirective</title>
</head>
<body>
<h1 align="center">include directive</h1>
<!--
include 지시자 태그를 이용하여 file 속성에 jsp 경로를 지정해주면 해당 jsp에 작성한 내용을 그대로 포함시켜 현재 jsp 파일을 동작시킨다.
-->
<div align="right"><%@ include file="today.jsp" %></div>
<%
/* 동일한 변수 이름을 사용했기 때문에 컴파일 에러가 발생한다. */
//String output = "";
%>
</body>
</html>
또다시 같은 경로에 Today 라고 하는 jsp 파일을 작성한다.
today.jsp 페이지 내 스크립틀릿 코드 안에 DATE 객체를 작성하는데, 이때는 임포트 되지않았으므로 에러가 발생할 것이다.
하나 더 페이지 지시자를 만들어도 문제없다.
그 다음 작성시 또다시 에러가 발생한다.
앞서 배웠듯이, import 가 여러개 필요할 때는 콤마(, ) 찍은 뒤 기술하면 된다고 했다.
SimpleDateFormat 으로 원하는 문자열 포맷을 나타내보자.
format 이라는 메소드를 사용하여 today를 담는다.
이후 익스프래션 태그를 이용해 화면에 출력하도록 한다.
import 전, today 만 보자면, (주소에 경로를 바꿔 입력했을때) 다음과같이 포맷화가 잘 된 시간을 확인할 수 있다.
하지만 확인하고 싶은 것은 include 지시자 태그이다.
돌아와서 지시자 태그 내에 inclue 와 파일속성을 사용해 경로를 작성한다.
주소 경로를 다시 입력하여 include 지시자 태그를 클릭하면
다음과같이 화면 오른쪽에 포맷화된 날짜를 확인할 수 있다.
유의할 점은 무엇이 있는가?
만약 포함하는 페이지에서 output 이라는 변수를 사용한다면
다음과 같은 변수의 중복으로 인한 에러메세지가 뜬다.
Duplicate local variable output
왜?
포함되는 today.jsp 영역안에 이미 output 이라는 지역변수가 선언되어 있기 때문이다.
전체적인 코드안에 '포함되는(include)' 코드들도 공유되어있다.
include 지시자를 통한 포함 처리를 할 때는 하나의 jsp 로 간주되기 때문에 중복된 변수이름을 사용할 수 없다.
다음으로 jsp를 이용한 응답처리를 테스트 한다.
index.html 내에 요청과 응답을 테스트 할 페이지를 만들어 준후 경로에 맞게 jsp 파일을 생성한다.
Action 속성 action 속성은 폼이 제출될 때 수행할 행동을 정의 한다. (서버에 폼을 제출하는 일반적인 방법은 submit 버튼을 사용) 일반적으로, 폼은 웹서버에 있는 웹페이지에 제출된다. 만약 action 속성이 생략되어 있으면, action 은 현재 페이지로 설정된다. (출처 : http://jun.hansung.ac.kr/CWP/htmls/HTML%20Forms.html)
Method 속성 method 속성은 폼을 제출할 때 사용될 HTTP method (GET or POST)를 지정한다. (출처 : http://jun.hansung.ac.kr/CWP/htmls/HTML%20Forms.html)
jsp 안에서 사용할 수 있게끔 몇가지 객체들이 선언되어 있기 때문이다.
코드속 request객체는 서블릿에서 사용하던 httpServletRequest 인스턴스이다.
contextPath는 프로젝트 내에서 chap13 이라고 임의로 설정이 되어있다.
즉 가장 root 인 경로부터 시작해서 경로를 작성하고자 하는 것이다.
"<%= request.getContextPath() %>/menu/order"는 chap13/menu/order 와 동일하다.
package com.greedy.menu.controller;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/menu/order")
public class MenuOrderServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
/* 서블릿에서 하는 일
* 1. 요청 받은 값 확인 및 검증
* 2. 비지니스 로직 처리
* 3. 응답 페이지 생성 후 응답
* */
/* 1. 요청에 대한 처리 */
request.setCharacterEncoding("UTF-8");
String menuName = request.getParameter("menuName");
int amount = Integer.parseInt(request.getParameter("amount"));
System.out.println("menuName : " + menuName);
System.out.println("amount : " + amount);
/* 2. 비즈니스 로직 처리 */
/* 비즈니스 로직은 대부분 DB에 CRUD 연산 등을 이용해 이루어지게 된다.
* 여기서는 DB 연결을 할 것은 아니기 때문에 간단한 로직 처리만 해보자.
* */
int orderPrice = 0;
switch(menuName) {
case "햄버거" : orderPrice = 6000 * amount; break;
case "짜장면" : orderPrice = 7000 * amount; break;
case "짬뽕" : orderPrice = 8000 * amount; break;
case "순대국" : orderPrice = 9000 * amount; break;
}
/* 3. 응답 페이지를 생성 후 응답한다.
* 직접 페이지를 문자열로 생성한 뒤 스트림으로 내보내기를 할 수도 있지만
* 페이지 작성이 더 쉽고 응답을 보여주는 역할에 대해 구분하여 응답만 전용으로 할 수 있는 jsp쪽으로
* request에 값을 담은 뒤 forward 해서 화면에 출력해보자.
* */
request.setAttribute("menuName", menuName);
request.setAttribute("amount", amount);
request.setAttribute("orderPrice", orderPrice);
/* 서블릿 컨테이너 내부에서는 /root가 적용된 상태이다. */
RequestDispatcher rd = request.getRequestDispatcher("/jsp/5_response.jsp");
rd.forward(request, response);
}
}
스크립틀릿(<% %>)을 이용해서 값을 꺼내준다.
jsp는 결국 서블릿 화 되기 때문에 위임의 과정역시 서블릿과 동일하다.
앞에 request에 setAttribute로 담은 값을 getAttribute로 꺼내올 수 있다.
getAttribute 로 꺼내올 때의 리턴값은 항상 Object 타입이다.
그러므로 필요한 타입으로 다운캐스팅이 필요하다.
넘어온 값을 화면에 표현해보자
화면에 출력할 수 있는 익스프레션 태그<%= %>를 사용한다.
<h3>주문하신 음식 : <%= menuName %></h3>
<h3>주문하신 수량 : <%= amount %>인분</h3>
<h3>결제하실 최종 금액 : <%= orderPrice %></h3>