1.synchronized和ReentranctLock有什么区别?
①底层实现:synchronized是jvm层面的锁,通过monitor对象完成。对象只能在同步代码块和同步方法中调用wait、notify方法。ReentranctLock提供的是API层面的锁。trylock/unlock
②是否可手动释放:synchronized不需要用户手动释放,代码块执行完后自动释放锁占用。
ReentranctLock需要用户调unlock()手动释放,否则会导致死锁。一般通过lock、unlock方法配合try/finally语法块完成。
③是否可中断:synchronized不可中断,除非加锁的代码块报错或正常执行完。ReentranctLock可以中断,通过trylock(long timeout,TimeUnit unit)设置过期时间就ok了。
④是否公平锁:synchronized非公平锁;ReentranctLock可以选非公平锁,也可选公平锁。创建对象时,传入boolean参数就行了。new ReentranctLock(fair)
2.ArrayList线程安全吗?怎么实现线程安全?
不安全!
①非线程安全的修改操作
ArrayList底层基于数组实现,长度固定。如果多个线程同时对ArrayList进行操作(增删改),会导致数据不一致或其他错误。
②不可见问题
多线程环境下,每个线程都有自己的工作内存,这些工作内存与主内存同步可能不及时。如果一个线程修改了集合中某个元素,另一个线程未及时同步这个修改,就会导致数据不一致。
解决方案:
可以考虑使用Vector、CopyOnWriteArrayList。
Vector的公共方法都用synchronized修饰了,因此可以保证线程安全。
CopyOnWriteArrayList底层基于数组实现,它使用一种‘copy-on-write’技术(写时复制)来确保线程安全。每当进行修改时,CopyOnWriteArrayList都会先复制整个数组,然后再进行修改操作,这样就避免了多个线程同时修改同一个数组的问题。
3.乐观锁、悲观锁了解吗?使用场景?
悲观锁:很悲观,因为它认为如果不锁住这个资源,别人(其他线程)就会来抢,就会造成数据结构错误。所以悲观锁为确保数据正确性,会在每次操作数据前,先锁住数据。
实现:synchronized和ReentrantLock锁。必须取到锁才能操作,这就是悲观锁的思想。
使用场景:竞争激烈,即并发量高时,悲观锁更有优势。因为此时若用乐观锁执行更新,频繁失败和重试,极其浪费CPU资源。
乐观锁:很乐观。相对悲观锁。它不锁资源,因为它认为自己在操作资源时不会有其他线程干扰。
为确保数据正确性,在操作之前,先判断在自己操作期间,是否有其他线程访问数据。如果没有,
直接操作;否则,根据业务要求报错或重试。
实现:<1>乐观锁底层依赖的是CAS算法(compare and swap:比较并交换)。所以它在操作之前并不需要获取锁,而是直接读取数据到自己工作内存中操作。
<2>版本号机制。比如在修改数据时加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1。当某个线程查询数据时,将该数据的版本号一起查出来;当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。
使用场景:竞争不激烈,即并发量小时,乐观锁优势更大。因为你此时若用悲观锁,会锁住数据资源,其他线程无法访问,影响并发,加锁和释放锁太耗资源了。
4.分布式锁用过吗?怎么实现?
顾名思义,就是分布式系统中使用的锁。它用来解决分布式系统中控制共享资源访问的问题。
①可以使用redis的setNX命令实现,释放锁需要原子操作,可以用lua脚本。
②可以使用Redisson组件实现redis的分布式锁,它可以实现锁的可重入性和锁续命。
③可以使用CP架构的锁,比如zk、consul。高一致性。
5.死锁?如何避免死锁?
死锁:多线程情况下,他们彼此等待对方释放资源,导致所有线程都无法继续执行的情况。
产生原因:
①互斥条件:一个资源只能被一个线程占有,当这个资源被占用后其他线程就只能等待。
②不可剥夺条件:当一个线程不主动释放资源时,那这个资源就一直被当前线程占用。
③请求并持有条件:一个线程已拥有一个资源后,又尝试请求新的资源。
④环路等待条件:产生死锁一定是发生了线程资源环路链。
如何避免?
改变死锁中的条件呀!
如③中,让获取了一把锁之后不再请求获取另一把锁。
④中,破坏环路等待条件。如下图,线程1释放锁后,再让线程2访问资源。