多线程与锁
乐观锁
认为读多写少,遇到并发的可能性低,拿数据的时候认为别人不会修改,所以不上锁,但是更新数据时,会判断一下有没有其他线程更新了数据,采取写时先读出当前版本号,比较跟上一次的版本号,如果一样则更新), 如果失败则要重复读-比较-写的操作。 java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入 值是否一样,一样则更新,否则失败。
悲观锁
即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。
自旋锁
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁 的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋), 等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
关于自旋的周期选择:jdk1.5 这个限度是一定的写死的,在 1.6 引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥 有者的状态来决定。
Synchronized 同步
Synchronized作用范围:
- 作用于方法时,锁住的是对象的实例(this);
- 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen (jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁, 会锁所有调用该方法的线程;
-
synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列, 当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
image.png
Synchronized核心组件
- Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;
- Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
- Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
- OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
- Owner:当前已经获取到所资源的线程被称为Owner;
- !Owner:当前释放锁的线程。
Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先 尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是 不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。
每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加 上monitorenter和monitorexit指令来实现的,方法加锁是通过一个标记位来判断的
synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线 程加锁消耗的时间比有用操作消耗的时间更多。
ReentrantLock
ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完 成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等 避免多线程死锁的方法。
非公平锁
JVM 按随机、就近原则分配锁的机制则称为不公平锁,ReentrantLock 在构造函数中提供了 是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率要远远超出公平锁,除非 程序有特殊需要,否则最常用非公平锁的分配机制。
公平锁
公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁, ReentrantLock在构造函数中提供了是否公平锁的初始化方式来定义公平锁。
ReentrantLock 与 synchronized
- ReentrantLock 通过方法 lock()与unlock()来进行加锁与解锁操作,与synchronized 会 被 JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出 现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。
- ReentrantLock相比synchronized的优势是可中断、公平锁、多个锁。这种情况下需要 使用ReentrantLock。
Condition类和 Object类锁方法区别
- Condition类的awiat方法和Object类的 wait方法等效
- Condition类的signal方法和Object类的 notify方法等效
- Condition类的signalAll方法和Object类的notifyAll方法等效
- ReentrantLock类可以唤醒指定条件的线程,而object的唤醒是随机的
Semaphore 信号量
Semaphore是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信 号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore 可以用来 构建一些对象池,资源池之类的,比如数据库连接池
Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也与之类似,通过 acquire()与 release()方法来获得和释放临界资源。
Semaphore的锁释放操作也由手动进行,因此与ReentrantLock一样,为避免线程因抛出异常而 无法正常释放锁的情况发生,释放锁的操作也必须在finally代码块中完成。
AtomicInteger
首先说明,此处 AtomicInteger,一个提供原子操作的 Integer 的类,常见的还有 AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等,他们的实现原理相同, 区别在与运算对象类型的不同。令人兴奋地,还可以通过 AtomicReference<V>将一个对象的所 有操作转化成原子操作。 我们知道,在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一。 通常我们会使用 synchronized 将该操作变成一个原子操作。
可重入锁
也叫 做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受 影响。在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁。
ReadWriteLock 读写锁
为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制。如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写 锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可。
轻量级锁 锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。
锁升级
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的, 也就是说只能从低到高升级,不会出现锁的降级)。
“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是, 轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量 级锁使用产生的性能消耗。
偏向锁
偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起 来让这个线程得到了偏护。。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级 锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所 以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
锁优化
1,减少锁持有时间
只用在有线程安全要求的程序上加锁
2,减小锁粒度
将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。 降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是 ConcurrentHashMap。
3,锁分离
最常见的锁分离就是读写锁ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互 斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能,具体也请查看[高并发Java 五] JDK并发包1。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如 LinkedBlockingQueue 从头部取出,从尾部放数据
4,锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完 公共资源后,应该立即释放锁。但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
5,锁消除
锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这 些对象的锁操作,多数是因为程序员编码不规范引起。
线程基本方法
4.1.10.1. 线程等待(wait) 调用该方法的线程进入WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的 是调用wait()方法后,会释放对象的锁。因此,wait方法一般用在同步方法或同步代码块中。
4.1.10.2. 线程睡眠(sleep) sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致 线程进入TIMED-WATING状态,而wait()方法会导致当前线程进入WATING状态
4.1.10.3. 线程让步(yield) yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下, 优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对 线程优先级并不敏感。
4.1.10.4. 线程中断(interrupt) 中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这 个线程本身并不会因此而改变状态(如阻塞,终止等)。
- 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线 程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
- 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出 InterruptedException,从而使线程提前结束TIMED-WATING状态。
- 许多声明抛出InterruptedException的方法(如Thread.sleep(long mills方法)),抛出异 常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。 4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止 一个线程thread的时候,可以调用thread.interrupt()方法,在线程的run方法内部可以 根据thread.isInterrupted()的值来优雅的终止线程。
4.1.10.5. Join 等待其他线程终止 join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞 状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
4.1.10.6. 为什么要用 join()方法? 很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要 在子线程结束后再结束,这时候就要用到 join() 方法。 System.out.println(Thread.currentThread().getName() + "线程运行开始!"); Thread6 thread1 = new Thread6(); thread1.setName("线程B"); thread1.join(); System.out.println("这时 thread1执行完毕之后才能执行主线程");
4.1.10.7. 线程唤醒(notify) Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象 上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调 用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继 续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞 争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
4.1.10.8. 其他方法:
- sleep():强迫一个线程睡眠N毫秒。
- isAlive(): 判断一个线程是否存活。
- join(): 等待线程终止。
- activeCount(): 程序中活跃的线程数。
- enumerate(): 枚举程序中的线程。
- currentThread(): 得到当前线程。
- isDaemon(): 一个线程是否为守护线程。
- setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线 程依赖于主线程结束而结束)
- setName(): 为线程设置一个名称。
- wait(): 强迫一个线程等待。
- notify(): 通知一个线程继续运行。
- setPriority(): 设置一个线程的优先级。
- getPriority()::获得一个线程的优先级。
线程上下文切换
巧妙地利用了时间片轮转的方式, CPU 给每个任务都服务一定的时间,然后把当前任务的状态保存 下来,在加载下一任务的状态后,继续服务下一任务,任务的状态保存及再加载, 这段过程就叫做 上下文切换。
上下文 是指某一时间点 CPU 寄存器和程序计数器的内容。
4.1.11.3. 寄存器
是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内 存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速 度。
4.1.11.4. 程序计数器
是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令 的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。
4.1.11.5. PCB-“切换桢”
上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行切换,上下 文切换过程中的信息是保存在进程控制块(PCB, process control block)中的。PCB还经常被称 作“切换桢”(switchframe)。信息会一直保存到CPU的内存中,直到他们被再次使用。
4.1.11.6. 上下文切换的活动:
- 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处。
- 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。
- 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序 中。
4.1.11.7. 引起线程上下文切换的原因
- 当前执行任务的时间片用完之后,系统CPU正常调度下一个任务;
- 当前执行任务碰到IO阻塞,调度器将此任务挂起,继续下一任务;
- 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
- 用户代码挂起当前任务,让出CPU时间;
- 硬件中断;
ThreadLocal 作用(线程本地存储)
ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal 的作用 是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或 者组件之间一些公共变量的传递的复杂度。
每个线程中都有一个自己的 ThreadLocalMap 类对象,可以将线程自己的对象保持到其中, 各管各的,线程可以正确的访问到自己的对象。 ThreadLocalMap存储的是公用的静态ThreadLocal实例,对应的是其对象的引用,使用的时候通过个静态ThreadLocal实例的get()方法取 得自己线程保存的那个对象
synchronized 和 ReentrantLock 的区别
4.1.19.1. 两者的共同点:
- 都是用来协调多线程对共享对象、变量的访问
- 都是可重入锁,同一线程可以多次获得同一个锁
- 都保证了可见性和互斥性
4.1.19.2. 两者的不同点:
- ReentrantLock显示的获得、释放锁,synchronized隐式获得释放锁
- ReentrantLock 可响应中断、可轮回,synchronized 是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性
- ReentrantLock是API级别的,synchronized是JVM级别的
- ReentrantLock可以实现公平锁
- ReentrantLock通过 Condition可以绑定多个条件
- 底层实现不一样, synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略
- Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言
实现。 - synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生; 而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象, 因此使用Lock时需要在finally块中释放锁。
- Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时, 等待的线程会一直等待下去,不能够响应中断。
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- Lock可以提高多个线程进行读操作的效率,既就是实现读写锁等。