김미썸코딩

11/26 - Java (13) : 멀티 스레드( 상태제어, sleep(), join(), yield(), 데몬스레드, 동기화, 스레드 그룹 ) 본문

빅데이터 플랫폼 구축을 위한 자바 개발자 양성과정

11/26 - Java (13) : 멀티 스레드( 상태제어, sleep(), join(), yield(), 데몬스레드, 동기화, 스레드 그룹 )

김미썸 2020. 11. 28. 15:41
728x90

데이터 저장 

          임시

                     변수 / 상수

                      -> Collection

          영구

                      - 로컬

                      - 원격(네트워크)

 

Java 기본

          file

데이터베이스

          mariaDB(내일 설치, 월요일부터 수업)
          windows update

Java 나머지

미니프로젝트

web - html/css/js

          * 내일 시험 - 간단한 프로그램 작성한 후에 - 스크린 캡처

 


 

 

 

 

 

 

 

멀티 스레드


▷p576

운영체제에서는 실행 중인 하나의 애플리케이션을 프로세스라고 한다. 하나의 애플리케이션은 다중 프로세스를 만들기도 한다.

멀티 태스킹은 두가지 이상의 작업을 동시에 처리하는 것을 말하는데, 운영체제는 멀티 태스킹을 할 수 있도록 CPU 및 메모리 자원을 프로세스마다 적절히 할당해주고, 병렬로 실행시킨다. 멀티 태스킹은 꼭 멀티 프로세스를 뜻하지는 않는다. 한 프로세스 내에서 멀티 태스킹을 할 수 있도록 만들어진 애플리케이션들도 있다.

하나의 프로세스가 두가지 이상의 작업을 처리하는 것의 비밀은 멀티스레드이다. 멀티 프로세스가 애플리케이션 단위의 멀티 태스킹이라면 멀티 스레드는 애플리케이션 내부에서의 멀티 태스킹이라고 볼 수 있다.

 

멀티스레드를 잘하면 병렬처리프로그램의 핵심이 된다!

 

여러개의 수행흐름을 만들어서 작업해보자! -> 멀티 스레드

 

멀티프로세스들은 운영체제에서 할당받은 자신의 메모리를 가지고 실행하기 때문에 서로 독립적이다. 따라서 하나의 프로세스에서 오류가 발생해도 다른 프로세스에게 영향을 미치지 않는다. (ex. 워드, 엑셀)

하지만 멀티스레드는 하나의 프로세스 내부에 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스 자체가 종료될 수 있어(반드시 종료X) 다른 스레드에게 영향을 미치게 된다. (ex. 메신저내의 파일전송, 채팅 스레드)

 

JVM - 하나의 프로세스

메인 클래스 - 메인 스레드

클래스 - 작업 스레드

 

 

 

순차 스레드

아래 사진은 순차 스레드의 실행결과를 보여준다.

go.run()이 실행되어 끝난 후에아 come.run()이 실행된다. 

 

 

 

 

 

1. Thread 사용

1-1. Thread 구현 (상속)

패키지를 하나더 만들어보자~거기서 원래 패키지의 클래스와 같은 이름으로 클래스를 만드는데,

Go, Come 만들 때 Thread(java.lang.thread)를 상속 하도록 해서 만든다.

 

package 별로 따로 돌아감

run()같이 함수 호출로 직접 실행시키면 안되고, start() 를 사용해야 한다.

 

왜 start()를 해야하나?  

▷p579

함수 직접 호출하면 그 전것이 완전히 끝난 후에야 다음게 실행된다. 실행 대기 상태가 없다.

start()를 하면 cpu한테 처리해야 할걸 위임해서 cpu가 스케줄링을 통해 알아서 실행, 실행대기를 번갈아 시키며 일을 조금씩 시킨다.

package pack2;

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

public class Go extends Thread {
	@Override
	public void run() {
		// 스레드에 작업할 (병렬처리할) 내용
		for(int i=1; i<=50; i++) {
			System.out.println("go : " + i);
		}
	}
}
package pack2;

public class ThreadEx01 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Go g = new Go();
		Come c = new Come();
		
		// 직접적으로 실행시키면 안되고 start해줘야한다.
		//g.run();
		//c.run();
		
		// 왜 start를 해야하냐?
		// cpu가 알아서 실행 스케줄을 잡고 일을 시킨다.
		// 실행, 실행대기를 시키면서 일을 조금씩 시킨다. 
		System.out.println("시작");
		g.start();
		c.start();
		System.out.println("종료");
	}

}

 

 

