Java多线性

什么是线程

线程是操作系统能够调度的最小单位,它是进程中实际运行的单位。

进程是什么

程序不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,这种执行的程序就是进程

进程和线程的区别

1.进程是资源分配的最小单位,线程是资源调度的最小单位
2.进程大,线程小,一个进程可以包含很多个线程,线程在进程下进行
3.进程有自己的独立的地址空间,同一个进程内的线程贡献这个进程的地址空间
4.进程之间的资源是独立的,同一个进程内的线程共享本进程的资源

线程的创建

1.继承Thread类,重写run方法
2.实现Runnable接口,重写run方法
3.实现callable接口,重写call方法
实现callable接口要和实现runnable接口类似,但是功能更强大
1.call()可以抛出异常,runnable的run方法不可以
2.callable可以在任务结束的时候返回一个返回值,runnable不可以
3.可以通过运行callable得到future对象监听线程调用call方法的结果,得到返回值,但存在一个问题,调用Future.get方法,当前线程就会阻塞,直到call()返回结果。

Thread的start和run方法的区别

start方法用来启动一个新创建的线程,而且start方法内部调用了run方法,这和直接调用run()效果不一样。
直接调用run()只会在原来的线程中调用,没有新的启动线程;
start()方法才会启动新的线程
也就是说用start()方法来启动线程才是真正的实现了多线程,而run只是一个普通的方法。

线程的状态(6种)

1.New:新建状态,new出来的,还没调用start()方法
2.Runnable:可运行(就绪)状态,调用start()进入可运行状态,可能运行也可能没有运行,要看操作系统的调度
3.running:运行状态,就绪状态的线程获得了CPU资源
4.blocked:阻塞状态,被锁阻塞,暂时不活动,线程因为某些原因放弃了CPU的使用权,即它让出了CUP timeslice,暂时停止运行。直到线程进入到可运行状态才能再次获得CPU Time Slice转到running状态。
Synchronized修饰的方法或代码块时的状态,当一个线程视图获取一个内部对象的锁,而这时候这个锁又被其他线程持有,那么这个线程就会进入阻塞状态。当其他线程释放锁,并且该线程的线程调度器允许本线程持有它的时候,该线程就变成非阻塞状态。
5.wait:等待状态,不活动,也不运行任何代码,等待线程调度器调度,当线程等待另一个线程通知调度器的一个条件时,他自己就进入了等待状态。在调用object.wait()或者Thread.jion()方法,或者等待Concurrent库中的Lock或Condition时就会出现这种情况。wait和sleep都是暂停当前线程执行,以毫秒为单位,任何其他线程都可可以中断当前线程的睡眠,这种情况下将会抛出interruptException异常。
(6)Timed waiting:有几个方法里有一个超时参数,调用他们就会进入到计时等待状态,超时等待,指在指定时间自动返回
6.Terminated:终止状态,包括正常终止和异常终止
a)因为run方法正常退出而自然死亡
b)因为一个没有捕获的异常而终止了run方法而意外死亡

线程中断

interrupt()并不会中断线程,他只是将这个中断的标识设置成true,具体的中断由程序来判断的。
我们在各个状态下调用interrupt()结果
1.New和terminated:线程不会理会中断请求,即不会设置标记位
2.Runnable和Running:线程会调用interrupt()将中断标志位设置为true
3.waiting和Blocked:会抛出interruptException
interrupt()设置标志位,;Thread.currentThread().isInterrupted()检查标志位;interrupted()清除标志位
interrupt()方法,它不会真正停止一个线程,它仅仅是给这个线程发了一个信号告诉它它应该结束了(设置一个停止标志)。真正符合安全的做法,就是让线程自己去结束自己,而不是让一个线程去结束另外一个线程。通过interrupt()和.interrupted()方法两者的配合可以实现正常去停止一个线程.
线程A通过调用线程B的interrupt方法通知线程B让它结束线程,
在线程B的run方法内部,通过循环检查.interrupted()方法是否为真来接收线程A的信号,
如果为真就可以抛出一个异常,在catch中完成一些清理工作,然后结束线程。
Thread.interrupted()会清除标志位,并不是代表线程又恢复了,可以理解为仅仅是代表它已经响应完了这个中断信号然后又重新置为可以再次接收信号的状态。
总之interrupt()方法仅仅是改变一个标志位的值而已,和线程的状态并没有必然的联系。

