Synchronized关键字

一、Synchronized的作用

作用:能够保证在 同一时刻最多只有 一个线程执行该代码,以达到保证并发安全的效果
public class DisappearRequest implements Runnable{
    
    static DisappearRequest dr = new DisappearRequest();
    
    static int count = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(dr);
        Thread t2 = new Thread(dr);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count="+count);
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }

}
// 结果count小于20000(线程不安全)

二、Synchronized的两个用法

1. 对象锁:包括同步代码块锁(自己指定锁对象)和方法锁(默认锁对象为this当前实例对象)

1.1 代码块形式:手动指定锁对象
public class SynchronizedObjectCodeBlock implements Runnable {
    
    static SynchronizedObjectCodeBlock instance = 
            new SynchronizedObjectCodeBlock();
    
    
    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        
        t1.start();
        t2.start();
        
        while(t1.isAlive() || t2.isAlive()){
            
        }
        System.out.println("finished");
    }

    @Override
    public void run() {
        // 两个线程串行操作
        synchronized(this){
            System.out.println("我是对象锁的代码块形式。我叫:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
    }

}

public class SynchronizedObjectCodeBlock implements Runnable {
    
    static SynchronizedObjectCodeBlock instance = 
            new SynchronizedObjectCodeBlock();
    
    Object lock1 = new Object();
    Object lock2 = new Object();
    
    
    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        
        t1.start();
        t2.start();
        
        while(t1.isAlive() || t2.isAlive()){
            
        }
        System.out.println("finished");
    }

    @Override
    public void run() {
        synchronized(lock1){
            System.out.println("我是对象锁的代码块形式。我叫:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
        
        synchronized(lock2){
            System.out.println("我是对象锁的代码块形式。我叫:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
    }

}
// CountDownLatch、信号量解决线程同步问题。
1.2 方法锁形式:synchronized修饰普通方法,锁对象默认为this
普通方法锁
public class SynchronizedMethodLock implements Runnable{

    static SynchronizedMethodLock instance = 
            new SynchronizedMethodLock();
    
    @Override
    public void run() {
        sync();
    }
    
    // 普通方法锁(不能是静态方法。锁对象默认是this)
    public synchronized void sync(){
        System.out.println("我是普通方法锁形式。我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");
    }
    
    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        
        t1.start();
        t2.start();
        
        while(t1.isAlive() || t2.isAlive()){
            
        }
        System.out.println("finished");
    }
    
    

}

2. 类锁:指synchronized修饰静态方法或指定锁为Class对象。Java类可能有很多个对象,但只有一个Class对象。
所谓的类锁,不过是Class对象的锁而已。
用法和效果:类锁只能在同一时刻被一个对象拥有,其他对象会被阻塞
对象锁如果不同的实例创建出来的,互相锁是不受影响的,你可以运行我也可以运行,并行运行,但是类锁只有一个可以运行。

2.1. synchronized加在static方法上。 场景:如果需要在全局情况下同步该方法,而不是一个小范围层面,则应该用这种形式去做同步保护。
public class SynchronizedMethodLock implements Runnable{

    // 创建两个实例对象
    static SynchronizedMethodLock instance1 = 
            new SynchronizedMethodLock();
    static SynchronizedMethodLock instance2 = 
            new SynchronizedMethodLock();
    
    @Override
    public void run() {
        sync();
    }
    
    // 创建两个实例对象,如果不是static方法的话,则并行操作,否则串行执行。
    public static synchronized void sync(){
        System.out.println("我是类锁的第一种形式:static形式。我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");
    }
    
    public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        
        t1.start();
        t2.start();
        
        while(t1.isAlive() || t2.isAlive()){
            
        }
        System.out.println("finished");
    }
    
    

}
2.2. synchronized(*.class)代码块
public class SynchronizedMethodLock implements Runnable{

    // 创建两个实例对象
    static SynchronizedMethodLock instance1 = 
            new SynchronizedMethodLock();
    static SynchronizedMethodLock instance2 = 
            new SynchronizedMethodLock();
    
    @Override
    public void run() {
        sync();
    }
    
    // 创建两个实例对象,如果不是static方法的话,则并行操作,否则串行执行。
    public void sync(){
        // 如果是this的话则并行执行,Class对象则串行执行
        synchronized(SynchronizedMethodLock.class){
            System.out.println("我是类锁的第二种形式:synchronized(*.class)形式。我叫:" + Thread.currentThread().getName());
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "运行结束");
            }
    }
    
    public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        
        t1.start();
        t2.start();
        
        while(t1.isAlive() || t2.isAlive()){
            
        }
        System.out.println("finished");
    }
    
    

}

