知识点干货—多线程同步【6】之synchronized

“明日复明日,明日何其多。
我生待明日,万事成蹉跎。
世人若被明日累,春去秋来老将至。
朝看水东流,暮看日西坠。
百年明日能几何?请君听我明日歌。
明日复明日,明日何其多!
日日待明日,万世成蹉跎。
世人皆被明日累,明日无穷老将至。
晨昏滚滚水东流,今古悠悠日西坠。
百年明日能几何?请君听我明日歌。”

这首《明日歌》是明朝的钱福所写。大意是,
明天又一个明天,明天何等的多。
我的一生都在等待明日,什么事情都没有进展。
世人和我一样辛苦地被明天所累,一年年过去马上就会老。
早晨看河水向东流逝,傍晚看太阳向西坠落才是真生活。
百年来的明日能有多少呢?请诸位听听我的《明日歌》。

这首诗七次提到“明日”,诗人在作品中告诫和劝勉人们要牢牢地抓住稍纵即逝的今天,要珍惜时间,今日的事情今日做,不要拖到明天,不要蹉跎岁月。不要把任何计划和希望寄托在未知的明天。诗歌的意思浅显,语言明白如话,说理通俗易懂。给人的启示是:世界上的许多东西都能尽力争取和失而复得,只有时间难以挽留。人的生命只有一次,时间永不回头。不要今天的事拖明天,明天拖后天,要“今天的事,今日毕”。告诫我们不要学寒号鸟,要珍惜时间,不要把事情都放到明天,今天的事情今天搞定。

继续总结多线程同步常用的方法或者类,之前介绍了CountDownLatch,CyclicBarriar和Exchanger,Phaser 以及Semaphore,这次介绍一个大家比较熟悉的关键字--synchronized。大多数人应该或多或少的使用过它,那我们对它是所有用法都彻底了解吗?这个就不见的了,今天我们就全方位的介绍一下它。

1、定义

先看一下百度百科给出的定义:“synchronized--Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍可以访问该object中的非加锁代码块。”
什么意思呢?就是说synchronized是Java中用来对对象和方法或者代码块进行加锁的一种方法,借助它可以在多线程并发时,保证同一时刻只有一个线程执行某个同步方法或代价块,这样能充分保证线程按顺序执行,保证它们同步进行,按照我们的逻辑使多线程按我们的心意来依次执行,这也是一种解决多线程同步的方法。

2、对象锁和类锁的概念

在使用synchronized前,我们要先理解两个概念,对象锁和类锁。

对象锁
对象锁是指Java为临界区(指程序中的一个代码段)synchronized(Object)语句指定的对象进行加锁。它用于程序片段或者method上,此时将获得对象的锁,所有想要进入该对象的synchronized的方法或者代码段的线程都必须获取对象的锁,如果没有,则必须等其他线程释放该锁。
当一个对象中有synchronized method或synchronized block的时候调用此对象的同步方法或进入其同步区域时,就必须先获得对象锁。如果此对象的对象锁已被其他调用者占用,则需要等待此锁被释放。

类锁
实际上是没有这个概念的,但是为了区分对象锁的不同使用场景,我们增加了一个类锁这样的概念。对象锁指的是对象的某个方法或代码块进行加锁,那类锁指的是针对类方法或者类变量进行加锁。由于一个class不论被实例化多少次,其中的静态方法和静态变量在内存中都只有一份。所以,一旦一个静态方法被申明为synchronized,此类所有的实例化对象在调用此方法,共用同一把锁,所以我们称之为类锁。
在程序中可以尝试用以下方式获取类锁

synchronized (xxx.class) {...}
synchronized (Class.forName("xxx")) {...}

同时获取类锁和对象锁是可以的,并不会产生问题。但使用类锁时要格外注意,因为一旦产生类锁的嵌套获取的话,就会产生死锁,因为每个class在内存中都只能生成一个Class实例对象。

3、使用方法

了解了对象锁和类锁后,我们知道了synchronized可以用于多个场景,既可以修改代码块和方法,还可以用来修饰类方法和类。虽然锁针的对象不同,但它们的含义是一样的。

synchronized具体有如下四种使用场景:
(1)、修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。多个线程在同时使用这个对象的此代码块时会遇到对象锁,需要进行同步等待;

代码示例:


class DemoThread implements Runnable {

    private static int count;

    public DemoThread() {
        count = 0;
    }

