1.synchronized是关键字,Lock是接口;
2.synchronized是隐式的加锁,lock是显式的加锁;synchronized在退出代码块时自动解锁,lock却必须调用unlock方法进行手动释放
3.synchronized可以作用于方法上,lock只能作用于方法块;
4.synchronized底层采用的是objectMonitor,lock采用的AQS;
5.synchronized是阻塞式加锁,lock是非阻塞式加锁支持可中断式加锁,支持超时时间的加锁;
6.synchronized在进行加锁解锁时,只有一个同步队列和一个等待队列, lock有一个同步队列,可以有多个等待队列;
偏向锁->轻量级锁->重量级锁
一、为什么需要线程同步
1.如果你正在写一个变量,它可能接下来被另外一个线程所读取
2.你所在的线程,正在读取上一次已经被另外一个线程所写过的变量。
这两种情况需要同步,并且,读写线程都必须用相同的监视器锁同步。
二、Spring线程同步问题
一般我们的web容器(比如tomcat)都是多线程的,而Spring容器是单例的,Controller如果不修改默认作用域singleton(基本作用域:singleton单例、prototype多例;Web 作用域(eqeust、session、globalsession),在Controller中定义的局部变量就可能被多线程修改,所以有同步问题。
三、java线程同步方式
方法一:使用synchronized关键字
1.由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
2.synchronized关键字可以修饰同步块,普通函数,静态函数,此时如果调用该静态方法,将会锁住整个类
3.同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可
synchronized锁升级:
1)当只有一个线程去争抢锁的时候,会先使用偏向锁,就是给一个标识,说明现在这个锁被线程1占有。
2)后来又来了线程2,线程3,需要公平的竞争,于是将标识去掉,也就是撤销偏向锁,升级为轻量级锁,三个线程通过CAS自旋进行锁的争抢
3)后面线程越来越多,CAS自旋会消耗大量CPU资源,于是将锁升级为重量级锁,向内核申请资源,直接阻塞等待中的线程。
偏向锁:
“偏向锁”是Java在1.6引入的一种优化机制,其核心思想在于,可以让同一个线程一直拥有同一个锁(用一个CAS操作和简单地判断比较,就可以让一个线程持续地拥有一个锁),直到出现其他线程竞争锁,才去释放锁。
自旋锁(轻量级锁):
一个或多个线程通过CAS去争抢锁,如果抢不到则一直自旋。前提是线程在临界区的操作非常快,所以它会非常快速地释放锁,所以只要让另外一个线程在那里地循环等待,然后当锁被释放时,它马上就能够获得锁,然后进入临界区执行,然后马上又释放锁,让给另外一个线程。
互斥锁(重量级锁):
synchronized因为是依赖于每个对象内部都有的monitor锁来实现的,而monitor又依赖于操作系统的MutexLock(互斥锁)来实现,由于需要在操作系统的内核态和用户态之间切换的,需要将线程阻塞挂起,切换线程的上下文,再恢复等操作,所以当synchronized升级成互斥锁,依赖monitor的时候,开销就比较大了
CAS:
没有获取到锁的线程不会阻塞,通过循环控制一直不断的获取锁。CAS在java中的原子操作类中会用到,比如java.util.concurrent.atomic.AtomicInteger,它是
Compare and Swap的缩写,翻译过来就是比较并替换,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
Java通过Unsafe提供的CAS方法调用本地cpu的cmpxchg指令,如果预期值和新值匹配,那处理器会自动将该位置值更新为新值,不匹配处理器将自旋重试,
执行cmpxchg指令时,先会判断系统是否为多核系统,如果是,就会给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行CAS操作,
也就是说,CAS原子性实际上是CPU实现独占,相比synchronized重量级锁,等待时间要短很多,所以在多线程情况下性能会比较好。
方法二:wait和notify
wait():使一个线程处于等待状态,并且释放所持有的对象的锁。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
wait和synchronized区别:
lock和synchronized最大的区别在于synchronized是退出作用域后自动释放锁;而lock则需要手动调用notify释放放,自由度更加高了,满足于更加复杂的业务逻辑。
方法三:使用特殊域变量volatile实现线程同步
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是
立即可见的。
2)禁止进行指令重排序。
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
volatile仅能实现变量的修改可见性,并不能保证原子性,无法保证线程安全
volatile不会造成线程的阻塞;
synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
synchronized可能会造成线程的阻塞。
volatile使用场景:
Volatile是Java中的一个关键字,用于修饰变量,表示该变量是易变的,可能会被多个线程同时访问和修改。在多线程编程中,Volatile的使用场景非常广泛,下面将按照不同的类别来介绍。
1、状态标记
在多线程编程中,经常需要使用状态标记来控制线程的执行流程。例如,一个线程需要等待另一个线程完成某个操作后才能继续执行,这时可以使用一个状态标记来表示另一个线程是否已经完成了操作。这个状态标记可以使用Volatile来修饰,以保证多个线程之间的可见性。
2、双重检查锁定
双重检查锁定是一种常见的单例模式实现方式,它可以保证在多线程环境下只有一个实例被创建。在双重检查锁定中,需要使用Volatile来修饰单例对象,以保证多个线程之间的可见性。
public class Singleton { private volatile static Instance instance; //设置为volatile public static Instance getInstance() { // 1 if (instance == null) { // 2:第一次检查 synchronized (Singleton.class) { // 3:加锁 if (instance == null) // 4:第二次检查 instance = new Instance(); // 5:问题的根源出在这里 } // 6 } // 7 return instance; // 8 } }
a.第一次判断install == null的目的是,为null才加锁,增大效率
b.sychronized(Sigleton.class)加锁保证只有一个线程进入
c.第二次判断install == null,因为可以保证其它线程不会重复new Instance()
d.把instance设置为volatile的目的是,防止JVM对指令顺序的重新排序。以免出现这种情况:
正常顺序:堆中分配内存空间、执行初始化,栈中的本地变量表分配一个指向该内存区域的引用,
重排序:多线程环境下,若线程A创建instance,首先分配了引用指针,此时线程B并发地去执行getInstance方法,那么会发现instance所指向的内存区域并不是null,那么线程B的getInstance方法则会返回这个instance,但实际上线程A仅仅是分配了这个指针,并没有在内存区域中完成初始化方法。
3、计数器
在多线程编程中,经常需要使用计数器来统计某个操作的执行次数。例如,一个线程需要等待多个线程都完成某个操作后才能继续执行,这时可以使用一个计数器来统计已经完成操作的线程数量。这个计数器可以使用Volatile来修饰,以保证多个线程之间的可见性。
4、轻量级同步
在多线程编程中,经常需要使用轻量级同步来保证线程安全。例如,一个线程需要对某个变量进行读写操作,但是这个变量的读写操作并不需要使用重量级的锁,这时可以使用Volatile来修饰这个变量,以保证多个线程之间的可见性。
四、使用重入锁ReentrantLock实现线程同步
ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它有两个方法:
lock() : 获得锁 (阻塞竞争锁)
tryLock():非阻塞竞争锁
unlock() : 释放锁
ReentrantLock和synchronized区别:
1.synchronized 可用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用在代码块上
2.ReentrantLock 需要手动调用 加锁 和 释放锁操作,synchronized 会自动加锁和释放锁
3.synchronized 属于非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁。
4.ReentrantLock通过调用lockInterruptibly可以响应中断指令,而 synchronized 不能响应中断。
可重入:就是获得锁的线程不用释放,可以重复的获取一个锁n次,只是在释放的时候,也需要相应的释放n次。
锁的竞争:ReentrantLock 是通过互斥变量,使用 CAS 机制来实现的,没有竞争到锁的线程,使用了 AbstractQueuedSynchronizer 这样一个队列同步器来存储,底层是通过双向链表来实现的。当锁被释放之后,会从 AQS 队列里面的头部唤醒下一个等待锁的线程。
锁的重入特性:在 AQS 里面有一个成员变量来保存当前获得锁的线程,当同一个线程下次再来竞争锁的时候,就不会去走锁竞争的逻辑,而是直接增加重入次数。
AQS:AbstractQueuedSynchronizer为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定。
五、ThreadLocal实现线程同步
用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
ThreadLocal的主要使用场景包括但不限于以下几点:
a.数据库连接和Session管理:这是ThreadLocal最广泛的应用之一,它可以帮助应用程序轻松地管理和维护数据库连接和会话状态,避免在多个线程之间共享这些资源时出现的并发问题。
b.替代参数的显式传递:在编写API接口时,可以通过将参数放入ThreadLocal中来简化方法的调用,减少参数传递的需要,使得代码更加简洁易读。
c.全局存储用户信息:特别是在前后端分离的场景下,用户信息的存储和管理变得尤为重要。通过使用ThreadLocal可以方便地在不同请求中共享用户信息。
六、使用原子变量实现线程同步
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作即-这几种行为要么同时完成,要么都不完成。在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器)