多线程编程与同步控制(Java与C)

23 篇文章 2 订阅
11 篇文章 0 订阅

一、线程池网摘文章/博客

1.40个Java多线程问题总结
2.生产者消费者模式实现
3.volatile关键字解析
4.java并发之生产者消费者模型
5.linux下C语言多线程   linux中fork--子进程是从哪里开始运行
6.一张图读懂Java多线程
7.Java并发编程:如何创建线程?
8.Java并发编程:synchronized
9.Java多线程的常见例子

一个进程内的多个线程是共享该进程的内存空间的,因此,同一进程内的多线程间需要采用必要措施来控制多线程的同步访问,常用的措施有:

  1. 互斥量:只能让一个线程运行,其他线程休眠等待;
  2. 信号量:能让多个线程运行,其他线程休眠等待;
  3. 临界区:只能让一个线程运行,其他线程休眠等待;
  4. 原子操作:没有线程的切换,执行速度最快;

而进程间是独立的,互不干扰,但进程间需要涉及到通信,常用的通信措施有:管道、内存映射等。

进程间通信:每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。

二、多线程编程中的三个核心概念

原子性
这一点,跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。关于原子性,一个非常经典的例子就是银行转账问题:比如A和B同时向C转账10万元。如果转账操作不具有原子性,A在向C转账时,读取了C的余额为20万,然后加上转账的10万,计算出此时应该有30万,但还未来及将30万写回C的账户,此时B的转账请求过来了,B发现C的余额为20万,然后将其加10万并写回。然后A的转账操作技术——将30万写回C的余额。这种情况下C的最终余额为30万,而非预期的40万。

可见性
可见性是指,当多个线程并发访问共享变量时,一个线程共享变量的修改,其它线程能够立即看到。可见性问题是好多个忽略或者理解错误的一点。CPU从主内存中读数据的效率相对来说不高,现在主流的计算机中,都有几级缓存。每个线程读取共享变量时,都会将该变量加载进其对应CPU的高速缓存里,修改该变量后,CPU会立即更新该缓存,但并不一定会立即将其写回主内存(实际上写回主内存的时间不可预期)。此时其它线程(尤其是不在同一个CPU上执行的线程)访问该变量时,从主内存中读到的就是旧的数据,而非第一个线程更新后的数据。这一点是操作系统或者说是硬件层面的机制,所以很多应用开发人员经常会忽略。

顺序性
顺序性指的是,程序执行的顺序按照代码的先后顺序执行。以下面这段代码为例:

1
2
3
4
boolean started = false; // 语句1
long counter = 0L; // 语句2
counter = 1; // 语句3
started = true; // 语句4


从代码顺序上看,上面四条语句应该依次执行,但实际上JVM真正在执行这段代码时,并不保证它们一定完全按照此顺序执行。处理器为了提高程序整体的执行效率,可能会对代码进行优化,其中的一项优化方式就是调整代码顺序,按照更高效的顺序执行代码。讲到这里,有人要着急了——什么,CPU不按照我的代码顺序执行代码,那怎么保证得到我们想要的效果呢?实际上,大家大可放心,CPU虽然并不保证完全按照代码顺序执行,但它会保证程序最终的执行结果和代码顺序执行时的结果一致。

三、Java如何解决多线程并发问题

Java如何保证原子性

锁和同步

常用的保证Java操作原子性的工具是锁和同步方法(或者同步代码块)。使用锁,可以保证同一时间只有一个线程能拿到锁,也就保证了只一时间只有一个线程能执行申请锁和释放锁之间的代码。

1
2
3
4
5
6
7
8
9
public void testLock () {
  lock.lock();
  try{
    int j = i;
    i = j + 1;
  } finally {
    lock.unlock();
  }
}

与锁类似的是同步方法或者同步代码块。使用非静态同步方法时,锁住的是当前实例;使用静态同步方法时,锁住的是该类的Class对象;使用静态代码块时,锁住的是synchronized关键字后面括号内的对象。下面是同步代码块示例

