Java并发编程的艺术(四)

简介

本系列为《Java并发编程的艺术》读书笔记。在原本的内容的基础上加入了自己的理解和笔记,欢迎交流!

chapter 4 Java并发编程基础

1. 线程基础知识

一个进程包括多个线程,进程是资源调度的基本单位,而线程的任务调度的基本单位,因为多个线程本质上是操作进程拿到的资源。

一个最简单的helloworld程序会创建四个线程:

  1. Signal Dispatcher
  2. Finalizer
  3. Reference Headler
  4. main

多线程的好处是什么呢?

  1. 现代处理器的性能提升从高主频转向多核心。一个线程一个时刻只能运行在一个处理器核心上。多个线程可以同时在多个核心上运行。
  2. 可以将一致性不强的操作并行化处理。
线程状态 说明
New 线程初始化阶段
Runnable 运行状态和就绪状态,即线程获取到了CPU的时间片,开始运行,随着时间片到期,进入就绪状态,等待获取下一个时间片
Blocked 线程阻塞
waiting 等待状态会终止当前线程的运行,并等待其他线程将等待线程唤醒。
time-waiting 超时等待,会在一定时间后自我唤醒
Terminated 线程结束

书中也总结一张很完整的图:
在这里插入图片描述

2. Java如何启动和终止线程

2.1 初始化

线程在运行之前需要构造一个线程对象,会调用一个init方法。一个线程的创建的父线程就是创建该线程对象的线程。比如在main方法中,我运行了ABC线程,ABC线程的父线程就是main方法。

新的线程是由父线程进行空间和资源分配的,子线程会继承父线程的 isDaemonPriorityContextClassLoader和可用的ThreadLocal
省去了一部分代码,如下:

private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
    
    
		...
        this.name = name;
	    // 父线程就是当前的线程,比如main线程
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        ...
        ...
        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        ...
        ...
        setPriority(priority);
		// 设置父线程相关信息
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

2.2 启动线程

线程初始化完成后,调用start方法就可以启动线程。任何线程都是由其父线程创建的。Main线程是由JVM创建的,所以任何程序的执行都必须有main方法。当在父线程中调用了start方法,父线程就会告知JVM,如果线程规划器空闲,就应该立即调用当前的线程。
程序中断是父线程修改了子线程的标志位,表示中断的子线程应该终止运行,可以使用isInterrupted()方法判断线程是否被中断了。中断的方式有:

  1. threadName.interrupt(); 在父线程中调用,中断子线程;
  2. Thread.interrupted(); 线程自我中断。

值得注意的是:对于某些方法会抛出interruptedException,那么此时在抛出异常之前,JVM会首先将中断标志位复位,所以在调用isInterrupted方法时,返回结果是false

针对上面的问题,需要在捕捉异常处添加一个Thread.currentThread().interrupt();来确保线程的中断位置为true。

2.3 终止线程

线程的终止可以通过修改中断标志位来达到。其次,可以通过添加一个boolean类型的共享变量来控制程序终止。

3. 线程间的通信

首先明确一点:线程间的通信就是线程之间如何交换信息,这是一个概念,具体实现方案有:消息传递和共享变量,Java采用的是共享变量的方式

3.1 同步:volatile和synchronized关键字

volatilesynchronized是用来同步的关键字,会使得线程进入blocked状态,被阻塞的线程会进入同步队列。进入同步队列的线程会按照策略,等待分配时间片。

关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性
关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性

前面提到过的Monitor对象和synchronized的关系。每个对象都有自己的Monitor对象,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED 状态。

3.2 等待:wait和notify/notifyAll

相关方法如下:
在这里插入图片描述

waitnotify/notifyAll是用来等待和唤醒的方法,会使得线程进入wait状态,等待的线程会被放入等待队列中,并等待被唤醒。被唤醒的线程会进入blocked状态,被放入同步队列中。
书中给出了一段代码,并配图进行了说明。我大致解释一下代码,就不贴出来了:

  1. 定义了两个静态内部类Wait和Notify并实现了Runnable接口,外部类中定义了一个Object对象,用作synchronized对象锁
  2. Wait线程在run()方法中调用了lock.wait(),进入等待状态;
  3. Notify线程在run()方法中调用了lock.notify(),唤醒了等待lock对象锁的线程。

整体流程如下:
在这里插入图片描述

3.4 等待通知同步设计范式

等待方遵循如下原则:

  1. 获取对象的锁;
  2. 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件(循环检查)。
  3. 条件满足则执行对应的逻辑。
	synchronized(对象) {
    
     
		while(条件不满足) {
    
     
			对象.wait();
		} 
		// 对应的处理逻辑 
	}

通知方遵循如下原则:
1. 获得对象的锁。
2. 改变条件。
3. 通知所有等待在对象上的线程。

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

3.5 管道:

管道分成PipedOutputStream、PipedInputStream、PipedRreader、PipedWriter四种流。主要用于线程间的数据传输,其存储位置为内存。

PipedWriter out = new PipedWriter(); 
PipedReader in = new PipedReader(); 
// 将输出流和输入流进行连接,否则在使用时会抛出IOException
out.connect(in);

猜你喜欢

转载自blog.csdn.net/qq_38684427/article/details/118155247
今日推荐