본문 바로가기
Web & Mobile/JAVA

Lecture 34 - Java(13) 프로세스, 쓰레드, 멀티태스킹, 멀티쓰레딩, Network, Client, Server, URL

by Bennyziio 2023. 6. 20.
반응형
프로세스
    실행 중인 프로그램
        => 데이터(메모리공간) + CPU 할당

    foreground process
        전면 프로세스
        화면동시에 지금 실행하고 있는 프로세스
        응용프로그램
    background process 
        후면 프로세스
        화면없이 실행되고 있는 프로세스
        서비스 / 서버류

    멀티 프로세스(멀티 태스킹 - Windows)

    프로세스 - 쓰레드(작업단위)로 구성

프로세스와 쓰레드
프로세스(process)란 간단히 말해서 '실행 중인 프로그램(program)'이다. 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다.
프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어 있으며 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드이다.
a) 싱글 쓰레드 : 자원 + 쓰레드
b) 멀티 쓰레드 : 자원 + 쓰레드 + 쓰레드 ...

쓰레드가 작업을 수행하는데 개별적인 메모리 공간(호출스택)을 필요, 프로세스의 메모리 한계에 따라 생성할 수 있는 쓰레드의 수가 결정된다.

멀티태스킹과 멀티쓰레딩
멀티태스킹(Multi-tasking, 다중작업) : 여러 개의 프로세스가 동시에 실행
멀티쓰레딩(Multi-threading) : 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행
CPU 코어(core)가 한 번에 단 하나의 작업만 수행할 수 있어, 처리해야하는 쓰레드의 수는 언제나 코어의 개수보다 훨씬 많기 때문에 각 코어가 아주 짧은 시간 동안 여러 작업을 번갈아 가며 수행함으로써 여러 작업들이 모두 동시에 수행되는 것처럼 보이게 한다.

멀티쓰레딩의 장점
- CPU 사용률을 향상시킨다
- 자원을 보다 효율적으로 사용할 수 있다.
- 사용자에 대한 응답성이 향상된다.
- 작업이 분리되어 코드가 간결해진다.

여러 사용자에게 서비스를 해주는 서버 프로그램의 경우 멀티쓰레드로 작성하는 것은 필수적, 서버 프로세스가 여러 개의 쓰레드를 생성해서 쓰레드와 사용자의 요청이 일대일로 처리되도록 프로그래밍해야 한다.

멀티쓰레딩의 단점 
: 여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 작업을 하기 때문에 발생할 수 있는 동기화(synchronization ) = 트랜잭션, 교착상태(deadlock)와 같은 문제들을 고려해서 신중이 프로그래밍 해야한다.

쓰레드의 구현과 실행
1. Thread클래스를 상속
2. Runnable인터페이스를 구현
Thread클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에, Runnable인터페이스를 구현하는 방법이 일반적이다.

쓰레드의 실행 - start()

Thread1

package Thread1;

public class Go {
	public void go() {
		for(int i=1; i<=10; i++) {
			System.out.println("go : " + i);
		}
	}
}
package Thread1;

public class Come {
	public void come() {
		for(int i=1; i<=10; i++) {
			System.out.println("come : " + i);
		}
	}
}
package Thread1;

public class Come {
	public void come() {
		for(int i=1; i<=10; i++) {
			System.out.println("come : " + i);
		}
	}
}
package Thread1;

public class MainEx {

	public static void main(String[] args) {
		Go g = new Go();
		Come c = new Come();
		
		// 순차처리
		g.go();
		c.come();
	}
}

Thread2 - Thread 상속

package Thread2;

public class Go extends Thread {

	@Override
	public void run() {
		// Thread에서 해야할 작업 기록
		for(int i=1; i<=10; i++) {
			System.out.println("go : " + i);
		}
	}
}
package Thread2;

public class Come extends Thread {
	@Override
	public void run() {
		for(int i=1; i<=10; i++) {
			System.out.println("come : " + i);
		}
	}
}
package Thread2;

public class MainEx {

	public static void main(String[] args) {
		Go g = new Go();
		Come c = new Come();
		
		// 이건 Thread 기법이 아님
		//g.run(); 
		//c.run();
		
		// 병렬 처리
		g.start();
		c.start();
	}

}

start()와 run()
쓰레드를 실행시킬 때 run()이 아닌 start()를 호출한다는 것에 대해서 다소 의문이 들었을 것이다. main메서드에서 run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아니라 단순히 클래스에 선언된 메서드를 호출하는 것일 뿐이다.
반면에 start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택(call stack)을 생성한 다음에 run()을 호출해서, 생성된 호출스택에 run()이 첫 번재로 올라가게 한다.
호출스택에서는 가장 위에 있는 메서드가 현재 실행중인 메서드이고 나머지 메서드들은 대기상태에 있다는 것을 기억하고 있을 것이다. 쓰레드가 둘 이상일 때는 호출스택의 최상위에 있는 메서드일지라도 대기상태에 있을 수 있다.
스케줄러는 실행대기중인 쓰레드들의 우선순위를 고려하여 실행순서와 실행시간을 결정하고, 각 쓰레드들은 작성된 스케줄에 따라 자신의 순서가 되면 지정된 시간동안 작업을 수행한다.
이 때 주어진 시간동안 작업을 마치지 못한 쓰레드는 다시 자신의 차례가 돌아올 때까지 대기상태로 있게 되며, 작업을 마친 쓰레드, 즉 run()의 수행이 종료된 쓰레드는 호출스택이 모두 비워지면서 이 쓰레드가 사용하던 호출스택은 사라진다.

