2013년 9월 3일 화요일

동시성 (2)

자원 공유

단일 스레드 프로그램에선 발생하지 않는 문제이지만, 여러 스레드를 사용하면 동시에 같은 자원을 사용하려 할 때 문제가 발생한다. 두 사람이 동시에 같은 장소에 주차하려는 상황저럼..

자원으로의 잘못된 접근

짝수를 생성하는 작업과 생성된 짝수를 검증하는 두개의 작업을 살펴보자.
public abstract class IntGenerator {
    private volatile boolean canceled = false;
    public abstract int next();
    public void cancel() {canceled = true;}
    public boolean isCanceled() {return canceled;}
}

public class EvenChecker implements Runnable {
    private IntGenerator generator;
    private final int id;

    public EvenChecker(IntGenerator generator, int id) {
        this.generator = generator;
        this.id = id;
    }
    public void run () {
        while(!generator.isCanceled()) {
            int val = generator.next();
            if(val % 2 != 0) {
                print(Thread.currentThread().getName() + " " + val + " not even!");
                generator.cancel();
            }
        }
        print(Thread.currentThread().getName() + " " + "Generator is canceled");
    }

    public static void test(IntGenerator gp, int count) {
        print("Press Control-C to exit");
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < count; i++) {
            exec.execute(new EvenChecker(gp, i));
        }
        exec.shutdown();
    }
    public static void test(IntGenerator gp) {
        test(gp, 10);
    }
}

public class EvenGenerator extends IntGenerator {
    private int currentEvenValue = 0;

    @Override
    public int next() {
        ++currentEvenValue;  // 위험한 부분
        ++currentEvenValue;
        return currentEvenValue;
    }

    public static void main(String[] args) {
        EvenChecker.test(new EvenGenerator());
    }
}
총 10개의 스레드가 같은 Generator 객체를 사용하고 있다. 한 스레드가 next()를 호출하여 currentEvenValue가 증가하는 순간 다른 스레드가 next()를 호출하는 경우는 충분이 발생할 수 있다. 이것은 내부의 값을 부정확한 생태로 빠뜨리게 한다.
Press Control-C to exit
pool-1-thread-1 3707 not even!
pool-1-thread-1 Generator is canceled
pool-1-thread-4 Generator is canceled
pool-1-thread-5 Generator is canceled
pool-1-thread-2 3711 not even!
pool-1-thread-2 Generator is canceled
pool-1-thread-3 3709 not even!
pool-1-thread-3 Generator is canceled
pool-1-thread-6 Generator is canceled
pool-1-thread-7 Generator is canceled
pool-1-thread-4 Generator is canceled
pool-1-thread-3 Generator is canceled
pool-1-thread-6 Generator is canceled
next() 메소드가 복수의 단계를 필요하므로 스레드 메커니즘에 의해 증가 단계의 중간에 작업이 끼어들 수 있다는 것을 명심해야 한다.

공유 자원 경쟁의 해결

동시성이 제대로 동작하려면 두 작업이 동시에 같은 자원에 접근하는 것을 막아야 한다. 이것은 해당 자원을 사용 중일 때 자물쇠를 잠그는 것으로 간단히 해결된다.
자원에 대한 충돌을 방지하기 위하여 자바는 기본적으로 synchronized 키워드를 제공한다. 작업이 synchronized 키워드로 보호되는 코드를 실행하면, 락(lock)이 사용가능한지 확인하고 획득한 다음 코드를 실행하고 완료되면 락을 해제한다.
메소드를 다음과 같이 synchronized 메소드로 선언하여 충돌을 방지할 수 있다.
synchronized void f() { ... }
synchronized void g() { ... }
다만 모든 객체는 모니터라고 불리는 단일 락을 가지고 있다. synchronized 메소드를 호출하면 그 객체가 가지고 있는 다른 모든 synchronized 메소드에 대한 접근이 블럭된다.
public class EvenGenerator extends IntGenerator {
    private int currentEvenValue = 0;

    @Override
    public synchronized int next() {
        ++currentEvenValue;  // 위험한 부분
        ++currentEvenValue;
        return currentEvenValue;
    }

    public static void main(String[] args) {
        EvenChecker.test(new EvenGenerator());
    }
}
클래스에 대한 단일 락도 존재하여 synchronized static 메소드는 클래스 당 static 데이터의 동시접근을 막기 위한 락을 제공한다.

Brian's Rule of Synchronization
이후에 다른 스레드가 읽을 수 있는 변수를 기록할 때나 이전에 다른 스레드가 기록했을 수 있는 변수를 읽을 때
동기화를 사용해야 한다. 추가적으로 읽는 객체나 기록하는 객체 모두 동일한 모니터 락을 사용하여 동기화를
해야 한다.

명시적인 락

java.util.concurrent.locks에 정의된 명시적인 뮤텍스 메커니즘을 제공한다. 락 객체를 명시적으로 생성, 잠그고 해제 한다.
public class MutexEvenGenerator extends IntGenerator {
    private int currentEvenValue = 0;
    private Lock lock = new ReentrantLock();

    @Override
    public int next() {
        lock.lock();
        try {
            ++currentEvenValue;
            ++currentEvenValue;
            return currentEvenValue;
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        EvenChecker.test(new MutexEvenGenerator());
    }
}
MutexEvenGenerator는 lock이라는 뮤텍스를 추가하고 next()안에서 임계영역을 생성하기 위하여 lock(), unlock() 메소드를 사용한다. lock()을 호출한 직후 try-finally 블럭을 사용하고 finally 안에서 unlock()을 호출한다.
try 블럭 안에서 return을 호출하여 unlock()이 너무 빨리 호출되지 않도록 유의한다.
synchronized 키워드 내부에서 예외가 발생하면 클린업을 수행할 기회가 없지만, 명시적인 lock 객체를 사용하면 finally 구문 안에서 사용 가능하다.
synchronized에서는 락 획득을 시도하거나 특정 시간 동안의 락 획득 시도 이후 포기와 같은 것을 할 수 없다. 명시적 lock 객체는 획득과 해제에 있어 synchronized 보다 좀 더 세밀한 제어를 가능하게 한다.

원자성(atomicity)과 휘발성(volatile)

원자성 오퍼레이션으 스레드 스케줄러에 의한 인터럽트가 발생하지 않는 오퍼레이션을 말하는데, 이러한 원자성을 신뢰하는 것은 위험한 것이다. 때때로 원자성 오퍼레이션이 안전해야 할 것 같은 상황에서도 실제로는 안전하지 않을 수 있다.
volatile 키워드는(SE5 이전에는 동작하지 않는다) 해당 필드에 수정이 가해지면 모든 읽기가 변화한다는 것을 의미한다. non-volatile 필드에 대한 원자성 오퍼레이션은 주 메모리에 저장하지 않고 레지스터 캐쉬에 저장될 수 있어 이 필드를 읽는 다른 작업은 새로운 값을 읽어 가지 않을 수도 있다. 복수의 작업이 한 필드에 접근을 한다면 그 필드는 volatile 되어야 한다.

원자(Atomic)클래스

자바 SE5는 AtomicInteger, AtomicLong, AtomicReference와 같이 원자 오퍼레이션을 가진 특별한 원자 클래스를 제공한다.
public class AtomicEvenGenerator extends IntGenerator {
    private AtomicInteger currentEvenValue = new AtomicInteger(0);

