Java——多线程

并发与并行

并发:指两个或多个事件在同一个时间段内发生 交替进行

并行:指两个或多个时间在同一时刻发生 同时发生

进程与线程

进程:一个内存中运行的应用程序 进入内存执行的程序

线程:属于进程,是进程中的一个执行单位,负责当前进程中程序的执行

线程的调度

分时调度

抢占式调度(Java)

创建多线程程序

Thread类

java.lang.Thread

实现步骤

  1. 创建一个Thread类的子类

  2. 在Thread类的子类中重写Thread类中的run方法,设置线程任务

  3. 创建Thread类的子类对象

  4. 调用Thread类中的方法start,开启新线程,自动执行run方法

void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法

结果是两个线程同时运行:当前线程(主线程 → 从调用返回到start方法)和另一个线程(新线程 → 执行其run方法)。

不止一次启动线程是不合法的。特别地,一旦线程完成执行就可能不会重新启动。

常用方法

获取线程的名称
  1. 使用Thread类中的方法getName()

    String getName() 返回此线程的名称

  2. 可以先获取到当前正在执行的线程,使用线程中的方法currentThread()

    static Thread currentThread() 返回对当前正在执行的线程对象的引用

设置线程的名称
  1. 使用Thread类中的setName(名字)

    void setName(String name) 将此线程的名称更改为等于参数 name

  2. 创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参构造方法,把线程名称传递给父类,让父类(Thread)给子线程取一个名字

    Thread(String name) 分配一个新的 Thread对象

sleep()

static void sleep(long millis) 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性

注意是静态方法

Runnable接口

java.lang.runnable

Runnable接口应由任何类实现,其实例将由线程执行。 该类必须定义一个无参数的方法,称为run 。

java.lang.Thread类的构造方法:

Thread(Runnable target) 分配一个新的 Thread对象

Thread(Runnable target, String name) 分配一个新的 Thread对象

实现步骤

  1. 创建一个Runnable接口的实现类

  2. 在实现类中重写Runnable接口的run方法,设置线程任务

  3. 创建一个Runnable接口实现类的对象

  4. 创建Thread类对象,构造方法中传递Runnable接口的实现类对象

  5. 调用Thread类中的start方法,开启新的线程执行run方法

Thread类和Runnable接口的区别

实现Runnable接口创建多线程程序的好处:

  1. 避免了单继承的局限性

    一个类只能继承一个类 实现了Runnable接口还可以实现其他的接口

  2. 增强程序的拓展性,降低了程序的耦合性(解耦)

  实现Runnable接口的方式,把设置线程任务开启线程任务进行了分类(解耦)

  实现类中,重写了run方法:用来设置线程任务

  常见Thread类对象,调用start方法:用来开启线程

多线程原理

匿名内部类

作用:简化代码

  把子类继承父类,重写父类的方法,创建子类对象合成一步完成

  把实现类实现接口,重写接口的方法,创建实现类对象合成一步完成

匿名内部类的最终产物:子类/实现类对象,而这个类没有名字

格式

new 父类/接口(){

重写父类/接口的方法

};

1  new Thread(){
2      public void run(){
3  for(int i = 0 ; i < 20; i++){
4      System.out.println(Thread.currentThread().getName());
5  }  
6      }
7  }.start();
 1  Runnable r = new Runnable(){
 2       public void run(){
 3  for(int i = 0 ; i < 20; i++){
 4      System.out.println(Thread.currentThread().getName());
 5  }  
 6      }
 7  }
 8  9  new Thread(r).start();
10 11  //简化版
12  new Thread(new Runnable(){
13       public void run(){
14  for(int i = 0 ; i < 20; i++){
15      System.out.println(Thread.currentThread().getName());
16  }  
17      }
18  }).start();

线程同步

问提分析

 1  public class RunnableImpl implements Runnable{
 2  3      private int ticket = 100;
 4      
 5      public void run() {
 6          while(ticket > 0) {
 7  //          try {
 8  //              Thread.sleep(10);
 9  //          } catch (InterruptedException e) {
10  //              e.printStackTrace();
11  //          }
12              System.out.println(Thread.currentThread().getName()+"在卖第"+ticket+"票");
13              ticket--;
14          }
15      }
16  }
17 18  public class Mian {
19      
20      public static void main(String[] args) {
21          
22          RunnableImpl run = new RunnableImpl();
23          
24          Thread t0 = new Thread(run);
25          Thread t1 = new Thread(run);
26          Thread t2 = new Thread(run);
27          
28          t0.start();
29          t1.start();
30          t2.start();
31      }
32 33  }

运行结果:

 1  Thread-2在卖第100票
 2  Thread-1在卖第100票
 3  Thread-0在卖第98票
 4  Thread-1在卖第97票
 5  Thread-2在卖第97票
 6  Thread-0在卖第95票
 7  Thread-2在卖第94票
 8  Thread-1在卖第94票
 9      ......
10  Thread-0在卖第2票
11  Thread-2在卖第1票
12  Thread-1在卖第1票
13  Thread-0在卖第-1票

发现不同的线程会卖相同的票号以及不存在的票号

解决方法:

使用线程同步

同步代码块

格式

synchronized(锁对象){

可能会出现线程安全问题的代码(访问了共享数据的代码)

}

注意

  1. 通过代码块中的锁对象,可以使用任意的对象

  2. 但是必须保证多个线程使用的锁对象是同一个

  3. 锁对象作用:

  把同步代码块锁住,只让一个线程在同步代码块中执行

原理

使用了一个锁对象,这个锁也叫同步锁/锁对象/对象监视器

