java 多线程学习总结

时间:2021-08-08 18:29:57

一、多线程基础
1.进程和线程的区别?
1).什么是进程?
进程是正在运行的应用程序,进程是线程的集合。
2).什么是线程?
线程是一条执行路径,一个独立的执行单元。
2.为什么使用多线程?
提高应用程序效率
3.多线程的应用场景?
多线程下载、QQ、前端开发ajax(异步上传)、分布式job(需要同一时间多个任务调度)
4.创建多线程有几种方式?
1.继承Tread类
2.实现Runnable接口
3.使用匿名内部类创建
4.callable
5.使用线程池创建
5.守护线程和非守护线程的区别?
守护线程:和主线程一起销毁
非守护线程: 和主线程互不影响
6.多线程有几种状态?
1.新建状态
2.就绪状态
3.运行状态
4.阻塞状态
5.死亡状态
二、线程安全问题
1.什么是线程安全问题?
当多个线程共享同一个全局变量,做写的时候可能会受到其他线程的干扰,导致数据问题,这种现象叫做线程安全问题。(做读的时候不会产生线程安全问题)。
2.当多个线程共享同一个局部变量,做写操作的时候会产生线程安全问题吗?为什么?
不会,因为
3.线程如何实现同步?有哪些解决办法?
多个线程共享同一个全局变量,数据安全问题,保证数据的原子性
解决办法:
1.synchronized ----自动锁
2.lock ----jdk1.5并发包 ---手动锁
4.同步需要有一些条件
1.必须有两个线程以上
2.多个线程想要同步,必须用同一把锁
3.保证只有一个线程进行执行
5.同步的原理
1.有一个线程已经拿到了锁,其他线程已经有CPU执行,一直在排队,等待其他线程释放锁。
2.锁什么时候释放?
代码执行完毕或者程序抛出异常都会被释放掉
3.锁已经释放掉,其他线程开始获取锁进入同步中去
4.锁的资源竞争
5.死锁问题
6.同步的缺点
效率低
7.什么是同步函数?
在方法上修饰synchronized称为同步函数
8.什么是静态同步函数?
在方法上加上static关键字,使用synchronized 关键字修饰 或者使用类.class文件。静态的同步函数使用的锁是该函数所属字节码文件对象
9.两个线程,一个线程使用同步函数,另一个线程使用静态同步函数能实现同步吗?
不能,同步函数使用this锁,静态同步函数使用当前字节码文件
10.加锁有什么好处?
加锁是为了保证同步,同步是保证数据安全问题和原子问题和分布式锁、高并发和JVM是没有关系的。
11.总结
synchronized 修饰方法使用锁是当前this锁。
synchronized 修饰静态方法使用锁是当前类的字节码文件
三、线程死锁
1.什么是多线程死锁?
同步中嵌套同步,导致锁无法释放,一直等待,变为死锁。
四、Java内存模型
1.什么是java内存模型?
java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。
2.多线程有哪些特性?
原子性、可见性、有序性
3.什么是原子性?
即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
4.什么是可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
5.什么是有序性
程序执行的顺序按照代码的先后顺序执行。一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
五、Volatile
1.什么是Volatile?
Volatile 关键字的作用是变量在多个线程之间可见。
2.已经将结果设置为fasle为什么还一直在运行呢?
线程之间是不可见的,读取的是副本,没有及时读取到主内存结果。解决办法使用Volatile关键字将解决线程之间可见性, 强制线程每次读取该值的时候都去“主内存”中取值
3.Volatile非原子性
Volatile不具备数据原子性
4.volatile与synchronized区别?
仅靠volatile不能保证线程的安全性(原子性)。
1.volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法
2.volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞
3.synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。
4.线程安全性
线程安全性包括两个方面,1.可见性。2.原子性。
仅仅使用volatile并不能保证线程安全性。而synchronized则可实现线程的安全性。
六、多线程之间实现通讯
1.什么是多线程之间通讯?
多个线程之间通讯是多个线程在操作同一个资源,但是操作的动作不同。
2.wait和notify的区别?
wait的作用:让当前线程从运行状态变为休眠状态,释放锁的资源。
notify的作用:让当前线程从休眠状态变为运行状态。
这两个方法只能在同步中使用。
3.wait和sleep的区别?
sleep方法属于Thread类中的,wait是object类中的,sleep方法导致了程序暂停执行指定的时间,让出了CPU该其他线程,但是他的监控状态依然保持着,当指定时间到了又会自动恢复运行状态。
在调用sleep方法的过程中,线程不会释放对象锁。
当调用wait方法的时候,线程会释放对象锁,进入等待此对象的等待线程池,只有针对此对象调用notify方法后本线程才进入对象线程池准备,获取对象锁进入运行状态。
七、JDK1.5-Lock锁
1.Lcok锁从什么时候开始上锁,什么时候释放锁?
手动上锁,手动释放锁,灵活性高。
2.多线程并发和网站并发有什么区别?
多线程并发是操作同一个资源,网站并发是多个请求同时访问一台服务。
3.Lock 接口与 synchronized 关键字的区别?
Lock 接口可以尝试非阻塞地获取锁 当前线程尝试获取锁。如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁。
Lock 接口能被中断地获取锁 与 synchronized 不同,获取到锁的线程能够响应中断,当获取到的锁的线程被中断时,中断异常将会被抛出,同时锁会被释放。
Lock 接口在指定的截止时间之前获取锁,如果截止时间到了依旧无法获取锁,则返回。
4.Condition用法
Condition的功能类似于在传统的线程技术中的,Object.wait()和Object.notify()的功能。
5.怎么停止线程?
八、ThreadLocal
1.什么是ThreadLocal?
ThreadLocal提高一个线程的局部变量,访问某个线程拥有自己局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
2.ThreadLocal的接口方法
1.void set(Object value):设置当前线程的线程局部变量的值。
2.public Object get():该方法返回当前线程所对应的线程局部变量。
3.public void remove():将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
4.protected Object initialValue():返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
3.ThreadLocal实现原理
ThreadLocal通过map集合实现
九、java并发包
1.vector和arrayList的区别?
实现原理:都是通过数组实现,查询速度快,增加、修改、删除速度慢
区别:线程安全问题,vector是线程安全的,ArrayList是线程不安全的,ArrayList效率高。
2.HasTable与HasMap的区别?
1.hashtable是线程安全的,hashMap是线程不安全的。
HastMap是一个接口 是map接口的子接口,是将键映射到值的对象,其中键和值都是对象,并且不能包含重复键,但可以包含重复值。HashMap允许null key和null value,而hashtable不允许。
实现原理:通过链表和数组实现,put()方法通过hashcode取模得到下标位置,一致性取模算法。
2.HashTable是线程安全的一个Collection。
3.HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别在于HashMap允许空(null)键值(key),由于非线程安全,效率上可能高于Hashtable。
4.HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。
HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。
3.怎么证明hashtable是线程安全的?
查看源码的put方法
4.synchronizedMap
Collections.synchronized*(m) 将线程不安全额集合变为线程安全集合
5.ConcurrentHashMap------并发包
实现原理:将一个整体拆分成多个小的hashtable,默认分成16段。
1.ConcurrentMap接口下有俩个重要的实现
1.ConcurrentHashMap
2.ConcurrentskipListMap (支持并发排序功能。弥补ConcurrentHashMap)
2.ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的HashTable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。把一个整体分成了16个段(Segment.也就是最高支持16个线程的并发修改操作。
这也是在重线程场景时减小锁的粒度从而降低锁竞争的一种方案。并且代码中大多共享变量使用volatile关键字声明,目的是第一时间获取修改的内容,性能非常好。
6.CountDownLatch
CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。
7.CyclicBarrier
CyclicBarrier初始化时规定一个数目,然后计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续。
8.Semaphore
是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore可以用来构建一些对象池,资源池之类的,比如数据库连接池,我们也可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。
十、并发队列
在并发队列上JDK提供了两套实现,一个是以ConcurrentLinkedQueue为代表的高性能队列,一个是以BlockingQueue接口为代表的阻塞队列,无论哪种都继承自Queue。
1.ConcurrentLinkedDeque
是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue.它是一个基于链接节点的*线程安全队列。该队列的元素遵循先进先出的原则。头是最先加入的,尾是最近加入的,该队列不允许null元素。
2.ConcurrentLinkedQueue重要方法
add 和offer() 都是加入元素的方法(在ConcurrentLinkedQueue中这俩个方法没有任何区别)
poll() 和peek() 都是取头元素节点,区别在于前者会删除元素,后者不会。
3.BlockingQueue
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
4.ArrayBlockingQueue
ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。
ArrayBlockingQueue是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。
5.LinkedBlockingQueue
LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。
和ArrayBlockingQueue一样,LinkedBlockingQueue 也是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。
6.PriorityBlockingQueue
PriorityBlockingQueue是一个没有边界的队列,它的排序规则和 java.util.PriorityQueue一样。PriorityBlockingQueue中允许插入null对象。
所有插入PriorityBlockingQueue的对象必须实现 java.lang.Comparable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的。
可以从PriorityBlockingQueue获得一个迭代器Iterator,但这个迭代器并不保证按照优先级顺序进行迭代。
7.SynchronousQueue
SynchronousQueue队列内部仅允许容纳一个元素。当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。
十一、线程池
1.什么是线程池?
java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。
好处:1.降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2.提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
3.提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。
2.线程池的作用
程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。
如果一个线程的时间非常长,就没必要用线程池了(不是不能作长时间操作,而是不宜。),况且我们还不能控制线程池中线程的开始、挂起、和中止。
3.线程池的分类
1.ThreadPoolExecutor
java使用线程核心走的ThreadPoolExecutor
2.ThreadPoolExecutor构造函数参数
1.corePoolSize:核心池的大小。当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
2.maximumPoolSize:线程池最大线程数,它表示在线程池中最多能创建多少个线程
3.keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。
4.unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性
4.线程池的创建方式(Executors封装好的)
1.newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
2.newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3.newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。
4.newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
5.newCachedThreadPool
创建可缓存线程池,线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
6.newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。定长线程池的大小最好根据系统资源进行设置。
7.newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
8.newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
十二、线程池原理剖析
提交一个任务到线程池中,线程池的处理流程:
1.判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2.线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3.判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
十三、合理配置线程池
要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
1.任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
2.任务的优先级:高,中和低。
3.任务的执行时间:长,中和短。
4.任务的依赖性:是否依赖其他系统资源,如数据库连接。
十四、Java锁的深度化
1.悲观锁、乐观锁、排他锁
1.悲观锁:悲观锁悲观的认为每一次操作都会造成更新丢失问题,在每次查询时加上排他锁。
每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
2.乐观锁:乐观锁会乐观的认为每次查询都不会造成更新丢失,利用版本字段控制
2.重入锁
锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) 。这些已经写好提供的锁为我们开发提供了便利。
重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁
3.读写锁
相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些。假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。
在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写
(译者注:也就是说:读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。Java5在java.util.concurrent包中已经包含了读写锁。尽管如此,我们还是应该了解其实现背后的原理。
4.CAS无锁机制
1.与锁相比,使用比较交换(下文简称CAS)会使程序看起来更加复杂一些。但由于其非阻塞性,它对死锁问题天生免疫,并且,线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。
2.无锁的好处:第一,在高并发的情况下,它比有锁的程序拥有更好的性能;第二,它天生就是死锁免疫的。就凭借这两个优势,就值得我们冒险尝试使用无锁的并发。
3.CAS算法的过程是这样:它包含三个参数CAS(V,E,N): V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
4.CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
5.简单地说,CAS需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,那说明它已经被别人修改过了。你就重新读取,再次尝试修改就好了。
6.在硬件层面,大部分的现代处理器都已经支持原子化的CAS指令。在JDK 5.0以后,虚拟机便可以使用这个指令来实现并发操作和并发数据结构,并且,这种操作在虚拟机中可以说是无处不在。
5.自旋锁
自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区。
当一个线程 调用这个不可重入的自旋锁去加锁的时候没问题,当再次调用lock()的时候,因为自旋锁的持有引用已经不为空了,该线程对象会误认为是别人的线程持有了自旋锁,使用了CAS原子操作,lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程。
当有第二个线程调用lock操作时由于owner值不为空,导致循环一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。
由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。
6.分布式锁
如果想在不同的jvm中保证数据同步,使用分布式锁技术。有数据库实现、缓存实现、Zookeeper分布式锁

7.悲观锁与乐观锁的区别?
悲观锁与乐观锁如果查询量小可以使用乐观锁,乐观锁使用版本控制操作。