코드 병렬처리

▷p578

System.out.println("시작");
g.start();
c.start();
System.out.println("종료");

위 코드의 결과는 아래와 같다. 

병렬처리는 내가 일을 시키기만 할 뿐 나머지는 cpu 가 알아서 하는것이다.

항상 시작, 종료가 먼저 출력되고, 실행할때마다 go와 come이 다르게 출력된다. 

 

 

 

1-2. Runnable 인터페이스 구현

패키지를 하나더 만든다.

Go, Come만들 때 상속이 아닌 implemets 에 add 눌러서 Runnable(java.lang.runnable)을 추가한다.

package pack3;

public class Come implements Runnable {

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

public class Go implements Runnable {

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

public class ThreadEx01 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		Go g = new Go();
		Come c = new Come();
		
		Thread t1 = new Thread(g);
		Thread t2 = new Thread(c);
		
		System.out.println("시작");
		t1.start();
		t2.start();
		System.out.println("끝");
	}

}

 

 

1-3. 익명 클래스

기본적으로 인터페이스를 가진다. 인터페이스를 가지고 익명클래스를 만든다.

스레드를 생성할 때 인자값으로 익명클래스를 준다.

여기서 인터페이스인 Runnable()을 익명 클래스로 만든다.

package pack4;

public class ThreadEx01 {

   public static void main(String[] args) {
      
      //익명클래스 <- 인테페이스 활용해서 만들기 
      //Thread 안에 Runnable 
      Thread t1 = new Thread(new Runnable() {   
         @Override
         public void run() {
            for(int i=1 ; i<=50 ; i++) {
               System.out.println( "go: " + i );
            }      
         }
      });
      
      Thread t2 = new Thread(new Runnable() {      
         @Override
         public void run() {
            for(int i=1 ; i<=50 ; i++) {
               System.out.println( "come: " + i );
            }         
         }
      });
      
      t1.start();
      t2.start();

   }

}

 

문제) 단수 입력받아서 구구단 출력하는 스레드

Gugudan클래스를 익명클래스로 하여 스레드 생성 인자값으로 Gugudan 익명객체를 생성하면서

Gugudan클래스를 호출하여 구현한다.

package pack5;

public class Gugudan implements Runnable {

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

	@Override
	public void run() {
		
		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));;
		}
	}

}

 

package pack5;

public class ThreadEx01 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		Thread t1 = new Thread(new Gugudan(3));
		Thread t2 = new Thread(new Gugudan(6));
		
		// 난 스레드 이름을 주고싶어!
		t1.setName("3단 구구단");
		t2.setName("6단 구구단");
		
		t1.start();	// Thread - 0
		t2.start();	// Thread - 1
	}

}

 

 

2. 스레드 이름

▷p586

스레드는 이름값을 가져서 스레드를 구분할 수 있다. 이게 어디의 스레든지 구분하는데 사용할 수 있다.

getName(), setName() 메서드 사용

 

setName()하고 안하고 차이 예제

package pack5;

public class ThreadA extends Thread {
	// 스레드 이름 정함
	
	// 기본 생성자를 정의하여 
	// 스레드 이름 정하는 메서드 정의해줌
	public ThreadA() {
		setName("ThreadA");
	}
	
	public void run() {
		for(int i = 0; i<2; i++) {
			System.out.println(getName() + "가 출력한 내용");
		}
	}
}

 

package pack5;

public class ThreadB extends Thread {
	
	// 스레드 이름 정하지 않음
	public void run() {
		for(int i = 0; i<2; i++) {
			System.out.println(getName() + "가 출력한 내용");
		}
	}
}

 

package pack5;

public class ThreadNameExample {

	public static void main(String[] args) {
		Thread mainThread = Thread.currentThread();
		System.out.println("프로그램 시작 스레드 이름: " + mainThread.getName());
		
		ThreadA threadA = new ThreadA();
		System.out.println("작업 스레드 이름: " + threadA.getName());
		threadA.start();
		
		ThreadB threadB = new ThreadB();
		System.out.println("작업 스레드 이름: " + threadB.getName());
		threadB.start();
		
	}

}

threadA는 setName("ThreadA")으로 이름이 있으므로 getName()으로 ThreadA가 리턴된다.

threadB는 setName()하지 않았기 때문에 getName() 하면 Thread-1이 리턴된다.

 

 

 

3. 스레드 우선순위

▷p588

