join, synchronized

예숑·2024년 1월 10일

JAVA-Thread

목록 보기
2/4
post-thumbnail

⭐join

thread.join() : 해당 thread가 끝날 때까지 대기하라는 의미를 가진다.

public class ThreadJoinTestB {

	public static void main(String[] args) {
		//스레드 생성
			Thread t = new Thread() {
				public void run() {
					try {
						//2초간 멈춤
						Thread.sleep(2000);
						System.out.println("MyThread 종료");
						//3초간 멈춤
						Thread.sleep(3000);
					}catch (InterruptedException e) {	}
				}
			};
			//스레드 시작
			t.start();
			try {
          		 //join()메소드 실행
				t.join();
			}catch (InterruptedException e) {
				e.printStackTrace();
			}
			//main 메소드 종료 메세지
			System.out.println("main() 종료");
	}
}

→ 위 코드에서 join() 가 실행되면 t스레드가 종료될 때까지 main스레드가 기다린다.
여기서 기다린다는 의미는 main이 join을 만나면 NOT RUNNABLE 상태로 되는 것을 말한다.
→ join()은 수행한 스레드가 영향을 받는 것이지 t 가 영향을 받는 것이 아니다.
InterruptedException : RUN에서 NOT RUNNABLE로 갈 때 항상 같이 따라 온다.

class ThreadEx13_1 extends Thread{
	public void run() {
		for(int i = 0; i<300; i++) {
			System.out.print("-");
		}
	}//run()
}

class ThreadEx13_2 extends Thread {
	public void run() {
		for(int i = 0; i<300; i++) {
			System.out.print("l");
		}
	}//run()
}

public class ThreadEx13 {
	static long startTime = 0;
	public static void main(String[] args) {
		ThreadEx13_1 th1 = new ThreadEx13_1();
		ThreadEx13_2 th2 = new ThreadEx13_2();
		
		th1.start();
		th2.start();
		
		startTime = System.currentTimeMillis();
		try {
			th1.join();
		}catch (InterruptedException e) {}
		try {
			th2.join();
		}catch (InterruptedException e) {}
		
		// th1, th2 이 끝나고 수행하는데 언제 끝나는지 알기 위해 join을 쓴다.
		System.out.print("소요시간 : " + (System.currentTimeMillis() -ThreadEx13.startTime));
	}//main
}

→ 위 코드에서 try 안에 두 개의 스레드를 한번에 넣게 되면 하나가 예외가 걸리면 나머지 하나가 수행을 하지 않기 때문에 서로 영향을 주지 않기 위해서 예외 처리를 따로 해준다.

⭐synchronized(동기화)

synchronized : 특정 스레드가 들어가면 문이 잠기고 다른 스레드가 들어오지 못하게 해준다.

class UnsafeAccount {
	private int balance;
	public int getBalance() {
		return balance;
	}
	public void deposit(int val) {
		balance += val;
	}
	//잔액 500원일때
	public void withdraw(int val) {
		if(balance >= val) {
			try {
				Thread.sleep(500);
			}catch (InterruptedException e) {
			}
			balance -= val;
		}
		System.out.println("name : " + Thread.currentThread().getName() + ", balance = " + this.getBalance());
	}
}

public class UnsafeAccountSample {

	public static void main(String[] args) {
		//공유 자원 : 객체는 하나인데 여러 스레드가 사용하는 것
		final UnsafeAccount account = new UnsafeAccount();
		account.deposit(5000);
		Runnable withdrawRun = new Runnable() {
			@Override
			public void run() {
				for(int i=0; i<10; i++) {
					account.withdraw(500);
				}
				
			}
		};
		
		Thread t1 = new Thread(withdrawRun);
		Thread t2 = new Thread(withdrawRun);
		t1.start();
		t2.start();
	}
}

