共享锁(S锁)
又称为读锁,可以查看但无法修改和删除的一种数据锁。如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排它锁。获准共享锁的事务只能读数据,不能修改数据。共享锁下其它用户可以并发读取,查询数据。但不能修改,增加,删除数据。资源共享.
在Java中实现共享锁通常是通过java.util.concurrent.locks.ReentrantReadWriteLock类的读锁来完成的。读锁允许多个线程同时持有读锁,从而实现共享资源的并发读取。
以下是一个使用ReentrantReadWriteLock实现共享锁的简单示例:
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class SharedLockExample {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void read() {
rwLock.readLock().lock();
try {
// 执行读操作
} finally {
rwLock.readLock().unlock();
}
}
public void write() {
rwLock.writeLock().lock();
try {
// 执行写操作
} finally {
rwLock.writeLock().unlock();
}
}
public static void main(String[] args) {
SharedLockExample example = new SharedLockExample();
// 多个读操作可以并发执行
new Thread(() -> example.read()).start();
new Thread(() -> example.read()).start();
// 写操作将会阻塞,直到所有的读操作完成
new Thread(() -> example.write()).start();
}
}
排它锁(X锁)
又称为写锁、独占锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A
排它锁(也称为独占锁)在Java中通常是通过synchronized关键字或ReentrantLock类实现的。以下是使用这两种方式实现排它锁的简单示例:
- 使用synchronized关键字:
public class MutexExample {
private Object mutex = new Object();
public void synchronizedMethod() {
synchronized (mutex) {
// 临界区代码
}
}
}
- 使用ReentrantLock类:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private ReentrantLock lock = new ReentrantLock();
public void lockMethod() {
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
}
}
在这两种情况下,当一个线程进入同步方法或同步块时,其他线程必须等待该线程退出同步块才能执行。这确保了在同一时刻只有一个线程可以执行临界区代码。
互斥锁
在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
在Java中,互斥锁通常是通过java.util.concurrent.locks.Lock接口实现的。Lock接口提供了比使用synchronized方法和语句可得到的更广泛的锁操作。
以下是使用Lock接口实现互斥锁的一个简单例子:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MutexExample {
private final Lock lock = new ReentrantLock();
public void doSynchronizedWork() {
lock.lock(); // 获取锁
try {
// 在此处添加需要同步的代码
// ...
} finally {
lock.unlock(); // 释放锁,确保释放锁,即使发生异常也不能遗漏
}
}
}
在这个例子中,ReentrantLock类实现了Lock接口。通过调用lock()方法获取锁,并在完成同步代码后调用unlock()方法释放锁。使用try...finally块确保即使同步代码抛出异常,锁也能被释放,防止死锁的发生。
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。
在Java中,悲观锁通常通过synchronized关键字或者ReentrantLock类实现。以下是使用ReentrantLock实现悲观锁的示例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class PessimisticLockExample {
private final Lock lock = new ReentrantLock();
public void performOptimisticOperation() {
// 尝试获取锁
lock.lock();
try {
// 在这个区域内执行需要乐观控制的代码
// ... 你的代码 ...
} finally {
// 释放锁,以避免死锁
lock.unlock();
}
}
public static void main(String[] args) {
PessimisticLockExample example = new PessimisticLockExample();
example.performOptimisticOperation();
}
}
在这个例子中,ReentrantLock实例是悲观锁,因为它尝试通过lock()方法获取对共享资源的独占访问权限。在获取锁之后,代码在try块内执行,并且确保在finally块中释放锁,以防止发生异常时造成死锁。
乐观锁
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
乐观锁在Java中通常通过使用版本号或者时间戳来实现。以下是一个简单的Java实现示例:
public class OptimisticLockingExample {
// 假设有一个带有版本号字段的实体类
public static class EntityWithVersion {
private int id;
private String data;
private int version;
// 构造函数、getter和setter省略
// 更新数据的方法,带有乐观锁的实现
public void updateData(String newData, int currentVersion) {
if (this.version != currentVersion) {
throw new OptimisticLockingException("Version mismatch");
}
this.data = newData;
this.version++; // 更新版本号
}
}
public static void main(String[] args) {
EntityWithVersion entity = new EntityWithVersion(1, "Initial data", 0);
// 假设这是第一个线程尝试更新数据
entity.updateData("Updated data", 0); // 成功更新
// 现在有另一个线程尝试同样的操作但是使用旧的版本号
try {
entity.updateData("Duplicate update", 0); // 引发异常,因为版本号不匹配
} catch (OptimisticLockingException e) {
// 处理乐观锁冲突
}
}
}
class OptimisticLockingException extends RuntimeException {
public OptimisticLockingException(String message) {
super(message);
}
}
在这个例子中,EntityWithVersion 类有一个版本号字段 version。每次更新数据时,都会检查版本号是否与传入的版本号相同。如果不同,则抛出 OptimisticLockingException 异常,表示有乐观锁冲突。这种方法可以避免使用传统的数据库锁定机制,从而提高系统的并发性能。
行级锁
行级锁是 MySQL 中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁和排他锁。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
在Java中实现行级锁通常涉及到数据库的事务控制和锁机制。如果你使用的是JDBC,可以通过Connection对象来控制事务并获取行级锁。
以下是一个简单的例子,展示了如何在JDBC中使用行级锁来控制并发访问:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class RowLevelLockingExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/yourdatabase";
String username = "yourusername";
String password = "yourpassword";
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
// 建立连接
connection = DriverManager.getConnection(url, username, password);
// 设置手动提交
connection.setAutoCommit(false);
// 查询语句,使用for update来获取行级排他锁
String sql = "SELECT * FROM your_table WHERE id = ? FOR UPDATE";
preparedStatement = connection.prepareStatement(sql);
// 设置查询参数
preparedStatement.setInt(1, 1);
// 执行查询
preparedStatement.executeQuery();
// 在这里执行你的业务逻辑
// 提交事务
connection.commit();
} catch (SQLException e) {
try {
if (connection != null) {
// 发生异常时回滚事务
connection.rollback();
}
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
} finally {
try {
if (preparedStatement != null) {
preparedStatement.close();
}
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
在这个例子中,FOR UPDATE语句会在查询时获取行级的排他锁。其他事务将不能在获取锁的行上执行任何类型的DML操作(除非它们也使用FOR UPDATE),直至锁被释放。
请注意,这个例子假设你已经有了一个名为yourdatabase的数据库,其中有一个名为your_table的表,并且表中有一个名为id的字段。确保你的数据库引擎支持事务(如InnoDB),并且已经启用了行级锁。
表级锁
表级锁是 MySQL 中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分 MySQL 引擎支持。最常使用的 MyISAM 与 InnoDB 都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。
在Java中,可以通过JDBC的Connection对象来实现表级锁。表级锁通常用于保证数据的完整性和一致性,在并发环境下防止数据被多个事务同时修改。
以下是使用java.sql.Connection实现表级锁的示例代码:
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class TableLockExample {
public void lockTable(Connection connection, String tableName) throws SQLException {
// 使用MySQL的表级锁定语法
String sql = "LOCK TABLES " + tableName + " WRITE";
PreparedStatement statement = connection.prepareStatement(sql);
statement.executeUpdate();
// 注意:在实际使用中,应该在try-finally块中释放锁
// 以确保即使在发生异常的情况下也能释放锁
}
public void unlockTable(Connection connection, String tableName) throws SQLException {
String sql = "UNLOCK TABLES";
PreparedStatement statement = connection.prepareStatement(sql);
statement.executeUpdate();
}
public static void main(String[] args) {
// 假设已经有一个数据库连接
Connection connection = ...;
TableLockExample tableLock = new TableLockExample();
try {
// 锁定表
tableLock.lockTable(connection, "my_table");
// 在这里执行需要表锁的操作
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
// 释放表锁
tableLock.unlockTable(connection, "my_table");
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
请注意,这个例子使用了MySQL的语法来锁定表。不同的数据库系统可能有不同的语法或方法来实现表级锁。另外,在实际应用中,应该总是在finally块中释放锁,确保即使在发生异常的情况下锁也能被释放。
页级锁
页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。BDB 支持页级锁。开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
在Java中,页级锁通常是由底层数据库或文件系统提供支持的,而不是由Java语言本身直接提供。如果你需要在Java中实现类似的功能,你可能需要借助JNI(Java Native Interface)或者直接使用Java Cryptography Extension(JCE)中的一些加密API,这些API可以提供对文件加锁的支持。
如果你想要在Java中实现类似数据库页级锁的功能,你可以使用第三方库,例如Apache Derby的XADataSource,它支持分布式事务和两阶段提交。
以下是一个简单的示例,使用Java Cryptography Extension来锁定文件:
import javax.crypto.Cipher;
import java.io.File;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.security.Key;
import java.security.SecureRandom;
public class FileLockExample {
public static void main(String[] args) {
try {
File file = new File("test.txt");
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
// 使用Cipher生成一个密钥
Key key = Cipher.generateKey(Cipher.TYPE_SECRET_KEY);
// 随机生成一个文件锁
SecureRandom sr = new SecureRandom();
byte[] token = new byte[20];
sr.nextBytes(token);
// 尝试获取文件锁
FileLock lock = randomAccessFile.getChannel().lock(0, Long.MAX_VALUE, true, token, key);
// 执行文件操作
// ...
// 释放文件锁
lock.release();
// 关闭文件
randomAccessFile.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
请注意,这个例子使用了Cipher.generateKey和SecureRandom来生成一个伪随机的锁,并且使用了一个密钥来尝试锁定整个文件。这不是一个真正的页级锁,而是一个简化的文件锁实现,用于演示如何在Java中实现文件锁的概念。实际的页级锁通常需要操作系统和数据库的支持。
丢失修改
指事务1和事务2同时读入相同的数据并进行修改,事务2提交的结果破坏了事务1提交的结果,导致事务1进行的修改丢失。
"java 丢失修改"这个问题可能是指在使用Java进行数据库操作时,更新数据失败的问题。这种情况通常是由于事务未正确提交导致的。
以下是一个简单的Java代码示例,演示了如何使用JDBC处理事务并实现数据修改:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class JdbcUpdateExample {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
try {
// 注册JDBC驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 打开连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
// 设置自动提交为false
conn.setAutoCommit(false);
// 执行查询
stmt = conn.createStatement();
String sql = "UPDATE mytable SET field = 'value' WHERE condition";
stmt.executeUpdate(sql);
// 提交事务
conn.commit();
System.out.println("修改成功");
} catch (ClassNotFoundException e) {
// 处理驱动注册错误
e.printStackTrace();
} catch (SQLException e) {
// 处理SQL错误
try {
if (conn != null) {
// 发生异常时回滚事务
conn.rollback();
System.out.println("修改失败,已回滚");
}
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
} finally {
// 关闭资源
try {
if (stmt != null) stmt.close();
} catch (SQLException se2) {
se2.printStackTrace();
}
try {
if (conn != null) conn.close();
} catch (SQLException se) {
se.printStackTrace();
}
}
}
}
在这个示例中,我们首先设置了数据库连接的自动提交属性为false,这意味着我们将自己负责提交或回滚事务。在执行更新操作后,我们调用commit()方法提交事务。如果在操作过程中发生了异常,我们通过捕获异常来回滚事务,确保数据的一致性。这是处理数据库修改丢失问题的一个基本模板。
不可重复读
一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!
Java中的“不可重复读”通常指的是在数据库事务中,一个事务读取数据后,再次读取数据时,数据发生了改变。为了实现不可重复读,可以将数据库事务隔离级别设置为READ_COMMITTED(读取已提交),这样在事务中间时期其他事务的更新会对当前事务可见,从而导致不可重复读问题。
以下是一个简单的例子,演示如何在Java中使用JDBC处理不可重复读的问题:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class UnrepeatedReadExample {
private static final String URL = "jdbc:mysql://localhost:3306/your_database";
private static final String USERNAME = "your_username";
private static final String PASSWORD = "your_password";
public static void main(String[] args) {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
// 设置数据库连接的隔离级别为READ_COMMITTED
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
connection.setAutoCommit(false); // 开始事务
// 第一次查询
statement = connection.prepareStatement("SELECT value FROM your_table WHERE id = 1");
resultSet = statement.executeQuery();
if (resultSet.next()) {
System.out.println("First read: " + resultSet.getString("value"));
}
// 模拟其他事务更新数据
statement = connection.prepareStatement("UPDATE your_table SET value = 'new_value' WHERE id = 1");
statement.executeUpdate();
connection.commit(); // 提交事务
// 第二次查询,数据已经发生改变
resultSet.beforeFirst(); // 重新定位结果集指针
if (resultSet.next()) {
System.out.println("Second read: " + resultSet.getString("value"));
}
connection.commit(); // 提交事务
} catch (SQLException e) {
try {
if (connection != null) {
connection.rollback(); // 回滚事务
}
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
} finally {
try {
if (resultSet != null) {
resultSet.close();
}
if (statement != null) {
statement.close();
}
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
在这个例子中,我们使用了Connection.setTransactionIsolation()方法来设置事务的隔离级别为READ_COMMITTED。这样在事务中,即使其他事务提交了对数据的更新,也会被我们的事务感知到,导致不可重复读问题的发生。记得在操作完成后,要关闭相关的资源,并处理异常。
读脏数据
事务T1修改某一数据,并将其写回磁盘,事务T2读取同一数据后,T1由于某种原因被撤消,这时T1已修改过的数据恢复原值,T2读到的数据就与数据库中的数据不一致,则T2读到的数据就为"脏"数据,即不正确的数据。
在Java中解决脏读通常涉及到数据库事务的隔离级别。脏读是指一个事务读取了另一个事务尚未提交的数据。为了避免脏读,可以将事务的隔离级别设置为READ_COMMITTED。
以下是一个简单的例子,演示如何在Java中使用JDBC设置事务隔离级别,从而避免脏读:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Savepoint;
public class TransactionExample {
public static void main(String[] args) {
Connection conn = null;
Savepoint savepoint = null;
try {
// 加载并注册JDBC驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 建立数据库连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
// 设置事务隔离级别为READ_COMMITTED
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
// 开启事务
conn.setAutoCommit(false);
// 执行数据库操作...
// 保存保点
savepoint = conn.setSavepoint();
// 提交事务
conn.commit();
} catch (ClassNotFoundException | SQLException e) {
try {
if (conn != null && !conn.isClosed()) {
if (savepoint != null) {
conn.rollback(savepoint);
} else {
conn.rollback();
}
conn.close();
}
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
}
}
}
在这个例子中,我们首先设置了数据库连接的事务隔离级别为TRANSACTION_READ_COMMITTED,然后开始一个事务,在事务中执行数据库操作。如果操作成功,我们提交事务;如果操作失败,我们回滚事务。这样可以避免脏读问题。
死锁
两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
死锁四个产生条件:
1)互斥条件: 指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件: 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件: 指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件: 指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
预防死锁打破上述之一的条件。
java 解决死锁问题
死锁是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种僵局,无一个进程或线程能够继续执行。在Java中,死锁问题通常是由于两个或多个线程相互等待对方释放资源,导致它们都无法继续执行。
为了解决死锁问题,可以采取以下策略:
- 避免嵌套锁:尽量不在持有一个锁的情况下去申请另一个锁。
- 锁顺序统一:当你需要获取多个锁时,确保所有线程以相同的顺序获取锁。
- 使用定时锁:对于那些不能一次就立即获取到所有锁的情况,可以使用Lock.tryLock(timeout, unit)方法,它可以在等待一定时间后放弃锁。
- 检测死锁:JVM提供了工具来检测死锁,例如使用jstack工具。
- 使用锁层次结构:通过定义锁的层次结构,确保按层次顺序获取锁。
下面是一个简单的Java代码示例,演示了如何使用ReentrantLock和Condition来避免死锁,并提供了一个锁顺序的例子:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockExample {
private Lock lockA = new ReentrantLock();
private Lock lockB = new ReentrantLock();
public void deadlock() {
lockA.lock();
try {
// 假设这里做了一些操作
lockB.lock();
try {
// 操作B
} finally {
lockB.unlock();
}
} finally {
lockA.unlock();
}
}
public void correctOrder() {
Lock locks[] = new Lock[]{lockA, lockB};
// 对锁进行排序,确保获取锁的顺序一致
Arrays.sort(locks, (o1, o2) -> Integer.compare(System.identityHashCode(o1), System.identityHashCode(o2)));
locks[0].lock();
try {
// 操作A
locks[1].lock();
try {
// 操作B
} finally {
locks[1].unlock();
}
} finally {
locks[0].unlock();
}
}
}
在这个例子中,deadlock方法可能会导致死锁,因为线程在获取lockA后,尝试获取lockB,同时没有释放lockA。而correctOrder方法通过排序锁,确保了在获取下一个锁之前,先释放当前持有的锁,从而避免了死锁的发生。