首页 > 编程语言 > 利用synchronized实现线程同步的案例讲解
2021
02-21

利用synchronized实现线程同步的案例讲解

一、前期基础知识储备

(1)线程同步的定义:多线程之间的同步。

(2)多线程同步原因:一个多线程的程序如果是通过Runnable接口实现的,则意味着类中的属性将被多个线程共享,由此引出资源的同步问题,即当多个线程要操作同一资源时,有可能出现错误。

(3)实现多线程同步的方式——引入同步机制:在线程使用一个资源时为其加锁,这样其他的线程便不能访问那个资源了,直到解锁后才可以访问。——这样做的结果,所有线程间会有资源竞争,但是所有竞争的资源是同步的,刷新的,动态的,不会因为线程间的竞争,导致资源“过度消耗”或者“虚拟消耗”。

上代码,具体展示“过度消耗/虚拟消耗”问题:

public class TestTicketRunnable{
  public static void main(String[] a){
    TicketThread tThread = new TicketThread();
    new Thread(tThread).start();
    new Thread(tThread).start();
    new Thread(tThread).start();
  }
};
class TicketThread implements Runnable {
  private int ticket = 5;
  public void run(){
    for (int i = 0; i < 5; i++){
      if (ticket > 0){
        try {
          Thread.sleep(300);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "卖票:ticket = " + ticket--);
      }
    }
  }
};

运行结果:

Thread-0卖票:ticket = 5
Thread-2卖票:ticket = 5 //虚拟消耗
Thread-1卖票:ticket = 4
Thread-1卖票:ticket = 2
Thread-2卖票:ticket = 3
Thread-0卖票:ticket = 3 //虚拟消耗
Thread-0卖票:ticket = -1 //过度消耗
Thread-1卖票:ticket = 1
Thread-2卖票:ticket = 0

如上所见,票一共5张,三个线程调用买票,线程1网上卖了售票第1张,紧接着线程2线下也卖了“第一张”出现了“虚拟消耗”的问题;线程3黄牛党卖了最后1张票,线程1网上又卖了最后1张,出现了“过度消耗”的问题,这两种问题都是实际生活中不可能发生的,但是在这个3个线程执行中却出现了,那一定是有问题的,问题的根源就在于,三个渠道的“售票员”不在一个频道上办事,或者说没有相互之间同步所共享的资源,导致这一问题的根本原因,就是相互之间实现方式不同步。

二、使用synchronized实现同步机制

synchronized关键字:Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。

当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

它包括两种用法:synchronized 方法和 synchronized 块。

即实现线程间同步的方式有两种:

①使用synchronized同步代码块;

②使用synchronized关键字创建synchronized()方法

下面分别进行解析,对上面售票的代码进行改造:

①代码——使用synchronized同步代码块

class TicketThread implements Runnable {
  private int ticket = 5;
  public void run(){
    for (int i = 0; i < 5; i++){
      synchronized(this){
        if (ticket > 0){
        try {
          Thread.sleep(300);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "卖票:ticket = " + ticket--);
      }
      }
    }
  }
}

②代码——使用synchronized关键字创建synchronized()方法

class TicketThreadMethod implements Runnable {
  private int ticket = 5;
  public void run(){
    for (int i = 0; i < 5; i++){
      this.sale();
    }
  }
  public synchronized void sale(){
      if (ticket > 0){
        try {
          Thread.sleep(300);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "卖票:ticket = " + ticket--);
      }
  }
}

三、synchronized方法和synchronized同步代码块的区别:

synchronized同步代码块只是锁定了该代码块,代码块外面的代码还是可以被访问的。

synchronized方法是粗粒度的并发控制,某一个时刻只能有一个线程执行该synchronized方法。

synchronized同步代码块是细粒度的并发控制,只会将块中的代码同步,代码块之外的代码可以被其他线程同时访问。

补充:多线程同步锁synchronized(对象锁与全局锁)总结

1.synchronized同步锁的引入

/*
 * 非线程安全
 * */
//多个线程共同访问一个对象中的实例变量,则会出现"非线程安全"问题
class MyRunnable1 implements Runnable{
 private int num = 10;
 public void run() {
 try {
  if(num > 0) {
  System.out.println(""+Thread.currentThread().getName()+"开始"+",num= "+num--);
  Thread.sleep(1000);
  System.out.println(""+Thread.currentThread().getName()+"结束");
  }
 } catch (InterruptedException e) {
  e.printStackTrace();
 }
 }
}public class Test5_5{
 public static void main(String[] args) {
 MyRunnable1 myRunnable1 = new MyRunnable1();
 Thread thread1 = new Thread(myRunnable1,"线程1");
 Thread thread2 = new Thread(myRunnable1,"线程2");
 thread1.start();
 thread2.start();
 }
}

上例说明两个线程同时访问一个没有同步的方法,如果两个线程同时操作业务对象中的实例变量,则会出现“线程不安全”问题。

由此我们引入synchronized关键字来实现同步问题:

在Java中使用synchronized关键字控制线程同步,控制synchronized代码段不被多个线程同时执行,synchronized即可以使用在方法上也可以使用在代码块中。

2. 对象锁

(1)synchronized方法(对当前对象进行加锁)

若我们对如上代码进行修改,在run()方法上加入synchronized关键字使其变为同步方法。

/*
 * 同步方法
 * */
class MyRunnable1 implements Runnable{
 private int num = 10;
 public void run() {
 this.print();
 }
 
 public synchronized void print() {
 if(this.num > 0) {
  System.out.println(""+Thread.currentThread().getName()+"开始"+",num= "+num--);
  try {
  Thread.sleep(1000);
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
  System.out.println(""+Thread.currentThread().getName()+"结束");
 }
 }
}public class Test5_5{
 public static void main(String[] args) {
 MyRunnable1 myRunnable1 = new MyRunnable1();
 Thread thread1 = new Thread(myRunnable1,"线程1");
 Thread thread2 = new Thread(myRunnable1,"线程2");
 thread1.start();
 thread2.start();
 }
}  

结论:若两个线程同时访问同一个对象中的同步方法时一定是线程安全的。

(2)synchronized代码块(对某一个对象进行加锁)

如果要使用同步代码块必须设置一个要锁定的对象,所以一般可以锁定当前对象:this.

/*
 * 同步代码块
 * */
class MyRunnable1 implements Runnable{
 private int num = 10;
 public void run() {
 try {
  synchronized (this) {
  if(num > 0) {
   System.out.println(""+Thread.currentThread().getName()+"开始"+",num= "+num--);
   Thread.sleep(1000);
   System.out.println(""+Thread.currentThread().getName()+"结束");
  } 
  }
 } catch (InterruptedException e) {
  e.printStackTrace();
 }
 }
}
 
public class Test5_5{
 public static void main(String[] args) {
 MyRunnable1 myRunnable1 = new MyRunnable1();
 Thread thread1 = new Thread(myRunnable1,"线程1");
 Thread thread2 = new Thread(myRunnable1,"线程2");
 thread1.start();
 thread2.start();
 }
} 

(3)synchronized锁多对象

/*
 * synchronized锁多对象
 * */
class Sync{
 public synchronized void print() {
 System.out.println("print方法开始:"+Thread.currentThread().getName());
 try {
  Thread.sleep(1000);
 } catch (InterruptedException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
 }
 System.out.println("print方法结束"+Thread.currentThread().getName());
 }
}
class MyThread extends Thread{
 public void run() {
 Sync sync = new Sync();
 sync.print();
 }
}
public class Test5_5{
 public static void main(String[] args) {
 for(int i = 0; i < 3;i++) {
  Thread thread = new MyThread();
  thread.start();
 }
 }
}

根据上例我们可以发现当synchronized锁多个对象时不能实现同步操作,由此可以得出关键字synchronized取得的锁都是对象锁,而不是将一段代码或者方法(函数)当作锁。哪个线程先执行带synchronized关键字的方法或synchronized代码块,哪个线程就有该方法或该代码块所持有的锁,其他线程只能呈现等待状态,前提是多个线程访问同一个对象。

只有共享资源的读写需要同步化,如果不是共享资源,那么就不需要同步化操作。

3.全局锁

实现全局锁有两种方式:

(1) 将synchronized关键字用在static方法上

synchronized加到static静态方法上是对Class类上锁,而synchronized加到非static方法上是给对对象上锁。Class锁可以对类的所有对象实例起作用。

/*
 * synchronized用在static方法上
 * */
class Sync{
 static public synchronized void print() {
 System.out.println("print方法开始:"+Thread.currentThread().getName());
 try {
  Thread.sleep(1000);
 } catch (InterruptedException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
 }
 System.out.println("print方法结束"+Thread.currentThread().getName());
 }
}
class MyThread extends Thread{
 public void run() {
 Sync.print();
 }
}
public class Test5_5{
 public static void main(String[] args) {
 for(int i = 0; i < 3;i++) {
  Thread thread = new MyThread();
  thread.start();
 }
 }
}

(2) 用synchronized对类的Class对象进行上锁

synchronized(class)代码块的作用与synchronized static方法的作用一样。

/*
 * synchronized对类的Class对象上锁
 * */
class Sync{
 public void print() {
 synchronized (Sync.class) {
  System.out.println("print方法开始:"+Thread.currentThread().getName());
  try {
  Thread.sleep(1000);
  } catch (InterruptedException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  }
  System.out.println("print方法结束"+Thread.currentThread().getName());
 }
 }
}
class MyThread extends Thread{
 public void run() {
 Sync sync = new Sync();
 sync.print();
 }
}
public class Test5_5{
 public static void main(String[] args) {
 for(int i = 0; i < 3;i++) {
  Thread thread = new MyThread();
  thread.start();
 }
 }
}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持自学编程网。如有错误或未考虑完全的地方,望不吝赐教。

编程技巧