第4章| Java并发编程基础

线程简介

什么是线程

现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。

Java程序本身就是多线程的,可参见一下示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MultiThread {

public static void main(String[] args) {
// 获取Java线程管理MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不需要获取同步的monitor和synchronizer信息,仅仅获取线程和线程堆栈信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
// 遍历线程信息,仅打印线程ID和线程名称信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
}
}
}

输出结果:
[5] Monitor Ctrl-Break
[4] Signal Dispatcher
[3] Finalizer
[2] Reference Handler
[1] main

Process finished with exit code 0

为什么要使用多线程

  1. 更多地利用多处理器核心,将计算逻辑分配到多个处理器核心上,减少程序的处理时间,提高效率;
  2. 提高响应时间,将数据一致性不强的操作派发给其他线程处理(也可以使用消息队列),来加快线程处理用户请求的响应,缩短响应时间,提升用户体验;
  3. 更好的编程模型,Java为多线程编程提供了良好、考究并且一致的编程模型,使开发人员能更专注于解决问题。

线程优先级

  1. 线程分配到的时间片多少决定了线程使用处理器资源的多少,而线程的优先级就决定线程需要多或者少分配一些处理器资源的线程属性。

  2. 在Java线程中,通过整形变量priority控制优先级,范围由1~10,默认等级是5,优先级高的线程分配到的时间片数量多于优先级低的。

  3. 频繁阻塞(休眠或IO操作)的线程需要设置较高优先级,偏重计算(需要较多CPU时间或偏运算)的线程则设置较低优先级,确保处理器不被独占。

由于不同的JVM以及操作系统中的线程规划存在差异,例如Mac和Ubuntu都会忽略线程优先级的设定,所以不能将线程的优先级作为程序正确性的依赖

线程的状态

线程共有6中状态,如下表所示:

状态名 说明
NEW 初始状态,线程被构建,但是还没有调用start()方法
RUNNABLE 运行状态,Java线程将操作系统中的就绪和运行两种状态统称为“运行中”
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITING 等待状态,表示当前线程需要等待其他线程作出一些特定动作(通知或中断)
TIME_WATIING 超市等待状态,可以在指定时间内自行返回
TERMINATED 终止状态,表示线程已被执行完毕

就绪+时间片=执行

jstack使用步骤

  1. 运行代码;
  2. jps查看当前Java进行有哪些,并确定代码的pid;
  3. jstack pid;

线程的状态随着代码的执行在不断切换,Java线程状态变迁图如下所示:
Java线程状态变迁

Daemon线程

  1. 一种支持性线程,被用作程序中后台调度以及支持性工作。当一个Java虚拟机不存在Daemon线程时,Java虚拟机就会退出。在启动线程之前,可用Thread.setDaemon(true)将线程设置为Daemon线程。

  2. 由于在JVM退出时Daemon线程中的finally代码块不一定会被执行,所以不可依靠finally代码块中的内容来确保执行关闭或清理资源等逻辑。

启动和终止线程

构造线程

  1. 构造线程对象要明确所需属性,如线程所属的线程组、线程优先级,是否为Daemon线程等。

  2. 一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。

启动线程

start() 含义:当前线程(即parent线程)同步告诉Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。

tips:自定义线程最好有名称,方便jstack分析

理解中断

  1. 中断可理解为线程的一个标识位属性,表示一个线程是否被其他线程进行了interrupt()中断操作。
  2. Java中某些声明抛出InterruptedException的方法在抛InterruptedException之前,JVM会先将该线程的中断标识位清除,然后再抛出InterruptedException,此时调用isInterrupted()方法将会返回false。

过期的suspend()、resume()和stop()

不建议使用的原因:

  1. suspend()调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,容易引发死锁问题。
  2. stop()方法在终结一个线程时不会保证线程资源的正确释放,通常是没有给予线程完成释放工作的机会,会导致线程工作在不确定的状态下。

暂停和恢复操作可用等待和通知机制代替。

安全地终止线程

通过标识位或中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种方式显得更加安全和优雅。

线程间通信

volatile和synchronized关键字

