线程安全、锁、线程通信、线程池

本文详细介绍了Java中的线程安全问题,通过售票案例展示了多线程可能导致的数据冲突。接着讨论了线程同步的重要性,讲解了synchronized关键字的使用以及如何避免死锁,还提到了线程通信的wait和notify方法。最后,文章阐述了线程池的概念、作用以及如何通过ThreadPoolExecutor定制线程池,强调了合理设置线程池参数和避免直接使用Executors创建线程池的注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程安全、锁、线程通信、线程池

1. 什么是线程安全问题

1.1 为什么要有线程安全问题?

当多个线程同时共享同一个全局变量或静态变量,做写的操作(修改变量值)时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作时不会发生数据冲突问题。

在这里插入图片描述

案例:需求现在有100张火车票,有两个窗口同时抢火车票,请使用多线程模拟抢票效果。

package com.lanou.day12.lessonEx.ticket;

public class Station implements Runnable{
    private int  ticketNum = 100;
    public Station() {
    }
	public Station(int ticketNum) {
        this.ticketNum = ticketNum;
    }
    @Override
    public void run() {
        while (true){
            synchronized ("aa"){
                if (ticketNum > 0 ){
System.out.println(Thread.currentThread().getName() + "正在卖第" + (100-ticketNum+1) + "张票");
                    ticketNum--;
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }else {
                    System.out.println("票已卖光!");
                    break;
                }
            }
        }
    }
}

有两位电工,长期驻扎在一个小区作为物业,一个电工 A,上早班,上班时间是 6:00 ~ 18:00,另一位电工 B,上班时间为 18:00 ~ 6:00。这两位电工轮流在小区值班。某一天,在下午 17:55 的时候,电工 A 接到小区居民投诉:小区中的电压不稳,希望电工能够修复一下。虽然马上要到下班时间了,但是电工 A 还是决定去查看一下。为了爬上电线杆进行高空带电作业,于是电工 A 暂时把小区的电闸给关了,然后再到高空进行电压不稳的检查。

在 A 在工作过程中,不知不觉到了 18:01,电工 B 上班了。这个时候,电工 B 接到小区居民投诉:小区停电了。电工 B 也决定去查看一下。当他查看到电闸时,一下就明白了:怪不得小区停电呢,谁把闸关了啊。于是,电工 B 就把电闸打开了。于是,听到一声惨叫,A 从天上掉了下来„„

半个月以后,等 A 把伤养得差不多了以后,变电所决定解决吸取教训,争取再也不要发生这样的问题。那这个问题是怎么发生的呢?

我们可以把 A 和 B 当做是两个线程,而电闸就当做是临界资源。因此,这样就形成了两个线程共同访问临界资源,由于 A 检修电路时,“关掉电闸、爬上电线杆检修、打开电闸、恢复供电”是不可分割的原子操作,而一旦其中的某一步被打断,就有可能产生问题,这就是两个线程数据不一致的情况。

那怎么办呢?变电所决定,要解决这个问题,这就借助于一样东西:锁。在电闸上挂一个挂锁。平时没有问题时,锁是打开的;但是一旦有一个电工需要操作电闸的话,为了防止别人动电闸,他可以把电闸给锁上,并把唯一的钥匙随身携带。这样,当他进行原子操作时,由于临界资源被他锁上了,其他线程就访问不了这个临界资源,因此就能保证他的原子操作不被破坏。

2. 线程同步

2.1 什么是线程同步

并行指的是线程同时执行。

同步不是线程同时执行,而是线程不同时执行。同步本质指的是数据的同步。一般情况下,线程之间是相互独立,如果都去访问同一个变量,极有可能让这个数据变乱。如果不想让数据变乱,应在不让他们同时访问同一个变量。这个控制过程称为线程同步。

2.2 synchronized与同步代码块

Java 中采取了类似的机制,也采用锁来保护临界资源,防止数据不一致的情况产生。下面我们就来介绍Java 中的同步机制以及 synchronized 关键字。

在 Java 中,每个对象都拥有一个“互斥锁标记”,这就好比是我们说的挂锁。这个锁标记,可以用来分给不同的线程。之所以说这个锁标记是“互斥的”,因为这个锁标记同时只能分配给一个线程。

