likes
comments
collection
share

java线程同步机制有哪些?

作者站长头像
站长
· 阅读数 7

前言

java知识体系中,多线程之间的数据同步机制在面试中经常被问到,因为在工作中用的也不频繁,很容易当时理解了,后面不用又忘记了,特别是各种锁的特点,很容易搞乱,这里做一个系统的梳理和总结,以便加深记忆。

Synchronized

Synchronized即同步锁,可以修饰方法和代码块,根据情况不同可以分为方法锁,对象锁和类锁。

方法锁

synchronized修饰方法时,称之为方法锁,如果此方法为非静态方法,也可称为对象锁,因为在同一对象实例下,两个线程同时访问此方法会共用一个锁。示例代码如下:

public class CLock{
        //非静态方法锁,使用的是对象锁。相同的CLock实例访问同一方法共用一个锁。不同实例的话无论是相同方法还是不同方法用的都不是一个锁。
        public  void synchronized  test1(){
        }
        //此方法也是非静态方法锁,在同一对象实例下,和test1共用一个对象,也就共用一把锁
        public void synchronized test2(){
        }
}

对象锁

对象锁不是说synchronized修饰在对象之上,而是指synchronized修饰的非静态方法或者代码块上,当不同线程访问的时候,共用的也是一把锁,称之为对象锁。示例代码如下:

public class CLock{
        //这里是非静态方法锁,也称之为对象锁
        public void synchronized  test1(){}
        public void synchronized test2(){}
        public void test3(){
                //非静态方法种的代码块,如果用this来加锁,也是对象锁。和上面的非静态方法使用同一个锁。
                synchronized(this){}
        }
}

类锁

类锁不是指synchronized修饰在类之上,而是在概念上理解锁住一个类,synchronized修饰在静态方法上或者代码块中如果用.class类来加锁,则代表此锁是类锁,也就是这个类无论建多少对象实例,在此类下访问用synchronized修饰的静态方法时用的都是同一个锁。

这里可以看出和对象锁的区别,对象锁是在同一对象下共用锁,在多个对象中是各自有独立的锁,这样理解就可以和类锁很好区分了。代码示例如下:

public class CLock{
        //静态方法锁,使用的是类锁。不同实例下,多个线程访问这两个方法也是共用一个锁
        public static synchronized void test1(){ }
        public static synchronized void test2(){}
        public void test3(){
                //非静态方法种的代码块,如果用.class类来加锁,也是类锁。和上面的静态方法使用同一个锁。
                synchronized(CLock.class){}
        }
}

ReentrantLock

ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。示例代码如下:

public class A {
 
    public static ReentrantLock reentrantLock = new ReentrantLock();
 
    public static void main(String[] args) {
        new Thread(() -> {
            testSync();
        }, "t1").start();
 
        new Thread(() -> {
            testSync();
        }, "t2").start();
    }
 
    public static void testSync(){
        //加锁更加灵活,可以在指定位置加锁
        reentrantLock.lock();
        try {
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            reentrantLock.unlock();
        }
    }
}

ReentrantLock与Synchronized对比

  1. Synchronized是独占锁,加锁和解锁过程自己进行,易于操作,但不够灵活,ReentrantLock也是独占锁,加锁和解锁需手动进行,不易操作,但非常灵活
  2. Synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。
  3. Synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以响应中断。
  4. Synchronized不能实现公平锁,ReentrantLock可以实现公平锁。

volatile

  1. volatile属于一种轻量级的同步机制,保证了可见性和有序性(不保证原子性),如果一个共享变量被volatile关键字修饰,那么线程A要修改此变量,修改结果会立即刷新到主存中.线程B的共享变量缓存就会失效,需要重新从主内存重新读取最新值。

  2. volatile通过内存屏障禁止指令重排序优化,保证了有序性。

  3. volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令

单例模式的双重锁为什么要加volatile

public class TestInstance{
  private volatile static TestInstance instance;
  public static TestInstance getInstance(){   //1
    if(instance == null){   //2
      synchronized(TestInstance.class){   //3
        if(instance == null){   //4
          instance = new TestInstance(); //5
        }
      }
    }
    return instance;   //6
  }
}

需要volatile关键字的原因是;在并发情况下;如果没有volatile关键字;在第5行会出现问题。instance = new TestInstance();可以分解为3行伪代码

a. memory = allocate() //分配内存
b. ctorInstanc(memory) //初始化对象
c. instance = memory   //设置instance指向刚分配的地址

上面的代码在编译运行时;如果没有使用volatile修饰可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行第5行代码时;B线程进来执行到第2行代码。假设此时A执行的过程中发生了指令重排序;即先执行了ac;没有执行b。那么由于A线程执行了c导致instance指向了一段地址;所以B线程判断instance不为null;会直接跳到第6行并返回一个未初始化的对象。

知识拓展

公平锁

公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁 加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得

非公平锁

JVM 随机就近原则分配锁的机制则称为非公平锁,非公平锁实际执行的效率要远超公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护等待队列 Java中的 synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的是非公平锁,也可以在创建ReentrantLock对象时指定构造函数参数来设置非公平锁

可重入锁(递归锁)

指的是以线程为单位,当一个线程获取对象锁后,这个线程可以再次获取对象上的锁,而其他线程是不可以的 , ReentrantLock 和 synchronized 都是 可重入锁

死锁

死锁是指两个或多个线程在执行过程中,因为争夺资源而相互等待,导致它们都进入停滞状态的现象,在Java中,这通常发生在多个线程尝试以不同的顺序获取相同的锁时。

避免死锁的策略

  1. 锁顺序:最基本的一条规则是:总是以固定的顺序获取锁。
  2. 锁超时:另一个策略是使用锁超时。这意味着线程在尝试获取锁时不会无限等待。
  3. 使用并发工具类:最后,Java并发API提供了一些高级工具,比如java.util.concurrent包中的类,可以帮助咱们更好地管理锁和避免死锁。例如,Semaphore可以用来控制对资源的并发访问数,而CountDownLatchCyclicBarrier可以用于线程间的同步。