Java多线程-详细

线程基础知识

  • 三高:高可用、高性能、高并发
  • 在操作系统中运行程序就是进程,如看视频, 一个进程可以有多个线程,
    如视频中同时播放声音、播放图像、显示字幕
  • Process:进程 Thread:进程
  • 进程是资源分配的基本单位、线程是CPU调度的基本单位
区别 进程 线程
根本区别 作为资源分配的基本单位 作为CPU调度和执行的基本单位
开销 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销 线程可以看成是轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小
所处环境 在操作系统中能同时运行多个任务(程序) 在同一应用程序中有多个顺序流同时执行
分配内存 系统在运行的时候会为每个进程分配不同的内存区域 除了CPU外,不会为线程分配内存(线程所使用的资源时它所属的进程的资源),线程组只能共享资源
包含关系 没有线程的进程是可以被看做单线程的,如果一个进程内拥有多个线程,则执行过程中不是一条线的,而是多条线(线程)共同完成的 线程是进程的一部分,所以线程有的时候被称为是轻权进程或者轻量级进程
  • 很多多线程是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器。如果是模拟出来的多线程,即一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉

  • 在程序运行时,即使没有自己创建线程,后台也会存在多个线程,如gc线程、主线程

  • main()称之为主线程,为系统的入口点,用于执行整个程序

  • 在一个进程中,如果开辟了多个线程,线程的运行是由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为干预的

  • 对同一份资源操作时,会存在资源抢夺问题,需要加入并发控制

  • 线程会带来额外的开销,如cpu调度时间、并发控制开销

  • 每个线程在自己的工作内存交互、加载和存储,主内存控制不当会造成数据不一致

进程和线程

  • 进程:正在运行的程序

    • 进程是系统进程资源分配和调用的基本单位
    • 每一个进程都有他自己的内存空间和系统资源
  • 线程:是进程中的单个顺序控制流,是一条执行路径

    • 单线程:一个进程如果只有一条执行路径,则称为单线程程序
    • 多线程:一个进程如果有多条执行路径,则称为多线程程序
  • 创建线程的三种方法

    • 1.继承Thread类,Thread是一个线程类

      • 要少用继承多用实现,因为java中类只能多继承,后期如果继承Thread的类需要继承其他类,就需要大量的重构代码
      • 实现方式:
        • 1.1. 定义一个Thread类的子类
        • 1.2. 在子类中重写Thread类的run()方法
        • 1.3. 然后创建Thread类的对象
        • 1.4 启动线程
      • Thread的子类中并不是所有的代码都需要被线程执行,为了区分哪些代码需要被线程执行,java就提供了run()方法
      • 直接调用子类的run方法并不会启动线程,应该调用子类的start()方法
      • start()方法可以让此线程开始执行,并且是由java虚拟机调用此线程的run()方法的
      • 重点:
        • Thread子类为什么要重写start()方法?
          • 因为run()方法是用来封装被线程执行的代码
        • run()方法和start()方法的区别
          • run():封装线程执行的代码,如果直接调用run()方法,相当于调用一个普通方法
          • start():启动线程,然后由JVM调用此线程的run()方法
      • Thread类中设置和获取线程名称的方法
        • void setName(String name):将此线程的名称更改为name,默认名称为Thread-i, i = 0, 1, 2…
        • String getName():返回此线程的名称
      • Thread类中: static Thread currentThread():返回当前正在执行的线程对象
      • Thread.currentThread().getName(): 获取当前正在执行的线程的名称
    • 2.实现Runnable接口

      • 2.1. 定义一个类MyRunnable实现Runnable接口
      • 2.2. 在MyRunnable类中重写run()方法
      • 2.3. 创建MyRunnable类的对象
      • 2.4. 创建Thread类的对象,把MyRunnable对象作为构造方法的参数传入Thread对象
      • 2.5. 启动线程
      public class MyThread1 implements Runnable {
          @Override
          public void run() {
              for (int i = 0; i < 100; i++) {
                  System.out.println(Thread.currentThread().getName() + ":" + i);
              }
          }
      }
      
      public class MainThread1 {
          public static void main(String[] args) {
              MyThread1 myThread1 = new MyThread1();
              // 这里是把myThread1作为1个公共的资源
              new Thread(myThread1, "线程A").start();
              new Thread(myThread1, "线程B").start();
              new Thread(myThread1, "线程C").start();
          }
      } 
      
    • 相比继承Thread类,实现Runnable接口的好处:

      • 避免了Java单继承的局限性
      • 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,
        较好的体现了面向对象的设计思想
    • 3.实现Callable接口

      • 属于JUC高并发领域
  • 如果想要执行线程,就必须调用start()方法,将线程加入到调度器中,此时线程不一定立即执行,具体是有系统安排调度分配执行,直接调用run()方法不是开启多线程,而是普通方法

