记JAVA的二三事(4)——线程的同步和互斥

概念

线程互斥的概念:当有多个线程(窗口)要访问同一个资源时(火车票),如果多个线程同时对该资源进行读和写操作,A窗口读取到剩余100张火车票,卖出一张,剩下99张,将剩余票数写入数据库。因为是同时进行,B窗口也读取到剩余100张火车票,卖出一张,剩下99张。此时数据库中显示火车票剩下99张。但是实际上卖掉两张火车票后应该只剩下98张火车票。显然多个线程不能同时访问一个资源,此时就引入了互斥的概念,同一时间一个资源只能被一个线程独占,其他线程要等待该线程执行完毕后才能访问这个资源。

线程同步的概念:同样还是多个窗口卖火车票。此时多了一个缓冲区(黄牛),官方窗口将票卖给黄牛,消费者从黄牛手中买票。当缓冲区满了(黄牛手里票满了)的时候,窗口进入休眠状态,而当黄牛手里没票的时候,消费者进入休眠状态。如何调度和唤醒窗口和消费者这两边的线程,就涉及到了同步的概念:多个线程按照某种逻辑顺序来访问临界区的资源。

同步和互斥的区别:同步也是互斥,并且是一种要求更严格更复杂的互斥。互斥只是线程对资源的独占使用,执行顺序是乱序的。而同步不仅是对资源的独占使用,并且协调关联线程合作完成任务,执行顺序是有序的。

实现线程同步有很多种方法:1.synchronized修饰符(同步方法+同步代码块) 2.volatile特殊域变量 3.Lock类 4.ThreadLocal修饰变量

下面给出一个多线程例子,首先是不考虑线程同步的情况:

package com.test.thread;

public class ThreadTest1 {
    public static void main(String[] args) {
        BankAcount acount = new BankAcount(500);
        for (int i = 0; i < 10; i++) {
            new Thread(new PutThread(acount, "小明")).start();
            new Thread(new GetThread(acount, "小紫")).start();
        }

    }
}

class BankAcount {
    // 账户余额
    private int value;
    private Object obj;
    private String name;

    public BankAcount(int value) {
        this.value = value;
        obj = new Object();
    }

    // 从账户取钱
    public void get(int i, String name) {
            this.name = name;
            if (value - i > 0) {
                value = value - i;
            } else {
                i = value;
                value = 0;
            }
            System.out.println(name + "从账户取了" + i + "元,账户里还剩下:" + value + "元");
    }

    // 往账户存钱
    public void put(int i, String name) {
            value = value + i;
            System.out.println(name + "往账户放了" + i + "元,账户里还剩下:" + value + "元");
    }
}

class PutThread implements Runnable {
    private BankAcount acount;
    private String name;

    public PutThread(BankAcount acount, String name) {
        this.acount = acount;
        this.name = name;
    }

