目录
管程
临界区
竞态条件
案例
通过synchronized阻塞解决
synchronized添加位置
设计模式之保护性暂停
Join原理
修改线程状态的几种方法
单向改变不可返回的状态
双向可改变的状态
多把锁
线程活跃性
死锁
定位死锁
活锁
饥饿
ReentrantLock
可重入
可打断
锁超时
固定顺序运行线程
管程
所谓管程:指的是管理共享变量以及对共享变量的操作过程,让它们支持并发。翻译为 Java 就是管理类的成员变量和成员方法,让这个类是线程安全的。
临界区
一段代码块中如果存在对共享资源的多线程读写操作,那么称这段代码块为临界区。
竞态条件
多个线程在临界区执行,由于代码的执行顺序不同导致结果无法预测。称之为发生了竞态条件。
案例
在操作系统中CPU使用的是分片来决定线程的执行,这样会进行线程的上下文切换。
以一个自增的例子来讲,它并不是一个原子操作,在字节码文件中分为了4步。首先是读取静态变量的值i,将i存进本地变量表,对i进行自增操作,将i自增结果返回静态变量。
那么就存在一个问题,当线程执行完自增后,还未将结果返回,但是时间片结束后静态变量值并没有发生改变,此时去执行其他线程,那么其他线程对i值又进行一次自增操作,并将结果返回后,再次回到未执行完的线程,此时执行了两次自增,但是得到的结果为自增一次的结果。其运行视图参考下图。
以上问题的发生是因为线程对共享资源的读写时指令操作交错导致的。
通过synchronized阻塞解决
通过互斥,在同一时刻中只有一个线程获取一把锁,其他线程获取时,会进入阻塞状态从而保证持有锁的线程安全执行临界区的代码,不用担心线程上下文的问题。
synchronized添加位置
添加在代码中,根据对谁添加锁取决于传入的参数
synchronized(){
}
添加在方法上,锁住的是this
public void synchronized a(){
}等价于
synchronized(this){
}
如果是静态方法,锁住的是类对象,与非静态方法锁住的不是同一个对象
设计模式之保护性暂停
线程与线程之间的通信,可以通过第三方类来实现,两个线程监听同一个类中的response属性,一个线程设值一个线程取值,类似于消息队列。
public class demo3 {public static void main(String[] args) {Result result = new Result();new Thread(()->{System.out.println(LocalDateTime.now()+"获取消息" );System.out.println(result.getResponse());System.out.println(LocalDateTime.now());},"t1").start();new Thread(()->{try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}result.setResponse("发送消息");},"t2").start();}
}class Result{Object response;public Object getResponse() {synchronized (this){while(response==null){try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}}return response;}public void setResponse(Object response) {synchronized (this){this.response = response;this.notifyAll();}}
}
Join原理
默认传参为0,即永久等待,如果设置了最大等待时间,那么截取开始时间,减去结束时间,与参数相比较,wait()方法内时间应设置为当前剩余时间。
join与保护性暂停区别在于,join是等待线程结束,保护性暂停是等待结果返回。
修改线程状态的几种方法
单向改变不可返回的状态
创建出线程调用start方法,从new状态转化为runnable状态。
线程运行结束,从runnable转化为terminated状态。
双向可改变的状态
wait():将runnable状态转化为waiting状态。通过notify()方法将其转化为runnable(如果争抢到锁的话)或blocked状态(被唤醒但是没有争抢到锁)。
join():需要等待某个线程运行结束后才会接着执行,需要等待的线程将会进入waiting状态。
pack():将runnable状态转化为waiting状态。通过interrupt()转化为runnable。
synchronized():如果竞争锁失败,将runnable转化为blocked状态。
多把锁
当只有一把锁时,并发度低,等于串行。为了提高并发度,可以将锁进行粒度细分。比如,一个房子,可以用来睡觉和学习,如果把房子作为锁对象,那么有人睡觉的时候就不能有人学习。将房子细分为卧室与书房,作为两个锁对象。睡觉的人与学习的人各持一把锁,提高并发度。
优点:增强并发度
缺点:当存在多把锁时,可能会存在死锁的情况。
线程活跃性
死锁
t1线程需要获取锁A,接下来要获取锁B。但是此时线程t2正在持有锁B,那么t1获取锁B失败,进入阻塞队列等待锁B的释放,t2此时需要获取锁A,但是锁A由t1持有,t2获取不到,因此也进入阻塞队列,导致死锁的情况发生
定位死锁
使用jconsole工具或jps锁定id再用jstack定位。
活锁
不断修改对方的结束条件导致无法结束线程。比如说线程t1执行count--。t2线程执行count++。t1的结束条件是while(count<0)。t2的结束条件是while(count>20)。导致t1与t2都无法结束线程。
解决方案:对两个线程设置随机的睡眠时间,使其变成两个交错运行的线程。
饥饿
由于线程优先级别太低而得不到CPU执行调度导致无法结束线程。
ReentrantLock
可重入锁。相对于synchronized作比较ReentrantLock具有以下特点
- 可中断
- 可设置超时时间
- 可设置为公平锁
- 支持多个条件变量
但是这两种都支持可重入
//获取锁
reentrantLock.lock()
try{//临界区
}finally{reentrantLock.unlock()
}
可重入
如果某线程已经获取到A对象的锁,在后续代码中,不释放A锁的情况下再次获取A锁,那么是可以的。
如果是不可重入锁即使已经拿到了A对象的锁也无法接着获取A锁。
可打断
此时不可以使用reentrant.lock()方法,而是使用reentrant.lockInterruputibly()方法来进行加锁,如果没有竞争,那么可以直接获取到锁。如果存在竞争,则会进入阻塞队列,在阻塞过程中,如果被interruput()打断,则会抛出异常。
public static void main(String[] args){ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(()->{try{//尝试去获取锁System.out.println("尝试获取锁");lock.lockInteruputibly();}catch(Exception e){System.out.println("获取锁失败");return;}try{System.out.println("获取锁成功");}finally{lock.unlock();}},"t1");//主线程加锁.抢占锁,导致t1抢锁失败。lock.lock();t1.start();sleep(1);System.out.println("打断t1");t1.interuput();
}
运行结果如下:
尝试获取锁
打断t1
获取锁失败
锁超时
使用tryLock()。如果规定时间没有获取到锁,可以选择主动结束来避免死锁
public static void main(){ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(()->{if(!lock.tryLock()){System.out.println("获取锁失败,返回");return;}try{System.out.println("获取锁成功");}finally{lock.unLock();}},"t1");lock.lock();System.out.println("主线程加锁");t1.start();
}
运行结果如下:
主线程加锁
获取锁失败,返回
tryLock()方法也是可以被打断后抛出异常。
固定顺序运行线程
对于多个线程情况下想要控制线程的运行先后顺序。
可以通过一个静态布尔类型的变量runned来控制默认值为false,然后第一个要优先运行的线程需要将其设置为true表示已经运行过了,如果其他线程要优于第一个线程启动的话,要对runned值进行判断,如果为false则进入wait队列。等待第一个要启动的线程执行结束后唤醒其他线程。
另一种方式是通过pack与unpack方法,第一个要启动的线程进行unpack第二个线程则是在执行前进行pack操作。对于多线程情况下,将所有线程进行pack后,主线程主动将第一个线程unpark后,将后续的线程对象进行unpark(Thread)。