进程与线程的区别

  • 操作系统是多进程多线程的
  • 进程:是操作系统运行程序的单元,每个进程有唯一的进程编号,拥有独立的内存空间
  • 线程:是进程运行的单元

image.png

启动一个Java程序, 是启动一个进程, 还是启动一个线程?

  • 启动一个进程
  • 同时启动了一个主线程

操作系统为什么需要多进程?

  • 如果操作系统是单进程的, 在同一时刻只能运行一个程序, 肯定是不满足用户需求

进程为什么需要多线程?

  • 如果进程是单线程的, 在同一时刻只能有一个用户在操作这个进程, 在同一时刻进程中只能运行一个程序
  • 主线程只有一个, 子线程可以有多个

为什么不直接调用run方法, 而是调用start()方法?

  • 调用start()方法, 才会将run()方法压入到子线程栈中执行
  • 如果直接调用run方法, run()方法会在主线程栈中执行

Thread类提供的操作线程的方法

方法名 作用
currentThread() 返回当前的线程对象
getName() 返回当前线程的名字
sleep() 让当前的线程进入休眠状态, 进入休眠状态的线程不参与争抢CPU
等休眠时间到了以后, 线程会自动苏醒, 苏醒后自动进入抢CPU的队列
yield() 让出本次CPU的使用权
join() 强势霸占CPU执行
setPriority() 设置线程的优先级 优先级的取值为 1-10 数值越大优先级越高
getPriority() 获取线程的优先级

如何创建线程

造线程就是造对象,线程也是对象

造线程的方式一

  • 编写XXXThread子类继承 Thread 类
  • 重写run方法
  • 造一个XXXThread对象, 调用start()方法启动线程


造线程的方式二

  • 编写XXXRunnable类实现Runnable接口
  • 重写run方法
  • 造一个XXXRunnable对象, 用XXXRunnable对象作为Thread类构造方法的参数造Thread对象


实现多个线程共享数据**

  • 思考: 如果用户来访问我们的系统, 把所有用户安排在一个线程中, 你认为合理吗?
    • 不合理, 因为单线程运行的情况下, 用户是需要排队的
  • 当用户来访问系统的时候, 必须给一个用户安排一个线程
  • 为什么要共享数据
    • 假设我们需要使用一个变量来统计用户的在线人数

程序计数器/PC寄存器

  • 程序计数器是线程私有的
  • 程序计数器用来记录线程在市区CPU的时候所执行的最后一条指令的地址,为了下次抢到CPU后还能从这里继续往后执行

    多线程如何共享数据

  • 局部变量 一定不会被多个线程共享

    • 与线程是否共享对象无关
    • 在栈区, 在方法的栈帧中
  • 成员变量 如果多个线程**共享同一个对象, 则共享对象中的成员变量 | 如果多个线程不共享同一个对象, 则**不共享对象中的成员变量
    • 与线程是否共享对象有关
    • 在堆区,在对象中
  • 静态变量 一定可以被多线程共享
    • 与线程是否共享对象无关
    • 在方法区,只会在类加载的时候初始化

多线程共享数据后带来的数据不安全的问题

当一个线程修改数据后, 还没有提交修改,丢失了CPU,数据被其他线程获取了
由于线程对数据进行修改的时候, 不具有原子性
例如: i++; 对应3条底层的字节码指令

  1. 取出 i
  2. +1
  3. 写入i

    使用加锁的方式解决多线程共享数据后带来的数据不安全的问题

    1. 使用synchronized关键字加锁

  • 锁方法

    1. public class DemoRunnable implements Runnable {
    2. int count = 0;
    3. @Override
    4. public synchronized void run() {
    5. this.count ++;
    6. System.out.println(Thread.currentThread().getName() + ":" +count );
    7. }
    8. }
  • 锁代码块 (需要指定锁的资源)

    • 锁对象

      1. public class DemoRunnable implements Runnable {
      2. int count = 0;
      3. @Override
      4. public void run() {
      5. synchronized (this){
      6. count ++;
      7. }
      8. System.out.println(Thread.currentThread().getName() + ":" +count );
      9. }
      10. }
    • 锁字节码文件

      1. public class DemoRunnable implements Runnable {
      2. static int count = 0;
      3. @Override
      4. public void run() {
      5. synchronized (DemoRunnable.class){
      6. count ++;
      7. }
      8. System.out.println(Thread.currentThread().getName() + ":" +count );
      9. }
      10. }

      2. 使用Lock类上锁

      1. public class DemoRunnable implements Runnable {
      2. // 造一个lock对象(锁)
      3. Lock lock = new ReentrantLock();
      4. int count = 0;
      5. @Override
      6. public void run() {
      7. lock.lock(); // 上锁
      8. count++;
      9. lock.unlock();// 开锁
      10. System.out.println(Thread.currentThread().getName() + ":" +count );
      11. }
      12. }

      ReentrantLock介绍

  • Reentrant译为可重入的, ReentrantLock 可重入锁

