线程的同步与死锁
1.线程互斥
多线程编程三大问题:
分工:
同步:多个线程同时做一件事(线程间通信:线程合作)
互斥:在多线程并发时,某一时刻只能由一个线程访问共享资源
锁:一个锁只能保护一个对象(相应对象/资源),不同锁保护的不同的对象/资源
线程安全问题:多线程访问了共享的数据,会产生线程安全问题。
单线程程序不会出现线程安全问题。多线程程序没有访问共享数据,不会产生线程安全问题。
解决办法:让一个线程在访问共享数据的时候无论是否失去了CPU的执行权,让其他的线程只能等待。等待当前线程完成,再让其他线程进入。保证使用只有一个线程在工作。
1.1 线程安全问题的解决办法:即锁的实现方式:
使用synchroinzed
关键字为程序逻辑上锁
synchroinzed
(锁的对象| Object及其子类| 类对象)
使用synchchroinzed
两种方法:
1.1.1同步代码块
使用同步代码块,需要设定锁的对象: this,任意一个Object子类对象 ,当前类.class(当前类的反射对象)
synchronized(锁的对象){
//此处代码块在任意一个时刻只能有一个线程进入
}
package ThreadTest.synchronizedTest;
/**
* @Name: 同步处理
* @Author:ZYJ
* @Date:2019-05-29-15:02
* @Description:
*/
public class synchronizedDemo {
public static void main(String[] args) {
MyThread myThead = new MyThread();
Thread t1 = new Thread(myThead,"黄牛A");
Thread t2 = new Thread(myThead,"黄牛B");
Thread t3 = new Thread(myThead,"黄牛C");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t3.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
t3.start();
}
}
class MyThread implements Runnable{
private int ticket =30;
@Override
public void run() {
for(int i=0;i<20;i++){
//在同一时刻,只允许一个线程进入代码块处理
synchronized (this){
if(this.ticket>0){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+
",还有"+this.ticket--+"张票");
}
}
}
}
}
1.1.2 同步方法
不需要手动上锁,解锁
修饰普通的对象方法,锁当前对象 this
修饰类的静态方法, 锁当前类.class (本类的class属性)
直接在方法声明上使用sychronized
,此时表示同步方法在任意时刻只能有一个线程进入。
public synchronized void sellTicket() :默认锁的是this
package ThreadTest.synchronizedTest;
/**
* @Name: 锁的实现 同步方法
* @Author:ZYJ
* @Date:2019-05-29-16:07
* @Description:
*/
public class tongbuDemo {
public static void main(String[] args) {
myThread m = new myThread();
Thread t1= new Thread(m,"黄牛A");
Thread t2= new Thread(m,"黄牛B");
Thread t3= new Thread(m,"黄牛C");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t3.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
t3.start();
}
}
class myThread implements Runnable{
private int ticket=1000;
@Override
public void run() {
for(int i=0;i<1000;i++){
this.sale();
}
}
//同步方法
public synchronized void sale() {
if (this.ticket > 0) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "还有" + this.ticket-- + "张票");
}
}
}
sychronized
**对象锁:**锁哪个对象:
若sychronized
修饰的时候静态方法或sychronized
(类名称.class)sychronized
锁的都是当前类的反射对象(全局唯一); 当同步方法为静态时,上诉属性 ticket也要为静态,这时所得对象不再是this ,而是当前类对象:即 myThread.class
类名称.class:d=反射对象 反射对象有且只有一个
1.1.3 LOCK锁(JDK1.5提供 基于java语言层面 的“锁”)
见下一篇
1.1.4总结
synchronized使用:
1,同步代码块
synchronized(this|任意一个Object子类对象|当前类.class) {
}
2,同步方法
修饰普通对象方法 锁当前对象this
修饰类的静态方法 锁当前类.class
保护的是什么?几个锁?
使用一把锁锁住了两个毫无关系的对象
class Account {
// 余额
int sal;
// 密码
String password;
// 余额资源的锁
private Object salLock = new Object();
// 密码资源的锁
private Object passLock = new Object();
public int getMoney() {
synchorinzed(salLock) {}
}
public void setMoney() {
synchorinzed(salLock) {}
}
public String getPassword() {
synchorinzed(passLock) {}
}
public void setPassword() {
synchorinzed(passLock) {}
}
}
如何保护毫无关系的资源?
使用多把锁锁住不同的资源
转账
A -> B 100
A -= 100;
B += 100;
如何保护有关联关系的对象:
使用同一把锁
public void zhuangzhang(Account target) {
synchroinzed(Account.class) {
this.sal -= 100;
target.sal += 100;
}
}
由于转账涉及两个账户间的sal操作,因此需要将两个账户同时锁定。
由于方法的synchrond只能锁一个对象,因此锁不住转账操作
2. synchronized
底层实现
在使用synchronized
时,必须保证锁定对象必须为Object及其子类对象。synchronized
使用的是JVM层级别的MoniteratioEnter
与MoniterExit
实现。
这两个指令都必须获取对象的同步监视器Monitor
2.1 对象锁Monitor(监视器)机制
执行同步代码块后首先要先执行monitorenter
指令,退出时执行monitorexit
指令。分析可得,使用synchronized进行同步, 其关键就是必须对对象的监视器monitor进行获取 ,当线程获取monitor后才能继续往下执行,否则只能等待。这个获取monitor的过程是互斥的,即同一时刻只有一个线程能够获取到monitor
程序中一般包含一个monitorenter
指令和多个monitorexit
指令。那是因为JVM需要需要保证所获得的锁 在正常的执行路径和异常执行路径上,都能够被解锁。所以会在异常体系中是释放锁,保证程序异常时,也能有释放锁。
但应synchronized标记方法时,字节码中的方法访问标记包括 ACC_SYNCHRONIZED.该标记表示,在进入该方法时,java虚拟机需要进行monitorenter
操作。而在退出该方法时,不管是正常返回,还是向调用者抛异常,JVM均需要进行monitorexit
操作。
monitorenter
和monitorexit
操作所对应的锁对象都是隐式的。对于实例方法来说,这两个操作对应的锁对象是this,对于静态方法来说,这两个操作对应的锁对象则是所在类Class实例。
MoniteratioEnter
:
检查锁定的对象的计数器是否为0,若为0表示此监视器还未被任意一个线程获取,此时线程可以进入同步代码块并且将Monitor值+1,将Monitor的持有线程标记为当前线程
当Monitor
计数器不为0,且持有线程不是当前线程,表示Monitor监视器吗已经被别的线程占用,当前线程只能阻塞等待。
当Monitor
计数器0,但持有线程恰好为当前线程
**可重入锁:**当执行MoniteratioEnter
使。对象的Monitor计数器值不为0,但是持有线程恰好是当前线程,此时将Monitor计数器值再次+1,当前线程继续进入同步方法或代码块。
//线程2
synchronized(0){
//线程1
}
Monitorexit
Monitor计数器值-
2.2 JDK1.6之后对于synchronized优化
synchronized:互斥
优化让每个线程通过同步代码块时速度提高
之前synchronized获取锁失败,将线程挂起-悲观锁策略
2.2.1 **CAS操作 **(非阻塞同步)
比较交换鉴别线程(无锁实现的同步-乐观锁)-自旋(循环)
CompareAndSwap(O,V,N)
O:当前线程存储的变量值
V:内存中该变量的具体值
N: 希望修改后的变量值
当O==V时,此时表示还没有线程修改共享变量的值,此时可以成功的将内存中的值修改为N
当O!==V时,表示此时内存中的共享变量值已被其他线程修改,此时返回内存中的最新值V,再次尝试修改变量。
CAS的操作需要硬件指令集的支持,在JDK1.5之后才可以使用处理器提供的CMPXCHG指令实现
线程挂起阻塞:车熄火
自旋:脚踩刹车,车不熄火
2.2.2 自旋出现的问题:
1.ABA问题 :
**解决ABA问题:**添加版本号 JDK1.5 之后atomic包中提供了AtomicStampedReference
解决该问题。
2.自旋在CPU上跑的无用指令,会浪费CPU资源
解决办法:自适应自旋:JVM尝试自旋一段时间,若在此时间内,线程成功获取到锁,再下次获取锁时, 适当延长自旋时间。若在此时间内,线程没有获取到锁,则下次获取锁时,适当缩短自旋时间。
3.公平性问题
不公平的锁机制:处于阻塞状态的线程,无法立刻竞争被释放的锁,而处于自旋状态的线程则很有可能优先获得这把锁。
**解决办法:**Lock锁可以实现公平性,synchronized无法实现公平锁。
2.3 JVM 中synchronizationized实现中锁的代价
2.3.1 偏向锁:
JDK1.6之后默认synchronized。最乐观的锁:进入同步块或同步方法的始终是一个线程,不加锁直接过;当出现另一个线程也尝试获取锁(不同时刻)时,偏向锁会升级为轻量级锁。
2.3.2轻量级锁:
多个线程在不同的时间段请求同一把锁,即没有锁的竞争。
2.3.3重量级锁:
JDK1.6之前synchronized都是重量级锁,将线程阻塞挂起(JDK1.6自适应自旋)。
2.3.4总结:
1.重量级锁会阻塞、唤醒请求加锁的线程。它针对的多个线程同时竞争一把锁的情况。JVM采用了自适应自旋,来避免线程在面对非常小的synchronized代码块时,仍会被阻塞、唤醒的情况
2.轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储的锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
3.偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。
**锁粗化:**当出现多次连续的加锁与解锁过程,会将多次加减锁过程粗话为一次加解锁过程
**锁消除:**当对象不属于共享资源时,对象内部的同步方法或同步代码块的锁会被自动解除
2.4 死锁
以下四种情况同时发生才会出现死锁
**1.互斥:**共享资源只能同时被一个线程占用
**2.占用且等待:**拿到了worker锁,不释放但去申请money锁;
3.不可抢占:线程Thread1拿到对象锁X之后,其他线程无法强行抢占X锁。
**4.循环等待/(循环占有):**线程T1拿到了资源X的锁,去申请Y的锁;
线程T2拿到了资源Y的锁,去申请X的锁;
package ThreadClass.DeadLock;
/**
* @Name: 模拟死锁
* @Author:ZYJ
* @Date:2019-06-03-20:27
* @Description:
*/
public class TestDemo {
private static Pen pen = new Pen();
private static Book book =new Book();
public static void main(String[] args) {
new TestDemo().deadLock();
}
public void deadLock(){
//使用匿名
new Thread(new Runnable() {
@Override
public void run() {
synchronized (pen){
System.out.println(Thread.currentThread()+":我有笔,我就不给你");
synchronized (book){
System.out.println(Thread.currentThread()+":把你的本给我!");
}
}
}
},"Pen").start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (book){
System.out.println(Thread.currentThread()+":我有本子,我就不给你!");
synchronized (pen){
System.out.println(Thread.currentThread()+":把你的笔给给我");
}
}
}
},"Book").start();
}
}
class Pen{
private String pen="笔";
public String getPen() {
return pen;
}
}
class Book{
private String book="书";
public String getBook() {
return book;
}
}
死锁一旦出现之后,整个程序就会中断执行,所以死锁属于严重性问题
解决:破坏其中一个 互斥不能破坏,不可抢占不可解决,占有且等待不可解决。