目 录CONTENT

文章目录

第四章 java并发编程基础

FatFish1
2024-10-22 / 0 评论 / 0 点赞 / 50 阅读 / 0 字 / 正在检测是否收录...

再看线程

线程是操作系统调度的最小单元,一个进程可以有多个线程。线程各自拥有计数器、堆栈、局部变量。

线程并发的实质是处理器在不同的线程间高速切换

线程优先级

线程优先级决定的是线程需要多或者少分配一些处理器资源(时间片)

可以通过如下逻辑设置优先级:

thread.setPriority(int p)

默认优先级是5,优先级高的线程分配时间片更多

设置优先级时,针对频繁阻塞(休眠或I/O操作多)的需要设置较高的优先级,偏重计算(需要较多CPU时间或者偏运算)的线程设置较低优先级,确保处理器不会被独占

注意:程序的正确性不依赖线程优先级

线程状态

复习线程六种状态NEW、RUNNABLE、WAIT、TIME_WAIT、BLOCKED、TERMINATED,跳转线程Thread部分

Daemon线程

Daemon线程用于完成支持性工作。

在Java虚拟机退出时Daemon线程中的finally块不一定执行

线程的构造、启动、中断、终止

构造线程

构造线程核心在于调用init方法,具体可以参考thread部分init源码分析

启动线程

执行thread.start()方法可以启动线程。

调用序列是start方法调用start0本地方法,进而调用到run方法,可以参考thread部分start源码分析

理解中断

与中断相关的三个方法:

thread.interrupt();   // 执行线程中断
thread.isInterrupt();   // 判断当前线程中断标志位是否是中断状态
Thread.interrupted();   // 复位中断标志位

首先执行线程中断的本质是什么?

thread.interrupt()的本质是把线程的中断标志位设置为中断状态,并不是真让线程立刻停止

设置中断标志位有什么用?

可以让一些能够感知中断标志位的线程结束。例如以下三个可执行对象:

class test1 implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("hi");
        }
    }
}

class test2 implements Runnable {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            System.out.println("hi");
        }
    }
}

class test3 implements Runnable {
    @Override
    public void run() {
        while (true) {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                System.out.println("over");
            }
        }
    }
}

test1的代码只是无限循环,很明显并不判断中断标志位,因此使用thread.interrupt()对test1所属的线程没任何卵用;

test2的代码显示判断了中断标志位,因此执行thread.interrupt()后,thread2所属的线程跳出循环,执行完毕,进入TERMINATED状态

test3的代码没有显示判断中断标志位,但是sleep方法本质上调用了native的sleep方法,其实也是在判断中断标志位,当发现中断标志位变成中断,抛出InterruptedException,这时test3所属线程进入的是TIME_WAITED状态。native方法sleep的部分逻辑如下:

if (os::sleep(thread, millis, true) == OS_INTRPT) {
      // 如果睡眠期间被中断,那么抛出中断异常。
      THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
}

总结下来:只要能感知中断标志位,就可以完成线程的中断,无论中断后进入什么状态,起码是中断了的

暂停suspend、继续resume、终止stop

thread.suspend()、thread.resume()、thread.stop()三个方法分别对应线程暂停、继续、终止。

但这些API已经被标记为弃用了,原因如下:

  • suspend方法会导致线程带着已有资源进入睡眠状态,可能导致死锁发生

  • stop方法在终止线程时不会保证线程资源释放,可能导致程序工作在某种不确定状态下

线程的暂停和恢复操作后续使用等待/通知机制来替代

安全地终止线程

尽量使用thread.interrupt并在可执行对象中显示判断中断标志位来进行线程的安全终止。

也可以通过设置独立的标志位,通过增加特殊的判断方法进行循环跳出和终结。

线程间的通信

多线程相互协作,必然需要通信

方案一、volatile、synchronized关键字

分析过volatile和synchronized的原理。

volatile本质是让修改共享变量的线程在修改之后向后续读取的线程发送一个消息,让读取共享变量的线程在读取之前首先去主内存中同步变量值。

synchronized的本质是通过在对应同步块中增加monitorenter和monitoerexit指令,或在方法修饰符增加ACC_SYNCHRONIZED,其实都是增加了一个申请和释放对象监视器锁实现的同步,这个锁的获取过程是排他的,同一时刻只有一个线程可以获取由synchronized所保护的对象的监视器

方案二、等待/通知机制

为什么需要等待/通知机制

想实现一个线程等待另一个线程的信号,可以使用两种方案:

  • 消费线程通过while循环判断共享变量,不符合条件,执行sleep

  • 使用等待/通知机制

方案1比较简单多,但是频繁的睡眠可能产生问题,比如,睡眠时间不好控制,太久可能影响处理及时性,太短可能产生频繁判断的开销。等待/通知机制就比较优雅地解决这个问题。

等待/通知机制相关方法和模板

相关的方法包括:

// 通知是让一个在共享对象上等待的线程,使其从wait()方法返回到获取锁的状态中返回,
WAIT -> BLOCKED,如果获取到锁,可以继续执行后续代码,BLOCKED -> RUNNING
object.notify()
object.notifyAll()

// 等待是让一个线程在一个共享变量上暂停,进入WAIT状态,调用后会释放锁
object.wait()
object.wait(long)  // 有条件的等待,超时会自动返回,不需要通知,但是也要竞争锁
object.wait(long, int)  // 更细粒度地控制超时时间,可以达到纳秒

等待通知机制的使用模板:

// 等待方
synchronized(对象) {
while(条件不满足) {
对象.wait();
}
对应的处理逻辑
}     