스레드 작업은 병렬적으로 실행되는 것처럼 보이지만, 사실은 번갈아가며 실행하는 동시성 작업이다. 번갈아 실행하는 것이 워낙 빠르다보니 병렬성으로 보일 뿐이다.1~10 까지 줄수 있고 숫자가 높을수록 우선 순위가 높다. 우선순위를 설정하지 않으면 5로 자동 설정된다. 우선순위를 정해준다고 해도 우선순위를 높게 정해준다고 해서 그게 빨리끝나는 것은 아니다!!!

  • setPriority() : 값을 직접 줄수도 있고, 상수값(MAX_PRIORITY, NORMAL_PRIORITY, MIN_PRIORITY)으로 줄수도 있다.
package pack5;

public class CalcThread extends Thread {
	public CalcThread(String name) {
		setName(name);
	}
	
	public void run() {
		for(int i = 0; i<2000000000; i++) {
		}
		System.out.println(getName());
	}
}

 

package pack5;

public class PriorityExample {

	public static void main(String[] args) {
		for(int i = 1; i<=10; i++) {
			Thread thread = new CalcThread("thread" + i);
			if( i != 10) {
				thread.setPriority(Thread.MIN_PRIORITY);
			}else {
				thread.setPriority(Thread.MAX_PRIORITY);
			}
			thread.start();
		}
	}

}

실행할 때마다 출력 결과가 다르게 나온다.

 

 

 

 

4. 스레드 상태 제어

▷p600

일시정지, 실행대기, 실행, 종료   이게 전부 상태인데 이걸 제어하는 것이다.

상태변화 위한 여러가지 메소드가 있다.

 

가장 많이 쓰는게 sleep()이다. 밀리세컨드동안 잠깐  멈추는 거다.

이걸 쓰려면 InterruptedException에 대한 예외처리를 해줘야한다.

 

 

4-1. 주어진 시간 동안 일시정지 sleep()

문제) 1초마다 시간이 찍히는 거 만들어보기

 

(1) 내 코드

import java.text.SimpleDateFormat;
import java.util.Calendar;

public class ThreadEx01 {