1
2
3
4
5
6
public void testLock () {
  synchronized (anyObject){
    int j = i;
    i = j + 1;
  }
}

无论使用锁还是synchronized,本质都是一样,通过锁来实现资源的排它性,从而实际目标代码段同一时间只会被一个线程执行,进而保证了目标代码段的原子性。这是一种以牺牲性能为代价的方法。

CAS(compare and swap)

基础类型变量自增(i++)是一种常被新手误以为是原子操作而实际不是的操作。Java中提供了对应的原子操作类来实现该操作,并保证原子性,其本质是利用了CPU级别的CAS指令。由于是CPU级别的指令,其开销比需要操作系统参与的锁的开销小。AtomicInteger使用方法如下。

1
2
3
4
5
6
7
8
AtomicInteger atomicInteger = new AtomicInteger();
for(int b = 0; b < numThreads; b++) {
  new Thread(() -> {
    for(int a = 0; a < iteration; a++) {
      atomicInteger.incrementAndGet();
    }
  }).start();
}


Java如何保证可见性

Java提供了volatile关键字来保证可见性。当使用volatile修饰某个变量时,它会保证对该变量的修改会立即被更新到内存中,并且将其它缓存中对该变量的缓存设置成无效,因此其它线程需要读取该值时必须从主内存中读取,从而得到最新的值。

Java如何保证顺序性

上文讲过编译器和处理器对指令进行重新排序时,会保证重新排序后的执行结果和代码顺序执行的结果一致,所以重新排序过程并不会影响单线程程序的执行,却可能影响多线程程序并发执行的正确性。Java中可通过volatile在一定程序上保证顺序性,另外还可以通过synchronized和锁来保证顺序性。synchronized和锁保证顺序性的原理和保证原子性一样,都是通过保证同一时间只会有一个线程执行目标代码段来实现的。除了从应用层面保证目标代码段执行的顺序性外,JVM还通过被称为happens-before原则隐式的保证顺序性。两个操作的执行顺序只要可以通过happens-before推导出来,则JVM会保证其顺序性,反之JVM对其顺序性不作任何保证,可对其进行任意必要的重新排序以获取高效率。

happens-before原则(先行发生原则)

  • 传递规则:如果操作1在操作2前面,而操作2在操作3前面,则操作1肯定会在操作3前发生。该规则说明了happens-before原则具有传递性
  • 锁定规则:一个unlock操作肯定会在后面对同一个锁的lock操作前发生。这个很好理解,锁只有被释放了才会被再次获取
  • volatile变量规则:对一个被volatile修饰的写操作先发生于后面对该变量的读操作
  • 程序次序规则:一个线程内,按照代码顺序执行
  • 线程启动规则:Thread对象的start()方法先发生于此线程的其它动作
  • 线程终结原则:线程的终止检测后发生于线程中其它的所有操作
    • 线程中断规则: 对线程interrupt()方法的调用先发生于对该中断异常的获取
  • 对象终结规则:一个对象构造先于它的finalize发生

volatile适用场景

volatile适用于不需要保证原子性,但却需要保证可见性的场景。一种典型的使用场景是用它修饰用于停止线程的状态标记。如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
boolean isRunning = false;

public void start () {
  new Thread( () -> {
    while(isRunning) {
      someOperation();
    }
  }).start();
}

public void stop () {
  isRunning = false;
}

在这种实现方式下,即使其它线程通过调用stop()方法将isRunning设置为false,循环也不一定会立即结束。可以通过volatile关键字,保证while循环及时得到isRunning最新的状态从而及时停止循环,结束线程。

线程安全十万个为什么

问:平时项目中使用锁和synchronized比较多,而很少使用volatile,难道就没有保证可见性?
答:锁和synchronized即可以保证原子性,也可以保证可见性。都是通过保证同一时间只有一个线程执行目标代码段来实现的。

问:既然锁和synchronized即可保证原子性也可保证可见性,为何还需要volatile?
答:synchronized和锁需要通过操作系统来仲裁谁获得锁,开销比较高,而volatile开销小很多。因此在只需要保证可见性的条件下,使用volatile的性能要比使用锁和synchronized高得多。