线程分类

  • 线程分为用户线程守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕,如后台记录操作日志、监控内存使用等
  • 线程默认是用户线程

线程调度:

  • 线程调度有两种调度模型
    • 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
    • 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,
      优先级高的线程获取的CPU时间片相对多一些
  • Java使用的是抢占式调度模型
  • 假如计算机只有一个CPU,那么CPU在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,
    才可以执行执行指令,所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的
  • Thread类中设置和获取线程优先级的方法
    • public final int getPriority():返回此线程的优先级
    • public final void setPriority(int newPriority):更改此线程的优先级
    • 线程默认优先级是5, main线程的优先级也是5
    • 线程优先级从1 - 10,均为整数
    • 线程优先级高,仅仅代表线程获取到CPU时间片的几率高,但并不是一定会优先执行,只有在次数较多或者多次运行的时候才能体现出优先级高,线程就优先执行

静态代理

  • 我们在使用Runnable接口,实现多线程的时候,启动必须借助Thread对象,那么这个Thread对象就叫做代理对象
  • 静态代理 和 动态代理的区别
    • 静态代理的代理类是我们已经写好的,可以直接拿来用,静态代理中真实类 和 代理类需要实现相同的接口
    • 动态代理的代理类是在运行过程中动态构建出来的

JDK1.8新特性-Lambda表达式

  • Lambda表达式主要用于简化匿名内部类,它属于函数式编程的概念
  • 内部类是随着外部类的使用而加载,如果外部类不使用,内部类就不会加载,不会随着程序的加载而加载,也就不会编译
  • 静态内部类放在类中、局部内部类放在方法体中、匿名内部类放在方法参数中
  • 使用Lambda表达式时:要求接口中只能有一个抽象方法
  • 扩展
    • lambda 表达式引用方法
      • 有时候我们不是必须要自己重写某个匿名内部类的方法,我们可以可以利用 lambda表达式的接口快速指向一个已经被实现的方法
      • 语法:方法归属者::方法名 静态方法的归属者为类名,普通方法归属者为对象
      public class Exe1 {
          public static void main(String[] args) {
              ReturnOneParam lambda1 = a -> doubleNum(a);
              System.out.println(lambda1.method(3));
      
              //lambda2 引用了已经实现的 doubleNum 方法
              ReturnOneParam lambda2 = Exe1::doubleNum;
              System.out.println(lambda2.method(3));
      
              Exe1 exe = new Exe1();
      
              //lambda4 引用了已经实现的 addTwo 方法
              ReturnOneParam lambda4 = exe::addTwo;
              System.out.println(lambda4.method(2));
          }
      
          /**
           * 要求
           * 1.参数数量和类型要与接口中定义的一致
           * 2.返回值类型要与接口中定义的一致
           */
          public static int doubleNum(int a) {
              return a * 2;
          }
      
          public int addTwo(int a) {
              return a + 2;
          }
      } 
      

线程控制

