在 Java 多线程编程中,Synchronized 关键字是一个非常常用的同步机制,能够保证同一时间只有一个线程访问共享资源。本文将通过示例代码介绍 Synchronized 关键字的具体使用方法,以及在开发中的常见应用场景。
示例代码如下:
public class Example {
private int count = 0;
// 方法一:修饰方法
public synchronized void add() {
count++;
}
// 方法二:同步代码块
public void print() {
synchronized (this) {
System.out.println("count: " + count);
}
}
// 方法三:静态同步方法
public static synchronized void sub() {
count--;
}
public static void main(String[] args) {
Example example = new Example();
// 示例一:多个线程同时访问共享资源
Runnable runnable = () -> {
for (int i = 0; i < 1000000; i++) {
example.add();
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count: " + example.count);
// 示例二:并发访问数据库
Runnable runnable1 = () -> {
synchronized (example) {
// 访问数据库的代码
}
};
Thread thread3 = new Thread(runnable1);
Thread thread4 = new Thread(runnable1);
thread3.start();
thread4.start();
// 示例三:实现单例模式
public static synchronized Example getInstance() {
if (instance == null) {
instance = new Example();
}
return instance;
}
}
}
代码说明:
- 在上述代码中,我们定义了一个 Example 类并在其中声明了一个成员变量 count,以及三个方法 add、print 和 sub。
- 方法一 add 使用 Synchronized 关键字修饰,保证了单线程访问,避免了数据竞争的问题。
- 方法二 print 使用同步代码块,锁对象为当前实例对象 this,同样能够保证线程安全。
- 方法三 sub 是一个静态同步方法,锁对象为 Example.class,用于保护静态的成员变量。
- 在 main 方法中,我们通过示例代码介绍了 Synchronized 关键字的应用场景:
- 示例一:多个线程同时访问共享资源 count,通过 Synchronized 关键字保证线程安全;
- 示例二:并发访问数据库,使用 Synchronized 关键字保证数据的一致性;
- 示例三:实现单例模式,使用 Synchronized 关键字避免同时创建多个实例对象。
需要注意的是,在实际开发过程中,Synchronized 关键字的应用场景非常广泛,并且对于同步代码块来说,锁的范围应该尽可能小,以避免影响程序运行效率。
在 Java 中使用 Synchronized 关键字时,锁的粒度和锁的释放都非常重要
锁的粒度:锁的范围应该尽可能小,以避免出现死锁等问题。如果锁住了整个方法或者类,将会导致程序的运行效率降低。因此,我们应该尽可能仅在必要的代码段上加锁。
锁的释放:如果在加锁的代码段中因为异常等情况提前退出了,那么在退出之前一定要记得显式地释放锁,否则将会导致锁无法被其他线程获取,从而影响程序的正常运行。
下面是一个示例,演示了锁的释放机制:
public class Example {
private Object lock = new Object();
public void add() {
synchronized (lock) {
// 加锁
try {
// 模拟耗时操作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行结束,即将释放锁");
}
}
public static void main(String[] args) {
Example example = new Example();
// 启动多个线程并发访问共享资源
for (int i = 0; i < 5; i++) {
new Thread(() -> example.add()).start();
}
}
}
在上面的代码中,我们定义了一个锁对象 lock,并在 add 方法中使用 synchronized (lock) 进行加锁。由于模拟了耗时操作,因此执行结束后显示释放锁。
运行该程序,我们可以看到如下输出:
Thread-2执行结束,即将释放锁
Thread-0执行结束,即将释放锁
Thread-1执行结束,即将释放锁
Thread-3执行结束,即将释放锁
Thread-4执行结束,即将释放锁
可以发现,每个线程都执行完毕之后显示了即将释放锁的提示信息,说明锁已经被成功释放。
除了 Synchronized 关键字以外,Java 中还有以下常用的同步机制
1、Lock
Lock 是 Java.util.concurrent 包中提供的锁接口,它可以替代 synchronized 来实现多线程间的互斥和同步。Lock 接口提供了以下三个重要的方法:
- lock(): 尝试获取锁,如果锁已经被其他线程获取,则当前线程会进入休眠状态,直到获取锁成功。
- unlock(): 释放锁,通知其他线程可以获取锁了。
- tryLock(): 尝试获取锁,如果获取失败则立即返回 false,否则返回 true。
使用 Lock 时需要注意以下事项:
- 必须手动加锁和解锁,即在 try...finally 块中使用 lock() 和 unlock() 方法。
- 如果线程无法获取到锁,则可以使用 tryLock() 方法进行尝试,避免因为等待时间过长导致时间浪费。
2、Semaphore
Semaphore(信号量)是一种同步工具,它可以限制同时访问某个资源的线程数量。Semaphore 维护了一个计数器,可以通过 acquire() 方法获取资源,而资源的数量则通过 release() 方法释放。
Semaphore 的主要作用:
- 对线程数量进行控制:Semaphore 可以限制同时访问某个资源的线程数量,避免这些线程之间的操作产生竞争和冲突。
- 控制多个共享资源的访问:Semaphore 可以用来实现限制数据库连接池的大小、同时访问某个共享文件的线程数量等。
Semaphore 使用示例:
public class Example {
private Semaphore semaphore = new Semaphore(3); // 限制同时最多有3个线程访问共享资源
public void access() {
try {
semaphore.acquire(); // 获取资源
// 访问共享资源
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放资源
}
}
public static void main(String[] args) {
Example example = new Example();
// 启动多个线程并发访问共享资源
for (int i = 0; i < 5; i++) {
new Thread(() -> example.access()).start();
}
}
}
在上述代码中,我们初始化了一个 Semaphore,限制最多同时有3个线程访问共享资源。在 access 方法中,使用 acquire() 方法获取资源后执行访问共享资源的操作,最后调用 release() 方法释放资源。
以上就是 Java 中常用的同步机制 Lock 和 Semaphore 的介绍及使用示例。需要注意的是,在使用这些同步机制时,都需要仔细考虑锁的粒度和锁的释放问题,以避免因线程之间的相互等待导致程序性能下降或者死锁等问题的出现。