Java并发编程实战

Java并发编程实战

并发简史

在早期的计算机中不包含操作系统,它们从头到尾只执行一个程序,并且这个程序能访问计算机中的所有资源。在这种裸机环境中,不仅很难编写和运行程序,而且每次只能运行一个程序,这对昂贵并且稀有的计算机资源来说也是一种浪费。

操作系统的出现,使计算机可以运行多个程序,并且不同的程序都在单独的进程中运行:操作系统为各个独立执行的进程分配各种资源,包括内存,文件句柄以及安全证书等。

为什么计算机中加入操作系统来实现多个程序的同时执行呢?主要是基于以下原因:

资源利用率。在某些情况下,程序必须等待某个外部操作执行完成才能继续往下执行,比如说当外部有输入操作的时候,如果外部不输入内容,我们的程序就不能往下继续执行。因此,如果计算机在等待的同时,还可以运行另外一个程序,这将会提高计算机资源的利用率。

公平性。不同的程序对计算机上的资源有着同等的使用权。那么怎样保证公平性呢?计算机中的一种高效的方式是,使用CPU时间分片,每个程序依次获得CPU的处理权,并且每个程序获得的时间分片时间都是一样的,这样就能保证计算机可以同时执行很多个程序,而不是只有执行完上一个程序才能执行下一个程序。

便利性。通常来说,在计算多个任务时,应该编写多个程序,每个程序执行一个任务并在必要时相互通信,这比只编写一个程序来计算所有任务更容易实现。

串行变成模型的优势在于其直观性和简单性,因为它模仿了人类的工作方式:每次只做一件事,做完之后再做另一件。例如,首先起床,穿上衣服,然后下楼,喝早茶。在编程语言中,这些现实世界中的动作可以被进一步抽象为一组粒度更细的动作。例如,喝早茶的动作可以被进一步细化为:打开橱柜,挑选喜欢的茶叶,将一些茶叶倒入杯中,看看茶壶中是否有足够的水,如果没有的话加些水,将茶壶放到火炉上,点燃火炉,然后等水烧开等等。在最后一步等水烧开的过程中包含了一定程度的异步性。当正在烧水时,你可以干等着,也可以做些其它事情,例如开始烤面包(这是另一个异步任务)或者看报纸,同时留意茶壶水是否烧开。茶壶和面包机的生产商都很清楚:用户通常会采用异步方式来使用他们的产品,因此当这些机器完成任务时都会发出声音提示。但凡做事高效的人,总能在串行性与异步性之间找到合理的平衡,对于程序来说同样如此。

这些促使进程出现的因素(资源利用率,公平性以及便利性等)同样也促使着线程的出现。线程允许在同一个进程中同时存在多个程序控制流。线程会共享进程范围内的资源,例如内存句柄和文件句柄,但每个线程都有各自的程序计数器,栈以及局部变量等。线程还提供了一种直观的分解模式来充分利用多处理器系统中的硬件并行性,而在同一个程序中的多个线程也可以被同时调度到多个CPU上运行。

线程也被称为轻量级进程。在大多数现代操作系统中,都是以线程为基本的调度单位,而不是进程。如果没有明确的协同机制,那么线程将彼此独立执行。由于同一个进程中的所有线程都将共享进程的内存地址空间,因此这些线程都能访问相同的变量并在同一个堆上分配对象,这就需要实现一种比在进程间共享数据粒度更细的数据共享机制。如果没有明确的同步机制来协同对共享数据的访问,那么当一个线程正在使用某个变量时,另一个线程可能同时访问这个变量,这将早晨不可预测的结果。

计算机,进程和线程

首先现在的计算机中有多个程序,每一个程序都被放到了一个进程里面单独处理,所以可以把电脑里面的各种程序理解成进程,我们后续再说进程的时候,就可以把它当成一个程序,而所有的进程共享计算机的所有资源。比如我们电脑中有qq程序,钉钉程序,360程序,这三个程序分别放到三个不同的进程里面处理,并且这三个程序可以共享计算机的所有资源,比如这三个程序里面都可以访问计算机里面的图片资源,视频资源,音乐资源等等。通过电脑里面的任务管理器可以很清晰的理解这个概念,如下图:

