Java 多线程及相关知识介绍

写在前边:

这篇博客比较系统的介绍了 Java 多线程以及相关知识,刚入 Java 的小白或者 基础不牢 的可以来看看。内容虽然很广泛,但是深度还不够,还请各位多多指点。由于篇幅问题有些知识点没有写上,在看这篇博客的有不明白的当时一定要记下来,及时去搜索一下。

【友情提示】

概念性的东西很多,文章也不太短,要耐心看鸭!


一、线程相关概念

1.并发与并行
并发:同一时间段内发生( 两个任务 交替执行
并行:同一时刻发生( 两个任务 同时执行
 
2.线程与进程
进程:指一个 内存中运行的应用程序(进入到内存的程序叫做进程)
线程:线程是 进程中的一个执行单元 ,负责当前进程中的程序执行,一个进程至少有一个线程。
           如果一个进程中有多个线程,这个应用程序可称之为多线程程序
 

3.线程的调度
分 时 调 度平均分配 每个线程占用 CPU 的时间
抢占式调度:优先级高的线程 优先使用 CPU
                     如果优先级相同,会随机选择一个(线程随机性)
                     Java为抢占式调度(线程执行的先后顺序是随机的)
 

4.主线程 (指Java中的主线程)
概念:执行 main 方法的线程
程序的执行

  • 1.JVM执行 main 方法,main 方法进入栈内存
  • 2.JVM会请求 CPU 分配资源,用于执行程序
  • 3.程序文件从硬盘读取到内存中,然后交给CPU执行程序

Java中默认只有一个线程,执行从main方法开始。可手动创建多个线程

二、线程的实现方式

创建新线程的三种方式:继承Thread类实现Runable接口通过 Callable 和 Future 接口创建

1.继承 Thread 类

Thread类:来自于java.lang.Thread,使用时不需要导包

实现步骤

1.新建一个 Thread 类的子类 public class Xxx extends Thread { }
2.在 Thread 类的子类中重写 run 方法,设置线程任务
3.创建 Thread 类的子类对象
4.调用 Thread 类的 start 方法 ,开启新的线程,自动执行 run 方法

代码实例

1.创建一个 Thread 类的子类

public class NewThread extends Thread{
	//重写 run 方法,设置线程(开启线程要做什么)
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("run"+i);
        }
    }
}

2.创建 Thread 类的子类,并开启线程

【PS】是调用 start 方法开启线程,而不是调用 run 方法。调用start方法,会自动执行run方法

public class Demo {
    public static void main(String[] args) {
        NewThread nt = new NewThread();
        nt.start();
        //nt.run();//调用run方法不会开启线程
        for (int i = 0; i < 5; i++) {
            System.out.println("main"+i);
        }
    }
}

程序运行结果:
在这里插入图片描述
【注意】

  • 调用 Thread 子类对象的 start 方法开启新线程调用 run 只会在当前线程继续执行 run 方法,不会开启新线程
  • Java程序属于抢占式调度,哪个线程优先级高优先执行那个线程的程序,优先级相同则随机选择一个
  • 由 start() 创建了新线程,Java 虚拟机调用该线程的 run 方法。
  • 多次启动一个线程是非法的。线程已经结束执行后,不能再重新启动。
    会抛出 java.lang.IllegalThreadStateException 异常

Thread 类的常用方法

静态方法

方法信息方法介绍
public static void yield()暂停当前正在执行的线程对象,并执行其他线程。
public static void sleep(long millisec)在指定的毫秒数内让当前正在执行的线程休眠。
public static boolean holdsLock(Object x)当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
public static Thread currentThread()返回对当前正在执行的线程对象的引用。
public static void dumpStack()将当前线程的堆栈跟踪打印至标准错误流。

成员方法

方法信息方法介绍
public void start()使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
public void run()如果该线程是使用独立的 Runnable 运行对象构造的,
则调用该 Runnable 对象的 run 方法;
否则,该方法不执行任何操作并返回。
public final void setName(String name)改变线程名称,使之与参数 name 相同。
public final void setPriority(int priority)更改线程的优先级。
public final void setDaemon(boolean on)将该线程标记为守护线程或用户线程。
public final void join(long millisec)等待该线程终止的时间最长为 millis 毫秒。
public void interrupt()中断线程。
public final boolean isAlive()测试线程是否处于活动状态。

2.实现 Runnable 接口

Runnable接口:来自于java.lang.Runnable,使用时不需要导包

实现步骤

1.新建 Runnable 的实现类
2.重写 Runnable 的 run 方法
3.通过 Thread 的构造方法,将 Runnable 的实现类对象传递给 Thread 类
4.通过 Thread 对象调用start方法开启线程
   因为 Runnable 接口中没有 start 方法

代码实例

1.新建Runable的实现类

