暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Java多线程入门

Hello 帅帅 2021-02-05
771

基础概念

并发与并行

并发:指两个或多个事件在同一个时间段内发生。

并行:指两个或多个事件在同一时刻发生(同时发生)

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行, 即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

线程与进程

进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

图中每一行代表一个进程

图中每一行代表一个线程

线程的创建

线程的创建共有两种方式,一种是继承 Thread 类,另一种就是实现 Runnable 接口

实现 Runnable 接口:

public class MyThread1 implements Runnable {
   private final String name;

   public MyThread1(String name) {
       this.name = name;
  }

   @Override
   public void run() {
       for (int i = 0; i < 10; i++) {
           System.out.println("线程:" + this.name + " "+ i + "-----");
      }
  }
}

执行:

public class Main {
   public static void main(String[] args) {
       MyThread1 m1 = new MyThread1("线程1");
       MyThread1 m2 = new MyThread1("线程2");

       Thread t1 = new Thread(m1);
       Thread t2 = new Thread(m2);

       t1.start();
       t2.start();
  }
}
输出:
线程:线程1  0-----
线程:线程2  0-----
线程:线程1  1-----
线程:线程2  1-----
线程:线程1  2-----
线程:线程2  2-----
线程:线程1  3-----
线程:线程2  3-----
线程:线程1  4-----
线程:线程2  4-----
线程:线程1  5-----
线程:线程2  5-----
线程:线程1  6-----
线程:线程2  6-----
线程:线程1  7-----
线程:线程2  7-----
线程:线程1  8-----
线程:线程2  8-----
线程:线程1  9-----
线程:线程2  9-----

继承 Thread 类:

public class MyThread2 extends Thread {
   private String name;
   public MyThread2(String name){
       this.name=name;
  }
   public void run(){
       for (int i = 0; i < 10; i++) {
           System.out.println("线程:" + this.name + " "+ i + "-----");
      }
  }
}

执行:

public class ThreadDemo2 {
   public static void main(String[] args) {
       MyThread2 m1=new MyThread2("线程1");
       MyThread2 m2=new MyThread2("线程2");

       m1.start();
       m2.start();
  }
}
输出:
线程:线程1  0-----
线程:线程2  0-----
线程:线程1  1-----
线程:线程2  1-----
线程:线程2  2-----
线程:线程1  2-----
线程:线程2  3-----
线程:线程1  3-----
线程:线程2  4-----
线程:线程1  4-----
线程:线程2  5-----
线程:线程1  5-----
线程:线程2  6-----
线程:线程1  6-----
线程:线程2  7-----
线程:线程1  7-----
线程:线程2  8-----
线程:线程1  8-----
线程:线程2  9-----
线程:线程1  9-----

实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。

  2. 可以避免java中的单继承的局限性。

  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

  4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

线程安全问题

三个售票口,同时售卖100张票,每张票售卖0.1秒,多线程解决:

public class Shopping implements Runnable {
   private int ticket;
   public Shopping(int ticket) {
       this.ticket = ticket;
  }
   @Override
   public void run() {
       while (true) {
           if (ticket > 0) {
               try {
                   Thread.sleep(100);
              } catch (InterruptedException e) {
                   e.printStackTrace();
              }
                   System.out.println(Thread.currentThread().getName() + "正在卖" + ticket--);
          }
      }
  }
}
public class Demo1 {
   public static void main(String[] args) {
       Shopping s = new Shopping(100);
       Thread t1 = new Thread(s,"售票口1");
       Thread t2 = new Thread(s,"售票口2");
       Thread t3 = new Thread(s,"售票口3");

       t1.start();
       t2.start();
       t3.start();
  }
}

输出问题:

  1. 票重复售卖

  2. 卖出负数票

卖出重复票的原因:线程二在执行完售出后,执行 ticket--前(此时票已售出,但票数没有减少),线程一开始执行,并且顺利售出了一张票,导致同时售出了第100张票。

卖出负数票的原因:在线程1执行if时,票数处于大于零状态,但在票数售出前其他线程执行了ticket- -,之后再执行此线程时便会出错。

解决方案:

同步方法:在方法前加入synchronized
关键字,保证线程执行该方法的时候,其他线程只能在方法外等着(对性能影响较大)

public synchronized void  run() {
   while (true) {
       if (ticket > 0) {
           try {
               Thread.sleep(100);
          } catch (InterruptedException e) {
               e.printStackTrace();
          }
           System.out.println(Thread.currentThread().getName() + "正在卖" + ticket--);
      }
  }
}

同步代码块:将可能出现线程安全的问题放入代码块中,并在代码块前加入synchronized关键字,并且双重检查,防止出错

public void run() {
   while (true) {
       if (ticket > 0) {
           try {
               Thread.sleep(100);
          } catch (InterruptedException e) {
               e.printStackTrace();
          }
           synchronized (Shopping.class) {
               if (ticket > 0) {
                   System.out.println(Thread.currentThread().getName()+"正在卖"+ ticket);
              }
               ticket--;
          }
      }
  }
}

加锁:

java.util.concurrent.locks.Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

  • public void lock()
    :加同步锁。

  • public void unlock()
    :释放同步锁。

