JavaSE -- 多线程

目录

一、相关概念

        1.并行与并发

        2.线程与进程

        3.多线程的好处与应用场景  

        4.线程调度方式  

二、线程的创建与启动

         1.方式一:继承Thread类

        2.方式二:实现Runnable接口

        3.线程的使用与启动注意事项:

        4.两种创建方式的比较:

        5.匿名内部类创建方式创建线程  

三、Thread类

  🚀构造方法

🚀 线程使用基础方法

🚀 线程控制常见方法

四、线程的生命周期状态  

五、线程安全问题

1.线程安全问题的原因(条件)

2.线程安全问题的解决

3.锁对象使用和选择的问题

4.同步代码的范围问题

5.编写多线多线程程序的原则:

6.单例模式之懒汉式的线程安全问题解决

六、等待唤醒机制

七、释放锁操作与死锁

1、释放锁的操作

2、不会释放锁的操作

3、死锁

4、面试题:sleep()和wait()方法的区别


一、相关概念

        1.并行与并发

  • 并行:同一个时间点,多个事件正在发生

  • 并发:同一个微小的时间片段内,多个事件正在发生。
    单核CPU只支持并发执行,不支持并行执行。

        2.线程与进程

  • 进程:程序运行过程的描述,系统以进程为单位为每个程序分配独立的系统资源,比如内存。

  • 线程:是进程中的执行单元,一个进程中至少有一个线程,如果有多个线程就是多线程程序。

        3.多线程的好处与应用场景  

  • 好处:充分利用CPU资源

  • 应用场景:

    • 多个线程做相同的任务,共同完成一个大的任务

    • 多个线程分别做不同的任务,共同完成一个大的任务

        4.线程调度方式  

    💡CPU以线程为单位分配资源

  • 分时调度,平均分配CPU资源

  • 抢占式调度方式,随机为每个线程分配CPU资源,Java支持的是抢占式调度方式

二、线程的创建与启动

         1.方式一:继承Thread类

  • 创建Thread类的子类并重写run方法

  • 创建子类对象

  • 调用start方法启动线程

        ⭕️自定义线程类:  

public class MyThread extends Thread {
	//定义指定线程名称的构造方法
	public MyThread(String name) {
		//调用父类的String参数的构造方法,指定线程的名称
		super(name);
	}
	/**
	 * 重写run方法,完成该线程执行的逻辑
	 */
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(getName()+":正在执行!"+i);
		}
	}
}

         ⭕️测试类:创建线程对象并启动线程

public class Demo01 {
	public static void main(String[] args) {
		//创建自定义线程对象
		MyThread mt = new MyThread("新的线程!");
		//开启新线程
		mt.start();
		//在主方法中执行for循环
		for (int i = 0; i < 10; i++) {
			System.out.println("main线程!"+i);
		}
	}
}

        2.方式二:实现Runnable接口

  • 创建Runnable接口的实现类,并重写run方法

  • 创建实现类对象(线程任务类)

  • 创建Thread对象,通过构造器传入实现类对象

  • 调用start方法启动线程

        ⭕️ 自定义线程任务类:

  public class MyRunnable implements Runnable{
  	@Override  
      public void run() {
          for (int i = 0; i < 20; i++) {
          	System.out.println(Thread.currentThread().getName()+" "+i);         
  		}       
  	}    
  }

        ⭕️ 测试类:创建线程对象并启动线程

  public class Demo {
      public static void main(String[] args) {
          //创建自定义类对象  线程任务对象
          MyRunnable mr = new MyRunnable();
          //创建线程对象
          Thread t = new Thread(mr, "小强");
          t.start();
          for (int i = 0; i < 20; i++) {
              System.out.println("旺财 " + i);
          }
      }
  }

        3.线程的使用与启动注意事项:

  • 调用线程对象的start方法启动线程,不要重复启动同一个线程

  • 不要直接调用run方法,这不是启动线程,而是普通的方法调用

  • 不要使用junit测试多线程程序

        4.两种创建方式的比较:

  • Thread类本身也是实现了Runnable接口的,run方法都来自Runnable接口,run方法也是真正要执行的线程任务。

    public class Thread implements Runnable {}
  • 因为Java类是单继承的,所以继承Thread的方式有单继承的局限性,但是使用上更简单一些。

  • 实现Runnable接口的方式,避免了单继承的局限性,并且可以使多个线程对象共享一个Runnable实现类(线程任务类)对象,从而方便在多线程任务执行时共享数据

 

        ✒️总结:方式一有单继承限制  , 方式二方便共享数据

        5.匿名内部类创建方式创建线程  

//方式一的匿名内部类创建方式
new Thread(){
    public void run(){
        //线程任务
    }
}.start();

//方式二的匿名内部类创建方式
new Thread(new Runnable(){
    public void run(){
        //线程任务
    }
}).start();

三、Thread类

  🚀构造方法

  • public Thread() :分配一个新的线程对象。

  • public Thread(String name) :分配一个指定名字的新的线程对象。

  • public Thread(Runnable target) :分配一个带有指定目标新的线程对象。

  • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

