最近刚开始学习多线程,写小球碰撞过程中遇到了问题:
多个线程小球在运动时,画布上会出现残影:
画小球的主要方法是设置画笔为背景色 - 画小球 - 设置画笔随机颜色 - 画小球,再通过调用另一个 move 方法让它们动起来,这样就能起到一个擦除轨迹实现运动的效果。关键代码如下:
//擦除小球
public void drawDelect(){
g.setColor(Color.lightGray); //设置背景色
g.fillOval(x - r - 1, y - r - 1, r * 2 + 3, r * 2 + 3);
}
//画新小球
public void drawNew(){
g.setColor(new Color(green,red,blue)); //设置小球颜色
g.fillOval(x - r / 2 - i / 2, y - r / 2 - i / 2, i * 2, i * 2);
}
线程的 run 方法:
public void run(){
System.out.println("Thread start");
for (;;){
this.drawDelect(); //擦除小球轨迹
this.move(); //移动小球
this.collide(); //判断碰撞
this.drawNew(); //画小球
}
}
经过分析,找到了原因:
每一个线程都创建了一个小球,但是不同线程的小球都是共用一个画笔 g ,也就是同一时间只有一个线程能拿到这支画笔。而每个线程对小球处理时,是先设置画笔颜色,再调用画笔画小球。
这里就会出现一个问题,比如,线程1设置背景颜色后,正想调用画笔 g 擦除小球轨迹,结果画笔 g 被线程2抢走了,线程1只能等待线程2画完才能拿起画笔,而这段时间小球依然在移动,移动的轨迹就没有被擦除,于是就留下了“残影”。
解决方法有两个,一个是彻底改写程序,只用一个线程,就不会有这个问题了。这里不讨论这个方法。
另一个方法就是,让执行这一小段代码时,画笔不能被其他线程抢走。
这里就是这篇博客要说的,synchronized 的使用了。
画小球的方法这样写,就不会出现这个问题了:
//擦除小球
public void drawDelect(){
//用 synchronized 声明同步块
synchronized (g) {
g.setColor(Color.lightGray);
g.fillOval(x - r - 1, y - r - 1, r * 2 + 3, r * 2 + 3);
}
}
//画新小球
public void drawNew(){
//用 synchronized 声明同步块
synchronized (g){
g.setColor(new Color(green,red,blue));
g.fillOval(x - r / 2 - i / 2, y - r / 2 - i / 2, i * 2, i * 2);
}
}
在这里,synchronized 就是一把锁,保证在同一时刻,只有一个线程可以拿到锁去执行某个方法或某个代码块(同步块)。这里的括号里的参数是对象锁,线程执行到这里必须要获得这个对象的锁才能执行下面的同步块。
好了,进入正题。
synchronized 实现原理
Java专门提供了负责管理线程对象中同步方法访问的工具—同步模型监视器,其原理是为每个具有同步代码的对象准备唯一一把“锁”。在Java程序中,通过 wait()、notify() 及 notifyAll() 方法可以完成线程间的消息传递。当前线程调用 wait() 方法可以使该线程进入不可运行状态,其他线程调用 notify() 或 notifyAll() 方法可以唤醒该线程。更底层的原理就不便展开了(我也不是太懂
synchronized 的用法
上面讲到的是对方法里的一小段代码加锁,锁是括号里面的对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。即:
public void method(){
synchronized(obj){
//……
}
}
还有就是对整个方法加锁,即将该方法声明为同步:
public class demo{}
public synchronized void method(){
//……
}
}
当两个线程同时对一个对象的一个方法进行操作,只有一个线程能够抢到锁。因为一个对象只有一把锁,一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,就不能访问该对象的其他 synchronized 实例方法,但是可以访问非 synchronized 修饰的方法。
但是如果两个个线程实例化出两个对象,对对象的一个方法进行操作,因为两个线程作用于不同的对象,获得的是不同的锁,所以互相并不影响。
例如以下代码创建了一个局部对象obj,由于每个线程执行到 Object obj = new Object()
时,都会产生一个 obj 对象,每个线程都可以获得创建的新的 obj 对象的锁,不会相互影响,因此这段程序不会起到同步作用。
public void method(){
Object obj = new Object(); //创建局部 Object 类型对象 obj
synchronized(obj){
//……
}
}
还有需要注意的是,synchronized 作用于静态方法时,两个线程实例化两个不同的对象,但是访问的方法是静态的,两个线程发生了互斥(即一个线程访问,另一个线程只能等着),因为静态方法是依附于类而不是对象的,当 synchronized 修饰静态方法时,锁是 class 对象。
如下面代码中 increase
方法是静态方法,用 synchronized 修饰时,就算是两个线程实例化两个不同的对象来访问,因为此时锁的其实是 class 对象,所以结果和两个线程同时对一个对象来访问的效果一样。
public class synchronizedTest implements Runnable {
//共享资源
static int i =0;
//synchronized 修饰静态方法
public static synchronized void increase(){
i++;
}
}