    public  void run() {
        synchronized(this) {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public int getCount() {
        return count;
    }
}

DemoThread demoThread = new DemoThread();
Thread thread1 = new Thread(demoThread, "DemoThread1");
Thread thread2 = new Thread(demoThread, "DemoThread2");

thread1.start();
thread2.start();

结果如下:

DemoThread1:0 
DemoThread1:1 
DemoThread1:2 
DemoThread1:3 
DemoThread1:4 
DemoThread2:5 
DemoThread2:6 
DemoThread2:7 
DemoThread2:8 
DemoThread2:9

分析:当两个并发线程(thread1和thread2)同事访问同一个对象(demoThread)中的synchronized代码块时,在同一时刻只能有一个线程执行,另一个线程阻塞在synchronized位置,必须等待正在访问的线程执行完这个代码块以后才能执行该代码块。所以才会看到这样的结果,开始只有DemoThread1的Log,DemoThread1执行完成后才能看到DemoThread的Log。

这里需要注意一下,当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。也就是说如果thread1正在访问synchronized修饰的代码块,thread2虽然此时无法访问这个代码块,但它可以访问其他的代码块。

(2)、修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。在多线程执行时和同步代码块相同,针对的是某个对象;

代码示例:

class DemoThread implements Runnable {
    private static int count;

    public DemoThread() {
        count = 0;
    }

    public synchronized void run() {
        for (int i = 0; i < 5; i++) {
            try {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public int getCount() {
        return count;
    }
}

DemoThread demoThread = new DemoThread();
Thread thread1 = new Thread(demoThread, "DemoThread1");
Thread thread2 = new Thread(demoThread, "DemoThread2");

thread1.start();
thread2.start();

结果如下:

DemoThread1:0 
DemoThread1:1 
DemoThread1:2 
DemoThread1:3 
DemoThread1:4 
DemoThread2:5 
DemoThread2:6 
DemoThread2:7 
DemoThread2:8 
DemoThread2:9

可以看到他们的执行结果是相同的。既然结果相同,那修饰代码块和修改方法名有什么区别呢?
这个问题也是synchronized的缺陷。
synchronized的缺陷:当某个线程进入同步方法获得对象锁,那么其他线程访问这里对象的同步方法时,必须等待或者阻塞,这对高并发的系统是致命的,这很容易导致系统的崩溃。如果某个线程在同步方法里面发生了死循环,那么它就永远不会释放这个对象锁,那么其他线程就要永远的等待,这问题一旦出现就是是一个致命的问题。既然无法完全避免这种缺陷,那么就应该将风险降到最低。同步代码块就是为了降低风险而存在的。因为如果某一线程调用synchronized修饰的代码方法,那么当某个线程进入了这个方法之后,这个对象其他同步方法都不能被其他线程访问了。假如这个方法需要执行的时间很长,那么其他线程会一直阻塞,影响到系统的性能。而如果这时用synchronized来修饰代码块,情况就不同了,这个方法加锁的对象是某个对象,跟执行这行代码的对象或者承载这个方法的对象没有关系,那么当一个线程执行这个方法时,其他同步方法仍旧可以访问,因为他们持有的锁不一样。

(3)、修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。多线程在使用此方法时,会涉及到类锁。

代码示例:

class DemoThread implements Runnable {
    private static int count;

    public DemoThread() {
        count = 0;
    }

    public synchronized static void method() {
        for (int i = 0; i < 5; i ++) {
            try {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void run() {
        method();
    }
}
DemoThread demoThread = new DemoThread();
Thread thread1 = new Thread(demoThread, "DemoThread1");
Thread thread2 = new Thread(demoThread, "DemoThread2");

thread1.start();
thread2.start();

结果如下:

DemoThread1:0 
DemoThread1:1 
DemoThread1:2 
DemoThread1:3 
DemoThread1:4 
DemoThread2:5 
DemoThread2:6 
DemoThread2:7 
DemoThread2:8 
DemoThread2:9

可以看到结果和上两例相同。DemoThread1和DemoThread2是DemoThread的两个对象,但在thread1和thread2并发执行时却保持了线程同步。这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁。

(4)、修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。多线程使用此类时涉及到类锁。

代码示例:

class DemoThread implements Runnable {
    private static int count;

    public DemoThread() {
        count = 0;
    }

    public static void method() {
        synchronized(DemoThread.class) {
            for (int i = 0; i < 5; i ++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public synchronized void run() {
        method();
    }
}
DemoThread demoThread = new DemoThread();
Thread thread1 = new Thread(demoThread, "DemoThread1");
Thread thread2 = new Thread(demoThread, "DemoThread2");

thread1.start();
thread2.start();

结果如下:

DemoThread1:0 
DemoThread1:1 
DemoThread1:2 
DemoThread1:3 
DemoThread1:4 
DemoThread2:5 
DemoThread2:6 
DemoThread2:7 
DemoThread2:8 
DemoThread2:9

可以看到结果也是相同的。synchronized作用于一个类时,是给这个类加锁,类的所有对象用的是同一把锁。

4、总结

synchronized对于使用过的人来说应该比较好理解,也更容易学习它的高级用法,运用起来会显得很轻松;对于没有使用过的,可能只是停留在理解概念的层面,实际在使用时还是不太好下手,不知在何时何地来使用。所以解决不熟悉的唯一办法就是要勇敢大胆的去使用它,不要怕出错,多实践和多练习,这样才能很好的掌握它。


知识点干货—多线程同步【6】之synchronized_第1张图片
本公众号将以推送Android各种技术干货或碎片化知识,以及整理老司机日常工作中踩过的坑涉及到的经验知识为主,也会不定期将正在学习使用的新技术总结出来进行分享。每天一点干货小知识把你的碎片时间充分利用起来。

你可能感兴趣的:(知识点干货—多线程同步【6】之synchronized)