java多线程包括创建线程,启动线程,控制线程,以及线程同步,以及利用java内建支持的线程池来提高多线程性能。
进程具有
独立性:是系统中独立存在的实体,拥有自己独立的资源,有自己的私有地址空间。
动态性:程序一旦进入内存,就变成一个进程,因此进程是系统中活动的指令集合,加入了时间的概念,进程有自己的生命周期和各种不同的状态。这在程序中是不具备的。
并发性:多个进程可以在单个处理器上并发执行,不会互相影响。
并发和并行的区别:并行是指同一时刻,剁掉指令在多个处理器上同时执行。并发是指同一时刻只有一条指令执行,但过个进程指令被快速轮换执行,在宏观上具有多个进程同时执行的效果。
线程是进程的执行单元,一个进程初始化后,主线程就被创建。线程可以由自己的堆栈,程序计数器,和局部变量。但没有系统资源。多个线程共享父进程的资源。
一个进程中线程可以并发执行,抢占式执行。
操作系统可以同时执行多个任务,每个任务就是进程。进程可以同时执行多个任务,每个任务就是线程。
线程的创建和启动
所有线程对象必须是Thread类或者子类的实例,线程的执行体代表线程的任务(一段程序流即一段顺序执行的代码)
1.定义Thread类的子类,并重写run方法,run方法就是线程执行体
2.创建Thread子类的实例,即创建了线程对象
3.调用线程对象的start方法启动多线程。
用继承Thread类的方法创建的线程,多个线程之间无法共享线程类的实例变量。
public class createByThread extends Thread{ private int i; public void run(){ for(;i<100;i++){ System.out.println(getName()+" "+i); } } public static void main(String[] args) { for(var i=0;i<100;i++){ System.out.println(Thread.currentThread().getName()+" "+i); if(i==20){ Thread th1=new createByThread(); th1.setName("线程NO1"); th1.start(); new createByThread().start(); } } } } 线程NO1 0 线程NO1 1 线程NO1 2 线程NO1 3 线程NO1 4 Thread-1 0 线程NO1 5 Thread-1 1 线程NO1 6 Thread-1 2 线程NO1 7 Thread-1 3 线程NO1 8
程序结果显示,两个线程各自有自己的私有成员变量i
实现Runnable接口创建线程类
1.定义Runnable接口的实现类,并重写接口的run方法,该run方法时该线程的执行体
2.创建实现类的实例,作为target
3创建Thread的实例,并用target做构造器的参数。
4调用线程对象的start()方法。
public class createByRunnable implements Runnable { private int i=0; public void run(){ for(;i<100;i++){ System.out.println(Thread.currentThread().getName()+"----"+i); } } public static void main(String[] args) { for(int i=0;i<100;i++){ System.out.println(Thread.currentThread().getName()+"----"+i); if(i==20){ var rn1=new createByRunnable(); Thread th3=new Thread(rn1,"新县城1"); Thread th4=new Thread(rn1,"新县城2"); th3.start(); th4.start(); } } } }
运行结果i是连续的。因为两个线程共享一个Runnable对象target,所以多个线程可以共享同一个线程类(实际上应该是线程的target类)的实例变量。
使用Callable和Future创建线程
run方法没有返回值。为了使得任意方法可以作为线程执行体,有了Callable接口。该接口有一个call方法,该方法有返回值。可以作为执行体。但是该方法不是直接被调用,返回值也不是直接返回。
使用Future接口,接口有一个实现类FutureTask类,该类作为target,封装了返回值。关联了call方法。
创建启动有返回值的线程步骤如下:
创建Callable接口的实现类,并实现Call方法,再创建实现类的实例。
使用FutureTask类保证Callable对象,封装了call方法的返回值。
调用FutureTask对象get方法获得子线程执行结束后的返回值。
public static void main(String[] args) { //使用lambda表达式定义callable实现类对象,并且封装为task FutureTask<Integer> task=new FutureTask<>((Callable<Integer>)()->{ var i=0; for(;i<100;i++){ System.out.println(Thread.currentThread().getName()+"----"+i); } return i; }); for(var i=0;i<100;i++){ System.out.println(Thread.currentThread().getName()+"----"+i); if(i==20){ //task作为target new Thread(task,"有返回值的方法").start(); } } try{ //获取线程返回值 System.out.println("子线程的返回值-----"+task.get()); }catch (Exception ex){ ex.printStackTrace(); } }
采用实现Runnable接口方式的多线程: 线程类只是实现了Runnable接口,还可以可以继承其他类。 在这种方式下,可以多个线程共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU,代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。 劣势是:编程稍稍复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。
采用继承Thread类方式的多线程: 劣势是:因为线程类已经继承了Thread类,所以不能再继承其他父类。 优势是:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
线程的生命周期
如图所示,线程分为新建,就绪,阻塞,运行,死亡五个声明周期
new了线程之后,就处于新建。调用isAlive方法,会返回false,新建状态没有任何动态性能
当程序启用了start方法,就进入就绪状态,等待执行。
就绪状态的线程获得了cpu,就开始执行run方法。
被cpu剥夺资源,就进入阻塞。
图中写出了引起阻塞的五个方法
调用yield方法,可以让运行状态的线程转入就绪状态。
run()方法执行完成,线程正常结束。 线程抛出一个未捕获的 Exception或Error。 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。
主线程结束,不会影响其他线程,地位相同。
不要重新启动已经死亡的线程。
线程控制
join
当在线程执行程序中调用其他线程的join方法后,会先执行这个线程,当他执行完或者等待了预定时间后,主线程开始执行
public class JoinThread extends Thread { public JoinThread(String name){ super(name); } public void run(){ for(var i=0;i<100;i++){ System.out.println(currentThread().getName()+"==="+i); } } //使用join必须抛出异常或者使用try/catch public static void main(String[] args) throws InterruptedException { new JoinThread("第一子线程").start(); for(var i=0;i<100;i++){ if(i==20){ var t1=new JoinThread("第二Join线程"); //调用join前需要调用start t1.start(); t1.join(); } System.out.println(currentThread().getName()+"===="+i); } } } 第二Join线程===94 第二Join线程===95 第二Join线程===96 第二Join线程===97 第二Join线程===98 第二Join线程===99 main====20 main====21 main====22 main====23 main====24
后台线程
调用Thread的setDaemon方法将线程设为后台线程。前台线程死亡后,后台线程自动死亡
前台线程创建的线程默认为前台线程
后台线程创建的线程默认为后台线程
前台线程死亡,jvm会通知后台线程死亡,但它从接收到指令做出响应,需要一段时间。
必须在线程启动前,将线程设为后台线程。否则抛异常。
线程睡眠
sleep阻塞线程
yield将线程转入就绪。重新让系统的线程调度器调度一次。这是有可能该线程继续被运行,有可能优先级比当前高的或者相同的处于就绪状态的线程获得执行的机会
sleep方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级。但yield方法只会给优先级相同,或优先级更高的线程执行机会。
sleep方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态。而yield不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield方法暂停之后,立即再次获得处理器资源被执行。
sleep方法声明抛出了InterruptedException异常,所以调用sleep方法时要么捕捉该异常,要么显式声明抛出该异常。而yield方法则没有声明抛出任何异常。
sleep方法比yield方法有更好的可移植性,通常不要依靠yield来控制并发线程的执行。
改变线程优先级
每个线程执行都具有一定的优先级,优先级高的线程获得较多的执行机会。
每个线程默认的优先级与创建他的父优先级相同。
Thread类提供了setPriority,1-10,但是与操作系统相关了以后,不能很好的对应
一般使用
MAX_PRIORITY:其值是10。
MIN_PRIORITY:其值是1。
NORM_PRIORITY:其值是5。
main线程具有普通优先级,即5.
线程同步
多个线程访问同一个数据时,容易出现线程安全问题
Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。 synchronized后括号里的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
一般吧共享的资源,作为obj
synchronized后括号里的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
同步方法
同步方法时使用synchronized关键字来修饰某个方法。无须指定同步监视器,是this,即调用方法的对象。
通过同步方法可以很方便的实现线程安全类
该类的对象可以被多个线程安全的访问。 每个线程调用该对象的任意方法之后都将得到正确结果。 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采用如下策略: 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步。例如上面的Account类中accountNo属性就无需同步,所以程序只对draw方法进行同步控制。 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本:线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。
线程会在如下几种情况下释放对同步监视器的锁定: 当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。 当线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器。 当线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时将会释放同步监视器。 当线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。
线程执行同步代码块或同步方法时,调用sleep,ield方法暂停执行,不会释放同步监视器
当其他线程调用了该线程的suspend方法,挂起线程时,不释放同步监视器。
同步锁Lock
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。不过,某些锁可能允许对共享资源并发访问,如 ReadWriteLock(读写锁)。当然,在实现线程安全的控制中,通常喜欢使用ReentrantLock(可重入锁)。使用该Lock对象可以显式地加锁、释放锁。 ReentrantLock锁具有可重入性,也就是说线程可以对它已经加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock方法的嵌套调用,线程在每次调用lock()方法加锁后,必须显式调用unlock()方法来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法
当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测、也没有采用措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁的出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续
线程通信
线程通信一定建立在线程安全的基础上
传统的线程通信
wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。该wait()方法有三种形式:无时间参数的wait(一直等待,直到其他线程通知),带毫秒参数的wait和带毫秒、微秒参数的wait(这两种方法都是等待指定时间后自动苏醒)。调用wait()方法的当前线程会释放对该同步监视器的锁定。
notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程。
notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
使用Condition控制线程通信
当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象、却无法继续执行的线程释放Lock对象,Condtion对象也可以唤醒其他处于等待的线程。 Condition 将同步监视锁方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与Lock对象组合使用,为每个对象提供多个等待集(wait-set)。在这种情况下,Lock 替代了同步方法或同步代码块,Condition替代了同步监视锁的功能。 Condition实例实质上被绑定在一个Lock对象上。要获得特定Lock实例的Condition实例,调用Lock对象newCondition()方法即可。Condtion类提供了如下三个方法: await():类似于隐式同步监视器上的wait()方法,导致当前线程等待,直到其他线程调用该Condtion的signal ()方法或signalAll ()方法来唤醒该线程。该await方法有更多变体:long awaitNanos(long nanosTimeout)、void awaitUninterruptibly()、awaitUntil(Date deadline)等,可以完成更丰富的等待操作。 signal ():唤醒在此Lock对象上等待的单个线程。如果所有线程都在该Lock对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该Lock对象的锁定后(使用await()方法),才可以执行被唤醒的线程。 signalAll():唤醒在此Lock对象上等待的所有线程。只有当前线程放弃对该该Lock对象的锁定后,才可以执行被唤醒的线程。
使用阻塞队列控制线程通信
java5提供了一个BlockingQueue接口,虽然是queue的子接口,但是用途是线程通信。
当生产者线程试图向BlockingQueue中放入元素时,如果队列已满,则线程被阻塞。put方法
当消费者线程试图向BlockingQueue中取出元素时,如果队列已空,则线程阻塞。take方法
因为继承queue接口
add,offer,put方法为在尾部插入元素。满则阻塞false
remove,poll,take在头部取出元素并删除。空则阻塞 false
element,peek,在头部取出但不删除。空则跑异常。返回false
ArrayBlockingQueue基于数组实现的
LinkedBlockingQueue基于链表实现的
PriorityBlockingQueue 不是取出队列中存在时间最长的元素,而是队列中最小的元素。实现了Comparable接口
SynchronousQueue同步队列,对该队列的存取操作必须交替进行
DelayQueue 底层基于priorityBlockingqueue实现,要求集合元素都实现Delay接口,接口有一个long getDelay方法,根据集合元素的getDalay方法返回值进行排序。
线程组合未处理的异常
如果用户创建的线程没有指定线程组,则输与默认线程组。
Java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。 一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组。 Thread类提供了如下几个构造器来设置新创建的线程属于哪个线程组: Thread(ThreadGroup group, Runnable target):以target的run方法作为线程执行体创建新线程,属于group线程组。 Thread(ThreadGroup group, Runnable target, String name):以target的run方法作为线程执行体创建新线程,该线程属于group线程组,且线程名为name。 Thread(ThreadGroup group, String name):创建新线程,新线程名为name,属于group线程组。
Thread类没有提供setThreadGroup的方法来改变线程所属的线程组,但提供了一个getThreadGroup()方法来返回该线程所属的线程组,getThreadGroup()方法的返回值是ThreadGroup对象,表示一个线程组。ThreadGroup类有如下两个简单的构造器来创建实例: ThreadGroup(String name):以指定线程组名字来创建新的线程组。 ThreadGroup(ThreadGroup parent, String name):以指定的名字、指定的父线程组创建一个新线程组。
从JDK1.5开始,Java加强了线程的异常处理,如果线程执行过程中抛出了一个未处理的异常,JVM在结束该线程之前会自动查找是否有对应的Thread.UncaughtExceptionHandler对象,如果找到该处理器对象,将会调用该对象的uncaughtException(Thread t, Throwable e)方法来处理该异常。 Thread.UncaughtExceptionHandler是Thread类的一个内部公共静态接口,该接口内只有一个方法:void uncaughtException(Thread t, Throwable e),该方法中的t代表出现异常的线程,而e代表该线程抛出的异常。
(1)如果该线程组有父线程组,则调用父线程组的uncaughtException方法来处理该异常。
(2)如果该线程实例所属的线程类有默认的异常处理器(由setDefaultUncaughtExceptionHandler方法设置的异常处理器),那就调用该异常处理器来处理该异常。
(3)如果该异常对象是ThreadDeath的对象,将不做任何处理;否则将异常跟踪栈的信息打印到System.err错误输出流,并结束该线程。
线程池
系统启动一个新线程的成本是比较高的,因为它涉及到与操作系统交互。在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。 与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run方法,当run方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run方法。
JDK1.5提供了一个Executors工厂类来产生线程池,该工厂类里包含如下几个静态工厂方法来创建连接池: newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。 newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池。 newSingleThreadExecutor():创建一个只有单线程的线程池,它相当于newFixedThreadPool方法时传入参数为1。 newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。 newSingleThreadScheduledExecutor():创建只有一条线程的线程池,它可以在指定延迟后执行线程任务。
ExecutorService
代表尽快执行线程的线程池(只要线程池中有空闲线程立即执行线程任务),程序只要将一个Runnable对象或Callable对象(代表线程任务)提交给该线程池即可,该线程池就会尽快执行该任务。ExecutorService里提供了如下三个方法: Future<?> submit(Runnable task):将一个Runnable对象提交给指定的线程池。线程池将在有空闲线程时执行Runnable对象代表的任务。其中Future对象代表Runnable任务的返回值——但run方法没有返回值,所以Future对象将在run方法执行结束后返回null。但可以调用Future的isDone()、isCancelled()方法来获得Runnable对象的执行状态。 <T> Future<T> submit(Runnable task, T result):将一个Runnable对象提交给指定的线程池。线程池将在有空闲线程时执行Runnable对象代表的任务,result显式指定线程执行结束后的返回值。,所以Future对象将在run方法执行结束后返回result。 <T> Future<T> submit(Callable<T> task):将一个Callable对象提交给指定的线程池。线程池将在有空闲线程时执行Callable对象代表的任务,Future代表Callable对象里call方法的返回值。
ScheduledExecutorService
代表可在指定延迟,或周期性执行线程任务的线程池,它提供了如下四个方法: ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit):指定callable任务将在delay延迟后执行。 ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit):指定command任务将在delay延迟后执行。 ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):指定command任务将在delay延迟后执行,而且以设定频率重复执行。也就是说,在initialDelay后开始执行,依次在 initialDelay+period 、initialDelay + 2 * period...处重复执行,依此类推。 ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。如果任务的任一次执行时遇到异常,就会取消后续执行。否则,只能通过程序来显式取消或终止来终止该任务。
使用线程池的步骤
(1)调用Executors类的静态工厂方法创建一个ExecutorService对象或ScheduledExecutorService对象,其中前者代表简单的线程池,后者代表能以任务调度方式执行线程的线程池。
(2)创建Runnable实现类或Callable实现类的实例,作为线程执行任务。
(3)调用ExecutorService对象的submit方法来提交Runnable实例或Callable实例;或调用ScheduledExecutorService的schedule来执行线程。
(4)当不想提交任何任务时调用ExecutorService对象的shutdown方法来关闭线程池。
使用ForkJoinPool利用多核cpu
java7提供了ForkJoinPool来支持将一个任务拆分成多个小人物,并行计算。再把多个小任务的结果合并成总的计算结果。ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池。
java8进一步扩展了它的功能。增加了通用池的功能。
commonPool 返回一个通用池,运行状态不收shutdown方法影响
getCommonPollParallelism 返回通用池的并行级别
创建了ForkJoinPool之后,就可以调用submit或invoke方法来执行任务。ForkJoinTask代表一个可以并行,合并的任务。
线程相关类
ThreadLocal
线程局部变量类。
为每一个使用该变量的线程都提供一个变量的副本,使每一个线程都可以独立改变自己的副本
get,remove set方法
这个方法和同步机制有区别,不能替代
如果多个线程之急需要共享资源,已带到线程之间的通信功能,就是用同步机制
如果仅仅需要隔离多个线程之间的共享冲突,则可以使用ThreadLocal。
包装线程不安全集合
如果程序有多条线程可能访问以上ArrayList、HashMap等集合,可以使用Collections提供的静态方法来把这些集合包装成线程安全的集合。Collections提供了如下几个静态方法:
static <T> Collection<T> synchronizedCollection(Collection<T> c):返回指定 collection 对应的线程安全的collection。
static <T> List<T> synchronizedList(List<T> list):返回指定List对应的线程安全的List对象。
static <K,V> Map<K,V> synchronizedMap(Map<K,V> m):返回指定Map对象对应的线程安全的Map对象。
static <T> Set<T> synchronizedSet(Set<T> s):返回指定Set对应的线程安全的Set。
static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m):返回指定SortedMap对象对应的线程安全的SortedMap对象。
static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s):返回指定SortedSet对象对应的线程安全的SortedSet对象。
线程安全的集合类
java.util.concurrent包下提供了提供了ConcurrentHashMap、ConcurrentLinkedQueue两个支持并发访问的集合,它们分别代表了支持并发访问的HashMap和支持并发访问的Queue。
ConcurrentLinkedQueue不允许使用null元素,实现类多线程的高效访问,无须等待
ConcurrentHashMap支持16个线程并发访问。
java8扩展了ConcurrentHashMap的功能
CopyOnWriteArrrayList写入操作会复制一份副本,因此效率比较低,所以适合读取远远大于写入操作的场景。
java9新增的发布订阅框架
Java 9新增了一个发布-订阅框架,这个发布-订阅框架是基于异步响应流的。这个发布-订阅框架可以非常方便地处理异步线程之间的流数据交换(比如两个线程之间需要交换数据)。而且这个发布-订阅框架不需要使用数据中心来缓冲数据,同时具有非常高效的性能。 这个发布-订阅框架使用Flow类的四个静态内部接口作为核心API: Flow.Publisher:代表数据发布者、生产者。 Flow.Subscriber:代表数据订阅者、消费者。 Flow.Subscription:代表发布者和订阅者之间的链接纽带。订阅者通过调用该对象的request()方法来获取 Flow.Processor:数据处理器,它可同时作为发布者和订阅者使用。 Flow.Subscriber接口定义了如下方法。 void onSubscribe(Flow.Subscription subscription):订阅者注册时自动触发该方法。 void onComplete():当订阅结束时触发发该方法。 void onError(Throwable throwable):订阅失败时触发该方法。 void onNext(T item):订阅者从发布者处获取数据项触发该方法,订阅者可通过该方法获取数据项。