Java语言基础(SE)-第八节 并发编程(Concurrent Programming)

线程的概念

进程(Process)

  1. 什么是进程?
    1. 在操作系统中运行的一个应用程序,比如同时打开QQ、微信,操作系统就会分别启动2个进程
  2. 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内
  3. 在 Windows 中,可以通过“任务管理器”查看正在运行的进程

线程(Thread)

  1. 什么是线程?
    1. 1个进程要想执行任务,必须得有线程(每1个进程至少要有1个线程
    2. 一个进程的所有任务都在线程中执行
    3. 比如使用酷狗播放音乐、使用迅雷下载文件,都需要在线程中执行
  2. 线程的串行
    1. 1个线程中任务的执行是串行
      1. 如果要在1个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务
      2. 在同一时间内,1个线程只能执行1个任务
    2. 比如在 1 个线程中下载 3 个文件(分别是文件 A、文件 B、文件 C)

多线程

  1. 什么是多线程
    1. 1 个进程中可以开启多个线程,所有线程可以 并行(同时) 执行不同的任务
    2. 进程 → 车间,线程 → 车间工人
    3. 多线程技术可以提高程序的执行效率
  2. 比如同时开启3个线程分别下载 3 个文件(分别是文件 A、文件 B、文件 C)
  3. 多线程的原理
    1. 同一时间,CPU 的1个核心只能处理1个线程(只有 1 个线程在工作)
    2. 多线程并发(同时)执行,其实是 CPU 快速地在多个线程之间调度(切换)
    3. 如果 CPU 调度线程的速度足够快,就造成了多线程并发执行的假象
    4. 如果是多核 CPU,才是真正地实现了多个线程同时执行
    5. 思考:如果线程非常非常多,会发生什么情况?
      1. CPU 会在 N 个线程之间调度,消耗大量的 CPU 资源,CPU 会累死
      2. 每条线程被调度执行的频次会降低(线程的执行效率降低)
  4. 多线程的优缺点
    1. 优点
      1. 能适当提高程序的执行效率
      2. 能适当提高资源利用率(CPU、内存利用率)
    2. 缺点
      1. 开启线程需要占用一定的内存空间,如果开启大量的线程,会占用大量的内存空间,降低程序的性能
      2. 线程越多,CPU 在调度线程上的开销就越大
      3. 程序设计更加复杂
        1. 比如线程之间的通信问题、多线程的数据共享问题

默认线程

  1. 每一个 Java 程序启动后,会默认开启一个线程,称为主线程(main 方法所在的线程)
  2. 每一个线程都是一个 java.lang.Thread 对象,可以通过 Thread.currentThread 方法获取当前的线程对象

     public static void main(String[] args) {
         //Thread[main,5,main]
         //通过查看Thread的toString可以看到[线程名称,优先级,线程组名称]
         System.out.println(Thread.currentThread());
     }
    

开启新线程

  1. 第1种方法

     //传入一个Runnable实例,在run方法中编写子线程需要执行的任务
     Thread thread = new Thread(new Runnable() {
         @Override
         public void run() {
             //存放线程要执行的代码
             System.out.println("开启了新线程:" + Thread.currentThread());
         }
     });
     //设置线程名称
     thread.setName("");
     //设置线程优先级
     thread.setPriority(10);
     //启动线程
     thread.start();
     //开启了新线程:Thread[,10,main]
    
  2. 第2种方法

     public class MyThread extends Thread {
         @Override
         public void run() {
             System.out.println("开启了新线程:" + Thread.currentThread());
         }
     }
     MyThread myThread = new MyThread();
     myThread.start();
     //开启了新线程:Thread[Thread-1,5,main]
    
    1. 注意: 直接调用线程的 run 方法并不能开启新线程,调用线程的 start 方法才能成功开启新线程
    2. Thread 类自己实现了 Runnable 接口

多线程的内存布局(重点!!!)

Java的内存区域(前几节已说明):PC 寄存器、Java 虚拟机栈、堆(Heap)、方法区(Method Area)、本地方法栈(Native Method Stack),那么一个线程的内存是如何布局的呢?

  1. PC 寄存器(Program Counter Register):每一个线程都有自己的 PC 寄存器
    1. 也叫程序计数器,用来存储指向下一条指令的地址(偏移地址),也即将要执行的指令代码,生命周期与线程的生命周期保持一致,一旦线程结束,当前PC寄存器也不在作用。
    2. 即每个线程执行每句代码编译的指令时,都是读取自己PC寄存器指向的指令
  2. Java 虚拟机栈(Java Virtual Machine Stack):每一个线程都有自己的 Java 虚拟机栈
    1. 程序在执行方法时,会开辟栈空间,也叫帧栈,用来存储函数内部的局部变量,且执行函数内部的数据计算,一旦函数执行完,栈恢复,生命周期和线程一致。它保存方法的局部变量、部分结果,并参与方法的调用和返回。
  3. 本地方法栈(Native Method Stack):每一个线程都有自己的本地方法栈
    1. 本地方法:有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因,比如Java需要与一些底层系统,如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。
    2. 一个Native Method就是一个Java调用非Java代码的接口,即该方法的实现由非Java语言实现,比如C.C+++
    3. 本地方法栈就是当java调用非java的方法时,需要开辟函数帧栈,需要栈空间,这块空间就是本地方法栈,与Java虚拟机栈类似。
  4. 堆(Heap):多个线程共享
  5. 方法区(Method Area):多个线程共享方法区
    1. 方法区:存储每一个类的结构信息(比如字段和方法信息、构造方法和普通方法的字节码(.class)等),类名、成员变量名称、方法名称等,也叫代码段

线程的状态

  1. 可以通过 Thread.getState 方法获得线程的状态(线程一共有6种状态,6个枚举值)
    1. NEW(新建):尚未启动
    2. RUNNABLE(可运行状态):正在 JVM 中运行
      1. 或者正在等待操作系统的其他资源(比如处理器)
    3. BLOCKED(阻塞状态):正在等待监视器锁(内部锁)
      1. 会消耗CPU时间片,一直在问锁是否被释放了
    4. WAITING(等待状态):在等待另一个线程
      1. 不会消耗CUP时间片,处于睡眠状态
      2. 调用以下方法会处于等待状态
        1. 没有超时值的 Object.wait
        2. 没有超时值的 Thread.join
        3. LockSupport.park
    5. TIMED_WAITING(定时等待状态)
      1. 调用以下方法会处于定时等待状态
        1. Thread.sleep
        2. 有超时值的 Object.wait
        3. 有超时值的 Thread.join
        4. LockSupport.parkNanos
        5. LockSupport.parkUntil
    6. TERMINATED(终止状态):已经执行完毕

线程的几个方法

  1. sleep、interrupt
    1. 可以通过 Thread.sleep 方法暂停当前线程,进入WAITING状态
      1. 在暂停期间,若调用线程对象的 interrupt 方法中断线程,会抛出 java.lang.InterruptedException 异常
       Thread thread = new Thread(()-> {
           System.out.println("begin");
           try {
               Thread.sleep(3000);
           } catch (InterruptedException e) {
               System.out.println("interrupt");
           }
           System.out.println("end");
       });
       thread.start();
       try {
           //主线程睡眠1s
           Thread.sleep(1000);
       } catch (InterruptedException e) {}
       //打断子线程的睡眠
       thread.interrupt();
       /*
       begin
       interrupt
       end
       */
      
  2. join、isAlive
    1. A.join 方法:等线程 A 执行完毕后,当前线程再继续执行任务。可以传参指定最长等待时间
    2. A.isAlive 方法:查看线程 A 是否还活着
     Thread t1 = new Thread(()-> {
         System.out.println("t1-begin");
         try {
             Thread.sleep(2000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("t1-end");
     });
     t1.start();
        	
     Thread t2 = new Thread(()-> {
         System.out.println("t2-begin");
         System.out.println("t1.isAlive - " + t1.isAlive());
         try {
             //t1的线程执行完毕,才会继续执行
             t1.join();
             //等t1 1s钟,如果没等到继续执行
             //t1.join(1000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("t1-state - " + t1.getState());
         System.out.println("t1-isAlive - " + t1.isAlive());
         System.out.println("t2-end");
     });
     t2.start();
    
    1. 打印结果

       t1-begin
       t2-begin
       t1.isAlive - true
       t1-end
       t1-state - TERMINATED
       t1-isAlive - false
       t2-end
      

线程安全问题

  1. 多个线程可能会共享(访问)同一个资源:比如访问同一个对象、同一个变量、同一个文件
  2. 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题,称为线程安全问题
  3. 什么情况下会出现线程安全问题?
    1. 多个线程共享同一个资源
    2. 且至少有一个线程正在进行写的操作(读取不会出问题,不会修改数据)
  4. 线程安全问题 – 示例,车站买票

     public class Station implements Runnable {
         private int tickets = 100;
         /**
         * 卖一张票
         * @return 是否还有票可以卖
         * */
         public boolean saleTicket() {
             if(tickets <1) return false;
             tickets --;
             String name = Thread.currentThread().getName();
             System.out.println(name + "卖了1张,剩" + tickets + "张");
             return tickets >0;
         }
         @Override
         public void run() {
             while (saleTicket());
         }
     }
     Station station = new Station();
     for (int i = 0; i < 4; i++) {
         Thread thread = new Thread(station);
         thread.setName("" + i);
         thread.start();
     }
    

线程同步

  1. 可以使用线程同步技术来解决线程安全问题
    1. 同步语句(Synchronized Statement)
    2. 同步方法(Synchronized Method)

同步语句

public boolean saleTicket() {
    //获得this对象的内部锁,加锁
    synchronized (this) {
        if(tickets <1) return false;
        tickets --;
        String name = Thread.currentThread().getName();
        System.out.println(name + "卖了1张,剩" + tickets + "张");
        return tickets >0;
    }//释放锁
}
  1. synchronized(obj) 的原理
    1. 每个对象都有一个与它相关的内部锁(intrinsic lock)或者叫监视器锁(monitor lock),理解成每个对象都有一把锁
    2. 第一个执行到同步语句的线程可以获得obj的内部锁,在执行完同步语句中的代码后释放此锁,synchronized可以获取对象的锁,并锁住
    3. 只要一个线程持有了内部锁,那么其它线程在同一时刻将无法再获得此锁,当它们试图获取此锁时,将会进入BLOCKED状态
  2. 多个线程访问同一个 synchronized(obj) 语句时
    1. obj 必须是同一个对象,才能起到同步的作用
    2. obj可以是任一个对象不一定是this,但是必须要求每个线程访问的是同一个对象

同步方法

//修饰实例方法
public synchronized boolean saleTicket() {
    if(tickets <1) return false;
    tickets --;
    String name = Thread.currentThread().getName();
    System.out.println(name + "卖了1张,剩" + tickets + "张");
    return tickets >0;
}
  1. synchronized 不能修饰构造方法
  2. 同步方法的本质
    1. 实例方法:synchronized (this)
      1. 就是等价于上面的同步语句
    2. 静态方法:synchronized(Class对象)

       public synchronized static void test() {}
       public  static void test2() {
           //类对象Station.class
           synchronized(Station.class) {}
       }
      
  3. 同步语句比同步方法更灵活一点
    1. 同步语句可以精确控制需要加锁的代码范围
  4. 使用了线程同步技术后
    1. 虽然解决了线程安全问题,但是降低了程序的执行效率
    2. 所以在真正有必要的时候,才使用线程同步技术

单例模式(懒汉式)改进

//之前的单例模式存在线程安全问题
//懒汉式,解决线程安全问题
class Rocket {
    //1.私有的静态的实例变量
    private static Rocket instance = null;
    //2. 构造方法私有化,让外界无法调用构造方法
    private Rocket() {}
    //3. 提供一个公共的静态的,返回一个唯一的那个实例
    public static synchronized Rocket getInstance() {
        if (instance == null) {
            instance = new Rocket();
        }
        return instance;
    }
}

几个常用类的细节(以下数据结构可以查看源码)

  1. 动态数组
    1. ArrayList:非线程安全
    2. Vector:线程安全
  2. 动态字符串
    1. StringBuilder:非线程安全
    2. StringBuffer:线程安全
  3. 映射(字典)
    1. HashMap:非线程安全
    2. Hashtable:线程安全

死锁(Deadlock)

  1. 什么是死锁?
    1. 两个或者多个线程永远阻塞,相互等待对方的锁
     new Thread(()->{
         //获取锁成功
         synchronized ("1") {
             System.out.println("1-1");
             try {
                 Thread.sleep(1000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             //获取锁失败,被另外线程占用占用
             synchronized ("2") {
                 System.out.println("1-2");
             }
         }
     }).start();
     new Thread(()->{
         //获取锁成功
         synchronized ("2") {
             System.out.println("2-1");
             try {
                 Thread.sleep(1000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             //获取锁失败,被另外线程占用占用
             synchronized ("1") {
                 System.out.println("2-2");
             }
         }
     }).start();
    
  2. 示例

     public class Person {
         private String name;
         public Person(String name) {
             this.name = name;
         }
         public synchronized void hello(Person p) {
             System.out.format("[%s] hello to [%s]%n",name,p.name);
             //另外一个对象
             p.smile(this);
         }
         public synchronized void smile(Person p) {
             System.out.format("[%s] hello to [%s]%n",name,p.name);
         }
     }
     Person jack = new Person("jack");
     Person rose = new Person("rose");
     //同时开口
     new Thread(()->{jack.hello(rose);}).start();
     new Thread(()->{rose.hello(jack);}).start();
    

线程间通信

  1. 可以使用 Object.wait、Object.notify、Object.notifyAll 方法实现线程之间的通信
  2. 若想在线程 A 中成功调用 obj.wait、obj.notify、obj.notifyAll 方法
    1. 线程A必须要持有obj的内部锁
  3. obj.wait :释放obj的内部锁,当前线程进入WAITING或TIMED_WAITING状态
  4. obj.notifyAll :唤醒所有因为 obj.wait 进入WAITING或TIMED_WAITING状态的线程
  5. obj.notify :随机唤醒1个因为 obj.wait 进入WAITING或TIMED_WAITING状态的线程
  6. 示例
    1. Drop:传递的信息

       package com.zh;
       public class Drop {
           private String food;
           //empty为true:代表消费者等待生产者生产东西
           //empty为false:代表生产完毕,生产者等待消费者消化完食品
           private boolean empty = true;
           //消费者获取,消费者线程执行
           public synchronized String get() {
               //为什么不用if,要用while,因为wait();会抛出异常,接着会往下执行
               //除非empty为false,否则不能继续执行。
               while (empty) {
                   try {
                       //此时生产者没有生产东西,这卡住,等待add中通知生产完成生产
                       //理解:等待过程中释放了this的内部锁,以便于其他线程可以拿到,生产者才能进入add方法。
                       wait();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
               empty =true;
               //通知所有生产线程可以取食物了
               notifyAll();
               return food;
           }
           //生产者生产,生产者线程执行
           public synchronized void add(String food) {
               //第一次时empty为true
               while (!empty) {
                   try {
                       //等待消费者告知,已经消费完了
                       //同理,释放锁,消费这才能进入get方法
                       wait();
                   } catch (InterruptedException e) {}
               }
               //设置标记,
               empty =false;
               //保存当前食物
               this.food = food;
               //通知所有消费线程可以取食物了
               notifyAll();
           }
       }
      
    2. Producer: 生产者

       public class Producer implements Runnable {
           private Drop drop;
           public Producer(Drop drop) {
               this.drop = drop;
           }
           //生产食物
           @Override
           public void run() {
               String foods[] = {"beef","bread","apple","cookie"};
               for (int i = 0; i < foods.length; i++) {
                   try {
                       Thread.sleep(1000);
                   } catch (InterruptedException e) {}
                   //将food[i],传递给消费者
                   drop.add(foods[i]);
               }
               //告诉消费者,不会生产任何东西了
               drop.add(null);
           }
       }
      
    3. Consumer:消费者

       public class Consumer implements Runnable {
           private Drop drop;
           public Consumer(Drop drop) {
               this.drop = drop;
           }
           //拿食物
           @Override
           public void run() {
               String food = null;
               while ((food = drop.get()) != null) {
                   System.out.format("消费者接收到食物:%s%n", food);
                   try {
                       Thread.sleep(1000);
                   } catch (InterruptedException e) {}
               }
           }
       }
      
    4. main函数使用

       //使用
       Drop drop = new Drop();
       //开启2个线程,2个线程中drop对象是同一个
       (new Thread(new Consumer(drop))).start();
       (new Thread(new Producer(drop))).start();
      
    5. 注意:

      1. 调用wait、notify必须是同一个obj对象
      2. 调用wait、notify的线程必须拥有obj对象的内部锁

ReentrantLock(可重入锁)

  1. ReentrantLock ,译为“可重入锁”
    1. 类的全名是:java.util.concurrent.locks.ReentrantLock
    2. 具有跟同步语句(加锁的语句)同步方法(加锁的方法) 一样的一些基本功能,但功能更加强大
  2. 什么是可重入?
    1. 同一个线程可以重复获取同一个锁
    2. 其实 synchronized 也是可重入的

lock、tryLock

  1. ReentrantLock.lock :获取此锁
    1. 如果此锁没有被另一个线程持有,则将锁的持有计数设为1,并且此方法立即返回
    2. 如果当前线程已经持有此锁,则将锁的持有计数加1,并且此方法立即返回
    3. 如果此锁被另一个线程持有,并且在获得锁之前,此线程将一直处于休眠状态,此时锁的持有计数被设为1。
  2. ReentrantLock.tryLock :仅在锁未被其他线程持有的情况下,才获取此锁
    1. 如果此锁没有被另一个线程持有,则将锁的持有计数设为1,并且此方法立即返回 true
    2. 如果当前线程已经持有此锁,则将锁的持有计数加1,并且此方法立即返回 true。
    3. 如果锁被另一个线程持有,则此方法立即返回 false,不会阻塞当前线程

unlock、isLocked

  1. ReentrantLock.unlock :尝试释放此锁
    1. 如果当前线程持有此锁,则将持有计数减 1
    2. 如果持有计数现在为 0,则释放此锁
    3. 如果当前线程没有持有此锁,则抛出 java.lang.IllegalMonitorStateException
  2. ReentrantLock.isLocked :查看此锁是否被任意线程持有

ReentrantLock 在卖票示例中的使用

private Lock lock = new ReentrantLock();
private int tickets = 100;
public boolean saleTicket() {
    //boolean flag = false;
    try {
        //不一定会加上锁,尝试加锁
        //flag = lock.tryLock();
        //上锁,一旦获取不到锁,当前线程会处于休眠状态,不会继续执行,直到获取锁为止
        lock.lock();
        if(tickets <1) return false;
        tickets --;
        String name = Thread.currentThread().getName();
        System.out.println(name + "卖了1张,剩" + tickets + "张");
        return tickets >0;
    } finally {
        //if(flag) {
            //lock.unlock();
        //}
        //解锁
        lock.unlock();
    }	
}

tryLock使用注意

Lock lock = new ReentrantLock();
new Thread(()->{
    try {
        lock.lock();
        System.out.println("1");
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }finally {
        System.out.println("3");
        lock.unlock();
    }
}).start();
new Thread(()->{
    boolean locked = false;
    try {
        //不管是否获取到锁,都不会阻塞当前线程
        locked = lock.tryLock();
        //如果获取不到锁,当前线程会阻塞,处于休眠状态,直至获取到锁为止,才继续执行
        //lock.lock();
        System.out.println(locked);
        System.out.println("2");
    }finally {
        System.out.println("41");
        if (locked) {
            System.out.println("4");
            lock.unlock();
        }
    }
}).start();
//打印,线程二使用trylock
1
false
2
41
3
//打印:线程二使用lock
1
3 //直到第一个线程把锁释放,第二个线程才会结束休眠,继续执行
false
2
41

线程池(Thread Pool)

  1. 从多线程的内存布局可知,每个线程都要开辟3块内存空间(PC寄存器、Java虚拟机栈、本地方法栈,其余2个是多个线程共享)
  2. 线程对象占用大量内存,在大型应用程序中,频繁地创建和销毁线程对象会产生大量内存管理开销
  3. 使用线程池可以最大程度地减少线程创建、销毁所带来的开销
  4. 线程池由 工作线程(Worker Thread) 组成
    1. 普通线程:执行完一个任务后,生命周期就结束了
    2. 工作线程: 可以执行多个任务(任务没来就一直等,任务来了就干活)
      1. 先将任务添加到队列(Queue)中,再从队列中取出任务提交到池中
  5. 常用的线程池类型是固定线程池(Fixed Thread Pool)
    1. 具有固定数量的正在运行的线程

基本使用

//创建拥有5条工作线程的固定线程池
ExecutorService pool = Executors.newFixedThreadPool(5);
//执行任务
pool.execute(()->{
    //11_pool-1-thread-1
    System.out.println(11 + "_" + Thread.currentThread().getName());
});
pool.execute(()->{
    //22_pool-1-thread-2
    System.out.println(22 + "_" + Thread.currentThread().getName());
});
pool.execute(()->{
    //33_pool-1-thread-3
    System.out.println(33 + "_" + Thread.currentThread().getName());
});
//关闭线程池
pool.shutdown();
Table of Contents