什么是线程安全

一段代码所在的进程里有多个线程在执行,而这些线程可能会同时运行这段代码,如果每次运行的结果和单线程运行的结果是一致的,并且其他的变量的值也和预期的一样,就是线程安全的。一个线程安全的计数器实例对象在被对个线程使用的情况下也不会出现计算失误。

Sleep和wait的区别

Sleep和wait都是让线程暂停执行的方法,但是他们是不同的
Sleep是Thread的静态方法,他会让线程暂停指定的时候,并且将执行的机会(CPU)让出来给其他的线程,但是它不会释放锁,所以休眠之后会立即恢复,线程回到就绪状态。
Wait是object类的方法,他也会让线程暂停活动,但是它会让线程释放对象的锁,线程进入对象的等待池,只有调用对象的notify方法或者notifyAll方法才能唤醒等待池里的线程进入等锁池,如果线程重新获得了对象的锁才可以重新进入就绪状态。

Sleep和yield区别?

Sleep:给其他线程执行机会不考虑线程的优先级,所以有的线程都有机会获得CPU运行;yield:只给和自己优先级一样或者优先级更高的线程运行的机会
Sleep执行后转入阻塞状态,yield执行后转入就绪状态
Sleep方法会抛出interruptException异常,yield没有声明任何异常
Sleep比yield具有更好的可移植性(跟操作系统CPU调度相关)

notify和notifyAll的区别

notify方法不能唤醒某个具体的线程,所以只有一个线程在等待池的时候他才有用武之地;
notifyAll唤醒所有的线程并允许他们争夺锁确保了至少有一个线程能继续运行

wait notify notifyAll为什么不在Thread里?

一个很明显的原因是Java的锁是对象级别的不是线程级别的。如果线程要等待某些锁,那么调用对象的wait方法就有意义,如果wait()在thread类中,那么线程要等待的是那个锁就不明显了,简单的说就是他们都是锁级别的操作,所以把它们定义在Object类中因为锁属于对象。

ThreadLocal

ThreadLocal线程的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供一个该变量的副本,这样每个线程可以独立的操作自己的副本而不会去影响其他线程对应的副本。这就是线程隔离,它主要依靠的是ThreadLocalmap类。
它是线程自身所有的,不在多个线程之间共享。它是用来维护线程中的变量不被其他的线程干扰出现的一个结构,内部包含一个ThreadLocalmap类,它是thread的一个局部变量,这个Map的key存放的key就是ThreadLoacl对象所在的线程对象,value存放就是我们要存储的对象,这样一来,每个线程都持有当前线程的变量副本,与其他的线程完全隔离,这样就可以保证线程执行过程中不收其他的线程影响。利用ThreadLocal可以实现让所有的线程持有初始值相同的一个变量副本,但在初始化以后每个线程都只能操作自己的那个变量副本,不同线程之间的变量副本互不干扰。

ThreadLocal如何为每个线程创建副本?

首先在每个Thread内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量ThreadLocals,这个ThreadLocals就是用来存储变量副本的,键值为当前ThreadLocal变量,value为副本。初始的时候,ThreadLocals为空,当通过ThreadLocal变量调用get()或set()时,就会对Thread类中的ThreadLocals进行初始化,并且当前ThreadLocal为键值,以ThreadLocal要保存的副本变量为value,存到ThreadLocals。然后再当前线程里,如果要使用副本变量就可通过get方法在ThreadLocals里找。
总结:
a、实际上通过ThreadLocal创建的副本是存储在每个线程自己的ThreadLocals里的
b、为何key是ThreadLocal对象?因为每个线程中有多个ThreadLocal变量
c、在get之前必须先set,否则会报空指针;如果没使用set就能正常访问的话,必须重写initialvalue方法

