【Java 并发笔记】并发基础整理
文前说明
作为码农中的一员,需要不断的学习,我工作之余将少量分析总结和学习笔记写成博客与大家一起交流,也希望采用这种方式记录自己的学习之旅。
本文仅供学习交流使用,侵权必删。
不用于商业目的,转载请注明出处。
1. 基本概念
1.1 进程
- 有独立的代码和数据空间(进程上下文)。
- 进程间的切换会有较大的开销。
- 一个进程包含 1~n 个线程。
- 进程是资源分配的最小单位。
- 多进程是指操作系统能同时运行多个任务(程序)。
1.2 线程
- 同一类线程共享代码和数据空间。
- 每个线程有独立的运行栈和程序计数器(PC)。
- 线程切换开销小。
- 线程是 CPU 调度的最小单位。
- 多线程是指在同一程序中有多个顺序流在执行,只能使用分配给程序的资源和环境。
- 线程和进程一样分为五个阶段:创立、就绪、运行、阻塞、终止。
- 线程是进程中负责程序执行的执行单元,线程本身依靠程序进行运行。
1.2.1 进程与线程的不同
- 一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者者一个应用。
- 线程是在进程中执行的一个任务。
- Java 运行环境是一个包含了不同的类和程序的单一进程。
- 线程可以被称为轻量级进程。
- 线程需要较少的资源来创立和驻留在进程中,并且可以共享进程中的资源。
1.2.2 多线程的好处
- 在多线程程序中,多个线程被并发的执行以提高程序的效率,CPU 不会由于某个线程需要等待资源而进入空闲状态。
- 多个线程共享堆内存(heap memory),因而创立多个线程去执行少量任务会比创立多个进程更好。
1.2.3 客户线程和守护线程区别
- 在 Java 程序中创立一个线程,它就被称为客户线程。
- 一个守护线程是在后端执行并且不会阻止 JVM 终止的线程。
- 当没有客户线程在运行的时候,JVM 关闭程序并且退出。
- 一个守护线程创立的子线程仍然是守护线程。
- 使用 Thread 类的 setDaemon(true) 方法可以将线程设置为守护线程,需要注意的是,需要在调用 start() 方法前调用这个方法,否则会抛出 IllegalThreadStateException 异常。
1.3 并行
- 多个 CPU 实例或者者多台机器同时执行一段解决逻辑,是真正的同时。
1.4 并发
- 通过 CPU 调度算法,让客户看上去同时执行,实际上从 CPU 操作层面不是真正的同时。
- 并发往往在场景中有公用的资源,针对这个公用的资源往往产生瓶颈,会用 TPS 或者者 QPS 来反应这个系统的解决能力。
1.5 线程安全
- 在并发的情况之下,代码经过多线程使用,线程的调度顺序不影响任何结果。
- 使用多线程,只要要关注系统的内存,CPU 是不是够用就可。
- 线程不安全就意味着线程的调度顺序会影响最终结果。
- 程序的正确性不能依赖线程的优先级高级来保证。
1.6 同步
- 通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的精确。
1.7 线程生命周期
| 状态名称 | 说明 |
|---|---|
| NEW | 初始状态,线程被构建,但是还没有调用 start() 方法。 |
| RUNNABLE | 运行状态,Java 线程将操作系统中的就绪(RUNNABLE)和运行(RUNNING)两种状态抽象称作运行中。 |
| BLOCKED | 阻塞状态,表示线程阻塞于锁。 |
| WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其余线程做出少量特定动作(通知或者中断)。 |
| DEAD | 终止状态,表示当前线程已经执行完毕。 |
- 当需要新起一个线程来执行某个子任务时,就创立一个线程。但是线程创立之后,不会立即进入就绪状态,由于线程的运行需要少量条件(比方分配肯定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态。
- 当线程进入就绪状态后,不代表立刻就能获取 CPU 执行时间,也许此时 CPU 正在执行其余的事情,因而它要等待。当得到 CPU 执行时间之后,线程便真正进入运行状态。
- 线程在运行状态过程中,可能有多个起因导致当前线程不继续运行下去,比方客户主动让线程睡眠(睡眠肯定的时间之后再重新执行)、客户主动让线程等待,或者者被同步块给阻塞,此时就对应着多个状态 time waiting(睡眠或者等待肯定的时间)、waiting(等待被唤醒)、blocked(阻塞)。
- 当因为忽然中断或者者子任务执行完毕,线程就会被消亡。
线程状态转换
1.7.1 上下文切换
- 对于单核 CPU 来说,CPU 在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是相似)。
- 因为可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。
- 对于线程的上下文切换实际上就是 存储和恢复 CPU 状态的过程,它使得线程执行能够从中断点恢复执行。
- 尽管多线程可以使得任务执行的效率得到提升,但是因为在线程切换时同样会带来肯定的开销代价,并且多个线程会导致系统资源占用的添加。
1.7.2 线程优先级
- 在操作系统中,线程可以划分优先级,优先级较高的线程得到的 CPU 资源较多,也就是 CPU 优先执行优先级较高的线程对象中的任务。
- 设置线程优先级有助于帮 线程调度器 确定在下一次选择哪一个线程来优先执行。
- 线程的优先级分为 1~10 这 10 个等级,假如小于 1 或者大于 10,则抛出异常 IllegalArgumentException。
- 线程优先级特性
- 比方 A 线程启动 B 线程,则 B 线程的优先级与 A 是一样的。(继承性)
- 高优先级的线程总是大部分先执行完,但不代表高优先级线程一律先执行完。(规则性)
- 优先级较高的线程不肯定每一次都先执行完。(随机性)
1.7.3 线程调度器(Thread Scheduler)和时间分片(Time Slicing)
- 线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。
- 一旦创立一个线程并启动它,它的执行便依赖于线程调度器的实现。
- 时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。
- 分配 CPU 时间可以基于线程优先级或者者线程等待的时间。
- 线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让程序依赖于线程的优先级)。
1.8 线程常用方法
| 方法 | 说明 |
|---|---|
| public void start() | 使该线程开始执行;Java 虚拟机调用该线程的 run() 方法。 |
| public void run() | 假如该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run() 方法;否则,该方法不执行任何操作并返回。 |
| public final void setName(String name) | 改变线程名称,使之与参数 name 相同。 |
| public final void setPriority(int priority) | 更改线程的优先级。 |
| public final void setDaemon(boolean on) | 将该线程标记为守护线程或者客户线程。 |
| public final void join(long millisec) | 等待该线程终止的时间最长为 millis 毫秒。 |
| public void interrupt() | 中断线程。 |
| public final boolean isAlive() | 测试线程能否处于活动状态。 |
| public static void yield() | 线程退让,暂停当前正在执行的线程对象,并执行其余线程。 |
| public static void sleep(long millisec) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和精确性的影响。 |
| public static Thread currentThread() | 返回对当前正在执行的线程对象的引用。 |
| public static void join() | 线程加入,等待其余线程终止。 |
1.8.1 sleep()、yield() 和 wait() 的区别
| / | sleep | yield | wait |
|---|---|---|---|
| 方法所在类 | Thread | Thread | Object |
| 锁行为 | 不会改变锁行为 | 不会改变锁行为 | 释放锁 |
| 进入状态 | 进入阻塞状态 | 进入就绪状态 | 进入等待状态 |
| 恢复 | 指定时间后恢复 | 与相同优先级线程争夺 | 等待别的线程 notify/notifyAll 唤醒 |
- sleep 是 Thread 类的方法。
- wait 是 Object 类中定义的方法。
- Thread.sleep 不会导致 锁行为的改变,假如当前线程是拥有锁的,那么 Thread.sleep 不会让线程释放锁。
- Thread.sleep 和 Object.wait 都会暂停当前的线程。OS 会将执行时间分配给其它线程。区别在于调用 wait 后,需要别的线程执行 notify/notifyAll 才能够重新取得 CPU 执行时间。
- 调用 Thread.yield 方法会让当前线程交出 CPU 权限,让 CPU 去执行其余的线程。它跟 sleep 方法相似,同样不会释放锁。
- 但是 yield 不能控制具体的交出 CPU 的时间。
- yield 方法只能让拥有相同优先级的线程有获取 CPU 执行时间的机会。
- 调用 yield 方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只要要等待重新获取 CPU 执行时间,这一点是和 sleep 方法不一样的。
- Thread 类的 sleep 和 yield 方法将在当前正在执行的线程上运行。
- 所以在其余处于等待状态的线程上调用这些方法是没有意义的。
- 所以这两个方法都是静态的。
1.8.2 join()
- 在很多情况下,主线程创立并启动了线程,假如子线程中要进行大量耗时运算,主线程往往将早于子线程结束之前结束。这时,假如主线程想等待子线程执行完成之后再结束,比方子线程解决一个数据,主线程要获得这个数据中的值,就要用到 join() 方法了。
- 方法 join() 的作用是让主线程等待调用它的线程结束。
1.8.3 阻塞的三种情况
- 等待阻塞:运行的线程执行 wait() 方法,JVM 会把该线程放入等待池中。(wait 会释放持有的锁)
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池中。
- 其余阻塞:运行的线程执行 sleep() 或者 join() 方法,或者者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep() 状态超时、join() 等待线程终止或者者超时、或者者 I/O 解决完毕时,线程重新转入就绪状态。
1.8.4 中止线程
- 使用退出标志,使线程正常退出,也就是当 run() 方法完成后线程终止。
- 使用 stop() 方法强行终止线程,但是不推荐使用这个方法,由于 stop() 和 suspend() 及 resume()
一样,都是作废过期的方法,使用他们可能产生不可意料的结果。 - 推荐使用 interrupt() 方法中断线程,但这个不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的中止。
private volatile boolean on = true;@Overridepublic void run() { wheile (on && !Thread.currentThread().isInterrupted()) { ...... }}public void cancel() { on = false;}1.8.5 interrupted 与 isInterrupted 的区别
- interrupted() 测试当前线程能否已经是中断状态,执行后具备状态标志清理为 false 的功能。
- isInterrupted() 测试线程 Thread 对象能否已经是中断状态,但不清理状态标志。
public static boolean interrupted() { return currentThread().isInterrupted(true);}public boolean isInterrupted() { return isInterrupted(false);}private native boolean isInterrupted(boolean ClearInterrupted);2. 线程的实现
2.1 继承 Thread 类
- Thread 类在 java.lang 包中定义,继承 Thread 类必需重写 run() 方法。
public class Test { public static void main(String[] args) { System.out.println("主线程ID:"+Thread.currentThread().getId()); MyThread thread1 = new MyThread("thread1"); thread1.start(); MyThread thread2 = new MyThread("thread2"); thread2.run(); }} class MyThread extends Thread { private String name; public MyThread(String name) { this.name = name; } @Override public void run() { System.out.println("name:"+name+" 子线程ID:"+Thread.currentThread().getId()); }}- start() 方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
- Thread.sleep() 方法调用目的是不让当前线程独自霸占该进程所获取的 CPU 资源,以留出肯定时间给其余线程执行的机会。
- 实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。
- start() 方法重复调用的话,会出现 java.lang.IllegalThreadStateException 异常。
2.2 实现 Runnable 接口
- 实现 Runnable 接口,必需重写其 run() 方法。
public class Test { public static void main(String[] args) { System.out.println("主线程ID:"+Thread.currentThread().getId()); MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start(); }} class MyRunnable implements Runnable { public MyRunnable() { } @Override public void run() { System.out.println("子线程ID:"+Thread.currentThread().getId()); }}2.3 Thread 和 Runnable 的区别
- Runnable 接口比继承 Thread 类,适合多个相同的程序代码的线程去解决同一个资源。
- Runnable 接口比继承 Thread 类,可以避免 Java 中的单继承的限制。
- Runnable 接口比继承 Thread 类,添加了程序的健壮性,代码可以被多个线程共享,代码和数据独立。
- 线程池只能放入实现 Runable 或者 Callable 类线程,不能直接放入继承 Thread 的类。
2.3.1 直接调用 Thread 类的 run() 方法
- 直接调用 Thread 的 run() 方法,行为就会和普通的方法一样,为了在新的线程中执行代码,必需使用 Thread.start() 方法。
3. 线程间通信
3.1 wait()、notify() 和 notifyAll()
- wait()、notify() 和 notifyAll() 都是 Object 类中的方法。
- wait()、notify() 和 notifyAll() 方法是本地方法,并且为 final 方法,无法被重写。
- 使用 wait()、notify() 和 notifyAll() 时需要先对调用对象加锁。
- 调用 wait() 方法后,线程状态由 RUNNING 变为 WAITING,并将当前线程放置到对象的等待队列。
- notify() 或者者 notifyAll() 方法调用后,等待线程仍旧不会从 wait() 返回,需要调用 notify() 或者者 notifyAll() 方法的线程释放锁之后,等待线程才有机会从 wait() 返回。
- notify() 方法将等待队列中的一个等待线程从等待队列移动到同步队列中,而 notifyAll() 方法则是将等待队列中的所有线程一律移动到同步队列中,被移动的线程状态由 WAITING 变为 BLOCKED。
- 从 wait() 方法返回的前提是获取调用对象的锁。
3.2 等待/通知机制
等待方(消费者)遵循的准则。
- 获取对象的锁。
- 假如条件不满足,那么调用对象的 wait() 方法,被通知后仍要检查条件。
- 条件满足则执行对应的事务逻辑。
通知方(生产者)遵循的准则。
- 获取对象的锁。
- 改变条件。
- 通知所有等待在对象上的线程。
public class Test { private int queueSize = 10; private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize); public static void main(String[] args) { Test test = new Test(); Producer producer = test.new Producer(); Consumer consumer = test.new Consumer(); producer.start(); consumer.start(); } class Consumer extends Thread{ @Override public void run() { consume(); } private void consume() { while(true){ synchronized (queue) { while(queue.size() == 0){ try { System.out.println("队列空,等待数据"); queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); queue.notify(); } } queue.poll(); //每次移走队首元素 queue.notify(); System.out.println("从队列取走一个元素,队列剩余"+queue.size()+"个元素"); } } } } class Producer extends Thread{ @Override public void run() { produce(); } private void produce() { while(true){ synchronized (queue) { while(queue.size() == queueSize){ try { System.out.println("队列满,等待有空余空间"); queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); queue.notify(); } } queue.offer(1); //每次插入一个元素 queue.notify(); System.out.println("向队列取中插入一个元素,队列剩余空间:"+(queueSize-queue.size())); } } } }}3.2 ThreadLocal
- ThreadLocal 是线程变量,是一个以 ThreadLocal 对象为键,任意对象为值的存储结构。
- 这个结构被附带在线程上,一个线程可以根据一个 ThreadLocal 对象查询到绑定在这个线程上的一个值。
- 其提供了一个线程副本的成员变量,从而在少量情况下可以巧妙避开并发问题。
- 在多线程情况下对共享变量的修改,假如不采用任何同步策略,那么结果很大的概率上都会发生错误,这个主要是因为线程的 CPU 的 cache 与主内存的变量视图不一致导致的。
- 除了采用加锁同步之外,在少量特定的情况下,也可以使用 ThreadLocal 来修饰成员变量,从而给每一个线程维持绑定一个自己的副本变量,这样不管何时都只有本线程可以修改它,所以就不存在并发问题。
| 方法 | 说明 |
|---|---|
| get | 获取 ThreadLocal 中当前线程共享变量的值。 |
| set | 设置 ThreadLocal 中当前线程共享变量的值。 |
| remove | 移除 ThreadLocal 中当前线程共享变量的值。 |
| initialValue | ThreadLocal 没有被当前线程赋值时或者当前线程刚调用 remove 方法后调用 get 方法,返回此方法值。 |
3.2.1 ThreadLocal 使用场景
- 每个线程都需要维护一个自己专用的线程的上下文变量,比方计数器,JDBC 链接,web session,事务 ID 等。
- 包装一个线程不安全的成员变量,给其提供一个线程安全的环境,比方 Java 里面的 SimpleDateFormat 是线程不安全的,所以在多线程下使用可以采用 ThreadLocal 包装,从而提供安全的访问。
- 对于少量线程级别,传递方法参数有许多层的时候,我们可以使用 ThreadLocal 包装,只在特定地方 set 一次,而后不论在什么地方都可以随意 get 出来,从而巧妙避免了多层传参。
3.2.2 ThreadLocal 原理
- 线程共享变量缓存是 Thread.ThreadLocalMap<ThreadLocal, Object>
- Thread 为当前线程。
- Object 为当前线程的共享变量。
- ThreadLocal 并不是替换 Java 里面同步操作的,它的使用场景非常有限,在肯定特定的情况下可以发挥比较棒的作用,比方在 Spring 和 Hibernate 框架中就大量采用了 ThreadLocal 来保存事务会话。
- 虽然 ThreadLocalMap 的 Key 对象继承了 WeakReference (弱引用)对象,能够确保在内存空间不足的时候来回收对象,但 ThreadLocalMap 的 Value 值确是强引用,当线程没有结束,但是 ThreadLocal 已经被回收,则可能导致线程中存在 ThreadLocalMap<null, Object> 的键值对,造成内存泄露。(ThreadLocal 被回收,ThreadLocal 关联的线程共享变量还存在),为了防止此类情况的出现。
- 使用完线程共享变量后,显示调用 ThreadLocalMap.remove 方法清理线程共享变量。
- JDK 建议将 ThreadLocal 定义为 private static,这样 ThreadLocal 的弱引用问题则不存在了。
说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 【Java 并发笔记】并发基础整理
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 【Java 并发笔记】并发基础整理