Java并发编程01-并发基础

date: 2020-03-23 00:50:25

一、 基本概念

并发:一个处理器处理多个任务,逻辑上的同时发生。

并行:多个处理器同时处理多个任务,物理上的同时发生。

并发与并行的区别

二、Java 线程基础

1. 线程

进程与线程

进程: 程序运行的一个实例。

线程:程序的执行单元,是程序使用 CPU 的基本单位。一个进程可以包含多个线程。

2. 线程实现/创建的方式

  1. 继承 Thread 类
  2. 实现 Runnable 接口
  3. ExecutorService、Callable、Future 有返回值线程
  4. 基于线程池的方式

3. 线程的生命周期

当线程启动后,不可能一直霸占着 CPU,CPU 需要在多个线程中来回切换,于是线程也会多次的在运行、阻塞状态之间切换。

线程的生命周期

1.新建(New)
当使用 new 关键字创建了一个线程后,该线程就处于新建状态。此时 JVM 为其分配内存,并初始化其成员变量。

2.就绪(Runnable)

当线程对象调用了 start() 方法后,该线程就处于就绪状态。JVM 为其创建方法调用栈和程序计数器,等待 CPU 调度执行。

3.运行(Running)

处于就绪状态的线程获得了 CPU,开始执行 run() 方法的线程执行体,则该线程就处于运行状态。

4.阻塞(Blocked)

线程因为某种原因,放弃了 CPU 的使用权,暂时停止运行,则该线程处于阻塞状态。

阻塞后的线程,需要进入就绪状态后,才有机会再次运行。

阻塞的三种情况:

  1. 等待阻塞(wait -> 等待队列)

    运行的线程执行 o.wait() 方法时,JVM 会把该线程放入到等待队列(Waitting Queue)中。

  2. 同步阻塞(lock -> 锁池)

    运行的线程在获取对象的同步锁时,若该锁正在被其它线程占用,则 JVM 会把该线程放入锁池(Lock Pool)中。

  3. 其它阻塞(sleep/join)

    运行的线程执行了 Thread.sheep(long ms) / t.join() 方法时,或者发出 IO 请求时,JVM 会把该线程置于阻塞状态。 当 sleep() 状态超时、join() 等待线程终止或者超时、IO 处理完毕时,该阻塞线程重新转入就绪状态。

5.死亡(Dead)

线程执行结束后,就是死亡状态。

线程的结束方式:

  1. 正常结束

    run() / call() 方法执行完成后,线程正常结束。

  2. 异常结束

    线程抛出一个未捕获的 Exception 或者 Error。

  3. 调用 stop() 方法

    调用 Thread.stop() 方法可以强行结束线程。 该方法不推荐使用,容易导致死锁。

  4. 使用退出标志推出线程

    使用一个变量来控制循环。
    最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false 来控制 while 循环是否退出,代码示例:

    public class MyThread extends Thread {
        public volatile boolean exit = false;
        public void run() {
            while (!exit){
                //do something
            }
        }
    }
    

    注意:使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。

  5. Interrupt() 方法结束线程

    1. 线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。

    2. 线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。

      代码示例:

      public class MyThread extends Thread {
          public void run() {
              while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
                  try{
                      Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
                  }catch(InterruptedException e){
                      e.printStackTrace();
                      break;//捕获到异常之后,执行 break 跳出循环
                  }
              }
          }
      }
      

4. 线程控制

1.start() 与 run()

2.sleep() 与 wait()

3.interrupt() 与 stop()

4.线程加入(join)

等待该线程执行完毕

public static void main(String[] args) throws InterruptedException {
    System.out.println("主线程开始执行...");

    Thread t = new Thread(() -> {
        System.out.println("子线程开始执行...");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });

    t.start();
    t.join();

    System.out.println("主线程执行结束...");
}

执行结果一定为:

主线程开始执行...
子线程开始执行...
子线程执行结束...
主线程执行结束...

5.线程礼让(yield)

暂停当前正在执行的线程对象,并执行其他线程,让多个线程的执行更和谐,但是不能靠它保证一人一次。

程序示例:

public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
            Thread.yield();
        }
    }).start();


    new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
            Thread.yield();
        }
    }).start();
}

6.等待/通知机制(wait、notify)

wait() 方法:该方法用来使得当前线程进入等待状态,直到接到通知或者被中断打断为止。在调用 wait() 方法之前,线程必须要获得该对象的对象级锁;换句话说就是该方法只能在同步方法或者同步块中调用,如果没有持有合适的锁的话,线程将会抛出异常 IllegalArgumentException。调用 wait() 方法之后,当前线程则释放锁。

