synchronized关键字

1、引言

我们知道Java API提供了丰富的多线程机制,但是要想多线程机制能够正常运转,需要采取一些措施来防止多个线程访问相同的资源。为防止出现这样的冲突,只需在线程使用一个资源时为其加锁即可。访问资源的第一个线程加上锁以后,其他线程便不能再使用那个资源,除非被解锁。

在Java中,对这种特殊的资源,Java提供了synchronized关键字来防止它们的冲突。用Java中的synchronized关键字来标记一个方法或者一个代码块就可以实现资源的同步

2、synchronized关键字

在java中实现资源同步非常简单,只需要用synchronized关键字来标记即可。需要记住的是,在java中,同步加锁的是一个对象或者一个类,而不是代码。在多线程环境中,对象的所有synchronized方法一次只能被一个线程访问,其它所有访问同步块的线程会被一直阻塞直到同步块中的线程退出

synchronized方法控制对类对象方法的访问,每个类对象都对应一把锁,每个 synchronized 方法都必须获得该方法所属对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该对象锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个对象实例,其所有声明为 synchronized 的实例函数中至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突。注意,其它非synchronized的函数仍可被其它线程同时访问。

在Java中,不光是类的对象,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为synchronized ,以控制其对类的静态成员变量的访问。

synchronized方法的不足之处在于,若将一个大的方法声明为synchronized将会大大影响效率。如果多个线程同时调用这个方法,只有等待一个线程访问结束,从该方法返回时才释放锁,此后被阻塞的线程才可调用。典型地,如将线程类的方法run()声明为synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何synchronized方法的调用都永远不会成功。通常,为避免此种缺陷,我们不建议将整个类或者方法声明为synchronized,而是尽可能的使用synchronized代码块。synchronized关键字可同步四种代码块:

(1)实例方法
(2)静态方法
(3)实例方法中的代码块
(4)静态方法中的代码块

3、同步实例方法

public synchronized void add(int value){
    this.count += value;
}

通过上面示例可以看出,同步实例方法很简单,在类的实例方法中添加synchronized关键字即可。需要注意,同步实例方法在java中同步加锁的其实是这个方法所属的对象。对于这个对象的同步方法,一次只能有一个线程访问。如果想让两个线程同时访问这个类的方法,那么为每个线程构造一个实例即可。因此,对于同步的实例方法,每个对象都会有自己的同步方法。在某个特定对象中,它的同步方法一次之能被一个线程访问,如果有多个对象,那么每个对象都可以允许一个线程访问自己的同步方法。下面我们通过两个例子来说明一下同步实例方法。

3.1 一个对象中的同步实例方法,一次只允许一个线程访问

public class CounterExample {

    //1.构造一个计数器类Counter,这个计数器可能会被多个线程访问,需要对其中的add()方法进行同步
    static class Counter {
        long count = 0;
        //同步实例方法
        public synchronized void add() {
            count++;
            try {
                Thread. sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System. out.println(Thread. currentThread().getName() + "--" + count);
        }
    }

    //2.构造计数器线程类CounterThread 来访问计数器对象的add()方法
    static class CounterThread extends Thread {
        protected Counter counter = null;
        public CounterThread( Counter counter) {
            this. counter = counter;
        }

        @Override
        public void run() {
            //用多个线程调用同步实例方法
            for ( int i = 0; i < 5; i++) {
                counter.add();
            }
        }
    }

    //构造一个对线实例,用两个线程来同时访问一个对象实例
    public static void main(String[] args) {
        //构造一个含同步方法的对象实例
        Counter counter = new Counter();
        //用两个线程来同时访问一个对象实例
        Thread threadA = new CounterThread( counter);
        Thread threadB = new CounterThread( counter);
        threadA.start();
        threadB.start();
    }
}

结果:

Thread-0--1
Thread-1--2
Thread-1--3
Thread-1--4
Thread-1--5
Thread-1--6
Thread-0--7
Thread-0--8
Thread-0--9
Thread-0--10

通过运行结果可以看到,每次打印的结果都增加了1,说明同步的add()方法一次只有一个线程访问。

3.2 为每个线程构造一个实例对象,每个线程调用自己对象的同步实例方法

