文章目录
一、基本概念
程序:
为完成特定任务,用某种语言编写的一组指令的集合
。即指一段静态的代码
,静态对象
进程(process):
程序的依次执行过程,或是正在内存中运行的应用程序。如:运行的微信,运行的有道云笔记,运行的360安全软件等等。
- 每个进程都有一个独立的内存空间(所以多进程下全局变量不能共享),系统运行一个程序即是一个从进程从创建,就绪,运行到消亡的过程(声明周期)
- 程序是静态的,进程是动态的。
- 进程作为操作系统调度和分配资源的最小单位(也是系统运行程序的基本单位),系统在运行时会为每个进程分配不同的内存区域。
- 现代的操作系统,大都支持多进程的,支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板
- 进程之间不共享内存,即全局变量不能共享。
线程(thread):
进程可进一步细化线程,是程序内部的一条执行路径
。一个进程至少有一个线程。比如360安全软件可以一边杀毒,一边清理垃圾文件,一边进行启动项优化;像QQ开多个窗口等等。
- 一个进程同一时间若
并行
执行多个线程,就是支持多线程的。就是操作系统的任务调度器能在不同的时间切换不同的线程,如下是简单的进程线程图
- 线程作为
CPU调度和执行的最小单位
- 一个进程中的多个线程共享相同的内存单元,他们从同一个内存堆中分配对象,可以访问相同的变量和对象。这使得线程间通信更简便、高效。但多线程操作共享的系统资源可能会带来
安全的隐患
。 - 下图是多线程的JVM实例图:
线程和进程的区别:
- 进程一个运行中的程序。
- 运行中的进程的一条或多条执行路径
线程的应用场景:
- 手机app应用的图片下载
- 迅雷下载
- Tomcat的web应用,多个客户端发起的请求,Tomcat针对多个请求开辟多个线程进行处理
并行和并发的概念
并行
:指两个或多个事件在同一个时间点
发生(同时发生)。指在同一个时间点,多条指令在多个CPU
上同时执行。比如,多个进程同时做不同的任务。比如一套采集系统,放在不同的服务器上爬取优秀的Java学习资料,其中A服务器只负责的是CSDN,B服务器只负责的是51cto,C服务器只负责的是稀土掘金等等。并发
:指两个或多个事件在同一个时间段
内发生。即在同一个时间段
内,有多条指令在单个CPU上``快速轮换,交替执行
,使得宏观上具有多个进程同时执行的效果。
二、线程的启动和创建
概述
- Java语言的JVM运行程序运行多个线程,使用
java.lang.Thread
类代表线程
,所有的线程对象都必须Thread类或其子类的实例。 - Thread类的特性
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,因此把
run()
方法体称为线程执行体
。 - 通过该Thread对象的
start()
方法来启动这个线程,而非直接调用run()
- 要想实现多线程,必须在主线程中创建新的线程对象。
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,因此把
实现方式1:继承Thread类
java通过继承Thread类来创建并启动多线程的步骤如下:
- 创建一个继承于Thread类的子类
- 重写Thread类的
run()
方法—>将此线程需要执行的操作,声明在此方法体中 - 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
实现方式2:实现Runnable接口
由于Java是单继承模式,如果一个类在继承了Thread类之后就不能再去继承其他类了,这样子处理业务有局限性,所以就通过实现Runnable接口的方式来实现多线程,同时又能继承其他类。
- 创建一个实现
Runnable
接口的类 - 实现接口中的
run()
方法—>将此线程需要执行的操作,声明在此方法体中 - 创建当前实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例,(其实此对象其实传递的是共享数据)
- Thread类的实例调用start();1.启动线程,2.调用当前线程的run()
两种方式的对比
- 共同点:
- 1、启动线程,使用的都是Thread类中定义的start()
- 2、创建线程对象,都是Thread类或其子类的实例。
- **不同点:**Thread是类的继承,Runnable是接口的实现。
- Runnable接口实现可以当作是一个任务处理,继承Thread就是当作是一个子线程
- 建议:使用实现Runnable接口的方式
- Runnable方式的好处:
- 1、实现的方式,避免类的单继承的局限性
- 2、更适合处理有共享数据的问题。
- 3、实现了代码和数据的分离。
- 代码格式不同:
- 继承自Thread类:
public class MyThreadTest {
public static void main(String args) {
MyThread t = new MyThread();
t.start(); //①启动线程,②调用t1.run()
}
}
class MyThread extends Thread {
@Override
public void run() {
//子线程的逻辑处理
...
}
}
- 实现Runnable接口:
public class MyTheadTest2 {
public static void main(String args) {
//创建一个MyRunnable对象作为一个任务传给线程
MyRunnable task = new MyRunnable();
MyThread2 t = new Thread(task);
t.start
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
//子线程的逻辑处理
...
}
}
- 两者关系:其实Thread类也是基于继承字Runnable接口来实现的
- public class Thread implements Runable
三、线程的常用结构
线程中的构造器
- public Thead():分配一个新的线程对象。
- public Thread(String name):分配一个指定名字的新的线程对象。
- public Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法
- public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象,并指定名字
线程中的常用方法
- start():①启动线程,②调用线程的run()方法
- run():将线程中要执行的操作,声明在run()中。
- currentThread():
静态方法
,获得当前执行代码对应的线程名 - setName(String name):给当前线程设置名字
- getName():获得当前线程的名字
- sleep(long millSecond):
静态方法
,当前线程休眠的指定毫秒数 - yield():
静态方法
,一旦执行此方法,当前线程就先暂停运行,释放CPU的执行权,切换给其他线程。等重新获得执行权后,接着上一个位置继续往下执行 - join():等待某个线程结束,在线程a通过线程b调用join(),意味着线程a进入阻塞状态,直到b线程执行结束,线程a才结束阻塞状态,继续往下执行。使用场景:a线程需要通过b线程获得的数据共享,才能处理下一步逻辑。像b线程在做网络请求获取数据,需要一定的时间,可以用这种方式。
- isAlive():线程是否存活。
stop():不建议使用,已过时。强行结束一个线程的执行,直接进入死亡状态。run()即刻停止,可能会导致一些清理工作无法完成,如文件,数据库等的关闭。同时,会立即释放该线程所持有的所有锁,导致数据得不到同步处理,出现数据不一致的问题。suspend() / resume():不建议使用,已过时。这两个操作就好比播放器的暂停和恢复。二者必须成对出现,否则容易死锁,因为suspend()调用会导致线程暂停,但不会释放锁,导致其他线程都无法访问被它占用的锁,直到调用resume()。
线程的优先级
每个线程都有一定的优先级,同时优先级线程组成先进先出队列(先到先服务),使用分时调度策略。优先级高的线程采用抢占式策略,获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。
在单核CPU下的运行效果明显,在多核CPU下,不怎么明显。
- Thread类的三个优先级常量
- MAX_PRIORITY(10):最高优先级
- MIN_PRIORITY(1):最低优先级
- NORM_PRIORITY(5):普通优先级,默认情况下main线程具有普通优先级,即使main优先级最高
- public final int getPriority():返回线程优先级
- public final void setPriority(int newPriority):改变线程的优先级,范围在[1,10]之间
4 线程的生命周期
Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下一些状态:
- JDK1.5之前5种状态:
- JDK1.5之后的6种状态:
或借鉴网上找的图:
四、线程安全
场景:库存超卖
- 由于秒杀业务是属于高并发的,需要使用多线程。当一个线程在处理并发请求时,遇到了耗时业务时可能会阻塞,其实流程并未结束,还没减库存,CPU把执行权让给其他线程;而其他线程进来还是认为库存>0的,继续往下执行,遇到耗时业务的时候又阻塞,同样也是流程没走完;
- 在耗时业务结束后,线程拿到CPU执行权,继续下一个流程即对库存进行减操作,当最后一个库存=1时,同时又存在多个线程来完成还未结束的流程进行减库存时,超卖现象就出现了。
- 因为库存是一份共享数据,所以那些未执行完流程的线程继续来对同一个库存数据进行减操作,导致库存变成负数
解决方案:同步机制
采用线程
同步机制
来处理,即必须保证当前线程A在减库存时,其他线程必须等待,直到线程A操作库存结束以后,其他线程才可以进来继续操作库。
方式1:同步代码块
- 代码格式:
...
synchronized(同步监视器) {
//需要被同步的代码
}
...
- 说明:
- 需要被同步(上锁)的代码即为操作共享数据的代码。
- 共享数据:即多个线程需要操作的数据,比如:库存
- 需要被同步的代码,在被synchronized包裹以后,就是的一个线程在操作这些代码过程中,其他线程必须等待。
- 同步监视器,俗称锁。哪个线程获取了锁,那个线程就能执行需要被同步的代码。
- 同步监视器,可以使用任何一个类的对象充当。但是,多个线程必须共用同一个监视器
- 注意:
- 在实现Runnable接口的方式中,因对象实例共享的是同一份,所以同步监视器可以考虑使用:this
- 在继承Thread类的方式中,因为多个线程是多个实例对象,同步监视器要慎用this,可以使用
当前类.class
或者使用一个静态的对象
来表示
方式2:同步方法
- Runnable接口实现的同步监视器:
// 此时的同步监视器是:this。外部线程调用的对象实例 xxx 是一个共享数据,是唯一的
public synchronized void showInfo() {
....
}
- 继承Thread类的同步监视:
// 此时同步监视器为当前类本身,即为ThreadSyncMethod.class
public synchronized void rushToBuy() {
...
}
- 说明:
- 如果操作共享数据的代码完整的声明在一个方法中,那么我们就可以将此方法声明为同步方法即刻
- 非静态的同步方法,默认同步监视器为
this
- 静态的同步方法,默认同步监视是
当前类.class
同步机制的利弊:
- 好处:解决了线程的安全问题
- 弊端:在操作共享数据时,多线程其实是串行执行的,意味着性能低。
同步机制带来的死锁
- 概念:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁。
-
如何看待死锁?
- 我们编写程序时,要避免死锁
-
诱发死锁的原因?
不管是同步代码块还是同步方法,两个线程s1,s2同时相互等待对方释放锁的时候,就会产生死锁;即线程s1的程序调用了s2的程序,s2里面上了同步锁,同时线程s2的程序也调用了s1的程序,s1里面也上了同步锁。这就会造成同时等待锁的释放,但都因为阻塞了,无法释放导致死锁了。
- 互斥条件
- 占用且等待
- 不可抢夺
- 循环等待
以上4个条件,同时出现就会触发死锁。
- 如何解决死锁?
死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。
- 针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
- 针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
- 针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
- 针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。
jdk1.5的Lock锁
除了使用synchronized同步机制处理线程安全问题处理外,还可以使用jdk5.0提供的ReentrantLock锁的方式,实际于JUC的。
- 创建Lock锁的步骤:
- 1、创建Lock的实例,需要确保多个线程共用同一个Lock实例,需要考虑将此对象声明为static final
- 2、执行lock()方法,锁定共享资源的调用
- 3、unlock()的调用,释放对共享数据的锁定
- synchronized的同步方式与Lock的对比?
- synchronized不管是同步代码块还是同步方法,都需要在结束一对{}之后释放对 同步监视器的调用。
- Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高。
五、线程间通信
线程之间通信
为什么要处理线程间通信:
当我们需要多个线程
来共同完成一件任务,并且我们希望他们有规律的执行
,那么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同操作一份数据。
比如:线程A用来生产包子的,线程B用来吃包子的,包子可以理解为同一个资源,线程A与线程B处理的动作,一个是生产,一个是消费,此时B线程必须等到A线程完成后才能执行,那么线程A与B之间就需要线程通信,就是–等待唤醒机制,以此实现多线程共同操作一份数据。
等待唤醒机制
- 当
线程1
在进入wait
状态后,就会阻塞,并释放同步锁(同步监视器),方便线程2
进来; 线程2
拿到同步锁之后,就会将等待状态的线程1
给notify
(多个则按优先级高低,相同则随机),线程1状态此时进入了就绪状态;- 当
线程2
执行到了wait
状态,并释放了同步锁,线程1
在得到了同步锁时,就会接着上一次wait
的位置继续往下执行到结束。 - 如下序列图所示:
涉及到三个方法的使用:
- wait():线程一旦执行此方法,就进入等待状态。同时,会释放对同步锁的调用。
- notify():一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程。(如果被wait()的多个线程的优先级相同,随机唤醒一个)。被唤醒的线程从当初被wait的位置继续执行。
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
注意点:
- 上面三个方法的使用,必须是在同步代码块或同步方法中使用。但不能在JUC中的Lock里面使用。
- 因为这三个方法是和同步监视器绑定的。否则就会报错。
- 因同步监视器对象继承自Object,所以这三个方法,所以这3个方法是在Object中声明。
wait()和sleep()的区别:
**相同点:**一旦执行,当前线程就会进入阻塞状态。
不同点:
- 声明的位置:
- wait():声明在Object类中
- sleep():声明在Thread类中,属于静态方法
- 使用的场景不同:
- wait():只能使用在同步代码块或同步方法中
- sleep():可以在任何需要使用的场景
- 使用在同步代码块或同步方法中:
- wait():一旦执行,会执行同步监视器
- sleep():一旦执行,不会释放同步监视
- 结束阻塞的方式:
- wait():到达指定时间自动结束阻塞或通过被notify唤醒,结束阻塞
- sleep():到达指定时间自动结束阻塞
六、线程池
待续
七、总结:
- 在多线程模式下,如果存在共享数据,就会出现因并发引起的资源抢占的情况,需要加同步机制,尽量避免出现thread.sleep()等线程阻塞的情况。