4. Java并发编程基础
4.1 线程简介
4.1.1 什么是线程
现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。
public class MultiThread { public static void main(String[] args) { ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); // 不需要获取同步的monitor和synchronizer信息,仅获取线程和线程堆栈信息 ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false,false); // 遍历线程信息,仅打印线程ID和线程名称信息 for (ThreadInfo thread: threadInfos) { System.out.println("["+thread.getThreadId()+"] "+thread.getThreadName()); } } }
4.1.2 为什么要使用多线程
(1)更多的处理器核心
(2)更快的响应时间
(3)更好的编程模型
4.1.3 线程优先级
在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。
线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定。
4.1.4 线程的状态
Java线程在运行的生命周期中可能有6种不同的状态
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还没有调用start()方法 |
RUNNABLE | 运行状态 |
BLOCKED | 阻塞状态,线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程作出一些特定动作(通知或中断) |
TIME_WAITING | 超时等待状态,不同于WAITING,可在指定的时间自行返回 |
TERMINATED | 终止状态 |
-
注意:Java将操作系统中的运行和就绪两个状态合并称为运行状态。阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态,因为java.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。
4.1.5 Daemon线程(守护线程)
Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。
在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
4.2 启动和终止线程
4.2.1 构造线程
/** * Initializes a Thread. * * @param g the Thread group * @param target the object whose run() method gets called * @param name the name of the new Thread * @param stackSize the desired stack size for the new thread, or * zero to indicate that this parameter is to be ignored. * @param acc the AccessControlContext to inherit, or * AccessController.getContext() if null * @param inheritThreadLocals if {@code true}, inherit initial values for * inheritable thread-locals from the constructing thread */ private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { if (name == null) { throw new NullPointerException("name cannot be null"); } this.name = name; Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); if (g == null) { /* Determine if it's an applet or not */ /* If there is a security manager, ask the security manager what to do. */ if (security != null) { g = security.getThreadGroup(); } /* If the security doesn't have a strong opinion of the matter use the parent thread group. */ if (g == null) { g = parent.getThreadGroup(); } } /* checkAccess regardless of whether or not threadgroup is explicitly passed in. */ g.checkAccess(); /* * Do we have the required permissions? */ if (security != null) { if (isCCLOverridden(getClass())) { security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } } g.addUnstarted(); this.group = g; // 将daemon、priority属性设置为父线程的对应属性 this.daemon = parent.isDaemon(); this.priority = parent.getPriority(); if (security == null || isCCLOverridden(parent.getClass())) this.contextClassLoader = parent.getContextClassLoader(); else this.contextClassLoader = parent.contextClassLoader; this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext(); this.target = target; setPriority(priority); // 将父线程的InheritableThreadLocal复制过来 if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; // 分配一个线程ID /* Set thread ID */ tid = nextThreadID(); }
一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。至此,一个能够运行的线程对象就初始化好了,在堆内存中等待着运行。
4.2.2 启动线程
线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。
4.2.3 理解中断
中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。
如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返 回false。
4.2.4 安全地终止线程
main线程通过中断操作和cancel()方法均可使CountThread得以终止。这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。
interrupt()方法用途
Thread.interrupt()方法不会中断一个正在运行的线程。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞 ,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。
4.3 线程间通信
4.3.1 volatile和synchronized关键字
关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。
4.3.2 等待/通知机制
1)使用wait()、notify()和notifyAll()时需要先对调用对象加锁。
2)调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。
3)notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。
4)notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED。
5)从wait()方法返回的前提是获得了调用对象的锁。
4.3.3 等待/通知的经典范式
等待方遵循如下原则。 1)获取对象的锁。 2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。 3)条件满足则执行对应的逻辑。
synchronized(对象) { while(条件不满足) { 对象.wait(); } 对应的处理逻辑 }
通知方遵循如下原则。 1)获得对象的锁。 2)改变条件。 3)通知所有等待在对象上的线程。
synchronized(对象) { 改变条件 对象.notifyAll(); }
4.3.4 管道输入/输出流
管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。
4.3.5 Thread.join()的使用
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时 时间里没有终止,那么将会从该超时方法中返回。
这个方法存在的意义是什么?
1、等待处理结果
为什么要用join()方法在很多情况下,主线程生成并启动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。当只有一个主线程和一个子线程的情况这个说法似乎站不住脚,那就往↓↓看
2、未捕获的异常
java多线程程序中所有线程都不允许抛出未捕获的checked exception,也就是说各个线程需要自己把自己的checked exception处理掉。但是无法避免的是unchecked exception,也就是RuntimeException,当抛出异常时子线程会结束,但不会影响主线程。难免会有 没有处理到的异常情况,如果放到一个方法(函数)中执行,一旦出现则会导致主线程崩溃,而放到子线程中去执行则不会受到影响。
3、等待指定时间
t.join(1000); //等待 t 线程,等待时间是1000毫秒
4.3.6 ThreadLocal的使用
定义:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。
作用:提供线程内的局部变量,不同线程间不会相互干扰,这种变量在线程的生命周期内起作用。
ThreadLocal和Synchronized的对比
Synchronized会使程序无法进行并发执行,需要等待前一个线程完成释放锁。同步机制采用“以时间换空间”,侧重于多个线程之间访问资源的同步。
ThreadLocal采用“以空间换时间”的方式,为每一个线程都提供了一份变量副本,从而实现同时访问而互不干扰。多线程之间键的数据仍是相互隔离的。我觉得很像JVM中为线程分配空间时采用的本地线程分配缓冲(TLAB)的一个思想。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
ThreadLocal能使程序拥有更高的并发性。
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说,一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。
JDK8中的ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap,这个map的key是ThreadLocal实例本身,value才是真正要存储的值。
具体过程如下:
(1)每个Thread线程内部都有一个ThreadLocalMap
(2)Map中存储ThreadLocal对象(key)和线程的变量副本(value)
(3)Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
(4)对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
这样设计的优点:
-
每个Map存储的Entry数量变少。
-
当Thread销毁时,
ThreadLocalMap
也会随之销毁,减少内存的使用
ThreadLocal的内存泄漏问题
Entry中的key是弱引用。因为ThreadLocalMap是Thread的一个属性,被当前线程引用,所以ThreadLocalMap的生命周期与Thread相同。Key被回收=null,而value存在强引用链,key为空则永远访问不到,则内存泄漏
出现内存泄漏的真实原因
(1)没有手动删除Entry
(2)CurrentThread仍然在运行
为什么用弱引用?
在ThreadLocalMap中的set/getEntry方法中,会对key为null进行判断,如果为null的话,会对value设置为null。弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。
ThreadLocal的使用场景:
在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。在下面会例举几个场景。