    @Override
    public int next() {
        return currentEvenValue.addAndGet(2);
    }
    public static void main(String[] args) {
        EvenChecker.test(new AtomicEvenGenerator());
    }
}
AtomicInteger를 사용하였기 때문에 별도의 동기화 기술이 필요 없다. java.util.concurrent의 클래스를 작성하기 위해 디자인 되었으며, 비록 이것이 문제를 일으키지 않을 것을 확신하더라도 특수한 상황에서만 사용해야 한다. 일반적으로 sychronized 키워드나 명시적 lock을 사용하는 것이 더 안전하다.

임계영역

때때로 메소드 전체가 아닌 일부분에 대하여 다수 스레드의 접근을 제한하고자 할 때가 있다. 이런 영역을 임계영역(Critical Section)이라고 하며 synchronized 키워드를 사용하여 생성한다. 여기서 synchronized는 내부의 코드를 동기화하는데 사용되는 락을 가지고 있는 객체를 지정하기 위해 사용된다.
synchronized(syncObject) {
    // 이 부분은 한번에 하나의 작업만
    // 접근 할 수 있다.
}
이는 동기화 블럭이라고 부르기도 한다. 이 부분을 수행하기 위해서는 우선 syncObject의 락을 획득해야 한다.
다음 코드에서 PairManager2의 락을 거는 시간이 현저히 작다. 이것이 전체 메소드의 동기화가 아닌 동기화 블럭을 사용하는 이유다.
abstract class PairManager {
    AtomicInteger checkCounter = new AtomicInteger(0);
    protected Pair p = new Pair();
    private List<Pair> storage = Collections.synchronizedList(new ArrayList<Pair>());
    public synchronized Pair getPair() {
        return new Pair(p.getX(), p.getY());
    }
    // 시간을 소비하는 오퍼레이션으로 가정
    protected void store(Pair p) {
        storage.add(p);
        try {
            TimeUnit.MILLISECONDS.sleep(50);
        } catch (InterruptedException e) {

        }
    }
    public abstract void increment();
}

// 전체 메소드 동기화
class PairManager1 extends PairManager {
    @Override
    public synchronized void increment() {
        p.incrementX();
        p.incrementY();
        store(getPair());
    }
}

// 임계영역 사용
class PairManager2 extends PairManager {
    @Override
    public void increment() {
        Pair temp;
        synchronized (this) {
            p.incrementX();
            p.incrementY();
            temp = getPair();
        }
        store(temp);
    }
}
시간을 소비하는 store() 메소드는 Pair 객체를 동기화된 ArrayList에 추가하므로 스레드에 안전하다고 할 수 있다. 그러므로 보호될 필요가 없으며 PairManager2의 동기화 블럭 외부에 위치한다.

다른 객체에 대한 동기화

동기화 블럭은 동기화에 사용할 객체를 지정해야 하며, 일반적으로 동기화에 가장 적합한 객체는 해당 메소드가 호출되고 있는 현재의 객체가 된다. 동기화 블럭에서 락을 획득하고 나면, 객체의 다른 동기화 메소드나 임계 영역에 대한 접근은 거부된다. 그러므로 this를 사용하여 동기화 블럭을 지정하면 임계영역으로 인해 동기화의 범위가 줄어든다. 반대로 말하면 동기화시 서로 다른 객체를 사용하면 두 작업이 동시에 접근 가능하다.
class DualSync {
    private Object syncObject = new Object();
    public synchronized void f() {
        for (int i = 0; i < 5; i++) {
            print("f()");
            Thread.yield();
        }
    }
    public void g() {
        synchronized (syncObject) { // this로 바꿀 경우 f(), g() 동시 접근이 불가능 하다.
            for (int i = 0; i < 5; i++) {
                print("g()");
                Thread.yield();
            }
        }
    }
}
public class SyncObject {
    public static void main(String[] args) {
        final DualSync ds = new DualSync();
        new Thread() {
            public void run() {
                ds.f();
            }
        }.start();
        ds.g();
    }
}

스레드 로컬 저장소

공유자원에 대한 작업 충돌을 방지하는 다른 방법은 공유 자체를 제거하는 것이다.객체를 사용하는 서로 다른 스레드마다 동일한 변수에 대하여 자동으로 서로 다른 저장소를 생성하는 메커니즘이다. 스레드 로컬 저장소의 생성 및 관리는 java.lang.ThreadLocal 클래스가 수행한다.
class Accessor implements Runnable {
    private final int id;

    public Accessor(int id) { this.id = id; }
    @Override
    public void run() {
        while(!Thread.currentThread().isInterrupted()) {
            ThreadLocalVariableHolder.increment();
            print(this);
            Thread.yield();
        }
    }
    @Override
    public String toString() {
        return "Accessor{id=" + id + " : " + ThreadLocalVariableHolder.get() + '}';
    }
}

public class ThreadLocalVariableHolder {
    private static ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
        private Random rand = new Random(47);
        protected synchronized Integer initialValue() {
            return rand.nextInt(10000);
        }
    };
    public static void increment() { value.set(value.get() + 1); }
    public static int get() { return value.get(); }
    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            exec.execute(new Accessor(i));
        }
        TimeUnit.MILLISECONDS.sleep(100); // 잠시 동안 실행
        exec.shutdownNow();  // 모든 Accessor 종료
    }
}
각 스레드에서 사용되는 ThreadLocal은 초기값을 다르게 설정하도록 되어 있다. 각 스레드에서 같은 static 객체를 참조하고 있지만 실제로는 각 스레드의 ThreadLocal.ThreadLocalMap에서 해당 값을 참조한다. 같은 객체를 참조하는 것처럼 보이지만 각 Thread별 다른 객체를 참조하도록 한다. 쓸데가 있을지는 고민을 해봐야겠다.


작업 종료하기

식물원

각 입구 마다 카운터가 설치되어 입장객의 수를 세고 있으며, 입구의 카운터가 늘어날 때 마다 공유하는 전체 카운터도 같이 증가한다.
class Count {
    private int count = 0;
    private Random rand = new Random(47);
    public synchronized int increment() {
        int temp = count;
        if(rand.nextBoolean())
            Thread.yield();
        return (count = ++temp);
    }
    public synchronized int value() { return count; }
}

class Entrance implements Runnable {
    private static Count count = new Count();
    private static List<Entrance> entrances = new ArrayList<Entrance>();
    private int number = 0;
    private final int id;
    private static volatile boolean canceled = false;
    // 휘발성 필드에 대한 원자성 오퍼레이션
    public static void cancel() { canceled = true; }
    public Entrance(int id) {
        this.id = id;
        // 현재 작업을 리스트에 보관
        // 죽은 작업의 가비지 컬렉션을 방지한다.
        entrances.add(this);
    }
    @Override
    public void run() {
        while(!canceled) {
            synchronized (this) {
                ++number;
            }
            print(this + " Total: " + count.increment());
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                print("sleep interrupted");
            }
        }
        print("stopping " + this);
    }
    public synchronized int getValue() { return number; }
    public String toString() {
        return "Enttance " + id + ": " + getValue();
    }
    public static int getTotalCount() {
        return count.value();
    }
    public static int sumEntrances() {
        int sum = 0;
        for(Entrance entrance : entrances) {
            sum += entrance.getValue();
        }
        return sum;
    }
}
public class OrnamentalGarden {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            exec.execute(new Entrance(i));
        }
        TimeUnit.MILLISECONDS.sleep(2000);
        Entrance.cancel();
        exec.shutdown();
        if(!exec.awaitTermination(250, TimeUnit.MILLISECONDS)) {
            print("Some tasks were not terminated!");
        }
        print("Total: " + Entrance.getTotalCount());
        print("Sum of Entrances: " + Entrance.sumEntrances());
    }
}
위 프로그램은 모든 것을 안전하게 종료하는 것에 조금 문제가 있다.

블럭 상태에서의 종료

스레드 상태

  • New : 스레드를 생성하는 순간, 이 후 실행 가능한 Runnable 혹은 Blocked 상태로 된다.
  • Runnable : 이용 가능한 CPU가 있으면 실행 할 수 있다는 것을 의미함, 실행 중이 아닐 수도 있으며 스케줄러가 할당되면 실행하는데 장애가 없음
  • Blocked : 스레드가 실행될 수 있지만 무언가가 막고 있는 상태
    • sleep()이 호출되어 지정된 시간 동안 실행이 중단된 경우
    • wait()을 통하여 스레드 실행이 지연되는 경우, notify() 혹은 notifyAll()을 수신하지 않으면 Runnable로 바뀌지 않는다.
    • 특정 I/O가 완료되기를 기다릴 때
    • 다른 객체의 synchronized 메소드를 호출 시도했으나 다른 작업에 의해 lock 된 경우
  • Dead : 죽거나 완료된 상태, CPU를 할당받지 않으며 작업이 완래되고 더이상 Runnable로 바뀌지 않는다. run()에서 return 되거나, interrupt 되는 경우다.
위의 Blocked 상태에서 작업을 종료하고자 할 때, 작업 내부의 로직이 스스로 종료를 감지하는 것을 기다릴 수 없다면 강제적으로 종료해야 한다.

인터럽트

블럭된 작업의 종료를 위해 Thread 클래스는 interrupt() 메소드를 제공하며 이는 스레드의 인터럽트 상태를 변경한다. 인터럽트 상태가 된 스레드는 이미 블럭되어 있거나 블럭될 오퍼레이션을 사용하려고 할 때 InterruptException을 발생시키며, 예외를 발생시킨 후 또는 작업이 Thread.interrupted()를 호출하면 인터럽트 상태를 다시 초기화한다.
다음 코드는 Executor를 사용하여 스레드를 시작할 때 execute()가 아닌 submit()을 호출하여 얻은 스레드의 컨텍스트를 통해 cancel()을 호출해서 해당 작업에 인터럽트를 전달하고 있다. cancel()에 true를 전달하면 해당 스레드에 interrupt()를 호출하여 중단시킬 권한을 부여하는 것이다.
class SleepBlocked implements Runnable {
    @Override
    public void run() {
        try {
            TimeUnit.SECONDS.sleep(100);
        } catch (InterruptedException e) {
            print("InterruptedException");
        }
        print("Exiting SleepBlocked.run()");
    }
}
class IOBlocked implements Runnable {
    private InputStream in;

    IOBlocked(InputStream in) {
        this.in = in;
    }

