Java 多线程

Java多线程

1、什么是进程

百度百科:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

2、什么是线程

百度百科:线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位(线程是进程中的一个实体)。

3、进程与线程之间的关系

  • 线程属于进程的实体,也就是说明线程是必须依赖于进程

  • 进程里面至少有一个线程, 没有线程此进程也无法运行

  • 一个进程允许有多个线程

4、进程的特点

  • 独立性:进程是系统进行资源分配和调度的一个独立单位

  • 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的

  • 并发性(支持多进程):任何进程都可以同其他进程一起并发执行

  • 结构性:进程由程序(控制进程执行的指令集),数据(数据集合是程序在执行时所需要的数据和工作区)和进程控制块(程序控制块包含进程的描述信息和控制信息是进程存在的唯一标志)三部分组成

5、线程的特点

  • 允许跨线程资源共享

  • 创建线程的开销要比进程小(进程要分配一大部分的内存,而线程只需要分配一部分栈就可以了)

  • 提高进程运行的效率

6、创建线程的三种方式

a、继承 Thread 类

class MyThread extends Thread{  //继承Thread类,作为线程的实现类 
     private String name ; // 表示线程的名称
     public MyThread(String name){
         this.name = name ; // 通过构造方法配置name属性
    }
     public void run(){  // 覆写run()方法,作为线程的操作主体
         for(int i=0;i<10;i++){
             System.out.println(name + "运行,i = " + i) ;
        }
    }
public class ThreadDemo02{
    public static void main(String args[]){
        // 实例化对象
        MyThread mt1 = new MyThread("线程A ") ;    
        MyThread mt2 = new MyThread("线程B ") ;
         //启动线程
        mt1.start() ;  
        mt2.start() ;  
    }
}

b、实现 Runnable 接口

class MyThread implements Runnable{ // 实现Runnable接口,作为线程的实现类 
     private String name ;       // 表示线程的名称
     public MyThread(String name){
        this.name = name ;      // 通过构造方法配置name属性
    }
     public void run(){  // 覆写run()方法,作为线程的操作主体
         for(int i=0;i<10;i++){
             System.out.println(name + "运行,i = " + i) ;
        }
    }
   public class RunnableDemo01{
    public static void main(String args[]){
            // 实例化对象
            MyThread mt1 = new MyThread("线程A ") ;    
            MyThread mt2 = new MyThread("线程B ") ;  
             // 实例化Thread类对象
            Thread t1 = new Thread(mt1) ;      
            Thread t2 = new Thread(mt2) ;      
            // 启动线程
            t1.start() ;    
        t2.start() ;  
    }
}

实现Runnable接口创建线程也可以通过匿名内部类来完成

 public static void main(String[] args) {
       new Thread(new Runnable() {
           @Override
           public void run() {
               for (int i = 0; i < 10; i++) {
                   System.out.println(i);
              }
          }
      }).start();
  }

实现Runnable接口创建线程还可以通过Lambda表达式来完成

public static void main(String[] args) {
       new Thread(()->{
           for (int i = 0; i < 10; i++) {
          System.out.println(i);
          }
      }).start();
  }

注意事项

  • Thread类本身也实现了Runnable接口(看源码)

  • 不能反复调用同一个线程的start()方法,会抛异常

  • 线程启动虽然调用的是 start() 方法,但实际上调用的却是 run() 方法定义的主体

