Java并发编程之synchronized关键字

1. synchronized基本用法

  • 定义:如果一个对象对多个线程可见,synchronized能够保证在同一时刻最多只有一个线程操作这个对象,以达到保证并发安全的效果。
  • 作用:保证可见性和原子性,可以避免线程安全问题:运行结果错误
  • 两种使用方法:
    1. 对象锁:

      1. 方法锁,默认锁对象为this当前实例对象
      public class ObjectLock3 implements Runnable {
          @Override
          public void run() {
              method();
          }
      
          public synchronized void method() {
              System.out.println(Thread.currentThread().getName() + "进入同步方法");
              try {
                  Thread.sleep(2000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      
          public static void main(String[] args) {
              ObjectLock3 objectLock3 = new ObjectLock3();
              Thread t1 = new Thread(objectLock3);
              Thread t2 = new Thread(objectLock3);
              t1.start();
              t2.start();
          }
      }
      
      1. 同步代码块锁,自己指定锁对象
      public class ObjectLock1 implements Runnable {
      
          @Override
          public void run() {
              synchronized (this) {
                  System.out.println(Thread.currentThread().getName() + "进入同步代码块");
                  try {
                      Thread.sleep(3000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName() + "退出同步代码块");
              }
          }
      
          public static void main(String[] args) {
              ObjectLock1 objectLock1 = new ObjectLock1();
              new Thread(objectLock1).start();
              new Thread(objectLock1).start();
          }
      }
      Thread-0进入同步代码块
      Thread-0退出同步代码块
      Thread-1进入同步代码块
      Thread-1退出同步代码块
      
      public class ObjectLock2 implements Runnable {
      
          private static final Object lock1 = new Object();
          private static final Object lock2 = new Object();
      
          @Override
          public void run() {
              synchronized (lock1) {
                  System.out.println(Thread.currentThread().getName() + "进入同步代码块1");
                  try {
                      Thread.sleep(3000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName() + "退出同步代码块1");
              }
              synchronized (lock2) {
                  System.out.println(Thread.currentThread().getName() + "进入同步代码块2");
                  try {
                      Thread.sleep(3000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName() + "退出同步代码块2");
              }
          }
          public static void main(String[] args) {
              ObjectLock2 objectLock2 = new ObjectLock2();
              new Thread(objectLock2).start();
              new Thread(objectLock2).start();
          }
      }
      Thread-0进入同步代码块1
      Thread-0退出同步代码块1
      Thread-0进入同步代码块2
      Thread-1进入同步代码块1
      Thread-1退出同步代码块1
      Thread-0退出同步代码块2
      Thread-1进入同步代码块2
      Thread-1退出同步代码块2
      
    2. 类锁:

      1. 静态方法锁,synchronized加在static方法上,锁对象为当前类
      public class ObjectStaticLock1 implements Runnable {
      
          @Override
          public void run() {
              method();
          }
      
          public static synchronized void method() {
              System.out.println(Thread.currentThread().getName() + "进入到同步静态方法中");
              try {
                  Thread.sleep(2000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(Thread.currentThread().getName() + "退出同步静态方法");
          }
      
          public static void main(String[] args) {
              ObjectStaticLock1 objectStaticLock1 = new ObjectStaticLock1();
              ObjectStaticLock1 objectStaticLock2 = new ObjectStaticLock1();
              Thread t1 = new Thread(objectStaticLock1);
              Thread t2 = new Thread(objectStaticLock2);
              t1.start();
              t2.start();
          }
      }
      
      1. 同步代码块锁,synchronized(*.class)代码块,指定锁对象为class对象,所谓的类锁,不过是Class对象的锁而已
      public class ObjectStaticLock2 implements Runnable {
          @Override
          public void run() {
              synchronized (ObjectStaticLock2.class) {
                  System.out.println(Thread.currentThread().getName() + "进入到同步代码块");
                  try {
                      Thread.sleep(2000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName() + "退出同步代码块");
              }
          }
      
          public static void main(String[] args) {
              ObjectStaticLock2 objectStaticLock1 = new ObjectStaticLock2();
              ObjectStaticLock2 objectStaticLock2 = new ObjectStaticLock2();
              Thread t1 = new Thread(objectStaticLock1);
              Thread t2 = new Thread(objectStaticLock2);
              t1.start();
              t2.start();
          }
      }
      

2. 多线程访问同步方法的7种情况

  1. 两个线程同时访问一个对象的同步方法:会发生同步,锁对象都为同一个实例对象;
  2. 两个线程同时访问两个对象的同步方法:互不影响,锁对象不同;
  3. 两个线程访问的是synchronized的静态方法:会发生同步,锁对象都为Class对象,Class对象只有一个;
  4. 同时访问同步方法和非同步方法:非同步方法不受影响,不发生同步;
  5. 访问同一个对象不同的普通同步方法:会发生同步,锁对象默认为同一个实例对象;
  6. 同时访问静态synchronized和非静态synchronized方法:互不影响,静态syn方法的锁对象为Class对象,非静态syn方法的锁对象为一个实例对象this,实例对象和Class对象不是同一个对象,实例对象在堆中,Class对象在方法区中;
  7. 方法抛出异常后,会释放锁

总结:
1. 一把锁只能同时被一个线程获取,没拿到锁的线程必须等待,如1、5;
2. 每个实例都有自己的一把锁,不同实例互不影响,当使用Class对象以及synchonized修饰的static方法的时候,所有对象共用同一把类锁,对应2、3、4、5;
3. 遇到异常,会释放锁,对应7;

3. synchronized关键字的性质

3.1 可重入

  • 定义:一个线程已经获取到锁,想再次获取到这把锁时不需要释放,直接可以用;
  • 什么是不可重入:一个线程获取到锁之后,想再次使用这个锁,必须释放锁之后还其他线程竞争;
  • 好处:避免死锁:假如一个类有两个synchronized方法,当一个线程执行了方法1获得了默认的this对象锁,这个时候要执行方法2,如果synchronized不具备可重入性,那么这个线程就无法获取到访问方法2的锁,又无法释放锁,就造成了死锁。
  • 粒度:线程范围,在一个线程中,只要这个线程拿到了这把锁,在这个线程内部就可以一直使用
    1. 同一个方法是可重入的;
    2. 可重入不要求是同一个方法;
    3. 可重入不要求是同一个类中;

3.2 不可中断

一旦这个锁已经被别的线程获得了,如果本线程还想获得,该线程只能等待或阻塞,直到别的线程释放这个锁。如果别的线程永远不释放锁,那么本线程则永远等待下去。
相比之下,Lock类,拥有可以中断的能力:

  • 如果等的时间过长,可以中断现在已经获取的锁的线程的执行;
  • 如果等待时间过长,也可以退出。

4. synchronized原理

4.1 加锁和释放锁原理

  • 每个一个对象都有一个内置的monitor锁,这个锁存储在对象头中的,锁的获取和释放实际上需要执行两个指令:monitorenter和monitorexit,当线程执行到monitorenter的时候会尝试获取这个锁;
  • 反编译:先javac demo.java,然后javap -verbose demo.class文件;
  • monitorenter和monitorexit在执行的时候会让对象锁的计数+1或-1;
  • 获取锁的过程:首先一个线程要获取一个对象锁的时候会查看这个monitor锁的计数器如果为0,那么就给他+1,这样别的线程就进不来了,如果一个线程有了这把锁,又重入了,在计数器再+1;如果monitor被其他线程持有了,直到计数器=0,才会获取这个锁。
  • 释放锁的过程:将monitor的计数器-1,直到=0,表示不再拥有所有权了,如果不是0,说明刚才是可重入进来的

4.2 可重入原理

一个线程拿到一把锁之后,还想再次进入由这把锁所控制的方法,则可以再次进入,原理是用了monitor锁的计数器。

  • JVM负责跟踪被加锁的次数
  • 线程第一次给对象加锁的时候,计数+1.每当这个相同的线程再次获取该对象锁的时候,计数器会递增;
  • 每当任务离开的时候,计数递减,当计数为0的时候,锁被完全释放;

4.3 可见性原理

线程A和线程B通信:

  1. 本地内存A把修改后的内容放到主内存中;
  2. 本地内存B从主内存从读取修改后的内容;

synchnized修饰的代码块对对象的任何修改,在释放锁之前都要将修改的内容先写回到主内存中,所以从主内存中读取的内容都是最新的。

5. synchronized的缺陷

  • 效率低:锁的释放情况少(只有代码执行完和抛异常)、试图获得锁时候不能设定超时、不能中断一个正在试图获得锁的线程
  • 不够灵活:加锁和释放的时机单一,每个锁仅仅有单一的条件,可能是不够的。读写锁更灵活。
  • 无法知道是否成功获取到锁,没法去尝试获取,去判断。Lock是可以通过tryLock方法尝试获取,返回true代表成功加锁。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章