什么是多线程?
是指从软件或者硬件上实现多个线程并发执行的技术。
具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
并行和并发(理解)
- 并行:在同一时刻,有多个指令在多个CPU上同时执行。
- 并发:在同一时刻,有多个指令在单个CPU上交替执行。
进程和线程(理解)
- 进程:是正在运行的程序。
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位。
动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的。
并发性:任何进程都可以同其他进程一起并发执行。 - 线程:是进程中实际执行运算的最小单位(CPU调度和分派的基本单位),可完成一个独立的顺序控制流程,是一条执行路径。
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序。
- 多线程:如果在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为“多线程”。多个线程交替占用CPU资源,而非真正的并行执行。线程每次执行时长由分配的CPU时间片长度决定。
- 多线程的好处:
- 充分利用CPU的资源,提高程序运行效率。
- 简化编程模型。
- 带来良好的用户体验。
- 多线程的好处:
- 主线程:
- main()方法即为主线程入口
- 产生其他子线程的线程
- 必须最后完成执行,因为它执行各种关闭动作
三种多线程实现方式
Java提供了java.lang.Thread类支持多线程编程。
一个程序一个进程。
一个程序包含好多个线程(必须有一个主线程)。
设置和获取线程名称:
方法名 | 说明 |
---|---|
void setName(String name) | 将此线程的名称更改为name |
String getName() | 返回此线程的名称 |
Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
实现多线程方式一:继承Thread类
方法名 | 说明 |
---|---|
void run() | 在线程开启后,此方法将被调用执行 |
void start() | 使此线程开始执行,Java虚拟机会调用run方法() |
实现步骤:
- 定义一个类MyThread继承Thread类。
- 在MyThread类中重写run()方法。
- 创建MyThread类的对象。
- 用创建的MyThread类对象调用start()方法启动线程。
两个小问题:
- 为什么要重写run()方法?
因为run()是用来封装被线程执行的代码。 - run()方法和start()方法的区别?
run():封装线程执行的代码,如果直接调用,就相当于普通方法的调用,而不是以线程的方式调用。
start():启动线程;然后由JVM调用此线程的run()方法。
- 为什么要重写run()方法?
public class MyThread extends Thread{
@Override
public void run(){
for (int i = 0; i < 10; i++) {
//子类Mythread线程调用父类Thread方法,可直接调用,获取线程名
System.out.println(getName()+" "+i);
}
}
}
public class ThreadMethod01 {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
}
}
实现多线程方式二:实现Runnable接口
Thread构造方法
方法名 | 说明 |
---|---|
Thread(Runnable target) | 分配一个新的Thread对象 |
Thread(Runnable target, String name) | 分配一个新的Thread对象 |
- 实现步骤
- 定义一个类MyRunnable实现Runnable接口。
- 在MyRunnable类中重写run()方法。
- 创建MyRunnable类的对象(创建MyRunnable对象,表示多线程要执行的任务)。
- 创建Thread类的对象,把MyRunnable对象作为构造方法的参数。
- 启动线程。
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//这里MyRunnable跟Thread没有继承关系
//需要先获取当前线程程对象,再获取线程名
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
public class ThreadMethod02 {
public static void main(String[] args) {
//创建MyRunnable对象,表示多线程要执行的任务
MyRunnable myRunnable = new MyRunnable();
//创建线程对象,传递对象的时候可以命名
//可以传递同一个任务对象
Thread thread1 = new Thread(myRunnable);//不给线程命名
Thread thread2 = new Thread(myRunnable,"线程二");//给线程命名
thread1.start();
thread2.start();
}
}
实现多线程方式三: 实现Callable接口
方法名 | 说明 |
---|---|
V call() | 计算结果,如果无法计算结果,则抛出一个异常 |
FutureTask(Callable callable) | 创建一个 FutureTask,一旦运行就执行给定的 Callable |
V get() | 如有必要,等待计算完成,然后获取其结果 |
- 实现步骤:
- 定义一个类MyCallable实现Callable接口。
- 在MyCallable类中重写call()方法(有返回值,表示多线程运行的结果)。
- 创建MyCallable类的对象。(表示多线程要执行的任务)
- 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数。(作用:管理多线程运行的结果)
- 创建Thread类的对象,把FutureTask对象作为构造方法的参数。(表示线程)
- 启动线程。
- 再调用get方法,就可以获取线程结束之后的结果。
public class MyCallable implements Callable<String> {
public Integer num;
public MyCallable(int i){
this.num = i;
}
@Override
public String call() throws Exception {
for (int i = 0; i < 10; i++) {
num += i;
System.out.println(Thread.currentThread().getName()+" "+i);
}
return "这时MyCallable的返回值:"+num;
}
}
public class ThreadMethod03 {
public static void main(String[] args) {
//1.*创建MyCallable类的对象。(表示多线程要执行的任务)
MyCallable myCallable = new MyCallable(0);
//2.创建Future的实现类FutureTask对象,
//把MyCallable对象作为构造方法的参数。
//(作用:管理多线程运行的结果)
FutureTask<String> futureTask = new FutureTask<>(myCallable);
//3.创建Thread类的对象,把FutureTask对象作为构造方法的参数。
//(表示线程)
Thread thread = new Thread(futureTask);
thread.start();
//获得线程返回值
try {
//调用get方法,就可以获取线程结束之后的结果。
String msg = futureTask.get();
System.out.println(msg);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
多线程三种实现方式的对比
- 一、实现Runnable、Callable接口:
- 好处: 扩展性强,实现该接口的同时还可以继承其他的类。
- 缺点: 编程相对复杂,不能直接使用Thread类中的方法。
- 二、继承Thread类:
- 好处: 编程比较简单,可以直接使用Thread类中的方法。
- 缺点: 可以扩展性较差,不能再继承其他的类。
- 三、继承Thread类和实现Runnable接口是无法获取多线程运行的结果的,而实现Callable接口是可以获取多线程运行的结果的。
线程常见的成员方法
方法名 | 说明 |
---|---|
String getName() | 返回此线程的名称 |
void setName(String name) | 设置线程的名字(Thread类构造方法也可以设置名字) |
static Thread currentThread() | 获取当前线程的对象 |
setPriority(int newPriority) | 设置线程的优先级(1~10),默认优先级是5 |
final int getPriority() | 获取线程的优先级 |
public static void yield() | 出让线程/礼让线程,但不一定会有其它线程执行 |
public static void join() | 插入线程/插队线程 |
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数,优先级高的线程获得CPU资源的概率较大 |
void interrupt() | 中断线程 |
boolean isAlive() | 测试线程是否处于活动状态 |
wait() | 暂停一个线程 |
notify() | 唤起一个线程 |
注意:join()写在哪个线程里面就阻塞谁,谁调用的join谁就强制执行。
sleep,join使用毫秒1000毫秒=1秒
线程休眠
方法名 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
线程优先级
- 线程调度
- 两种调度方式
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
- Java使用的是抢占式调度模型
- 随机性:
假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的。
- 两种调度方式
- 优先级相关方法
方法名 | 说明 |
---|---|
final int getPriority() | 返回此线程的优先级 |
final void setPriority(int newPriority) | 更改此线程的优先级,线程默认优先级是5。线程优先级的范围是:1-10。优先级高的线程获得CPU资源的概率较大 |
线程的状态
创建状态:new Thread()
就绪状态:调用start()方法
运行状态:执行run方法
阻塞状态:执行sleep(毫秒),join(毫秒)进入阻塞,恢复后改为就绪状态->运行状态
死亡状态:run()运行结束
线程调度
线程调度指按照特定机制为多个线程分配CPU的使用权。
线程安全
多个线程操作同一个共享资源时,将引发数据不安全问题。
线程锁:使用synchronized
关键字,实现代码同步。
方法是否同步 | 效率比较 | 适合场景 | |
---|---|---|---|
线程安全 | 是 | 低 | 多线程并发共享资源 |
非线程安全 | 否 | 高 | 单线程 |
常用对象线程安全问题:
ArrayList线程不安全 | Vector线程安全 |
---|---|
HashMap线程不安全 | HashTable线程安全 |
StringBuilder 线程不安全 | StringBuffer线程安全 |
同步代码块解决数据安全问题
安全问题出现的条件:
- 是多线程环境
- 有共享数据
- 有多条语句操作共享数据
解决多线程安全问题的基本思想就是让程序处在没有安全问题的环境。
实现:把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可,Java提供了同步代码块的方式来解决。
同步代码块格式:
synchronized(任意对象) {
多条语句操作共享数据的代码
}
synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁。
同步的好处和弊端:
- 好处:解决了多线程的数据安全问题。
- 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
同步方法解决数据安全问题
同步方法:就是把synchronized关键字加到方法上。
同步方法的格式:
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
同步方法的锁对象是:this
静态同步方法:
同步静态方法:就是把synchronized关键字加到静态方法上。
修饰符 static synchronized 返回值类型 方法名(方法参数) {
方法体;
}
同步静态方法的锁对象是:类名.class
Lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化。
- ReentrantLock构造方法
方法名 | 说明 |
---|---|
ReentrantLock() | 创建一个ReentrantLock的实例 |
- 加锁解锁方法
方法名 | 说明 |
---|---|
void lock() | 获得锁 |
void unlock() | 释放锁 |
注意:若是用一个类(下面简称 子类)通过继承Thread类来实现多继承,实现多线程通常会创建多个子类线程对象(new 子类()),这时若在子类中有创建了锁对象的代码(Lock lock = new ReentrantLock()
),则需要在前面加一个static
关键字以实现所有子类共享同一个锁,因为不然每次创建子类对象时,每个子类对象都会创建各自的锁对象。
若当一个线程已经拿到了锁对象,并且睡觉sleep()了,此时CPU的执行权会被其它线程抢走,但依旧会被锁挡住,因为此时无法获取到锁对象,锁已经关闭了。
可以用try-catch-finally强制执行finally代码块的特性来释放锁锁对象.unlock()
,避免其它线程无法拿到锁对象,导致程序无法结束。
public class Ticket implements Runnable{
//票的数量
private int ticket = 100;
private Object obj = new Object();
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//synchronized (obj){//多个线程必须使用同一把锁.
try {
//当前线程拿到锁对象
lock.lock();
if (ticket <= 0) {
//卖完了
break;
} else {
Thread.sleep(100);
ticket--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
//利用try-catch-finally强制执行finally代码块的特性
//强制执行释放锁的代码,避免因没有解锁而导致其它线程无法通过锁进来
//从而导致程序不会结束
lock.unlock();
}
// }
}
}
}
public class LockDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
Thread t3 = new Thread(ticket);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
死锁(了解)
- 概述:线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
- 产生死锁的情况
- 资源有限
- 同步嵌套
- 死锁不是一个知识点,而是一个错误,学习死锁是为了以后不犯这种错误。
public class DeadLockDemo {
public static void main(String[] args) {
Object objA = new Object();//定义A锁
Object objB = new Object();//定义B锁
//匿名内部类
new Thread(()->{
while(true){
synchronized (objA){
//假设线程一拿到了A锁,但B锁被线程二抢走
//若被抢走,线程A就会卡住
synchronized (objB){
System.out.println("小康同学正在走路");
}
}
}
}).start();
//匿名内部类
new Thread(()->{
while(true){
synchronized (objB){
//若线程二拿到B锁,但A锁已被线程一抢走,线程二就会卡住
synchronized (objA){
System.out.println("小薇同学正在走路");
}
}
}
}).start();
}
}