Java线程的同步与死锁

什么是多线程同步(管程)?
所谓的同步指的是所有线程不是一起进入到方法中执行,而是按照顺序一个一个进来。

多线程同步

1. synchronized对象锁处理同步问题

锁的对象是什么?
我们来看一个例子:取钱和查看密码。很明显这两个是操作不同的属性,所以是异步的。而取钱和存钱是同步的,查看密码和修改密码也是同步的。如果要用代码实现这个操作,怎么办?

class Account{
	double sal;
	String pass;
	synchronized quSal(){}
	synchronized cunSal(){}
	
	synchronized checkPass(){}
	synchronized modifyPass(){}
}

如果像上面这样采用同步方法加锁,取钱和存钱同步了,查看密码和修改密码也同步了,但是使用synchronized锁的是当前账户,那么当前账户所有的操作都被锁住了,存钱和查看密码就不再是异步而是同步,性能变低,锁的粒度太粗。
如果要把它变成异步,就要使用不同的锁,锁不同的对象。上面只有一把锁,锁了两个对象,现在要拆成两把锁,注意,任意对象都可以作为锁。

class Account{
	double sal;
	String pass;
	//锁sal属性
	private Object salLock = new Object();
	//锁pass属性
	private Object passLock = new Object();
	quSal(){
		synchronized(salLock){}
	}
	cunSal(){
		synchronized(salLock){}
	}
	
	checkPass(){
		synchronized(passLock){}
	}
	modifyPass(){
		synchronized(passLock){}
	}
}
1.1 synchronized的使用

使用synchronized关键字处理线程同步有两种模式:同步代码块和同步方法

  • 使用同步代码块
    • 锁的是任意对象,锁的是类的反射对象
class MyThread implements Runnable {
    private int ticket = 1000;
    @Override
    public void run() {
       for (int i=0;i<1000;i++){
           synchronized (this){
               if (this.ticket>0){
                   try {
                       Thread.sleep(20);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println(Thread.currentThread().getName()+",还有"+this.ticket--+"张票");
               }
           }
       }
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
       MyThread mt = new MyThread();
       Thread t1 = new Thread(mt,"黄牛A");
       Thread t2 = new Thread(mt,"黄牛B");
       Thread t3 = new Thread(mt,"黄牛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--+"张票");
        }
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
       MyThread mt = new MyThread();
       Thread t1 = new Thread(mt,"黄牛A");
       Thread t2 = new Thread(mt,"黄牛B");
       Thread t3 = new Thread(mt,"黄牛C");
       t1.setPriority(Thread.MIN_PRIORITY);
       t2.setPriority(Thread.MAX_PRIORITY);
       t3.setPriority(Thread.MAX_PRIORITY);
       t1.start();
       t2.start();
       t3.start();
    }
}
1.2 synchronized的实现原理

对象锁Monitor机制
Java中所有类的对象都有对象监视器(Monitor),获取一个对象的锁实际就是获取该对象的Monitor。(获取的过程是互斥的,即同一时刻只有一个线程能获取到Monitor,从而进入同步代码块或同步方法中)
当一个线程尝试获取对象的Monitor时:
1)若此时Monitor值为0,表示此对象Monitor未被任何线程持有,当前线程进入同步块,并且将Monitor持有线程置为当前线程,Monitor值加1.
2)若此时Monitor值不为0,并且持有线程不是当前线程,当前线程等待Monitor值减为0.
3)若此时Monitor值不为0,但是持有线程为当前线程,当前线程再次进入同步块,Monitor值再次加1(可重入锁)。

之所以采用这种计数器的方式,是为了允许同一个线程重复获取同一把锁。

1.3 JDK1.6对synchronized进行优化

JDK1.5中,synchronized的性能比较低,因为这是一个重量级操作,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力,相比使用Java提供的Lock对象,性能更高一些。
JDK1.6之后,对synchronized进行了优化,导致synchronized的性能并不比Lock差。

对synchronized锁进行优化,也就是对获取锁的时间进行优化。(锁的优化也就是锁的几种状态)

1.3.1 CAS操作

使用锁时,线程获取锁是一种悲观锁的策略,即假设每一次执行临界区的代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS,又称无锁操作,是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突也就不会阻塞其它线程的操作。

无锁操作是使用CAS(Compare and Swap)又叫比较交换来鉴别线程是否发生冲突,出现冲突就重试当前操作知道没有冲突为止。
CAS(V,O,N) 中的三个值分别为:

  • V:主内存中存放的实际值
  • O:当前线程认为主内存的值
  • N:希望更新的值

当V和O相同时,表明该值没有被其他线程更改过,可以将N赋值给V,反之V和O不相同,表明该值已经被其他线程改过了,所以不能将N赋值给V,返回V即可。当多个线程使用CAS操作一个变量时,只有一个线程会成功,并且成功更新,其余将会失败。失败的线程会重新尝试,也可以选择挂起线程。

synchronized和CAS的主要区别:
synchronized未优化前,在存在线程竞争的情况下,会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步),而CAS并不是直接把线程挂起,当CAS操作失败后会进行一定的尝试,因此也叫非阻塞同步。

