一、进程(Process)和线程(Thread)
1、进程
先说下程序的概念
- 程序
指令和数据的有序集合,本身没有运行的含义,是一个静态的概念 - 进程
程序的一次执行过程,是一个动态的概念,是系统资源分配的单位
2、线程
- 定义
一个进程可能包含若干个线程,一个进程至少有一个线程,线程是CPU调度和执行的单位
3、区别
- 资源开销
每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大
的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
- 包含关系
如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共
同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程与进程之间的地址空间和资源是相互独立的
- 影响关系
一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃有
可能导致整个进程都死掉。所以多进程要比多线程健壮。
- 执行过程
每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独
立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
二、多线程
1、什么是多线程
- 原理:上下文切换
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务
优点:
- 提高CPU利用率:
在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务
缺点:
- 占用内存资源:
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
多线程需要协调和管理,所以需要 CPU 时间跟踪线程;
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
2、实现线程的4种方式
2.1、继承 Thread 类实现多线程
------ 继承Thread 类,重写run方法
public class ThreadTest extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "方法执行 中...");
}
}
------ 测试开启线程,start()启动线程
public class ThreadTest extends Thread {
@Override
public void run() {
for(int i=0;i<20;i++){
System.out.println(Thread.currentThread().getName() + "方法执行"+i+" 中...");
}
}
public static void main(String[] args) {
//线程实例
ThreadTest test = new ThreadTest();
//start(),启动线程
test.start();
}
}
运行结果
2.2、实现 Runnable 接口,实现多线程
实现Runrable接口,重写run()方法
public class RunnableTest implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "方法执行 中...");
}
}
------测试开启线程
public class RunnableTest implements Runnable {
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println( "RunnableTest方法执行"+i+" 中...");
}
}
public static void main(String[] args) {
//创建RunnableTest实例
RunnableTest test = new RunnableTest();
//创建线程对象,通过线程对象开启线程,start(),启动线程
new Thread(test).start();
}
}
运行结果
- 说明:
Runnable 接口 run 方法无返回值,只能抛出运行时异常,且无法捕获处理
2.3、实现Callable
重写call()方法,实现有返回结果的多线程
public class CallableTest implements Callable {
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName() + "方法执行 中...");
return null;
}
}
------测试开启线程
public class CallableTest implements Callable<Object> {
@Override
public Object call() throws Exception {
for(int i=0;i<10;i++){
System.out.println( "CallableTest方法执行"+i+" 中...");
}
return "结束了";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建CallableTest实例
CallableTest test = new CallableTest();
//创建执行服务
ExecutorService executorService = Executors.newFixedThreadPool(1);
//提交执行
Future<Object> future = executorService.submit(test );
//获取结果
Object result = future.get();
System.out.println("结果---:"+result);
//关闭服务
executorService.shutdownNow();
}
运行结果
- 说明:
Callable 接口 call 方法有返回值,是个泛型,和Future、
FutureTask.get()配合可以用来获取异步执行的结果,Callable 接口 call 方法允许抛出
异常,可以获取异常信息,会阻塞主进程的继续往下执行,如果不调用不会阻塞
2.4、使用匿名内部类方式
public class CreateRunnableTest {
public static void main(String[] args) {
Thread thread=new Thread(new Runnable(){
public void run(){
for (int i=0;i<5;i++){
System.out.println("i:" + i);
}
}
});
thread.start();
}
}
3、 多线程应用场景
- 案例:
当我们在网上购物时,为了提升响应速度,需要拆分,减库存,生成订单等等这些操作,就可以进行拆分利用多线程的技术完成。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分
4. 三个必要因素
- 原子性:
原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
- 可见性:
一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
- 有序性:
程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
5.并行、并发和串行
- 并发:
多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
- 并行:
单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。
- 串行:
有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。
6.守护线程和用户线程
- 用户 (User) 线程:
运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
- 守护 (Daemon) 线程:
运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作
7.线程死锁、活锁、饥饿、无锁
- 死锁
- 死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。
- 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
- 如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
- 活锁
- 活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿到资源却又相互释放不执行资源在多个线程之间跳动而又得不到执行,这就是活锁
- 饥饿
- 优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无法得到执行,这就是饥饿
- 与死锁不同的是饥饿在以后一段时间内还是能够得到执行的,如那个占用资源的线程结束了并释放了资源
- 无锁
- 无锁,即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁典型的特点就是一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下一次循环尝试
- 多个线程修改同一个值必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。CAS 原理及应用即是无锁的实现
8、线程的生命周期状态
- NEW:表示的是刚创建的线程,还没有开始启动
- RUNNABLE:表示线程已经触发 start()方式调用,线程正式启动,线程处于运行中状态
- BLOCKED:表示线程阻塞,等待获取锁,如碰到 synchronized、lock 等关键字等占用临界区的情况,一旦获取到锁就进行 RUNNABLE 状态继续运行
- WAITING:表示线程处于无限制等待状态,等待一个特殊的事件来重新唤醒
- 如通过wait()方法进行等待的线程等待一个 notify()或者 notifyAll()方法
- 通过join()方法进行等待的线程等待目标线程运行结束而唤醒
- 一旦通过相关事件唤醒线程,线程就进入了 RUNNABLE 状态继续运行
- TIMED_WAITING:表示线程进入了一个有时限的等待,如sleep(3000),等待3秒后线程重新进行 RUNNABLE 状态继续运行
- TERMINATED:表示线程执行完毕后,进行终止状态,一旦线程通过 start 方法启动后就再也不能回到初始 NEW 状态,线程终止后也不能再回到RUNNABLE状态
9、wait() sleep() 区别
- 相同
两者都是放弃CUP一定时间的使用权 - 区别
- wait 会放弃对象的监视器(monitor),要在同步块中使用
- sleep不会放弃对象的监视器
10、CyclicBarrier 和 CountDownLatch 的区别
都在 java.util.concurrent 下,都可以用来表示代码运行到某个点上
- CyclicBarrier 的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行
- CountDownLatch 则不是,某线程运行到某个点上之后,只是给某个数值-1 而已,该线程继续运行
- CyclicBarrier 只能唤起一个任务,CountDownLatch 可以唤起多个任务
- CyclicBarrier 可 重 用 , CountDownLatch 不 可 重用,计数值为0该CountDownLatch就不可再用了
11、原子性、可见性、有序性
- 原子性
原子性是指一个线程的操作是不能被其他线程打断,同一时间只有一个线程对一个变量进行操作。在多线程情况下,每个线程的执行结果不受其他线程的干扰,使用 AtomicInteger 保证原子性 - 可见性
可见性是指某个线程修改了某一个共享变量的值,而其他线程是否可以看见该共享变量修改后的值 - 有序性
我们都知道程序是按代码顺序执行的,对于单线程来说确实是如此,但在多线程情况下就不是如此了。为了优化程序执行和提高 CPU 的处理性能,JVM和操作系统都会对指令进行重排,也就说前面的代码并不一定都会在后面的代码前面执行,即后面的代码可能会插到前面的代码之前执行,只要不影响当前线程的执行结果。所以,指令重排只会保证当前线程执行结果一致,但指令重排后势必会影响多线程的执行结果。虽然重排序优化了性能,但也是会遵守一些规则的,并不能随便乱排序,只是重排序会影响多线程执行的结果
12 、线程运行时发生异常的情况
如果异常没有被捕获该线程将会停止执行
- Thread.UncaughtExceptionHandler
- Thread.UncaughtExceptionHandler 是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口
- 当一个未捕获异常将造成线程中断的时 候 JVM 会 使 用 Thread.getUncaughtExceptionHandler() 来查询线程 的UncaughtExceptionHandler 并 将 线 程 和 异 常 作 为参数传递给handler 的uncaughtException()方法进行处理
13、线程 yield()方法
- Yield 方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行
- 它是一个静态方法而且只保证当前线程放弃 CPU 占用而不能保证使其它线程一定能占用 CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行
14、乐观锁和悲观锁
- 乐观锁
- 对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁
- 将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑
- 悲观锁
- 对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生
- 对某资源进行操作时,都会持有一个独占的锁,就像synchronized
15、notify 和 notifyAll 区别
- notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地
- notifyAll()唤醒所有线程并允许他们争夺锁,确保了至少有一个线程能继续运行
16、为什么 wait/notify/notifyAll 这些方法不在thread类里面
- 一个很明显的原因是JAVA 提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得
- 如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了
- 如果wait()方法定义在 Thread 类中,线程正在等待的是哪个锁就不明显了
- 由于wait,notify 和 notifyAll 都是锁级别的操作,所以把他们定义在Object 类中因为锁属于对象