多线程总结

本文详细介绍了Java线程的概念,包括进程、线程、多线程的关系,以及创建线程的两种方式。重点讲解了线程同步、线程池的使用、生产者与消费者问题,分析了线程同步的多种方法和线程池的参数配置。文中还探讨了线程安全类、线程死锁和如何避免线程问题,以及在Spring Boot中使用线程池的注意事项。此外,提到了使用线程池避免资源浪费和线程上下文切换的重要性。
摘要由CSDN通过智能技术生成

1.任务、线程、进程、多线程

线程、进程、多线程、多进程和多任务的关系

彻底搞懂线程、进程、多线程、多进程和多任务的关系_linux大本营的博客-CSDN博客_多任务 多线程 多进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

任务是指人们在日常生活、工作、娱乐活动中所从事的各种各样有目的的活动。在现代计算机中,“任务”也是基本工作单位,在多数实时操作系统(如uCOS,FreeRTOS)中,是实时操作系统 进行运算调度的最小单位。

 进程和线程的区别:

1.线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;

2.一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;

3.进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;

4.调度和切换:线程上下文切换比进程上下文切换要快得多。

进程与线程的关系图:

 进程与线程的资源共享关系图:

2.创建线程

2.1、方法一:实现Thread类(不建议)

创建线程方法一:继承Thread类,重写run()方法,调用start开启线程

package com.fan.demo01;

//创建线程方法一:继承Thread类,重写run()方法,调用start开启线程、
//注意:线程开启不会立即执行,由CPU调度执行
public class TestThread1 extends Thread{
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("我在看代码--"+i);
        }
    }

    public static void main(String[] args) {
        //main线程,主线程
        //创建一个线程对象
        TestThread1 thread1 = new TestThread1();
        //调用start()方法
        thread1.start();
        for (int i = 0; i < 200; i++) {
            System.out.println("我在学习多线程--"+i);
        }
    }
}

运行结果:

可以从结果中看出:两个线程是同时进行的。

注意:线程开启不会立即执行,有CPU调度执行。

2.2、方法二:实现Runnable接口(推荐)

创建线程方法二:继承runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start方法

package com.fan.demo01;

//创建方法二:继承runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start方法
public class TestThread2 implements Runnable{
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("我在看代码--"+i);
        }
    }

    public static void main(String[] args) {
        //main线程,主线程
        //创建runnable接口实现类对象
        TestThread2 testThread2 = new TestThread2();
        //创建线程对象
        Thread thread = new Thread(testThread2);
        //调用start方法
        thread.start();

        for (int i = 0; i < 200; i++) {
            System.out.println("我在学习多线程--"+i);
        }
    }
}

3.代理模式

静态代理和动态代理

3.1、静态代理

静态代理需要代理类和被代理类实现相同的接口。静态代理的缺点是冗余,因为一个代理类只能代理一个接口,因此如果需要代理多个接口时就会产生非常多的代理类,这样就会造成大量的资源消耗。另外也不利于维护,当接口增加方法,代理类和委托类都要进行修改,耦合性太大。下面是简单实例:

接口:

public interface Dog {
 
     void eat();
}

委托类:

public class BlackDog implements Dog {
    @Override
    public void eat() {
        System.out.println("小黑");
    }
}

代理类 :

public class StaticHandler implements Dog {
 
    private Dog dog;
 
    public StaticHandler(Dog dog){
        this.dog = dog;
    }
    @Override
    public void eat() {
        dog.eat();
    }
}

客户端:

public class Client {
 
    public static void main(String[] args) {
        BlackDog blackDog = new BlackDog();
        Dog dog = new StaticHandler(blackDog);
        dog.eat();
    }
}

3.2、JDK动态代理

动态代理利用了JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为JDK代理或接口代理。

动态代理和静态代理的区别:

1、静态代理只代理一个类,而动态代理是代理接口下的多个实现类

2、静态代理在编译时就知道要代理的类,而动态代理是在运行期动态生成的代理类。

3 、动态代理类不需要实现接口,但是委托类还是需要实现接口。

简单实例:

接口:

public interface Dog {
 
     void eat();
}

委托类:

public class WhiteDog implements Dog{
    @Override
    public void eat() {
        System.out.println("白狗吃骨头");
    }
}

代理类:

public class ProxyHandler implements InvocationHandler {
 
    private Object dog;
 
    public ProxyHandler(Object dog){
        this.dog = dog;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(dog,args);
    }
}

测试:

public class Test {
 
    public void testProxy(){
 
        WhiteDog whiteDog = new WhiteDog();
        Dog dog = (Dog) Proxy.newProxyInstance(Dog.class.getClassLoader(),
                new Class[]{Dog.class},new ProxyHandler(whiteDog));
        dog.eat();
    }
 
    public static void main(String[] args) {
        Test t = new Test();
        t.testProxy();
    }
}

3.3、CGLIB动态代理

cglib (Code Generation Library )是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。

使用cglib代理的对象无需实现接口,达到代理类无侵入

public class WsTest {
    public static void main(String[] args) {
       final List<String> list = new ArrayList<>();
       List<String> proxy =(List<String>) Proxy.newProxyInstance(
               list.getClass().getClassLoader(),
               list.getClass().getInterfaces(),
               new InvocationHandler() {
                   @Override
                   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                       return method.invoke(list,args);
                   }
               });
       proxy.add("你好");
       String ss = proxy.get(0);
       System.out.println(list);
        System.out.println(ss);
 
    }
}

4. Lamda表达式

使用前提:必须是函数式接口(一个接口中只有一个方法)

推导Lamda表达式过程:

package com.fan.demo;

//推导Lambda表达式
public class TestLambda {

    //3.静态内部类
    static class Like2 implements ILike{
        @Override
        public void Lambda() {
            System.out.println("I like Lambda2");
        }
    }

    public static void main(String[] args) {
        ILike like = new Like();
        like.Lambda();

        like = new Like2();
        like.Lambda();

        //4.局部内部类
        class Like3 implements ILike{
            @Override
            public void Lambda() {
                System.out.println("I like Lambda3");
            }
        }

        like = new Like3();
        like.Lambda();

        //5.匿名内部类,没有名称,必须借助接口或者父类
        like = new ILike() {
            @Override
            public void Lambda() {
                System.out.println("I like Lambda4");
            }
        };
        like.Lambda();
        

        //6.用Lambda简化
        like = ()-> {
            System.out.println("I like Lambda5");
        };
        like.Lambda();

    }

}

//1.定义一个函数式接口
interface ILike{
    void Lambda();
}

//2.实现类
class Like implements ILike{
    @Override
    public void Lambda() {
        System.out.println("I like Lambda1");
    }
}

5.线程静止

5.1、线程状态

5.2、线程方法

5.3、线程停止

1.建议线程正常停止-->利用次数,不建议使用死循环

2.建议使用标志位-->设置一个标志位

3.不要使用stop或destroy等过时的方法或者JDK不建议使用的方法

package com.fan.demo02;

//线程停止
//1.建议线程正常停止-->利用次数,不建议使用死循环
//2.建议使用标志位-->设置一个标志位
//3.不要使用stop或destroy等过时的方法或者JDK不建议使用的方法

public class TestStop implements Runnable{

    //设置一个标志位
    private Boolean flag = true;
    @Override
    public void run() {
        int i = 0;
        System.out.println("run-------->Thread"+i++);
    }