光有锁标记还不行,还要利用 synchronized 关键字进行加锁的操作。synchronized 关键字有两种用法,我们首先介绍第一种:synchronized + 代码块。

这种用法的语法如下:

synchronized(obj){

​ 代码块…

}

synchronized 关键字后面跟一个圆括号,括号中的是某一个引用,这个引用应当指向某一个对象。后面紧跟一个代码块,这个代码块被称为“同步代码块”。

这种语法的含义是,如果某一个线程想要执行代码块中的代码,必须要先获得 obj 所指向对象的互斥锁标记。也就是说,如果有一个线程 t1 要想进入同步代码块,必须要获得 obj对象的锁标记;而如果 t1线程正在同步代码块中运行,这意味着 t1 有着 obj 对象的互斥锁标记;而这个时候如果有一个 t2 线程想要访问同步代码块,会因为拿不到 obj 对象的锁标记而无法继续运行下去。

需要注意的是,synchronized 与同步代码块是与对象紧密结合在一起的,加锁是对对象加锁。例如下面的例子,假设有两个同步代码块:

synchronized(obj1){

代码块 1;

}

synchronized(obj1){

代码块 2;

}

synchronized(obj2){

代码块 3;

}

假设有一个线程 t1 正在代码块 1 中运行,那假设另有一个线程 t2,这个 t2 线程能否进入代码块 2 呢?能否进入代码块 3 呢?

由于 t1 正在代码块 1 中运行,这也就意味着 obj1 对象的锁标记被 t1 线程获得,而此时t2 线程如果要进入代码块 2,也必须要获得 obj1 对象的锁标记。但是由于这个标记正在 t1手中,因此 t2 线程无法获得锁标记,因此 t2 线程无法进入代码块 2。但是 t2 线程能够进入代码块 3,原因在于:如果要进入代码块 3 中,要获得的是 obj2对象的锁标记,这个对象与 obj1 不是同一个对象,此时 t2 线程能够顺利的获得 obj2 对象的锁标记,因此能够成功的进入代码块 3。

从上面这个例子中,我们可以看出,在分析、编写同步代码块时,一定要搞清楚,同步代码块锁的是哪个对象。只有把这个问题搞清楚了之后,才能正确的分析多线程以及同步的相关问题。

下面我们结合线程的状态转换,来考察一下 synchronized 关键字在程序运行中的作用。

首先,如果一个线程获得不了某个对象的互斥锁标记,这个线程就会进入一个状态:锁池状态。

如下图:

在这里插入图片描述

当运行中的线程,运行到某个同步代码块,但是获得不了对象的锁标记时,会进入锁池状态。在锁池状态的线程,会一直等待某个对象的互斥锁标记。如果有多个线程都需要获得同一个对象的互斥锁标记,则可以有多个线程进入锁池,而某个线程获得锁标记,执行同步代码块中的代码。

当对象的锁标记被某一个线程释放之后,其他在锁池状态中的线程就可以获得这个对象的锁标记。假设有多个线程在锁池状态中,那么会由操作系统决定,把释放出来的锁标记分配给哪一个线程。当在锁池状态中的线程获得锁标记之后,就会进入可运行状态,等待获得CPU 时间片,从而运行代码。

2.3 如何实现线程的同步?

同步代码块

synchronized(对象){ 
    共享资源//我们所谓的那个变量。 
}

示例代码:

public class SellWindow implements Runnable { 
    private int tickets = 100;
    private Object lock = new Object(); 
    @Override
    public void run() { 
        while(tickets > 0) { 
            String threadName = Thread.currentThread().getName(); 
            synchronized (lock) {
                tickets--;
                if(tickets >= 0) { 
                    System.out.println(threadName + "卖掉1张票,剩余" + tickets); 
                } 
            } 
        } 
    } 
}

同步代码块synchronized (对象),多个线程要公用同一个对象,才能真正意义上加上锁。对象没有特殊要求,可以是任何继承于Object类的对象。包括this

同步方法

被synchronized修饰的方法称为同步方法。

