Java并行程序基础

  1. 进程(Process)与线程(Thread) 

    1. 程序:是指令和数据的集合,其本身没有任何运行的含义,是一个静态的概念。
    2. 进程:是程序执行的一次过程,它是一个动态的概念,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。进程是线程的容器。进程是程序的实体。
    3. 线程:通常一个进程可以包含多个线程,一个进程至少包含一个线程。线程是CPU调度和执行的单位。轻量级进程,是程序执行的最小单位。
      1. 真正多线程是指有多个CPU,即多核。如果是模拟出来的多线程,即在一个CPU的情况下,在同一个时间段,CPU也只能执行一个代码,由于切换的速度很快,就让我们有了同时执行的错觉。
      2. Java中main()方法称之为主线程,为系统的入口,用于执行整个程序。
      3. 在一个进程中,如果开启了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为干涉的。
      4. 对同一份资源操作时,会存在资源抢夺的问题,需要假如并发控制。
      5. 线程会带来额外的开销,如CPU调度时间,并发控制开销。
  2. 线程的创建

    1. Thread
      1. 自定义线程类继承Thread
      2. 重写run()方法
      3. 创建线程对象,调用start()启动线程
      4. 不建议使用:避免OOP单继承局限性。因为Java是单继承的,所以继承也是一种宝贵的资源。
    2. Runnable
      1. 自定义线程类实现Runnable接口
      2. 重写run()方法
      3. 创建线程对象,通过Thread对象调用start()启动线程
      4. 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多线程使用
    3. Callable
      1. 实现Callable接口,需要返回值类型
      2. 重写call方法,需要抛出异常
      3. 创建目标对象
      4. 创建执行服务:ExecutorService ser = Executors.newFixedTreadPool(1);
      5. 提交执行:Future<Boolean> result = ser.submit(t1);
      6. 获取结果:boolean res = result.get();
      7. 关闭服务:ser.shundownNow();
    4. Lambda
      1. 函数式接口:任何接口,如果只包含一个抽象方法,那么它就是一个函数式接口
      2. 例如Runnable接口
        1. public interface Runnable {
              public abstract void run();
          }
        2. 对于函数式接口,可以通过lambda表达式来创建该接口的对象
      3. 类内部可以有静态内部类(在方法外定义),局部内部类(方法内定义),匿名内部类(直接重写接口或者抽象类的方法)。
      4. lambda可以避免匿名内部类定义过多。
  3. 线程的5大状态

  4. 线程的终止

    1. stop()方法是被废弃的方法,因为它会强行把执行到一半的线程终止,可能会引起一些数据的不一致问题。
      1. 比如说在处理用户记录的时候,有两条记录
        1. ID = 1, Name = 小明
        2. ID = 2, Name = 小王
      2. 如果用User对象进行保存,希望只保存要不记录1,要不记录2,。如果调用了stop()方法,就可能导致ID = 1,Name = 小王。这种现象出现。那么如果保存进行数据库,后果将不堪设想。
      3. stop()方法会直接终止线程,并且会立即释放这个线程所持有的的锁。而这些锁恰恰是用来维持对象一致性的,如果此时,写线程吸入数据正写到一半,并强行终止,那么对象就会被写坏,同时,由于锁已经被释放,另外一个等待该锁的读线程就顺理成章的读到这个不一致的对象。
    2. 一般终止可以通过设置一个boolean的标志,来控制线程是否继续执行下去。或者等待线程自动执行完。
  5. 线程的中断

    1. 线程中断是一种重要的线程协作机制。
    2. 中断并不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出啦!至于目标线程接到通知后如何处理,则完全由目标线程自行决定。这点很重要,否则无条件的退出,跟stop()方法就没什么两样了。
    3. 与线程中断有以下三个方法
    4. public void Thread.interrupt()              //中断线程
      public void Thread.isInterrupted()          //判断是否被中断
      public static boolean Thread.interrupted()  //判断是否被中断,并清除当前中断状态
    5. interrupt()方法是一个实例方法。它通知目标线程中断,也就是设置中断标志位。中断标志位表示当前线程已经被中断了。
    6. isInterrupted()方法也是实例方法,它判断当前线程是否有被中断(通过检查中断标志位)
    7. 静态方法interrupted()也是用来判断当前线程的中断状态,但同时会清除当前线程的中断标志位状态
    8. public static void main(String[] args) throws InterruptedException {
              Thread t1 = new Thread(()->{
                 int i = 1;
                 while (true) {
                     if (Thread.currentThread().isInterrupted()) {
                         System.out.println("Interrupt!");
                         break;
                     }
                     System.out.println("I'm working " + i++);
                     Thread.yield();
                 }
              });
      
              t1.start();
              Thread.sleep(2000);
              t1.interrupt();
          }
    9. 这看起来跟前面增加标志位的手法非常相似,但是中断的功能更为强劲。比如,如果在循环体中,出现了类似于wait()或者sleep()这样的操作,则只能通过中断来识别了。
  6. 线程的睡眠sleep

    1. public static native void sleep(long millis) throws InterruptedException;
    2.  该方法会让当前线程休眠若干时间,会抛出InterruptedException中断异常。InterruptedException不是运行时异常,也就是程序必须捕获并且处理它,当线程在sleep休眠时,如果被中断,这个异常就会发生。
    3. public static void main(String[] args) throws InterruptedException {
              Thread t1 = new Thread(()->{
                 while (true) {
                     if (Thread.currentThread().isInterrupted()) {
                         System.out.println("Interrupt!");
                         break;
                     }
      
                     try {
                         Thread.sleep(2000);
                     } catch (InterruptedException e) {
                         System.out.println("Interrupted When Sleep");
                         Thread.currentThread().interrupt();
                     }
                     Thread.yield();
                 }
              });
      
              t1.start();
              Thread.sleep(1000);
              t1.interrupt();
          }
    4. 如果在sleep的时候,线程被中断,则程序会抛出异常,并进入异常处理。在catch字句里,由于已经捕获了中断,我们可以立即退出线程,但是并没有这么做。因为也许在这段代码中,还必须进行后续的处理,保障数据的一致性和完整性。因此,执行了interrupt()方法再次中断自己,置上中断标志位。只有这么做,在检查isInterrupted(),才能发现当前线程已经被中断了。可以试一下将catch的interrupt注释掉进行验证。
    5. Thread.sleep()方法由于中断而抛出异常,此时,它会清除中断标记,如果不加处理,那么在下一次循环开始时,就无法不会这个中断,所以在异常处理中,再次设置中断标志位。
  7. 等待(wait)和通知(notify)

    1. wait和notify不是在Thread类中的方法,而是在Object类中,意味着任何对象都能调用这两个方法。
    2. 如果一个线程调用了wait()方法,那么它就会计入object对象的等待队列。这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待同一个对象。当notify()被调用是,它就会从这个等待队列中,随机选择一个线程,并将其唤醒。但是这个选择不是公平的,并不是先等待的线程会优先被选择,这个选择完全是随机的。
    3. notifyAll()方法会唤醒这个等待队列的所有线程。
    4. 无论是wait()或者是notify()方法,必须包含在对应的synchronized语句中,无论是wait()或者notify()都需要首先获取目标对象的一个监视器。
    5. 而wait()方法执行后,会释放这个监视器,当被重新notify()后,要做的第一件事不是继续执行后续的代码,而是要尝试重新获取object的监视器。如果暂时无法获得,线程还必须要等待这个监视器。当监视器顺利获得后,才可以真正意义上的继续执行。
    6. wait()方法和sleep()的区别就是,wait会释放对象的锁,而sleep不会释放锁。
  8. 挂起(Suspend)和继续执行(resume)

    1. 被挂起的线程必须要等到resume操作后,才能继续指定、
    2. 但是已经被标注为废弃方法,不推荐使用。因为suspend()在导致线程暂停的同时,并不会去释放任何资源。此时,任何线程想要访问被它暂用的锁,都会备受牵连,导致无法正常运行。直到对应的线程上进行了resume()操作,被挂起的线程才能继续操作。但是如果resume操作在suspend之前就执行了,那么被挂起的线程就很难有机会被继续执行了。
    3. 如果想要实现suspend跟resume,可以通过wait跟notify进行使用。
  9. 等待线程结束(join)和谦让(yield)

    1. 一个线程的输入可能非常依赖于另外一个或者多个线程的输出,所以,这个线程就需要等待依赖线程执行完毕,才能继续执行。
    2. public final void join() throws InterruptedException
      public final synchronized void join(long millis) throws InterruptedException 
    3. 第一个join()方法表示无限等待,它会一直阻塞当前线程,知道目标线程执行完毕。
    4. 第二个join()给出了一个最大等待时间,如果超过给定时间目标线程还在执行,当前线程也会因为“等不及”,而继续往下执行。
    5. join就是加入的意思,因此一个线程要加入另外一个线程,那么最好的方法就是等着它一起走
    6. public class JoinMain {
          public volatile  static int i = 0;
          public static class AddThread extends Thread {
              @Override
              public void run() {
                  for (i = 0; i < 1000000; i++);
              }
          }
      
          public static void main(String[] args) throws InterruptedException {
              AddThread at = new AddThread();
              at.start();
              at.join();
              System.out.println(i);
          }
      }
    7. 主函数中,如果不用join()等待AddThread,那么得到的i很可能是0或者一个非常小的数字。因为AddThread还没执行完,i的值就已经被输出了。但使用join方法后,表示主线程愿意等待AddThread执行完毕,跟着AddThread一起往前走,所以在join()返回,AddThread已经执行完成,故i总是1000000;
    8. join的本质是让调用线程wait()在当前线程对象实例上。
    9. if (millis == 0) {
         while (isAlive()) {
            wait(0);
         }
      } 
    10. 可以看到,它调用线程在当前线程对象上进行等待。当执行完成后,被等待的线程会在退出前调用notifyAll()通知所有的等待线程继续执行。
    11. 因此需要注意,不要在应用程序中,在Thread对象上使用类似wait()或者notify()等方法,因为这很有可能影响系统API的工作,或者被系统API所影响。
    12. public static native void yield();
    13. yield()这是个静态方法。一旦执行,它会使当前线程让出CPU。当前线程让出CPU后,还会进行CPU资源的争夺,但是是否能被再次分配到,就不一定了。
  10. ​​​​​​​volatile与Java内存模型(JMM)

    1. ​​​​​​​Java内存模型都是围绕着原子性,有序性,可见性展开。
    2. Java使用了一些特殊的操作或者关键字来什么,告诉虚拟机,在这个地方,尤其注意,不能随意变动优化目标指令volatile就是其中之一。
    3. volatile:易变的,不稳定的。
    4. 当volatile去申明一个变量,就等于告诉虚拟机。这个变量极有可能会被某些线程修改。为了确保这个变量被修改后,应用程序范围内所有线程都能够“看到”。虚拟机就必须采用一些特殊的手段,保证这个变量的可见性等特点。
    5. volatile并不能替代锁。也无法保证一些符合操作的原子性。volatile无法保证i++原子性操作。
    6. volatile能保障数据的可见性和有序性。
  11. ​​​​​​​守护线程(Daemon)

    1. ​​​​​​​守护线程是一种特殊的线程,是系统的守护者,在后台默默地完成一些系统性的服务。比如垃圾回收线程,JIT线程就可以理解为守护线程。
  12. 线程的优先级

    1. Java使用1~10表示线程优先级。数字越大优先级越高。​​​​​​​
  13. 同步方法以及同步块

    1. 线程同步

      1. 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保障数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后是否锁即可。但是存在以下问题:
        1. 一个线程持有锁会导致其他所有需要此锁的线程挂起。
        2. 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
        3. 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先倒置,引起性能问题。
      2. 关键字synchronized的作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性。
        1. 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
        2. 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
        3. 直接作用于静态方法:相当于对当前类加锁,进入同步代码块要获得当前类的锁。
      3. synchronized除了保证线程同步,还可以保证线程之间的可见性和有序性。从可见性上来说,synchronized可以完全代替volatile,只是使用上没那么方便。就有序性而言,由于synchronized限制每次只有一个线程可以访问同步块,无论同步块内的代码如何被乱序执行,只要保证串行语义一致性,那么执行结果总是一样的。
    2. 同步方法

      1. 可以通过private关键字来保证数据对象只能被方法访问,所以只需要针对方法提出一套机制,这套机制就是synchronized关键字。有synchronized方法synchronized块
      2. synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就会独占该锁,直到方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
        1. 缺陷:若将一个大的方法申明为synchronized将会影响效率。
    3. 同步块

      1. 同步块:synchronized(obj){}
      2. Obj 称之为 同步监视器
        1. obj可以使任何对象,推荐使用共享资源作为同步监视器
        2. 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class
      3. 同步监视器的执行过程
        1. 第一个线程访问,锁定同步监视器,执行其中代码
        2. 第二个线程访问,返现同步监视器被锁定,无法访问
        3. 第一个线程访问完毕,解锁同步监视器
        4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
  14. 死锁

    1. 多个线程各自占有一些资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方资源释放,都停止执行的情形,某一个同步块同时拥有“两个以上对象的锁”时,就可能发生“死锁”
    2. 产生死锁的四个必要条件:
      1. 互斥条件:一个资源每次只能被一个进程使用
      2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
      3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
      4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
      5. 只要破坏其中任意一个或多个条件就可以避免死锁发生
  15. ​​​​​​​Lock(锁)

    1. java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
    2. ReentrantLock(可重入锁),它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁,释放锁。
  16. synchronzied跟Lock对比

    1. Lock是显示锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,除了作用域自动释放
    2. Lock只有代码块锁,synchronized有代码块锁和方法锁
    3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性
    4. 优先使用顺序
      1. Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)
  17. 线程协作(生产者与消费者模式)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值