并发笔记二 Java并行程序基础(二)
# Volatile 与 Java 内存模型(JMM)
当用 volatile 去申明一个变量时,就等于告诉虚拟机,这个变量极有可能会被某些程序或线程修改。
但是 volatile 不能代替锁,也不能保证复合操作的原子性。
volatile 可以保证数据的可见性和有序性,见[示例代码](#volatile 保证可见性和有序性)。 同时 JDK9 中新加入了 Thread.onSpinWait() 方法。指示调用方暂时无法进行,直到其他活动发生一个或多个操作。运行时可以采取措施来提高调用自旋等待循环构造的性能。
# 线程组
一个线程组核心的信息是:名称、线程最大优先级、是否守护、父线程组、子线程组
# 守护线程(Daemon)
public final void setDaemon(boolean on)
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;
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++;
}
2
3
4
5
实际上 i++ 执行时变成了:
i = Integer.valueOf(i.intValue() + 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);
}
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);
}
}
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
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24