Java多线程基础

多线程基础

  • 1. 前置知识
    • 1.1 程序、进程、线程
  • 2. 创建和使用线程
    • 2.1 方式一:通过继承于java.lang.Thread类的子类实现
    • 2.2 方式二:通过实现java.lang.Runnable接口实现
    • 2.3 两种方式对比
  • 3. 线程的生命周期
  • 4. 线程的同步
    • 4.1 线程安全
    • 4.2 线程同步
      • 4.2.1 隐式锁:
      • 4.2.2 显式锁:
      • 4.2.3 两种方式对比:
    • 4.3 案例:
    • 4.4 单例模式-懒汉模式
    • 4.5 死锁问题
  • 5.线程通信问题
  • 6.另外两种创建线程的方式(JDK5.0新增)
    • 6.1 实现java.util.concurrent.Callable接口
    • 6.2 线程池
  • 面试题:

1. 前置知识

1.1 程序、进程、线程

  • 程序=数据结构+算法;数据结构:如何存储数据;算法:如何高效的处理数据;
  • 进程之间通信的方式:1)内存共享;2)管道(如Linux系统中的grep命令);3)网络机制;4)文件;5)信号;6)信号量;
  • 线程之间的通信更简单高效:内存共享(堆、方法区);
  • 线程独立拥有自己的虚拟机栈和程序计数器;

Java多线程基础_第1张图片
Java多线程基础_第2张图片
Java多线程基础_第3张图片
Java多线程基础_第4张图片
Java多线程基础_第5张图片

Java多线程基础_第6张图片

2. 创建和使用线程

  • 多线程创建方式:1)创建继承于java.lang.Thread类的子类,重写其run方法;2)实现java.lang.Runnable接口,实现其run方法;3)实现Callable接口;4)线程池;

Java多线程基础_第7张图片
Thread类相关方法:

  • sleep方法、join方法等会导致某线程处于阻塞状态,当该线程不再处于阻塞状态时,不会立即执行而是等待CPU分配资源进行调用才会执行;

Java多线程基础_第8张图片
Java多线程基础_第9张图片

线程调度:
Java多线程基础_第10张图片
线程优先级:

  • 线程优先级分为10个级别,可进行自定义设置;
  • 高优先级只是表示在概率上来看,该线程会被优先执行,而不是一定就会先执行高优先级线程;

Java多线程基础_第11张图片
Java多线程基础_第12张图片

2.1 方式一:通过继承于java.lang.Thread类的子类实现

  • 创建自定义类,实现Thread类;
  • 重写其run方法,run方法的方法体用于完成该线程所对应的具体任务;
  • 在主线程中实例化该自定义类对象,通过start方法启动该线程;

start方法的作用:1)启动当前线程;2)调用当前线程的run方法;
此实现方式有一个更简单的形式:使用Thread类的匿名子类实现

/**
 * Author:NorthSmile
 * Create:2023/3/28 11:45
 * 使用两个不同的子线程分别打印偶数、奇数
 */