notify() 方法:该方法用来唤醒处于等待状态获取对象锁的其他线程。如果有多个线程则线程规划器任意选出一个线程进行唤醒,使其去竞争获取对象锁,但线程并不会马上就释放该对象锁,wait() 所在的线程也不能马上获取该对象锁,要程序退出同步块或者同步方法之后,当前线程才会释放锁,wait() 所在的线程才可以获取该对象锁。

程序示例,通过notify()方法去唤醒一个休眠中的线程的执行:

public class N16_Wait_Notify {
    private volatile int signal;

    public void set(int signal) {
        this.signal = signal;
    }

    public int get() {
        return this.signal;
    }


    public static void main(String[] args) {
        N16_Wait_Notify n16 = new N16_Wait_Notify();

        // wait, notify 必须在同步代码块中,并且是锁的实例,不是当前线程的实例

        new Thread(() -> {
            System.out.println("线程A开始执行...");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (n16) {
                System.out.println("线程A修改信号值为1...");
//                n16.set(1);
                n16.notify();
            }
        }).start();

        new Thread(() -> {
            // 这样写,虽然也能达到通知等待的效果,但是一直占用CPU资源,损耗性能
            // 即使循环体使用sleep休眠,休眠时间不好控制,也不是优雅的方式
//            while (n16.get() != 1) {
//            }

            synchronized (n16) {
//                while (n16.get() != 1) {
                    try {
                        n16.wait(); // 阻塞当前
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
//                }
                System.out.println("线程B开始执行...");
            }

        }).start();
    }
}

7.notify与notifyAll的区别

5. 守护线程(Daemon)

定义:又称“服务线程”、“后台线程”,为用户线程提供公共服务,在没有用户线程可服务时会自动离开。

生命周期:只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。

优先级:低

设置:t.setDaemon(true)

注意:

  • 在Daemon线程中产生的线程也是Daemon线程
  • 必须在start()之前设置

6. 线程调度

线程调度

线程有两种调度模型:分时调度、抢占式调度。

Java 使用的是抢占式调度模型:优先让优先级高的线程使用 CPU,优先级高的线程获取 CPU 时间片也相对多一些。 如果线程的优先级相同,会随机选择一个。

注意:设置了优先级也只是提高了获得 CPU 的几率。并不能保证哪个线程在某个时刻一定会抢到。

线程优先级

Java 线程中,通过一个整型成员变量 priority 来控制优先级,优先级范围为 0~10,在线程构建时可以通过 setPriority(int n) 方法来修改优先级,默认优先级为 5。

7. 线程变量 ThreadLocal

ThreadLocal,即线程变量,是一个线程实例为 key,任意对象为 value 的 Map 存储结构。通过 get()/set() 方法来获取/设置值,通过实现 initialValue() 方法来赋初始值。

程序示例:

public class N17_ThreadLocal {
    private ThreadLocal<Integer> count = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public Integer getNext() {
        Integer value = count.get();
        count.set(value + 1);
        return value;
    }


    public static void main(String[] args) {
        N17_ThreadLocal n = new N17_ThreadLocal();

        new Thread(() -> {
            while (true) {
                System.out.println(Thread.currentThread().getName() + " " + n.getNext());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(() -> {
            while (true) {
                System.out.println(Thread.currentThread().getName() + " " + n.getNext());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(() -> {
            while (true) {
                System.out.println(Thread.currentThread().getName() + " " + n.getNext());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

三、Java 内存模型

Java 内存模型(JMM),线程之间的通信由 JMM 控制。JMM 定义了线程和主内存之间的抽象关系:

线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。

本地内存是 JMM 的一个抽象概念,不真实存在。它覆盖了缓存、写缓冲区、寄存器以及其它硬件和编译器优化。

Java 内存模型的抽象结构示意如下图:

Java内存模型的抽象结构示意图

从上图看,如果线程 A 与线程 B 之间要通信的话,必须要经过以下步骤:

  1. 线程 A 把本地内存 A 中更新的共享变量刷新到主内存中;
  2. 线程 B 到主内存中去读取线程 A 之前已经更新过的共享变量

可见,JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 Java 程序员提供内存可见性保证。

四、并发的优缺点

优点:

  • 资源利用率高
  • 程序响应更快

缺点:

  • 安全性:多个线程变量共享会存在问题
  • 活跃性:需要考虑死锁等问题
  • 性能:线程过多导致 CPU 切换开销大,消耗性能。线程需要内存空间,增加内存消耗。
posted @ 2020-03-23 00:50  吹不散的流云  阅读(130)  评论(0)    收藏  举报