美文网首页
Java基础20-初识synchronized

Java基础20-初识synchronized

作者: Tian_Peng | 来源:发表于2019-08-13 00:01 被阅读0次

    概述

    如何在Java语言中写出线程安全的程序,如何在Java语言中解决非线程安全的相关问题?

    基本上所有的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。

    通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。

    在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。

    Java内置锁

    Java的内置锁:每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁。
    线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。

    Java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。

    Java的对象锁和类锁:java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的

    synchronized

    synchronized是Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

    synchronized是非公平可重入锁。

    在Java中,每一个对象都拥有一个锁标记(monitor),也称为监视器,多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问

    synchronized使用
    • 同步代码块
      被synchronized修饰的代码块称为同步代码块。
      其作用的范围是调用这个代码块的对象,我们在用synchronized关键字的时候,能缩小代码段的范围就尽量缩小,能在代码段上加同步就不要再整个方法上加同步。这叫减小锁的粒度,使代码更大程度的并发。
    public void test1() {
        synchronized (this) {
            int i = 5;
            while (i-- > 0) {
                System.out.println(Thread.currentThread().getName() + " : " + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ie) {
    
                }
            }
        }
    }
    
    • 同步方法
      被synchronized修饰的方法称为同步方法。
      其作用范围是整个方法,作用对象是调用这个方法的对象(一个类可能有多个实例,只对此类的当前实例有效)。
    public synchronized void test2() {
        int i = 5;
        while (i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
    
            }
        }
    }
    

    但当synchronized修饰一个static静态方法时,其作用范围是整个静态方法,作用对象是这个类的所有对象。

    public static synchronized void test3() {
        int i = 5;
        while (i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
    
            }
        }
    }
    

    synchronized(),()中是锁住的对象, synchronized(this)锁住的只是对象本身,同一个类的不同对象调用的synchronized方法并不会被锁住,而synchronized(className.class)实现了全局锁的功能,所有这个类的对象调用这个方法都受到锁的影响,此外()中还可以添加一个具体的对象,实现给具体对象加锁。
    来看一个示例:

    class TestSynchronized {
    
        public void test1() {
            synchronized (this) {
                int i = 5;
                while (i-- > 0) {
                    System.out.println(Thread.currentThread().getName() + " : " + i);
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException ie) {
    
                    }
                }
            }
        }
    
        public synchronized void test2() {
            int i = 5;
            while (i-- > 0) {
                System.out.println(Thread.currentThread().getName() + " : " + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ie) {
    
                }
            }
        }
    
        public static synchronized void test3() {
            int i = 5;
            while (i-- > 0) {
                System.out.println(Thread.currentThread().getName() + " : " + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ie) {
    
                }
            }
        }
    
        public static void main(String[] args) {
            final TestSynchronized testSynchronized = new TestSynchronized();
            Thread test1 = new Thread(new Runnable() {
                public void run() {
                    testSynchronized.test1();
                }
            }, "test1");
    
            Thread test2 = new Thread(new Runnable() {
                public void run() {
                    testSynchronized.test2();
                }
            }, "test2");
    
            Thread test3 = new Thread(new Runnable() {
                public void run() {
                    TestSynchronized.test3();
                }
            }, "test3");
    
            test1.start();
            test2.start();
            // test3.start();
        }
    }
    

    运行结果:

    test2 : 4
    test2 : 3
    test2 : 2
    test2 : 1
    test2 : 0
    test1 : 4
    test1 : 3
    test1 : 2
    test1 : 1
    test1 : 0
    

    分析:
    第一个方法用了同步代码块的方式进行同步,传入的对象实例是this,表明是当前对象(当然,如果需要同步其他对象实例,也可传入其他对象的实例)。

    第二个方法是修饰方法的方式进行同步。因为第一个同步代码块传入的this,所以两个同步代码所需要获得的对象锁都是同一个对象锁。

    下面main方法时分别开启两个线程,分别调用test1和test2方法,那么两个线程都需要获得该对象锁,另一个线程必须等待。上面也给出了运行的结果可以看到:直到test2线程执行完毕,释放掉锁,test1线程才开始执行。

    代码里面明明是先开启test1线程,为什么先执行的是test2呢?这是因为java编译器在编译成字节码的时候,会对代码进行一个重排序,也就是说,编译器会根据实际情况对代码进行一个合理的排序,编译前代码写在前面,在编译后的字节码不一定排在前面,所以这种运行结果是正常的。
    如果我们去掉test2方法的synchronized关键字,结果会如何呢?

    test1 : 4
    test2 : 4
    test1 : 3
    test2 : 3
    test1 : 2
    test2 : 2
    test1 : 1
    test2 : 1
    test1 : 0
    test2 : 0
    

    我们可以看到,结果输出是交替着进行输出的,这是因为:某个线程得到了对象锁,但是另一个线程还是可以访问没有进行同步的方法(普通方法)或者代码。进行了同步的方法(加锁方法)和没有进行同步的方法(普通方法)是互不影响的,一个线程进入了同步方法,得到了对象锁,其他线程还是可以访问那些没有同步的方法(普通方法)。

    对象的内置锁和对象的状态之间是没有内在的关联的,虽然大多数类都将内置锁用做一种有效的加锁机制,但对象的域并不一定通过内置锁来保护。当获取到与对象关联的内置锁时,并不能阻止其他线程访问该对象,当某个线程获得对象的锁之后,只能阻止其他线程获得同一个锁。之所以每个对象都有一个内置锁,是为了免去显式地创建锁对象。

    换句话说:无论是同步代码块、同步方法(包括静态方法)、普通方法,是否多个线程可以同时访问某个方法或者代码块,关键看他们竞争的是不是同一个锁,如果不是同一个锁或者压根没有上锁,那么就可以同时访问

    synchronized缺陷

    当某个线程进入同步方法获得对象锁,那么其他线程访问这里对象的同步方法时,必须等待或者阻塞,这对高并发的系统是致命的,这很容易导致系统的崩溃。

    如果某个线程在同步方法里面发生了死循环,那么它就永远不会释放这个对象锁,那么其他线程就要永远的等待。这是一个致命的问题。

    当然同步方法和同步代码块都会有这样的缺陷,只要用了synchronized关键字就会有这样的风险和缺陷。既然避免不了这种缺陷,那么就应该将风险降到最低。这也是同步代码块在某种情况下要优于同步方法的原因(同步代码块并不是在任何场景下都优于同步方法)。

    相关文章

      网友评论

          本文标题:Java基础20-初识synchronized

          本文链接:https://www.haomeiwen.com/subject/rgmqjctx.html