Java线程基础

发布时间 2023-09-04 10:36:23作者: Ahci

相关概念

一个进程包含多个线程, 这些线程共享进程的资源(内存空间和文件句柄), 还有属于自己的内存空间, 这段空间是建立线程时由系统分配的, 用来保存线程内部所使用的数据, 如线程执行函数中所定义的变量;

Java中的多线程是抢占机制;

并发: 同一时刻, 多个任务交替执行, 造成一种同时执行的错觉. 比如单核CPU实现的多任务就是并发;

并行: 同一时刻, 多个任务同时进行. 多核CPU就可以实现并行;

创建线程

创建线程有两种方法, 一个是继承Thread类, 第二种是实现Runnable接口. 两种方法都需要重写run()方法, 但要注意, 重写的run()方法可以被JVM调用, 但重载的不行;

run()方法中抛出的异常只能由方法内部捕获处理, 或使用记录异常等方式通知调用方, 不能直接抛出异常;

继承Thread类

由于Java的单继承机制, 不推荐使用这种方法;

public class Thread01 {
    public static void main(String[] args) throws InterruptedException {
		Cat cat = new Cat();
        new Thread(cat).start();
    }
}

class Cat extends Thread {
    @Override
    public void run() {
		System.out.println("Cat Thread");
    }
}

使用Runnable接口

实现Runnable接口的类不能通过对象名.start()来启动线程, 而要创建一个Thread对象并传入实现了Runnable接口的对象作为参数, 然后使用thread.start()来启动线程;

public class Thread02 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        new Thread(dog).start();
    }
}

class Dog implements Runnable {
    @Override
    public void run() {
        System.out.println("Dog Thread");
    }
}

执行过程

run()

Thread中的run()方法实际是实现了Runnable的run(), 源码为:

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

当程序启动后, 会创建一个主线程(通常称为"main"线程), 主线程会调用其他线程(例如"cat"线程)来执行其中的任务, 但这个调用不会阻塞主线程的继续执行;

主线程结束了不代表程序结束;

start()

在创建Cat对象后, 调用start()方法, 最终会调用Cat中的run()方法开启线程, 那么为什么不直接调用run()方法呢? 如果直接调用, 那么就相当于直接由主线程调用Cat中的run()方法, 此时输出的线程名称不是Thread-01是main, 也就是说并没有真正的开启线程. 程序会先执行完run()方法的内容再执行main的后续内容.

start()方法执行后调用start0()这个方法, start0()方法是由JVM调用, 由它来调用run()方法实现多线程.

start0()方法被调用后, 该线程不会马上执行, 而是将线程变为可运行状态, 具体什么时候执行, 取决于CPU, 由CPU统一调度.

常用方法

方法名 作用
setName() 设置线程名称, 使之与参数name相同
getName() 返回该线程的名称
start() 使线程开始执行
run() 调用线程对象的run()方法
setPriority() 更改线程优先级
getPriority() 获取线程优先级
sleep() 在指定的毫秒数内让当前正在运行的线程休眠
interrupt() 中断线程
  • start()底层会创建新的线程, 调用run(), run就是一个简单的方法调用, 不会启动新的线程
  • 线程优先级常用的有三种: MAX_PRIORITY(10), MIN_PRIORITY(1), NORM_PRIORITY(5); 优先级高的线程只是执行几率比优先级低的高
  • sleep()指的是最小不执行时间, 因为休眠时间结束无法保证会被JVM立即调度; 线程休眠时不会失去拥有的对象锁, 而是让出CPU, 给其他线程执行的机会
  • interrupt()中断线程而不是结束线程, 一般用于中断正在休眠的线程
// main线程启动t线程, 在休眠2秒后中断t线程
public class ThreadMethod01 {
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        t.start();
        Thread.sleep(2000);
        t.interrupt();

    }
}
// 每输出20次, 休眠5秒
class T extends Thread {
    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 20; i++) {
                System.out.println(Thread.currentThread().getName() + " 次数: " + i);
            }
            try {
                System.out.println(Thread.currentThread().getName() + "休眠中");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "被Interrupt了");
            }
        }
    }
}
方法 作用
yield() 让出CPU让其他线程执行, 但让出时间不确定
join() 线程插队, 若插队成功则肯定先执行完插入的线程的所有任务
  • yield()方法用于让出CPU使用权, 给同等优先权的线程运行, 若没有同等优先权的进程, yield()不会起作用
// main线程在循环五次后让tm2线程先执行
public class ThreadMethod02 {
    public static void main(String[] args) throws InterruptedException {
        Tm2 tm2 = new Tm2();
        tm2.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("hi");
            Thread.sleep(1000);
            if (i == 5) {
                System.out.println("主线程让子线程先执行");
                tm2.join();
            }
        }
    }

}
// 输出20次hello
class Tm2 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("hello");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

线程销毁

线程销毁有两种方式:

  1. 当线程完成任务后, 会自动退出.
  2. 通过使用变量控制run()方法退出, 这种方式也称为通知方式.

比如启动一个线程t, main线程休眠五秒后去停止线程t:

public class ThreadExit {
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        t.start();

        Thread.sleep(5000);
        t.setLoop(false);

    }
}

class T extends Thread {
    private boolean loop = true;
    @Override
    public void run() {
        while (loop) {
            System.out.println("线程还在运行");
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void setLoop(boolean loop) {
        this.loop = loop;
    }
}

守护线程和用户线程

用户线程: 也叫工作线程, 线程的结束靠的是任务执行完或通知方式结束.

守护线程: 一般为工作线程服务, 当所有用户线程结束, 守护线程自动结束. 常见的比如垃圾回收机制.

当JVM中不存在任何一个正在运行的非守护线程时, JVM进程就会退出.

设置守护线程

通过setDaemon(true)将该线程设为守护线程.

public class DaemonThread {
    public static void main(String[] args) throws InterruptedException {
        MyDaemonThread myDaemonThread = new MyDaemonThread();
        myDaemonThread.setDaemon(true);
        myDaemonThread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程工作中...");
            Thread.sleep(100);
        }
    }
}

class MyDaemonThread extends Thread {
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("主线程还没退出, 守护线程还在运行中...");
        }
    }
}

线程的生命周期

JDK中使用Thread.State枚举了线程的几种状态:

状态 行为
NEW 尚未启动的线程处于此状态
RUNNABLE 在Java虚拟机中执行的线程处于此状态
BLOCKED 被阻塞等待监视器锁定的线程处于此状态
WAITING 正在等待另一个线程执行特定动作的线程处于此状态
TIMED_WAITING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
TERMINATED 已退出的线程处于此状态

  • 当一个线程实例被创建就属于NEW状态, 当前状态的线程有自己的内存空间
  • 调用线程实例的start()方法使线程进入RUNNABLE状态, 此时线程若在等待系统分配CPU, 则是READY状态, 一旦获取CPU, 则进入RUNNING状态, 线程的run()方法开始执行
  • 只有线程处于BLOCKED状态时才会在锁释放时竞争锁

线程同步状态

线程同步机制: 在多线程中一些敏感数据不允许被多个线程同时访问, 此时就要使用同步技术, 保证数据在任何时刻最多只有一个线程访问, 以保证数据的完整性; 通俗来讲就是当一个线程A对内存进行操作时, 其他线程都不能对这个内存地址操作, 直到A线程完成操作, 其他线程才能对该内存地址进行操作;

Synchronized

同步代码块:

Synchronized (对象) {	//得到对象的锁, 才能操作同步代码
	// 需要被同步的代码
}

同步方法:

public synchronized void method(String name) {
	// 需要被同步的代码
}

互斥锁

Java中引入了互斥锁的概念保证共享数据操作的完整性;

每个对象都对应一个可称为互斥锁的标记, 这个标记用来保证在任意时刻, 只能有一个线程访问该对象;

关键字synchronized来与对象的互斥锁联系, 当某个对象用synchronized修饰时, 表明该对象在任意时刻只能由一个线程访问;

同步的局限性: 导致程序的执行效率降低

同步方法(非静态的)的锁可以是this, 也可以是其他对象(要求是同一个对象)

如下就不是同一个对象

synchronized(new Object()) {
    //other code...
}

如下就是同一个对象:

Object obj = new Object();
synchronized(obj) {
    //other code...
}

下面代码中前两个锁在它们的代码块/方法上, 最后一个代码块的锁加载Thread01类的obj对象上

public void method02() {
    synchronized (Object.class) {
        //other code...
    }
}
public synchronized void method02() {
    //other code...
}
public class SellTicket {
    public static void main(String[] args) {
        Thread01 thread = new Thread01();
        new Thread(thread).start();
        new Thread(thread).start();
        new Thread(thread).start();
    }
}

class Thread01 {
    Object obj = new Object();
    public void method02() {
        synchronized (obj) {
			// other code...
        }
    }
}

同步方法(静态的)的锁为当前类本身: 下面两个锁都是在它们的类上, 而非代码块/方法上

public static void method02() {
    synchronized (Thread01.class) {
        //other code...
    }
}
public synchronized static void method02() {
    //other code...
}

死锁

多个线程占用了对方的资源且都不释放, 就形成了死锁.

释放锁

下面的操作会释放锁:

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

  • 当前线程在同步方法,同步代码块中遇到break, return

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

  • 当前线程在同步方法,同步代码块中执行了线程对象wait()方法, 当前线程暂停, 并释放锁

下面的操作不会释放锁:

  • 线程执行同步方法,同步代码块时, 程序调用Thread.sleep(), Thread.yield()方法暂停当前线程的执行
  • 线程执行同步方法,同步代码块时, 其他线程调用了该线程的suspend()方法将该线程挂起(尽量避免使用suspend()和resume()来控制线程)