并发编程【待完善】
1.线程
1.1 上下文切换
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
1.1.1 时间片
即使是单核CPU也能运行多线程,多个线程会争抢CPU的执行权,也叫时间片
(通常是几十毫秒),CPU会通过时间片分配算法在之间来回切换,让用户
感觉这些程序像是在并行执行一样。
1.1.2 并发与并行的概念?并行一定比串行更快吗?
并行:多个CPU实例或者多台机器同时执行一段处理逻辑
并发:通过CPU调度算法,让用户看上去是同时执行的,在CPU层面不是同时。
相同的程序,并行不一定比串行快,因为CPU在由一个线程切换到另一个线程,需要
保留该线程当前的执行状态(比如执行到哪一行,有哪些变量和数据),在下次切换回
来继续执行该线程时可以恢复到原来的状态,这个保存和恢复会消耗额外的时间
1.1.3 如何避免频繁的上下文切换
通常我们以如下的纬度来处理:
1)减轻锁的粒度
例如对数据进行切分,分段id哈希取模,每个线程只操作一段数据,
就是concurrentHashMap1.7的分段锁的做法
2)CAS无锁化编程
synchronized的获取锁和释放锁,会引起上下文切换,可以用CAS操作
来代替原子性,但是频繁的cas也会消耗CPU
3)使用最少的线程
线程的创建和销毁都要消耗系统资源,所以尽量使用池化技术来管理线程
同时也避免了CPU在大量线程之间切换的问题
1.2 死锁
如果出现死锁了,那么对于应用程序而言是灾难性的(无法响应)
1.2.1 形成死锁的四个必要条件
互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞
1.2.2 如何避免死锁
1) 避免一个线程获取多个锁
2) 避免一个锁占用多个资源,这边可以借鉴ConcurrentHashMap分段锁的思想
3) 避免获取不到锁就无限期等待
使用Lock.tryLock(Timeout)代替内部锁机制,一定时间内获取不到就返回
1.3 资源限制带来的挑战
1.3.1 软件层面
池化复用:数据库连接数,socket连接
1.3.2 硬件层面
带宽有限的情况下,再怎么并发编程,也无法使下载和上传速度超过带宽速度
CPU,内存有限的情况下,可以采取集群的方式来处理
硬盘读写速度有限,将数据放到内存中,如redis,elasticsearch。
1.4 多线程
1.4.1 线程与进程概念
进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
1.4.2 并行和并发的概念
并行:多个CPU实例或者多台机器同时执行一段处理逻辑
单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。
并发:通过CPU调度算法,让用户看上去是同时执行的,在CPU层面不是同时。
多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
1.4.3 为什么要使用多线程?优与劣
可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
优点:
1)充分的利用CPU资源,如果只有一个线程的话,第二个任务必须等第一个任务完成之后才能进行。
2)进程之间无法共享数据,但是线程可以
3)创建进程需要为这个进程分配系统资源,创建线程的代价小
劣势:
1)线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
2)多线程需要协调和管理,所以需要 CPU 时间跟踪线程;
3)线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
1.4.4 并发编程三要素?java怎么保证多线程运行安全?
并发编程三要素(线程的安全性问题体现在):
原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
1.4.4.1 出现线程安全问题的原因?
线程切换带来的原子性问题
缓存导致的可见性问题
编译优化带来的有序性问题
1.4.4.2 解决办法
JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
synchronized、volatile、LOCK,可以解决可见性问题
Happens-Before 规则可以解决有序性问题
1.4.5 线程状态
java的线程生命周期大体可以分成五种状态
1)新建(new): 新建了一个线程对象
2)可运行(runnable): 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
3)运行(running): 可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
4)阻塞(blocked): 阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中
同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
5)死亡(dead):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
1.4.6 多线程实现的手段
继承 Thread 类;
实现 Runnable 接口;
实现 Callable 接口;
使用 Executors 工具类创建线程池
1.4.6.1 继承Thread类
package com.Allen.test; import java.util.concurrent.TimeUnit; public class testThread extends Thread{ public static void main(String[] args) { testThread t1=new testThread(); testThread t2=new testThread(); t1.start(); t2.start(); } public void run(){ System.out.println("start"); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end"); } }
1.4.6.2 继承runnable接口
package com.Allen.test; import java.util.concurrent.TimeUnit; public class testRunnable { public static void main(String[] args) { testAllen th1=new testAllen(); for(int i=0;i<5;i++){ Thread t1=new Thread(th1); t1.start(); } } } class testAllen implements Runnable{ @Override public void run() { System.out.println("start"); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end"); } }
1.4.6.3 future callable
package com.Allen.test; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; public class testFuture { public static void main(String[] args) throws InterruptedException, ExecutionException { futureTask task=new futureTask(); // //线程池 单线程的线程池 // ExecutorService service=Executors.newCachedThreadPool(); // Future<Integer> future=service.submit(task); // //说到下面这个方法就要说起线程池的状态 四个种状态 // service.shutdown(); FutureTask<Integer>future=new FutureTask<>(task); Thread t1=new Thread(future); t1.start(); System.out.println("run task ...."); TimeUnit.SECONDS.sleep(1); System.out.println("result : "+future.get()); } } class futureTask implements Callable<Integer>{ @Override public Integer call() throws Exception { TimeUnit.SECONDS.sleep(2); int result=0; //模拟一个庞大的计算 for(int i=0;i<100;i++){ for(int j=0;j<i;j++){ result+=j; } } return result; } }