Java 多线程(小结)


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);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值