温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

JAVA中如何应用synchronized关键字

发布时间:2021-07-23 16:10:40 来源:亿速云 阅读:113 作者:Leah 栏目:编程语言

这篇文章给大家介绍JAVA中如何应用synchronized关键字,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。

sychronized关键字有哪些特性?

可以用来修饰方法;  可以用来修饰代码块;  可以用来修饰静态方法;  可以保证线程安全;  支持锁的重入;  sychronized使用不当导致死锁;

了解sychronized之前,我们先来看一下几个常见的概念:内置锁、互斥锁、对象锁和类锁。

内置锁

在Java中每一个对象都可以作为同步的锁,那么这些锁就被称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。

互斥锁

内置锁同时也是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B抛出异常或者正常执行完毕释放这个锁;如果B线程不释放这个锁,那么A线程将永远等待下去。

对象锁和类锁

对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的。

对象锁是用于对象实例方法;  类锁是用于类的静态方法或者一个类的class对象上的

一个对象无论有多少个同步方法区,它们共用一把锁,某一时刻某个线程已经进入到某个synchronzed方法,那么在该方法没有执行完毕前,其他线程无法访问该对象的任何synchronzied 方法的,但可以访问非synchronzied方法。

如果synchronized方法是static的,那么当线程访问该方法时,它锁的并不是synchronized方法所在的对象,而是synchronized方法所在对象的对应的Class对象,

因为java中无论一个类有多少个对象,这些对象会对应唯一一个Class对象,因此当线程分别访问同一个类的两个对象的static,synchronized方法时,他们的执行也是按顺序来的,也就是说一个线程先执行,一个线程后执行。

synchronized的用法:修饰方法和修饰代码块,下面分别分析这两种用法在对象锁和类锁上的效果。

对象锁的synchronized修饰方法和代码块

public class TestSynchronized {  public void test1() {    synchronized (this) {      int i = 5;      while (i-- > 0) {        System.out.println(Thread.currentThread().getName() + " : " + i);        try {          Thread.sleep(500);        } catch (InterruptedException ie) {        }      }    }  }   public synchronized void test2() {    int i = 5;    while (i-- > 0) {      System.out.println(Thread.currentThread().getName() + " : " + i);      try {        Thread.sleep(500);      } catch (InterruptedException ie) {      }    }  }   public static void main(String[] args) {    final TestSynchronized myt2 = new TestSynchronized();    Thread test1 = new Thread(new Runnable() {      public void run() {        myt2.test1();      }    }, "test1");    Thread test2 = new Thread(new Runnable() {      public void run() {        myt2.test2();      }    }, "test2");    test1.start();    test2.start();  }}

打印结果如下:

test2 : 4test2 : 3test2 : 2test2 : 1test2 : 0test1 : 4test1 : 3test1 : 2test1 : 1test1 : 0

上述的代码,第一个方法用了同步代码块的方式进行同步,传入的对象实例是this,表明是当前对象;第二个方法是修饰方法的方式进行同步

。因为第一个同步代码块传入的this,所以两个同步代码所需要获得的对象锁都是同一个对象锁,下面main方法时分别开启两个线程,分别调用test1和test2方法,那么两个线程都需要获得该对象锁,另一个线程必须等待。

上面也给出了运行的结果可以看到:直到test2线程执行完毕,释放掉锁,test1线程才开始执行。这里test2方法先抢到CPU资源,故它先执行,它获得了锁,它执行完毕后,test1才开始执行。

如果我们把test2方法的synchronized关键字去掉,执行结果会如何呢? 

test1 : 4test2 : 4test2 : 3test2 : 2test2 : 1test2 : 0test1 : 3test1 : 2test1 : 1test1 : 0

我们可以看到,结果输出是交替着进行输出的,这是因为,某个线程得到了对象锁,但是另一个线程还是可以访问没有进行同步的方法或者代码。进行了同步的方法(加锁方法)和没有进行同步的方法(普通方法)是互不影响的,一个线程进入了同步方法,得到了对象锁,其他线程还是可以访问那些没有同步的方法(普通方法)。

类锁的修饰(静态)方法和代码块  

