目前CPU 的运算速度已经达到百亿次/秒 ,甚至更高的量级,家用电脑维持操作系统正常运行的进程也会有数十个,线程更是数以百计, 所以,在现实场景中,为了提高生产效率和高效的完成任务,处处均采用多线程和并发的运作方式.
并发(Concurrency) 是指在某个时间段内,多任务交替处理的能力 . 所谓不患寡而患不均,每个CPU不可能只顾着执行某个进程,让其他线程一直处于等待状态, 所以CPU把可执行时间均匀分成若干份,每个进程执行一段时间后,记录当前的工作状态,释放相关的执行资源并进入等待状态,让其他进程抢占CPU 资源. 并行(Parallelism) 是指同时处理多任务的能力 . 目前,CPU已经发展为多核,可以同时执行多个互不依赖的指令及执行块. 并发与并行两个概念非常容易混淆,他们的核心区别在于进程是否同时执行. 以KTV唱歌为例,并行指的是有多少人可以使用话筒同时唱歌; 并发指的是同一个话筒被多个人轮流使用 . 并发与并行的目标都是尽可能快的执行完所有任务 . 以医生为例, 某个科室有个专家同时出诊,这就是两个并行任务; 其中一个医生,时而问诊,时而查看化验单,然后继续问诊,突然又中断去处理病人的咨询,这就是并发. 在并发环境下,由于程序的封闭性被打破,出现了以下特点:
并发程序之间有相互制约的关系. 直接制约体现为一个程序需要另一个程序的计算结果; 间接制约体现为多个程序竞争共享资源,如处理器, 缓冲区等.并发程序的执行过程是断断续续的 . 程序需要记忆现场指令及执行点 .当并发数设置合理并且CPU 拥有足够的处理能力时, 并发会提高程序的运行效率 .线程是CPU 调度和分派的基本单位,为了更充分地利用CPU 资源,一般都会使用多线程进行处理 . 多线程的作用是提高任务的平均速度,但是会导致程序可理解性变差,编程难度加大 . 例如,楼下有一车砖头需要工人搬到21楼,如果10个人一起搬,速度一定会比1个人搬要快,完成任务的总时间会极大减少,但是论单次的时间成本,由于楼梯交会等因素10人比1个人要慢 , 如果无限的增加人手,比如10000 人参与搬砖时,反而会因为楼道拥堵不堪变得更慢,所以合适的人数才会使工作效率最大化 . 同理,合适的线程数才能让CPU 资源被充分利用, 如图所示: 这是计算机的资源监视数据,红色箭头指向的PID 就是进程ID ,绿色箭头表示Java 进程运行着30 个线程.
线程可以拥有自己的操作栈, 程序计数器,局部变量表等资源,它与同一进程内的其他线程共享该进程的所有资源. 线程在生命周期内存在多种状态. 有 NEW(新建状态)、RUNNABLE(就绪状态)、RUNNING(运行状态)、BLOCKED(阻塞状态)、DEAD(终止状态)五种状态。 (1) NEW, 即新建状态,是线程被创建且未启动的状态 。 创建线程的方式有三种: 第一种是继承自Thread 类; 第二种是实现 Runnable 接口; 第三种是实现 Callable 接口; 相比第一种,,推荐使用第二种方式,因为继承自Thread 类往往不符合里氏代换原则,而实现Runnable 接口可以使编程更加灵活,对外暴露的细节比较少,让使用者专注于实现线程的run()方法上。第三种Callable 接口的call()声明如下:
V call()throws Exception;由此可见,Callable 与Runable 有两种不同: 第一,可以通过call()获取返回值。前两种方式都有一个共同的缺陷,即在任务执行完成后,无法直接获取执行结果,需要借助共享变量等获取,而Callable 和Future 则很好的解决了这个问题; 第二,call()可以抛出异常。而Runable 只有通过 setDefaultUncaughtExceptionHandler()的方式才能在主线程中捕获到子线程异常。
(2)RUNNABLE,即就绪状态,是调用start() 之后运行之前的状态。 线程start()不能被多次调用,否则会抛出 IllegalStateException 异常。
(3)RUNNING ,即运行状态,是run()正在执行时新城的状态。 线程可能会由于某些因素而退出RUNNING,如时间、异常、锁、调度等。
(4)BLOCKED,即阻塞状态,进入此状态,有以下种情况:
同步阻塞: 锁被其他线程占用。主动阻塞: 调用Thread 的某些方法,主动让出CPU 执行权,比如sleep()、join()等。等待阻塞: 执行了wait()。(5)DEAD,即终止状态,是run()执行结束,或因异常退出后的状态,此状态不可逆转。
在计算机的处理过程中,因为各个线程轮流占用CPU 的计算资源,可能会出现某个线程尚未执行完就不得不中断的情况,容易导致线程不安全。 例如,在服务端某个高并发业务共享用户数据,首先A 线程执行用户数据的查询任务,但数据尚未返回就退出CPU 时间片;然后B线程抢占了CPU 资源执行并覆盖了该用户数据,最后A 线程返回到执行现场,直接将B 线程处理后的用户数据返回给前端,导致页面显示数据错误。 为了保证线程安全,在多个线程并发地竞争共享资源时,通常采用同步机制协调各个线程的执行,以确保得到正确的结果。
线程安全问题只在多线程环境下才出现,单线程串行执行不存在此问题, 保证高并发场景下的线程安全,可以从以下四个维度考量:
数据单线程内可见。 单线程总是安全的,通过限制数据仅在单线程内可见,可以避免数据被其他线程篡改。最典型的就是线程局部变量,它存储在独立虚拟机栈帧的局部变量表中,与其他线程毫无瓜葛。ThreadLocal 就是采用这种方式来实现线程安全的 。只读对象。 只读对象总是安全的。它的特性是允许复制、拒绝写入。最典型的只读对象有String、Integer等。一个对象想要拒绝任何写入,必须要满足以下条件: 使用final 关键字修饰类,避免被继承;使用private final 关键字避免属性被中途修改;没有任何更新方法;返回值不能为可变对象;线程安全类。 某些线程安全类的内部有非常明确的线程安全机制。比如StringBuffer 就是一个线程安全类,它采用synchronized 关键字来修饰相关方法。
同步与锁机制。 如果想要对某个对象进行并发更新操作,但又不属于上述三类,需要开发工程师在代码中实现安全的同步机制。 虽然这个机制支持的并发场景很有价值,但非常复杂且容易出现问题。
线程安全的核心理念就是“要么只读,要么加锁” 。合理利用好JDK 提供的并发包,往往可以化腐朽为神奇。Java 并发包(java.uril.concurrent, JUC) 中大多数类注解都写有:@author Doug Lea . 并发包主要分为以下几个类族:
线程同步类。 这些类是线程间的协调更加容易,支持了更加丰富的线程协调场景,逐步淘汰了使用Object的 wait()和notify()进行同步的方式。主要代表为CountDownLatch、Semaphore、CyclicBarrier 等。
并发集合类。 集合并发操作的要求是执行速度快,提取数据准。最著名的类非ConcurrentHashMap 莫属,它不断的优化,有刚开始的锁分段到后来的CAS,不断地提升并发性能。其他还有ConcurrentSkipListMap、CopyOnWriteArrayList、BlockingQueue 等。
线程管理类。 虽然Thread 和ThreadLocal 在JKD1.0 就已经引入,但是真正把Thread 发扬光大的是线程池。 根据实际场景的需要,提供了多种创建线程池的快捷方式,如使用Executors 静态工厂或者使用 ThreadPoolExecutor 等。另外,通过ScheduleExecutorService 来执行定时任务。
锁相关类。 锁以Lock 接口为核心,派生出在一些实际场景中进行互斥操作的锁相关类。最有名的是ReentrantLock 。锁的很多概念在弱化,是因为锁的实现在各种场景中已经通过类库封装进去了。