回顾线程与线程池

Posted SSimeng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了回顾线程与线程池相关的知识,希望对你有一定的参考价值。

1.线程与进程

  • 进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。 进程在执行过程中拥有独立的内存单元,一个进程可以有多个线程,而多个线程共享内存资源,减少切换次数,效率更高。
  • 线程是CPU调度和分配的基本单位,同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

2.线程的状态

  1. 创建:线程一旦被创建就进入了创建状态。
  2. 就绪:调用start()方法后,线程进入了就绪状态。

除过start()方法外,还有其他三种方式可以使线程进入就绪状态:

  • 本来处于阻塞状态,后来阻塞解除;
  • 如果运行的时候调用 yield() 方法,避免一个线程占用资源过多,中断一下,会让线程重新进入就绪状态。注意,如果调用 yield() 方法之后,没有其他等待执行的线程,此线程就会马上恢复执行
  • JVM 本身将本地线程切换到了其他线程,那么这个线程就进入就绪状态。
  1. 运行:当CPU选定了一个就绪状态的线程执行,这时候线程就进入了运行状态,线程真正开始执行线程体的具体代码块,基本是 run() 方法。一定是从 就绪状态 - > 运行状态,不会从阻塞到运行状态的。

  2. 阻塞:阻塞状态指的是代码不继续执行,而在等待,阻塞解除后,重新进入就绪状态。

阻塞的方法:

  • sleep()方法,是Thread的方法,不释放资源在睡眠的,可以限制等待多久;
  • wait() 方法,和 sleep() 的不同之处在于,是不占用资源的,限制等待多久;
  • join() 方法,加入、合并或者是插队,这个方法阻塞线程到另一个线程完成以后再继续执行;
  • 有些 IO 阻塞,比如 write() 或者 read() ,因为IO方法是通过操作系统调用的。
  1. 死亡:线程体的代码执行完毕或者中断执行。

死亡方式:

  • stop() 和 destroy() 但是都不推荐使用,jdk里也写了已过时。
  • 自然死亡:这个线程体里调用执行完了就完了。
  • 如果不能自然死亡:加一些终止变量,然后用它作为run的条件,这样,外部调用的时候根据时机,把变量设置为false。

3.线程的创建方式

线程有4种创建方式

  • 继承Thread类创建线程
  • 实现Runnable接口创建线程
  • 使用Callable和Future创建线程
  • 使用线程池例如用Executor框架
  1. 继承Thread类创建线程的步骤:
  • 定义一个类继承Thread类,并重写Thread类的run()方法,run()方法的方法体就是线程要完成的任务,因此把run()称为线程的执行体;
  • 创建该类的实例对象,即创建了线程对象;
  • 调用线程对象的start()方法来启动线程;
  1. 实现Runnable接口创建线程
  • 定义一个类实现Runnable接口;
  • 创建该类的实例对象obj;
  • 将obj作为构造器参数传入Thread类实例对象,这个对象才是真正的线程对象;
  • 调用线程对象的start()方法启动该线程;
  1. 使用Callable和Future创建线程
  • 创建Callable接口实现类,并实现call()方法,该方法将作为线程执行体,且该方法有返回值,再创建Callable实现类的实例;
  • 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
  • 使用FutureTask对象作为Thread对象的target创建并启动新线程;
  • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
  1. 通过线程池创建
    通过Executors的以上四个静态工厂方法获得 ExecutorService实例,而后调用该实例的execute(Runnable command)方法即可。一旦Runnable任务传递到execute()方法,该方法便会自动在一个线程上

4.start()和run()方法的区别

  • start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行操作的, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
  • run()方法当作普通方法的方式调用。程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码; 程序中只有主线程——这一个线程, 其程序执行路径还是只有一条, 这样就没有达到写线程的目的。

5.线程池的定义

  • 线程池的概念

线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象;

  • 线程池的好处

1:线程和任务分离,提升线程重用性;
2:控制线程并发数量,降低服务器压力,统一管理所有线程;
3:提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间;

6.线程池的创建

newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

public class Test1 {
    public static void main(String[] args) {
        //1.创建可缓存的线程池,可重复利用
        ExecutorService newExecutorService = Executors.newCachedThreadPool();
        //创建了10个线程
        for (int i = 0; i < 10; i++) {
            int temp = i;
            newExecutorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("threadName;"+Thread.currentThread().getName()+",i"+temp);
                }
            });
        }

    }
}

newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

public class Test2 {
    public static void main(String[] args) {
        //1.创建可固定长度的线程池
        ExecutorService newExecutorService = Executors.newFixedThreadPool(3);
        //创建了10个线程
        for (int i = 0; i < 10; i++) {
            int temp = i;
            newExecutorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("threadName;"+Thread.currentThread().getName()+",i"+temp);
                }
            });
        }

    }
}

