synchronized关键字(一)

synchronized的简介:

在java中synchronized关键字是同步锁,同步锁是依赖于对象而存在的,而且每一个对象有且仅有一个同步锁。当我们调用某对象的synchronized方法时,就获取了该对象的同步锁。例如,synchronized(obj)就获取了obj 这个对象的同步锁。

synchronized的原理:

Synchronized是通过对象内部的一个叫做monitor的锁来实现的,但是monitor锁的本质又是依赖于底层的操作系统的互斥锁Mutex Lock)来实现的,而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。这种依赖于操作系统互斥锁Mutex Lock)所实现的锁我们称之为重量级锁

不使用并发手段会有什么后果?

package synchronize;

/**
 * 消失的请求数
 */
public class DisappearRequest1 implements Runnable{

    static int i = 0;
    static DisappearRequest1 instance = new DisappearRequest1();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        // 同时启动线程t1与线程t2,然后调用run方法
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }

    @Override
    public void run() {
        for (int j = 0; j < 100000; j++) {
            i++;
        }
    }
}

通过运行以上代码,我们发现:两个线程同时a++操作,最后结果会比预计的少。

原因:
count++, 看上去只是一个操作, 实际上包含了三个操作,它并非原子性操作:

  1. 读取count
  2. count 加 1
  3. count的值写入内存中

在多线程环境下,任何一步执行完都有可能被打断, 都有可能轮到第二个线程去执行。假设目前我们的count的值是9,当第一个线程执行完count++操作时,正好被第二个线程打断,而此时第一个线程执行完count++操作后并没有把值写入内存,所以此时第二个线程在读取count的值时,count的值依旧是9。按正常逻辑来讲,两次count++操作后,count的值为11,然而在这样的一种情况下,两次count++操作后,count的值只是10。对于这样的一种现象,我们称作线程不安全

synchronized的两个用法:

对象锁

包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)

代码块形式

手动指定锁对象

package synchronize;

/**
 * 描述 对象锁实例1,代码块形式
 */
public class SynchronizedObjectCodeBlock2 implements Runnable {

    static SynchronizedObjectCodeBlock2 instance = new SynchronizedObjectCodeBlock2();

    // 当两个线程一起调用该方法时,线程会进入阻塞状态,从而达到串行的效果,以保证线程安全
    @Override
    public void run() {
        // this值为SynchronizedObjectCodeBlock2对象
        synchronized (this) {
            System.out.println("this的值为:"+ this);
            System.out.println("我是对象锁的代码块形式。我叫" + Thread.currentThread().getName());

            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
    }

    public static void main(String[] args) {

        System.out.println("instance实例对象:" + instance);

        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();

        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");


    }
}

方法锁形式

synchronized修饰普通方法, 锁对象默认为this

package synchronize;

/**
 * 描述:对象锁实例2,方法锁形式
 */
public class SynchronizedObjectMethods3 implements Runnable {

    static SynchronizedObjectMethods3 instance = new SynchronizedObjectMethods3();

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);

        t1.start();
        t2.start();

        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");

    }

    @Override
    public void run() {
        methods();
    }

    // 当两个线程一起调用该方法时,线程会进入阻塞状态,从而达到串行的效果,以保证线程安全
    public synchronized void methods(){
        System.out.println("我是对象锁的方法修饰符形式,我叫"+ Thread.currentThread().getName());

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"运行结束");
    }

}


类锁

synchronized修饰静态的方法或指定锁Class对象。在java类中可能有很个对象,但只有1Class对象。所谓的类锁,其本质不过就是Class 对象锁而已。

类锁与对象锁的区别

类锁只能在同一时刻被一个对象拥有。如果有个对象进行竞争的话,线程与线程之间将会阻塞;但对象锁则不同,不同的对象锁之间是不会有任何干扰的,他们会并行执行,并不会造成阻塞。因为锁的当前对象是this同一个类中,当锁对象不一样的多个线程同时运行时,会同时执行;如果是类锁,且两个对象都属于同一个类,则意味着这两个对象共享同一把锁,这样一来则会进行阻塞

形式一:

static方法上加synchroized关键字。

package synchronize;

/**
 * 描述:类锁的第一种形式:静态方法锁
 */
public class SynchronizedClassStatic4 implements Runnable {

    static SynchronizedClassStatic4 instance1 = new SynchronizedClassStatic4();
    static SynchronizedClassStatic4 instance2 = new SynchronizedClassStatic4();

    @Override
    public void run() {
        methods();
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();

        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }

    // TODO why? 为什么methods方法加上 static 关键字就会造成阻塞?没加static关键字时,是一种的并行的状态?
    //  原因是该方法加上static关键字后就会属于类锁(锁定的对象是class对象),
    //  而class对象又是唯一的,要争取资源必须等待,所以先后执行了;
    //  然而一旦去掉static关键字,methods方法则变成对象方法,因此不同的对象都执行各的run方法,
    //  即锁(this)对象不同,所以则是并行执行。
    //
    public static synchronized void methods(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("我是类锁的第一形式:static形式,我叫"+ Thread.currentThread().getName());

    }
}

形式二:

synchroized(*.class)代码块。

package synchronize;

/**
 * 描述:类锁的第二种形式:`synchroized`(*.class)代码块。
 */
public class SynchronizedClassClass5 implements Runnable {

    static SynchronizedClassClass5 instance1 = new SynchronizedClassClass5();
    static SynchronizedClassClass5 instance2 = new SynchronizedClassClass5();

