首页 > 编程笔记 > Java笔记 阅读:35

Java synchronized实现线程同步的2种方法(附带实例)

在多线程的程序中,有多个线程并发运行,这多个并发执行的线程往往不是孤立的,它们之间可能会共享资源,也可能要相互合作完成某一项任务,如何使多个并发执行的线程在执行过程中不产生冲突,是多线程编程必须解决的问题。否则,可能导致程序运行的结果不正确,甚至造成死锁问题。

多线程引发的问题

有时候在多线程的程序设计中需要实现多个线程共享同一段代码,从而实现共享同一个私有成员或类的静态成员的目的。这时,由于线程和线程之间互相竞争 CPU 资源,使得线程无序地访问这些共享资源,最终可能导致得不到正确的结果。这些问题通常称为线程安全问题。

下面讲解一个共享数据对象的例子。

【实例】多线程并发可能引发的问题。在主线程中通过同一个 Runnable 对象创建 10 个线程对象,这 10 个线程共享 Runnable 对象的成员变量 num,在线程中通过循环实现对成员变量 num 加 1000 的操作,10 个子线程运行过之后,显示相加的结果。
public class ThreadUnsafe {
    public static void main(String argv[]) {
        ShareData shareData = new ShareData();    // 实例化 shareData 对象
        for (int i = 0; i < 10; i++) {
            new Thread(shareData).start();    // 通过 shareData 对象创建线程并启动
        }
    }
}

class ShareData implements Runnable {
    public int num = 0;    // 记数变量
    private void add() {
        int temp;    // 临时变量
        // 循环体让变量 num 执行加 1 操作,使用 temp 是为了增加线程切换的概率
        for (int i = 0; i < 1000; i++) {
            temp = num;
            temp++;
            num = temp;
        }
        // 输出线程信息和 num 的当前值
        System.out.println(Thread.currentThread().getName() + "~" + num);
    }
    public void run() {
        add();    // 调用 add() 方法
    }
}
程序运行结果为:

Thread-0-2000
Thread-9-9930
Thread-8-9930
Thread-7-8000
Thread-6-7000
Thread-5-6000
Thread-4-5000
Thread-3-4000
Thread-2-3000
Thread-1-2000

由于线程的并发执行,多个线程对共享变量 num 进行修改,导致每次运行输出的内容都不一样,很少会出现线程输出 10000 的结果。

为了解决这一类问题,必须要引入同步机制,那么什么是同步,如何实现在多线程访问同一资源的时候保持同步?Java 使用“锁”机制来实现线程的同步。锁机制要求每个线程在进入共享代码之前都要取得锁,否则不能进入,在退出共享代码之前释放该锁,这样就能防止几个或多个线程竞争共享代码的情况,从而解决线程不同步的问题。

Java 的同步机制可以通过对关键代码段使用 synchronized 关键字修饰来实现针对该代码段的同步操作。实现同步的方式有两种,一种是利用同步代码块来实现同步,另一种是利用同步方法来实现同步。下面分别介绍这两种方法。

同步代码块

JAVA 虚拟机为每个对象配备一把锁和一个等候集,这个对象可以是实例对象,也可以是类对象。对实例对象进行加锁,可以保证与这个实例对象关联的线程可以互斥地使用对象的锁;对类对象进行加锁,可以保证与这个类相关联的线程可以互斥地使用类对象的锁。

用 synchonized 声明的语句块称为同步代码块,同步代码块的语法格式如下:
synchronized(synObject) {
    // 关键代码
}
其中的关键代码必须获得对象 synObject 的锁才能执行。当一个线程欲进入该对象的关键代码时,JVM 将检查该对象的锁是否被其他线程获得,如果没有,则 JVM 把该对象的锁交给当前请求锁的线程,该线程获得锁后就可以进入关键代码区域。

