Java--彻底搞懂线程

线程

作者:CSDN 双鱼座的老王

目录

线程

1.多线程

1.1 什么是进程?

1.2 对于java程序来说,当在DOS命令窗口中输入:

1.3 进程和线程的关系?

1.4 分析一个问题:对于单核的CPU来说,真的可以做到真正的多线程并发吗?

1.5 java语言中,实现线程的两种方式

1.6 线程的生命周期

1.7 获取线程的名字

1.8 关于线程的sleep方法

1.9 终止线程的睡眠。

1.10 强行终止线程的方法

1.11 线程的调度概况

2.多线程的线程安全问题

2.1 为什么安全是重点?

2.2 什么是线程不安全?

2.3 怎么解决安全问题?

2.4 两个专业术语

2.5 线程不安全的模拟演示

2.6 对于synchronized的理解

2.7总结:synchronized有三种写法:

3 锁

3.1 死锁现象

3.2 聊一聊,开发中如何解决线程安全问题

4.守护线程

5.定时器

6.实现线程的第三种方式

7.关于Object类中的wait和notify方法

8.生产者和消费者模式!

9.线程池

9.1池化思想

9.2 线程池的使用

9.3 线程池的工作原理

9.4功能线程池

9.5 对比线程池

9.6 总结

10.Lock接口

10.1重入锁:

10.2 读写锁:

11.线程安全的集合


1.多线程

1.1 什么是进程?

进程是一个应用程序。(一个软件)

线程是一个进程的执行场景/执行单元。

一个进程可以启动多个线程

进程:

1.2 对于java程序来说,当在DOS命令窗口中输入:

java HelloWorld 回车之后

会先启动JVM,而JVM就是一个进程。

JVM再启动一个主线程调用main方法,同时再启动一个垃圾回收线程负责看护,回收垃圾,那么此时,java程序中,至少有两个线程并发,一个为垃圾回收线程,一个为执行main方法的主线程

1.3 进程和线程的关系?

举个例子,淘宝是阿里巴巴旗下的产品

那么如果阿里巴巴是进程,那么阿里巴巴的员工就为线程,马云是阿里巴巴的线程,前台也是一个线程

如果京东是一个进程,那么刘强东就是京东的一个线程,奶茶妹妹就是京东的一个线程。

进程可以看作是现实生活中的公司。线程可以看作是公司当中的某个员工

注意:进程A和进程B的内从独立不共享(阿里巴巴和京东不会共享资源!!)

but!同一进程下的线程A和线程B,在Java中,线程A和线程B在堆内存和方法区内存共享。但是栈是独立的。

lol是一个进程,网易云音乐是一个进程,他们不共享资源,但是,你在打lol的时候一边打游戏一边电竞钢琴家,那么他们是共享资源的,假如启动10个线程,会有10个栈空间,每个栈和每个栈之间互不干扰,各自执行各自的,这就是多线程并发。

Java中之所以有多线程机制,目的就是为了提高程序的处理效率

1.4 分析一个问题:对于单核的CPU来说,真的可以做到真正的多线程并发吗?

什么是真正的多线程并发?

T1线程执行T1,T2线程执行T2,互不干扰,叫做真正的多线程并发。

对于单核的CPU来说,他只有一个大脑,所以,单核CPU无法做到真正的多线程并发,但是可以到给人一种多线程并发的感觉。(执行的快,例如胶卷电影,超过60帧就觉得很流畅)

1.5 java语言中,实现线程的两种方式

java支持多线程机制,并且java已经将多线程实现了,我们只需要继承即可

第一种方式:

编写一个类,直接继承java.lang.Thread 然后重写run方法

 //实现线程的第一种方式:编写一个类,直接继承java.lang.Thread 然后重写run方法
  ​
  public class xiancheng {
      public static void main(String[] args) {
          //怎么创建线程对象?怎么启动线程?
          //这里是main方法,这里的代码属于主线程,在主栈中运行
          //在这里要新建一个分支对象
          MyThread myThread = new MyThread();
          //启动线程 myThread.start();
          //start的作用是,在JVM中开辟一个新的栈空间,这段代码在瞬间就结束了,
          //这段代码的任务只是为了开空间,开出来就结束,线程就启动成功了
          //启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部,main在主栈的栈底部,双方平级
          myThread.start();
  ​
          //下面的代码还在主线程中运行
          for (int i = 0; i < 1000; i++) {
              System.out.println("mian线程--->"+i);
          }
      }
  }
  ​
  class MyThread extends Thread{
      @Override
      public void run() {
          //编写程序,这段程序运行在分支栈中
          for (int i = 0; i < 1000; i++) {
              System.out.println("线程分支--->"+i);
          }
      }
  }
  ​
run方法的内存图如下:

第二种方式

编写一个类,实现java.lang.Runnable接口,实现run方法

//实现多线程的第二种方式,编写一个实现类继承Runnable接口
  public class dierzhong {
      public static void main(String[] args) {
          //先创建线程类,此时还并不是一个线程
          MyRunnable myRunnable = new MyRunnable();
          //封装为线程类
          Thread thread = new Thread(myRunnable);
          //启动线程
          thread.start();
          for (int i = 0; i < 1000; i++) {
              System.out.println("mian线程--->"+i);
          }
      }
  }
  ​
  //这并不是一个线程类
  class MyRunnable implements Runnable{
  ​
      @Override
      public void run() {
          for (int i = 0; i < 1000; i++) {
              System.out.println("线程分支--->"+i);
          }
      }
  }

那么问题来了,我们平时更喜欢用哪种方法呢?

答案是接口类多,原因在于,java是单继承,如果我使用了继承方法,那么该类只能继承这个线程类,所以我们推荐接口,不耽误这个方法继承别的类。

由此延申,我们可以试着采用匿名内部类。

  public class niming {
      public static void main(String[] args) {
          //创建线程对象,采用匿名内部类方式
          Thread thread = new Thread(new Runnable() {
              @Override
              public void run() {
                  for (int i = 0; i < 1000; i++) {
                      System.out.println("线程分支--->"+i);
                  }
              }
          });
          thread.start();
          for (int i = 0; i < 1000; i++) {
              System.out.println("mian线程--->"+i);
          }
      }
  }

1.6 线程的生命周期

1.7 获取线程的名字

1.怎么获取当前线程的对象?

这玩意以后要用!!

什么是当前线程对象?在java.lang包下有一个Thread类中有一个currentThread方法

  
  static Thread.currentThread();
  //返回值t就是当前的线程
  Thread t = Thread.currentThread();
  //这段代码出现在哪,得到的就是哪个线程。

2.怎么获取线程对象的名字?

  
  线程对象.getName();

3.修改线程对象的名字?

  
  线程对象.setName("修改后的名字");

注意,每个线程都有默认的线程名字Thread-x ,当线程没有设置名字的时候,默认名称为Thread-第X个生成的线程。

1.8 关于线程的sleep方法

关于sleep方法:

static void sleep( long millis)

1.静态方法: Thread.sleep(1000)

2.参数是毫秒

3.作用:让当前线程进入休眠,进入"阻塞状态",放弃占有CPU的时间片,让给其他线程使用,出现在谁那里,就睡谁。

4.sleep 可以做到间隔特定的时间,去执行一段特定的代码,每隔XX执行一次。

关于sleep的注意点:

这段代码的MyThread线程会进入休眠吗

答案:不会

解读:t.sleep的作用是让当前进程进入睡眠,这行代码出现在main方法中,所以,HelloWorld会在五秒钟之后输出。

1.9 终止线程的睡眠。

听不懂?简单来说,就是sleep睡得太久了,你怎么叫醒?换句话说就是,你怎么把他从睡眠中途叫醒

如图所示,这个线程被我睡了一年。如何叫醒?

注意:此方法不是终端线程的执行,是终端线程的睡眠(sleep方法)

解决方案如图所示,请详细的读一下备注

 t.interrupt();
  //干扰,报异常,进入睡眠的异常catch,依靠异常处理机制。

结果如图,主方法唤醒的代码如下:

运行结果如下:

当然,如果你不喜欢"见红",可以把打印异常的那行注释掉(我就喜欢这么干)