    @Override
    public void run() {
        try {
            print("Waiting for read()");
            in.read();
        } catch (IOException e) {
            if(Thread.currentThread().isInterrupted()) {
                print("Interrupted from block I/O");
            } else {
                throw new RuntimeException(e);
            }
        }
        print("Exiting IOBlocked.run()");
    }
}
class SynchronizedBlocked implements Runnable {
    public synchronized void f() {
        while(true) {
            Thread.yield();
        }
    }
    public SynchronizedBlocked() {
        new Thread() {
            public void run() {
                f();
            }
        }.start();
    }
    public void run() {
        print("Trying to call f()");
        f();
        print("Exiting SynchronizedBlocked.run()");
    }
}
public class Interrupting {
    private static ExecutorService exec = Executors.newCachedThreadPool();
    static void test(Runnable r) throws InterruptedException {
        Future f = exec.submit(r);
        TimeUnit.MILLISECONDS.sleep(100);
        print("Interrupting " + r.getClass().getName());
        f.cancel(true);  // 실행중이라면 인터럽트
        print("Interrupt sent to " + r.getClass().getName());
    }
    public static void main(String[] args) throws Exception {
        test(new SleepBlocked());
        test(new IOBlocked(System.in));
        test(new SynchronizedBlocked());
        TimeUnit.SECONDS.sleep(3);
        print("Aboarting with System.exit(0)");
        System.exit(0); // 두 번의 인터럽트가 실패할 경우
    }
}
SleepBlock, IOBlockd, SynchronizedBlocked 세 종류의 작업이 있다. 결과를 보면 sleep()에 대하여는 인터럽트를 걸 수 있다는 것을 알 수 있다. 하지만 동기화 락이나 I/O를 수행하는 상태의 블럭에 대해서는 인터럽트가 발생하지 않는다. I/O의 문제를 위한 한가지 방법은 해당 락을 유발한 자원을 닫는 것이다.
I/O 장에서 소개된 nio 클래스는 세련된 인터럽트를 제공한다. ReentrantLock에 의해 블록 된 작업은 인터럽트 처리가 가능하다.

인터럽트 감지

스레드에 interrupt()를 호출하면 작업이 블럭 상태에 들어가려고 하거나 이미 들어간 상태에서만 인터럽트가 발생한다.(I/O 및 동기화 메소드를 통한 블럭은 인터럽트가 발생하지 않는다.) 하지만 작업을 멈추기 위해 interrupt()를 호출했다면, 블럭 상황이 아니더라도 빠져 나올 수 있는 방법이 필요하다.
이것은 interrupt() 호출로 변경되는 인터럽트 상태로 해결 될 수 있다. interrupted()를 호출하여 상태를 확인 할 수 있고, 상태를 초기화하는 역할도 한다.(동일한 인터럽트를 두번 받지 않는다.) 즉 인터럽트의 확인은 한 번의 InterruptedException 감지나 한 번의 Thread.interrupted()의 성공적인 반환으로 가능하다.
class NeedsCleanup {
    private final int id;
    NeedsCleanup(int ident) {
        this.id = ident;
        print("NeedsCleanup " + id);
    }
    public void cleanup() {
        print("Cleaning up " + id);
    }
}
class Blocked3 implements Runnable {
    private volatile double d = 0.0;

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                // 첫번째 포인트
                NeedsCleanup n1 = new NeedsCleanup(1);
                // n1의 정의 직후 try-finally 구문 시작
                // n1의 정확한 소거를 보장하기 위해서이다
                try {
                    print("Sleeping");
                    TimeUnit.SECONDS.sleep(1);
                    // 두번째 포인트
                    NeedsCleanup n2 = new NeedsCleanup(2);
                    // n2의 정확한 소거 보장
                    try {
                        print("Calculating");
                        // 시간을 소비하지만 블로킹 되지 않은 오퍼레이션
                        for (int i = 1; i < 250000; i++) {
                            d = d + (Math.PI + Math.E) / d;
                        }
                        print("Finished time-consuming operation");
                    } finally {
                        n2.cleanup();
                    }
                } finally {
                    n1.cleanup();
                }
            }
            print("Exiting via while() test");
        } catch (InterruptedException e) {
            print("Exiting via InterruptedException");
        }
    }
}
public class InterruptingIdiom {
    public static void main(String[] args) throws InterruptedException {
        if (args.length != 1) {
            print("Usage: java InterruptingIdiom delay-in-ms");
            System.exit(1);
        }
        Thread t = new Thread(new Blocked3());
        t.start();
        TimeUnit.MILLISECONDS.sleep(new Integer(args[0]));
        t.interrupt();
    }
}
예외가 발생하여 순환문을 빠져 나올 때는 적절한 자원 반환(CleanUp)이 꼭 필요하다. interrupt()에 반응하는 클래스를 만들 때에는 객체 생성시 뒤 따르는 try-finally를 통해 객체 소거가 보장되어야 한다.

2013년 8월 29일 목요일

동시성 (1)

병렬 프로그래밍을 해야할 일이 거의 없기 때문에 아주 단편적인 내용만 알고 있었으나 좀 더 자세히 알 필요가 있을 듯 해서
Thinking In JAVA의 21장 동시성 부분을 다시 보면서 정리해본다.


동시성의 다양한 면

더 빠른 실행

프로그램을 더 빠르게 하고 싶다면 동시성을 사용하여 여분의 프로세서를 활용하는 방법을 알아야 한다.
언뜻 생각하면 프로그램의 각 부분을 순차적으로 실행하는 것보다 동시에 병렬적으로 실행할 경우 작업 간 전환이라는 컨텍스트-스위칭까지 처리해야 하기 때문에 더 많은 부하가 걸린다고 생각할 수 있다. 하지만 Block이 발생할 경우 해당 문제가 해결되기 전까지 전체 프로그램이 멈추게 된다.
동시성을 적용한다면 하나의 작업이 정지되더라도 나머지 작업은 진행할 수 있기에 프로그램은 계속적으로 진행이 되는 것이다. Block을 고려하지 않는다면 싱글 프로세서 환경에서 동시성이 가져오는 성능상의 이점은 없다.

코드 디자인의 향상

동시성은 복잡성과 같은 비용이 들어가는 기술이다. 그러나 프로그램 디자인, 자원의 분배 그리고 사용의 편의성 등의 장점을 고려하면 이러한 비용은 지불할 만하다. 또한 스레드를 사용하면 느슨하게 연결된 코드를 디자인할 수 있다.


기본적인 스레드 기법

작업의 정의

스레드는 작업을 처리하므로, 작업을 기술하는 방법이 필요하다. Runnable 인터페이스가 바로 이 작업을 정의한다. Runnable 인터페이스를 구현하고 run() 메소드를 작성하면 된다.
class LiftOff implements Runnable {
    protected int countDown = 10;
    private static int taskCount = 0;
    private final int id = taskCount++;

    public LiftOff() {
    }

    public LiftOff(int countDown) {

        this.countDown = countDown;
    }

    @Override
    public void run() {
        while(countDown-- > 0) {
            printnb(status());
            Thread.yield();  // 중요한 작업은 끝났다는 표시
        }
    }

    private String status() {
        return "#" + id + "(" + (countDown > 0 ? countDown : "Liftoff!") + "), ";
    }
}

public class MainThread {
    public static void main(String[] args) {
        LiftOff launch = new LiftOff();
        launch.run();
    }
}
이것은 스레드 기능을 생성하지 않으므로 단순히 run() 메소드를 실행한 것에 지나지 않는다. 스레드 기능을 이용하기 위해서는 위에서 정의한 작업을 스레드에 올려야 한다.

Thread 클래스

전통적인 방법은 Thread 생성자로 작업을 넘겨 처리하는 것이다.
public class MainThread {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(new LiftOff());
            t.start();
        }
        print("Waiting for Liftoff");
    }
}
Thread 생성자는 Runnable 객체만을 필요로 한다. start() 메소드 호출은 Runnable의 run() 메소드를 호출하여 새로운 스레드에서 작업을 시작하도록 한다.
main() 메소드가 스레드 객체를 생성할 때, 생성한 객체에 대한 참조를 저장하지 않는다. 일반적인 객체라면 가비지 컬렉터에 의해 제거되겠지만, 스레드는 다르다. 각 스레드는 자체적으로 특정 위치에 참조를 등록하기 때문에 작업이 run()에서 빠져 나와 종료하기 전에는 가비지 컬렉터가 이를 제거할 수 없다.

Executor의 사용

java.util.concurrent.Executors는 스레드 객체를 관리하여 동시성 프로그래밍을 간단하게 만들어 준다.
public class MainThread {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i &lt 5; i++) {
            exec.execute(new LiftOff());
        }
        exec.shutdown();
        print("Waiting for Liftoff");
    }
}
shutdown()의 호출은 Executor가 더 이상의 작업을 할당하지 못하게 설정한다. shutdown()의 호출 이전에 Executor에 할당한 모든 작업이 완료되면 현재 스레드(이번 경우는 main() 스레드)는 종료된다.

CachedThreadPool은 다른 유형의 Executor로 대체 가능하다.
FixedThreadPool은 제한된 개수의 작업만 할당 가능하다.
SingleThreadExecutor는 크기가 1인 FixedThreadPool과 유사하다. 만약 1개 이상의 작업이 등록되면 순차적으로 처리된다.

작업의 결과 반환하기

Runnable은 return 값이 없다. return 값이 필요하다면 Runnable이 아닌 Callable 인터페이스를 사용해야 하며, 이는 run()이 아닌 call() 메소드를 가지고, call()의 반환값을 표현하는 타입 파라미터를 사용하는 제너릭이다. ExecutorService.submit() 메소드를 통해서만 실행해야 한다.
class TakeWithResult implements Callable%ltString&gt {
    private int id;
    public TakeWithResult(int id) {
        this.id = id;
    }

