每天学一点之多线程

多线程

一、相关概念

并发与并行

并行(parallel):指多个事件任务在同一时刻发生(同时发生)。
并发(concurrency):指两个或多个事件在同一个微小的时间段内发生。程序并发执行可以在有限条件下,充分利用CPU资源。
在这里插入图片描述
单核CPU:只能并发
多核CPU:并行+并发

线程与进程
  • 程序:为了完成某个任务和功能,选择一种编程语言编写的一组指令的集合。

  • 软件:1个或多个应用程序+相关的素材和资源文件等构成一个软件系统。

  • 进程是对一个程序运行过程(创建-运行-消亡)的描述,系统会为每个运行的程序建立一个进程,并为进程分配独立的系统资源,比如内存空间等资源。

  • 线程:线程是进程中的一个执行单元,负责完成执行当前程序的任务,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这时这个应用程序也可以称之为多线程程序。多线程使得程序可以并发执行,充分利用CPU资源。

    面试题:进程是操作系统调度和分配资源的最小单位,线程是CPU调度的最小单位。不同的进程之间是不共享内存的。进程之间的数据交换和通信的成本是很高。不同的线程是共享同一个进程的内存的。当然不同的线程也有自己独立的内存空间。对于方法区,堆中中的同一个对象的内存,线程之间是可以共享的,但是栈的局部变量永远是独立的。

多线程的优点与应用场景
  • 主要优点:
    • 充分利用CPU空闲时间片,用尽可能短的时间完成用户的请求。也就是使程序的响应速度更快 。
  • 应用场景:
    • 多任务处理。多个用户请求服务器,服务端程序可以开启多个线程分别处理每个用户的请求,互不影响。
    • 单个大任务处理。下载一个大文件,可以开启多个线程一起下载,减少整体下载时间。
线程调度

指CPU资源如何分配给不同的线程。常见的两种线程调度方式:

  • 分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java采用的是抢占式调度方式

二、线程的创建与启动

继承Thread类

通过继承Thread类来创建并启动多线程的步骤:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

多线程执行情况分析
在这里插入图片描述
注意事项:

  • 手动调用run方法不是启动线程的方式,只是普通方法调用。

  • start方法启动线程后,run方法会由JVM调用执行。

  • 不要重复启动同一个线程,否则抛出异常IllegalThreadStateException

  • 不要使用Junit单元测试多线程,不支持,主线程结束后会调用System.exit()直接退出JVM;

实现Runnable接口
  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动线程。
两种创建线程方式比较
  • Thread类本身也是实现了Runnable接口的,run方法都来自Runnable接口,run方法也是真正要执行的线程任务。

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

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

匿名内部类对象创建线程

匿名内部类对象的方式创建线程,并不是一种新的创建线程的方式,只是在线程任务只需执行一次的情况下,我们无需单独创建线程类,可以采用匿名对象的方式:

new Thread("新的线程!"){
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(getName()+":正在执行!"+i);
		}
	}
}.start();

new Thread(new Runnable(){
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+":" + i);
        }
    }
}).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():检查当前线程是否为守护线程。

  • volatile的作用是确保不会因编译器的优化而省略某些指令,volatile的变量是说这变量可能会被意想不到地改变,每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份,这样,编译器就不会去假设这个变量的值了。

线程生命周期

传统线程模型的五种线程状态
在这里插入图片描述

JDK定义的六种线程状态
java.lang.Thread类内部定义了一个枚举类用来描述线程的六种状态:

    public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }

在这里插入图片描述
说明:当从WAITINGTIMED_WAITING恢复到Runnable状态时,如果发现当前线程没有得到监视器锁,那么会立刻转入BLOCKED状态。

线程安全

当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,但是如果多个线程中对资源有读和写的操作,就会出现前后数据不一致问题,这就是线程安全问题。

线程安全问题引出

  • 局部变量不能共享:局部变量是每次调用方法都是独立的,那么每个线程的run()的数据是独立的,不是共享数据。
  • 不同对象的实例变量不共享 :不同的实例对象的实例变量是独立的。
  • 静态变量是共享的
  • 同一个对象的实例变量共享

总结:线程安全问题的出现因为具备了以下条件

  1. 多线程执行
  2. 共享数据
  3. 多条语句操作共享数据
线程安全问题解决方式

在这里插入图片描述
同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着。

public synchronized void method(){
    可能会产生线程安全问题的代码
}

同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。

synchronized(同步锁){
     需要同步操作的代码
}
锁对象选择

首选this其次是类.class也可以是" "空字符串对象

同步锁对象:

  • 锁对象可以是任意类型。
  • 多个线程对象 要使用同一把锁。

同步代码块的锁对象

  • 静态代码块中:使用当前类的Class对象
  • 非静代码块中:习惯上先考虑this,但是要注意是否同一个this

锁的范围太小:不能解决安全问题,要同步所有操作共享资源的语句。

锁的范围太大:因为一旦某个线程抢到锁,其他线程就只能等待,所以范围太大,效率会降低,不能合理利用CPU资源。

单例设计模式的线程安全问题

1、饿汉式没有线程安全问题

饿汉式:上来就创建对象

2、懒汉式线程安全问题

public class Singleton {
    private static Singleton ourInstance;

    public static Singleton getInstance() {
        //一旦创建了对象,之后再次获取对象,都不会再进入同步代码块,提升效率
        if (ourInstance == null) {
            //同步锁,锁住判断语句与创建对象并赋值的语句
            synchronized (Singleton.class) {
                if (ourInstance == null) {
                    ourInstance = new Singleton();
                }
            }
        }
        return ourInstance;
    }

    private Singleton() {
    }
}

等待唤醒机制

在一个线程满足某个条件时,就进入等待状态(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、死锁

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

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

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

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

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

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

练习

要求两个线程,同时打印字母,每个线程都能连续打印3个字母。两个线程交替打印,一个线程打印字母的小写形式,一个线程打印字母的大写形式,但是字母是连续的。当字母循环到z之后,回到a。

在这里插入图片描述

public class PrintLetterDemo {
	public static void main(String[] args) {
		// 2、创建资源对象
		PrintLetter p = new PrintLetter();

		// 3、创建两个线程打印
		new Thread("小写字母") {
			public void run() {
				while (true) {
					p.printLower();
					try {
						Thread.sleep(1000);// 控制节奏
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}.start();

		new Thread("大写字母") {
			public void run() {
				while (true) {
					p.printUpper();
					try {
						Thread.sleep(1000);// 控制节奏
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}.start();
	}
}

// 1、定义资源类
class PrintLetter {
	private char letter = 'a';

	public synchronized void printLower() {
		for (int i = 1; i <= 3; i++) {
			System.out.println(Thread.currentThread().getName() + "->" + letter);
			letter++;
			if (letter > 'z') {
				letter = 'a';
			}
		}
		this.notify();
		try {
			this.wait();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public synchronized void printUpper() {
		for (int i = 1; i <= 3; i++) {
			System.out.println(Thread.currentThread().getName() + "->" + (char) (letter - 32));
			letter++;
			if (letter > 'z') {
				letter = 'a';
			}
		}
		this.notify();
		try {
			this.wait();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
} 
  • 31
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力奋斗的JAVA小余

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值