新闻资讯

新闻资讯 行业动态

实现线程同步

编辑:006     时间:2020-02-11
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问造成的这种问题。

由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized 方法和 synchronized 块。

▪ synchronized方法

通过在方法声明中加入 synchronized关键字来声明,语法如下:

public synchronized void accessVal(int newVal); 
  • 1

synchronized方法控制对“对象的类成员变量”的访问:每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。(即表明上锁的是方法,实际上锁的是对象)

▪ synchronized块

synchronized方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。
Java为我们提供了更好的解决办法,那就是synchronized块。块可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率。

synchronized块:通过 synchronized关键字来声明synchronized 块,语法如下:

synchronized(Object){    //允许访问控制的代码 } 
  • 1
  • 2
  • 3
  • 4

“synchronized (Object)” 意味着线程需要获得对象Object的“锁”才有资格访问对象。对象的“锁”也称为“互斥锁”,在同一时刻只能被一个线程使用。A线程拥有锁,则可以调用“同步块”中的代码;B线程没有锁,则进入Object对象的“锁池队列”等待,直到A线程使用完毕释放了Object对象的锁,B线程得到锁才可以开始调用“同步块”中的代码。

• 同步监视器

• synchronized (obj){ }中的obj称为同步监视器
• 同步代码块中同步监视器可以是任何对象,但是推荐使用共享资源作为同步监视器

• 同步方法中无需指定同步监视器,因为同步方法的同步监视器是this,也就是该对象本身

• 同步监视器的执行过程

• 第一个线程访问,锁定同步监视器,执行其中代码
• 第二个线程访问,发现同步监视器被锁定,无法访问
• 第一个线程访问完毕,解锁同步监视器

• 第二个线程访问,发现同步监视器未锁,锁定并访问

看两个实际的例子

//模拟抢票
public class SynTest01 {
    public static void main(String[] args) {
        Safeweb123 web = new Safeweb123();
        new Thread(web, "1").start();
        new Thread(web, "2").start(); //访问的是同一个对象web,
        new Thread(web, "3").start(); //所以可以用synchronized方法来锁住整个对象
    }
}

class Safeweb123 implements Runnable {
    private int tick = 100;
    private boolean flag = true;
    @Override
    public void run() {
        while (flag) {
            test();
        }
    }
    //同步方法:方法里涉及的变量全是this对象的成员变量
    public synchronized void test() {
        if (tick <= 0) {
            flag = false;
            return;
        }
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"-->"+tick--);
    }
}


在上述例子中,如果不对方法进行synchronized修饰,那么就有可能导致抢到负数张票。

再看一个例子。

//模拟提款
public class SynBlockTest01 {
    public static void main(String[] args) {
        Account account = new Account(100, "结婚礼金");   //账户里有100元
        SynDrawing you = new SynDrawing(account, 80, "可悲的你");  //你取80
        SynDrawing wife = new SynDrawing(account, 90, "Happy的她");  //你妻子取90
        
        you.start();  //这里的两个线程各自访问不同的SynDrawing对象,
        wife.start(); //所以锁this对象无效,因为这两个对象都各只有一个线程在访问
    }
}
class Account {
    int money; //金额
    String name;

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}
class SynDrawing extends Thread {
    Account account; //取款账户
    int drawingMoney;//取的款
    int packetTotal;//取的总数

    public SynDrawing(Account account, int drawingMoney, String name) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
        test();
    }

    public void test() {
        if(account.money <= 0)
            return;
        //同步块:锁的是account对象,因为两个线程都会访问它
        synchronized (account) {
            if (account.money - drawingMoney < 0)
                return;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.money -= drawingMoney;
            packetTotal += drawingMoney;
            System.out.println(getName() + "-->账户余额为:" + account.money);
            System.out.println(getName() + "-->口袋的钱为:" + packetTotal);
        }
    }
}

性能分析

在第一个例子里,我们是这样锁的:

public synchronized void test() {
        if (tick <= 0) {
            flag = false;
            return;
        }
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"-->"+tick--);
    }


显然,无论票数是否小于等于0,后边的线程都会排队等待解锁,这样做其实效率不够高,所以我们考虑缩小锁的范围

1、锁tick

public void test1() {
        synchronized (Integer.valueOf(tick)) {
            if (tick <= 0) {
                flag = false;
                return;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "-->" + tick--);
        }
    }


这样的结果会导致负数票,原因是由于tick的数目在不断变化导致它的对象也在不断变化,锁一个不断变化的对象是无意义的。

2、把判断语句移出同步块

public void test2() {
        if (tick <= 0) {   
            flag = false;
            return;
        }
        synchronized (this) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "-->" + tick--);
        }
    }


这样做看上去没问题,但很遗憾,这实际上是线程不安全的,当剩最后一张票时,三个线程都通过了前面的判断条件,后两个线程只需等待第一个线程执行完同步块就可以继续执行,所以还是会出现负数票。

3、在同步块中额外判断tick是否小于等于0

public void test3() {
        if (tick <= 0) {   //考虑没票
            flag = false;
            return;
        }
        synchronized (this) {
            if (tick <= 0) {   //考虑最后一张票
                flag = false;
                return;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "-->" + tick--);
        }
    } 

这样线程就安全了,虽然代码量看上去多了,但确实提高了运行效率,这就是double checking

郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。

回复列表

相关推荐