方法名 说明
static void sleep(long millis) 使当前正在执行的线程停留(暂停执行)指定的毫秒数
void join() 等待这个线程死亡(如果某个线程调用了这个方法,那么其它的线程必须等待这个线程执行完毕才有机会执行)
void setDaemon(boolean on) 将此线程标记位守护线程,当运行的线程都是守护线程时,Java虚拟机将退出
  • 静态方法

    • static Thread currentThread():返回当前正在执行的线程对象
    • static void sleep(long millis): 使当前正在执行的线程停留(暂停执行)指定的毫秒数
  • 非静态方法

    • setName(String name):将此线程的名称更改为name,默认名称为Thread-i, i = 0, 1, 2…

    • getName():获取当前正在执行的线程的名称

    • setPriority(int newPriority):更改此线程的优先级

    • getPriority():返回此线程的优先级
      线程优先级的设定建议在start()调用前

    • run():封装线程执行的代码,如果直接调用run()方法,相当于调用一个普通方法

    • start():启动线程,然后由JVM调用此线程的run()方法

    • void join():插队,等待这个线程死亡(如果某个线程调用了这个方法,那么其它的线程必须等待这个线程执行完毕才有机会执行)

            myThread1.start();
            
            try {
                myThread1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            myThread2.start();
            myThread3.start(); 
    
    • void setDaemon(boolean on):将此线程标记位守护线程,当运行的线程都是守护线程时,Java虚拟机将退出

线程生命周期

Java多线程-详细_第1张图片

Java多线程-详细_第2张图片Java多线程-详细_第3张图片

  • 线程进入就绪状态的方法:
    • 1.线程调用start()方法
    • 2.阻塞解除(同步解除、调用notify唤醒线程、wait等待时间到了、join执行完了、sleep休眠时间到了、IO流阻塞结束)
    • 3.调用static yield()方法
    • 4.jvm本身将cpu从本地线程切换到其他线程
  • 线程进入阻塞状态的四种原因:
    • 1.调用static sleep()方法
    • 2.调用wait()方法:
    • 3.调用join()方法:插队,等待这个线程死亡(如果某个线程调用了这个方法,那么其它的线程必须等待这个线程执行完毕才有机会执行)
    • 4.I/O
  • 线程进入死亡状态:
    • 1.代码执行完了,正常结束
    • 2.这个线程被强制截止:stop()、destroy() 但是这两个方法不推荐使用,已经过时了,不安全

线程相关方法

  • 1.线程停止:stop()、destroy()

    • jdk不推荐使用这两个方法
    • 解决方案:提供一个boolean类型的终止变量,当这个变量置为false时,则线程终止运行
    class MyThread implements Runnable {
       /**
        *  线程类中定义线程体使用的标识
        */
       private boolean flag = true;
       @Override
       public void run() {
           /**
            *  2.线程体使用该标识
            */
           while (true) {
               System.out.println("线程正在运行");
           }
       }
    
       /**
        * 3.对外提供方法改变标识
        */
       public void stop() {
           this.flag = false;
       }
    } 
    
  • 2.static void sleep(long millis): 使当前正在执行的线程停留(暂停执行)指定的毫秒数

    • sleep()存在异常InterruptedException, 由于sleep()存在异常,但是run()方法不可以throws线上抛出异常,所以如果在run()方法体内调用sleep()方法,只能用try catch包裹
    • sleep()调用后线程进入阻塞状态
    • sleep()时间达到后,线程进入就绪状态
    • 每一个对象都有一个锁,sleep不会释放锁,sleep会占据资源,使其他线程也不能使用资源,直到时间结束后,才可以继续运行
    • 使用方法:Thread.sleep(millis)
  • 3.礼让:static void yield()

    • 让当前正在执行的线程暂停,注意不是阻塞线程,而是将线程从运行状态转入就绪状态
    • 礼让后,其他线程是可以继续使用资源的
    • 使用方法:Thread.yield()
    • 使用yield()后,该线程重回调度队列,与其它线程公平竞争
    • 礼让不是每次都一定成功,礼让有可能失败
  • 4.插队:join()

    • 插队,等待这个线程死亡(如果某个线程调用了这个方法,那么其它的线程必须等待这个线程执行完毕才有机会执行),此时其它的线程阻塞
  • 5.等待:wait()

    • wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。 “直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)
    • notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。
    • wait(long timeout)让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的notify()方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。
  • 6.Thread.activeCount():当前正在活动的线程数

  • 7.boolean isAlive():判断线程是否还活着,即线程是否还未终止

  • 8.setName(): 给线程取一个名字

  • 9.getName(): 获取线程名字

  • 10.currentThread():获取当前正在运行的线程对象

线程的6中状态

  • NEW:尚未启动的线程处于此状态
    • new Thread(),即处于新生状态的线程
  • RUNNABLE:在Java虚拟机中执行的线程处于此状态
    • 处于就绪状态运行状态的线程
  • BLOCKED:被阻塞,等待监视器锁定的线程处于此状态
    • wait()、 I/O
  • WAITING:正在等待另一个线程执行特定动作的线程处于此状态
    • wait(),with no timeout
    • join()
  • TIMED_WAITING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
    • static sleep(long millis)
    • join(long millis)
  • TERMINATED:已经退出的线程处于此状态
    • 处于死亡状态
    • 1.代码执行完了,正常结束
    • 2.这个线程被强制截止:stop()、destroy() 但是这两个方法不推荐使用,已经过时了,不安全

线程同步

  • 需求:某电影院目前正在上映国产大片,共有100张票,而它有三个窗口,请设计一个程序模拟该电影院买票
    • 1.定义一个类SellTicket实现Runnable接口,里边定义一个成员变量:private int tickets = 10;
    • 2.在SellTicket类中重写run()方法实现买票
      • A:判断票数大于0, 就卖票,并告知是哪个窗口卖的
      • B:买了票之后,总票数-1
      • C:票没有了,也可能有人来问,所以这里用死循环让卖票的动作一致执行
public class SellTicket implements  Runnable{
    private int ticket  = 100;

    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票");
                ticket--;
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        SellTicket sellTicket = new SellTicket();

        new Thread(sellTicket, "窗口A").start();
        new Thread(sellTicket, "窗口B").start();
        new Thread(sellTicket, "窗口C").start();
    }
}

  • 3.如果没出售一张票都需要一定的时间延迟,那么假设每次出票时间100毫秒,用sleep()方法实现