Main쓰레드
main메서드의 작업을 수행하는 것도 쓰레드이며, 이를 main쓰레드라고 한다. 프로그램을 실행하면 기본적으로 하나의 쓰레드(일꾼)를 생성하고, 그 쓰레드가 main메서드를 호출해서 작업이 수행되도록 하는 것이다.
지금까지는 main메서드가 수행을 마치면 프로그램이 종료되었으나, main메서드가 수행을 마쳤다하더라도 다른 쓰레드가 아직 작업을 마치지 않은 상태라면 프로그램이 종료되지 않는다.
실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다

Thread 3

package Thread3;

public class Go implements Runnable {

	@Override
	public void run() {
		for(int i=1; i<=10; i++) {
			System.out.println("go : " + i);
		}
	}
}
package Thread3;

public class Come implements Runnable {

	@Override
	public void run() {
		for(int i=1; i<=10; i++) {
			System.out.println("come : " + i);
		}
	}
}
package Thread3;

public class MainEx {

	public static void main(String[] args) {
		Go g = new Go();
		Come c = new Come();
		// implement로 불러 온 거라 Thread로 바꿔줘야 한다.
		Thread t1 = new Thread(g);
		Thread t2 = new Thread(c);
		
		t1.start();
		t2.start();
	}
}

Thread 4

package Thread4;

public class Gugudan extends Thread {
	private int dan;

	public Gugudan(int dan) {
		this.dan = dan;
	}

	@Override
	public void run() {
		
		System.out.println(this.getName() + " 시작");
		
		for(int i=1; i<9; i++) {
			System.out.printf("%s X %s = %s%n", dan, i, (dan*i));
		}
		
		System.out.println(this.getName() + " 끝");
	}
	
}
package Thread4;

public class MainEx {

	public static void main(String[] args) {
		Gugudan g1 = new Gugudan(3);
		Gugudan g2 = new Gugudan(6);
		
		g1.start();
		g2.start();
	}
}

package Thread4;

public class MainEx {

	public static void main(String[] args) {
		Gugudan g1 = new Gugudan(3);
		Gugudan g2 = new Gugudan(6);
		
		g1.setName("3단");	// this.getName과 한 쌍이다
		g2.setName("6단");	// this.getName과 한 쌍이다
		
		g1.start();
		g2.start();
	}
}

Thread클래스를 상속받으면, 자손 클래스에서 조상인 Thread클래스의 메서드를 this.을 통해 직접 호출할 수 있지만, Runnable을 구현하면 Thread클래스의 static 메서드인 currentThread()를 호출하여 쓰레드에 대한 참조를 얻어 와야만 호출이 가능하다.
그래서 Thread를 상속받은 클래스에서 간단히 getName()을 호출하면 되지만, Runnable을 구현한 클래스에서는 멤버라고는 run()밖에 없기 때문에 Thread클래스의 getName()을 호출하려면, "Thread.currentThread().getName()'와 같이 해야 한다.

Thread 5

package Thread5;

public class Gugudan implements Runnable {
	private int dan;

	public Gugudan(int dan) {
		this.dan = dan;
	}
	
	@Override
	public void run() {
		// Runnalbe을 구현하면 Thread클래스의 static메서드인 current Thread()를 
		// 호출하여 쓰레드에 대한 참조를 얻어와야만 호출이 가능함
		System.out.println(Thread.currentThread().getName() + " 시작");
		
		for(int i=1; i<=9; i++) {
			System.out.printf("%s X %s = %s%n", dan, i, (dan*i));
		}
		
		System.out.println(Thread.currentThread().getName() + " 끝");
	}
}
package Thread5;

public class MainEx {

	public static void main(String[] args) {
		Gugudan g1 = new Gugudan(3);
		
		Thread t1 = new Thread(g1);
		// runnable은 run()값을 받아오기 때문에 thread.currentThread().를 통해서 get이든 set이든 가져오지만
		// 해당 클래스에서는 위에 implement를 thread로 변환시켰기 때문에
		// t1.setName("3단");으로 해도 바로 가져 올 수 있는 것이다.
		t1.setName("3단");
		
		t1.start();
	}
}

싱글쓰레드와 멀티쓰레드
하나의 쓰레드로 두 작업을 처리하는 경우는 한 작업을 마친 후에 다른 작업을 시작하지만, 두 개의 쓰레드로 작업 하는 경우에는 짧은 시간동안 2개의 쓰레드가 번갈아 가면서 작업을 수행해서 동시에 두 작업이 처리되는 것과 같이 느끼게 한다
하나의 쓰레드로 두개의 작업을 수행한 시간과 두개의 쓰레드로 두 개의 작업을 수행한 시간은 거의 같으나 오히려 두개의 쓰레드로 작업한 시간이 싱글쓰레드로 작업한 시간보다 더 걸리게 되는데 그 이유는 쓰레드간의 작업전환(context switching)에 시간이 걸리기 때문이다.
작업 전환을 할 때는 현재 진행 중인 작업의 상태, 예를 들면 다음에 실행해야할 위치(PC, 프로그램 카운터) 등의 정보를 저장하고 읽어 오는 시간이 소요된다. 참고로 쓰레드의 스위칭에 비해 프로세스의 스위칭이 더 많은 정보를 저장해야하므로 더 많은 시간이 소요된다.

