前言
平时的开发中,我们经常与多线程接触,有时候某个变量是可以多个线程共享的,那么很有可能就会引入同步问题,使用synchronized关键字,将存在同步问题的代码块或者方法修饰起来,便是我们经常用来解决多线程同步的问题,下面我们一起看看它的使用及原理。
正文
1.同步问题:
private static void test() {
Runnable task = new Runnable() {
int count = 10;
@Override
public void run() {
//a
if (count > 0) {
//b
System.out.println("count:" + count);
//c
--count;
}
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
}
像上面的例子,就有可能出现同步问题,比如输出了count:0,这里分析一下为什么会出现同步问题:
如过count=1时,t1线程执行到b处,然后被t2线程抢占了,这个时候t2线程执行完b、c,这时c=0,然后t1恢复了,这时就会输出了count:0,很明显不符合预期。
那么我们如果使用synchronized,那么就可以防止这种情况发生,下面我们一起看看它的使用。
2.synchronized介绍
每个Java对象都可以作为锁,如果有线程访问同步代码,那么会先尝试获取对象锁,等到退出同步代码或者出现异常时才释放掉锁。
3.synchronized的使用场景
- 同步方法
private synchronized void fun() {
//
}
同步方法是使用synchronized关键字修饰的方法,同步锁则是这个对象本身,即this。
- 同步代码块
final Object lock = new Object();
private void fun() {
synchronized (lock) {
//
}
}
同步代码块则需要显式制定锁对象
- 静态同步方法
private static synchronized void fun() {
//
}
静态同步方法锁对象则是class对象T.class。
4.原理
- 同步代码块:编译后,将monitorenter指令插入到代码块的开始处,然后将monitorexit指令插入到代码块结束处或者异常处。每个对象都有一个关联的monitor,当遇到monitorenter时,就需要去获取monitor,获取不到则进入阻塞队列,等待这个monitor被持有者释放后的唤醒通知。
- 同步方法:在method_info的结构里,有ACC_Sychronized标记,线程执行时如果识别到这个标记,则会去获取对应的锁。
这两者的细节不一样,但是本质上都是尝试获取对象的monitor,如果获取不到,则线程会被阻塞(状态为BLOCKED),进入同步队列。当持有monitor的线程释放了锁后,就会唤醒阻塞在同步队列的线程,然后大家重新尝试获取monitor。
5.特性
- 重入锁
当一个线程获得对象锁时,可以再一次获取该对象锁,synchronized就支持:
final Object lock = new Object();
private void fun2() {
synchronized (lock) {
fun1();
}
}
private void fun1() {
synchronized (lock) {
}
}
以上的fun2获取到lock对象锁后,调用fun1,fun1里边再次获取lock对象锁。
- 非公平锁
CPU调度线程的时候,会在等待队列中随机选一个线程,这样就没办法保证先到先得,有的线程(优先级比较低)可能永远都没发获取到CPU到执行权,会造成饥饿现象。公平锁就会保证线程按照时间的先后顺序执行,避免饥饿现象的产生。但synchronized控制的锁是非公平锁。
结语
本文的分享到这里就结束了,内容涵盖了synchronized的使用和原理,当然我们在平时的使用中,要合理考量,避免比较粗暴地加上synchronized来防止同步问题,应该需要在合理的粒度上加。