public class SellTicket implements  Runnable{
    private int ticket  = 100;

    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票");
                ticket--;
            }
        }
    }
}
public class Main {
    public static void main(String[] args) {
        SellTicket sellTicket = new SellTicket();

        new Thread(sellTicket, "窗口A").start();
        new Thread(sellTicket, "窗口B").start();
        new Thread(sellTicket, "窗口C").start();
    }
} 
 * 这样会出现票的重复卖出、卖出负数票的情况
  • 使用同步代码块解决上述出现的线程问题

    • 判断多线程程序出现数据安全问题的标准
    • 1.是否是多线程环境
    • 2.是否有共享数据
    • 3.是否有多条语句操作共享数据
  • 由于我们上述的案例满足了这三个要求,所以会出现安全问题

  • 只有三条都满足了,才会出现安全问题

  • 如何解决多线程安全问题?

    • 基本思想:让程序没有安全问题的环境
  • 如何实现?

    • 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
    • Java提供了同步代码块的方法来解决
  • 同步代码块:

    • 锁住操作共享数据的多条语句,可以使用同步代码块实现
    • 基本语法:
      synchronized(任意对象){
      多条语句操作共享数据的代码
      }
    • synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁
    • 用的对象不能是局部变量,因为我们要求每次用的锁是同一把锁
  • 同步的好处和弊端:

    • 好处:解决了多线程的数据安全问题
    • 弊端:当线程很多时,因为每个线程都回去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
  • 同步方法:就是把synchronized关键字加到方法上

    • 基本语法:
      修饰符 synchronized 返回值类型 方法名(方法参数) {
      多条语句操作共享数据的代码
      }
    • 同步方法的锁是:this
  • 同步静态方法:就是把synchronized关键字加到静态方法上

    • 基本语法:
      修饰符 static synchronized 返回值类型 方法名(方法参数) {
      多条语句操作共享数据的代码
      }
    • 同步静态方法的锁是:类的字节码文件,即类名.class
  • 线程安全的类:StringBuffer、Vector、Hashtable

  • 一般来说:多线程中使用StringBuffer,但是Vector和Hashtable在多线程中也不使用,

    • java.util.Collections类下:static List synchronizedList(List list):返回指定列表支持的同步(线程安全)列表
    • set 和 map同理
    • java.util.concurrent.CopyOnWriteArrayList;
    • CopyOnWriteArrayList securityList = new CopyOnWriteArrayList<>();// 得到一个线程安全的ArrayList,内部加了锁

锁机制的优缺点

  • 优点:

    • 同一进程的多个线程共享同一块存储空间时,加锁,保证了数据的正确性
  • 缺点:

    • 1.一个线程持有锁会导致其它所有需要此锁的线程挂起
    • 2.在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题
    • 3.如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
  • synchronized 方法控制对“成员变量|类变量”对象的访问:每个对象对应一把锁,每个synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
    缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。

  • 由于我们可以通过 private关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法synchronized块

  • 同步块可以粒度更小的锁定资源

  • 可重入锁:如果某个线程试图获取一个已经由它自己持有的锁时,那么这个请求会立即成功,并且会将这个锁的计数值加1,而当线程退出同步代码块时,计数器会递减,并且当计数值等于0时,机会释放锁。如果没有可重入锁的支持,在第二次企图获得锁时将会进入死锁状态

  • 可重入锁:某个线程已经获得某个锁,可以再次获取锁而不会出现死锁,而其他的线程是不可以的

  • synchronized 和 ReentrantLock 都是可重入锁。

  • 可重入锁的意义之一在于防止死锁。

  • 可重入锁的实现原理:

    • 通过为每个锁关联一个请求计数器一个占有它的线程
      当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1
      如果同一个线程再次请求这个锁,计数器将递增;
      每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放
  • 自定义不可重入锁:

public class LockTest {
    private MyLock myLock = new MyLock();

    public void a() throws InterruptedException {
        myLock.lock(); // (isLocked = false) -> myLock.lock() --> isLocked = true 
        doSomething();// isLocked = true --> myLock.lock()  --> wait() 于是本线程就一直处于阻塞状态,无法解除
        myLock.unlock();
    }

    public void doSomething() throws InterruptedException {
        myLock.lock();
        System.out.println("测试。。。。。");
        myLock.unlock();
    }

    public static void main(String[] args) throws InterruptedException {
        LockTest lockTest = new LockTest();
        lockTest.a();
    }
}

/**
 * 不可重入锁
 */
class MyLock{
    /**
     * 是否被持有
     * true:被持有 false:未被持有
     */
    private boolean isLocked = false;

    /**
     * 使用锁
     */
    public synchronized void lock() throws InterruptedException {
     while (isLocked) {
         wait();
     }
     isLocked = true;
    }

    /**
     * 释放锁
     */
    public synchronized void unlock() {
        isLocked = false;
        notify();
    }
}
                      
 
  • 自定义可重入锁:
public class LockTest1 {
    private MyLock1 myLock1 = new MyLock1();

