1.线程生命周期
1.1新建
new关键字创建了一个线程之后,该线程就处于新建状态
JVM为线程分配内存,初始化成员变量值
1.2就绪
当线程对象调用了start()方法之后,线程就处于就绪状态
JVM为线程创建方法栈和程序计数器,等待线程调度器调度
1.3运行
就绪状态的线程获取CPU资源,开始运行run()方法,该线程进入运行状态
1.4阻塞
当发生如下状况时,线程就会进入阻塞状态
线程调用sleep()方法主动放弃所占用的处理器资源
线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
线程试图获取一个同步锁(同步监视器),但该同步锁正被其他线程所持有
线程正在等待某个通知(notify)
程序调用了线程的suspend方法将该线程挂起,但这个方法容易导致死锁。所以应该尽量避免使用该方法
1.5死亡
线程会以如下3种方式结束,结束后处于死亡状态
1.run()或call()方法执行完成,程序正常结束
2.线程抛出一个未捕获的Exception或error
3.调用该线程stop()方法结束该线程,该线程容易导致死锁,不推荐使用
2.线程安全
2.1什么是线程安全
如果有多个线程同时运行同一个实现Runnable接口的类,程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全;反之,则是线程不安全的
例子:
演示售票
2.1.1创建售票线程类
public class Ticket implements Runnable {
private int ticktNum = 100;
public void run() {
while(true){
if(ticktNum > 0){
//1.模拟出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.打印进程号和票号,票数减1
String name = Thread.currentThread().getName();
System.out.println("线程"+name+"售票:"+ticktNum--);
}
}
}
}
2.1.2创建测试类
public class TicketDemo {
public static void main(String[] args){
Ticket ticket = new Ticket();
Thread thread1 = new Thread(ticket, "窗口1");
Thread thread2 = new Thread(ticket, "窗口2");
Thread thread3 = new Thread(ticket, "窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
运行结果如下:
程序出现了两个问题:
1. 相同的票数,比如5这张票被卖了两回。
2. 不存在的票,比如0票与-1票,是不存在的。
2.2.1问题分析
线程安全问题都是由全局变量及静态变量引起的
若每个线程对全局变量,静态变量只读,不写,一般来说,这个变量是线程安全的;
若有多个线程同时执行写操作,一般都需要考虑线程安全,否则的话可能影响线程安全。
综上所述,线程安全问题根本原因:1.多线程在操作共享数据;2.操作共享数据额线程代码有多条;3.多线程对共享数据有写操作
2.3问题解决-线程同步
要解决以上线程问题,只要在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象
为了保证每个线程都能正常执行共享资源,Java引入了7种线程同步机制,
- 同步代码块(synchronized)
- 同步方法(synchronized)
- 同步锁(ReenreantLock)
- 特殊域变量(volatile)
- 局部变量(ThreadLocal)
- 阻塞队列(LinkedBlockingQueue)
- 原子变量(Atomic*)
2.3.1同步代码块(synchronized)
synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问
synchronized(同步锁){
需要同步操作的代码
}
同步锁:对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁,锁对象可以是任意类型,多线程要使用同一把锁
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能等着(BLOCKED)
同步代码块如下
public class Ticket implements Runnable {
private int ticktNum = 100;
//定义锁对象
Object obj = new Object();
public void run() {
while(true){
synchronized (obj){
if(ticktNum > 0){
//1.模拟出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.打印进程号和票号,票数减1
String name = Thread.currentThread().getName();
System.out.println("线程"+name+"售票:"+ticktNum--);
}
}
}
}
}
2.4同步方法(synchronized)
同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等候
格式:
public synchronized void method(){
可能会产生线程安全的代码
}
同步方法如下:
public class Ticket implements Runnable {
private int ticktNum = 100;
//定义锁对象
Object obj = new Object();
public void run() {
while(true){
sellTicket();
}
}
private synchronized void sellTicket(){
if(ticktNum > 0){
//1.模拟出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.打印进程号和票号,票数减1
String name = Thread.currentThread().getName();
System.out.println("线程"+name+"售票:"+ticktNum--);
}
}
}
2.5同步锁(ReenreantLock)
同步锁:java.util.concurrent.locks.Lock机制提供了比synchronized代码和synchronized方法更广泛的锁定操作,同步代码块、同步方法具有的功能Lock都有,除此之外更强大,更体现面对对象
代码如下
public class Ticket implements Runnable {
private int ticktNum = 100;
//定义锁对象:构造函数参数为线程是否公平获取锁true-公平;false-不公平,即由某个线程独占,默认是false
Lock lock = new ReentrantLock(true);
public void run() {
while(true){
lock.lock();
try{
//加锁
if(ticktNum > 0){
//1.模拟出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.打印进程号和票号,票数减1
String name = Thread.currentThread().getName();
System.out.println("线程"+name+"售票:"+ticktNum--);
}
} finally {
//放锁
lock.unlock();
}
}
}
}
总结
synchronized和lock区别
synchronized是Java内置关键字,在JVM层面上,lock是Java类;
synchronized无法判断是否获取锁的状态,lock可以判断是否获取锁;
synchronized会自动释放锁(a线程执行完同步代码会释放锁,b线程执行过程中发生异常会释放锁),lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2会一直等待下去,而lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束
synchronized的锁可重入,不可中断,非公平,而lock锁可重入,可判断,可公平(两者皆可)
lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题
3.线程死锁
线程死锁:多线程以及多线程改善了系统资源的利用率并提高了系统的处理能力,然而,并发执行也带来了新的问题--死锁
所谓死锁就是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进
3.1死锁产生的必要条件
一下这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁
3.1.1互斥条件
进程要求对所有分配的资源(入打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有,此时若有其他进程请求该资源,则请求进程只能等待
3.1.2不可剥夺条件
进程所获取的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获取该资源的进程自己来释放(只能是主动释放)
3.1.3请求与保持条件
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获取的资源保持不放
3.1.4循环等待
存在一种进程资源的循环等待链,链中每一个进程已获取的资源同时被链中的下个进程所请求,即存在一个处于等待状态的进程集合{P1,P2,...Pn},其中Pi等待的资源被P(i+1)占有(i=0,1...),Pn等待的资源被P0占有,如图
死锁代码演示
public class DeadLock implements Runnable {
private static Object obj1 = new Object();//定义成静态变量,使线程可以共享实例
private static Object obj2 = new Object();//定义成静态变量,使线程可以共享实例
public int flag = 0;
public void run() {
if(flag == 0){
System.out.println("flag:"+flag);
synchronized (obj1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2){
System.out.println("flag:"+flag);
}
}
}
if(flag == 1){
System.out.println("flag:"+flag);
synchronized (obj2){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1){
System.out.println("flag:"+flag);
}
}
}
}
}
调用
public class DeadLockDemo {
public static void main(String[] args){
DeadLock deadLock1 = new DeadLock();
DeadLock deadLock2 = new DeadLock();
deadLock2.flag = 1;
Thread thread1 = new Thread(deadLock1);
Thread thread2 = new Thread(deadLock2);
thread1.start();
thread2.start();
}
}
执行效果:只打印两个flag值,表示死锁产生
3.2死锁处理
1.预防死锁:通过设置某些限制条件,去破坏产生死锁的四个必要条件的一个或几个条件,来预防死锁的发生
2.避免死锁:在资源的动态分配过程中,用某种方法来防止系统进入不安全状态,从而避免死锁的发生
3.检测死锁:允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除
4.解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来
3.2.1死锁预防
3.2.1.1破坏互斥条件
“互斥”条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件。
3.2.1.2破坏“占有并等待”条件
破坏“占有并等待”条件,就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源。即要想出一个办法,阻止进程在持有资源的同时申请其他资源。
- 方法一:一次性分配资源,即创建进程时,要求它申请所需的全部资源,系统或满足其所有要求,或什么也不给它。
- 方法二:要求每个进程提出新的资源申请前,释放它所占有的资源。这样,一个进程在需要资源S时,须先把它先前占有的资源R释放掉,然后才能提出对S的申请,即使它可能很快又要用到资源R。
3.2.1.3破坏“不可抢占”条件
破坏“不可抢占”条件就是允许对资源实行抢夺。
- 方法一:如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
- 方法二:如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另一个进程,要求它释放资源。只有在任意两个进程的优先级都不相同的条件下,方法二才能预防死锁。
3.2.1.4破坏“循环等待”条件
破坏“循环等待”条件的一种方法,是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。
3.2.2死锁避免
避免死锁不严格限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁
3.2.2.1有序资源分配法
该算法实现步骤如下:
- 必须为所有资源统一编号,例如打印机为1、传真机为2、磁盘为3等
- 同类资源必须一次申请完,例如打印机和传真机一般为同一个机器,必须同时申请
- 不同类资源必须按顺序申请
例如:有两个进程P1和P2,有两个资源R1和R2
P1请求资源:R1、R2
P2请求资源:R1、R2
这样就破坏了环路条件,避免了死锁的发生。
3.2.2.2银行家算法
银行家算法(Banker's Algorithm)是一个避免死锁(Deadlock)的著名算法,是由艾兹格·迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法。它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。流程图如下:
银行家算法的基本思想是分配资源之前,判断系统是否是安全的;若是,才分配。它是最具有代表性的避免死锁的算法。
设进程i提出请求REQUEST [i],则银行家算法按如下规则进行判断。
- 如果REQUEST [i]<= NEED[i,j],则转(2);否则,出错。
- 如果REQUEST [i]<= AVAILABLE[i],则转(3);否则,等待。
- 系统试探分配资源,修改相关数据:
AVAILABLE[i]-=REQUEST[i];//可用资源数-请求资源数
ALLOCATION[i]+=REQUEST[i];//已分配资源数+请求资源数
NEED[i]-=REQUEST[i];//需要资源数-请求资源数
4.系统执行安全性检查,如安全,则分配成立;否则试探险性分配作废,系统恢复原状,进程等待。
3.2.3顺序加锁
当多线程需要相同的一些锁,按照不同的顺序加锁,死锁就很容易发生
Thread 1:
lock A (when C locked)
lock B (when C locked)
wait for C
Thread 2:
wait for A
wait for B
lock C (when A locked)
如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。 例如以下两个线程就不会死锁
Thread 1:
lock A
lock B
lock C
Thread 2:
wait for A
wait for B
wait for C
按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要事先知道所有可能会用到的锁,但总有些时候是无法预知的,所以该种方式只适合特定场景。
3.4限时加锁
限时加锁是线程在尝试获取锁的时候加一个超时时间,若超过这个时间则放弃对该锁请求,并回退并释放所有已经获得的锁,然后等待一段随机的时间再重试
以下是一个例子,展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:
Thread 1 locks A
Thread 2 locks B
Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked
Thread 1’s lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.
Thread 2’s lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.
在上面的例子中,线程2比线程1早200毫秒进行重试加锁,因此它可以先成功地获取到两个锁。这时,线程1尝试获取锁A并且处于等待状态。当线程2结束时,线程1也可以顺利的获得这两个锁。
这种方式有两个缺点:
- 当线程数量少时,该种方式可避免死锁,但当线程数量过多,这些线程的加锁时限相同的概率就高很多,可能会导致超时后重试的死循环。
- Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。
3.5死锁检测
预防和避免死锁系统开销大且不能充分利用资源,更好的方法是不采取任何限制性措施,而是提供检测和解脱死锁的手段,这就是死锁检测和恢复。
死锁检测数据结构:
- E是现有资源向量(existing resource vector),代码每种已存在资源的总数
- A是可用资源向量(available resource vector),那么Ai表示当前可供使用的资源数(即没有被分配的资源)
- C是当前分配矩阵(current allocation matrix),C的第i行代表Pi当前所持有的每一种类型资源的资源数
- R是请求矩阵(request matrix),R的每一行代表P所需要的资源的数量
死锁检测步骤:
- 寻找一个没有结束标记的进程Pi,对于它而言R矩阵的第i行向量小于或等于A。
- 如果找到了这样一个进程,执行该进程,然后将C矩阵的第i行向量加到A中,标记该进程,并转到第1步
- 如果没有这样的进程,那么算法终止
- 算法结束时,所有没有标记过的进程都是死锁进程。
3.6死锁恢复
利用抢占恢复。
临时将某个资源从它的当前所属进程转移到另一个进程。
这种做法很可能需要人工干预,主要做法是否可行需取决于资源本身的特性。
利用回滚恢复
周期性的将进程的状态进行备份,当发现进程死锁后,根据备份将该进程复位到一个更早的,还没有取得所需的资源的状态,接着就把这些资源分配给其他死锁进程。
通过杀死进程恢复
最直接简单的方式就是杀死一个或若干个进程。
尽可能保证杀死的进程可以从头再来而不带来副作用。
4.线程通讯
4.1为什么要线程通讯
多线程并发执行时,在默认情况下CPU是随机切换线程的,有时我们希望CPU按我们的规律执行线程,此时就需要线程之间协调通信
4.2线程通讯方式
线程间通信常用方式如下:
- 休眠唤醒方式:
Object的wait、notify、notifyAll
Condition的await、signal、signalAll
- CountDownLatch:用于某个线程A等待若干个其他线程执行完之后,它才执行
- CyclicBarrier:一组线程等待至某个状态之后再全部同时执行
- Semaphore:用于控制对某组资源的访问权限
4.2.1休眠唤醒方式
Object的wait、notify、notifyAll
public class WaitNotifyRunnable{
private Object obj = new Object();
private Integer i=0;
public void odd() {
while(i<10){
synchronized (obj){
if(i%2 == 1){
System.out.println("奇数:"+i);
i++;
obj.notify();
} else {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public void even(){
while(i<10){
synchronized (obj){
if(i%2 == 0){
System.out.println("偶数:"+i);
i++;
obj.notify();
} else {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args){
final WaitNotifyRunnable runnable = new WaitNotifyRunnable();
Thread t1 = new Thread(new Runnable() {
public void run() {
runnable.odd();
}
}, "偶数线程");
Thread t2 = new Thread(new Runnable() {
public void run() {
runnable.even();
}
}, "奇数线程");
t1.start();
t2.start();
}
}
Condition的await、signal、signalAll
public class WaitNotifyRunnable{
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private Integer i=0;
public void odd() {
while(i<10){
lock.lock();
try{
if(i%2 == 1){
System.out.println("奇数:"+i);
i++;
condition.signal();
} else {
condition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public void even(){
while(i<10){
lock.lock();
try{
if(i%2 == 0){
System.out.println("偶数:"+i);
i++;
condition.signal();
} else {
condition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args){
final WaitNotifyRunnable runnable = new WaitNotifyRunnable();
Thread t1 = new Thread(new Runnable() {
public void run() {
runnable.odd();
}
}, "偶数线程");
Thread t2 = new Thread(new Runnable() {
public void run() {
runnable.even();
}
}, "奇数线程");
t1.start();
t2.start();
}
}
object和condition休眠唤醒区别
1.object wait()必须在synchronized(同步锁)下使用
2.object wait()必须要通过notify()方法进行唤醒
3.condition await()必须和lock(互斥锁、共享锁)配合使用
4.condition await()必须通过signal()方法进行唤醒
4.3CountDownLatch方式
CountDownLatch是在java1.5被引入的,存在于java.util.concurrent包下。
CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。
每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
示例代码:
public class CountDown {
private Integer i = 0;
private CountDownLatch countDownLatch = new CountDownLatch(1);
public void odd(){
while(i < 10){
if(i%2 == 1){
System.out.println("奇数:"+i);
i++;
countDownLatch.countDown();
} else {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void even(){
while(i < 10){
if(i%2 == 0){
System.out.println("偶数:"+i);
i++;
countDownLatch.countDown();
} else {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args){
final CountDown countDown = new CountDown();
Thread t1 = new Thread(new Runnable() {
public void run() {
countDown.odd();
}
},"奇数");
Thread t2 = new Thread(new Runnable() {
public void run() {
countDown.even();
}
},"偶数");
t1.start();
t2.start();
}
}
4.4 CyclicBarrier方式
CyclicBarrier是在java1.5被引入的,存在于java.util.concurrent包下。
CyclicBarrier实现让一组线程等待至某个状态之后再全部同时执行。
CyclicBarrier底层是
三个线程同时启动,示例代码如下:
public class CyclicBarrierDemo {
public static void main(String[] args){
final CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
new Thread(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+":准备...");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"启动完毕:"+new Date().getTime());
}
},"线程1").start();
new Thread(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+":准备...");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"启动完毕:"+new Date().getTime());
}
},"线程2").start();
new Thread(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+":准备...");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"启动完毕:"+new Date().getTime());
}
},"线程3").start();
}
}
4.5Semaphore方式
Semaphore是在java1.5被引入的,存在于java.util.concurrent包下。
Semaphore用于控制对某组资源的访问权限。
工人使用机器工作,示例代码如下:
public class SemaphoreDemo {
static class Machine implements Runnable{
private int num;
private Semaphore semaphore;
public Machine(int num, Semaphore semaphore) {
this.num = num;
this.semaphore = semaphore;
}
public void run() {
try {
semaphore.acquire();//请求机器
System.out.println("工人"+this.num+"请求机器,正在使用机器");
Thread.sleep(1000);
System.out.println("工人"+this.num+"使用完毕,已经释放机器");
semaphore.release();//释放机器
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args){
int worker = 8;//工人数
Semaphore semaphore = new Semaphore(3);//机器数
for (int i=0; i< worker; i++){
new Thread(new Machine(i, semaphore)).start();
}
}
}
总结
sleep和wait区别
wait和notify区别
1.wait和notify都是object的方法
2.wait和notify执行前都必须获取锁对象
3.wait的作用是使当前线程进行等待
4.notify的作用是通知其他等待当前线程的对象锁的线程