概述
一、基本概念
1.多线程的作用?
提高cpu利用率 【回答】
单核:比如一个线程使用的时候CPU计算,IO空闲,IO操作时候CPU空闲,当CPU空闲时,执行另一个线程IO操作对方迟迟没有返回,这时CPU就空闲,而用户阻塞。为了防止阻塞,我们开启多线程。这样提高CPU利用率。
多核:提高CPU利用率,多核分别执行多个线程。比如一个复杂任务,让多核并行,提高效率。
2.线程、进程、协程的区别
进程:是资源分配和调度的基本单位,有独立的内存空间。 比如一个迅雷应用程序,既不共享堆,亦不共享栈。一个进程中至少有一个线程。
线程:是进程的一个实体,是cpu进行调度的基本单位。共享堆,不共享栈。在迅雷程序中开启多个下载任务下载就是多线程。如果只有单个线程,那么我们就只能下载一个任务,然后等待他下载完了再下载,而开启多个线程就可以同时下载多个任务。
线程运行需要内存空间,包括线程程序的数据空间、存储空间、运行空间等。
进程有独立的进程地址空间,线程没有独立的线程地址空间,但是多个线程共享同一个进程的地址空间。
协程:协程被称为“轻量级线程”。可以认为协程是线程里不同的函数,这些函数之间可以相互快速切换,和线程一样共享堆,不共享栈,协程由程序员调度。而线程是CPU进行调度。
协程和线程的区别是:协程避免了无意义的调度,由此可以提高性能,但也因此,**程序员必须自己承担调度的责任,**同时,协程也失去了标准线程使用多CPU的能力。
在线程里面可以开启协程,让程序在特定的时间内运行。也就是说一个线程执行不同的协程。
3.什么是线程安全
如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。【回答】
这里可以举例说明不安全的一个实例,就是多线程处理共享数据比如仅剩一张票时,多线程操作会造成重复卖票情况。也就是说在多线程环境中,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
4.一个线程终止,程序会终止吗
所有的线程都结束的时候才说明程序运行Over了;
若有线程调用system.exit()则整个程序终止。【回答】
子线程被创建后就跟母线程没什么关系了。每个线程都要去处理自己的事情,包括异常。当所有的线程都结束的时候才说明程序运行Over了;
如果线程的异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放;
若有线程调用system.exit()则整个程序终止。
5.一个线程如果出现了运行时异常会怎么样
Exception分为RuntimeException和非运行时异常。
非运行时异常必须处理,比如thread中sleep()时,必须处理InterruptedException异常,才能通过编译。
运行时异常可以抛出,然后线程继续运行,如果不抛出或者不处理,则该线程则会停止。
6.你对线程优先级的理解是什么?
线程优先级是一个int变量(从1-10),1代表最低优先级,10代表最高优先级。每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。
二、java多线程
1.java中用的是什么线程调度算法
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片这个也比较好理解。
java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,一个线程用完CPU之后,操作系统会根据线程优先级,饥饿程度等数据计算出优先级并执行下一个线程。
2.创建线程的方式
方式一:继承Thread类,并重写run方法
方式二:实现Runnable接口,并重写run方法
方式三:实现Callable接口,并重写call方法
方式四:通过线程池创建
比较
A. Java不支持多继承.因此扩展Thread类就不能继承其他类.而实现Runnable接口的类可以继承其他类。方法一和方法二
B. 好处可以有返回值,和获取线程状态。方法三
参考:
创建线程的几种方式 - 吃个橘子丶 - 博客园
3.Runnable接口和Callable接口的区别是什么?
Runnable接口中的run()方法的返回值是void;Callable接口中的call()方法是有返回值的,
Callable用于产生结果,Future用于获取结果,可以方便获取多线程运行的结果和线程运行状态等信息。
4.Thread类中的start()和run()方法有什么区别?
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,所以他会开启新线程并执行run方法。
当你调用run()方法的时候,只会是在原来的线程中调用方法,没有新的线程启动。所以还是单线程的。
5.JAVA中++操作符是线程安全的吗?
自增自减运算符不是线程安全的。因为,它们包含了读取、计算、写入三个步骤,并不是原子的。这个过程可能会出现多个线程交叉执行。
6.如何实现++原子?
用AtomicInteger、AtomicLong的incrementAndGet和getAndIncrement方法。
7.介绍java线程状态
线程的五种状态:
1)新建状态(New):线程对象实例化后就进入了新建状态。
2)就绪状态(Runnable):线程对象实例化后,其他线程调用了该对象的start()方法,虚拟机便
会启动该线程,处于就绪状态的线程随时可能被调度执行。
3)运行状态(Running):线程获得了时间片,开始执行。只能从就绪状态进入运行状态。
4)阻塞状态(Blocked):线程因为某个原因暂停执行,并让出CPU的使用权后便进入了阻塞状
态。
等待阻塞:调用运行线程的wait()方法,虚拟机会把该线程放入等待池。
同步阻塞:运行线程获取对象的同步锁时,该锁已被其他线程获得,虚拟机会把该线程放入锁
定池。
其他阻塞:调用运行线程的sleep()方法或join()方法,或线程发出I/O请求时,进入阻塞状态。
5)结束状态(Dead):线程正常执行完或异常退出时,进入了结束状态。
8.如何实现多线程通信和协作
通过共享内存和网络
9.sleep() 方法和 wait() 方法区别?
(1)sleep() 方法是 Thread 类中的方法,而 wait() 方法是 Object 类中的方法;
(2)sleep() 方法是静态方法,而wait() 方法是实例的方法;
(3)sleep() 方法,不需要锁就可以直接使用,而wait() 方法需要锁;
(4)线程调用 sleep() 之后不需要被唤醒(休眠时开始阻塞,线程的监控状态依然保持着,当指定的休眠时间到了就会自动恢复运行状态),但是 wait() 方法需要被重新唤醒(不指定时间需要被别人中断);
(5)(6)
(1)(2)
(3)
(4)
10.守护线程(Daemon Thread
)和用户线程(User Thread
)
用户线程即我们手动创建的线程,而守护线程是程序运行的时候在后台提供一种通用服务的线程。垃圾回收线程就是典型的守护线程。
如果JVM中所有的线程都是守护线程,那么JVM就会退出,进而守护线程也会退出。
如果JVM中还存在用户线程,那么JVM就会一直存活,不会退出。
由此可以得到:
守护线程是依赖于用户线程,用户线程退出了,守护线程也就会退出,典型的守护线程如垃圾回收线程。
用户线程是独立存在的,不会因为其他用户线程退出而退出。
参考:
守护线程是什么?守护线程和非守护线程的区别是?守护线程的作用是?_阿拉阿伯的博客-CSDN博客_守护线程与非守护线程的区别
11.如何在两个线程之间共享数据,线程间通信
通过在线程之间共享对象/变量。
12.ThreadLocal
ThreadLocal:是线程本地化存储技术。
作用:他为每一个线程都提供了一份变量副本,因此可以同时访问而互不影响,所以肯定线程安全。
如何用:
13. Thread.Sleep(0)的作用
Thread.Sleep(0)的作用,就是“触发操作系统立刻重新进行一次CPU竞争”。给其他线程获得CPU执行一次的机会。
竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。这也是我们在大循环里面经常会写一句Thread.Sleep(0) ,因为这样就给了其他线程比如Paint线程获得CPU控制权的权力。
14.不可变对象对多线程有何帮助
不可变对象即对象一旦被创建他的状态就不能改变。
不可变对象的类为不可变类。如String等
不可变对象永远线程安全。Erlang中定义不可变变量也是为了此种目的。
15.什么是可重入锁
线程可以进入任何一个他已经拥有的锁的同步代码块。
synchronized. ReetrantLock都是可重入的锁。
三、多线程的特性
1.可见性,原子性和有序性
可见性:如果一个多线程修改了一个共享变量,如果其他线程能够看到该共享变量值已经改变,则为可见。volatile和synchronized保证可见性。
原子性:是指一个操作是不可中断的。即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。当一个线程执行操作的时候,不要被其他线程加塞。比如三个线程操作n++,当一个线程写1时候,这时第二个线程和第三个线程都写为1,也就是写丢了两次。这就没有保证原子性。(写丢失)
解决:
- 使用synchronized
- 使用AtomicInteger,底层使用CAS。
有序性:
处理器在处理并发时,会进行指令重排优化导致结果无法预测。程序的执行可能会出现乱序。给人的直观感觉就是:写在前面的代码,会在后面执行。重排后的指令与原指令的顺序未必一致。
2.synchronized关键字
保证可见性和原子性。
synchronized关键字解决的是多个线程之间访问资源的同步性,可见性。
synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行,原子性。
3.volatile与synchronized区别
olatile 关键字的主要作用就是保证变量的可见性然后还有一个作用是和有序性,不保证原子性。
volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证,也就是保证资源的同步。
volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。
四、多线程同步和锁
1.什么是多线程的同步和异步
一个进程启动的多个不相干线程,它们相互之间关系为异步。
同步必须执行到底之后才能执行其他操作,而异步可以任意操作,是多个线程同时访问同一资源,等待资源访问结束。
2.同步的好处与弊端
好处:解决了线程的安全问题。
弊端:每次都有判断锁,降低了效率。
但是在安全与效率之间,首先考虑的是安全。
3.谈谈 synchronized和ReentrantLock 的区别
-
用法不同:synchronized 可以用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用于代码块。
-
获取锁和释放锁的机制不同:synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。
-
锁类型不同:synchronized 是非公平锁,而 ReentrantLock 默认为非公平锁,也可以手动指定为公平锁。
-
响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
-
底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的
4.生产者消费者模型的作用是什么
一个程序是面包工场,开启多个线程来生产面包,多个线程来消费面包。每生产一个就wait然后notify消费者去消费。往复循环。
通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率
解耦,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约。
5.如何判断一个线程是否持有某个对象的锁
线程有一个名为holdsLock(Object obj)的静态方法,它根据线程是否对传递的对象持有锁来返回true或false。
6.同步块还是同步方法效果更好
同步方法直接在方法上加synchronized实现加锁,范围更大,
同步代码块则在方法内部需要同步的语句加锁,同步代码块范围要小点
一般同步的范围越大,性能就越差,一般需要加锁进行同步的时候,肯定是范围越小越好,这样性能更好。
7.什么是同步容器,什么是并发容器
同步容器:可以简单地理解为通过synchronized来实现同步的容器,如果有多个线程调用同步容器的方法,竞争锁。比如Vector,Hashtable,可以通过查看Vector,Hashtable等这些同步容器的实现代码,在需要同步的方法上加上关键字synchronized。
并发容器:使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在ConcurrentHashMap中采用了一种粒度更细的加锁机制,可以称为分段锁。
允许任意数量的读线程并发地访问map,并且执行读操作的线程和写操作的线程也可以并发的访问map,同时允许一定数量的写操作线程并发地修改map,所以它可以在并发环境下实现更高的吞吐量。
五、线程池
1.什么是线程池?
线程池里面存放了若干数量的线程,这些线程给我们程序去使用,使用的时候,就去线程池里面 取一个,用完了再还回来,而不再是自我销毁。
2.为什么要用线程池?线程池优点
- 降低资源消耗
- 提高相应速度
- 提高线程的可管理型
3.创建线程池
Executors框架实现的就是线程池的功能。
Executors工厂类中提供的newCachedThreadPool、newFixedThreadPool 、newScheduledThreadPool 、newSingleThreadExecutor 等方法其实也只是ThreadPoolExecutor的构造函数参数不同而已。通过传入不同的参数,就可以构造出适用于不同应用场景下的线程池。
A.固定数量的线程池
B.缓存线程池
C.单线程线程池
D.周期性执行任务的线程池
4.线程池的核心参数
corePoolSize 核心线程数量
maximumPoolSize 最大线程数量
keepAliveTime 线程保持时间,N个时间单位
unit 时间单位(比如秒,分)
workQueue 阻塞队列
threadFactory 线程工厂
handler 线程池拒绝策略
5.线程池的工作原理或者步骤
1、线程池刚创建时候,没有线程。
2、线程池指定任务execute时:
【初始执行阶段】
如果线程池线程数量 < corePoolSize,会立 刻创建线程并执行任务
如果线程池线程数量 > corePoolSize,会把 任务放入阻塞队列中
【任务队列满】
如果阻塞队列任务已满,且正在运行的线程 数< maximumPoolSize,继续创建非核心 线程执行任务 。
如果阻塞队列任务已满,且正在运行的线程 数 = maximumPoolSize,线程池会抛出RejectExecutionException由handler处理
【线程空闲】
线程如果空闲超过keepAliveTime,而且正 在运行的线程数 > corePoolSize,该线程会 被销毁。
6.如何合理配置线程池(配置线程数)
看任务是CPU密集型还是IO密集型。
- 如果是CPU密集型,大量运算,一般就是CPU核数+1 。尽量减少切换。
- 若IO密集型。因为CPU不是一直在执行任务,则可以尽可能多配置线程数,一般为2*CPU+1
7.Java线程池中submit() 和 execute()方法有什么区别?
两个方法都可以向线程池提交任务,
execute()方法的返回类型是void,类似于run方法,它定义在Executor接口中。
而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口。
8.线程池拒绝策略有哪些?
- ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
- ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
- ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
- ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
六、锁
1.乐观锁和悲观锁
定义
对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)
使用场景
根据从上面的概念描述我们可以发现:
-
悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
-
乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
实现
Java中,synchronized关键字和Lock的实现类都是悲观锁。
乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
2.自旋锁和适应性自旋锁
最后
以上就是懵懂奇异果为你收集整理的java多线程总结二、java多线程三、多线程的特性四、多线程同步和锁五、线程池六、锁的全部内容,希望文章能够帮你解决java多线程总结二、java多线程三、多线程的特性四、多线程同步和锁五、线程池六、锁所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复