(1)并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生;
(2)并行是在不同实体上的多个事件,并发是在同一实体上的多个事件;
进程:是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
线程:是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。
1. 继承Thread类:(1)创建一个继承于Thread类的子类;(2)重写Thread类的run():将此线程要执行的操作声明在run()中;(3)创建子类对象;(4)通过此对象调用start():启动当前线程,调用当前线程的run();
2. 实现Runnable接口:(1)创建一个实现了Runnable接口的类;(2)实现类去重写Runnable中的run()方法;(3)创建实现类的对象;(4)创建Thread类的对象,将实现类的对象作为参数传递到Thread类的构造器中;(5)通过Thread类的对象调用start()。
3. 通过实现Callable接口:(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。然后再创建Callable实现类的对象;(2)使用FutureTask类来包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值;(3)使用FutureTask对象作为Thread对象的target创建并启动新线程;(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
run()方法被称为线程执行体,它的方法体代表了线程需要完成的任务,每个线程都是通过某个特定Thread对象所对应的run()方法来完成操作的。
start()方法用来启动线程。调用start()方法启动线程时,系统会把该run()方法当成线程执行体来处理,这时无需等待run()方法体代码执行完毕,可以直接继续执行下面的代码;这时此线程是处于就绪状态,并没有运行。但如果直接调用线程对象的run()方法,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。
(1)线程调用sleep()方法主动放弃所占用的处理器资源;(2)线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;(3)线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有;(4)线程在等待某个通知;(5)程序调用了线程的suspend()方法将该线程挂起。
在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。
当线程对象调用了start()方法之后,该线程处于就绪状态,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。
如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态;当线程数大于处理器数时,会存在多个线程在同一个CPU上轮换的现象。
当一个线程开始运行后,它不可能一直处于运行状态,线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。当发生如下情况时,线程将会进入阻塞状态:(1)线程调用sleep()方法主动放弃所占用的处理器资源;(2)线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;(3)线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有;(4)线程在等待某个通知;(5)程序调用了线程的suspend()方法将该线程挂起。
当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态:(1)调用sleep()方法的线程经过了指定时间;(2)线程调用的阻塞式IO方法已经返回;(3)线程成功地获得了试图取得的同步监视器;(4)线程正在等待某个通知时,其他线程发出了一个通知;(5)处于挂起状态的线程被调用了resume()恢复方法。
线程会以如下三种方式结束,结束后就处于死亡状态:(1)run()或call()方法执行完成,线程正常结束;(2)线程抛出一个未捕获的Exception或Error;(3)直接调用该线程的stop()方法来结束该线程。
sleep()方法让正在执行的线程主动让出cpu,然后cpu就可以去执行其他任务,在sleep指定时间后cpu再回到该线程继续往下执行,sleep方法只让出了cpu,而并不会释放同步资源锁,到时间自动恢复。
wait()方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了notify()方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行。wait()方法会放弃对象锁,进入等待队列。
sleep()可以在任何地方使用,而wait()只能在同步方法或同步代码块中使用。
sleep()不会释放锁,而wait()会释放锁,并需要通过notify()/notifyAll()重新获取锁。
启动子线程后,立即调用该线程的join()方法,则主线程必须等待子线程执行完成后再执行。
(1)使用Synchronized关键字:被该关键字修饰的方法或语句块会自动被加上内置锁,从而实现同步。
(2)使用wait和notify:wait()方法可以让当前线程释放对象锁并进入阻塞状态;notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
(3)ReentrantLock:ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
(4)使用特殊域变量volatile:volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新。
(5)使用可重入锁实现线程同步;
(6)使用阻塞队列实现线程同步;
wait()、notify()、notifyAll()用来实现线程通信,这三个方法都不是Thread类中所声明的方法,而是Object类中声明的方法,并且被final修饰,无法被重写。原因是每个对象都拥有锁,所以让当前线程等待某个对象的锁,所以应该通过这个对象来操作。
wait()方法可以让当前线程释放对象锁并进入阻塞状态。
notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了已就绪(将要竞争锁)的线程,阻塞队列存储了被阻塞的线程。当一个阻塞线程被唤醒后,才会进入就绪队列,进而等待CPU的调度。反之,当一个线程被wait后,就会进入阻塞队列,等待被唤醒。
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
synchronized关键字最主要的三种使用方式:(1)修饰实例方法:对当前对象实例加锁,进入同步代码前要获得当前对象实例的锁;(2)修饰静态方法:对当前类对象加锁,进入同步代码前要获得当前类对象的锁。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员。(3)修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。和synchronized方法一样,synchronized代码块也是锁定当前对象的。
synchronized可以修饰静态方法,但不能修饰静态代码块。
(1)synchronized是Java关键字,在JVM层面实现加锁和解锁;Lock是一个接口,在代码层面实现加锁和解锁。
(2)synchronized可以用在代码块上、方法上;Lock只能写在代码里。
(3)synchronized在代码执行完或出现异常时自动释放锁;Lock不会自动释放锁,需要在finally中显示释放锁。
(4)synchronized会导致线程拿不到锁一直等待;Lock可以设置获取锁失败的超时时间。
(5)synchronized无法得知是否获取锁成功;Lock则可以通过tryLock得知加锁是否成功。
(6)synchronized锁可重入、不可中断、非公平;Lock锁可重入、可中断、可公平/不公平,并可以细分读写锁以提高效率。
synchronized是关键字,依赖于JVM;ReentrantLock是类,依赖于API;它提供了比synchronized更多更灵活的特性:等待可中断、可实现公平锁、可实现选择性通知(锁可以绑定多个条件)。
volatile关键字是用来保证有序性和可见性的。当一个变量被定义成volatile之后,它将具备两项特性:(1)保证可见性;(2)禁止指令重排。即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。
虽然volatile能够保证可见性,但它不能保证原子性。volatile变量在各个线程的工作内存中是不存在一致性问题的,但是Java里面的运算操作符并非原子操作,这导致volatile变量的运算在并发下一样是不安全的。
(1)volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
(2)volatile仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别的。
(3)volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
(4)volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
(5)volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
volatile:volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。Java中悲观锁是通过synchronized关键字或Lock接口来实现的。
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于多读的应用类型,这样可以提高吞吐量。乐观锁一般会使用版本号机制或者CAS(Compare And Swap)算法实现。
抽象队列同步器AbstractQueuedSynchronizer(AQS),用来构建锁或者其他同步组件,减少了各功能组件实现的代码量,也解决了在实现同步器时涉及的大量细节问题,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,例如:ReentrantLock可重入锁(支持公平和非公平的方式获取锁);Semaphore计数信号量;ReentrantReadWriteLock读写锁。
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
ThreadLocal是线程私有的局部变量存储容器,它用来存储线程私有变量,内部真正存取是一个Map。每个线程可以通过set()和get()存取变量,多线程间无法访问各自的局部变量。只要线程处于活动状态,它所对应的ThreadLocal实例就是可访问的,线程被终止后,它的所有实例将被垃圾收集。ThreadLocal存储的变量属于当前线程。
ThreadLocal经典的使用场景是为每个线程分配一个JDBC连接 Connection,这样就可以保证每个线程的都在各自的Connection上进行数据库的操作。另外ThreadLocal还经常用于管理Session会话,将Session保存在ThreadLocal中,使线程处理多次处理会话时始终是同一个Session。
系统启动一个新线程的成本是比较高的,在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。线程池提供了一种限制和管理资源(包括执行一个任务)的方式。
使用线程池的好处:(1)降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗;(2)提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行;(3)提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
(1)通过ThreadPoolExecutor 的构造方法实现;
(2)通过Executor 框架的工具类 Executors 来实现。
(1)判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。
(2)判断任务队列是否已满,没满则将新提交的任务添加在工作队列。
(3)判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和(拒绝)策略。
RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务。
SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用shutdown()方法会使线程池进入到该状态。
STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于RUNNING或SHUTDOWN状态时,调用shutdownNow()方法会使线程池进入到该状态。
TIDYING:如果所有的任务都已终止了,有效线程数为0,线程池进入该状态后会调用terminated()方法进入TERMINATED 状态。
TERMINATED:在terminated()方法执行完后进入该状态,默认terminated()方法中什么也没有做。
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:(1)AbortPolicy:丢弃任务并抛出异常。(2)DiscardPolicy:丢弃任务,但是不抛出异常。(3)DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复该过程)。(4)CallerRunsPolicy:由调用线程处理该任务。
(1)CPU密集型任务:尽量使用较小的线程池,一般为CPU核心数+1。
(2)IO密集型任务:可以使用稍大的线程池,一般为2*CPU核心数。
(63)混合型任务:可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
(1)corePoolSize(核心工作线程数):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize。
(2)maximumPoolSize(最大线程数):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
(3)keepAliveTime(多余线程存活时间):当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
(4)TimeUnit(线程活动保持时间的单位)
(5)workQueue(队列):用于传输和保存等待执行任务的阻塞队列。
(6)threadFactory(线程创建工厂):用于创建新线程。
(7)handler(拒绝策略):当线程池和队列都满了,再加入线程会执行此策略。