并发笔记二 Java并行程序基础(二)

2021/11/11

# Volatile 与 Java 内存模型(JMM)

  • 当用 volatile 去申明一个变量时,就等于告诉虚拟机,这个变量极有可能会被某些程序或线程修改。

  • 但是 volatile 不能代替锁,也不能保证复合操作的原子性

  • volatile 可以保证数据的可见性和有序性,见[示例代码](#volatile 保证可见性和有序性)。 同时 JDK9 中新加入了 Thread.onSpinWait() 方法。指示调用方暂时无法进行,直到其他活动发生一个或多个操作。运行时可以采取措施来提高调用自旋等待循环构造的性能。

# 线程组

一个线程组核心的信息是:名称、线程最大优先级、是否守护、父线程组、子线程组

# 守护线程(Daemon)

public final void setDaemon(boolean on)
1

setDaemon() 必须要再 start() 方法执行之前设置

否则会抛出 IllegalThreadStateException 异常。但是程序依然可以进行,只是会被当做用户线程而已。

如果应用内只有守护线程,JVM会自然退出。

# 线程优先级

Thread 类中定义了3个值。10为最高。

public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;
1
2
3

高优先级线程在竞争资源时会更有优势。但是不保证一定会竞争到。

# 线程安全与 synchronized

  • volatile 并不能保证线程安全。只能确保一个线程修改数据之后,其他线程能看到这个改动。但是当两个线程同时修改某个数据时,依然会产生冲突。

  • synchronized 作用是实现线程间同步。它会对同步的代码加锁,试得每次只能有一个线程进入同步块,从而保证线程安全。

  • synchronized 作用在实例方法上,表示进入该方法前必须拿到实例对象的锁

  • synchronized 作用在静态方法上,表示进入该方法前必须拿到当前类的锁

  • synchronized 可以保证线程安全和线程间可见性。完全可以替代 volatile 的功能。

  • synchronized 也可以保证有序性,无论同步块内如何被乱序执行,只要保证串行语义一致,结果总是一样的。其他线程在拿到锁之前是无法进入的,只能看到最终结果,而非执行过程。(synchronized 限制的多个线程是串行执行的

# 程序中隐蔽的错误

# 并发下的 ArrayList

ArrayList 是一个线程不安全的容器。

  • 因为 ArrayList 扩容时,内部一致性被破坏,两个线程访问到了不一致的内部状态,导致越界。

  • 因为保存容器大小的变量被多线程不正常访问,可能导致两个线程对同一个位置进行赋值。

# 并发下的 HashMap

JDK1.7 下的 HashMap 很容易出现死循环的问题。

具体原因这里不讨论。(后期再研究一下,涉及到 HashMap 的存储和扩容)

最佳办法是使用 ConcurrentHashMap 代替 HashMap。

# 错误的加锁:Integer

详细代码见[参考代码](#Integer 类加锁的问题)

如果定义 Integer 类型变量 i, 并使用 i++。

Integer i = 0;

synchronized (i) {
    i++;
}
1
2
3
4
5

实际上 i++ 执行时变成了:

i = Integer.valueOf(i.intValue() + 1);
1

进一步的看 Integer.valueOf 方法:

// JDK9 中新增 @HotSpotIntrinsicCandidate 注解,
// JDK16 中重名为 @IntrinsicCandidate
@IntrinsicCandidate
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
1
2
3
4
5
6
7
8

Integer.valueOf 其实是一个工厂方法,倾向于返回一个代表指定数值的 Integer 对象实例。

因此 i++ 实际上是创建一个新的对象,并将其引用值赋值给 i 。

因此 synchronized (i) 可能导致两个线程加锁在不同的对象上,导致对临界区代码控制出现问题。

# 代码示例

# volatile 保证可见性和有序性

在 JVM 的 server 模式下,子线程无法打印,因为无法“看到”主线程对于ready的修改。

添加 -server JVM 参数可以让 JVM 使用 server 模式。

public class NoVisibility {
//    volatile 可以解决问题
//    private static volatile boolean ready;
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        @Override
        public void run() {
            while (!ready) {
// onSpinWait() 是 JDK9 新加入的方法,指示调用方暂时无法进行,
// 直到其他活动发生一个或多个操作。运行时可以采取措施来提高调用自旋等待循环构造的性能。
//                Thread.onSpinWait();
                ;
            }
            System.out.println(number);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ReaderThread().start();
        Thread.sleep(1000);
        number = 42;
        ready = true;
        Thread.sleep(10000);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# Integer 类加锁的问题

预期返回 20 000 000, 但是实际并没有达到。

public class BadLockOnInteger implements Runnable {
    public static Integer i = 0;
    static BadLockOnInteger instance = new BadLockOnInteger();

    @Override
    public void run() {
        for (int j = 0; j < 10000000; j++) {
// 					IDEA 会在这个 i 上提示:
// 					Attempt to synchronize on an instance of a value-based class
            synchronized (i) {
                i++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
//        输出 14954114
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Last Updated: 2021/12/7 下午2:23:53