Java并发——synchronized关键字,内置锁解析,可见性,互斥性浅谈

版权声明:博主GitHub地址https://github.com/suyeq欢迎大家前来交流学习 https://blog.csdn.net/hackersuye/article/details/84844338

    Java中为了保证每个线程中的原子操作,引入了内置锁,或者称为监视器锁,其中,每个Java对象都可以作为一个实现锁的对象,synchronized关键字修饰的代码块被称为同步代码块,线程进入同步代码块自动获取内置锁,退出同步代码块则释放锁,不需要调用者考虑它的创建以及消除,但是得十分熟悉内置锁的机制。

互斥性、可见性

    Java中的锁机制具有可见性、互斥性两大通性,内置锁也不例外,关于互斥性,如其名,即在同一时间只允许一个线程持有某个锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。互斥性我们也往往称为操作的原子性。简单的来说,当线程A需要获取一个来自于线程B中正在持有的锁时,线程A必须等待线程B执行完并释放该锁,才能获取该锁执行。如果线程B处于某些意外一直不释放锁,那么线程A就要一直等待下去。如示例代码:

public class ThreadTest1 extends Thread {
    private Object object;
    public ThreadTest1(Object object){
        this.object=object;
    }
    public void run(){
        synchronized (object){
            System.out.println("蕾姆拿到锁啦");
            try {
                sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("5s后。。。。");
            System.out.println("蕾姆释放锁啦");
        }
    }
}
public class ThreadTest2 extends Thread{
        private Object object;
        public ThreadTest2(Object object){
            this.object=object;
        }
        @Override
        public void run(){
            System.out.println("拉姆等锁中。。。。");
            synchronized (object){
                System.out.println("拉姆拿到锁啦");
            }
        }
}
public class Test {
    public static void main (String args[]){
        Object object=new Object();
        ThreadTest1 threadTest1=new ThreadTest1(object);
        ThreadTest2 threadTest2=new ThreadTest2(object);
        threadTest1.start();
        threadTest2.start();
    }
}
//输出:
//蕾姆拿到锁啦
//拉姆等锁中。。。。
//5s后。。。。
//蕾姆释放锁啦
//拉姆拿到锁啦

    在蕾姆拿到锁后拉姆便一直在等待蕾姆释放锁,事实上,上述的输出是我构建的理想状态,不一定是这样的输出,有可能是拉姆先拿到锁,而蕾姆就陷入了等待中,不管是谁先拿到锁,总会保持一点的相同,就是锁的互斥事件,一个线程拿到,另外一个线程便必须等待。而且,这种因为内置锁而陷入等待的线程是不能用Object类的interrupt方法中断的。而关于可见性,简单的来说就是线程在对共享变量做了修改后,能够被其他的线程及时看到,对于内置锁来说,必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值)

    可见性的全称是内存可见性,在上一篇博客讲到,每个线程都有一个独立的内存空间,即线程栈,线程的计算存贮主要在上面进行,当我们要一个线程计算一个值时,线程会把这个值从主存中取出来放入线程栈中计算,计算完毕后才放入主存中,只有在主存中的数据才能让其它线程可见,所以这可能引发一个错误,就是线程A、B同时对主存中某个数据进行运算加1,当A从主存中拿到值为1的数据,还在计算时,B也从主存中获取到了这个数据,当A把计算结果2写入主存后,B也跟着把计算结果2写入主存中,这样线程A,B对该值计算了两次,但是得到的结果却是2。这无疑是恐怖的,而且对于多线程是灾难性的问题,而保证线程之间的可见性方法之一就是利用锁将该数据锁起来,只让一个线程更改,这样就利用线程的封闭性保证了线程之间的可见性。 如图:

在这里插入图片描述

可重入性

    内置锁的第三个特性是可重入性,即当某个线程请求一个它自己已经在使用的锁时,这个请求可以成功。可重入性避免了死锁的发生,如何理解呢?看下面这个例子:

public class SynchronizedLockTest {
    public synchronized void gc(){
        gcc();
    }
    private synchronized void gcc(){
        System.out.println("蕾姆进来啦");
    }
}
//Test
new SynchronizedLockTest().gc();
//输出:蕾姆进来啦

    当主线程获取到SynchronizedLockTest 对象的锁时,再次接着调用另外一个同步方法,并不会陷入等待,也就是说线程在获取自己持有的锁时能得到成功。在JVM中当线程请求一个未被持有的锁时,将记录下锁的持有者,并将获取锁的计数从0加1,当锁的获取者再次获取这个锁时,计数再加1,获取者退出一个同步代码块计数则减1,直到为0表示不再持有该锁。

锁的种类

    内置锁分为两类,一类为对象锁,另外一类是类锁。对象锁对应的是对象,任何一个对象都可以成为对象锁,对象锁的使用分为两种,一类是非静态的同步方法,另外一类是同步代码块(入口是对象),当时用非静态的同步方法时,它的调用者便是对象锁,它们的格式如下:

public synchronized void gc(){
}

synchronized (object/this){    
}

    类锁的使用也分为两种,一类是静态的同步方法,另外一类是同步代码块(入口是类对象)。之所以强调静态与非静态的同步方法,是因为静态的同步方法时是属于类级别的,调用它的是它所属类的Class类对象,所以它是类锁。它们的格式如下:

public static synchronized void gc(){
}

synchronized (类名.class){    
}

    对象锁是在对象的级别上锁住对应的操作,而类锁是在类级别上,当用类锁锁住一组原子性操作时,其对应的对象锁是不是不起作用了呢?并没有,类锁与对象锁是两种互不相干的锁,同一个类的类锁在被使用时,其对象锁依旧可以使用,两者互不相干。如下代码:

public class ClassLockTest extends Thread {
    @Override
    public void run(){
        synchronized (Object.class){
            System.out.println("蕾姆进入类锁了");
            try {
                sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("蕾姆退出类锁了");
        }
    }
}

public class ObjectLockTest extends Thread {
    private Object object;
    public ObjectLockTest(Object object){
        this.object=object;
    }
    @Override
    public void run(){
        synchronized (object){
            System.out.println("拉姆获取到对象锁啦");
        }
        System.out.println("拉姆释放对象锁啦");
    }
}
public class Test {
    public static void main (String args[]){
        Object object=new Object();
        ClassLockTest classLockTest=new ClassLockTest();
        ObjectLockTest objectLockTest=new ObjectLockTest(object);
        classLockTest.start();
        objectLockTest.start();
    }
}
//输出:
//蕾姆进入类锁了
//拉姆获取到对象锁啦
//拉姆释放对象锁啦
//蕾姆退出类锁了

    当蕾姆获取到Object的类锁时,拉姆依旧能获得Object对象的对象锁。

扫描二维码关注公众号,回复: 4393244 查看本文章

总结

    一个线程可以拥有多个不同的锁,锁与锁之间是不相干扰的(不考虑锁的嵌套)。线程的优点是并发性,但是缺点很多:无序性、不可见性、非原子性等。内置锁解决了线程的这些缺点,但要注意的是,不管是什么锁,给一组行为加锁,让其成为原子性的操作,实质上是让线程的执行操作串行化了,这就引出了锁机制得到一个明显的问题,就是当一组操作耗时很长时,给其加锁成为原子性的操作后就会明显的影响到线程的并发性,所以我们在使用锁机制的时候,应当将加锁的跨度,亦或者称之为加锁的粒度尽可能的小,尽可能的让它在串行化的时候所花费的时间少,这样才不会对线程并发的性能产生严重的影响。

猜你喜欢

转载自blog.csdn.net/hackersuye/article/details/84844338