多线程编程(五)——Synchronized关键字详解

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/swadian2008/article/details/99328700

目录

一、变量的线程安全问题

1、方法内的变量为线程安全

2、方法外变量为非线程安全

二、Synchronized方法和锁对象

1、多个对象多个锁

2、Synchronized修饰方法(锁为对象锁)

3、Synchronized锁重入和同步不具备继承性

(1)重入锁

(2)同步不具备继承性

4、Synchronized同步语句块,任一对象监视器

4.1数据类型String的常量池特性

5、静态同步Synchronized方法和Synchronized(class)代码块,类锁

5.1静态Synchronized方法和非静态Synchronized方法的区别

6、Synchronized关键字和内部类及静态内部类


一、变量的线程安全问题

1、方法内的变量为线程安全

java在运行时,数据区、虚拟机 栈或者本地方法栈(执行方法所在的内存区域)是线程私有的,每个线程都有自己的虚拟机栈、存储区和本地方法栈,所以线程在执行方法时,方法内部的变量不会牵扯到共享这一原则,只有共享内存才会牵扯到线程安全问题,方法外变量的内存在堆。

2、方法外变量为非线程安全

脏读一定会出现在操作实例变量(方法外变量)的情况下,是由不同线程争抢实例变量而引发的非线程安全问题。

二、Synchronized方法和锁对象

1、多个对象多个锁

两个线程分别访问同一个类的两个不同实例的相同名称的Synchronized同步方法,执行是异步的。这是因为Synchronized取得的锁都是对象锁,多个对象就具有多个锁,互不干涉。因此要使线程同步,前提是多个线程访问的是同一个对象。

2、Synchronized修饰方法(锁为对象锁)

(1)创建MyService对象

public class MyService {

