锁的优化升级过程~线程通信~死锁

一、锁的优化升级

1、锁为什么要进行优化升级?

Synchronized是重量级锁也是悲观锁,每次在进行锁的请求的时候,如果当前资源被其他线程占有,那么将当前的线程阻塞,加入到阻塞队列当中,然后清空当前线程的缓存,等到锁释放的时候,再使用notify或者notifyAll唤醒当前的线程,并让其处于就绪状态。这样线程之间的来回切换是非常消耗系统资源的,而Java的线程是映射到操作系统的原生线程之上的,每次线程的阻塞或者唤醒都要经过用户态到核心态或者核心态到用户态之间的切换,这样是十分浪费资源的,容易造成性能上的降低。因此自Java SE 1.6上的JVM对Synchronized进行优化,将Synchronized分为三种级别上的锁:偏向锁、轻量级锁、重量级锁。

在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。

2、锁的升级

  升级过程 适用场景
无锁状态    
偏向锁状态

一段代码一直被同一个线程所访问,为了降低线程获得锁的代价引入了偏向锁。

当一个线程获得偏向锁的时候,会将对象头和栈帧中的锁记录里存储锁偏向的线程ID,在后续的操作的时候,只需要简单的测试一下对象头中的Mark Word中是否存储着指向当前线程的偏向锁,如果测试成功则表示已经获取了锁。

适用于只有一个线程访问同步块场景
轻量级锁状态

当锁是偏向锁的时候,该段代码又被另一个线程所访问,那么偏向锁就会升级为轻量级锁。

线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word,然后线程使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程尝试使用自旋来获取锁。

轻量级锁所适应的场景是线程交替执行同步块的情况
重量级锁状态

当锁为轻量级锁的时候,另一个线程虽然是自旋,但是自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。

当锁升级为重量级锁的时候,其他任何线程试图获取锁的时候,都会被阻塞住,当持有锁的线程释放锁以后,会唤醒其他的线程,被唤醒的锁会进行新一轮的夺锁之争。代码同步的开始位置织入monitorenter,在结束同步的位置(正常结束和异常结束处)织入monitorexit指令实现

追求吞吐量,同步块执行速度较长

二、线程通信

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或者其余操作。

Java中,常用的实现等待/通知机制的两种方式是: 
1、在使用synchronized关键字实现的同步方法或同步代码块中,由被上锁的对象调用其自身的wait()方法和notify()方法以及notifyAll()方法,进行实现等待/通知机制; 
2、在使用ReentrantLock类的对象实现的同步方法或同步代码块中,使用Contion类的对象的await()方法和signal()方法以及signalAll()方法,进行实现等待/通知机制;

三、生产者—消费者模型

实际上准确的来说,应该是“生产者-消费者-仓库”模型,对于此模型,首先要明确以下四点:

1>生产者仅仅只是在仓库未满的情况下进行生产,当仓库满了的时候,停止生产。

2>消费者仅仅只是在仓库有货的情况下进行消费,当仓库空了的时候,停止消费。

3>当消费者发现仓库中没有货的时候,通知生产者进行生产。

4>当生产者生产出货的时候,通知消费者进行消费。

在生产者-消费者模式中:通常有两类线程,即若干个生产者的线程和若干个消费者的线程。生产者线程负责提交用户请求,消费者线程负责具体处理生产者提交的任务,在生产者和消费者之间通过共享内存缓存区(仓库)进行通信。

代码演示如下:

public class TestDemo{
    private ArrayList<Integer> list=new ArrayList<>();
    //生产者
    class produce implements Runnable{
        private  Random r=new Random();
        @Override
        public void run() {
            synchronized (list) {
            while (list.size() == 1) {
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                list.add(r.nextInt(10));
                System.out.println("生产产品成功" + list.toString());
                list.notify();
            }
        }
    }
    //消费者
    class consumer implements Runnable{
        private  Random r=new Random();
        @Override
        public void run() {
            synchronized (list){
            while (list.size() == 0) {
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                list.notify();
                System.out.println("消费产品成功" + list.remove(0));
            }
        }
    }
    public static void main(String[] args) {
        TestDemo t = new TestDemo();
         new Thread(t.new produce()).start();
         new Thread(t.new consumer()).start();
    }
 }

四、死锁

产生死锁的4个必要条件

1、互斥条件:进程对所分配到的资源不允许其他的进程访问,若其他进程请求访问该资源,只能等待,直到占有该资源的进程使用完后释放了该资源。

2、请求和保持条件:进程获得一定的资源之后,又对其他的资源发出强求,但是该资源可能被其他的进程所占有,此时情求阻塞,但是又不释放自身拥有的资源。

3、不可剥夺条件:是指进程对已经获得资源,在未完成之前,不可被剥夺,只能再使用完后自己释放。

4、环路等待条件:进程发生死锁之后,必然存在一个进程-资源之间的环形链,通俗点来讲,就是你登我的资源、我等你的资源,大家一直等。

死锁问题问题定位思路

1.首先需要确定java进程是否发生死锁
2.打开jvisualvm工具,专门分析JVMCPU,内存使用情况,以及线程的运行信息查看当前java进程各个线程运行的状态(颜色)
3.通过jvisualvm的线程dump或者jstack命令,把当前java进程所有线程的调用堆栈信息打印出来
4.分析main线程和子线程有没有关键短语:
    waiting for(资源地址)
   waiting to lock(资源地址)
5.看线程函数调用栈,定位到源码上,具体问题具体分析

猜你喜欢

转载自blog.csdn.net/qq_40303781/article/details/86505660
今日推荐