    @Override
    public String call() throws Exception {
        return "result of TaskWithResult " + id;
    }
}

public class CallableDemo {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        ArrayList&ltFuture&ltString&gt&gt results = new ArrayList&ltFuture&ltString&gt&gt();

        for (int i = 0; i < 10; i++) {
            results.add(exec.submit(new TakeWithResult(i)));
        }

        for (Future&ltString&gt fs : results) {
            try {
                // get()은 종료시까지 Block 된다.
                print(fs.get());
            } catch (InterruptedException e) {
            } catch (ExecutionException e) {
            } finally {
                exec.shutdown();
            }
        }
    }
}
Callable에 의해 반환되는 결과는 파라미터화 된 Future 객체다.
Future의 isDone()으로 종료 여부를 확인 할 수 있으며, 그냥 get()을 호출하면 결과가 준비될 때까지 Block된다. get()에 타임아웃을 설정할 수도 있다.

Sleeping

일정시간 작업을 멈추게 하는 Sleep()을 호출하여 변화를 줄 수 있다. yield()를 Sleep()으로 대체하면 다음과 같다
public class SleepingTask extends LiftOff {
    @Override
    public void run() {
        while(countDown-- > 0) {
            printnb(status());
            try {
                // 예전스타일
                // Thread.sleep(100);
                // 자바 SE 5/6 스타일
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
            }
        }
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            exec.execute(new SleepingTask());
        }
        exec.shutdown();
    }
}
Sleep()의 호출은 InterruptedException을 발생시킬 수 있다. main()으로 전달 되지 않기 때문에 반드시 잡아야 한다.
각 작업에서 Sleep()이 호출되기 때문에 균등하게 분산된 순서로 처리 될 수 있으나 전적으로 신뢰해서는 안된다.

우선 순위

CPU가 스레드를 실행하는 순서를 결정할 순 없지만 동일한 조건이라면 우선순위가 높은 스레드를 실행하고 낮은 스레드는 대기시킨다. 우선순위가 낮은 스레드가 실행되지 않음을 의지하는 것은 아니다.
우선순위를 조절하는 것은 큰 위험을 수반하므로 신중하게 사용해야 한다.
getPriority(), setPriority()를 사용하여 언제든 변경 가능하다.
public class SimplePriorities implements  Runnable {
    private int priority;
    private volatile double d;
    private int countDown = 5;

    public SimplePriorities(int priority) {
        this.priority = priority;
    }

    @Override
    public String toString() {
        return Thread.currentThread() + ": " + countDown;
    }

    @Override
    public void run() {
        Thread.currentThread().setPriority(priority);
        while(true) {
            for (int i = 1; i < 100000; i++) {
                d += (Math.PI + Math.E) / (double) i;
                if(i % 1000 == 0)
                    Thread.yield();
            }
            print(this);
            if(--countDown == 0) return;
        }
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        for(int i = 0; i < 5; i++) {
            exec.execute(new SimplePriorities(Thread.MIN_PRIORITY));
        }
        exec.execute(new SimplePriorities(Thread.MAX_PRIORITY));
        exec.shutdown();
    }
}
스레드는 의미없는 double형 계산을 수행하고 있다. 마지막에 실행한 스레드가 가장 높은 우선순위로 설정되었다.
결과는 아래와 같다. 높은 우선순위의 스레드가 더 선호되는 것은 확인되지만, 무조건 최우선으로 처리 되진 않았다.
Thread[pool-1-thread-3,1,main]: 5
Thread[pool-1-thread-6,10,main]: 5
Thread[pool-1-thread-5,1,main]: 5
Thread[pool-1-thread-4,1,main]: 5
Thread[pool-1-thread-3,1,main]: 4
Thread[pool-1-thread-5,1,main]: 4
Thread[pool-1-thread-6,10,main]: 4
Thread[pool-1-thread-2,1,main]: 5
Thread[pool-1-thread-1,1,main]: 5
Thread[pool-1-thread-5,1,main]: 3
Thread[pool-1-thread-3,1,main]: 3
Thread[pool-1-thread-5,1,main]: 2
Thread[pool-1-thread-3,1,main]: 2
Thread[pool-1-thread-6,10,main]: 3
Thread[pool-1-thread-4,1,main]: 4
Thread[pool-1-thread-2,1,main]: 4
Thread[pool-1-thread-1,1,main]: 4
Thread[pool-1-thread-5,1,main]: 1
Thread[pool-1-thread-3,1,main]: 1
Thread[pool-1-thread-4,1,main]: 3
Thread[pool-1-thread-6,10,main]: 2
Thread[pool-1-thread-2,1,main]: 3
Thread[pool-1-thread-1,1,main]: 3
Thread[pool-1-thread-4,1,main]: 2
Thread[pool-1-thread-6,10,main]: 1
Thread[pool-1-thread-2,1,main]: 2
Thread[pool-1-thread-1,1,main]: 2
Thread[pool-1-thread-4,1,main]: 1
Thread[pool-1-thread-2,1,main]: 1
Thread[pool-1-thread-1,1,main]: 1
JAVA에서는 10단계의 우선순위를 제공하지만 OS와 고정적으로 매핑되어 있진 않다. 이식성을 위해서 MAX_PRIORITY, MIN_PRIORITY를 권장한다.

양보하기

run() 메소드에서 작업이 완료되었다고 판단이 되면 yield() 메소드를 통해 스케줄러에게 다른 스레드에게 CPU를 할당 해도 된다고 알려 줄 수 있다.
단지 동일한 우선순위를 가지는 다른 스레드가 실행되어도 된다는 것을 알려주는 것일 뿐 이다. 튜닝이나, 민감한 제어가 yield()에 의존해서는 안된다.

데몬 스레드

데몬 스레드는 주된 기능은 아니지만 프로그램이 실행되는 동안 백그라운드로 실행되는 서비스를 제공하기 위해 사용된다.
데몬 스레드는 일반 스레드와는 다르게 비-데몬스레드가 종료되면 실행 중이더라도 모든 데몬 스레드가 종료된다.
public class SimpleDaemons implements Runnable {
    @Override
    public void run() {
        try {
            while(true) {
                TimeUnit.MILLISECONDS.sleep(500);
                print(Thread.currentThread() + " " + this);
            }
        } catch (InterruptedException e) {
            print("sleep() interrupted");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            Thread daemon = new Thread(new SimpleDaemons());
            daemon.setDaemon(true);  // start() 이전에 호출되어야 함
            daemon.start();
        }
        print("All daemons started");
        TimeUnit.MILLISECONDS.sleep(600);
    }
}
심지어 데몬 스레드의 finally 구문도 실행되지 않고 강제 종료된다.
이것은 스레드를 정상적으로 종료할 수 없다는 의미이므로 바람직한 사용은 아니다.


다양한 코딩 방법

작업을 Runnable로 구현하지 않고 Thread로부터 직접 상속받을 수도 있다.
public class SimpleThread extends Thread {
    private static int threadCount = 0;
    private int countDown = 5;
    public SimpleThread() {
        super(Integer.toString(++threadCount));
        start();
    }
    
    @Override
    public String toString() {
        return getName() + "{" + countDown + "}, ";
    }

    @Override
    public void run() {
        while(true) {
            printnb(this);
            if(--countDown == 0)
                return;
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new SimpleThread();
        }
    }
}
Thread를 상속하면 다른 클래스를 상속할 수 없다는 단점이 있지만, Runnable 인터페이스를 구현하면 그런 제한이 사라진다.
위 예제는 생성자에서 start() 메소드를 호출하고 있다. 이는 매우 단순한 예제이기 때문에 안전하지만, 생성자에서 스레드를 시작하는 것은 매우 위험하다. 생성자가 완료되기 전에 또 다른 작업이 시작되어 불완전한 객체에 접근할 수도 있기 때문이다.
class ThreadMethod {
    private int countDown = 5;
    private Thread t;
    private String name;
    public ThreadMethod(String name) {
        this.name = name;
    }
    public void runTask() {
        if(t == null) {
            t = new Thread(name) {
                public void run() {
                    try {
                        while(true) {
                            print(this);
                            if(--countDown == 0) return;
                            sleep(10);
                        }
                    } catch (InterruptedException e) {
                        print("sleep() interrupted");
                    }
                }
                public String toString() {
                    return getName() + ": " + countDown;
                }
            };
            t.start();
        }
    }
}
ThreadMethod 클래스는 메소드 안에서 스레드 생성을 보여준다.

용어 정리

실행되는 작업과 작업을 구동하는 스레드를 구별해야 한다. 일단 작업을 생성(Runnable)하고 생성한 작업을 스레드(Thread)에 탑재해야 한다. 작업과 스레드가 혼용되어 혼란을 시킬 수 있다.

조인

