跳至主要內容

Java 多线程基础

Zenghr大约 7 分钟Java

Java 多线程基础

提示

本文用于理解Java多线程的基础以及实现方式

带着BAT大厂的面试题去理解

  • 线程有几种状态?
  • 线程有哪些实现方式?
  • 线程的常用方法?
  • 线程的同步方法有哪些?怎么选择
  • sleep 和 wait 有什么区别?

线程状态

mark

新生状态

使用 new 关键字建立一个线程后,该线程对象就处于新生状态,通过调用 start() 方法进入 就绪状态

就绪状态

处于就绪状态的线程具备了运行条件,但是还没有分配到 CPU,处于线程就绪队列,等待 CPU 调度,进入 运行状态

运行状态

在运行状态的线程执行自己的 run 方法中代码,如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态,等待下一次 CPU 的调度

处于运行状态的线程在某些情况下,如执行了 sleep (睡眠)、join 方法,或等待 I/O设备等资源,将让出 CPU 并暂时停止自己运行,进入阻塞状态。

阻塞状态

在阻塞状态的线程不能进入就绪队列,只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的 I/O 设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续执行

死亡状态

死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有三个,一个是正常运行的线程完成了它的全部工作;另一个是线程被强制性地终止,如通过 stop方法来终止一个线程【不推荐使用】;三是线程抛出未捕获的异常

线程使用

在 Java 中实现线程的方式有:

  • 继承 Thread
  • 实现 Runnable 接口

实现 Runnable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。

继承 Thread 类

继承 Thread 类实现多线程,重写 run 方法,因为 Thread 类也实现了 Runable 接口,当调用 start() 方法启动一个线程时,虚拟机会将该线程放入 就绪队列 中等待被调度,当一个线程被调度时会执行该线程的 run() 方法

public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("MyThread => " + i);
        }
    }
}
public class ThreadExample {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start(); // 开启多线程

        for (int i = 0; i < 10; i++) {
            System.out.println("main => " + i);
        }
    }
}

实现 Runnable 接口

通过 Runnable 接口实现多线程,必须实现 run 方法,通过 Thread 调用 start() 方法来启动线程。

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("MyRunnable => " + i);
        }
    }
}
public class RunnableExample {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("main => " + i);
        }
    }
}

线程常用方式

sleep()

Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。

sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

public void run() {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

wait()

wait(long timeout) ,参数是毫秒,timeout设置0,是无限等待的意思,如果没有notify,那么就会一直等待下去

调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。

它们都属于 Object 的一部分,而不属于 Thread

只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。

使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。

public class WaitNotify {
    private final static Object lock = new Object();

    public static void main(String[] args) {
        // 线程 A
        new Thread(() -> {
            System.out.println("线程 A 等待拿锁");
            synchronized (lock) {
                try {
                    System.out.println("线程 A 拿到锁了");
                    TimeUnit.SECONDS.sleep(1L);
                    System.out.println("线程 A 开始等待并放弃锁");
                    lock.wait();
                    System.out.println("被通知可以继续执行 则 继续运行至结束");
                } catch (InterruptedException e) {
                }
            }
        }, "线程 A").start();

        // 休眠 1秒 保证 线程 A 先获取 cpu 时间片,拿到锁
        try {
            TimeUnit.SECONDS.sleep(1L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 线程 B
        new Thread(() -> {
            System.out.println("线程 B 等待锁");
            synchronized (lock) {
                System.out.println("线程 B 拿到锁了");
                try {
                    TimeUnit.SECONDS.sleep(3L);
                } catch (InterruptedException e) {
                }
                lock.notify();
                System.out.println("线程 B 随机通知 Lock 对象的某个线程");
            }
        }, "线程 B").start();
    }
}
// 输出结果
线程 A 等待拿锁
线程 A 拿到锁了
线程 A 开始等待并放弃锁
线程 B 等待锁
线程 B 拿到锁了
线程 B 随机通知 Lock 对象的某个线程
被通知可以继续执行 则 继续运行至结束

yield()

对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

public void run() {
    Thread.yield();
}

Daemon

守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分,当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。

使用 setDaemon() 方法将一个线程设置为守护线程。

public static void main(String[] args) {
    Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);
}

join()

线程的强制执行方法,b.join() b线程强制执行,导致其他线程进入阻塞状态,当 b 线程执行结束后,其他线程阻塞原因解除,进入就绪态

public static void main(String[] args) {
    JoinThread ta = new JoinThread();
    ta.start();

    for (int i = 0; i < 10; i++) {
        if (i == 2) {
            try {
                // 使得 ta 线程强制执行
                ta.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("main => " + i);
    }
}

setPriority()

优先级越高,被 CPU 调动的可能性越大,但不一定是优先级越高就一定先执行

// 系统默认的三种优先级
System.out.println(Thread.MAX_PRIORITY);
System.out.println(Thread.MIN_PRIORITY);
System.out.println(Thread.NORM_PRIORITY);

public class PriorityExample {
    public static void main(String[] args) {
        PriorityThread pt1 = new PriorityThread("线程A");
        pt1.setPriority(Thread.MAX_PRIORITY);
        pt1.start();

        PriorityThread pt2 = new PriorityThread("线程B");
        pt2.setPriority(Thread.NORM_PRIORITY);
        pt2.start();
    }
}

线程互斥同步

Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized ,而另一个是 JDK 实现的 ReentrantLock

synchronized

1. 同步代码块

它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步

public void func() {
    synchronized (this) {
        // ...
    }
}

2. 同步方法

和同步代码的作用域一样,作用于同一个对象

public synchronized void func () {
    // ...
}

3. 同步类

作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。

public void func() {
    synchronized (SynchronizedExample.class) {
        // ...
    }
}

4. 同步静态方法

作用于整个类,和同步类的作用域一致

public synchronized static void fun() {
    // ...
}

ReentrantLock

ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。

public class LockExample {
    private Lock lock = new ReentrantLock();

    public void func() {
        lock.lock();
        try {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        } finally {
            lock.unlock(); // 确保释放锁,从而避免发生死锁。
        }
    }
}
public static void main(String[] args) {
    LockExample lockExample = new LockExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> lockExample.func());
    executorService.execute(() -> lockExample.func());
}