public class NewThread implements Runnable{
    @Override
    public void run() {
        System.out.println("使用Runnable接口创建线程");
    }
}

2.开启新线程

public class Demo {
    public static void main(String[] args) {
    	//创建 Runable 的实现类对象
    	NewThread nt = new NewThread();
    	//创建 Thread 对象,并通过构造方法将实现类对象传递给Thread类
    	Thread t = new Thread(nt);
    	//调用 start 方法开启新线程
    	t.start();
//=======下方一行代码和上方三行是等价的=========
        new Thread(new NewThread()).start();//使用 匿名对象 开启线程避免繁琐

        System.out.println("main主线程");
    }
}

程序运行结果:

两线程优先级相同,两条语句的打印顺序依旧是随机执行,不再截图演示

3.使用 Callable 和 Future 接口

通过此方式创建线程,可以获取新线程任务的返回值

实现步骤

1.创建 Callable<V> 接口的实现类
2.在实现类中重写 Callable 接口中的 call 方法(注意这里不是 run 方法)
3.创建 FutureTask<V> 类的对象
4.创建 Thread 类对象,将 FutureTask 对象传递到 Thread 的构造方法
5.使用 Thread 对象调用 start 方法,开启新线程

代码实例

1.新建Callable的实现类,并重写call方法

public class CallableThread implements Callable<Integer>{
	//重写call方法,编写线程任务
	//call方法的返回值类型由Callable接口的泛型决定
    @Override
    public Integer call() throws Exception {
        int i = 0;
        for (; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+i);
        }
        return i;
    }
}

2.开启新线程

public class Demo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    	//创建Callable实现类对象
        CallableThread ct = new CallableThread();
        //创建FutureTask对象,将Callable实现类对象传递到其构造方法中
        FutureTask<Integer> ft = new FutureTask<>(ct);
        //将FutureTask对象传递给Thread对象,并调用start方法开启新线程
        new Thread(ft).start();
        
        //main方法的线程任务
        int i = 0;
        for (; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+i);
        }
        
        //获取call方法的返回值
        Integer num = ft.get();
        System.out.println("call方法的返回值为:" + num);
    }
}

程序运行结果
在这里插入图片描述

【tips】

  • 传递给Callable<V>接口的泛型是作为call方法返回值类型的
  • 获取新线程任务的返回值,可以使用FutureTaskget方法
  • get方法存在两个异常,分别是ExecutionException, InterruptedException
    需要进行处理(throws或者try/catch)

4.创建线程三种方式的对比

继承Thread类创建新线程

优势

  • 实现过程简便,直接创建Thread对象调用start方法
  • 可直接使用this获取当前线程的信息

劣势

  • 程序扩展性低,继承Thread类就不能再继承其它类

实现Runnable、Callable接口的方式创建新线程

优势

  • 增强了程序的扩展性,可以继承其它类
  • 把设置线程任务和开启线程任务进行了分离(解耦)
  • 更适合多个线程共享一份资源的情况(便于理解和代码编写),
    可很好的将CPU代码及数据分离开

劣势

  • 编写过程稍微繁琐,需要创建实现类对象,并将对象传递给Thread
  • 只能使用Thread.currentThread()访问当前线程信息

Thread和Runable的区别

写法上的区别,本质并没有什么不同
Thread是Runnable接口的实现和扩展
不论使用Thread还是Runnable开启新线程,都需要 new 出一个 Thread 对象来执行 start 方法
Thread同样可以简单的实现资源共享

使用Thread类新建线程任务:

  • 可以通过子类本身调用start方法(每开启一个线程,都是操作新的子类对象)
  • 也可新建Thread对象开启新线程(将子类对象作为参数传递给Thread类)
  • 已经继承了Thread类不能继承其它类了

使用Runnable接口新建线程任务:

  • 必须通过Thread对象开启新线程(将实现类对象作为参数传递给Thread类)

使用建议:
如果有复杂的线程操作需求,那就选择继承Thread
如果只是简单的执行一个任务,那就实现Runnable

给大家配上代码,方便理解

public class Demo {
    public static void main(String[] args) {
        //继承Thread类创建线程
        NewThread1 n1 = new NewThread1();
        //子类对象直接开启线程
        //操作了两个子类对象
        n1.start();
        n1.start();
        //通过新的Thread对象开启线程
        //操作了一个子类对象
        new Thread(n1).start();
        new Thread(n1).start();
        //实现Runnable接口创建线程
        //操作了一个实现类对象
        NewThread2 n2 = new NewThread2();
        new Thread(n2).start();
        new Thread(n2).start();
    }
}

class NewThread1 extends Thread {
    @Override
    public void run() {
        synchronized (this) {
            System.out.println("使用Thread类创建线程");
        }
    }
}

