1、多线程基本概念
- 程序(program)是为了完成特定任务,用某种语言编写的一组指令的集合
- 进程(process)是程序执行的一次过程,或是正在运行的程序,是一个动态的过程,有自己的生命周期
- 线程(thread)是程序内部执行的一条路径,每个线程拥有独立的运行栈、计数器
- 多个线程共享一个进程内的内存地址、堆数据
2、并行与并发的区别
- 并行:多个cpu同时执行多个任务,发生在某一刻
- 并发:一个cpu同时执行多个任务,发生在某个时间段,如: 秒杀活动
3、创建线程的方式
3.1 方式一:继承Thread类
1、继承Thread类,重写父类的run()方法
2、调用线程对象的start()方法,而不是run()方法
3.2 方式二:实现Runnable接口
1、实现Runnable接口,重写接口的run()方法
2、通过Thread类的构造器创建多线程
3、将Runnable的实现类作为参数传递给Thread类的构造器,创建Thread类的对象
4、调用Thread类的start()方法开启线程
3.3 方式三:实现Callable接口
1、实现Callable接口,重写call()方法
2、将Callable接口的实现类作为参数,传递给FutureTask类的构造器,创建FutureTask类的对象
3、将FutureTask类作为参数传递给Thread类的构造器,创建Thread类的对象
4、调用Thread类的start()方法开启线程
5、较Runnable相比,具有返回值、可抛出异常
6、获取返回值,需借助FutureTask类的get()方法,在开启start()方法之后调用
3.4 方式四:创建线程池
背景:经常创建和销毁,使用量特别大的资源,比如并发编程对性能影响很大
解决思路:提前创建好多个线程,放入线程池,使用时直接获取,使用放回池中,避免了频繁创建和销毁线程的开销
image.png
注意事项:
1、如果调用的run()方法,那就跟调用普通方法一样,无法启动多线程
2、一个线程只能调用一次start()方法,多次调用将抛出异常 "IIIeglThreadStateException"
4、继承方式和实现方式的区别
- 继承Thread:线程代码存放在Thread子类run()方法中
- 实现Runnable:线程代码存放在接口的子类的run()方法中
- 实现Runnable方式,避免了单继承的局限性
- 多个线程可以共享同一个接口实现类的对象(也叫共享数据),适合多个线程来处理同一份资源
5、多线程引发出来的连锁思考
- 由于单线程不能很好的利用cpu资源,因此使用多线程来同时处理程序,加快响应速度
- 使用多线程,肯定会涉及到多个线程共同抢占一个资源的问题,会带来线程不安全
- 解决线程不安全,就要使用同步方式,锁定共享数据
- 即一次只能被一个线程使用,使用完之后,释放锁让下一个线程使用
6、Thread常用方法
1、start():启动当前线程;调用当前线程的run()
2、run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3、currentThread():静态方法,返回执行当前代码的线程
4、getName():获取当前线程的名字
5、setName():设置当前线程的名字
6、yield():释放当前cpu的执行权
7、join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
8、stop():已过时。当执行此方法时,强制结束当前线程。
9、sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
10、isAlive():判断当前线程是否存活
7、 线程优先级
- MAX_PRIORITY:10
- MIN _PRIORITY:1
- NORM_PRIORITY:5 -->默认优先级
- getPriority():获取线程的优先级
- setPriority(int p):设置线程的优先级
高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下
被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
7、线程的生命周期
1、新建:当一个Thread类或其子类的对象被实例化,新生的线程处理新建状态
2、就绪:当新建的线程被start()后,线程处理就绪,等待着cpu时间片,具备运行条件
3、运行:当就绪的线程分配到cpu资源时,便进入了运行状态,调用run()方法,执行业务逻辑
4、阻塞:当被人为挂起或执行输入输出操作时,让出cpu时间片并临时终止自己的执行,进入阻塞状态
5、死亡:线程完成工作后、线程被强制退出、出现异常,都会导致线程结束,进入死亡状态
8、线程同步
8.1、出现的问题
当多个线程同时操作同一份共享数据时,一个线程执行了一部分,另一个线程参与进来,会造成共享数据的不完整性
8.2、如何解决线程共享带来的问题
给共享数据加锁(Synchronized),一次只能被一个线程访问,当线程操作完共享数据时,释放锁
8.3、线程同步机制
1、必须确保使用同一资源的多个线程共用一把锁,否则无法保证线程安全
2、 如果使用Runnable方式实现的多线程,用this关键字,可充当同步监视器
3、如果使用Thread继承方式实现的多线程,请用当前类.class或者静态对象,充当同步监视器,如果使用this关键字的话,即每个线程实例对象同时拥有自己的共享数据
8.3.1、同步代码块
- 操作共享数据的代码,即为需要被同步的代码
- 同步监视器,俗称锁,任何一个对象可以充当锁,多个线程必须使用同一把锁,否则无效
synchronized (this){
}
8.3.2、同步方法
- Runnable实现方式
private synchronized void show(){//同步监视器:this
}
- Thread继承方式
private static synchronized void show(){//同步监视器:即为当前类.calss
}
8.3.3、Lock(锁)
- 从JDK5.0开始,Java提供Lock线程同步机制,通过显示定义同步锁Lock对象来实现同步机制
- 访问共享资源之前,应先取得Lock对象,在之前手动开启lock锁,访问共享资源之后,手动关闭lock锁
- ReentrantLock类实现的Lock接口,它与synchronized一样,都是对共享资源同步机制的访问
8.3.4、synchronized与Lock的区别
synchronized:
1、隐式锁出了作用域自动释放
2、具有同步代码块锁和方法锁,2种锁方式
Lock:
1、显示锁(需手动开启和关闭锁)
2、只有代码块锁,没有方法锁
3、JVM将花费更少时间调度线程,性能会更好些
9、线程的死锁
9.1、死锁原因
1、不同的线程分别占用对方需要的同步资源不放弃,都在等待对方释放自己需要的同步资源,就形成了死锁
2、出现死锁后,并不会抛出异常和提示信息,所有线程处于阻塞状态,无法继续执行操作
9.2、解决方案
1、尽量减少同步资源的定义
2、尽量避免使用嵌套同步
10、线程的通信
线程与线程之间难免会相互协作去完成一件较复杂的功能,典型的例子生产者-消费者
- wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
- notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程
1、wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中
2、wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会出现IllegalMonitorStateException异常
3、wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中
网友评论