    @Override
    public void run() {
        try {
            methods();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();

        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }

    // TODO 当使用 SynchronizedClassClass5.class 时,线程以串行的方式执行;
    //  当我们使用this关键字进行替换时,线程差不多是以并行的方式来执行 ? 为什么呢 ?
    private void methods() throws InterruptedException {
        synchronized (SynchronizedClassClass5.class){
            System.out.println("我是类锁的第二种形式:synchronized(*.class)。" +
                    "我叫"+ Thread.currentThread().getName());
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
    }
}

思考?

  1. 两个线程同时访问一个对象的同步方法。

    • 会进行阻塞。原因是他们的锁对象是同一个,两者之间会形成互斥,以串行的方式执行。
  2. 两个线程访问的是两个对象的同步方法。

    • 这个时候,synchroized是不起作用的,它俩是并行执行,原因是他们的锁对象并不是同一个,两者之间并不会形成互斥。
  3. 两个线程访问的是synchroized的静态方法。

    • 锁生效,线程会进行阻塞,两者之间并会形成互斥,因为一旦在方法名上加上static关键字,则会构成类锁,多个对象共用一个方法。因此会造成阻塞。具体如SynchronizedClassStatic4案例所示。
  4. 同时访问同步方法与非同步方法(通过代码演示)。

package synchronize;

/**
 *  描述: 同时访问同步方法与非同步方法,执行结果:同时开始,同时结束,可见非同步方法不受到影响
 */
public class SynchronizedYesAndNo6 implements Runnable {

    static SynchronizedYesAndNo6 instance = new SynchronizedYesAndNo6();

    @Override
    public void run() {
        if(Thread.currentThread().getName().equals("Thread-0")){
            methods1();
        }else {
            methods2();  
        }
    }

    // 同步方法
    public synchronized void methods1(){
        System.out.println("我是加锁方法。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");

    }

    // 非同步方法
    public void methods2(){
        System.out.println("我是没加锁方法。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");

    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }
}

  1. 访问同一个对象的不同的普通同步方法(代码如下)。
package synchronize;

/**
 * 描述: 同时访问一个类的不同的普通同步方法
 */
public class SynchronizedDifferenceMethods7 implements Runnable {

    static SynchronizedDifferenceMethods7 instance1 = new SynchronizedDifferenceMethods7();

    // TODO 从执行结果来看,虽然没有指定锁对象,但本质上是指定了this作为锁的对象 
    @Override
    public void run() {
        if(Thread.currentThread().getName().equals("Thread-0")){
            methods1();
        }else {
            methods2();
        }
    }

    // 同步方法
    public synchronized void methods1(){
        System.out.println("我是加锁方法。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");

    }

    // 同步方法
    public synchronized void methods2(){
        System.out.println("我是加锁方法。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");

    }

    public static void main(String[] args) {

        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance1);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");

    }
}

  1. 同时访问静态synchronized和非静态synchronized的方法
package synchronize;

/**
 *  描述:同时访问静态synchronized和非静态synchronized的方法
 */
public class SynchronizedStaticAndNormal8 implements Runnable {

    static SynchronizedStaticAndNormal8 instance = new SynchronizedStaticAndNormal8();

    // TODO 从执行结果来看,两个线程同时开始,同时结束。这是因为两个线程走的是不同的方法,即线程锁对象并非是同一个。
    @Override
    public void run() {
        if(Thread.currentThread().getName().equals("Thread-0")){
            // 第一个线程走methods1()方法
            methods1();
        }else {
            // 第二个线程走methods2()方法
            methods2();
        }
    }

    // 非静态同步方法(该方法的锁对象为:对象实例本身)
    public synchronized void methods1(){
        System.out.println("我是非静态加锁方法。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");

    }

    // 静态同步方法(该方法的锁对象为:类锁)
    public static synchronized void methods2(){
        System.out.println("我是静态加锁方法。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");

    }

    public static void main(String[] args) {

        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");

    }
}

  1. 方法抛出异常后,会释放锁吗?
    • lock不一样,synchronized在抛出异常后,会释放锁,而lock则需要在异常的`finaly中进行释放。
package synchronize;

/**
 * 描述: 方法抛弃后,会释放锁。
 * 展示不抛出异常前和抛出异常后的对比:一旦抛出异常,第二个线程会立刻进入同步方法,意味着锁已经释放
 */
public class SynchronizedException9 implements Runnable {

    static SynchronizedException9 instance = new SynchronizedException9();

    @Override
    public void run() {
        if(Thread.currentThread().getName().equals("Thread-0")){
            methods1();
        }else {
            methods2();
        }
    }

    // 非静态同步方法
    public synchronized void methods1(){
        System.out.println("我是加锁方法。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            throw new Exception();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");

    }

    public synchronized void methods2(){
        System.out.println("我是加锁方法。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");

    }

    public static void main(String[] args) {

        // TODO 这两个线程使用的是同一个对象,这意味着是同一个对象锁
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");

    }
}

总结:

  1. 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(如案例1和案例5)。
  2. 每个实例都对应有自己的一把锁,不同实例之间互不影响,例外:锁对象是.class以及synchronized修饰的是static方法的时候,所有对象共同使用一把类锁(如2,3,4,6案例所示)。
  3. synchronized代码块中,无论是正常使用还是在执行过程中抛出异常,锁都会得到释放(如案例7所示)。

在这里我们在补充一下,当线程调用synchronized关键字修饰的方法时,而这个方法中再次调用一个没有被synchronized关键字修饰的其他方法时,这个时候还是线程安全吗?

答案是否定的。一旦离开synchronized关键字修饰的方法,进入到另一个方法时,这个方法就有可以被多个线程同时访问。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,923评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,154评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,775评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,960评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,976评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,972评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,893评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,709评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,159评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,400评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,552评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,265评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,876评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,528评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,701评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,552评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,451评论 2 352