目录
概述:
程序、进程、线程的理解:
总结:
1.程序:是为了完成特定任务、用某种语言编写的一组指令集合。即指一段静态的代码。
2.进程:正在内存中运行的一个程序即进程。进程也是程序的一次执行过程。
说明:进程作为资源分配的单位,系统在运行时会为每个进程分配不同的独立的内存区域。
区分程序和进程:放在硬盘中的静态代码叫程序,放在内存中运行的代码叫进程。
3.线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。
说明:线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
总结:一个程序运行后至少有一个进程,一个进程中可以包含多个线程。
程序,进程,线程的举例:比如360安全卫士,我们打开360安全卫士/腾讯电脑管家等,看任务管理器,发现运行的就是进程(正在运行的程序,即进程),我们既可以用360程序体检,同时可以杀毒,同时可以电脑清理,系统修复等。而我们可以认为体检、杀毒、清理,系统修复等是一个个线程。
如下图演示:
线程调度的方式:
1.分时调度:所有线程轮流使用cpu的使用权,平均分配每个线程占用cpu的时间。
2.抢占式调度:优先让优先级高的线程使用cpu,如果线程的优先级相同,那么会随机选择一个(线程随机性),注意java采用的是抢占式调度。
还可以通过如下图方式设置线程的优先级:
线程抢占式调度详情:
jvm内存结构图:
上图说明:
1.进程可以细化为多个线程。
2.每个线程,拥有自己独立的:栈(即虚拟机栈),程序计数器。
3.多个线程,共享同一个进程中的结构:方法区、堆。
并行与并发:
形象理解并发和并行:
并发相当于闪电侠(相当于一个cpu),并行相当于鸣人的影分身(多个cpu,或者多核cpu),他们两个人都同时 画两幅画,闪电侠快速在两幅画之间切换着画,而鸣人直接开启影分身 同时分别 在两幅画上作画。
并发:在一个时间段内发生若干事件;单核cpu同时执行多个任务时,时间片进行很快的切换,线程轮流执行cpu。比如一个人一个时间段内做不同的事。
并行:在同一时刻发生若干事件;多核cpu同时执行多个任务。比如,高速公路8车道同时通过好几辆车,秒杀。
如下图演示:
总结:
所以,并发是在一段时间内宏观上多个程序同时运行,并行是在某一时刻,真正有多个程序在运行。
并行和并发的区别:
并发,指的是多个事情,在同一时间段内同时发生了。
并行,指的是多个事情,在同一时间点上同时发生了。
并发的多个任务之间是互相抢占资源的。
并行的多个任务之间是不互相抢占资源的、
只有在多CPU或者一个CPU多核的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。
例如使用单核CPU,多个工作任务是以并发方式运行的,因为只有一个CPU,各个任务分别占用一段时间,再切换到其他任务,等到下一次CPU使用权是再次执行未完成的任务。
使用多核CPU时,可以将任务分配到不同的核同时运行,实现并行。
多线程概述:
多线程概念:
多线程是指,将原本线性执行的任务分开成若干个子任务同步执行,这样做的优点是防止线程“堵塞”,增强用户体验和程序的效率。缺点是代码的复杂程度会大大提高,而且对于硬件的要求也相应地提高 。
主线程:执行主方法(main方法)的线程。
单线程程序:java程序只有一个线程。执行从main方法开始,从上到下依次执行。
为什么用多线程?
多线程能实现的都可以用单线程来完成,那单线程运行的好好的,为什么java要引入多线程的概念呢?
多线程的好处:
1.方便的通信和数据交换。
2.充分利用cpu资源,目前几乎没有线上的cpu是单核的,发挥多核cpu强大的能力
多线程的优点:
多进程程序结构和多线程程序结构有很大的不同,多线程程序结构相对于多进程程序结构有以下的优势:
1、方便的通信和数据交换
线程间有方便的通信和数据交换机制。对于不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其他线程所用,这不仅快捷,而且方便 。
2、更高效地利用CPU
使用多线程可以加快应用程序的响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作置于一个新的线程,就可以避免这种尴尬的情况 。
同时,多线程使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
何时使用多线程?
1.当我们处理一个消耗大的任务(如上传或下载图片),如果让主线程执行这个任务,它会等到动作完成,才继续后面的代码。在这段时间之内,主线程处于“忙碌”状态,也就是无法执行任何其他功能。体现在界面上就是,用户的界面完全“卡死”,这时候就需要另外一个单独开辟的路径(多线程)去执行下载上传图片。
多线程执行的内存图解:
当每次创建一个线程的时候,都会开辟新的栈空间,运行其run方法。进行压栈和出栈操作。
上下文切换
多核cpu下,多线程是并行工作的,如果线程数多(一个进程运行的任务多,开辟的线程多),单个核又会并发的调度线程,运行时会有上下文切换的概念
cpu执行线程的任务时,会为线程分配时间片,以下几种情况会发生上下文切换。
线程的cpu时间片用完
垃圾回收
线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当发生上下文切换时,操作系统会保存当前线程的状态,并恢复另一个线程的状态,jvm中有块内存地址叫程序计数器,用于记录线程执行到哪一行代码,是线程私有的。
多线程的创建:
多线程的创建方式一:继承Thread类
1.创建一个继承Thread类的子类。
2.重写Thread类的run()方法—>将此线程执行的操作声明在run()中。
3.创建Thread类的子类对象。(在主线程中做)
4.通过此对象调用start().
代码演示:
package com.fan.thread1;
//1.创建一个继承于Thread类的子类
class MyThread extends Thread {
//2.重写Thread类的run方法
@Override
public void run() {
//我们自己写的要执行的逻辑
for (int i = 0; i < 100; i++) {
if (i % 2 == 0){
System.out.println(i);
}
}
}
}
public class MyThreadTest{
public static void main(String[] args) {
//3.创建Thread类的子类的对象
MyThread t1 = new MyThread();
//4. 通过此对象调用start()
t1.start();
//问题一:我们不能通过直接调用run()的方式启动线程,只能普通调用run方法
//t1.run();
/*问题二:再启动一个线程,遍历100以内的偶数,不可以
还让已经start()的线程去执行。会报java.lang.IllegalThreadStateException异常
*/
//t1.start();
//两个for循环交替进行。两个线程并列运行。
for (int i = 0; i < 100; i++) {
if (i % 2 == 0){
System.out.println(i + "main()" + "****");
}
}
}
}
代码解释:
t1是一个子线程,就好比孩子是从母亲肚子里出来的一样,此t1也是主线程帮我们造的。当我们调用t1.start()的时候,相当于t1自己独立去运行了。然后运行的是run里面的逻辑。此时程序中既有主线程,也有分线程,两者是并列的。孩子和母亲各自忙自己的。
说明:对象调用start()方法有两个作用:第一个是启动当前线程,第二个是调用当前线程的run方法。
第二个带main()" + “****” 的for循环是仍然在main线程中执行的。
问题一:
如果仅仅通过线程对象调用run方法,则仅仅是第二个是调用当前线程的run方法。 然而并没有启动线程。
问题二:再启动一个线程,遍历100以内的偶数,不可以还让已经start()的线程去执行。会报java.lang.IllegalThreadStateException异常。如果要启动多线程,那我们就创建多个线程对象并调用start方法。
如图运行:注意此时主线程和子线程同时抢夺cpu的时间片段。所以谁先执行不确定。
练习:
代码演示:
package com.fan.thread1;
public class MyThreadTest{
public static void main(String[] args) {
new Thread(){//创建Thread类的 匿名子类对象 并调用start方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
new Thread(){//创建Thread类的 匿名子类对象 并调用start方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
}
}
多线程的创建方式二:实现Runnable接口
1.创建一个实现了Runnable接口的类。
2.实现类去实现Runnable中的抽象方法:run().
3. 创建实现类的对象。
4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象。
5. 通过Thread类的对象调用start().
代码演示创建线程的第二种方式:
package com.fan.thread2;
/*1.创建一个实现了Runnable接口的类。
2.实现类去实现Runnable中的抽象方法:run().
3. 创建实现类的对象。
4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象。
5. 通过Thread类的对象调用start().*/
public class RunnableTest {
public static void main(String[] args) {
//3. 创建实现类的对象。
MyRunnable myRunnable = new MyRunnable();
//4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象。
Thread t1 = new Thread(myRunnable);
//5. 通过Thread类的对象调用start().
t1.start();
//再创建另外一个分的子线程
Thread t2 = new Thread(myRunnable);
t2.start();
}
}
//1.创建一个实现了Runnable接口的类。
class MyRunnable implements Runnable {
//2.实现类去实现Runnable中的抽象方法:run().
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
//注意:这里获取当前线程的名字不能通过直接getName()来实现(本类没有继承Thread类及其getName方法)
// 本类实现的接口中全是抽象方法,根本就没有getName方法,所以我们要通过类调用静态方法的方式。
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
两种创建线程方式的比较:
开发中:优先选择:实现Runnable接口的方式。
原因:
1.实现的方式,没有类的单继承的局限性(可能本类还有一个自己的父类,这时候就不能再多继承线程类了)。
2.实现的方式更适合来处理多个线程有共享数据的情况(天然共享一份new Runnable()对象。当开多个线程的时候,参数都是同一份Runnable的子类对象。)。
联系:public class Thread implements Runnable
相同点:两种方式都是需要重写run(),将线程要执行的逻辑声明在run方法中。目前两种方式,想启动线程,都是调用的Thread类中的start();
多线程的创建方式三:实现Callable接口(JDK5.0新增的)
第一步:创建一个实现Callable的实现类。
第二步:实现call方法,将此线程需要执行的操作声明在calll()方法中。
第三步:创建Callable接口实现类的对象。
第四步:将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
第五步:将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start().
第六步:获取Callable中call方法的返回值。( Object o = futureTask.get();)
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
1.call()方法是可以有返回值的。
2.call()方法可以抛出异常,被外面的操作捕获,获取异常信息。
3.Callable是支持泛型的。
代码演示实 现Callable接口的方式创建多线程:
package com.fan.callable;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象。
MyCallable myCallable = new MyCallable();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(myCallable);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start().
Thread t1 = new Thread(futureTask);
t1.start();
try {
//6.获取Callable中call方法的返回值。
//get()返回值即为FutureTask构造器参数Callable实现类重写call()的返回值。
Object o = futureTask.get();
System.out.println("返回值总和为:" + o);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
//1.创建一个实现Callable的实现类
class MyCallable implements Callable {
//实现call方法,将此线程需要执行的操作声明在calll()方法中。
public Object call() throws Exception {
int sum= 0;
for (int i = 0; i <= 100 ; i++) {
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
多线程的创建方式四:使用线程池(JDK5.0新增的)
举例,当我们用手机浏览一个旅游网或者图片多的网页(像一些赌博网站)的时候,一张图片下面配一段文字说明来描述图片。当我们往上滑的时候,就是一个线程帮我们在加载,然后每一个条目(item,这里假设是一张图片和一段文字),当我们上划一下的时候,就划过好几个条目,每一个条目需要一个线程去下载图片,我们这一页就需要很多线程去创建和销毁(当我们划走的时候),然后当我们一直上划的话,有些图片由于网速差还没下载下来我们就划走它了,这就造成了大量线程的创建和销毁。浪费cpu资源,提高响应速度等。
鉴于上述的资源消耗问题,我们会像使用我们的工具箱一样(重复利用原则),提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。(工具箱)。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
比如你要去北京天安门,你需要一辆车,然后发现没有车,你自己造了一辆自行车,到了天安门,使用完了,这个车你就销毁了,然后你就回不来了。(如果你在天安门乱喊,可能抓你。动不动有人查你身份证的)
代码演示线程池创建线程:
package com.fan.threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPool {
public static void main(String[] args) {
//1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread1());//适合于Runnable接口的实现类
service.execute(new NumberThread2());
//service.submit(Callable callable);//适合于使用Callable
//关闭连接池/归还线程
service.shutdown();
}
}
class NumberThread1 implements Runnable{
public void run() {
//循环打印偶数的逻辑
for (int i = 0; i <= 50; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
//另一个线程任务类
class NumberThread2 implements Runnable{
public void run() {
//循环打印偶数的逻辑
for (int i = 0; i <= 50; i++) {
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
Thread类的常用方法:
- start():启动当前线程;并调用当前线程的run方法。
- run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中。
- currentThread() :静态方法,返回执行当前代码的线程。
- getName() :获取当前线程的名字。
- setName() : 设置当前线程的名字。
- yield():线程的礼让方法, 释放当前cpu的执行权,让运行中的线程切换到就绪状态,重新争抢cpu的时间片。
- join(): 在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态,继续执行。
- stop():已经过时,当执行次方法时,强制结束当前线程。
- sleep(long millitime):让当前运行中的线程“睡眠”指定的毫秒数。在指定的毫秒数时间内,当前线程是阻塞状态。当休眠时间结束后,重新争抢cpu的时间片继续运行
- isAlive():判断当前线程是否存活。
守护线程:
守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默完成一些系统性的服务,比如垃圾回收线程,JIT线程就可以理解为守护线程。
与守护线程相对的是用户线程,用户线程可以认为是系统的工作线程,它会完成这个程序要完成的业务员操作。如果用户线程全部结束,则意味着这个程序无事可做。守护线程要守护的对象已经不存在了,那么整个应用程序就应该结束。因此,当一个Java应用内只有守护线程时,Java虚拟机自然退出。
可以通过Thread.setDaemon设置守护线程。
注意:守护线程必须在start之前设置,否则会报错。
代码演示:
public class DaemonDemo {
public static void main(String[] args) {
Thread daemonThread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
System.out.println("i am alive");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("finally block");
}
}
}
});
daemonThread.setDaemon(true);//设置成为守护线程
daemonThread.start();
//确保main线程结束前能给daemonThread能够分到时间片
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
线程的打断-interrupt()
// 相关方法的定义
public void interrupt() {
}
public boolean isInterrupted() {
}
public static boolean interrupted() {
}
isInterrupted() 获取线程的打断标记 ,调用后不会修改线程的打断标记.
打断标记:线程是否被打断,true表示被打断了,false表示没有
interrupt()方法用于中断线程
1.可以打断sleep,wait,join等显式的抛出InterruptedException方法的线程,但是打断后,线程的打断标记还是false(没有被打断)
2.打断正常线程 ,线程不会真正被中断,但是线程的==打断标记(isInterrupted)==为true(被打断了)
interrupt实例: 有个后台监控线程不停的监控,当外界打断它时,就结束运行。代码如下
@Slf4j
class TwoPhaseTerminal{
// 监控线程
private Thread monitor;
public void start(){
monitor = new Thread(() ->{
// 不停的监控
while (true){
Thread thread = Thread.currentThread();
// 判断当前线程是否被打断
if (thread.isInterrupted()){
log.info("当前线程被打断,结束运行");
break;
}
try {
Thread.sleep(1000);
// 监控逻辑中被打断后,打断标记为true
log.info("监控");
} catch (InterruptedException e) {
// 睡眠时被打断时抛出异常 在该处捕获到 此时打断标记还是false
// 在调用一次中断 使得中断标记为true
thread.interrupt();
}
}
});
monitor.start();
}
public void stop(){
monitor.interrupt();
}
}
按快捷键 Alt+7就能显示当前类中的所有方法、全局常量,方法还包括形参和返回值,一目了然……打开界面如下:
代码演示线程常用方法(getName()、setName()、Thread.currentThread() ):
package com.fan.thread1;
import static java.lang.Thread.yield;
public class MyThread1Test {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
//给子线程起名字的方式一:
t1.setName("子线程1:");
t1.start();//从这里才进入子线程,其他代码全部是主线程执行的。
//给子线程起名字的方式二:通过类的构造器,传一个String name 参数。
/*MyThread1 t2 = new MyThread1("子线程2");
t2.start();*/
//同样,我们也可以给主线程起名字
Thread.currentThread().setName("主线程:");
//主线程中的其他代码。
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
if(i % 20 == 0){
yield();//释放cpu的执行权
}
}
}
}
class MyThread1 extends Thread{
//子类无参构造
public MyThread1() {
super();
}
//子类有参构造调用父类有参构造
public MyThread1(String name) {
super(name);//我们直接去调用父类Thread的单参构造去起名字
}
@Override
public void run() {
//我们要让分线程帮我们运行的逻辑
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
join方法的代码演示:
join(): 在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态,继续执行。
package com.fan.thread1;
public class MyThread1Test {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
//给子线程起名字的方式一:
t1.setName("子线程1:");
t1.start();//从这里才进入子线程,其他代码全部是主线程执行的。
//同样,我们也可以给主线程起名字
Thread.currentThread().setName("主线程:");
//主线程中的其他代码。
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
if(i == 20 ){
try {
t1.join();//t1加入了主线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//判断线程是否存活
System.out.println(t1.isAlive());
}
}
class MyThread1 extends Thread{
//子类无参构造
public MyThread1() {
super();
}
//子类有参构造调用父类有参构造
public MyThread1(String name) {
super(name);//我们直接去调用父类Thread的单参构造去起名字
}
@Override
public void run() {
//我们要让分线程帮我们运行的逻辑
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
sleep(毫秒数):方法演示
线程的优先级:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5 -->默认的优先级
2.如何获取和设置当前线程的优先级:
getPriority():获取线程的优先级。
setPriority(int p):设置线程的优先级。
说明:高优先级的线程要抢占低优先级cpu的执行权,但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。(只是高优先级被执行的概率高一点。就和中彩票一样)
线程的分类:
守护线程
为用户线程服务的线程:如垃圾回收,内存管理等线程。新建的线程最初都是用户级线程,可以通过setDaemon()方法设置成守护线程。
用户线程
一般用户使用的线程,通过继承Thread类后者实现Runnable接口等实现的线程。
线程的生命周期:
线程的5种状态:
1.新建状态:创建线程对象时的状态
2.可运行状态(就绪状态):调用start()方法后进入就绪状态,也就是准备好被cpu调度执行
3.运行状态:线程获取到cpu的时间片,执行run()方法的逻辑
4.阻塞状态: 线程被阻塞,放弃cpu的时间片,等待解除阻塞重新回到就绪状态争抢时间片
5.死亡状态: 线程执行完成或抛出异常后的状态
另外–线程的6种状态:
1.NEW(新建状态): 线程对象被创建,没调用start前。
2.Runnable(可运行状态): 可运行不一定正在运行,看是否争取到锁。线程调用了start()方法后进入该状态,该状态包含了三种情况
1)就绪状态 :等待cpu分配时间片
2)运行状态:进入Runnable方法执行任务
3)阻塞状态:BIO 执行阻塞式io流时的状态
3.Blocked(锁阻塞状态) 没获取到锁时的阻塞状态(同步锁章节会细说)
4.WAITING (无限等待)调用wait()、join()等方法后的状态,进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
5.TIMED_WAITING (计时等待):同waiting状态,调用 sleep(time)、wait(time)、join(time)等方法后的状态
6.TERMINATED(被终止) 线程执行完成或抛出异常后的状态。