文章目录
1. 多线程技术
1.1 线程与进程
-
进程
是指一个内存中运行的应用程序(现在很多软件都是多进程的了,比如百度云),是系统运行程序的基本单位,每个进程都有一个独立(进程之间的内存不共享)的内存空间 。
系统运行一个程序即是一个进程从创建、运行到消亡的过程。
-
线程
是进程中的一个执行路径,线程是进程中的一个执行单元,共享一个内存空间(每个线程都有自己的栈空间,堆内存是共有的),线程之间可以自由切换,并发执行。一个进程最少有一个线程,一个进程如果没有线程在执行了的话就运行结束了,线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。
注意:
很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。
1.2 守护线程和用户线程
-
守护线程
- daemonThread.setDaemon(true):设置daemonThread为守护线程。
守护线程是一种特殊的线程,用于守护用户线程,当最后一个用户线程结束时,所有守护线程自动死亡。
-
用户线程
用户线程可以认为是系统的工作线程,它会完成这个程序要完成的指定任务,我们直接创建的线程都可以看作是用户线程。如果用户线程全部结束,那么整个应用程序就应该结束。因此,当一个Java应用内只有守护线程时,Java虚拟机自然退出。
1.3 线程的六种状态
Thread.State类里面的,这是一个枚举Enum类。
-
NEW(新建状态)
至今尚未启动的线程处于这种状态。
-
RUNNABLE(运行状态)
正在JVM中执行的线程处于这种状态。
-
BLOCKED(阻塞状态)
受阻塞并等待某个监视器锁的线程处于这种状态。
-
WAITING(等待状态)
无限期等待另一个线程来执行某一特定操作的线程处于这种状态。
-
TIMED WAITING(休眠状态)
等另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。
-
TERMINATED(死亡状态)
已退出的线程处于这种状态。
1.4 线程的调度
-
分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
-
抢占式调度
-
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性), Java使用的为 抢占式调度。
-
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻, 只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高,整体需要的运行时间甚至可能会变多,因为线程切换会浪费一定的时间。
1.5 同步与异步
-
同步
排队执行 , 效率低但是安全
-
异步
同时执行 , 效率高但是数据不安全.
1.6 并发与并行
-
并发
指两个或多个事件在同一个时间段内发生。
-
并行
指两个或多个事件在同一时刻发生(同时发生)。
1.7 Thread类
1.7.1 Thread类的常用构造方法
-
Thread()
分配新的 Thread对象。
-
Thread(Runnable target)
分配一个带有指定任务的、新的 Thread对象。
-
Thread(Runnable target, String name)
分配一个具有指定名字的、带有指定任务的、新的 Thread对象。
-
Thread(String name)
分配具有指定名字的、新的 Thread对象。
-
void setDaemon(boolean on)
将此线程标记为 daemon线程或用户线程
1.7.2 Thread类的常用方法
-
String getName()
返回此线程的名称。
-
void run()
如果此线程是使用单独的Runnable运行对象构造的,则调用该Runnable对象的run方法; 否则,此方法不执行任何操作并返回。
-
void start()
导致此线程开始执行; Java虚拟机调用此线程的run方法。
-
static void sleep(long millis)
导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数。
-
static Thread currentThread()
返回对当前正在执行的线程对象的引用。
-
void join()
等待该线程终止,待此线程执行完成后,再执行其他线程,其他线程阻塞(可以看作插队)
-
setPriority(int newPriority)
更改线程的优先级
-
void interrupt()
中断此线程,就是给这个线程打个标记,告诉它你应该死亡了。
-
static boolean interrupted()
测试当前线程是否已被中断。
-
boolean isAlive()
测试线程是否处于活动状态
1.8 线程的开启方法
main方法是运行在main线程里的,即主线程,后面开启的线程都是分支。
- 创建一个类A,让他继承Thread类
- 重写run方法;
- A a = new A();
- 调用start方法:a.start()。
- 创建一个类B,实现Runnable接口
- 实现抽象方法run();
- B b = new B();//创建一个任务对象
- new Thread(b);//创建一个线程,为其分配一个任务
- 然后调用start方法。
注意:
通过实现Runnable接口的方式更好!!!
通过创建任务、给线程分配任务的形式创建线程,更适合多个线程同时执行相同任务的情况,并且可以避免单继承的局限性,任务与线程分离可以提高程序的健壮性,后续学习的线程池技术只接受Runnable类型的任务,不接受Thread类型的线程。
1.9 线程安全
1.9.1 线程安全的原因、背景
如果多个线程在同时运行,而这些线程可能会同时执行一样的代码,比如说售票问题,售票窗口在卖票,PC端也在卖票,移动端也在卖票,因此我们必须保证多线程的数据同步性。
如果多线程不访问共享数据,是不会出现线程安全问题的。
1.9.2 线程安全的解决方法
在使用多线程且访问共享数据的时候,必须保证数据的同步性,解决线程不安全的现象。为了保证每个线程都能正常执行原子操作,java引入了线程同步机制,有三种方式完成同步操作:
1.9.2.1 同步代码块(synchronized)
synchronized(同步锁对象){
//需要保证线程安全的代码段
}
- 对象的同步锁(也叫对象锁)只是一个概念,可以看作是在对象上标记了一个锁。
- 锁对象可以是任意类型
- 多个线程对象要使用同一把锁(谁有钥匙谁进入代码块,其他线程进入阻塞状态)。
1.9.2.2 同步方法
public synchronized void method(){
//需要保证线程安全的代码段
}
- 在这个方式中,对于非静态方法,同步锁就是this;对于静态方法来说,就是我们使用当前方法所在类的字节码对象(所在类.class)
- 如果是静态的方法,那访问的共享数据,也必须是静态的。
1.9.2.3 显式锁:Lock锁
同步代码块和同步方法都是属于隐式锁,Lock锁是接口,创建这样的显示锁的时候,通过其子类实现(ReentrantLock,比较常用)。
-
创建Lock锁
Lock l = new ReentrantLock();
-
加锁
l.lock();
-
解锁
l.unLock();
1.9.2.4 显示锁和隐式锁的区别
所谓的显式锁和隐式锁的区别,也就是Synchronized和Lock的区别。
-
(一)实现的层次不同
- Synchronized
- Java中的关键字,是由JVM来维护的。是JVM层面的锁。
- 底层是通过monitorenter进行加锁(底层是通过monitor对象来完成的,其中的wait/notify等方法也是依赖于monitor对象的。只有在同步块或者是同步方法中才可以调用wait/notify等方法的。因为只有在同步块或者是同步方法中,JVM才会调用monitor对象的);通过monitorexit来退出锁的。
- Lock
- 是JDK5以后才出现的具体的接口以及类。使用lock是调用对应的API。是API层面的锁。使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- Lock是通过调用对应的API方法来获取锁和释放锁的。
- Synchronized
-
(二)使用的方式不同
-
synchronized
-
有代码块锁和方法锁,程序能够自动获取锁和释放锁。其是由系统维护的,除非逻辑问题,不然不会出现死锁。
-
原始采用的是CPU悲观锁,即独占锁。当很多线程竞争锁的时候,会引起CPU频繁的上下文切换,效率低。
-
-
Lock
-
Lock只有代码块锁,需要手动的获取和释放锁,并且需要配合try/finally语句块来完成(保证资源释放,不被死锁)。如果没有释放锁,就有可能导致出现死锁的现象。死锁的四个必要条件(破坏其一):
-
互斥条件
一个资源每次只能被一个进程使用;
-
请求与保持条件
一个进程因请求资源而阻塞时,对已获得的资源保持不放;
-
不剥夺条件
进程已获得的资源,在末使用完之前,不能强行剥夺;
-
循环等待条件
若干进程之间形成一种头尾相接的循环等待资源关系。
-
-
采用得到是乐观锁的方式(Compare and Swap, CAS),每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
-
-
-
(三)等待是否可以被中断
-
synchronized
不可中断,除非抛出异常或者正常运行完成。 -
Lock
可以中断的,中断方式:-
调用设置超时方法tryLock(long timeout ,timeUnit unit)
-
调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断
-
-
-
(四)是否可以设置成公平锁
-
synchronized
只能为非公平锁。 -
lock
两者都可以的。默认是非公平锁。在其构造方法的时候可以传入Boolean值。
true:公平锁、false:非公平锁
-
-
(五)锁绑定多个条件condition
-
synchronized
不能精确唤醒线程。要么随机唤醒一个线程;要么是唤醒所有等待的线程。 -
Lock
用来实现分组唤醒需要唤醒的线程,可以精确的唤醒。
-
-
(六)性能区别
-
synchronized
-
托管给JVM执行,Java1.5中,由于需要调用操作接口,可能导致加锁消耗时间过长,与Lock比性能低。
-
1.6以后,语义定义更加清晰,有适应自旋、锁粗化、锁消除、轻量级锁、偏向锁等,可进行许多优化,性能提高了,与Lock差不多。
-
-
Lock
java写的控制锁的代码,性能高。
-
1.10 公平锁和非公平锁
-
公平锁:先到先得,得排队。
-
非公平锁:大家一起抢,谁抢到算谁的。
1.10.1 公平锁的实现方式
Lock l = new ReentrantLock();//默认是非公平锁
Lock l = new ReentrantLock(true);//true:公平锁、false:非公平锁
1.11 线程通信(等待与唤醒机制)
1.11.1 线程通信的概念
多线程并发执行的时候,默认情况下CPU随机切换线程,而可能存在多个线程处理同一个资源,但是处理的动作不同,这是就需要他们之间协调通信,完成对同一个资源的操作。
线程通信问题,即生产者与消费者问题。
例如,A是厨师,B是顾客,菜是要处理的统一资源,A做好了菜,就要唤醒B去吃,然后自己等待;B吃完就去唤醒A再去做,自己等待。
1.11.2 线程通信的方法
-
wait()
线程不再活动,进入wait set中,因此不会浪费资源,也不会去竞争锁,此时状态为WAITING。等待别的线程调用notify方法唤醒它,然后它才会重新进入调度队列。
-
wait(long millis)
进入Timed WAITING状态,在指定时间内不再活动,进入wait set中。
-
notify()
随机选取所通知对象的wait set中的一个线程释放。
-
notifyAll()
选取所通知对象的wait set中的全部线程释放。
1.12 Callable接口
主流认知上,线程的开启(创建)有两种方式,但是实际上Callable也可以,Callalble接口支持返回执行结果。
- 主线程如果需要获取Callalble线程的返回值,那就调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞;
- 主线程如果想在指定时间内等待获取,那就调用FutureTask.get((long timeout, TimeUnit unit)得到;
- 主线程如果想判断Callalble线程是否执行完毕,可以使用FutureTask.isDone();
- 主线程之前等待了,但是如果不想继续等待了,可以调用FutureTask.cannel()取消等待(干掉这个线程)。
//Callable接口
public interface Callable<V> {
V call() throws Exception;
}
//Runnable接口
public interface Runnable {
public abstract void run();
}
1.12.1 Callable使用步骤
//1. 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T;
}
}
//2. 创建FutureTask对象 , 并传入第一步编写的Callable类对象
FutureTask<Integer> future = new FutureTask<>(callable);//callable是上面的实例对象
//3. 通过Thread,启动线程
new Thread(future).start();//也是通过指定任务的方式创建线程
//
1.12.2 Runnable和Callable的相同点
- 都是接口 ;
- 都可以编写多线程程序 ;
- 都采用Thread.start()启动线程。
1.12.3 Runnable和Callable的不同点
- Runnable没有返回值;
- Callable可以返回执行结果 Callable接口的call()允许抛出异常;Runnable的run()不能抛出。
2. 线程池
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
2.1 线程池的好处
- 减少资源消耗;
- 提高响应速度;
- 提高线程的可管理性。
2.2 线程池的分类
线程池可以分为四类:缓存线程池、定长线程池、单线程线程池、周期性任务定长线程池。
线程池中分为线程数组和任务列表,任务进来分配给线程数组中的线程执行。
2.2.1 缓存线程池
长度无限制,执行时会判断线程池是否有空闲线程,如果有就使用,没有就创建一个线程,并放入线程池中,然后使用。
//缓存线程池示例
//第一步:创建缓存线程池
ExecutorService service = Executors.newCachedThreadPool();//默认是非公平锁
//第二步:向线程池中加入新的任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
2.2.2 定长线程池
长度是指定的数值,执行时会判断线程池是否有空闲线程,有三种情况:
- 如果有就使用;
- 如果没有,而且线程池没有满,就创建一个线程,并放入线程池中,然后使用;
- 如果没有,而且线程池满了,则等待线程池存在空闲线程。
//定长线程池示例
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
2.2.3 单线程线程池
效果与定长线程池 创建时传入数值1效果一致,执行时会判断线程池是否有空闲线程,如果有就使用,没有就等待池中的单个线程空闲后使用。
//单线程线程池示例
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
2.2.4 周期性任务定长线程池
即周期任务 + 定长线程池,周期性任务执行时会定时执行, 当某个时机触发时, 自动执行某任务。
执行时判断线程池是否存在空闲线程 :
-
存在则使用;
-
不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用;
-
不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
//周期性任务定长线程线程池示例 ScheduledExecutorService service = Executors.newScheduledThreadPool(2); /** * 定时执行一次 * 参数1. runnable类型的任务 * 参数2. 时长数字 * 参数3. 时长数字的单位(通过TimeUnit枚举类的常量指定) */ /* service.schedule(new Runnable() { @Override public void run() { System.out.println("俩人相视一笑~ 嘿嘿嘿"); } },5,TimeUnit.SECONDS); */ /** * 周期执行 * 参数1. runnable类型的任务 * 参数2. 时长数字(延迟执行的时长,第一次执行是在什么时间之后) * 参数3. 周期时长(每次执行的间隔时间,每隔多久执行一次) * 参数4. 时长数字的单位(通过TimeUnit枚举类的常量指定) */ service.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("俩人相视一笑~ 嘿嘿嘿"); } },5,2,TimeUnit.SECONDS);
2.3 线程池创建问题
在上述线程池使用时,我们使用了java.util.concurrent.Executors
去创建线程池,但是在阿里巴巴开发手册中这是有问题的、不规范的。在实际应用时,我们应该使用ThreadPoolExecutor
的方式创建,这样的处理方式可以更明确线程池的运行规则,规避资源耗尽的风险。而实际上,Executors
内部封装调用了ThreadPoolExecutor
去创建的。相信聪明的你可能已经发现了端倪,这里要说的问题就出在参数设置上。
Executors
返回的线程池对象的弊端主要有:
FixedThreadPool
和SingleThreadPool
允许的请求队列长度为Integer.MAX_VALUE
,可能会导致堆积大量的请求,从而OOM;CachedThreadPool
允许的创建线程数量为Integer.MAX_VALUE
,这可能会创建大量的线程,从而导致OOM。
3. Lambda表达式
3.1 函数式编程
-
面向对象编程(OOP)
做一件事情,找一个能解决这个事情的对象new,然后调用这个对象的方法,完成事情。
-
函数式编程思想
只要能获取到结果,谁去做,怎么做都不重要,重视结果,不重视过程。
3.2 Lambda表达式的使用条件
- 使用lambda表达式必须具有接口,无论是JDK内置的Runnable、Comparator接口还是自定义接口,都要求接口中有且仅有一个抽象方法;
- 使用Lambda表达式必须具有上下文推断,即方法或局部变量的类型必须为Lambda对应的接口类型。
3.3 Lambda表达式的标准格式
Lambda表达式由3部分组成:
- 一些参数
- 一个箭头
- 一段代码
(参数类型 参数名称) -> {代码语句}
3.4 Lambda表达式的省略格式
- 小括号内参数的类型可以省略;
- 如果小括号内有且仅有一个参数,则小括号可以省略;
- 如果大括号内(方法体内)有且仅有一条语句,则无论是否有返回值,可以同时省略大括号、return关键字以及语句分号。