Thread 클래스를 상속받을 경우, 다른 클래스를 상속받을 수 X
"쓰레드를 구현하는 것"
= 단지 쓰레드를 통해 run() 메서드의 { } 몸통안에 작업할 내용을 채우는 것일 뿐이다.
EX 클래스
class EX {
public static void main(String args[]) {
ThreadEx t = new ThreadEx(); // 쓰레드 생성 : Thread의 자식 클래스의 인스턴스 생성
t.start(); // 쓰레드 실행
}
}
ThreadEx 클래스
class ThreadEx extends Thread {
public void run() { // Thread 클래스의 run() 메소드를 오버라이딩
for (int i = 0; i < 5; i++) {
System.out.println(getName()); // 부모인 Thread의 getName() 메서드 호출
}
}
}
EX 클래스
class EX {
public static void main(String args[]) {
Runnable r = new ThreadEx(); // Runnable을 구현한 클래스의 인스턴스 생성
Thread t = new Thread(r); // 생성자 Thread : 위에서 생성한 인스턴스를 Thread 클래스의 생성자의 매개변수로 둔다
/* 위 두 줄을 하나로 줄일 수 있다.
Thread t = new Thread(new ThreadEx());
*/
t.start(); // 쓰레드 실행
}
}
ThreadEx 클래스
class ThreadEx implements Runnable {
public void run() { // Runnable 인터페이스의 run() 메소드를 구현
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
start() 를 호출하면, 쓰레드가 실행된다.
실행대기 중 쓰레드가 없을 경우
실행대기 중 쓰레드가 있을 경우
// 잘못된 방법
ThreadEx t = new ThreadEx();
t.start();
...
t.start(); // IllegalThreadStateException 발생
// 올바른 방법
ThreadEx t = new ThreadEx();
t.start();
t = new ThreadEx(); // 쓰레드를 다시 생성
t.start(); // start() 다시 호출
한 번 실행되고 종료된 쓰레드는 다시 실행 X
쓰레드의 작업을 한번더 해야 할 경우
하나의 쓰레드에 대해 start() 를 두 번 호출하면, 예외 발생
우선순위(작업 중요도)의 값에 따라, 쓰레드가 얻는 실행시간이 달라진다.
예시1 : 메신저에서 파일 전송할 경우
예시 2
void setPriority(int newPriority)
: 쓰레드의 우선순위를 지정한 값으로 변경
int getPriority()
: 쓰레드의 우선순위를 반환
public static fianl int MAX_PRIORITY = 10
: 최대 우선순위
public static fianl int NORM_PRIORITY = 5
: 보통 우선순위
public static fianl int MIN_PRIORITY = 1
: 최소 우선순위
쓰레드의 우선순위의 범위 : 1 ~ 10
숫자가 높을수록, 우선순위 높다.
(10 大 <--- 1 小)
ThreadEx1 클래스
class ThreadEx1 extends Thread {
publid void run() {
...
}
}
ThreadEx2 클래스
class ThreadEx2 extends Thread {
publid void run() {
...
}
}
메인 클래스
class Ex {
public static void main(String args[]) {
ThreadEx1 th1 = new ThreadEx1();
ThreadEx2 th2 = new ThreadEx2();
th2.setPriority(7); // 쓰레드 우선순위 변경
System.out.println(th1.getPriority()); // th1 쓰레드의 우선순위 반환
System.out.println(th2.getPriority()); // th2 쓰레드의 우선순위 반환
th1.start(); // th1 쓰레드 실행
th2.start(); // th2 쓰레드 실행
}
}
주의!
main메서드를 수행하는 쓰레드의 우선순위가 5
"그룹" = "폴더" 라고 생각하면 된다
보안상의 이유로 도입된 개념
쓰레드 그룹을 지정하지 않고 생성한 경우
에시
(이 외에도 다른 메서드 있음)
ThreadGroup getThreadGroup()
: 자기 쓰레드가 쓰레드 그룹을 반환
void uncaughtException(Thread t, Throwable e)
: 처리되지 않은 예외에 의해 쓰레드 그룹의 쓰레드가 실행 종료됐을 때, JVM에 의해 이 메서드가 자동 호출됨
동영상을 보다가 일시 정지 or 종료 할 수 있다.
이렇게 실행 중인 스레드의 상태를 변경하는 것을 '스레드 상태 제어'라고 한다.
sleep(long millis)
// 3초 동안 일시 정지 상태로 보내고, 3초 후 다시 실행 준비 상태로 돌아온다
public class SleepExample {
public static void main(String[] args) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i=0; i<10; i++) {
toolkit.beep();
try {
Thread.sleep(3000); // 3초 동안 메인 스레드를 일시 정지 상태로 만든다
} catch(Interruptedexception e) {} // 예외 처리
}
}
}
interrupt()
// 예시 1 (위의 코드와 동일)
public class SleepExample {
public static void main(String[] args) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i=0; i<10; i++) {
toolkit.beep();
try {
Thread.sleep(3000); // 3초 동안 메인 스레드를 일시 정지 상태로 만든다
} catch(Interruptedexception e) {} // 예외 처리
thread.interrupt(); // 스레드를 종료하기 위해, interruptedException 을 발생시킴
}
}
}
public class PrintThread2 extends Thread {
public void run() {
while(true) {
System.out.println("실행 중");
if (Thread.interrupted()) { // interrupted() 를 이용해서, PrintThread 의 interrupt()가 호출되었는지 확인
break;
}
}
// while문을 빠져나옴
System.out.println("자원 정리");
System.out.println("실행 종료");
}
}
stop()
public class PrintThread1 extends Thread {
private boolean stop;
public void setStop(boolean stop) {
this.stop = stop;
}
public void run() {
while (!stop) {
System.out.println("실행 중"); // while문의 조건식(!stop)이 true 일 경우
}
System.out.println("자원 정리"); // while문의 조건식(!stop)이 false 일 경우, while문을 빠져나오게 된다
System.out.println("실행 종료");
}
}
한 스레드에서 실행되는 코드가 동일한 객체를 사용할 수 있도록 해주기 때문에
관련된 코드에서 파라미터를 사용하지 않고, 객체를 각자가 가져다 쓸 때 사용된다.
Thread pool 환경에서 ThreadLocal을 사용하는 경우, 변수에 보관된 데이터 사용이 끝나면 반드시 해당 데이터를 삭제해야한다.
그렇지 않으면, 재사용 되는 스레드가 올바르지 않은 데이터를 참조할 가능성
(= 메모리 누수)
일반 쓰레드의 작업을 돕는 보조 역할을 수행하는 쓰레드
(일반 쓰레드 : 데몬 쓰레드가 아닌 쓰레드)
일반 쓰레드가 종료되면, 데몬 쓰레드도 강제 종료된다.
(데몬 쓰레드가 일반 쓰레드를 보조하는 역할이기 때문)
일반 쓰레드와 데몬 쓰레드 공통점
일반 쓰레드와 데몬 쓰레드 차이점
예시
boolean isDaemon()
: 해당 쓰레드가 데몬 쓰레드인지 확인 (true/false)
void setDaemon(boolean on)
: 해당 쓰레드를 데몬 쓰레드/사용자 쓰레드로 변경
: 매개변수 on의 값을 true로 지정할 경우, 데몬 쓰레드로 변경됨
class Ex implements Runnable {
static boolean autoSave = false;
public static void main(String args[]) {
Thread t = new Thread(new Ex());
t.setDaemon(true); // 데몬 쓰레드로 지정 -> 이 부분이 없으면, 절대 종료되지 않음
t.start(); // 쓰레드 실행
for (...) {
...
}
...
}
}
파일 다운로드 받는 중 + 메신저로 채팅 X
메인 스레드가 종료될 시, 싱글 스레드의 프로세스도 같이 종료된다.
멀티 스레드 어플리케이션에서 실행 중인 스레드가 하나라도 있을 경우, 싱글 스레드의 프로세스는 종료되지 않는다.
(멀티 쓰레드에서와 달리) main 쓰레드가 종료되면 프로세스도 종료된다.
메신져 프로세스 같은 경우, 채팅 기능을 제공하면서 동시에 파일 업로드 기능을 수행할 수 있다.
이렇게 한 프로세스에서 멀티 태스킹(두 가지 이상의 작업을 동시에 처리하는 것)이 가능한 이유는 멀티 스레드 덕분이다.
즉, 멀티태스킹을 하는 방식 中 한 코어에서 여러 스레드를 이용해서 번갈아 작업을 처리하는 방식
CPU 사용률 향상
자원을 보다 효율적으로 사용
사용자에 대한 응답성 향상
작업이 분리되어, 코드 간결함
동시성(concurrency) 이슈
교착상태(deadlock)
우리가 사용하던 main메서드 또한 쓰레드다.
실행 중인 사용자 쓰레드가 하나도 없을 경우에만, 프로그램이 종료됨
public static void main(String[] args) {
String data = null;
if (...) {
}
while (...) {
}
System.out.println("...");
}
main() 메소드를 실행하면서 메인 스레드가 시작되고,
main() 메소드의 마지막 코드를 실행 or return문을 만날 경우, 실행이 종료된다.
문법
이름 설정하는 방법
1) 기본 이름
2) Thread-n 대신, 이름을 직접 설정하고 싶을 경우
threa.setName("스레드 이름");
스레드의 이름을 알고 싶을 경우
thread.getName();
예시
ThreadA
public class ThreadA extends Thread {
public ThreadA() {
setName("ThreadA"); // 스레드의 이름 설정
}
public void run() {
// ThreadA 실행 내용
for(int i=0; i<2; i++) {
System.out.println(getName() + "가 출력한 내용"); // 스레드의 이름 얻기
}
}
}
ThreadB
public class ThreadB extends Thread {
public void run() {
// ThreadB 실행 내용
for(int i=0; i<2; i++) {
System.out.println(getName() + "가 출력한 내용"); // 스레드의 이름 얻기
}
}
}
메인 스레드 이름 출력, UserThread 생성/시작
public class ThreadNameExample {
public static void main(String[] args) {
Thread mnainThread = Thread.currentThread(); // 이 코드를 실행하는 스레드 객체 얻기
Systelm.out.println("프로그램 시작 스레드 이름: " + mainThread.getName(); // 스레드의 이름 얻기
ThreadA threadA = new ThreadA(); // ThreadA 생성
System.out.println("작업 스레드 이름: " + ThreadA.getName()); // 스레드의 이름 얻기
threadA.start(); // ThreadA 시작 (ThreadA 내부에서 getName() 메소드를 출력하게 됨)
ThreadB threadB = new ThreadB(); // ThreadB 생성
System.out.println("작업 스레드 이름: " + ThreadB.getName()); // 스레드의 이름 얻기
threadB.start(); // ThreadB 시작 (ThreadB 내부에서 getName() 메소드를 출력하게 됨)
}
}
/* 실행 결과
프로그램 시작 스레드 이름: main // ThreadNameExample 클래스
작업 스레드 이름: ThreadA // ThreadNameExample 클래스
ThreadA가 출력한 내용 // ThreadNameExample 클래스
ThreadA가 출력한 내용 // ThreadA 클래스
작업 스레드 이름: Thread-1 // ThreadNameExample 클래스
Thread-1가 출력한 내용 // ThreadNameExample 클래스
Thread-1가 출력한 내용 // threadB 클래스 */
스레드 참조
setName(), getName() 은 Thread 클래스의 인스턴스 메소드이므로, 스레드 객체의 참조가 필요하다.그러나 만약, 스레드 객체의 참조를 가지고 있지 않다면
Thread 클래스의 정적 메소드인 currentThread() 를 이용해서, 현재 스레드의 참조를 얻을 수 있다.
어떤 자바 애플리케이션이든 메인 스레드는 반드시 존재하므로,
메인 작업 외의 추가적인 작업의 수 만큼 스레드를 생성하면 된다.
또한, 자바에서는 작업 스레드도 객체로 생성되기 때문에 클래스가 필요하다.
멀티 스레드로 실행하는 어플리케이션 개발을 위해서는
몇 개의 작업을 병렬로 실행할지 결정하고, 각 작업별로 스레드를 생성한다.
Thread 클래스로부터 직접 생성
1) Runnable 인터페이스 타입의 매개값을 갖는 생성자를 호출해야 한다
Thread thread = new Thread(Runnable target);
2) Runnable 에는 run() 메소드 하나가 정의되어 있는데,
구현 클래스는 run()을 재정의 해서, 작업 스레드가 실행할 코드를 작성해야 한다.
public class Task implements Runnable{
@Override
public void run() {
// 여기에 작업 스레드가 실행할 코드 작성
}
}
3-1) Runnable 구현 객체(task)를 생성 후,
이것을 매개값으로 해서 Thread 생성자를 호출해야 비로소 작업 스레드가 생성된다.
public class MultiThread {
Runnable task = new Task();
Thread thread = new Thread(task); // 구현 객체를 매개값으로 해서 Thread 생성자를 호출 -> 작업 스레드 생성.
}
3-2) 3-1 방법 보다는, Thread 생성자를 호출할 때
Runnable 익명 객체를 매개값으로 사용하는 방법을 더 많이 사용한다.(코드 절약)
4) 이렇게 작업 스레드를 생성 후, start() 메소드를 호출해야만 비로소 실행된다.
(작업 스레드는 생성되는 즉시 실행되는 것이 아니다.)
thread.start();
5) start()메소드가 호출되면,
작업 스레드는 매개값으로 받은 Runnable의 run() 메소드를 실행하면서 자신의 작업을 처리한다.
Runnable
- 작업 스레드가 실행할 수 있는 코드를 가지고 있는 객체 라고 해서 붙여진 이름
- 인터페이스 타입이기 때문에, 구현 객체를 만들어서 대입해야 한다.
- Runnable은 작업 내용을 가지고 있는 객체이지,
실제 스레드는 아니다.
Thread 하위 클래스로부터 생성
1-1) Thread 클래스를 상속한 후 , run() 메소드를 재정의해서 스레드가 실행할 코드를 작성.
public class WorkerThread extends Thread{
// run() 메소드 재정의
@Override
public void run() {
스레드가 실행할 코드;
}
}
Thread thread = new WorkerThread();
1-2) Thread 익명 객체로 작업 스레드 객체를 생성할 수도 있다.(코드 절약)
2) 이렇게 생성된 작업 스레드 객체에서 start() 메소드를 호출하면,
작업 스레드는 자신의 run() 메소드를 실행하게 된다.
방법1 VS 방법2
- 방법1
- Runnable 인터페이스의 구현 객체를 만들 때, 우선 클래스를 작성하고 implements Runnable 해준 다음 Runnable 에 있는 run() 메소드를 클래스에서 재정의.
- 작업 스레드가 매개값으로 받은 Runnable의 run() 메소드를 실행하면서 자신의 작업을 처리.
- 방법2 : extends Thread 를 한 다음 run() 메소드를 재정의
멀티 스레드 프로그램에서 스레드들이 객체를 고유해서 작업할 경우,
스레드A가 사용하던 객체를 스레드B가 A의 상태를 변경할 수 있기 때문에
스레드A가 의도했던 것과는 다른 결과를 산출할 수도 있다.
'공유 객체 사용의 문제' 이다.
사람A가 계산기로 작업하다가 계산 결과를 메모리에 저장한 뒤 잠시 자리를 비운다.
이때 사람B가 계산기를 만져서 사람A가 메모리에 저장한 값을 다른 값으로 변경해버린다.
사람A가 다시 돌아와 계산기에 저장된 값을 이용해서 이후 작업을 진행하게 된다.
스레드가 사용 중인 객체를 다른 스레드가 변경할 수 없게 하려면,
스레드 작업이 끝날 때까지 해당 객체에 잠금(Lock 락)을 걸어서, 다른 스레드가 해당 객체를 사용할 수 없게 만들면 된다.
암시적 Lock (synchronized)
1) 동시성 해결에 가장 간단하면서 쉬운 방법
2) 문제가 된 메서드, 변수에 각각 synchronized 키워드
를 넣는다
3) Java 는 임계 영역(critical section, 멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역)을 지정하기 위해,
동기화 메소드를 제공한다.
4) 스레드가 객체 내부의 동기화 메소드를 실행할 경우, 즉시 객체에 잠금을 걸어 다른 스레드가 동기화 메소드를 실행하지 못하게 한다.
5) synchronized 키워드를 붙이면, 동기화 메소드를 만들 수 있다.
// 예시 1
public class Calculator {
private int memory;
public int getMemory() {
return memory;
}
// synchronized 키워드로 인해, Calculator 객체가 잠금 처리됨
public synchronized void setMemory(int memory) {
this.memory = memory;
try {
Thread.sleep(2000);
} catch(InterruptedException e) {}
System.out.println(Thread. currentThread().getName() + this.memory);
}
}
/*
User1 스레드는 Calculaor 객체의 동기화 메소드인 setMemory()를 실행하는 순간 Calculator 객체를 잡금 처리한다.
메인 스레드가 User2 스레드를 실행하지만, 동기화 메소드인 seMemory()를 실행하지는 못하고 User1 이 setMemor()를 모두 실행할 동안 대기해야 한다.
Userl 스레드가 setMemory() 메소드를 모두 실행하고 나면, User2 스레드가 setMemory()메소드를 실행할 수 있게 된다.
*/
// 예시 2
class Count {
private int count;
public synchronized int view() { // 문제가 된 메서드
return count++;
}
}
class Count {
private Integer count = 0;
public int view() {
synchronized (this.count) { // 문제가 된 변수
return count++;
}
}
}
명시적 Lock
1) synchronized 키워드 없이, 명시적으로 ReentrantLock 키워드
를 사용하는 방법
2) when? 해당 Lock의 범위를 메서드 내부에서 한정하기 어렵거나 or 동시에 여러 Lock을 사용하고 싶을 때
3) 직접적으로 Lock 객체를 생성하여 사용
(Reentrant: 재진입)
public class CountingTest {
public static void main(String[] args) {
Count count = new Count();
for (int i = 0; i < 100; i++) {
new Thread(){
public void run(){
for (int j = 0; j < 1000; j++) {
count.getLock().lock();
System.out.println(count.view());
count.getLock().unlock();
}
}
}.start();
}
}
}
class Count {
private int count = 0;
private Lock lock = new ReentrantLock();
public int view() {
return count++;
}
public Lock getLock(){
return lock;
};
}
스레드 안전한 객체 사용
1) Concurrent 패키지
- concurrent 패키지에 존재하는 컬랙션들은 락을 사용할 때 발생하는 성능 저하를 최소한으로 만든다.
- Lock Striping 기법(락을 여러 개로 분할하여 사용)을 사용하여,
동시에 여러 스레드가 하나의 자원에 접근하더라도, 동시성 이슈가 발생하지 않도록 돕는다.
class Count {
private AtomicInteger count = new AtomicInteger(0);
public int view() {
return count.getAndIncrement();
}
}
2) ConcurrentHashMap
- 내부적으로 여러개의 락을 가지고 해시값을 이용해, 이러한 락을 분할하여 사용
- 분할 락을 사용하여, 병렬성과 성능을 모두 잡은 컬랙션
- 일반적인 map을 사용할 때처럼 구현하면, 내부적으로 알아서 락을 자동으로 사용
```java
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
불변 객체 (Immutable Instance)
1) 스레드 안전한 프로그래밍을 하는 방법 中 효과적인 방법
2) 예시: String
3) 불변 객체는 락을 걸 필요가 없는데,
내부적인 상태가 변하지 않으니 여러 스레드에서 동시에 참조해도 동시성 이슈가 발생하지 않기 때문
(즉, 불변 객체는 언제나 '스레드 안전(Thread-safe)하다.')
4) 불변 객체는 생성자로 모든 상태 값을 생성할 때 세팅하고,
객체의 상태를 변화시킬 수 있는 부분을 모두 제거해야 한다.
--> 방법 1: 세터(setter)를 만들지 않기
--> 방법 2: 내부 상태가 변화 없도록 모든 변수를 final로 선언(단, final 을 쓰면 무조건 초기화 해야함)
--> 방법 3: 데이터 자체를 Stream()안에서 캡슐화해서 결과를 도출하기(함수형 프로그래밍을 사용하는 이유)
스레드 안전성(Thread safe)
여러 스레드가 작동하는 환경에서도 문제 없이 동작하는 것을 '스레드 안전하다'고 말한다.
즉, 동시성 이슈를 해결하고 일어나지 않는다면, 'Thread safe하다'고 하는 것
I/O 블락킹(blocking)
입력받는 작업과 화면에 출력하는 작업을 하나의 쓰레드로 할 경우, I/0 블락킹이 나타나게 된다.
외부 기기와의 입출력이 필요한 작업일 경우
즉, 두 쓰레드가 서로 다른 자원을 사용하는 경우
쓰레드 간의 작업 전환 시간
대기 시간
싱글 코어
멀티 코어
글 잘 봤습니다.