public class Shopping implements Runnable {
   private int ticket;
   Lock lock = new ReentrantLock();
   public Shopping(int ticket) {
       this.ticket = ticket;
  }
   @Override
   public void run() {
       while (true) {
           lock.lock();
           if (ticket > 0) {
               try {
                   Thread.sleep(100);
              } catch (InterruptedException e) {
                   e.printStackTrace();
              }
               System.out.println(Thread.currentThread().getName()+"正在卖" +ticket--);
          }
           lock.unlock();
      }
  }
}

线程的状态

在API中java.lang.Thread.State
这个枚举中给出了六种线程状态:

public enum State {
       NEW,
       RUNNABLE,
       BLOCKED,
       WAITING,
       TIMED_WAITING,
       TERMINATED;
}
线程状态导致状态发生条件
NEW(新建)线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可 运行)线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操 作系统处理器。
Blocked(锁阻 塞)当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状 态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限 等待)一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个 状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
TimedWaiting(计时 等待)同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态 将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep、 Object.wait。
Teminated(被 终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

线程优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY
) ~ 10 (Thread.MAX_PRIORITY
)。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

取得和设置线程的名称

Thread.currentThread().getName()//返回当前线程的名字:Thread-0

等待唤醒机制

什么是等待唤醒机制

这是多个线程间的一种协作机制。

就是在一个线程进行了规定操作后,就进入等待状态wait()
, 等待其他线程执行完他们的指定代码过后 再将其唤醒notify()
;在有多个线程进行等待时, 如果需要,可以使用 notifyAll()
来唤醒所有的等待线程。

等待唤醒中的方法

等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:

  1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是通知(notify)在这个对象等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中

  2. notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。

  3. notifyAll:则释放所通知对象的 wait set 上的全部线程。

注意:

哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。

调用wait和notify方法需要注意的细节

  1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。

  1. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。

  1. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。

包子问题:

包子铺线程生产包子,吃货线程消费包子。当包子没有时(包子状态为false),吃货线程等待,包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。接下来,吃货线程能否进一步执行则取决于锁的获取情况。如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待。包子铺线程能否进一步执行则取决于锁的获取情况。

BaoZi

public class BaoZi {
   String pier ;
   String xianer ;
   boolean flag = false ;//包子资源 是否存在 包子资源状态
}

BaoZiPu

public class BaoZiPu extends Thread {
   private BaoZi bz;
   public BaoZiPu(String name, BaoZi bz) {
       super(name);
       this.bz = bz;
  }
   @Override
   public void run() {
       int count = 0;
       //造包子
       while (true) {
           //同步
           synchronized (bz) {
               if (bz.flag) {//包子资源 存在
                   try {
                       bz.wait();
                  } catch (InterruptedException e) {
                       e.printStackTrace();
                  }
              }
               // 没有包子 造包子
               System.out.println("包子铺开始做包子");
               if (count % 2 == 0) {
                   // 冰皮 五仁
                   bz.pier = "冰皮";
                   bz.xianer = "五仁";
              } else {
                   // 薄皮 牛肉大葱
                   bz.pier = "薄皮";
                   bz.xianer = "牛肉大葱";
              }
               count++;
               bz.flag = true;
               System.out.println("包子造好了:" + bz.pier + bz.xianer);
               System.out.println("吃货来吃吧");
               //唤醒等待线程 (吃货)
               bz.notify();
          }
      }
  }
}

ChiHuo

public class ChiHuo extends Thread {
   private BaoZi bz;

   public ChiHuo(String name, BaoZi bz) {
       super(name);
       this.bz = bz;
  }

   @Override
   public void run() {
       while (true) {
           synchronized (bz) {
               if (!bz.flag) {
                   try {
                       bz.wait();
                  } catch (InterruptedException e) {
                       e.printStackTrace();
                  }
              }
               System.out.println("吃货正在吃" + bz.pier + bz.xianer + "包子");
               bz.flag = false;
               bz.notify();
          }
      }
  }
}

Demo

public class Demo {
   public static void main(String[] args) {
       //等待唤醒案例
       BaoZi bz = new BaoZi();

       ChiHuo ch = new ChiHuo("吃货",bz);
       BaoZiPu bzp = new BaoZiPu("包子铺",bz);

       ch.start();
       bzp.start();
  }
}

线程池

概念

线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源。

java.util.concurrent.Executors
线程工厂类里面提供了一些静态工厂,生成一些常用的线程池

Java里面线程池的顶级接口是java.util.concurrent.Executor
,但是严格意义上讲Executor
并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads)
    :返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService
对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task)
    :获取线程池中的某一个线程对象,并执行

    Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。

使用线程池中线程对象的步骤:

  1. 创建线程池对象。

  2. 创建Runnable接口子类对象。(task)

  3. 提交Runnable接口子类对象。(take task)

  4. 关闭线程池(一般不做)。

线程池的使用

MyRunnable:

public class MyRunnable implements Runnable {
   @Override
   public void run() {
       System.out.println("开始,接下来等待两秒");
       try {
           Thread.sleep(2000);
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
       System.out.println("两秒结束:" + Thread.currentThread().getName());
       System.out.println("本线程完成工作");
  }
}

ThreadPoolDemo:

public class ThreadPoolDemo {
   public static void main(String[] args) {
       ExecutorService service = Executors.newFixedThreadPool(2);
       MyRunnable r = new MyRunnable();
       service.submit(r);
       service.submit(r);
       service.submit(r);
  }
}










文章转载自Hello 帅帅,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论