1.10 强行终止线程的方法

在java中,如何强行终止一个线程的执行?

首先,我们说一种过时的方法

  
  线程.stop()

去java输入这个就可以看见这个效果

它可以干掉这个线程,But!

他有一个致命的缺点,他的工作类型有点像小时候玩游戏卡了,然后任务管理器直接结束进程一样,所以,如果你这个终止线程,容易丢失线程未保存的数据。

那么怎么正确并且合理的终止线程的执行?

我们可以在线程类写一个boolean值,如图所示,那么,如果我需要终止这个线程,只需要更改他的run属性为false即可:

运行结果如下

当你的线程有什么没有保存的时候,可以在else的return之前写好就ok了

1.11 线程的调度概况

关于整个调度情况,了解即可,但是我觉得如果你会了,你对线程的学习会很轻松

1.1 常见的线程调度模型有那些?(了解)

抢占式调度模型:

哪个线程的优先级比较高,抢到的CPU的时间片的概率就高一些。而Java采用的就是抢占式调度模型。

均分式调度模型:

平均分配CPU时间片,每个线程占有的CPU时间片长度一样,平均分配,一切平等,java未采用这种调度模式,而有一些编程语言采用了平等的分配模式。

1.2 java中提供了哪些方法是和线程调度有关呢?

1.1优先级方法

这是一个实例方法:

  
  void setPriority(int newPriority)
  //设置线程的优先级

最低优先级是1,默认优先级是5,最高优先级是10

But!他不是百分百优先,而是所谓抢夺时间片的概率大了,并不完全一定是他抢到(这点巨坑)

1.2 让位方法

  
  static void yield()
  //yield 方法不是阻塞方法,而是让当前线程让位,从运行状态变为就绪抢夺时间片的状态.

你退半步的动作认真的吗?~

👆就这个意思,注意,他回到就绪状态之后,可不是百分之百能抢夺到时间片哦。但是,有可能让为了还抢到了,那么在你的视觉效果上,等于白给,还不如不让。

1.3 合并方法

插队线程。打饭没被插过队吗?

