2.8.6 并发错误
2.8.6.1 概念
并发错误:多个线程共享操作同一个对象的时候,线程体当中连续的多行操作未必能够连续执行 很可能操作只完成了一部分,时间片突然耗尽,此时,另一个线程抢到时间片,直接拿走并访问了操作不完整的数据(操作不完整的数据,从逻辑上讲是错误数据)
根本原因:多个线程共享操作同一份数据
直接原因:线程体当中连续的多行语句,未必能够连续执行,很可能操作只完成了一半 时间片突然耗尽
此时另一个线程刚好抢到时间片,直接拿走了操作不完整的数据 - 错误数据
导火索:时间片突然耗尽
面试题: 并发错误和并发修改异常什么关系?
并发修改异常:是为了避免出现并发错误,而主动做的校验机制,当迭代器发现自己理想的modCount与集合反馈的modCount
不一致的时候,会认为有别的线程在同时操作这个集合,于是主动throw的异常 ConcurrentModificationException
2.8.6.2 解决并发错误
加锁:
第一种语法级别的加锁 = 互斥锁
-
使用synchronized修饰符
互斥锁=互斥标记=锁标记=锁旗标=监视器=Monitor
用法:
-
修饰代码块
synchronized(临界资源){ 需要连续执行的操作1; 需要连续执行的操作2; ···············; }
-
修饰整个方法
public synchronized void add(){ } //等价于 public void add(){ synchronized(){ } }//都是对对象加锁
注意:即便synchronized加在方法上,其实还是对对象进行加锁,而且锁的是调用方法的那个对象…
Java世界里只有每个对象才有锁标记,所以加锁只能对对象加锁。
*:Vector Hashtable StringBuffer之所以线程安全,是因为底层大量方法都使用了synchronized修饰的
*:单例模式的懒汉式,需要synchronized修饰那个getter方法
public static synchronized Sun getInstance(){ return x; }
*: synchronized有什么特性?它不能被子类方法继承得到
父类当中线程安全的方法,当子类继承得到的时候,就没有synchronized修饰了,必须重写(覆盖)*:如果synchronized修饰静态方法,等价于对这个类的.class加锁(其实是对这个类的元对象加锁)
-
-
第二种面向对象思想的加锁 = 可重入锁
java.util.concurrent.locks.ReentrantLock(jdk 5.0开始):java包的工具包的并发包的 可重入锁
ReentrantLock :lock(加锁) unlock(解锁):放在finally{}中
另外 可重入锁的构造方法可以传参指定
公平锁 或 非公平锁 默认非公平锁*:JDK6.0之前这个Lock的机制比synchronized效率高很多
JDK6.0开始 重新对synchronized修改了底层实现,加入了一堆新的概念 (偏向锁 轻量级锁 锁的自旋机制)
从JDK6.0开始 synchronized 跟 Lock性能上不相上下*:ReentrantLock可以在构造方法中传公平锁和非公平锁(公平与否针对第一个先来的线程而言)
公平锁:new Reetrantlock(true);
解决并发错误案例
import java.util.concurrent.*; public class TestCurrentError{ public static void main(String[] args){ Student stu=new Student("zml","女士"); Lock lock=new ReentrantLock(); Print p=new Print(stu); Change c=new Change(stu,lock); p.start(); c.start(); } } class Change extends Thread{ Student stu; Lock lock; public Change(Student stu,Lock lock){ this.stu=stu; this.lock=lock; } @Override public void run(){ boolean isOkay=true; while(true){ //synchronized(stu){ try{ lock.lock();//ReentrantLock加锁 if(isOkay){ stu.name="梁朝伟"; stu.gender="男士"; }else{ stu.name="张曼玉"; stu.gender="女士"; } isOkay=!isOkay; }finally{ lock.unlock();//解锁 } } //} } } class Print extends Thread{ Student stu; public Print(Student stu){ this.stu=stu; } @Override public void run(){ while(true){ synchronized(stu){//synchronized实现加锁 System.out.println(stu.name+":"+stu.gender);} } } } class Student{ String name; String gender; public Student(String name,String gender){ this.name=name; this.gender=gender; } }
2.8.7 死锁
互斥锁标记使用过多、或者使用不当,就会造成多个线程相互持有对方想要的资源不释放的情况下
又去申请对方已经持有的资源,从而双双进入阻塞死锁经典案例:中美联合国饿死事件
public class TestDeadLock{ public static void main(String[] args){ Resturant r=new Resturant(); Resturant.Chinese c=r.new Chinese(); Resturant.American a=r.new American(); c.start(); a.start(); }} class Resturant{ Object knife=new Object(); Object chopsticks=new Object(); class Chinese extends Thread{ @Override public void run(){ System.out.println("中国人进入了餐厅"); synchronized(knife){ System.out.println("中国人拿了刀具"); try{Thread.sleep(100);}catch(Exception e){e.printStackTrace();} synchronized(chopsticks){ System.out.println("中国人拿到了筷子"); } } System.out.println("中国人可以正常吃面条了"); } } class American extends Thread{ @Override public void run(){ System.out.println("美国人进入了餐厅"); synchronized(chopsticks){ System.out.println("美国人拿了筷子"); try{Thread.sleep(100);}catch(Exception e){e.printStackTrace();} synchronized(knife){ System.out.println("美国人拿到了刀具"); } } System.out.println("美国人可以正常吃牛排了"); } } }
2.8.8 如何解决死锁问题
一块空间:对象的等待池
三个方法:Object类的
wait():让当前线程释放对象的锁标记,并且进入调用方法的那个对象的等待池
notify(): 从调用方法的那个对象的等待池当中,随机的唤醒一个线程
notifyAll():从调用方法的那个对象的等待池当中,唤醒所有阻塞的线程
*:这三个方法都是Object类的方法,不是线程的方法,每个对象都有等待池,每个对象都可能操作等待池
*:这三个方法都必须在持有锁标记的前提下才能使用,所以它们必须出现在synchronized的{}当中,如果没有拿到对象的锁标记 就直接操作等待池,不但会操作失败,还会引发运行时异常illegalMonitorStateException
锁池和等待池的概念
在Java中,每个对象都有两个池,锁(monitor)池和等待池
锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.
等待池与锁池的区别?
都是java当中每个对象都有一份的空间,而且都是存放线程对象的
锁池:存放想要拿到对象的锁标记,但是还没得到锁标记的线程
等待池:原本已经拿到锁标记,又不想跟别人形成相互制约,于是又主动释放锁标记的线程
三点区别:
进入的时候是否需要释放资源:锁池不需要释放;等待池需要
离开的时候是否需要调用方法:离开锁池不需要操作;离开等待池需要notify()/noyifyAll()
离开之后去到什么状态:离开锁池直接返回就绪;离开等待池直奔锁池
*:利用wait()和notify()实现两个线程交替执行
public class TestSwitchThread{
public static void main(String[] args){
Right r=new Right();
Left l=new Left(r);
l.start();
}
}
class X {
static Object obj=new Object();//定义一个锁机制(锁对象)
}
class Left extends Thread{
Right r=new Right();
public Left(Right r){//利用传参方式实现数据共享
this.r=r;
}
@Override
public void run(){
synchronized(X.obj){
r.start();//在左脚拿到锁拥有权时启动右脚线程【此时右脚总会在左脚之后执行操作,就不会出现卡死现象】
for(int i=0;i<1000;i++){
System.out.println("左脚");//1
try{X.obj.wait();}catch(Exception e){e.printStackTrace();}//2左脚等待,右脚执行
X.obj.notify();//6左脚执行完毕,通知右脚进入锁池
}
}
}
}
class Right extends Thread{
@Override
public void run(){
synchronized(X.obj){
for(int i=0;i<1000;i++){
System.out.println(" 右脚");//3
X.obj.notify();//4右脚执行完毕之后,提醒正在等待池的左脚进入锁池
try{X.obj.wait();}catch(Exception e){e.printStackTrace();}//5右脚等待,左脚执行
}
}
}
}
感谢您的浏览与点赞,让我们一起快乐学java!