Comments
link to Github.
package rule66.compare.stop; /** * 한 스레드의 boolean 값이 true가 되면 후면 스레드는 중지되는 프로그램이다. * 하지만 최소 약 10밀리세컨드 이상을 sleep하면 후면 스레드는 해당 루프를 돌면서 boolean 값은 자신이 바꿀 수 없는 값이고 항상 동일하다고 생각하여 * 해당 변수를 무시하도록 소스를 변경한다. 이런 최적화를 JVM(Hotspot JVM)최적화 기술인 끌어올리기(hoisting)라고 하는데 * 그 덕에 후면 스레드는 절대 끝나지 않는 경우가 발생한다. * * @author gwon * @history * 2019. 6. 24. initial creation */ public class FirstStopThread { private static boolean stopRequested = false; public static void main(String[] args) throws InterruptedException { Thread bgThread = new Thread(new Runnable() { @Override public void run() { int i = 0; while (!stopRequested) { // System.out.println(i++); // 최적화가 발생하지 않는다. 해당 메서드는 Thread-safe하기 때문. i++; } /** * hoisting할 경우 위 소스는 아래와 같이 변경된다. * if (!stopRequested) { * while (true) { * i++; * } * } * *****************************/ System.out.println("finished"); // 호출되지 않음 } }); bgThread.start(); // Thread.sleep(1); // 아주 빠르게 true를 줄 경우, 최적화가 되기전에 true 되므로 후면 스레드는 종료된다. Thread.sleep(10); stopRequested = true; } }
package rule66.compare.stop; /** * {@link FirstStopThread} 수정하는 한 가지 방법은 boolean 값을 동기화하는 것이다. * synchronized 사용하여 동기화 할 경우, 당연하게 상호배제(mutually exclusive)를 제공한다. * 하지만 이 프로그램에서는 상호배제를 위한 동기화가 아니다. 동기화는 한 스레드가 만든 변화를 다른 스레드에서 관측할 수 있다. * 이 프로그램에서는 이를 위하여 동기화를 사용한다. 즉, 후면 스레드는 동기화 메서드인 stopRequested()의 변화를 지속적으로 관찰할 수 있도록 하여 * 앞서 본 {@link FirstStopThread}의 문제점인 hoisting 최적화를 막아 정상적으로 프로그램이 종료되도록 한다. * * @author gwon * @history * 2019. 6. 24. initial creation */ public class SecondStopThreadSync { private static boolean stopRequested = false; private static void requestStop() { stopRequested = true; } private static synchronized boolean stopRequested() { return stopRequested; } public static void main(String[] args) throws InterruptedException { Thread bgThread = new Thread(new Runnable() { @Override public void run() { int i = 0; while (!stopRequested()) { i++; } System.out.println("finished"); } }); bgThread.start(); Thread.sleep(10); requestStop(); } }
package rule66.compare.stop; /** * {@link SecondStopThreadSync}에서 설명했듯이, 이 프로그램에서 동기화는 상호배제를 목적으로 사용하지 않았다. * 즉, 락이 필요없다. 비록 순환문의 각 단계마다 동기화를 실행하는 비용이 크진 않지만 그래도 동기화를 쓰지 않는것보다는 비용이 크다. * 락이 필요없고 비용을 줄이고 싶다면 volatile를 사용하면 된다. volatile은 어떤 스래드건 가장 최근에 기록된 값을 읽도록 보장한다. * (하지만 volatile이 volatile을 사용하지 않는것보다 비용이 적은것은 아니다.) * * @author gwon * @history * 2019. 6. 24. initial creation */ public class ThirdStopThreadVolatile { private static volatile boolean stopRequested = false; public static void main(String[] args) throws InterruptedException { Thread bgThread = new Thread(new Runnable() { @Override public void run() { int i = 0; while (!stopRequested) { i++; } System.out.println("finished"); } }); bgThread.start(); Thread.sleep(10); stopRequested = true; } }
package rule66.compare.increment; /** * volatile은 데이터를 읽을때는 가장 최근에 기록된 값을 읽도록 보장하지만 쓰기에 대해서는 동기화를 지원하지 않는다. * getNum()으로 가장 최근에 기록된 값에서 ++을 하지만, 두 개의 쓰레드가 동시에 같은 값을 읽어서 ++할 경우에는 결국 같은 값을 쓰게 된다. * 따라서 아래 결과는 2000이 되지 않는다. 따라서 이럴 땐 상호배제를 위해 synchronized를 사용해야 한다. {@link SecondIncrementSync} * * @author gwon * @history * 2019. 6. 24. initial creation */ final class FirstIncrementVolatile { private static volatile int cnt = 0; public static int getNum() { return cnt++; } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { getNum(); } System.out.println("t1 finished"); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { getNum(); } System.out.println("t2 finished"); } }); t1.start(); t2.start(); Thread.sleep(1000); System.out.println(cnt); // not 2000 } }
package rule66.compare.increment; /** * 메서드를 동기화로 선언하면 여러 스레드가 동시에 호출하더라도 서로 겹쳐 실행되지 않는다. * 더 좋은 방법도 있다. 사실 아래와 같은 경우를 위해 자바에서는 atomic 패키지에 다양한 클래스를 제공한다. {@link ThirdAtomicLong} * * @author gwon * @history * 2019. 6. 24. initial creation */ final class SecondIncrementSync { private static int cnt = 0; // synchronized 메서드 public static synchronized int getNum() { return cnt++; } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { getNum(); } System.out.println("t1 finished"); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { getNum(); } System.out.println("t2 finished"); } }); t1.start(); t2.start(); Thread.sleep(1000); System.out.println(cnt); // 2000 } }
package rule66.compare.increment; import java.util.concurrent.atomic.AtomicLong; /** * {@link rule47} 어떤 라이브러리가 있는지 파악하고, 적절히 활용하라 규칙처럼 {@link SecondIncrementSync}과 같이 * 원하는 일을 해주면서도, 동기화를 사용한 해법보다 성능도 좋은 클래스는 자바에서 제공한다. * 좀 더 자세한 설명은 {@link CAS} 참고 * * * @author gwon * @history * 2019. 6. 24. initial creation */ final class ThirdAtomicLong { private static final AtomicLong cnt = new AtomicLong(); public static long getNum() { return cnt.getAndIncrement(); } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { getNum(); } System.out.println("t1 finished"); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { getNum(); } System.out.println("t2 finished"); } }); t1.start(); t2.start(); Thread.sleep(1000); System.out.println(cnt); // 2000 } }
package rule66.compare.increment.cas; /** * java.util.concurrent.atomic 패키지가 제공하는 클래스들은 원자적 연산을 수행할 수 있는 클래스들을 제공한다. * synchronized 키워드 없이도 여러 스레드들에 의해 병렬적으로 수행되어도 결과의 안전성을 보장한다. * 내부적으로는 CAS(Compare-And-Swap)을 활용하는데 이는 네이티브 메서드로 짜여 있으며 실제 메모리를 참조하여 구현되어 있다. * 아래 함수는 예시로 만든 CAS메서드와 CAS메서드를 이용하여 AtomicLong 클래스가 어떻게 원자적 연사를 수행하는지 보여준다. * (자바는 포인터가 없으므로 컴파일은 되지 않는다.) * * @author gwon * @history * 2019. 6. 25. initial creation */ public class CAS { /** * 값을 변경하기전 자신이 기억하고 있는 값과 현재 모든 스레드가 공유하는 메모리 영역의 값을 비교한다. * 비교하여 같지 않을 경우, 다른 스레드가 먼저 해당 값을 변경하였으므로 false를 리턴하여 호출한 곳에서 다시 새로운 값으로 호출하도록 한다. * 비교하여 값이 같을 경우, 현재 스레드가 값을 변경할 차례로 판단하여 메모리 영역에 새로운 값을 저장하고 true를 리턴하여 호출한곳에서 끝내도록 한다. * * @param *pointer 모든 스레드가 공유하는 메모리 영역의 포인터. * @param oldVal 값을 변경하기전 자신이 기억하고 있는 값 * @param newVal 변경하고자 하는 값 * @return */ public boolean compareAndSwap(int *pointer, int oldVal, int newVal) { if (*pointer != oldVal) { return false; } *pointer = newVal; return true; } /** * 모든 스레드가 공유하는 메모리 영역의 값을 가지고 와, 현재 자신이 기억하는 값으로 저장한다. * 1을 증가시키기 위해 CAS를 호출하는데 이 때 메모리 영역과, 현재 자신이 기억하는 값, 변경값을 인자로 주어 호출한다. * 만약 이 때 동안 다른 스레드가 변경을 할 경우, 자신이 기억하고 있는 값과 메모리 영역의 값이 달라 CAS에서 false가 리턴되어 * 다시 현재 자신이 기억하는 값을 새롭게 업데이트하여 반복한다. * */ public long getAndIncrement(){ boolean done = false; int oldVal; while(!done){ oldVal = *pointer; done = compareAndSwap(pointer, oldVal, oldVal + 1); } return oldVal + 1; } }