    //为每个线程构造一个对象实例,每个线程调用自己对象的同步实例方法
    public static void main(String[] args) {
        //构造两个实例,让每个线程访问一个实例
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();
        Thread threadA = new CounterThread( counter1);
        Thread threadB = new CounterThread( counter2);
        threadA.start();
        threadB.start();
    }

结果:

Thread-0--1
Thread-1--1
Thread-0--2
Thread-1--2
Thread-0--3
Thread-1--3
Thread-0--4
Thread-1--4
Thread-0--5
Thread-1--5

我们构造了两个对象,然后再构造了两个线程,让每个线程访问一个对象的同步方法。此时,两个线程会同时访问各自对象的同步方法。我们看到,每个线程打印出来的结果都是顺序加1,两个线程互不干扰。

4、同步静态方法

同步静态方法和同步实例方法的唯一区别就是同步静态方法把synchronized关键字加到了静态方法上,看下面例子:

public static synchronized void add(int value){
  count += value;
}

我们知道静态方法即类方法,它属于一个类而不是某个对象。因此同步静态方法同步的是类的方法,不是实例方法。所以即使是多个线程访问不同的对象的同步静态方法,它们之间每次也只能有一个线程访问。

public class CounterExample {

    //1.构造一个计数器类Counter ,这个计数器可能会被多个线程访问,需要对其中的add()方法进行同步
    static class Counter {
        static long count = 0;
        //同步静态方法
        public static synchronized void add() {
            count++;
            try {
                Thread. sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System. out.println(Thread. currentThread().getName() + "--" + count);
        }
    }

    //2.构造计数器线程类CounterThread 来访问计数器对象的add()方法
    static class CounterThread extends Thread {
        protected Counter counter = null;
        public CounterThread( Counter counter) {
            this. counter = counter;
        }

        @Override
        public void run() {
            //用多个线程调用同步实例方法
            for ( int i = 0; i < 5; i++) {
                counter.add();
            }
        }
    }

    //为每个线程构造一个对象实例,每个线程调用自己对象的同步实例方法
    //多个线程访问多个实例
    public static void main(String[] args) {
        //构造两个实例,让每个线程访问一个实例
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();
        Thread threadA = new CounterThread( counter1);
        Thread threadB = new CounterThread( counter2);
        threadA.start();
        threadB.start();
    }
}

结果:

Thread-0--1
Thread-1--2
Thread-1--3
Thread-1--4
Thread-1--5
Thread-1--6
Thread-0--7
Thread-0--8
Thread-0--9
Thread-0--10

我们可以看到,两个线程打印的结果都是连续加1的,add()方法是被同步访问的。如果我们去掉上面add()方法中的synchronized关键字,我们看一下结果:

Thread-0--2
Thread-1--2
Thread-0--4
Thread-1--5
Thread-0--6
Thread-1--7
Thread-0--8
Thread-1--9
Thread-0--10
Thread-1--10

可以看到,两个线程每次打印结果都增加了2,因为add()方法没有加同步关键字,所以add()方法同时被两个线程访问,所以每个线程打印出来结果都增加了2。

5、同步实例方法中的代码块

我们不必每次同步整个方法,有些时候我们希望仅仅是同步方法中的某个代码块。同步代码块主要有三种方式:synchronized(this)、synchronized(obj)、synchronized(Object.class),这三种方式分别是指同步本对象、同步其它对象、同步某个类。下面我们先分别举例说明前两种情况。

5.1 同步代码块之synchronized(this)

public class CounterExample {

    //同步代码块
    static class Counter {
        long count = 0;
        public void add() {
            //同步代码块
            synchronized(this) {
                count++;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "--" + count);
            }
        }
    }

    //2.构造计数器线程类CounterThread 来访问计数器对象的add()方法
    static class CounterThread extends Thread {
        protected Counter counter = null;
        public CounterThread( Counter counter) {
            this. counter = counter;
        }

        @Override
        public void run() {
            //用多个线程调用同步实例方法
            for ( int i = 0; i < 5; i++) {
                counter.add();
            }
        }
    }

