Java多线程
Posted miraclemaker
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程相关的知识,希望对你有一定的参考价值。
1.基本概念:
- 进程和线程:一个进程之内可以分为一到多个线程;进程是不活动的,只是作为线程的容器;进程拥有共享的资源;同一台计算机的进程通信称为 IPC。不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP。进程间互不影响,线程则不一定。
- 并行与并发:并发:线程通过上下文切换在执行多个任务,并行:单位时间下多个线程同时执行任务。
- 同步与异步:需要等待结果返回,才能继续运行。不需要等待结果返回,就能继续运行。
- 栈与栈帧:每个线程启动后,虚拟机就会为其分配一块栈内存,每个栈由多个栈帧(Frame)组成,每调用一个方法,就会生成一段栈帧,方法执行完后就会被释放。
- 线程上下文切换:当并发出现时,cpu分配的时间片用完后会进行线程上下文切换,操作系统保存当前线程的状态,并恢复另一个线程的状态。程序计数器就会记住当前线程指令的地址。
- 多线程的问题:当时间片用完后操作的数据还没存储,就分给二线程了,二线程对其作修改后又转回一线程,一线程会存储上次的数据,则二线程操作无效了,但外界认为二线程操作已经完成了。
- lock:主要分为ReentrantLock(可重入锁)和ReentrantReadWriteLock(读写锁),通过AQS生效。
- ThreadLocal:当同一对象需要在不同线程中相互独立,即每个线程中都需要该对象的一个副本时使用,实现变量隔离互不影响。
- AQS:通过尝试获取volatile修饰的共享变量 state 的结果来对线程的状态作出处理。获取成功的线程CAS修改state的之后直接进行自己的处理。未能成功获取共享变量的线程会被封装成结点放入 一个队列中,然后 自旋的检查自己的状态,看是否能再次去获取state资源,获取成功则退出当前自旋状态,获取失败则找一个安全的点(成功的找到一个状态<0前驱结点,然后将其状态设置为SIGNAL),调用LockSupport.park()方法进入waiting状态。然后等待被前驱结点调用release方法(实际上是调用 LockSupport.unPark())或者被中断唤醒。signal(-1),表示当前节点释放锁的时候,需要唤醒下一个节点
- Semaphore:信号量,当调用acquire()时判断是否有信号,有就获取一个信号量,没有就阻塞等待其他线程释放信号量,当调用release()时释放一个信号量,唤醒阻塞,允许多个线程同时访问同一资源
2.定义线程的四种方式:
- (1)继承 Tread 类;实现 Runnable 接口;实现 Callable 接口,带有返回值;线程池创建线程。
3.锁的问题:
- 死锁:多个线程在执行过程中,因争夺相同资源的同时不放开自身资源而造成的相互等待僵局。
。四个必要条件:
。互斥:同一资源同时只能一个线程在使用,不能共享;
。占有并等待:每个线程都占有资源并互相等待对方先释放自己需要的资源;
。非抢占:线程未释放时,其他线程不能强行抢占其资源;
。循环等待:线程占有和需要的资源形成一个循环。
。死锁预防:
。层次分配策略:将所有资源都分层,线程申请时只能申请比自己拥有资源层次更高的资源,释放时必须先释放更高层次的资源。这样就避免了资源的循环等待。
。银行家算法:线程进行前判断资源全部获取后系统是否是安全的,是就就分配资源。
。死锁检测和解除:
。用jps查找到运行线程的pid,再根据jstack生成线程快照排查死锁,通过逐步撤销涉及的线程,或者从几个线程手中抢占资源解除死锁。
- 活锁:线程没有暂停,但是由于互相改变了对方线程的结束条件,导致两个线程一直在不停地运行。
- 饥饿:线程优先级导致某个线程一直无法获得cpu的使用权。
4.重入,打断:
- 重入:重进入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞。
- 打断:锁在执行过程中可能会被强制interrupt。
5.并发安全三大特性:
- 原子性:操作在执行期间不被其他线程影响。
- 可见性:当一个线程在工作内存修改了变量,其他线程能立刻知道。
- 有序性:JVM对指令的优化会让指令执行顺序改变,有序性是禁止指令重排。
6.cas锁:
- 保证原子性。CompareAndSwap,他会每次更新前都通过Unsafe对象读取主存中的值,调用unsafe的compareandset方法比较读取的值与预期值是否一致。如果不一致,会将读取到的值设置成预期值,自旋重试。如果一致,才会去更改为更新值。在这个过程中,cas底层会加一个lock指令保证操作的原子性,但是这个lock很轻量,不会导致其他线程被阻塞。但是cpu开销大,并且存在ABA问题,可以加版本号避免aba问题。
7.Volatile关键字:
- 使用了 volatile 修饰变量后,在变量修改后会立即同步到主存中,每次用这个变量前会从主存刷新,并且禁止指令重排。
- 能够保证可见性和有序性。
- 单例模式要加volatile:new Object()简单分为三个步骤:为对象分配内存;构造器初始化;将对象引用赋值给变量。如果在这个过程中发生了指令重排,a线程先分配内存,构造器初始化还没进行先有了对象引用,但线程b检测到对象有引用就直接返回了。因此要加volatile禁止指令重排。
- 保证可见性:每次修改工作内存的变量后都会同步到主存,其他存有该变量的工作内存中该变量就会失效,这样下次其他线程要修改该变量就要重新去主存读。
- 为什么能禁止指令重排序:有四种内存屏障,是load和store的四种排列,表示后面操作进行之前,前面的操作要执行完毕并可见。加完volatile后,读时会对volatile读操作后加loadload和loadstore(等我读完你在读写);写时会对volatile写操作前加storestore,后加storeload(等你写完我写,我写完你再读)。
- 为什么不能保证原子性:在复合操作中,比如i++,分三步:读取i值,i自增,刷新i值。如果两个线程同时完成了前两步,此时一线程抢先进行了刷新,那么二线程的i值就会失效,在需要获取i的时候需要去主存中重新读,但是最后一步刷新值不需要读,所以就会直接赋值,无法保证原子性。
8.synchronized:
- 原子性,可见性,有序性都能保证。
- 使用:初始化的时候锁定线程对象;加在方法上(调用该方法的就是锁住的对象),加到静态方法上,锁住的对象就是类对象。锁住的是同一对象才能生效。通过监视器生效。
- 缺点:
。synchronized锁住的线程无法被打断,出现阻塞其他线程只能一直等待,但有异常会自动释放锁。
。发生读写操作时,无法保证读读操作同时进行,同一时间只允许一个线程操作。
。无法获知某一线程是否获得锁。
- 锁升级:
。偏向锁:程序最初为线程附加的都是偏向锁,本线程进入无需复杂的判断,快速进入锁内部。
。轻量级锁:其余时间有其他线程获得过锁,升级为轻量级锁,会有部分的性能消耗。(轻量级锁为什么只有部分的消耗提升?)
。重量级锁:同一时间多个线程发生锁的争抢,升级为重量级锁。当多个线程来竞争时,后来的线程会每隔一段时间尝试一次,次数多了才进等待队列等待(自旋优化)。
9.sleep和wait方法的区别:
- sleep不会释放锁,wait会释放锁。
- sleep是Thread的方法,wait时Object的方法。
- wait其实是调用nofity唤醒,sleep是直接唤醒。
- wait只能在同步代码块(Synchronized)中使用,sleep随时可以使用。
10.yield和join方法的区别:
- yield是让出本次机会给其他线程,join也是运行其他线程,但是运行时本线程于阻塞状态。
11.线程池:
- 七大参数:
。corePoolSize:线程池中的常驻核心线程数。
。maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值大于等于1。
。keepAliveTime:多余的空闲线程存活时间,当空间时间达到keepAliveTime值时,多余的线程会被销毁直到只剩下corePoolSize个线程为止。
。unit:keepAliveTime的单位。
。workQueue:任务队列,被提交但尚未被执行的任务。
。threadFactory:表示生成线程池中工作线程的线程工厂,用户创建新线程,一般用默认即可。
。handler:拒绝策略,表示当线程队列满了并且工作线程大于等于线程池的最大线程数(maxnumPoolSize)时如何拒绝的策略。(1)抛异常(2)丢弃任务不抛异常(3)打回任务(4)尝试与最老的线程竞争。
- 创建方式:
。Executors:通过Executors可以创建多种类型的线程池,比如FixedThreadPool(固定线程数,多于任务在队列中等待)CachedThreadPool(动态线程数,线程不够就无限制创建,空闲了再回收),SingleThreadExecutor(只有一个线程),延迟执行任务的线程池。
。ThreadPoolExecutor:通过自己定义七大参数去构建线程池。
12.JMM:
- 屏蔽掉所有硬件和操作系统的差异,使得java代码在所有平台都能够达到一致的并发效果。并且规定所有变量都存储在主内存中,线程内只有自己需要变量的拷贝,线程无法直接操作内存中的变量,线程间变量的传递需要自己的工作内存和主存之间进行数据同步。
- 变量本身就是共享的,但是不可见,线程间先复制一份共享变量到自己工作内存操作后释放,这样是不安全的,就用了volatile实现可见性。这只是操作共享变量的一种方式。threadlocal的目的是让线程之间操作共享变量后,共享变量并不改变,形成隔离。
13.线程状态:
- 创建:线程刚创建未调用start方法;
- 运行:线程正在运行中;就绪+运行。发生上下文切换时,线程还是运行状态(其实是运行->就绪);
- 阻塞:争抢synchronized锁失败正在被阻塞中;
- 等待:不唤醒就无限期的等待。join等待其他线程的执行;synchronized代码块内调用无参数的wait();进入aqs锁的阻塞队列中等待(park,unpark);
- 超时等待:有限期的等待;线程内调用有参数的sleep,join,wait方法。
- 终止:线程运行结束;
14.synchrozied和Reentrantlock对比:
- synchrozied:1.是关键字2.使用成本比较大,对对象进行加锁,3.可以自动释放,4.底层原理:底层根据监视器实现的,每个对象都有一个监视器,对象头里面也保存着锁的标志,表明现在的锁是偏向,轻量还是重量。
- Reentrantlock1.类2.直接用lock和unlock3.必须手动释放,4.功能更全面,比如实现公平锁和非公平锁,实现等待锁的时间。5.底层用aqs实现,不会锁升级。
15.ThreadLocal的内存泄露问题:
- 我们在使用ThreadLocal的时候,通常会new一个ThreadLOcal对象,形成了强引用。这样如果线程运行不结束,ThreadLocal变量就不会被回收。而实际生产中运用的是线程池,线程存活的时候很长,就会导致threadlocal对象一直不会被回收导致内存泄露问题。虽然threadlocal内部对threadlocalmap的key做了弱引用处理,但是entry依然不会被全部回收,所以我们建议在事务执行完之后手动调用remove方法。之所以不将value也设置为弱引用,因为我们不知道除了map还有没有其他引用value的,如果有其他引用,但是我们的value被清理了就会报空指针异常。
16.公平锁与非公平锁实现的原理:
- fairsync和unfairsync都继承ReentrantLock的内部抽象类Sync,sync继承aqs,他们都是实现aqs的一套流程。
- 最大的区别在于有没有一个方法hasQueuedPredecessors判断队列是否为空以及自己前面有没有线程,公平锁前面有这个方法,非公平没有。
17.Atomic类的原理和使用场景:
- 原理:使用Unsafe实现的,很多方法都是调用unsafe类中的本地方法,通过unsafe实现的CAS保证了原子性。并且存放数据的value属性用volatile修饰,保证数据的可见性与有序性。
- 使用场景:有原子更新基本类型,原子更新数组,原子更新引用,原子更新属性四种类型,通过这些类可以实现线程安全操作。
18.保证线程安全的方式:
- 加锁:悲观,synchrozied或者lock。
- 原子变量:乐观,即cas+volatile,如果高并发下会一直自旋重试,性能就变低了。
19.线程直接调用run方法行不行?
- 调用start后线程会进行准备,分配到时间片后会自动执行run方法。如果直接执行run方法,会认为run是main线程下的一个普通方法来执行。
20.构造方法是线程安全的吗?
- 构造方法并不是完全线程安全的,理论上来说多线程调用构造方法时,他们创建的成员变量都是独一份的,但是在构造方法中出现静态变量或者出现this逃逸(构造器中启动其他线程or构造器中使用监视器类)时就可能会发生同步问题。
以上是关于Java多线程的主要内容,如果未能解决你的问题,请参考以下文章