Java多线程以及锁

这个领域之前有涉足过,但是由于要消化的东西过多,因此难以理解。现在就要必须强制自己做理解了。
reference: https://uzshare.com/view/824323#_390

首先,理解进程与线程的区别:
一个进程可以包含多条线程,一条线程一定在一个进程里面。所以进程就是正在执行的程序,而线程就是这个程序里面的一个操作。

那么 创建线程有哪些方式呢?

  1. 继承Thread父类
  2. 实现Runnable接口
  3. 实现Callable接口

虽然不太懂 但是上述三种创建线程之间有什么区别呢?
1.第一种和第二种的区别
java支持单继承,多实现。实现Runnable接口还可以继承其他类,而使用继承Thread就不能继承其他类了。所以当你想创建一个线程又希望继承其他类的时候就该选择实现Runnable接口的方式。
2.第二种和第三种的区别
Callable执行的方法是call() ,而Runnable执行的方法是run()。
call() 方法有返回值还能抛出异常, run() 方法则没有没有返回值,也不能抛出异常。

简述一下多线程的优缺点:
优点:提高CPU的使用率,提高程序的工作效率)
缺点:线程数量太多会影响性能,因为:CPU需要在他们之间切换,而且大量线程会占用更多的内存空间,而且多线程会带来线程安全以及死锁的问题

多线程分为两种,是哪两种呢?
并行和并发,并行是多个处理器同时执行不同任务,是真正意义上平行。而并发是一个处理器处理多个任务,需要经常在线程之间切换。

能简单的谈一下什么是线程池吗?
首先 我们要知道为什么引入线程池这个概念:
线程的创建和销毁是非常耗CPU和内存的,因为它涉及到要与操作系统进行交互。这样就可能导致创建与销毁线程的开销比实际业务还大,而线程池就能很好的解决这些问题。
那么线程池是如何解决这些问题的呢?
线程池里的每一个线程结束后,并不会销毁(可以设置超时销毁),而是回到线程池中成为空闲状态,等待下一个对象来使用。

那么线程池有哪些总类呢?
newFixedThreadPool:创建固定大小的线程池,这样可以控制线程最大并发数,超出的线程会在队列中等待。如果线程池中的某个线程由于异常而结束,线程池则会再补充一条新线程。
newSingleThreadExecutor创建一个单线程的线程池,即这个线程池永远只有一个线程在运行,这样能保证所有任务按指定顺序来执行。如果这个线程异常结束,那么会有一个新的线程来替代它。
newCachedThreadPool: 创建带有缓存的线程池,在执行新的任务时,当线程池中有之前创建的可用线程就重用之前的线程,否则就新建一条线程。如果线程池中的线程在60秒未被使用,就会将它从线程池中移除。
newScheduledThreadPool: 创建定时和周期性执行的线程池。

进一步缩小范围,能说一下一般线程池这个概念中的比较重要的类吗?
Executor:java中线程池的顶级接口,可以称它为一个执行器,通过查看源码也知道,他只有一个简单的方法execute(Runnable command),就是用来执行提交的任务。
ExecutorService:Executor的子类,也是真正的线程池接口。它提供了提交任务和关闭线程池等方法。调用submit方法提交任务还可以返回一个Future对象,利用该对象可以了解任务执行情况,获得任务的执行结果或取消任务。
Executors:由于线程池配置比较复杂,自己配置的线程池可能性能不是最好的。Executors就是用来方便创建各种常用线程池的工具类。
ThreadPoolExecutor:ExecutorService的默认实现,Executors创建各种线程池的时候内部其实就是调用了ThreadPoolExecutor的构造方法。

能进一步讲一讲ThreadPoolExecutor吗?
对其构造参数进行进一步说明:

    public ThreadPoolExecutor(int corePoolSize, //核心线程数,如果运行的线程数少于corePoolSize,当有新的任务过来时会创建新的线程来执行这个任务,即使线程池中有其他空闲的线程。
                              int maximumPoolSize, //线程池中允许的最大线程数。
                              long keepAliveTime, //如果线程数多于核心线程数,那么这些多出来的线程如果空闲时间超过keepAliveTime将会被终止。
                              TimeUnit unit, //keepAliveTime参数的时间单位。
                              BlockingQueue<Runnable> workQueue, //任务队列,通过线程池的execute方法会将任务Runnable存储在队列中。
                              ThreadFactory threadFactory, //线程工厂,用来创建新线程。
                              RejectedExecutionHandler handler) { //添加任务出错时的策略捕获器,默认是ThreadPoolExecutor.AbortPolicy ,即添加任务出错就直接抛出异常 。
    }