在这里插入图片描述

从上面的两张图片中也可以看出,我们的计算机里面的所有的应用程序,其实都会单独给它分配一个相应的进程,去处理这个程序。电脑中的所有进程共用计算机的所有文件。

在一个进程里面有多个线程,这些线程可以并发执行,看似好像是同时执行的,其实是CPU通过特定的事件分片确保每个线程都能分到相同的时间,通过这种方式实现的。那么怎样更直观的理解一个进程里面是有多个线程的呢?我们可以打开一个程序软件,看看它里面的构造就清楚了,比如我们打开360软件如下图:

在这里插入图片描述

用过360安全卫士的都知道,这里的木马查杀,电脑清理,系统修复,优化加速,它们可以同时进行,其实这四个功能就可以理解成是程序里面的四个多线程,其实也就是进程里面的四个多线程。

多处理器的强大能力

过去,多处理器系统是非常昂贵和稀少的,通常只有在大型数据中心和科学计算设备中才会使用多处理器系统。但现在,多处理器系统正日益普及,并且价格也在不断降低,即使在低端服务器和中端桌面系统中,通常也会采用多个处理器。这种趋势还在进一步加快,因为通过提高时钟频率(其实也就是减少CPU分片时间)来提升性能一遍的越来越困难,处理器生产厂商都开始转而在单个芯片上放置多个处理器核。所有的主流芯片制造商都开始了这种转变,而我们也已经看到了在一些机器上出现了更多的处理器。

由于基本的调度单位是线程,因此如果在程序中只有一个线程,那么最多同时只能在一个处理器上运行。在双处理器系统上,单线程的程序只能使用一半的CPU资源,而在拥有100个处理器的系统上,将有99%的资源无法使用。另一方面,多线程程序可以同时在多个处理器上执行。如果设计正确,多线程程序可以通过提高处理器资源的利用率来提升系统吞吐率。

使用多个线程还有助于在单处理器系统上获得更高的吞吐率。如果程序是单线程的,那么当程序等待某个同步I/O操作完成时,处理器将处于空闲状态。而在多线程程序中,如果一个线程在等待I/O操作完成,另一个线程可以继续运行,使程序能够在I/O阻塞期间继续运行。(这就好比在等待水烧开的同时看报纸,而不是等到水烧开之后再开始看报纸)。

并发问题的一个例子

多线程执行的时候,一个进程中的多线程都可以访问进程中的资源,但是问题是,每个线程都有自己的程序计数器,栈以及局部变量等,这些是每个线程私有的,因此如果现在有两个线程,它们拿到进程中的共享资源:一个变量初始值为0;现在它们对这个变量++操作,正常情况下,如果能保证一个线程先进getNext()方法,进去之后运行了加加操作之后,cpu时间分片开始切片给另外一个线程,另外一个线程在进入getNext()方法,这样能够保证我们两个线程执行两次getNext()方法,进行两次加加操作,得到变量值为1和2,但是问题是我们没办法控制两个线程什么时候进入getNext方法内部,没办法控制必须一个线程先进入getNext()方法,没有执行加加操作的时候另外一个线程不允许进入。所以就可能会出现一种情况,两个线程同时进入getNext()方法,因为两个线程都可以共享程序也就是进程的内存地址,所以它们取出来的变量value的初始值都是0,这个时候执行加加操作之后,得到的新的变量值都是1,因为每个线程的栈,局部变量,程序计数器都是私有的,因此会把1存在每个线程中,这样就出问题了,因为我们本该得到1和2,现在得到了两个1,如下:

/**
 * @author 望轩
 * @date 2023/6/18 15:14
 */
public class Test {
    public static void main(String[] args) {
        UnsafeSequence unsafeSequence = new UnsafeSequence();
        Thread thread1 = new Thread(unsafeSequence);
        Thread thread2 = new Thread(unsafeSequence);
        thread1.start();
        thread2.start();
    }
}

class UnsafeSequence implements Runnable {
    private int value = 0;
    public int getNext() throws InterruptedException {
        Thread.sleep(1000);
        return ++value;
    }

