java知识汇总 2 上

多线程相关

1.什么是线程?

要解释线程,就必须明白什么是进程。

什么是进程呢?

进程是指运行中的应用程序,每个进程都有自己独立的地址空间(内存空间),比如用户点击桌面的IE浏览器,就启动了一个进程,操作系统就会为该进程分配独立的地址空间。当用户再次点击左面的IE浏览器,又启动了一个进程,操作系统将为新的进程分配新的独立的地址空间。目前操作系统都支持多进程。

要点:用户每启动一个进程,操作系统就会为该进程分配一个独立的内存空间。

线程--概念

在明白进程后,就比较容易理解线程的概念。

什么是线程呢?

是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。线程有就绪、阻塞和运行三种基本状态。

2.java什么叫线程安全?什么叫不安全?

就是线程同步的意思,就是当一个程序对一个线程安全的方法或者语句进行访问的时候,其他的不能再对他进行操作了,必须等到这次访问结束以后才能对这个线程安全的方法进行访问
什么叫线程安全:
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,
就是线程安全的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。
线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
存在竞争的线程不安全,不存在竞争的线程就是安全的。

比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B
得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。
那好,我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了

3.什么是自旋锁?

加锁,保护的是数据,而不是代码
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。

它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

简单例子:

public class SpinLock {
    private AtomicReference<Thread> cas = new AtomicReference<Thread>();
    public void lock() {
        Thread current = Thread.currentThread();
        // 利用CAS
        while (!cas.compareAndSet(null, current)) {
            // DO nothing
        }
    }
    public void unlock() {
        Thread current = Thread.currentThread();
        cas.compareAndSet(current, null);
    }
}

自旋锁存在的问题
1.如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
2.上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

自旋锁的优点
1.自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
2.非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

在使用自旋锁的时候需要注意哪些方面?

a.自旋锁不能递归 
    原因:在递归的时候,如果在递归的过程中,需要重新获得一把已经被持有的自旋锁,那么你就会一直在等待这个锁被释放,才可以继续往下走,但是因为是递归需要返回才能继续执行上一次调用自身函数的地方,假设释放锁的操作,在返回之后,那么就会造成死锁. 
b.在获取自旋锁的时候,一定要禁止本地中断的发生 
    原因:假如一个自旋锁已经被持有了,这时候来了个中断,又要去获得这把锁,中断处理程序就会在这里旋转,但是释放锁的程序在中断处理程序执行完之前,是不会执行的,so~~~双重请求产生死锁;   但是如果是在多处理器上面的话,中断和该锁的持有者不在同一个处理器上,那么即使中断处理程序要去获得该锁,也并不会产生死锁. 
c.还要注意自旋锁与下半部的共同使用 
    下半部会抢占进程上下文,所以在下半部和进程上下文的共享数据的时候,要对进程上下文的数据进行保护,同时还要禁止下半部的发生;中断处理程序会抢占下半部,所以当下半部和中断处理程序共享数据的时候,要对下半部的数据进行保护,同时要禁止本地中断; 
    但是下半部之间共享数据是没问题的.

贴个与linux中断有关的链接:https://blog.csdn.net/qq_20553613/article/details/80629255

4.什么是java内存模型?

贴地址:https://www.jianshu.com/p/d3fda02d4cae

https://blog.csdn.net/bjweimengshu/article/details/79597938

 Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。

  Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存(可以与前面将的处理器的高速缓存类比),线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成。

5.什么是CAS?

贴地址:https://blog.csdn.net/ls5718/article/details/52563959

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

这样说或许有些抽象,我们来看一个例子:
1.在内存地址V当中,存储着值为10的变量。
2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。
3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。
5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。
7.线程1进行SWAP,把地址V的值替换为B,也就是12。
从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

CAS的缺点:
1.CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

6.什么是乐观锁和悲观锁?

贴地址:http://www.hollischuang.com/archives/934

