安卓开发中如何使用java锁
一、java锁的概念
在Java中,锁(lock)是一种用于实现线程同步的机制,它可以保证多个线程在共享资源上的互斥访问,避免数据的不一致或者冲突。Java提供了两种主要的锁机制:内置锁(intrinsic lock)和显式锁(explicit lock)。
内置锁是通过synchronized关键字实现的,它可以修饰方法或者代码块,表示只有获得了该对象的锁才能执行该方法或者代码块。内置锁是可重入的,也就是说一个线程可以多次获取同一个对象的锁,但必须释放相同次数的锁才能让其他线程进入。
显式锁是通过java.util.concurrent.locks包中提供的Lock接口及其实现类实现的,例如ReentrantLock、ReadWriteLock等。显式锁需要手动地获取和释放,并且提供了更多的功能和灵活性,例如可中断、可超时、可公平等。
二、java锁的原理
Java中的锁机制主要依赖于JVM和操作系统层面提供的支持。JVM在每个对象头部存储了一个标记字(mark word),用于存储对象自身信息以及与同步相关信息。当一个线程尝试获取一个对象上的内置锁时,会先检查该对象头部是否已经被其他线程占用(即是否有hashcode或者指向其他线程栈帧),如果没有,则尝试使用CAS操作将自己的栈帧指针写入到标记字中,表示成功获取到了该对象上的内置锁;如果已经被占用,则会进入阻塞状态,并等待其他线程释放该对象上锁,支持乐观读、悲观读和写操作,以及条件等待。乐观读意味着不加锁而只是验证一个版本戳(stamp),如果在读操作期间没有发生写操作,则认为读取成功;否则需要重试或者升级为悲观读。悲观读和写操作与ReentrantReadWriteLock类似。
使用显式锁的一般步骤是:
- 创建一个Lock实例
- 在需要同步的代码前调用lock()或者tryLock()方法获取锁
- 在同步代码中执行业务逻辑
- 在同步代码后调用unlock()方法释放锁
例如,以下代码使用ReentrantLock实现了一个简单的计数器:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count;
private Lock lock;
public Counter() {
count = 0;
lock = new ReentrantLock();
}
public void increment() {
lock.lock(); // 获取锁
try {
count++; // 执行业务逻辑
} finally {
lock.unlock(); // 释放锁
}
}
public int getCount() {
return count;
}
}
四、java锁的代码优化方式
使用java锁可以提高多线程程序的正确性和性能,但也需要注意一些编码规范和技巧,以避免出现死锁、活锁、饥饿、性能下降等问题。以下是一些常见的java锁的代码优化方式12:
- 尽量减少同步范围:只对必须保证原子性的最小代码块进行同步,以减少线程阻塞和竞争的时间。
- 尽量避免嵌套同步:如果一个线程需要同时获取多个对象上的内置锁或者显式锁,可能会导致死锁或者性能下降。如果必须使用嵌套同步,应该按照固定的顺序获取和释放锁,并且尽量缩短持有时间。
- 尽量使用非阻塞算法:非阻塞算法是指不依赖于线程阻塞来实现同步的算法,例如CAS(Compare And Swap)操作。非阻塞算法可以提高并发性能和可伸缩性,并且避免死锁等问题。
- 尽量使用高级并发工具类:Java提供了一些高级并发工具类来实现更复杂和更高效的同步功能,例如Semaphore(信号量)、CountDownLatch(倒计时门闩)、CyclicBarrier(循环屏障)、Exchanger(交换器)等。这些工具类可以简化编码难度,并且提供更好的性能和可靠性。
五、总结
本文介绍了安卓开发中如何使用java锁,包括java锁的概念、原理、api的使用方式、代码优化方式等方面。通过合理地使用java锁,可以提高多线程程序在安卓平台上运行时的正确性和性能。
六、附录:java锁的api的示例代码
以下是一些使用java锁的api的示例代码,供您参考:
6.1 ReentrantLock的使用
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private Lock lock = new ReentrantLock();
public void print() {
lock.lock(); // 获取锁
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
System.out.println();
} finally {
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) {
ReentrantLockDemo demo = new ReentrantLockDemo();
// 创建两个线程,分别调用print方法
Thread t1 = new Thread(() -> demo.print());
Thread t2 = new Thread(() -> demo.print());
t1.start();
t2.start();
}
}
6.2 ReentrantReadWriteLock的使用
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReentrantReadWriteLockDemo {
private ReadWriteLock rwlock = new ReentrantReadWriteLock();
private int data;
public void read() {
rwlock.readLock().lock(); // 获取读锁
try {
System.out.println(Thread.currentThread().getName() + " read data: " + data);
Thread.sleep(1000); // 模拟读操作耗时
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwlock.readLock().unlock(); // 释放读锁
}
}
public void write(int newData) {
rwlock.writeLock().lock(); // 获取写锁
try {
System.out.println(Thread.currentThread().getName() + " write data: " + newData);
data = newData;
Thread.sleep(1000); // 模拟写操作耗时
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwlock.writeLock().unlock(); // 释放写锁
}
}
public static void main(String[] args) {
StampedLockDemo demo = new StampedLockDemo();
// 创建三个线程,分别调用read和write方法
Thread t1 = new Thread(() -> demo.read());
Thread t2 = new Thread(() -> demo.write(100));
Thread t3 = new Thread(() -> demo.read());
t1.start();
t2.start();
t3.start();
}
锁的种类 | 优点 | 缺点 | 应用场景 | java中的api |
---|---|---|---|---|
乐观锁 | 不会阻塞线程,减少上下文切换开销,适合竞争不激烈的情况。 | 需要不断重试获取锁,消耗CPU资源,适合竞争激烈的情况。 | 读多写少的场景,如数据库中的CAS操作。 | java.util.concurrent.atomic包下的原子类,如AtomicInteger等。 |
悲观锁 | 简单易用,避免数据不一致和脏读问题。 | 会阻塞线程,导致性能下降和死锁风险,不利于并发性能。 | 写多读少的场景,如数据库中的行级锁、表级锁等。 | synchronized关键字或者Lock接口及其实现类。 |
公平锁 | 遵循先来先得原则,保证线程获取锁的公平性。 | 等待队列会增加额外开销,并且可能导致饥饿现象(等待时间过长)。 | 需要保证线程获取锁顺序与请求顺序一致时使用。 | ReentrantLock构造函数传入true参数表示公平锁,默认为非公平锁;ReentrantReadWriteLock也有相同设置。 |
非公平锁 | 不遵循先来先得原则,允许插队获取锁,提高吞吐量和效率。 | 不保证线程获取锁的公平性,并且可能导致某些线程永远获取不到锁(饥饿现象)。 | 不需要保证线程获取顺序与请求顺序一致时使用。 | ReentrantLock构造函数传入false参数或者无参表示非公平锁;ReentrantReadWriteLock也有相同设置。 |
可重入锁 | 允许一个线程多次获得同一个锁对象,避免死锁问题和递归调用问题。 | 增加了复杂度和开销,并且可能导致其他线程长时间等待。(注意:可重入指同一个线程可以多次获得同一个对象上的同步代码块或方法). | 当一个类中有多个synchronized方法或代码块时使用。(注意:synchronized是可重入性质). | synchronized关键字或者ReentrantLock类都是可重入性质。(注意:ReentrantReadWriteLock是部分可重入性质). |
读写锁(共享/独占) | 共享模式下充分利用资源并发访问提高效率;独占模式下保证数据安全性和一致性;读写分离提高并发度和吞吐量;支持降级(写转读),不支持升级(读转写)。 (注意:共享模式指多个线程可以同时访问共享资源;独占模式指只有一个线程可以访问共享资源). | 增加了复杂度和开销,并且可能导致写操作被饿死 | 需要对同一个资源进行读写操作时使用。 | ReentrantReadWriteLock类是java中实现读写锁的api。 |
互斥锁(独占/排他) | 保证数据安全性和一致性,避免脏读和不可重复读问题。 (注意:独占模式指只有一个线程可以访问共享资源;排他模式指获取锁的线程在释放锁之前会阻止其他线程获取该锁). | 会阻塞线程,导致性能下降和死锁风险,不利于并发性能。 | 写多读少的场景,如数据库中的行级锁、表级锁等。 | synchronized关键字或者Lock接口及其实现类都是互斥锁。 |
共享锁(非独占/非排他) | 充分利用资源并发访问提高效率和吞吐量,适合读多写少的场景。 (注意:非独占模式指多个线程可以同时访问共享资源;非排他模式指获取锁的线程在释放锁之前不会阻止其他线程获取该锁). | 不保证数据安全性和一致性,可能出现脏读和不可重复读问题。 | 读多写少的场景,如数据库中的共享锁等。 | ReentrantReadWriteLock类中的ReadLock是共享锁。(注意:ReentrantReadWriteLock类中的WriteLock是独占/排他/互斥/写入 锁)。 |
分段锁 | 将一个大范围的资源划分为若干个小范围,并为每个小范围分配一个独立的锁对象,从而减少竞争冲突和提高并发度。 (注意:分段锁是一种思想或设计,并不是具体实现)。 | 增加了复杂度和开销,并且可能导致死锁问题。(注意:要避免死锁问题,需要按顺序获取所有涉及到的分段)。 | 需要对大范围资源进行并发操作时使用。(注意:java.util.concurrent包下很多类都使用了分段思想来提高并发度)。 | ConcurrentHashMap类就是使用了分段思想来实现高效并发操作。(注意:ConcurrentHashMap内部有一个Segment数组,每个Segment对象相当于一个小型HashMap,并且有自己独立的ReentrantLock对象)。 |
锁的种类 | 优点 | 缺点 | 应用场景 | java中的api |
---|---|---|---|---|
偏向锁 | 减少无竞争情况下的同步开销 | 在有竞争情况下会增加额外的偏向锁撤销的成本 | 适用于只有一个线程访问同步块的场景 | 需要通过JVM参数来开启或关闭 |
轻量级锁 | 减少无竞争情况下的线程阻塞开销 | 在有竞争情况下会增加额外的自旋和CAS操作开销 | 适用于执行时间短、线程竞争不激烈的代码块 | synchronized关键字在无竞争时自动升级为轻量级锁 |
重量级锁(synchronized) | 可以保证多个线程之间的同步效果 | 导致争用不到锁的线程进入阻塞状态,消耗系统资源 | 适用于执行时间长、线程竞争激烈或者可能长时间等待的代码块 | synchronized关键字或者Object类中的wait()、notify()、notifyAll()方法 |
自旋锁(spinlock) | 减少线程上下文切换的消耗,提高性能 | 循环会消耗CPU资源,如果持有锁的时间过长,会造成性能浪费 | 适用于持有锁时间非常短、并且不容易发生内存顺序冲突(memory order conflicts) 的临界区 | java.util.concurrent.atomic包中提供了基于CAS操作实现自旋锁功能的类,如AtomicInteger、AtomicBoolean等 |
待完善