多线程知识总结
在日常开发过程中,作为一个需要经常编写代码的码农,多线程的知识在开始进入编程工作时,或许多线程的知识并不必要,可当深入代码的海洋,总是会遇到一些使用场景,传统的单线程编程无法应对,此时就需要采用多线程技术处理复杂的应用场景,进而获得更好的用户体验。
另外,在因特网上下载一个文件时,多会提示如下的内容:
延迟下载,可以理解为,快速响应,延迟下载。本质上就是多线程的一种下载。
在bm审查期间,和同事合作,抽空优化了同事开发的考勤项目,通过代码重构手法以及多线程的使用,程序的运行耗时从一分钟多降低到了2秒钟,强有力的保障了用户的体验,而且使得页面超时问题不再出现。在这个过程中,还是要多感谢自己的工作经验,其中有两个较为显著的地方:
- 代码重构手法,用以理解代码的组织,通过调整结构使得关键耗时节点得以明晰
- 多线程机制的使用,关键是找到多线程机制的使用地方,在处理考勤业务时核心时处理多个电子表格并行处理,并且将处理每个电子表格中的一条记录也并行,通过两处并行将处理速度优化了很多倍,也增加对多CPU的利用率。
- Lambda并行处理,之前一直在同事中宣贯Lambda的用法,甚至有点滥用的迹象,但没有办法,为了推广必须矫枉过正。另外一个较为关键的是就是Lambda并行流处理,这使得Java的多线程触发更加灵活方便,一两行代码的变化就能使程序的变化非常的显著。真正实现了四两拨千斤的作用。
也正是体会到了多线程的强大,因此自己在bm审查期间花了两天的时间,认真的总结了多线程的概念,通过阅读**《Java 疯狂讲义》**深入的理解多线程的方方面面,通过阅读的过程,自己对于多线程的认识和理解确实到了一个新的高度。本文就是基于在阅读时整理的XMind,梳理成文。以期能对多线程有帮助的程序员有所帮助。
线程概述
在线程概述中,较为重要的是理解线程和进程的概念以及理解上的区别。
进程
其实理解起来也并不复杂,进程的概念是相对宏观的,进程可以理解成QQ和微信。当我们在日常工作生活中,QQ和微信两个程序时互不干扰的,这是两个进程,而不是两个线程。但在QQ程序内部,却有可能存在多个线程,比如一个线程和某个人聊天,另外一个线程则是批量下载QQ中的图片。
- 操作系统的多任务就是多进程。
- 一个进程可以拥有多个线程。
- 操作系统创建一个进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源。
线程
- 单线程程序: 只有一个顺序的执行流,在Java世界中,该线程为main函数所在的线程
- 多线程的程序:可以包括多个顺序执行流,多个顺序执行流之间互不干扰。每个顺序执行流就是一个线程
线程的优点如下所示:
线程的创建和启动
线程的创建和启动,指的是在Java的世界里,如何通过编程实现一个线程的创建,并启动该线程实现具体的业务。
Thread
Java 提供了编写一个类来继承Thread,然后覆写run方法,然后调用start方法来启动线程。这时这个类就会以另一个线程的方式来运行run方法里面的代码。另一种是编写一个类来实现Runnable接口,然后实现接口方法run
,然后创造一个Thread对象,把实现了Runnable接口的类当做构造参数,传入Thread对象,最后该Thread对象调用start方法。
这里的start方法是一个有启动功能的方法,该方法内部回调run方法。所以,只有调用了start方法才会启动另一个线程,直接调用run方法,还是在同一个线程中执行run,而不是在另一个线程执行run
此外,start方法只是告诉虚拟机,该线程可以启动了,也就说该线程在就绪的状态,但不代表调用start就立即运行了,这要等待JVM来决定什么时候执行这个线程。也就是说,如果有两个线程A,B ,A先调用start,B后调用start,不代表A线程先运行,B线程后运行。这都是由JVM决定了,可以认为是随机启动。
public class ExampleThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("这是一个继承自Thread的ExampleThread");
}
}
Runnable
另一种,实现了Runnable接口
public class ExampleRunable implements Runnable{
public void run() {
System.out.println("这是实现Runnable接口的类");
}
}
package java.lang;
/**
* The <code>Runnable</code> interface should be implemented by any
* class whose instances are intended to be executed by a thread. The
* class must define a method of no arguments called <code>run</code>.
* <p>
* This interface is designed to provide a common protocol for objects that
* wish to execute code while they are active. For example,
* <code>Runnable</code> is implemented by class <code>Thread</code>.
* Being active simply means that a thread has been started and has not
* yet been stopped.
* <p>
* In addition, <code>Runnable</code> provides the means for a class to be
* active while not subclassing <code>Thread</code>. A class that implements
* <code>Runnable</code> can run without subclassing <code>Thread</code>
* by instantiating a <code>Thread</code> instance and passing itself in
* as the target. In most cases, the <code>Runnable</code> interface should
* be used if you are only planning to override the <code>run()</code>
* method and no other <code>Thread</code> methods.
* This is important because classes should not be subclassed
* unless the programmer intends on modifying or enhancing the fundamental
* behavior of the class.
*
* @author Arthur van Hoff
* @see java.lang.Thread
* @see java.util.concurrent.Callable
* @since JDK1.0
*/
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
从上述类定义可以看出,Runnable类注解了函数式接口,因此可以通过Lambda的格式来实现Runnable。这也是当前较为常用的实现方式。
你不知道的Runnable接口,深度解析Runnable接口
可以通过上文深入理解Lambda来实现Runnable,语法很凝练,也很方便。
另一个重要的好处就是,因为Runnable是一个接口,在自定义类实现该接口的同时,仍然可以继承其他类,而不像Thread类,由于Java的单继承,是无法继承其他的类
Callable和Future
当实现Callable接口之后,要重写接口中的call方法,call方法中的代码作为线程执行体,此时的call方法可以有返回值。
Callable接口是Java5中新增的接口,不是Runnable接口的子接口,所以Callable对象不能直接作为Thread对象的Target,于是Java5中提供了Future接口来代表Callable接口里call方法的返回值,并为Future接口提供了一个FutureTask实现类,它实现了Future接口和Runnable接口,可以作为Thread类的Target。所以在创建Callable
接口实现类之后,要用FutureTask来包装Callable对象(实现手动装箱)。然后用FutureTask对象作为Target。
package java.util.concurrent;
/**
* A task that returns a result and may throw an exception.
* Implementors define a single method with no arguments called
* {@code call}.
*
* <p>The {@code Callable} interface is similar to {@link
* java.lang.Runnable}, in that both are designed for classes whose
* instances are potentially executed by another thread. A
* {@code Runnable}, however, does not return a result and cannot
* throw a checked exception.
*
* <p>The {@link Executors} class contains utility methods to
* convert from other common forms to {@code Callable} classes.
*
* @see Executor
* @since 1.5
* @author Doug Lea
* @param <V> the result type of method {@code call}
*/
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
Java多线程的三种实现方式(重点看Collable接口实现方式)
对比
线程的生命周期
线程状态
线程转换图
IllegalThreadStateException
在多线程编程中常见的异常,常见发生的时机为如下图所示:
控制线程
控制线程的主要内容如下图所示:
join
join() 的作用:让“主线程”等待“子线程”结束之后才能继续运行。
// 主线程
public class Father extends Thread {
public void run() {
Son s = new Son();
s.start();
s.join();
...
}
}
// 子线程
public class Son extends Thread {
public void run() {
...
}
}
上面的有两个类Father(主线程类)和Son(子线程类)。因为Son是在Father中创建并启动的,所以,Father是主线程类,Son是子线程类。
在Father主线程中,通过new Son()新建“子线程s”。接着通过s.start()启动“子线程s”,并且调用s.join()。在调用s.join()之后,Father主线程会一直等待,直到“子线程s”运行完毕;在“子线程s”运行完毕之后,Father主线程才能接着运行。 这也就是我们所说的“join()的作用,是让主线程会等待子线程结束之后才能继续运行”!
后台线程
线程睡眠
由于sleep()方法是Thread类的方法,因此它不能改变对象的锁。所以当在一个Synchronized方法中调用sleep()时,线程虽然休眠了,但是对象的机锁没有被释放,其他线程仍然无法访问这个对象。而wait()方法则会在线程休眠的同时释放掉机锁,其他线程可以访问该对象。
wait()方法和notify()方法:当一个线程执行到wait()方法时(线程休眠且释放机锁),它就进入到一个和该对象相关的等待池中,同时失去了对象的机锁。当它被一个notify()方法唤醒时,等待池中的线程就被放到了锁池中。该线程从锁池中获得机锁,然后回到wait()前的中断现场。
yield()方法是停止当前线程,让同等优先权的线程运行。如果没有同等优先权的线程,那么Yield()方法将不会起作用。 join()方法使当前线程停下来等待,直至另一个调用join方法的线程终止。线程的在被激活后不一定马上就运行,而是进入到可运行线程的队列中。
改变线程优先级
我们可以设置线程的优先级来让CPU尽可能的将执行的资源给优先级高的线程。Java设置了1-10这10个优先级,又有三个静态变量来提供三个优先级:
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
我们可以通过setPriority来设置线程的优先级,可以直接传入上诉三个静态变量,也可以直接传入1-10的数字。设置后线程就会有不同的优先级。如果我们不设置优先级,会是什么情况?
线程的优先级是有继承的特性,如果我们在A线程中启动了B线程,则AB具有相同的优先级。一般我们在main线程中启动线程,就和main线程有一致的优先级。main线程的优先级默认是5。
下面说一下优先级的一些规则:
- 优先级高的线程一般会比优先级低的线程获得更多的CPU资源,但是不代表优先级高的任务一定先于优先级低的任务先执行完。因为不同优先级的线程中run方法内容可能不一样。
- 优先级高的线程一定会比优先级低的线程执行的快。如果两个线程是一样的run方法,但是优先级不一样,确实优先级高的线程先执行完。
线程同步
线程同步大抵上是线程最核心也是最重要的方面了。
因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块。假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个呢?很难说清楚。因此多线程同步就是要解决这个问题。
线程同步的基本内容如下图所示:
线程安全问题
同步代码块
如果同步方法是需要执行一个很长时间的任务,那么多线程在排队处理同步方法时就会等待很久,但是一个方法中,其实并不是所有的代码都需要同步处理的,只有可能会发生线程不安全的代码才需要同步。这时,可以采用synchronized
来修饰语句块让关键的代码进行同步。用synchronized
修饰同步块,其格式如下:
synchronized(对象){
//语句块
}
这里的对象,可以是当前类的对象this,也可以是任意的一个Object对象,或者间接继承自Object的对象,只要保证synchronized
修饰的对象被多线程访问的是同一个,而不是每次调用方法的时候都是新生成就就可以。但是特别注意String对象,因为JVM有String常量池的原因,所以相同内容的字符串实际上就是同一个对象,在用同步语句块的时候尽可能不用String。
下面,看一个例子来说明同步语句块的用法和与同步方法的区别:
public class LongTimeTask {
private String getData1;
private String getData2;
public void doLongTimeTask(){
try{
System.out.println("begin task");
Thread.sleep(3000);
String privateGetData1 = "长时间处理任务后从远程返回的值 1 threadName=" + Thread.currentThread().getName();
String privateGetData2 = "长时间处理任务后从远程返回的值 2 threadName=" + Thread.currentThread().getName();
synchronized (this){
getData1 = privateGetData1;
getData2 = privateGetData2;
}
System.out.println(getData1);
System.out.println(getData2);
System.out.println("end task");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class LongTimeServiceThreadA extends Thread{
private LongTimeTask task;
public LongTimeServiceThreadA(LongTimeTask task){
super();
this.task = task;
}
@Override
public void run() {
super.run();
CommonUtils.beginTime1 = System.currentTimeMillis();
task.doLongTimeTask();
CommonUtils.endTime1 = System.currentTimeMillis();
}
}
public class LongTimeServiceThreadB extends Thread{
private LongTimeTask task;
public LongTimeServiceThreadB(LongTimeTask task){
super();
this.task = task;
}
@Override
public void run() {
super.run();
CommonUtils.beginTime2 = System.currentTimeMillis();
task.doLongTimeTask();
CommonUtils.endTime2 = System.currentTimeMillis();
}
}
测试的代码如下:
public class LongTimeServiceThreadATest extends TestCase {
public void testRun() throws Exception {
LongTimeTask task = new LongTimeTask();
LongTimeServiceThreadA threadA = new LongTimeServiceThreadA(task);
threadA.start();
LongTimeServiceThreadB threadB = new LongTimeServiceThreadB(task);
threadB.start();
try{
Thread.sleep(1000 * 10);
}catch (InterruptedException e){
e.printStackTrace();
}
long beginTime = CommonUtils.beginTime1;
if (CommonUtils.beginTime2 < CommonUtils.beginTime1){
beginTime = CommonUtils.beginTime2;
}
long endTime = CommonUtils.endTime1;
if (CommonUtils.endTime2 < CommonUtils.endTime1){
endTime = CommonUtils.endTime2;
}
System.out.println("耗时:" + ((endTime - beginTime) / 1000));
Thread.sleep(1000 * 20);
}
}
结果如下:
begin task
begin task
长时间处理任务后从远程返回的值 1 threadName=Thread-1
长时间处理任务后从远程返回的值 2 threadName=Thread-1
end task
长时间处理任务后从远程返回的值 1 threadName=Thread-1
长时间处理任务后从远程返回的值 2 threadName=Thread-1
end task
耗时:3
两个线程并发处理耗时任务只用了3s, 因为只在赋值的时候进行同步处理,同步语句块以外的部分都是多个线程异步处理的。
下面,说一下同步语句块的一些特性:
- 当多个线程同时执行
synchronized(x){}
同步代码块时呈同步效果。 - 当其他线程执行x对象中的
synchronized
同步方法时呈同步效果。 - 当其他线程执行x对象中的
synchronized(this)
代码块时也呈现同步效果。
细说一下每个特性,第一个特性上面的例子已经阐述了,就不多说了。第二个特性,因为同步语句块也是对象锁,所有当对x加锁的时候,x对象内的同步方法也呈现同步效果,当x为this的时候,该对象内的其他同步方法也要等待同步语句块执行完,才能执行。第三个特性和上面x为this是不一样的,第三个特性说的是,x对象中有一个方法,该方法中有一个synchronized(this)
的语句块的时候,也呈现同步效果。即A线程调用了对x加锁的同步语句块的方法,B线程在调用该x对象的synchronized(this)
代码块是有先后的同步关系。
上面说同步语句块比同步方法在某些方法中执行更有效率,同步语句块还有一个优点,就是如果两个方法都是同步方法,第一个方法无限在执行的时候,第二个方法就永远不会被执行。这时可以对两个方法做同步语句块的处理,设置不同的锁对象,则可以实现两个方法异步执行。
注意同步监视器的使用流程为:加锁、修改、释放锁
同步方法
其实,无论是同步代码块和同步方法,同步监视器都是存在的,只不过同步方法的同步监视器是this,即调用该方法的对象。
释放同步监视器的锁定
同步锁
- 即Lock接口及其子接口所代表的对象;
- 它是一种显式锁,它自己本身就是同步监视对象,它的用法是和try-final块搭配使用:
死锁
线程通信
线程通信的主要内容如下:
传统的线程通信
wait(),notify(),notifyAll()
Object提供了三个方法wait(), notify(), notifyAll()在线程之间进行通信,以此来解决线程间执行顺序等问题。
- wait():释放当前线程的同步监视控制器,并让当前线程进入阻塞状态,直到别的线程发出notify将该线程唤醒。
- notify():唤醒在等待控制监视器的其中一个线程(随机)。只有当前线程释放了同步监视器锁(调用wait)之后,被唤醒的线程才有机会执行。
- notifyAll():与上面notify的区别是同时唤醒多个等待线程。
值得注意的是这三个方法是属于Object而不是属于Thread的,但是调用的时候必须用同步监视器来调用,
- 对于synchronized修饰的同步方法,由于方法所在类对象(this)就是同步监视器,因此可以直接在同步方法中调用这三个方法;
- 对于同步代码块,synchronized(obj) { … },则需要用空号钟的obj来调用。
生产者-消费者问题模型
在经典的生产者-消费者问题中,需要使用线程通信来解决。
假设有这么一个场景,有一个线程需要存钱进一个账户,有多个线程需要从这个账户取钱,要求是每次必须先存钱之后才能取钱,而且取钱之后必须存钱,
存钱和取钱不能同时发生两次,而是要保持顺序不变,如何实现这个需求呢。
下面是用同步方法结合线程通信的方式来实现的思路,
- 首先在Account类中定义两个同步方法,deposit和draw用来确保存款和取款操作的原子性。
- 在Account类中定义用标识符flag, 由deposit和draw共用。初始值为false,表示只能存款。 如果为false,表示只能取款。
- 定义一个存款线程类,去调用Account类的同步方法deposit,在deposit中先对flag进行判断,如果不为false,则调用wait阻塞存款线程,等待取款线程发出notice。存款完成之后,将flag改为true.
- 定义一个取款线程类,去调用Account类的同步方法draw,在draw中先对flag进行判断,如果不为true,则调用wait阻塞取款线程,等待存线程发出notice。取款完成之后,将flag改为false.
- 定义测试类,同时启动一个(或多个)存款线程进行存款,同时启动多个取款线程去取款,存款(取款)线程之间不会有先后顺序,但是存款和取款直接会有严格的先后顺序,这就解决了生产者消费者问题
其中实现的内容为了阅读方便,如下所示:
![](智能截图%205.png)
使用Condition控制线程通信
如果程序使用lock来同步线程的话,就要使用condition来进行线程通信。
在lock同步线程中,lock 对象就是一个显示的同步监视器,但是这个显示的同步监视器不直接阻塞或者通知线程,而是通过condition——lock对象通过调用newCondition方法返回一个与lock关联的condition对象,由condition对象来控制线程阻塞(await)和发出信号(single)唤醒其他线程。
与synchronized同步线程方式对应的是,conditions方式也提供了三个方法,
-
await:类似于synchronized隐式同步控制器对象调用的wait方法,可以阻塞当前线程,直到在别的线程中调用了condition的singal方法唤醒该线程。
-
signal:随机唤醒一个被await阻塞的线程。注意只有在当前线程已经释放lock同步监视器之后,被唤醒的其他线程才有机会执行。
-
signalAll:与上面类似,但是是唤醒所有线程。
阻塞队列控制线程通信
BlockingQueue是JAVA5提供的一个队列接口,但这个队列并不是用作一个容器,而是作为线程的同步工具。
它可以很好地解决生产者消费者问题,而且比前面提到的两种方式更为灵活,
BlockingQueue的特征是,
当生产者线程试图向BlockingQueue存入元素时,如果队列已满,生产者线程将会阻塞,
当消费者线程试图从BlockingQueue取出元素时,如果队列为空,消费者线程将会阻塞
对比前面线程通信的例子,synchronized同步方法/代码块和lock+condition方式中,都只能控制生产者和消费者按固定顺序执行,
但BlockingQueue则是可以通过集合中的元素个数(商品数量)来控制线程执行顺序,通过调整集合容量可以控制线程切换的条件。
集合(商品)为空时,消费者阻塞,只能执行生产者线程;集合(商品)已满时,生产者阻塞,只能执行消费者线程。
线程组和未处理的线程
一个线程组核心的信息是:名称、优先级、是否守护、父线程组、子线程组
线程池
线程池的基本内容如下:
其中Lambda并行发生时,使用的便是ForkJoinPool线程池。
多线程
与数据库连接池相似,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable和一个Callable对象传给线程池,线程池就会启动一个空闲的线程来执行它们的run()或call()方法,当run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run()或call()方法
系统启动一个新线程的的成本比较高。
Java 8改进的线程池
Executors
Executors类
Executors类是生成线程池的工厂类
一、四种线程池
Java通过Executors提供四种线程池,分别为:
1、newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
2、newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3、newScheduledThreadPool
创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行。
4、newCachedThreadPoo
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
Executor接口
ExecutorService接口
ScheduledExecutorService接口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RBlP551a-1604758713723)(scheduledhttps://i-blog.csdnimg.cn/blog_migrate/9d7c34e597d37aacfc8ba0e7b3d5701d.png)]
使用步骤
Java 8增强的ForkJoinPool
线程相关类
线程相关类基本内容如下:
ThreadLocal
包装线程不安全的集合
线程安全的集合类
总结
这篇文章,到这里也就结束了,最近笔者的腰很不舒服,尤其是长时间坐着就更加难受,希望看到这篇文章的各位程序员照顾好自己的身体,在自己最佳的时间里做最好的工作,拥有好的身体和心情,开开心的工作。
2020年11月7日22:04:43于AUX