三、线程不安全导致请求丢失问题解决

场景:前面【 一、Synchronized的作用 】中的demo计数场景。
如下四种方式解决(结果均为20000):
3.1
@Override
    public synchronized void run() {
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }
3.2
@Override
    public void run() {
        synchronized (this){
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        }
    }
3.3
@Override
    public void run() {
        synchronized(DisappearRequest.class){
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        }
    }
3.4
@Override
    public void run() {
        add();
    }
    
    public static synchronized void add(){
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }

四、多线程访问同步方法的7中情况

4.1、 两个线程同时访问一个对象的同步方法

两个线程争抢的是同一把锁,线程间相互等待,只能有一个线程持有该锁
public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while(t1.isAlive() || t2.isAlive()){
        }
        System.out.println("finished");
    }

4.2、 两个线程访问的是两个对象的同步方法

创建两个实例对象,如果不是static方法的话,则并行操作,否则串行执行,这两个实例真正采用的锁对象不是同一个,所以不会被干扰。
// 创建两个实例对象,如果不是static方法的话,则并行操作,否则串行执行。
    public void sync(){
        // 如果是this的话则并行执行,指向的是不同的实例对象,若为Class对象则串行执行
        synchronized(this){
        // TO DO...
        }
    }
public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();
        while(t1.isAlive() || t2.isAlive()){    
        }
        System.out.println("finished");
    }

4.3、 两个线程访问的synchronized的静态方法

如果两个线程访问的是同一个对象的同步方法则串行执行,如果访问的是不同对象的同步方法,若该方法是非静态static方法则并行执行,否则两个线程访问的锁对象为同一把锁,串行执行。
    public static synchronized void sync(){
    // TO DO...
    }
    
    public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        
        t1.start();
        t2.start();
        
        while(t1.isAlive() || t2.isAlive()){
            
        }
        System.out.println("finished");
    }

4.4 同时访问同步方法合肥同步方法

非同步方法不受到影响

4.5 访问同一个对象的不同的普通同步方法

同一个对象,两个同步方法拿到的this是一样的,同一把锁,所以串行执行

4.6 同时访问静态synchronized和非静态synchronized方法

两个线程指定的锁对象不是同一把锁,所以锁之间不冲突,并行执行
// 创建两个实例对象,如果不是static方法的话,则并行操作,否则串行执行。
    public static synchronized void sync(){
        // 如果是this的话则并行执行,Class对象则串行执行
        System.out.println("我是静态方法:synchronized(*.class)形式。我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");
    }
    
    // 创建两个实例对象,如果不是static方法的话,则并行操作,否则串行执行。
    public synchronized void sync1(){
        // 如果是this的话则并行执行,Class对象则串行执行
        System.out.println("我是非静态方法:synchronized(*.class)形式。我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");
    }
    
    public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance1);
        
        t1.start();
        t2.start();
        
        while(t1.isAlive() || t2.isAlive()){
            
        }
        System.out.println("finished");
    }

4.7 方法抛异常后,是否会释放锁

抛出异常之后jvm会释放锁,后面的线程会进入同步方法。
// 创建两个实例对象
    static SynchronizedMethodLock instance1 = 
            new SynchronizedMethodLock();
    static SynchronizedMethodLock instance2 = 
            new SynchronizedMethodLock();
    
    @Override
    public void run() {
        if(Thread.currentThread().getName().equals("Thread-0")){
            sync();
        }else{
            sync1();
        }
    }
    
    // 创建两个实例对象,如果不是static方法的话,则并行操作,否则串行执行。
    public synchronized void sync(){
        // 如果是this的话则并行执行,Class对象则串行执行
        System.out.println("我是方法1:我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        throw new RuntimeException();
//        System.out.println(Thread.currentThread().getName() + "运行结束");
    }
    
    // 创建两个实例对象,如果不是static方法的话,则并行操作,否则串行执行。
    public synchronized void sync1(){
        // 如果是this的话则并行执行,Class对象则串行执行
        System.out.println("我是方法2:我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");
    }
    
    public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance1);
        
        t1.start();
        t2.start();
        
        while(t1.isAlive() || t2.isAlive()){
            
        }
        System.out.println("finished");
    }

