新闻资讯
实现线程同步
由于我们可以通过 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
回复列表