- 今天面试了一波,一个北京的大哥,一口北京腔,那边有点吵,导致好多问题听不清,不停的让他重复,很抱歉 ^ - ^
- 可能是因为外包的原因吧,提问问题也是感觉条理不是很清晰,当然,也是因为我是渣渣…
好了,开始正题:
造成线程安全问题的主要诱因有两点 :
- 一是存在共享数据(也称临界资源),
- 二是存在多条线程共同操作共享数据。
了解volatile吗?
简单介绍下
当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
应用限制
对于volatile关键字,当且仅当满足以下所有条件时可使用:
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
- 该变量没有包含在具有其他变量的不变式中。
能举个简单的场景吗
- 有一个布尔类型的flag,各个线程用它来判断是否执行某个操作
- 当线程A对它赋值时,其他线程能立刻看到它值得变化,
- 如下代码
//线程1 volatile boolean flag= false; while(!flag){ fun(); } //线程2 while(flag){ fun(); } //线程3 flag= true;
了解synchronized吗?
- 当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。其本质是将并行的运行顺序改为串行。
- 在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能),这点确实也是很重要的。
能简单说说java中synchronized的原理吗
- 在JVM中,对象在堆内存中的布局分为三块区域:对象头、实例数据和对齐填充。
-
- 实例数据存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐;
- 对其填充不是必须部分,由于虚拟机要求对象起始地址必须是8字节的整数倍,对齐填充仅仅是为了使字节对齐。
- 对象头是是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark Word 和 Class Metadata Address组成,其中Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例。
- 每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。
你能说说你在项目中怎么应用它吗
简单demo,模拟火车卖票,共享数据是ticket
public class TicketThread implements Runnable {
Integer ticket = 100;
//锁对象
Object object = new Object();
@Override
public void run() {
//由于车站售票及时票卖完了 依然可以进入售票口 只是买不到票
//所有此处总会进入
while (true) {
method();
}
}
private synchronized void method(){
if (ticket > 0) {
//此时假设某窗口的售票员接到女朋友电话了没空理乘客(只是假设)
try {
//该窗口沉睡100毫秒 时间到了后 开始售票
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出第" + ticket-- + "张票");
}
}
}
synchronized线程安全吗?
答案:不一定安全!!!
synchronized关键字有下面三种用法:
修饰实例方法:
-
对当前实例加锁,进入方法需要获得当前实例的锁修饰静态方法:
-
对当前类对象加锁,进入静态方法需要获得当前类对象的锁修饰代码块:
-
对指定对象进行加锁,进入代码块需要获得指定对象的锁
synchronized锁对应一个monitor对象,因为静态方法和实例方法锁的对象是不一致的(Monitor不是同一个),所以导致最终没有达到预期效果。
你上面提到synchronized是将其运行顺序某种意义上变为串行的,那么他的效率肯定较低,那么你怎么在保证线程安全的前提下,提高效率呢?
合理使用synchronized,将其使用在较少的代码上
volatile和synchronized比较呢
-
首先需要理解线程安全的两个方面:执行控制和内存可见。
执行控制的目的是控制代码执行(顺序)及是否可以并发执行。
内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。
-
synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,它还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before于随后获得这个锁的线程的操作。
-
volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。
区别:
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
- synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
了解java的哪些集合容器
关系型数据库中的乐观锁和悲观锁呢
乐观锁:
乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了。
一般是这么实现的:
在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1。
一般实现乐观锁,不使用数据库的锁,而是用户自己实现
举例
下单操作
- 查询出商品信息
select (status,status,version) from t_goods where id=#{id}
-
根据商品信息生成订单
-
修改商品status为2
update t_goods set status=2,version=version+1 where id=#{id} and version=#{version};
悲观锁
悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作
悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。
相对于乐观锁的用户自己实现,悲观锁就需要用到关系型数据库的锁机制了
mysql InnoDB 中的实现方法:
SQL语句
SELECT … FOR UPDATE
当该语句中有指定的主键时,是行级锁;主键不定时,是表锁
参考:
https://blog.csdn.net/javazejian/article/details/72828483
https://www.cnblogs.com/liyunfeng17/p/10891293.html
https://blog.csdn.net/suifeng3051/article/details/52611233