1.Synchronized 的介绍和使用
介绍:在 Java 中,synchronized
是一种关键字,用于实现多线程之间的同步。它可以应用于方法或代码块,以确保在同一时刻只有一个线程可以访问被 synchronized
修饰的方法或代码块,从而避免多线程并发访问共享资源时产生的数据竞争和不一致性。
synchronized
的主要作用是解决多线程之间的资源竞争问题,特别是在多个线程同时修改共享数据时,可能导致数据不一致的情况。通过使用 synchronized
,可以确保同一时刻只有一个线程访问共享资源,从而避免竞争条件和线程安全问题。
使用:
synchronize 的使用方式可分为应用在代码块上和运用在方法上,代码如下
public synchronized void synchronizedMethod() {
// 同步代码块
}
public void someMethod() {
// 非同步代码块
synchronized (sharedObject) {
// 同步代码块
}
// 非同步代码块
}
以上两种方式锁的都是对象锁(简单理解第一种,就是需要创建对象才能使用该方法,所以锁的也是对象,第二种sharedObject就是一个对象,显而易见),除了对象锁,当synchronized加在了静态方法上面时,那它就是一个类锁,锁的就是xx.class的Class对象,相当于锁的是类源信息,每个类只有一个class对象,所以每个类只有一个类锁。
public class Test1 {
private long count = 0;
private Object object = new Object();
public long getCount() {
return count;
}
public void setCount(long count) {
this.count = count;
}
/*不加锁则计算结果会小于等于20000*/
public void incCountNoSyn() {
count++;
}
public synchronized void incCountSyn() {
count++;
}
public void incCountSynn() {
synchronized (this) {
count++;
}
}
/*用在同步块上,但是锁的是单独的对象实例*/
public void incCountObj() {
synchronized (object) {
count++;
}
}
private static class Count extends Thread {
private Test1 simplOper;
public Count(Test1 simplOper) {
this.simplOper = simplOper;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
simplOper.incCountObj();
}
}
}
public static void main(String[] args) throws InterruptedException {
Test1 test1 = new Test1();
Count count = new Count(test1);
Count count1 = new Count(test1);
count.start();
count1.start();
Thread.sleep(50);
System.out.println(test1.count);//20000
}
}
上面是一段使用多线程进行累加的代码,当不加锁的时候最后的累加结果将会小于等于预期结果,可见锁的重要性,需要注意的是,锁的对象必须要是同一个才会生效,不然不会生效的。
2.volatile,最轻量的同步/通信机制
在 Java 中,volatile
是一种关键字,用于声明一个变量为“易变”的。当一个变量被声明为 volatile
,它会具有一些特殊的属性,主要涉及多线程之间的可见性和禁止指令重排序。
volatile
主要用于确保多线程环境下的共享变量能够正确地被访问和修改,以避免线程之间的数据不一致问题。以下是volatile
的一些特性:
-
可见性:
volatile
变量在一个线程中被修改后,会立即被写回主内存,并通知其他线程。其他线程读取这个volatile
变量时,会从主内存中读取最新的值,确保所有线程都能看到最新的修改。 -
禁止指令重排序:
volatile
变量的读写操作会建立一个内存屏障,这会禁止指令重排序。这确保了在volatile
变量之前的指令不会被重排序到volatile
变量之后,从而保证了变量的写操作先于读操作。
public class VolatileExample {
private volatile boolean flag = false;
public void toggleFlag() {
flag = !flag;
}
public boolean isFlag() {
return flag;
}
public static void main(String[] args) {
VolatileExample example = new VolatileExample();
Thread writerThread = new Thread(() -> {
example.toggleFlag();
System.out.println("Flag set to true");
});
Thread readerThread = new Thread(() -> {
while (!example.isFlag()) {
// 等待
}
System.out.println("Flag is true");
});
writerThread.start();
readerThread.start();
}
}
上诉这个例子就是通过volatile 保证了flag的可见性,当一个线程改变flag的值后,另一个线程可以立马察觉,并执行它的业务逻辑代码,如果不用volatile,另一个线程则无法实时获取flag的值,会有并发问题!
3.等待/通知机制
等待通知范式:
等待(Wait):一个线程调用某个对象的 wait()
方法,使自己进入等待状态,释放对该对象的锁。线程会一直等待,直到其他线程调用相同对象的 notify()
或 notifyAll()
方法。
通知(Notify):一个线程调用某个对象的 notify()
或 notifyAll()
方法,唤醒一个或多个等待在该对象上的线程。被唤醒的线程会重新尝试获取该对象的锁,并继续执行。
等待与通知范式的基本使用步骤如下:
-
获得对象锁:在等待线程和通知线程之间需要共享同一个对象,确保它们在操作时都获得了对象的锁。
-
等待条件:在等待线程中,使用
wait()
方法进入等待状态,等待某个条件的满足。 -
通知条件:在通知线程中,使用
notify()
或notifyAll()
方法唤醒等待的线程。notify()
唤醒一个等待线程,notifyAll()
唤醒所有等待线程。 -
释放对象锁:被唤醒的线程重新尝试获取对象的锁,从而继续执行。
import java.util.LinkedList;
import java.util.Queue;
/**
* @Author coder huang
* @Date 2023 08 08 15 56
**/
public class WaitNotifyExample {
public static void main(String[] args) {
final Object lock = new Object();
final int CAPACITY = 5;
Queue<Integer> queue = new LinkedList<>();
Thread producer = new Thread(() -> {
synchronized (lock) {
while (queue.size() >= CAPACITY) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.offer(888);
System.out.println("Produced 1, Queue size: " + queue.size());
lock.notifyAll();
}
});
Thread consumer = new Thread(() -> {
synchronized (lock) {
while (queue.isEmpty()) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int value = queue.poll();
System.out.println("Consumed " + value + ", Queue size: " + queue.size());
lock.notifyAll();
}
});
producer.start();
consumer.start();
}
}
这段代码就模拟了生产者和消费者 producer
线程负责生产数据,consumer
线程负责消费数据。通过 wait()
和 notifyAll()
方法,实现了生产者和消费者之间的协作,确保队列满时生产者等待,队列空时消费者等待。
注意:尽可能使用 notifyAll()
,谨慎使用 notify()
,因为 notify()
只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。
面试题:调用 yield() 、sleep()、wait()、notify()等方法对锁有何影响?
-
yield()
、sleep()
被调用后,都不会释放当前线程所持有的锁。 -
调用
wait()
方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait
方法后面的代码。 -
调用
notify()
系列方法后,对锁无影响,线程只有在syn
同步代码执行完后才会自然而然地释放锁,所以notify()
系列方法一般都是syn
同步代码的最后一行。