🚀 线程使用基础方法

  • public void run() :此线程要执行的任务在此处定义代码。

  • public String getName() :获取当前线程名称。

  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。

  • public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。

  • public final int getPriority() :返回线程优先级

  • public final void setPriority(int newPriority) :改变线程的优先级

    • 每个线程都有一定的优先级,优先级高的线程将获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。Thread类提供了setPriority(int newPriority)和getPriority()方法类设置和获取线程的优先级,其中setPriority方法需要一个整数,并且范围在[1,10]之间,通常推荐设置Thread类的三个优先级常量:

    • MAX_PRIORITY(10):最高优先级

    • MIN _PRIORITY (1):最低优先级

    • NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。

🚀 线程控制常见方法

  • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。

  • public static void sleep(long millis) :线程睡眠,使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

  • public static void yield():线程礼让,yield只是让当前线程暂时失去执行权,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。

  • void join() :加入线程,当前线程中加入一个新线程,等待加入的线程终止后再继续执行当前线程。

    void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。

    void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。

  • public final void stop():强迫线程停止执行。 该方法具有不安全性,已被弃用,最好不要使用。

    • 调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。

    • 调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。

  • public void interrupt():中断线程,实际上是给线程打上一个中断的标记,并不会真正使线程停止执行。

  • public static boolean interrupted():检查线程的中断状态,调用此方法会清除中断状态(标记)。

  • public boolean isInterrupted():检查线程中断状态,不会清除中断状态(标记)

  • public void setDaemon(boolean on):将线程设置为守护线程。必须在线程启动之前设置,否则会报IllegalThreadStateException异常。

    • 守护线程,主要为其他线程服务,当程序中没有非守护线程执行时,守护线程也将终止执行。JVM垃圾回收器也是守护线程。

  • public boolean isDaemon():检查当前线程是否为守护线程。

四、线程的生命周期状态  

五、线程安全问题

1.线程安全问题的原因(条件)

  • 使用多线程

  • 多个线程共享数据

  • 理解为多条语句操作共享数据

2.线程安全问题的解决

        💡Java使用同步机制解决线程安全问题,使用关键字synchronized实现同步机制:

        💡一个线程要执行同步代码,必须先获取同步锁对象,在执行同步代码时会持有锁对象,执行完后会自动释放锁对象,其他线程才有机会执行同步代码。

  • 同步代码块

    synchronized(锁对象){
        //同步执行的代码
    }
  • 同步方法,由synchronized修饰的方法
     

    public synchronized void sale(){
        
    }

3.锁对象使用和选择的问题

  • 锁对象可以是任意Java对象,但是必须保证多个线程使用同一个锁对象,否则解决不了线程安全问题。

  • 非静态域中的同步代码块建议使用this做为锁对象,非静态方法默认使用this做为锁对象

  • 静态域中的同步代码块建议使用当前类的Class实例作为锁对象。静态方法默认使用当前类的Class实例作为锁对象

4.同步代码的范围问题

        💡多线程同步执行的代码范围越小越好,效率高。但是至少也要包含操作共享数据的所有关键代码,否则解决不了线程安全问题。

5.编写多线多线程程序的原则:

        把线程任务类与共享资源类解耦合

6.单例模式之懒汉式的线程安全问题解决

public class Singleton{
    private static Singleton instance = null;
    private Singleton(){}
    
    public static Singleton getInstance(){
       if(instance == null)
            synchronized(Singleton.class){
                if(instance == null)
                    instance = new Singleton();
            }
        return instance;
            
    }
}

 

六、等待唤醒机制

等待唤醒机制是指使用Object类中的waitnotify方法实现线程间协调通信的一种工作机制。

什么是等待唤醒机制

这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。

就是在一个线程满足某个条件时,就进入等待状态(wait()/wait(time)), 等待其他线程执行完他们的指定代码过后再将其唤醒(notify());或可以指定wait的时间,等时间到了自动唤醒;在有多个线程进行等待时,如果需要,可以使用 notifyAll()来唤醒所有的等待线程。wait/notify 就是线程间的一种协作机制。

  1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING或TIMED_WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”或者等待时间到,在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中

  2. notify:则选取所通知对象的 wait set 中的一个线程释放;

  3. notifyAll:则释放所通知对象的 wait set 上的全部线程。

注意:

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

总结如下:

  • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE(可运行) 状态;

  • 否则,线程就从 WAITING 状态又变成 BLOCKED(等待锁) 状态

调用wait和notify方法需要注意的细节

  1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。

  2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。

  3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用,并且必须要通过锁对象调用这2个方法。

七、释放锁操作与死锁

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?

1、释放锁的操作

当前线程的同步方法、同步代码块执行结束。

当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致当前线程异常结束。

当前线程在同步代码块、同步方法中执行了锁对象的wait()方法,当前线程被挂起,并释放锁。

2、不会释放锁的操作

线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。

线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该该线程挂起,该线程不会释放锁(同步监视器)。应尽量避免使用suspend()和resume()这样的过时来控制线程。

3、死锁

不同的线程分别锁住对方需要的同步监视器对象不释放,都在等待对方先放弃时就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

4、面试题:sleep()和wait()方法的区别

(1)sleep()不释放锁,wait()释放锁

(2)sleep()指定休眠的时间,wait()可以指定时间也可以无限等待直到notify或notifyAll

(3)sleep()在Thread类中声明的静态方法,wait方法在Object类中声明

因为我们调用wait()方法是由锁对象调用,而锁对象的类型是任意类型的对象。那么希望任意类型的对象都要有的方法,只能声明在Object类中。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值