多线程Java多线程学习笔记 | 多线程基础知识

Posted Fxtack

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程Java多线程学习笔记 | 多线程基础知识相关的知识,希望对你有一定的参考价值。

Java多线程学习笔记 | 多线程基础知识


文章目录


一. 线程与进程

  • 进程是一个应用程序,线程是一个进程中的执行场景/执行单元。一个进程可以启动多个线程。

  • 在 java 程序运行时,先启动 JVM 进程,然后JVM进程将启动一个线程调用 main 方法。同时 JVM 还会启动一个GC线程因此一个 Java 程序至少存在两个线程并发。

  • 进程间内存独立不共享,而线程间堆内存与方法区内存共享,栈不共享,一个线程一个栈

  • 主线程(执行main方法的线程)结束后,Java 程序进程可能任然在运行。因为主线程结束,其他线程依然可以继续运行。


二. Java中创建线程

1.编写继承 Thread 的类,重写 run 方法

package org.example;

public class ThreadTest 
    public static void main(String args[]) 
        MyThread myThread = new MyThread();
        //start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间
        //这段代码任务完成后,start()方法瞬间就结束了
        //线程启动成功,自动调用run()方法,并且run()方法在分支栈底部
        myThread.start();
        for(int i = 0 ; i < 100 ; i++) 
        	//输出用于测试,主线程
            System.out.println("The main thread:"+i);
        
    


class MyThread extends Thread 
    @Override
    void run() 
     	for(int i = 0 ; i < 100 ; i++) 
     		//输出用于测试,新创建的线程
            System.out.println("The second thread:"+i);
           
    

  • 注意start()方法的作用是分配新的分支栈并启动run()方法(由JVM完成)。若直接在主方法中调用run()方法只会执行在主线程的栈中执行run()方法
  • start()方法只有瞬间结束或者说不持续运行,才能执行start()方法以下的主线程中的代码。

2. 编写实现 Runnable 的类,重写 run 方法

package org.example;

public class ThreadTest 
    public static void main(String args[]) 
        //创建可运行对象
        MyRunnable myRunnable = new MyRunnable();
        //将可运行对象传入线程类,使其封装为一个线程,从而执行线程
        Thread myThread = new Thread(myRunnable);
        myThread.start();
        for(int i = 0 ; i < 100 ; i++) 
            System.out.println("The main thread:" + i);
        
    


//定义一个实现Runnable接口的类重写run()方法
class MyRunnable implements Runnable 
    @Override
    void run() 
     	for(int i = 0 ; i < 100 ; i++) 
            System.out.println("The second thread:" + i);
           
    

  • 若使用接口的线程创建方式,则这个类可以继承其他类。

3. 其他方式的创建

  • 匿名内部类创建线程

    package org.example;
    
    public class ThreadTest 
        public static void main(String args[]) 
        	//此处是使用实现了Runnable接口的匿名内部类对象创建了线程对象
            Thread myThread = new Thread(new Runnable 
                public void run() 
                    for(int i = 0 ; i < 100 ; i++) 
                    	System.out.println("The thread:" + i);
                    
                
            );
        
    
    
  • Lambda 表达式创建线程(Java8)

    package org.example;
    
    public class ThreadTest 
        public static void main(String args[]) 
        	//Thread接口只有一个待实现的方法,因此可视为函数接口来使用Lambda表达式
            Thread myThread = new Thread(()->
               for(int i = 0 ; i < 100 ; i++) 
                    	System.out.println("The thread:" + i);
                    
            );
        
    
    

三. 线程的生命周期

  • 由 new 出来的线程对象,进入新建状态
  • 调用线程对象的start()方法后,进入就绪状态,又称为可运行状态。此时的线程具有抢夺CPU时间片的权利,即抢夺执行权。
  • 当一个处于就绪状态的线程抢夺到CPU时间片后,开始执行run()方法,run()方法的执行标志着线程进入运行状态
  • 当前的CPU时间片耗尽之后,线程重新进入就绪状态,抢夺CPU时间片。直到再次获得CPU时间片,则再次进入运行状态,接着run()方法上次运行的状态继续运行。就绪状态与运行状态的切换由JVM调度
  • 若遇到阻塞事件,发生阻塞事件的线程进入阻塞状态。阻塞状态的线程会放弃之前占有的CPU时间片
  • 若线程的阻塞事件结束,则该现场重新进入就绪状态,抢夺时间片
  • run()方法运行结束,线程进入死亡状态
start方法 JVM调度 JVM调度 run方法结束 阻塞事件 阻塞解除 新建状态 就绪状态 运行状态 死亡状态 阻塞状态

四. 获取线程对象

1. 线程名称

  • 线程的默认名称

    支线线程的默认名字规律为

    Thread-0

    Thread-1

    Thread-n

    按照创建的顺序进行命名

    主线程的名字为:main

  • 获取线程的名称

    thread.getName()

  • 设置线程的名称

    thread.setName()

2. 获取线程对象

  • 获取当前线程

    通过 Thread 类中的静态方法 currentThread()获取当前线程。

    Thread currentThread = Thread.currentThread();
    

    该方法在什么线程中调用,得到的就是什么线程的对象。如在主线程中调用,获取的对象则为主线程对象。

五. 线程操作

1. 线程睡眠,阻塞状态

  • sleep方法

    通过 Thread 中的静态方法 sleep() 来使线程进入休眠(阻塞)状态。sleep方法的参数值为休眠的毫秒数。在什么线程中调用该方法,什么线程就会进入休眠状态。

    try
        //静态方法
        Thread.sleep(1000 * 5);
    catch(InterruptedException e) 
        e.printStackTrace();
    
    
  • sleep阻塞唤醒

    对线程对象进行 interrupt() 唤醒sleep的线程

    thread.interrupt();
    

    在用 Interrupt() 方法唤醒后,在唤醒线程的 sleep() 方法处将会出现 InterruptedException 异常