    public void run() {
        try {
            System.out.println(getNext());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
//输出结果如下图:

在这里插入图片描述

那么怎么解决这个问题呢?只需要让一个线程进入getNext()方法的时候另外一个线程不允许进入就行了,有三种实现方式,第一种方式是给线程不安全的方法加一个synchronized关键字,这样就能控制此方法一次只能由一个线程进入,当前线程没有执行完这个方法的时候,其它线程不能进入此方法,这样就能杜绝两个线程同时操作同一个进程内存地址中的变量,从而导致出现数据异常的问题发生了,来看一下运行结果,如下图:

在这里插入图片描述

如果把synchronized关键字加到一个方法上,那么这个方法就是一个同步方法,如果它里面牵涉到的进程也就是程序的共享变量是非static静态类型的,那么此同步方法使用的锁就是对象本身;而如果共享变量是静态类型的,那么此同步方法使用的锁就是对象的Class类型的比如这就是UnsafeSequence.class类型模板。

第二种解决多线程问题的是,同步代码块,即把synchronized关键字加到一个代码块上,但这种方式必须要配上一个锁,如下图:

在这里插入图片描述

因为这里的共享变量是非static类型的,因此我们给同步代码块加锁的时候,只需要使用Object对象就可以。但是假如共享变量是static类型的,那么我们给同步代码块加锁的时候,必须要使用UnsafeSequence.class类型模板才可以。

第三种解决线程安全问题的方式是使用ReentrantLock对象,把共享变量变化的区域锁起来,如下图:

在这里插入图片描述

看一下最终的输出结果也是正常的,如下图:

在这里插入图片描述

什么是线程安全类?它的定义是什么?

线程安全类的定义:当多个线程访问某个类的时候,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,而在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就说这个类是线程安全的。在线程安全类中封装了必要的同步机制,因此客户端无须进一步采取同步措施。

ConcurrentHashMap这个类是线程安全的map集合,它里面已经使用synchronized关键字实现了同步机制,对于程序中的共享变量,也就是对于我们程序代码中使用ConcurrentHashMap集合定义的map集合变量,程序中的所有线程都可以访问这个变量,但是所有的线程不能同时访问这个ConcurrentHashMap变量,只能一个一个的访问,这样就保证了我们在程序中创建的ConcurrentHashMap集合变量永远都不会被另外一个线程错误的修改,所以ConcurrentHashMap集合是线程安全类。

以后在公司开发游戏的时候,如果想要在一个类里面生成一个map集合,但是同时考虑到,这个集合可能会被很多个线程访问,那么这个时候就要创建一个ConcurrentHashMap类型的map集合,这样才是线程安全的,才不会出现线程安全问题。

原子性与复合操作

/**
 * @author 望轩
 * @date 2023/6/18 15:14
 */
public class Test {
    public static void main(String[] args) {
        UnsafeSequence unsafeSequence = new UnsafeSequence();
        Thread thread1 = new Thread(unsafeSequence);
        Thread thread2 = new Thread(unsafeSequence);
        thread1.start();
        thread2.start();
    }
}

class UnsafeSequence implements Runnable {
    private int value = 0;
    public int getNext() throws InterruptedException {
        Thread.sleep(1000);
        return ++value;
    }

    public void run() {
        try {
            System.out.println(getNext());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在前面已经讲过UnsafeSequence类是线程不安全的,不需要使用synchronized加同步,才能保证它是线程安全的。还有一种办法,就是这个类之所以是线程不安全的,是由于它的共享变量value的类型是int,这个类型是线程不安全的。每个线程对value的操作是一个复合操作,int类型不是原子性的,什么是复合操作,就是一个线程在操作int value的时候,会先获取value的值,然后给这个值加一,最后把计算后的值在赋值给value变量,也就是“读取—修改—写入”,其实对value变量的操作是一个复合操作。因此如果前一个线程现在是对value进行修改,但是还没有改,而后一个线程进来读取这个值,加入说value的初始值是1,那么第一个线程读取的是1,修改之后变成2,而第二个线程读取的也是1,修改之后也是2,这显然就和我们想要的效果不同了。因此我们不能使用int类型,因为它是复合操作,不是原子操作,我们要使用一个原子变量类AtomicInteger,这个类是线程安全的,它是原子操作,也就是说一个线程对它进行“读取–修改–写入”是一个整体,这个中间其它线程不能读取此变量。

所以如果我们用一个线程安全的变量类型代替int,使用一个原子变量类,那么即便不使用snynorized关键字加同步,我们的代码也是线程安全的,修改之后的代码如下图:

在这里插入图片描述

输出结果如下:

在这里插入图片描述

内置锁

每个java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或者监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时,自动释放锁。

以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,如果是用synchronized修饰的非静态的方法,它的锁就是调用方法的对象本身。如果是使用synchronized修饰的静态方法,它的锁就是对象对应的类的class类型模板。

锁的重入

当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可以重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。

重入的一种实现方法是,为每个锁关联一个计数器。当计数器为0时表明这个锁没有被任何线程持有,当线程请求一个未被持有的锁时,计数器,被设置为1,如果同一个线程再次获取这个锁,计数器值将递增,而当线程退出同步代码块时,计数器会相应递减。当计数器为0时,这个锁将被释放。

比如下面的这个例子:

public class Widget {
    public synchronized void doSomething() {
        ...
    }
}

public class LoggingWidget extends Widget {
    public synchronized void doSomething() {
        System.out.println(toString());
        super.doSomething();
    }
}

加入我们有一个线程访问LoggingWidget子类中的doSomething方法,那么首先会获取到LoggingWidget子类对象所代表的锁,然后计数器变为1,接着这个方法中会调用另外一个方法Widget#doSomething,因为这个方法在同一个线程中,并且和LoggingWidget方法使用了同一把锁,因此由于锁的重入,是可以调用成功的,并且这个时候计数器加1。

用锁来保护状态

对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。当获取与对象关联的锁时,并不能阻止其他线程访问该对象,因为某个线程在获得对象的锁之后,只能阻止其他线程获取同一个锁,但是假如我一个类中的状态变量,在这个类中的很多方法中的同步代码块中都有调用,但是这些同步代码块使用的是不同的锁,那么这样仍然是线程不安全的,仍然会有好几个线程同时访问我们的共享变量,比如下图这个例子:

在这里插入图片描述

这个类中的共享变量是value,这里面定义了两个锁对象lock1和lock2。在getNext1方法中我们使用了lock1锁,锁起来了一个同步代码块,其它线程再进入这个方法的时候由于获取不到lock1锁,它就会被阻塞到外面。但是,现在的问题是,其它线程可以进入getNext2方法,因为getNext2方法里面使用的是lock2锁,而这个锁还没有被别的线程持有,因此第二个线程可以获取到lock2锁,也就意味着,第二个线程可以去访问共享变量value了,这样就会出现问题,因为同一个共享变量可以被两个不同的线程同时访问,就会出现并发安全问题。

因此如果想保证共享变量的线程安全,所有使用共享变量的方法都需要使用同步策略,并且仅仅使用同步策略让其变成同步代码块还不足够,还必须要让所有的同步代码块使用的都是同一把锁。

我们之所以每个对象都有一个内置锁,比如说使用syncronized修饰的非静态方法的内置锁就是对象本身,而静态方法的内置锁就是类的Class类型模板对象,也就意味着对于一个类中的所有的非静态方法都会让其使用同一把锁:对象本身。而对于类中的所有静态方法也都会让其使用同一把锁:类的class类型模板。之所以每个对象都有一个内置锁,就是为了避免显式的创建锁对象。

活跃性与性能

使用同步代码块的时候,尽量不要包含不影响共享变量且执行时间较长的操作,要把这些操作从同步代码块中分离出去,因为要不然的话,其它线程运行到同步代码块之前的时候,需要阻塞很长的时间。

既然synchronized关键字可以防止多线程问题,那么为什么不把程序所有的地方都加上synchronized关键字,让其变为一个同步代码块呢?因为每次加锁和释放锁也是非常耗费资源的,那样做的话会让我们程序的性能变得非常低。

原子变量类AtomicLong,AtomicInteger不要和synchronized同步代码块共用,这是两种同步机制,使用其中的一种就可以了。而如果同时使用两种不同的同步机制不仅会带来混乱,也不会在性能或安全性上带来任何好处。因此后续开发的过程中,如果使用了原子变量类就不要使用synchronized关键字了,而如果使用了synchronized关键字就不要使用原子变量了。

对象的共享

可见性

什么是可见性?可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。在单线程环境中,如果向某个变量先写入值,然后在没有其他写入操作的情况下读取这个变量,那么总能得到相同的值。这看起来很自然。然而,当读操作和写操作在不同的线程中执行时,情况却并非如此,这听起来或许有些难以接受。通常,我们无法确保执行读操作的线程能适时的看到其他线程写入的值,又是甚至是根本不可能的事情。

简单来说,可见性带来的问题就是,写线程中写入的值,读线程中可能全部能看见,也可能全部能看见,也可能部分能看见部分看不见。

怎样解决可见性问题?为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

看一下下面这个代码,

public class NoVisibity {
    private static boolean ready;
    private static int number;
    
    private static class ReaderThread extends Thread {
        public void run() {
            while(!ready) {
                Thread.yield();
            }
            System.out.private(number);
        }
    }
    
    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}

在上面的代码中,主线程和读线程都将访问共享变量ready和number。主线程启动读线程,然后将number设为42,并将ready设为true。读线程一直循环知道发现ready的值变为true,然后输出number的值。虽然NoVisibility看起来会输出42,但事实上很可能会输出0,或者根本无法终止。这是因为在代码中没有使用足够的同步机制,因此无法保证主线程写入的ready值和number值对于读线程来说是可见的。

NoVisibility可能会持续的循环下去,因为读线程可能永远都看不到ready的值。一种更奇怪的现象是,NoVisibility可能会输出0,因为读线程可能看到了写入的ready的值,但却没看到之后写入的number的值,这种现象被称为“重排序”。只要在某个线程中无法检测到重排序情况,那么就无法确保线程中的操作将按照程序中指定的顺序来执行。当主线程首先写入number,然后在没有同步的情况下写入ready,那么读线程看到的顺序可能与写入的顺序完全相反。

在没有同步的情况下,编译器和处理器都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论。

NoVisibility是一个简单的并发程序,只包含两个线程和两个共享变量,但即便如此,在判断程序的执行结果以及是否会结束时仍然很容易得出错误结论。要对哪些缺乏足够同步的并发程序的执行情况进行推断是十分困难的。

这听起来有点恐怖,但实际情况也确实是如此。幸运的是,有一种简单的方法能避免这些复杂的问题:只要有数据在多个线程之间共享,就是用正确的同步。

使用volatile可以保证可见性

如果想要确保上面的可见性可以怎么办呢?可以使用volatile关键字,如下图:

在这里插入图片描述

这样主线程中对number和ready的修改,在读线程中就是可见的了。并且读线程中也不会对number=42和ready=true重排序。

加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。

当且仅当满足以下所有条件时,才应该使用volatile变量:

  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
  • 该变量不会与其它状态变量一起纳入不变性条件中。
  • 在访问变量时不需要加锁。

任务执行

在一个线程中串行的执行任务

看一下最初服务器接收客户端请求的代码结构,如下:

class SingleThreadWebServer {
    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(80); //服务器的80端口
        while(true) {
            Socket connection = socket.accept(); //服务器的80端口接收到来自客户端的请求
            handleRequest(connection); //服务器处理请求
        }
    }
}

上面程序中的SingleThreadWebServer单线程将串行的处理客户端发送给服务器的请求(即通过80端口接收到的HTTP请求),至于如何处理请求的细节问题,在这里并不重要,我们感兴趣的是如何表现不同调度策略的同步特征。

SingleThreadWebServer很简单,且在理论上是正确的,但在实际生产环境中的执行性能却很糟糕,因为它每次只能处理一个请求。主线程在接受连接与处理相关请求等操作之间不断地交替运行。当服务器正在处理请求时,新到来的连接必须等待直到请求处理完成,然后服务器再次调用accept。如果处理请求的速度很快并且handleRequest可以立即返回,那么这种方法是可行的,但现实世界中的Web服务器的情况却并非如此。

现实世界当服务器处理请求的时候,可能会有IO操作,会发生阻塞,也可能会访问数据库,也会发生阻塞,等等。在单线程中,服务器中处理客户端请求的线程发生了阻塞,又因为服务器中只有这一个线程,那么当服务器再次接受到客户端发送来的请求之后就会发生阻塞,就没办法再次处理客户端请求,因为客户端发送来的上个请求还没有处理完。

显式的为每个客户端请求都创建一个单独的线程执行

如何解决上面的单线程中一次只能处理一个客户端请求的问题呢?可以创建多个线程,让每一个客户端请求都放到一个不同的线程中去处理。代码如下:

public ThreadPerTaskWebServer {
    pubic static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(80);
        while(true) {
            final Socket connection = socket.accpet();
            Runnable task = new Runnable() {
                public void run() {
                    handleRequest(connection);
                }
            }
            new Thread(task).start();//每一个客户端请求都为之单独的创建一个线程执行
        }
    }
}

ThreadPerTaskWebServer在结构上类似于前面的单线程版本,主线程仍然不断地交替执行“接受外部连接”与“分发请求”等操作。区别在于,对于每个连接,主循环都将创建一个新线程来处理流请求,而不是在主循环中进行处理。由此可得出3个主要结论:

  • 任务处理过程从主线程中分离出来,使得主循环能够更快地重新等待下一个到来的连接。这使得程序在完成前面的请求之前可以接受新的请求,从而提高响应性。
  • 任务可以并行处理,从而能同时服务多个请求。
  • handleRequest里面的请求处理代码必须要是线程安全的,因为当有多个任务时会并发地调用这段代码。

无限制创建线程的不足

在生产环境中“为每个任务分配一个线程”这种方法存在一些缺陷,尤其是当需要创建大量的线程时:

  • 线程生命周期的开销非常高。线程的创建与销毁并不是没有代价的。根据平台的不同,实际的开销也有所不同,但线程的创建过程都会需要时间,延迟处理的请求,并且需要JVM和操作系统提供一些辅助操作。如果请求的到达率非常高且请求的处理过程是轻量级的,例如大多数服务器应用程序就是这种情况,那么为每个请求创建一个新线程将消耗大量的计算机资源。
  • 资源消耗。活跃的线程会消耗系统资源,尤其是内存。如果可运行的线程数量多于可用处理器的数量,那么有些线程将闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量线程在竞争CPU资源时还将残生其它的性能开销。如果你已经拥有足够多的线程使所有CPU保持忙碌状态,那么再创建更多的线程反而会降低性能。
  • 稳定性。在可创建线程的数量上存在一个限制。这个限制值将随着平台的不同而不同,并且受到多个因素制约,包括JVM的启动参数,Thread构造函数中请求的栈大小,以及底层操作系统对线程的 限制等。如果破坏了这些限制,那么很可能抛出OutOfMemoryError异常,要想从这种错误中恢复过过来是非常危险的,更简单的办法是通过构造程序来避免超出这些限制。在一定范围内,增加线程可以提高系统的吞吐率(吞:也就是服务器接收客户端请求,吐:也就是服务端处理请求之后返回给客户端的响应,吞吐率就是说程序执行的速率可以先这样理解),但如果超出了这个范围,再创建更多的线程只会降低程序的执行速度,并且如果过多地创建一个线程,那么整个应用程序将崩溃。要想避免这种危险,就应该对应用程序可以创建的线程数量进行限制,并且全面地测试应用程序,从而确保在线程数量达到限制时,程序也不会耗尽资源。

Executor框架

任务是一组逻辑工作单元,而线程则是使任务异步执行的机制。我们已经分析过了两种通过线程来执行任务的策略,即把所有任务放在单个线程中串行执行,以及将每个任务放在各自的线程中执行。这两种方式都存在一些严格的限制:串行执行的限制:串行执行的问题在于其糟糕的响应性和吞吐量,而“为每个任务分配一个线程”的问题在于资源管理的复杂性。

Executor框架在java编程思想那篇文章中的并发模块已经讲过了,可以参照之前的文章。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr-X~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值