(1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

(2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

7.什么是AQS?

贴地址:https://blog.csdn.net/u010862794/article/details/72892300
https://blog.csdn.net/chenssy/article/details/60479594

AQS(AbstractQueuedSynchronizer),AQS是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。这个抽象类被设计为作为一些可用原子int值来表示状态的同步器的基类。如果你有看过类似 CountDownLatch 类的源码实现,会发现其内部有一个继承了 AbstractQueuedSynchronizer 的内部类 Sync 。可见 CountDownLatch 是基于AQS框架来实现的一个同步器.类似的同步器在JUC下还有不少。

8.什么是原子操作?

假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,

要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。

原子操作是指对于方位同一个状态的所有操作(包括该操作本身)来说,

这个操作是一个以原子方式执行的操作。

9.什么是Executors框架

Executors框架介绍
Executors框架其内部采用了线程池机制,他在java.util.cocurrent包下,通过该框架来控制线程的启动、执行、关闭,可以简化并发编程的操作。因此,通过Executors来启动线程比使用Thread的start方法更好,而且更容易管理,效率更好,还有关键的一点:有助于避免this溢出。

Executors框架包括:线程池、Executor,Executors,ExecutorService、CompletionServince,Future、Callable。

贴地址:http://www.cnblogs.com/yangdagaoge/articles/9689852.html

10.什么是阻塞队列?如何使用阻塞队列实现生产者-消费者模型

阻塞队列是一个在队列基础上又支持了两个附加操作的队列。
2个附加操作:

支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程,直到队列不满。

支持阻塞的移除方法:队列空时,获取元素的线程会等待队列变为非空。

贴地址:https://blog.csdn.net/yanpenglei/article/details/79556591

https://mp.weixin.qq.com/s/cHM2ysK7hIDnOd5apQZxpA

11.什么是callable和future?

创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。
这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。
如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。
而自从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。

Callable和Future介绍

Callable接口代表一段可以调用并返回结果的代码;Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。

Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法在线程池中执行Callable内的任务。由于Callable任务是并行的(并行就是整体看上去是并行的,其实在某个时间点只有一个线程在执行),我们必须等待它返回的结果。
java.util.concurrent.Future对象为我们解决了这个问题。在线程池提交Callable任务后返回了一个Future对象,使用它可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法让我们可以等待Callable结束并获取它的执行结果。

贴地址:http://ifeve.com/java-concurrency-tutorial-callable-future/

12.什么是futureTask?

  事实上,FutureTask是Future接口的一个唯一实现类。

贴地址:http://www.cnblogs.com/dolphin0520/p/3949310.html

13.同步容器和并发容器的实现?

贴地址:https://www.cnblogs.com/xujian2014/p/5001789.html

http://www.cnblogs.com/konck/p/9473665.html

同步容器

Java常用的容器有ArrayList、LinkedList、HashMap等等,这些容器都是非线程安全的。
如果有多个线程并发地访问这些容器时,就会出现问题。
因此,在编写程序时,必须要求程序员手动地在任何访问到这些容器的地方进行同步处理,这样导致在使用这些容器的时候非常地不方便。
所以,Java提供了同步容器供用户使用。
在Java中,同步容器主要包括2类:

1)Vector、Stack、HashTable
Vector实现了List接口,Vector实际上就是一个数组,和ArrayList类似,但是Vector中的方法都是synchronized方法,即进行了同步措施。
Stack也是一个同步容器,它的方法也用synchronized进行了同步,它实际上是继承于Vector类。
HashTable实现了Map接口,它和HashMap很相似,但是HashTable进行了同步处理,而HashMap没有。

2)Collections类中提供的静态工厂方法创建的类
Collections类是一个工具提供类。在Collections类中提供了大量的方法,比如对集合或者容器进行排序、查找等操作。更重要的是,在它里面提供了几个静态工厂方法来创建同步容器类,如下图所示:
从同步容器的具体实现源码可知,同步容器中的方法采用了synchronized进行了同步,那么必然会影响到程序的执行性能。于是Java提供了性能更优并发容器。
并发容器
java.util.concurrent提供了多种并发容器,总体上来说有4类:

队列Queue类型的BlockingQueue和ConcurrentLinkedQueue

Map类型的ConcurrentMap

Set类型的ConcurrentSkipListSet和CopyOnWriteArraySet

List类型的CopyOnWriteArrayList

14.什么是多线程?优缺点

多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术

多线程的好处:

1.使用线程可以把占据时间长的程序中的任务放到后台去处理

2.用户界面更加吸引人,这样比如用户点击了一个按钮去触发某件事件的处理,可以弹出一个进度条来显示处理的进度

3.程序的运行效率可能会提高

4.在一些等待的任务实现上如用户输入,文件读取和网络收发数据等,线程就比较有用了.

多线程的缺点:

1.如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换.

2.更多的线程需要更多的内存空间

3.线程中止需要考虑对程序运行的影响.

4.通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生

15.什么是多线程的上下文切换?

多线程的速度不一定比单线程快