    //设置一个停止的方法,转换标志位
    public void stop(){
        flag = false;
    }

    public static void main(String[] args) {

        TestStop testStop = new TestStop();
        new Thread(testStop).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("main"+i);
            if (i == 800){
                testStop.stop();
                System.out.println("线程停止!");
            }
        }
    }
}

5.4、线程休眠--sleep

每一个对象都有一个锁,sleep不会释放锁。

  • 模拟网络延时:放大问题的发生性

package com.fan.demo02;

//模拟网络延时

public class TestSleep implements Runnable{

    //票数
    private int ticketNums = 10;

    @Override
    public void run() {
        while (true){
            if (ticketNums<=0){
                break;
            }

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNums--+"票");
        }
    }

    public static void main(String[] args) {
        TestSleep testSleep = new TestSleep();

        new Thread(testSleep,"小明").start();
        new Thread(testSleep,"老师").start();
        new Thread(testSleep,"黄牛").start();
    }
}
  • 模拟倒计时:

1.10秒倒计时

package com.fan.demo02;

//模拟倒计时

public class TestSleep2{

    //10秒倒计时
    public void tenDown() throws InterruptedException {
        int num = 10;
        while (true){
            Thread.sleep(1000);
            System.out.println(num--);
            if (num<0){
                break;
            }
        }
    }

    public static void main(String[] args) {

        TestSleep2 testSleep2 = new TestSleep2();
        try {
            testSleep2.tenDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2.获取系统当前时间

Date startTime = new Date(System.currentTimeMillis());//获取系统当前时间
        while (true){
            try {
                Thread.sleep(1000);
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
                startTime = new Date(System.currentTimeMillis());//更新当前时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

5.5、线程礼让--yield

礼让线程:让正在执行的线程暂停,但不阻塞。

线程礼让不一定成功,看CPU心情。

package com.fan.demo02;

//线程礼让
//线程礼让不一定成功,看CPU心情
public class TestYield {

    public static void main(String[] args) {
        MyYield myYield = new MyYield();

        new Thread(myYield,"a").start();
        new Thread(myYield,"b").start();
    }
}


class MyYield implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始执行");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"线程停止执行");
    }
}

5.6、线程强制执行--join(不建议使用)

join合并线程,待此线程执行完毕后,再执行其他线程,其他线程阻塞。(可以想象成插队)

package com.fan.demo02;
//线程强制执行
public class TestJoin implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程强制执行"+i);
        }
    }

    public static void main(String[] args) {

        //开启线程
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        thread.start();

        //主线程
        for (int i = 0; i < 500; i++) {
            if (i == 200){
                try {
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
            System.out.println("main"+i);
        }
    }
}

6.线程同步

线程同步和锁:线程的同步与锁_穷水叮咚的博客-CSDN博客

6.1、同步问题提出

线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏。

例如:两个线程Thread A、Thread B都操作同一个对象Foo对象,并修改Foo对象上的数据。

public class Foo {    
   private int x = 100;    

   public int getX() {    
       return x;    
  }    

   public int fix(   int y) {    
      x = x - y;    
      return x;    
  }    
}  
public class MyRunnable implements Runnable {    
    private Foo foo = new Foo();    
   
    public static void main(String[] args) {    
    MyRunnable r = new MyRunnable();    
    Thread ta = new Thread(r,"Thread-A");    
    Thread tb = new Thread(r,"Thread-B");    
    ta.start();    
    tb.start();    
  }    
   
     public void run() {    
       for (int i = 0; i < 3; i++) {    
         this.fix(30);    
         try {    
        Thread.sleep(1);    
      } catch (InterruptedException e) {    
        e.printStackTrace();    
      }    
      System.out.println(Thread.currentThread().getName() + " : 当前foo对象的x值= " + foo.getX());    
    }    
  }    
   
     public int fix(   int y) {    
       return foo.fix(y);    
  }    
} 

运行结果:

Thread-A : 当前foo对象的x值= 40    
Thread-B : 当前foo对象的x值= 40    
Thread-B : 当前foo对象的x值= -20    
Thread-A : 当前foo对象的x值= -50    
Thread-A : 当前foo对象的x值= -80    
Thread-B : 当前foo对象的x值= -80       
Process finished with exit code 0 

从结果发现,这样的输出值明显是不合理的。原因是两个线程不加控制的访问Foo对象并修改其数据所致。

如果要保持结果的合理性,只需要达到一个目的,就是将对Foo的访问加以限制,每次只能有一个线程在访问。这样就能保证Foo对象中数据的合理性了。

在具体的Java代码中需要完成一下两个操作:

把竞争访问的资源类Foo变量x标识为private;

同步哪些修改变量的代码,使用synchronized关键字同步方法或代码。

6.2、同步和锁定

1、锁的原理

Java中每个对象都有一个内置锁

当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁也

称为获取锁、锁定对象、在对象上锁定或在对象上同步。

当程序运行到synchronized同步方法或代码块时才该对象锁才起作用。

一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任

何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。

释放锁是指持锁线程退出了synchronized同步方法或代码块。

关于锁和同步,有一下几个要点:

1)只能同步方法,而不能同步变量和类;

2)每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?

3)不必同步类中所有的方法,类可以同时拥有同步和非同步方法。

4)如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个

需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。

5)如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。

6)线程睡眠时,它所持的任何锁都不会释放。

7)线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。

8)同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。

9)在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。例如:

public int fix(int y) { 
   synchronized (this) { 
     x = x - y; 
   } 
   return x; 
}

当然,同步方法也可以改写为非同步方法,但功能完全一样的,例如:

public synchronized int getX() { 
    return x++; 
  }

public int getX() { 
  synchronized (this) { 
   return x; 
  } 
}

效果是完全一样的。

6.3、静态方法同步

要同步静态方法,需要一个用于整个类对象的锁,这个对象是就是这个类(XXX.class)。

例如:

public static synchronized int setName(String name){

    Xxx.name = name;

}

等价于:

public static int setName(String name){ 
   synchronized(Xxx.class){ 
      Xxx.name = name; 
   } 
}

6.4、如果线程不能获得锁会怎么样

如果线程试图进入同步方法,而其锁已经被占用,则线程在该对象上被阻塞。实质上,线程进入该对象的的一种池中,必须在哪里等

待,直到其锁被释放,该线程再次变为可运行或运行为止。

当考虑阻塞时,一定要注意哪个对象正被用于锁定:

1、调用同一个对象中非静态同步方法的线程将彼此阻塞。如果是不同对象,则每个线程有自己的对象的锁,线程间彼此互不干预。

2、调用同一个类中的静态同步方法的线程将彼此阻塞,它们都是锁定在相同的Class对象上。

3、静态同步方法和非静态同步方法将永远不会彼此阻塞,因为静态方法锁定在Class对象上,非静态方法锁定在该类的对象上。

4、对于同步代码块,要看清楚什么对象已经用于锁定(synchronized后面括号的内容)。在同一个对象上进行同步的线程将彼此阻塞,

在不同对象上锁定的线程将永远不会彼此阻塞。

6.5、何时需要同步

在多个线程同时访问互斥(可交换)数据时,应该同步以保护数据,确保两个线程不会同时修改更改它。

对于非静态字段中可更改的数据,通常使用非静态方法访问。

对于静态字段中可更改的数据,通常使用静态方法访问。

如果需要在非静态方法中使用静态字段,或者在静态字段中调用非静态方法,问题将变得非常复杂。已经超出SJCP考试范围了。