public class EvenOdd {
    public static void main(String[] args) {
        System.out.println("主线程:"+Thread.currentThread().getName());
//        EvenThread thread1 = new EvenThread();
//        OddThread thread2 = new OddThread();
//        thread1.start();
//        thread2.start();

        // 匿名子类的方式
        new Thread(){
            @Override
            public void run() {
                // 打印偶数
                for (int i = 0; i < 50; i++) {
                    if (i%2==0){
                        System.out.println(Thread.currentThread().getName()+":"+i);
                    }
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                // 打印偶数
                for (int i = 0; i < 50; i++) {
                    if (i%2!=0){
                        System.out.println(Thread.currentThread().getName()+":"+i);
                    }
                }
            }
        }.start();
    }
}


class EvenThread extends Thread{
    @Override
    public void run() {
        // 打印偶数
        for (int i = 0; i < 50; i++) {
            if (i%2==0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}

class OddThread extends Thread{
    @Override
    public void run() {
        // 打印奇数
        for (int i = 0; i < 50; i++) {
            if (i%2!=0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}

2.2 方式二:通过实现java.lang.Runnable接口实现

  • 创建自定义类,实现java.lang.Runnable接口;
  • 实现其run方法,run方法的方法体用于完成该线程所对应的具体任务;
  • 在主线程中实例化该自定义类对象;
  • 将自定义类对象作为new Thread()的构造器参数,实例化Thread对象;
  • 通过Thread对象启动该线程,调用其run方法:实质是调用Thread类的Runnable型参数target的run方法;
package com.northsmile.thread;


/**
 * Author:NorthSmile
 * Create:2023/3/28 14:44
 * 多线程创建:
 *  * 方式二: 通过实现java.lang.Runnable接口实现
 *  - 创建自定义类,实现java.lang.Runnable接口;
 *  - 实现其run方法,run方法的方法体用于完成该线程所对应的具体任务;
 *  - 在主线程中实例化该自定义类对象;
 *  - 将自定义类对象作为new Thread()的构造器参数,实例化Thread对象;
 *  - 通过Thread对象启动该线程,调用其run方法:实质是调用Thread类的Runnable型参数target的run方法;
 */

// 1.创建自定义类,实现java.lang.Runnable接口;
class EThread implements Runnable{
    // 2.实现其run方法,run方法的方法体用于完成该线程所对应的具体任务;
    @Override
    public void run() {
        // 打印偶数
        for (int i = 0; i < 50; i++) {
            if (i%2==0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}
public class RunnableDemo {
    public static void main(String[] args) {
        // 3.在主线程中实例化该自定义类对象;
        EThread eThread = new EThread();
        // 4.将自定义类对象作为new Thread()的构造器参数,实例化Thread对象;
        Thread t1 = new Thread(eThread);
        // 5.通过Thread对象启动该线程,调用其run方法:实质是调用Thread类的Runnable型参数target的run方法;
        t1.start();
        Thread t2 = new Thread(eThread);
        // 5.通过Thread对象启动该线程,调用其run方法:实质是调用Thread类的Runnable型参数target的run方法;
        t2.start();
    }
}

2.3 两种方式对比

通过实现Runnable接口的方式比通过继承Thread类的方式更常用,原因在于:

  • 方式一通过继承Thread类实现,而继承具有单继承限制,接口可以多实现;
  • 方式二更适合多线程需处理有共享数据的情况;

二者联系:

Thread类本身实现了Runnable接口;

相同点:

均要在实现类中重写其run()方法,并将具体任务的逻辑生命在其方法体中;

3. 线程的生命周期

五种状态:新建、就绪、运行、阻塞、死亡
Java多线程基础_第13张图片
线程状态之间的转换:
Java多线程基础_第14张图片

4. 线程的同步

4.1 线程安全

出现线程安全的原因:

  • 数据共享任务可能存在线程安全问题;
  • 当多个线程操作同一对象(共享数据),且至少有一个线程对共享数据进行修改时,则可能会出现线程安全问题;

解决思想:

  • 设法使得当前线程在操作共享数据时,其他线程不能参与,直到当前线程操作结束后其他线程才可以进行操作;

Java多线程基础_第15张图片

4.2 线程同步

Java中使用同步机制(隐式锁:同步代码块或同步方法、显式锁:Lock)解决线程安全问题
使用同步机制处理时具有一定的局限性:操作同步代码时只能有一个线程参与,其他线程等待,相当于一个单线程处理过程,效率较低。

4.2.1 隐式锁:

方式一:同步代码块:

synchronized(同步监视器){
	// 需要进行同步的代码(即操作共享数据的代码)
}

其中同步监视器称为锁,Java中任意不为null的引用均可作为锁,前提要求是:多个线程必须共用同一把锁

  • 在实现Runnable方式中,同步监视器可以为this(表示当前对象);
  • 在继承Thread方式中,可以使用当前类(xxx.class)来充当同步监视器;

Java多线程基础_第16张图片

方式二:同步方法:

权限修饰符 synchronized 返回值类型 (参数列表){
	// 需要进行同步的代码(即操作共享数据的代码)
}

当操作共享数据的代码组成完整的方法时,可使用同步方法解决线程安全问题:

  • 在实现Runnable方式中,同步监视器默认为this(表示当前对象);
  • 在继承Thread方式中,同步监视器默认为当前类xxx.class,且该同步方法必须为静态方法;

Java多线程基础_第17张图片

4.2.2 显式锁:

Java多线程基础_第18张图片

class Window implements Runnable{
    private int tickets=100;
    // 多个线程共用同一把锁
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            try {
                // 手动加锁
                lock.lock();
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + "售卖车票,票号为:" + tickets);
                    tickets--;
                } else {
                    break;
                }
            }finally {
                // 手动释放锁
                lock.unlock();
            }
        }
    }
}

4.2.3 两种方式对比:

Java多线程基础_第19张图片

4.3 案例:

使用继承的方式实现多线程问题:

package com.northsmile.thread;


/**
 * Author:NorthSmile
 * Create:2023/3/29 8:45
 * 使用多个窗口卖票:
 *  1.涉及多线程;
 *  2.涉及共享数据;
 *  3.线程对共享数据会进行修改;
 *
 * 可能存在的问题:线程安全,此处表现为重票、错票问题
 * 解决方案:同步机制(同步代码块、同步方法)
 * 此方式中通过static修饰共享数据,保证数据的共享性
 */
public class TicketOfThread {
    public static void main(String[] args) {
        WindowOfThread window1 = new WindowOfThread("1号窗口");
        WindowOfThread window2 = new WindowOfThread("2号窗口");
        WindowOfThread window3 = new WindowOfThread("3号窗口");

        window1.start();
        window2.start();
        window3.start();
    }
}


class WindowOfThread extends Thread{
    private static int tickets=100;

    public WindowOfThread(String name){
        super(name);
    }

    @Override
    public void run() {
        while (true){
            // 方式一:同步代码块
//            synchronized(WindowOfThread.class) {
//                if (tickets > 0) {
//                    System.out.println(getName() + "售卖车票,票号为:" + tickets);
//                    tickets--;
//                } else {
//                    break;
//                }
//            }

            // 方式二:同步方法
            saleTickets();
        }
    }

    // 方式二:同步方法
    private static synchronized void saleTickets() {
        if (tickets > 0) {
            System.out.println(Thread.currentThread().getName() + "售卖车票,票号为:" + tickets);
            tickets--;
        }
    }
}

使用实现接口的方式实现多线程问题:

package com.northsmile.thread;

/**
 * Author:NorthSmile
 * Create:2023/3/29 8:45
 * 使用多个窗口卖票:
 *  1.涉及多线程;
 *  2.涉及共享数据;
 *  3.线程对共享数据会进行修改;
 *
 * 可能存在的问题:线程安全,此处表现为重票、错票问题
 * 解决方案:同步机制(同步代码块、同步方法)
 * 此方式中Runnable接口的实现类始终只有一个,其成员变量自然只有一个,也就是多个线程共享;
 */
public class TicketOfRunnable {
    public static void main(String[] args) {
        WindowOfRunnable base=new WindowOfRunnable();
        Thread window1 = new Thread(base,"1号窗口");
        Thread window2 = new Thread(base,"2号窗口");
        Thread window3 = new Thread(base,"3号窗口");

        window1.start();
        window2.start();
        window3.start();
    }
}

class WindowOfRunnable implements Runnable{
    private int tickets=100;

    @Override
    public void run() {
        while (true){
            // 方式一:同步代码块
            synchronized(WindowOfThread.class) {
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + "售卖车票,票号为:" + tickets);
                    tickets--;
                } else {
                    break;
                }
            }

            // 方式二:同步方法
//            saleTickets();
        }
    }

    // 方式二:同步方法
    private synchronized void saleTickets() {
        if (tickets > 0) {
            System.out.println(Thread.currentThread().getName() + "售卖车票,票号为:" + tickets);
            tickets--;
        }
    }
}

4.4 单例模式-懒汉模式

单例模式-懒汉模式:

  • 单例模式:整个应用中有且仅有一个该实例对象;
  • 单例模式下,该实例对象为静态属性,为共享数据,存在多线程安全问题;
  • 懒汉模式:仅在需要该实例对象时才去申请获取;

单线程下,单例模式-懒汉模式示例代码:

package com.northsmile.thread;

/**
 * Author:NorthSmile
 * Create:2023/3/29 9:42
 * 单例模式-懒汉模式
 */
 class Phone {
    // 1.提供私有无参构造方法
    private Phone(){

    }

    // 2.在类内部实例化一个自身的唯一对象,要求该属性为静态属性
    private static Phone instance;

    // 3.提供静态方法getInstance用于提供外部访问接口
    public static Phone getInstance(){
        // 第一次需要该实例对象,对其进行创建
        if (instance==null){
            instance=new Phone();
        }
        return instance;
    }
}

多线程下,单例模式-懒汉模式示例代码:

class Phone {
    // 1.提供私有无参构造方法
    private Phone(){

    }

    // 2.在类内部实例化一个自身的唯一对象,要求该属性为静态属性
    private static Phone instance;

    // 3.提供静态方法getInstance用于提供外部访问接口
    public static Phone getInstance(){
        // 第一次需要该实例对象,对其进行创建
        // 方式一:效率较低,所有线程都要来抢夺“锁”
//        synchronized (Phone.class){
//            if (instance==null){
//                instance=new Phone();
//            }
//        }
        // 方式二:效率较高,只有开始的部分线程要来抢夺“锁”
        if (instance==null) {
            synchronized (Phone.class) {
                if (instance == null) {
                    instance = new Phone();
                }
            }
        }
        return instance;
    }
}

4.5 死锁问题

原则:尽可能避免死锁问题
Java多线程基础_第20张图片

5.线程通信问题

涉及到三个方法:

  • wait():一旦执行该方法,当前线程进入阻塞状态,等待唤醒,并释放锁(同步监视器);
  • notify():一旦执行该方法,则会唤醒等待线程中的某一个(根据线程优先级确定);
  • notifyAll():一旦执行该方法,则会唤醒所有等待线程;

这三个方法只能使用在同步代码块或同步方法中;
这三个方法只能通过同步代码块或同步方法中的同步监视器进行调用;
这三个方法均继承于Object类;

代码示例:

class WindowOfRunnable implements Runnable{
    private int tickets=100;

    @Override
    public void run() {
        while (true){
            synchronized(this) {
                notify();  // 唤醒一个等待的线程,令其从阻塞变为就绪状态
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + "售卖车票,票号为:" + tickets);
                    tickets--;

                    // 令当前线程进入阻塞状态,等待唤醒
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                } else {
                    break;
                }
            }
        }
    }
}

生产者-消费者问题:
Java多线程基础_第21张图片

package com.northsmile.thread.ex;

/**
 * Author:NorthSmile
 * Create:2023/3/29 14:39
 * 生产者-消费者问题
 * 多线程问题:生产线程、消费线程
 * 共享数据:店员或其把握的产品数量
 * 线程对共享数据会进行修改;
 *
 * 存在线程安全问题,解决方案:三种同步机制
 * 涉及线程通信问题,使用wait/notify/notifyAll
 *
 */
public class ProducerAndCustomer {
    public static void main(String[] args) {
        Clerk clerk=new Clerk();
        Producer producer = new Producer(clerk);
        Custom customer = new Custom(clerk);

        Thread t1 = new Thread(producer,"生产者");
        Thread t2 = new Thread(customer,"消费者");
        t1.start();
        t2.start();
    }
}

// 店员类
class Clerk{
    private long products;
    public Clerk(){

    }