2. 线程终止

  • 使用 stop() 强行终止线程

    thread.stop()

    该方法是强行终止线程。该方法已过时,使用该方法会丢失数据。

  • 使用传递值控制线程程序化的终止

    在线程内创建一个变量,线程的执行会轮询该变量,若变量的值改变到某状态时,则程序化的结束run方法,从而实现程序化的结束线程。

    package org.example;
    
    class MyRunnable implements Runnable 
        boolean isRun = true;
        
        @Override
        public void run() 
            for(int i = 0 ; i < 10 ; i++) 
                //如果在该线程运行的某时间段获取的isRun为false,则运行else语句块
                if(isRun) 
                    System.out.println("The thread is running" + i);
                    try 
                        Thread.sleep(1000 * 5);
                    catch (InterruptedException e)
                        e.printStackTrace();
                    
                else
                    //在else语句块内可对该线程中的资源进行回收保存,从而避免数据的丢失
                    //run方法结束,线程进入死亡状态
                    return;
                
            
        
        
    
    

六. 线程调度模型

1. 常见的线程调度模型

  • 抢占式调度模型

    Java 的多线程采用的就是抢占式的调度模型,按照优先级的不同,优先级高的线程更有可能抢到CPU 时间片

  • 均分式调度模型

    平均分配 CPU 时间片。每个线程占有的 CPU 时间长度一样,平均分配

2. Java中的线程调度操作

  • Java线程的优先级

    • 默认优先级,最低优先级,最高优先级

      Java中的最高线程优先级为10,最低为1,默认为5

    • 获取一个线程的优先级

      thread.getPriority() 获取thread线程的优先级

    • 设置一个线程的优先级

      thread.setPriority(10) 设置线程thread的优先级为10

  • 线程让步

static void yield() 让步方法的方法签名

调用方法与 sleep() 方法相同

Thread.yield() 让步方法的调用

暂停当前正在执行的线程对象(处于运行状态的线程),并执行其他线程。yield() 方法不会让线程进入阻塞状态而是让线程进入就绪状态。该线程的让步只是一瞬间,很快该线程又有机会抢夺到 CPU 时间片重新进入运行状态。

  • 线程合并

    thread.join() 线程合并的调用(在有别于 thread 线程的其他线程中调用)

    若在主线程中运行该代码,将会使得 thread 线程与主线程线程合并,主线程将会等待 thread 线程执行完成再继续执行该代码以下的主线程中剩下的任务。相当于单线程

    该方法并不会消除 thread 线程栈的存在,而是等待 thread 线程运行结束

七. 多线程的安全

满足三个条件时,会出现多线程的数据安全问题:

  • 多线程并发
  • 存在多线程共享的数据
  • 共享数据会被修改

在 Java 的三大变量(实例变量存储于堆中,静态变量存储于方法区,局部变量存储于栈中),局部变量不存在线程安全问题。因为局部变量在栈中不共享。

1. 线程同步

线程排队执行,此时多线程不能并发,只能排队等候执行。这种机制被称为线程同步机制

2. 同步编程模型,异步编程模型

  • 异步编程模型

    线程t1月t2,各自执行,互不干扰,不等候,这种编程模型被称为异步编程模型。即多线程并发,效率高

  • 同步编程模型

    线程t1与t2,在t1执行的时候,必须等待t2线程执行结束,或者说在t2执行的时候,必须等待t1的执行结束。即线程排队,效率较低。所谓同步就是排队。

3. Java中实现线程同步

使用关键字 synchronized 创建 synchronized 语句块来实现多线程操作数据时的线程同步

//synchronized语句块
synchronized(Object) 
    //操作

  • 小括号中填入的是线程共享的数据或对象。若要对t1与t2操作的数据或对象进行线程同步则填入的是t1与t2共同操作的数据对象。若是n个线程都进行同步,则填入n个线程将会共同访问的对象,进行线程同步。

  • 实现原理

    • 假设 t1 和 t2 并发,开始执行,直到 synchronized 语句块必然有先后。
    • 假设 t1 先执行,遇到了 synchronized,这时候自动找共享对象的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在执行过程中一直都是占有这把对象锁。直到同步代码块结束,这把锁才释放。
    • 假设 t1 占有这把锁,此时 t2 也遇到了 synchronized 关键字,也会去占有后面共享对象的这把锁,结果这把锁被 t1 占有,t2 只能等待 t1 执行完并释放该锁。直到锁释放,t2 得到这把锁,然后 t2 占有该锁并进入同步代码块执行程序
  • 锁池 lockpool

    运行当中的线程遇到了 synchronized 关键字,会放弃 CPU 时间片,进入 lockpool 中寻找共享对象的对象锁。若没有找到共享对象的对象锁,则在锁池中等待,直到找到共享对象锁(例如其他线程释放了共享对象的对象锁)。若找到了共享对象锁则进入就绪状态,继续抢夺 CPU 时间片

    以上是关于多线程Java多线程学习笔记 | 多线程基础知识的主要内容,如果未能解决你的问题,请参考以下文章

    Java基础学习笔记二十一 多线程

    Java多线程学习笔记

    Java学习笔记之三十四超详解Java多线程基础

    尚硅谷_Java零基础教程(多线程)-- 学习笔记

    Java学习笔记---多线程同步的五种方法

    多线程编程学习笔记——线程同步