  • 不调用start()方法,直接调用run()方法并没有创建线程,不具备多线程效果(https://www.cnblogs.com/heqiyoujing/p/11355264.html参考资料

c、实现Callable接口

直接继承Thread和实现Runnable接口都有一个缺陷就是:在执行完任务之后无法获取执行结果。但是实现Callable接口方法可以获取执行结果

首先,我们需要知道以下类或接口:

  • Future接口

    • 对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。

  • RunnableFuture接口

    • RunnableFuture继承了Runnable接口和Future接口

  • FutureTask

    • FutureTask实现了RunnableFuture接口,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

代码实现

public class MyThread implements Callable<Long> {
   
   @Override
   public Long call() throws Exception {
       //输出线程名和线程id
       System.out.println(Thread.currentThread().getName()+" "+Thread.currentThread().getId());
       //返回线程id
       return Thread.currentThread().getId();
  }
}
public class MyThreadTest {
   
   public static void main(String[] args) throws ExecutionException, InterruptedException {
       MyThread myThread=new MyThread();
       FutureTask futureTask=new FutureTask(myThread);
       //创建线程
       Thread thread=new Thread(futureTask);
       //启动线程
       thread.start();
       //得到线程返回结果并输出(如果没有启动线程而直接获取返回值,程序陷入类似死循环)
       System.out.println(futureTask.get());
  }

}

也可以写成匿名内部类

public static void main(String[] args) throws ExecutionException, InterruptedException {
     
       new Thread(new FutureTask<Long>(new Callable<Long>() {
           @Override
           public Long call() throws Exception {
               //输出线程名和线程id
           System.out.println(Thread.currentThread().getName()+" "+Thread.currentThread().getId());
               //返回线程id
               return Thread.currentThread().getId();
          }
      })).start();

  }

还可以写成Lambda表达式

public static void main(String[] args) throws ExecutionException, InterruptedException {

       new Thread(new FutureTask<Long>(()->{
           //输出线程名和线程id
           System.out.println(Thread.currentThread().getName()+" "+Thread.currentThread().getId());
           //返回线程id
           return Thread.currentThread().getId();
      })).start();
   
  }

7、继承 Thread 类和实现Runnable接口两种方式的区别

  • 实现Runnable 接口,可以方便实现资源的共享

image-20200806145221250

8、volatile关键字

如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的(简单提一下,具体内容不过多阐述,还需要自己查阅资料)

9、线程的生命周期

  • 线程从创建到销毁的过程,包含五个状态:

    • 新建(new一个线程)

    • 就绪:有执行资格,没有执行权

    • 运行:有执行资格,有执行权

    • 阻塞:由于一些操作让线程处于该状态,没有执行资格,没有执行权,但另一些操作可以使它激活,激活后处于就绪状态

    • 死亡:线程对象变成垃圾,等待回收

  • 线程状态转换图解

    image-20200806155959385

在此提出一个问题,Java 程序每次运行至少启动几个线程?

回答:至少启动两个线程,每当使用 Java 命令执行一个类时,实际上都会启动一个 JVM,每一个JVM实际上就是在操作系统中启动一个线程,Java 本身具备了垃圾的收集机制。所以在 Java 运行时至少会启动两个线程,一个是 main 线程,另外一个是垃圾收集线程。

10、控制线程的方法

a、sleep()和wait()的区别

  • sleep是线程中的方法,但是wait是Object中的方法。

  • sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中(重点)

  • sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。

  • sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。

  注意一点:阻塞被唤醒后还是从原来被阻塞的点开始往下执行

b、join()

join在线程里面意味着“插队”,哪个线程调用join代表哪个线程插队先执行——但是插谁的队是有讲究了,不是说你可以插到队头去做第一个吃螃蟹的人,而是插到在当前运行线程的前面,比如系统目前运行线程A,在线程A里面调用了线程B.join方法,则接下来线程B会抢先在线程A面前执行,等到线程B全部执行完后才继续执行线程A。

  • 代码体验

/** 定义的线程类 * */ 
class MyThread extends Thread {
   
   @Override
   public void run() {
       for(int i=0;i<10;i++) {
           System.out.println("thread id="+Thread.currentThread().getId()+" i="+i);
           //睡眠1秒
           try {
               Thread.sleep(1000);
          } catch (InterruptedException e) {
               // TODO Auto-generated catch block e.printStackTrace();
          }
      }
  }
}
public class MainTest {

   public static void main(String[] args) {
       System.out.println("主线程的main开始执行");
       //创建及启动子线程
       MyThread t=new MyThread();
       t.start();
       try {
           //等待子线程执行完成,才继续执行主线程
           t.join();
      } catch (InterruptedException e) {
           // TODO Auto-generated catch block
           e.printStackTrace();
      }
       System.out.println("主线程的main结束执行");
  }
}

当系统中正在运行多个线程时,join()到底是暂停了哪些线程?t.join()方法会使所有线程都暂停并等待t的执行完毕吗

答:t.join()方法只会使主线程(或者说调用t.join()的线程)进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。

c、yield()

由运行态到就绪态,停止一下后再由就绪态到运行态

代码体验

//创建一个线程类
class MyThread extends Thread {
   
   @Override
   public void run() {
       for(int i=0;i<100;i++) {
          System.out.println("子线程ID="+Thread.currentThread().getId()+" i="+i);
           if(i==30) {
               System.out.println("子线程调用yield方法");
          Thread.yield();
            }
      }
  }
}
public class MainTest {
   
public static void main(String[] args) {
       //创建线程
       MyThread t1=new MyThread();
       //启动线程
       t1.start();
       for(int j=0;j<100;j++) {
           if(j==60) {  
          System.out.println("主线程调用yield方法");
               //当前线程暂停(从运行态---->就绪态)
               Thread.yield();
          }
System.out.println("主线程ID="+Thread.currentThread().getId()+" j="+j);
      }
  }
}

d、中断线程

参考资料https://www.cnblogs.com/liyutian/p/10196044.html

e、设置线程优先级

java 中的线程优先级的范围是1~10,默认的优先级是5。10最高。

可以通过getPriority()获取线程优先级,setPriority()设置线程优先级。设置线程的优先级应该在线程启动之前。

f、后台线程

参考资料https://blog.csdn.net/staticFinal_shuaibi/article/details/84865688

10、解决线程同步问题

当一个类中的属性被多个线程共享,那么就会造成一种问题,如果这多个线程要操作同一个资源时就有可能出现资源同步问题。

实现线程同步有很多种方法,这里讲三种(文末资料有补充)

a、同步方法

  • 用synchronized关键字修饰方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

  • 代码体现

public class Bank {
   private int count = 0;// 账户余额

   // 存钱
   public synchronized void addMoney(int money) {
       count += money;
       System.out.println(System.currentTimeMillis() + "存进:" + money);
  }

   // 取钱
   public synchronized void subMoney(int money) {
       if (count - money < 0) {
           System.out.println("余额不足");
           return;
      }
       count -= money;
       System.out.println(+System.currentTimeMillis() + "取出:" + money);
  }

   // 查询
   public void lookMoney() {
       System.out.println("账户余额:" + count);
  }
}

b、同步代码块

  • 用synchronized关键字修饰语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步

  • 同步代码块格式

    synchronized(同步对象)
     需要同步的代码
  • 代码体现

    public class Bank {
       private int count = 0;// 账户余额

       // 存钱
       public  void addMoney(int money) {
           synchronized(this){
               count += money;
          }
           
           System.out.println(System.currentTimeMillis() + "存进:" + money);
      }

       // 取钱
       public  void subMoney(int money) {
           synchronized(this){
               if (count - money < 0) {
                   System.out.println("余额不足");
                   return;
              }
               count -= money;
          }
           
           System.out.println(+System.currentTimeMillis() + "取出:" + money);
      }

       // 查询
       public void lookMoney() {
           System.out.println("账户余额:" + count);
      }
    }

注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

c、显示加锁

  • 在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法具有相同的基本行为和语义,并且扩展了其能力。

    • ReenreantLock类的常用方法有:

      • ReentrantLock() : 创建一个ReentrantLock实例

      • lock() : 获得锁

      • unlock() : 释放锁

    • 注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用

  • 代码体验

    public class Bank {
       private int count = 0;// 账户余额

       // 需要声明这个锁
       private Lock lock = new ReentrantLock();

       // 存钱
       public void addMoney(int money) {
           lock.lock();
           try {
               count += money;
               System.out.println(System.currentTimeMillis() + "存进:" + money);
          } finally {
               lock.unlock();
          }
      }

       // 取钱
       public void subMoney(int money) {
           lock.lock();
           try {

               if (count - money < 0) {
                   System.out.println("余额不足");
                   return;
              }
               count -= money;
               System.out.println(+System.currentTimeMillis() + "取出:" + money);
          } finally {
               lock.unlock();
          }
      }

       // 查询
       public void lookMoney() {
           System.out.println("账户余额:" + count);
      }
    }

    1、ReentrantLock()还可以通过public ReentrantLock(boolean fair)构造方法创建公平锁,即,优先运行等待时间最长的线程,这样大幅度降低程序运行效率。 2、关于Lock对象和synchronized关键字的选择: (1)、最好两个都不用,使用一种java.util.concurrent包提供的机制,能够帮助用户处理所有与锁相关的代码。 (2)、如果synchronized关键字能够满足用户的需求,就用synchronized,他能简化代码。 (3)、如果需要使用更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally中释放锁。

11、线程同步之死锁

同步可以保证资源共享操作的正确性,但是过多同步也会产生问题。例如,现在张三想要李四的画,李四想要张三的书,张三对李四说“把你的画给我,我就给你书”,李四也对张三说“把你的书给我,我就给你画”两个人互相等对方先行动,就这么干等没有结果,这实际上就是死锁的概念。

所谓死锁,就是两个线程都在等待对方先完成,造成程序的停滞,一般程序的死锁都是在程序运行时出现的。

  • 代码体验

class Zhangsan{ // 定义张三类
   public void say(){
       System.out.println("张三对李四说:“你给我画,我就把书给你。”") ;
  }
   public void get(){
       System.out.println("张三得到画了。") ;
  }
}
class Lisi{ // 定义李四类
   public void say(){
       System.out.println("李四对张三说:“你给我书,我就把画给你”") ;
  }
   public void get(){
       System.out.println("李四得到书了。") ;
  }
}
public class ThreadDeadLock implements Runnable{
    // 实例化static型对象(注意这里必须是static修饰s,因为静态成员依赖类存在只加载一次,不是静态就不止加载一次,对象不同,锁不同)
   private static Zhangsan zs = new Zhangsan() ;
    // 实例化static型对象(注意这里必须是static修饰,因为静态成员依赖类存在只加载一次,不是静态就不止加载一次,对象不同,锁不同)
   private static Lisi ls = new Lisi() ;      
   private boolean flag = false ;  // 声明标志位,判断那个先说话
   public void run(){  // 覆写run()方法
       if(flag){
           synchronized(zs){// 同步张三
               //张三说话
               zs.say() ;
               try{
                   //线程睡眠
                   Thread.sleep(500) ;
              }catch(InterruptedException e){
                   e.printStackTrace() ;
              }
               synchronized(ls){//同步李四
                   //张三得到画
                   zs.get() ;
              }
          }
      }else{
           synchronized(ls){//同步李四
               //李四说话
               ls.say() ;
               try{
                   //线程睡眠
                   Thread.sleep(500) ;
              }catch(InterruptedException e){
                   e.printStackTrace() ;
              }
               synchronized(zs){
                   //李四得到书
                   ls.get() ;
              }
          }
      }
  }
   public static void main(String args[]){
       // 控制张三
       ThreadDeadLock t1 = new ThreadDeadLock() ;
       // 控制李四
       ThreadDeadLock t2 = new ThreadDeadLock() ;      
       t1.flag = true ;
       t2.flag = false ;
       //创建线程
       Thread thA = new Thread(t1) ;
       Thread thB = new Thread(t2) ;
       //启动线程
       thA.start() ;
       thB.start() ;
  }
}
  • 程序进入死锁状态

12、线程通信

当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,但Java也提供了一些机制来保证线程协调运行

线程通信通常用于生产者/消费者模式

a、传统的线程通信

假设现在系统中有两个线程,这两个线程分别代表存款者和取钱者----现在有一种特殊的要求:存款者和取钱者不断地重复存款、取款动作,而且要求每当存款者将钱存入指定账户后,取钱者就立即取出该笔钱。不允许连续存款和取款。

为了实现这种功能,可以借助于Object类提供的wati()、notify()、notifyAll()三个方法。下面是关于这三个方法的解释:

  • wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()或notufyAll()来唤醒该线程(带参数则等待指定时间后自动苏醒)

  • notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则随机选择一个线程唤醒

  • notifyAll():唤醒在此同步监视器上等待的所有线程

代码体验

/**
*账户类
*/
public class Account {
   private double balance;//存款
   private int id;//编号
   private boolean flag=true;//判断账户是否有存款

   public Account() {
  }

   public Account(double balance, int id) {
       this.balance = balance;
       this.id = id;
  }

   public int getId() {
       return id;
  }

   public void setId(int id) {
       this.id = id;
  }

   public double getBalance() {
       return balance;
  }

   /**
    * 取钱
    */
   public synchronized void draw(int money){
       if (flag){//账户有存款
           System.out.println(DrawThread.currentThread().getName()+"取钱"+money);
           //取钱
           balance-=money;
           System.out.println("账户余额为"+balance);
           //账户无存款
           flag=false;
           //唤醒存钱线程
           notifyAll();
      }else{//账户没存款
           //线程等待
           try {
               wait();
          } catch (InterruptedException e) {
               e.printStackTrace();
          }
      }
  }
   /**
    * 存钱
    */
   public synchronized void deposit(int money){
       if (!flag){//账户没存款
           System.out.println(DrawThread.currentThread().getName()+"存钱"+money);
           //存钱
           balance+=money;
           System.out.println("账户余额为"+balance);
           //账户有存款
           flag=true;
           //唤醒取款
           notifyAll();
      } else{//账户有存款
           //线程等待
           try {
               wait();
          } catch (InterruptedException e) {
               e.printStackTrace();
          }
      }
  }
}
/**
* 存钱线程
*/
public class DepositThread extends Thread{
   private Account account;//账户
   private String name;//线程名
   private int money;//存款金额

   public DepositThread(String name, Account account, int money) {
       super(name);
       this.account = account;
       this.money = money;
  }

   @Override
   public void run() {
       //存钱十次
       for (int i = 0; i < 10; i++) {
           account.deposit(money);
      }
  }
}
/**
* 取钱线程
*/
public class DrawThread extends Thread{
   private Account account;//账户
   private String name;//线程名
   private int money;//取款金额

   public DrawThread(String name, Account account, int money) {
       super(name);
       this.account = account;
       this.money = money;
  }

   @Override
   public void run() {
       //取款十次
       for (int i = 0; i < 10; i++) {
           account.draw(money);
      }
  }
}
/**
*测试类
*/
public class ThreadTest {

   public static void main(String[] args) {
       //账户
       Account account=new Account(1000,1);
       //创建线程
       DepositThread depositThread=new DepositThread("lsy",account,1000);
       DrawThread drawThread=new DrawThread("yl",account,1000);
       //启动线程
       depositThread.start();
       drawThread.start();
  }

}

b、使用Condition控制线程通信

如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait()、notify()、notifyAll()方法进行线程通信了。(这三个方法是依赖于同步方法或同步代码块存在)

当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调

Condition将同步监视器方法(wait()、notify()、notifyAll())分解成截然不同的对象与Loc对象组合使用,Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能

Condition实例被绑定在一个Lock对象上。要获得特定Lock实例的Condition实例,调用Lock对象的newCondition()即可。Condition提供了如下三个方法:

  • await():类似wait(),导致当前线程等待,直到其他线程调用该Condition的signal()或signalAll()来唤醒该线程

  • signal():唤醒在此Lock对象上等待的单个线程。如果所有线程都在此Lock对象上等待,则随机选择一个线程唤醒

  • signalAll():唤醒在此Lock对象上等待的所有线程

代码体验

/**
* 账户
*/
public class Account {
   private double balance;//存款
   private int id;//编号
   private boolean flag=true;//判断账户是否有存款
   private final Lock lock=new ReentrantLock();//锁
   private final Condition condition=lock.newCondition();//获取锁对应的condition

   public Account() {
  }

   public Account(double balance, int id) {
       this.balance = balance;
       this.id = id;
  }

   public int getId() {
       return id;
  }

   public void setId(int id) {
       this.id = id;
  }

   public double getBalance() {
       return balance;
  }

   /**
    * 取钱
    */
   public void draw(int money){
       //加锁
       lock.lock();
       try {
           //账户没存款
           while (!flag){
               //线程等待
               wait();
          }
           //账户有存款
           System.out.println(DrawThread.currentThread().getName()+"取钱"+money);
           //取钱
           balance-=money;
           System.out.println("账户余额为"+balance);
           //账户无存款
           flag=false;
           //唤醒存钱线程
           notifyAll();
      } catch (InterruptedException e) {
           e.printStackTrace();
      }finally {
           //释放锁
           lock.unlock();
      }
  }
   /**
    * 存钱
    */
   public synchronized void deposit(int money){
       //加锁
       lock.lock();
       try {
           while (flag){//账户有存款
               //线程等待
               wait();
          }
           //账户没存款
           System.out.println(DrawThread.currentThread().getName()+"存钱"+money);
           //存钱
           balance+=money;
           System.out.println("账户余额为"+balance);
           //账户有存款
           flag=true;
           //唤醒取款
           notifyAll();
      } catch (Exception e) {
           e.printStackTrace();
      } finally {
           //释放锁
           lock.unlock();
      }
  }
}
/**
* 存钱线程
*/
public class DepositThread extends Thread{
   private com.yl.threadCommunication.Account account;//账户
   private String name;//线程名
   private int money;//存款金额

   public DepositThread(String name, Account account, int money) {
       super(name);
       this.account = account;
       this.money = money;
  }

   @Override
   public void run() {
       //存钱十次
       for (int i = 0; i < 10; i++) {
           account.deposit(money);
      }
  }
}
/**
* 取钱线程
*/
public class DrawThread extends Thread{
   private com.yl.threadCommunication.Account account;//账户
   private String name;//线程名
   private int money;//取款金额

   public DrawThread(String name, Account account, int money) {
       super(name);
       this.account = account;
       this.money = money;
  }

   @Override
   public void run() {
       //取款十次
       for (int i = 0; i < 10; i++) {
           account.draw(money);
      }
  }
}
/**
* 测试
*/
public class ThreadTest {

   public static void main(String[] args) {
       //账户
       com.yl.threadCommunication.Account account=new Account(1000,1);
       //创建线程
       com.yl.threadCommunication.DepositThread depositThread=new DepositThread("lsy",account,1000);
       com.yl.threadCommunication.DrawThread drawThread=new DrawThread("yl",account,1000);
       //启动线程
       depositThread.start();
       drawThread.start();
  }

}

c、使用阻塞队列(BlockingQueue)控制线程通信(了解)

Java5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途并不是作为容器,而是作为线程同步的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞

程序的两个线程通过交替向BlockingQueue中放入元素,取出元素,即可很好地控制线程的通信。BlockingQueue提供如下两个支持阻塞的方法

  • put(E e):尝试把E元素放入BlockingQueue中,如果队列已满,则阻塞

  • take():尝试从BlockingQueue的头部取出元素,如果队列已空,则阻塞

13、线程池

a、线程池的好处

系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互。在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池

与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个线程来执行它们的run()或call();当run()或call()执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run()或call()

除此之外,使用线程池可以有效地控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致jvm崩溃,而线程池的最大线程参数可以控制系统中并发线程数不超过此数

b、线程池工作原理

1、如果线程池中的线程数量未达到核心线程的数量,那么会直接启动-一个核心线程来执行任 2、如果线程池中的线程数量已经达到或者超过核心线程的数量,那么任务会被插入到任务队列中 排队等待执行。 3、如果在步骤2中无法将任务插入到任务队列中,这往往是由于任务队列已满,这个时候如果线 程数量未达到线程池规定的最大值,那么会立刻启动一个非核心线程来执行。 4、如果步骤3中线程数量已经达到线程池规定的最大值,那么就拒绝执行此任务。

c、线程池执行线程任务步骤

  • 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池

  • 创建Runnable实现类或Callable实现类的实例,作为线程执行任务

  • 调用ExecutorService对象的submit()提交Runnable实例或Callable实例

  • 当不想提交任何任务时,调用ExecutorService对象的shutdown()关闭线程池

代码体验

public static void main(String[] args) {
       //保存等待线程的队列
       BlockingQueue<Runnable> blockingQueue=new ArrayBlockingQueue<>(3);
       //线程阻止处理程序
       ThreadPoolExecutor.AbortPolicy abortPolicy=new ThreadPoolExecutor.AbortPolicy();
       //线程池
       ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(3,3,2000,TimeUnit.SECONDS,                                    blockingQueue,abortPolicy);
       //Runnable
       MyThread myThread1=new MyThread();
       MyThread myThread2=new MyThread();
       MyThread myThread3=new MyThread();
       //提交线程
       threadPoolExecutor.submit(myThread1);
       threadPoolExecutor.submit(myThread2);
       threadPoolExecutor.submit(myThread3);
       //关闭线程池
       threadPoolExecutor.shutdown();
  }

14、ThreadLocal类

ThreadLocal为每一个使用该变量的线程都提供了一个变量值的副本,使每一个线程都可以独立的改变自己的副本,而不会和其他线程的副本冲突。从线程的角度看,就好像每个线程都完全拥有该变量一样

  • 用法

    • T get():返回此线程局部变量中当前线程副本的值

    • void remove():删除此线程局部变量中当前线程的值

    • void set(T value):设置此线程局部变量中当前线程副本的值

代码体验

class MyRunnable implements Runnable {
   private int i = 0; //ThreadLocal默认初始化数据值0
   ThreadLocal<Integer> threadId = new ThreadLocal<Integer> () {
@Override
   protected Integer initialValue() {
   return 0;
}
};
@Override public void run() {
   for (int j = 0; j < 10; j++) {
       //每个线程都有一份threadId变量的副本
       //针对副本+1操作
threadId.set(threadId.get()+1);
       try {
           Thread.sleep(200);
      } catch (InterruptedException e) {
           // TODO Auto-generated catch block
           e.printStackTrace();
      }
       //针对副本-1操作
       threadId.set(threadId.get()-1);
       System.out.println("thread id=" +Thread.currentThread().getId() + " i=" + threadId.get());
  }
}
}
public class MainTest {
   public static void main(String[] args) {
       MyRunnable r = new MyRunnable();
       // 创建线程一
       new Thread(r).start();
       // 创建线程二
       new Thread(r).start();
  }
}

参考资料

本文参考了以下资料

 

记得快乐
原文地址:https://www.cnblogs.com/Y-wee/p/13463587.html