【JAVA】线程、互斥锁、死锁

37 阅读6分钟

线程基础

线程创建

线程的定义

创建线程的两种方法

创建线程有两种方法:通过实现Runnable接口创建线程和通过继承Thread类创建线程。

  • MyThread类继承Thread类;重写run方法;实例化对象mythread可以作为线程使用,mythread.start()

A类已经继承了B类怎么办?不能继承Thread类了

  • 实现Runnable接口;重写run方法;传入实例化对象new Thread(dog),这里Thread使用代理模式

区别:1. 本质上没有区别,从jdk文档可以看到Thread类本身实现了Runnable接口;2. 从使用上有区别,实现Runnable接口更加适合多个线程共享一个资源的情况,并且不受单继承的限制。

线程运行的原理

内存中发生了什么?run和start的调用?

运行程序,相当于启动进程,首先开启主线程main,主线程开启了子线程,子线程开启后,主线程 不会阻塞,两个线程交替执行。主线程结束了,并不意味着子线程结束,子线程还在执行,那么进程也没有结束。

run方法是一个普通的方法, 并没有真正启动线程,程序是串行化执行,并不是并行执行,会阻塞在run方法处,当run方法执行完毕后,才会继续向下执行。

Thread类的start方法会调用start0方法, start0是native本地方法,由JVM调用。start方法调用后,该线程并不会立马执行,而是将该线程变成了可运行状态,具体什么时候执行,取决于CPU,由CPU统一调度。

Thread t = new Thread(dog);t.start();

线程终止

  1. 当线程完成任务后,会自动终止
  2. 通过变量控制run方法的结束,来终止线程,这种方法被称为通知

线程中断

Thread常用方法

  • setName:设置线程名称
  • getName:Thread.currentThread().getName()获取当前线程的名称
  • start
  • run
  • setPriority
  • getPriority
  • sleep
  • interrupt 中断不是停止
  1. 线程优先级的范围:MAXPRIORITY=10, NORMPRIORITY=5,MINPRIORITY=1
  2. interrupt并没有结束线程,用于中断正在休眠的线程,提前结束休眠
  3. sleep使当前线程休眠

线程插队

常用方法第二组:

协调线程的方法。在线程t1中调用Thread.yield()方法,可以让出cpu,让t2先执行。在线程t1中调用t2.join()方法,可以让cpu先执行t2

  • yield:线程的礼让。让出cpu,让其他线程先执行,礼让的时间不确定,所以不一定能让成功。
  • join:线程的插队。插队的线程一旦插队成功,则先执行完插入线程的所有任务,再执行其他线程。

守护线程

  1. 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
  2. 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束,常见的守护线程有垃圾回收机制。

线程七大状态

Thread.State_静态属性有六种取值

  • NEW
  • RUNNABLE: READY/RUNNING,就绪态或运行态
  • BLOCKED:等待进入同步代码块的锁
  • WAITING:wait或join进入,notify唤醒
  • TIMED_WAITING:sleep或wait进入,时间结束后进入runnable
  • TERMINATED

image.png

查看状态:thread01.getState()

线程同步机制

  1. 在多线程中,一些数据不允许被多个线程同时访问,此时使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。
  2. 理解:当有一个线程在对内存进行操作时,其他线程都不能对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。

两种同步:synchronized

  1. 同步代码块:synchronized (对象){}
  2. 同步方法:public synchronized void method01(){}

这里的锁加在当前对象this

利用关键字解决售票问题:

  1. 采用synchronized关键字修饰run方法,不合理,导致的结果是只有第一个线程会一直执行,直到没有票
  2. 将买票作为sell方法,run方法执行sell方法。对sell方法采用synchronized关键字修饰,多个线程轮流卖票,不会出现超卖。

互斥锁

互斥锁的基本理解:

  1. java中引入对象互斥锁的概念,来保证共享数据操作的完整性
  2. 每个对象都对应一个互斥锁的标记,该标记可以保证某一时刻,只能由一个线程访问该对象
  3. 关键字synchronized用来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象只能某一时刻由一个线程访问
  4. 同步的局限性:程序执行效率降低
  5. 非静态的同步方法的锁默认是当前对象this,也可以是其他对象(要求是同一个)
  • 第一种:当前对象this
    public synchronized void sell(){..同步代码..} 相当于
public void sell(){

synchronized (this) {..同步代码..}

}
  • 第二种:先定义一个属性为obj对象Object obj = new Object()
    public synchronized void sell(){..同步代码..} 相当于
public void sell(){

    synchronized (obj) {..同步代码..}

}

这里一定要保证不同线程的obj是同一个对象

  1. 静态的同步方法的锁为当前类本身,当前类.class

public synchronized static void m1(){..同步代码..} 相当于

public static void m1(){
    synchronized (SellTicket03.class){..同步代码.}
}

互斥锁的使用:解决售票问题

  1. 先分析需要上锁的代码
  2. 选择同步代码块或者同步方法
  3. 要求多个线程的锁对象为同一个

死锁

是什么? 多个线程都占用了对方的锁资源,并且互不相让,导致了死锁。

为什么? 假设线程AB已经分别拿到obj1,2的对象锁,此时线程A尝试获取obj2的锁,但是线程B还没有释放obj2的锁,所以线程A会持有obj1的锁并且阻塞,同样的,由于线程A没有释放obj1的锁,线程B也不能获取obj1的对象锁,会持有obj2的锁并且阻塞,这就会造成线程死锁。

怎么做? 模拟线程死锁:在线程构造器中给定属性flag,run方法中,有if-else分支,不同的flag进入不同的分支,每个分支中都有两把锁,分支1先获取obj1的对象锁,再尝试获取obj2的对象锁,分支2先获取obj2的对象锁,再尝试获取obj1的对象锁。

如何释放锁?

  1. 当前线程的同步方法或同步代码块执行结束
  2. 当前线程在同步代码块或同步方法中遇到break和return
  3. 当前线程在同步代码块或同步方法中出现error或exception
  4. 当前线程在同步代码块,同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁

什么情况不会释放锁?

  • 当线程执行同步代码和同步方法时,程序调用了Thread.yield()或Thread.sleep()方法,会暂停当前程序的执行,但不会释放锁
  • 当线程执行同步代码和同步方法时,其他线程调用了该线程的suspend()方法将该线程挂起,但不会释放锁