Java攻城狮的入门课程Java学习笔记程序员

(二)synchronized详解

2017-03-24  本文已影响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接口实现生产者与消费者


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

猜你喜欢

热点阅读