6.6、线程安全类

当一个类已经很好的同步以保护它的数据时,这个类就称为“线程安全的”。

即使是线程安全类,也应该特别小心,因为操作的线程是间仍然不一定安全。

举个形象的例子,比如一个集合是线程安全的,有两个线程在操作同一个集合对象,当第一个线程查询集合非空后,删除集合中所有元

素的时候。第二个线程也来执行与第一个线程相同的操作,也许在第一个线程查询后,第二个线程也查询出集合非空,但是当第一个执行

清除后,第二个再执行删除显然是不对的,因为此时集合已经为空了。

看个代码:

public class NameList {     
    private List nameList = Collections.synchronizedList(    new LinkedList());     
    
    public void add(String name) {     
        nameList.add(name);     
    }     
    
    public String removeFirst() {     
        if (nameList.size() > 0) {     
          return (String) nameList.remove(0);     
        }else{     
          return null;     
    }     
  }     
}   
public class Test {     
    public static void main(String[] args) {     
    final NameList nl = new NameList();     
    nl.add("aaa");     
    class NameDropper extends Thread{     
         public void run(){     
             String name = nl.removeFirst();     
             System.out.println(name);     
         }     
    }     
    
    Thread t1 =  new NameDropper();     
    Thread t2 =  new NameDropper();     
    t1.start();     
    t2.start();     
  }     
} 

虽然集合对象

private List nameList = Collections.synchronizedList(new LinkedList()); 

是同步的,但是程序还不是线程安全的。

出现这种事件的原因是,上例中一个线程操作列表过程中无法阻止另外一个线程对列表的其他操作。

解决上面问题的办法是,在操作集合对象的NameList上面做一个同步。改写后的代码如下:

public class NameList {     
   private List nameList = Collections.synchronizedList(new LinkedList());     
    
   public synchronized void add(String name) {     
      nameList.add(name);     
   }     
    
   public synchronized String removeFirst() {     
       if (nameList.size() > 0) {     
       return (String) nameList.remove(0);     
       }else{     
          return     null;     
       }     
   }     
} 

这样,当一个线程访问其中一个同步方法时,其他线程只有等待。

6.7、线程死锁

死锁对Java程序来说,是很复杂的,也很难发现问题。当两个线程被阻塞,每个线程在等待另一个线程时就发生死锁。

还是看一个比较直观的死锁例子:

public class DeadlockRisk {    
    private static class Resource {    
       public int value;    
    }    
   
    private Resource resourceA =  new Resource();    
    private Resource resourceB =  new Resource();    
   
    public int read() {    
       synchronized (resourceA) {    
         synchronized (resourceB) {    
           return resourceB.value + resourceA.value;    
      }    
    }    
  } 
     public void write(int a,int b) {    
       synchronized (resourceB) {    
         synchronized (resourceA) {    
        resourceA.value = a;    
        resourceB.value = b;    
      }    
    }    
  }    
} 

假设read()方法由一个线程启动,write()方法由另外一个线程启动。读线程将拥有resourceA锁,写线程将拥有resourceB锁,两者都坚

持等待的话就出现死锁。

实际上,上面这个例子发生死锁的概率很小。因为在代码内的某个点,CPU必须从读线程切换到写线程,所以,死锁基本上不能发生。

但是,无论代码中发生死锁的概率有多小,一旦发生死锁,程序就死掉。有一些设计方法能帮助避免死锁,包括始终按照预定义的顺序

获取锁这一策略。已经超出SCJP的考试范围。

6.8、线程同步小结

1、线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。

2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对

象的线程就无法再访问该对象的其他同步方法。

3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同

步方法中访问另外对象上的同步方法时,会获取这两个对象锁。

4、对于同步,要时刻清醒在哪个对象上同步,这是关键。

5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作

期间别的线程无法访问竞争资源。

6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。

7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使,呵呵。但是,一旦程序发生

死锁,程序将死掉。

7.生产者与消费者

生产者和消费者问题是线程模型中的经典问题:生产者和消费者在同一时间段内共用同一个存储空间,生产者向空间里存放数据,而消费者取用数据,如果不加以协调可能会出现以下情况:存储空间已满,而生产者占用着它,消费者等着生产者让出空间从而去除产品,生产者等着消费者消费产品,从而向空间中添加产品。互相等待,从而发生死锁。

7.1、关系

生产者:生产者:负责生产数据的模块(可能是方法﹐对象,线程﹐进程); 消费者∶负责处理数据的模块(可能是方法,对象,线程,进程); 缓冲区∶消费者不能直接使用生产者的数据,)他们之间有个“缓冲区

7.2、管程法

public class ProducerAndConsumer {
    public static void main(String[] args) {
        //创建货架对象
        GoodsShelf goodsShelf = new GoodsShelf();
        //生产者消费者都是对一个货架进行操作,所以传参数都一样
        Producer producer = new Producer(goodsShelf);
        Consumer consumer = new Consumer(goodsShelf);
        //开始
        new Thread(producer).start();
        new Thread(consumer).start();
    }

}

//商品
class Iphone{
    int number;
    public Iphone(int number){
        this.number = number;
    }
}

