(二)synchronized详解


1、了解synchronized

synchronized是Java中的关键字,是一种同步锁。当多个并发线程访问同一个对象中用synchronized修饰的代码块时,在同一时刻只能有一个线程得到执行,其他的线程均受阻塞,必须等待当前线程执行完毕,其他线程才能执行该代码块。
  当前执行线程和其他线程是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。


2、使用synchronized修饰方法

实现线程有序计数

class Num implements Runnable {
    private int count;

    public Num() {
        count = 0;
    }

    public synchronized void run() {//获取锁并排斥其他线程
        /*
         * 使用synchronized修饰run()方法 
         * 被修饰的方法称为同步方法 
         * 其作用的范围是整个方法 
         * 作用的对象是调用这个方法的对象
         */
        for (int i = 0; i < 5; i++) {

            System.out.println(Thread.currentThread().getName() + "数了" + (++count));
        }
    }//释放锁

}

public class Demo1 {

    public static void main(String[] args) {
        Num n = new Num();
        /*
         * 使用Thread(Runnable target, String name) 这种构造方法
         * 参数name就是新线程名称
         */
        Thread thread1 = new Thread(n, "Tom");
        Thread thread2 = new Thread(n, "Mike");
        thread1.start();
        thread2.start();
    }

}

使用synchronized修饰的结果如下:
当一个线程在执行任务代码时,另一个线程是被阻塞的,因此只有在Mike计数完成之后,Tom才开始计数。

对之前的代码稍作修改加以对比:

public class Demo1 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Num(), "Tom");
        Thread thread2 = new Thread(new Num(), "Mike");
        thread1.start();
        thread2.start();
    }
}

此时结果如下:

之所以出现使用了synchronized修饰,仍然乱序的结果,是因为因为synchronized只锁定对象,每个对象只有一个锁(lock)与之相关联,修改代码后相当于每个线程各自又创建了一个对象,因此存在两个对象以及两把锁,所以出现乱序,这也就意味着如果要使用同步锁,必须保证至少有两个或以上的线程,同时这多个线程都使用同一把锁。

注意:

  1. synchronized关键字不能继承。
      虽然可以使用synchronized来修饰方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中重写了这个方法,在子类中的这个方法默认情况下并不是同步的,必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下:
//在子类方法中添加synchronized关键字
class Parent {
    public synchronized void fun() {
    }
}
class Child extends Parent {
    public synchronized void fun() {
    }
}
//在子类方法中调用父类的同步方法
class Parent {
    public synchronized void fun() {
    }
}
class Child extends Parent {
    public void fun() {
        super.fun();
    }
}
  1. 在创建接口中的方法时不能使用synchronized关键字。
  2. 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。

3、使用synchronized修饰静态方法

静态同步方法在进内存时不存在对象,但是存在其所属类的class类型的字节码文件对象,因此静态同步方法的锁就是该对象(.class),锁定的是所属类的所有对象。

对 “ 实现线程有序计数 ” 案例做以下修改


class Num implements Runnable {

    // 静态方法需要调用静态变量
    private static int count;

    public Num() {
        count = 0;
    }

    // 定义静态同步方法
    public synchronized static void fun() {

        for (int i = 0; i < 5; i++) {

            System.out.println(Thread.currentThread().getName() + "数了" + (++count));
        }

    }

    // 重写run()方法,调用静态同步方法
    public void run() {
        fun();
    }

}

public class Demo2 {

    public static void main(String[] args) {

        Thread thread1 = new Thread(new Num(), "Tom");
        Thread thread2 = new Thread(new Num(), "Mike");
        thread1.start();
        thread2.start();

    }

}

此时结果如下:

虽然2个线程在执行时分别创建了2个对象,但由于run()方法调用了静态同步方法fun(),静态方法是属于类的,所以这2个对象相当于用了同一把锁,即所属类的字节码文件。虽然结果与Demo1相同,但实现原理是不同的。


4、使用synchronized修饰代码块

使用synchronized修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象,例如依然修改上述代码,将修饰方法改为修饰代码块:

class Num implements Runnable {
    private static int count;

    public Num() {
        count = 0;
    }

    public void run() {

        synchronized (this) {
            for (int i = 0; i < 5; i++) {

                System.out.println(Thread.currentThread().getName() + "数了" + (++count));
            }
        }
    }

    public int getCount() {
        return count;
    }
}

运行结果与修饰方法是相同的,只是形式不同,这里在方法内修饰代码块的作用域与直接修饰方法是一样的,都是run()方法内部。

当一个程序内存在使用synchronized修饰的代码块以及普通代码块时,多个线程可以同时访问这些代码块,访问普通代码块的线程之间仍然是争抢CPU的状态,访问同步代码块的线程受同步锁的影响会在结果上呈现先后顺序,示例代码如下:

class Test implements Runnable {
    /*
     * 创建测试类Test继承Runnable接口 
     * 该类包含两个方法 fun1()方法是使用synchronized修饰的 
     * 由线程A、C执行
     * fun2()方法是普通方法 
     * 由线程B、D执行
     * 重写run()方法
     * 使4个线程分别能够执行fun1()和fun2()
     */
    private int count;

    public Test() {
        count = 0;
    }

    public void fun1() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
            }
        }
    }

    public void fun2() {

        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "抢到了CPU!");
        }
    }

    public void run() {
        String threadName = Thread.currentThread().getName();
        if (threadName.equals("A")) {
            fun1();
        } else if (threadName.equals("B")) {
            fun2();
        } else if (threadName.equals("C")) {
            fun1();
        } else if (threadName.equals("D")) {
            fun2();
        }
    }
}