    @Override
    public void run() {
        int random = (int) (Math.random() * 1000 % 50);
        acount.put(random, name);
        try {
            Thread.currentThread().sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class GetThread implements Runnable {
    private BankAcount acount;
    private String name;

    public GetThread(BankAcount acount, String name) {
        this.acount = acount;
        this.name = name;
    }

    @Override
    public void run() {
        int random = (int) (Math.random() * 1000 % 50);
        acount.get(random, name);
        try {
            Thread.currentThread().sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

syso:

小明往账户放了28元,账户里还剩下:508元
小明往账户放了6元,账户里还剩下:508元
小紫从账户取了22元,账户里还剩下:508元
小紫从账户取了4元,账户里还剩下:508元
小紫从账户取了37元,账户里还剩下:471元
小明往账户放了13元,账户里还剩下:484元
小明往账户放了8元,账户里还剩下:492元
小紫从账户取了5元,账户里还剩下:487元
小明往账户放了31元,账户里还剩下:518元
小紫从账户取了22元,账户里还剩下:496元
小明往账户放了4元,账户里还剩下:500元
小明往账户放了7元,账户里还剩下:507元
小紫从账户取了46元,账户里还剩下:461元
小明往账户放了25元,账户里还剩下:486元
小明往账户放了22元,账户里还剩下:508元
小紫从账户取了7元,账户里还剩下:501元
小紫从账户取了24元,账户里还剩下:477元
小紫从账户取了43元,账户里还剩下:434元
小紫从账户取了25元,账户里还剩下:409元
小明往账户放了5元,账户里还剩下:414元

根据打印信息可以发现账户余额跟我们预想的不一致,所以我们要使用synchronized来保证线程的互斥性。

一、synchronized修饰符

修饰方法

我们只要保证put(存钱)和get(取钱)两个方法在同一时间只被一个线程调用即可,所以给get和put方法加一个synchronized修饰符

// 从账户取钱
    public synchronized void get(int i, String name) {
            this.name = name;
            if (value - i > 0) {
                value = value - i;
            } else {
                i = value;
                value = 0;
            }
            System.out.println(name + "从账户取了" + i + "元,账户里还剩下:" + value + "元");
    }

    // 往账户存钱
    public synchronized void put(int i, String name) {
            value = value + i;
            System.out.println(name + "往账户放了" + i + "元,账户里还剩下:" + value + "元");
    }

看一下打印信息:

小紫从账户取了31元,账户里还剩下:469元
小紫从账户取了3元,账户里还剩下:466元
小明往账户放了9元,账户里还剩下:475元
小紫从账户取了4元,账户里还剩下:471元
小紫从账户取了20元,账户里还剩下:451元
小明往账户放了26元,账户里还剩下:477元
小明往账户放了15元,账户里还剩下:492元
小明往账户放了6元,账户里还剩下:498元
小明往账户放了6元,账户里还剩下:504元
小紫从账户取了49元,账户里还剩下:455元
小紫从账户取了8元,账户里还剩下:447元
小紫从账户取了0元,账户里还剩下:447元
小明往账户放了18元,账户里还剩下:465元
小明往账户放了6元,账户里还剩下:471元
小明往账户放了23元,账户里还剩下:494元
小紫从账户取了15元,账户里还剩下:479元
小紫从账户取了36元,账户里还剩下:443元
小明往账户放了20元,账户里还剩下:463元
小明往账户放了16元,账户里还剩下:479元
小紫从账户取了4元,账户里还剩下:475元

ok了,这次的账户余额信息就符合我们的预期了。synchronized修饰方法本质上是锁住了该类(BankAcount)的this对象。当一个线程调用了该类的某个对象中的synchronized方法时,这个对象中的所有方法都不允许被其他线程调用。如果修饰的是静态方法,那么这个类的所有对象中的所有方法在同一时间都只能被一个线程独占。那么有些人可能会疑惑:既然修饰一个方法其他方法就不允许被调用了,那上面的例子中为何还要将两个方法都修饰起来?当我只修饰get方法时,线程调用此方法,其他线程不会来调用get和put方法,这没有问题。但是若线程调用put方法,那么其他线程依旧会访问put和get方法。因此我要把put和get两个方法都修饰起来。

修饰代码块

跟修饰方法类似,新建一个对象,在要求互斥的代码块上锁住这个对象,在同一时间就只有一个线程访问该代码块了。

// 从账户取钱
    public void get(int i, String name) {
        synchronized (obj) {
            this.name = name;
            if (value - i > 0) {
                value = value - i;
            } else {
                i = value;
                value = 0;
            }
            System.out.println(name + "从账户取了" + i + "元,账户里还剩下:" + value + "元");
        }
    }

    // 往账户存钱
    public  void put(int i, String name) {
        synchronized(obj){
            value = value + i;
            System.out.println(name + "往账户放了" + i + "元,账户里还剩下:" + value + "元");}
    }

syso:

小紫从账户取了48元,账户里还剩下:452元
小紫从账户取了29元,账户里还剩下:423元
小明往账户放了29元,账户里还剩下:452元
小明往账户放了34元,账户里还剩下:486元
小明往账户放了10元,账户里还剩下:496元
小紫从账户取了35元,账户里还剩下:461元
小明往账户放了2元,账户里还剩下:463元
小紫从账户取了1元,账户里还剩下:462元
小明往账户放了20元,账户里还剩下:482元
小紫从账户取了24元,账户里还剩下:458元
小明往账户放了17元,账户里还剩下:475元
小紫从账户取了37元,账户里还剩下:438元
小明往账户放了26元,账户里还剩下:464元
小紫从账户取了17元,账户里还剩下:447元
小明往账户放了24元,账户里还剩下:471元
小紫从账户取了21元,账户里还剩下:450元
小明往账户放了45元,账户里还剩下:495元
小紫从账户取了41元,账户里还剩下:454元
小明往账户放了44元,账户里还剩下:498元
小紫从账户取了35元,账户里还剩下:463元

当然,这个新建对象不一定是Object类型,任意类型都可以,如果是在一些对空间要求较高的项目中,可以新建一个byte[]类型的对象。

二、volatile特殊域变量

用voloatile修饰变量的一个最大特性就是使变量具有线程可见性,当一个线程中修改了volatile变量时,其他线程能够马上得到该变量的最新对象。如上述例子中,我们可以对value(账户余额)这个变量加一个volatile修饰符,只是在上面的例子中,线程修改value与打印value并不一致(volatile不能保证原子操作),因此控制台显示的打印信息估计会有误差,但最终结果应该是正确的。

三、Lock类

我们可以使用ReentrantLock类的lock()和unlock()方法对代码块进行上锁操作以此实现线程的互斥。代码跟synchronized类似。

class BankAcount {
    private Lock lock;// 锁
    // 账户余额
    private volatile int value;
    // private Object obj;
    private String name;

    public BankAcount(int value) {
        lock = new ReentrantLock();
        this.value = value;
        // obj = new Object();
    }

    // 从账户取钱
    public void get(int i, String name) {
        lock.lock();//上锁
        try {
            this.name = name;
            if (value - i > 0) {
                value = value - i;
            } else {
                i = value;
                value = 0;
            }
            System.out.println(name + "从账户取了" + i + "元,账户里还剩下:" + value + "元");
        } finally {
            lock.unlock();//解锁
        }
    }

// 往账户存钱
    public void put(int i, String name) {
        lock.lock();
        try {
            value = value + i;
            System.out.println(name + "往账户放了" + i + "元,账户里还剩下:" + value + "元");
        } finally {
            lock.unlock();
        }
    }
}

syso:

小明往账户放了18元,账户里还剩下:518元
小紫从账户取了7元,账户里还剩下:511元
小明往账户放了14元,账户里还剩下:525元
小紫从账户取了24元,账户里还剩下:501元
小明往账户放了5元,账户里还剩下:506元
小紫从账户取了27元,账户里还剩下:479元
小紫从账户取了0元,账户里还剩下:479元
小明往账户放了2元,账户里还剩下:481元
小紫从账户取了18元,账户里还剩下:463元
小明往账户放了43元,账户里还剩下:506元
小紫从账户取了16元,账户里还剩下:490元
小明往账户放了4元,账户里还剩下:494元
小紫从账户取了38元,账户里还剩下:456元
小明往账户放了47元,账户里还剩下:503元
小紫从账户取了18元,账户里还剩下:485元
小明往账户放了36元,账户里还剩下:521元
小紫从账户取了5元,账户里还剩下:516元
小明往账户放了12元,账户里还剩下:528元
小紫从账户取了47元,账户里还剩下:481元
小明往账户放了24元,账户里还剩下:505元

在每次上锁完毕后都要记得解锁,不然会发生死锁。

四、ThreadLocal修饰变量

这种方法我也没怎么使用过,在上面的例子中,就是将value的int类型改成ThreadLocal类型,当然相关代码都要修改。用处是在线程调用value时创建一个value的副本,每个线程调用的value实质上都是value的副本,相互之间不会有什么影响,与同步机制各有优劣。

ok,实现线程同步的方法暂时就想到这么多。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值