1.多线程概述
线程是独立的执行路径;
在程序运行时,即使没有自己创建线程,后台也会有多个线程,比如主线程main()、gc线程(守护线程);
main()称之为主线程,为程序的总入口,用于执行整个程序;
在一个进程中,如果开辟了多个线程,线程的运行是由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序不能人为干预;
对同一份资源操作时,可能会存在资源抢占的问题,需要加入并发控制;
线程会带来额外开销,比如cpu调度时间,并发控制开销等;
每个线程在自己的工作内存内交互,如果内存控制不当可能导致数据不一致。
2.继承Thread类
创建一个线程:
1)首先需要自定义一个线程类继承Java中的Thread类;
2)重写线程中的run()方法;
3)在main方法中新建该线程对象,调用start()方法启动线程。
示例:
1)调用线程对象的start()方法:
线程类; Main方法以及新建线程对象
结果:
主方法先执行,同时线程对象也在执行,多线程过程。
2)若调用线程对象的run()方法(不正确):
结果:
一定会先执行run方法中的内容,再执行主方法,这里并不存在多线程过程。
注意:线程开启不一定立即执行,由CPU调度执行。
3.实现Runnable接口
继承Thread类:
1)子类继承Thread类具备多线程能力;
2)启动线程:子类对象.start();
3)不建议使用,因为OOP存在单继承局限性。
实现Runnable接口:
1)实现Runnable接口同样具备多线程能力;
2)启动线程:new Thread(子类对象).start(); //传入目标对象+Thread对象.start();
3)推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用。
继承Thread类实例:
结果:
实现Runnable接口实例:
结果:
4.并发问题
多线程操作的时候可能存在并发问题:当一个对象被多个线程使用时,线程内的数据可能会变得不安全,造成数据紊乱的情况。
示例:
结果:
从结果看,存在重复抢票的情况,甚至还可能会抢到第0张票,这就是并发过程中的数据紊乱问题,可以通过线程同步解决。
5.实现callable接口(了解)
步骤:
1)实现callable接口,需要配置返回值类型;
2)重写call方法(Runnable接口是重写run方法),需要抛出异常;
3)创建目标对象;
4)创建执行服务:ExcutorService ser = Executors.newFixedThreadPool(1); //实参是当前申请的线程池大小,1为1个线程;
5)提交执行:Future<Boolean> result1 = ser.submit(t1); //提交线程t1,多线程需要用多个语句分别提交;
6)获取结果:boolean r1 = result.get(); //获取callable接口的返回值(true or false);
7)关闭服务:ser.shutdownNow(); //关闭之前创建的服务
特点(与Runnable接口功能基本一致,具体使用参考不同业务的特点及需求):
1)可以定义返回值
2)可以抛出异常
6.静态代理模式
代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式,即通过代理对象访问目标对象。这样做的好处是可以在目标对象实现的基础上,增加额外的功能操作,拓展目标对象的功能,同时目标对象可以更专注于自己实现的功能。
这也体现了编程中的一种思想:不要随意修改已有的代码或方法,如果有其他需求,可以通过代理的方式来对该方法进行拓展。
静态代理在使用时需要定义接口或者父类,目标对象(被代理对象)和代理对象一起实现相同的接口或者继承相同的父类。
实例:
结果:
对比前面学习的多线程,
我们可以发现Thread类对于我们编写的线程也是一个静态代理的过程,二者均实现了Runnable接口,我们通过Thread对象访问线程对象,在Thread对象中通过start()来调用重写的run方法,这便是多线程实现的一个基本原理。
7.Lambda表达式
Lambda表达式主要是用于简化函数式接口的实现方法,于Jdk1.8引入,可以使代码更简洁明了。
函数式接口的定义:
1)任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口;
2)对于函数式接口,我们常常用Lambda表达式来创建给接口的对象。
简化实例:
第一步,外部类实现接口:
结果:
第二步简化为静态内部类:
结果:
第三步简化为局部内部类:
结果:
第四步简化为匿名内部类,需要借助父类或者接口:
结果:
第五部简化为Lambda表达式,借助父类或者接口:
结果:
包含传入参数的例子(多个输入参数同理):
结果:
备注:
多个参数也可以用Lambda表达式,但是不能简化参数括号。
8.线程停止
线程状态:
1)new:Thread t = new Thread(); 线程对象一旦创建就进入到了new状态;
2)就绪状态:当调用start()方法,线程立即进入就绪状态,但不意味着立即调度执行;
3)运行状态:通过CPU调度之后,线程进入运行状态,此时线程才真正执行线程体的代码块,运行状态可以转向阻塞状态或者dead状态;
4)阻塞状态:当调用sleep、wait或同步锁定的时候,线程进入阻塞状态,此时代码不往下执行,阻塞事件接触后,重新转向阻塞状态,等待CPU调度执行;
5)dead:线程中断或结束,一旦进入dead状态,线程便无法重新启动。
常见的线程方法:
对于停止线程这个操作,一般不推荐使用JDK提供的stop()、destroy()方法(已废弃),建议使用一个标志位来对线程进行控制,当flag=false时,终止线程。
1)线程需要正常停止——利用计数,不建议使用死循环;
2)使用标志位来控制进程终止;
3)不要使用stop()、destroy()等过时或JDK不建议使用的方法。
实例:
结果:
9.线程休眠_sleep
1)sleep()参数指定当前线程阻塞的毫秒数;
2)sleep方法可能存在异常InterruptedException,需要通过try-catch方法处理;
3)sleep时间达到后线程进入就绪状态;
4)sleep可以模拟网络延时、倒计时等(延时情况有时可以暴露一些潜在的问题,比如多线程同步问题);
5)每一个对象都有一个锁,sleep不会释放锁。
TIPS——打印系统时间
10.线程礼让_yield
礼让线程,就是让当前正在执行的线程A,从运行状态回到就绪状态(非阻塞态),此时CPU会重新调度,但礼让不一定成功,取决于CPU调度情况,有可能仍是线程A执行。
11.线程强制执行_join
Join合并线程,即让该线程A执行完成后,再执行其他线程,此时其他线程处于阻塞状态(可以想象成插队)。
实例:
结果:
一开始多线程同时执行,当调用join方法后,主线程阻塞,线程插队直到执行完毕,主线程才继续执行。
12.线程状态观测
Thread.State类
通过Thread.getState()方法获取线程当前状态,实例如下:
结果:
13.线程的优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程执行。优先级只是增加了调度几率的权重,最终还是取决于CPU的调度情况。
线程优先级用数字表示,范围为1-10,默认值为5。
——Thread.MIN_PRIORITY、Thread.MAX_PRIORITY、Thread.NORM_PRIORITY
通过getPriority()、setPriority(int XXX)来获取或设置优先级。
实例:
结果:
修改为优先级范围后:
14.守护(daemon)线程
线程分为用户线程和守护线程,Thread类中存在一个daemon布尔值,用来区分用户线程和守护线程。用户线程默认为false,守护线程为true,通过setDaemon()方法配置。
对于用户线程,虚拟机必须确保其执行完毕,而对于守护线程,虚拟机不用等其执行完毕。
守护线程一般用于后台的功能执行,如后台记录操作日志、监控内存、垃圾回收等。
实例:
结果:
备注:虚拟机关闭线程需要一段时间,所以后面守护线程仍然执行了一小段时间。
15.线程同步机制
在多线程场景中,常常会出现多个线程访问同一个对象并进行读写的操作,这个时候如果不进行处理和保护,就会导致多线程之间互相影响,导致数据紊乱。为了解决这个问题,我们就需要线程同步。线程同步其实是一种等待机制,我们让多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。
另外,由于同一进程的多个线程共享同一块存储空间,这里也存在访问冲突的问题,为了保证数据在方法中被访问时的正确性,我们一般还会在访问时加入锁机制(Synchronized),当一个线程获得对象的排它锁,独占资源,其他线程必须等待,等这次使用后释放锁才可继续访问。不过这同样存在一部分问题:
1)一个线程持有锁会导致其他所有需要此锁的线程挂起;
2)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。
由于我们可以通过privated关键字来保证数据对象只能被方法访问,所以只需要针对方法提出一套机制保证同步效果即可。这套机制就是synchronized关键字,包括两种方法:
1)同步方法:public synchronized void method(int args){}
synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个方法synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面阻塞的线程才能拿到这个锁,继续执行。但同样这里存在一个缺陷,当一个大的方法被申明为synchronized时将会影响效率。
实例:
结果:
存在数据紊乱。
加入同步关键字后:
结果:
2)同步块:synchronized(Obj){}
OBj称为同步监视器,可以是任何对象,一般推荐使用共享资源作为同步监视器。在同步方法中,同步监视器默认指定的是this,为该对象本身(或者是class,反射课程中讨论)。
实例:
结果:
数据紊乱
若改为用synchrod方法,由于默认同步监视器为对象本身,所以并未起到同步的作用:
结果:
此时应该用synchronized同步块方法,将同步监视器设置为account对象:
结果:
16.死锁
死锁的情况一般指多个线程各自占用对方需要的共享资源,但同时又在等待对方线程释放占用资源的情形。这种情况下多个线程都处于阻塞状态,系统无法继续向下运行。
当某一个同步块同时拥有“两个以上对象的锁”时,就可能发生死锁的问题。
实例:
结果:
两个线程都处于阻塞中,无法继续进行。
要解决这个问题,我们只需要控制一个同步块内不要放两个以上对象即可。
结果:
17.lock锁
从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来进行同步,方式是以Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得Lock对象。
ReentrantLock可重入锁类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁释放锁。
Synchronized和Lock的对比:
1)Lock是显式锁(手动开关),Synchronized是隐式锁,出了作用域自动释放;
2)Lock只有代码块锁,Synchronized有代码块锁和方法锁;
3)使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
4)优先顺序:Lock>同步代码块(已经进入了方法体,分配了相应资源,比如前面的银行取款例子)>同步方法(在方法体之外)
实例(以前面的黄牛买票为例):
结果:
18.生产者消费者问题
生产者消费者是一类经典同步问题:一组生产者和一组消费者进程共享一个初始为空,大小为n的缓冲区,只有缓冲区没满时,生产者才能把消息放入缓冲区,否则必须等待;只有缓冲区不空时,消费者才能从中取出消息,否则必须等待。同时由于缓冲区是临界资源,它也只允许一个生产者放入消息,或一个消费者从中取出消息。
整体上来说,生产者和消费者对缓冲区互斥访问是互斥关系,同时生产者和消费者又是一个相互协作的关系,只有生产者生产之后,消费者才能消费,这其中为同步关系。
一些常见的解决方法:
1)管程法
通过wait()和notifyAll()方法实现线程等待和唤醒,也可以用notify()方法,但notify()只随机唤醒该对象的一个等待线程(wait状态),具体唤醒对象由线程调度器决定。即notifyAll()唤醒后,对象等待池内的所有线程都会进入锁池等待锁竞争。
假设存在两个生产者两个消费者的情况,当所有生产者都满了进入等待池中,此时如果用notify()恰好每次都唤醒的是消费者,这个时候就容易造成线程挂起,影响程序性能。
2)信号灯法
当生产者和消费者之间无缓冲区(即缓冲区为1)时,此时可以采用信号灯法来进行处理,即通过一个标志位为判断线程等待的条件(管程法中是通过count来判断),如标志位flag为真时,生产者等待;flag为假时,消费者等待。生产者和消费者运行后,用notifyAll()唤醒阻塞线程,并转换标志位。
19.线程池
当用户经常创建、销毁和使用量特别大的资源,比如并发情况下的线程时,系统的性能所受影响会特别大,这种情况一般通过使用线程池来处理。我们提前创建一部分线程,放入线程池中,需要使用时直接获取,用完后再放回池中。这样可以避免频繁创建销毁,实现线程的重复利用。
优点:
1)提高响应速度,减少了创建新线程的时间;
2)降低资源消耗,重复利用线程池中线程,不需要每次都创建;
3)便于线程管理,可以通过已有的属性和方法进行控制:
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间会停止
线程池的使用:
JDK5.0期提供了线程池相关API:ExecutorService和Executors
线程池接口:ExecutorService。常见子类为ThreadPoolExecutor
void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
<T>Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable
void shutdown():关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池。
实例:
结果:
原文:https://www.cnblogs.com/Kknock/p/14563698.html