쓰레드의 우선 순위
쓰레드는 우선순위(priority)라는 속성(멤버변수)을 가지고 있는데, 이 우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라진다. 쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 서로 다르게 지정하여 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수 있다.
예를 들어 파일전송기능이 있는 메신저의 경우, 파일다운로드를 처리하는 쓰레드보다 채팅내용을 전송하는 쓰레드의 우선순위가 더 높아야 사용자가 채팅하는데 불편함이 없을 것이다.
이처럼 시각적인 부분이나 사용자에게 빠르게 반응해야하는 작업을 하는 쓰레드의 우선순위는 다른 작업을 수행하는 쓰레드에 비해 높아야 한다.
쓰레드가 가질 수 있는 우선순위의 범위는 1~10이며 숫자가 높을수록 우선순위가 높다.

public static final int MAX_PRIORITY = 10 // 최대우선순위
public static final int MIN_PRIORITY = 1 // 최소우선순위
public static final int NORM_PRIORITY = 5 // 보통우선순위

package Thread6;

public class MainEx {

	public static void main(String[] args) {
		
		System.out.println(Thread.currentThread().getPriority());
		
		System.out.println(Thread.MAX_PRIORITY);
		System.out.println(Thread.MIN_PRIORITY);
		System.out.println(Thread.NORM_PRIORITY);
		
		Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
		System.out.println(Thread.currentThread().getPriority());
	}
}

쓰레드는 '사용자 쓰레드(user thread)'와 '데몬 쓰레드(daemon thread)', 두 종류가 있다.
데몬 쓰레드(daemon thread)
데몬 쓰레드는 다른 일반 쓰레드(데몬 쓰레드가 아닌 쓰레드)의 작업을 돕는 보조적인 역활을 수행하는 쓰레드이다. 일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동 종료되는데, 그 이유는 데몬 쓰레드는 일반 쓰레드의 보조역활을 수행하므로 일반 쓰레드가 모두 종료되고 나면 데몬 쓰레드의 존재의 의미가 없기 때문이다.
이 점을 제외하고는 데몬 쓰레드와 일반 쓰레드는 다르지 않다. 데몬 쓰레드의 예로는 가비지 컬렉터, 워드프로세서의 자동저장, 화면자동갱신 등이 있다.
데몬 쓰레드는 무한루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.

ThreadEx10

package ThreadEx10;

public class ThreadEx10 implements Runnable {
	static boolean autoSave = false;
	
	@Override
	public void run() {
		while(true) {
			try {
				Thread.sleep(3 * 1000); // 3초마다
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			if(autoSave) {
				autoSave();
			}
		}
	}
	
	public void autoSave() {
		System.out.println("작업파일이 자동저장되었습니다.");
	}

	public static void main(String[] args) {
		Thread t = new Thread(new ThreadEx10());
		t.setDaemon(true);
		t.start();
		
		for(int i=1; i<=10; i++) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(i);
			
			if(i==5) {
				autoSave = true;
			}
			
			System.out.println("프로그램을 종료합니다.");
		}
	}
}

ThreadEx11

package ThreadEx11;

public class ThreadEx11_1 extends Thread {
	ThreadEx11_1(String name) {
		super(name);
	}
	
