如何进行多道编程
在多道编程的情况下,想要保证线程按我们预想的方式协作,抛开原子性、可见性、有序性的概念,白话总结下心得:
1. 从涉及到操作共享数据的指令入手,保证它们对共享数据操作的正确性。
2. 首先保证单线程情况下,程序执行的正确性。然后判断线程中,与操作共享变量的指令存在数据依赖的指令构成的指令集。保证指令集执行过程中,共享数据不会发生改变。比如对共享变量a:
int b=a; b+=1; a=b;
从 b=a 到 a=b ,a 的值不能被其它线程改变。
3. 控制依赖也是同理,如果决定一段指令如何执行的竞态条件包含共享数据的值,需保证从判断竞态条件开始,到语句集执行结束,不能有其它线程改变共享变量的值。比如一个对阻塞队列的存取操作(阻塞队列的完整代码在文末):
// 移除并返回队尾节点 public synchronized T remove() throws InterruptedException { if (length == 0) { wait(); } Node node = lastNode.pre; if (length <= 0) { throw new RuntimeException("outOfIndex:" + String.valueOf(length - capacity)); } node.pre.next = lastNode; lastNode.pre = node.pre; length--; notifyAll(); return node.value; } // 新增节点,放入队首 public synchronized void put(T value) throws InterruptedException { if (length == capacity) { wait(); } Node node = new Node(value); node.next = firstNode.next; node.pre = firstNode; firstNode.next = node; node.next.pre = node; length++; print(); notifyAll(); }
length 与 capacity 均为共享变量。如果不满足竞态条件,便 wait,等待被其它线程唤醒。
在 wait 期间,因为线程释放了锁,可能有其它线程修改了 length 与 capacity 的值,因此当线程被从 wait 唤醒后,可能共享变量已经不再符合竞态条件,继续向下执行可能会带来问题。
实际测试非常容易验证(测试代码在文末):
应该将 if 改为 where ,这样线程在被从 wait 唤醒后会再次判断竞态条件,而此时如果竞态条件不满足 wait 条件,因为此时线程持有锁,不会有其它线程改变共享变量的值,因此此时继续向下执行没有问题。存在控制依赖的语句集执行期间不会有其它线程修改共享数据的值。
4. 因为硬件结构的特性,在多道编程的情况下,需要保证每个线程对共享变量的操作结果对其它线程可见。可见性的保障是分层次的:内存屏障保证数据存CPU缓冲区及时刷新到CPU缓存,缓存一致性协议保证多核环境下各核心缓存数据的一致性。我们做应用层开发只需要注意 1 即可,通过 volatile 或 synchronized 关键字便可以保证这一点。
5. 编译器和处理器在处理指令时为了提升效率,会在保证单线程环境下语义正确的前提下对指令进行重排序。单线程下,不存在控制依赖和数据依赖的指令在多线程情况下可能存在依赖关系。比如:
a = a+100;
lock.notifyAll();
这两条指令在单线程情况下不存在任何依赖关系,但如果我们期望,被唤醒的线程会立即处理共享变量 a ,这两条语句便发生了依赖关系。如果重排序时将这两条语句的执行顺序颠倒,在改变 a 变量的值之前便唤醒了处理线程 a 的线程,线程便无法按我们预想的方式进行协作。
对此应该熟悉 happen-before 原则,该原则外我们如果需要保证执行的有序性,也可以通过 volatile 或 synchronized 关键字来保证。
6. 在设计并发类程序时,我们应当从共享变量着手,设计各个操作(线程)间的同步互斥关系。互斥同步的粒度越粗设计越简单,但性能也越差,因为将太多的指令纳入临界区会造成不必要纳入临界区的指令执行也会受限与锁的竞争。互斥同步的粒度越细设计越复杂,要避免死锁等锁之间的依赖关系,但同时会带来性能的提升,这两方面的取舍需要根据实际情况来斟酌。
7. 造成死锁有四个必要条件:持有等待、循环依赖、不可抢占、资源有限。可以破除任意一个条件都可以避免死锁的发生,一般来说我们会从循环依赖方面入手避免死锁,也就是控制各个线程对锁的获取顺序,保证相对顺序的统一。因为其它三个条件直接影响了程序的设计,只有在一些特殊情况下允许我们对其作出优化。
8. 大部分情况下,建议设计同步互斥关系的粒度从粗到细。先设计最上层方法的同步互斥关系,在从其中寻找可以优化的部分。很多时候从细粒度的下层方法设计同步互斥关系,反映到上层方法后,反而会发现可能上层方法本身整体就需要加锁。下面的阻塞队列就是例子,共享变量有链表,容量和当前长度,如果开始就针对这三个变量设计细粒度的锁,反应到 remove 方法或者 put 方法后发现,还不如整体加锁来的合适。因为这两个上层方法中绝大多数语句不是在操作这个共享变量,就是在操作另一个共享变量。
下面是阻塞队列的简单实现:
public class SynchronizedQueue<T> { /** * @Author Niuxy * @Date 2020/6/8 9:01 下午 * @Description 双向链表节点 */ class Node { private T value; private Node next; private Node pre; Node(T value) { this.value = value; } public T getValue() { return value; } public void setValue(T value) { this.value = value; } public Node getNext() { return next; } public void setNext(Node next) { this.next = next; } public Node getPre() { return pre; } public void setPre(Node pre) { this.pre = pre; } } // 队列容量 private int capacity; // 队列当前长度 private int length; //虚拟头结点 private Node firstNode; //虚拟尾结点 private Node lastNode; SynchronizedQueue(int capacity) { this.capacity = capacity; this.length = 0; firstNode = new Node(null); lastNode = new Node(null); firstNode.setNext(lastNode); lastNode.setPre(firstNode); } // 移除并返回队尾节点 public synchronized T remove() throws InterruptedException { if (length == 0) { wait(); } Node node = lastNode.pre; while (length <= 0) { throw new RuntimeException("outOfIndex:" + String.valueOf(length - capacity)); } node.pre.next = lastNode; lastNode.pre = node.pre; length--; notifyAll(); return node.value; } // 新增节点,放入队首 public synchronized void put(T value) throws InterruptedException { while (length == capacity) { wait(); } Node node = new Node(value); node.next = firstNode.next; node.pre = firstNode; firstNode.next = node; node.next.pre = node; length++; print(); notifyAll(); } private synchronized void print() { Node node = firstNode.next; while (node != lastNode) { System.out.print(node.value + ","); node = node.next; } System.out.println("---------"); } }
测试代码:
public class Demo { SynchronizedQueue<Integer> queue = new SynchronizedQueue<Integer>(2); private int preRe; Thread putThread = new Thread(() -> { try { int i = 100; while (!Thread.currentThread().isInterrupted()) { queue.put(i); // Thread.sleep(100); i++; } } catch (Exception ie) { ie.printStackTrace(); } }); public void get() { try { while (!Thread.currentThread().isInterrupted()) { int re = queue.remove(); System.out.println(Thread.currentThread().getName() + " get : " + re); } } catch (Exception ie) { System.out.println(Thread.currentThread().getName()+" error!"); ie.printStackTrace(); } } public static void main(String[] args) throws Exception { Demo demo = new Demo(); demo.putThread.start(); ExecutorService executor = Executors.newFixedThreadPool(11); for (int i = 0; i < 10; i++) { executor.execute( new Runnable() { @Override public void run() { demo.get(); } } ); } } }