java并发基础 (2) -编程基础:1. 线程生命周期、2. 实现线程的五种编程方式、3. 实现线程安全方式

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


1. 线程的生命周期

在这里插入图片描述

生命周期解释
1. 新建状态
通过new创建了线程对象
2. 就绪状态(可运行状态)
调用了start()方法,就进入就绪状态,等待jvm的线程调度器的调度
3. 运行状态
当获取了CPU资源就执行run()方法,就是运行状态
4. 阻塞状态
a. 等待阻塞:执行了wait(),进入等待池
b. 同步阻塞:获取synchronized失败,进入到锁池中
c. 调用sleep()或是join():未超时,进入阻塞,超时后进入就绪等待CPU调度。
5. 死亡状态
a. run()执行结束,或异常退出后的状态,此状态不可逆转。
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()
当获取synchronized的线程,调用 Object.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()
java 并发包中的锁,都是基于它实现的。
调用 LockSupport.park() 方法,当前线程会阻塞,线程的状态会从 RUNNABLE 转换到 WAITING。
调用 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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

roman_日积跬步-终至千里

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值