多线程
并发&并行
并行 ==> 两个或多个时间在同一时刻发生(同时)-->需要多核CPU
并发 ==> 两个或多个时间在同一时间段内发生-->CPU轮流安排,由于时间较短,使人感觉两个任务都在进行
线程与进程
所有的程序都是在内存中运行
在内存中每个正在运行着的程序,都是进程
单线程程序: --> 进程中只有一个线程,同时只能执行一个任务
多线程程序: --> 进程中有多个线程,可执行多个任务
真正意义上的同时执行多个任务指的是同一个时间点有多个任务同时执行
进程 ==> Process
定义: 正在运行的应用程序(程序的执行过程 --> 系统运行程序的基本单位)
-->系统运行一个程序即是一个进程创建/运行/消亡的过程
特点: 独立的内存空间 --> 该内存空间内有多个线程
--> 一个应用程序可以同时运行多个进程(至少包含一条线程)
线程==> Thread
定义: 进程内部的一个独立执行单元,一个线程对应一个任务
特点:
--> 一个进程可以同时并发的运行多个线程
--> 线程是由CPU调度执行的,同一个时间点,CPU只能去调度执行一个线程(CPU在多个线程之间频繁快速切换因为速度很快,所以可以看成同时)
--> 一个进程相当于单CPU操作系统,线程便是这个系统中运行的多个任务
线程调度方式: --> 决定权在CPU手中
分时调度: --> 多个线程轮流使用CPU资源
抢占式调度: --> 多个线程去抢夺CPU的执行权,哪个线程抢到了,那么CPU即执行哪个线程,哪个线程能够抢到,完全是随机的(形象比喻)
区别:
进程: 有独立的内存空间,进程中的数据存放空间(Heap & Stack),是独立的,至少有一个线程
线程: 堆内存是共享的,栈空间是独立的,线程消耗的资源比进程小的多
栈内存是私有的,每个线程都有自己的栈空间,用来运行自己的方法
堆内存是线程共享的,锁有线程都在一个堆内存中.
注意:
java程序运行原理: --> java命令会启动jvm.等于启动了一个应用程序,即进程,该进程自动启动一个主线程,主线程就会调用某个类的main方法
1.进程中多个线程并发运行--> 有先后顺序,取决于CPU的调度,程序员无法干涉,造成多线程的随机性
2.Java程序的进程里至少包含2个线程,主进程main方法线程(主线程的执行体是由main方法体代表的) + 垃圾回收机制线程
3.单线程程序只能同时执行一个任务,如果有多个任务,只能执行完成其中一个后再执行另外一个任务
--> 可以使用多线程
4.由于创建一个线程的开销比创建一个进程的开销小很多,通常考虑多线程
创建线程类
1.java.lang.Thread类代表线程
--> 所有线程对象必须是Thread类或其子类的实例
java通过继承Thread来创建并启动多线程
1.定义Thread子类,重写该类run()方法,--(run方法体代表线程需要完成的任务,即线程执行体)--
2.创建Thread子类的实例(对象)
3.调用线程对象的start(方法),启动该线程
程序启动main时,java虚拟机启动一个进程,随着调用对象start方法,另一个进程也启动了
java.lang.Thread类
构造方法
public Thread()==> 分配新的线程对象
public Thread(String name)==> 分配一个指定名字的新的线程对象
public Thread(Runnable target)==> 分配一个带有制定目标的新的线程对象
public Thread(Runnable target . String name)==> 分配带有指定目标并指定名字的线程对象
常用方法:
public String getName()==> 获取当前线程名字
public void start()==>导致次线程开始执行,java虚拟机调用此线程run方法
public void run()==>此线程要执行的任务在此处定义代码
public static void sleep(long millis)==> 使用当前正在执行的线程以指定毫秒数暂停(暂时停止执行) 毫秒
public static Thread currentThread()==> 返回当前正在执行的线程 对象的引用
setDaemon() --> 设置线程为守护线程,该线程不会单独执行,当其他非守护线程都执行后,自动退出
join() --> 当前线程暂停,等待指定的线程执行结束后,当前线程再继续
join(int) --> 可以等待指定的毫秒之后继续
Thread.yield() --> 让出CPU
setPriority() --> 设置线程的优先级
2.采用java.lang.Runnable
1.定义Runnable接口的实现类,重写该接口run()方法,该run()方法方法体 ==> 线程执行体
2.创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,,,,该对象才是真正的线程对象
3.调用start方法启动线程
总结
实现Runnable接口比继承Thread所具有的优势
1.适合多个相同的程序代码的线程去共享同一个资源
2.可以避免java中的单继承的局限性
3.可增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立
4.线程池只能放入实现Runnable 或 Callable类线程.,不能直接放入继承Thread类
5.可以更加方便的实现线程之间的数据传递
--(Thread必须视同Runnable对象的run方法作为线程执行体)--
Thread和Runnable两种方式的区别
查看源码的区别:
继承Thread: 由于子类重写了Thread类的run(),当调用start()方法时,直接找子类的run()方法
实现Runnable: 构造函数中传入了Runnable的引用,成员变量记住了它,start调用run方法时,内部成员变量Runnable的引用是否为空,不为空时候编译看的是Runnable的run(),运行时执行的是子类的run()
继承Thread:
好处: 可以直接使用Thread类中的方法.代码简单
弊端: 如果已经有了父类,就不能用这种方法
实现Runnable接口
好处: 即使自己定义的线程类有了父类也没关系,因为有了父类也可以实现接口,即接口的多实现
弊端: 不能直接使用Thread中的方法,需要先获取线程对象后,才能得到Thread方法,代码复杂
多线程同步代码块
什么情况下需要同步:
当多线程并发,有多段代码同时执行时,我们希望某一代码块执行的过程中CPU不要切换到其他线程工作,这就需要同步
如果两段代码是同步的,那么同一时间只能执行一段,在一段代码没执行结束之前,不会执行另外一段代码
同步代码块:
synchronized(){} --> 锁对象可以使任意对象,但被锁的代码块需要保证是同一把锁,不能用匿名对象
synchronized(){} --> 修饰方法,该方法的所有代码都是同步的
例: public static synchrtonized void ...(){}
非静态的同步方法的锁对象是this,静态的同步方法的锁对象是该类的字节码对象
线程同步
==> 多个线程同时运行,可能会同时运行这段代码,程序每次运行结果和单线运行的结果是一样的,而且其他的变量的值也和预期一样,就是线程安全的
==> 线程安全问题都是由全局变量&静态变量引起的
-->若每个线程中对全局变量.静态变量只有读操作,无写操作,一般来说,这个全局变量是线程安全的;
-->若有多个线程同时执行写操作,一般需要考虑线程同步,否则会影响线程安全
线程同步
==>多线程并发访问一个资源的安全性问题 java提供了同步机制synchronized来解决
同步操作繁重方式:
1.同步代码块
2.同步方法
3.锁机制
1.同步代码块
synchronized(锁对象){ 需要同步操作的代码 }
关键字
==> synchronized关键字可以用于方法中的某个区块中表示对这个区块的资源实行互斥访问
==> 该关键字表示同步,可以修饰代码块,也可以修饰方法
==> 锁对象就是个普通的java对象,可以是任意的一个对象,仅是标记,谁持有锁,谁才进
==> 一定要保证是同一个代码块
同步锁
1.锁对象 ==> 任意类型
2.多个线程对象 ==> 使用同一把锁
tips:任何时候,最多允许一个线程拥有同步锁,谁拿到锁,谁就进入代码块,其他的等候(BLOCKED)
2.同步方法
使用synchronized修饰的方法--> 保证A线程执行该方法的时候其他线程只能在外面等着
作用: 相当于把整个方法都加锁
格式
public synchronized void method(){ 可能会产生线程安全问题的代码}
如果这个方法是非静态的方法,那么这个锁对象是this
如果这个方法是静态的方法,那么这个锁对象是当前字节码文件的.class
同步方法和同步代码块比较
同步代码块更灵活
同步方法更简洁清晰
Lock锁
java.util.concurrent.locks.lock机制提供了比synchronized代码块和方法方便的锁定操作
lock是一个接口,如果要用,需要使用其实现类ReentrantLock
public void lock()==>加同步锁 --> 手动获取锁
public void unlock()==>释放同步锁 --> 手动释放锁
单例设计模式
定义: 保证类在内存中只有一个对象
如何保证?
1.控制类的创建,不让其他类来创建本类的对象 private
2.在本类中定义一个本类的对象 Singleton s
3.提供公共的访问方式 public static Singleton getInstance(){return s}
单例写法(2种)
饿汉式
Class Singleton {
//私有构造方法 --> 其他类不能访问该构造方法
private Singleton(){}
//创建本类对象 --> 成员变量被私有,不能外部更改,不能类名.成员变量调用
private static Singleton s = new Singleton();
//对外提供公共的访问方法 --> 获取实例
public static Singleton getInstance(){
return s;
}
public static void print(){
sout("111111111111")
}
}
懒汉式
Class Singleton {
private Singletion(){};
//声明引用
private static Singleton s;
//对外提供公共访问方法
public static Singleton getInstance(){
if( s == null ){
//多线程时,不满足单例,有可能多创建对象
s = new Singleton();
return s;
}
public static void print(){
sout("1111111111")
}
}
}
饿汉式和懒汉式的区别
1.饿汉式空间换时间,懒汉式时间换空间
2.在多线程访问时,饿汉式不会创建多个对象,而懒汉式有可能会创建多个对象
第三种格式
Class Singeton{
private Singleton(){};
public static final Singleton s =new Singleton();
}
单例设计模式的应用
Runtime --> 底层为饿汉式应用
Timer类 --> 计时器 在指定时间安排指定任务
schedule(安排的任务, 执行的时间, 过多长时间再重复执行)
线程通信
概念: 多个线程在处理同一个资源,但是线程的任务却不相同
多个线程并发执行,在默认情况下CPU是随机切换线程的 ,当共同完成一件任务,并且希望他们有规律的执行,则需要他们一些协调通信
如何保证线程间通信有效利用资源
等待唤醒机制
两个线程间的通信
等待唤醒机制
wait() --> 当前线程等待
notify() --> 随机唤醒单个等待的线程
notifyAll() --> 释放所有通知对象的wait线程
注意:
通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下:
- 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
- 否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态
if()语句在哪里等待,就在哪里起来
while()语句循环判断,每次都判断标记
tips:
1.在重复代码块中用哪个对象锁,就用哪个对象调用wait方法
1.wait和notify必须在同步代码块或者同步函数中使用,因为必须通过锁对象调用这两个方法
2.为什么wait方法和notify方法定义在object这类中?
因为锁对象可以使任意对象,Object是所有类的基类,所以需要定义在object这个类中
3.sleep方法和wait方法的区别?
sleep方法必须传入参数,参数就是时间,时间到了自动醒来
wait方法可以传入参数也可以不传入参数,传入参数就是在参数时间结束后等待,不传入参数就是直接等待
sleep方法在同步函数或者代码块中,不释放锁
wait方法在同步函数或者同步代码块中,释放锁
线程状态
在API中java.lang.Thread.State这个枚举中给出了六种线程状态:
线程状态 | 导致状态发生条件 |
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
线程的五种状态new blocked runnable waiting timed-waiting dead
线程池
线程池思想概述
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
线程池概念
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
容器+直接使用+某个线程执行完后,不会死亡,而是回到线程池中成为空闲状态,还可以执行其他线程
线程池中有很多操作都是与优化资源相关的
合理利用线程池能够带来三个好处:
降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
线程池的使用
Java里面线程池的顶级接口是java.util.concurrent.Executor 顶层接口
--> 严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。
--> 真正的线程池接口是java.util.concurrent.ExecutorService。
--> 在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,
--> 里面有方法可以直接获取到一个线程池
--> 线程池对象不是new出来的,而是通过Executors调用方法获取到的
--> 官方建议使用Executors工具+类来创建线程池对象。
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
方法:
public Future<?> submit(Runnable task): 获取线程池中的某一个线程对象,并执行
shutdown --> 关闭线程池
Future接口:用来记录线程任务执行完毕后产生的结果。
使用步骤
1.调用Executors的newFixedThreadPool方法获取一个线程池
2.定义一个类,然后实现Runnable接口,重写run方法,定义线程要执行的任务
3.调用submit方法,传递runnable接口实现对象,执行任务
submit(Runnable task) : 提交任务并且执行
4.销毁线程池(一般不做)
shut down () : 销毁线程池
创建一个长度可变的线程池 ExecutorService newCachedThreadPool()
线程池扩展
static ExecutorService newCachedThreadPool(): 创建一个长度可变的线程池.
static ExecutorService newFixedThreadPool(int nThreads):创建一个定长的线程池.
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize): 创建一个支持周期性执行任务的线程池。
static ExecutorService newSingleThreadExecutor():创建一个单线程的线程池。这个线程池每次只能执行一个任务,但是可以执行每个线程他们的执行顺序。
Lambda表达式
JDK1.8中新加的重量级新特性
Runnalbe接口的匿名内部类写法 ==> 等效的Lamdba表达式
new Thread(() -> System.out.println("多线程任务执行")).start();
匿名内部类优缺点 --> 省去实现类的定义 / 比较复杂
Lambda标准格式
(参数类型 参数名称) -> { 代码语句 };
说明:
小括号内的语法与传统方法参数列表一致: 无参数则留空; 多参数则用逗号分隔
-> 是新引入的语法格式,代表指向动作
大括号内的语法与传统方法体要求基本一致
Lambda省略格式
1.小括号内的参数的类型可以省略
2.如果小括号内有且仅有一个参数,则小括号可以省略
3.如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号,return关键字及语句分号
Lambda使用前提
1.使用Lanmbda必须具有接口,且要求接口中有且仅有一个抽象方法
无论JDK内置的Runnable.Comparator接口还是自定义的接口,只有当接口中的抽象方法存在且唯一,且需要重写时,才可以使用
2.使用Lambda必须具有上下文推断
也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例
tips: 有且仅有一个抽象方法的接口,称为"函数式接口"
lambda表达式是否可以取代匿名内部类
1.匿名内部类可以使普通类.抽象类,以及接口,lambda只支持接口
2.匿名内部类可以重写多个方法,lambda只能一个方法
lambda表达式的原理和匿名内部类不同
lambda表达式不是匿名内部类的语法
Lambda表达式是使用的动态的字节码指令。 invokedynamic
匿名内部类用的是静态的字节码指定。 invokestatic invokeinterface........