高并发程序编程1

一.多线程相关的一些概念

1.同步和异步

        同步和异步通常用来形容一次方法的调用。同步方法调用一旦开始,调用者必须等待方法调用返回后,才能进行后续的操作。异步方法调用更像一个消息传递,一旦开始,方法调用就好立即返回,调用者就可以继续后续的操作。异步方法通常在另一个线程中执行。整个过程,不会阻碍调用者的工作。对于调用者来说,异步调用似乎是一瞬间就完成的。如果异步调用需要返回结果,那么当这个异步调用真实完成时,则会通知调用者。

 

2.并发(Concurrency)和并行(Parallelism)

          并行和并发时非常容易被混淆的概念。他妈都可以表示两个或者多个任务一起执行,但是偏重点有些不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。而并行时真正意义上的同时执行。

       严格意义上来说,并行的多个任务时真实的同时执行,而对于并发来说,这个过程只是交替的,一会儿运行任务A,一会儿执行任务B,系统会不停的在两个之间切换。但对于外部观察者来说,即使多个任务之间时串行并发的,也会造成多任务间时并行执行的错觉。

      实际上,如果系统内只有一个cpu,而使用多进程或者多线程任务,那么真实环境中这些任务不可能是真实并行的。毕竟一个cpu一次只能执行一条指令,这种情况霞多进程或者多线程就是并发的,而不是并行的(操作系统会不停的切换多个任务执行)。真实的并行也只可能出现在多核cpu系统中。

3.临界区

       临界区用来表示一种公共资源或者是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。

     比如,在一个办公室里有一台打印机。打印机一次只能执行一次任务。如果小王核小米同时需要打印文件,很显然,如果小王先下了打印任务,打印机就会开始打印小王的文件。小米的任务只能等待小王打印完成后才能打印。这里的打印机就是一个临界区的例子。

    在并行程序中,临界区资源是保护的对象,如果意外出现打印机同时执行两个打印任务,那么最可能的结果就是打印出来的文件就会是损坏的文件。它既不是小王需要的,也不是小米需要的。

4.阻塞(blocking)核非阻塞(Non-Blocking)

       阻塞和非阻塞通常用来形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中等待。等待会导致线程挂起,这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区的线程都不能工作。

     非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行。所有的线程都会尝试不断向前执行。

5.死锁(Deadlock)、饥饿(Starvation)、活锁(Livelock)

    死锁、饥饿和活锁都属于多线程的活跃性问题。如果发现上述这几种情况,那么相关线程可能就不再活跃,也就是说它可能很难再继续往下执行了。

死锁:就是各个线程彼此占用对方需要的资源不释放,却在等待彼此占用的资源。这样就导致彼此执行不下去了。例如:线程a占用资源a,等待资源b。线程b占用资源b,等待资源a。谁都不愿意释放自己的资源,这种状态将一直维持下去就是死锁。

饥饿:是指某一个或者多个线程因为杂种原因无法获得所需要的资源,导致一直无法执行。比如它的线程优先级可能太低,而高优先级的线程不断抢占它需要的资源,导致低优先级线程无法工作。另外一种可能是,某一个线程一值占着关键资源不放,导致其他需要这个资源的线程无法正常执行,这种情况也是饥饿的一种。与死锁相比,饥饿还是有可能在未来一段时间内解决的。(比如高优先级的线程已经执行完)

二.并发级别

     由于临界区的存在,多线程之间的并发必须受到控制。根据控制并发的策略,我们可以把并发的级别进行分类,大致上可以分为阻塞,无饥饿,无障碍,无锁,无等待这几种。

1.阻塞

    一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行。当我们使用synchronized关键字,或者重入锁时,我们得到的就是阻塞的线程。

   无论时synchronized或者重入锁,都会试图在执行后续代码前,得到临界区的锁。如果得不到,线程就会被挂起等待,直到占有了所需资源为止。

2.无饥饿(Starvation-Free)

     如果线程之间是由优先级的,那么线程调度的时候总是会倾向于满足高优先级的线程。也就是说,对于同一个资源的分配,是不公平的。如下图所示,显示了非公平于公平两种情况(五角星代表高优先级线程)。对于非公平的锁来说,系统允许高优先级的线程插队。这样由可能导致低优先级产生饥饿。但如果锁是公平的,满足先来后到,那么饥饿就不会产生。不管新来的线程要学会多高。想获得资源,就必须乖乖排队。那么所有的线程都有机会执行。、

3无障碍(Obstruction-Free)

    无障碍是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么他们不会因为临界区的问题导致一方被挂起。换言之,大家都可以大摇大摆的进入临界区了。那么如果大家一起修改共享数据,把数据改坏了可怎么办?对于无障碍的线程来说,一旦检测到这种情况,它就会立即对自己所做的修改进行回滚,确保数据安全。但如果没有数据竞争发生,那么线程就可以顺利完成自己的工作,走出临界区。

