어제 머리가 터지는줄 알았다~
새벽1시까지 공부를 하다가 드디어 어느정도
사용방법에 익숙해져서 만족하면서 잠을 청했다.

아마 이해하지 못했다면 계속해서
오늘 공부하는데 지장이 있지 않았을까 싶다…
여하튼! 하루하루 성장해나간다는 재미가 쏠쏠하다
오늘도 열심히 공부해보자!

image


어제는 주요하게 배웠던 것은
람다와 스트림의 사용 방법이다.

뭔가 생략되는 부분들이 많아 난해하긴한데
람다식에 대한 이해와 스트림에 대한 이해는 충분히 되었다.

그리고 부수적으로 애노테이션과 파일의 입출력관련
공부도 했는데, 애노테이션 같은 경우는
여러가지 애노테이션의 역할을 알고있는 것이 나중에 도움이 될 것같다고 생각한다.
오늘은 쓰레드와 JVM을 공부해볼 시간이다.

Thread

프로세스 스레드란?

프로세스는 실행 중인 애플리케이션을 의미한다.
즉, 실행한 프로그램을 얘기한다.
운영체제로부터 실행에 필요한 만큼 메모리를 할당받아 프로세스가 된다.

프로세스는 데이터,컴퓨터 자원, 스레드로 구성이된다.
여기서 스레드는 데이터와 애플리케이션이 확보한 자원을 활용하여
소스코드를 실행한다. 이말은 즉슨, 스레드는 하나의 코드 실행 흐름이로 생각할 수 있다.

image


메인 스레드(Main thread)
자바 애플리케이션을 실행하면
가장 먼저 main메서드가 실행된다. 메인 스레드는 main메서드에서 실행시켜주고
코드 처음부터 끝까지 순차적으로 실행시켜고, return을 만나면 실행을 종료한다.

image


멑티 스레드(Multi-Thread)
하나의 프로세스에서 여러개의 스레드를 가진 것을, 멀티 스레드 프로세스라한다.
여러 스레드가 동시에 작업을 수행할 수 있고, 멀티 스레딩이라고 한다.
즉, 메인 스레드에서 또 다른 스레드를 생성하여 실행시키면 멀티스레드로 동작하는 것이다.

image


스레드의 생성과 실행

위에서 보았듯이 메인스레드 외에 다른 작업 스레드를 활용한다는 것은
작업 스레드가 수행할 코드를 작성하고, 작업 스레드를 생성해 실행 시킨다는 의미다.

방법1. Runnable 인터페이스를 구현한 run(); 메서드를 구현하여 스레드 생성,실행. 

public class ThreadExample {
    public static void main(String[] args) {

        //Runnable 인터페이스로 task1 라는 객체 생성
        Runnable task1 = new ThreadRun1();

        //Thread 클래스에 task1객체를 생성자로 thread1라는 객체를 만듬.
        Thread thread1 = new Thread(task1);

        //작업 스레드를 실행시켜줌
        thread1.start();
		
        //메인스레드 포문
        for (int i=1;i<=100;i++){
            System.out.print("/");
        }
        System.out.print("메인 스레드 종료");
    }
}

//Runnable을 구현하는 클래스
class ThreadRun1 implements Runnable{
    @Override
    public void run() {
    	//작업스레드 포문
        for(int i=1;i<=100;i++){
            System.out.print(i); // 1 부터 100까지 출력
        }
        System.out.print("작업 스레드 종료");
    }
}

Runnble이라는 인터페이스의를 상속한
클래스를 만들고 해당클래스의 run() 메서드를 오버라이드하여 구현해준다.
1~100까지 숫자를 출력하는 구문.

다형성에 의해
상위클래스가 하위클래스를 참조하는 객체를 만든다.
Runnable task1 = new ThreadRun1();

task1이라는 객체를 Thread클래스 객체의 생성자에 넣어 새로운 객체를
만들어준 후에 .start(); 메서드를 실행시켜주면
내가만든 해당 구현체가 작업스레드로 동작이 되는 모습을 볼 수있다.

