一、多线程基础理论-概念_特性_分类_状态_线程池_线程数配置

1.1 线程概念
    从以下4个方面理解
    1)线程是进程中执行运算的最小单位,每一个线程是进程中的一条执行路径。是被系统独立调度和分派的基本单位。
    2)线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源
    3)一个线程可以创建和撤消另一个线程
    4)同一进程中的多个线程之间可以并发执行。
1.2 线程和进程有区别
    线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。
1.3 使用多线程的优缺点
    优点:
    1)发挥多核CPU的优势
    2)防止阻塞:单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率,多线程时,一条线程的阻塞,不会影响其他任务的执行。
    3)便于建模:一个大的任务可以通过将其分解成多个小任务,分别建立程序模型,并通过多线程分别运行这几个任务,就会变得简单很多。
    缺点:
    1)等候使用共享资源时造成程序的运行速度变慢
    2)对线程进行管理要求额外的cpu开销
    3)可能出现线程死锁情况。即较长时间的等待或资源竞争以及死锁等症状。

1.4 线程的三大特性与分类(示例1)
线程的三大特性
1)原子性:
    即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。一个很经典的例子就是银行账户转账问题:
比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。这2个操作必须要具备原子性才能保证不出现一些意外的问题。
2)可见性:
    当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。
3)有序性:
    程序执行的顺序按照代码的先后顺序执行。一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的
线程的分类
java中有两种线程,一种是用户线程(也称为非守护线程),另一种是守护线程(daemon thread)。
1)用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止。
2)守护线程是个服务线程,准确来说就是服务其他线程的线程。当进程不存在或主线程停止,守护线程也会被停止。 使用setDaemon(true)方法设置为守护线程。
例如:main线程和垃圾回收线程,就是最典型的守护线程。即:主线程挂了,守护线程也会停止

public class DaemonThread {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            int i=0;
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(100);
                    } catch (Exception e) {
                        // TODO: handle exception
                    }
                    System.out.println("我是子线程..."+i++);
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(100);
            } catch (Exception e) {

            }
            System.out.println("我是主线程--->"+i);
        }
        System.out.println("主线程执行完毕!");
    }

}
View Code

运行结果为:

我是主线程--->0
我是子线程...0
我是主线程--->1
我是子线程...1
我是主线程--->2
我是子线程...2
我是主线程--->3
我是子线程...3
我是主线程--->4
我是子线程...4
我是主线程--->5
我是子线程...5
我是主线程--->6
我是子线程...6
我是主线程--->7
我是子线程...7
我是主线程--->8
我是子线程...8
我是主线程--->9
主线程执行完毕!
我是子线程...9
View Code

1.5 线程的状态
  线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态:
1)新建状态
  当用new操作符创建一个线程时, 例如new Thread(r),线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时, 程序还没有开始运行线程中的代码。
2)就绪状态
  一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。
3)运行状态
  当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.    
4)阻塞状态
  线程运行过程中,可能由于各种原因进入阻塞状态:
  1>线程通过调用sleep方法进入睡眠状态;
  2>线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
  3>线程试图得到一个锁,而该锁正被其他线程持有;
  4>线程在等待某个触发条件;
5)死亡状态
  有两个原因会导致线程死亡:
  1> run方法正常退出而自然死亡;
  2>一个未捕获的异常终止了run方法而使线程猝死;
     为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.
1.6 线程池
  Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。
线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。
如果一个线程的时间非常长,就没必要用线程池了(不是不能作长时间操作,而是不宜。),况且我们还不能控制线程池中线程的开始、挂起、和中止。
1.7 线程池原理剖析
  提交一个任务到线程池中,线程池的处理流程如下:
1)判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2)线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3)判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

1.8 如何合理的配置线程数
  要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
1)任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
2)任务的优先级:高,中和低。
3)任务的执行时间:长,中和短。
4)任务的依赖性:是否依赖其他系统资源,如数据库连接。
  任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。
总结:
  CPU密集型时:任务可以少配置线程数,大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务
  IO密集型时:大部分线程都阻塞,故需要多配置线程数,2*cpu核数
操作系统之名称解释:
  某些进程花费了绝大多数时间在计算上,而其他则在等待I/O上花费了大多是时间,
  前者称为计算密集型(CPU密集型)computer-bound,后者称为I/O密集型,I/O-bound。
1.9 线程安全理解
  个人理解:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的 。线程安全性包括2个方面:原子性与可见性。
这个问题有值得一提的地方,就是线程安全也是有几个级别的:
1)不可变
像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用。说明:
  设置不可变对象的原因:因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误.此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读取数据时不会有任何问题.我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象.
2)绝对线程安全
  不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
3)相对线程安全
  相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是 fail-fast机制 。
4)线程非安全
ArrayList、LinkedList、HashMap等都是线程非安全的类

 

posted @ 2018-09-05 21:21  爱笑的berg  阅读(214)  评论(0编辑  收藏  举报