接下来 要讲一个更加深入的东西,比如说:
现成的调度,线程的生命周期,线程同步,线程的同步

线程的调度:
线程调度是指按照特定机制为多个线程分配CPU的使用。
调度策略:如果同一个优先级,那么久使用时间片策略,利用队列
如果不是一个优先级,那就高优先级优先。
有一点重要的要补充:** 高优先级的程序要抢占低优先级CPU的执行权,但只是从概率上讲,高有优先级的程序高概率被执行。并不意味着只有当高优先级执行完之后才执行低优先级。**

那么线程有哪些分类呢?
Java中的线程分为两类:
    1. 一种是守护线程。
    2. 一种是用户线程。
它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。

  1. 守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。 Java垃圾回收就是一个典型的守护线程。
  2. 前台线程死亡后,JVM会通知后台线程死亡,但从它接收到指令到做出相应,需要一定的时间。如果要将某个线程设置为后台线程,就是必须在该线程启动之前设置,即,设置必须在start()方法调用之前。否则会报异常。
    3.若JVM中都是守护线程,当前JVM将退出

详细的讲一下线程的生命周期?
线程的生命周期包含五种状态,分别是:新建,就绪,运行,阻塞,死亡。
下面分别来讲:

  1. 新建:当我们使用new关键字创建了一个线程之后,该线程就处于新建状态,此时,它和普通java对象一样,有JVM分配内存,仅仅是一个具有特殊名字的java对象。(人类的出生)
    2.就绪:当线程对象调用start()方法后,该线程就就处于就绪状态,但它不会立即运行,它会等待CPU的调度,然后才开始运行,当一个对象处于就绪状态时,只表示它当前可以运行了。(大学毕业)
    3.运行:就恰好对应我们大学毕业找到了一份好工作,在勤勤恳恳的努力着。
    4.阻塞:对应到我们身上就是失业的时候,线程就是在等CPU的重新调度或其他线程的帮助。
    进一步的补充说明:一个处于就绪状态的线程,在获得CPU的执行权时,开始执行它的run()方法,这时,该线程就处于运行状态。然而一个线程不可能一直处于运行状态,CPU会执行线程调度,在每个线程之间轮换执行。当正在执行的线程,有其他线程调度进来时,该线程就处于阻塞状态。
    当发生如下情况时,线程会进入阻塞状态。
      ① 线程调用sleep()方法时,表示当前线程主动放弃CPU的执行权
      ② 当前的同步监视器被其他线程获得,该线程只能处于阻塞状态。
      ③ 当前线程在等待某个线程的notify().
    对于以上情况,在阻塞结束时,该线程会重新进入就绪状态。
      ① sleep()指定时间已过
      ② 线程成功获得了同步监视器
      ③ 线程得到了其他线程的通知。
    5.线程死亡
    线程在以下情况下会死亡:
      ① run()方法或call()方法执行完毕
      ② 线程执行过程中调用的stop()方法,强制让该线程死亡,但该方法容易发生死锁。
      ③ 线程捕获了一个异常。

使用isAlive()方法检测一个线程是否死亡,当线程处于新建和死亡两种状态时,该方法返回false,当线程处于就绪、运行、阻塞状态时,该方法返回true。

总结一下:
在这里插入图片描述

接下来是另外一节:
线程同步:
为什么要同步?因为当多个线程同时操作一个共享数据的时候,会发生线程安全问题,比如说多人同时买一张火车票。
那么如何解决这个问题呢?
有三种方法,分别是:
  ① 同步代码块
  ② 同步方法
  ③ Lock锁

同步代码块:
就是把要共享数据的代码放在带有 同步 关键字的代码块中:

synchronized(obj){
      //需要被同步的代码,即:操作共享数据的代码
}

也就是说,同一时间只有一个线程在操作共享数据。就相当于把共享数据锁了起来。同一个时间只能有一个线程获得这个锁的钥匙。
注意:
  ① 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
  ② 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),this的使用需谨慎,确保是同一个对象才可以。
  ③ 在实现Runnable接口创建多线程的方式中,我们可以使用this充当锁来代替手动new一个对象,因为后面我们只创建了一个线程的对象。
  ④ 在继承Thread类创建多线程的方式中,慎用this,考虑我们的this是不是唯一的。