	@Override
	public void run() {
		try {
			sleep(5*1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
package ThreadEx11;

import java.util.Iterator;
import java.util.Map;

public class ThreadEx11_2 extends Thread {
	ThreadEx11_2(String name) {
		super(name);
	}
	
	@Override
	public void run() {
		Map map = getAllStackTraces();
		Iterator it = map.keySet().iterator();
		
		int x=0;
		while(it.hasNext()) {
			Object obj = it.next();
			Thread t = (Thread)obj;
			StackTraceElement[] ste = (StackTraceElement[])(map.get(obj));
			
			System.out.println( "[" + ++x + "] name : " + t.getName()
								+ ", group : " + t.getThreadGroup().getName()
								+ ", daemon : " + t.isDaemon());
			
			for(int i=0; i<ste.length; i++) {
				System.out.println(ste[i]);
			}
			
			System.out.println();
		}
	}
	
}
package ThreadEx11;

public class ThreadEx11 {

	public static void main(String[] args) throws Exception {
		ThreadEx11_1 t1 = new ThreadEx11_1("Thread1");
		ThreadEx11_2 t2 = new ThreadEx11_2("Thread2");
		t1.start();
		t2.start();
	}
}

순서는 상관없이 현재 만들어둔 패키지개수가 8개 이며 각 패키지 별로 현재 작동중인 쓰레드의 정보를 나열한 예제이다

Map : key와 value가 있어서 key를 통해서 value를 얻어올 수 있다
Iterator는 자바의 컬렉션 프레임웍에서 컬렉션에 저장되어 있는 요소들을 읽어오는 방법에 사용한다
keySet() : Hashmap에 저장된 모든 키가 저장된 set을 반환
hasNext() : Iterator가 가르키는 데이터 저장소의 현재 위치에서 이동할 항목이 있는 체크하고 있으면 true 없으면 false를 리턴
next() : 실제로 Iterator가 자신이 가르키는 데이터 저장소에서 현재위치를 순차적으로 하나 증가해서 이동
StackTraceElement() : 스택트레이스 정보를 구하여 임의의 처리를 하고 싶은 경우에 사용
getThreadGroup() : 쓰레드 자신이 속한 쓰레드 그룹 반환
getAllStackTraces()를 이용하면 실행 중 또는 대기상태, 즉 작업이 완료되지 않은 모든 쓰레드의 호출스택을 출력할 수 있다.

sleep(long millis) - 일정시간동안 쓰레드를 멈추게 한다
sleep()은 지정된 시간동안 쓰레드를 멈추게 한다
밀리세컨드(millis, 1000분의 일초)와 나노세컨드(nanos, 10억분의 일초)의 시간단위로 세밀하게 값을 지정할 수 있지만 어느 정도의 오차가 발생할 수 있다는 것은 염두에 둬야 한다.
sleep()에 의해 일시정지 상태가 된 쓰레드는 지정된 시간이 다 되거나 interrup()가 호출되면, InterruptedException이 발생되어 잠에서 깨어나 실행대기 상태가 된다.
그래서 sleep()을 호출할 때는 항상 try-catch문으로 예외를 처리해줘야 한다.
ThreadEx12 - sleep()

package ThreadEx12;

public class ThreadEx12_1 extends Thread {
	@Override
	public void run() {
		for(int i=0; i<300; i++) {
			System.out.print("-");
		}
		System.out.print("<<th1 종료>>");
	}
}
package ThreadEx12;

public class ThreadEx12_2 extends Thread {
	@Override
	public void run() {
		for(int i=0; i<300; i++) {
			System.out.print("|");
		}
		System.out.print("<<th2 종료");
	}
}
package ThreadEx12;

public class ThreadEx12 {

	public static void main(String[] args) {
		ThreadEx12_1 th1 = new ThreadEx12_1();
		ThreadEx12_2 th2 = new ThreadEx12_2();
		
		th1.start();
		th2.start();
		
		try {
			th1.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.print("<<main 종료>>");
	}
}

interrupt()와 interrupted() - 쓰레드의 작업을 취소한다
진행 중인 쓰레드의 작업이 끝나기 전에 취소시켜야할 때가 있다. 예를 들어 큰 파일을 다운로드 받을때 시간이 너무 오래 걸리면 중간에 다운로드를 포기하고 취소할 수 있어야 한다. interrupt()는 쓰레드에게 작업을 멈추라고 요청한다. 단지 멈추라고 요청만 하는 것일 뿐 쓰레드를 강제로 종료시키지는 못한다.

suspend(), resume(), stop()
suspend()는 sleep()처럼 쓰레드를 멈추게 한다. suspend()에 의해 정지된 쓰레드는 resume()을 호출해야 다시 실행대기 상태가 된다. stop()은 호출되는 즉시 쓰레드가 종료된다.
그러나 이들은 현재 Deprecated되어 있는데 이유는 교착상태를 일으키기 쉽기 때문이다.

ThreadEx15 - suspend(), resume(), stop()

package ThreadEx15;

public class RunImplEx15 implements Runnable {

	@Override
	public void run() {
		while(true) {
			System.out.println(Thread.currentThread().getName());
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}
package ThreadEx15;

public class ThreadEx15 {

	public static void main(String[] args) {
		RunImplEx15 r = new RunImplEx15();
		Thread th1 = new Thread(r, "*");
		Thread th2 = new Thread(r, "**");
		Thread th3 = new Thread(r, "***");
		th1.start();
		th2.start();
		th3.start();
		
		try {
			Thread.sleep(2000);
			th1.suspend();			// 쓰레드 th1을 잠시 중단시킨다
			Thread.sleep(2000);
			th2.suspend();
			Thread.sleep(3000);
			th1.resume();			// 쓰레드 th1이 다시 동작 하도록 한다
			Thread.sleep(3000);
			th1.stop();				// 쓰레드 th1을 강제종료시킨다
			th2.stop();
			Thread.sleep(2000);
			th3.stop();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

Yield() - 다른 쓰레드에게 양보한다.
yield()는 쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보(yield)한다. 예를 들어 스케쥴러에 의해 1초의 실행시간을 할당받은 쓰레드가 0.5초의 시간동안 작업 한 상태에서 yield()가 호출되면, 나머지 0.5초는 포기하고 다시 실행대기상태가 된다.
yield()와 interrupt()를 적절히 사용하면, 프로그램의 응답성을 높이고 보다 효율적인 실행이 가능하게 할 수 있다.

ThreadEx18 - Yield

package ThreadEx18;

public class Gugudan implements Runnable {
	private int dan;
	
	
	public Gugudan(int dan) {
		this.dan = dan;
	}

	@Override
	public void run() {
		if(dan == 8) {
			System.out.println("8단 yield ...");
			Thread.yield();
		}
		for(int i=1; i<=9; i++) {
			System.out.printf("%s X %s = %s%n", dan, i, (dan*i));
		}
	}
}
package ThreadEx18;

public class MainEx {

	public static void main(String[] args) {
		Gugudan g1 = new Gugudan(1);
		Gugudan g2 = new Gugudan(8);
		Gugudan g3 = new Gugudan(9);
		
		Thread th1 = new Thread(g1);
		Thread th2 = new Thread(g2);
		Thread th3 = new Thread(g3);
		
		th1.start();
		th2.start();
		th3.start();
	}
}

Join() - 다른 쓰레드의 작업을 기다린다.
쓰레드 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 할 때 join()을 사용한다.
시간을 지정하지 않으면, 해당 쓰레드가 작업을 모두 마칠 때까지 기다리게 된다. 작업 중에 다른 쓰레드의 작업이 먼저 수행되어야 할 필요가 있을 때 join()을 사용한다.
join()도 sleep()처럼 interrupt()에 의해 대기상태에서 벗어날 수 있으며, join()이 호출되는 부분을 try-catch문으로 감싸야 한다. join()은 여러모로 sleep()과 유사한 점이 많은데, sleep()과 다른 점은 join()은 현재 쓰레드가 아닌 특정 쓰레드에 대해 동작하므로 static메서드가 아니라는 것이다.

ThreadEx19

package ThreadEx19;

public class ThreadEx19_1 extends Thread {
	@Override
	public void run() {
		for(int i=0; i<300; i++) {
			System.out.print("-");
		}
	}
}
package ThreadEx19;

public class ThreadEx19_2 extends Thread {
	@Override
	public void run() {
		for(int i=0; i<300; i++) {
			System.out.print("|");
		}
	}
}
package ThreadEx19;

public class ThreadEx19 {
	static long startTime = 0;
	
	public static void main(String[] args) {
		ThreadEx19_1 th1 = new ThreadEx19_1();
		ThreadEx19_2 th2 = new ThreadEx19_2();
		
		th1.start();
		th2.start();
		startTime = System.currentTimeMillis();
		
		try {
			th1.join();
			th2.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.print("소요시간 : " + (System.currentTimeMillis() - ThreadEx19.startTime));
	}
}

쓰레드의 동기화 - transaction
멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원(멤버변수)을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게 된다. 만일 쓰레드 A가 작업하던 도중에 다른 쓰레드 B에게 제어권이 넘어갔을 때, 쓰레드 A가 작업하던 공유데이터를 쓰레드 B가 임의로 변경하였다면, 다시 쓰레드 A가 제어권을 받아서 나머지 작업을 마쳤을 때 원래 의도했던 것과는 다른 결과를 얻을 수 있다.
이러한 일이 발생하는 것을 방지하기 위해서 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하는 것이 필요하다. 그래서 도입된 개념이 바로 '임계영역(critical section)'과 '잠금(락, lock)'이다.
공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정해놓고, 공유 데이터(객체)가 가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 한다. 그리고 해당 쓰레드가 임계 영역내의 모든 코드를 수행하고 벗어나서 lock을 반납해야만 다른 쓰레드가 반납된 lock을 획득하여 임계 영역의 코드를 수행할 수 잇게 된다.
한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 '쓰레드의 동기화(synchronization)'라고 한다.

synchronized를 이용한 동기화
첫번째 방법 : 메서드 앞에 synchronized를 붙이는 것인데, synchronized를 붙이면 메서드 전체가 임계 영역으로 설정된다. 쓰레드는 synchronized메서드가 호출된 시점부터 해당 메서드가 포함된 객체의 lock을 얻어 작업을 수행하다가 메서드가 종료되면 lock을 반환한다.
두번째 방법(많이 사용) : 메서드 내의 코드 일부를 블럭{}으로 감싸고 블럭 앞에 'synchronized(참조변수)'를 붙이는 것인데, 이때 참조변수는 락을 걸고자하는 객체를 참조하는 것이어야 한다. 이 블럭을 synchronized블럭이라고 부르며, 이 블럭의 영역 안으로 들어가면서부터 쓰레드는 지정된 객체의 lock을 얻게 되고, 이 블럭을 벗어나면 lock을 반납한다.

ThreadEx21

package ThreadEx21;
// 통장 역활
public class Account {
	// 통장 잔고
	int balance = 1000;
	
	// 인출
	public void withdraw(int money) {
		if(balance >= money) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			balance -= money;
		}
	}
}
package ThreadEx21;
// 고객
public class User implements Runnable {
	Account account = null;
	
	public User(Account account) {
		this.account = account;
	}

	@Override
	public void run() {
		while(account.balance > 0) {
			int money = (int)(Math.random() * 3 + 1) * 100;
			// 인출
			account.withdraw(money);
			System.out.println("통장 잔고 : " + account.balance);
		}
	}
}
package ThreadEx21;

public class ATMEx {

	public static void main(String[] args) {

		Account account = new Account();
		// 유저 1
		User user1 = new User(account);
		// 유저 2
		User user2 = new User(account);
		
		Thread t1 = new Thread(user1);
		Thread t2 = new Thread(user2);
		
		t1.start();
		t2.start();
	}
}

 

synchronized

package ThreadEx21;
// 통장 역활
public class Account {
	// 통장 잔고
	int balance = 1000;
	
	// 인출
	public synchronized void withdraw(int money) {
		if(balance >= money) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			balance -= money;
		}
	}
}
package ThreadEx21;
// 고객
public class User implements Runnable {
	Account account = null;
	
	public User(Account account) {
		this.account = account;
	}

	@Override
	public void run() {
		while(account.balance > 0) {
			int money = (int)(Math.random() * 3 + 1) * 100;
			// 인출
			account.withdraw(money);
			System.out.println("통장 잔고 : " + account.balance);
		}
	}
}
package ThreadEx21;

public class ATMEx {

	public static void main(String[] args) {

		Account account = new Account();
		// 유저 1
		User user1 = new User(account);
		// 유저 2
		User user2 = new User(account);
		
		Thread t1 = new Thread(user1);
		Thread t2 = new Thread(user2);
		
		t1.start();
		t2.start();
	}
}

ThreadWaitEx1

package ThreadEx1;

public class Cook implements Runnable {
	private Table table;

	public Cook(Table table) {
		this.table = table;
	}

	@Override
	public void run() {
		while(true) {
			int idx = (int)(Math.random() * table.dishNum());
			table.add(table.dishNames[idx]);
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}
package ThreadEx1;

public class Customer implements Runnable {
	private Table table;
	private String food;
	
	public Customer(Table table, String food) {
		this.table = table;
		this.food = food;
	}

	@Override
	public void run() {
		while(true) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			String name = Thread.currentThread().getName();
			
			if(eatFood()) {
				System.out.println(name + " ate a " + food);
			} else {
				System.out.println(name + " failed to eat. :(");
			}
		}	
	}
	boolean eatFood() {
		return table.remove(food);
	}
}
package ThreadEx1;

import java.util.ArrayList;

public class Table {
	String[] dishNames = {"donut", "donut", "burger"};
	final int MAX_FOOD = 6;
	private ArrayList<String> dishes = new ArrayList<>();
	
	public synchronized void add(String dish) {
		if(dishes.size() >= MAX_FOOD) {
			return;
		}
		dishes.add(dish);
		System.out.println("Dishes:" + dishes.toString());
	}
	
	public boolean remove(String dishName) {
		for(int i=0; i<dishes.size(); i++) {
			if(dishName.equals(dishes.get(i))) {
				dishes.remove(i);
				return true;
			}
		}
		return false;
	}
	public int dishNum() {
		return dishNames.length;
	}
}
package ThreadEx1;

public class ThreadWaitEx1 {

	public static void main(String[] args) throws Exception {
		Table table = new Table();
		
		new Thread(new Cook(table), "COOK1").start();
		new Thread(new Customer(table, "donut"), "CUST1").start();
		new Thread(new Customer(table, "burger"), "CUST2").start();
		Thread.sleep(100);
		System.exit(0);
	}
}

이 예제를 반복해서 실행하면 2가지 종류의 예외가 발생하는데 cook쓰레드가 테이블에 음식을 놓는 도중에 , customer 쓰레드가 음식을 가져가려했기 때문에 발생하는 예외(concurrentModificationException)이고 다른 하나는 customer쓰레드가 테이블의 마지막 남은 음식을 가져가는 도중에 다른 customer쓰레드가 먼저 음식을 낚아채버려서 있지도 않은 음식을 테이블에서 제거하려 했기 때문에 발생하는 예외(IndexOutOfBoundsException)이다. 이런 예외들이 발생하는 이유는 여러 쓰레드가 테이블을 공유하는데도 동기화를 하지 않았기 때문이다.

ThreadWaitEx2

package ThreadEx2;

public class Cook implements Runnable {
	private Table table;

	public Cook(Table table) {
		this.table = table;
	}

	@Override
	public void run() {
		while(true) {
			int idx = (int)(Math.random() * table.dishNum());
			table.add(table.dishNames[idx]);
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}
package ThreadEx2;

public class Customer implements Runnable {
	private Table table;
	private String food;
	
	public Customer(Table table, String food) {
		this.table = table;
		this.food = food;
	}

	@Override
	public void run() {
		while(true) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			String name = Thread.currentThread().getName();
			
			if(eatFood()) {
				System.out.println(name + " ate a " + food);
			} else {
				System.out.println(name + " failed to eat. :(");
			}
		}	
	}
	boolean eatFood() {
		return table.remove(food);
	}
}
package ThreadEx2;

import java.util.ArrayList;

public class Table {
	String[] dishNames = {"donut", "donut", "burger"};
	final int MAX_FOOD = 6;
	private ArrayList<String> dishes = new ArrayList<>();
	
	public synchronized void add(String dish) {
		if(dishes.size() >= MAX_FOOD) {
			return;
		}
		dishes.add(dish);
		System.out.println("Dishes:" + dishes.toString());
	}
	
	public boolean remove(String dishName) {
		while(dishes.size() == 0) {
			String name = Thread.currentThread().getName();
			System.out.println(name+" is waiting.");
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		for(int i=0; i<dishes.size(); i++) {
			if(dishName.equals(dishes.get(i))) {
				dishes.remove(i);
				return true;
			}
		}
		return false;
	}
	public int dishNum() {
		return dishNames.length;
	}
}
package ThreadEx2;

public class ThreadWaitEx2 {

	public static void main(String[] args) throws Exception {
		Table table = new Table();
		
		new Thread(new Cook(table), "COOK1").start();
		new Thread(new Customer(table, "donut"), "CUST1").start();
		new Thread(new Customer(table, "burger"), "CUST2").start();
		Thread.sleep(5000);
		System.exit(0);
	}
}

이 예제에서는 여러 쓰레드가 공유하는 객체인 테이블(Table)의 add()와 remove()를 동기화 하였다. 더이상 전과 같은 예외는 발생하지 않지만 뭔가 원활히 진행되고 있는 것 같지는 않다. customer 쓰레드가 원하는 음식이 테이블에 없으면, 'fail to eat'을 출력하고 테이블에 음식이 하나도 없으면, 0.5초마다 음식이 추가되었는지 확인하면서 기다리도록 작성되어있다. 그런데, cook 쓰레드는 왜 음식을 추가하지 않고 customer쓰레드를 기다리게 하는 것인가? 그 이유는 손님 쓰레드가 테이블 객체의 lock을 쥐고 기다리기 때문이다. cook 쓰레드가 음식을 새로 추가하려해도 테이블 객체의 lock을 얻을 수 없어서 불가능하다. 이럴때 사용하는 것이 바로 'wait() & notify()'이다 customer 쓰레드가 lock을 쥐고 기다리는게 아니라, wait()으로 lock을 풀고 기다리다가 음식이 추가되면 notify()로 통보를 받고 다시 lock을 얻어서 나머지 작업을 진행하게 할 수 있다.

ThreadWaitEx3 - wait()과 notify() 추가한 것

package ThreadEx3;

public class Cook implements Runnable {
	private Table table;
	

	public Cook(Table table) {
		this.table = table;
	}

	@Override
	public void run() {
		while(true) {
			int idx = (int)(Math.random() * table.dishNum());
			table.add(table.dishNames[idx]);
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}
package ThreadEx3;

public class Customer implements Runnable {
	private Table table;
	private String food;
	
	public Customer(Table table, String food) {
		this.table = table;
		this.food = food;
	}

	@Override
	public void run() {
		while(true) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			String name = Thread.currentThread().getName();
			table.remove(food);
			System.out.println(name + " ate a " + food);
		}
	}
}
package ThreadEx3;

import java.util.ArrayList;

public class Table {
	String[] dishNames = {"donut", "donut", "burger"};
	final int MAX_FOOD = 6;
	private ArrayList<String> dishes = new ArrayList<>();
	
	public synchronized void add(String dish) {
		while(dishes.size() >= MAX_FOOD) {
			String name = Thread.currentThread().getName();
			System.out.println(name+" is waiting.");
			try {
				wait();
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		dishes.add(dish);
		notify();
		System.out.println("Dishes:" + dishes.toString());
	}
	
	public void remove(String dishName) {
		synchronized (this) {
			String name = Thread.currentThread().getName();
			
			while(dishes.size()==0) {
				System.out.println(name+" is waiting.");
				try {
					wait();
					Thread.sleep(500);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			while(true) {
				for(int i=0; i<dishes.size(); i++) {
					if(dishName.equals(dishes.get(i))) {
						dishes.remove(i);
						notify();
						return;
					}
				}
				
				try {
					System.out.println(name+" is waiting.");
					wait();
					Thread.sleep(500);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
	public int dishNum() {
		return dishNames.length;
	}
}
package ThreadEx3;

public class ThreadWaitEx3 {

	public static void main(String[] args) throws Exception {
		Table table = new Table();
		
		new Thread(new Cook(table), "COOK1").start();
		new Thread(new Customer(table, "donut"), "CUST1").start();
		new Thread(new Customer(table, "burger"), "CUST2").start();
		Thread.sleep(2000);
		System.exit(0);
	}
}

wait()과 notify()를 추가하였고 테이블에 음식이 없을 때뿐만 아니라, 원하는 음식이 없을 때도 customer가 기다리도록 바꾸었다. 실행이 잘되는 것 같다가도 테이블 객체의 waiting pool에 요리사 쓰레드와 손님 쓰레드가 같이 기다린 다는 것이다. 그래서 notify()가 호출되었을 때, 요리사 쓰레드와 손님 쓰레드 중에서 누가 통지를 받을지 알 수 없다.
만일 테이블의 음식이 줄어들어서 notify()가 호출되었다면, 요리사 쓰레드가 통지를 받아야 한다. 그러나 notify()는 그저 waiting pool에서 대기 중인 쓰레드 중에서 하나를 임의로 선택해서 통지할 뿐 요리사 쓰레드를 선택해서 통지할 수 없다. 운 좋게 요리사 쓰레드가 통지를 받으면 다행인데, 손님 쓰레드가 통지를 받으면 lock을 얻어도 여전히 자신이 원하는 음식이 없어서 다시 waiting pool에 들어가게 된다.

1. 
    intranet - 사내망
    internet - 전세계망

    이더넷
2.
    java.net
        랜카드 조정
3.
    IP(v4 : version 4)
    IPv6 - 랜카드 주소

    => 도메인(사는 거)
        www.naver.com

네트워킹(Networking)
네크워킹(networking)이란 두 대 이상의 컴퓨터를 케이블로 연겨랗여 네트워크(network)를 구성하는 것을 말한다. 네트워킹의 개념은 컴퓨터들을 서로 연결하여 데이터를 손쉽게 주고받거나 또는 자원프린터와 같은 주변기기를 함께 공유하고자 하는 노력에서 시작되었다.

클라이언트/서버(client/server)
'클라이언트/서버'는 컴퓨터간의 관계를 역활로 구분하는 개념이다. 서버(server)는 서비스를 제공하는 컴퓨터(service provider)이고, 클라이언트(client)는 서비스를 사용하는 컴퓨터(service user)가 된다.
일반적으로 서버는 다수의 클라이언트에게 서비스를 제공하기 때문에 고사양의 하드웨어를 갖춘 컴퓨터이지만, 하드웨어의 사양으로 서버와 클라이언트를 구분하는 것이 아니기 때문에 하드웨어의 사양에 관계없이 서비스를 제공하는 소프트웨어가 실행되는 컴퓨터를 서버라 한다.
서비스는 서버가 클라이언트로부터 요청받은 작업을 처리하여 그 결과를 제공하는 것을 뜻하며 서버가 제공하는 서비스의 종류에 따라 파일서버(file server), 메일서버(mail server), 어플리케이션 서버(application server) 등이 있다.
서버에 접속하는 클라이언트의 수에 따라 하나의 서버가 여러 가지 서비스를 제공하기도 하고 하나의 서비스를 여러 대의 서버로 제공하기도 한다.
서버가 서비스를 제공하기 위해서는 서버프로그램이 있어야 하고 클라이언트가 서비스를 제공받기 위해서는 서버프로그램과 연결할 수 있는 클라이언트 프로그램이 있어야 한다. 예를 들어 웹서버에 접속하여 정보를 얻기 위해서는 웹브라우저(클라이언트 프로그램)가 있어야하고, FTP서버에 접속해서 파일을 전송받기 위해서는 알FTP와 같은 FTP클라이언트 프로그램이 필요하다.
일반 PC의 경우 주로 서버에 접속하는 클라이언트 역할을 수행하지만, FTP Serv-U와 같은 FTP서버프로그램이나 Tomcat과 같은 웹서버프로그램을 설치하면 서버역할도 수행할 수 있다.
소리바다나 푸르나와 같은 프로그램은 클라이언트프로그램과 서버프로그램을 하나로 합친 것으로 이를 설치한 컴퓨터는 클라이언트인 동시에 서버가 되어 다른 컴퓨터로부터 파일을 가져오는 동시에 또 다른 컴퓨터에게 파일을 제공할 수 있다.
네트워크를 구성할 때 전용서버를 두는 것을 서버기반모델(server - based model)이라하고 별도의 전용서버없이 각 클라이언트가 서버역할을 동시에 수행하는 것을 P2P모델(peer to peer)이라고 한다.

서버기반 모델 장점
 - 안정적인 서비스의 제공
- 공유 데이터의 관리와 보안이 용이
- 서버구축비용과 관리비용 발생

P2P 모델 장점
- 서버구축 및 운용비용 절감
- 자원의 활용을 극대화
- 자원의 관리 어려움
- 보안 취약

IP주소(IP address)
IP주소는 컴퓨터(호스트, host)를 구별하는데 사용되는 고유한 값으로 인터넷에 연결된 모든 컴퓨터는 IP주소를 갖는다. IP주소는 4 byte(32 bit)의 정수로 구성되어 있으며, 4개의 정수가 마침표를 구분자로'a, b, c, d'와 같은 형식으로 표현된다. a, b, c, d는 부호없는 1 byte값, 즉 0~255사이의 정수이다.
InetAddress
자바에서는 IP 주소를 다루기 위한 클래스로 InetAddress를 제공한다.

NetworkEx01 - 도메인, IP주소

import java.net.InetAddress;
import java.net.UnknownHostException;

public class InetAddressEx01 {

	public static void main(String[] args) {

		try {
			InetAddress inetAddress1
			= InetAddress.getByName("www.naver.com");
			System.out.println(inetAddress1.getHostName());
			System.out.println(inetAddress1.getHostAddress());
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
	}
}

import java.net.InetAddress;
import java.net.UnknownHostException;

public class InetAddressEx01 {

	public static void main(String[] args) {

		try {
			// 도메인 1개 여러개의 IP를 가질 수 있음
			InetAddress[] inetAddress1
			= InetAddress.getAllByName("www.naver.com");
			for(InetAddress inetAddress : inetAddress1) {
				System.out.println(inetAddress.getHostAddress());
			}
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
	}
}

URL(Uniform Resource Locator)
URL은 인터넷에 존재하는 여러 서버들이 제공하는 자원에 접근할 수 있는 주소를 표현하기 위한 것으로 '프로토콜://호스트명:포트번호/경로명/파일명?쿼리스트링#참조'의 형태로 이루어져 있다.

http://www.codechobo.com:80/sample/hello.html?referer=codechobo#index1

프로토콜 :  자원에 접근하기 위해 서버와 통신하는데 사용되는 통신규약(http)
호스트명 : 자원을 제공하는 서버의 이름(www.codechobo.com)
포트번호 : 통신에 사용되는 서버의 포트번호(80)
경로명 : 접근하려는 자원이 저장된 서버상의 위치(/sample/)
파일명 : 접근하려는 자원의 이름(hello.html)
쿼리(query) : URL에서 '?'이후의 부분(referer=codechobo)
참조(anchor) : URL에서 '#'이후의 부분(index1)

NetworkEx2 - URL 객체생성

import java.net.URL;

public class NetworkEx2 {

	public static void main(String[] args) throws Exception {
		URL url = new URL("http://www.codechobo.com:80/sample/"
				+ "hello.html?referer=codechobo#index1:");
		
		System.out.println("url.getAuthority():"+ url.getAuthority());
		//System.out.println("url.getContent():"+ url.getContent());
		System.out.println("url.getDefaultPort():"+ url.getDefaultPort());
		System.out.println("url.getPort():"+ url.getPort());
		System.out.println("url.getFile():"+ url.getFile());
		System.out.println("url.getHost():"+ url.getHost());
		System.out.println("url.getPath():"+ url.getPath());
		System.out.println("url.getProtocol():"+ url.getProtocol());
		System.out.println("url.getQuery():"+ url.getQuery());
		System.out.println("url.getRef():"+ url.getRef());
		System.out.println("url.getUserInfo():"+ url.getUserInfo());
		System.out.println("url.toExternalForm():"+ url.toExternalForm());
		System.out.println("url.toURI():"+ url.toURI());
	}
}

URLEx01

import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

public class URLEx01 {

	public static void main(String[] args) {
		BufferedInputStream bis = null;
		
		try {
			URL url = new URL("https://m.naver.com");
			bis = new BufferedInputStream(url.openStream());
			
			int data = 0;
			while((data = bis.read()) != -1) {
				System.out.print((char)data);
			}
		} catch (MalformedURLException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if(bis != null) try {bis.close();} catch(IOException e) {};
		}
	}
}

다른 이름으로 저장이랑 같은 기능이다

URLEx02 - 한글도 읽어올 수 있게 BufferedReader로 변환

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;

public class URLEx02 {

	public static void main(String[] args) {
		BufferedReader br = null;
		
		try {
			URL url = new URL("https://m.naver.com");
			br = new BufferedReader(new InputStreamReader(url.openStream()));
					
			String data = "";
			while((data = br.readLine()) != null) {
				System.out.println(data);
			}
		} catch (MalformedURLException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if(br != null) try {br.close();} catch(IOException e) {};
		}
	}
}

 

반응형

댓글