public class SellWindow3 implements Runnable {
    private int tickets = 100; 
    @Override
    public void run() {
        while(tickets > 0) {
           //method(); 
            method2(); 
        }
    }
    public void method() { 
        synchronized (this) {
            tickets--;
            String threadName = Thread.currentThread().getName();
            if(tickets >= 0) { 
                System.out.println(threadName + "卖掉1张票,剩余" + tickets); 
            }
        } 
    }
    public synchronized void method2() {
        tickets--; 
        String = Thread.currentThread().getName(); 
        if(tickets >= 0) { 
            System.out.println(threadName + "卖掉1张票,剩余" + tickets); 
        } 
    }
}

使用锁对象上锁和解锁

public class SellWindow4 implements Runnable { 
    private int tickets = 100;
    Lock lock = new ReentrantLock();
    @Override
    public void run() { 
        while (tickets > 0) {
            String threadName = Thread.currentThread().getName(); 
            lock.lock(); 
            tickets--;
            if (tickets >= 0) { 
                System.out.println(threadName + "卖掉1张票,剩余" + tickets);
            }
            lock.unlock(); 
        } 
    } 
}

线程同步小节

同步不是线程同时执行,而是线程不同时执行。同步本质指的是数据的同步。一般情况下,线程之间是相互独立,如果都去访问同一个变量,极有可能让这个数据变乱。如果不想让数据变乱,应不让他们同时访问同一个变量。这个控制过程称为线程同步。

在开发中,如果多个线程访问一个资源(某变量),为了保证数据的正确性,可以使用3种方式来实现线程同步:使用synchronized(){}代码块,使用synchronized方法,或者给共享资源加锁和解锁。

3. 线程通信

3.1 wait与notify

在 synchronized 关键字的作用下,还有可能产生新的问题:死锁。

考虑下面的代码,假设 a 和 b 是两个不同的对象。

synchronized(a){
    ...//1 
        synchronized(b){
    }
}
synchronized(b){
    ... //2 
        synchronized(a){ 
    } 
}

假设现在有两个线程,t1 线程运行到了//1 的位置,而 t2 线程运行到了//2 的位置,接下来会发生什么情况呢?

此时,a 对象的锁标记被 t1 线程获得,而 b 对象的锁标记被 t2 线程获得。对于 t1 线程而言,为了进入对 b 加锁的同步代码块,t1 线程必须获得 b 对象的锁标记。由于 b 对象的锁标记被 t2 线程获得,t1 线程无法获得这个对象的锁标记,因此它会进入 b 对象的锁池,等待 b 对象锁标记的释放。而对于 t2 线程而言,由于要进入对 a 加锁的同步代码块,由于a 对象的锁标记在 t1 线程手中,因此 t2 线程会进入a 对象的锁池。此时,t1 线程在等待 b 对象锁标记的释放,而 t2 线程在等待 a 对象锁标记的释放。由于两边都无法获得所需的锁标记,因此两个线程都无法运行。这就是“死锁”问题。

3.2 如何解决死锁问题?

在 Java 中,采用了 wait 和 notify 这两个方法,来解决死锁机制。

首先,在 Java 中,每一个对象都有两个方法:wait 和 notify 方法。这两个方法是定义在 Object 类中的方法。对某个对象调用 wait()方法,表明让线程暂时释放该对象的锁标记。

例如,上面的代码就可以改成:

synchronized(a){
    ...//1 
        a.wait(); 
    synchronized(b){
    } 
}
synchronized(b){ 
    //2
    synchronized(a){
        ... 
        a.notify(); 
    } 
}

这样的代码改完之后,在//1 后面,t1 线程就会调用 a 对象的 wait 方法。此时,t1 线程会暂时释放自己拥有的 a 对象的锁标记,而进入另外一个状态:等待状态。

要注意的是,如果要调用一个对象的 wait 方法,前提是线程已经获得这个对象的锁标记。如果在没有获得对象锁标记的情况下调用 wait 方法,则会产生异常。

由于 a 对象的锁标记被释放,因此,t2 对象可以获得 a 对象的锁标记,从而进入对 a加锁的同步代码块。在同步代码块的最后,调用 a.notify()方法。这个方法与 wait 方法相对应,是让一个线程从等待状态被唤醒。