CAS问题:
1)ABA问题:因为CAS会检查O有没有变化,如果O由A变成了B,再变成了A,而CAS在检查的时候发现O并没有变化依然为A,但实际上却发生了变化。JDK1.5后的atomic包提供了AtomicStampedReference来解决ABA问题。(添加版本号)
2)自旋浪费大量的处理器资源
阻塞是线程停止运行,而自旋是线程仍处于运行状态,只不过跑的都是无用指令。它期望在运行无用指令的过程中,锁能被释放出来。(阻塞相当于熄火停车,自旋相当于怠速停车)但是JVM无法根据等待时间的长短来选择是自旋还是阻塞,JVM给出的方案是自适应自旋,根据以往自旋等待时能否获取锁,来动态调整自旋的时间。
3)自旋还有一个副作用,不公平锁机制。处于阻塞状态的线程,无法立刻竞争被释放的锁,然而处于阻塞状态的线程,很有可能优先获得这把锁。

锁的四种状态:无锁、偏向锁、轻量级锁、重量级锁
这几个状态会随着竞争情况逐渐升级,但不能降级,目的是为了提高获得锁和释放锁的效率。

  • 偏向锁
    偏向锁(乐观锁):JDK1.6之后synchronized默认的锁——任意时刻只有一个线程请求某一把锁。此时只有一个线程在来回尝试获取锁,直接将加锁和解锁的过程都免了,只是简单判断下是否是同一个线程在获取锁,若是直接进入同步块(临界区)。就相当于你在你的私人庄园里装了一个红绿灯,并且庄园里只有你在开车,偏向锁就是在红绿灯处识别车牌号,如果识别到你的车牌号,直接亮绿灯。
    当不同时刻有不同的线程尝试获取锁时,偏向锁会膨胀为轻量级锁。

偏向锁等到竞争才释放锁,当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

  • 轻量级锁
    轻量级锁:在不同时刻有不同线程尝试获取锁。每次获取锁均需要加锁与解锁。就相当于深夜的十字路口,四个方向都闪黄灯。由于来往车辆少,如果红绿灯交替,可能出现四个方向只有一辆车在等红灯。闪黄灯代表车辆可以自由通过,但司机需要观察。
    当同一时刻有多个线程尝试获取锁时,轻量级锁会膨胀为重量级锁。

  • 重量级锁
    重量级锁(悲观锁):JDK 1.6之前synchronized的锁,在这种状态下,JVM会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。
    关于重量级锁的自适应自旋:
    获取重量级锁失败的线程不是立即进入阻塞态,而是自旋一段时间,若在此时间内成功获取到锁,则在下次等待时适当延长自旋时间,否则适当缩短自旋时间。

1.3.2 其他优化
  • 锁粗化
    将多个连续的加锁与解锁过程粗化为一次范围大的加锁与解锁,减少因为加减锁带来的CPU开销。
	static StringBuffer sb = new StringBuffer();
    public static void main(String[] args) {
        sb.append("hello");
        sb.append("bit");
        sb.append("hello");
    }

上面这段代码中每调用一次append()方法,都伴随着一次加锁和解锁,锁粗化就是在第一次调用append()方法的时候进行加锁,而在最后一个append()方法执行完才解锁。

  • 锁消除
    在不会出现锁竞争的场景,会将线程安全的集合或类中的锁取消。
    public static void main(String[] args) {
        StringBuffer sb = new StringBuffer();
        sb.append("hello");
        sb.append("bit");
        sb.append("hello");
    }

这段代码将StringBuffer的创建放到了方法里边,这时多线程并不会访问到sb,只有主线程可以,因为它在自己的工作内存中,隔离了,不会出现多线程竞争问题,如果再进行加减锁就没有必要,JVM就会将锁取消。

2、死锁

死锁:程序“卡死”,以下四个条件同时满足,会造成程序死锁。

  • 互斥:共享资源只能被一个线程占用。
  • 占有且等待:线程A已经取得共享资源X,在等待获取资源Y时,不释放X。
  • 不可抢占:线程A已经获取X之后,其他线程不能强行抢占X。
  • 循环等待:线程A占用X,线程B占用Y,A等待Y,B等待X。

jps命令—查看jvm正在跑的线程

如何解决死锁问题: 只要破坏任何一个条件即可解决。

class Pen {
}

class Book {
}

public class DeadLockTest {
    public static void main(String[] args) {
        Pen pen = new Pen();
        Book book = new Book();
        Thread penThread = new Thread(() -> {
            synchronized (pen) {
                System.out.println("我有笔,需要本子");
                synchronized (book) {
                    System.out.println("笔线程同时获取到笔与本子");
                }
            }
        });
        Thread bookThread = new Thread(() -> {
            synchronized (book) {
                System.out.println("我有本子,需要笔");
                synchronized (pen) {
                    System.out.println("本子线程同时获取本子与笔");
                }
            }
        });
        penThread.start();
        bookThread.start();
    }
}

由于synchronized这种内建锁在解决死锁问题时比较麻烦,所以就产生了Lock体系来解决死锁问题。(性能不是Lock体系产生的原因)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值