文章目录
多线程基础知识
线程与进程
进程:每个进程都拥有独立的内存空间,进程切换有较大的开销,一个进程包含1-n个线程(是资源分配的最小单位)
线程:同一进程的线程共享内存空间,每个线程有独立的运行栈和程序计数器,线程切换开销小(是CPU调度的最小单位)
同步与异步
同步: 排队执行 , 效率低但是安全.
异步: 同时执行 , 效率高但是数据不安全.
并发与并行
并发: 指两个或多个事件在同一个时间段内发生。
并行: 指两个或多个事件在同一时刻发生(同时发生)。
线程的创建
线程的创建一般有两种方法,一是继承java.lang.Thread类,二是实现java.lang.Runnable接口。(其实还有第三种实现Callable接口)。
继承Thread
(1)只使用一条线程的话,可以通过继承java.lang.Thead类来创建线程,语法如下:
创建MyThread继承Thread类:
/**
* 继承Thread
*/
public class MyThread extends Thread{
/**
* run方法是线程要执行的任务方法
* 触发条件时通过Thread对象的start()来启动任务
*/
@Override
public void run() {
//这里的代码是一条新的执行路径
System.out.println("这是一条线程执行的代码");
}
}
在main方法调用start()方法:
//继承Tread
MyThread m = new MyThread();
m.start();
实现Runnable接口
(2)需要多个线程同时执行任务的就需要实现java.lang.Runnable接口。语法如下:
创建MyRunnable继承Runnable方法:
public class MyRunnable implements Runnable{
@Override
public void run() {
//线程的任务
System.out.println("这是一个任务");
}
}
在main方法创建任务对象,再创建Thread并传入任务,然后启动:
//1. 创建一个任务对象
MyRunnable r = new MyRunnable();
//2. 创建一个线程,并为其分配一个任务
Thread t = new Thread(r);
//3. 执行这个线程
t.start();
相比于继承Thread,实现Runnable有如下优点
- 通过创建任务,然后给线程分配任务的方式实现多线程,更适合多个线程同时执行任务的情况
- 可以避免单继承所带来的局限性
- 任务与线程是分离的,提高了程序的健壮性
- 线程池技术中,接受Runnable类型的任务,不接受Thread类型的线程
实现Callable接口
(3)实现Callable接口
使用语法:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* Callable的使用
*/
public class demo2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1. 编写类实现Callable接口,实现call方法
Callable<Integer> call = new MyCallable();
//2. 创建FutureTask对象,传入第一步的Callable对象
FutureTask<Integer> task = new FutureTask<>(call);
//3. 通过Thread类,启动线程
new Thread(task).start();
//调用FutureTask的get()方法会阻塞主线程,先执行Callable再执行主线程
Integer num = task.get();
//task.isDone();//判断子线程是否执行完毕
//task.cancel(true);//取消任务
System.out.println(num);
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 {
for (int i=0;i<10;i++){
Thread.sleep(100);
System.out.println("我是Callable线程"+i);
}
return 100;
}
}
}
Callable和Runnable的相同点与不同点
相同点:
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
不同点:
- Runnable没有返回值;
- Callable可以返回执行结果
- Callable接口的call()允许抛出异常;
- Runnable的run()不能抛出
- Callable获取返回值
- Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
线程的六种状态转换
JDK11API文档说明线程有以下6种状态
- 初始(NEW) :尚未启动的线程处于此状态。
- 运行(RUNNABLE) :在Java虚拟机中执行的线程处于此状态(包括等待CPU时间片(READY)的线程和运行中(RUNNING)的线程)。
- 阻塞(BLOCKED) :被锁的线程处于此状态。
- 等待(WAITING) :无限期等待另一个线程执行特定操作(唤醒或者中断)的线程处于此状态。
- 超时等待(TIMED_WAITING) :有限时间(参数指定)等待另一个线程执行特定操作的线程处于此状态。
- (终止)TERMINATED :执行完毕线程处于此状态。
线程状态转换图:
来源于http://www.uml-diagrams.org/java-thread-uml-state-machine-diagram-example.html
线程常用方法:
方法 | 作用 |
---|---|
long getId() | 返回此Thread的标识符 |
String getName() | 返回此线程的名称 |
int getPriority() | 返回此线程的优先级 |
void interrupt() | 中断此线程(只是中断的信号) |
static boolean interrupted() | 测试当前线程是否已被中断 |
void setDaemon(boolean on) | 参数为true将此线程标记为守护线程 |
static void sleep(long millis) | 令当前正在执行的线程休眠(暂时停止执行)指定的毫秒数 |
static void sleep(long millis, int nanos) | 令当前正在执行的线程休眠(暂时停止执行)指定的毫秒数加上指定的纳秒数 |
static void yield() | 暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。 |
void join() | 等待这个线程执行完毕 |
void join(long millis) | 此线程等待 millis毫秒 |
void join(long millis, int nanos) | 此线程等待 millis毫秒加上 nanos纳秒 |
void start() | 令此线程开始执行; Java虚拟机调用此线程的run方法 |
void setName(String name) | 将此线程的名称更改为等于参数 name |
void setPriority(int newPriority) | 更改此线程的优先级 |
static Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
线程的调度
1.线程的优先级范围是整数1~10,默认优先级是5。
优先级越高不一定先执行,只是抢到时间片的概率高一点。线程优先级具有继承关系,如果A线程中创建了B线程,则B线程拥有和A线程一样的优先级。
Thread类有以下三个字段描述线程优先级。
变量和类型 | 字段 | 描述 |
---|---|---|
static int | MAX_PRIORITY | 线程可以拥有的最大优先级,取值为10 |
static int | MIN_PRIORITY | 线程可以拥有的最低优先级,取值为1 |
static int | NORM_PRIORITY | 分配给线程的默认优先级,取值为5 |
2.线程等待:Object类的wait()方法。需要依赖synchronized关键字。会释放锁。
3.线程唤醒:Object类的notify()和notifyAll()方法。需要依赖synchronized关键字。notify()唤醒此对象下的任意一个线程,notifyAll()唤醒当前对象下的所有线程。
3.线程休眠:Thread类的sleep()方法,让线程休眠指定时间,期间不会释放锁,不依赖synchronized关键字
4.线程让步:Thread类的yield()方法,暂停当前线程,让步给同级别或高级别线程。
5.线程加入:Thread类的join()方法,等待该线程执行完毕。
守护线程
线程分为守护线程和用户线程
用户线程: 当一个进程不包含任何的存活的用户线程时,进行结束
守护线程: 守护用户线程的,当最后一个用户线程结束时,所有守护线程自动死亡。
通过Thread类的setDaemon()方法可以设置当前线程为守护线程。
线程同步
实现线程同步可以通过锁机制来实现,锁又分为隐式锁和显式锁。
隐式锁
隐式锁是靠synchronized关键字完成的,一共有2种使用方式,分别是同步代码块和同步方法。
同步代码块
下面通过一段代码演示同步代码块的使用:
public class Demo8 {
public static void main(String[] args) {
//格式:synchronized(锁对象){
//
//
// }
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
private Object o = new Object();//成员变量才是同一把锁
@Override
public void run() {
//Object o = new Object(); //局部变量则是每人一把锁。这里不是同一把锁,所以锁不住
while (true) {
synchronized (o) {//锁住if判断语句
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;
}
}
}
}
}
}
同步代码块的使用首先需要创建一个锁对象,需要主要创建的位置,再锁住需要同步的代码部分就可以实现同步效果。
同步方法
依然通过一段代码演示同步方法的使用
//线程同步synchronized
public class Demo9 {
public static void main(String[] args) {
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {
while (true) {
boolean flag = sale();
if(!flag){
break;
}
}
}
public synchronized boolean sale(){
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
return true;
}
return false;
}
}
}
使用同步方法只需要再方法的权限修饰符后面加上synchronized关键字就可以完成同步效果,需要注意的是非静态同步方法则调用this作为锁对象。静态同步方法调用类.class作为锁对象。
显式锁
显示锁使用的是Lock类的子类ReentrantLock,lock()方法上锁,unlock方法解锁。依然通过下面代码进行演示。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo10 {
public static void main(String[] args) {
// 显示锁 Lock 子类 ReentrantLock
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
//参数为true表示公平锁 默认是false 不是公平锁
private Lock l = new ReentrantLock(true);
@Override
public void run() {
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();
}
}
}
}
关于公平锁和非公平锁,隐式锁和显式锁都默认使用非公平锁。
公平锁:线程按照它们发出请求的顺序获取锁。
非公平锁:可以“插队”,哪个线程抢到就是哪个线程的。
Java的四种线程池
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程 就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
线程池的好处:
- 降低资源消耗。
- 提高响应速度。
- 提高线程的可管理性。
缓存线程池
特点:无长度限制
执行流程:
A:判断线程池是否存在空闲线程
B:存在则使用
C:不存在则创建线程并使用
创建语法:
//创建缓存线程池
ExecutorService service = Executors.newCachedThreadPool();
//指挥线程池执行新的任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
定长线程池
特点:长度指定
执行流程:
A: 判断线程池是否存在空闲线程
B: 存在则使用
C:不存在空闲线程,且线程池未满的情况下,则创建线程,并放入线程池, 然后使用
D:不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程单线程线程池
创建语法:
ExecutorService service = Executors.newFixedThreadPool(2);//指定线程池长度
//指挥线程池执行新的任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
单线程线程池
特点:长度为1的定长线程池
执行流程:
A:判断线程池的那个线程是否空闲
B :空闲则使用
C :不空闲则等待空闲后使用
创建语法:
ExecutorService service = Executors.newSingleThreadExecutor();//创建单线程线程池
//执行任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
周期性任务定长线程池
有定时执行任务和周期执行任务两种方式
//创建周期性任务定长
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
//定时执行一次
//参数1:定时执行的任务
//参数2:时长数字
//参数3:2的时间单位 Timeunit的常量指定
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
},5, TimeUnit.SECONDS); //5秒钟后执行
/*
周期性执行任务
参数1:任务
参数2:延迟时长数字(第一次在执行上面时间以后)
参数3:周期时长数字(没隔多久执行一次)
参数4:时长数字的单位
* **/
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
},5,1,TimeUnit.SECONDS);
}