Java线程同步synchronized和Lock锁

1.为什么需要线程同步?

 一个对象是否需要是线程安全的,取决于它是否被多个线程访问。

要使得对象是线程安全的,需要采用同步机制来协同对对象的可变状态的访问。如果无法实现协同,那么可能会导致数据破坏以及其他不该出现的结果。

影响线程不安全因素:

1、抢占式执行
java中线程调度采用抢占式调度方法。许多线程可能是可运行状态但只能有一个线程在运行该线程将持续运行直到它自行终止或者是由于其他的事件导致阻塞亦或者是出现高优先级线程成为可运行的则该线程失去CPU的占用权。而非其他编程语言采用有轮回式的方式。

2、多个线程修改同一个变量

3、非原子性操作
对于比如 count++ 操作,它并不是一个原子性操作,它的操作是分为三步的:1. 查询 count 当前的值【load】 2.进行 count+1 操作【++】 3. 刷新 count 的最新值【save】。
当在两个线程中同时执行时,

想要得到的结果是0但是最终的结果却不是我们想要的也不是我们可以控制的。

4、内存可见性问题
可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.

线程之间的共享变量存在主内存 (Main Memory).

每一个线程都有自己的 “工作内存” (Working Memory) .

当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据. 当线程要修改一个共享变量的时候,
也会先修改工作内存中的副本, 再同步回主内存。

如下场景:
① 初始情况下, 两个线程的工作内存内容一致。
② 一旦线程1 修改了 a 的值, 此时主内存不一定能及时同步. 对应的线程2 的 工作内存的 a 的值也不一定能及时同步。

这就是内存的不可见性~

5、指令重排序
指令重排序是编译器优化代码的一种操作,我们大部分写的代码中,彼此的顺序(不会影响程序逻辑),谁在前谁在后无所谓~编译器会通过调整代码的前后顺序从而提高程序效率。 那么如果是一个多线程的程序,编译器在优化代码时出现了误判,优化了不应该更改的顺序。就也会导致线程安全问题。
 

2.synchronized同步处理

2.1 synchronized处理同步问题

分类 具体分类 被锁的对象 伪代码
方法 实例方法 类的实例对象

//实例方法

public synchronized void method(){

        ...

}

静态方法 类对象

//静态方法,锁住的是类对象

public static synchronized void method(){

        ...

}

代码块 实例对象 类的实例对象

//同步代码块,锁住的是该类的实例对象

synchronized(this){

        ...

}

class对象 类对象

//同步代码块,锁住的是该类的实例对象

synchronized(XXX.class){

        ...

}

2.2 synchronized 特性

① 互斥(排他性)
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也 执行到同一个对象 synchronized 就会阻塞等待.

② 刷新内存(内存不可见问题)
synchronized 的工作过程:

获得互斥锁
从主内存拷⻉变量的最新副本到⼯作的内存
执行代码
将更改后的共享变量的值刷新到主内存
释放互斥锁

③ 可重入
synchronized 同步块对同一线程来说是可重入的,不会出现自己把自己锁死的问题。

/**
 * synchronized 可重入性测试
 */
public class ThreadSynchronized {
    public static void main(String[] args) {
        synchronized (ThreadSynchronized.class) {
            System.out.println("当前主线程已经得到了锁");
            synchronized (ThreadSynchronized.class) {  //可以进入第二层
                System.out.println("当前主线程再次得到了锁");
            }
        }
    }
}

会打印2次:当前主线程再次得到了锁

2.3 synchronized 是如何实现的?

synchronized 同步锁是通过 JVM 内置的 Monitor 监视器实现的,而监视器又是依赖操作系统的互斥锁 Mutex 实现的。JVM 监视器的执行流程是:线程先通过自旋 CAS 的方式尝试获取锁,如果获取失败就 进入 EntrySet 集合,如果获取成功就拥有该锁。当调用 wait() 方法时,线程释放锁并进入 WaitSet 集合,等其他线程调用 notify 或 notifyAll 方法时再尝试获取锁。锁使用完之后就会通知 EntrySet 集合中的线程,让它们尝试获取锁。

