多线程与高并发基础回顾 -- synchronized关键字

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);
        }
    }
}
  1. 使用当前对象锁 this
  public class T {
    
    private int count = 10;
    
    public void m() {
        synchronized(this) { //任何线程要执行下面的代码,必须先拿到this的锁
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
}
  1. 使用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++;
    }
}

  1. 使用字节码对象作为锁对象,其中如果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的底层实现

  1. JDK早期的 重量级 - OS
  2. 后来的改进锁升级的概念:

    1.markword 记录这个线程ID (偏向锁)
    2.如果线程争用:升级为 自旋锁
    3.自选10次后升级为重量级锁 - OS

  3. 自旋锁和系统锁如何取舍

    执行时间短(加锁代码),线程数少,用自旋
    执行时间长,线程数多,用系统锁

后续还会继续补充synchronized的实现方式和锁升级的过程

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。