同步的时候非同步也可以同时执行.
一、内置锁Synchronized:
1、概念
- synchronized同步的不是一个代码块,而是一个对象。static的话是class对象。
- 一个加锁的原则是对不变性加锁:不变性条件中涉及的所有变量都需要由同一个锁来保护。
- 居有三大特性:有序性,原子性,可见性
- 不要使用双重锁定,因为JAVA内存模型允许所谓的“无序写入”。
- 线程对内部锁的申请与释放的动作由JAVA虚拟机负责代为实施。
- 这是一种非公平的内部所调度策略。
内置锁:synchronized(锁句柄){ //do.. } 称为同步代码块,进入代码块之前自动获得锁,退出的时候自动解开锁 (Java的内置锁相同于一种互斥体,最多只有一个线程能持有这种锁, 即当A线程执行这块代码时,B线程等待而不是同时执行,避免了竞态)
2、深入理解底层
临界区:同步代码块称临界区。即锁的持有线程在其获得锁之后和释放锁之前的这段时间内所执行的代码被称为临界区。
ynchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象
过程如下:
1如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。
3如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
深入理解:JAVA synchronized实现原理以及其中锁优化的归纳总结
3、sychronzied的使用
注意:
一:
锁句柄是一个对象的引用(或者能够返回对象表达式),可以填写this,那么对象为class。
作为锁句柄的变量常用final修饰,因为锁句柄的值一旦被修改,会导致执行一个同步快的多个线程实际上使用不同的锁,导致竞态。所以会用private来修饰。
private final Object lock = new Object();
二:
内部锁并不会导致锁泄漏,因为临界区抛出异常的话,那么锁会自动释放。
三:
Sychronized影响活跃性(Liveness)和性能(Performance):
为什么不在每个方法声明中加synchronized呢?考虑性能和活跃性。
- 滥用synchronized,导致程序中出现过多的同步
- 如果只是将每个方法作为同步方法,不足已确保复合操作都是原子的
- 还可能导致活跃性问题或性能问题
不把整个代码都放入同步块中,而是里面分解,优点如下:
- 尽量降低同步块的大小,提高并发性能。
- 将和状态变量无关的操作,耗时的操作从同步块中提取出来,提供并发性能。
- 将读和写进行分开处理(分阶段,这样子便于提取阶段的同步块),读和写的时候都保持不变性
- 使用不可变的特性保持离开同步块外的不变性。
- 同一个不变性由一个锁进行控制。
- 通常,在简单性与性能之间存在着互相制约因素。当实现某个同步策略时,一定不要盲目为了性能牺牲简单性,这可能破坏安全性。
- 当执行时间较长的计算或者无法快速完成的操作时(例如,网络I/O或控制台I/O),一定不要持有锁。
// 1这个Servlet能正确缓存最新的计算结果,但并发性却非常糟糕(不要这么做) @ThreadSafe public class SynchronizedFactorizer extends GenericServlet implements Servlet { @GuardedBy("this") private BigInteger lastNumber; @GuardedBy("this") private BigInteger[] lastFactors; public synchronized void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); if (i.equals(lastNumber)) encodeIntoResponse(resp, lastFactors); else { BigInteger[] factors = factor(i); lastNumber = i; lastFactors = factors; encodeIntoResponse(resp, factors); } } }
@ThreadSafe //推荐这样的写法 public class UnsafeCachingFactorizer implements Servlet { @GuardedBy("this") private BigInteger lastNumber; @GuardedBy("this") private BigInteger[] lastFactors; public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extraceFromRequest(req); BigInteger[] factors = null; // 将读和写分离,先用一个同步块进行读 synchronized (this) { if (i.equals(lastNumber.get()) // 注意访问的是clone,如果直接引用LastFactors,可能导致最后返回客户端的是别的线程修改后的数值,违背了不变性。 factors = lastFactors.clone(); } if (factors == null) { // 将计算时间长的代码提取出来,不要放到同步块中 factors = factor(i); // 将写使用同步块保护,注意也是clone保持不变性。 synchronized (this) { lastNumber = i; lastFactors = factors.clone(); } } encodeIntoResponse(resp,factors); } }
二、显示锁LOCK:
1、概念
作为一种线程同步机制,作用和内部所相同,但不是其代替品,不同的是提供了一些内部锁所不具备的特性:
- 尝试非阻塞获取锁:当前线程尝试获取锁,如果这一时刻,锁没有被其他线程占有,那么成功获取锁并返回。
- 能被中断地获取锁:当线程正在等待获取锁,则这个线程能够 响应中断,即当中断来了,线程不会阻塞等待获取锁,抛出中断异常。
- 超时获取锁:在指定的截止时间前获取锁,如果截止时间到了仍旧无法获取锁,则返回;
Case 1 :
在使用synchronized关键字的情形下,假如占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待,别无他法。这会极大影响程序执行效率。因此,就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间 (解决方案:tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案:lockInterruptibly())),这种情况可以通过 Lock 解决。
Case 2 :
当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突。同样地,Lock也可以解决这种情况 (解决方案:ReentrantReadWriteLock) 。
Case 3 :
我们可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock) ,但这个是synchronized无法办到的。
2、操作
显示锁(Explicit Lock)是java.util.concurrent.lcoks.Lock接口的实例。加锁和解锁都要用户显式地控制。
- 显示锁默认为非公平调度策略,可以修改成公平策略调度。
- ReentrantLock 支持公平锁,可以在构造方法中传入参数设置,默认为非公平锁。
//显式锁的加锁和解锁都是由用户来操作,所以用户一旦忘记释放锁了,很可能就会造成线程讥饿。正确的用是使用 try-finally 确保锁能被正确释放 public void m() { lock.lock(); // block until condition holds try { // ... method body } finally { lock.unlock(); //放在finally里面避免了锁泄漏 } } //lock.lock和lock.unlock之间是临界区
//中断和可中断锁 static Lock lock =new ReentrantLock();//静态变量 public static void main(String[] args) { Thread A = new Thread("A"){ @Override public void run() { //不可中断锁,在等待获取锁的过程,忽略中断 lock.lock(); try { System.out.println("线程"+getName()+"成功获取锁"); } finally { lock.unlock(); } } }; Thread B = new Thread("B"){ @Override public void run() { try { //Lock提供可中断方式获取锁 lockInterruptibly,则要 try-finally 要处于 捕获中断异常的 try-catch 块间,或者在方法上抛出中断异常 //可中断锁,在等待获取锁的过程中,如果有中断到来,将会停止获取锁,并抛出中断异常 lock.lockInterruptibly(); try{// System.out.println("线程"+getName()+"成功获取锁"); }finally{ lock.unlock(); } } catch (InterruptedException e) { e.printStackTrace(); } } }; //mian线程保持着锁时,再启动A、B线程,确保中断A、B线程时,A、B线程在等待获取锁 lock.lock(); try{ A.start(); B.start(); System.out.println("中断A、B线程"); A.interrupt(); B.interrupt(); }finally{ lock.unlock(); } } //输出 //中断A、B线程 //线程A成功获取锁
//非阻塞获取锁 tryLock() public static void main(String[] args) throws InterruptedException { Lock lock = new ReentrantLock(); Thread A = new Thread("A"){ @Override public void run() { if(lock.tryLock()){//尝试非阻塞获取锁 try{ System.out.println(getName()+"成功获取锁"); }finally {//释放锁 lock.unlock(); } }else{ System.out.println(getName()+"获取锁失败!"); } } }; if(lock.tryLock()){//main线程成功获取锁后,启动线程A try{ A.start(); System.out.println(Thread.currentThread().getName()+"启动线程A"); //sleep可以保持锁,模拟main线程还要运行1秒 TimeUnit.SECONDS.sleep(1); }finally { lock.unlock(); } }else{ System.out.println("程序结束!"); } //main启动线程A //A线程获取锁失败
//与tryLock( )相比,除了不是立刻返回,而是超时等待外,tryLock(long time, TimeUnit unit)还是可以被中断的。 public static void main(String[] args) throws InterruptedException { Lock lock = new ReentrantLock(); Thread A = new Thread("A"){ @Override public void run() { try { if(lock.tryLock(1,TimeUnit.SECONDS)){//超时等待获取锁 try{ System.out.println(getName()+"成功获取锁"); }finally { lock.unlock(); } }else{ System.out.println(getName()+"获取锁失败!"); } } catch (InterruptedException e) { e.printStackTrace(); } } }; if(lock.tryLock()){//main线程成功获取锁后,启动线程A try{ A.start(); System.out.println(Thread.currentThread().getName()+"启动线程A"); //sleep可以保持锁,模拟main线程还要运行1秒 TimeUnit.SECONDS.sleep(1); }finally { lock.unlock(); } }else{ System.out.println("程序结束!"); } } //main启动线程A //A成功获取锁
//可重入的锁:是指线程持有了某个锁,便可以进入任意的该锁同步着的代码块。 //使用可重入锁,可以很大程度地避免死锁,所以不可重入锁的应用场景很少, //JDK提供的锁(synchronize、ReentrentLock、ReentrantReadWriteLock)都是可重入的锁。当线程获取重入锁时,先判断线程是不是已经持有该锁, //如果是,那么重入计数器加一,否则去获取该锁。ReentrentLock中,提供了锁被线程重入的次数的方法 - - getHoldCount()。 static ReentrantLock lock = new ReentrantLock();//静态变量 static int num = 5; public static void main(String[] args) Thread B = new Thread("B"){ @Override public void run() { lock.lock(); try{ int aa = 5*5; //countNumber里面也要获取同步锁,而且与当前线程所拥有的锁是同一个 countNumber(aa); //可重入,意味着不需要再次去等待获取锁 System.out.println("num的值是:"+num); }finally { lock.unlock(); } } }; B.start(); } public static void countNumber(int a){ //包含同步代码块 lock.lock(); //如果是重入,则重入计数器加一 try{ num+=a; System.out.println("锁lock被当前线程重入的次数:"+lock.getHoldCount()); }finally { lock.unlock();//如果是重入,则重入计数器减一 } } //重入次数2 //num的值为30
读写锁(Read/Write Lock):允许多个线程可以同时读取共享变量,但是一次只允许一个线程对共享变量进行更新 适用场景: 只读操作比写操作要频繁的多 读线程持有锁的时间比较长
重点介绍以下两个方法: 1、boolean isHeldByCurrentThread( ): 查询当前线程是否保持此锁。 与内置监视器锁的 Thread.holdsLock(java.lang.Object) 方法类似,此方法通常用于调试和测试。例如,只在保持某个锁时才应调用的方法可以声明如下: ------------------------ class X { ReentrantLock lock = new ReentrantLock(); // ... public void m() { //在保持某个锁的条件下才进入, assert lock.isHeldByCurrentThread(); // ... method body } } ------------------------ //还可以用此方法来确保某个重入锁是否以非重入方式使用的,例如: ------------------------ class X { ReentrantLock lock = new ReentrantLock(); // ... public void m() { assert !lock.isHeldByCurrentThread(); lock.lock(); try { // ... method body } finally { lock.unlock(); } } } ------------------------ 2、public int getHoldCount( ):查询当前线程保持此锁的次数。 对于与解除锁操作不匹配的每个锁操作,线程都会保持一个锁。 保持计数信息通常只用于测试和调试。例如,如果不应该使用已经保持的锁进入代码的某一部分,则可以声明如下: class X { ReentrantLock lock = new ReentrantLock(); // ... public void m() { assert lock.getHoldCount() == 0; lock.lock(); try { // ... method body } finally { lock.unlock(); } } } ------------------------
3、Lock和synchronized的区别
显示锁和内部锁的比较:
- 内部锁简单,但是不灵活
- 显示锁支持在一个方法内申请锁,却在另一个方法里释放锁
- 显示锁定义了一个tryLock()方法,尝试去获取锁,成功返回true,失败并不会导致其执行的线程被暂停而是直接返回false,即可以避免死锁
Lock 实现提供了比 synchronized 关键字 更灵活、更广泛、粒度更细 的锁操作,它能以更优雅的方式处理线程同步问题。也就是说,Lock提供了比synchronized更多的功能。但是要注意以下几点:
- synchronized是Java的关键字,因此是Java的内置特性,是基于JVM层面实现的,其经过编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit 两个字节码指令;而Lock是一个Java接口,是基于JDK层面实现的,通过这个接口可以实现同步访问;
- 采用synchronized方式不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock则必须要用户去手动释放锁 (发生异常时,不会自动释放锁),如果没有主动释放锁,就有可能导致死锁现象。
(总的来说sychronized是自动加锁解锁,而且是内置特性,而Lock是需要手动,他只是提供一个接口而已)
最后注意一下:
synchronized是内置在JVM中的,所以它在以后的获得性能提升将会更加直接。所以在没有使用到Lock的高级功能,尽可能地使用synchronized。
三、关键字volatile
1、具体作用
volatile这是一种稍微弱一点的同步机制,主要就是用于将变量的更新操作通知到其它线程:
- 用来读取变量的相对新值
volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此读取volatile类型的变量时总会返回最新写入的值。
volatile用以声明变量的值可能随时会别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。
- volatile被称为轻量级锁:volatile具有可见性、有序性。
可见性:在读取volatile类型的变量时总会返回最新写入的数据。即当AB线程都使用到了一个变量,Java默认是A线程中保留一个copy,如果B线程修改了变量,A未必知道,使用volatile就是当修编变量的时候,让所有线程都返回重新读到变量修改的值copy到自己这。
- 仅仅能保证volatile变量写操作的原子性,而没有对变量进行赋值的原子性。而且没有锁的排他性。
volatile int i = 0; i++的原子性不保证。
- 但是它不会导致上下文切换
- 作用volatile关键字可以保证对long/double型变量的写操作具有原子性
非volatile类型的64位数值变量。Java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。
- volatile会禁止指令重排。
把变量声明为volatile类型后,编译器和运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。
2、适用场合:
仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。
当且仅当满足以下所有条件时,才应该使用volatile变量:
1对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
2该变量不会与其他状态变量一起纳入不变性条件中。
3在访问变量时不需要加锁。
下面列举几个Java中使用volatile的几个场景。
1.状态标记量
2.double check 双重检查
3.保障可见性
4.代替锁
- 利用该变量对写操作的原子性
- volatile适合多个线程共享一个状态变量,锁适合多个线程共享一组状态变量
- 我们可以将多个线程共享的一组状态变量合并成一个对象,用于一个volatile变量来引用该对象,从而替代锁
5.实现简易版读写锁
3、内存屏障与开销
volatile变量写操作与内存屏障:
- 普通变量的读写操作
- 释放屏障
- 写操作
- 存储屏障
volatile变量读操作与内存屏障:
- 加载屏障
- 读操作
- 获取屏障
- 普通变量的读写操作
volatile变量的开销:
- 不会导致上下文切换,开销比锁小
- 读取变量的成本比临界区中读取变量要低,但是其读取成本可能比读取普通变量要高;因为每次读取都从高速缓存或者主内存中读取,无法被暂存在寄存器中,从而无法发挥访问的高效性
//标识 volatile boolean flag = false; while(!flag){ doSomething(); } public void setFlag() { flag = true; } ------------------------------- volatile boolean inited = false; //线程1: context = loadContext(); inited = true; //线程2: while(!inited ){ sleep() } doSomethingwithconfig(context);
//双重检查 单例模式 class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; } }
a失效数据:
在没有同步的情况下,线程去读取变量时,可能会得到一个已经失效的值。失效值可能不会同时出现:一个线程可能获得某个变量的最新值,而获得另一个变量的失效值。
b最低安全性:
线程没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性(out-of-thin-airsafety)。
最低安全性适用于绝大多数变量,当时存在一个例外:非volatile类型的64位数值变量。Java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。
c加锁和可见性:
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步
4、volatile关键字和synchronized关键字的区别
(1)、volatile只能作用于变量,使用范围较小。synchronized可以用在变量、方法、类、同步代码块等,使用范围比较广。
(2)、volatile只能保证可见性和有序性,不能保证原子性。而可见性、有序性、原子性synchronized都可以包证。
(3)、volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。
(4)、volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化.
四、CSA和原子操作
1、CSA:
i++其实是一个read-modify-write操作,通过CAS可以转变为if-then-act操作,如下用判断
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。1ABA问题,2循环时间长开销大和3只能保证一个共享变量的原子操作。
ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A
2、原子操作:
原子变量最主要的一个特点就是所有的操作都是原子的,synchronized关键字也可以做到对变量的原子操作。只是synchronized的成本相对较高,需要获取锁对象,释放锁对象,如果不能获取到锁,还需要阻塞在阻塞队列上进行等待。而如果单单只是为了解决对变量的原子操作,建议使用原子变量。
- Atomic包中都是基于CAS实现
- java自带的原子操作 AtomicXXX,但是多个AtomicXX不具备原子性。
Java给我们提供了以下几种原子类型:
- AtomicInteger和AtomicIntegerArray:基于Integer类型
- AtomicBoolean:基于Boolean类型
- AtomicLong和AtomicLongArray:基于Long类型
- AtomicReference和AtomicReferenceArray:基于引用类型
public class AtomicIntegerTest { private static final int THREADS_CONUT = 20; public static AtomicInteger count = new AtomicInteger(0);//运用原子类实现原子操作,得到结果为20000 public static void increase() { count.incrementAndGet(); } public static void main(String[] args) { Thread[] threads = new Thread[THREADS_CONUT]; for (int i = 0; i < THREADS_CONUT; i++) { threads[i] = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { increase(); } } }); threads[i].start(); } while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println(count); } }