前言
多线程技术概述: 线程与进程
进程:
- 是指一个内存中运行的应用程序,每个程序都有一个独立的内存空间
线程:
- 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。
一 、基本概念
1.1 程序 - 进程 - 线程
- 程序(Program) 是为完成 特定的任务、 用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
- 进程(Progress)是程序的一次执行过程,或是正在运行的一个程序。动态过程:有它自身的产生、存在和消亡的过程。
如:
- 运行中的QQ,运行中的MP3播放器;
- 程序是静态的,进程是动态的;
线程(thread):
进程可进一步细化为线程,是一个程序内部的一条执行路径。
若一个程序可同一时间执行多个线程,就是支持多线程的。
Q:何时需要多线程?
A:
程序需要同时执行两个或多个任务;
程序需要实现一些需要等待的任务时,如用户输入 、文件读写操作、网络操作、搜索等;
需要一些后台运行的程序时。
1.2 线程调度
分时调度:
- 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度:
- 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么随机选择一个(线程随机性),Java 使用的为抢占式调度。
- CPU 使用抢占式调度模式在多个线程间进行着高速的切换。对于 CPU 的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。其实,多线程出现并不能提高程序的运行速度,但能够提高程序运行效率,让 CPU 的使用率更高。
1.3 同步与异步
同步:
排队执行,效率低但是安全。
异步:
同时执行,效率高但是数据不安全。
1.4 并发与并行
并发:
指的是两个或多个事件在 同一时间段内 发生。
并行:
指的是两个或多个事件在 同一时刻 发生(同时发生)。
二、多线程的创建和启动
Java 语言的 JVM 允许程序运行多个线程,它通过 java.lang.Thread 类来实现。
Thread 类的特性:
- 每个线程都是通过某个特定 Thread 对象的 run() 方法来完成操作的,经常把 run() 方法的主体称为 线程体。
- 通过该 Thread 对象的 start() 方法来调用这个线程。
2.1 Thread 类
构造方法
- Thread():创建新的 Thread 对象;
- Thread(String threadname): 创建线程并指定线程实例名;
- Thread(Runable target):指定创建线程的目标对象,它实现了 Runable 接口中的 run 方法;
- Thread(Runable target, String name):创建新的 Thread 对象。
2.2 创建线程的两种方式
1、继承 Thread类
- 定义子类继承 Thread 类。
- 子类中重写 Thread类中的 run 方法。
- 创建 Thread 子类对象,即创建了线程对象。
- 调用线程对象 start 方法:启动线程,调用 run 方法。
2、实现 Runnable 接口
- 定义子类,实现 Runnale 接口。
- 子类中重写 Runnable 接口中的 run 方法。
- 通过 Thread 类含参构造器创建线程对象。
- 将 Runnale 接口的子类对象作为实际 参数传递给 Thread类的构造方法中。
- 调用 Thread 类的 start 方法:开启线程,调用 Runnable 子类接口的 run 方法。
3、Runnale 与 Callable
接口定义
//Callable 接口
public interface Callable<V>{
V call() throws Exception;
}
//Runnable 接口
public interface Runnable{
public abstrast void run();
}
4、Callable 使用步骤
- 编写类实现 Callable 接口,实现 call 方法
class xxx implements Callable<T>{
@Override
public <T> call() throws Exception{
return T;
}
}
- 创建 FutureTask 对象,并传入第一步编写的 Callable 类对象
FutureTask<Integer> future = new FutureTask<>(Callable)
- 通过 Thread,启动线程
new Thread(future).start();
5、Runnable 与 Callable 的相同点
- 都是接口
- 都可以编写多线程程序
- 都采用 Thread.start() 启动线程
Runnable 与 Callable 的不同点
- Runnable 没有返回值;Callable 可以返回执行结果
- Callable 接口的 call() 允许抛出异常;Runnable 的 run() 不能抛出
6、Callable 获取返回值
Callable 接口支持返回执行结果,需要调用 FutureTask.get() 得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
例(源码):
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* java中的第三种线程执行方式:Callable
*/
public class D6_3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> c = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(c);
//task.isDone();//判断子线程是否执行完毕
//task.cancel(true);//取消线程执行
new Thread(task).start();
Integer j = task.get();
System.out.println("返回值为:"+j);
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
}
static class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
//Thread.sleep(3000);
for (int i=0;i<10;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
return 100;
}
}
}
运行结果:
【注意】 继承方式和实现方式的联系与区别
public class Thread extends Object implements Runnable
区别:
- 继承 Thread:线程代码存放 Thread 子类 run 方法中,重写 run 方法。
- 实现 Runnable:线程代码存在接口的子类的 run 方法,实现 run 方法。
实现接口方式的好处:
- 避免了单继承的局限性;
- 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
一般使用实现接口方式来实现多线程。
2.3 多线程的优点
多线程程序的优点
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统 CPU的利用率。
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
2.4 线程的优先级
线程的优先级控制:
- MAX_PRIORITY(10):
- MIN_PRIORITY(1):
- NORM_PRIORITY(5):
涉及的方法:
getPriority():返回线程优先值
setPriority(int newPriority):改变线程的优先级
线程创建时继承父线程的优先级
2.5 Thread 类的有关方法(1)
void start():启动线程,并执行对象的 run() 方法
run():线程在被调度时执行的操作
String getName():返回线程的名称
void setName(String name):设置该线程名称
static currentThread():返回当前线程
2.6 Thread 类的有关方法(2)
static void yield(): 线程让步
暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
若队列中没有同优先级的线程,忽略此方法
join(): 当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止。
低优先级的线程也可以获得执行
static void sleep(long mills):(指定时间:毫秒)
令当前活动线程在指定时间段内放弃对 CPU 控制,使其他线程有机会被执行,时间到后重排队。
抛出 InterruptedException 异常
stop(): 强制线程生命期结束
boolean isAlive(): 返回 blooean,判断线程是否还活着。
三、线程的生命周期
3.1 JDK 中用 Thread State 枚举表示了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象,Java 语言使用 Thread 类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建:当一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建状态;
- 就绪:处于新建状态的线程被 start() 后,将进入线程队列等待 CPU 时间片,此时它已具备了运行的条件;
- 运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态,run() 方法定义了线程的操作和功能;
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态;
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止。
3.2 图解
四、线程的同步
4.1 问题的提出
- 多个线程执行的不确定性引起执行结果不稳定;
- 多个线程对账本的共享,会造成操作的不完整性,会破坏数据。
解释如下图:
4.2 多线程出现了安全问题
问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
Synchronized 的使用方法
Java 对于多线程的安全问题提供了专业的解决方式(同步机制):
1、synchronized 还可以放在方法声明中,表示整个方法为同步方法。
例:
public synchronnized void show(String name){
...
}
2、synchroized(对象){
//需要被同步的代码:
}
源码:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class D6 {
/**
* 同步代码块 和 同步方法 都属于隐式锁
* 线程同步:synchronized
* @param args
*/
public static void main(String[] args) {
Object o = new Object();
//线程不安全和安全问题
//解决方案3、显示锁 Lock 子类 ReentrantLock
//格式: synchronized(锁对象){
//
// }
Runnable run = new Ticket();
//三个线程启动
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
/*new Thread(new Ticket()).start();
new Thread(new Ticket()).start();
new Thread(new Ticket()).start();*/
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
//显式锁 l : fair参数为true 就表示是公平锁
private Lock l = new ReentrantLock(true);
//private Object o = new Object();
@Override
public void run() {//A B C
while (true) {
l.lock();
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"出票成功,余票:" + count);
}else{
break;
}
l.unlock();
}
}
}
}
运行结果:
线程的死锁问题
死锁:
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
- 使用专门的算法、原则,比如加锁顺序一致;
- 尽量减少同步资源的定义,尽量避免锁未释放的场景;
例(源码):
public class D6_1 {
public static void main(String[] args) {
//线程死锁
Culprit c = new Culprit();
Police p = new Police();
new MyThread(c,p).start();
c.say(p);
}
static class MyThread extends Thread{
private Culprit c;
private Police p;
public MyThread(Culprit c, Police p){
this.c = c;
this.p = p;
}
@Override
public void run() {
p.say(c);
}
}
//罪犯
static class Culprit{
public synchronized void say(Police p){
System.out.println("罪犯:你放了我,我放人质");
p.fun();
}
public synchronized void fun(){
System.out.println("罪犯被放走了,罪犯也放了人质");
}
}
//警察
static class Police{
public synchronized void say(Culprit c){
System.out.println("警察:你放了人质,我放过你");
c.fun();
}
public synchronized void fun(){
System.out.println("警察救了人质,但是罪犯跑了");
}
}
}
运行结果:
五、线程通信
5.1 wait() 与 notify() 和 notefyAll()
- wait():令当前线程挂起并放弃 CPU、同步资源,使别的线程可访问并修改共享资源,而当前线程排队等待再次对资源的访问;
- notify():唤醒正在排队等待同步资源的线程中优先级最高者;
- notifyAll():唤醒正在排队等待资源的所有线程结束等待,Java.lang.Object 提供的这三个方法只有在 synchronized 方法或 Java.lang.illegalMonitorStateExcception异常
wait() 方法
在当前线程中调用方法:对象名.wait()
使当前线程进入等待(某对象)状态,直到另一线程对该对象发出 notify(或 notifyAll)为止。
调用方法的必要条件:当前线程必须具有对象的监控权(加锁)
在当前线程被 notify 后,要重新获得监控权,然后从断点处继续代码的执行。
notify()/notifyAll() 方法
在当前线程中调用方法:对象名.notify()
功能:唤醒等待该对象监控权的一个线程。
调用方法的必要方法:当前线程必须具有该对象的监控权(加锁)
5.2 经典例题:生产者/消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如 20个产品),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
生产者比消费者快时,消费者会漏掉一些数据没有取到。
消费者比生产者快时,消费者会取相同的数据。
例(源码解释):
public class D6_2 {
/**
* 多线程通信问题,生产者与消费者问题
* @param args
*/
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
//厨师
static class Cook extends Thread{
private Food f;
public Cook(Food f) {
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2==0){
f.setNameAndTaste("老干妈小米粥","香辣味");
}else {
f.setNameAndTaste("煎饼果子","甜辣味");
}
}
}
}
//服务生
static class Waiter extends Thread{
private Food f;
public Waiter(Food f) {
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
//食物
static class Food extends Thread{
private String name;
private String taste;
private boolean flag = true;
//true 表示可以生产
public synchronized void setNameAndTaste(String name,String taste){
if(flag){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get(){
if (!flag){
System.out.println("服务员端走的菜的名称是:"+name+",味道:"+taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
运行结果:
六、线程池 Executors
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间,线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
6.1 线程池的好处
- 降低资源消耗
- 提高响应速度
- 提高线程的课管理性
6.2 Java 中的四种线程池 ExecutorService
1、缓存线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class D6_4 {
/**
* 缓存线程池
* (长度无限制)
* 任务加入后的执行流程:
* 1、判断线程池是否存在空闲线程
* 2、存在则使用
* 3、不存在,则创建线程,并放入线程池,然后使用
* @param args
*/
public static void main(String[] args) {
//创建线程
//创建任务
//执行任务
//关闭线程
ExecutorService service = Executors.newCachedThreadPool();
//指挥线程池向线程池中加入新的任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
}
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
}
});
}
}
2、定长线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class D6_5 {
/**
* 定长线程池
* (长度是指定的数值)
* 任务加入后的执行流程
* 1、判断线程是否存在空闲线程
* 2、存在则使用
* 3、不存在空闲线程,且线程池未满的情况下则创建线程并放入线程池然后使用
* 4、不存在空闲线程,且线程池已满的情况下则等待线程池存在空闲然后使用
* @param args
*/
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
}
});
}
}
3、单线程线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class D6_6 {
/**
* 单线程线程池
* 执行流程:
* 1、判断线程池的那个线程是否空闲
* 2、空闲则使用
* 3、不空闲,则等待,池中的单个线程空闲后 使用
* @param args
*/
public static void main(String[] args) {
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日耽误");
}
});
}
}
4、周期性任务定长线程池
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class D6_7 {
/**
* 周期任务:定长线程池
* 执行流程:
* 1、判断线程池是否存在空闲线程
* 2、存在则使用
* 3、不存在空闲线程,且线程池未满的情况下则创建线程并放入线程池然后使用
* 4、不存在空闲线程,且线程池已满的情况下则等待线程池存在空闲然后使用
*
* 周期性任务执行时:
* 定时执行,当某个时机触发时,自动执行某任务
* @param args
*/
public static void main(String[] args) {
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
/**
*1、定时执行一次
* 参数1、定时执行的任务
* 参数2、时长数字
* 参数3、 时长数字的时间单位,TimeUnit的常量指定
*
*/
/*service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("锄禾日耽误");
}
},5, TimeUnit.SECONDS);*/
/**
* 周期性执行任务
* 参数1、任务
* 参数2、延迟时长数字(第一次执行在什么时间以后)
* 参数3、周期时长数字(每隔多久执行一次)
* 参数4、时长数字的单位
*/
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("旱地和下入");
}
},5,1,TimeUnit.SECONDS);
}
}