public class TestSynchronized {  public void test1() {    synchronized (TestSynchronized.class) {      int i = 5;      while (i-- > 0) {        System.out.println(Thread.currentThread().getName() + " : " + i);        try {          Thread.sleep(500);        } catch (InterruptedException ie) {        }      }    }  }   public static synchronized void test2() {    int i = 5;    while (i-- > 0) {      System.out.println(Thread.currentThread().getName() + " : " + i);      try {        Thread.sleep(500);      } catch (InterruptedException ie) {      }    }  }   public static void main(String[] args) {    final TestSynchronized myt2 = new TestSynchronized();    Thread test1 = new Thread(new Runnable() {      public void run() {        myt2.test1();      }    }, "test1");    Thread test2 = new Thread(new Runnable() {      public void run() {        TestSynchronized.test2();      }    }, "test2");    test1.start();    test2.start();  }}

输出结果如下:

test1 : 4test1 : 3test1 : 2test1 : 1test1 : 0test2 : 4test2 : 3test2 : 2test2 : 1test2 : 0

类锁修饰方法和代码块的效果和对象锁是一样的,因为类锁只是一个抽象出来的概念,只是为了区别静态方法的特点,因为静态方法是所有对象实例共用的,所以对应着synchronized修饰的静态方法的锁也是唯一的,所以抽象出来个类锁。其实这里的重点在下面这块代码,synchronized同时修饰静态和非静态方法

public class TestSynchronized {  public synchronized void test1() {    int i = 5;    while (i-- > 0) {      System.out.println(Thread.currentThread().getName() + " : " + i);      try {        Thread.sleep(500);      } catch (InterruptedException ie) {      }    }  }   public static synchronized void test2() {    int i = 5;    while (i-- > 0) {      System.out.println(Thread.currentThread().getName() + " : " + i);      try {        Thread.sleep(500);      } catch (InterruptedException ie) {      }    }  }   public static void main(String[] args) {    final TestSynchronized myt2 = new TestSynchronized();    Thread test1 = new Thread(new Runnable() {      public void run() {        myt2.test1();      }    }, "test1");    Thread test2 = new Thread(new Runnable() {      public void run() {        TestSynchronized.test2();      }    }, "test2");    test1.start();    test2.start();  }}

输出结果如下:

test1 : 4test2 : 4test1 : 3test2 : 3test2 : 2test1 : 2test2 : 1test1 : 1test1 : 0test2 : 0

上面代码synchronized同时修饰静态方法和实例方法,但是运行结果是交替进行的,这证明了类锁和对象锁是两个不一样的锁,控制着不同的区域,它们是互不干扰的。同样,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的。

synchronized是如何保证线程安全的

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

我们通过一个案例,演示线程的安全问题:

我们来模拟一下火车站卖票过程,总共有100张票,总共有三个窗口卖票。