注意,不是把两个栈合并了,而是出现了某个栈等待的关系。

 
  public class join {
      public static void main(String[] args) {
          System.out.println("main  begin");
          Thread thread = new Thread(new ceshi());
          thread.setName("t");
          thread.start();
          try {
              thread.join();
              //thread合并到当前线程中,当线程受到阻塞,t线程执行到结束。
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          System.out.println("main end");
      }
  }
  ​
  class ceshi implements  Runnable{
  ​
      @Override
      public void run() {
          for (int i = 0; i < 10; i++) {
              System.out.println(Thread.currentThread().getName() + "执行了"+i);
          }
      }
  }

执行结果:main end一定最后执行

2.多线程的线程安全问题

关于多线程并发环境下,数据的安全问题(重点!!)

2.1 为什么安全是重点?

以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程定义了,线程对象的创建,启动都实现了,这些代码不用我们编写。

所以重要的是,我们编写的程序需要放多多线程的环境下运行,你更需要关注的是这些运行的环境是否安全。

2.2 什么是线程不安全?

小时候想过,假如两个人拿着一模一样(想象)的银行卡去两个ATM同时取钱。网络延时一下会不会取两次钱赚银行一笔?

答案一定是不能的。(年少无知),所以,如果银行的线程是不安全的,我此时就不会在这里写文档了。咔咔咔就是一顿刷卡卡银行的BUG,然后估计就蹲号去了。

现在想一想,小日子又有判头了。

那么总结一下,什么时候数据在多线程并发的环境下会存在安全问题?

三个条件:

1.多线程并发。

2.有共享数据。

3.共享数据有修改行为。

2.3 怎么解决安全问题?

当多线程并发的情况下,有共享数据,并且这个数据还涉及到修改,此时就存在线程安全问题。

那么怎么解决这个问题呢?

线程排队执行(不能并发),用排队执行解决线程安全问题,这种机制成为线程同步机制。专业术语叫:线程同步。

线程同步就是让线程去排队了,线程排队,就会牺牲效率。但是,数据安全是第一位,只有数据安全了,才能去谈效率。

2.4 两个专业术语

1.异步编程模型:

线程1和线程2,各自执行各自的,谁也不用等谁,互不干扰,这种编程模型就叫异步编程模型。其实就是多线程并发(效率较高)

2.同步变成模型

线程1和线程2,线程1执行的时候,线程2像执行必须等到线程1结束,两个线程之间发生了等待关系,这就是同步变成模型,效率较低,线程排队执行。

2.5 线程不安全的模拟演示

接下来我们编写程序模拟一下。

 package threadsafe;
  ​
  /**
   银行账户
   **/
  public class Account {
      private String actnode;
  ​
      private double balance;
  ​
      public Account(String actnode, double balance) {
          this.actnode = actnode;
          this.balance = balance;
      }
  ​
      public Account() {
      }
  ​
      public String getActnode() {
          return actnode;
      }
  ​
      public void setActnode(String actnode) {
          this.actnode = actnode;
      }
  ​
      public double getBalance() {
          return balance;
      }
  ​
      public void setBalance(double balance) {
          this.balance = balance;
      }
  ​
      //取款的方法。
      //先不考虑够不够的问题。
      public void withdraw(double money){
          //t1和t2并发执行这个方法(t1,t2是两个栈),两个栈造作堆中的同一个对象
          //取款之前的余额
          double before = this.getBalance();
          //取款后的余额
          double after = before-money;
          //刷新余额
          //假如:t1执行到这里,还没来得及刷新,t2访问了这个对象,此时一定出现线程安全问题
          this.setBalance(after);
      }
  ​
  }

有可能这个方法不出问题,但是,你不能保证此次不出问题,关键是看t1线程能不能在时间片内执行完这个方法并且刷新余额。

参观一下,总共设置的1W,每个方法执行完之后,一人拿了5000,而且还剩下5000

这就是线程安全问题。如果在run方法里面搞个sleep模拟网络延迟,那么百分之百出问题。

我们来尝试一下用线程同步机制解决线程安全问题。

//改一改就ok,我们把取钱的方法拿过来
  public void withdraw(double money){
      //synchronized 线程同步代码块,后面的小括号的数据是相当关键的。这个数据必须是共享的关键数据,才能达到多线程排队
          //()中写什么?
          /*
          * 要看你想让哪些线程同步,假设你有t1,t2,t3,t4,t5五个线程
          * 你只希望 t1,t2,t3排队,t4,t5不排队
          * 你就一定要在()写入123共享的对象,而对于t4,t5不共享
          * 本次例子共享的对象是账户对象。
          * 那么this就是账户对象
          * 当然 不一定是this,这次是碰巧
          * */
      synchronized (this){
          double before = this.getBalance();
          double after = before-money;
          this.setBalance(after);
      }
  }

这样我们再去执行,就开始排队

2.6 对于synchronized的理解

你可以理解为,去洗手间锁门,这种情况下不能并发,得锁门,而synchronized,就是锁。

在java语言中,任何一个对象都有一把锁,这把锁就是标记,上面的代码执行原理就是,假设两个线程并发,开始执行代码的时候,一个先一个后,t1先执行遇到了synchronized,这个时候会自动找后面共享对象的对象所,找到之后占有这个锁然后执行synchronized中的代码块,结束之后才会释放,而此时t2也遇到了synchronized关键字,也会去后面尝试占有共享对象的锁,此时发现t1占用了,必须等到t1结束归还这个锁。才能进入

这样就达到了线程排队执行。不过,需要注意的是,共享对象一定要是需要排队执行的这些线程对象锁共享的。

我们此时就可以更新线程生命周期的图片了。

2.7总结:synchronized有三种写法:

第一种:同步代码块,灵活

  synchronized(线程共享对象){

      同步代码块;
  }

第二种:在实例方法尚使用synchronized

表示共享对象一定是this,并且同步代码块是整个方法体。

第三种:在静态方法上使用synchronized

表示找类锁,类锁永远只有一把。

3 锁

3.1 死锁现象

指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

 //给大家表演个死锁
  public class DeadLock {
      public static void main(String[] args) {
          Object o1 = new Object();
          Object o2 = new Object();
  ​
          Thread t1=new MyThread1(o1,o2);
          Thread t2=new MyThread2(o1,o2);
  ​
          t1.start();
          t2.start();
      }
  }
  ​
  ​
  class MyThread1 extends Thread{
      Object o1;
      Object o2;
      public MyThread1(Object o1,Object o2){
          this.o1 = o1;
          this.o2 = o2;
      }
  ​
      @Override
      public void run() {
          synchronized (o1){
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              synchronized (o2){}
          }
      }
  }
  ​
  class MyThread2 extends Thread{
      Object o1;
      Object o2;
      public MyThread2(Object o1,Object o2){
          this.o1 = o1;
          this.o2 = o2;
      }
  ​
      @Override
      public void run() {
          synchronized (o2){
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              synchronized (o1){}
          }
      }
  }

之前面试的时候有面试官问我会不会写死锁,给我整蒙了,人家问的都是怎么规避死锁,而他偏偏让我写出来,记忆犹新

3.2 聊一聊,开发中如何解决线程安全问题

是一上来就选择线程同步吗? (synchronized)不是,synchronized会让程序的执行效率降低,用户体验不好。系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择线程同步机制

第一种方案:尽量使用局部变量代替"实例变量和静态变量"。

第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了)