하나의 스레드는 다른 스레드에 대하여 join()을 호출하여 그 스레드가 종료될 때까지 대기 할 수 있다.
class Sleeper extends Thread {
    private int duration;
    public Sleeper(String name, int sleepTime) {
        super(name);
        this.duration = sleepTime;
        start();
    }
    public void run() {
        try {
            sleep(duration);
        } catch (InterruptedException e) {
            print(getName() + " was interrupted. " + "isInterrupted(): " + isInterrupted());
            return;
        }
        print(getName() + " has awakened");
    }
}

class Joiner extends Thread {
    private Sleeper sleeper;

    Joiner(String name, Sleeper sleeper) {
        super(name);
        this.sleeper = sleeper;
        start();
    }
    public void run() {
        try {
            sleeper.join();
        } catch (InterruptedException e) {
            print("Interrupted");
        }
        print(getName() + " join complete");
    }
}

public class Joining {
    public static void main(String[] args) {
        Sleeper sleepy = new Sleeper("Sleepy", 1500);
        Sleeper grumpy = new Sleeper("Grumpy", 1500);

        Joiner dopey = new Joiner("Dopey", sleepy);
        Joiner doc = new Joiner("Doc", grumpy);

        grumpy.interrupt();
    }
}
Joiner는 Sleeperj이 정상 종료되거나 인터럽트 될 때까지 대기한다. 자바 SE5에는 join()보다 더 충실한 기능을 제공하는 CyclicBarrier같은 툴도 제공한다.

사용자 인터페이스 만들기

class UnresponsiveUI {
    private volatile double d = 1;

    UnresponsiveUI() throws IOException {
        while (d > 0) {
            d = d + (Math.PI + Math.E) / d;
            System.in.read(); // 이 라인은 실행되지 않는다.
        }
    }
}
public class ResponsiveUI extends Thread {
    private static volatile double d = 1;

    public ResponsiveUI() {
        setDaemon(true);
        start();
    }
    public void run() {
        while(true) {
            d = d + (Math.PI + Math.E) / d;
        }
    }
    public static void main(String[] args) throws IOException {
        //! new UnresponsiveUI(); // kill로만 종료할 수 있다.
        new ResponsiveUI();
        System.in.read();
        print(d); // 진행상태 출력
    }
}
UnresponsiveUI는 무한 while문에서 연산을 수행하므로 콘솔 입력을 처리할 수 없다. 프로그램의 반응성을 향상시키기 위해 이와 같은 연산을 run() 메소드 안에 위치시켜 백그라운드로 실행하는 것이다.

예외 감지

스레드의 속성 때문에 스레드로부터의 예외는 감지할 수 없다. 예외가 run() 메소드를 벗어나면 이 예외를 감지하기 위한 특별한 처리를 하지 않을 경우 콘솔로까지 확대된다.
자바 SE5 이전에는 스레드 그룹을 사용했으나 이제는 Executor로 해결할 수 있다.
스레드에 처리되지 않은 예외 때문에 중단될 상황이 되면 Thread.UncaughtExceptionHandler.uncaughtException()이 자동으로 호출된다. JAVA SE5에서는 새로 제공하는 Thread.UncaughtExceptionHandler 인터페이스를 통해 이를 활용할 수 있다.
다음은 Executors에서 스레드를 생성할 때 setUncaughtExceptionHandler() 메소드를 통해 별도로 처리하고 있다.
class ExceptionThread2 implements Runnable {
    @Override
    public void run() {
        Thread t = Thread.currentThread();
        print("run() by " + t);
        print("eh = " + t.getUncaughtExceptionHandler());
        throw new RuntimeException();
    }
}
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        print("caught " + e);
    }
}
class HandlerThreadFactory implements ThreadFactory {
    @Override
    public Thread newThread(Runnable r) {
        print(this + " creating new Thread");
        Thread t = new Thread(r);
        print("created " + t);
        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        print("eh = " + t.getUncaughtExceptionHandler());
        return t;
    }
}
public class CaptureUncaughtException {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool(new HandlerThreadFactory());
        exec.execute(new ExceptionThread2());
        exec.shutdown();
    }
}
스레드에 UncaughtExceptionHandler가 없으면 defaultUncaughtExceptionHandler를 호출한다.
static 메소드인 Thread.setDefaultUncaughtExceptionHandler로 변경 가능하다.

2013년 7월 15일 월요일

중국어 입력기

개발자의 입장에서 보자면
한글도 Latin계열의 문자들에 비해, 자소들을 조합해서 음절을 만들어야 하니 어렵다고 볼 수 있겠으나,
일본어, 중국어는 그 이상이다.
일본어는 차치하고 중국어 입력기만 한 번 정리해보자

중국 본토에서 사용하느냐, 홍콩, 대만 등지에서 사용하거나에 따라 사전에 포함되어야 하는 글자다 달라진다.
또한 상당한 수의 낱자들 중에서 입력할 글자를 골라야 하기에 추천 기능이 필수적이다.

중국어의 여러 입력법들은 사용자가 입력하는 Key가 다를 뿐이지 기본 동작은 같다.
사용자가 Key를 입력하면, Key에 맞는 낱자 혹은 단어를 사전에서 찾아 목록을 만들고, 사용자는 그 중 하나를 선택해 입력하는 방식이다.
다음은 주로 사용되는 입력 방식들이다.

Pinyin 입력법

  • 병음이 검색의 Key가 되며, 병음은 영문 키보드로 입력한다.
  • 각 글자의 병음 첫 알파벳만 입력해도 해당 단어가 추천되어 입력 가능하다.
  • 输入法의 경우 sherufa로 전체 병음을 모두 입력해도 되지만 한자의 병음에 해당하는 첫 알파벳(SRF)만 입력하면 병음에 해당하는 한자가 히트된다.
  • 대부분의 중국 본토(간체 입력시)에서 사용하는 입력법이다.
  • Sogou 입력기

Cangjie 입력법

  • wiki::Cangjie input method
  • 창제 코드가 각인된 키보드를 통해 창제 코드를 입력한다.
  • 예들 들면 車를 입력하기 위해 十 田 十 를 입력해야 한다.
  • 따라서 사전에 포함된 낱자들의 창제코드가 필요해진다.
GroupKeyNamePrimary meaning
Philosophical groupA日 sun日, 曰, 90° rotated 日 (as in 巴)
B月 moonthe top four strokes of 目, 冂, 爫, 冖, the top and top-left part of 炙, 然, and 祭, the top-left four strokes of 豹 and 貓, and the top four strokes of 骨
C金 golditself, 丷, 八, and the penultimate two strokes of 四 and 匹
D木 wooditself, the first two strokes of 寸 and 才, the first two strokes of 也 and 皮
E水 water氵, the last five strokes of 暴 and 康, 又
F火 firethe shape 小, 灬, the first three strokes in 當 and 光
G土 earth
Stroke groupH竹 bambooThe slant and short slant, the Kangxi radical 竹, namely the first four strokes in 笨 and 節
I戈 weaponThe dot, the first three strokes in 床 and 庫, and the shape 厶
J十 tenThe cross shape and the shape 宀
K大 bigThe X shape, including 乂 and the first two strokes of 右, as well as 疒
L中 centreThe vertical stroke, as well as 衤 and the first four strokes of 書 and 盡
M一 oneThe horizontal stroke, as well as the final stroke of 孑 and 刁, the shape 厂, and the shape 工
N弓 bowThe crossbow and the hook
Body parts groupO人 personThe dismemberment, the Kangxi radical 人, the first two strokes of 丘 and 乓, the first two strokes of 知, 攻, and 氣, and the final two strokes of 兆
P心 heartThe Kangxi radical 忄, the second stroke in 心, the last four strokes in 恭, 慕, and 忝, the shape 匕, the shape 七, the penultimate two strokes in 代, and the shape 勹
Q手 handThe Kangxi radical 手
R口 mouthThe Kangxi radical 口
Character shapes groupS尸 corpse匚, the first two strokes of 己, the first stroke of 司 and 刀, the third stroke of 成 and 豕, the first four strokes of 長 and 髟
T廿 twentyTwo vertical strokes connected by a horizontal stroke; the Kangxi radical 艸 when written as 艹 (whether the horizontal stroke is connected or broken)
U山 mountainThree-sided enclosure with an opening on the top
V女 womanA hook to the right, a V shape, the last three strokes in 艮, 衣, and 長
W田 fieldItself, as well as any four-sided enclosure with something inside it, including the first two strokes in 母 and 毋
Y卜 fortune tellingThe 卜 shape and rotated forms, the shape 辶, the first two strokes in 斗
Collision/Difficult key*X重/難 collision/difficult(1) disambiguation of Cangjie code decomposition collisions, (2) code for a "difficult-to-decompose" part
Special character key*Z(See note)Auxiliary code used for entering special characters (no meaning on its own). In most cases, this key combined with other keys will produce Chinese punctuations (such as 。,、,「 」,『 』).
Note: Some variants use Z as a collision key instead of X. In those systems, Z has the name "collision" (重) and X has the name "difficult" (難); but the use of Z as a collision key is neither in the original Cangjie nor used in the current mainstream implementations. In other variants, Z may have the name "user-defined" (造) or some other name.