public class SellTicket {  public static void main(String[] args) {    // 创建票对象    Ticket ticket = new Ticket();    // 创建3个窗口    Thread t1 = new Thread(ticket, "窗口1");    Thread t2 = new Thread(ticket, "窗口2");    Thread t3 = new Thread(ticket, "窗口3");    t1.start();    t2.start();    t3.start();  }} // 模拟票class Ticket implements Runnable {  // 共100票  int ticket = 100;   @Override  public void run() {    // 模拟卖票    while (true) {      if (ticket > 0) {        // 模拟选坐的操作        try {          Thread.sleep(1);        } catch (InterruptedException e) {          e.printStackTrace();        }        System.out.println(Thread.currentThread().getName() + "正在卖票:"            + ticket--);      }    }  }}

运行结果发现:上面程序出现了问题

票出现了重复的票  错误的票 0、-1

其实,线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

那么出现了上述问题,我们应该如何解决呢?

线程同步(线程安全处理Synchronized)

java中提供了线程同步机制,它能够解决上述的线程安全问题。

线程同步的方式有两种:

方式1:同步代码块  方式2:同步方法

同步代码块

同步代码块: 在代码块声明上 加上synchronized

synchronized (锁对象) {  可能会产生线程安全问题的代码}

同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。

使用同步代码块,对火车站卖票案例中Ticket类进行如下代码修改:

public class SellTicket {  public static void main(String[] args) {    // 创建票对象    Ticket ticket = new Ticket();    // 创建3个窗口    Thread t1 = new Thread(ticket, "窗口1");    Thread t2 = new Thread(ticket, "窗口2");    Thread t3 = new Thread(ticket, "窗口3");    t1.start();    t2.start();    t3.start();  }} // 模拟票class Ticket implements Runnable {  // 共100票  int ticket = 100;   Object lock = new Object();   @Override  public void run() {    // 模拟卖票    while (true) {      // 同步代码块      synchronized (lock) {        if (ticket > 0) {          // 模拟选坐的操作          try {            Thread.sleep(1);          } catch (InterruptedException e) {            e.printStackTrace();          }          System.out.println(Thread.currentThread().getName()              + "正在卖票:" + ticket--);        }      }    }  }}

当使用了同步代码块后,上述的线程的安全问题,解决了。

同步方法

同步方法:在方法声明上加上synchronized

public synchronized void method(){    可能会产生线程安全问题的代码}

同步方法中的锁对象是 this

使用同步方法,对火车站卖票案例中Ticket类进行如下代码修改:

public class SellTicket {  public static void main(String[] args) {    // 创建票对象    Ticket ticket = new Ticket();    // 创建3个窗口    Thread t1 = new Thread(ticket, "窗口1");    Thread t2 = new Thread(ticket, "窗口2");    Thread t3 = new Thread(ticket, "窗口3");    t1.start();    t2.start();    t3.start();  }} // 模拟票class Ticket implements Runnable {  // 共100票  int ticket = 100;   Object lock = new Object();   @Override  public void run() {    // 模拟卖票    while (true) {      // 同步方法      method();    }  }   // 同步方法,锁对象this  public synchronized void method() {    if (ticket > 0) {      // 模拟选坐的操作      try {        Thread.sleep(10);      } catch (InterruptedException e) {        e.printStackTrace();      }      System.out.println(Thread.currentThread().getName() + "正在卖票:"          + ticket--);    }  }}

synchronized支持锁的重入吗?  

我们先来看下面一段代码:

public class ReentrantLockDemo {  public synchronized void a() {    System.out.println("a");    b();  }   private synchronized void b() {    System.out.println("b");  }   public static void main(String[] args) {    new Thread(new Runnable() {      @Override      public void run() {        ReentrantLockDemo d = new ReentrantLockDemo();        d.a();      }    }).start();  }}

上述的代码,我们分析一下,两个方法,方法a和方法b都被synchronized关键字修饰,锁对象是当前对象实例,按照上文我们对synchronized的了解,如果调用方法a,在方法a还没有执行完之前,我们是不能执行方法b的,方法a必须先释放锁,方法b才能执行,方法b处于等待状态,那样不就形成死锁了吗?那么事实真的如分析一致吗?

运行结果发现:

ab

代码很快就执行完了,实验结果与分析不一致,这就引入了另外一个概念:重入锁。在 java 内部,同一线程在调用自己类中其他 synchronized 方法/块或调用父类的 synchronized 方法/块都不会阻碍该线程的执行。就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入。在JDK1.5后对synchronized关键字做了相关优化。

synchronized死锁问题

同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。

synchronzied(A锁){  synchronized(B锁){  }}

我们进行下死锁情况的代码演示:

public class DeadLock {  Object obj1 = new Object();  Object obj2 = new Object();   public void a() {    synchronized (obj1) {      synchronized (obj2) {        System.out.println("a");      }    }  }   public void b() {    synchronized (obj2) {      synchronized (obj1) {        System.out.println("b");      }    }  }   public static void main(String[] args) {    DeadLock d = new DeadLock();    new Thread(new Runnable() {      @Override      public void run() {        d.a();      }    }).start();     new Thread(new Runnable() {      @Override      public void run() {        d.b();      }    }).start();  }}

上述的代码,我们分析一下,两个方法,我们假设两个线程T1,T2,T1运行到方法a了,拿到了obj1这把锁,此时T2运行到方法b了,拿到了obj2这把锁,T1要往下执行,就必须等待T2释放了obj2这把锁,线程T2要往下面执行,就必须等待T1释放了持有的obj1这把锁,他们两个互相等待,就形成了死锁。

为了演示的更明白,需要让两个方法执行过程中睡眠10ms,要不然很难看到现象,因为计算机执行速度贼快

public class DeadLock {  Object obj1 = new Object();  Object obj2 = new Object();   public void a() {    synchronized (obj1) {      try {        Thread.sleep(10);      } catch (InterruptedException e) {        e.printStackTrace();      }      synchronized (obj2) {        System.out.println("a");      }    }  }   public void b() {    synchronized (obj2) {      try {        Thread.sleep(10);      } catch (InterruptedException e) {        e.printStackTrace();      }      synchronized (obj1) {        System.out.println("b");      }    }  }   public static void main(String[] args) {    DeadLock d = new DeadLock();    new Thread(new Runnable() {      @Override      public void run() {        d.a();      }    }).start();     new Thread(new Runnable() {      @Override      public void run() {        d.b();      }    }).start();  } }

关于JAVA中如何应用synchronized关键字就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI