文章目录

  • 一、线程基础知识
  • 1.1 进程是什么
  • 1.2 线程是什么
  • 1.3为什么使用线程
  • 1.4 线程的生命周期
  • 二、线程基础操作
  • 2.1 新建线程
  • 2.1.1 如何创建线程
  • 2.1.2 调用start()和调用run()的区别
  • 2.2 终止线程
  • 2.2.1 stop()方法
  • 2.2.2 为什么不推荐使用stop()方法
  • 2.3 线程中断
  • 2.3.1 线程中断
  • 2.3.2 与线程中断有关的方法
  • 2.3.3 sleep()方法
  • 2.3.4 sleep()由于中断而导致的问题
  • 2.4 等待(wait)和通知(notify)
  • 2.4.1 wait()、notify()和notifyAll()工作原理
  • 2.4.2 wait()方法的使用限制
  • 2.4.3 wait()和sleep()区别
  • 2.5 挂起(suspend)和继续执行(resume)
  • 为什么不推荐使用suspend()
  • 2.6 等待结束(join)和谦让(yield)
  • 2.6.1 join方法
  • 2.6.2 join方法的本质
  • 2.6.3 yield方法
  • 三、volatile与Java内存模型(JMM)
  • 四、守护线程
  • 4.1 守护线程
  • 4.2 用户线程
  • 4.3 何时设置守护线程
  • 五、线程安全与Synchronized
  • 5.1 volatile的问题
  • 5.2 synchronized的作用
  • 5.3 synchronized的用法


一、线程基础知识


1.1 进程是什么

进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础

1.2 线程是什么

线程就是轻量级的进程,是程序执行的最小单位

1.3为什么使用线程

线程间的切换和调度成本远远小于进程

1.4 线程的生命周期

Java高级程序设计 java高级程序设计实战教程_并发编程

  • 线程的相关状态在Thread类中的State枚举中定义
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

注意:从 NEW 状态出发后,线程不能再回到 NEW 状态;同理,处于 TERMINATED 状态的线程也不能再回到 RUNNABLE 状态

二、线程基础操作


2.1 新建线程

2.1.1 如何创建线程

  • 继承 Thread
  • 实现 Runnable 接口
  • 实现 Callable 接口
  • 使用线程池

2.1.2 调用start()和调用run()的区别

  • 调用 start() :会新建一个新的线程,用这个新的线程执行这个 run() 方法
  • run() :就是普通的方法调用,不会创建新线程

2.2 终止线程

2.2.1 stop()方法

JDK 的 Thread 类提供了一个 stop() 方法,使用它可以立即停止一个线程。但注意它是一个被标注为废弃的方法

2.2.2 为什么不推荐使用stop()方法

  • stop() 方法会强行终止线程,如果把执行到一半的线程终止,可能会引起一些数据不一致的问题
  • stop() 方法在强行终止线程时,会立即释放这个线程所持有的锁,而这些锁或许恰恰是用来维持对象一致性的。如果线程写到一半,使用 stop() 将其强行终止,那么对象就会被写坏,同时由于锁被释放,另一个等待该锁的线程就会读到这个不一致的对象。

写线程获取锁,读线程等待,写线程本来想将ID和Name改为1,在将ID改为1后,由于使用了 stop() 方法,写线程被强行终止并被强行释放了锁,但此时Name还未写入;写线程释放锁之后,读线程就可以获取锁了,但是因为写线程写数据时并没有写入完整的数据,导致读线程读取的数据是有问题的(本来读取ID=1,Name=1;但现在读取的是ID=1,Name=0)

2.3 线程中断

2.3.1 线程中断

线程中断就是给线程发送一个通知,告知目标线程希望其退出。线程接收到通知后如何处理,有线程自己决定

2.3.2 与线程中断有关的方法

// 1、中断线程:这是一个实例方法
public void Thread.interrrupt()
// 2、判断线程是否被中断
public boolean Thread.isInterrupted()
// 3、判断线程是否被中断,并清除当前中断状态
public static boolean Thread.interrupted()
  • Thread.interrrupt() :这是一个实例方法,它通知目标线程中断,即设置中断标志位。(中断标志位表示当前线程已被中断)
  • Thread.isInterrupted() :这是一个实例方法,它通过检查中断标志位来判断目标线程是否被中断
  • Thread.interrupted() :这是一个静态方法,它可以判断当前线程的中断状态,但同时会清除当前线程的中断标志位

2.3.3 sleep()方法

public static native void sleep(long millis) throws InterruptedException;
  • Thread.sleep() 方法会让当前线程睡眠一段时间。当线程睡眠时,如果被中断,会抛出一个 InterruptedException 异常,这不是运行时异常,所以程序必须捕获并处理它。

2.3.4 sleep()由于中断而导致的问题

Thread.sleep() 方法由于中断而抛出异常时会清除中断标记。如果不对其进行处理,那么在下一次循环时,无法捕获这个中断。所以在异常处理中,要再次设置中断标记。

2.4 等待(wait)和通知(notify)

2.4.1 wait()、notify()和notifyAll()工作原理

注意:

  • wait() 方法和 notify() 方法在 Object 类中,而不是 Thread 类中,所以任何对象都可以调用这个方法。
  • notify() 方法选择线程是随机的,所以是不公平的
  • 如果一个线程调用了 object.wait() 方法,那么它就会进入 object 对象的等待队列(因为系统运行多个线程时可能会同时等待一个对象,所以这个等待队列可能会有多个线程)。
  • object.notify() 方法被调用时,对象就会从等待队列中随机选择一个线程,将其唤醒。
  • object.notifyAll() 方法被调用时,对象就会将等待队列中所有线程唤醒。

