首页 > 代码库 > Java 线程与锁

Java 线程与锁

Synchronization

synchronized语法可以获取锁, 当其他线程持有锁的时候该线程想要获取锁将会进入等待状态, 直到没有其他线程持有该锁

显示使用 synchronized (lock) 将会获取lock对象的锁

没有显示指定锁对象将会获取当前类的class对象的锁

 

Wait and Notification

每个对象m都有一个wait set, 调用该对象的wait方法的线程会被加入到wait set

1. Wait

调用某个对象m的wait方法会使调用线程t进入等待状态, 他有如下几个派生方法

wait(): 无限期等待, 直到被wait对象被notify或notifyAll

wait(long timeout): 等待timeout毫秒, 或被notify

wait(long timeout, int nanos): 等待 1000000*timeout+nanos 纳秒

调用lock.wait()必须持有lock的monitor(锁), 否则会抛出IllegalMonitorStateException, 例如

    private final static Object lock = new Object();
    
    public static void main(String[] args) throws InterruptedException {
    
        synchronized (lock) { // 该线程必须持有锁
            while (condition not hold) {
                lock.wait();
            }
            // logic code
        }
        
    }

线程t会在如下情况从对象的wait set移除:

1) 在t被选为从wait set中移除的线程的时候, 调用m的notify方法

2) 调用m的notifyAll方法

3) t被中断

4) wait的时间到达指定值

*: 一般来说wait都是放在一个循环里, 这个循环会一直判断wait()的条件是否成立, 这样就不会在多线程情况下一瞬间的条件不成立让线程继续往下走

 

2. Notification

调用m的notify方法的线程t必须持有m的monitor, 否则抛出IllegalMonitorStateException

1) notify(): 随机唤醒m的wait set中的其中一个线程t1, 并将t1从wait set中移除

2) notifyAll(): 唤醒所有m的wait set中的所有线程并移除

 

3. Interruptions

调用线程t.interrupt()会将t的 interruption status 设置为 true

如果 t 在 m 的 wait set 里, 调用 t.interrupt() 会将 t 从 m 的 wait set 里移除, 然后wait()方法会抛出InterruptedException, 并将 interruption status 清除.

 

4. Waits, Notification, Interruption的交互

如果一个线程处于waiting状态, 但是同时被notify和interrupt 它要么:

a. 如果notify在前, 则正常唤醒, 并调用线程的interrupt, interrupt标志位置为true

b. 如果interrupt在前, 则t抛出InterruptedException, 然后另一个线程u将被唤醒并从wait set移除

 

Sleep and Yield

yeild 会让当前执行线程暂停, 并让同优先级的线程执行.

sleep 指定某个线程的暂停一定的时间

yeild和sleep都不需要获得锁, 也不能被唤醒

调用 sleep 和 yield 不会刷新寄存器缓存到共享内存中, 也不会让寄存器缓存重载.

while (!this.done)
    Thread.sleep(1000);

所以如果 this.done 如果不是volatile的, 则有可能使得循环内一直使用相同的this.done值, 这样循环可能永远停不下来, 即使别的线程修改了this.done的值

 

Memory Model

没有正确同步的程序

由于Java编译器和微处理器会对程序进行优化, 如果不进行正确的同步操作, 可能会出现很诡异的行为.

例如, 以下程序使用本地变量 r1, r2, 并共享变量 A B, 最初 A == B == 0

Thread 1 Thread 2
1: r2 = A; 3: r1 = B;
2: B = 1; 4: A = 2;

可能会觉得 r2 == 2, r1 == 1 并不可能. 从直觉上来看, 指令1先执行, r2则看不到指令4中A的变化, 指令3先执行, r1则看不到指令2中B的变化.

然而, 由于编译器的优化, 很有可能会打乱执行顺序, 造成最后的结果是r2 == 2, r1 == 1, 如下

Thread 1 Thread 2
B = 1; r1 = B;
r2 = A; A = 2;

像这种程序在多线程中是不正确的:

1. 在一个线程中有写共享变量操作

2. 在另一个线程中读这个共享变量

3. 读和写的顺序没有被同步控制

这种情况叫做数据竞争(data race), 如果代码里出现数据竞争, 则有可能出现反直觉的结果.

另一个例子如下, 最初 p == q 并且 p.x == 0. 这个程序也是不正确的, 因为它在不同的线程中读写共享变量, 并且不适用同步控制

Thread 1 Thread 2
r1 = p; r6 = p;
r2 = r1.x; r6.x = 3;
r3 = q;  
r4 = r3.x;  
r5 = r1.x;  

这种情况下, 编译器可能将r2的值复用优化给r5, 变为如下

Thread 1 Thread 2
r1 = p; r6 = p;
r2 = r1.x; r6.x = 3;
r3 = q;  
r4 = r3.x;  
r5 = r2;