第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制。

4.守护线程

java中有两种线程,一种是用户线程,例如main方法,一种是守护线程。例如垃圾回收线程就是守护线程。

守护线程的特点:

一般情况下,守护线程是个死循环,所有的用户线程只要结束,守护线程自动结束。

守护线程有什么用呢?

例如,每天00:00的时候系统数据自动备份

用法:

当main执行结束后,守护循环自动结束,死循环也不好使

5.定时器

定时器的作用:

间隔特定的时间,执行特定的程序。例如,每周要进行银行账户的总账操作,每天要进行数据的备份操作。在实际开发中,每隔多久执行一段特定的程序是很常见的

第一,我们可以用sleep的方法,但是比较low

所以我们可以利用java.util.Timer,不过也很少用了,目前很多高级框架都是支持定时任务的。例如SpringTask框架

 

由此可得,这个Timer是一个后台线程。

6.实现线程的第三种方式

实现Callable接口

之前的两种方式是无法获得现成的返回值的。因为run方法返回void,而Callable是可以拿到返回值内容的。

实现线程的第三种方式:实现Callable接口这种方式的优点:可以获取到线程的执行结果。这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。

 ​
  public class Thread0001 {
      public static void main(String[] args) throws ExecutionException, InterruptedException {
          FutureTask futureTask = new FutureTask(new Callable() {
              @Override
              public Object call() throws Exception {
                  return 10;
              }
          });
  ​
          Thread t = new Thread(futureTask);
          t.start();
          Object o = futureTask.get();
          System.out.println(o);
      }
  }

7.关于Object类中的wait和notify方法

第一: wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是object类中自带的。wait方法和notify方法不是通过线程对象调用,等待方法和通知方法不是通过线程对象调用

第二: wait()方法作用?

  Object o = new Object () ;
  o.wait( );

表示:

让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止-

o.wait( );方法的调用,会让"当前线程(正在o对象上活动的线程)"进入等待状态。

第三: notify ()方法作用?


  Object o = new Object () ;
  o.notify () ;

表示:唤醒正在o对象上等待的线程。还有一个notifyAll()方法:这个方法是唤醒o对象上处于等待的所有线程。

8.生产者和消费者模式!