ReentrantLock不仅可以替代隐式的synchronized关键字,而且能够提供超过关键字本身的多种功能。
这里提到一个锁获取的公平性问题,如果在绝对时间上,先对锁进行获取的请求一定被先满足,那么这个锁是公平的,反之,是不公平的,也就是说等待时间最长的线程最有机会获取锁,也可以说锁的获取是有序的。ReentrantLock这个锁提供了一个构造函数,能够控制这个锁是否是公平的。
而锁的名字也是说明了这个锁具备了重复进入的可能,也就是说能够让当前线程多次的进行对锁的获取操作,这样的最大次数限制是Integer.MAX_VALUE,约21亿次左右。
事实上公平的锁机制往往没有非公平的效率高,因为公平的获取锁没有考虑到操作系统对线程的调度因素,这样造成JVM对于等待中的线程调度次序和操作系统对线程的调度之间的不匹配。对于锁的快速且重复的获取过程中,连续获取的概率是非常高的,而公平锁会压制这种情况,虽然公平性得以保障,但是响应比却下降了,但是并不是任何场景都是以TPS作为唯一指标的,因为公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。

形成死锁的四个必要条件是什么

1). 互斥条件:锁具体排他性, 当资源被一个线程锁住的时候, 对于其他线程是互斥的, 直到该线程使用完资源后释放锁
2). 请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
3). 不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
4). 循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞

如何避免线程死锁

我们只要破坏产生死锁的四个条件中的其中一个就可以了。
1). 破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
2). 破坏请求与保持条件
一次性申请所有的资源。
3). 破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
4). 破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

说说线程的生命周期及五种基本状态?

多线程 - 图2

  1. 新建(new):新创建了一个线程对象。
  2. 可运行(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
  3. 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
  4. 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。
    阻塞的情况分三种:(一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;(二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;(三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
  5. 死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

sleep() 和 wait() 有什么区别?

两者都可以暂停线程的执行

  • 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
  • 是否释放锁:sleep() 不释放锁;wait() 释放锁。
  • 用途不同:wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
  • 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。

    为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?

    Java中,任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。
    wait(), notify()和 notifyAll()这些方法在同步代码块中调用
    有的人会说,既然是线程放弃对象锁,那也可以把wait()定义在Thread类里面啊,新定义的线程继承于Thread类,也不需要重新定义wait()方法的实现。然而,这样做有一个非常大的问题,一个线程完全可以持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁?当然了,这种设计并不是不能实现,只是管理起来更加复杂。
    综上所述,wait()、notify()和notifyAll()方法要定义在Object类中。

    为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?

    当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。

生产者和消费者设计模式

image.png

Goods类

package edu.td.chapter09;

public class Goods {
    // 用来标识当前处于生产还是消费的状态
    // 生产数据后, 让flag = false, 然后通知消费
    // 消费数据后, 让flag = true, 然后通知生产
    boolean flag = false;
    // 商品库存数量
    int stock;
    // 生产方法
    public synchronized void set(){
        if(!flag){
            try {
                wait(); // 调用Object类的wait()方法,让当前线程进入等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        stock ++; // 库存+1,通知消费
        flag = false;
        System.out.println("生产库存: " + stock + ",等待消费");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        notify(); // 通知消费者线程苏醒进行消费
    }
    // 消费方法
    public synchronized void get(){
        if(flag){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("消费库存: " + stock + ",等待生产");
        flag = true;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        notify(); // 通知生产者线程苏醒进行消费
    }
}

Customer类

package edu.td.chapter09;

public class Customer implements Runnable {

    Goods goods;

    public Customer(Goods goods){
        this.goods = goods;
    }

    @Override
    public void run() {
        while (true){
            // 调用get()方法进行消费
            goods.get();
        }
    }
}

Producter类

package edu.td.chapter09;

public class Producter implements Runnable {

    Goods goods;

    public Producter(Goods goods){
        this.goods = goods;
    }

    @Override
    public void run() {
        while (true){
            // 调用get()方法进行生产
            goods.set();
        }
    }
}

测试类

package edu.td.chapter09;

public class Demo {
    public static void main(String[] args) {
        Goods goods = new Goods();
        Customer customer = new Customer(goods);
        Producter producter = new Producter(goods);
        Thread t1 = new Thread(customer);
        Thread t2 = new Thread(producter);
        t1.setName("生产者线程");
        t2.setName("消费者线程");
        t1.start();
        t2.start();
    }
}