	public static void main(String[] args) {
		
		SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		
		System.out.println("시작");
		
		while(true) {
			
			try {
				
				Calendar c = Calendar.getInstance();
				
				String time = format.format(c.getTime());
				
				System.out.println(time);
				
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
		//System.out.println("끝");
	}
}

 

 

(2) 강사님 코드

import java.text.SimpleDateFormat;
import java.util.Calendar;

public class ThreadEx01 {

	public static void main(String[] args) {
		
		System.out.println("시작");
		
		while(true) {
			
			try {
				
				Calendar c = Calendar.getInstance();
				int hour = c.get(Calendar.HOUR);
				int minute = c.get(Calendar.MINUTE);
				int second = c.get(Calendar.SECOND);
				
				System.out.printf("%s:%s:%s%n", hour, minute, second);
				
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
		//System.out.println("끝");
	}
}

 

 

4-2. 다른 스레드에게 실행 양보 yield()

 

4-3. 다른  스레드의 종료를 기다림 join()

public class SumThread extends Thread {
	private long sum;
	
	public long getSum() {
		return sum;
	}
	
	public void setSum(long sum) {
		this.sum = sum;
	}
	
	public void run() {
		for(int i = 0; i<=100; i++) {
			sum+=i;
		}
	}
}


public class JoinExample {

	public static void main(String[] args) {
		
		SumThread sumThread = new SumThread();
		sumThread.start();
		
		try {
			// sumThread 가 종료할 때까지 메인 스레드를 일시정지 시킴
			// sumThread에서 getSum()을 완전히 수행후 
			// 그 결과값을 불러오려고 
			sumThread.join();
			
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		
		System.out.println("1~100 합: " + sumThread.getSum());
	}

}

 

 

위 코드에서 JoinExample 클래스의 

sumThread.join();

은 SumThread 클래스에서 sumThread.start() 로 인해 아래 코드인

public void run() {
    for(int i = 0; i<=100; i++) {
        sum+=i;
    }
}

run() 메서드가 호출된 이후 위의 for문이 모두 실행된 후에 그결과값을 마지막

System.out.println("1~100 합: " + sumThread.getSum());

위 코드로 불러올 수 있도록 한다.

 

 

4-4. 스레드 간 협업 wait(), notify(), notifyAll()

▷p608

 

 

 

5. 데몬 스레드

▷p618

주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드이다. 주 스레드가 종료되면 데몬 스레드는 강제적으로 자동 종료되는데, 그 이유는 주 스레드의 보조 역할을 수행하므로 주 스레드가 종료되면 데몬 스레드의 종료 의미가 없어지기 때문이다.

 

멀티 스레드의 경우 메인 스레드가 작업 스레드보다 먼저 종료되어도 프로세스는 종료되지 않는다. 

  • setDaemon( boolean ) :  인자값이 true면 데몬스레드.
public class AutoSaveThread extends Thread {
	public void save() {
		System.out.println("작업 내용을 저장함.");
	}
	
	@Override
	public void run() {
		while(true) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
				break;
			}
			save();		// 1초 후에 저장 반복
		}
	}
}

 

public class DaemonExample {

	public static void main(String[] args) {
		AutoSaveThread autoSaveThread = new AutoSaveThread();
		autoSaveThread.setDaemon(true);
		autoSaveThread.start();
		
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		System.out.println("메인 스레드 종료");
	}

}

1초마다 저장을 반복하는 autoSaveThread는 주 스레드인 DaemonExample 클래스가 

Thread.sleep(3000);

위 코드로 인해 3초 뒤에 종료되면 같이 종료된다.

 

 

 

6. 스레드 상태

▷p598

  • getState() : 스레드 상태에 따라서 Thread, State 열거 상수를 리턴한다.
public class StatePrintThread extends Thread {
	private Thread targetThread;
	
	public StatePrintThread(Thread targetThread) {
		this.targetThread = targetThread;
	}
	
	public void run() {
		while(true) {
			Thread.State state = targetThread.getState();
			System.out.println("타겟 스레드 상태: "+ state);
			
			if(state == Thread.State.NEW) {
				targetThread.start();
			}
			
			if(state == Thread.State.TERMINATED) {
				break;
			}
			
			try {
				// 0.5초간 일시 정지
				Thread.sleep(500);
			}catch(Exception e) {}
		}
	}
}

 

public class TargetThread extends Thread {
	public void run() {
		for(long i = 0; i<1000000000; i++) {}
		
		try {
			Thread.sleep(1500);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		for(long i = 0; i<1000000000; i++) {}
	}
}

 

public class ThreadStateExample {

	public static void main(String[] args) {
		StatePrintThread statePrintThread = new StatePrintThread(new TargetThread());
		statePrintThread.start();
	}

}

 

7. 동기화 메소드와 동기화 블록 (가장 중요)

▷p591

공유 객체를 사용하게 되면 스레드A에 의해 변경된 객체의 값을 스레드B가 가져가서 쓰게될 수도 있다.

 

동기화가 필요한 예

package bank;

public class Account {
	private int balance = 1000;
	
	public int getBalance() {
		return balance;
	}
	
	// 인출 메서드
	public synchronized void withdraw(int money) {
		if(balance >= money) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			// 인출
			balance -= money;
		}else {
			System.out.println("잔고가 없습니다.");
		}
	}
}

 

package bank;

public class Client implements Runnable {

	private Account account;
	
	public Client(Account account) {
		super();
		this.account = account;
	}

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

}

 

package bank;

public class MainEx {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		Account account = new Account();
		
		Client client1 = new Client(account);
		Client client2 = new Client(account);
		
		Thread t1 = new Thread(client1);
		Thread t2 = new Thread(client2);
		
		t1.start();
		t2.start();
	}

}

통장잔고가 없으면 다음에 잔고가 출력되면 안되는데 출력이 된다.

 

 

 

▷p593

스레드가 사용중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금(lock)을 걸어서 다른 스레드가 사용할 수 없도록 해야한다. 멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계 영역이라고 한다. 자바는 임계영역을 지정하기 위해 동기화 메소드동기화 블록을 제공한다. 

그렇다면 동기화 메소드동기화 블록을 어떻게 사용하나?

synchronized라는 키워드를 사용한다. 

 

위의 Account.withraw(int money)에 synchronized를 붙여주면 잔고가 음수값이 나오지 않는다.

 

동기화 사용 예

package cal;

public class Calculator {
	private int memory;
	
	public int getMemory() {
		return memory;
	}
	
	public synchronized void setMemory(int memory) {
		this.memory = memory;
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName() + ": " + this.memory);
		
	}
}

 

package cal;

public class User1 extends Thread {
	private Calculator calculator;
	
	public void setCalculator(Calculator calculator) {
		// extends Thread 했으니까 현재 스레드의 이름을 User1로 설정
		this.setName("User1");
		this.calculator = calculator;
	}
	
	public void run() {
		calculator.setMemory(100);
	}
}

 

package cal;

public class User2 extends Thread {
	private Calculator calculator;
	