Zhuyin 입력법

  • Bopomofo 입력법이라고도 한다.
  • 법체 입력시 사용되는 입력법이다.
  • 주음 기호가 각인된 키보드를 통해 발음을 입력한다.
  • 맞지 않는 주음 기호 조합인 경우 입력이 사전에 차단된다.
  • wiki::Zhuyin table로 주음 기호를 병음기호로 변경가능 해 보인다.
  • Pynyin입력 모듈을 재사용 할 수 있겠다.

획 입력법

  • 필기구로 글자를 쓸 때처럼 5가지의 획을 순서대로 입력한다.
  • 해당 글자의 모든 획을 다 입력하지 않아도 글자는 추천된다.
  • 순서대로 입력하지 않으면 추천되지 않는다.
  • 5 종류의 획을 key가 아닌 drawing으로 입력 받으면 handwriting으로 보여질 것 같다.

2013년 7월 9일 화요일

옵저버 패턴

옵저버 패턴에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고
자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의합니다.
신문 구독으로 간단한 예를들 수 있다.
신문사는 새로운 신문을 발행한다. 신문을 구독 중인 고객들에게는 새 신문이 배달되고, 고객들은 자동으로 구독해지, 신청을 언제든지 할 수 있다.
신문사는 구독 중인 고객들에게 신문을 배달하는 순서는 신경쓰지 않는다.

간단한 기상 스테이션을 Observer 패턴으로 구현해보자.
다음과 같이 subject, observer 인터페이스를 만들어 간단히 구현 가능하다.

public interface Subject {
    public void registerObserver(Observer o);
    public void removeObserver(Observer o);
    public void notifyObservers();
}

public interface Observer {
    public void update(float temp, float humidity, float pressure);
}

public interface DisplayElement {
    public void display();
}
신문사에 해당하는 Subject 인터페이스와 고객인 Observer 인터페이스, 기상 정보를 표현할 DisplayElement 인터페이스로 구성되어 있다.
public class WeatherData implements Subject {
    private final ArrayList<Observer> observers;
    private float pressure;
    private float humidity;
    private float temperature;

    public WeatherData() {
        this.observers = new ArrayList<Observer>();
    }

    public void setMeasurements(int temperature, int humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    private void measurementsChanged() {
        notifyObservers();
    }

    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        observers.remove(o);
    }

    @Override
    public void notifyObservers() {
        for(Observer o : observers) {
            o.update(this.temperature, this.humidity, this.pressure);
        }
    }
}

public class CurrentConditionsDisplay implements Observer, DisplayElement {
    private final Subject weatherData;
    private float humidity;
    private float temperature;

    public CurrentConditionsDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    @Override
    public void display() {
        System.out.println("Current conditions: " + this.temperature + "F degrees and " + this.humidity + "% humidity");
    }

    @Override
    public void update(float temp, float humidity, float pressure) {
        this.temperature = temp;
        this.humidity = humidity;
        display();
    }
}

WeatherData 클래스는 Observer들을 관리할 Observer 리스트를 가지고 있으며, registerObserver를 통해 등록된 Observer들에게 기상정보가 변경될 때 update메세지를 보낸다.

Observer인 CurrentConditionsDisplay 클래스는 weatherData의 registerObsever를 통해 Subject 클래스로 자신의 참조를 전달하고, Subject에서 update를 호줄하면 예정된 동작을 실행하게 된다.
현재 클래스에서는 생성자에서 register를 했으나 원할 때 remove, register 가능하다.

자바에서 Observer 패턴을 위한 기능이 있다.

java.util.Observable;
java.util.Observer;
위의 WeatherData와 CurrentConditionsDisplay를 아래와 같이 변경가능하다.
public class WeatherData extends Observable {
    private float pressure;
    private float humidity;
    private float temperature;

    public void setMeasurements(int temperature, int humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {

        return humidity;
    }

    public float getPressure() {

        return pressure;
    }

    private void measurementsChanged() {
        setChanged();
        notifyObservers();
    }
}

public class CurrentConditionsDisplay implements Observer, DisplayElement {
    private final Observable weatherData;
    private float humidity;
    private float temperature;

    public CurrentConditionsDisplay(Observable weatherData) {
        this.weatherData = weatherData;
        weatherData.addObserver(this);
    }

    @Override
    public void display() {
        System.out.println("Current conditions: " + this.temperature + "F degrees and " + this.humidity + "% humidity");
    }

    @Override
    public void update(Observable o, Object arg) {
        if (o instanceof WeatherData) {
            WeatherData weatherData = (WeatherData) o;
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            display();
        }
    }
}

주의할 점은 Observable이 인터페이스가 아니라 Class라는 점이다. 따라서 Observer를 위한 자료구조를 만들 필요가 없으며, addObserver/deleteObserver 등의 메소드가 이미 구현되어 있다.
implements가 아니라 extends해야 하기 때문에 사용에 제약이 있을 수 있다.
setChanged()를 호출한 후 notifyObservers()를 호출하면 등록된 Observer들의 update가 호출된다.

Observer들은 update(Observable, Object)를 통해 메세지를 받는다.
Observable은 update를 호출한 객체를 전달하고, Object에는 Observable에서 notifyObservers(Object)로 넘겨준 param이 전달된다.(notifyObservers()는 notifyObservers(null)과 같다.)
위 코드에서 처럼 Observable의 getter를 통해 데이터를 가져와도 되고(pull), notifyObservers(Object)로 넘겨줘도 된다.(push)


참고도서 :

2013년 7월 3일 수요일

PrivateCommand란?

reference를 보자
http://developer.android.com/reference/android/view/inputmethod/InputMethodSession.html

public abstract void appPrivateCommand (String action, Bundle data)
Process a private command sent from the application to the input method. This can be used to provide domain-specific features that are only known between certain input methods and their clients.
어플리케이션에서 InputMethod로 보내는 private command이다. action에는 command의 이름을 넣고 Bundle에 데이터를 넣어 전달한다. action은 단지 String일 뿐이므로 conflicting에 주의해서 만들도록 한다.
InputMethod에서는 InputMethodService.onAppPrivateCommand(String, Bundle)를 통해 받을 수 있다.

반대로 InputMethod에서 editor로 private command를 보낼 때는 InputConnection의

public abstract boolean performPrivateCommand (String action, Bundle data)
를 사용한다.

Bundle(A mapping from String values to various Parcelable types.)은 대부분의 타입들을 지원하며, key를 통해 put/get을 사용한다.

PendingIntent란?

다음 블로그에 정리가 잘 되어 있다.
http://huewu.blog.me/110084228131
간략히 하면,
Intent를 직접 보내지 않고 다른 클래스에게 Intent를 위임해주기 위한 클래스 정도 되겠다.
Notification Bar와 상호작용하는 어플리케이션을 작성할 때 널리 사용된다.

http://developer.android.com/reference/android/app/PendingIntent.html를 보면
A description of an Intent and target action to perform with it.라고 한다.
다음의 메소드들로 PendingIntent의 instance를 만들 수 있다.

LatinIME에서 다음과 같은 코드가 발견된다.

    /**
     * Arrange for the UploaderService to be run on a regular basis.
     *
     * Any existing scheduled invocation of UploaderService is removed and rescheduled.  This may
     * cause problems if this method is called often and frequent updates are required, but since
     * the user will likely be sleeping at some point, if the interval is less that the expected
     * sleep duration and this method is not called during that time, the service should be invoked
     * at some point.
     */
    public static void scheduleUploadingService(Context context) {
        final Intent intent = new Intent(context, UploaderService.class);
        final PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
        final AlarmManager manager =
                (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        manager.cancel(pendingIntent);
        manager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent);
    }

getService를 통해 PendingIntent의 instance를 만들고 AlarmManager에 등록한다.
각 method의 parameters를 간단히 살펴보자

public static PendingIntent getService (Context context, int requestCode, Intent intent, int flags)
  • context : The Context in which this PendingIntent should start the service.
  • requestCode : Private request code for the sender (currently not used).
  • intent : An Intent describing the service to be started.
  • flags : May be FLAG_ONE_SHOT, FLAG_NO_CREATE, FLAG_CANCEL_CURRENT, FLAG_UPDATE_CURRENT, or any of the flags as supported by Intent.fillIn() to control which unspecified parts of the intent that can be supplied when the actual send happens.
public void setInexactRepeating (int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)
  • type : One of ELAPSED_REALTIME, ELAPSED_REALTIME_WAKEUP}, RTC or RTC_WAKEUP.
  • triggerAtMillis : time in milliseconds that the alarm should first go off, using the appropriate clock (depending on the alarm type). This is inexact: the alarm will not fire before this time, but there may be a delay of almost an entire alarm interval before the first invocation of the alarm.
  • intervalMillis : interval in milliseconds between subsequent repeats of the alarm. If this is one of INTERVAL_FIFTEEN_MINUTES, INTERVAL_HALF_HOUR, INTERVAL_HOUR, INTERVAL_HALF_DAY, or INTERVAL_DAY then the alarm will be phase-aligned with other alarms to reduce the number of wakeups. Otherwise, the alarm will be set as though the application had called setRepeating(int, long, long, PendingIntent).
  • operation : Action to perform when the alarm goes off; typically comes from IntentSender.getBroadcast().