//货架
class GoodsShelf{
    //可以放10个商品的货架
    Iphone[] iphones = new Iphone[10];
    //显示货架商品数量
    int count = 0;
    //对象调用的wait()方法时一定要在同步块或者同步方法中调用,以确保代码段不会被多个线程调用。
    public synchronized void pushIphone(Iphone iphone){
        //如果货架数量等于货架的最大容量则停止生产
        if (count == iphones.length){
            try {
                System.out.println("=============货架已满============");
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //商品放入货架
        iphones[count] = iphone;
        //商品数量加1
        count++;
        //通知消费者可以消费了
        this.notifyAll();
    }
    public synchronized Iphone popIphone(){
        //没有商品了等待生产线生产
        if (count == 0){
            try {
                System.out.println("=============货架没有商品了============");
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //产品数量减1。注意:商品的下标等于商品数量减1,所以要先减
        count--;
        //通知生产者生产
        this.notifyAll();
        return iphones[count];
    }
}

//生产者
class Producer implements Runnable{
    private final GoodsShelf goodsShelf;
    public Producer(GoodsShelf goodsShelf){
        this.goodsShelf = goodsShelf;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                //睡眠0.01秒方便观察。睡眠不释放锁
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            goodsShelf.pushIphone(new Iphone(i));
            System.out.printf("生产了%3d号手机\r\n",i);
        }
    }
}
//消费者
class Consumer implements Runnable{
    private final GoodsShelf goodsShelf;
    public Consumer(GoodsShelf goodsShelf){
        this.goodsShelf = goodsShelf;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                //睡眠
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Iphone iphone = goodsShelf.popIphone();
            System.out.printf("消费了%3d号手机\r\n",iphone.number);
        }

    }
}

7.3、信号灯法

生产者消费者通过标志变量来控制生产和消费

public class ProducerAndConsumer2 {

    public static void main(String[] args) {
        IPhone iPhone = new IPhone();
        new PhoneConsumer(iPhone).start();
        new PhoneProducer(iPhone).start();
    }
}

//操作的对象
class IPhone{
    private int number;
    //标志变量
    boolean flag = true;

    public synchronized void makeAPhone(int number){
        if (!flag){
            try {
                //等待消费者消费
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.number = number;
        System.out.println("+生产了"+number+"号手机");
        //修改标志变量,并通知消费者消费
        this.flag = !this.flag;
        this.notifyAll();
    }

    public synchronized void sellAPhone(){
        if (flag){
            try {
                //等待生产者生产
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("-卖出了"+number+"号手机");
        //修改标志变量,并通知生产者生产
        this.flag = !this.flag;
        this.notifyAll();
    }
}
//生产者
class PhoneProducer extends Thread{

    IPhone iPhone;
    public PhoneProducer(IPhone iPhone){
        this.iPhone = iPhone;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            iPhone.makeAPhone(i);
        }
    }
}
//消费者
class PhoneConsumer extends Thread{
    IPhone iPhone;
    public PhoneConsumer(IPhone iPhone){
        this.iPhone = iPhone;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            iPhone.sellAPhone();
        }
    }
}

以上为狂神说Java内容,以下为网络搜索内容(网址:Java四种方式解决生产者消费者问题_@HarveyMr的博客-CSDN博客_java生产者消费者问题)。

7.4、wait()/notify()方法

Java 中,可以通过配合调用 Object 对象的 wait() 方法和 notify()方法或 notifyAll() 方法来实现线程间的通信。在线程中调用 wait() 方法,将阻塞当前线程,直至等到其他线程调用了 notify() 方法或 notifyAll() 方法进行通知之后,当前线程才能从wait()方法出返回,继续执行下面的操作。

package com.study.thread;
 
import java.util.LinkedList;
 
class ShareDataSync {
    private int maxNum = 10;
    private LinkedList<String> list = new LinkedList<>();
 
    //生产者
    public void produceData() {
        synchronized (list) {
            try {
                while (list.size() >= maxNum) {
                    //仓库已满,等待被消费
                    list.wait();
                }
                list.add("1");
                System.out.println(Thread.currentThread().getName() + "生产者,剩余库存=" + list.size());
                list.notifyAll();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
 
    //消费者
    public void consumeData() {
        synchronized (list) {
            try {
                while (list.size() <= 0) {
                    //等待数据被生产
                    list.wait();
                }
                list.remove(0);
                System.out.println(Thread.currentThread().getName() + "消费者,剩余库存=" + list.size());
                list.notifyAll();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
 
public class ProductConsumeSynchorizeDemo {
    public static void main(String[] args) {
        ShareDataSync shareData = new ShareDataSync();
        //生产
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                shareData.produceData();
            }
        }, "AA").start();
        //消费
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                shareData.consumeData();
            }
        }, "BB").start();
        //生产
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                shareData.produceData();
            }
        }, "CC").start();
        //消费
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                shareData.consumeData();
            }
        }, "DD").start();
    }
}

注意:notifyAll()方法可使所有正在等待队列中等待同一共享资源的“全部”线程从等待状态退出,进入可运行状态。此时,优先级最高的哪个线程最先执行,但也有可能是随机执行的,这要取决于JVM虚拟机的实现。即最终也只有一个线程能被运行,上述线程优先级都相同,每次运行的线程都不确定是哪个,后来给线程设置优先级后也跟预期不一样,还是要看JVM的具体实现吧。

7.5、await()/signal()方法

package com.study.thread;
 
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
class ShareData {
    private int maxNum = 10;
    private LinkedList<String> list = new LinkedList<>();
    Lock lock = new ReentrantLock();
    Condition full = lock.newCondition();
    Condition empty = lock.newCondition();
 
    //生产者
    public void produceData() {
        lock.lock();
        try {
            while (list.size() >= maxNum) {
                //仓库已满,等待被消费
                full.await();
            }
            list.add("1");
            System.out.println(Thread.currentThread().getName() + "生产者,剩余库存=" + list.size());
            empty.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
 
    //消费者
    public void consumeData() {
        lock.lock();
        try {
            while (list.size() <= 0) {
                //等待数据被生产
                empty.await();
            }
            list.remove(0);
            System.out.println(Thread.currentThread().getName() + "消费者,剩余库存=" + list.size());
            full.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
 
public class ProductConsumeLockDemo {
    public static void main(String[] args) {
        ShareData shareData = new ShareData();
        //生产
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                shareData.produceData();
            }
 
        }, "AA").start();
        //消费
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                shareData.consumeData();
            }
        }, "DD").start();
        //消费
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                shareData.consumeData();
            }
        }, "BB").start();
        //生产
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                shareData.produceData();
            }
        }, "CC").start();
    }
}

7.6、BlockingQueue阻塞队列方法

BlockingQueue是JDK5.0的新增内容,它是一个已经在内部实现了同步的队列,底层实现方式采用的是我们第2种await() / signal()方法。它可以在生成对象时指定容量大小,用于阻塞操作的是put()、offer()和take()、poll()方法。

package com.study.thread;
 
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
 
class ShareDataBlockQueue {
    private boolean flag = true;
    //保证多线程下生产者的线程安全
    private AtomicInteger atomicInteger = new AtomicInteger(0);
    BlockingQueue blockingQueue = null;
 
    public ShareDataBlockQueue(BlockingQueue blockingQueue) {
        this.blockingQueue = blockingQueue;
    }
 
    //生产者
    public void produceData() throws InterruptedException {
        while (flag) {
            int i = atomicInteger.incrementAndGet();
            boolean offer = blockingQueue.offer(i, 3, TimeUnit.SECONDS);
            if (offer) {
                System.out.println(Thread.currentThread().getName() + "生产" + i);
            } else {
                System.out.println(Thread.currentThread().getName() + "生产失败" + i);
            }
            Thread.sleep(1000);
        }
    }
 
    //消费者
    public void consumeData() throws InterruptedException {
        while (flag) {
            Object poll = blockingQueue.poll(2, TimeUnit.SECONDS);
            if (poll == null || poll.equals("")) {
                flag = false;
                System.out.println("超过2秒没有取到数据,消费退出");
                return;
            } else {
                System.out.println(Thread.currentThread().getName() + "消费" + poll);
            }
        }
    }
 
    public void stop() {
        this.flag = false;
    }
}
 