→ 위 코드는 synchronized를 사용하지 않았을 때 생기는 문제를 알아볼 수 있는 코드이다.
(동기화가 깨짐)
→ 위의 결과처럼 -500이라는 음수가 나오게 된다.
음수가 되는 경우의 수는 상당히 많은데 그 중에서 synchronized와 연관된 이유를 말하자면, 코드를 내려오면서 실행될 때 갑자기 t1과 t2의 순서가 교대가 될 수도 있다.
(이때, 교대가 된다는 게 한 줄 실행하고 난 다음이라고 생각하면 안된다. 위에서 보면 balance의 값을 알아보는 도중에 교대가 일어날 수 있기 때문이다.)

임계 영역(critcal section) : 공유자원에서 정보의 불일치가 일어날 수 있는 공간을 말한다.
(공유자원이란 1개의 객체를 여러 thread가 사용하는 것을 말한다.)
즉, 어떤 하나에 같이 접근할 때 생기는 문제를 말한다.
(정보의 불일치 = 동기화가 깨짐)

→ 위 UnsafeAccount 클래스에서 balance에 접근할 때, 임계 영역이 생기는 것이다.
예를 들어 화장실을 공유 자원이라고 할 때, 볼일을 보는데 다른 사람이 들어와서 같이 사용하는 문제가 생기는 것을 말한다.

이럴 때, 문제를 해결하기 위해 화장실 각 칸마다 문이 달려있고, 다른 사람이 들어오지 못하도록 한다.
이를 synchronized라고 생각하면 된다.

→ t1이 일을 다 하고 나가면 그때 t2가 들어올 수 있다.
MUTEX(Mutual Exclusion) - 상호배제 라고 한다.

💜synchronized 메소드

💙락(lock)

→ 자바의 모든 객체는 lock을 가지고 태어난다. 이때, 락은 객체마다 하나씩만 가지고 태어난다.
→ 동기화된 메소드는 락을 가지고 상호배제를 하게 된다.
락의 주체는 this이며, 락을 가졌다라는 말은 모니터를 취득했다라는 말과 동일한 말이다.

class SyncAccount {
	private int balance;
	public synchronized int getBalance() {
		return balance;
	}
	public synchronized void deposit(int val) {
		balance += val;
	}
	public synchronized void  withdraw(int val) {
		if(balance >= val) {
			try {
				Thread.sleep(500);
			}catch (InterruptedException e) {
			}
			balance -= val;
		}
		System.out.println("name : " + Thread.currentThread().getName() + ", balance = " + this.getBalance());
	}
}

public class SyncAccountSample {

	public static void main(String[] args) {
		final SyncAccount account = new SyncAccount();
		account.deposit(5000);
		Runnable withdrawRun = new Runnable() {
			@Override
			public void run() {
				for(int i=0; i<10; i++) {
					account.withdraw(500);
				}
			}
		};
		Thread t1 = new Thread(withdrawRun);
		Thread t2 = new Thread(withdrawRun);
		t1.start();
		t2.start();
	}
}

→ 위의 코드를 보면 첫번째 스레드가 account의 락을 가지게 되면, synchronized 메소드에 들어갈 수 있다.
synchronized 메소드에는 lock을 가진 스레드만 들어올 수 있으므로 두번째 스레드는 첫번째 스레드가 락을 돌려줄 때까지 기다리다가 돌려주면 그때 취득 후 들어갈 수 있다.
이런 방식으로 동기화가 깨지는 현상을 막을 수 있다.
독립성을 보장해준다

→ 위 그림을 보면 더 이해하기 쉬울 것이다.
하다가 걸린다고 해도 다음을 수행하는 것이 아니라 기다리다가 수행을 한다.

💜synchronized block

1) 블록을 설정하면 범위(구간) 설정이 가능하기 때문에, 병목현상을 최소화할 수 있다.
2) lock 검사 대상을 설정할 수 있다.(서로 다른 멤버변수에 대한 연산)

