synchronized 用法详解

x33g5p2x  于2021-12-18 转载在 其他  
字(4.0k)|赞(0)|评价(0)|浏览(267)

一、线程不安全实例

我们先来创建一个业务类 TestService,代码如下:

public class TestService {
    private int num;

    public void addName(String username) {
        try {
            if ("a".equals(username)) {
                num = 100;
                System.out.println("ThreadA is over!");
                Thread.sleep(1000);
                System.out.println("ThreadA num = " + num);
            } else {
                num = 200;
                System.out.println("ThreadB is over!");
                System.out.println("ThreadB num = " + num);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

再创建两个线程类 ThreadA 和 ThreadB,代码如下:

public class ThreadA extends Thread {
    private TestService service;

    public ThreadA(TestService service) {
        this.service = service;
    }

    @Override
    public void run(){
        service.addName("a");
    }
}
public class ThreadB extends Thread {
    private TestService service;

    public ThreadB(TestService service) {
        this.service = service;
    }

    @Override
    public void run(){
        service.addName("b");
    }
}

最后创建运行类 Run,代码如下:

public class Run {
    public static void main(String[] args) {
        TestService service = new TestService();
        ThreadA threadA = new ThreadA(service);
        threadA.start();
        ThreadB threadB = new ThreadB(service);
        threadB.start();
    }
}

控制台输出如下:

ThreadA is over!
ThreadB is over!
ThreadB     num = 200
ThreadA     num = 200

从控制台输出可以看出,执行结果和我们预想的不一样,这是因为两个线程并发访问业务对象中的变量 num,所以出现了线程不安全的问题,那么我们该如何解决这个问题呢?很简单,只需要加上关键字 sychronized 即可。

二、解决方案

1. sychronized 同步方法

我们只需要在 TestService 类中的 addName(String username) 方法前加上 sychronized 关键字即可,代码如下:

public synchronized void addName(String username) {
    try {
        if ("a".equals(username)) {
            num = 100;
            System.out.println("ThreadA is over!");
            Thread.sleep(1000);
            System.out.println("ThreadA num = " + num);
        } else {
            num = 200;
            System.out.println("ThreadB is over!");
            System.out.println("ThreadB num = " + num);
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

控制台输出如下:

ThreadA is over!
ThreadA     num = 100
ThreadB is over!
ThreadB     num = 200

2. sychronized 同步代码块

我们可以把 addName(String username) 方法中的部分代码写成代码块格式,然后对代码块加锁,代码如下:

public void addName(String username) {
    try {
        synchronized (this) {
            if ("a".equals(username)) {
                num = 100;
                System.out.println("ThreadA is over!");
                Thread.sleep(1000);
                System.out.println("ThreadA num = " + num);
            } else {
                num = 200;
                System.out.println("ThreadB is over!");
                System.out.println("ThreadB num = " + num);
            }
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

控制台输出如下:

ThreadA is over!
ThreadA     num = 100
ThreadB is over!
ThreadB     num = 200

三、sychronized 详解

在 Java 中只有“将对象作为锁”的说法,并没有“锁方法”的说法,我们可以理解成“锁”就是“对象”,“对象”可以映射为“锁”,哪个线程拿到这把锁,哪个线程就可以执行对应的 sychronized 同步方法或同步代码块。

1. 同步方法

在 Java 中,方法分为非静态方法和静态方法,在使用 sychronized 同步非静态方法和同步静态方法之间是不一样的,区别如下:

在使用 sychronized 同步非静态方法时,使用当前对象作为锁

在使用 sychronized 同步静态方法时,使用当前静态方法所在类对应的 Class 单例对象作为锁

2. 同步方法的弊端

假如某同步方法需要耗费大量时间执行,但其内部只有一小段代码涉及到线程安全的问题,这时就没必要使用 sychronized 同步方法了,我们只需要同步那一小段代码,即使用同步代码块。

3. 同步代码块

在 Java 中,代码块同样分为非静态代码块和静态代码块,但是由于静态代码块不能定义在任何方法(包括静态方法)内,而 sychronized 必须作用于方法上或方法内部,所以静态代码块和 sychronized 是不可能会见面的,所以也就谈不上使用了,况且静态代码块只会在类加载时执行,且只会执行一次,也就谈不上线程安不安全的问题了。

同步代码块可以使用任意对象作为锁

public void method() {
    synchronized (object) {
    	// object 可以是任何对象,如:this,类对象,字符串对象等所有对象
    }
}

4. 可重入锁

sychronized 具有锁重入的功能,即在一个线程得到一个对象锁后,再次请求此对象锁是可以得到该对象锁的,这也说明在一个 sychronized 方法 / 代码块内部调用本类的其他 sychronized 方法 / 代码块时,时永远可以得到锁的。

锁重入支持继承的环境,当存在父子类继承关系时,子类可以通过锁重入调用父类的同步方法,示例代码如下:

public class Run {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        Son son = new Son();
        son.sonMethod();
    }
}

class Son extends Person {
    public synchronized void sonMethod() {
        while (i > 0) {
            i--;
            System.out.println("Son " + i);
            super.personMethod();
        }
    }
}

class Person {
    public int i = 10;
    
    public synchronized void personMethod() {
        i--;
        System.out.println("Person " + i);
    }
}

控制台输出如下:

Son    9
Person 8
Son    7
Person 6
Son    5
Person 4
Son    3
Person 2
Son    1
Person 0

5. 出现异常,锁自动释放

6. 重写方法

重写方法如果不使用 synchronized 关键字,就不是同步方法了,可能会出现线程安全问题,在使用了 sychronized 方法后,才会变成同步方法。

四、sychronized 常见用法对比

常用写法代码如下:

public class MyService {
    public synchronized static void method1() {
    }
    
    public void method2() {
        synchronized (MyService.class) {
        }
    }
    
    public synchronized void method3() {
    }
    
    public void method4() {
        synchronized (this) {
        }
    }
    
    public void method5() {
        synchronized ("hello") {
        }
    }
}
  • method1() 和 method2() 持有的锁是同一个,即当前类 MyService 对应的 Class 对象
  • method3() 和 method4() 持有的锁是同一个,即当前对象
  • method5() 持有的锁是字符串 hello

相关文章