synchronized介绍
java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间,对于简单的同步块,状态转换的时间可能比用户代码执行的时间还要长,所以synchronized是java语言中一个重量级操作。
synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorenxit两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定的解锁的对象。
根据虚拟机规范的要求,在执行monitorenter指令的时候,首先要尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放了。如果获取对象锁失败,那么当前线程就要阻塞等待,直到对象锁被占有线程释放为止
synchronized同步块对同一个线程来说是可重入的,不会出现自己把自己锁死的情况。
通过以前volatile的介绍我们知道,volatile关键字是只能保证有序性和可见性,但是不能保证原子性,synchronized是能保证原子性、有序性和可见性的。
为什么要使用synchronized
我们来看一个售票的案例
@RunWith(SpringRunner.class)
@SpringBootTest
public class SynchronizedTest {
@Autowired
private ExecutorService newFixThreadPool;
//一百张票
private int ticket = 100;
public void increase() {
//模拟售票
if (ticket == 0) {
System.out.println("票已售罄");
} else {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println("当前剩余票数" + ticket);
}
}
//使用线程池执行售票业务
@Test
public void addTest() {
for (int i = 1; i <= 200; i++) {
Runnable task = new Runnable() {
@Override
public void run() {
increase();
}
};
newFixThreadPool.execute(task);
}
}
}
以上代码会遭成一个问题,那就是超卖的问题,我们来分析一下原因:
在并发条件下,例如只剩下一张票的时候,这个时候多个线程判断是否还有票,这个时候判断的都是大于0,所以就都进入到售票的业务逻辑,所以就会出现超卖的现象。因此我们就可以引入重量级锁synchronized来控制同时只能有一个线程进入到售票的逻辑,这样就不会出现超卖的现象了。
synchronized的三种应用方式
普通同步方法
普通同步方法(实例方法),锁是当前实例对象,进入同步代码前要获得当前实例的锁
@RunWith(SpringRunner.class)
@SpringBootTest
public class SynchronizedTest {
@Autowired
private ExecutorService newFixThreadPool;
//一百张票
private int ticket = 100;
// 作用于同步方法
public synchronized void increase() {
//模拟售票
if (ticket == 0) {
System.out.println("票已售罄");
} else {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println("当前剩余票数" + ticket);
}
}
//使用线程池执行售票业务
@Test
public void addTest() {
for (int i = 1; i <= 200; i++) {
Runnable task = new Runnable() {
@Override
public void run() {
increase();
}
};
newFixThreadPool.execute(task);
}
}
}
运行结果:
可以看到,已经解决超卖的这个问题。
静态同步方法
静态同步方法,锁是当前类的class独享,进入同步代码前要获得当前类对象的锁。
// 作用于静态方法
public static synchronized void increase() {
//模拟售票
if (ticket == 0) {
System.out.println("票已售罄");
} else {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println("当前剩余票数" + ticket);
}
}
同步代码块
同步代码块,锁是括号里面的对象,对给定对象加锁,进入同步代码块前需要获取给定对象得锁。
// 作用于同步代码块
public void increase() {
synchronized (this) {
//模拟售票
if (ticket == 0) {
System.out.println("票已售罄");
} else {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println("当前剩余票数" + ticket);
}
}
}
其实我们使用平常使用的锁,JVM会自动对其进行一些优化,其中包含偏向锁、轻量级锁、自旋锁和自适应自旋锁、锁消除、锁粗化,想要了解的可以看下我之前发的线程安全的文章>> 线程安全及锁优化
前面有介绍到volatile关键字如果不是很了解的同学可以看下:>>Volatile关键字
文章中有用到线程池,有需要了解的可以看下:>>彻底搞懂线程池
感谢各位大佬的❤️关注+点赞❤️,原创不易,鼓励笔者创作更好的文章