一个多线程的程序如果是通过Runnable接口实现的,则意味着类中的属性将被多个线程享用,那么就造成一个问题,如果这个多线程要同时操作同一资源时就有可能出现资源的同步问题。例如前面的卖票程序,多个线程同时执行时就会把票数卖为负数(线程的实现)。
- 问题的引出
现在通过Runnable接口来实现多线程,共产生3个线程对象,同时卖出5张票。
【观察程序的问题】
class MyThread implements Runnable
{
private int ticket=5;
public void run(){
for (int i=0;i<10 ;i++ )
{
if (ticket>0)
{
try
{
Thread.sleep(300);
}
catch (Exception e)
{
}
System.out.println(Thread.currentThread().getName()+"线程,卖票:ticket="+ticket--);
}
}
}
}
public class SyncDemo01
{
public static void main(String args[])
{
MyThread my=new MyThread();
new Thread(my).start();
new Thread(my).start();
new Thread(my).start();
}
}
运行结果可能出现:
Thread-2线程,卖票:ticket=5
Thread-0线程,卖票:ticket=4
Thread-1线程,卖票:ticket=4
Thread-1线程,卖票:ticket=3
Thread-2线程,卖票:ticket=2
Thread-0线程,卖票:ticket=3
Thread-2线程,卖票:ticket=0
Thread-1线程,卖票:ticket=1
Thread-0线程,卖票:ticket=-1
票数出现负数。
从程序中可以看出在程序中加入了延迟操作,所以在运行的最后出现了负数情况,那么为什么会这种问题呐?
从上面的操作代码中可以发现对于票数的操作步骤如下:
(1)判断票数是否大于0,大于0则表示还有票可以卖
(2)如果票数大于0,则将票卖出
但是,在上面的操作代码中,在步骤之间加入了延迟操作,那么就有可能在一个线程进行休眠时,另一个线程进行了操作,之后线程又进行了减减操作,导致出现负数的情况。
- 使用同步解决问题
解决资源共享的同步操作,可以使用同步代码块和同步方法两种方式完成。
- 同步代码块
在前面讲到代码块的概念代码块所谓的代码块就是指使用“{}”括起来的一段代码,根据使用位置和声明的不同可以分为普通代码块、构造块,静态块3种,如果在代码块前加上synchronized关键字,则此代码块就称为同步代码块。同步代码块使用:
【使用同步代码块解决同步问题】
- 同步代码块
class MyThread implements Runnable
{
private int ticket=5;
public void run(){
for (int i=0;i<10 ;i++ )
{
synchronized(this){
if (ticket>0)
{
try
{
Thread.sleep(300);
}
catch (Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖票:ticket="+ticket--);
}
}
}
}
};
public class SyncDemo02
{
public static void main(String args[])
{
MyThread my=new MyThread();
new Thread(my,"A").start();
new Thread(my).start();
new Thread(my).start();
}
}
运行结果:
A卖票:ticket=5
A卖票:ticket=4
Thread-1卖票:ticket=3
Thread-0卖票:ticket=2
Thread-0卖票:ticket=1
从程序运行中可以看出,以上代码将取值和修改的操作进行了同步,所以不会再出现卖出负数的情况了。
2. 同步方法
除了可以将需要的代码设置成同步代码块外,还可以使用synchronized关键字将一个方法声明为同步方法。
【使用同步方法解决上面问题】
class MyThread implements Runnable
{
private int ticket=5;
public void run(){
for (int i=0;i<10 ;i++ )
{
this.size();
}
}
public synchronized void size(){
if (ticket>0)
{
try
{
Thread.sleep(300);
}
catch (Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖票:ticket="+ticket--);
}
}
};
public class SyncDemo02
{
public static void main(String args[])
{
MyThread my=new MyThread();
new Thread(my,"A").start();
new Thread(my).start();
new Thread(my).start();
}
}
运行结果:
A卖票:ticket=5
A卖票:ticket=4
A卖票:ticket=3
Thread-1卖票:ticket=2
Thread-1卖票:ticket=1
从程序的运行结果可以发现,此代码完成了和之前同步代码块同样的功能。
学习完了synchronized关键字之后,既可以给出java中方法定义的完整格式:
访问权限 {public|default|protected|private} [final][static][synchronized]
返回值类型|void 方法名称(参数类型 参数名称,....)[throws Exception1,Exception2 ]{
[return [返回值|返回调用处]]
}
- 死锁
同步可以保证资源共享操作的正确性,但是过多的同步会产生死锁问题。所谓的死锁问题就是指在两个线程都在等待对方先完成,造成了程序的停滞,一般程序的死锁都是在程序运行时出现的。下面观察一个死锁的范例
class Zhangsan{ // 定义张三类
public void say(){
System.out.println("张三对李四说:“你给我画,我就把书给你。”") ;
}
public void get(){
System.out.println("张三得到画了。") ;
}
};
class Lisi{ // 定义李四类
public void say(){
System.out.println("李四对张三说:“你给我书,我就把画给你”") ;
}
public void get(){
System.out.println("李四得到书了。") ;
}
};
public class ThreadDeadLock implements Runnable{
private static Zhangsan zs = new Zhangsan() ; // 实例化static型对象
private static Lisi ls = new Lisi() ; // 实例化static型对象
private boolean flag = false ; // 声明标志位,判断那个先说话
public void run(){ // 覆写run()方法
if(flag){
synchronized(zs){ // 同步张三
zs.say() ;
try{
Thread.sleep(500) ;
}catch(InterruptedException e){
e.printStackTrace() ;
}
synchronized(ls){
zs.get() ;
}
}
}else{
synchronized(ls){
ls.say() ;
try{
Thread.sleep(500) ;
}catch(InterruptedException e){
e.printStackTrace() ;
}
synchronized(zs){
ls.get() ;
}
}
}
}
public static void main(String args[]){
ThreadDeadLock t1 = new ThreadDeadLock() ; // 控制张三
ThreadDeadLock t2 = new ThreadDeadLock() ; // 控制李四
t1.flag = true ;
t2.flag = false ;
Thread thA = new Thread(t1) ;
Thread thB = new Thread(t2) ;
thA.start() ;
thB.start() ;
}
};
运行结果:
李四对张三说:“你给我书,我就把画给你”
张三对李四说:“你给我画,我就把书给你。”
[一下代码不再执行,程序进入死锁状态]
从程序的运行结果来看,两个线程都在彼此等待对方的执行完成,这样,程序就无法在进行下去了,从而造成了死锁的现象。
注意的是,多线程共享同一个资源时需要进行同步,以保证资源操作的完整性,但是过多的同步可能会造成死锁。