问:既然锁和synchronized可以保证原子性,为什么还需要AtomicInteger这种的类来保证原子操作?
答:锁和synchronized需要通过操作系统来仲裁谁获得锁,开销比较高,而AtomicInteger是通过CPU级的CAS操作来保证原子性,开销比较小。所以使用AtomicInteger的目的还是为了提高性能。

问:还有没有别的办法保证线程安全
答:有。尽可能避免引起非线程安全的条件——共享变量。如果能从设计上避免共享变量的使用,即可避免非线程安全的发生,也就无须通过锁或者synchronized以及volatile解决原子性、可见性和顺序性的问题。

问:synchronized即可修饰非静态方式,也可修饰静态方法,还可修饰代码块,有何区别
答:synchronized修饰非静态同步方法时,锁住的是当前实例;synchronized修饰静态同步方法时,锁住的是该类的Class对象;synchronized修饰静态代码块时,锁住的是synchronized关键字后面括号内的对象。

参考文章

1.Java并发控制机制


四、线程池

线程池是一个很典型的生产者-消费者模型,如下:


在线程池的实现中,生产者为调用者,调用者会调用线程池的execute/submit方法提交一个任务到存储中介中,线程池中的线程(消费者)会在存储中介中取出一个个任务执行。存储中介在线程池中的实现是`java.util.concurrent.BlockingQueue`(阻塞队列)的一个实现类,jdk已经帮我们默认实现了几个最常用的实现:
1. java.util.concurrent.ArrayBlockingQueue 底层为数组的queue,遵循先进先出的原则,这个队列在线程池中用的比较少,因为线程池中做的最多的操作为插入及取出数据而不是查询。
2. java.util.concurrent.LinkedBlockingQueue 底层为链表的queue,遵循先进先出的原则
3. java.util.concurrent.PriorityBlockingQueue 底层为数组的queue, 这个queue带有权重,也可以自己传入`Comparator`来对queue中的权重进行调整,我们可以用这个queue来控制一些重要的任务会被优先执行。
4. java.util.concurrent.SynchronousQueue 长度为1的队列,使用这个队列可以节省队列元素放入取出的时间
....
如果默认的队列不能满足需求,也可以自己实现`java.util.concurrent.BlockingQueue`接口来做更深层的定制。
下面是一个任务提交到线程池的完整流程

提交任务流程
线程池中的每个任务都是Runable/callable的一个实例,在讲述流程之前有必要先简单介绍一个线程池构造函数的各个参数:
1. corePoolSize 核心线程数,也就是上述消费者的数量。
2. maximumPoolSize 最大的线程数量,这个最大线程数量有点歧义,因为这个数量只有当任务队列满的时候,任务无法再加入到队列中时,线程池会再新增线程到线程池中,这个参数就是当上述情况发生的时候控制线程池中最大的线程数量
3. keepAliveTime 空闲线程的最大存活时间,超过这个时间的线程会被打断,默认只对非核心线程,也就是超过corePoolSize数量的线程有用,但是也可以使用`allowCoreThreadTimeOut(boolean)`方法设置对所有的线程作用
4. workQueue 存放任务的队列,也就是生产者消费者模型中的存储介质
5. threadFactory 线程工厂,工作线程创建时使用的工厂,可以根据自己需要自己实现,默认为`java.util.concurrent.Executors.DefaultThreadFactory`
6. handler 拒绝策略,是一个回调函数,当线程池中队列已满而且线程数也已经达到maximumPoolSize时,线程池就会拒绝接收任务并且调用这个回调函数,它也有几个默认的策略,我们也可以根据需要自己定义
下面这张图就是线程池的一个大概的流程:


下面是jdk1.7版本executor的完整代码


总的流程我在代码注释中都有解释,其实就是三步走,外加一些异常情况的判断。


参考文章:

1.Java多线程中的同步
2.C++多线程中的同步
3.使用 Executors,ThreadPoolExecutor,创建线程池,源码分析理解


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值