    public void a() throws InterruptedException {
        myLock1.lock(); // (isLocked = false) -> myLock.lock() --> isLocked = true
        doSomething();// isLocked = true --> myLock.lock()  --> wait() 于是本线程就一直处于阻塞状态,无法解除
        myLock1.unlock();
    }

    public void doSomething() throws InterruptedException {
        myLock1.lock();
        System.out.println("测试。。。。。");
        myLock1.unlock();
    }

    public static void main(String[] args) throws InterruptedException {
        LockTest1 lockTest1 = new LockTest1();
        lockTest1.a();
    }
}

/**
 * 可重入锁
 */
class MyLock1{
    /**
     * 是否被持有
     * true:被持有 false:未被持有
     */
    private boolean isLocked = false;
    /**
     * 记录这个锁是否是当前线程中的,如果是,那就不等待
     */
    private Thread lockedBy = null;

    /**
     * 锁的计数器
     */
    private int holdCount = 0;

    /**
     * 使用锁
     */
    public synchronized void lock() throws InterruptedException {
        Thread thread = Thread.currentThread();
        while (isLocked && lockedBy != thread) {
            wait();
        }
        isLocked = true;
        lockedBy = thread;
        holdCount++;
    }

    /**
     * 释放锁
     */
    public synchronized void unlock() {
        if (Thread.currentThread() == lockedBy) {
            holdCount--;
            if (holdCount == 0) {
                isLocked = false;
                notify();
                lockedBy = null;
            }
        }
    }
} 
  • synchronized是可重入锁
  • ReentrantLock是可重入锁
  • 之前说的synchronized中套synchronized只是可能会发生死锁

锁的分类

  • 根据锁是否沿用:

    • 可重入锁
      • 公平锁
      • 不公平锁
    • 不可重入锁
  • 悲观锁:synchronized是独占锁即悲观锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁

  • 乐观锁:每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止

    • 比如更新数据,如果失败了就不断重试,直到成功为止
    • 现实中的秒杀,有一个version
  • Compare and Swap:比较并交换

  • 乐观锁的实现:

    • 有三个值:当前的内存值Version、旧的预期值A、可以更新的值B。
    • 操作:先获取到内存当中的内存值Version,再将内存值Version和原值A作比较,如果相等就可以修改B并返回true,
      否则,什么都不能做,并返回false
  • CAS是硬件级别的操作,(利用CPU的CAS指令,同时借助JNI来完成的非阻塞算法),效率要比加锁操作高

  • CAS是一组原子操作,不会被外部打断

  • 但是存在AVA的问题:如果变量Version初次读取的时候也是A,并且在准备赋值的时候检查到它仍然是A,
    那能说明它的值没有被其它线程修改过了吗?如果子啊这段期间曾经被改为B,然后又改回A,那CAS操作就会误认为它从来没有被修改过

