synchronized关键字
同步代码块:
cpu正在执行某个线程时可能会被其它线程打断,导致出现我们不想要的结果。比如:
这段代码:
publicclass Lianxi10 {
public static void main(String[] args) {
final Printer p = new Printer();
new Thread(){//线程一
public void run(){
while(true){
p.print1();
}
}
}.start();
new Thread(){//线程二
public void run(){
while(true){
p.print2();
}
}
}.start();
}
}
class Printer{
D d = new D();
public void print1(){
// synchronized(newD()){
System.out.print("郑");
System.out.print("州");
System.out.print("大");
System.out.print("学");
System.out.println();
// }
}
public void print2(){
//synchronized(newD()){
System.out.print("中");
System.out.print("原");
System.out.print("福");
System.out.print("塔");
System.out.println();
//}
}
}
在执行过程中可能出现如下情况:
在线程一执行过程中只打印了”郑州”二字时,CPU被线程二抢夺,开始执行打印“中原福塔”并换行,这样的结果并不是我们想要的,我们期望的结果是线程一的代码块在执行过程中作为一个整体不被打断,这时就需要使用synchronized关键字。把一段代码当做一个整体在执行的过程中不会被打断。
特别要注意的是,在synchronized关键字中需要传入一个锁对象,锁对象可以是任意对象,包括Object对象,但这个锁对象必须是同一个对象,如果不是同一个锁对象,那么相当于这个整体的代码块有了另一个锁,那么在执行的过程中还是会被打断。
比如:
在每一个关键字中传入的锁对象都是新创建的对象(锁对象不能用匿名对象),那么这两个对象不同,即使上了锁还是会被另一把钥匙打开,执行的结果如图:
所以要想保证整个代码块在执行过程中不被打断,那么就应该传入同一个锁对象,例如上图中创建了一个D类的对象d,那么我们就可以在传入锁对象时都传入d(如下图所示),这样就不会出现被打断的结果。
同步方法:
同步方法只需要在方法声明时加上syncornized关键字即可,非静态方法的锁对象是this。
代码示例:
静态方法的锁对象是class文件,或者创建一个静态对象。在静态方法中不能使用this,但是在类加载的时候也有对象,这个对象就是字节码对象(.class对象)。但是如果是使用字节码文件,那么都用字节码文件作为锁对象,如果是使用静态对象,那么都用静态对象,这样才能保证是同一个对象。
代码示例:
都使用字节码文件作为锁对象:
都使用静态对象:
如果一个是字节码文件作为锁对象,一个是静态对象作为锁对象,就还是会出现被打断。错误代码示例:
线程安全问题:
例如火车站窗口卖票的问题:
使用继承Thread类的方法创建线程对象,定义非静态变量ticket=100,总票数是100,创建多个线程对象,它们在访问ticket时,都使用自己一人一份ticket,这样就会导致同一张票在每个窗口都卖出,出现非正常现象。错误代码示例如下:
运行结果:
为了使每个窗口在卖票的时候共享同一个变量ticket,我们可以把ticket设置为静态变量,这样该变量就属于整个类,被所有对象共享。但是还是会出现同一张票被多个窗口卖出的情况,运行结果如下:
这里的第100张票被线程0和线程1都执行了,出现这种情况的原因是在线程0执行的过程中,在输出卖出第100张票时还没来得及对ticket做--运算,线程1就抢占了CPU,那么它访问到的ticke就还是100,在线程1准备输出时,线程0抢占了CPU,执行了ticket--运算,线程2抢占,访问到的ticket是已经--过的,执行了输出语句,线程1抢占继续它上次的执行,即输出卖出第100张票。也就是说虽然每个线程对象现在共享了同一个变量,但是还是会因为CPU抢占的问题导致一张票被多个窗口卖出。所以我们还是需要进行同步代码块的处理。把if判断和输出作为一个整体,因为这两部分都涉及到了ticket,在同一个线程访问时不希望被打断,所以把这一部分设置为同步代码块。
代码示例:
特别要注意的是在Thread子类中创建的锁对象一定要是静态对象,让每一个子类对象执行子类中的锁对象时共享同一个对象,否则在创建的三个子类对象在执行线程时会各自创建一个锁对象,这样就会出现被打断现象。
实现Runnable接口的方式共享数据:
使用Runnable接口可以实现数据共享是因为创建的三个线程对象都使用了同一个线程目标执行类对象(m),所以就不会出现每一张票都要被三个窗口卖出。但是为了保证ticket在一个线程执行过程中不会被打断,还需要进行同步代码块,即在while循环中加上synchronized关键字。