    public long getProducts() {
        return products;
    }

    public void setProducts(long products) {
        this.products = products;

    }

    public synchronized void consume() {
        if (products>0) {
            products--;
            System.out.println(Thread.currentThread().getName()+"开始消费,当前产品数量为:"+products);
            notify();
        }else{  // 没有产品,进行等待,继续生产
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public synchronized void produce() {
        if (products<20) {
            products++;
            System.out.println(Thread.currentThread().getName()+"开始生产,当前产品数量为:"+products);
            notify();
        }else{  // 没有空位,进行等待,继续消费
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

class Producer implements Runnable{
    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk=clerk;
    }

    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            clerk.produce();
        }
    }
}

class Custom implements Runnable{
    private Clerk clerk;
    public Custom(Clerk clerk){
        this.clerk=clerk;
    }

    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            clerk.consume();
        }
    }
}

6.另外两种创建线程的方式(JDK5.0新增)

6.1 实现java.util.concurrent.Callable接口

  • 自定义类,实现Callable接口,重写其call方法;
  • 创建实现类的对象;
  • 将此对象作为参数,实例化java.util.concurrent.FutureTask对象;
  • 将FutureTask对象作为参数实例化Thread类对象,并启动线程;
  • 有需求可以通过FutureTask对象获取call方法的返回值;

Java多线程基础_第22张图片
Java多线程基础_第23张图片

package com.northsmile.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * Author:NorthSmile
 * Create:2023/3/29 16:33
 * 实现Callable接口,创建线程
 *
 *  - 自定义类,实现Callable接口,重写其call方法;
 *  - 创建实现类的对象;
 *  - 将此对象作为参数,实例化FutureTask对象;
 *  - 将FutureTask对象作为参数实例化Thread类对象,并启动线程;
 *  - 有需求可以通过FutureTask对象获取call方法的返回值;
 */
public class CallableDemo {
    public static void main(String[] args) {
        EvenT evenT = new EvenT();
        FutureTask<Integer> task = new FutureTask<Integer>(evenT);
        Thread thread = new Thread(task, "子线程");
        thread.start();

        Integer sum = null;
        try {
            sum = task.get();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
        System.out.println(sum);

    }
}

class EvenT implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        int sum=0;
        for (int i = 0; i <=50; i++) {
            if (i%2==0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
            sum+=i;
        }
        return sum;
    }
}

6.2 线程池

Java多线程基础_第24张图片
Java多线程基础_第25张图片
Java多线程基础_第26张图片

Java多线程基础_第27张图片

Java多线程基础_第28张图片

package com.northsmile.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * Author:NorthSmile
 * Create:2023/3/29 18:22
 * 使用线程池创建线程
 */
public class PoolDemo {
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);// 此处nThreads参数实质指定了该线程池的corePoolSize和maximumPoolSize;
        ThreadPoolExecutor executor=(ThreadPoolExecutor)executorService;