전체적인 흐름을보기위해
작업스레드 실행시킨후
아래에 메인스레드에 “/”를 출력해주는 For문을 만들었다.

////////////////////////////12345678910111213141516171819202122232425/////////////////////////////////////////////////2627282930///////////////////////313233343536메인 스레드 종료373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100작업 스레드 종료

결과는 실행할때마다 바뀌고
메인스레드와 작업스레드가 동시에 병렬로 실행되는 모습을 볼 수 있다.


방법2. Thread 클래스를 상속 받은 하위 클래스에서 run()을 구현하여 스레드 생성,실행

public class ThreadExample {
    public static void main(String[] args) {
        ThreadRun1 thread1 = new ThreadRun1();
        thread1.start();
        for(int i=1;i<=100;i++){
            System.out.print("/"); // 1 부터 100까지 출력
        }
        System.out.print("메인스레드 종료");
    }
}

//Runnable을 구현하는 클래스
class ThreadRun1 extends Thread{
    @Override
    public void run() {
        for(int i=1;i<=100;i++){
            System.out.print(i); // 1 부터 100까지 출력
        }
        System.out.print("작업 스레드 종료");
    }
}

위와의 차이점은 방법1 은 Runnable 인터페이스를 직접 구현한 것이고
방법2는 Thread 클래스는 Runnable이라는 인터페이스를 상속받고있고
그 Thread 클래스를 상속받음으로서 run()을 구현 시켜서
작업 스레드를 실행 시켜주는 것이다.

image

출력을 할 경우

///////123456789101112131415161718////////////192021222324/////////////////////////////////////////25262728293031/////////32333435363738394041424344454647///////////////4849505152535455565758////////////59606162636465666768697071727374757677787980////메인스레드 종료81828384858687888990919293949596979899100작업 스레드 종료

방법1과 동일하게 병렬로 스레드가 진행되어 출력된 모습이 보여진다.
익명 하위객체를 활용한 스레드 생성 및 실행

public class ThreadExample {
    public static void main(String[] args) {

        // 익명 Thread 하위 객체를 활용한 스레드 생성
        Thread thread1 = new Thread() {
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    System.out.print(i);
                }
            }
        };

        thread1.start();

        for (int i = 1; i <= 100; i++) {
            System.out.print("/");
        }
    }
}


스레드의 이름

작업스레드로 생성한 스레드들은
“Thread-n”이라는 이름을 가진다.
이름을 조회하고 설정이 가능하다.


getName() - 이름조회.

public class ThreadGetName {
    public static void main(String[] args) {
        Runnable task1 = new ThreadRun1();
        Runnable task2 = new ThreadRun2();
        Runnable task3 = new ThreadRun3();
        Runnable task4 = new ThreadRun4();
        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);
        Thread thread3 = new Thread(task3);
        Thread thread4 = new Thread(task4);

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();

        System.out.println(thread1.getName()); // Thread-n 이름이 기본이름이다.
        System.out.println(thread2.getName());
        System.out.println(thread3.getName());
        System.out.println(thread4.getName());


    }
}

class ThreadRun1 implements Runnable{
    @Override
    public void run() {
        System.out.println("작업 스레드 1번");
    }
}
class ThreadRun2 implements Runnable{
    @Override
    public void run() {
        System.out.println("작업 스레드 2번");
    }
}
class ThreadRun3 implements Runnable{
    @Override
    public void run() {
        System.out.println("작업 스레드 3번");
    }
}
class ThreadRun4 implements Runnable{
    @Override
    public void run() {
        System.out.println("작업 스레드 4번");
    }
}

//getName만 출력시
Thread-0
Thread-1
Thread-2
Thread-3

Thread 클래스안에 이름을 조회하려면
getName()을 이용해 호출하면된다.