    // synchronized修饰的同步方法
    public synchronized void serviceA(){
        try {
            System.out.println(Thread.currentThread().getName()+"serviceA方法开始...");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+"serviceA方法结束...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void serviceB(){
        try {
            System.out.println(Thread.currentThread().getName()+"serviceB方法开始...");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+"serviceB方法结束...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

(2)创建执行线程A和B

线程A

public class MyThreadA extends Thread {

    public MyService myService;

    public MyThreadA(MyService myService){
        super();
        this.myService = myService;
    }

    @Override
    public void run() {
        myService.serviceA();
    }

    public static void main(String[] args) throws InterruptedException {
        MyService myService = new MyService();
        MyThreadA myThreadA = new MyThreadA(myService);
        myThreadA.setName("A线程");
        MyThreadB myThreadB = new MyThreadB(myService);
        myThreadB.setName("B线程");
        myThreadA.start();
        myThreadB.start();
    }
}

线程B

public class MyThreadB extends Thread {

    public MyService myService;

    public MyThreadB(MyService myService){
        super();
        this.myService = myService;
    }

    @Override
    public void run() {
        myService.serviceB();
    }

}

执行结果:

实验结果1:A线程先持有MyService对象锁,B线程可以以异步的方式调用MyService对象中的非Synchronized方法。

此时修改MyService对象中serviceB方法为同步方法

public synchronized void serviceB(){
        // 此时方法B也设为同步
        try {
            System.out.println(Thread.currentThread().getName()+"serviceB方法开始...");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+"serviceB方法结束...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

执行结果:

实验结果2:A线程先持有MyService对象锁,B线程这时在调用MyService对象中的Synchronized类型的方法时需要等待,也就是按顺序同步执行。

3、Synchronized锁重入和同步不具备继承性

(1)重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。可重入锁的作用是在一定程度上避免死锁。

测试代码:

创建MyService对象

public class MyService {

    public int i = 10;

    public synchronized void service() {
        try {
            i--;
            System.out.println("父类操作数:i=" + i);
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

创建MyService的子对象

public class Child_Service extends MyService {

    public synchronized void childService() {
        try {
            while (i > 0) {
                // 子类和父类同时操作父类中的变量i,此时子类已经持有了子类的对象锁
                i--;
                System.out.println("子类操作数:i=" + i);
                Thread.sleep(100);
                // 调用父类方法,但此时依然只是持有子类对象的锁(第二次获取锁)
                this.service();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

创建执行线程

public class MyThread extends Thread {


    @Override
    public void run() {
        Child_Service child_service = new Child_Service();
        child_service.childService();
    }

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

打印结果

从上述结果看出,父类和子类依次交替打印,这说明锁是可以重入的,如果锁不能重入的话,父类方法根本就不会执行,也就是说,在持有一个锁的情况下,可以再次获取这个锁,这变是锁的重入性。

通俗来说:当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。

同一线程在调用自己类中其他 synchronized 方法/块或调用父类的 synchronized 方法/块都不会阻碍该线程的执行。就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入。

重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。

(2)同步不具备继承性

子类重写父类Synchronized方法时,如果不在方法前再次使用Synchronized进行修饰,此方法不具备同步性。

4、Synchronized同步语句块,任一对象监视器

(1)使用同步代码块的好处是,把只有需要同步的代码进行同步,提高程序运行效率。

(2)Synchronized(this)代码块中的锁,锁的是当前对象,也就是对象锁。这是因为使用了this作为“对象监视器”,此时的this指定的是当前对象。

(3)Synchronized(非this)使用优点,如果在一个类中有很多个Synchronized方法,方法与方法之间并不存在同步关系,那么就可以使用Synchronized(非this),使得每一个方法都拥有自己锁,进而提高程序运行效率。

Synchronized(非this)使得锁的控制更加的细化,Synchronized(this)锁的是整个对象,那么Synchronized(非this)可以细化到具体方法里边。

java中可以使用“任一对象”作为“对象监视器”来实现同步功能。这个“任一变量”大多数是实例变量或者方法参数。

代码示例:

创建MyService类,其中一个方法为对象监视器(this),另一个方法为非this监视器

public class MyService {

    private String anyString = new String();

    public void a(){
        try {
            // 此锁非this对象锁
            synchronized (anyString){
                System.out.println(Thread.currentThread().getName()+":进入方法A...");
                Thread.sleep(3000);
                System.out.println(Thread.currentThread().getName()+":方法A结束...");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public void b(){
        // this对象锁
        synchronized (this) {
            System.out.println(Thread.currentThread().getName()+":方法B开始...");
            System.out.println(Thread.currentThread().getName()+":方法B结束...");
        }
    }
}

创建线程——线程A:

public class ThreadA extends Thread {

    private MyService myService;

    public ThreadA(MyService myService){
        super();
        this.myService = myService;
    }

    public void run(){
        myService.a();
    }

    public static void main(String[] args) {
        // 代码执行如下
        MyService myService = new MyService();
        ThreadA threadA = new ThreadA(myService);
        ThreadB threadB = new ThreadB(myService);
        threadA.start();
        threadB.start();
    }
}

创建线程——线程B:

public class ThreadB extends Thread {

    private MyService myService;

    public ThreadB(MyService myService){
        super();
        this.myService = myService;
    }

    public void run(){
        myService.b();
    }
}

执行结果:

由于对象监视器不同,所以运行结果就是异步的。

4.1数据类型String的常量池特性

在JVM中具有String常量池缓存的功能,所以使用String作为对象监视器时,需要注意此问题

想了解常量池特性?请看这篇文章:

https://www.cnblogs.com/syp172654682/p/8082625.html

测试代码:

创建MyService类

public class MyService {

    public void method(String str){
        try {
            synchronized (str) {
                System.out.println(Thread.currentThread().getName()+":静态方法A开始...");
                Thread.sleep(3000);
                System.out.println(Thread.currentThread().getName()+":静态方法A结束...");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

创建线程——A线程

public class ThreadA extends Thread {

    private MyService myService;

    public ThreadA(MyService myService){
        super();
        this.myService = myService;
    }

    public void run(){
        myService.method("str");
    }

    public static void main(String[] args) {
        MyService myService = new MyService();
        ThreadA threadA = new ThreadA(myService);
        ThreadB threadB = new ThreadB(myService);
        threadA.start();
        threadB.start();
    }
}

创建线程——B线程

public class ThreadB extends Thread {

    private MyService myService;

    public ThreadB(MyService myService){
        super();
        this.myService = myService;
    }

    public void run(){
        myService.method("str");
    }
}

执行结果:

以上结果说明,两个线程持有的是同一把锁,它们的对象监视器都是“str”,为同一监视器。

为什么会出现这种情况呢?明明是分两次传进去的,接下来看一个小测试

public static void main(String[] args) {
        String strA = "string";
        String strB = "string";
        String strC = new String("string");
        String strD = new String("string");
        System.out.println("字符串A、B相等吗?"+(strA == strB));
        System.out.println("字符串C、D相等吗?"+(strC == strD));
    }

输出结果:

说明在常量池中创建的对象是同一个对象,而使用new关键字创建的变量是重新分配内存的,是两个不同的对象。

5、静态同步Synchronized方法和Synchronized(class)代码块,类锁

关键字Synchronized应用在静态方法上,对Class类进行持锁,类锁和对象锁是两种不同的锁。

测试代码:

创建MyService类:

public class MyService {

    /**
     * 静态方法锁:class锁
     */
    public synchronized static void a(){
        try {
            System.out.println(Thread.currentThread().getName()+":静态方法A开始...");
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName()+":静态方法A结束...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 静态方法锁:class锁
     */
    public synchronized static void b(){
        System.out.println(Thread.currentThread().getName()+":静态方法B开始...");
        System.out.println(Thread.currentThread().getName()+":静态方法B结束...");
    }

    /**
     * 非静态方法锁:对象锁
     */
    public synchronized void c(){
        System.out.println(Thread.currentThread().getName()+":方法C开始...");
        System.out.println(Thread.currentThread().getName()+":方法C结束...");
    }
}

创建线程——A线程:

public class ThreadA extends Thread {

    private MyService myService;

    public ThreadA(MyService myService){
        super();
        this.myService = myService;
    }

    public void run(){
        myService.a();
    }

    public static void main(String[] args) {
        // 执行代码
        MyService myService = new MyService();
        ThreadA threadA = new ThreadA(myService);
        ThreadB threadB = new ThreadB(myService);
        ThreadC threadC = new ThreadC(myService);
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

创建线程——B线程:

public class ThreadB extends Thread {

    private MyService myService;

    public ThreadB(MyService myService){
        super();
        this.myService = myService;
    }

    public void run(){
        myService.b();
    }
}

创建线程——C线程:

public class ThreadC extends Thread {

    private MyService myService;

    public ThreadC(MyService myService){
        super();
        this.myService = myService;
    }

    public void run(){
        myService.c();
    }
}

执行结果:

从执行结果可以看出,持相同类锁的两个静态方法是同步执行的,而持对象锁的方法则与静态方法异步执行,因此,类锁和对象锁并不是同一把锁。

那么这两把锁到底有什么区别呢?

5.1静态Synchronized方法和非静态Synchronized方法的区别

区别:

静态Synchronized方法,多个类对象持一把锁

非静态Synchronized方法,多个对象多把锁

同样还是用上边的MyService类作为测试:

A线程:

public class ThreadA extends Thread {

    private MyService myService;

    public ThreadA(MyService myService){
        super();
        this.myService = myService;
    }

    public void run(){
        myService.a();
    }

    public static void main(String[] args) {
        // 多个MySevice对象,线程A,B同时执行方法a
        MyService myService1 = new MyService();
        MyService myService2 = new MyService();
        ThreadA threadA = new ThreadA(myService1);
        ThreadB threadB = new ThreadB(myService2);
        threadA.start();
        threadB.start();
    }
}

B线程:

public class ThreadB extends Thread {

    private MyService myService;

    public ThreadB(MyService myService){
        super();
        this.myService = myService;
    }

    public void run(){
        myService.a();
    }
}

执行结果:

静态的Synchronized方法,即使在多个对象的情况下,它也是同步执行的,它并不具备多对象多把锁的特征。

6、Synchronized关键字和内部类及静态内部类

关于内部类:

1.内部类中隐含有外部类的引用。所以可以使用外部类中所有的东西。

2.创建内部类对象时,必须与外围类对象相关联,创建外围类对象之前,不可能创建内部类的对象。

特殊情况:静态内部类(也称嵌套类)

1.创建静态内部类是不需要外部类。

2.静态内部类不能访问外部类的非静态成员。

明白以上内部类的知识后,其实对内部类多线程锁的使用也就基本上明白了,内部类可以看成是放在一个类里边的普通类,因此锁的使用,也就跟普通的类是一样的,关键是要区分锁到底使用的是什么样的锁。

然后静态的内部类只是区别这个内部类跟外部类的关系,在使用锁时,性质与普通内一样。

一般,内部类中有两个同步方法,但使用的却是不同锁,那么执行是异步的。

测试代码:

public class MyService {

    static class Child01{
        // 使用Child02对象锁
        public void method01(Child02 child02){
            synchronized (child02) {
                try {
                    System.out.println(Thread.currentThread().getName()+":进入child01的method01方法...");
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName()+":退出child01的method01方法...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        // 持有child01的对象锁
        public synchronized void method02(){
            try {
                System.out.println(Thread.currentThread().getName()+":进入child01的method02方法...");
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+":退出child01的method02方法...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    static class Child02{
        // 使用Child02对象锁
        public synchronized void method01(){
            try {
                System.out.println(Thread.currentThread().getName()+":进入child02的method01方法...");
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+":退出child02的method01方法...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Child01 child01 = new Child01();
        Child02 child02 = new Child02();
        // 线程1
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 传入child02作为锁对象
                child01.method01(child02);
            }
        });
        // 线程2
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 使用child01对象作为锁
                child01.method02();
            }
        });
        // 线程3
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 使用child02对象作为锁
                child02.method01();
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }
}

执行结果:

分析上述执行结果:

线程1和线程0是异步执行的,这是因为两个方法所持有的锁不同,线程1持有的是Child02的锁,线程0持有的是Child01的锁,锁不一样,所以执行异步。

线程2和线程0是同步执行的,它们共同争夺Child02的锁,所以线程2必须等线程1执行完才能继续执行,在线程0执行时,线程2成阻塞状态。

总之,不管是普通内还是内部类,关键看的还是持有的锁对象是否相同,相同则同步执行,不同则异步执行。

猜你喜欢

转载自blog.csdn.net/swadian2008/article/details/99328700