- 程序,进程,线程
- 程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念;
- 进程是执行程序的一次执行过程,是一个动态的概念,是系统资源分配的单位;
- 通常在一个进程中可以包含若干个线程,线程是CPU调度和执行的单位;
- 若是单核cpu,则多线程是模拟出来的,在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,由于切换的块,就有同时执行的错觉;而 真正的多线程是指有多个cpu,即多核;
- 一些概念
- 线程就是独立的执行路径;
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
- main()为主线程,为系统的入口,用于执行整个程序;
- 在一个进程中,若开辟了多个线程,线程的运行是由调度器安排调度的,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的;
- 对同一份资源进行操作时,会存在资源抢夺的问题,需要加入并发的控制;
- 线程会带来额外的开销,如cpu调度时间,并发控制开销;
- 每个线程在自己的工作内存交互,内存控制不当回造成数据不一致;
- 创建线程的方法
继承Thread类:重写run()方法,调用start()方法开启线程,线程开启不一定立即执行,由cpu调度执行;由这种方法创建线程,子类继承Thread类具备多线程能力,但由于OOP单继承局限性不建议使用;
实现Runnable()接口:创建一个类实现Runnable接口的类,重写run()方法,创建实现类的对象,将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象,通过Thread类的对象调用start()方法,推荐使用此方法,
方便同一个对象被多个线程使用;
实现Callable接口,需要返回值类型,重写call方法,需要抛出异常,创建目标对象,创建执行服务,提交执行,获取结果,关闭服务。 - 静态代理:静态代理是定义父类或者接口,然后被代理对象(即目标对象)与代理对象一起实现相同的接口或者继承相同父类。代理对象与目标对象事项相同的接口,然后通过调用相同的方法来调用目标对象的方法;
优点:可不修改目标对象的功能,通过代理对象对目标功能扩展;
缺点:由于实现一样的接口,会有很多代理类,一旦接口增加方法,目标对象与代理对象都要维护。 - 函数式接口:任何接口,如果只包含唯一一个抽象方法,那么他就是一个函数式接口,对于函数式接口,就可以通过lambda表达式来创建该接口的对象;
- 线程的状态:
- 创建:程序使用new关键字创建了一个线程之后,该线程就处于一个新建状态(初始状态);
- 就绪:当线程对象调用了Thread.start()方法之后,该线程处于就绪状态;
- 运行:就绪之后,若抢到了cpu的资源分配,就进入了运行状态;
- 阻塞:线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态,阻塞状态可分为等待阻塞,同步阻塞和其他阻塞;
- 死亡:线程正常结束或者抛出未补货的异常都可以结束线程;
- 用sleep模拟倒计时
public class TestCountDown {
public static void main(String[] args) {
down();
}
public static void down(){
int num=10;
while (true){
try {
Thread.sleep(1000);
System.out.print(num--);
if(num<=0){
break;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println();
}
}
}
- yield():礼让线程,让当前正在执行的线程暂停,但不阻塞,将线程从运行状态转为就绪状态,让cpu重新调度,礼让不一定成功。
- join():使调用join()方法的线程进入等待池并等待线程执行完毕后才会被唤醒,并不影响同一时刻处在运行状态的其他线程。
- 观测线程的状态:thread.getState();
- 线程的优先级:优先级用数字表示,范围从1~10,用setPriority()设置优先级,优先级的设定建议在start()调度前,优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看cpu的调度;
- 用户线程和守护线程:守护线程(Daemon Thread)也被称之为后台线程或服务线程,守护线程是为用户线程服务的,当程序中的用户线程全部执行结束之后,守护线程也会跟随结束。
可以通过 Thread.setDaemon(true) 方法将线程设置为守护线程;默认情况下我们创建的线程或线程池都是用户线程,gc就是守护线程。 - 线程同步:由于统一进程的多个线程共享同一块存储空间,会有访问冲突的问题,为了保证数据被访问时的正确性,在访问的同时加入锁机制,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可。
- 同步方法默认用this或者当前类class对象作为锁;同步代码块选择会发生同步问题的部分代码进行锁住,锁会变化的对象。
- 产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用;
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺;
- 循环等待条件:若干进程形成一种头尾相接的循环等待资源关系;
- synchronized与lock对比
- lock是显示锁(手动开启与关闭),synchronized是隐式锁,出了作用域自动释放;
- lock只有代码块锁,synchronized有代码块锁和方法锁;
- 使用lock锁,JVM将话诶较少的时间来调度线程,性能更好,并有更好的扩展性;
- 优先使用顺序:lock,同步代码块,同步方法;
- 线程通信问题:
- wait():表示线程一直等待,知道其他线通知,与sleep不同,会释放锁;
- wait(long timeout):指定等待的毫秒数;
- notify():唤醒一个处于等待状态的线程;
- notifyAll():唤醒同一个对象上所有调用wait()方法的线程,优先级高的线程有限调度;
- 生产者消费者问题:是多线程同步问题的经典案例。也就是两个想爱你成在实际运行中会有互相通知的问题,解决这个问题可以利用管程法(缓冲区法)或者信号灯法;
- 为什么要有线程池?
经常创建和销毁线程会使用特别大的资源,尤其是并发情况下,对性能影响很大,因此要提前创建好多个线程,放入线程池中,使用获取,用完放回,高效利用。
java通过Executors提供四种线程池:分别为:工厂方法 corePoolSize maximumPoolSize keepAliveTime workQueue 应用场景 newCachedThreadPool 0 Integer.MAX_VALUE 60s SynchronousQueue 执行数量多,耗时少的线程任务 newFixedThreadPool nThreads nThreads 0 LinkedBlockingQueue 控制线程最大并发数 newSingleThreadExecutor 1 1 0 LinkedBlockingQueue 单线程(不适合并发但可能引起IO阻塞或影响UI线程相应的操作,如数据库操作) newScheduledThreadPool corePoolSize Integer.MAX_VALUE 0 DelayedWorkQueue 执行定时/周期性任务 //举例:定长线程池(FixedThreadPool) //创建方法的源码: public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory); } //特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。 //应用场景:控制线程最大并发数。 //如何使用: // 1. 创建定长线程池对象 & 设置线程池线程数量固定为3 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3); // 2. 创建好Runnable类线程对象 & 需执行的任务 Runnable task =new Runnable(){ public void run() { System.out.println("执行任务啦"); } }; // 3. 向线程池提交任务 fixedThreadPool.execute(task);
- 总结:Executors 的 4 个功能线程池虽然方便,但现在已经不建议使用了,而是建议直接通过使用 ThreadPoolExecutor 的方式,这样的处理可以更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 的 4 个功能线程有如下弊端:
FixedThreadPool 和 SingleThreadExecutor:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,可能会耗费非常大的内存,甚至 OOM。
CachedThreadPool 和 ScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。//ThreadPoolExecutor构造方法 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } /* 创建线程池,在构造一个新的线程池时,必须满足下面的条件: corePoolSize(线程池基本大小)必须大于或等于0; maximumPoolSize(线程池最大大小)必须大于或等于1; maximumPoolSize必须大于或等于corePoolSize; keepAliveTime(线程存活保持时间)必须大于或等于0; workQueue(任务队列)不能为空; threadFactory(线程工厂)不能为空,默认为DefaultThreadFactory类 handler(线程饱和策略)不能为空,默认策略为ThreadPoolExecutor.AbortPolicy。 */ //创建实例 //任务队列 BlockingQueue queue = new LinkedBlockingQueue(10); ThreadPoolExecutor executor = new ThreadPoolExecutor(4,10,2,TimeUnit.SECONDS,queue); executor.execute(new Runnable() { @Override public void run() { System.out.println("test"); } }); 合理配置线程池:需要针对具体情况而具体处理,不同的任务类别应采用不同规模的线程池,任务类别可划分为CPU密集型任务、IO密集型任务和混合型任务。 对于CPU密集型任务(计算密集型):线程池中线程个数应尽量少,不应大于CPU核心数; 对于IO密集型任务(大量网络,文件操作):由于IO操作速度远低于CPU速度,那么在运行这类任务时,CPU绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高CPU利用率; 对于混合型任务:可以拆分为CPU密集型任务和IO密集型任务,当这两类任务执行时间相差无几时,通过拆分再执行的吞吐率高于串行执行的吞吐率,但若这两类任务执行时间有数据级的差距,那么没有拆分的意义。