多线程
1、线程与进程
进程
- 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间
线程
- 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行。一个进程最少 有一个线程
进程和线程的关系:
(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。
(3)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
(4)处理机分给线程,即真正在处理机上运行的是线程。
(5)线程是指进程内的一个执行单元,也是进程内的可调度实体。
进程与线程的区别:
(1)调度:进程是系统进行资源分配和调度的基本单位,线程是进程中CPU分配和调度的基本单位。
(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行。
(3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
2、线程调度
分时调度
- 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度 (Java)
- 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核来说,某个时刻, 只能执行一个线程,而CPU在多个线程之间的切换速度相对我们的感觉要快,看上去就像在同一时刻运行。其实,多线程程序并不能提高程序的运行速度,但能让CPU的使用率更高。
3、同步与异步
同步
- 所有的操作都做完,才返回给用户结果。(排队执行 , 效率低但是安全)
异步
- 不用等所有操作都做完,就响应用户请求。(同时执行 , 效率高但是数据不安全)
同步:你用热水壶烧一壶水,你要一直等着,直到水烧开,你再往杯子里倒。
异步:你用热水壶烧一壶水,这段时间里,你不需要等着,你可以去做其他事情,等水烧好,你再去往杯子倒。
4、并发与并行
并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时间点发生(同时发生)。
通过一张图来更好的理解一下两者的区别:
5、多线程创建的方式
(1)继承Thread类
public static void main(String[] args) {
MyThread m = new MyThread();
m.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程" + i);
}
}
public class MyThread extends Thread{
// 重写run()方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {java
System.out.println("子线程" + i);
}
}
}
(2)实现Runnable接口
public static void main(String[] args) {
// 1、创建一个任务对象
MyRunnable r = new MyRunnable();
// 2、创建一个线程,并为其分配一个任务
Thread t = new Thread(r);
// 3、执行这个线程
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程" + i);
}
}
public class MyRunnable implements Runnable{
// 重写run()方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程" + i);
}
}
}
(3)实现Callable接口
// 1.编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T;
}
}
// 2.创建FutureTask对象 , 并传入第一步编写的Callable类对象
FutureTask<Integer> future = new FutureTask<>(callable);
// 3.通过Thread,启动线程
new Thread(future).start();
(4)通过线程池创建
实现Runnable与继承Thread相比有如下优势
- 通过创建任务给线程分配的方式来实现的多线程。更适合多个线程同时执行相同任务的情况。
- 可以避免单继承所带来的局限性。
- 任务与线程本身是分离的,提高了程序的健壮性。
- 线程池技术,接受Runnable类型的任务,不接收Thread 类型的线程。
Runnable和Callable的区别
- Callable(重写)的方法是call(),Runnable(重写)的方法是run()。
- Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
- Call方法可以抛出异常,run方法不可以。
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
6、获取和设置线程名称
public static void main(String[] args) {
// currentThread()返回对当前正在执行的线程对象的引用
System.out.println(Thread.currentThread().getName());
// 方式一(默认)
new Thread(new MyRunnable()).start();
// 方式二
new Thread(new MyRunnable(),"子线程1").start();
// 方式三
Thread t = new Thread(new MyRunnable());
t.setName("子线程2");
t.start();
}
static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
7、线程中断
/**
* 想要在main线程执行完就结束,无论子线程是否执行完毕
*/
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new MyRunnable());
t.setName("子线程");
t.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
Thread.sleep(1000);
}
// 方法:给子线程t添加中断标记,子线程在特殊操作时会检查标记,发现中断标记后会抛出异常,进入try-catch{}内
t.interrupt();
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//e.printStackTrace();
System.out.println("发现中断标记,子线程自杀!");
// return即表示run()方法结束
return;
}
}
}
}
8、守护线程
线程:分为用户线程和守护线程
- 用户线程:当一个进程不包含任何的存活的用户线程时,进程结束。
- 守护线程:守护用户线程的,当最后一个用户线程结束时,所有守护线程自动死亡。
Thread t = new Thread();
// 将子线程t设置为守护线程(注意:在start()方法前进行设置)
t.setDaemon(true);
t.start();
9、线程安全(解决方法)
(1)synchronized(隐式锁)
/**
* 多线程存在线程不安全问题
* (几个线程一块卖票,如不考虑线程安全问题,则会抢着卖票,可能会有负数余票)
* 解决方案1:同步代码块
* 格式 synchronized (锁对象){
* // 注意必须是同一个锁对象
* }
*/
public static void main(String[] args) {
Runnable r = new MyRunnable();
new Thread(r).start();
new Thread(r).start();
}
static class MyRunnable implements Runnable{
// 票数
int count = 10;
// 必须是同一个对象的锁
Object o = new Object();
@Override
public void run() {
/**
* 如果在线程run()方法内部创建对象
* 那么有几个线程就在启动时会创建几个对象
* 每个线程只看自己的对象的锁(则不需要排队),那么将无法解决线程安全问题
*/
// Object o = new Object();
while (true){
// 加入同步代码块
synchronized (o){
if (count > 0){
System.out.print(Thread.currentThread().getName() + "正在准备卖票,");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println("余票:" + count);
}else {
break;
}
}
}
}
}
/**
* 多线程存在线程不安全问题
* 解决方案2:同步方法
* 给方法添加synchronized修饰
*/
public static void main(String[] args) {
Runnable r = new MyRunnable();
new Thread(r).start();
new Thread(r).start();
}
static class MyRunnable implements Runnable{
// 票数
int count = 10;
@Override
public void run() {
while (true){
boolean flag = sale();
if (!flag){
break;
}
}
}
// 给方法添加synchronized修饰
public synchronized boolean sale(){
/**
* 锁对象
* 如果不是static修饰的方法,则锁对象是 this(谁调用方法就是谁)
* 如果是static修饰的方法,则锁对象是 MyRunnable.class
*/
if (count > 0){
System.out.print(Thread.currentThread().getName() + "正在准备卖票,");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println("出票成功,余票:" + count);
return true;
}
return false;
}
}
(2)Lock(显式锁)
/**
* 多线程存在线程不安全问题
* 解决方案3:显式锁 Lock
* Lock 子类 ReentrantLock
* 自己创建锁,执行方法前加锁,方法执行完毕后释放锁
*/
public static void main(String[] args) {
Runnable r = new MyRunnable();
new Thread(r).start();
new Thread(r).start();
}
static class MyRunnable implements Runnable{
// 票数
int count = 10;
// 显式锁 Lock
Lock l = new ReentrantLock();
@Override
public void run() {
while (true){
// 加锁
l.lock();
if (count > 0){
System.out.print(Thread.currentThread().getName() + "正在准备卖票,");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println("出票成功,余票:" + count);
}else {
break;
}
// 释放锁
l.unlock();
}
}
}
10、公平锁与非公平锁
ReentrantLock实现了Lock接口,ReentrantLock的内部类Sync继承了AQS,分为
- 公平锁(FairSync):线程获取锁的顺序和调用lock的顺序一样,FIFO;
- 非公平锁(NonfairSync):线程获取锁的顺序和调用lock的顺序无关,全凭运气抢。
/**
* Java中默认为 非公平锁
* fair参数为 true,就表示为 公平锁
*/
Lock l = new ReentrantLock(true);
11、生产者与消费者
/**
* 生产者与消费者(简单案例)
*/
public static void main(String[] args) {
Food f = new Food();
new Chef(f).start();
new Waiter(f).start();
}
// 生产者(线程) — 厨师 ---------------------------------------------------------
static class Chef extends Thread{
private Food f;
public Chef(Food f) {
this.f = f;
}
@Override
public void run() {
// 生产者生产20份(2种)菜品
for (int i = 0; i < 20; 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 < 20; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
// 数据 - 食物 -----------------------------------------------------------------
static class Food{
private String name;
private String taste;
// 添加一个标记,true表示可以生产
private boolean flag = 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;
// 生产者生成完毕,改为false,停止生产
flag = false;
// 唤醒消费者线程(唤醒在当前this下 等待的所有线程)
this.notifyAll();
// 生产者线程进入等待状态
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 消费方法
public synchronized void get(){
// 消费者开始消费
if (!flag){
System.out.println("服务员端走的菜是:" + name + ",味道是:" + taste);
// 消费者消费完毕,改为true,停止消费
flag = true;
// 唤醒生产者线程
this.notifyAll();
// 消费者线程进入等待状态
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
12、线程的基本状态
-
新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
-
就绪状态(Runnable):当调用线程对象的start()方法时,线程即进入就绪状态,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
-
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。**注:**就绪状态是进入运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
-
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。
- 等待阻塞 – 运行状态中的线程执行wait()方法,使该线程进入到等待阻塞状态;
- 同步阻塞 – 线程在获取synchronized同步锁失败,它会进入同步阻塞状态;
- 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。
-
等待(Waiting):无限期等待另一个线程执行特定操作的线程处于此状态。
-
时间等待(Timed_Waiting):正在等待另一个线程执行指定等待时间的操作的线程处于此状态。
-
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程同步以及线程调度相关的方法
(1)wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
(3)notify():唤醒一个处于等待状态的线程,并且是由 JVM 确定唤醒哪个线程,而且与优先级无关;
(4)notityAll():唤醒所有处于等待状态的线程,并且让它们竞争锁,只有获得锁的线程才能进入就绪状态;
sleep() 和 wait() 有什么区别?
两者都可以暂停线程的执行
- sleep():是 Thread线程类的静态方法,调用 sleep 后,当前线程会让出cpu的使用权,自己进入休眠状态,但并不会释放锁,在休眠时间结束后,恢复之前的运行状态。(sleep 通常被用于暂停执行)
- wait() :是 Object类的方法,调用 wait 后,当前线程会让出cpu的使用权,进入等待阻塞状态,并且释放所持有对象的锁,只有在调用了notify,notifyAll方法后,该线程才会被唤醒,重新去获取对象的锁。(Wait 通常被用于线程间交互/通信)
13、线程池概述
线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务
四种线程池的创建:
(1)newCachedThreadPool 创建一个可缓存线程池
/**
* 缓存线程池 (长度无限制)
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在,则创建线程 并放入线程池, 然后使用
*/
ExecutorService service = Executors.newCachedThreadPool();
//向线程池中加入新的任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
(2)newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。
/**
* 定长线程池 (长度是指定的数值)
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池,然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
*/
ExecutorService service = Executors.newFixedThreadPool(2);
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());
}
});
(3)newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
/**
* 周期任务 定长线程池
* 周期性任务执行时: 定时执行, 当某个时机触发时, 自动执行某任务
*/
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
/**
* 定时执行
* 参数1. runnable类型的任务
* 参数2. 时长数字
* 参数3. 时长数字的单位
*/
service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,TimeUnit.SECONDS);
/**
* 周期执行
* 参数1. runnable类型的任务
* 参数2. 时长数字(延迟执行的时长)
* 参数3. 周期时长(每次执行的间隔时间)
* 参数4. 时长数字的单位
*/
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,2,TimeUnit.SECONDS);
(4)newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。
/**
* 单线程线程池 (效果与定长线程池 创建时传入 数值1 效果一致)
*/
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
线程池的优点:
(1)重复使用线程池里的线程,减少对象创建销毁的开销。
(2)可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
(3)提供定时执行、定期执行、单线程、并发数控制等功能。
14、Lambda 表达式
Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。
Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
使用 Lambda 表达式可以使代码变的更加简洁紧凑。
// 正常格式
public static void main(String[] args) {
print(new MyMath() {
@Override
public int sum(int x,int y) {
return x+y;
}
},100,200);
}
public static void print(MyMath m,int x,int y){
int num = m.sum(x,y);
System.out.println(num);
}
static interface MyMath{
int sum(int x,int y);
}
// Lambda 表达式
public static void main(String[] args) {
print((int x, int y) -> { return x+y; },100,200);
}
public static void print(MyMath m,int x,int y){
int num = m.sum(x,y);
System.out.println(num);
}
static interface MyMath{
int sum(int x,int y);
}