        // 线程管理
        executor.setCorePoolSize(2);
        executor.setMaximumPoolSize(3);
        // 将任务交个线程池执行
        WindowPool runnable = new WindowPool();
        executor.execute(runnable);
        executor.execute(runnable);
        executor.execute(runnable);
        executor.execute(runnable);

        // 关闭线程池
        executorService.shutdown();
    }
}

class WindowPool implements Runnable {
    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
//                try {
//                    Thread.sleep(100);
//                } catch (InterruptedException e) {
//                    throw new RuntimeException(e);
//                }
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + "售卖车票,票号为:" + tickets);
                    tickets--;
                } else {
                    break;
                }
            }

        }
    }
}

面试题:

1.创建多线程的方式有哪几种?
四种:1)继承Thread;2)实现Runnable接口;3)实现Callable接口;4)线程池;
2.如何解决线程安全问题,有哪几种方式?
同步机制,有三种方式:1)同步代码块;2)同步方法;3)显式锁Lock;
3.隐式锁和显式锁的异同?
自动、手动。
4.sleep方法和wait方法的异同?
答:
相同点:均可使当前线程从运行态进入阻塞状态;
不同点:1)sleep在经过设定的休眠时长之后自动转为就绪状态,而wait需要等待notify或者notifyAll唤醒;2)sleep方法是java.lang.Thread类的方法,而wait是声明在java.lang.Object类下;3)wait方法只能在同步代码块或者同步方法中使用;4)sleep不会释放同步监视器,而wait会释放。

资料来源:尚硅谷

你可能感兴趣的:(Java开发,java,jvm,开发语言)