현재는 4개의 스레드를 일일이 구현했지만
한개의 구현채를 가지고도 여러개의 스레드를 구현할 수 있다.
먼저 생성되는 객체 순서의 스레드 기준으로 Thread-0 부터 숫자가 증가한다.


setName - 이름 변경

public class ThreadGetName {
    public static void main(String[] args) {
        Runnable task = new ThreadRun1();
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        Thread thread3 = new Thread(task);
        Thread thread4 = new Thread(task);

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread1.setName("작업스레드 1번");
        thread2.setName("작업스레드 2번");
        thread3.setName("작업스레드 3번");
        thread4.setName("작업스레드 4번");

        System.out.println(thread1.getName()); // Thread-n 이름이 기본이름이다.
        System.out.println(thread2.getName());
        System.out.println(thread3.getName());
        System.out.println(thread4.getName());
    }
}

class ThreadRun1 implements Runnable{
    @Override
    public void run() {

    }
}

//출력
작업스레드 1
작업스레드 2
작업스레드 3
작업스레드 4

setName 메서드를 이용해 이름을 변경할 수 있다.
캡슐화에서 배운 getter,setter와 동일하게 동작하는 느낌이다.
변경된 이름을 출력하면 위와같이 스레드 이름이 바뀐다.


currentThread().getName(); 현재 스레드의 이름을 출력하는 법

public class ThreadGetSetName {
    public static void main(String[] args) throws InterruptedException {
        Runnable task = new ThreadRun1();
        Thread thread1 = new Thread(task);
        
        thread1.setName("작업스레드 1번");
        thread1.start();


        //주소값 얻기
        System.out.println(Thread.currentThread().getName());

    }
}

class ThreadRun1 implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

//출력
main
작업스레드 1

Thread 클래스의 currentThread()를 호출해
이름을 얻을 수 있다.


스레드의 동기화

만약 같은 객체에 두개이상의 스레드를 동작시킬 경우
오작동하는 경우를 볼 수 있다.
예를들어 if문 동작중 다른 스레드가 끼어들어 조건문이 무시될 수 있다.
여기서 우리는 동기화를 적용해야한다.

임계 영역(Critical section)은 오로지 하나의 스레드만 코드를 실행할 수 있는 코드 영역을 의미한다.
락(Lock)은 임계 영역을 포함하고 있는 객체에 접근할 수 있는 권한을 의미한다.

스레드 A, 스레드 B가 있다고 가정하자
임계영역으로 설정된 객체가의 코드가 있을때
해당 코드를 접근할 경우에 락을 획득하여 임계 영역내의 코드를 실행한다.
(다른 스레드에 의해 작업이 이루어지지 않을때만)

즉, 스레드 B는 이미 스레드 A가 락상태이므로
임계영역에 코드를 읽을 수 없는 것이다.
임계영역에 코드를 모두 실행하면 락을 반납하여 다음스레드가 락을 획들할 수 있다.

한마디로 임계영역의 락이라는 권한을 스레드에 부여해
동시에 실행시킬 수 없도록 설정하는 것이다.
임계영역 설정할때는 synchronized 키워드를 사용한다.

설정 예시를 보자

class Calculate {
	...
	public synchronized int sum(int x,int y) {
		int sum = x+y;      
       	if(sum<0) return 0;    
        return sum;    
	}
}

접근제어자와 반환타입 사이에

synchronized 키워드를 사용해주면
메서드에 임계영역이 설정되어진다.

만약 메서드 안의 특정 영역을 임계영역으로 설정하고 싶을경우

class Calculate {
	...
	public synchronized int sum(int x,int y) {
		int sum = x+y;      

        synchronized (this) {
            if(sum<0) return 0;    
        }
		return sum;    
	}
}

If문에만 임계영역을 설정하고 싶은 경우에는
위와 같이 this에 해당하는 객체에 락을 얻고 배타적으로
임계영역 내의 코드를 실행한다.


스레드의 상태와 실행제어