public class ProductConsumeBlockQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(10);
        ShareDataBlockQueue shareData = new ShareDataBlockQueue(blockingQueue);
        //生产
        new Thread(() -> {
            try {
                shareData.produceData();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "AA").start();
        new Thread(() -> {
            try {
                shareData.produceData();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "BB").start();
        //消费
        new Thread(() -> {
            try {
                shareData.consumeData();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "CC").start();
        //消费
        new Thread(() -> {
            try {
                shareData.consumeData();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "DD").start();
 
        Thread.sleep(5000);
        System.out.println("mian进程10s后停止生产,队列大小:" + shareData.blockingQueue.size());
        shareData.stop();
    }
}

7.7、信息量

Semaphore是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore可以用来构建一些对象池,资源池之类的,比如数据库连接池,我们也可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。计数为0的Semaphore是可以release的,release之后加1,然后就可以acquire,acquire之后减1

package com.study.thread;
 
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
 
class ShareDataSemaphore {
    //可以生产的总数量。 通过生产者调用acquire,减少permit数目
    Semaphore canProduceCount = new Semaphore(10);
    //互斥锁 互斥量,控制共享数据的互斥访问
    Semaphore block = new Semaphore(1);
    //可以消费的数量。通过生产者调用release,增加permit数目
    Semaphore canConsumerCount = new Semaphore(0);
    private LinkedList list = new LinkedList<>();
 
    //生产者
    public void produceData() throws InterruptedException {
        try {
            // 可生产数量 -1
            canProduceCount.acquire();
            //加锁
            block.acquire();
            list.add("1");
            System.out.println(Thread.currentThread().getName() + "生产者,仓库大小" + list.size());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放锁
            block.release();
            //能消费的数量 +1
            canConsumerCount.release();
        }
 
    }
 
    //消费者
    public void consumeData() throws InterruptedException {
        try {
            //能消费的数量 -1
            canConsumerCount.acquire();
            //加锁
            block.acquire();
            list.remove();
            System.out.println(Thread.currentThread().getName() + "消费者,仓库大小" + list.size());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            block.release();
            //能生产的数量 +1
            canProduceCount.release();
        }
    }
}
 
public class ProductConsumeSemaphoreDemo {
    public static void main(String[] args) throws InterruptedException {
        ShareDataSemaphore shareData = new ShareDataSemaphore();
        //生产
        new Thread(() -> {
            try {
                shareData.produceData();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "AA").start();
        new Thread(() -> {
            try {
                shareData.produceData();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "BB").start();
        //消费
        new Thread(() -> {
            try {
                shareData.consumeData();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "CC").start();
        //消费
        new Thread(() -> {
            try {
                shareData.consumeData();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "DD").start();
 
        // Thread.sleep(5000);
        // shareData.stop();
    }
}

8.线程池

Java线程池详解:Java线程池详解_饭一碗的博客-CSDN博客_线程池

8.1、线程池使用场景?

java中经常需要用到多线程来处理一些业务,我们非常不建议单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样势必

有创建及销毁线程耗费资源、线程上下文切换问题。同时创建过多的线程也可能引发资源耗尽的风险,这个时候引入线程池比较合理,方

便线程任务的管理。java中涉及到线程池的相关类均在jdk1.5开始的java.util.concurrent包中,涉及到的几个核心类及接口包括:

Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等。

  • 加快请求响应(响应时间优先)

比如用户在饿了么上查看某商家外卖,需要聚合商品库存、店家、价格、红包优惠等等信息返回给用户,接口逻辑涉及到聚合、级联等查

询,从这个角度来看接口返回越快越好,那么就可以使用多线程方式,把聚合/级联查询等任务采用并行方式执行,从而缩短接口响应时

间。这种场景下使用线程池的目的就是为了缩短响应时间,往往不去设置队列去缓冲并发的请求,而是会适当调高corePoolSize和

maxPoolSize去尽可能的创造线程来执行任务。

  • 加快处理大任务(吞吐量优先)

比如业务中台每10分钟就调用接口统计每个系统/项目的PV/UV等指标然后写入多个sheet页中返回,这种情况下往往也会使用多线程方式

来并行统计。和"时间优先"场景不同,这种场景的关注点不在于尽可能快的返回,而是关注利用有限的资源尽可能的在单位时间内处理更

多的任务,即吞吐量优先。这种场景下我们往往会设置队列来缓冲并发任务,并且设置合理的corePoolSize和maxPoolSize参数,这个时

候如果设置了太大的corePoolSize和maxPoolSize可能还会因为线程上下文频繁切换降低任务处理速度,从而导致吞吐量降低。

以上两种使用场景和JVM里的ParallelScavenge和CMS垃圾收集器有较大的类比性,ParallelScavenge垃圾收集器关注点在于达到可观的

吞吐量,而CMS垃圾收集器重点关注尽可能缩短GC停顿时间。

 

8.2、线程池的创建及重要参数

线程池可以自动创建也可以手动创建,自动创建体现在Executors工具类中,常见的可以创建newFixedThreadPool、

newCachedThreadPool、newSingleThreadExecutor、newScheduledThreadPool;手动创建体现在可以灵活设置线程池的各个参数,

体现在代码中即ThreadPoolExecutor类构造器上各个实参的不同:

  public static ExecutorService newFixedThreadPool(int var0) {
        return new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
  }
	
  public static ExecutorService newSingleThreadExecutor() {
        return new Executors.FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()));
  }
 
  public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue());
  }
 
  public static ScheduledExecutorService newScheduledThreadPool(int var0) {
        return new ScheduledThreadPoolExecutor(var0);
  }
 public ThreadPoolExecutor(int corePoolSize,
                           int maximumPoolSize,
                           long keepAliveTime,
                           TimeUnit unit,
                           BlockingQueue<Runnable> workQueue,
                           ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {……}

ThreadPoolExecutor中重要的几个参数详解

corePoolSize:核心线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务
maximumPoolSize:最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)
keepAliveTime:非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);
unit:keepAliveTime的时间单位
workQueue:用于保存任务的队列,可以为无界、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中
threadFactory:创建线程的工厂类,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建
handler:线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、CallerRunsPolicy、          DiscardOldestPolicy、DiscardPolicy

线程池中的线程创建流程图:

举个栗子:现有一个线程池,corePoolSize=10,maxPoolSize=20,队列长度为100,那么当任务过来会先创建10个核心线程数,接下来

进来的任务会进入到队列中直到队列满了,会创建额外的线程来执行任务(最多20个线程),这个时候如果再来任务就会执行拒绝策略。

workQueue队列

SynchronousQueue(同步移交队列):队列不作为任务的缓冲方式,可以简单理解为队列长度为零
LinkedBlockingQueue(无界队列):队列长度不受限制,当请求越来越多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致内存占用  过多或OOM
ArrayBlockintQueue(有界队列):队列长度受限,当队列满了就需要创建多余的线程来执行任务

常见的几种自动创建线程池方式

自动创建线程池的几种方式都封装在Executors工具类中:
newFixedThreadPool:使用的构造方式为new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()),设置了corePoolSize=maxPoolSize,keepAliveTime=0(此时该参数没作用),无界队列,任务可以无限放入,当请求过多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致占用过多内存或直接导致OOM异常
newSingleThreadExector:使用的构造方式为new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new                                     LinkedBlockingQueue(), var0),基本同newFixedThreadPool,但是将线程数设置为了1,单线程,弊端和newFixedThreadPool一致
newCachedThreadPool:使用的构造方式为new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue()),corePoolSize=0,maxPoolSize为很大的数,同步移交队列,也就是说不维护常驻线程(核心线程),每次来请求直接创建新线程来处理任务,也不使用队列缓冲,会自动回收多余线程,由于将maxPoolSize设置成Integer.MAX_VALUE,当请求很多时就可能创建过多的线程,导致资源耗尽OOM
newScheduledThreadPool:使用的构造方式为new ThreadPoolExecutor(var1, 2147483647, 0L, TimeUnit.NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue()),支持定时周期性执行,注意一下使用的是延迟队列,弊端同newCachedThreadPool一致

所以根据上面分析我们可以看到,FixedThreadPool和SigleThreadExecutor中之所以用LinkedBlockingQueue无界队列,是因为设置了

corePoolSize=maxPoolSize,线程数无法动态扩展,于是就设置了无界阻塞队列来应对不可知的任务量;而CachedThreadPool则使用的

是SynchronousQueue同步移交队列,为什么使用这个队列呢?因为CachedThreadPool设置了corePoolSize=0,

maxPoolSize=Integer.MAX_VALUE,来一个任务就创建一个线程来执行任务,用不到队列来存储任务;SchduledThreadPool用的是延

迟队列DelayedWorkQueue。在实际项目开发中也是推荐使用手动创建线程池的方式,而不用默认方式,关于这点在《阿里巴巴开发规

范》中是这样描述的:

 handler拒绝策略

AbortPolicy:中断抛出异常
DiscardPolicy:默默丢弃任务,不进行任何通知
DiscardOldestPolicy:丢弃掉在队列中存在时间最久的任务
CallerRunsPolicy:让提交任务的线程去执行任务(对比前三种比较友好一丢丢)

关闭线程池

shutdownNow():立即关闭线程池(暴力),正在执行中的及队列中的任务会被中断,同时该方法会返回被中断的队列中的任务列表
shutdown():平滑关闭线程池,正在执行中的及队列中的任务能执行完成,后续进来的任务会被执行拒绝策略
isTerminated():当正在执行的任务及对列中的任务全部都执行(清空)完就会返回true

8.3、线程池实现线程复用的原理

1.线程池里执行的是任务,核心逻辑在ThreadPoolExecutor类的execute方法中,同时ThreadPoolExecutor中维护了HashSet<Worker>     workers;
2.addWorker()方法来创建线程执行任务,如果是核心线程的任务,会赋值给Worker的firstTask属性;
3.Worker实现了Runnable,本质上也是任务,核心在run()方法里;
4.run()方法的执行核心runWorker(),自旋拿任务while (task != null || (task = getTask()) != null)),task是核心线程Worker的firstTask或者getTask();
5.getTask()的核心逻辑:
        1.若当前工作线程数量大于核心线程数->说明此线程是非核心工作线程,通过poll()拿任务,未拿到任务即getTask()返回null,然后会在processWorkerExit(w, completedAbruptly)方法释放掉这个非核心工作线程的引用;
        2.若当前工作线程数量小于核心线程数->说明此时线程是核心工作线程,通过take()拿任务
        3.take()方式取任务,如果队列中没有任务了会调用await()阻塞当前线程,直到新任务到来,所以核心工作线程不会被回收; 当执行execute方法里的workQueue.offer(command)时会调用Condition.singal()方法唤醒一个之前阻塞的线程,这样核心线程即可复用

 

手动创建线程池(推荐)

那么上面说了使用Executors工具类创建的线程池有隐患,那如何使用才能避免这个隐患呢?对症下药,建立自己的线程工厂类,灵活设

置关键参数:

//这里默认拒绝策略为AbortPolicy
private static ExecutorService executor = new ThreadPoolExecutor(10,10,60L, TimeUnit.SECONDS,new ArrayBlockingQueue(10));

使用guava包中的ThreadFactoryBuilder工厂类来构造线程池:

private static ThreadFactory threadFactory = new ThreadFactoryBuilder().build();
 
private static ExecutorService executorService = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10), threadFactory, new ThreadPoolExecutor.AbortPolicy());

通过guava的ThreadFactory工厂类还可以指定线程组名称,这对于后期定位错误时也是很有帮助的

ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-pool-d%").build();

8.4、Springboot中使用线程池

springboot可以说是非常流行了,下面说说如何在springboot中优雅的使用线程池

/**
 * @ClassName ThreadPoolConfig
 * @Description 配置类中构建线程池实例,方便调用
 * @Author simonsfan
 * @Date 2018/12/20
 * Version  1.0
 */
@Configuration
public class ThreadPoolConfig {
    @Bean(value = "threadPoolInstance")
    public ExecutorService createThreadPoolInstance() {
        //通过guava类库的ThreadFactoryBuilder来实现线程工厂类并设置线程名称
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-pool-%d").build();
        ExecutorService threadPool = new ThreadPoolExecutor(10, 16, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100), threadFactory, new ThreadPoolExecutor.AbortPolicy());
        return threadPool;
    }
}
  //通过name=threadPoolInstance引用线程池实例
  @Resource(name = "threadPoolInstance")
  private ExecutorService executorService;
 
  @Override
  public void spikeConsumer() {
    //TODO
    executorService.execute(new Runnable() {
    @Override
    public void run() {
      //TODO
     }});
  }

8.5、其它相关

在ThreadPoolExecutor类中有两个比较重要的方法引起了我们的注意:beforeExecute和afterExecute

 protected void beforeExecute(Thread var1, Runnable var2) {
 }
 
 protected void afterExecute(Runnable var1, Throwable var2) {
 }

这两个方法是protected修饰的,很显然是留给开发人员去重写方法体实现自己的业务逻辑,非常适合做钩子函数,在任务run方法的前后

增加业务逻辑,比如添加日志、统计等。这个和我们springmvc中拦截器的preHandle和afterCompletion方法很类似,都是对方法进行

环绕,类似于spring的AOP,参考下图:

Callable和Runnable

Runnable和Callable都可以理解为任务,里面封装这任务的具体逻辑,用于提交给线程池执行,区别在于Runnable任务执行没有返回

值,且Runnable任务逻辑中不能通过throws抛出cheched异常(但是可以try catch),而Callable可以获取到任务的执行结果返回值且抛出

checked异常。

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
 
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Future和FutureTask

Future接口用来表示执行异步任务的结果存储器,当一个任务的执行时间过长就可以采用这种方式:把任务提交给子线程去处理,主线程

不用同步等待,当向线程池提交了一个Callable或Runnable任务时就会返回Future,用Future可以获取任务执行的返回结果。Future的

主要方法包括:

get()方法:返回任务的执行结果,若任务还未执行完,则会一直阻塞直到完成为止,如果执行过程中发生异常,则抛出异常,但是主线程是感知不           到并且不受影响的,除非调用get()方法进行获取结果则会抛出ExecutionException异常;
get(long timeout, TimeUnit unit):在指定时间内返回任务的执行结果,超时未返回会抛出TimeoutException,这个时候需要显式的取                                   消任务;
cancel(boolean mayInterruptIfRunning):取消任务,boolean类型入参表示如果任务正在运行中是否强制中断;
isDone():判断任务是否执行完毕,执行完毕不代表任务一定成功执行,比如任务执行失但也执行完毕、任务被中断了也执行完毕都会返回true,           它仅仅表示一种状态说后面任务不会再执行了;
isCancelled():判断任务是否被取消;

下面来实际演示Future和FutureTask的用法:知识库页面发布接口逻辑

    @Override
    public ResultResponse publish(PageIndex page) {
        ResultResponse<Boolean> result = new ResultResponse<>();
        ResultResponse<PageIndex> recordResult = getPageRecord(page.getPageId());
        if (!recordResult.isSuccess()) {
            return recordResult;
        }
        PageIndex record = recordResult.getData();
        String currentUser = RequestContext.getUser().getUserCode();
        // 生成html文件,后续导出pdf要用
        FutureTask htmlFileTask = new FutureTask<>(new CreateHtmlFileTask(page.setName(record.getName())));
        // 同步更新页面所属知识库的最后更新人及更新时间
        FutureTask wikiUpdateTask = new FutureTask<>(new WikiUpdateTask(record.getWikiId(), currentUser));
        // 生成页面历史版本记录
        FutureTask pageVersionTask = new FutureTask<>(new GeneratePageVersionTask(new PageVersion(record.getPageId(), record.getWikiId(), page.getHtmlContent(), page.getContent(), currentUser, new Date(), null, null)));
        // 更新页面最新信息
        FutureTask pageUpdateTask = new FutureTask<>(new PageUpdateTask(page));
        List<FutureTask<String>> futureTaskList = ImmutableList.of(htmlFileTask, wikiUpdateTask, pageVersionTask, pageUpdateTask);
        // 任务提交
        futureTaskList.forEach(task -> executorService.submit(task));
        try {
            for (FutureTask<String> task : futureTaskList) {
                // 阻塞式等待任务执行结果
                String taskResult = task.get();
                logger.info("publish taskResult={}", taskResult);
            }
        } catch (Exception e) {
            logger.error("发布接口异常:{}", e, e.getMessage());
            e.printStackTrace();
        }
        // 释放锁
        pageLockService.unlock(page.getPageId(), currentUser);
        return result.data(true);
    }
 
    class CreateHtmlFileTask implements Callable<String> {
        private PageIndex pageIndex;
 
        public CreateHtmlFileTask(PageIndex pageIndex) {
            this.pageIndex = pageIndex;
        }
 
        @Override
        public String call() throws Exception {
            return generateHtmlFile(pageIndex);
        }
    }

实现优先使用运行线程及调整线程数大小的线程池(线程池的优化)

当前在JDK中默认使用的线程池 ThreadPoolExecutor,在具体使用场景中,有以下几个缺点

core线程一般不会timeOut
新任务提交时,如果工作线程数小于 coreSize,会自动先创建线程,即使当前工作线程已经空闲,这样会造成空闲线程浪费
设置的maxSize参数只有在队列满之后,才会生效,而默认情况下容器队列会很大(比如1000)

如一个coreSize为10,maxSize为100,队列长度为1000的线程池,在运行一段时间之后的效果会是以下2个效果:

系统空闲时,线程池中始终保持10个线程不变,有一部分线程在执行任务,另一部分线程一直wait中(即使设置allowCoreThreadTimeOut)
系统繁忙时,线程池中线程仍然为10个,但队列中有还没有执行的任务(不超过1000),存在任务堆积现象

本文将描述一下简单版本的线程池,参考于 Tomcat ThreadPoolExecutor, 实现以下3个目标

新任务提交时,如果有空闲线程,直接让空闲线程执行任务,而非创建新线程
如果coreSize满了,并且线程数没有超过maxSize,则优先创建线程,而不是放入队列
其它规则与ThreadPoolExecutor一致,如 timeOut机制

首先看一下ThreadPoolExecutor的执行逻辑, 其基本逻辑如下

如果线程数小于coreSize,直接创建新线程并执行(coreSize逻辑)
尝试放入队列
放入队列失败,则尝试创建新线程(maxSize逻辑)

而执行线程的任务执行逻辑,就是不断地从队列里面获取任务并执行,换言之,即如果有执行线程,直接往队列里面放任务,执行线程就

会被通知到并直接执行任务.

空闲线程优先

空闲线程优先在基本逻辑中,即如果线程数小于coreSize,但如果有空闲线程,就取消创建线程的逻辑. 在有空闲线程的情况下,直接将

任务放入队列中,即达到任务执行的目的。

这里的逻辑即是直接调整默认的ThreadPoolExecutor逻辑,通过重载 execute(Runnable) 方法达到效果. 具体代码如下所示:

public void execute(Runnable command) {

    //此处优先处理有活跃线程的情况,避免在<coreSize时,直接创建线程

    if(getActiveCount() < getPoolSize()) {

        if(pool1.offer(command)) {

            return;

        }

    }

    super.execute(command);

}

coreSize满了优先创建线程

从之前的逻辑来看,如果放入队列失败,则尝试创建新线程。在这个时候,相应的coreSize肯定已经满了。那么,只需要处理一下逻辑,

将其offer调整为false,即可以实现相应的目的。

这里的逻辑,即是重新定义一个BlockingDeque,重载相应的offer方法,相应的参考如下:

public boolean offer(Runnable o) {

    //这里的parent为ThreadPoolExecutor的引用

    int poolSize = parent.getPoolSize();

    int maxPoolSize = parent.getMaximumPoolSize();

    //还没到最大值,先创建线程

    if(poolSize < maxPoolSize) {

        return false;

    }

    //默认逻辑

    return super.offer(o);

}

即判定当前线程池中线程数如果小于最大线程数,即直接返回false,达到放入队列失败的效果。

总结

按照以上的调整,只需要通过继承自默认的ThreadPoolExecutor和默认的BlockingQueue(如LinkedBlockingDeque),重载2个主要的方

法 ThreadPoolExecutor#execute 和 LinkedBlockingDeque#offer 即达到调整的目的。

缺点在于,实现后的类,在定义时,需要互相引用,因为相应的逻辑中需要互相调用相应的方法,以处理逻辑。此外,

ThreadPoolExecutor的相应方法 getXXX 方法,在调用时都为通过加锁式实现,以精确返回数据,这里在多线程环境中可能会存在一些性

能上的考虑。

在Tomcat默认的worker线程池中,即是采用以上的逻辑来达到工作线程的调整逻辑。因此在spring boot tomcat embedded中,通过参

数 server.tomcat.max-thread 和 min-thread 即是通过优先调整线程来达到控制工作线程的目的。 相应的处理类为

org.apache.tomcat.util.threads 下的 ThreadPoolExecutor 和 TaskQueue。

在基于spring体系的业务中正确地关闭线程池

在业务代码中,特别是基于spring体系的代码中,均会使用线程池进行一些操作,比如异步处理消息,定时任务,以及一些需要与当前业

务分离开的操作等。常规情况下,使用spring体系的TaskExecutor或者是自己定义ExecutorService,均可以正常地完成相应的操作。不

论是定义一个spring bean,或者是使用 static Thread工具类均是能满足条件。

但是,如果需要正常地关闭spring容器时,这些线程池就不一定能够按照预期地关闭了。结果就是,当使用代码 context.close() 时,期望

进程会正常地退出,但实际上进程并不会退出掉.原因就在于这些线程池中还在运行的线程。

本文描述了在基于spring boot的项目中,如何正确地配置线程池,以保证线程池能够正确的在整个spring容器周期内运行,并且在容器

正常关闭时能够一并退出掉.

定义bean时添加destroyMethod方法或相应生命周期方法
设置线程池中线程为daemon
为每个线程池正确地命名及使用ThreadFactory
丢弃不再需要的周期性任务
监听ContextClosedEvent,触发额外操作

定义Bean时的Lifecycle定义

将线程池定义为spring bean,为保证在容器关闭时,线程池一并关闭,可以为其定义相应的关闭方法。以下为特定的bean的关闭方法

ThreadPoolTaskExecutor#destroy

ThreadPoolTaskExecutor#destroy

ScheduledThreadPoolExecutor#shutdown

针对 ConcurrentTaskExecutor 这种并没有实现关闭方法的bean,则需要保证其所使用 executor 能够被正常地关闭

在spring 体系,让一个bean可以接收Lifecycle管理,有多种方法,如下所示

@Bean(destroyMethod) //定义时

@PreDestroy //bean方法

DisposableBean  //接口

AutoCloseable   //接口

Lifecycle   //接口

//有方法名为 shutdown

设置线程池中线程为Daemon

一般情况下,关闭线程池后,线程池会自行将其中的线程结束掉.但针对一些自己伪装或直接new Thread()的这种线程,则仍会阻塞进程关闭。

按照,java进程关闭判定方法,当只存在Daemon线程时,进程才会正常关闭。因此,这里建议这些非主要线程均设置为 daemon,即不会阻塞进程关闭.

Thread.setDaemon(true)

不过更方便的是使用ThreadFactory,其在 newThread 时,可以直接操作定义出来的Thread对象(如后面所示)

正确命名Thread

在使用线程池时,一般会接受 ThreadFactory 对象,来控制如何创建thread。在java自带的ExecutorService时,如果没有设置此参数,

则会使用默认的 DefaultThreadFactory. 效果就是,你会在 线程栈列表中,看到一堆的 pool-x-thread-y,在实际使用 jstack时,根本看

不清这些线程每个所属的组,以及具体作用。

这里建议使用 guava 工具类 ThreadFactoryBuilder 来构建,如下代码参考所示

ThreadFactory threadFactory = new ThreadFactoryBuilder().setDaemon(true).setNameFormat("定义任务-%d").build()

此代码,一是定义所有创建出来的线程为 daemon,此外相应的线程name均为 指定前缀开始。在线程栈中可以很方便地查找和定位. 此

外,在判断影响进程退出时,也可以很方便地判断出是否是相关的线程池存在问题(查看daemon属性及name).

丢弃不再可用周期性任务

一般情况下,使用 java 自带的 ScheduledThreadPoolExecutor, 调用 scheduleAtFixedRate 及 scheduleWithFixedDelay 均会将任务设

置为周期性的(period)。在线程池关闭时,这些任务均可以直接被丢弃掉(默认情况下). 但如果使用 schedule 添加远期的任务时,线程池

则会因为其不是 周期性任务而不会关闭所对应的线程

如 spring 体系中 TriggerTask(包括CronTask), 来进行定时调度的任务,其最终均是通过 schedule 来实现调度,并在单个任务完成之后

,再次 schedule 下一次任务的方式来执行。这种方式会被认为并不是 period. 因此,使用此调度方式时,尽管容器关闭时,执行了

shutdown 方法,但相应底层的 ScheduledExecutorService 仍然不会成功关闭掉(尽管所有的状态均已经设置完)。最终效果就是,会看

到一个已经处于shutdown状态的线程池,但线程仍然在运行(状态为 wait 任务)的情况.

为解决此方法,java 提供一个额外的设置参数 executeExistingDelayedTasksAfterShutdown, 此值默认为true,即 shutdown 之后,仍然

执行。可以通过在定义线程池时将其设置为 false,即线程池关闭之后,不再运行这些延时任务.

监听ContextClosedEvent事件

针对在业务中自己构建的bean时,可以通过上面的方式进行相应的控制,如果是三方的代码。则需要在容器关闭时通过触发额外的操

作,来显示地进行三方的代码。如,在代码中获取到三方的对象,主动调用其的close方法.

惟一需要作的就是监听到容器关闭事件,然后在回调代码中进行相应的操作。因此,只需要定义beqn,实现以下接口即可

CallbackObj implements ApplicationListener<ContextClosedEvent> {

    public void onApplicationEvent(ContextClosedEvent event) {}

}

当此对象声明为bean时,当容器关闭时,会触发相应的事件,并回调起相应的操作.

总结

上面的几个方面均是从仔细控制线程池的创建和销毁来进行描述。基本思路即是从对象管理,生命周期以及代码控制多个方面来处理.在

实际项目中,业务使用方只需要使用相应的组件即可,但组件提供方需要正确地提供,才能保证基础架构的完整性.

在java web项目中慎用Executors以及非守护线程

最近研究embeded tomcat,特别是关于tomcat启动和关闭的模块。通过查看相应的源代码, 我们知道tomcat的关闭是通过往相应的关

闭端口发送指定的关闭指令来达到关闭tomcat的目的。但是有的时候,通过shutdown.bat或shutdown.sh却不能有效地关闭tomcat,网

上也有很多人提出这个问题。通过相关资料,最后问题出现线程上。

首先看java虚拟机退出的条件,如下所示:

a,调用了 Runtime 类的 exit 方法,并且安全管理器允许退出操作发生。

b,非守护线程的所有线程都已停止运行,无论是通过从对 run 方法的调用中返回,还是通过抛出一个传播到 run 方法之外的异常。

如上所示,第一条是通过exit退出,第二条指出了一个正常程序退出的条件,就是所有的非守护线程都已经停止运行。我们看相应embed

tomcat的启动代码,如下所示:

tomcat.start();
tomcat.getServer().await();

最后一条有效的运行命令即是await,通过调用shutdown命令时,这个await就会成功的返回。按照常理来说,整个程序即会成功的完

成。但是程序有时候并没有成功的结束,原因就在于程序中还存在着非守护进程。

对于tomcat来说,tomcat程序中开启的所有进程都是守护进程,所以tomcat自身可以保证程序的正常结束。当await结束时,tomcat所

就正常的结束了,包括相应的监听端口等,都已经成功结束。然而,由于项目程序中仍然还有其它线程在运行,所以导致java虚拟机并没

有成功的退出。

在我们的项目中,很多时候都运用到了线程。比如,异步调用等。不过,幸运的是,这些线程往往都是守护线程,原因就在于tomcat在

运行我们的项目时,对于每一个请求,tomcat是使用了守护线程来进行相应的请求调用,这个保证在以下代码:

AprEndpoint

 // Start poller threads
        pollers = new Poller[pollerThreadCount];

        for (int i = 0; i < pollerThreadCount; i++) {

            pollers[i] = new Poller(false);

            pollers[i].init();

            Thread pollerThread = new Thread(pollers[i], getName() + "-Poller-" + i);

            pollerThread.setPriority(threadPriority);

            pollerThread.setDaemon(true);

            pollerThread.start();

        }

所以,一般情况下,在我们的项目代码中使用new Thread建立的线程都是守护线程,原因就是新建线程默认上使用建立线程时的当前线

程所处的守护状态。tomcat的请求处理线程为守护线程,所以我们一般情况下建立的线程也是守护线程。然而,Executors除外。

使用Executors建立后台线程并执行一些多线程操作时,Executors会使用相对应的threadFactory来对runnable建立新的thread,所以

使用默认的threadFactory时就会出问题。默认的ThreadFactory强制性的将新创建的线程设置为非守护状态,如下所示:

 public Thread newThread(Runnable r) {       
        Thread t = new Thread(group, r,

                              namePrefix + threadNumber.getAndIncrement(),

                              0);

        if (t.isDaemon())

            t.setDaemon(false);

        if (t.getPriority() != Thread.NORM_PRIORITY)

            t.setPriority(Thread.NORM_PRIORITY);

        return t;

    }

所以,一般情况下,我们使用executors创建多线程时,就会使用默认的threadFactory(即调用只有一个参数的工厂方法),而创建出来

的线程就是非守护的。而相应的程序就永远不会退出,如采用Executors创建定时调度任务时,这个调试任务永远不会退出。解决的办法

就是重写相对应的threadFactory,如下所示:

new ThreadFactory() {    
    public Thread newThread(Runnable r) {

        Thread s = Executors.defaultThreadFactory().newThread(r);

        s.setDaemon(true);

        return s;

    }

}

同理,对于java web项目中的线程程序,一定要记住将相应的线程标记为守护线程(尽管它默认就是守护的)。而对于使用Executors,

一定要记住传递相应的threadFactory实现,以重写相应的newThread方法,将线程标记为守护线程。

以上的结论对于普通的java项目同样有效,为了正常的结束相应的程序,一定要正确的使用相应的线程,以避免java程序不能退出的问

题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值