现代多核处理器为了加快程序的执行,每个执行的线程不仅能从共享内存中获取变量,其自身也可以拥有一分拷贝的变量,所以一个线程在程序执行的时候获取到变量不一定是最新的。

  • volatile修饰字段(成员变量)是在告诉程序中的线程需要从共享内存中访问该变量,而对它的改变必须刷新回共享内存中,从而保证所有线程对该变量访问的可见性(不推荐过多使用volatile变量,因为过多的获取和写入将降低程序执行效率);
  • synchronzied修饰方法或以同步块方式使用,确保多个线程在同一时刻,只有一个线程能操作方法或同步块,保证了线程对变量的可见性与排他性。
  1. 可以用javap -v *.class命令来查看生成clas文件信息,分解class文件。进而可以发现同步块的实现使用了monitorentermonitorexit,而同步方法的实现是依靠修饰符上的ACC_SYNCHRONIZED完成的。这两种方式本质上都是获取对象的监视器,获取过程是排他的,即一个时刻只有一个线程能获取到synchronized所保护对象的监视器
  2. 对象、对象监视器、同步队列和执行线程间的关系如下:
    对象、对象监视器、同步队列和执行线程间的关系
    进入同步队列的线程处于阻塞状态!

等待/通知机制

  1. A线程修改了一个对象的值,而B线程感知到了变化,然后进行相应的操作。整个过程开始于A线程,当最终执行是B线程,这里A是生产者B是消费者。这种模式隔离了“做什么”和“怎么做”,在功能层实现了解耦,体系结构具备了良好的伸缩性。
  2. 等待/通知机制,指A线程调用O对象的wait()方法进入等待状态,而B线程调用notify()或notifyAll()方法后,A线程收到通知从O对象的wait()方法返回,进而执行后续操作。

wati()、notify()和notifyAll()方法使用细节:

  • wati()、notify()和notifyAll()方法需要先对调用对象加锁;
  • 调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列中;
  • notify()和notifyAll()方法调用后,等待线程不会立即从wait()方法返回,而是需要调用notify()和notifyAll()方法的线程释放锁,等待线程才有机会从wait()返回;
  • notify()/notifyAll()方法将等待队列中的一个/所有等待线程从等待队列移到同步队列,被移动线程的状态从WAITING变为BLOCKED;
  • 从wait()方法返回的前提是获得了调用对象的锁。

等待/通知的经典范式

  • 等待方遵循如下规则:
    1)获取对象的锁;
    2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件;
    3)条件满足则执行对应的逻辑

    1
    2
    3
    4
    5
    6
    7
    对应的伪代码:
    synchronized(对象){
    while(条件不满足){
    对象.wait();
    }
    执行对应的逻辑;
    }
  • 通知方遵循如下规则:
    1)获取对象的锁;
    2)改变条件;
    3)通知所有等待该对象的线程;

    1
    2
    3
    4
    5
    对应的伪代码:
    synchronized(对象){
    改变条件;
    对象.notifyAll();
    }

管道输入/输出流

  1. 明晰概念:管道输入/输出流主要用于线程之间的数据传输,传输的介质为内存,由4种具体实现:PipedOutputStream(字节)、PipedInputStream(字节)、PipedReader(字符)和PipedWriter(字符)。

  2. Piped流必须先进行绑定,即调用connect()方法,如果没有将输入/输入流绑定起来,访问该流将会抛出异常(实测,并没有抛出异常?!)。

Thread.join()的使用

现有两个线程A和B,如果在A线程中执行了B.join(),那么A线程需要等待B线程终止后才能从B.join()返回。

ThreadLocal的使用

  1. ThreadLocal即线程变量,以ThreadLocal对象为键、任意对象为值的存储结构。
  2. 优势:可以计算两个方法不在一个方法或类中时的前后间隔时间,比如在AOP中计算方法调用前到方法调用后的间隔时间。

线程应用实例

等待超时模式

设:①等待时长REMANINING=mills;②超时时间FUTURE=now+mills。

1
2
3
4
5
6
7
8
9
10
11
12
伪代码
// 当前对象加锁
public synchronized Object get(long mills) throw InterruptedException{
long remanining = mills;
long future = System.currentTimeMills()+mills;
// 当超时大于0并且result返回值不满足时
while(remanining>0 && result==null){
Thread.wait(remanining);
remaining=future - System.currentTimeMills();
}
return result;
}

一个简单的数据库连接池实例

手写简易数据库连接池

线程池技术及示例

一个基于线程技术的简单web服务器

  • 手写简易web服务器
码哥 wechat
欢迎关注个人订阅号:「码上行动GO」