持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
本文的主要围绕着下面这个问题展开的,在阅读之前可以先自己思考一下问题的答案是什么?
synchronized
的实现原理大致是什么样的?- 你能想到多少
synchronized
的加锁误区呢?
一、基础原理
在谈到误区之前,我们先了解一下synchronized
的基本实现原理,在JDK6之前,synchronized
都是重量级锁,也就是通过操作系统加锁,这时候可能会导致加锁花费的系统时间比加锁以外需要使用的时间还要多,性能偏低。从JDK6开始,开始进行大量优化,引入偏向锁,自选锁,轻量级锁等,性能得到很大的提升。
在HotSpot的实现中,当线程第一访问对象时,会被对象头中的MarkWord中记录下线程的ID,当线程再次访问同步代码块时,发现其中记录的是自己的线程ID,那么可以直接进入同步代码块,这里也是偏向锁。
当有其他线程来竞争资源时,那么锁就要升级为轻量级锁,当有线程在执行同步代码块时,后来的线程并不会进入等待队列,而是不断的尝试获取锁,这就是自选锁,自己不断的旋转转圈,这里要消耗CPU资源的。
如果尝试10次之后还没有获取执行同步代码块的机会,那么这时候就需要升级为重量级锁,也就是通过操作系统上锁,这时候所有尝试获取锁的线程都变为阻塞状态,也会释放CPU资源。
这里也能获得一些其他结论:同步代码执行时间短,线程数少的时候,可以考虑使用自选锁,减少使用重量级锁带来的时间消耗。当同步代码执行时间长,线程数多的时候,可以考虑使用重量级锁,减少CPU资源的消耗。
二、synchronized加锁的误区
1、在同一个类中,在静态方法和非静态方法上使用synchronized
保证两者同步
这种情况下,这两者的锁标志是不一样的,非静态方法锁的是类的实例,静态方法锁的是类,这时候两者是可以同时执行的。
示例代码
public class T {
/**
* 相当于方法的代码执行时要synchronized(this)
*/
public synchronized void m1() {
System.out.println(Thread.currentThread().getName() + "m1 start");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "m1 end");
}
/**
* 相当于方法的代码执行时要synchronized(T.class)
*/
public synchronized static void m2() {
System.out.println(Thread.currentThread().getName() + "m2 start");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "m2 end");
}
public static void main(String[] args) {
T t = new T();
// 两者使用的不是同一个锁,方法1执行完之前方法2就开始执行了
new Thread(t::m1, "t1").start();
new Thread(T::m2, "t2").start();
}
}
2、创建Lock对象,代码使用 synchronized (lock)
和lock.lock()
的混合双打保证不同的方法同步
这两者加锁的原理也是不一样的,synchronized
会把lock实例当作锁标志,lock方法加锁时,锁的是其他东西。
示例代码
public class T {
private final Lock lock = new ReentrantLock();
public void m1() {
System.out.println(Thread.currentThread().getName() + " m1 start");
synchronized (lock) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m1 end");
}
}
public void m2() {
lock.lock();
System.out.println(Thread.currentThread().getName() + " m2 start");
try {
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + " m2 end");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
T t = new T();
// 此处两者使用的不是同一把锁
new Thread(t::m1, "t1").start();
new Thread(t::m2, "t2").start();
}
}
3、使用实体类加锁保证静态变量的同步
这里是行不通的,当加锁的是对象时,锁只能保证对象的变量是同步的。此时如果有其他线程修改静态变量时,就会导致该线程访问静态变量时获取到错误的值。
示例代码
public class T {
private static volatile int count = 10;
public void m() {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " count = " + count);
--count;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
public static void main(String[] args) {
// 此处锁的颗粒只限于实例本身,修改静态变量时无法保证同步
for (int i = 0; i < 10; i++) {
new Thread(new T()::m, "t" + i).start();
}
}
}
4、使用String,Integer(-128~127),Boolean等对象作为锁
在这里String有常量池,Integer也有缓冲池等,这会导致本来执行顺序的代码,因为使用了缓冲池中的同一个变量作为锁标志,变得有先后次序,出现不可预期的后果。
示例代码
public class T {
public static void main(String[] args) {
// T1 和 T2 没有顺序性,因为使用了同一把锁,变成了先后顺序执行
// Boolean、封包的Integer对象(-128~127)、String常量等可能被重用的对象
new Thread(new T1()::m, "T1").start();
new Thread(new T2()::m, "T2").start();
}
}
class T1 {
private final Integer lock = 10;
public void m() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " T1 start");
try {
for (int i = 0; i < 3; i++) {
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " T1 end");
}
}
}
class T2 {
private final Integer lock = 10;
public void m() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " T2 start");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " T2 end");
}
}
}
5、程序出现异常,synchronized锁会被释放吗?
程序在执行过程中,如果出现异常,默认情况锁会被释放,在第一个线程抛出异常,其他线程就会进入同步代码块,有可能访问到异常产生的数据,所以这里要注意数据的一致性
/**
* 程序在执行过程中,如果出现异常,默认情况锁会被释放
* 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况
* 比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适
* 在第一个线程抛出异常,其他线程就会进入同步代码块,有可能访问到异常产生的数据
* 因此要非常小心的处理同步业务逻辑中的异常
*/
public class T {
int count = 0;
synchronized void m() {
System.out.println(Thread.currentThread().getName() + " start");
while (true) {
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5) {
// 此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
int i = 1 / 0;
System.out.println(i);
}
}
}
public static void main(String[] args) {
T tx = new T();
Runnable runnable = tx::m;
new Thread(runnable, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(runnable, "t2").start();
}
}
6、同步方法和非同步方法可以同时调用执行吗?
当然是可以的呀,你上厕所蹲坑的时候会影响别人在你后面擦马桶吗?
/**
* 同步方法和非同步方法可以同时调用执行吗?
*/
public class T {
public synchronized void m1() {
System.out.println(Thread.currentThread().getName() + "m1 start");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "m1 end");
}
public void m2() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "m2 end");
}
public synchronized void m3() {
System.out.println(Thread.currentThread().getName() + "m3 start");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "m3 end");
}
public static void main(String[] args) {
T t = new T();
new Thread(t::m1, "t1").start();
new Thread(t::m3, "t3").start();
new Thread(t::m2, "t2").start();
}
}