public class Demo3 {
    public static void main(String arg[]) {
        Test t = new Test();
        Thread thread1 = new Thread(t, "A");
        Thread thread2 = new Thread(t, "B");
        Thread thread3 = new Thread(t, "C");
        Thread thread4 = new Thread(t, "D");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

此时结果如下:

线程B、D执行普通代码块,始终在争抢CPU,线程A、C执行同步代码块,因此是线程A执行完任务代码后,线程C才开始执行;但要注意线程A在执行时也同线程B、D在争抢CPU,这也证明了程序内存在同步代码块以及普通代码块的时候,线程是可以同时访问这些代码块并且互相之间不排斥的。


5、修改同步锁

对 Demo3 案例做一下修改

class Test implements Runnable {

    private int count;
    Object obj = new Object();

    public Test() {
        count = 0;
    }

    public void fun1() {
        synchronized (obj) {// 该锁是obj对象
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
            }
        }
    }

    public synchronized void fun2() {// 该锁是this对象

        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + "抢到了CPU!");
        }
    }

    public void run() {
        String threadName = Thread.currentThread().getName();
        if (threadName.equals("A")) {
            fun1();
        } else if (threadName.equals("B")) {
            fun2();
        }
    }
}

public class Demo4 {
    public static void main(String arg[]) {
        Test t = new Test();
        Thread thread1 = new Thread(t, "A");
        Thread thread2 = new Thread(t, "B");

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

    }
}

此时结果如下:

虽然2个线程传入的是同一个对象t,但在调用方法是,同步代码块的锁是obj对象,同步方法的锁是this对象,因此呈现在结果中,线程A、B依然在争抢CPU。

对上述代码加以修改:

class Test implements Runnable {
    private int count;
    public Test() {
        count = 0;
    }
    public void fun1() {
        synchronized (this) {// 该锁改为this对象
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
            }
        }
    }
    public synchronized void fun2() {// 该锁依然是this对象
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + "抢到了CPU!");
        }
    }
    public void run() {
        String threadName = Thread.currentThread().getName();
        if (threadName.equals("A")) {
            fun1();
        } else if (threadName.equals("B")) {
            fun2();
        }
    }
}

此时结果如下:

当两个线程调用的方法的锁都是this对象时,线程A、B受到同步锁的影响,只有一个线程执行完任务代码之后,另一个线程才开始执行。

提示:
当有一个明确的对象作为锁时,就采用以下类似的代码:

public void fun(){
   // obj 锁定的对象
   synchronized(obj){
      // 要完成的任务
   }
}

当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:

class Test implements Runnable
{
   Object obj = new Object();  // 特殊的对象
   public void fun() {
      synchronized(obj) {
         // 要完成的任务
      }
   }
}

6、单例设计模式中懒汉式并发访问的安全问题

有关单例设计模式的内容,请看单例设计模式

class Single {
    private Single() {
    }

    private static Single s;

    public static Single getInstance() {
        if (s == null) {
            /*
             * 之所以说懒汉式并发访问存在安全问题
             * 原因就在这里
             * 假设当线程t1抢占到CPU
             * 执行到该注释位置时,CPU被t2抢走
             * 当t2执行到该注释位置时
             * CPU再次被t1抢回来
             * 那么此时t1已经判断过s为空
             * 会直接执行下一句代码创建对象
             * 当t2也抢到CPU
             * 也已经判断过s为空
             * 同样会直接执行下一句代码创建对象
             * 那么此时就会创建2个对象
             * 无法保证单例
             */
            s = new Single();
        }
        return s;
    }
}

class Test implements Runnable {
    public void run(){
        Single s = Single.getInstance();
    }
}

public class Demo5 {

    public static void main(String[] args) {

        Test t = new Test();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        t1.start();
        t2.start();

    }
}

避免懒汉式出现该问题的方法之一,就是结合使用synchronized代码块,修改代码如下:

class Single {
    private Single() {
    }

    private static Single s;

    public static Single getInstance() {
        /*
         * 使用synchronized代码块包围创建对象部分
         * 此时的锁是Single.class
         * 由于判断锁需要消耗更多性能
         * 因此添加if判断
         * 可保证线程过多时
         * 从第三个线程开始
         * 先判断对象是否存在
         * 而不是直接判断锁
         * 从而提高性能
         */
        if (s == null) {
            synchronized (Single.class) {
                if (s == null) {
                    s = new Single();
                }

            }
        }
        return s;
    }

}

7、总结

  1. 当synchronized关键字作用的对象是非静态的,那么它取得的锁是对象;当synchronized作用的对象是静态的,那么它取得的锁是该类的字节码文件,该类的所有对象用同一把锁。

  2. 每个对象只有一个锁(lock)与之相关联,哪个线程获得这个锁,该线程就可以运行它所控制的那段代码,此时其他线程无法访问。

  3. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

注意:
随着JKD版本的更新,在1.5版本之后出现比synchronized更加强大的实现同步锁的方法,详情参考使用Lock接口与Condition接口实现生产者与消费者


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

推荐阅读更多精彩内容

  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,690评论 0 11
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,562评论 18 399
  • 一:java概述:1,JDK:Java Development Kit,java的开发和运行环境,java的开发工...
    ZaneInTheSun阅读 2,627评论 0 11
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,438评论 1 15
  • 但是可想而知,这样绝对会让你的生命痛苦不已,耗尽你的能量,直到你怀疑人生。 我们在生活中经常会听到指责的声音,比如...
    如如RURULILY阅读 930评论 0 1