Java 多线程基础
Java 多线程基础
提示
本文用于理解Java多线程的基础以及实现方式
带着BAT大厂的面试题去理解
- 线程有几种状态?
- 线程有哪些实现方式?
- 线程的常用方法?
- 线程的同步方法有哪些?怎么选择
- sleep 和 wait 有什么区别?
线程状态
新生状态
使用 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());
}