Synchronized关键字_第1张图片

五、synchronized的性质

【5.1 可重入】:指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁
好处:避免死锁,提升封装性
比如:现在有两个均被synchronized修饰的方法f1和f2,此时线程A执行到了f1,并且获得了这把锁,由于方法二f2也是synchronized修饰(也就是说要执行f2必须拿到f2的锁),如果synchronized不具备可重入性,此时线程A只是拿到了f1的锁,没有拿到f2的锁,所以线程A即想拿到f2的锁又不释放f1锁,那么就会造成永远等待,即死锁,所以synchronized是具有可重入性。

【可重入粒度如下】

5.1.1 证明同一个方法是可重入的(递归)

5.1.2 证明可重入不要求是同一个方法

public synchronized void method1(){
        System.out.println("我是method1");
        method2();
    }
    public synchronized void method2(){
        System.out.println("我是method2");
    }
5.1.3 证明同可重入不要求是同一个类中的
public class SyncSuperClass{
        public synchronized void doSomething(){
            System.out.println("我是父类方法");
        }
    }
    
    class TestClass extends SyncSuperClass{
        public synchronized void doSomething(){
            System.out.println("我是子类方法");
            super.doSomething();
        }
    }

【5.2 不可中断性】

一旦这个锁已经被别人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁。如果别人永远不释放锁,那么我只能永远等下去。
Lock类:相比之下,Lock类,可以拥有中断的能力,第一点,如果我觉得我等的时间太长了,有权中断现在已经获取到锁的线程的执行;第二点,如果我觉得等待时间太长了不想等了,可以退出。
Lock lock = new ReentrantLock(); 
    // 下面这两种形式的锁是等价的
    public synchronized void method1(){
        System.out.println("我是Synchronized形式的锁");
    }
    
    public void method2(){
        lock.lock();
        try{
            System.out.println("我是Lock形式的锁");
        }finally{
            lock.unlock();
        }
    }

六、synchronized的缺陷

【6.1 效率低】

锁的释放情况少,试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程。
6.1.1、 当一个线程获得了对应的sync锁的时候,其他线程只能等待我释放之后才能获取该锁。
6.1.2、 只有两种情况才释放锁:1.执行完了这段代码,2.发生异常自动释放锁
6.1.3、 不能中断,但是Lock是有中断能力的

【6.2 不够灵活(如:读写锁比较灵活:读的时候不加锁,写才加锁)】

加锁和释放锁的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的

【6.3 无法知道是否成功获取到锁】

七、Lock锁常用方法

    Lock lock = new ReentrantLock(); // 非公平锁
    // Lock lock = new ReentrantLock(true); // 公平锁
    // Lock lock = new ReentrantReadWriteLock();
        
        lock.lock();
        lock.unlock();
        lock.tryLock(); // 获取锁
        lock.tryLock(10, TimeUnit.SECONDS);

八、常见面试题

8.1 使用synchroinzed注意点:锁对象不能为空、作用域不宜过大、避免死锁
注:一个对象作为锁对象,这个对象必须是被new过的,或者是被其他方法创建好的,而不是一个空对象,因为锁的信息保存在对象头中的。

8.2 如何选择Lock和synchronized关键字
尽量使用concurrent包下的CountDownLatch或者atomic包,或者信号量

九、思考

9.1 多个线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的是哪个县城?
9.2 synchronized使得同时只能有一个线程可以执行,性能较差,有什么办法可以提升性能?
9.3 我想更灵活的控制锁的获取和释放(现在释放锁的时机都被规定死了),怎么办?
9.4 什么是锁的升级、降级?什么事JVM里的偏斜所、轻量级锁。重量级锁?

你可能感兴趣的:(java)