// 通知方
synchronized(对象) {
改变条件
对象.notifyAll();
}

案例分析

static final Object lock = new Object();
static boolean flag = true;

static class Wait implements Runnable {
    public void run() {
        synchronized (lock) {
            while (flag) {
                try {
                    System.out.println(Thread.currentThread() + " step1: flag is true.
                    lock.wait();
                } catch (InterruptedException e) {
                }
            }
            System.out.println(Thread.currentThread() + " step2: flag is false. runnin
        }
    }
}
static class Notify implements Runnable {
    public void run() {
        synchronized (lock) {
            System.out.println(Thread.currentThread() + " step3: hold lock. notify ");
            lock.notifyAll();
            flag = false;
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        // 通知方再次加锁
        synchronized (lock) {
            System.out.println(Thread.currentThread() + "step4: hold lock again. ");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

// 执行
public static void main(String[] args) throws Exception {
Thread waitThread = new Thread(new Wait(), "WaitThread");
waitThread.start();
TimeUnit.SECONDS.sleep(1);
Thread notifyThread = new Thread(new Notify(), "NotifyThread");
notifyThread.start();
}

执行流程分析:

  1. 首先waitThread.start(),waitThread获取到lock的锁,同时释放锁,执行lock.wait()进入等待

  2. sleep1秒,notifyThread.start(),notifyThread获取到lock的锁,执行lock.notifyAll(),同时修改flag为false,这时锁没释放,waitThread拿不到锁,进入BLOCKED状态,然后notifyThread执行sleep5

  3. 5s后,notifyThread第一个同步块执行完毕,释放锁,开始第二个同步块,这时会跟waitThread一起竞争锁,谁竞争到,谁继续执行

因此这块的执行结果可能是:

step1 -> step3 -> step4 -> step2或step1 -> step3 -> step2 -> step4

这是因为step2和step4

总结

  • 使用notify()、wait()系列方法首先要对调用的共享对象加锁

  • 调用wait()方法后,线程状态由RUNNING变成WAITING,并将当前线程放置到对象等待队列

  • notify()、notifyAll()方法调用后,等待线程不会离开从WAITING状态返回,需要调用notify()或notifyAll()的线程释放锁,拿到锁才能变成RUNNING状态返回,拿不到锁变成BLOCKED状态

方案三、管道输入/输出流

管道输入/输出流也是流的一种,不同于文件流或网络流,它用于线程之间传递数据,传输媒介是内存。

实现包括:

  • 面向字节的:PipedOutputStream、PipedInputStream

  • 面向字符的:PipedReader、PipedWriter

案例:

static class Print implements Runnable {
    private PipedReader in;
    public Print(PipedReader in) {
        this.in = in;
    }
    public void run() {
        int receive = 0;
        try {
            while ((receive = in.read()) != -1) {
                System.out.print((char) receive);
            }
        } catch (IOException ex) {
        }
    }
}

public static void main(String[] args) throws Exception{
    PipedWriter out = new PipedWriter();
    PipedReader in = new PipedReader();
    // 绑定输入/输出流
    out.connect(in);
    Thread printThread = new Thread(new Print(in), "PrintThread");
    printThread.start();
    int receive = 0;
    try {
        // 通过System.in接收传入字符
        while ((receive = System.in.read()) != -1) {
            // 有传入字符时,由输出流写入到输入流,跨线程通信
            out.write(receive);
        }
    } finally {
        out.close();
    }
}

通过System.in接受console传递的字符,通过管道流传递给其他线程

回顾下字节流和字符流:字节流单位是二进制,一次一个byte(8bit),xxStream就是字节流;字符流单位是unicode码,一次2个byte(16bit),xxReader、xxWriter是字符流

方案四、Thread.join()

thread.join()
thread.join(long millis)
thread.join(long millis, int nanos)

如果线程A中执行threadB.join(),线程A会阻塞在join()方法处,直到threadB线程终止后才返回

案例如下:

static class Domino implements Runnable {
    private Thread thread;
    public Domino(Thread thread) {
        this.thread = thread;
    }
    public void run() {
        try {
            thread.join();
        } catch (InterruptedException e) {
        }
        System.out.println(Thread.currentThread().getName() + " terminate.");
    }
}

public static void main(String[] args) throws Exception{
    Thread previous = Thread.currentThread();
    for (int i = 0; i < 10; i++) {
        // 每个线程拥有前一个线程的引用,需要等待前一个线程终止,才能从等待中返回
        Thread thread = new Thread(new Domino(previous), String.valueOf(i));
        thread.start();
        previous = thread;
    }
    TimeUnit.SECONDS.sleep(5);
    System.out.println(Thread.currentThread().getName() + " terminate.");
}

这个案例中,通过循环构造了10个线程,依次持有前一个线程的引用,第0个线程持有主线程的引用,依次join前者,只有主线程结束,后面的线程才会依次结束。

注意:如果threadB先结束,threadA再执行threadB.join(),则会立即返回不阻塞

方案五、threadLocal

通过threadLocal全局变量,在不同的线程中设置,可以得到不同的结果

一般threadLocal变量使用static final进行设置

参考threadLocal部分

InheritableThreadLocal是一种特殊的可继承的threadLocal,子线程将直接继承父线程的threadLocal值,案例如下:

static final InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) throws Exception{
    threadLocal.set(1);
    Thread t = new Thread(new Domino(), "d");
    t.start();
    TimeUnit.SECONDS.sleep(2);
}
static class Domino implements Runnable {
    @Override
    public void run() {
        Integer i = threadLocal.get();
        System.out.println(i);
    }
}

案例中输出1,如果用普通的,输出就是null

0

评论区