2.4.2 wait()方法的使用限制

必须要在对应的 synchronized 语句中

2.4.3 wait()和sleep()区别

  • 相同点: wait()sleep() 都可以让线程等待若干时间
  • 不同点:
  • wait() 方法可以被唤醒
  • wait() 方法释放锁,而 Thread.sleep() 方法不释放任何资源(包括锁)

2.5 挂起(suspend)和继续执行(resume)

线程挂起(suspend)和继续执行(resume)是一对相反的操作,被挂起的线程,必须要等到 resume() 方法操作后,才能继续执行。
但这两个方法已被标注为废弃的方法,不推荐使用

为什么不推荐使用suspend()

suspend() 在挂起线程的同时,线程不会释放任何锁资源。所以任何需要访问此锁的线程,都无法继续运行。只有对应线程上执行了 resume() 方法,被挂起的线程才可以继续执行,从而其它需要访问此锁的线程才能继续运行。

  • resume() 方法在suspend()方法前就执行了,那么被挂起的线程很难有机会继续执行。
  • 更严重的是,此线程被挂起后不释放锁,因此可能会导致整个系统工作不正常

2.6 等待结束(join)和谦让(yield)

2.6.1 join方法

有时候,一个线程的继续执行,可能依赖于另一个/多个线程的输出。此时,这个线程就需要等待依赖线程全部执行完毕,才能继续执行

  • public final void join() throws InterruptedException :表示无限等待,他会一直阻塞当前线程,直到目标线程执行完毕
  • public final synchronized void join(long millis) throws InterruptedException :设定一个最大等待时间,如果超过给定的时间目标线程仍未执行完毕,当前线程不会等待目标线程执行完毕,而是会直接继续运行

2.6.2 join方法的本质

  • join() 方法的本质是让调用线程wait()方法在当前线程对象实例上
  • 下面是 JDKjoin() 方法的核心代码
public final synchronized void join(long millis)
    while (isAlive()) {
        wait(0);
    }
}

它让调用线程在当前线程对象上进行等待。当线程执行完毕后,被等待的线程会在退出前调用 notifyAll() 方法通知所有等待的线程继续执行。

2.6.3 yield方法

  • public static native void yield() :它会使当前线程让出CPU

注意:
让出CPU不代表当前线程不执行。当前线程让出CPU后,还会继续争夺CPU,至于能否抢到,随缘。

三、volatile与Java内存模型(JMM)


  • volatile 对保证操作的原子性有很大帮助,但它不能代替锁,因为它无法保证一些复合操作的原子性,例如: i++
public class volatileTest {
    // 计数器
    private static volatile int i = 0;

    /**
     * 自增任务
     */
    public static class PlusTask implements Runnable {
        @Override
        public void run() {
            for (int j = 0; j < 10000; j++) {
                i++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] ts = new Thread[10];
        for (int j = 0; j < 10; j++) {
            ts[j] = new Thread(new PlusTask());
            ts[j].start();
        }
        for (int j = 0; j < 10; j++) {
            ts[j].join();
        }
        System.out.println(i);
    }
}
  • 如果 i++ 是原子性的,那么输出结果应该是100000
  • 但是多个线程同时对i进行写入时,其中一个线程的结果可能会覆盖另一个的,所以实际上每次输出的值都会小于100000

四、守护线程


4.1 守护线程

  • 守护线程是一种特殊的线程,在后台完成一些系统性的服务,比如:垃圾回收线程、JIT线程等

4.2 用户线程

  • 用户线程可以认为是系统的工作线程,他会完成相关的业务操作。

当Java应用内只有守护线程时,Java虚拟机就会退出
原因:如果用户线程全部结束,就意味着程序无事可做了;守护线程要守护的对象已经不存在了,那么整个应用程序就应该结束了

4.3 何时设置守护线程

  • 设置守护线程必须要在线程 start() 之前设置,否则设置守护线程会失败并且会得到一个异常。但是线程会被当作用户线程继续执行。

五、线程安全与Synchronized


5.1 volatile的问题

  • volatile 不能真正保证线程安全。它只能确保一个线程修改了数据后,其它线程能够看到这个改动;若多个线程同时修改同一个数据,依然会产生冲突
public class volatileTest {
    // 计数器
    private static volatile int i = 0;

    /**
     * 自增任务
     */
    public static class PlusTask implements Runnable {
        @Override
        public void run() {
            for (int j = 0; j < 10000; j++) {
                i++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] ts = new Thread[10];
        for (int j = 0; j < 10; j++) {
            ts[j] = new Thread(new PlusTask());
            ts[j].start();
        }
        for (int j = 0; j < 10; j++) {
            ts[j].join();
        }
        System.out.println(i);
    }
}
  • 如果i++是原子性的,那么输出结果应该是100000
  • 但是多个线程同时对i进行写入时,其中一个线程的结果可能会覆盖另一个的,所以实际上每次输出的值都会小于100000

5.2 synchronized的作用

  • 关键字 synchronized 的作用是线程间的同步。对需要同步的代码进行加锁,使得每一次只有一个线程进入同步代码块,从而保证线程间的安全性

5.3 synchronized的用法

  • 指定加锁对象:对给定对象加锁,进入同步代码块前要获得给定对象的锁
  • 直接作用于实例方法:相当于给当前对象进行加锁,进入同步代码块前要获得当前对象的锁
  • 直接作用于静态方法:相当于给当前类对象进行加锁,进入同步代码块前要获得当前类对象的锁