    //构造一个对线实例,用两个线程来同时访问一个对象实例
    public static void main(String[] args) {
        //构造一个含同步方法的对象实例
        Counter counter = new Counter();
        //用两个线程来同时访问一个对象实例
        Thread threadA = new CounterThread( counter);
        Thread threadB = new CounterThread( counter);
        threadA.start();
        threadB.start();
    }
}

结果:

Thread-0--1
Thread-0--2
Thread-1--3
Thread-1--4
Thread-0--5
Thread-1--6
Thread-0--7
Thread-0--8
Thread-1--9
Thread-1--10

通过同步代码块的方式同步代码,运行结果和3.1的是一致的。请注意,同步代码块中有一个构造参数,这个参数值是一个对象,被称作监控对象,意思是同步的是此监控对象中的同步方法。上面这个例子中,(this)指的本对象。

5.2 同步代码块之synchronized(obj)

public class CounterExample {

    //同步代码块
    static class Counter {
        long count = 0;
        public void add() {
            count++;
            try {
                Thread. sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System. out.println(Thread. currentThread().getName() + "-" + count);
        }
    }

    static class CounterSynBlock {
        //声明一个实例变量
        private Counter counter;
        CounterSynBlock(Counter counter){
            this. counter= counter;
        }
        public void add() {
            //同步的是Counter对象实例
            synchronized ( counter) {
                counter.add();
            }
        }
    }

    static class CounterThread extends Thread {
        protected CounterSynBlock counter = null;
        public CounterThread( CounterSynBlock counter) {
            this. counter = counter;
        }
        public void run() {
            //用多个线程调用同步实例方法
            for ( int i = 0; i < 5; i++) {
                counter.add();
            }
        }
    }

    public static void main(String[] args) {
        //构造两个实例,让每个线程访问一个实例
        Counter counter= new Counter();
        CounterSynBlock counterSynBlock1 = new CounterSynBlock(counter);
        CounterSynBlock counterSynBlock2 = new CounterSynBlock(counter);
        Thread threadA = new CounterThread(counterSynBlock1);
        Thread threadB = new CounterThread(counterSynBlock2);
        threadA.start();
        threadB.start();
    }
}

结果:

Thread-0-1
Thread-1-2
Thread-1-3
Thread-1-4
Thread-1-5
Thread-1-6
Thread-0-7
Thread-0-8
Thread-0-9
Thread-0-10

可以看到,我们分别实例化了2个CounterSynBlock实例,然后用两个线程分别访问这两个实例的同步代码块,但这两个实例中的同步的不是CounterSynBlock 对象,而是counter对象,所以,counter中的add()方法应该是被同步访问的。

6、同步静态方法代码块

上面讲到,除了在代码块中使用synchronized(this)、synchronized(obj)之外,还可以使用synchronized(Object.class)同步一个类,既然这个类被同步了,那么多线程对所有的类变量访问也该是同步的,下面我们就举例说明。

6.1 同步静态代码块之synchronized(Object.class)

public class CounterExample {

    //同步代码块
    static class Counter {
        //注意,此处的变量是静态的,属于类变量
        static long count = 0;
        public  void add() {
            //同步的是这个类本身
            synchronized(Counter.class){
                count++;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "--" + count);
            }
        }
    }

    static class CounterThread extends Thread {
        protected Counter counter = null;
        public CounterThread(Counter counter) {
            this.counter = counter;
        }
        public void run() {
            //用多个线程调用同步实例方法
            for (int i = 0; i < 5; i++) {
                counter.add();
            }
        }
    }

    public static void main(String[] args) {
        //构造两个实例,让每个线程访问一个实例
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();
        CounterThread threadA = new CounterThread(counter1);
        CounterThread threadB = new CounterThread(counter2);
        threadA.start();
        threadB.start();
    }
}

结果:

Thread-1--1
Thread-1--2
Thread-1--3
Thread-1--4
Thread-1--5
Thread-0--6
Thread-0--7
Thread-0--8
Thread-0--9
Thread-0--10

虽然每个线程访问的是各自的对象,由于Counter类中的count变量是static的,所以count是类的变量,因为我们同步的是Counter类本身,而不是它的实例,因此多线程对类变量的访问是同步的。

7、总结

(1)在多线程环境中,可以使用synchronized关键字对资源进行同步
(2)synchronized关键字可以同步方法和代码块
(3)同步的是对象或者类,而不是代码
(4)一个对象中的同步方法一次只能被一个线程访问,如果有多个同步方法,一个线程一次也只能访问其中的一个同步方法,但是非同步方法不受任何影响
(5)同步是通过加锁的形式来控制的,让一个线程访问一个同步方法时会获得这个对象的锁,只有退出同步方法时才会释放这个锁,其它线程才可访问

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值