那么 t2 线程唤醒 t1 线程之后,t1 线程处于什么状态呢?由于 t1 线程唤醒之后还要在对 a 加锁的同步代码块中运行,而 t2 线程调用了 notify()方法之后,并没有立刻退出对 a 加锁的同步代码块,因此此时t1 线程并不能马上获得 a 对象的锁标记。因此,此时,t1 线程会在 a 对象的锁池中进行等待,以期待获得 a 对象的锁标记。也就是说,一个线程如果之前调用了 wait 方法,则必须要被另一个线程调用notify()方法唤醒。唤醒之后,会进入锁池状态。线程状态转换图如下:

在这里插入图片描述

由于可能有多个线程先后调用 a 对象 wait 方法,因此在 a 对象等待状态中的线程可能有多个。而调用a.notify()方法,会从 a 对象等待状态中的多个线程里挑选一个线程进行唤醒。与之对应的,有一个notifyAll()方法,调用 a.notifyAll() 会把 a 对象等待状态中的所有线程都唤醒。

3. 3 线程通信

不同线程之间可以相互的发信号。这就是线程通信。之所以需要进行线程通信,是因为有些时候,一个线程的执行需要依赖另外一个线程的执行结果。在结果到来之前,让线程等待(wait),有了结果只之后再进行后续的操作。对于另外一个线程而言,计算完结果,通知(notify)一下处于等待状态的线程.

线程通信借助的是Object类的wait,notify,notifyAll方法。

wait作用是让当前线程阻塞,阻塞多久,取决于有没有其他线程唤醒它。

notify作用是唤醒处于wait状态的线程。必须是同一个监视器下的线程。

notifyAll作用是唤醒所有处于wait状态的线程。必须是同一个监视器下的线程。

一般情况下,多线程里会出现线程同步的问题,我们不但要进行线程通信,还要解决线程同步的问题。

3.4 wait与notify 应用:生产者-消费者模式

这是一个比较经典的多线程场景。有商品的时候,消费者才可以消费,没有商品的时候,消费者等待。商品库存充足的时候,生产者等待,库存不满的时候,生产者生产商品。

public class Saler {//售货员类 
    private int productCount = 10; //商品数量 
    public synchronized void stockGoods() { 
        if(productCount < 2000) {
            productCount++; 
        System.out.println(Thread.currentThread().getName() + "生产了1件商品, 库存是:" + productCount);
            this.notifyAll(); 
        }else { 
            System.out.println("库存满了"); 
            try {
                this.wait(); 
            } catch (InterruptedException e) { 
                e.printStackTrace();
            }   
       } 
    }
    public synchronized void sellGoods() {
        if(productCount > 0) {
            productCount--; 
System.out.println(Thread.currentThread().getName() + "购买了1件商品, 库存剩余:" + productCount); 
            this.notifyAll();
        }else { 
            System.out.println("库存不足"); 
            try {this.wait(); 
                } catch (InterruptedException e) { 
                // TODO Auto-generated catch block 
                e.printStackTrace();
            }
        } 
    } 
}

public class Productor implements Runnable{//生产者类 
    private Saler s; 
    public Productor(Saler s) {
        super(); 
        this.s = s;
    }
    @Override 
    public void run() { 
        while(true) {
            s.stockGoods();
        }
    }
}


public class Customer implements Runnable{//消费者类
    private Saler s; 
    public Customer(Saler s) { 
        super(); 
        this.s = s; 
    }
    @Override 
    public void run() { 
        while(true) {
            s.sellGoods();
        }
    }
}  

public class TestTread { 
    public static void main(String[] args) { //生产者-消费者模式。模拟生产和消费过程 
        Saler s = new Saler();
        Customer c = new Customer(s);
        Productor p = new Productor(s); 
        Thread t1 = new Thread(c, "客户1"); 
        t1.start(); 
        Thread t2 = new Thread(p,"厂家"); 
        t2.start(); 
        Customer c2 = new Customer(s); 
        Thread t3 = new Thread(c2, "客户2");
        t3.start(); 
    } 
}

4. 线程池

4.1 什么是线程池

水池:存放水的池子。

线程池:存放线程的池子。