2013년 7월 2일 화요일

Content Providers 정리

http://developer.android.com/guide/topics/providers/content-provider-basics.html
페이지를 보면서 아래에 내용을 요약 정리 해 둔다.

Content Provider Basics

Together, providers and provider clients offer a consistent, standard interface to data that also handles inter-process communication and secure data access.

Overview


A content provider presents data to external applications as one or more tables that are similar to the tables found in a relational database. A row represents an instance of some type of data the provider collects, and each column in the row represents an individual piece of data collected for an instance.

Accessing a provider

An application accesses the data from a content provider with a ContentResolver client object. This object has methods that call identically-named methods in the provider object, an instance of one of the concrete subclasses of ContentProvider. The ContentResolver methods provide the basic "CRUD" (create, retrieve, update, and delete) functions of persistent storage.
Note:To access a provider, your application usually has to request specific permissions in its manifest file. This is described in more detail in the section Content Provider Permissions

Content URIs

A content URI is a URI that identifies data in a provider. Content URIs include the symbolic name of the entire provider (its authority) and a name that points to a table (a path). When you call a client method to access a table in a provider, the content URI for the table is one of the arguments.
content://user_dictionary/words

Retrieving Data from the Provider


Requesting read access permission

To retrieve data from a provider, your application needs read access permission for the provider. You can't request this permission at run-time; instead, you have to specify that you need this permission in your manifest, using the element and the exact permission name defined by the provider.

Constructing the query

In the next snippet, if the user doesn't enter a word, the selection clause is set to null, and the query returns all the words in the provider. If the user enters a word, the selection clause is set to UserDictionary.Words.WORD + " = ?" and the first element of selection arguments array is set to the word the user enters.
/*
 * This defines a one-element String array to contain the selection argument.
 */
String[] mSelectionArgs = {""};

// Gets a word from the UI
mSearchString = mSearchWord.getText().toString();

// Remember to insert code here to check for invalid or malicious input.

// If the word is the empty string, gets everything
if (TextUtils.isEmpty(mSearchString)) {
    // Setting the selection clause to null will return all words
    mSelectionClause = null;
    mSelectionArgs[0] = "";

} else {
    // Constructs a selection clause that matches the word that the user entered.
    mSelectionClause = UserDictionary.Words.WORD + " = ?";

    // Moves the user's input string to the selection arguments.
    mSelectionArgs[0] = mSearchString;

}

// Does a query against the table and returns a Cursor object
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,  // The content URI of the words table
    mProjection,                       // The columns to return for each row
    mSelectionClause                   // Either null, or the word the user entered
    mSelectionArgs,                    // Either empty, or the string the user entered
    mSortOrder);                       // The sort order for the returned rows

// Some providers return null if an error occurs, others throw an exception
if (null == mCursor) {
    /*
     * Insert code here to handle the error. Be sure not to use the cursor! You may want to
     * call android.util.Log.e() to log this error.
     *
     */
// If the Cursor is empty, the provider found no matches
} else if (mCursor.getCount() < 1) {

    /*
     * Insert code here to notify the user that the search was unsuccessful. This isn't necessarily
     * an error. You may want to offer the user the option to insert a new row, or re-type the
     * search term.
     */

} else {
    // Insert code here to do something with the results

}
This query is analogous to the SQL statement:
SELECT _ID, word, locale FROM words WHERE word = &ltuserinput&gt ORDER BY word ASC;

Protecting against malicious input

If the data managed by the content provider is in an SQL database, including external untrusted data into raw SQL statements can lead to SQL injection.
To avoid this problem, use a selection clause that uses ? as a replaceable parameter and a separate array of selection arguments. When you do this, the user input is bound directly to the query rather than being interpreted as part of an SQL statement. Because it's not treated as SQL, the user input can't inject malicious SQL.

Displaying query results

The ContentResolver.query() client method always returns a Cursor containing the columns specified by the query's projection for the rows that match the query's selection criteria. A Cursor object provides random read access to the rows and columns it contains. Using Cursor methods, you can iterate over the rows in the results, determine the data type of each column, get the data out of a column, and examine other properties of the results. Some Cursor implementations automatically update the object when the provider's data changes, or trigger methods in an observer object when the Cursor changes, or both.
The following snippet continues the code from the previous snippet. It creates a SimpleCursorAdapter object containing the Cursor retrieved by the query, and sets this object to be the adapter for a ListView:
// Defines a list of columns to retrieve from the Cursor and load into an output row
String[] mWordListColumns =
{
    UserDictionary.Words.WORD,   // Contract class constant containing the word column name
    UserDictionary.Words.LOCALE  // Contract class constant containing the locale column name
};

// Defines a list of View IDs that will receive the Cursor columns for each row
int[] mWordListItems = { R.id.dictWord, R.id.locale};

// Creates a new SimpleCursorAdapter
mCursorAdapter = new SimpleCursorAdapter(
    getApplicationContext(),               // The application's Context object
    R.layout.wordlistrow,                  // A layout in XML for one row in the ListView
    mCursor,                               // The result from the query
    mWordListColumns,                      // A string array of column names in the cursor
    mWordListItems,                        // An integer array of view IDs in the row layout
    0);                                    // Flags (usually none are needed)

// Sets the adapter for the ListView
mWordList.setAdapter(mCursorAdapter);

Getting data from query results

// Determine the column index of the column named "word"
int index = mCursor.getColumnIndex(UserDictionary.Words.WORD);

/*
 * Only executes if the cursor is valid. The User Dictionary Provider returns null if
 * an internal error occurs. Other providers may throw an Exception instead of returning null.
 */

if (mCursor != null) {
    /*
     * Moves to the next row in the cursor. Before the first movement in the cursor, the
     * "row pointer" is -1, and if you try to retrieve data at that position you will get an
     * exception.
     */
    while (mCursor.moveToNext()) {

        // Gets the value from the column.
        newWord = mCursor.getString(index);

        // Insert code here to process the retrieved word.

        ...

        // end of while loop
    }
} else {

    // Insert code here to report an error if the cursor is null or the provider threw an exception.
}
Cursor implementations contain several "get" methods for retrieving different types of data from the object. For example, the previous snippet uses getString(). They also have a getType() method that returns a value indicating the data type of the column.

Content Provider Permissions


To get the permissions needed to access a provider, an application requests them with a element in its manifest file. When the Android Package Manager installs the application, a user must approve all of the permissions the application requests. If the user approves all of them, Package Manager continues the installation; if the user doesn't approve them, Package Manager aborts the installation.

Inserting, Updating, and Deleting Data


Inserting data

To insert data into a provider, you call the ContentResolver.insert() method. This method inserts a new row into the provider and returns a content URI for that row. This snippet shows how to insert a new word into the User Dictionary Provider:
// Defines a new Uri object that receives the result of the insertion
Uri mNewUri;

...

// Defines an object to contain the new values to insert
ContentValues mNewValues = new ContentValues();

/*
 * Sets the values of each column and inserts the word. The arguments to the "put"
 * method are "column name" and "value"
 */
mNewValues.put(UserDictionary.Words.APP_ID, "example.user");
mNewValues.put(UserDictionary.Words.LOCALE, "en_US");
mNewValues.put(UserDictionary.Words.WORD, "insert");
mNewValues.put(UserDictionary.Words.FREQUENCY, "100");

mNewUri = getContentResolver().insert(
    UserDictionary.Word.CONTENT_URI,   // the user dictionary content URI
    mNewValues                          // the values to insert
);
The data for the new row goes into a single ContentValues object, which is similar in form to a one-row cursor. The columns in this object don't need to have the same data type, and if you don't want to specify a value at all, you can set a column to null using ContentValues.putNull().
The snippet doesn't add the _ID column, because this column is maintained automatically. The provider assigns a unique value of _ID to every row that is added. Providers usually use this value as the table's primary key.
The content URI returned in newUri identifies the newly-added row, with the following format:
content://user_dictionary/words/&ltid_value&gt

Updating data

To update a row, you use a ContentValues object with the updated values just as you do with an insertion, and selection criteria just as you do with a query. The client method you use is ContentResolver.update(). You only need to add values to the ContentValues object for columns you're updating. If you want to clear the contents of a column, set the value to null.
The following snippet changes all the rows whose locale has the language "en" to a have a locale of null. The return value is the number of rows that were updated:
// Defines an object to contain the updated values
ContentValues mUpdateValues = new ContentValues();

// Defines selection criteria for the rows you want to update
String mSelectionClause = UserDictionary.Words.LOCALE +  "LIKE ?";
String[] mSelectionArgs = {"en_%"};

// Defines a variable to contain the number of updated rows
int mRowsUpdated = 0;

...

/*
 * Sets the updated value and updates the selected words.
 */
mUpdateValues.putNull(UserDictionary.Words.LOCALE);

mRowsUpdated = getContentResolver().update(
    UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
    mUpdateValues                       // the columns to update
    mSelectionClause                    // the column to select on
    mSelectionArgs                      // the value to compare to
);

Deleting data

