Java多线程
什么是线程?
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,可以使用多线程对进行运算提速。
比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒
什么是线程安全和线程不安全?
通俗的说:加锁的就是是线程安全的,不加锁的就是是线程不安全的
线程安全
线程安全: 就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问,直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可以将集合类分成两组,线程安全和非线程安全的。Vector 是用同步方法来实现线程安全的, 而和它相似的ArrayList不是线程安全的。
线程不安全
线程不安全:就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
什么是自旋锁?
基本概念
自旋锁是SMP架构中的一种low-level的同步机制。
当线程A想要获取一把自旋锁而该锁又被其它线程锁持有时,线程A会在一个循环中自旋以检测锁是不是已经可用了。
自旋锁需要注意:
由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在那里自旋,这就会浪费CPU时间。
持有自旋锁的线程在sleep之前应该释放自旋锁以便其它线程可以获得自旋锁。
实现自旋锁
一个简单的while就可以满足你的要求。
目前的JVM实现自旋会消耗CPU,如果长时间不调用doNotify方法,doWait方法会一直自旋,CPU会消耗太大。
什么是CAS?
CAS(compare and swap)的缩写,中文翻译成比较并交换。
CAS 不通过JVM,直接利用java本地方 JNI(Java Native Interface为JAVA本地调用),直接调用CPU 的cmpxchg(是汇编指令)指令。
利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,实现原子操作。其它原子操作都是利用类似的特性完成的。
整个java.util.concurrent都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。
CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS应用
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS优点
确保对内存的读-改-写操作都是原子操作执行
CAS缺点
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作
总结
使用CAS在线程冲突严重时,会大幅降低程序性能;CAS只适合于线程冲突较少的情况使用。
synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
什么是乐观锁和悲观锁?
悲观锁
Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有共享变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲观锁,所以可以说synchronized是悲观锁。
乐观锁
乐观锁( Optimistic Locking)其实是一种思想。相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。
什么是AQS?
AbstractQueuedSynchronizer简称AQS,是一个用于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解决了在实现同步容器时设计的大量细节问题。
AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus。
什么是Callable和Future?
Callable 和 Future 是比较有趣的一对组合。当我们需要获取线程的执行结果时,就需要用到它们。Callable用于产生结果,Future用于获取结果。
Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法去在线程池中执行Callable内的任务。由于Callable任务是并行的,必须等待它返回的结果。java.util.concurrent.Future对象解决了这个问题。
在线程池提交Callable任务后返回了一个Future对象,使用它可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法,等待Callable结束并获取它的执行结果。
什么是FutureTask?
FutureTask可用于异步获取执行结果或取消执行任务的场景。
通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,
之后可以在外部通过FutureTask的get方法异步获取执行结果,
因此,FutureTask非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
另外,FutureTask还可以确保即使调用了多次run方法,它都只会执行一次Runnable或者Callable任务,或者通过cancel取消FutureTask的执行等。
1.执行多任务计算
FutureTask执行多任务计算的使用场景
利用FutureTask和ExecutorService,可以用多线程的方式提交计算任务,主线程继续执行其他任务,当主线程需要子线程的计算结果时,在异步获取子线程的执行结果。
2.高并发环境下
FutureTask在高并发环境下确保任务只执行一次
在很多高并发的环境下,往往我们只需要某些任务只执行一次。这种使用情景FutureTask的特性恰能胜任。
什么是同步容器和并发容器的实现?
一、同步容器
主要代表有Vector和Hashtable,以及Collections.synchronizedXxx等。锁的粒度为当前对象整体。迭代器是及时失败的,即在迭代的过程中发现被修改,就会抛出ConcurrentModificationException。
二、并发容器
主要代表有ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentSkipListMap、ConcurrentSkipListSet。锁的粒度是分散的、细粒度的,即读和写是使用不同的锁。迭代器具有弱一致性,即可以容忍并发修改,不会抛出ConcurrentModificationException。
JDK 7 ConcurrentHashMap
采用分离锁技术,同步容器中,是一个容器一个锁,但在ConcurrentHashMap中,会将hash表的数组部分分成若干段,每段维护一个锁,以达 到高效的并发访问;
JDK 8 ConcurrentHashMap
采用分离锁技术,同步容器中,是一个容器一个锁,但在ConcurrentHashMap中,会将hash表的数组部分分成若干段,每段维护一个锁,以达 到高效的并发访问;
三、阻塞队列
主要代表有LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue(Comparable,Comparator)、SynchronousQueue。提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。适用于生产者、消费者模式(线程池和工作队列-Executor),同时也是同步容器
四、双端队列
主要代表有ArrayDeque和LinkedBlockingDeque。意义:正如阻塞队列适用于生产者消费者模式,双端队列同样适用与另一种模式,即工作密取。在生产者-消费者设计中,所有消费者共享一个工作队列,而在工作密取中,每个消费者都有各自的双端队列。如果一个消费者完成了自己双端队列中的全部工作,那么他就可以从其他消费者的双端队列末尾秘密的获取工作。具有更好的可伸缩性,这是因为工作者线程不会在单个共享的任务队列上发生竞争。在大多数时候,他们都只是访问自己的双端队列,从而极大的减少了竞争。当工作者线程需要访问另一个队列时,它会从队列的尾部而不是头部获取工作,因此进一步降低了队列上的竞争。适用于:网页爬虫等任务中
五、比较及适用场景
如果不需要阻塞队列,优先选择ConcurrentLinkedQueue;如果需要阻塞队列,队列大小固定优先选择ArrayBlockingQueue,队列大小不固定优先选择LinkedBlockingQueue;如果需要对队列进行排序,选择PriorityBlockingQueue;如果需要一个快速交换的队列,选择SynchronousQueue;如果需要对队列中的元素进行延时操作,则选择DelayQueue。
什么是多线程?优缺点?
什么是多线程?
多线程:是指从软件或者硬件上实现多个线程的并发技术。
多线程的好处:
使用多线程可以把程序中占据时间长的任务放到后台去处理,如图片、视屏的下载
发挥多核处理器的优势,并发执行让系统运行的更快、更流畅,用户体验更好
多线程的缺点:
大量的线程降低代码的可读性;
更多的线程需要更多的内存空间
当多个线程对同一个资源出现争夺时候要注意线程安全的问题。
什么是多线程的上下文切换?
即使是单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)
上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态
从任务保存到再加载的过程就是一次上下文切换
ThreadLocal变量 是什么?有哪些使用场景?
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。
但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,
在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
并行和并发有什么区别?
并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群。
所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。
线程和进程的区别?
简而言之,进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。
守护线程是什么
守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程。
创建线程有哪几种方式
①. 继承Thread类创建线程类
定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
创建Thread子类的实例,即创建了线程对象。
调用线程对象的start()方法来启动该线程。
②. 通过Runnable接口创建线程类
定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
调用线程对象的start()方法来启动该线程。
③. 通过Callable和Future创建线程
创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
使用FutureTask对象作为Thread对象的target创建并启动新线程。
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
说一下 runnable 和 callable 有什么区别
Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
线程有哪些状态?
线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。
创建状态:在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
就绪状态:当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态:在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
运行状态:线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
阻塞状态:线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
死亡状态:如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪
Java中用到的线程调度算法是什么?
采用时间片轮转的方式.可以设置线程的优先级.会映射打哦下层的系统上面的优先级.
如非特别需要.尽量不要用,防止线程饥饿.
sleep() 和 wait() 有什么区别?
sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
wait():wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程
notify()和 notifyAll()有什么区别?
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
为什么wait,notify 和notifyAll 这些方法不再thread类中?
一个很明显的原因 是因为Java提供的锁都是对象级的不是线程级的,每个对象都有锁.
通过线程获得,由于wait.notify和notifyAll都是所级别的操作,所以把他们定义在Object类中,因为锁属于对象.
线程的 run()和 start()有什么区别?
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程。
start()方法来启动一个线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码; 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
创建线程池有哪几种方式?
①. newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
②. newCachedThreadPool()
创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。
③. newSingleThreadExecutor()
这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。
④. newScheduledThreadPool(int corePoolSize)
创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
线程池都有哪些状态?
线程池有5种状态:Running、ShutDown、Stop、Tidying、Terminated。
线程池中 submit()和 execute()方法有什么区别?
接收的参数不一样
submit有返回值,而execute没有
submit方便Exception处理
在 java 程序中怎么保证多线程的运行安全?
线程安全在三个方面体现:
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
多线程锁的升级原理是什么?
在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称,最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计领域最难处理的问题之一。
怎么防止死锁?
死锁的四个必要条件:
互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和 解除死锁。
所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确 定资源的合理分配算法,避免进程永久占据系统资源。
此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。
什么是活锁?
活锁:任务或者执行者没有被阻塞.由于某些条件没有满足.导致一直重复尝试.失败.
活锁和死锁的区别在于:处于活锁的实体是在不断的改变状态.是所谓的活.而处于死锁的实体表现为等待.
所以活锁有可能自行解开.死锁不能.
什么是饥饿?
饥饿:一个或者多个线程因为种种原因无法获得所需要的资源.导致一直无法执行的状态.
Java中导致饥饿的原因?
- 高优先级线程吞噬所有的低优先级线程的cpu时间
- 线程被永久堵塞在一个等待进入同步块的状态.因为其他线程总是能在它之前持续的对该同步块进行访问.
- 线程在等待一个本身也处于永久等待完成的对象.(比如调用对象的wait方法),因为其他线程总是被持续的获得唤醒.
什么是线程组,为什么在java中不推荐使用?
ThreadGroup类.可以把线程归属到某个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程.
这样的组织接口有点类似于树的形式.
为什么不推荐使用? 因为使用有很多的安全隐患.所以推荐使用线程池.
在java中Executor 和 Exrcutors的区别?
Executor工具类的不同方法按照我们的需求创建了不同的线程池.来满足业务的需求.
Executor接口对象能执行我们线程任务
ExecutorService接口集成了Execcutor接口并进行扩展,提供了很多的方法我们能活的任务执行的状态并且可以获取任务的返回值.
使用ThreadPoolExecutor可以创建自定义线程池.
Future表示异步计算的结果.他提供了监察计算能否完成的方法.以等待计算的完成.并可以私用get()方法获得计算的结果.
synchronized 5连击
说一说对于synchronized 关键字的了解
synchronized关键字解决的是多个线程之间访问资源的同步性. synchronized关键字可以保证被它修饰的方法或者代码块在任意时间内只能有一个线程执行.
另外, 在java早起版本中, synchronized属于重量级锁. 效率低下. 因为监视器锁( monitor ) 是依赖于底层操作系统的 Mutex lock来实现的. java的线程是映射到操作系统的原生线程之上的. 如果要挂起或者环形一个线程. 都需要操作系统帮忙完成. 而操作系统实现线程之间的切换时.需要从用户态 转换到 内核态.这个状态之间的转化需要相对比较长的时间. 时间成本高. 这也是早期 synchronized效率低的原因.
在java6 之后java 官方从JVM 层面对 synchronized 有了较大的优化.所以现在的synchronized 锁 效率也提高了很多. jdk1.6对锁的实现引入了大量的优化. 如自旋锁. 适应性自旋锁. 锁消除. 锁粗化, 偏向锁. 轻量级锁等技术减少锁操作的开销.
说说在项目中 自己如何使用 synchronized
synchronized 最主要的三种使用方式:
修饰实例方法 , 作用于当前对象实例加锁. 进入同步代码前要获得当前对象实例的锁.
修饰静态方法. 作用于当前类对象加锁. 进入同步代码前要获得当前类对象的锁. 也就是给当前类加锁. 会作用与类的所有对象实例.因为静态成员不属于任何一个实例对象.是类成员( static 表明这是该类的一个静态资源. 不管new了多少个对象. 只有一份. 所以对该类的所有对象都加了锁 ), 所以如果一个线程A调用一个实例对象的非静态synchronized方法. 而线程B 需要调用这个实例对象所属类的静态synchronized方法. 是允许的 不回发生排斥. 因为访问静态synchronized 方法占用的当前类的锁. 而访问非静态synchronized方法是占用当前实例对象的锁.
修饰代码块.指定加锁对象. 对给定对象加锁.进入同步代码库前要获得给定对象的锁. 和synchronized方法一样.synchronized(class) 代码块上都是 给 class类上锁. 注意: synchronized关键字加到非static静态方法上是给对象实例上锁. 另外. 不要使用 synchronized( String a ) .因为JVM中, 字符串常量池具有缓冲功能.
下面手写一个 单例模式的 双重校验锁实现单例(线程安全)
1 public class Singleton{ 2 3 private volatile static Singleton singleton; 4 5 private Singleton(){ 6 7 } 8 9 public static Singleton getInstance(){ 10 11 if(singleton == null){ 12 13 // 类对象加锁 14 15 synchronized(Singleton.class){ 16 17 if(singleton == null){ 18 19 singleton = new Singleton(); 20 21 } 22 23 } 24 25 } 26 27 return singleton; 28 29 } 30 31 }
方法中实例的对象使用 volatile 关键字也很重要. 也很有必要.
singleton = new Singleton(); 这段代码是分为三步执行:
1.为 singleton 分配内存空间
2.初始化 singleton
3.将 singleton 指向分配好的内存地址
但是由于JVM具有指令重排的特性. 执行顺序可能会发生变化例如: 1->3->2. 指令重排在单线程环境下不会出现问题. 但是在多线程下会导致一个线程获得没有初始化实例的对象. 例如: 线程A 执行了1 , 3 此时.线程B调用getInstance() 方法后判断 singleton不为null , 因此返回 singleton. 但此时的singleton还未初始化.
volatile 关键字可以禁止JVM指令重排. 保证在多线程环境下也可以正常运行.
synchronized 底层实现
synchronized关键字底层原理属于JVM层面.
synchronized 修饰语句块的情况.
synchronized 修饰语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向语句块的开始位置, monitorexit指令指向的语句块结束的位置. 当执行 monitorenter 指令时. 线程试图获取锁也就是获取monitor( monitor对象存在于每个java对象的对象头里.synchronized就是通过这种方式获取锁的.也是为什么java中任意对象可以作为锁的原因 ) 的持有权. 当计数器为0可以成功获取. 获取后将锁计数器设置为1, 相应的在执行 monitorexit 指令后,将计数器设为0,表示锁被释放, 如果获取锁失败 那当前进程就要阻塞等待.知道锁被释放为止.
synchronized 修饰 方法的情况
synchronized修饰的方法并没有 monitorenter 和 monitorexit 指令. 但是有ACC_SYNCHRONIZED 标识 , 该标识指明了该方法是一个同步方法, JVM通过ACC_SYNCHRONIZED 访问标志来辨别一个方法是不是同步方法.从而执行相应同步调用.
jdk1.6后synchronized有哪些优化 详细说一下
jdk1.6对锁的实现引入了大量的优化. 如偏向锁. 轻量级锁. 自旋锁.适应性自旋锁.锁 消除.锁粗化等技术来减少锁的开销.
锁主要存在四种状态: 无锁状态. 偏向锁状态. 轻量级锁状态. 重量级锁状态. 他们会随着竞争的激烈而逐渐升级. 注意锁可以升级但不能降级. 这种策略视为了提高获得锁和释放锁的效率.
synchronized 和 ReentrantLock 区别
两者都是可重入锁.
两者都是可重入锁. 可重入锁的概念是: 自己可以再次获取自己内部的锁.比如一个线程获得了某个对象的锁. 此时这个对象锁还没有释放, 当其再次想要获取这个对象的锁的时候还是可以获取的. 如果不可锁重入的话, 就会造成死锁.同一个线程每次获得锁, 锁的计数器都自增1, 所以要等到锁的计数器下降为0才能释放锁.
synchronized 依赖于JVM 而 ReemtrantLock 依赖于 API
synchronized 是依赖于Jvm 实现的.JVM团队在JDK1.6为 synchronized关键字进行了很多优化.但是这些优化都是在虚拟机层面实现的.并没有直接暴露给我们. ReentrantLock 是 JDK 层面实现的.( 也就是 API 层面. 属于lock() 和 unlock() 方法配合 try/finally 语句块完成的. ) 所以我们可以直接查看他的源码.看他如何实现,
ReentrantLock 比 synchronized 增加了一些高级功能
相比synchronized , ReentrantLock 增加了一些高级功能, 主要有三点:
等待可中断.
可实现公平锁.
可实现选择性通知( 锁可以绑定多个条件 )
ReentrantLock 提供了一种能够中断等待锁的线程的机制. 通过 lock.lockinterruptibly() 来实现. 也就是说正在等待的线程可以选择放弃等待.改为处理其他事情
ReentrantLock 可以指定是公平锁还是非公平锁. 而 synchronized 是能是非公平锁. 所谓的公平锁就是先等待的线程获得锁. ReentranLock 默认清空是非公平的. 可以通过ReentrantLock类的ReentrantLock( boolean fair ) 构造方法来制定是否公平.
synchronized 关键字 与 wait() 和 notify/notifyAll() 方法相结合可以实现等待/通知机制. ReentrantLock类当然也可以实现. 但是需要借助Condition接口与 newCondition() 方法. Condition是JDK1.5之后才有的.Condition具有很好的灵活性.比如可以实现多路通知功能也就是在一个Lock对象中创建多个Condition实例.( 即对象监视器 ) . 线程对象可以注册在制定的Condition中. 从而可以有选择性的进行线程通知. 在调度线程上更加灵活. 在使用notify/notifyAll() 方法进行通知时. 被通知的线程由JVM选择. 用ReentrantLock类结合Condition可以实现"选择性通知", 这个功能很重要. 而且是Condition接口默认实现的. 而synchronized关键字就相当于整个Lock对象中只有一个Condition实例.所有的线程都注册在它身上. 如果执行notifyAll() 方法会通知所有正在等待的线程, 这样会造成很大的效率问题. 而Condition实例的 signalAll() 方法. 只会唤醒注册在该Condition实例中的所有线程.
如果你想使用 选择性通知. 选择ReentrantLock
性能已经不是选择的标准.
面试中关于 volatile 和 synchronized
java内存模型
在JDK1.2 之前. java 的内存模型 实现总是从 主存 ( 共享内存 ) 读取数据. 是不需要进行特别注意的. 而在当前的java 内存模型下. 线程可以把变量保存本地内存( 机器的寄存器 ) 中 , 而不是直接在主存中进行读写. 这就可能造成一个线程在主存中修改了一个变量的值. 而另一个线程还继续使用它在寄存器中的变量值的拷贝. 造成数据的不一致.
解决这个问题, 需要给变量声明为 volatile 这就告诉JVM. 这个变量是不稳定的. 每次使用它都到主存中进行读取.
说白了. volatile 关键字的主要作用就是保证变量的可见性然后还有就是防止指令重排
synchronized 和 volatile 的区别
volatile 关键字是线程同步的轻量级实现. 所以 volatile的性能肯定要比synchronized 要好. 但是volatile只能用于变量. 而synchronized可以修饰方法以及代码块. synchronized关键字 在 jdk1.6后进行了主要包括减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁, 以及它的各种优化后执行效率有了明显提高.实际开发中synchronized的场景更多一些.
多线程访问volatile不会发生阻塞. synchronized可能会发生阻塞
volatile 主要用于解决变量在多个线程之间的可见性. synchronized 解决的是多个线程之间访问资源的同步性
面试中关于线程池的 4 连击
为什么要用线程池
线程池提供了一种闲置和管理资源. ( 包括执行一个任务 ) . 每个线程池还维护一些基本统计信息. 例如已完成任务的数量.
使用线程池的好处:
降低资源消耗. 通过重复利用已创建的线程降低线程创建和销毁造成的消耗
提高响应速度. 当任务到达时. 任务可以不需要等到线程创建就能立即执行
提高线程的可管理性. 线程是稀缺资源. 如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性.使用线程池可以进行统一的分配. 调优 和 监控.
实现 Runnable 接口 和 Callable接口的区别
如果想让线程池执行任务需要实现 Runnable接口 或 Callable 接口.
Runnable接口或 Callable接口实现类都可以被 ThredPoolExecutor 或 ScheduledThreadPoolExecutor 执行.
两者的区别在于 Runnable 接口不回有返回结果 但是 Callable接口可以返回结果.
注: 工具类 Executors 可以实现 将 Runnable对象 转为 Callable 对象
方法: Executors.callable( Runnable task ) 返回值是null 或 Executors.callable( Runnable task, Object result) 返回值是 result
执行 execute() 方法 和 submit() 方法的区别
execute() 方法用于提交不需要返回值的任务 ( Runnable ) . 所以无法判断任务是否被线程池执行成功.
submit() 方法用于提交需要返回值的任务 ( Runnable , Callable ). 线程池会返回一个 future 类型的对象. 通过这个 future 对象可以判断任务是否被执行成功. 并且通过 future的get() 方法获取返回值. get() 方法会阻塞当前线程直到任务完成. 而使用 get( long timeOut , TimeUnit unit ) 方法则会阻塞当前线程一段时间后返回. 这个时候有可能任务还没有执行完.
如何创建线程池
《阿里巴巴java开发手册》 中强制线程池不允许使用 Executors 去创建. 而是通过ThreadPoolExecutor 的方式, 这样的处理方式让写的同学更加明确线程池的运行规则 规避资源耗尽的风险.
Executors 返回线程池对象的弊端:
FixedThreadPool 和 singleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE. 可能堆积大量的请求 从而导致 oom
CachedThreadPool 和 ScheduledThreadPool : 允许线程的创建数量为 Integer.MAX_VALUE, 可能会创建大量线程 从而导致 oom
构造方式一 : 通过构造方法实现
new ThreadPoolExecutor() new ThreadPoolExecutor(int,int,long,TimeUnit,BlockingQueue<Runnable>) new ThreadPoolExecutor(int,int,long,TimeUnit,BlockingQueue<Runnable>,ThreadFactory) new ThreadPoolExecutor(int,int,long,TimeUnit,BlockingQueue<Runnable>,RejectedExecutionHandler) new ThreadPoolExecutor(int,int,long,TimeUnit,BlockingQueue<Runnable>,ThreadFactory,RejectedExecutionHandler)
构造方式二: 通过Executor 框架的工具类 Executors 来实现. 我们可以创建三种类型的ThreadPoolExecutor :
FixedThreadPool : 该方法返回一个固定线程数量的线程池. 该线程池的线程数量始终不变, 当有一个新的任务提交时, 线程池中若有空闲线程, 则立即执行, 若没有,则行动任务会暂存在一个任务队列中.待有线程空闲时, 便处理在任务队列中的任务.
SingleThreadExecutor : 方法返回一个只有一个线程的线程池. 若多余一个任务被提交到该线程池. 任务会被保存在一个任务队列中, 待线程空闲. 按先入先出的顺序执行队列中的任务,
CahedThreadPool : 该方法返回一个可根据实际情况调整线程数量的线程池, 线程池的线程数量不确定. 但若有空闲线程可以复用. 则会优先使用可复用的线程, 若所有线程都在工作. 又有新的任务提交.则会创建线程处理任务.所有线程在当前任务执行完后. 将返回线程池等待复用.
面试中关于 Atomic 原子类的 4 连击
Atomic 原子类介绍
Atomic 翻译成中文就是原子的意思. 在化学上, 原子是构成一般物质的最小单位.在化学反应中是不可分割的. 在 我们这里. Atomic 是指一个操作是不可中断的.即使是多个线程一起执行的时候, 一个操作一旦开始.就不会被其他线程打扰.
所以. 所谓原子类说简单点就是带有 原子/ 原子操作特征 的类
并发包 存放在 java.util.concurrent 包. 原子类都在 java.util.concurrent.atomic 包下.
JUC ( java.util.concurrent ) 包中的原子类是哪四类
基本类型
使用原子的方式更新基本类型
AtomicInteger 整型原子类
AtomicLong 长整型原子类
AtomicBoolean 布尔型原子类
数组类型
使用原子的方式更新数组中的某个元素
AtomicIntegerArray 整型数组原子类
AtomicLongArray 长整型数组原子类
AtomicReferenceArray 引用类型数组原子类
引用类型
AtomicReference 引用类型原子类
AtomicStampedRerence 原子更新引用类型里的字段原子类
AtomicMarkableReference 原子更新带有标记位的引用类型
对象的属性修改类型
AtomicIntegerFieIdUpdater 原子更新整型字段的更新器
AtomicLongFieldUpdater 原子更新长整型字段的更新器
AtomicStampedReference 原子更新带有版本号的引用类型. 该类将整数值与引用关联起来. 可用于解决原子的更新数据和数据的版本号, 可以解决使用CAS进行原子更新时可能出现的ABA问题.
进程与线程的区别是什么?进程间的几种通信方式说一下? 线程间的几种通信方式知道不?
进程与线程的区别是什么?
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同
的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工
作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。另外,也正是因为共享资源,所以线程中执
行时一般都要进行同步和互斥。总的来说,进程和线程的主要差别在于它们是不同的操作系统资源管理方式。
进程间的几种通信方式说一下?
管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有血缘关系的进程间使用。
进程的血缘关系通常指父子进程关系。管道分为pipe(无名管道)和fififo(命名管道)两种,有名管道也是半双
工的通信方式,但是它允许无亲缘关系进程间通信。
信号量(semophore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它通常作为一种锁
机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同
线程之间的同步手段。
消息队列(message queue):消息队列是由消息组成的链表,存放在内核中 并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。 消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件
接收特定类型的消息。
信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某一事件已经发生。
共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进
程创建,但多个进程都可以访问,共享内存是最快的IPC方式,它是针对其他进程间的通信方式运行效率低而专
门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。
套接字(socket):socket,即套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)
系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网
络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来
线程间的几种通信方式知道不?
1、锁机制
互斥锁:提供了以排它方式阻止数据结构被并发修改的方法。
读写锁:允许多个线程同时读共享数据,而对写操作互斥。
条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件测试是在互斥锁的保护下进行
的。条件变量始终与互斥锁一起使用。
2、信号量机制:包括无名线程信号量与有名线程信号量
3、信号机制:类似于进程间的信号处理。
线程间通信的主要目的是用于线程同步,所以线程没有象进程通信中用于数据交换的通信机制。