ThreadLocal内存泄漏的问题

ThreadLocalMap中可以是ThreadLocal的弱引用,弱引用比较容易被回收,因此如果ThreadLocal(ThreadLocalMap的key)被垃圾回收器回收 了,但是因为ThreadLocalMap的生命周期是和Thread一样的,它这时候如果没有被回收,就会出现这样情况:ThreadLocalMap的key没了但是value还在,这就会造成内存泄漏。所以我们在使用完ThreadLocal后及时调用remove方法释放存储空间。
ThreadLocal一般在数据库连接池和会话管理中使用。
守护线程
守护线程和唯一的用处就是为其它线程提供服务。可以调用thread.Daemon()把一个线程变成守护线程。计时器就是一个例子,JVM 中的垃圾回收线程就是典型的守护线程,当只有守护线程的时候,虚拟机就退出了。守护线程永远都不会去访问固有资源,如文件,数据库,因为他会在任何操作的中间发生中断。
守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。通常来说,守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。

进程之间通信方式

(1) 管道(PIPE)
(2) 命名管道(FIFO)
(3) 信号量(Semphore)
(4) 消息队列(MessageQueue)
(5) 共享内存(SharedMemory)
(6) Socket

并行和并发的区别

并行是多个事件在同一个时间段执行,并发是多个事件在同一个时间点执行。
竞争条件:两个以上的线程需要共享同一数据的存取,如果两个线程存取相同的对象,并且每一个线程都调用了一个修改该对象状态的方法,那么就会产生错误的对象,这种情况就可以称为竞争条件。

ReentrantLock

可重入锁,就是锁是可重入的,因为线程可以重复获得已经持有的锁,锁保持有一个计数器(hold count)来跟踪对锁方法的嵌套调用。线程每一次调用lock都要调用unlock来释放锁,由于这个特性,被一个锁保护的代码可以调用另一个使用相同锁的方法。
ReentrantLock底层使用了CAS+AQS队列来说实现,下面分别介绍这连个技术
CAS(Compare And Swap)是一种无锁算法,当多个线程尝试通过CAS同时更新同一个变量的时候,只有其中一个线程能够更新变量的值,其他的线程都失败,但失败的线程并不会被挂起,而是被告知在这次竞争中失败了,并可以再次尝试。CAS是一种非阻塞式的同步方式。
它有三个操作数:内存值V,旧的预期值A和要修改的新值B。如果内存值V和旧的预期值A相等,那么处理器就会自动将这个位置的值更新为B,否则处理器不做任何操作。无论哪种情况他会在CAS指令之前返回该位置的值(CAS一些特殊情况仅返回CAS成功否,不提取当前的值)。CAS有效的说明了‘我认为位置V应该是包含值A的,如果包含了,那就在该位置上填上B,否则就不要更改该位置,只告诉我这个位置的值就好了’。这其实和乐观锁的冲突检查+数据更新原理是一样的。
AQS是一个用于构建锁和同步容器的框架。AQS使用一个FIFO队列(CLH队列,是CLH锁的一种变形)表示排队等待锁的线程。队列头结点称为“哨兵节点”,他不与任何线程关联,其他的节点与等待线程关联,每个节点都维护一个等待状态waitStatus。

ReentrantLock的流程:

