Java基础20-初识synchronized

2019-08-13  本文已影响0人  Tian_Peng

概述

如何在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使用
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) {

        }
    }
}

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

上一篇下一篇

猜你喜欢

热点阅读