Deleting rows is similar to retrieving row data: you specify selection criteria for the rows you want to delete and the client method returns the number of deleted rows. The following snippet deletes rows whose appid matches "user". The method returns the number of deleted rows.
// Defines selection criteria for the rows you want to delete
String mSelectionClause = UserDictionary.Words.APP_ID + " LIKE ?";
String[] mSelectionArgs = {"user"};

// Defines a variable to contain the number of rows deleted
int mRowsDeleted = 0;

...

// Deletes the words that match the selection criteria
mRowsDeleted = getContentResolver().delete(
    UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
    mSelectionClause                    // the column to select on
    mSelectionArgs                      // the value to compare to
);

Provider Data Types

Content providers can offer many different data types. The User Dictionary Provider offers only text, but providers can also offer the following formats:

  • integer
  • long integer (long)
  • floating point
  • long floating point (double)

Another data type that providers often use is Binary Large OBject (BLOB) implemented as a 64KB byte array. You can see the available data types by looking at the Cursor class "get" methods.

Alternative Forms of Provider Access

Batch access

Batch access to a provider is useful for inserting a large number of rows, or for inserting rows in multiple tables in the same method call, or in general for performing a set of operations across process boundaries as a transaction (an atomic operation).

To access a provider in "batch mode", you create an array of ContentProviderOperation objects and then dispatch them to a content provider with ContentResolver.applyBatch(). You pass the content provider's authority to this method, rather than a particular content URI. This allows each ContentProviderOperation object in the array to work against a different table. A call to ContentResolver.applyBatch() returns an array of results.

        // Prepare contact creation request
        //
        // Note: We use RawContacts because this data must be associated with a particular account.
        //       The system will aggregate this with any other data for this contact and create a
        //       coresponding entry in the ContactsContract.Contacts provider for us.
        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
        ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
                .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, mSelectedAccount.getType())
                .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, mSelectedAccount.getName())
                .build());
        ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
                .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
                .withValue(ContactsContract.Data.MIMETYPE,
                        ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
                .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name)
                .build());
        ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
                .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
                .withValue(ContactsContract.Data.MIMETYPE,
                        ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
                .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
                .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, phoneType)
                .build());
        ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
                .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
                .withValue(ContactsContract.Data.MIMETYPE,
                        ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
                .withValue(ContactsContract.CommonDataKinds.Email.DATA, email)
                .withValue(ContactsContract.CommonDataKinds.Email.TYPE, emailType)
                .build());

        // Ask the Contact provider to create a new contact
        Log.i(TAG,"Selected account: " + mSelectedAccount.getName() + " (" +
                mSelectedAccount.getType() + ")");
        Log.i(TAG,"Creating contact: " + name);
        try {
            getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
        } catch (Exception e) {
            // Display warning
            Context ctx = getApplicationContext();
            CharSequence txt = getString(R.string.contactCreationFailure);
            int duration = Toast.LENGTH_SHORT;
            Toast toast = Toast.makeText(ctx, txt, duration);
            toast.show();

            // Log exception
            Log.e(TAG, "Exceptoin encoutered while inserting contact: " + e);
        }

Asynchronous queries

You should do queries in a separate thread. One way to do this is to use a CursorLoader object. The examples in the Loaders guide demonstrate how to do this

Data access via intents

Although you can't send an intent directly to a provider, you can send an intent to the provider's application, which is usually the best-equipped to modify the provider's data.

Getting access with temporary permissions

You can access data in a content provider, even if you don't have the proper access permissions, by sending an intent to an application that does have the permissions and receiving back a result intent containing "URI" permissions. These are permissions for a specific content URI that last until the activity that receives them is finished. The application that has permanent permissions grants temporary permissions by setting a flag in the result intent:

  • Read permission: FLAG_GRANT_READ_URI_PERMISSION
  • Write permission: FLAG_GRANT_WRITE_URI_PERMISSION

For example, you can retrieve data for a contact in the Contacts Provider, even if you don't have the READ_CONTACTS permission. You might want to do this in an application that sends e-greetings to a contact on his or her birthday. Instead of requesting READ_CONTACTS, which gives you access to all of the user's contacts and all of their information, you prefer to let the user control which contacts are used by your application. To do this, you use the following process:

  1. Your application sends an intent containing the action ACTION_PICK and the "contacts" MIME type CONTENT_ITEM_TYPE, using the method startActivityForResult().
  2. Because this intent matches the intent filter for the People app's "selection" activity, the activity will come to the foreground.
  3. In the selection activity, the user selects a contact to update. When this happens, the selection activity calls setResult(resultcode, intent) to set up a intent to give back to your application. The intent contains the content URI of the contact the user selected, and the "extras" flags FLAG_GRANT_READ_URI_PERMISSION. These flags grant URI permission to your app to read data for the contact pointed to by the content URI. The selection activity then calls finish() to return control to your application.
  4. Your activity returns to the foreground, and the system calls your activity's onActivityResult() method. This method receives the result intent created by the selection activity in the People app.
  5. With the content URI from the result intent, you can read the contact's data from the Contacts Provider, even though you didn't request permanent read access permission to the provider in your manifest. You can then get the contact's birthday information or his or her email address and then send the e-greeting.

Using another application

A simple way to allow the user to modify data to which you don't have access permissions is to activate an application that has permissions and let the user do the work there.

For example, the Calendar application accepts an ACTION_INSERT intent, which allows you to activate the application's insert UI. You can pass "extras" data in this intent, which the application uses to pre-populate the UI. Because recurring events have a complex syntax, the preferred way of inserting events into the Calendar Provider is to activate the Calendar app with an ACTION_INSERT and then let the user insert the event there.

Contract Classes


A contract class defines constants that help applications work with the content URIs, column names, intent actions, and other features of a content provider. Contract classes are not included automatically with a provider; the provider's developer has to define them and then make them available to other developers. Many of the providers included with the Android platform have corresponding contract classes in the package android.provider.

MIME Type Reference


Content providers can return standard MIME media types, or custom MIME type strings, or both.

For example, the well-known MIME type text/html has the text type and the html subtype. If the provider returns this type for a URI, it means that a query using that URI will return text containing HTML tags.

Custom MIME type strings, also called "vendor-specific" MIME types, have more complex type and subtype values. The type value is always

vnd.android.cursor.dir

for multiple rows, or

vnd.android.cursor.item

for a single row.

2013년 6월 26일 수요일

Context

안드로이드 레퍼런스의 설명만으로 정리가 되어 버린다.
Interface to global information about an application environment.
This is an abstract class whose implementation is provided by the Android system. It allows access to application-specific resources and classes, as well as up-calls for application-level operations such as launching activities, broadcasting and receiving intents, etc.
왜 진작 찾아 보지 않았는지.. Activity든 Service든 Context를 간접적으로 상속받고 있다.

ContentObserver

안드로이드 레퍼런스에서는 다음과 같이 설명한다.
Receives call backs for changes to content. Must be implemented by objects which are added to a ContentObservable.
Latin IME의 소스에서 사용되는 부분을 발췌하면 다음과 같다.
com.android.inputmethod.latin.UserBinaryDictionary의 생성자에서 아래 코드를 발견할 수 있다.
        ContentResolver cres = context.getContentResolver();

        mObserver = new ContentObserver(null) {
            @Override
            public void onChange(final boolean self) {
                // This hook is deprecated as of API level 16, but should still be supported for
                // cases where the IME is running on an older version of the platform.
                onChange(self, null);
            }
            // The following hook is only available as of API level 16, and as such it will only
            // work on JellyBean+ devices. On older versions of the platform, the hook
            // above will be called instead.
            @Override
            public void onChange(final boolean self, final Uri uri) {
                setRequiresReload(true);
                // We want to report back to Latin IME in case the user just entered the word.
                // If the user changed the word in the dialog box, then we want to replace
                // what was entered in the text field.
                if (null == uri || !(context instanceof LatinIME)) return;
                final long changedRowId = ContentUris.parseId(uri);
                if (-1 == changedRowId) return; // Unknown content... Not sure why we're here
                final String changedWord = getChangedWordForUri(uri);
                ((LatinIME)context).onWordAddedToUserDictionary(changedWord);
            }
        };
        cres.registerContentObserver(Words.CONTENT_URI, true, mObserver);
이코드만 보면, getContentResolver에서 ContentResolver를 가져와서 Observer를 등록하면 된다.
종료 때는 위 소스에는 빠져있지만 unregisterContentObserver를 사용해서 해제하면 된다.
전혀 복잡할 내용도 없고 간결한데 뭔가 답답한 이유는,
context, Uri 에 대한 개념이 부족해서이리라.

contextUri를 좀 더 확인 후 여기에도 정리해 두겠다.


== 추가 내용 ==
Uri는 특별한게 없고, 예상하던 Uniform Resource Identifiers가 맞다.
어렵게 생각할 필요 없었다.
Words.CONTENT_URI는 android.jar에 포함되어 있었다. 이 값이 궁금해 진행이 늦었다.
latin IME소스에 있는 줄 알고 계속 Find usages 호출해서 헤메고 있었는데. Navigation bar를 잘 봤어야지..
어쟀든 값은
public static final Uri CONTENT_URI = Uri.parse("content://user_dictionary/words");