多线程介绍
多线程:线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,而多线程就是指从软件或者硬件上实现多个线程并发执行的技术,具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
线程创建方式
- 在JDK1.5之前,创建线程就只有两种方式,即继承java.lang.Thread类和实现java.lang.Runnable接口;
- 在JDK1.5以后,增加了两个创建线程的方式,即实现java.util.concurrent.Callable接口和线程池。
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 创建线程池
Runnable的run方法没有返回值 ;
Callable的call方法有返回值 , 需要调用其get方法获取 , 该方法阻塞 ;
线程状态
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权。即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
// 阻塞的情况分三种:
(1)、等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,
(2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
(3)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
在调用 sleep(…)方法的过程中,线程不会释放对象锁。而当调用wait()方法的时候,线程会放弃对象锁。wait():释放锁
优先级
分时调度模型:轮流获取CPU的使用权,并且平均分配每个线程占用CPU的时间片。
抢占式调度模型:让可运行迟中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其它线程获取CPU的使用权。
线程同步
加锁,让一个资源每次只能有一个线程访问,使用互斥锁
守护线程
用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止。
守护线程当进程不存在或用户线程停止,守护线程也会被停止。
线程安全
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
解决方案:加锁,synchronized,lock
线程并发的三个特性
原子性、可见性、有序性
可见性问题:volatile,volatile可以让多个线程之间可见,但是不具备原子性。
线程池
四种线程池:
- FixedThreadPool
- SingleThreadExecutor
- CachedThreadPool
- ScheduledThreadPool
线程池的7个参数
ThreadPoolExecutor:7个参数
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:空闲时间
- unit:时间单位
- workQueue:阻塞队列
- threadFactory:创建线程的工厂
- handler:拒绝策略
死锁
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法执行。当线程进入对象的synchronized代码块时,便占有了资源,直到它退出该代码块或者调用wait方法,才释放资源,在此期间,其他线程将不能进入该代码块。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。
产生死锁的原因
产生死锁的原因:
- 竞争资源
系统中的资源可以分为两类:
可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU和主存均属于可剥夺性资源;
另一类资源是不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。
产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)
产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁
进程间推进顺序非法
若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁
例如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞;当P2运行到P2:Request(R1)时,也将因R1已被P1占用而阻塞,于是发生进程死锁
死锁产生的4个必要条件
死锁产生的4个必要条件
互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
解决死锁的基本方法
解决死锁的基本方法
预防死锁:
资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
1、以确定的顺序获得锁
2、超时放弃
避免死锁:
预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。
银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的。
检测死锁:
首先为每个进程和每个资源指定一个唯一的号码;
然后建立资源分配表和进程等待表。
解除死锁:
当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:
剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
多线程死锁:同步中嵌套同步,导致锁无法释放。
死锁解决办法:不要在同步中嵌套同步
检查死锁方式
Jstack命令
JConsole工具
synchronized
解决可见性:
- 获得互斥锁(同步获取锁)
- 清空本地内存
- 从主内存拷贝变量的最新副本到本地内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存
- 释放互斥锁
同步原理:
- 普通同步方法,锁是当前实例对象this
- 静态同步方法,锁是当前类的class对象
- 同步方法块,锁是括号里面的对象
synchronized的同步操作主要是monitorenter和monitorexit这两个jvm指令实现的.
volatile与synchronized
- volatile : 可以解决可见性 , 有序性 ;
- synchronized : 可以解决可见性 , 有序性 , 原子性 ;
- 在jdk1.6之前,synchronized属于重量级锁,monitor(监视器锁)依赖于底层操作系统的Lock实现,java的线程是映射到操作系统的原生线程上,切换成本较高(从用户态-内核态),每一个切换线程,锁线程都要使用操作系统的方式,这个太麻烦,太笨重了。
在1.6之后引进了自旋锁,适应性自旋锁,锁粗化,轻量级锁等技术。
锁有四种状态:
- 无锁状态
- 偏向锁状态
- 轻量级锁状态
- 重量级锁状态
锁的竞争会随着状态的升级而变得激烈,锁只能升级,不能降级,获得锁和释放锁的效率会随着锁的升级而提高。
synchronized和lock的区别
synchronized和lock的区别
- lock是一个接口,synchronized是java一个关键字,是内置的语言实现
- synchronized异常时会自动释放所占有的锁,因此不会出现死锁;而lock发生异常时,不会主动释放锁,必须手动unlock释放锁,最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。
Lock可以通过trylock来知道有没有获取锁,而synchronized不能;
synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度;
synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
CAS
同步组件中大量使用CAS技术实现了Java多线程的并发操作。整个AQS同步组件、Atomic原子类操作等等都是以CAS实现的
缺陷:循环时间太长、只能保证一个共享变量原子操作、ABA问题版本号。
并发工具类
- CyclicBarrier:同步屏障,多个线程互相等待,直到到达同一个同步点,再继续一起执行。
- CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行。
- 区别:
- CyclicBarrier:可复用,当计数为0之后,下次调用await方法还会恢复到初始值
- CountDownLatch:不可复用。yclicBarrier 的await方法是多个线程相互等待; 而CountDownLatch是一个调用了await方法的线程, 等待其他线程的倒计时;
Semaphore:是一个控制访问多个共享资源的计数器,和CountDownLatch一样,其本质上是一个“共享锁”。
Semaphore维护了一个信号量许可集。线程可以获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以释放它所持有的信号量许可,被释放的许可归还到许可集中,可以被其他线程再次获取。