class NewThread2 implements Runnable {
    @Override
    public void run() {
        synchronized (this) {
            System.out.println("继承Runnable接口创建线程");
        }
    }
}

Runnable和Callable的区别

  • Callable规定重写的方法是 call()Runnable规定 重写 的方法是 run()
  • Callable的任务执行后 有返回值,而 Runnable 的任务是 不能有返回值 的。
  • call 方法 可以抛出异常 ,run 方法不可以
  • 运行 Callable 任务可以拿到一个 Future 对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过 Future 对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

5.线程优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。
但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

三、线程安全问题

什么是线程安全问题

在多线程环境下,不同线程对同一份数据操作,就可能会产生不同线程中数据状态不一致的情况,这就是线程安全问题的定义或者说原因。

保证线程安全的方式

使用 同步代码块同步方法锁机制

1.使用 同步代码块 保证线程安全

格式

synchronized (对象锁[对象检测器]) { 
	可能出现线程安全的代码; 
}

代码实例

public class Ticket01 implements Runnable{

    //设置线程任务:卖票
    private Integer ticket = 10;
    Object obj = new Object();
    
    @Override
    public void run() {
        //判断票是否存在
        while(true){
            //使用synchronized关键字保证线程安全
            synchronized (obj){//obj可以换成this,也可以换成 本类类名.class(也可是继承的父类或者实现的接口)
                if(ticket > 0){
                    System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票。");
                    ticket--;
                }else{
                    System.out.println(Thread.currentThread().getName()+"票已售空");
                    break;
                }
            }
        }
    }
}        

注意

1.锁对象可以使用任意对象(例如:this、super、本类类名.class、实现的接口 等)
2.必须保证多个线程使用的锁对象是同一个
3.锁对象可以把同步代码块锁住,只让一个线程在同步代码块中执行

2.使用 同步方法 保证线程安全

格式

在方法声明时加 synchronized 关键字

代码实例

public class Ticket02 {

    //设置线程任务:卖票
    private Integer ticket = 10;
    
    //使用 synchronized 关键字修饰方法,保证线程安全
    //同步方法的锁对象是 this
    public synchronized void playTicket(){
        if(ticket > 0){
            System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票。");
            ticket--;
        }else{
            System.out.println(Thread.currentThread().getName()+"票已售空");
        }
    }
    
    //静态同步方法
    //静态方法的锁对象是 本类 的 class 属性
    static int i = 100;
    public static synchronized void playTicketStatic(){
        if(i > 0){
            System.out.println(Thread.currentThread().getName()+"正在卖第"+i+"张票。");
            i--;
        }else{
            System.out.println(Thread.currentThread().getName()+"票已售空");
        }
    }
}     

注意

1.同步方法的锁对象是 this
2.静态方法的锁对象是 本类 的 class 属性

3.使用 锁机制 保证线程安全

相关知识

需要使用的到 Lock 接口(来自java.util.concurrent.locks包)
获取锁的方法 void lock() 
释放锁的方法 void unlock() 
Lock的实现类 ReentrantLock 

使用步骤:

1.·在成员位置创建一个ReentrantLock对象·
2.在可能出现线程安全问题的代码之前调用lock()方法获取锁
3.在可能出现线程安全问题的代码之后调用unlock()方法释放锁

代码实例

class Ticket03 implements Runnable{

    private Integer ticket = 10;
    Lock lock = new ReentrantLock();
    
    @Override
    public void run() {
        //判断票是否存在
        while(true){
            //调用lock方法获取锁
            lock.lock();
            try{
                if(ticket > 0){
                    System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票。");
                    ticket--;
                }else{
                    System.out.println(Thread.currentThread().getName()+"票已售空");
                    break;
                }
            }catch (Exception e){
                System.out.println("怎么可能有异常~");
                System.out.println("try{}catch{}流程控制");
                e.printStackTrace();
            }finally {
                //调用unlock方法释放锁
                lock.unlock();
            }
        }
    }
}    

注意

获取锁对象后一定要释放锁对象,否则当前线程会一直持有锁,从而导致其它线程无法访问

四、等待唤醒机制

1.线程间的通信

多个线程并发执行时,CPU的执行时随机的,当需要多个线程共同执行一项任务时,需要有规律的进行,那么就需要协调通信,共同操作一份数据

2.线程的生命周期

下图概括一下就是:任何线程一般具有 5 种状态,即创建、就绪、运行、阻塞、终止。
图是 菜鸟教程 偷来的:https://www.runoob.com/java/java-multithreading.html

在这里插入图片描述

3.相关方法

Object类的 wait、notify、notifyall 方法
Thread类的 sleep、yield、join 方法

