多线程简介(全)
线程实现方式
并发与并行
- 并发:指两或多个事件在同一个时间段内发生
- 并行:指两或多个事件在同一个时刻发生(同时发生)
进程的概念
内存:所有的应用程序都需要进入到内存中执行 临时存储RAM
硬盘:永久存储ROM
- 进入到内存的程序叫进程
- 任务管理器-->结束进程
- 那么就把进程从内存中清除了
线程的概念
点击一个应用程序的功能执行,就会开启一条应用程序到cpu的执行路径,cup就可以通过这个路径执行功能,这个路径有一个名字,叫线程。
线程属于进程:是进程中的一个执行单元,负责程序的执行
线程的好处:
- 效率高
- 多线程之间互不影响
如:
单核心线程cpu
- cpu在多个线程之间做高速的切换
- 轮流执行多个线程
- 效率低
- 切换的速度块(1/n毫秒)
4核心8线程
- 有8个线程,可以同时执行8个线程
- 8个线程在多个任务之间做高速的切换
- 速度是单线程cpu的8倍(每个执行到的几率都被提高了8倍)
线程的调度
分时调度
- 所有线程轮流使用cpu的使用权
抢占式调度
- 优先级高的线程使用cpu先使用,若相同,随机选择一个。Java使用的就是抢占式调度。
主线程
主线程:执行主(main)方法的线程
单线程程序:Java程序中只有一个线程
执行从main方法开始,从上到下依次执行
- JVM执行main方法,main方法会进入到栈内存
- JVM会找操作系统开辟一条main方法通向cpu的执行路径
- cpu就可以通过这个路径来执行main方法
- 而这个路径有一个名字,叫main(主)线程
创建多线程
创建多线程程序的第一种方式:创建Thread的子类
java.lang.Thread类:是描述线程的类,我们想要实现多线程程序,就必须继承Thread类。
实现步骤:
- 创建一个Thread类的子类
- 在Thread类的子类中重写Thread类的run方法,设置线程任务(开启线程要做什么?)
- 创建Thread类的子类对象
- 调用Thread类中的方法start方法,开启新的线程,执行run方法。
- void start() 使该线程开始执行;Java虚拟机调用该线程的run方法。
- 结果是两个线程并发地运行;当前线程(main线程)和另一个线程(创建地新线程,执行其run方法)。
- 多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
Java使用的就是抢占式调度。优先级高的线程使用cpu先使用,若相同,随机选择一个。
//1.创建一个Thread类的子类 public class MyThread extends Thread{ //2.在Thread类的子类中重写Thread类的run方法,设置线程任务(开启线程要做什么?) @Override public void run(){ for (int i = 0;i<20;i++){ System.out.println("run:"+1); } } } public class CaiNiao{ public static void main(String[] args){ //3 创建Thread类的子类对象 MyThread mt = new MyThread(); //4.调用Thread类中的方法start方法,开启新的线程,执行run方法。 mt.start(); //主线程会继续执行主方法中的代码 for (int i = 0;i<37;i++){ System.out.println("main:"+i); } } }
创建多线程程序的第二种方法:实现Runnable接口
java.lang.Runnable
- Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run的无参数方法。
java.lang.Thread类的构造方法
- Thread(Runnable target)分配新的 Thread对象。
- Thread(Runnable target,String name)分配新的 Thread对象。
实现步骤:
- 创建一个Runnable接口的实现类
- 在实现类中重写Runnable接口的run方法,设置线程任务
- 创建一个Runnable 接口的实现类对象
- 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
- 调用Thread类中的方法start方法,开启新的线程,执行run方法。
实现Runnable接口创建多线程程序的好处:
1 避免了单继承的局限性
- 一个类只能继承一个类(一个人只能有一个亲爹),类继承了Thread类就不能继承其他的类
- 实现Runnable接口,还可以继承其他的类,实现其他的接口
2 增强了程序的扩展性,降低了程序的耦合性(解耦)
- 实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)
- 实现类中,重写了run方法:用来设置线程任务
- 创建Thread类对象,调用Thread类中的方法start方法,开启新的线程,执行run方法。
匿名内部类方式实现线程的创建
- 匿名:没有名字
- 内部类:写在其他类内部的类
匿名内部类的作用:简化代码
- 把子类继承父类,重写父类的方法,创建子类对象合一步完成
- 把实现类实现类接口,重写接口中的方法,创建实现类对象合一步完成
格式:
new 父类/接口(){ 重置父类/接口中的方法 }; //线程的父类是Thread //new MyThread().start(); //线程的接口Runnable //Runnable r = RunnableTmpl();//多态 //new Runnable(r).start();
Thread类的常用方法
获取线程的名称:
- 使用Thread类中的方法getName() 返回该线程的名称。
- static Thread currentThread() 返回对当前正在执行的线程对象的引用。
设置线程的名称:
- 使用Thread类中的方法setName(名字)
- void setName(String name) 改变线程的名称,使之参数 name相同。
- public void start():使该线程开始执行;Java虚拟机调用该线程的run方法
- public void run():此线程要执行的任务在此处定义代码
- public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
线程的安全问题
模拟卖票案例
创建三个的线程,同时开启,对共享的票进行出售
public class RunnableImpl implementsc Runnable{ //定义一个多线程共享的票源 private int ticket = 100; //设置线程任务:买票 @Override public void run(){ //使用死循环,让卖票操作重复执行 while (true){ //先判断票是否存在 if(ticket>0){ //提高安全问题出现的概率,让程序睡眠 try{ Thread.sleep(10); }catch (InterruptedException e){ e.printStackTrace(); } //票存在,卖票 ticket-- System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票"); ticket --; } } } } public class CaiNiao{ public static void main(String[] args){ //创建Runnable接口的实现类对象 RunnableImpl run = new RunnableImpl(); //创建Thread类对象,构造方法中传递Runnable接口的实现类对象 Thread t0 = new Thread(run); Thread t1 = new Thread(run); Thread t2 = new Thread(run); //调用start方法开启多线程 t0.start(); t1.start(); t2.start(); } }
这样会导致一个结果
- Thread-0 -->正在卖第1张票
- Thread-1 -->正在卖第1张票
- Thread-2 -->正在卖第0张票
解决线程安全问题的一种方案:使用同步代码块
格式:
格式:
syncharonized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
注意:
- 通过代码块中的锁对象,可以使用任意的对象
- 但是必须要保证多个线程使用的锁对象是同一个
锁对象作用
- 把同步代码块锁住,只让一个线程在同步代码中执行
public class RunnableImpl implementsc Runnable{ //定义一个多线程共享的票源 private int ticket = 100; //设置线程任务:买票 @Override public void run(){ //使用死循环,让卖票操作重复执行 while (true){ //同步代码块 syncharonized(obj){ //先判断票是否存在 if(ticket>0){ //提高安全问题出现的概率,让程序睡眠 try{ Thread.sleep(10); }catch (InterruptedException e){ e.printStackTrace(); } //票存在,卖票 ticket-- System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票"); ticket --; }
解决线程安全问题的二种方案:使用同步方法
使用步骤:
- 把访问了共享数据的代码抽取出来,放到一个方法中
- 在方法上添加synchronized修饰符
格式:定义方法的格式
修饰符 synchronized 返回值类型 方法名(参数列表){ 可能会出现线程安全问题的代码(访问了共享数据的代码) }
解决线程安全问题的三种方案:使用Lock锁
java.util.concurrent.Locks.Lock接口
Lock实现提供了比使用synchronized 方法和语句可获得的更广泛的锁定操作。
Lock接口中的方法:
void Lock()获取锁 void unLock() 释放锁 java.util.concurrent.Locks.ReentrantLock implements Lock 接口
使用步骤:
- 在成员位置创建一个ReentrantLock对象
- 在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁
- 在可能会出现安全问题的代码前调后用Lock接口中的方法unLock释放锁
//1.在成员位置创建一个ReentrantLock对象 Lock l = new ReentrantLock(); @Override public void run(){ //使用死循环,让卖票操作重复执行 while (true){ //2. 在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁 l.lock(); //同步代码块 syncharonized(obj){ //先判断票是否存在 if(ticket>0){ //提高安全问题出现的概率,让程序睡眠 try{ Thread.sleep(10); }catch (InterruptedException e){ e.printStackTrace(); } //票存在,卖票 ticket-- System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票"); ticket --; } //3.在可能会出现安全问题的代码前调后用Lock接口中的方法unLock释放锁 l.unLock();//无论程序释放异常,锁都会释放
等待唤醒机制
线程的状态
- NEW 至今尚未启动的线程处于这种状态
- RUNNABLE 正在Java虚拟机中执行的线程处于这种状态
- BLOCKED 受阻塞并等待某个监视器锁的线程处于这种状态
- WAITING 无限期的等待另一个线程来执行某一待定操作的线程处于这种状态
- TIMED_WAITNG 等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态
- TERMINATED 已退出的线程处于这种状态。
- 阻塞状态:具有cpu的执行资格,等待cpu空闲时执行
- 休眠状态:放弃cpu的执行资格,cpu空闲,也不执行
等待唤醒案例分析
public static void sleep(Long millis):使用当前正在执行的线程以指定的毫秒数暂停(暂停停止执行).
- 毫秒数结束之后,线程继续执行
等待唤醒案例:线程之间的通信
- 创建一个顾客线程(消费者):告知老板要包子的中类和数量,调用wait方法,放弃cpu的执行,进入到WAITNG状态(无限等待)
- 创建一个老板线程(生产者):花了5秒做包子,做好包子之后,调用notify方法,唤醒顾客吃包子
注意:
- 顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行
- 同步使用的锁对象必须保证唯一
- 只有锁对象才能调用wait和notify方法
Object类中的方法
void wait()
- 在其他线程调用此对象的notify()方法或notify() 方法前,导致当前线程等待。
void notify()
- 唤醒在此对象监视器上等待的单个线程。
- 会继续执行wait方法之后的代码
public class CaiNiao{ public static void main(String[] args){ //创建锁对象,保证唯一 Object obj = new Object(); //创建一个顾客线程(消费者) new Thread(){ @Override public void run(){ //一直等着买包子 while(true){ //保证等待和唤醒的线程只能有一个执行,需要使用同步技术 syncharonized (obj){ System.out.println("告知老板要的包子的种类和数量"); //调用wait方法,放弃cpu的执行,进入到WAITNG状态(无限等待) try{ obj.wait(); }catch (InterruptedException e){ e.printStackTrace(); } //唤醒之后执行的代码 System.out.println("包子已经做好了,开吃!"); System.out.println("--------------"); } } } }.start(); //创建一个老板线程(生产者) new Thread(){ @Override public void run(){ //一直做包子 while(true){ //花5秒做包子 try{ Thread.sleep(5000);//花5秒做包子 }catch(InterruptedException e); e.printStackTrace(); } //保证等待和唤醒只能有一个在执行,需要使用同步技术 syncharonized (obj){ System.out.println("花了5秒做包子,做好包子之后,调用notify方法,唤醒顾客吃包子"); //做好包子之后,调用notify方法,唤醒顾客吃包子 obj.notify(); } } }.start(); }
Object类中wait带参方法和notify
进入到TimeWaiting(计时等待)有两种方法
- 使用sleep(Long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blacked状态
- 使用wait(Long m))方法,wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blacked状态
唤醒的方法:
- void notify()唤醒在此对象监视器上等待的单个线程。
- void notifyAll()唤醒在此对象监视器上等待的所有线程。
调用wait和notify方法需要注意的细节
- wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程
- wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
- wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这两个方法。
线程间通信
概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同
重点:有效的利用资源
分析:需要那些类
1 资源类:包子类
- 设置包子的属性
- 包子的状态:有true 没有false
2 生产者(包子铺)类:是一个线程类,可以继承Thread
- 设置线程任务(run):生产包子
- 对包子的状态进行判断
true:有包子
- 包子铺调用wait方法进入等待状态
false:没有包子
- 包子铺生产包子
- 增加一些多样性:交替生产两种包子
- 有两种状态(i%2 == 0 )
- 包子铺生产好了包子
- 修改包子的状态为true有
- 唤醒吃货线程,让吃货线程吃包子
3 消费者(吃货)类:是一个线程类,可以继承Thread
- 设置线程任务(run):吃包子
- 对包子的状态进行判断
false:没有包子
- 吃货线程调用wait方法进入等待状态
true:有包子
- 吃货吃包子
- 吃货吃完包子
- 修改包子的状态为false没有
- 吃货唤醒包子铺线程,生产包子
4 测试类:
- 包含main方法,程序执行的入口,启动程序
- 创建包子对象:
- 创建包子铺线程,开启,生产包子
- 创建吃货线程,开启,吃包子;
注意:
- 包子铺线程和包子线程关系-->通信(互斥)、
- 必须同时同步技术保证两个线程只能有一个在执行
- 锁对象必须保证唯一,可以使用包子对象作为锁对象
- 包子铺类和吃货的类就需要把包子对象作为参数传递进来
- 需要在成员位置创建一个包子变量
- 使用带参数的构造方法,为这个包子变量赋值
线程池
线程池概念:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多的资源。
线程池:容器-->集合(ArrayList,HashSet,LinkedList<Thread>,HashMap)
- 当程序第一次启动的时候,创建多个线程,保存到一个集合中
- 当我们想要使用线程的时候,就可以从集合中取出来线程使用
Thread t = list.remove(0);返回的是被移除的元素,(线程只能被一个任务使用)
Thread t = linked.removeFist();
- 当我们使用完毕线程,需要把线程归还给线程池
list.add(t);
linked.addLast(t);
在JDK1.5 之后,JDK内置了线程池,我们可以直接使用
合理利用线程池带来的好处:
- 降低资源消耗
- 提高响应速度
- 提高线程的可管理性。
线程池的代码实现:JDK1.5之后提供的
java.util.concurrent.Executors;线程池的工厂类,用来生成线程池
Executors类中的静态方法:
static ExecutorService newFixedThreadPool(int nThreads)创建一个可重用的固定线程数的线程池
参数:
int nTherad:创建线程池中包含的线程数量
返回值:
ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)
java.util.concurrent.ExecutorService:线程池接口
- 用来从线程池中获取线程,调用start方法开启多线程,执行线程任务
submit(Runnable task)提交一个Runnable任务用于执行
关闭/销毁线程池的方法
void shutdown()
线程池的使用步骤:
- 使用线程池的工厂类Executors里边提供的静态方法,newFixedThreadPool生产一个指定线程数量的线程池
- 创建一个类,实现Runnable接口,重写了run方法:用来设置线程任务
- 调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法。
- 调用ExecutorService中的方法shotdown销毁线程池(不建议执行)
ExecutorService es = Executors.newFixedThreadPool(2) es.submit(new RunnableImpl());//创建了一个新的线程执行
Lambda表达式
函数式编程思想概述
---强调做什么,而不是以什么形式做
面向对象的思想:
- 做一件事情,找一个能解决这个的事情的对象,调用对象的方法,完成事情
函数式编程思想
- 只要能获取到结果,谁去做的,这么做的都不重要,重视的是结果,不重视过程
传统写法和Lambda写法对比
传统写法
public class CaiNiao{ public static void main(String[] args){ //匿名内部类,实现多线程 //Runnable task = new Runnable() Runnable task = new Runnable(){ @Override public void run(){//覆盖重写抽象方法 System.out.println(Thread.currentThread().getName()+"新线程创建了"); System.out.println("多线程任务执行!"); } }; new Thread(task).start();//开启线程 } }
匿名内部类的好处与弊端
- 一方面,匿名内部类可以帮助我们省去实现类的定义;
- 另一方面,匿名内部类的语法确实太复杂了
public class CaiNiao{ public static void main(String[] args){ //匿名内部类,实现多线程 //Runnable task = new Runnable() new Thread(new Runnable(){ @Override public void run(){//覆盖重写抽象方法 System.out.println(Thread.currentThread().getName()+"新线程创建了"); System.out.println("多线程任务执行!"); } }).start();//开启线程 } }
Lambda写法
()->{}
public class CaiNiao{ public static void main(String[] args){ //匿名内部类,实现多线程 //Runnable task = new Runnable() new Thread(()->{//覆盖重写抽象方法 System.out.println(Thread.currentThread().getName()+"新线程创建了"); System.out.println("多线程任务执行!"); } ).start();//开启线程 } }
Lambda表达式的标准格式
由三部分组成:
- 一些参数
- 一个箭头
- 一段代码
格式:
- (参数列表)-> {一些重写方法的代码}:
解释说明格式:
- ():接口中抽象方法的参数列表,没有参数,就空着
- ->:传递的意思,把参数传递给方法体{}
- {}:重写接口的抽象方法的方法体
Lambda表达式:是可推导,可以省略
- 凡是根据上下文推导出来的内容,都可以省略书写
可以省略的内容:
- (参数列表):括号中的参数列表的数据类型,可以省略不写
- (参数列表):括号中的参数如果只有一个,那么类型和()都可以省略
- (一些代码):如果()中的代码只有一行,无论是否由返回值,都可以省略({},return,分号)
注意:要省略{},return,分号必须一起省略
- JDK1.7 版本之前,创建集合对象必须把前后的泛型都写上
- JDK1.7 版本之后,=号后边的泛型可以省略,后边的泛型可以根据前边的泛型推导出来
new Thread(()->{//覆盖重写抽象方法 System.out.println(Thread.currentThread().getName()+"新线程创建了"); } ).start();//开启线程 new Thread(()->System.out.println(Thread.currentThread().getName()+"新线程创建了")).start();//开启线程
Lambda的使用前提
- 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法;
- 使用Lambda必须具有上下文推断。
备注:有且仅有一个抽象方法的接口,成为“函数式接口”。