- synchronized 执行流程
在 Java 中,synchronized 是非公平锁,也是可以重入锁。

- 所谓的非公平锁是指,线程获取锁的顺序不是按照访问的顺序先来先到的,而是由线程自己竞争,随机获取到锁。
- 可重入锁指的是,一个线程获取到锁之后,可以重复得到该锁。也就是多层的获取进入这个锁


3、Lock

3.1 Lock 用法及注意事项

Lock 是一个接口,一般使用 ReentrantLock 类作为锁。在加锁和解锁处需要通过 lock() 和 unlock() 显示指出。所以一般会在 finally 块中写 unlock() 以防死锁。

public class MyReentrantLock  implements  Runnable
{
    private Lock  numLock=new ReentrantLock();
    @Override
    public void run() {
       for (int i=1;i<10;i++)
       {
           //加锁
           numLock.lock();
           System.out.println(Thread.currentThread().getName()+":还有"+i+"人");
           //释放锁
           numLock.unlock();
       }
    }
}
public class demo {
    public static void main(String[] args) {
        MyReenrantLock  myTread=new MyReenrantLock();
        Thread T1= new   Thread(myTread,"线程1");
        Thread T2= new   Thread(myTread,"线程2");
        Thread T3=new   Thread(myTread,"线程3");
        T1.start();
        T2.start();
        T3.start();
    }
 
}
    lock.lock();
    try {
            
    }finally {
            lock.unlock();
    }

注意1 :把 unlock() 放在 finally 块中以防死锁
注意2 :把 lock() 放在 try 外或者try的首行,因为1. try 代码中的异常导致加锁失败,还会执行 finally 释放锁操作。2. 释放锁的错误信息会覆盖业务代码报错信息,从而增加调试程序和修复程序的复杂度。

Lock 公平锁和非公平锁

构造方法 :

ReentrantLock() : 创建一个 ReentrantLock的实例。

ReentrantLock(boolean fair) : 根据给定的公平政策创建一个 ReentrantLock的实例。

 默认创建一个非公平锁(线程竞争,非公平锁性能更高),fair为true时创建公平锁。

4、synchronized 和 Lock 的区别

类别     synchronized     Lock
存在层次     Java的关键字,在jvm层面上     是一个类
锁的释放     1、以获取锁的线程执行完同步代码,释放锁; 2、线程执行发生异常,jvm会让线程释放锁     在finally中必须释放锁,不然容易造成线程死锁
锁的获取    假如A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待     分情况而定,Lock有多个锁获取的方式,可以尝试继续获取锁,线程不用一直等待
锁的状态     无法判断     可以判断
锁类型     可重入、不可中断、非公平     可重入、可判断、可公平
性能     少量同步    大量同步

区别:

  • Lock 是一个接口,而 synchronized 是 java 的一个关键字,synchronized 是内置语言实现;异常是否释放锁
  • synchronized 在发生异常时会自动释放占有的锁,因此不会出现死锁;而 Lock 发生异常的时候,不会主动释放占有的锁,必须手动 unlock() 来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
  • Lock 等待锁过程中可以用 interrupt 来中断等待,而 synchronized 只能等待锁的释放,不能响应中断;
  • Lock 可以通过 trylock() 来知道有没有获取锁,而synchronized 不能;
  • synchronized 使用 Object 对象本身的 wait、notify、notifyAll 调度机制,而 Lock 可以使用 Condition 进行线程之间的调度。
  • synchronized 是托管给 JVM 执行的,而 Lock 是 java 写的控制锁的代码。

//Condition定义了等待/通知两种类型的方法
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();...condition.await();...condition.signal();
condition.signalAll();

  • synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。 独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
  • 而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。 乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令

synchronized 原语和 ReentrantLock 在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面2种需求的时候。

  1. 某个线程在等待一个锁的控制权的这段时间需要中断
  2. 需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程

猜你喜欢

转载自blog.csdn.net/u013773608/article/details/129624163