即使是单核的cpu也能够执行多线程,cpu通过给每个线程分配时间片来实现这个机制。时间片是cpu分配给各个线程的时间,因为时间非常短,所以cpu通过不断地切换线程,让我们感觉多个线程是同时执行的,一般时间片为几十毫秒。
cpu通过时间片分配算法来循环执行任务,当前任务执行一个时间片后切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

从研发的角度讲:多线程环境中,当一个线程的状态由Runnable转换为非Runnable(Blocked、Waiting、Timed_Waiting)时,相应线程的上下文信息(包括cpu的寄存器和程序计数器在某一时间点的内容等)需要被保存,以便相应线程稍后再次进入Runnable状态时能够在之前的执行进度的基础上继续前进。而一个线程从非Runnable状态进入Runnable状态可能涉及恢复之前保存的上下文信息。这个对线程的上下文进行保存和恢复的过程就被称为上下文切换。

16.ThreadLocal的设计理念,作用?

贴地址:https://www.cnblogs.com/dolphin0520/p/3920407.html
https://www.jianshu.com/p/98b68c97df9b

ThreadLocal用于保存某个线程共享变量:对于同一个static ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。

1、ThreadLocal.get: 获取ThreadLocal中当前线程共享变量的值。

2、ThreadLocal.set: 设置ThreadLocal中当前线程共享变量的值。

3、ThreadLocal.remove: 移除ThreadLocal中当前线程共享变量的值。

4、ThreadLocal.initialValue: ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值。

17.ThreadPool用法与优势?

贴地址:https://www.cnblogs.com/kuoAT/p/6714762.html
https://www.imooc.com/article/23728?block_id=tuijian_wz

合理利用线程池能够带来三个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。

线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。但是要做到合理的利用线程池,必须对其原理了如指掌。

18.Concurrent包里的内容 ArrayBlockingQueue,CountDownLatch等

1:BlockingQueue,如果BlockingQueue是空的,从BlockingQueue取东西的操作将会被阻断进入等待状态,直到BlockingQueue进了东西才会被唤醒,同样,如果BlockingQueue是满的,任何试图往里存东西的操作也会被阻断进入等待状态,直到BlockingQueue里有空间时才会被唤醒继续操作。
2:BlockingQueue有四个具体的实现类,
ArrayBlockingQueue:规定大小的BlockingQueue,其构造函数必须带一个int参数来指明其大小。其所含的对象是以FIFO(先入先出)顺序排序的。
LinkedBlockingQueue:大小不定的BlockingQueue,若其构造函数带一个规定大小的参数,生成的BlockingQueue有大小限制,若不带大小参数,所生成的BlockingQueue的大小由Integer.MAX_VALUE来决定。其所含的对象是以FIFO顺序排序的。
PriorityBlockingQueue:类似于LinkedBlockingQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数所带的Comparator决定的顺序。
SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的。
3:semaphore:Java 并发库 的Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,acquire()获取一个许可,如果没有就等待,而release()释放一个许可。比如在Windows下可以设置共享文件的最大客户端访问个数。
4:countdownLatch:CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0为止。(当每个线程调用countdown方法直到将countdownlatch方法创建时数减为0时,那么之前调用await()方法的线程才会继续执行。有一点注意,那就是只执行一次,不能到0以后重新执行)
5:CyclicBarrier:类有一个整数初始值,此值表示将在同一点同步的线程数量。当其中一个线程到达确定点,它会调用await() 方法来等待其他线程。当线程调用这个方法,CyclicBarrier阻塞线程进入休眠直到其他线程到达。当最后一个线程调用CyclicBarrier 类的await() 方法,它唤醒所有等待的线程并继续执行它们的任务。(当等待的线程数量达到CyclicBarrier线程指定的数量以后(调用await方法的线程数),才一起往下执行,否则大家都在等待,注意:如果达到指定的线程数量的时候:则可以重新计数,上面的过程可以循环)
6:CountDownLatch是减计数方式,计数==0时释放所有等待的线程;CyclicBarrier是加计数方式,计数达到构造方法中参数指定的值时释放所有等待的线程。

19.synchronized和ReentranLock的区别?

1、ReentrantLock 拥有Synchronized相同的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等候

 线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,
 如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断
 如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情

2、synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中

3、在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

贴个使用:https://www.cnblogs.com/baizhanshi/p/6419268.html

20.Semaphore有什么用

 Semaphore也是一个线程同步的辅助类,可以维护当前访问自身的线程个数,并提供了同步机制。使用Semaphore可以控制同时访问资源的线程个数,例如,实现一个文件允许的并发访问数。

https://www.cnblogs.com/liuling/p/2013-8-20-03.html

此处评论已关闭