一张图解释生产者和消费者的关系。

 public class Card {
      private int yue=0;
      private boolean bzw=false;
      //没钱是F,有钱时T;
  ​
      public int getYue() {
          return yue;
      }
  ​
      public void setYue(int yue) {
          this.yue = yue;
      }
  ​
  ​
      //有钱的时候不让存,没钱的时候不让取
      //存钱的
      public synchronized void cunqian(int money){
          //if风改为while
          //解决了程序出现负数问题
          while(bzw){
              try {
                  this.wait();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
          //接下来是没钱存钱
          yue+=money;
          System.out.println(Thread.currentThread().getName()+"存入了1000元,余额为"+yue);
          bzw=!bzw;
          this.notifyAll();
      }
  ​
      //取钱
      public synchronized void quqian(int money){
          while(!bzw){
              try {
                  this.wait();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
          //接下来是没钱存钱
          yue-=money;
          System.out.println(Thread.currentThread().getName()+"取出了了1000元,余额为"+yue);
          bzw=!bzw;
          this.notifyAll();
      }
  }

  
  public class Test {
      public static void main(String[] args) {
          Card card = new Card();
          Thread ykq = new Thread(new Boy(card), "1");
          Thread ykq1 = new Thread(new Girl(card), "2");
  ​
          //激活线程
          ykq.start();
          ykq1.start();
  ​
  ​
      }
  }
  ​
  class Boy implements Runnable{
  ​
      private Card card;
  ​
      public Boy(Card card) {
          this.card = card;
      }
  ​
      @Override
      public void run() {
          for (int i = 0; i <10 ; i++) {
  ​
          card.cunqian(1000);
          }
      }
  }
  ​
  ​
  class Girl implements Runnable{
  ​
      private Card card;
  ​
      public Girl(Card card) {
          this.card = card;
      }
  ​
      @Override
      public void run() {
          for (int i = 0; i < 10; i++) {
  ​
          card.quqian(1000);
          }
      }
  }

9.线程池

线程是宝贵的内存资源,单个线程约占1M,过多分配容易内存溢出,频繁的创建和销毁会造成虚拟机的性能下降。

线程池

线程容器,可以设定线程分配的数量上限。

常用的线程池和在 java.util.concurrent (juc)下

Executor:线程池的顶级接口

Executor Service:线程池接口,可以通过submit提交代码;

Executors:工具类,通过此类获得一个线程池。

通过newFixedThreadPool(int nThreads) 获取固定长度的线程池。

通过newCachedThreadPool 获得动态的线程池,如果不够创建型的,没有上限

创建线程的方式

 
  // Executor: 线程池的顶级接口。 execute方法 该方法会执行Runable类型的类型
  //    ExecutorService: 子接口 shutdown关闭线程池  submit()该方法可以执行Runable类型的任务和Callable类型的任务
  // Executors:工具类 获取相应的线程池
  public class Test {
  ​
      public static void main(String[] args) {
          //1.固定长度的线程池
          //ExecutorService executorService = Executors.newFixedThreadPool(3);
          //2.可变长度得线程池 如果存在空闲的线程,则不会创建新的。
          //ExecutorService executorService = Executors.newCachedThreadPool();
          //3. 创建单一线程池
          //ExecutorService executorService = Executors.newSingleThreadExecutor();
          //4. 创建定长延迟线程池。
  //        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
  ​
          //5.ExcutorsService实现类创建.ThreadPoolExecutor
          ArrayBlockingQueue blockingQueue=new ArrayBlockingQueue(2);
          ExecutorService executorService=new ThreadPoolExecutor(3,5,30,TimeUnit.SECONDS,blockingQueue);
  ​
  ​
          Runnable tast1=new Runnable() {
              public void run() {
                  System.out.println(Thread.currentThread().getName()+"执行");
              }
          };
          for(int i=0;i<8;i++) {
              executorService.execute(tast1);
          }
      }
  }

9.1池化思想

例如线程池,字符串常量池,数据库连接池。

目的是为了提高资源的利用路。

1.手动创建线程对象

2.执行任务。

3.执行完毕,释放线程对象。

例如,手机没电就扔掉,就很浪费,我们应该让手机重复利用。

线程池的优点

1.提高线程的利用率。

2.提高程序的响应速度(节省了创建和销毁的响应时间)

3.便于统一管理线程对象

4.可以控制最大的并发数

举个例子。线程池是怎么运作的。

柜台里有五个窗口。假设,4,5窗口没上班。那么123窗口就能接待顾客,再来新的顾客,就会在等待区,而当你去银行发现,等待区满了,三个柜台也都在忙,那么银行就会打开另外两个窗口,But,如果五个窗口都打开了,等候区也满了,那么保安就会劝你明天再来或者去别的分行。

这个例子就和线程池运行原理一模一样。

当然,线程池有一个概念,存活时间,套用刚才的例子,假如人不多,三个窗口足以供应办理业务,那么,4,5窗口的工作人员就可以放假回家了,这就是线程的存活时间的概念,假如这个线程太长时间没被调用,那么线程池就会销毁这个线程

9.2 线程池的使用

线程池的真正实现类是ThreadPoolExecutor ,其构造方法有如下四种:

public ThreadPoolExecutor(int corePoolSize,
                            int maximumPoolSize,
                            long keepAliveTime,
                            TimeUnit unit,
                            BlockingQueue<Runnable> workQueue) {
      this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
           Executors.defaultThreadFactory(), defaultHandler);
  }
   
  public ThreadPoolExecutor(int corePoolSize,
                            int maximumPoolSize,
                            long keepAliveTime,
                            TimeUnit unit,
                            BlockingQueue<Runnable> workQueue,
                            ThreadFactory threadFactory) {
      this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
           threadFactory, defaultHandler);
  }
   
  public ThreadPoolExecutor(int corePoolSize,
                            int maximumPoolSize,
                            long keepAliveTime,
                            TimeUnit unit,
                            BlockingQueue<Runnable> workQueue,
                            RejectedExecutionHandler handler) {
      this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
           Executors.defaultThreadFactory(), handler);
  }
   
  public ThreadPoolExecutor(int corePoolSize,
                            int maximumPoolSize,
                            long keepAliveTime,
                            TimeUnit unit,
                            BlockingQueue<Runnable> workQueue,
                            ThreadFactory threadFactory,
                            RejectedExecutionHandler handler) {
      if (corePoolSize < 0 ||
          maximumPoolSize <= 0 ||
          maximumPoolSize < corePoolSize ||
          keepAliveTime < 0)
          throw new IllegalArgumentException();
      if (workQueue == null || threadFactory == null || handler == null)
          throw new NullPointerException();
      this.corePoolSize = corePoolSize;
      this.maximumPoolSize = maximumPoolSize;
      this.workQueue = workQueue;
      this.keepAliveTime = unit.toNanos(keepAliveTime);
      this.threadFactory = threadFactory;
      this.handler = handler;
  }

可以看到,其需要如下几个参数:

 corePoolSize(必需):
  核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
  ​
  maximumPoolSize(必需):
  线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
  ​
  keepAliveTime(必需):
  线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
  ​
  unit(必需):
  指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
  ​
  workQueue(必需):
  任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
  ​
  threadFactory(可选):
  线程工厂。用于指定为线程池创建新线程的方式。
  ​
  handler(可选):
  拒绝策略。当达到最大线程数时需要执行的饱和策略。

线程的使用流程如下:

// 创建线程池
  ThreadPoolExecutor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
                                               MAXIMUM_POOL_SIZE,
                                               KEEP_ALIVE,
                                               TimeUnit.SECONDS,
                                               sPoolWorkQueue,
                                               sThreadFactory);
  // 向线程池提交任务
  threadPool.execute(new Runnable() {
      @Override
      public void run() {
          ... // 线程执行的任务
      }
  });
  // 关闭线程池
  threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
  threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表

