初识 synchronized 关键字
synchronized 提供了一种排他机制,也就是在同一时间只能有一个线程执行某些操作。也就是说如果多个线程对index变量(共享变量/资源同时操作)而引起的数据不一致的问题,我们可以使用该关键字去解决。
什么是 synchronized?
synchronized 关键字可以实现一个简单的策略来防止线程干扰 和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都将通过同步的方式来进行。
synchronized 关键字的用法
synchronized 可以用于对代码块或方法进行修饰,而不能够用于对 class 以及变量 进行修饰。
同步方法
就是在方法前面加上synchronized 关键字。
public synchronized void sync() { ... }
public synchronized static void staticSync() { ... }
同步代码块
private final Object MUTEX = new Object();
public void sync() { synchronized (MUTEX) { ... } }
举个例子:模拟大厅叫号,如果不使用synchronized关键字来保证数据的一致性,那么该程序就会出现数据不一致性,比如号的重复和超过最大值。
/**
* 具体策略类 叫号机
*/
public class CounterWindowRunnable03 implements Runnable {
// 最多受理 500 笔业务
private static final int MAX = 500;
// 起始号码,不做 static 修饰
private int index = 1;
private static final Object MONITOR = new Object();
@Override
public void run() {
while (true) {
synchronized (MONITOR) {
if (index > MAX)
break;
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.format("请【%d】号到【%s】办理业务\n", index++, Thread.currentThread().getName());
}
}
}
public static void main(String[] args) {
final CounterWindowRunnable03 task = new CounterWindowRunnable03();
new Thread(task, "一号窗口").start();
new Thread(task, "二号窗口").start();
new Thread(task, "三号窗口").start();
new Thread(task, "四号窗口").start();
}
}
而加了synchronized 关键字之后,程序不会出现数据不一致的情况。
深入 synchronized 关键字
线程堆栈分析
synchronized 关键字提供了一种互斥机制,也就是说在同一时刻,只能有一个线程访问同步资源。下面看代码:
public class Mutex {
private static final String MUTEX = new String();
private void accessResource() {
synchronized (MUTEX) {
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final Mutex mutex = new Mutex();
for (int i = 0; i < 5; i++) {
new Thread(mutex::accessResource).start();
}
}
}
上面的代码中定义了一个方法 accessResource,并且使用同步代码块的方式对 accessResource 进行了同步,同时定义了5个线程调用 accessResource 方法,由于同步代码块的互斥性,只能有一个线程获取了mutex monitor 的锁,其他线程只能进入阻塞状态,等待获取 mutex monitor 锁的线程对其进行释放,运行上面的程序然后打开 JConsole 工具监控,如下图所示。
选中我画出来的本地进程,然后点击【连接】按钮进入 JConsole 控制台,将 tab 切换至【线程】,如上图所示。
随便选中程序中创建的某个线程,会发现只有个线程在 IMED WAITING(sleeping)状 态,其他线程都进人了 BLOCKED 状态,如下图所示。
使用 jstack 命令打印进程的线程堆栈信息
Thread-0 持有 monitor <0x00000000d7f8a910>的锁并且处于休眠状态中,那么其他线程将会无法进入 accessResource 方法。
Thread-1 线程进入 BLOCKED 状态并且等待着获取 monitor <0x00000000d7f8a910> 的锁,其他的几个线程同样也是 BLOCKED状态。
使用 synchronized 需要注意的问题
与 monitor 关联的对象不能为空
private static final Object MUTEX = null;
private void accessResource() {
synchronized (MUTEX) {
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Mutex 为 null,很多人还是会犯这么简单的错误,每一个对象和一个 monitor 关联, 对象都为 nul l了,monitor肯定就不存在了。
synchronized 作用域太大
由于 synchronized 关键字存在排他性,也就是说所有的线程必须串行地经过 synchronized 保护的共享区域,如果 synchronized 作用域越大,则代表着其效率越低, 甚至还会丧失并发的优势。
@Override
public synchronized void run() {
//
}
上面的代码对整个线程的执行逻辑单元都进行了 synchronized 同步,从而丧失了并发的能力,synchronized 关键字应该尽可能地只作用于共享资源(数据)的读写作用域。
不同的 monitor 企图锁相同的方法
public class Task implements Runnable {
private final Object MUTEX = new Object();
@Override
public void run() {
synchronized (MUTEX) { }
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(Task::new).start();
}
}
}
上面的代码构造了五个线程,同时也构造了五个 Runnable 实例,Runnable 作为线程逻辑执行单元传递给Thread。但是,synchronized 根本互斥不了与之对应 的作用域,线程之间进行 monitor lock 的争抢只能发生在与 monitor 关联的同一个引用上,上面的代码每一个线程争抢的 monitor 关联引用都是彼此独立的,因此不可能起到互斥的作用。
多个锁的交叉导致死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成 的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系 统产生了死锁,这些永远在互相等待的进程称为死锁进程。死锁代码如下:
AService:
public class AService {
private BService bService;
private final Object LOCK = new Object();
public void m1() {
synchronized (LOCK) {
System.out.println("AService m1");
bService.s1();
}
}
public void m2() {
synchronized (LOCK) {
System.out.println("AService m2");
}
}
public void setbService(BService bService) {
this.bService = bService;
}
}
BService:
public class BService {
private AService aService;
private final Object LOCK = new Object();
public void s1() {
synchronized (LOCK) {
System.out.println("BService s1");
}
}
public void s2() {
synchronized (LOCK) {
System.out.println("BService s2");
aService.m2();
}
}
public void setaService(AService aService) {
this.aService = aService;
}
}
DeadLockTest:
public class DeadLockTest {
public static void main(String[] args) {
AService aService = new AService();
BService bService = new BService();
aService.setbService(bService);
bService.setaService(aService);
new Thread(() -> {
while (true)
aService.m1();
}).start();
new Thread(() -> {
while (true)
bService.s2();
}).start();
}
}
该程序就会产生死锁。
总结:
好了,本次分享就到这里,下一节,我会给大家详细讲解一下锁对象。