【实例】构建一个信用卡账户,起初信用额为 10000,然后模拟透支、存款等多个操作。显然银行账户 User 对象是个竞争资源,应该把修改账户余额的语句放在同步代码块中,并将账户的余额设为私有变量,禁止直接访问。
public class CreditCard {
    public static void main(String[] args) {
        // 创建一个用户对象
        User u = new User("张三", 10000);
        // 创建6线程对象
        UserThread t1 = new UserThread("线程A", u, 200);
        UserThread t2 = new UserThread("线程B", u, -600);
        UserThread t3 = new UserThread("线程C", u, -800);
        UserThread t4 = new UserThread("线程D", u, -300);
        UserThread t5 = new UserThread("线程E", u, 1000);
        UserThread t6 = new UserThread("线程F", u, 200);
        // 依次启动线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
        t6.start();
    }
}

class UserThread extends Thread {
    private User u;    // 创建一个 User 对象
    private int y = 0;
    // 构造方法,初始化成员变量
    UserThread(String name, User u, int y) {
        super(name);    // 调用父类的构造方法,设置线程名
        this.u = u;
        this.y = y;
    }

    public void run() {
        u.oper(y);    // 调用 User 对象的 oper() 方法操作共享数据
    }
}

class User {
    private String code; // 用户卡号
    private int cash;    // 用户卡上的余额
    User(String code, int cash) {
        this.code = code;
        this.cash = cash;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    // 存取款操作方法
    public void oper(int x) {
        try {
            Thread.sleep(10);
            // 把修改共享数据的语句放在同步代码块中
            synchronized (this) {
                this.cash += x;
                System.out.println(Thread.currentThread().getName() + " 运行结束,增加 " + x + ",当前用户账户余额为:" + cash);
            }
            Thread.sleep(10);    // 线程休眠10ms
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public String toString() {
        return "User{" + "code='" + code + '\'' + ", cash=" + cash + '}';
    }
}
程序运行结果为:

线程A运行结束,增加“200”,当前用户账户余额为:10200
线程F运行结束,增加“200”,当前用户账户余额为:10400
线程C运行结束,增加“-800”,当前用户账户余额为:9600
线程D运行结束,增加“-300”,当前用户账户余额为:9300
线程E运行结束,增加“1000”,当前用户账户余额为:10300
线程B运行结束,增加“-600”,当前用户账户余额为:9700

注意,在使用 synchronized 关键字时,应该尽可能避免在 synchronized 方法或 synchronized 块中使用 sleep() 或者 yield() 方法,因为 synchronized 程序块占有着对象锁,一旦处于休眠状态,其他线程无法获得所需对象,就只能继续等待,这会严重影响程序的执行效率。

同步方法

同步方法和同步代码块的功能是一样的,都是利用互斥锁实现关键代码的同步访问。只不过在这里通常关键代码就是一个方法的方法体,此时只需要调用 synchronized 关键字修饰该方法即可。一旦被 synchronized 关键字修饰的方法已被一个线程调用,那么所有其他试图调用同一实例中的该方法的线程都必须等待,直到该方法被调用结束,释放锁给下一个等待的线程。

通过在方法声明中加入 synchronized 关键字来声明 synchronized 方法,示例代码如下:
public synchronized void accessVal(int newVal);

【实例】在主线程中通过同一个 Runnable 对象创建两个线程对象,Runnable 对象中有一个同步方法实现输出线程信息,一个线程输出完之后,另一个线程才能开始输出信息。在主线程中启动这两个线程,实现对同步方法的调用。
public class PrintThread {
    private String name;

    public static void main(String[] args) {
        MethodSync ms = new MethodSync();    // 实例化 MethodSync 对象
        Thread t1 = new Thread(ms, "线程 A");    // 通过 MethodSync 对象创建线程
        Thread t2 = new Thread(ms, "线程 B");    // 通过 MethodSync 对象创建线程
        t1.start();    // 启动线程
        t2.start();    // 启动线程
    }
}

class MethodSync implements Runnable {
    // 同步方法
    public synchronized void show() {
        System.out.println(Thread.currentThread().getName() + " 同步方法开始");
        System.out.println(Thread.currentThread().getName() + " 其他信息...... ");
        System.out.println(Thread.currentThread().getName() + " 同步方法结束");
    }

    public void run() {
        show();    // 调用 show() 方法显示线程的相关信息
    }
}
程序运行结果为:

线程A 同步方法开始
线程A其它信息......
线程A 同步方法结束
线程B 同步方法开始
线程B其它信息......
线程B 同步方法结束

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

相关文章