9.3 线程池的工作原理

9.4功能线程池

嫌上面使用线程池的方法太麻烦?其实Executors已经为我们封装好了 4 种常见的功能线程池,如下:


  定长线程池(FixedThreadPool)
  定时线程池(ScheduledThreadPool )
  可缓存线程池(CachedThreadPool)
  单线程化线程池(SingleThreadExecutor)

9.4.1 定长线程池(FixedThreadPool)

创建方法的源码:

 public static ExecutorService newFixedThreadPool(int nThreads) {
      return new ThreadPoolExecutor(nThreads, nThreads,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>());
  }
  ​
  public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
      return new ThreadPoolExecutor(nThreads, nThreads,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory);
  }

使用实例:

  
  // 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
  ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
  // 2. 创建好Runnable类线程对象 & 需执行的任务
  Runnable task =new Runnable(){
    public void run() {
       System.out.println("执行任务啦");
    }
  };
  // 3. 向线程池提交任务
  fixedThreadPool.execute(task);

9.4.2 定时线程池(ScheduledThreadPool )

创建方法的源码:

 
  private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
   
  public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
      return new ScheduledThreadPoolExecutor(corePoolSize);
  }
  public ScheduledThreadPoolExecutor(int corePoolSize) {
      super(corePoolSize, Integer.MAX_VALUE,
            DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
            new DelayedWorkQueue());
  }
   
  public static ScheduledExecutorService newScheduledThreadPool(
          int corePoolSize, ThreadFactory threadFactory) {
      return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
  }
  public ScheduledThreadPoolExecutor(int corePoolSize,
                                     ThreadFactory threadFactory) {
      super(corePoolSize, Integer.MAX_VALUE,
            DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
            new DelayedWorkQueue(), threadFactory);
  }
  • 特点:核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。

  • 应用场景:执行定时或周期性的任务。

