Java面试准备-锁机制

问题链接转载  Java面试通关要点汇总集【终极版】

一、说说线程安全问题

  1. 线程安全问题是多个线程类并发操作某类的函数,修改某个成员变量的值,很容易造成错误,数据不同步。
  2. 线程安全出现问题的根本原因是存在多个线程对象共享同个资源或操作共享资源代码有多个语句
  3. 常用的解决方案有同步代码块或同步函数
  • 同步代码块

格式:synchronized (锁对象) {

           // 需要被同步的代码

}

注意事项:

  1. 锁对象可以是任意的一个对象
  2. 一个线程在同步代码中sleep也不会释放锁对象
  3. 如果不存在线程安全问题,千万别使用同步代码块,相对降低了效率,因为同步外的线程的都会判断同步锁
  4. 锁对象必须是多线程共享的一个资源,否则锁不住
  • 同步函数,使用synchronized修饰一个函数

注意事项:

  1. 如果函数是一个非静态的同步函数,锁对象是this对象
  2. 如果函数是静态的同步函数,锁对象就是当前函数所属的类的字节码文件(class对象)
  3. 同步函数的锁对象是固定的,不能由自己指定
//同步代码块
synchronized(obj){
    // ...
}

//同步函数
public static synchronized void show(){
    // ....
}

死锁

public class Beetle implements Runnable{
	private boolean flag;
	Beetle(boolean flag){
		this.flag = flag;
	}
	public void run(){
		if(flag){
			while(true){
				synchronized(MyLock.locka){
					System.out.println(Thread.currentThread().getName()+"..if locka...");
					synchronized(MyLock.lockb){
						System.out.println(Thread.currentThread().getName()+"..if lockb...");
					}
				}
			}
		}else{
			while(true){
				synchronized(MyLock.lockb){
					System.out.println(Thread.currentThread().getName()+"..if lockb...");
					synchronized(MyLock.locka){
						System.out.println(Thread.currentThread().getName()+"..if locka...");
					}
				}
			}
		}
	}
	public static void main(String[] args){
		Beetle a = new Beetle(true);
		Beetle b = new Beetle(false);
		Thread t1 = new Thread(a);
		Thread t2 = new Thread(b);
		t1.start();
		t2.start();
	}
}
class MyLock{
	public static final Object locka = new Object();
	public static final Object lockb = new Object();
}

饿汉式,没有线程安全问题

//饿汉式
class Single {
    private static final Single s = new Single();
    private Single(){}
    public static Single getInstance(){
        return s;
    }
}

//懒汉
class Single{
    private static Single s = null;
    private Single(){}
    public static Single getInstance(){
        if(s == null){
            synchronized(Single.class){
                if(s == null)
                    s = new Single();
            }
        }
        return s;
    }
}

ArrayList是非线程安全的,Vector是线程安全的;HashMap是非线程安全的,HashTable是线程安全的;StringBuilder是非线程安全的,StringBuffer是线程安全的

二、volatile 实现原理

有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,里头有个Lock前缀指令。而这个指令在多核处理器下引发两件事:

  1. 将当前处理器缓存行的数据写回到系统内存
  2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效

如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到内存。每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是否过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操作时会重新从系统内存中把数据读到处理器缓存里。

三、synchronize 实现原理

Java中每个对象都可以作为锁,这是synchronized实现同步的基础:

  1. 普通同步方法,锁是当前实例对象
  2. 静态同步方法,锁是当前类的class对象
  3. 同步方法块,锁是括号里的对象

同步代码块:monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置。当且一个monitor被持有之后,它将于锁定状态。线程执行到monitorenter指令时将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁。

同步方法:synchronized方法则会被翻译成普通的方法调用和返回指令。在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的函数,而在Class文件的方法表将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass作为锁对象

四、synchronized 与 lock 的区别

  1. synchronized是java的内置关键字,在JVM层面,Lock是个java类
  2. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁
  3. synchronized会自动释放锁(线程执行完同步代码会释放或发生异常会释放),Lock需要在finally中手动释放锁(unlock()释放锁),否则容易造成线程死锁
  4. synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2等待。如果线程1阻塞,线程2会一直等待。而Lock锁不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待
  5. synchronized的锁可重入,不可中断,非公平。而Lock锁可重入,可判断,可公平
  6. Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题

五、CAS 乐观锁

1 Java中的Synchronized属于悲观锁

2 乐观锁的核心算法是CAS(Compareand Swap,比较并交换),它涉及到三个操作数:内存值,预期值和新值,当且仅当预期值和内存值相等时才将内存值修改为新值。其逻辑为首先检查某块内存的值是否跟之前我读取时一样,如不一样则表示期间此内存值已被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。

public class Beetle implements Runnable{
	private static AtomicBoolean flag = new AtomicBoolean(true);
	public static void main(String[] args){
		Beetle b = new Beetle();
		Thread t1 = new Thread(b);
		Thread t2 = new Thread(b);
		t1.start();
		t2.start();
	}
	public void run(){
		System.out.println("thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
		if(flag.compareAndSet(true, false)){
			System.out.println(Thread.currentThread().getName()+""+flag.get());
			try{
				Thread.sleep(5000);
			}catch(InterruptedException e){
				e.printStackTrace();
			}
			flag.set(true);
		}else{
			System.out.println("重试机制thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
			try{
				Thread.sleep(500);
			}catch(InterruptedException e){
				e.printStackTrace();
			}
			run();
		}
	}
}

CAS的缺点:

1.CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

六、ABA 问题

在运用CAS做Lock-Free操作中有一个经典的ABA问题:

线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题,例如下面的例子:

现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:

head.compareAndSet(A,B);

在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:

此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:

其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。

七、乐观锁的业务场景及实现方式

乐观锁是在应用层加锁,而悲观锁是在数据库层加锁(for update)

乐观锁顾名思义就是在操作时很乐观,这数据只有我在用,我先尽管用,最后发现不行时就回滚。

乐观锁的核心算法是CAS(Compareand Swap,比较并交换),它涉及到三个操作数:内存值,预期值和新值,当且仅当预期值和内存值相等时才将内存值修改为新值。其逻辑为首先检查某块内存的值是否跟之前我读取时一样,如不一样则表示期间此内存值已被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。

额外知识:

在并发编程中我们一般都会遇到这三个基本概念:原子性,可见性和有序性

  • 原子性:即一个操作或多个操作,要么全部执行并且执行过程不会被任何因素打断,要么就都不执行

i = 0     // 是原子性操作

j = i      // 不是原子性操作,包含了两个操作:读取 i,将其赋值给 j 

i ++     // 不是原子性操作,包含了三个操作:读取 i,i + 1 , 将结果赋值给 i

i = j+1  // 不是原子性操作,包含了三个操作:读取 j,i + 1 , 将结果赋值给 i

  • 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
  • 有序性:即程序执行的顺序按照代码的先后顺序执行

猜你喜欢

转载自blog.csdn.net/haima95/article/details/84329419