(1)ReentrantLock先尝试通过CAS获取锁,如果此锁已经被占用,该线程加入AQS队列并等待;当前驱线程的锁被释放,挂在CLH队列为首的线程就会被notify()然后继续CAS尝试获取锁。这时有两种锁:
a)非公平锁,如果有其他线程尝试lock,有可能被其他刚好申请锁的线程抢占
b)公平锁,只有CLH为首的线程才可以获取锁,新来的线程只能插入到队尾
ReentrantLock默认是非公平锁,也可以指定为公平锁
lock()的实现:如果线程成功通过CAS修改了state,指定当前线程是该锁的独占线程,标志自己成功获得锁;如何失败了了则调用acquire();
acquire():首先调用tryAcquire(),会尝试再次通过CAS修改state为1,如果失败了并且发现该锁是被当前线程占用的,就支持重入,state++
如果发现是被其他线程占用的,那么当前线程执行tryAcquire()返回失败,并执行addWait()进入等待队列,并挂起自己的interrupt()
tryAcquire():检查state字段,若state==0,表示当前锁未被占用,那么尝试占用;若state不为0,检查当前锁是不是被自己占用,若是被自己占用,那么state++,表示重入的次数;若不是被自己占用,那么就获取锁失败,返回false。
addWait():当前线程加入AQS队列。写入之前要将当前线程封装成一个Node节点,先判断队列是否为空,不为空的时候,就将Node节点通过CAS写入队尾,如果出现写入失败,就调用eqn(node)来写入,equ的处理逻辑相当于自旋+CAS保证了一定能写入队列。
写入队列之后就要挂起当前线程,首先会根据node.predecessor()获取上一个节点是不是头结点,如果是,则尝试获取一次锁,如果成功了就万事大吉,如果失败或者不是头结点,就会根据上一个节点的waitstatus状态来处理。shouldparkAfterFailedAcquire返回是否需要挂起,如果需要挂起则利用LockSupport的part方法来挂起当前线程,直到被唤醒。
unclock():释放锁,state–,通过state == 0 来判断锁是否完全被释放。成功释放锁的话,就唤起一个被挂起的线程。
总结:
1.每个reentrantLocal都维护着一个AQS队列,这个队列保存申请锁的线程。
2.通过大量的CAS保证了多线程竞争锁的时候并发安全
3.可重入的功能通过维护State变量来记录重入次数实现的
4.公平锁需要维护队列,通过AQS队列的先后顺序获取锁,缺点是会造成大量线程上下文的切换,
5.非公平锁可以直接抢占,所以效率更高
非公平锁可重入锁:
1.获取state值,若为0,则意味着没有线程获取到资源,CAS将其设置成1,设置成功则代表获取到排他锁了。
2.若state大于0,则肯定有其他线程抢占了锁,此时再去判断是不是自己持有的锁,是的话,就state++,返回true,重入成功
3.其他情况下,获取锁失败
公平锁:
公平锁和非公平锁大致一样,不同点在于要判断hasQueuePreecessor这个逻辑判断,即使是state ==0,也要判断有没有还在排队的线程,若没有,才能尝试去获取,做后面的处理。否则返回false,获取锁失败。

reentrantLock的tryRelease()方法原理:

若state值为0,表示当前线程已经释放干净,返回true,上层的AQS会意识到资源已经空出。若不为0 ,则表示线程还占有资源,只不过将此次重入的资源释放了而已,返回false。
ReentrantLock是一种可重入的,可实现公平性的互斥锁,它的设计基于AQS框架,可重入和公平性的实现逻辑都不难理解,每重入一次,state就+1,当然释放的时候,也得一层一层释放。至于公平锁,在尝试获取锁的时候多了一个判断,是否有比自己申请早的线程在同步队列中等待,若有,就继续等待,若没有,就去抢占。

synchronized机制

1.一段syncchronized修饰的代码块在被一个程序执行之前,要先拿到执行这段代码的权限,在java中就是要拿到谋而同步对象的锁(一个对象只有一把锁)
2.如果这个同步对象的锁被其他线程拿走了,那么这个线程只能等了(线程阻塞在锁池等待队列中)
3.取到锁后,该线程就可以开始执行同步代码(被synchronized修饰的代码块)
4.线程执行完代码块后就马上把锁还给对象,其他在锁池中等待的某个线程就可以拿到锁执行同步代码块。
这样就保证了同步代码在统一的时刻只有一个线程在执行。
一个线程可以多次对一个对象上锁(可重入锁)。对于每一个对象,java虚拟机维护着一个加锁计数器,线程每获得一次该对象,计数器就加一,每释放一次锁,计数器就减一,当计数器为0 的时候,锁就完全被释放了。

