【Java 并发笔记】并发基础整理

作者 : 开心源码 本文共8937个字,预计阅读时间需要23分钟 发布时间: 2022-05-12 共141人阅读

文前说明

作为码农中的一员,需要不断的学习,我工作之余将少量分析总结和学习笔记写成博客与大家一起交流,也希望采用这种方式记录自己的学习之旅。

本文仅供学习交流使用,侵权必删。
不用于商业目的,转载请注明出处。

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() 的区别

/sleepyieldwait
方法所在类ThreadThreadObject
锁行为不会改变锁行为不会改变锁行为释放锁
进入状态进入阻塞状态进入就绪状态进入等待状态
恢复指定时间后恢复与相同优先级线程争夺等待别的线程 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 中当前线程共享变量的值。
initialValueThreadLocal 没有被当前线程赋值时或者当前线程刚调用 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 并发笔记】并发基础整理

发表回复