1.synchronized的锁对象
synchronized关键字其作用是对某个对象加锁,当线程执行到synchronized同步的方法或者代码块时,会先去看是否可以获得锁对象,可以获得锁对象后再执行对应代码块
image.png
实例代码:
1.使用普通对象锁 Object o = new Object();
public class T {
private int count = 10;
private Object o = new Object();
public void m() {
synchronized(o) { //任何线程要执行下面的代码,必须先拿到o的锁
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}
- 使用当前对象锁 this
public class T {
private int count = 10;
public void m() {
synchronized(this) { //任何线程要执行下面的代码,必须先拿到this的锁
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}
- 使用synchronized修饰方法,在这个类中,m()必须争抢锁执行,在m()执行过程中,n()可以同时执行,因为n()不是同步方法
public class T1 {
private int count = 10;
public synchronized void m() { //等同于在方法的代码执行时要synchronized(this)
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public void n() { //访问这个方法的时候不需要上锁
count++;
}
}
- 使用字节码对象作为锁对象,其中如果synchronized修饰静态方法,则锁对象为T的字节码对象
public class T {
private static int count = 10;
public synchronized static void m() { //这里等同于synchronized(FineCoarseLock.class)
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void mm() {
synchronized(T.class) { //考虑一下这里写synchronized(this)是否可以?
count --;
}
}
}
2. synchronized使用效果
public class T implements Runnable {
private int count = 100;
public /*synchronized*/ void run() {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void main(String[] args) {
T t = new T();
for(int i=0; i<100; i++) {
new Thread(t, "THREAD" + i).start();
}
}
}
当run()是非同步方法时,最后的执行结果count !=0,和我们代码的执行预期不等,当run()是同步方法时,count==0,说明synchronized可以有效的解决线程安全的问题。
3. 同步和非同步方法是否可以同时调用
代码示例:
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 ");
}
public static void main(String[] args) {
T t = new T();
new Thread(t::m1, "t1").start();
new Thread(t::m2, "t2").start();
}
}
通过执行该代码,依次显示为:m1 start...,m2,m1 end
由此可得出结论,当m1同步方法执行时,m2非同步方法也可同时执行。
面试题:模拟银行账户
对业务写方法加锁,对业务读方法不加锁,这样行不行?
容易产生脏读问题(dirtyRead),但是实际的选择还需要结合业务场景
double balance;
String name;
//写方法
public synchronized void set(String name, double balance) {
this.name = name;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance = balance;
}
//读方法
public /*synchronized*/ double getBalance(String name) {
return this.balance;
}
4. synchronized是否是可重入锁
一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁,这就是可重入锁
示例1:调用其他同步方法
synchronized void m1() {
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
m2();
System.out.println("m1 end");
}
synchronized void m2() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m2");
}
public static void main(String[] args) {
new T().m1();
}
执行后发现,在执行同步方法m1时,也执行了m2(),由此可知synchronized是一种可重入锁
示例2:子类调用父类的同步方法
public class T {
synchronized void m() {
System.out.println("m start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m end");
}
public static void main(String[] args) {
new TT().m();
}
}
class TT extends T {
@Override
synchronized void m() {
System.out.println("child m start");
super.m();
System.out.println("child m end");
}
}
5. 同步代码块中的异常处理
程序在执行过程中,如果出现异常,默认情况锁会被释放所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。因此要非常小心的处理同步业务逻辑中的异常
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) {
int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
System.out.println(i);
}
}
6. synchronized的底层实现
- JDK早期的 重量级 - OS
- 后来的改进锁升级的概念:
1.markword 记录这个线程ID (偏向锁)
2.如果线程争用:升级为 自旋锁
3.自选10次后升级为重量级锁 - OS - 自旋锁和系统锁如何取舍
执行时间短(加锁代码),线程数少,用自旋
执行时间长,线程数多,用系统锁
后续还会继续补充synchronized的实现方式和锁升级的过程