	public void setCalculator(Calculator calculator) {
		this.setName("User2");
		this.calculator = calculator;
	}
	
	public void run() {
		calculator.setMemory(50);
	}
}

 

package cal;

public class MainThreadExample {

	public static void main(String[] args) {
		Calculator calculator = new Calculator();
		
		User1 user1 = new User1();
		user1.setCalculator(calculator);
		user1.start();
		
		User2 user2 = new User2();
		user2.setCalculator(calculator);
		user2.start();
	}

}

여기서 User1을 start한 후 User2를 start 하는데

둘다 Calculator 객체를 공유하여 

Calculator클래스안의 setMemory(int memory)메서드 실행시

 

synchronized 되어 있지 않으면

User1이 먼저 start되어 setMemory메서드를 안의 마지막 코드인 system.out.println으로 멤버변수를 출력하러 가는 도중에 

User2가 setMemory메서드에 접근해서 멤버변수값을 자기 걸로 바꿔버린다.

따라서 User1과 User2 모두 User2가 바꿔버린 멤버변수로 결과값이 나오게 된다.

 

synchronized가 되어 있으면 User1, User2의 각각의 결과 확인이 가능하다.

 

동기화를 wait()와 notify()를 통해서도 할수 있다. 이게 더 고급이다.

▷p611

 

 

 

 

8. 스레드 그룹 

▷p620

스레드 그룹은 관련된 스레드를 묶어서 관리할 목적으로 이용된다. JVM이 실행되면 system 스레드 그룹을 만들고, JVM 운영에 필요한 스레드들을 생성해서 system 스레드 그룹에 포함시킨다.

 

public class AutoSaveThread extends Thread {
	public void save() {
		System.out.println("작업 내용을 저장함.");
	}
	
	@Override
	public void run() {
		while(true) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
				break;
			}
			save();		// 1초 후에 저장 반복
		}
	}
}
import java.util.Map;
import java.util.Set;

public class ThreadInfoExample {

	public static void main(String[] args) {
		AutoSaveThread autoSaveThread = new AutoSaveThread();
		autoSaveThread.setName("AutoSaveThread");
		autoSaveThread.setDaemon(true);
		autoSaveThread.start();
		
		Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
		Set<Thread> threads = map.keySet();
		for(Thread thread: threads) {
			System.out.println("Name: " + thread.getName() +
								((thread.isDaemon())?"(데몬)":"(주)"));
			System.out.println();
			
		}
	}

}

 

Map과 Set을 사용했다. 키 뭉치를 가져오기 위해서 Set을 사용했다.

(Map 에서 키 뭉치를 가져오려면 Set, Iterator 둘 중하나를 사용해야함)

getAllStackTraces() 메서드는 Map 타입의 객체를 리턴하는데,

키는 스레드 객체이고 값은 스레드의 상태 기록들을 갖고 있는 StackElement[] 배열이다. 

 

 

▷p622

8-1. 스레드 그룹 생성

명시적으로 스레드 그룹을 만들고 싶다면 다음 생성자 중 하나를 이용해서 ThreadGroup 객체를 만들면 된다.

ThreadGroup 이름만 주거나, 부모 ThreadGroup과 이름을 매개값으로 줄수 있다.

ThreadGroup tg = new ThreadGroup(String name);
TrheadGroup tg = new ThreadGroup(ThreadGroup parent, String name);

 

8-2. 스레드 그룹 일괄 interrupt()

public class WorkThread extends Thread {
	public WorkThread(ThreadGroup threadGroup, String threadName) {
		super(threadGroup, threadName);
	}
	
	@Override
	public void run() {
		while(true) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				// e.printStackTrace();
				
				System.out.println(getName() + "interrupted");
				break;
			}
			
		}
		
		System.out.println(getName() + " 종료");
	}
}
public class ThreadEx04 {

	public static void main(String[] args) {
		
		ThreadGroup threadGroup = new ThreadGroup("myGroup");
		
		WorkThread workThreadA = new WorkThread(threadGroup, "workThreadA");
		WorkThread workThreadB = new WorkThread(threadGroup, "workThreadB");
		
		workThreadA.start();
		workThreadB.start();
		
		System.out.println("리스트 보기");
		ThreadGroup group = Thread.currentThread().getThreadGroup();
		group.list();
		System.out.println();
		
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		System.out.println("[ myGroup 스레드 그룹의 interrupt() 메소드 호출 ]" );
		group.interrupt();
	}

}

 

 

 

 

728x90
Comments