java并发基础(1) 介绍了并发的理论基础,接下来继续看下并发的编程基础



并发编程基础

  • 1. 线程的生命周期
  • 2. 线程状态的转换
  • 2.1. runnable<=>blocked
  • 2.2. RUNNABLE <=> WAITING
  • 2.3. RUNNABLE 到 TERMINATED
  • stop() 方法
  • interrupt() 方法
  • 3. 使用线程的方式
  • 3.1. 实现 Runnable 接口
  • 3.2. 实现 Callable 接口
  • 3.3. 继承 Thread 类
  • 3.4 实现接口还是继承 Thread
  • 3.5. Executor
  • 4. 线程互斥同步
  • 4.1. synchronized
  • 4.2. ReentrantLock
  • 4.3. 使用哪一个


1. 线程的生命周期

java 分批次调用接口_jvm

生命周期

解释

1. 新建状态

通过new创建了线程对象

2. 就绪状态(可运行状态)

调用了start()方法,就进入就绪状态,等待jvm的线程调度器的调度

3. 运行状态

当获取了CPU资源就执行run()方法,就是运行状态

4. 阻塞状态



c. 调用sleep()或是join():未超时,进入阻塞,超时后进入就绪等待CPU调度。



5. 死亡状态


b.强制死亡:执行了stop;destroy



2. 线程状态的转换

2.1. runnable<=>blocked

两种状态的相互转换的场景,只有synchronized这一种场景。
没有获取synchronized的线程会处于blocked状态,获取了synchronized之后,就会处于Runnable,等待CPU的调度。

线程调用阻塞式 API 时,是否会转换到 BLOCKED 状态呢
不会。线程会阻塞,指的是操作系统线程的状态,并不是 Java 线程的状态。
 
JVM 层面并不关心操作系统调度相关的状态,在 JVM 看来,等待 CPU 使用权(操作系统层面此时处于可执行状态)与等待 I/O(操作系统层面此时处于休眠状态)没有区别,都是在等待某个资源,所以都归入了 RUNNABLE 状态。



2.2. RUNNABLE <=> WAITING

操作

解释

wait()


需其它线程唤醒(Object.notify/ Object.notifyAll),否则会一直等待堵塞。


Thread.sleep(mills)

线程调用 Thread.sleep(timeOut) 时,会堵塞自己。当到达timeOut时,会进入RUNNABLE。

join()

例如有一个线程A,当线程 B 调用 A.join() 时,B进入waiting状态,(线程 B )等待 A 执行完,A执行完之后,B切换到RUNNABLE状态。

LockSupport.park()



调用 LockSupport.unpark(Thread thread) 可唤醒目标线程,目标线程的状态又会从 WAITING 状态转换到 RUNNABLE。




2.3. RUNNABLE 到 TERMINATED

当程序执行完run,或者执行run内的方法抛出异常时,线程会变成TERMINATED。

可以通过stop() 和 interrupt()强制中断run的执行。

stop() 方法

会杀死线程,而不释放锁(调用unlock() )。如果线程持有 ReentrantLock 锁,执行stop之后,因为没有释放锁,其他线程再也获取不到ReentrantLock 锁。

interrupt() 方法

仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知。

被 interrupt 的线程,是怎么收到通知的呢?

当线程 A 处于 WAITING(调用了类似 wait/join/sleep)时,如果线程B调用线程 A 的 interrupt() 方法,会使线程 A 返回到 RUNNABLE状态,同时线程 A 的代码会触发 InterruptedException 异常。
 
当线程 A 处于 RUNNABLE 状态时,如果线程B调用A 的 interrupt() 方法,那么线程 A 可以通过 isInterrupted() 方法,检测是不是自己被中断了,拿到被中断的信号之后,可以忽视也可以做相应的操作。




3. 使用线程的方式

有三种使用线程的方法 : 实现 Runnable 接口、实现 Callable 接口、继承 Thread 类。

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

3.1. 实现 Runnable 接口

//需要实现 run() 方法。
public class MyRunnable implements Runnable {
    public void run() {
        // ...
    }
}
//通过 Thread 调用 start() 方法来启动线程。
public static void main(String[] args) {
    MyRunnable instance = new MyRunnable();
    Thread thread = new Thread(instance);
    thread.start();
}



3.2. 实现 Callable 接口

与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

public class MyCallable implements Callable<Integer> {
    public Integer call() {
        return 123;
    }
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}



3.3. 继承 Thread 类

也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。

当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。

public class MyThread extends Thread {
    public void run() {
        // ...
    }
}
public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();
}



3.4 实现接口还是继承 Thread

实现接口会更好一些:

  • Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
  • 类可能只要求可执行就行,继承整个Thread 类开销过大。




3.5. Executor

Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。

主要有三种 Executor:

  • CachedThreadPool: 一个任务创建一个线程;
  • FixedThreadPool: 所有任务只能使用固定大小的线程;
  • SingleThreadExecutor: 相当于大小为 1 的 FixedThreadPool

一般建议使用线程池的方式去创建线程

public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
        executorService.execute(new MyRunnable());
    }
    //会等待线程都执行完毕之后再关闭
    executorService.shutdown();
    //调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。
}

如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。

Future<?> future = executorService.submit(() -> {
    // ..
});
future.cancel(true);

4. 线程互斥同步

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

4.1. synchronized

可以同步一个代码块、方法、类。看一个例子:

public class SynchronizedExample {

    public void func1() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}

public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e1.func1());
}
//两个线程由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。
//0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9


//两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可以看出,两个线程交叉执行。
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e2.func1());
}
//0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9

4.2. ReentrantLock

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());
}


//0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

4.3. 使用哪一个

比较

解释

锁的实现

synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。

等待可中断


ReentrantLock 可中断,而 synchronized 不行。


公平锁


synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。


锁绑定多个条件

一个 ReentrantLock 可以同时绑定多个 Condition 对象。


除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。
 
因为 synchronized JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。
并且使用 synchronized不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

 
 
 
参考:
https://pdai.tech/md/java/thread/java-thread-x-overview.html