개발공부/JAVA

[JAVA] 멀티 스레드

jnnjnn 2024. 3. 26. 18:24

멀티 스레드

  • 멀티 태스킹이란 두 가지 이상 작업을 동시에 처리하는 것을 말함
  • 하나의 스레드에서 멀티 태스킹을 할 수 있도록 한 것이 멀티 스레드
  • 멀티 스레드에서 하나의 스레드의 오류는 다른 스레드에도 영향을 미친다

 

작업 스레드 생성

-Thread 클래스를 통해서 멀티 스레드를 구현할 수 있다.
-스레드를 구현하려면 run() 메소드 내부에 스레드에서 실행될 코드를 구현해야 한다.

-Thread 구현 객체의 start() 메소드를 호출하면 재정의된 run()을 실행시킨다

1) Thread의 자식 객체 생성

  • Thread를 상속받는 자식 객체를 생성한다.
  • run() 메소드를 재정의하여 스레드가 실행할 코드를 작성하고 객체를 생성한다.
  • Thread의 익명 자식 객체를 사용할 수도 있다.
public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("MyThread is running");
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();  // Thread 객체 생성
        myThread.start();  // 쓰레드 실행
    }
}

2) Thread 클래스로 직접 생성

  • Thread 클래스로부터 작업 스레드 객체를 직접 생성하려면 Runnable 구현 객체를 매개값으로 갖는 생성자를 호출한다
  • Runnable의 구현 클래스가 Thread로 Runnable에는 run() 메소드가 정의되어 있다.
  • 일반적으로 Thread의 생성자에 Runnable 익명 구현 객체를 매개값으로 사용한다
  • Thread의 자식 객체를 생성하는 것보다 다형성이 확보된다.
public class Main {

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Anonymous Runnable is running");
        });

        thread.start();  // 쓰레드 실행
    }
}

 

스레드 상태

1) join 메소드

해당 스레드의 작업이 종료될 때까지 join() 메소드를 호출한 스레드를 대기 상태로 만든다

class SumThread extends Thread {
    private long sum;

    public long getSum() {
        return sum;
    }

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

public class C08Join {
    public static void main(String[] args) throws InterruptedException {
        SumThread threadA = new SumThread();

        // threadA 실행
        threadA.start();
        // Main 스레드를 대기 상태로 만듬
        threadA.join();

        // threadA 종료 후 Main 스레드 실행
        System.out.println(threadA.getSum());
    }
}

 

2) sleep(long millis)

주어진 시간 동안 스레드를 일시 정지 상태로 만든다.

public class C09Sleep {
    public static void main(String[] args) {
        Thread threadA = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("ThreadA 실행");
                try {
                    // 3초 일시정지 후 실행
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                }
            }
        });
    }
}

스레드 동기화

멀티 스레드가 하나의 객체를 공유해서 작업할 경우 스레드의 작업이 겹치면서 의도했던 것과 다른 결과가 나올 수 있다.

다음과 같은 방법으로 스레드 안전한 연산을 수행할 수 있다.

 

1) synchronized 키워드 사용

 

-synchronized 키워드는 인스턴스와 정적 메소드 어디든 붙일 수 있다.
-메소드 전체가 아닌 일부 영역을 실행할 때만 객체 잠금을 걸고 싶다면 동기화 블록을 만들고 synchronized(공유객체)를 선언한다.
-this 키워드는 메소드가 실행하는 객체를 나타낸다

 public class C03SynchronizedMethod {
    public static void main(String[] args) throws InterruptedException {
        MyObject3 o = new MyObject3();
        Thread t1 = new Thread(o);
        Thread t2 = new Thread(o);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(o.getValue());
    }
}

class MyObject3 implements Runnable {
    private long value;

    public long getValue() {
        return value;
    }

    public synchronized void update() {
        for (int i = 0; i < 30000; i++) {
            value++;
        }
    }

    @Override
    public void run() {
        update();
    }
}

 

 

2) Atomic 클래스 사용

Atomic 클래스를 사용하면 별도의 동기화 없이 스레드 안전한 연산을 수행할 수 있다.

import java.util.concurrent.atomic.AtomicLong;

public class C04Concurrency {
    public static void main(String[] args) throws InterruptedException {
        MyObject4 o = new MyObject4();
        Thread t1 = new Thread(o);
        Thread t2 = new Thread(o);

        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

class MyObject4 implements Runnable {
    //    private long value;
    private AtomicLong value;

    public MyObject4() {
        this.value = new AtomicLong();
    }

    public long getValue() {
        return value.longValue();
    }

    public void update() {
        for (int i = 0; i < 30000; i++) {
//            value++;
            value.getAndIncrement();
        }
        System.out.println(Thread.currentThread().getName() + ": " + getValue());
    }

    @Override
    public void run() {
        update();
    }
}

 

스레드 안전한 자료구조

흔히 사용하는 List의 ArrayList나 Set의 HashSet은 멀티 스레드로부터 안전하지 않다.

Vector나 Collections의 synchronizedList, synchronizedSet 등을 사용하면 스레드 안전한 연산을 수행할 수 있다.

public class C07SynchronizedList {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = Collections.synchronizedList(new ArrayList<>());

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 30000; i++) {
                list.add("a");
                list.remove("a");
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 30000; i++) {
                list.add("b");
                list.remove("b");
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(list);  //[]
    }
}