synchronized는 lock을 사용해 동기화를 시킨다. 하지만 사용 방식에 따라 혼동되기 쉽다.
synchronized는 4가지의 사용법이 있다.
sychronized method
sychronized block
static sychronized method
static synchonized block.
이 포스팅에서는 이 4가지 방식의 차이인 lock이 적용되는 범위를 중점으로 다룬다.
synchronized method 는 클래스의 인스턴스에 대하여 lock을 건다. 다음과 같은 상황을 보자.
결과는 ?
순서대로 lock을 획득하고 반납하였다.
결과는 ?
이 상황에서는 lock을 공유하지 않기 때문에 동기화가 발생하지 않는다.
실험 결과를 보면 알 수 있듯이 synchronized method는 인스턴스에 대하여 lock을 건다.
인스턴스 접근 자체가 lock이 걸리는 것인가에 대한 다른 예시를 보자.
이번에는 동기화가 발생했다.
결과를 정리해보자면 synchronize메서드는 인스턴스 단위로 lock을 건다. 이때, synchronized가 적용된 모든 object에 대해서 lock을 공유한다.
static이 포함된 synchronized method방식은 우리가 일반적으로 생각하는 static의 성질을 갖는다. 인스턴스가 아닌 클래스 단위로 lock이 발생한다.
public static synchronized void run(String name) {
System.out.println(name + " lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " unlock");
}
public static void main(String[] args) throws InterruptedException {
A a1 = new A();
A a2 = new A();
Thread thread1 = new Thread(() -> {
a1.run("thread1");
});
Thread thread2 = new Thread(() -> {
a2.run("thread2");
});
thread1.start();
thread2.start();
}
결과는 ?
다른 인스턴스이지만 클래스 단위로 lock이 발생했다.
그렇다면 만약, static synchronized method와 synchronized method가 섞여있다면 어떨까?
public synchronized void print(String name) {
System.out.println(name + "hi ?");
}
public static synchronized void run(String name) {
System.out.println(name + " lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " unlock");
}
package com.example.thread;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ThreadApplication {
public static void main(String[] args) throws InterruptedException {
A a1 = new A();
A a2 = new A();
Thread thread1 = new Thread(() -> {
a1.run("thread1");
});
Thread thread2 = new Thread(() -> {
a2.print("thread2");
});
thread1.start();
thread2.start();
}
}
결과는 ?
인스턴스 단위의 lock과 클래스 단위의 lock은 공유되지 않았다.
static synchronized method를 정리해보면 클래스 단위로 lock을 걸지만, 인스턴스 단위의 synchronized method와 lock을 공유하지 않는다.
synchronized block은 인스턴스의 block단위로 lock을 건다. 이때, lock객체를 지정해줘야한다.
public void run(String name) {
synchronized (this) {
System.out.println(name + " lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " unlock");
}
}
public static void main(String[] args) throws InterruptedException {
A a1 = new A();
Thread thread1 = new Thread(() -> {
a1.run("thread1");
});
Thread thread2 = new Thread(() -> {
a1.run("thread2");
});
thread1.start();
thread2.start();
}
결과는 ?
이렇게 block을 지정해준다. 이때 this는 A객체를 의미하고 block이 method전체에 적용되어있기 때문에 method단위로 lock을 거는 것과 같다.
위와 같이 여러 로직이 섞여있는 사이에 필요한 부분만 lock을 걸 수 있다. lock은 synchronized block에 진입할 때 획득하고 빠져나오면서 반납하므로 block으로 범위를 지정하는게 더 효율적이다.
synchronized block도 method와 동일하게 인스턴스에 대해서 적용된다. 따라서 다음과 같은 상황에서 lock은 각각의 인스턴스별로 관리된다.
public static void main(String[] args) throws InterruptedException {
A a1 = new A();
A a2 = new A();
Thread thread1 = new Thread(() -> {
a1.run("thread1");
});
Thread thread2 = new Thread(() -> {
a2.run("thread2");
});
thread1.start();
thread2.start();
}
public void run(String name) {
synchronized (this) {
System.out.println(name + " lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " unlock");
}
}
결과는 ?
인스턴스 별로 lock 이 관리되는 모습
아래는 synchronized block을 사용하는 다른 방식이다.
public static void main(String[] args) throws InterruptedException {
A a = new A();
Thread thread1 = new Thread(() -> {
a.run("thread1");
});
Thread thread2 = new Thread(() -> {
a.run("thread2");
});
Thread thread3 = new Thread(() -> {
a.print("B와 상관없는 thread3");
});
thread1.start();
thread2.start();
thread3.start();
}
public class A {
B b = new B();
public synchronized void print(String name) {
System.out.println(name + " hi?");
}
public void run(String name) {
synchronized (b) {
System.out.println(name + " lock");
b.run();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " unlock");
}
}
}
public class B extends Thread {
public synchronized void run() {
System.out.println("B lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("B unlock");
}
}
결과는 ?
이전보다 약간 복잡해졌다. lock 객체를 A클래스가 아닌 B클래스의 인스턴스 b로 사용하고 있다.
thread1, thread2는 b를 사용하는 method를 호출하고 있지만 thread3는 b와 상관없는 method이기 때문에 b의 lock과 상관없이 출력되었다.
따라서 thread1 과 B의 순서를 보장받고 thread3만 관련없이 출력된 모습
이번에는 lock 객체를 인스턴스가 아닌 class로 사용해보자.
public static void main(String[] args) {
A a1 = new A();
A a2 = new A();
Thread thread1 = new Thread(() -> {
a1.run("thread1");
});
Thread thread2 = new Thread(() -> {
a2.run("thread2");
});
thread1.start();
thread2.start();
}
public class A {
B b = new B();
public void run(String name) {
synchronized (B.class) {
System.out.println(name + " lock");
b.run();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " unlock");
}
}
}
public class B extends Thread {
public synchronized void run() {
System.out.println("B lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("B unlock");
}
}
block에 .class형식을 사용하면 인스턴스가 아닌 class단위로 lock을 건다.
정리하자면, synchronized block은 lock을 거는 객체를 지정할 수 있다. 객체를 넘기면 인스턴스 단위로 lock을 걸고 .class형식으로 넘기면 클래스 단위의 lock을 건다.
static method안에 synchronized block을 지정할 수 있다. static의 특성상 this같이 현재 객체를 가르키는 표현을 사용할 수 없다.
static synchroinzed method방식과 차이는 lock객체를 지정하고 block으로 범위를 한정지을 수 있다는 점이다. 이외에 클래스 단위로 lock을 공유한다는 점은 같다.
결과는 ?
synchronized는 thread별 동기화 순서를 보장할까?
public static void main(String[] args) throws InterruptedException {
A a = new A();
Thread[] threads = new Thread[5];
for (int i = 0; i < threads.length; i++) {
final int order = i;
threads[i] = new Thread( () -> {
a.run("thread " + order);
});
}
threads[0].start();
Thread.sleep(100);
threads[1].start();
Thread.sleep(100);
threads[2].start();
Thread.sleep(100);
threads[3].start();
Thread.sleep(100);
threads[4].start();
}
public void run(String name) {
synchronized (A.class) {
System.out.println(name + " lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " unlock");
}
}
결과는 ?
순서를 보장하지 않는다.
인스턴스 단위로 lock이 걸린다.
메서드가 시작될 때부터 종료될 때까지 동기화가 발생한다.
동일 인스턴스내에서 synchronized키워드가 적용된 곳에서는 lock을 공유한다.
인스턴스 단위로 lock이 걸린다.
block내부에서 동기화가 발생한다.
lock 객체를 지정하여 특정 대상에만 lock을 걸 수 있다.
lock을 객체로 설정하면 해당 인스턴스만 lock이 걸리고 .class형식으로 설정하면 클래스 단위로 lock을 건다.
클래스 단위로 lock이 걸린다.
메서드가 시작될 때부터 종료될 때까지 동기화가 발생한다.
static synchronized와 synchronized가 혼용되어있을 때 각자의 lock으로 관리된다.
클래스 단위로 lock이 걸린다.
block내부에서 동기화가 발생한다.
lock객체를 지정하여 특정 대상에만 lock을 걸 수 있다.
lock을 객체로 설정하면 해당 인스턴스만 lock이 걸리고 .class형식으로 설정하면 클래스 단위로 lock을 건다.
해당 포스트는 java 동기화 정리를 참고하며 직접 실습하면서 정리된 글 입니다.