스레드의 상태와 제어를 할 수 있는
메서드들을 공부해보자.

image

생성 = NEW
실행대기 = RUNNABLE
일시정지= TIMED_WAITING
소멸 = TERMINATED

각각의 스레드 상태는 .getState();로 확인이 가능하다.
스레드 생성 : strat()
스레드 일시정지 : sleep() , wait(), join()
스레드 일시정지 복귀 : interrupt() , notify() / (일시정지->실행대기)


sleep();

.sleep(long milliSecond)

밀리세컨드만큼 스레드를 일시정지하는 메서드이다.
실행대기로 넘어가는 경우는 2가지이다.

  1. 인자의 시간이 경과한 경우
  2. interrupt() 를 호출한 경우.
try { 
	Thread.sleep(1000); 
} catch (Exception e) {
	//인터럽트 발생시 catch로 이동
}

interrupt()로 강제로 실행대기로 호출하는 경우에는
try…catch문으로 반드시 예외처리를 해주어야한다.


interrupt();

void interrupt()

sleep(), wait(), join()에 의해 일시정지 상태에 있는 스레드를
실행 대기 상태로 복귀시킨다.

일시정지중인 스레드에
인터럽트함수를 호출하게되면
예외처리가 발생되어 catch구문을 읽고
다음구문으로 빠져나오게된다.


yield();

static void yield()

yield(); 메서드는 다른 스레드에게 실행을 양보하는 메서드이다.
실행에서 실행대기로 이동한다는 뜻이다.

예를들어

public void Example() {
        while (true) {
                if (example) {
                        ...
                } else Thread.yield();
        }
}

반복문을 순회하는 경우에
if문이 example == false인 경우에는 불필요하게 while문을 계속 실행하고 있는 것이다.
이럴 경우에 yield(); 메서드를 호출하면
exmaple의 값이 false일때 무의미한 반복문을 멈추고 실행대기 상태로 바뀌게된다.
자신에게 남은 실행 시간을 실행 대기열 상 우선순위가 높은 스레드로 양보해주는 것이다.


join();

void join()
void join(long milliSecond)

일시 중지 상태로 만드는 상태제어 메서드다.
인자로 시간을 전달하여 끝낼 수도있고
interrupt();를 호출해 종료할 수 있다.
sleep();과 유사한 기능이다.
sleep(); 은 static 메서드이고
join();은 인스턴스 메서드라는 차이점이 있다.
즉, 클래스명.sleep(); / 인스턴스명.join();의 차이이다.


wait(); notify();

한 객체를 가지고 작업할때 스레드 간의 협업에 사용된다.
서로 교대하며 일시정지하고 호출할때 사용한다.
wait() : 해당 스레드 일시정지 상태로 만듬
notify() : 상대 스레드 호출


JVM

이전에 자바 가상머신 JVM을 간단히 알아보는 시간을
부트캠프 첫주차에 가졌었다. 오늘은 조금더 상세히 알아보자

JVM 이란?

JVM은 Java Virtual Machine의 약자로
자바 프로그램을 실행시켜주는 도구라 생각하면된다.

JVM은 자바로 작성한 소스코드를 해석해 실행하는 별도의 프로그램이다.
즉, 프로그램을 실행시키는 프로그램이다.
번역기라 생각하면 비유가 될 것같다
어떠한 운영체제가 오더라도 그 사이에서
해석해 실행시켜주는 프로그램이다.

자바가 운영체제로 부터 독립적으로 동작할 수 있는 이유이다.

JVM의 내부 구조 , 실행 순서

image

Class Loader
-> Loading
-> Linking
-> Initialization

Execution Engine
-> Interpreter
-> JIT compiler
-> Garbage Collector

이렇게 구성되어있다.