newScheduledThreadPool 创建一个定时线程池,支持定时及周期性任务执行。

public class Test3 {
    public static void main(String[] args) {
        //1.创建可定时线程池
        ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
        for (int i = 0; i < 10; i++) {
            final int temp = i;
            newScheduledThreadPool.schedule(new Runnable() {
                public void run() {
                    System.out.println("i:" + temp);
                }
            }, 3, TimeUnit.SECONDS);//表示延迟3秒执行
        }

    }
}

newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

public class Test001 {
    public static void main(String[] args) {
        //1.创建单线程
        ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            newSingleThreadExecutor.execute(new Runnable() {

                @Override
                public void run() {
                    System.out.println("index:" + index);
                    try {
                        Thread.sleep(200);
                    } catch (Exception e) {
                        // TODO: handle exception
                    }
                }
            });
        }
        newSingleThreadExecutor.shutdown();
    }
}

7.线程池的核心参数

public ThreadPoolExecutor(int corePoolSize, //核心线程数量
                              int maximumPoolSize,//     最大线程数
                              long keepAliveTime, //       最大空闲时间
                              TimeUnit unit,         //        时间单位
                              BlockingQueue<Runnable> workQueue,   //   任务队列
                              ThreadFactory threadFactory,    // 线程工厂
                              RejectedExecutionHandler handler  //  饱和处理机制
	) 
  1. corePoolSize 线程池核心线程大小
    线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。
  2. maximumPoolSize 线程池最大线程数量
    一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
  3. keepAliveTime 空闲线程存活时间
    一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
  4. unit 空闲线程存活时间单位
  5. workQueue 工作队列
  6. threadFactory 线程工厂
    创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
  7. handler 拒绝策略
    当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:

①CallerRunsPolicy
该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
②AbortPolicy
该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
③DiscardPolicy
该策略下,直接丢弃任务,什么都不做。
④DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列

8.线程池的工作原理

1)当提交一个新任务到线程池时,线程池判断corePoolSize线程池是否都在执行任务,如果有空闲线程,则从核心线程池中取一个线程来执行任务,直到当前线程数等于corePoolSize;

2)如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;

3)如果阻塞队列满了,那就创建新的线程执行当前任务,直到线程池中的线程数达到maxPoolSize,这时再有任务来,由饱和策略来处理提交的任务

9.sleep()、wait()、yield()、join()

  • sleep():需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是sleep()方法不会释放“锁标志”,也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据。
  • wait():与sleep的不同之处在于,wait()方法会释放对象的“锁标志”。当调用某一对象的wait()方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用了notify()方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的notifyAll()方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池。
  • yield():和sleep()方法类似,也不会释放“锁标志”,区别在于,它没有参数,即yield()方法只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,另外yield()方法只能使同优先级或者高优先级的线程得到执行机会,这也和sleep()方法不同。
  • join():会使当前线程等待调用join()方法的线程结束后才能继续执行。

10.synchronized()和Lock的区别

  1. lock是一个接口,是java写的控制锁的代码,而synchronized是java的一个内置关键字,synchronized是托管给JVM执行的;
    synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
    lock:一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。

  2. synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)

  3. lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;

  4. Lock可以尝试获取锁,synchronized获取不到锁只能一直阻塞
    synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;

  5. synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、等待时可中断、可判断、可公平可非公平

10.LockSupport

LockSupport是用于创建锁和其他同步类的线程阻塞基本原语。

  • 某个线程调用LockSupport.park,如果对应的“许可证”可用,则此次调用立即返回,否则线程将会阻塞直到中断发生、超时或者许可证状态变为可用。
  • 线程调用LockSupport.unpark 可以使许可证变得可用。许可证只有一个,不会累积,多次调用unpark没什么用
  • 调用一次park
    如果count=0,阻塞,等待count 变成1
    如果count=1,修改count=0,并且直接运行,整个过程没有阻塞
  • 调用一次unpark
    如果count=0,修改count=1
    如果count=1,保持count=1
  • 多次连续调用unpark 效果等同于一次
    所以整个过程即使你多次调用unpark,他的值依然只是等于1,并不会进行累加

以上是关于回顾线程与线程池的主要内容,如果未能解决你的问题,请参考以下文章

回顾线程与线程池

回顾线程与线程池

newCacheThreadPool()newFixedThreadPool()newScheduledThreadPool()newSingleThreadExecutor()自定义线程池(代码片段

线程池与并行度

日常回顾——Java线程池123

一起Talk Android吧(第三百七十回:多线程之线程池回顾)