Lock锁

  • 虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,
    在哪里释放了锁,为了更清晰的表达如何 加锁 和 释放锁,JDK1.5以后提供了一个新的锁对象Lock
  • Lock类是一个接口
  • Lock的实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
  • 其中的2个方法:
    • void lock():获得锁
    • void unlock():释放锁
    • ReentrantLock():无参构造方法
  • ReentrantLock类实现了Lock接口
 public class SellTicket implements  Runnable{
     private int ticket  = 100;
     private Lock lock = new ReentrantLock();
 
     @Override
     public void run() {
         while (true) {
             // 通过lock获得锁
             lock.lock();
             
             try {
 
                 if (ticket > 0) {
                     try {
                         Thread.sleep(100);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                     System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票");
                     ticket--;
                 }
             }finally {
                 
                 // 通过lock释放锁
                 lock.unlock();
             }
         }
     }
 }
public class Main {
    public static void main(String[] args) {
        SellTicket sellTicket = new SellTicket();

        new Thread(sellTicket, "窗口A").start();
        new Thread(sellTicket, "窗口B").start();
        new Thread(sellTicket, "窗口C").start();
    }
}

生产者消费者

  • 生产者消费者模式是一个十分经典的多线程协作的模式

  • 所谓生产者消费者问题,实际上主要包含了两类线程:

    • 一类是生产者线程,用于生产数据
    • 一类是消费者线程,用于消费数据
  • 为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库

    • 生产者生产数据之后直接放置公共数据区中,并不需要关心消费者的行为
    • 消费者只需要从共享数据区中去获得数据,并不需要关心生产者的行为
      生产者 ---------> 共享数据区域 <-------- 消费者
  • 为了体现生产和消费过程中的等待和唤醒,Java就提供了几个方法供我们使用功能。
    这几个方法在Object类中,Object类的等待和唤醒方法

    方法名 说明
    void wait() 导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法
    void notify() 唤醒正在等待对象监视的单个线程
    void notifyAll() 唤醒正在等待对象监视的所有线程
  • 生产者消费者案例中包含的类:

    • 奶箱类(Box):定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作
    • 生产者类(Producer):实现Runnable接口,重写run()方法,调用存储牛奶的操作
    • 消费者类(Customer):实现Runnable接口,重写run()方法,调用获取牛奶的操作
    • 测试类(BoxDemo): 里边有main()方法,main()方法中的代码步骤如下:
      • 1.创建奶箱对象,这是共享数据区域
      • 2.创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
      • 3.创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
      • 4.创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
      • 5.启动线程
  • 生产者消费者问题是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖、互为条件

    • 对于生产者,没有生产产品之前,需要通知消费者等待。而生产了产品之后,又需要马上通知消费者消费
    • 对于消费者,在消费之后,要通知生产者已经消费结束,需要继续产生新产品以供消费
    • 在生产者消费者问题中,仅有synchronized是不够的
      • synchronized可以阻止并发更新同一个共享资源,实现了同步
      • synchronized不能用来实现不同线程之间的消息传递(即通信)
  • 线程通信

    • 解决方式1:并发协作模型 “生产者/消费者模式” --> 管程法
      • 生产者:负责生产数据的模块(这里的模块可能是:方法、对象、线程、进程)
      • 消费者:负责处理数据的模块(这里的模块可能是:方法、对象、进程、线程)
      • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个"缓冲区";
        生产者将生产好的数据放入"缓冲区",消费者从"缓冲区"拿要处理的数据
      • 管程法:使用共享内存(缓冲区)的方式结局生产者并发协作,缓冲区看起来像一个管道,因此叫做管程法
    /**
     * 协作模型:生产者消费者实现方式-管程法
     * 角色:
     *   多线程生产者
     *   多线程消费者
     *   缓冲区
     *   数据
     * @author NiuQun
     * @date 2021/9/14
     */
    public class Main {
        public static void main(String[] args) {
            Container container = new Container();
            new Producer(container).start();
            new Consumer(container).start();
        }
    }
    
    /**
     * 多线程生产者
     */
    class Producer extends Thread {
        private Container container;
        private int produceDadaCount = 0;
    
        public Producer(Container container) {
            this.container = container;
        }
    
        /**
         * 持续生产数据
         */
        @Override
        public void run() {
            while (true) {
                synchronized (container) {
                    container.push(new Data(++produceDadaCount));
                    System.out.println("正在生产第" + produceDadaCount + "个数据");
                }
            }
        }
        /* 生产有限个数据
        @Override
        public void run() {
            // 生产
            for (int i = 0; i < 100; i++) {
                container.push(new Data(i));
                System.out.println("生产第" + i + "个数据");
            }
        }
         */
    }
    
    /**
     * 多线程消费者
     */
    class Consumer extends Thread {
        private Container container;
    
        public Consumer(Container container) {
            this.container = container;
        }
        /**
         * 持续消费数据
         */
        @Override
        public void run() {
            while (true) {
                synchronized (container) {
                    System.out.println("正在消费生产的第" + container.pop().getId() + "个数据");
                }
            }
        }
    
        /* 消费有限个数据
        @Override
        public void run() {
    
            // 消费有限个数据
            for (int i = 0; i < 1000; i++) {
                System.out.println("消费第" + container.pop().getId() + "个数据");
            }
        }
         */
    
    }
    
    
    
    /**
     * 缓冲区
     */
    class Container {
        /**
         * 存储数据的容器
         */
        private Data[] dataArr = new Data[10];
    
        /**
         * count是计数器,count指向下一个可以存储数据的位置
         */
        private int count = 0;
    
        /**
         * 存储数据
         * 容器存在空间时,才能生产
         * @param data
         */
        public void push(Data data) {
            if (count == dataArr.length) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 先在当前位置存,然后索引先后移动一位,所以count指向的是下一个可以存储数据的位置
            dataArr[count++] = data;
            notifyAll(); // 存在生产了,可以唤醒消费者
        }
    
    
        /**
         * 获取数据
         * 只有当容器中存在数据时才可以消费
         * 容器中没有数据时线程只能等待
         */
        public Data pop() {
            if (count == 0) {
                try {
                    wait(); // 此时线程阻塞 ,当生产者通知消费时,才能解除阻塞
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 由于count指向的是下一个可以存储数据的位置,所以count应当先减1,再取值
            Data data = dataArr[--count];
            notifyAll();// 存在空间了,可以通知生产者生产
            return data;
        }
    
        public int getCount() {
            return count;
        }
    
        public void setCount(int count) {
            this.count = count;
        }
    }
    
    
    /**
     *  数据
     */
    class Data {
        /**
         * id用于统计生产的数据总量
         */
        private int id;
    
        public int getId() {
            return id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        public Data(int id) {
            this.id = id;
        }
    } 
    
    • 解决方式2:并发协作模型 “生产者/消费者模式” --> 信号灯法
      • 信号灯法:借助标志位
      /**
       * 生产者
       * 消费者
       * 资源
       * @author NiuQun
       * @date 2021/9/15
       */
      public class Main {
          public static void main(String[] args) {
              Data data = new Data();
      
              new Producer(data).start();
              new Consumer(data).start();
          }
      }
      
      class Producer extends Thread{
          private Data data;
      
          public Producer(Data data) {
              this.data = data;
          }
      
          @Override
          public void run() {
              for (int i = 0; i < 20; i++) {
                  if (i % 2 == 0) {
                      data.setResource("偶数");
                  }else {
                      data.setResource("奇数");
                  }
              }
          }
      }
      
      class Consumer extends Thread{
          private Data data;
      
          public Consumer(Data data) {
              this.data = data;
          }
      
          @Override
          public void run() {
              for (int i = 0; i < 20; i++) {
                  data.getResource();
              }
          }
      }
      
      class Data {
          /**
           * resource代表存储的资源
           */
          private String resource;
          /**
           * signal为true表示生产者生产,消费者等待
           * signal为false表示生产者等待,消费者消费
           */
          private boolean signal = true;
      
          /**
           * 获取资源
           * @return
           */
          public synchronized String getResource() {
              // 1.(共享区域无资源时)消费者无法获取数据时
              // 生产者要生产资源,消费者等待
              if (signal) {
                  try {
                      wait();
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
      
      
              // 2.(共享区域有资源时)消费者可以消费
              System.out.println("消费者消费了" + resource);
              notifyAll();
              signal = true;
              return resource;
          }
      
          /**
           * 存储资源
           * @param resource
           */
          public synchronized void setResource(String resource) {
              // 1.(共享区域有资源时)生产者无法生产数据
              // 消费者要消费数据,生产者等待
      
              if (!signal) {
                  try {
                      wait();
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
      
              // 当signal = true时,生产者可以生产
              // 2.(共享区域无资源时)生产者可以生产数据
              this.resource = resource;
              System.out.println("生产者生产了" + resource);
              notifyAll();
              signal = false;
          }
      } 
      
  • 方法:

    方法名 作用
    void wait() 导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法,与sleep()不同,wait()会释放锁
    void wait(long timeout) 指定等待的毫秒数
    void notify() 唤醒一个处于等待状态的线程
    void notifyAll() 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度
  • 以上四个方法军事java.lang.Object类中的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常

  • wait()和sleep():

    • 调用wait()后,会释放锁,即该线程处于阻塞状态,但是他并不占用资源
    • 调用sleep()后,该线程处于阻塞状态,但是它依旧占用资源,他所占用的资源在sleep()时间未到之前,不能被其它线程使用

任务定时调度

  • 作用:可以指定在固定的时间点,执行代码
  • 通过Timer和TimeTask,我们可以实现定时启动某个线程
    • java.util.Timer:类似闹钟的功能,本身实现的就是一个线程
    • java.util.TimerTask:一个抽象类,该类实现了Runnable接口,所以该类具备多线程功能
  • Timer:
    • 线程调度任务 以供将来在后台线程中执行的功能,任务可以安排依次执行,或者定期重复执行
/**
 * 任务调度:Timer类和TimerTask抽象类
 * @author NiuQun
 * @date 2021/9/15
 */
public class MyTimerTest {
    public static void main(String[] args) {
        Timer timer = new Timer();
        // 执行安排,1秒后执行线程new MyTask()线程,之后每隔200毫秒执行一次
        // timer.schedule(new MyTask(), 1000);

        // 执行安排,1秒后执行new MyTask()线程,之后每隔200毫秒执行一次
        // timer.schedule(new MyTask(), 1000, 200);

        // 执行安排, 指定时间第一次执行new MyTask()线程,之后每隔200毫秒执行一次
        Calendar cal = new GregorianCalendar(2021, 12, 21, 21, 21, 21);
        timer.schedule(new MyTask(), new Date(5000), 200);

    }
}

/**
 * 任务类
 */
class MyTask extends TimerTask {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("放空大脑,休息一会儿");
        }
        System.out.println("end............");
    }
} 

任务定时调度框架QUARTZ

  • 该框架有四大部分组成:
    • Scheduler:调度器,控制所有的调度
    • Trigger:触发条件,采用DSL模式
    • JobDetail:需要处理的JOB
    • Job: 执行逻辑
  • QUARTZ已经集成到Spring中
  • DSL:Domain-specific language领域特定语言,针对一个特定的领域,具有受限表达性的一种计算机程序语言,
    即领域专用语言,声明式编程:
    • 1.Method Chaining 方法链 Fluent Style流畅风格, builder模式构建器
    • 2.Nested Function 嵌套函数
    • 3.Lambda Expressions/Closures
    • 4.Functional Sequence

volatile:

  • volatile:易变、可变:说的是变量在主内存和工作内存出现不一致的情况
  • 只要变量用volatile修饰,对变量的修改就会立刻写入到主内存当中
  • volatile保证线程间变量的可见性,简单地说就是当线程A对变量x进行了修改之后,
    在线程A后面执行的其他线程能看到变量x的变动,更详细地说是要符合以下两个规则:
    • 线程对变量进行修改之后,要立刻回写到主内存
    • 线程对变量读取的时候,要从主内存中读,而不是缓存
  • volatile不能保证原子性,但可以避免指令重排

为了解决缓存不一致性问题,通常来说有以下2种解决方法:

  • 1)通过在总线加LOCK#锁的方式
  • 2)通过缓存一致性协议
  • 这2种方式都是硬件层面上提供的方式。

死锁(DeadLock)

  • 概念:多个线程各自占有一些共享资源,并且互相等待其它线程占有的资源才能进行,
    而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有"两个以上对象的锁"时,
    就可能会发生死锁
  • 过多的同步可能造成相互不释放资源,从而相互等待,一般发生于同步中持有多个对象的锁,进而发生死锁
  • 解决死锁方案:不要在同一个代码块中同时持有多个对象锁

单例模式

  • 在多线程环境下,对外存在一个对象
  • 单例模式
    • 1.构造器私有化 --> 避免外部new构造器
    • 2.内部提供私有的静态属性 --> 存储对象的地址(这里如果直接new对象了,我们就称之为饿汉式,如果没有new对象就称之为懒汉式)
    • 3.提供公共的静态方法 --> 获取属性

ThreadLocal

  • 在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,
    因为全局变量只有线程自己才能看见,不会影响其它线程
  • ThreadLocal能够放一个线程级别的变量,其本身能够被多个线程共享使用,并且又能够达到线程安全的目的。
    说白了,ThreadLocal就是想在多线程的环境下去保证成员变量的安全,常用方法:get/set/initialValue
  • JDK建议ThreadLocal定义为private static
  • ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,身份信息等,
    这样一个线程的所有调用到的方法都可以非常方便地访问这些资源
  • 每个线程在ThreadLocal中都会有自己的一片区域,在每个线程中通过threadLocal.set(保存的对象)时,
    修改的只是ThreadLocal中自己的那个线程的那片区域中的存内容
  • ThreadLocal中的两种初始值设置方式是设置所有的线程在ThreadLocal中存储的初始值
  • ThreadLocal不会继承上下文环境的数据,也就是说A线程中创建了B线程,那么B线程在ThreadLocal中的初始值为最开始初始化的值,
    即使之前A线程该变量自身在ThreadLocal存储的值,B线程在ThreadLocal中的初始值并不会受影响
public class ThreadLocalTest {

    /**
     * 更改初始值方式1
     * 采用匿名内部类的方式
     */
    private static ThreadLocal threadLocal = new ThreadLocal(){
        @Override
        protected Integer initialValue() {
            return 200;
        }
    };

    /**
     * 更改初始值方式2
     * jdk1.8之后提供的:Lambda表达式
     */
    private static ThreadLocal threadLocal1 = ThreadLocal.withInitial(()->300);

    public static void main(String[] args) {
        // 300
        System.out.println(Thread.currentThread().getName() + "--->" + threadLocal.get());
        threadLocal.set(100);
        // 400
        System.out.println(Thread.currentThread().getName() + "--->" + threadLocal.get());

        new Thread(new MyRun()).start();
    }

    public static class MyRun implements Runnable {
        @Override
        public void run() {
           // threadLocal.set(400);
            // 400
            System.out.println(Thread.currentThread().getName() + "--->" + threadLocal.get()); // 200
        }
    }
} 
  • ThreadLocal上下文:
    • threadLocal.get()在构造器中时,哪里调用就属于哪里,要找线程体,注意new Thread(new MyThread())是属于调用线程的
    • threadLocal.get()在run方法内,取出的值是该线程自身的
  • InheritableThreadLocal: 是ThreadLocal的一个子类
    • 它会继承上下文环境的数据,也就是说A线程中创建了B线程,那么B线程在ThreadLocal中的初始值就和A一样
    • 只是初次的时候会拷贝,但并不是共享
public class InheritableThreadLocalTest {
    private static ThreadLocal threadLocal = new InheritableThreadLocal();

    public static void main(String[] args) {
        // null
        System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
        threadLocal.set(100);
        // 100
        System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());

        new Thread(()->{
            // 100
            System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
            threadLocal.set(200);
            // 200
            System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
        }).start();
    }
} 

你可能感兴趣的:(Java基础,java,linux)