(二)synchronized详解

作者: 黒猫 | 来源:发表于2017-03-24 17:45 被阅读108次

    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接口实现生产者与消费者


    版权声明:欢迎转载,欢迎扩散,但转载时请标明作者以及原文出处,谢谢合作!             ↓↓↓
    

    相关文章

      网友评论

        本文标题:(二)synchronized详解

        本文链接:https://www.haomeiwen.com/subject/jolonttx.html