首页 > 代码库 > 多线程同步

多线程同步

问题引入:线程的安全问题

以下程序发现出现0号票甚至负号票,原因都是由多线程操作共享资源saleTask所导致的线程安全问题。

 1 class Example1
 2 {
 3     public static void main(String[] args) 
 4     {
 5         SaleTask saleTask = new SaleTask();     //创建SaleTask对象
 6         //创建四个线程并启动
 7         new Thread(saleTask,"窗口一").start();
 8         new Thread(saleTask,"窗口二").start();
 9         new Thread(saleTask,"窗口三").start();
10         new Thread(saleTask,"窗口四").start();
11         
12     }
13 }
14 
15 //定义SaleThread类实现Runnable接口
16 class SaleTask implements Runnable
17 {
18     private int tickets = 10;
19     public void run()
20     {
21         while(tickets>0){
22             try{
23                 Thread.sleep(10);    //次出让线程休眠10毫秒,模拟售票延迟。
24             }catch(InterruptedException e){
25                e.printStackTrace();
26             }
27              System.out.println(Thread.currentThread().getName()+"...sales..."+tickets--);
28          }
29     }
30 }

 

问题解决:同步代码块或同步方法

线程安全问题其实是由于多个线程同时处理共享资源导致的。要解决这个问题,那么必须保证用于处理共享资源的代码在任何时刻都只能有一个线程在访问。

为了实现这种限制,Java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个代码块中,使用synchronized关键字来修饰,被称作同步代码块,语法格式如下:

synchronized (lock)
{
   操作共享资源的代码块
}

上面的代码中,lock是一个锁对象,它是同步代码块的关键。当线程执行同步代码块时,首先会检查锁对象的标志位,默认情况下标志位为1,此线程会执行同步代码块,同时将锁对象的标志位置0。当一个新的线程执行到这段同步代码块时,由于锁对象的标志位是0,新线程会发生阻塞,等待当前线程执行完同步代码块后,锁对象的标志位被置为1,新线程才能进入同步代码块执行其中的代码。循环往复,直到共享资源被处理完为止。这个过程好比一个公共电话亭,只有前面一个人打完电话出来以后,后面的人才能进去打电话。

下面用同步代码块改进程序:

 1 class Example2
 2 {
 3     public static void main(String[] args) 
 4     {
 5         Ticket1 ticket = new Ticket1();
 6         new Thread(ticket,"窗口一").start();
 7         new Thread(ticket,"窗口二").start();
 8         new Thread(ticket,"窗口三").start();
 9         new Thread(ticket,"窗口四").start();
10         
11     }
12 }
13 
14 
15 class Ticket1 implements Runnable
16 {
17     private int tickets = 10;
18     Object lock = new Object();             //定义任意一个对象,作为同步代码块的锁,必须放在run方法之外!
19     public void run()
20     {
21         while(true){
22             synchronized(lock){            //定义同步代码块
23              try{
24                 Thread.sleep(10);
25                 }catch(InterruptedException e){
26                     e.printStackTrace();
27                   }
28               if(tickets>0){
29                        System.out.println(Thread.currentThread().getName()+"...sales..."+tickets--);
30                }else{
31                       break;
32                }
33          }
34       }
35    }
36 }

注意:同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的。

 

同步方法

在方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能。

被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会被阻塞,直到当前线程访问完毕后,其他线程才有机会执行方法。

运用如下:

 1 class Example3
 2 {
 3     public static void main(String[] args) 
 4     {
 5         Ticket1 ticket = new Ticket1();
 6         new Thread(ticket,"窗口一").start();
 7         new Thread(ticket,"窗口二").start();
 8         new Thread(ticket,"窗口三").start();
 9         new Thread(ticket,"窗口四").start();
10         
11     }
12 }
13 
14 
15 class Ticket1 implements Runnable
16 {
17     private int tickets = 10;
18     public void run()
19     {
20         while(true){
21             saleTicket();      //调用售票方法
22             if(tickets<=0){
23                  break;
24               }
25          }
26      }
27 
28     //定义一个同步方法saleTicket()
29     private synchronized void saleTicket()
30     {
31         if(tickets>0)
32         {
33              try{
34                    Thread.sleep(10);
35                 }catch(InterruptedException e){
36                     e.printStackTrace();
37                   }
38               System.out.println(Thread.currentThread().getName()+"...sales..."+tickets--);
39         }
40     
41     }
42 }             
43 
                 

同步方法的锁就是当前调用该方法的对象,也就是this指向的对象。这样做的好处是,同步方法被所有线程共享,方法所在的对象相对于所有线程来说是唯一的,从而保证了锁的唯一性。当一个线程执行该方法时,其他线程就不能进入该方法中,直到这个线程执行完该方法为止,从而达到线程同步的效果。

静态同步函数进内存的时候不存在对象,但存在其所属类的字节码文件对象,属于Class类型的对象,所以静态同步函数的锁是其所属类的字节码文件对象,该对象可以直接用“类名.class”的方式获取。

总结:

同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行。但线程在执行同步代码时每次都要判断锁的状态,非常消耗资源,效率较低。

多线程同步