예를 들어서
methodA1( ), methodA2( ) 가 있고
methodB1( ), methodB2( ) 가 있을 때
각각에 synchronized 를 붙이면 methodA1( ) 가 일을 하면 나머지는 일을 하지 못하고 기다려야 한다.
즉, 시간이 오래 걸리므로 일의 효율이 떨어진다.

이를 해결하기 위해
methodA1( ), methodA2( ) 에 synchronized block 을 걸고,
methodB1( ), methodB2( ) 에 synchronized block 을 걸면

전체적으로 A, B 가 별도로 관리가 가능하다.
즉, methodA1( ) 이 수행되면 methodA2( )는 수행되지 않지만
methodB1( ), methodB2( ) 에는 영향을 주지 않는다.

class Some {
	private Object key = new Object();
	public void methodA() {
		//lock 검사대상 객체 지정
		Thread temp = Thread.currentThread();
		synchronized (this) {
			for (int i = 0; i < 100; i++) {
				try {
					System.out.println(temp.getName() +
							" -> doing methodA"
							);
					Thread.sleep(10);
				}catch (Exception e) {	}
			}
		}
	}
	public void methodB() {
		Thread temp = Thread.currentThread();
		synchronized (key) {
			for (int i = 0; i < 100; i++) {
				try {
					System.out.println(temp.getName() +
							" -> doing methodB"
							);
					Thread.sleep(10);
				}catch (Exception e) {	}
			}
		}
	}

}
public class ThreadTest{
	public static void main(String[] args) {
		final Some s = new Some();
		
		Thread t1 = new Thread() {
			@Override
			public void run() {
				s.methodA();
			}
		};
		Thread t2 = new Thread() {
			@Override
			public void run() {
				s.methodB();
			}
		};
		t1.start();
		t2.start();
	}
}

💙synchronized 메소드 와 synchronized 블록 비교하기

class Human1 {
	private String name;
	private int age;

	
	//같은 lock을 가지고 있어서 t1이 블로킹 되어 있어도 t2가 되지 않음
	//둘에게 영향을 준다.(이름을 바꾸지 않으면 나이도 안된다. 나이를 바꾸지 않는 이상 이름도 안된다.)
	public synchronized void setName(String name) {
		try {
			this.name = name;
			System.out.println("name changed");
			Thread.sleep(3000);
		}catch (Exception e) {}
	}
	public synchronized void setAge(int age) {
		try {
			this.age = age;
			System.out.println("age changed");
			Thread.sleep(3000);
		}catch (Exception e) {}
	}
}
public class SynchronizedMethodCase {

	public static void main(String[] args) {
		final Human1 h = new Human1();
		Thread t1 = new Thread() {
			@Override
			public void run() {
				h.setName("춘식");
			}
		};
		t1.setName("first");

		Thread t2 = new Thread() {
			@Override
			public void run() {
				h.setAge(10);
			}
		};
		t2.setName("second");
		
		t1.start();
		t2.start();
	}
}
class KeyA{
	
}
class KeyB{
	
}
class Human {
	private String name;
	private int age;
	private KeyA keyA = new KeyA();
	private KeyB keyB = new KeyB();
	
	//이름과 나이가 서로 영향을 받지 않음
	public void setName(String name) {
		synchronized (keyA) {
			try {
				this.name = name;
				System.out.println("name changed");
				Thread.sleep(3000);
			}catch (Exception e) {}
		}
	}
	public void setAge(int age) {
		synchronized (keyB) {
			try {
				this.age = age;
				System.out.println("age changed");
				Thread.sleep(3000);
			}catch (Exception e) {}
		}
	}
}
public class SynchronizedBlockCase {

	public static void main(String[] args) {
		final Human h = new Human();
		Thread t1 = new Thread() {
			@Override
			public void run() {
				h.setName("춘식");
			}
		};
		t1.setName("first");

		Thread t2 = new Thread() {
			@Override
			public void run() {
				h.setAge(10);
			}
		};
		t2.setName("second");
		
		t1.start();
		t2.start();
	}
}

0개의 댓글