Java基础常见面试题——锁

1.synchronized锁实现原理? (Lock)和 (synchronized)两种锁区别?

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
synchronized是用java的monitor机制来实现的,就是synchronized代码块或者方法进入及退出的时候会生成monitorenter跟monitorexit两条命令。线程执行到monitorenter时会尝试获取对象所对应的monitor所有权,即尝试获取的对象的锁;monitorexit即为释放锁。
Synchronized是java语言中的一个重量级的操作,因为java线程是映射到操作系统的原生线程上的,阻塞或者唤醒一条线程,都需要操作系统来帮忙完成,需要从用户态切换到核心态,转换需要消耗很多处理时间,可能比用户代码执行的时间还长。虚拟机对此作了一些优化,比如 自旋锁,避免频繁进入切换到核心态中。
ReentrantLock重入锁
ReentrantLock和 Synchronized类似,一个表现为API 层面上的互斥(lock 和 unlock 方法),一个表现为原生语法层面上的互斥。ReentrantLock 比 Synchronized增加了一些高级功能。
①等待中断:持有锁的线程长期不释放锁(执行时间长的同步块)的时候,正在等待的线程可以选择放弃等待,做其他事情。
②实现公平锁:ReentrantLock 默认是非公平的,通过构造参数可设置为公平锁,Synchronized是非公平的。 公平:按照申请锁的时间,先来先得,有序。
③锁可以绑定多个条件。
jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

2.什么是死锁(deadlock)?为什么会死锁?死锁出现后如何消除?

所谓死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。比如:进程A占有对象1的锁,进程B占有对象2的锁,进程A需要对象2的锁才能继续执行,所以进程A会等待进程B释放对象2的锁,而进程B需要对象1的锁才能继续执行,同样会等待进程A释放对象1的锁,由于这两个进程都不释放已占有的锁,所以导致他们进入无限等待中
产生死锁的原因主要是:
(1) 因为系统资源不足。
(2) 进程运行推进的顺序不合适。
(3) 资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
死锁的解除与预防:
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。
消除死锁的3种方式:
1.最简单、最常用的方式就是系统重启(代价大,前面的计算作废);
2.撤销进程,剥夺资源(一次性撤销和剥夺全部或逐步撤销与剥夺);
3.进程回退策略(让进程回退到为死锁的某一时刻或状态)。

3.场景题:现在有三个线程,同时start,用什么方法可以保证线程执行的顺序,线程一执行完线程二执行,线程二执行完线程三执行?

一个简单的办法:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁。简单来说,就是确定前一线程已经执行完毕,才可以执行下一线程。
法1:调用Thread.join(),确定Thread线程执行完;
法2:CountDownLatch,创建线程类的时候,将上一个计数器和本线程计数器传入。运行前执行上一个计数器.await(前一线程为0才可以执行),再执行本计数器.countDown(本线程计数器减少)。
例题:如下程序输出为?

public class TestSync2 implements Runnable {
    int b = 100;          
    synchronized void m1() throws InterruptedException {
        b = 1000;
        Thread.sleep(500); //6     导致下面输出语句执行在后面
        System.out.println("b=" + b);
    }
    synchronized void m2() throws InterruptedException {
        Thread.sleep(250); //5
        b = 2000;
    }
    public static void main(String[] args) throws InterruptedException {
        TestSync2 tt = new TestSync2();
        Thread t = new Thread(tt);  //1
        t.start(); //2
        tt.m2(); //3
        System.out.println("main thread b=" + tt.b); //4
}
    @Override
    public void run() {
        try {
            m1();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出:main thread b=2000  或  main thread b=1000
b=1000                 b=1000

分析:java 都是从main方法执行的,上面说了有2个线程,但是这里就算修改线程优先级也没用,优先级是在2个程序都还没有执行的时候才有先后,现在这个代码一执行,主线程main已经执行了。当执行1步骤的时候(Thread t = new Thread(tt); //1)线程是new状态,还没有开始工作。当执行2步骤的时候(t.start(); //2)当调用start方法,这个线程才正真被启动,进入runnable状态,runnable状态表示可以执行,一切准备就绪了,但是并不表示一定在cpu上面执行,有没有真正执行取决服务cpu的调度。在这里当执行3步骤必定是先获得锁(由于start需要调用native方法,并且在用完成之后在一切准备就绪了,但是并不表示一定在cpu上面执行,有没有真正执行取决服务cpu的调度,之后才会调用run方法,执行m1方法)。这里其实2个synchronized方法里面的Thread.sheep其实要不要是无所谓的,估计是就为混淆增加难度。3步骤执行的时候其实很快子线程也准备好了,但是由于synchronized的存在,并且是作用同一对象,所以子线程就只有必须等待了。由于main方法里面执行顺序是顺序执行的,所以必须是步骤3执行完成之后才可以到4步骤,而由于3步骤执行完成,子线程就可以执行m1了。这里就存在一个多线程谁先获取到问题,如果4步骤先获取那么main thread b=2000,如果子线程m1获取到可能就b已经赋值成1000或者还没有来得及赋值4步骤就输出了,可能结果就是main thread b=1000或者main thread b=2000,在这里如果把6步骤去掉那么b=执行在前和main thread b=在前就不确定了。但是由于6步骤存在,所以不管怎么都是main thread b=在前面,那么等于1000还是2000看情况,之后b=1000是一定固定的。

你可能感兴趣的:(二.Java并发编程篇)