所属类方法信息方法介绍
Objectfinal void wait()在其他线程调用此对象的 notify() 方法
或 notifyAll() 方法前,导致当前线程等待。
 final void notify()唤醒在此对象监视器上等待的单个线程。
 final void notifyAll()唤醒在此对象监视器上等待的所有线程。
Threadstatic void sleep(long millis)指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。
 static void yield()暂停当前正在执行的线程对象,并执行其他线程。
 final void join()等待该线程终止。

【tips】

注意 wait、sleep、join 方法有重载形式(参数为时间)

  • wait() 、wait(long timeout) 、wait(long timeout, int nanos)
  • sleep(long millis) 、sleep(long millis, int nanos)
  • join() 、join(long millis) 、join(long millis, int nanos)

4.等待唤醒实例

做包子

案例分析:
1.顾客要买包子,首先需要告知包子铺老板
2.顾客要 等待 包子制作完毕
3.老板做好了包子,通知 顾客可以享用了

public class Demo01 {
    public static void main(String[] args) {
    
        //创建锁对象
        Object obj= new Object();
        
        new Thread(){
            @Override
            public void run() {
                //保证等待和唤醒只有一个在执行
                synchronized (obj){
                    System.out.println("告诉老板包子的种类和数量");
                    try {
                        //阻塞线程,等待老板做包子
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //唤醒之后执行的代码
                    System.out.println("包子已经做好了");
                }
            }
        }.start();

        new Thread(() -> {
                try {
                    //阻塞线程,花 5 秒钟做包子
                    Thread.sleep(5*1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj){
                    System.out.println("老板用 5 秒做好了包子");
                    //唤醒等待线程,告诉顾客包子已经做好了
                    obj.notify();
                }
        }).start();
    }
}

【tips】

哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行
因为它当初中断的地方是在同步块内,而此刻它已经不持有锁
所以需要再次尝试去获取锁(很可能面临其它线程的竞争),
成功后才能在当初调用 wait 方法之后的地方恢复执行。

调用wait和notify方法需要注意的细节
    1.wait方法与notify方法必须要由同一个锁对象调用。
        因为∶对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
    2.wait方法与notify方法是属于Object类的方法的。
        因为∶锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
    3.wait方法与notify方法必须要在同步代码块或者是同步函数中使用。
        因为∶必须要通过锁对象调用这2个方法。

注意事项

1.wait 方法如果有参数,则到了指定时间可以自动唤醒,否则只能阻塞到被 notify 方法唤醒
2.sleep 和 wait 方法都会阻塞线程,但是 wait 会释放 CPU 资源,而 sleep 不会 释放 CPU 资源
3.join 方法主要是在主线程中等待其他子线程执行完 run 方法后再继续执行主线程的后续代码,当然也可以用到其他线程中
4.yield 方法把现在正在执行的线程的状态由执行转成就绪状态,等待资源重新执行。如果 CPU 资源不紧张,会立即继续执行

五、线程池

概念

线程池:可以理解为存放线程的集合,需要开启新线程时,从线程池里边直接取,用完再放回线程池

相关类和方法

Executors 类 
    创建线程池的方法:
        static ExecutorService newFixedThreadPool(int nThreads)
          创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。
        参数:
            nThreads - 池中的线程数
        返回:
            新创建的线程池(ExecutorService(接口)对象)
        
ExecutorService接口
    取出线程的方法:
        Future<?> submit(Runnable task)
          提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。 该 Future 的 get 方法在成功 完成时将会返回 null。
        参数:
            task - 要提交的任务
        返回:
            表示任务等待完成的 Future
    销毁线程的方法:
        void shutdown()
          启动一次顺序关闭,执行以前提交的任务,但不接受新任务。

线程池的使用步骤

1.使用线程池 Executors 工厂类里的 newFixedThreadPool 静态方法生产一个指定数量的线程池
2.创建一个实现类实现 Runnable 接口,重写run方法,设置线程任务
3.调用 ExecutorService 接口中的 submit 方法,传递线程任务(Runnable的实现类),开启线程
4.调用 ExecutorService 接口中的 shutdown 方法销毁线程池(不建议使用)

代码实例

public class Demo01 {
    public static void main(String[] args) {
        //开启线程池,并放入 5 个线程
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        int i = 1;
        //使用while循环从线程池开启 10 个线程
        while(i<=10){
            i++;
            //开启一个线程
            executorService.submit(new Thread(() -> {
                System.out.println("线程池开启线程!"+Thread.currentThread().getName());
            }));
        }
        //销毁线程池
        executorService.shutdown();
    }
}

运行结果
在这里插入图片描述
线程池的优势

对已存在的线程进行复用,减低系统资源的开销
提高系统的相应速度,无需等待线程建立再执行新任务
方便并发管控,控制线程数量,增强系统可靠性与安全性


就到这里,没了没了…


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值