1. Sychronized和ReentrantLock有什么区别?
它们的主要区别体现在以下几个方面:
-
语法层面:
- Synchronized: 是Java语言的关键字,可以直接修饰实例方法、静态方法或代码块。使用简单,无需手动管理锁的获取和释放,由JVM自动完成。
- ReentrantLock: 是Java.util.concurrent包下的一个类,需要通过实例化对象来使用。使用时需要调用
lock()方法获取锁和unlock()方法释放锁,如果不手动释放,可能会导致死锁。相比synchronized,使用起来稍微复杂,但提供了更多灵活性。
-
锁的特性:
- 可重入性: 两者都支持可重入,即同一个线程可以重复获取同一把锁。
- 公平性: Synchronized锁默认是非公平的,而ReentrantLock既可以设置为非公平锁(默认),也可以设置为公平锁。
- 中断: ReentrantLock支持中断,线程在等待锁的过程中可以响应中断请求,而synchronized不支持中断等待的线程。
- 条件队列: ReentrantLock通过
newCondition()方法可以创建多个Condition对象,从而实现多条件的等待唤醒,synchronized则不支持此特性。 - 锁获取方式: Synchronized是隐式获取和释放锁,ReentrantLock则是显式操作,可以根据业务需求选择是否需要等待一定时间或者尝试获取锁。
-
异常处理:
- 使用synchronized时,如果在同步代码块或方法中抛出异常,JVM会自动确保锁的释放。
- 使用ReentrantLock时,必须在finally块中显式调用
unlock()方法来释放锁,否则在异常情况下可能导致锁无法释放,进而引起死锁。
-
底层实现:
- Synchronized基于JVM的Monitor(监视器)机制实现。
- ReentrantLock基于AbstractQueuedSynchronizer(AQS)框架实现,提供了更灵活的同步机制。
综上所述,synchronized适用于简单的同步场景,代码简洁,但功能有限;而ReentrantLock提供了更多高级功能,更适合复杂的并发控制场景,但需要程序员更加小心地管理锁的获取和释放。
2. Sychronized和ReentrantLock哪个效率更好?该使用哪个?
早先synchronized在某些情况下性能略逊于ReentrantLock,主要原因是synchronized在早期Java版本中缺乏细粒度的控制和优化机制。Java 7引入了一些优化,如偏向锁、轻量级锁等,极大地提高了synchronized的性能。在最新的Java版本中,对于大多数普通同步场景而言,synchronized与ReentrantLock的性能差异已经变得非常小。
具体使用哪个,取决于应用场景和具体实现。如果只需要基本的互斥锁功能,并且不需要响应中断请求或尝试非阻塞获取锁等功能,那么synchronized由于其简洁性和JVM的优化,可能更适合。如果需要更高级功能,比如公平锁、锁超时、可响应中断等,ReentrantLock的优势就体现出来了。
3. 不使用锁,如何保证并发安全?
-
原子操作和原子类:
- Java并发包(java.util.concurrent.atomic)提供了原子整数(AtomicInteger, AtomicLong等)、原子引用(AtomicReference)等原子类,它们的更新操作是原子性的,能够在不使用锁的情况下保证线程安全。
-
CAS(Compare-and-Swap)操作:
- CAS是一种无锁算法,它在更新内存值时会比较旧值和预期值,如果相等则更新,否则不更新,整个过程是原子操作。Java的原子类就基于CAS实现。
-
volatile关键字:
- volatile变量能确保多个线程之间的可见性,即一个线程对volatile变量的修改会立刻对其他线程可见。尽管volatile并不能代替锁来实现深层次的同步,但在某些简单场景下(如单例双重检查锁定的DCL模式),它可以保证初始化过程的线程安全性。
-
不可变对象:
- 不可变对象在创建后其状态就不能再更改,因此天生线程安全。多线程环境下,可以安全地共享不可变对象。
-
Thread-Local变量:
- ThreadLocal为每个线程提供独立的变量副本,不同线程之间彼此不影响,因此无需锁来同步。
-
并发集合类:
- Java并发包提供了很多并发安全的集合类,如ConcurrentHashMap、ConcurrentLinkedQueue等,它们内部通过各种无锁或细粒度锁的算法实现并发安全。
4.HashMap为什么线程不安全?怎么解决?
HashMap在多线程环境下线程不安全的原因主要包括以下几个方面:
非线程同步: HashMap的内部并没有实现任何的线程同步机制,这意味着多个线程可以同时对HashMap进行读写操作。当多个线程同时执行put、remove等操作时,可能会造成数据的不一致。
结构修改操作的原子性: HashMap的操作(如put、resize等)并非原子性的。例如,在添加元素时,可能涉及到数组定位、链表插入、数组扩容等多个步骤。如果在这期间有其他线程同时修改HashMap,可能导致数据结构的混乱,如链表形成环状结构(特别是在JDK 1.7的扩容过程中尤为突出)。
扩容时的并发问题: 当HashMap的容量不够时,会进行扩容操作,这是一个复杂的操作,涉及新数组的创建、旧数组元素的迁移。如果在扩容过程中有其他线程也在进行put操作,可能会出现数据丢失、重复添加或链表断裂等问题。
哈希冲突时的并发问题: 在哈希冲突的情况下,HashMap使用链地址法(拉链法)解决冲突,即同一个哈希桶内的元素构成链表。如果多个线程同时对同一个桶中的元素进行操作,可能会导致链表结构破坏,进而引发数据不一致。
解决方法:
- 使用线程安全的集合类: 最常用且高效的解决方式是使用Java并发包
java.util.concurrent中的ConcurrentHashMap。ConcurrentHashMap内部采用分段锁(Segment)机制,允许多个线程同时访问不同分段的数据,大大提高了并发性能。 - 使用Collections.synchronizedMap()包装: 可以通过
Collections.synchronizedMap()方法对HashMap进行包装,返回一个线程安全的Map,但这将对整个Map的所有操作进行同步,可能会导致并发性能下降,尤其是在高并发场景下,因为每次操作都需要获取锁。
Map<String, String> syncHashMap = Collections.synchronizedMap(new HashMap<>());
- 使用Hashtable: 虽然
Hashtable是线程安全的,但由于它使用的是全表锁,性能较低,且已逐渐被ConcurrentHashMap替代,因此现在并不推荐使用。