Java中线程同步的方法

一、同步方法

即有 synchronized 关键字修饰的方法。 由于 java 的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

注: synchronized 关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。

二、同步代码块

即有 synchronized 关键字修饰的语句块。 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步

代码如:

synchronized(object){
}

注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用 synchronized 代码块同步关键代码即可

package com.xhj.thread;

    /**
     * 线程同步的运用
     *
     * @author XIEHEJUN
     *
     */
    public class SynchronizedThread {

        class Bank {
            private int account = 100;
            public int getAccount() {
                return account;
            }

            /**
             * 用同步方法实现
             *
             * @param money
             */
            public synchronized void save(int money) {
                account += money;
            }

            /**
             * 用同步代码块实现
             *
             * @param money
             */
            public void save1(int money) {
                synchronized (this) {
                    account += money;
                }
            }
        }
class NewThread implements Runnable {
            private Bank bank;

            public NewThread(Bank bank) {
                this.bank = bank;
            }

            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    // bank.save1(10);
                    bank.save(10);
                    System.out.println(i + "账户余额为:" + bank.getAccount());
                }
            }

        }

        /**
         * 建立线程,调用内部类
         */
        public void useThread() {
            Bank bank = new Bank();
            NewThread new_thread = new NewThread(bank);
            System.out.println("线程1");
            Thread thread1 = new Thread(new_thread);
            thread1.start();
            System.out.println("线程2");
            Thread thread2 = new Thread(new_thread);
            thread2.start();
        }

        public static void main(String[] args) {
            SynchronizedThread st = new SynchronizedThread();
            st.useThread();
        }

    }

synchronized 同步方法小结:

给一个方法增加 synchronized 修饰符之后就可以使它成为同步方法,这个方法可以是静态方法和非静态方法,但是不能是抽象类的抽象方法,也不能是接口中的接口方法。下面代码是一个同步方法的示例:

public synchronized void aMethod() {
    // do something
}

public static synchronized void anotherMethod() {
    // do something
}

三、wait 与 notify

wait():使一个线程处于等待状态,并且释放所持有的对象的 lock。Object.wait()

sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉 InterruptedException 异常。Thread.sleep()

notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且不是按优先级。

Allnotity():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。

四、使用特殊域变量(volatile)实现线程同步

a. volatile 关键字为域变量的访问提供了一种免锁机制

b. 使用 volatile 修饰域相当于告诉虚拟机该域可能会被其他线程更新

c. 因此每次使用该域就要重新计算,而不是使用寄存器中的值

d. volatile 不会提供任何原子操作,它也不能用来修饰 final 类型的变量

例如:在上面的例子当中,只需在 account 前面加上 volatile 修饰,即可实现线程同步。

//只给出要修改的代码,其余代码与上同
        class Bank {
            //需要同步的变量加上volatile
            private volatile int account = 100;

            public int getAccount() {
                return account;
            }
            //这里不再需要synchronized
            public void save(int money) {
                account += money;
            }

五、使用重入锁实现线程同步

在 JavaSE5.0 中新增了一个 java.util.concurrent 包来支持同步。

ReentrantLock 类是可重入、互斥、实现了 Lock 接口的锁,它与使用 synchronized 方法和快具有相同的基本行为和语义,并且扩展了其能力。

ReenreantLock 类的常用方法有:

ReentrantLock() // 创建一个ReentrantLock实例
lock() // 获得锁
unlock() // 释放锁

注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用

//只给出要修改的代码,其余代码与上同
        class Bank {

            private int account = 100;
            //需要声明这个锁
            private Lock lock = new ReentrantLock();
            public int getAccount() {
                return account;
            }
            //这里不再需要synchronized
            public void save(int money) {
                lock.lock();
                try{
                    account += money;
                }finally{
                    lock.unlock();
                }

            }

注:关于 Lock 对象和 synchronized 关键字的选择:

a. 最好两个都不用,使用一种 java.util.concurrent 包提供的机制,能够帮助用户处理所有与锁相关的代码。

b. 如果 synchronized 关键字能满足用户的需求,就用 synchronized,因为它能简化代码

c. 如果需要更高级的功能,就用 ReentrantLock 类,此时要注意及时释放锁,否则会出现死锁,通常在 finally 代码释放锁

六、使用阻塞队列实现线程同步

前面 5 种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。 使用 javaSE5.0 版本中新增的 java.util.concurrent 包将有助于简化开发。

本小节主要是使用 LinkedBlockingQueue来实现线程的同步 LinkedBlockingQueue是一个基于已连接节点的,范围任意的 blocking queue。

LinkedBlockingQueue 类常用方法

LinkedBlockingQueue() // 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue
put(E e) // 在队尾添加一个元素,如果队列满则阻塞
size() // 返回队列中的元素个数
take() // 移除并返回队头元素,如果队列空则阻塞

注:BlockingQueue定义了阻塞队列的常用方法,尤其是三种添加元素的方法,我们要多加注意,当队列满时:

add()方法会抛出异常
offer()方法返回false
put()方法会阻塞

七、使用原子变量实现线程同步

需要使用线程同步的根本原因在于对普通变量的操作不是原子的。

那么什么是原子操作呢?原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作即-这几种行为要么同时完成,要么都不完成。在 java 的 util.concurrent.atomic 包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。其中 AtomicInteger 表可以用原子方式更新 int 的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换 Integer;可扩展 Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。

AtomicInteger 类常用方法:

AtomicInteger(int initialValue) // 创建具有给定初始值的新的
AtomicIntegeraddAddGet(int dalta) // 以原子方式将给定值与当前值相加
get() // 获取当前值

补充:原子操作主要有:

  1. 对于引用变量和大多数原始变量(long 和 double 除外)的读写操作;
  2. 对于所有使用 volatile 修饰的变量(包括 long 和 double)的读写操作。

实例:

class Bank {
    private AtomicInteger account = new AtomicInteger(100);
    public AtomicInteger getAccount() {
        return account;
    }
    public void save(int money) {
        account.addAndGet(money);
    }
}