JAVA高并发一 前言

1.关于高并发的重要概念

      1.1同步和异步

       同步

       所谓同步,就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。

      例如:B/S模式中的表单提交,具体过程是:客户端提交请求->等待服务器处理->处理完毕返回,在这个过程中客户端(浏览器)不能做其他事。

      异步

      异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。

      对于通知调用者的三种方式,具体如下:

      状态

      即监听被调用者的状态(轮询),调用者需要每隔一定时间检查一次,效率会很低。

      通知

      当被调用者执行完成后,发出通知告知调用者,无需消耗太多性能。

      回调

      与通知类似,当被调用者执行完成后,会调用调用者提供的回调函数。

      例如:B/S模式中的ajax请求,具体过程是:客户端发出ajax请求->服务端处理->处理完毕执行客户端回调,在客户端(浏览器)发出请求后,仍然可以做其他的事。

      1.2并发与并行

      并发

      并发是一个应用同时处理多个任务。

       并行

       并行是应用把任务分割成可以并行处理的子任务

        注:单个cpu是不能做并行的,只能是并发。

       1.3临界区

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

       1.4阻塞和非阻塞

       阻塞

       当某个事件或者任务在执行过程中,它发出一个请求操作,但是由于该请求操作需要的条件 
不满足,那么就会一直在那等待,直至条件满足

      非阻塞

      当某个事件或者任务在执行过程中,它发出一个请求操作,如果该请求操作需要的条件不 
满足,会立即返回一个标志信息告知条件不满足,不会一直在那等待

      1.6锁

      死锁

      是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

package test;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
 * Created by Administrator on 2018/8/7.
 */
public class DeadLock {
    private static DateFormat format = new SimpleDateFormat("HH:mm:ss");

    public synchronized void tryOther(DeadLock other) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + " enter tryOther method at " + format.format(new Date()));
        TimeUnit.SECONDS.sleep(3);
        System.out.println(Thread.currentThread().getName() + " tryOther method is about to invoke other method at " + format.format(new Date()));
        other.other();
    }

    public synchronized void other() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + " enter other method at " + format.format(new Date()));
        TimeUnit.SECONDS.sleep(3);
    }

    public static void main(String[] args) throws InterruptedException {
        final DeadLock d1 = new DeadLock();
        final DeadLock d2 = new DeadLock();

        Thread t1 = new Thread(new Runnable() {
            public void run() {
                try {
                    d1.tryOther(d2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "threadA");

        Thread t2 = new Thread(new Runnable() {
            public void run() {
                try {
                    d2.tryOther(d1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "threadB");

        t1.start();
        //让threadA先运行一秒
        TimeUnit.SECONDS.sleep(1);
        t2.start();

        //运行10秒后尝试中断线程
        TimeUnit.SECONDS.sleep(10);
        t1.interrupt();
        t2.interrupt();

        System.out.println("Is threadA is interrupted? " + t1.isInterrupted());
        System.out.println("Is threadB is interrupted? " + t2.isInterrupted());
    }
}

       可以看到在threadA和threadB并没有进入到other方法中,说明程序发生了死锁,threadA在等待threadB的资源,threadB在等待threadA的资源(这里由于使用了同步方法,所以资源确切地说是指d1和d2的对象级别锁)而导致了死锁。

       饥饿

       指的线程无法访问到它需要的资源而不能继续执行时,引发饥饿最常见资源就是CPU时钟周期。虽然在Thread API中由指定线程优先级的机制,但是只能作为操作系统进行线程调度的一个参考,换句话说就是操作系统在进行线程调度是平台无关的,会尽可能提供公平的、活跃性良好的调度,那么即使在程序中指定了线程的优先级,也有可能在操作系统进行调度的时候映射到了同一个优先级。通常情况下,不要去修改线程的优先级,一旦修改程序的行为就会与平台相关,并且会导致饥饿问题的产生。在程序中使用的Thread.yield或者Thread.sleep表明该程序试图克服优先级调整问题,让优先级更低的线程拥有被CPU调度的机会。    

        活锁

        指的是线程不断重复执行相同的操作,但每次操作的结果都是失败的。尽管这个问题不会阻塞线程,但是程序也无法继续执行。活锁通常发生在处理事务消息的应用程序中,如果不能成功处理这个事务那么事务将回滚整个操作。解决活锁的办法是在每次重复执行的时候引入随机机制,这样由于出现的可能性不同使得程序可以继续执行其他的任务。

        1.6并发级别

        并发级别:阻塞和非阻塞(非阻塞分为无障碍、无锁、无等待)

        阻塞(Blocking

        一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行。当我们使用synchronized关键字,或者重入锁时就会产生阻塞的线程。无论是synchronized或者重入锁,都会试图在执行后续代码前,得到临界区的锁,如果得不到,线程就会被挂起等待,直到占有了所需资源为止。

       无饥饿(Starvation-Free)

       这个取决于线程之间是否有优先级的存在,如果系统允许高优先级的线程插队。这样有可能导致低优先级线程产生饥饿。

       无障碍(Obstruction-Free) 

       无障碍是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么他们不会因为临界区的问题导致一方被挂起。换言之,大家都进入临界区了。那么如果一起修改共享数据,把数据改坏了可怎么办呢?对于无障碍的线程来说,一旦检测到这种情况,它就会立即对自己所做的修改进行回滚,确保数据安全。如果说阻塞的控制方式是悲观策略,相对来说非阻塞的调度就是一种乐观的策略。 
       从这个策略中也可以看到,当临界区中存在严重的冲突时,所有的线程可能都会不断地回滚自己的操作,而没有一个线程可以走出临界区。这种情况会影响系统的正常执行。 
一种可行的无障碍实现可以依赖一个“一致性标记”来实现。线程在操作之前,先读取并保存这个标记,在操作完成后,再次读取,检查这个标记是否被更改过,如果两者是一致的,则说明资源访问没有冲突。如果不一致,则说明资源可能在操作过程中与其他写线程冲突,需要重试操作。而任何对资源有修改操作的线程,在修改数据前,都需要更新这个一致性标记,表示数据不再安全。

       无锁(Lock-Free)

       无锁的并行都是无障碍的。在无锁的情况下,所有的线程都能尝试对临界区进行访问,但不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区,一个典型的特点是可能会包含一个无穷循环。在这个循环中,线程会不断尝试修改共享变量。如果修改成功,程序退出,否则继续尝试修改。但无论如何,无锁的并行总能保证有一个线程是可以胜出的。至于临界区中竞争失败的线程,它们则不断重试,直到自己获胜。如果总是尝试不成功,则会出现类似饥饿的现象,线程会停止不前。

       无等待(Wait-Free)

       无锁只要求有一个线程可以在有限步内完成操作,而无等待则在无锁的基础上更进一步进行扩展。它要求所有的线程都必须在有限步内完成,这样就不会引起饥饿问题。如果限制这个步骤上限,还可以进一步分解为有界无等待和线程数无关的无等待几种,它们之间的区别只是对循环次数的限制不同。一种典型的无等待结构就是RCU(Read-Copy-Update)。它的基本思想是,对数据的读可以不加控制。因此,所有的读线程都是无等待的,它们既不会被锁定等待也不会引起任何冲突。但在写数据的时候,先取得原始数据的副本,接着只修改副本数据(这就是为什么读可以不加控制),修改完成后,在合适的时机回写数据。

2.有关并行的两个重要定律

        2.1 Amdahl(阿姆达尔定律)

        2.2 Gustafson定律(古斯塔夫森定律)

原文链接

猜你喜欢

转载自blog.csdn.net/qq_34479912/article/details/81475151