Java中的线程池:是一个管理线程的池子。可以在需要的时候开辟线程,可以控制最大开辟的线程个数,可以在不需要的时候关闭线程,可以让任务排队执行。这些管理过程不需要我们干预,线程池能帮我们完成。我们所要做的就是往线程池中放任务。

4.2 为什么要有线程池?

多线程解决了任务并发问题,但是开辟和关闭线程很消耗系统的性能,开辟和关闭一个线程要处理很多细节,频繁的开辟和关闭线程会给系统增加很多开销。

线程池使用了重用的概念,可以控制线程开辟的数量,复用这些线程执行任务。这样就不用频繁的开辟和关闭线程了。

4.3 线程池使用场景及优势

线程池适合处理的任务:执行时间短、工作内容较为单一。

合理使用线程池带来的好处**:**

1)降低资源消耗:重复利用已创建的线程降低线程创建和销毁造成的开销

2)提高响应速度:当任务到达时,任务可以不用等待线程创建就能立即执行

3)提高线程的可管理性:可以统一对线程进行分配、调优和监控

4)提供更多强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池SchedulerThreadPoolExecutor,允许任务延期执行或定期执行

线程池使应用能更加充分利用CPU、内存、网络、IO等系统资源。线程的创建需要开辟虚拟机栈、本地方法栈、程序计数器等线程私有的内存空间。在线程销毁时需要回收这些系统资源。因此频繁的创建和销毁线程会浪费大量的系统资源,增加并发编程风险。另外,在服务器负载过大的时候,如何让新的线程等待或者友好地拒绝服务?这些都是线程本身无法解决的。所以需要通过线程池协调多个线程,并实现类似主次线程隔离、定时执行、周期执行等任务。

线程池的作用包括:

1):利用线程池管理并复用线程、控制最大并发数等

2):实现任务线程队列缓存策略和拒绝机制

3):实现某些与时间相关的功能,如定时执行、周期执行

4):隔离线程环境。通过配置两个或多个线程池,将一台服务器上较慢的服务和其他服务隔离开,避免各服务线程相互影响。

4.4 ThreadPoolExecutor及线程池各参数含义

ThreadPoolExecutor

UML类图:

在这里插入图片描述

ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。

ExecutorService接口增加了一些能力:

(1)扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;

(2)提供了管控线程池的方法,比如停止线程池的运行。

AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。

最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,

handler)

主要参数:

1)corePoolSize 线程池核心线程的大小

2)maximumPoolSize 线程池最大线程的大小

3)keepAliveTime 空闲线程的存活时间

4)BlockingQueue 用来暂时保存任务的工作队列

5)RejectedExecutionHandler 线程池已经关闭或者饱和(达到了最大线程数且工作队列已满),executor()方法将调用Handler

参数详细说明:

  1. corePoolSize

表示常驻核心线程数,如果大于0,则即使执行完任务,线程也不会被销毁(allowCoreThreadTimeOut为false)。因此这个值的设置非常关键,设置过小会导致线程频繁地创建和销毁,设置过大会造成浪费资源

  1. maximumPoolSize

表示线程池能够容纳的最大线程数。必须大于或者等于1。

  1. keepAliveTime

表示线程池中的线程空闲时间,当空闲时间达到keepAliveTime值时,线程会被销毁,避免浪费内存和句柄资源。在默认情况下,当线程池中的线程数大于corePoolSize时,keepAliveTime才起作用,达到空闲时间的线程会被销毁,直到只剩下corePoolSize个线程为止。但是当ThreadPoolExecutor的allowCoreThreadTimeOut设置为true时(默认false),核心线程超时后也会被回收。(一般设置60s)

  1. TimeUnit

表示时间单位,keepAliveTime的时间单位通常是TimeUnit.SECONDS

  1. BlockingQueue

表示缓存队列。

  1. threadFactory

表示线程工厂。它用来生产一组相同任务的线程。线程池的命名是通过给threadFactory增加组名前缀来实现的。在用jstack分析时,就可以知道线程任务是由哪个线程工厂产生的。

  1. handler

表示执行拒绝策略的对象。当超过workQueue的缓存上限的时候,就可以通过该策略处理请求,这是一种简单的限流保护。