但是现在我们想一下,线程的确是安全了,但是每个时间点只有一个线程参与,其他线程等待 相当于单线程。

那么在实际过程中 会出现什么呢?
如何找问题,即代码是否存在线程安全?(非常重要)
  ① 明确哪些代码是多线程运行的代码。
  ② 明确多个线程是否有共享数据。
  ③ 明确多线程运行代码中是否有多条语句操作共享数据
如何解决呢?(非常重要)
  对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即:所有操作共享数据的这些语句都要放在同步范围中
切记:
  ① 范围太小:没锁住所有有安全问题的代码
  ② 范围太大:没发挥多线程的功能

同步方法:
与同步代码块相对应的,是同步方法。使用synchronized关键字修饰操作共享数据的的方法。该方法就被称为同步方法。即:如果操作共享数据的代码完整的声明在一个方法中,我们可以将这个方法声明为同步的。 也就是说 我们直接把这个同步方法写在类里面。简而言之,我们使用同步方法来解决懒汉式创建单例对象的线程安全问题。

class Bank{
    //无参构造器
    private Bank()
    {}
    //缓存单例类的对象
    private static Bank instance=null;
    //同步方法
    public static synchronized Bank getInstance()
    {
        //表示还没有创建单例对象
        if(instance==null)
        {
            instance = new Bank();
        }
        return instance;

    }
}

第三种方法 Lock锁
这是一种通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当,每次只能有一个线程对Lock对象加锁,线程开始操作共享数据之前,要先获得Lock对象。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
使用ReentrantLock对象来充当锁进行同步时,我们建议把释放锁的操作放在finally语句块中,确保一定会释放锁,防止死锁的出现。
Lock锁也遵循“加锁–修改–释放锁”的逻辑。

那么不管是不是真的理解了上面的方法,现在我们要对比上述三种方法:
首先总结一下:
解决线程安全问题,有几种方式?
    ① synchronized
      同步代码块
      同步方法
    ② Lock
那么,synchronized 与 Lock 有什么区别?
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
Lock只有代码块锁,synchronized有代码块锁和方法锁
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

下面进入最后一个专题 线程死锁
  不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续类似与死循环。

如何解决线程死锁的问题?
**使用定时锁:**使用Lock对象加锁是,可以指定当前线程的锁定时间,超过该时间后,当前线程的同步监视器被自动释放。
避免多次锁定:在编写程序时,要避免同一个线程对多个同步监视器的锁定。
尽量避免嵌套同步:在一个线程的同步中,尽量避免对另外一个线程的同步。
使用相同的加锁顺序;如果需要多个线程对多个同步监视器进行锁定时,尽量保证他们具有相同的加锁顺序。

最后一块专题:
线程之间是如何进行通信的?
当执行多线程程序时,CPU会在多个线程之间进行调度,而调度具有一定的随机性,我们无法在程序中准确控制线程之间的轮换执行。但是,当我们开发时,会经常需要多个模块之间的协调,比如A模块需要B模块给出一个参数,这时,在执行A模块时,就必须转去执行B模块,这就是线程之间的通信。
线程通信的常用方法:
wait(); 、 notify();、notifyAll();
wait();是调用其的线程进入阻塞状态。并释放锁。
notify();唤醒被阻塞的线程。如果有多个线程,就唤醒优先级高的那个。
notifyAll();唤醒所有被阻塞的线程。
在使用上述三个方法时 要注意以下几点:

  1. 上面三个方法只能出现在同步方法或同步代码块中。不能在lock中。
  2. 这三个方法是定义在Object类中。
  3. 这三个方法的调用者必须是同步方法或同步代码块的同步监视器(锁),否则,会出现非法监视器的错误。

此外 还有sleep(),这个方法和wait()方法一样,都可以让当前线程处于阻塞状态。不同点是sleep()是用在Tread中的,而且调用的范围不一样。sleep()方法可以在任意需要的位置调用。wait()方法必须使用在同步代码块或同步方法中。

posted @ 2020-05-07 05:12  EvanMeetTheWorld  阅读(29)  评论(0)    收藏  举报