同步中的线程没有执行完毕不会释放锁对象,同步外的的线程没有获取到锁对象进不去同步

优缺点

  优点:同步保证了只能有一个线程在同步中执行共享数据,保证了安全

  缺点:程序频繁判断锁、释放锁、获取锁,程序的效率会降低

同步方法

格式

定义方法的格式

修饰符 synchronized 返回值类型 方法名 (参数列表) {

可能会出现线程安全问题的代码(访问了共享数据的代码)

}

原理

定义一个同步方法,同步方法也会把方法内的代码锁住,只让一个线程执行

锁对象是实现类对象(实现Runnable接口的对象)

锁机制

java.util.locks.Lock接口

Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。

Lock接口中的方法

  void lock() 获得锁

  void unlock() 释放锁

java.util.concurrent.locks.ReentrantLock类 implements Lock接口

使用步骤

  1. 在成员位置处创建一个ReentrantLock对象

  2. 在可能出现安全的代码前调用Lock接口中的方法lock获取锁

  3. 在可能出现安全的代码后调用Lock接口中的方法unlock释放锁

线程的状态

状态名称状态描述
NEW 尚未启动的线程处于此状态
RUNNABLE 在Java虚拟机中执行的线程处于此状态
BLOCKED 被阻塞等待监视器锁定的线程处于此状态
WAITING 正在等待另一个线程执行特定动作的线程处于此状态
TIMED_WAITING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
TERMINATED 已退出的线程处于此状态

TIMED_WAITING状态

进入此状态的两种方式:

  1. 使用sleep(long m)方法,线程睡醒后进入到Runnable/Blocked状态

  2. 使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程进入到Runnable/Blocked状态

唤醒的方法:

  1. void notify() 唤醒正在等待对象监视器的单个线程(如果有多个等待的线程就随机唤醒一个等待的线程

  2. void notifyAll() 唤醒正在等待对象监视器的所有线程

线程间的通信

为什么要处理线程间的通信?

多个线程并发执行时,在默认的情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一项任务时,并且我们希望他们有规律地执行,那么多线程之间需要一些协调通信,以此来达到多线程处理一份数据地要求。

如何保证线程之间通信、有效利用资源?

等待唤醒机制

等待唤醒机制

即线程之间的通信

机制中的方法

  1. wait()

  2. notify()

  3. notifyAll()

调用wait()和notify()方法的注意事项
  1. wait方法和notify方法必须要由同一个锁对象调用

    对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程

  1. wait方法和notify方法是属于Object类的方法

    锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的

  1. wait方法和notify方法必须要在同步代码或是同步方法中使用

    必须通过锁对象调用这两个方法

线程池

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

Executors类

java.util.concurrent.Executors 线程池的工厂类,用来生成线程池

静态方法

static ExecutorService newFixedThreadPool(int nThreads) 创建一个线程池,该线程池重用固定数量的从共享无界队列中运行的线程

参数

int nThreads:创建线程池中包含的线程数量

返回值

ExecutorService接口,返回的是ExecutorService 接口中的实现类对象,我们可以使用ExecutorService 接口来接收(面向接口编程)

ExecutorService接口

java.util.concurrent.ExecutorService 线程池接口

用来从线程池获取线程,调用start方法,执行线程任务

方法

  Future<?> submit(Runnable task) 提交一个可运行的任务执行,并返回一个表示该任务的未来

  void shutdown() 启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务

使用步骤

  1. 使用线程池的工厂类Executors里提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池

  2. 创建一个类,实现Runnable接口,重写run方法,设置线程任务

  3. 调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法

  4. 调用ExecutorService中的方法shutdown销毁线程(不建议使用)

Lambda表达式

函数式编程思想

面向对象的思想:

  做一件事情要找一个能解决这个事情的对象调用对象的方法,完成事情

函数式编程思想:

  只要能获取到结果,谁去做的或者是怎么做的都不重要,重视的是结果,不重视过程

Lambda更优写法

 1          //使用匿名内部类的方式 实现多线程
 2          new Thread(new Runnable() { 
 3              public void run() {
 4                  System.out.println(Thread.currentThread().getName()+"新线程建立了");
 5              }
 6          }).start();
 7          
 8          //使用Lambda表达式 实现多线程
 9          new Thread(()-> {
10                  System.out.println(Thread.currentThread().getName()+"新线程建立了");
11              }).start();

 1 () -> {System.out.println(Thread.currentThread().getName()+"新线程建立了");} 

  前面的一对小括号()即run方法的参数(这里没有),代表不需要任何条件

  中间的一个箭头 ->代表将前面的参数传递给后面的代码

  后面的输出语句即业务逻辑代码

Lambda标准格式

  1. 一些参数

  2. 一个箭头

  3. 一段代码

(参数列表) -> {重写方法的代码}

解释说明格式

() : 接口中抽象方法的参数列表,没有参数就空着;有参数就写出参数,多个参数之间用逗号分隔

-> : 传递的意思,把参数传给方法体

{} : 重写接口的抽象方法

Lambda表达式省略格式

凡是根据上下文推导出来的内容,都可以省略书写

可以省略的内容:

  1. (参数列表) : 括号中参数列表的数据类型可以省略不写

  2. (参数列表) : 括号中的参数如果只有一个,那么类型和括号都可以省略

  3. {一些代码} : 如果{}中的代码只有一行,无论是否有返回值,都可以省略{} return ;

    注意:要省略必须一起省略

posted @ 2021-02-23 15:28  栎眠尔  阅读(60)  评论(0编辑  收藏  举报