Java复习---多线程
Posted 赵jc
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java复习---多线程相关的知识,希望对你有一定的参考价值。
多线程
- 并行和并发有什么区别?
- 线程和进程的区别?
- 多线程的好处
- 守护线程是什么?
- 创建线程有哪几种方式?
- 线程有哪些状态?(7种)
- sleep() 和 wait() 有什么区别?
- 在 java 程序中怎么保证多线程的运行安全?
- 说一下volatile关键字?
- 说一下 synchronized 底层实现原理?
- synchronized锁升级的过程
- synchronized 和 Lock 有什么区别?
- synchronized 和 ReentrantLock 区别是什么?
- ThreadLocal 是什么?有哪些使用场景?
- 死锁
- 线程池都有哪些状态?(5种)
- 线程池中 submit()和 execute()方法有什么区别?
- 线程池的关闭
- 多线程的应用有哪些?
并行和并发有什么区别?
可以借鉴物理的知识。
- 并发是指两个或者多个事件在同一时间发生,但在这个时间段中也是又先后顺序的,是假的同时),多个进程在同一个CPU下采用时间片轮转的方式,在一段时间内,让多个进程都得以推进。
- 而并行是指两个或多个事件在同一时刻隔发生。(多个事件真正的在同一时刻发生),多个进程在多个CPU下同时运行
线程和进程的区别?
进程(系统资源分配得最小单位)
线程(系统调度的最小单位,进程的一个实体)
-
一个程序至少有一个进程,一个进程至少有一个线程。
-
进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,从而减少切换次数,使效率更高。
-
同一进程中的多个线程之间可以并发执行。
-
线程的创建、切换即终止效率相对于进程来说相对较高。
多线程的好处
- 程序运行的更快!
- 充分利用cpu资源
- 让阻塞的代码不影响后续代码的执行(后续的代码在其他线程执行)
守护线程是什么?
守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程。
创建线程有哪几种方式?
①. 继承Thread类创建线程类
- 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
- 创建Thread子类的实例,即创建了线程对象。
// 方法一 继承Thread 单继承
static class MyThread extends Thread {
@Override
public void run() {
System.out.println("子线程" + Thread.currentThread().getName());
}
}
②. 通过Runnable接口创建线程类
- 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象的参数,该Thread对象才是真正的线程对象。
③. 通过Callable和Future创建线程
-
创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,
并且有返回值
。 -
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
-
使用FutureTask对象作为Thread对象的target创建并启动新线程。
-
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int n = 1;
return n;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
int ret = futureTask.get();
System.out.println(ret);
}
}
④. 通过ThreadPoolExecutor创建线程
这种是最经典的线程池创建方式,也时最常用的方式,这种方式可以
优点
- 这种方式可以解决线程数量不可控的问题
- 这种方式可以解决任务数量不可控的问题
public class ThreadPoolExecuteTest {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5, 10, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new ThreadPoolExecutor.DiscardPolicy()
);
for(int i = 0; i < 10; i++) {
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
}
}
7个参数:
- corePoolSIze:核⼼线程数,
- maximumPoolSize:最⼤线程数。
- keepAliveTime:空闲线程的保活时间,
- TimeUnit:保活时间单位。
- BlockingQueue:任务丢列,⽤于存储线程池的待执⾏任务的。
- ThreadFactory:⽤于⽣成线程,⼀般我们可以⽤默认的就可以了。或者自定义我们需要的任务。
- handler:拒绝策略,当线程池已经满了,但是⼜有新的任务提交的时候,该采取什么策略由这个来指定。有⼏种⽅式可供选择,像抛出异常、直接拒绝然后返回等,也可以⾃⼰实现相应的接⼝实现⾃⼰的逻辑。
5种拒绝策略
拒绝策略:达到最大线程数且阻塞队列已满,采取的拒绝策略
- AbortPolicy:直接抛RejectedExecutionException(不提供handler时的默认策略)
- CallerRunsPolicy:谁(某个线程)交给我(线程池)任务,我拒绝执行,由谁自己执行
- DiscardPolicy:交给我的任务,直接丢弃掉(尾删)
- DiscardOldestPolicy:丢弃阻塞队列中最旧的任务(头删)
- 我们自定的拒绝策略,可以写进日志里或者存储到数据库当中
线程有哪些状态?(7种)
线程通常都有五种状态,创建(new)、、运行Runnable(running、和ready)、等待(waiting、timed_waitting、blocked)和终止terminated。
sleep() 和 wait() 有什么区别?
wait和sleep的区别
相同点
- wait和sleep都是让线程进入休眠状态
- wait和sleep在执行的过程中都可以接收到线程终止的通知
不同点
- wait必须配合synchronized一起使用,而sleep不用
- wait会释放锁,而sleep不会释放锁(
sleep必须要传入一个最大等待时间,也就是说sleep是可控的(对于时间层面来说),而wait是不可以传递参数的,如果wait不主动释放锁的话就,没被唤醒前就会一直阻塞)
- wait是Object的方法,而sleep是Thread(线程)的方法(
wait需要操作锁,而锁是属于对象级别的(存放在对象头当中)它不是线程级别的,一个线程可以有多把锁,为了灵活起见,所以讲wait放在了Object当中)
- 默认情况下wait(不传递任何参数或者参数为0的情况下)它会进入waiting状态,而sleep会进入timed_waiting状态
- 使用wait时可以主动的唤醒线程,而使用sleep时不能主动地唤醒线程
解决wait/notify随机唤醒的问题(指定唤醒某个线程)
- LockSupport park()/unpark(线程)
- LockSupporrt()虽然不会报Interrupt的异常,但依然可以监听到线程终止的指令
在 java 程序中怎么保证多线程的运行安全?
CPU是抢占式执行的(万恶之源)所以导致了线程 不安全
线程安全在三个方面体现:
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
- 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile)(为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题)
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序.(编译器优化/指令重排序)(volatile关键字)
说一下volatile关键字?
- 可以解决内存不可见(从主内存中取值,然后在工作内存中修改后存入主内存中,然后刷新情况自己的工作内存)和指令重排序的问题,
- 但不可以解决原子性的问题
说一下 synchronized 底层实现原理?
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区
针对Java语言来说,是将锁信息存放在对象头(标识,锁的状态,所得拥有者)
对象头中保存着偏向锁的线程id(下面介绍什么是偏向锁)
针对JVM层面,它是依靠monitor来实现
monitor是线程私有的数据结构,每一个线程都有一个可用monitor列表,其中的owner字段表名拥有该锁的线程名称
针对操作系统层面,它是依靠互斥锁mutex
- 当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中
- 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
synchronized锁升级的过程
当一个线程A刚创建出来便是无锁状态,当A竞争到锁之后会升级为偏向锁(偏向锁偏向锁,意思便是有偏向的意思啦,偏向第一个获取到的线程加锁,但这个线程比较坏,不会自己释放锁,只有当别的线程来竞争的时候才会释放锁),线程A升级为偏向锁之后,当线程B来尝试获取锁的时候,如果没有获取到会自旋等待一直尝试获取锁(而不会因没有获取到锁而阻塞),但当现线程B自旋一定次数,或者另一个线程C也来获取锁的时候,线程A又会升级为重量级锁,(当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程(B和C)进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。)
- 上面说的云里雾里的下面我们来举个例子吧,有一个公共的娱乐设施(锁),线程A第一个来玩了(偏向锁),但线程A一直玩,这时线程B也想来玩,但是线程A脸皮比较厚接着玩,线程B就在旁边边吐槽边等(自旋),当线程B吐槽了好一会后线程A受不了了,但是还想玩,所以想着在玩一会(轻量级锁),但线程B还是在旁边吐槽(自旋),过了一会线程A实在受不了了,说你先等着,等我玩完了再叫你。(重量级锁)
synchronized 和 Lock 有什么区别?
- synchronized自行进行加锁和释放锁,而lock需要手动进行加锁和解锁
- lock是Java层面锁的实现的,而synchronized是JVM层面实现的
- synchronized可以修饰代码块、静态方法、实例方法,而lock只能修饰代码块
- synchronized只能实现非公平锁,但lock可以实现非公平锁和公平锁
- lock的灵活性更高(tryLock)
synchronized 和 ReentrantLock 区别是什么?
- synchronized是关键字,而ReentrantLock是一个类
- synchronized只能实现非公平锁,但ReentrantLock 可以实现非公平锁和公平
- ReentrantLock可以获取各种锁的信息
另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word。
ThreadLocal 是什么?有哪些使用场景?
ThreadLocal是线程的本地变量,每一个线程会创建一个私有变量。
ThreadLocal三板斧,set(T)、get()、remove()
- set(T):将内容存储到ThreadLocal。没有 set 操作的 ThreadLocal 容易引起脏数据。
- get:从线程中取私有变量。没有 get 操作的 ThreadLocal 对象没有意义。
- remove:从线程中移除私有变量。没有 remove 操作容易引起内存泄漏。
ThreadLocal 的使用场景
- 解决线程安全的问题
- 实现线程级别的数据传递(实现一定程度上的解耦)
ThreadLocal 带来的问题
- 不可继承性(InheritableThreadLocal来解决不可继承性的问题,但前提两个线程必须是父子线程的关系(或者从属进程的关系),不能实现并列线程之间的数据传输(数据设置和获取)为什么会出现这种情况?因为⽆论是ThreadLocal 还是 InheritableThreadLocal 本质都是线程本地变量,所以不能跨线程进⾏数据共享也是正常的。)
- 产生脏读数据(配合线程池使用时,没有remove)
- 内存泄漏(配合线程池使用,没有remove,而且ThreadLocal中存的value是强声明周期且占用1mb)
死锁
什么是死锁
- 在两个或者两个以上的线程运行中,因为资源抢占而造成线程一直等待的问题。
造成死锁的四个条件:
- 互斥条件:当资源被一个线程拥有之后,就不能被其他的线程拥有了(不可更改)
- 请求拥有条件:当一个线程拥有了一个资源之后又试图请求另一个资源(可以解决)
- 不可剥夺条件:当一个资源被一个线程拥有之后,如果不是这个线程主动释放此资源的情况下,其他线程不能拥有此资源(不可更改)
- 环路等待条件:两个或两个以上的线程在拥有了资源之后,试图获取对方资源的时候形成了一个环路(可以解决)
如何解决死锁
- 控制加锁的顺序(解决环路的等待条件)
线程池都有哪些状态?(5种)
线程池有5种状态:Running、ShutDown、Stop、Tidying、Terminated。
线程池中 submit()和 execute()方法有什么区别?
- 1.执行的任务无返回值 execute(new Runnable)execute执行任务如果有OOM异常会将异常打印到控制台,方便Exception处理
- 2.执行的的任务有返回值 submit(可以有返回值也可以没有返回值)(new Runnable 无返回值/ new Callable 有返回值)execute执行任务如果有OOM异常不会将异常打印到控制台,
线程池的关闭
- shutdown:拒绝新任务加入,等待线程池中的任务队列执行完之后在停止线程池
- shutdownNow:拒绝执行新任务,并且会立即停止,不会等待任务队列中的任务执行完,才停止线程池
多线程的应用有哪些?
阻塞队列
线程池
单例模式(重要)
- 饿汉模式
- 懒汉模式
- 静态内部类
- 枚举实现
以上是关于Java复习---多线程的主要内容,如果未能解决你的问题,请参考以下文章