Synchronized原理

Synchronized代码块是由一对monitorexit和monitorenter实现的。同步方法是通过ACC_Synchronized标志实现的。Mintor对象是同步的基本实现,而Synchronized的同步方法使用了ACC_Synchronized访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
为了保证放在异常发生时,monitorexit和monitorenter指令依然可以正确配对执行,编译器会自动产生一个异常处理器,异常处理器可以处理所以的异常,它的目的就是用来执行monitorexit指令。
每个对象有一个监视器锁(Monitor)。当Monitor被占用的时候就会处于锁定状态,
线程执行monitorenter指令尝试获取Monitor的所有权,过程如下:
1.若果Monitor的进入数为0,则该线程进入Monitor,进入计数器设置为1,该线程就是Monitor的持有者
2.如果该线程已经占有该Monitor了,只是重新进入,则进入计数器+1;
3.如果其他线程已经占用了Monitor,则线程进入阻塞状态,直到Monitor的进入计数器为0,该线程再尝试去获取Monitor的所有权。
执行monitorexit的线程必须是ObjectRef所对应的Monitor的所有者。
monitorexit指令执行时,Monitor计数器减一,如果减一后计数器为0,那线程退出该Monitor。其它被这个Monitor阻塞的线程可以尝试去获取这个Monitor的所有权。
Java 1.6之前,Monitor的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户状态到内核状态的切换,所以同步操作是一个无差别的重量级操作。现代的JDK中,JVM对此进行了改进,提供了三种不同的Monitor实现,也就是三种不同的锁:便宜锁,轻量级锁和重量级锁。所谓的锁升级,降级,就是JVM优化synchronized运行的机制,当JVM检测到不同的竞争状态时,就会自动地切换到合适的锁实现,这种切换就是锁的升级降级。
偏向锁:一旦第一次获得了Monitor对象,之后就让这个Monitor对象“偏向”这个线程,之后多次调用则可以避免CAS操作,说白了就是设置一个标志位,当它为true的时候就不需要再走各种加锁解锁的流程了。
轻量级锁:由偏向锁升级而来,偏向锁是运行在一个线程进入同步代码块的情况下,当有第二线程加入锁的竞争时,偏向锁就会升级为轻量级锁。
重量级锁:在JVM中又叫对象监视器,他很像C中的Mutex,除了具备Mutex互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含有个竞争锁的队列和一个信号阻塞队列(wait)队列,前者负责做互斥,后者负责做线程同步。
自旋锁:如果持有锁的线程能够在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核状态和用户状态之间的切换进入阻塞挂起状态,他们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这可以避免用户线程和内核之间的切换的消耗。但是线程自旋需要消耗CPU,就是说它会让CPU做无用功,如果一直获取不到锁,线程也不能一直占着CPU自旋,所以要设置一个自旋的最大时间。如果持有锁的线程在自旋的最大等待时间内没有释放锁,那么竞争锁的线程就会停止自旋进入阻塞状态。
当没有竞争出现的时候,默认会使用偏向锁,JVM会利用CAS操作,在对象头上的MarkWord部分设置线程的ID,表示这个对象偏向当前线程,所以并不涉及真正的互斥锁。
如果有另外的线程尝试锁定某个已经被偏向过的对象,JVM就要撤销偏向锁(revoke),并切换到轻量级的锁实现。轻量级的锁依赖CAS操作MarkWord来视图获取锁,如果重试成功,就使用普通的轻量级锁,否则就进一步升级为重量级锁(可能会先进行自旋锁升级,如果失败再尝试重量级锁升级)
锁降级:当JVM进入安全点的时候,会检查闲置的Monitor,然后试图进行降级。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值