提供了四个预定义的处理程序策略:

  1. 在默认 ThreadPoolExecutor.AbortPolicy ,处理程序会引发运行RejectedExecutionException 后排斥反应。

  2. 在 ThreadPoolExecutor.CallerRunsPolicy 中,调用 execute 本身的线程运行任务。 这提供了一个简单的反馈控制机制,将降低新任务提交的速度。

  3. 在 ThreadPoolExecutor.DiscardPolicy 中 ,简单地删除无法执行的任务。

  4. 在 ThreadPoolExecutor.DiscardOldestPolicy 中 ,如果执行程序没有关闭,则工作队列头部的任务被删除,然后重试执行(可能会再次失败,导致重复)。

  5. BlockingQueue

    表示缓存队列。

  6. threadFactory

    表示线程工厂。它用来生产一组相同任务的线程。线程池的命名是通过给threadFactory增加组名前缀来实现的。在用jstack分析时,就可以知道线程任务是由哪个线程工厂产生的。

  7. handler

    表示执行拒绝策略的对象。当超过workQueue的缓存上限的时候,就可以通过该策略处理请求,这是一种简单的限流保护。

    提供了四个预定义的处理程序策略:

    1. 在默认 ThreadPoolExecutor.AbortPolicy ,处理程序会引发运行

    RejectedExecutionException 后排斥反应。

    1. 在 ThreadPoolExecutor.CallerRunsPolicy 中,调用 execute 本身的线程运行任务。 这提供了一个简单的反馈控制机制,将降低新任务提交的速度。

    2. 在 ThreadPoolExecutor.DiscardPolicy 中 ,简单地删除无法执行的任务。

    3. 在 ThreadPoolExecutor.DiscardOldestPolicy 中 ,如果执行程序没有关闭,则工作队列头部的任务被删除,然后重试执行(可能会再次失败,导致重复)。

在这里插入图片描述

首先,所有的任务都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来的执行流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:

  1. 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。

  2. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。

  3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。

  4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。

  5. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

  6. 线程池中存活的线程执行完当前任务后,会在循环中反复从BlockingQueue队列中获取任务来执行

4.5 线程池工具类

Exectors 是线程池工具类,可以帮我们快速构建线程池。

三种常见的线程池:

  1. 固定线程个数的线程池

  2. 不限线程个数的线程池

  3. 单个线程的线程池(串行任务池)

public class TestExecutors { 
    public static void main(String[] args) { 
        //创建一个固定个数的线程池 
        ExecutorService es = Executors.newFixedThreadPool(3);
        //创建一个不限容量的线程池 
   ExecutorService es = Executors.newCachedThreadPool();
        //创建一个只有一条线程的线程池 
         ExecutorService es = Executors.newSingleThreadExecutor(); 
        es.submit(new Runnable() { 
            @Override 
            public void run() { 
 System.out.println(Thread.currentThread().getName()); 
            }
        }); 
    } 
}

5. 课堂总结

总结使用线程池需要注意以下几点

  1. 合理设置各类参数,应根据实际业务场景来设置合理的工作线程数

  2. 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程

  3. 创建线程或线程池请指定有意义的线程名称,方便出错时回溯

  4. 自定义线程池不允许使用Executors,而是通过ThreadPoolExecutor的方式来创建,这样的处理方式能更加明确线程池的运行规则,规避资源耗尽的风险。

rService es = Executors.newCachedThreadPool();
//创建一个只有一条线程的线程池
ExecutorService es = Executors.newSingleThreadExecutor();
es.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
}


## 5. 课堂总结

**总结使用线程池需要注意以下几点**: 

1. 合理设置各类参数,应根据实际业务场景来设置合理的工作线程数

2. 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程

3. 创建线程或线程池请指定有意义的线程名称,方便出错时回溯

4. 自定义线程池不允许使用Executors,而是通过ThreadPoolExecutor的方式来创建,这样的处理方式能更加明确线程池的运行规则,规避资源耗尽的风险。

另外,线程池中的线程数量不是越多越好,具体的数量需要评估每个任务的处理时间,以及当前计算机的处理器能力和数量。使用的线程过少,无法发挥处理器的性能;使用的线程过多,将会增加上下文切换的开销,反而起到相反的作用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

王斐

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

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

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

打赏作者

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

抵扣说明:

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

余额充值