가볍게 순서를 이해해보자

  1. 우리가 자바로 소스코드를 작성하고 실행하게되면
    컴파일을 진행하게 된다.
    컴파일 결과로 .java 확장자를 가졌던 자바 소스코드가
    .class 확장자를 가진 바이트 코드 파일로 변환된다.

  2. Class Loader가 바이트 코드 파일을 JVM 내부로 불러들여
    런타임 데이터 영역에 적재시킨다. (자바 소스코드를 메모리에 로드시키는것)
    위에 그림을 보면 런타임 데이터 영역에는 많은 구역이 존재한다.

  3. 로드가 완료되면 실행엔진(Execution Engine)이 런터임 데이터 영역에
    적재된 바이트 코드를 실행 시킨다. 두가지 방식으로 바이트 코드로 실행시킨다.

1). 인터프리터를 통해 한줄씩 기계어로 번역하고 실행
2). JIT 컴파일러를 통해 바이트 코드 전체를 기계어로 번역하고 실행
실행 엔진은 기본적으로 1번방법으로 번역하다가
특정 바이트 코드가 자주 실행되면 2번방법을 통해 실행시킨다.


Runtime Data Area

위에서 순서를 알아보았는데 런타임 데이터 영역을 자세히 알아보자
위에서 보듯이 런타임 영역은
크게 5가지로 구분되어있다.

image

여기서 중요한 Heap과 Stack만 먼저 공부해보자.

Stack Area

스택은 일종의 자료구조로 프로그램이 데이터를 저장하는 방식을 의미한다.
저장 방식중 Stack이 있는 것이고 그영역에 저장하는 데이터가
Stack Area이다.

내가 일하던 스마트 팩토리 자동화 라인에서는
선입선출, 후입선출 이라는 개념이 있다.
선입선출은 먼저들어온 제품은 무조건 먼저 나가야하는 것이고
후입선출은 가장마지막에 들어온 제품이 첫번째로 나가는 것이다.
스택방식이 후입선출과 동일한 방식을 채택한 저장 방식이다.

IT 업계 용어로는 LIFO(Last in First out)이라는 단어를 쓰는 것 같다.

image

Heap Area

JVM은 단 하나의 Heap 영역이 존재한다.
JVM이 작동하면 자동생성되며, 이 영역에는 객체, 인스턴스 변수, 배열이 저장된다.
객채 생성이 실행되면
Heap 영역에는 인스턴스가 생성된 위치의 주소값을 할당해준다.
여기서 할당된 변수는 Stack 영역에 선언된 변수이다.

즉, 우리가 객체를 다룬다는 것은 Stack 영역에 저장되어 있는 참조변수를 통해
Heap 영역에 존재하는 객체를 다룬다는 의미가 된다.


마치며…

오늘은 오전에는 stream 관련 문제를 쭉풀었다.
어제 공부를 많이해두어서 그런지
문제가 술술풀렸다!!!

역시 반환타입을 잘 알아두니까 반환되는 타입의
메소드들을 사용할때 API 문서 찾아보기도 진짜 유용했다.
앞으로 쭉 이렇게 공부하면서 익숙해져보자.

그리고 오후에는 주로 이론 위주 공부였고
조금 중요했던 Thread에 관한 내용을 배웠다.
멀티쓰레드 관리하는 것이 조금 중요하게 느껴졌고
실제 웹동작을 시킬때 여러행위가 일어날때에 대한 처리를
어떻게 해줘야하지? 고민해본적이 있는데
이런점을 조심 해소 시켜준 공부시간이였던 것 같다.

그리고 이전에 1주차에 찍먹했던 JVM에 대해 조금더 어떻게 구조가 되고
자바 프로그램이 어떻게 컴파일되어가는지 조금 더 이전보다 자세히 알게되었다.
이렇게 차근차근하다보면 언젠간 지식이 쌓여서
빛이 될날을 기다리며 내일도 열심히 달려보자!!!

오늘 공부는 여기서 끝


오늘의 커피량: ☕️ ☕️ ☕️ ☕️️️️
오늘의 점심: 라면, 밥