使用实例:

 // 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
  ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
  // 2. 创建好Runnable类线程对象 & 需执行的任务
  Runnable task =new Runnable(){
    public void run() {
       System.out.println("执行任务啦");
    }
  };
  // 3. 向线程池提交任务
  scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
  scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务

9.4.3 可缓存线程池(CachedThreadPool)

创建方法的源码:

  public static ExecutorService newCachedThreadPool() {
      return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>());
  }
  public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
      return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>(),
                                    threadFactory);
  }
  • 特点:无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。

  • 应用场景:执行大量、耗时少的任务。

使用实例:


  // 1. 创建可缓存线程池对象
  ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
  // 2. 创建好Runnable类线程对象 & 需执行的任务
  Runnable task =new Runnable(){
    public void run() {
       System.out.println("执行任务啦");
    }
  };
  // 3. 向线程池提交任务
  cachedThreadPool.execute(task);

9.4.4 单线程化线程池(SingleThreadExecutor)

创建方法的源码:

  
  public static ExecutorService newSingleThreadExecutor() {
      return new FinalizableDelegatedExecutorService
          (new ThreadPoolExecutor(1, 1,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>()));
  }
  public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
      return new FinalizableDelegatedExecutorService
          (new ThreadPoolExecutor(1, 1,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory));
  }
  • 特点:只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。

  • 应用场景:不适合并发但可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等。

使用实例:

// 1. 创建单线程化线程池
  ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
  // 2. 创建好Runnable类线程对象 & 需执行的任务
  Runnable task =new Runnable(){
    public void run() {
       System.out.println("执行任务啦");
    }
  };
  // 3. 向线程池提交任务
  singleThreadExecutor.execute(task);

9.5 对比线程池

9.6 总结

Executors 的 4 个功能线程池虽然方便,但现在已经不建议使用了,而是建议直接通过使用 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

其实 Executors 的 4 个功能线程有如下弊端:

  FixedThreadPool 和 SingleThreadExecutor:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,可能会耗费非常大的内存,甚至 OOM。
  CachedThreadPool 和 ScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

这部分借鉴了 孙强 Jimmy大佬的文章,原文如下 Java 多线程:彻底搞懂线程池_孙强 Jimmy 的博客-CSDN博客_java多线程线程池

10.Lock接口

JDK5加入,与synchronized比较,显示定义,结构更灵活。

提供了更多实用性的方法,功能更强大。(JDK7以前)

常用方法:

  void lock()//获取锁
  void unlock() //释放锁
  boolean tryLock() //尝试获取锁,成功返回true,失败返回false,不阻塞

10.1重入锁:

ReentrantLock:lock接口的实现类,与synchronized一样具有互斥锁功能。

注意,请在finally中写入解锁,否则一旦有异常,代码断掉就不会执行unlock;

10.2 读写锁:

ReentrantReadWriteLock

一种支持一写多读的同步锁,读写分离,可分别分配读,写。

支持多次分配读锁,使多个读操作可以并发执行。

11.线程安全的集合

白色的均为线程不安全的,蓝色的为线程安全的。绿色为被淘汰的集合。

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值