如果说阻塞的控制方式是保管策略。也就是说,系统认为两个线程之间很有可能发生百姓的冲突,因此,以保护共享数据为第一优先级。相对来说,非阻塞的调度就是一种乐观的策略。它认为多个线程之间很有可能不会发生冲突,或者说这种概率不大。因此大家都应该无障碍的执行,但是一旦检测到冲突,就应该进行回滚。 

    从这个策略中也可以看到,无障碍的多线程程序并不一定能顺畅的运行。因为当临界区中存在严重的冲突时,所有的线程可能都会不断的回滚自己的操作,而没有一个线程可以走出临界区。这种情况会影响系统的正常执行。所以,我们可能会非常希望在这一堆线程中,至少可以有一个线程能够在有限的时间内完成自己的操作。而退出临界区。至少这样可以保证系统不会在临界区中进行无限的等待。

     一种可行的无障碍实现可以依赖一个“一致性标记”来实现。线程在操作之前,先读取并保存这个标记,在操作完成后,再次读取,检查这个标记是否被更改过,如果两者一致,则说明资源访问没有冲突。如果不一致,则说明资源可能在操作过程中与其他写线程冲突,需要重试操作。而任何对资源有修改操作的线程,在修改数据前,都需要更新这个一致性标记,表示数据不再安全。

4.无锁(Lock-Free)

   无锁的并行都是无障碍的。在无锁的情况下。所有的线程都能尝试对临界区进行访问,但不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区。 

    在无锁的调用中,一个典型的特点是可能会包含一个无穷循环。在这个循环中,线程会不断尝试修改共享变量。如果没有冲突,修改成功,那么程序退出,否则继续尝试修改。但无论如何,无锁的并行总能保证有一个线程是可以胜出的,不至于全军覆没。至于临界区中竞争失败的线程,它们则不断重试,直到自己获胜。如果运气不好,总是尝试不成功,则会程序类似饥饿的现象,线程会停止不前。

5.无等待(wait-Free)

     无锁只要求有一个线程可以在有限步内完成操作,而无等待则在无锁的基础上更进一步进行扩展。它要求所有的线程都必须在有限步内完成,这样就不会引起饥饿问题。如果限制这个步骤上限,还可以进一步分解为有界无等待和线程数无关的无等待几种。它们之间的区别只是对循环次数的限制不同.

    一种典型的无等待结构就是RCU(Read-Copy-Update) .它的基本思想是,对数据的读可以不加控制。因此,所有的读线程都是无等待的,它们既不会被锁定等待也不会引起任何冲突。但是在写数据的时候,先取得原始数据的副本,接着只修改副本数据(这就是为什么读可以不加控制),修改完成后,在合适的时机回写数据。

 三。Java内存模型(JMM)

       由于并发程序要比串行程序复杂很多,其中一个重要原因是并发程序下数据访问的一致性和安全性将会受到严重挑战。如何保证一个线程可以看到正确的数据呢?对于串行程序来说,根本小菜一碟,如果你读取一个变量,这个变量的值是1,那么你读到的一定是1,就这么简单的问题在并行程序中居然变得复杂起来。事实上,如果不加控制地任由线程胡乱并行,即使原本是1的数值,你也有可能读到2.因此,我们需要在深入了解并行机制的前提下,再定义一种规则,保证多个线程间可以有效地,正确的协同工作。而JMM也就是为此而生的。

     JMM的关键技术点都是围绕着多线程的原子性,可见性和有序性来建立的。

  1.原子性

       原子性就是一个操作时不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

2.可见性

    可见性是指当一个线程修改了某个共享变量的值,其他线程是否能够立即知道这个修改。显然,对于串行程序来说,可见性问题是不存在的。因为你在任何一个操作步骤中修改了某个变量,那么在后续的步骤中,读取这个变量的值,一定是修改后的新值。

3.有序性

  对于一个线程的执行代码来说,我们总是习惯的认为代码的执行是从先往后,依次执行的。这么理解也不能说完全错误,因为就一个线程内而言,确实会表现成这样。但是,在并发是,程序的执行可能就会出现乱序。实际上jvm为了优化CPU处理性能,代码底层汇编指令可能会做指令重排,在并发中可能会带来乱序问题。

     哪些指令不能重排:Happen-Before规则

   程序顺序原则:一个线程内保证语义的串行性

  volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性

  锁规则:解锁(unlock)必然发生于随后的加锁(lock)前

 传递性:A先于B,B先于C,那么A必然先于C

 线程的start()方法先于他的每一个动作

 线程的所有操作先于线程的终结

 线程的中断(interrupt)先于被中断的代码

 对象的构造函数执行,结束先于finalize()方法

 

          volatile不能保证原子性,确保可见性和有序性 

   

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值