(1)技术背景
线程执行的任务,任务量比较小的时候,线程安全需要使用synchronized(多个线程同时竞争对象锁)加锁,效率比较低(竞争失败的线程很快的在阻塞态和被唤醒状态之间切换)
(2)使用前提
代码执行速度非常快
(3)目的
在安全的前提下优化效率—使用较多的场景:对变量修改保证线程安全
(4)原理
使用CAS,不造成线程阻塞(一直处于运行态)
全称Compare and swap,即比较并交换
当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号,可见CAS是一个乐观锁
实现源码
public final class Unsafe{
public final int getAndSetInt(Object var1,int var4){
int vars;
do{
var5 = this.getInVolatile(var1,var2);
}while(this.compareAndSwapInt(var1,var2,var5,var4));
return vars;
注意:
第一个线程修改成功:主存变量值=1,
第二个线程修改:失败,compareAndSwapXXX()不阻塞,直接返回false,再次执行do代码—>重新获取最新的变量值,再修改
基于原始值+修改值 尝试修改操作,判断是否等于预期值,不满足就是不修改并返回false,满足则直接修改
ABA问题
**产生的原因:**当前线程拷贝主存值到工作内存进行修改的时间段,其他线程把主存中变量值A—>B—>A相当于已经被其他线程修改过了,但是在这个期间我们不清楚这个过程,还在改…
解决方案:采取乐观锁的设计,引入版本号做控制
自旋尝试设置值的操作
设计上解决线程安全的一种思想
设计上解决线程安全的一种思想
(1)乐观锁
设计上总是乐观的认为数据修改大部分场景都是线程并发修改,少量情况下才存在,线程安全上采取版本号来控制—用户自己判断版本号,并处理
(2)悲观锁
悲观的认为总是有其他线程并发修改,每次都是加锁操作,
悲观锁存在的问题:总是需要竞争锁,进而导致发生线程切换,挂起其他线程,所以性能不高
乐观锁存在的问题:并不总是能处理所有问题,所以会引入一定的系统复杂度
实现原理:
while(抢锁(lock) == 失败){
}
缺点
技术背景:
数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥,如果两种场景下都用同一个锁,就会产生极大的性能消耗,读写锁因此而产生
读写锁:在执行加锁操作时需要额外表名读写意图,复数读者之间并不互斥,则写者则要求任何人互斥
允许同一个线程多次获取同一把锁,比如一个递归函数里有加锁操作,递归过程中这个锁不会阻塞自己,称为可重入锁(可重入锁也叫递归锁)
java中只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有线程的lock实现类,包括synchronized关键字锁都是可重入的
通过对象头加锁操作
monitor机制:编译为字节码时,生成monitorenter,monitorexit
(1)无锁
没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功
(2)偏向锁
偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放
偏向锁的撤销:
需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于锁定状态,如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁
如果线程处于活动状态,升级为轻量级锁的状态
(3)轻量级锁
当锁是偏向锁的时候,被第二份线程B访问,此时偏向锁就会升级为轻量级锁,线程B通过自旋的形式尝试获取锁,线程不会阻塞.从而提高性能
当前只有一个等待线程,则该线程将通过自旋进行等待,但是当自旋超过一定次数时,轻量级锁便会升级为重量级锁,当有一个线程已经持有锁,另一个线程在自旋,而此时又有第三个线程来访问时,轻量级锁会升级为重量级锁
(4)重量级锁
当有一个线程获取锁后,其余所有等待获取该锁的线程都会处于阻塞状态
注意点:java虚拟机需要确保锁在正常执行路径,以及异常执行路径上都能够被解锁
根据不同场景,使用不同的锁机制
注意:以下锁策略的级别由低到高!!!
(1)偏向锁:
(2)轻量级锁:
(3)重量级锁:
说明:synchronized锁只能升级不能降级(为了提高获得锁和释放锁的效率),
其他优化方案:
public class Test{
private static StringBuffer sb = new StringBuffer();
public static void main(String[] args){
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
}
}
public class Test{
public static void main(String[] args){
StringBuffer sb = new StringBuffer();
sb.append("a").append("b").append("c");
}
}
死锁是这样一种情况,多个线程同时被阻塞,他们中的一个或者前部都在等待某个资源被释放,由于线程被无限期的阻塞.因此程序不可能正常终止
同步的本质在于:一个线程等待另外一个线程执行完毕后才可以继续执行,但是如果现在相关的几个线程彼此之间都在等待着,那么就会造成死锁
说明:
至少有两个线程,互相持有对方申请的对象锁,造成相互等待,导致没法执行
四个必要条件
当上述四个条件都成立的时候,便形成死锁,打破任何一个条件,便可让死锁消失
线程阻塞等待,无法向下执行
一个银行家如何将一定数目的资金安全地借给若干个客户,使这些客户既能借到钱完成要干的事,同时银行家又能收回全部资金而不至于破产。
银行家算法需要确保以下四点:
1、当一个顾客对资金的最大需求量不超过银行家现有的资金时就可接纳该顾客;
2、顾客可以分期贷款, 但贷款的总数不能超过最大需求量;
3、当银行家现有的资金不能满足顾客尚需的贷款数额时,对顾客的贷款可推迟支付,但总能使顾客在有限的时间里得到贷款;
4、当顾客得到所需的全部资金后,一定能在有限的时间里归还所有的资金。
jdk提供的一种除Synchronized之外的加锁方式,定义了锁对象来进行操作
和synchronized相比:
失去了Synchronized关键字隐式加锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种Synchronized关键字所不具备的同步特性
Lock lock = new ReentrantLock();
lock.lock();//设置当前线程的同步状态,并在队列中保存线程及线程的同步状态
//设置同步成功,往下执行,失败,则阻塞在上行所示代码
try{
......
}finally{
lock.unlock();
}
注意:synchronized同步块执行完成或者遇到异常时锁会自动释放,而Lock必须调用unlock()方法释放锁,因此在finally中释放锁
AQS(AbstractQueueSynchronizer)
AQS: 队列式的同步器
(1) 提供公平锁和非公平锁
是否按照入队的顺序设置线程的同步状态—多个线程申请加锁操作时,是否按照时间顺序来加锁
(2)AQS提供的独占式和共享式设置同步状态(独占锁,共享锁)
独占式:只允许一个线程获取到锁
共享式:一定数量的线程共享式获取锁
(3)带Reentrant关键字的Lock包下的API:可重入锁
允许多次获取同一个Lock对象的锁
public class ReadWriteTest {
private static ReadWriteLock LOCK = new ReentrantReadWriteLock();
private static Lock READLOCK = LOCK.readLock();
private static Lock writeLock = LOCK.writeLock();
public static void readFile(){
try {
READLOCK.lock();
//IO读文件
} finally {
READLOCK.unlock();
}
}
public static void writeFile(){
try {
writeLock.lock();
//IO写文件
} finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
//20个线程读文件
for(int i = 0;i < 20;i++){
//
}
//20个线程写文件
for(int i = 0;i < 20;i++){
//
}
}
}
优势:针对读读并发执行,提高运行效率
condition:线程间通信
public static Lock LOCK = new ReemtrantLock();
public static Condition CONDITION = LOCK.newCondition();
public static void t3(){
try{
LOCK.lock()//=synchronized()加锁的代码
while(库存达到上限){
CONDITION.wait();//=wynchronized锁的对象.wait()
}
System.out.println("t3");
CONDITION.signal();//=synchronized锁对象.notify();
CONDITION.signalAll();//=synchronized锁对象.notifyAll()
}finally{
LOCK.unlock();
}
案例:入口线程执行t方法:入口线程阻塞等待,直到所有子线程执行完毕
public class Main {
//第一种方式:yield()
@Test
public void t1(){
for(int i = 0;i < 20;i++){
new Thread(()->{
System.out.println(Thread.currentThread().getName());
}).start();
}
while(Thread.activeCount() > 1){
Thread.yield();
}
System.out.println("执行完毕:"+Thread.currentThread().getName());
}
//第二种方式:join()
@Test
public void t2() throws InterruptedException {
List<Thread> threads = new ArrayList<>();
for(int i = 0;i < 20;i++){
Thread t = new Thread(()->{
System.out.println(Thread.currentThread().getName());
});
threads.add(t);
t.start();
}
for(Thread t : threads){
t.join();
}
System.out.println("执行完毕:"+Thread.currentThread().getName());
}
//第三种方式:CountDownLatch
@Test
public void t3() throws InterruptedException {
CountDownLatch cdl = new CountDownLatch(20);//计数器的初始值
for(int i = 0;i < 20;i++){
Thread t = new Thread(()->{
System.out.println(Thread.currentThread().getName());
cdl.countDown();//计数器的值-1
});
t.start();
}
cdl.await();//当前线程阻塞等待,直到计数器的值等于0
System.out.println("执行完毕:"+Thread.currentThread().getName());
}
}
一个计数信号量,主要用于控制多线程对共同资源库访问的控制
(1)使用场景:
(2)常见API
new Semaphore(int);
//给定数量的初始值,无参的默认为0release(int);
//计数器值增加给定的数量acquire(int);
//当前线程获取Semaphore对象中的给定数量的资源,获取到:资源数量减少,当前线程往下执行,获取不到:当前线程阻塞等待public class Semaphore {
//阻塞等待一组线程执行完毕,再执行XX任务
@Test
public void t1() throws InterruptedException {
java.util.concurrent.Semaphore s = new java.util.concurrent.Semaphore(0);
for(int i = 0;i < 20;i++){
Thread t = new Thread(()->{
System.out.println(Thread.currentThread().getName());
s.release();//释放资源量(无参的话就是1)
});
t.start();
}
s.acquire(20);//获取资源量,子线程执行完毕后,获取到,子线程执行不完,一直阻塞等待
System.out.println("执行完毕:"+Thread.currentThread().getName());
}
//限制有限资源访问
//模拟服务端接收客户端http请求:只1000个并发
//在一个时间点,客户端任务数达到1000.再有客户端请求,将阻塞等待
@Test
public void t2() throws InterruptedException {
java.util.concurrent.Semaphore s = new java.util.concurrent.Semaphore(1000);
for(;;){
Thread t = new Thread(()->{
try {
s.acquire();//获取资源量,如果没有,一直阻塞等待
//模拟每个线程处理客户端http请求
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
s.release();//释放资源量(无参的话就是1)
}
});
t.start();
}
}
}
ThreadLocal用于提供线程局部变量.,在多线程环境下可以保证各个线程里的变量独立于其他线程里的变量,也就是说ThreadLocal可以为每个线程创建一个[单独的变量副本],相当于线程的private static 类型变量
ThreadLocal的作用和同步机制有些相反,同步机制是为了保证多线程环境下数据的一致性,而ThreadLocal是保证了多线程环境下数据的独立性
隔离线程间的变量.保证每个线程是使用自己线程内的变量副本
public class ThreadLocalTest {
private static ThreadLocal<String> HOLDER = new ThreadLocal<>();
@Test
public void t1() {
//都和线程绑定,里边的值每个线程间是隔离开的
// HOLDER.get();//获取当前线程中,某个ThreadLocal对象的值
// HOLDER.remove();//移除当前线程中,某个ThreadLocal对象的值
// HOLDER.set("abc");//设置当前线程中,某个ThreadLocal对象的值
try {
HOLDER.set("abc");
for (int i = 0; i < 20; i++) {
final int j = i;
new Thread(() -> {
try {
HOLDER.set(String.valueOf(j));
if (j == 5) {
Thread.sleep(500);
System.out.println(HOLDER.get());
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
HOLDER.remove();
}
}).start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(HOLDER.get());
} finally {
HOLDER.remove();//只要在某个线程设置了ThreadLocal值,在线程结束前,一定要remove.
}
}
}
说明:只要在某个线程设置了ThreadLocal值,在线程结束前,一定要remove
代码推荐写法:
定义类变量:static ThreadLocal<保存的数据类型> ThreadLocal = new ThreadLocal<>();
ThreadLocal多个线程使用的都是同一个,但是里边的值是和线程绑定的,线程间不相干
当有线程设置值的时候,在线程结束前,remove
new Thread(()->{
try{
threadLocal.set(设置的值);
}finally{
threadLocal.remove();
}
}).start();
Thread对象中都有自己的ThreadLocalMap.调用ThreadLocal对象设置值set(value),获取值操作get(),删除值操作remove(),都是对当前线程中ThreadLocalMap对象的操作,所以每个线程中变量是隔离开的
如果Entry没有继承弱引用类型(设置为键引用),导致在ThreadLocalMap的Set(),get()和remove()方法中,都有清除无效Entry的操作.这样做是为了降低内存泄漏发生的可能,Entry中的key使用了弱引用的方式,这样做是为了降低内存泄漏的概率,但不能完全避免内存泄漏假设Entry中的key没有使用弱引用的方式,而是使用了强引用:由于ThreadLocalMap的生命周期和当前线程一样长,那么当引用ThreadLocal的对象被回收后,由于ThreadLocalMap还持有ThreadLocal和对应value的强引用,ThreadLocalMap和对应的value是不会被回收的,这就导致了内存泄漏,所以Entry以弱引用的方式避免了ThreadLocal没有被回收而导致的内存泄漏,但是此时的value仍然是无法回收的,依然会导致内存泄漏,ThreadLocalMap已经考虑到这种情况,并且有一些防护措施:在调用ThreadLocalMap的get(),set()和remove()的时候都会清除当前线程ThreadLocalMap中所有key为null的value,这样可以降低内存泄漏发生的概率,所以我们在使用ThreadLocal的时候,每次用完ThreadLocal都调用remove()方法,清除数据,防止内存泄漏
如果Entry没有继承弱引用类型(设置为键引用),导致线程没有使用值时,也一直有引用指向V,产生内存泄漏
假设线程长时间没执行完,K是强引用:ThreadLocal对象一直不能回收,V也没法使用,导致内存泄漏
设置K为弱引用的好处:降低内存泄漏的风险
每次垃圾回收,只要没其他强引用指向ThreadLocal对象,就回收,ThreadLocalMap实现时.检查键为null的,把V变量设置为null.V指向的对象就没有了,可以回收
非线程安全的,JDK1.7基于数组+链表,JDK1.8基于数组+链表+红黑树
(1)JDK1.7:
transient Entry
final float loadFactor;//负载因子
static clas Entry<K,V> implements Map.Entry<K,V>{
final K key;
V value;
Entry<K,V> next;//链表(单向)
int hash;
}
static class Node<K,V> implements Map.Entry<K,V>{
final int hash;
final K key;
V value;
Node<K,V> next;
}
线程安全的,1.7和1.8都是数组+链表,全部方法都是基于synchronized加锁,效率非常低
线程安全的,并且很多场景下支持并发操作,提高了效率
(1)JDK1.7
static final class Segment<K,V> extends ReentrantLock implements Serializable{
transient volatile HashEntry<K,V>[] table;
final float loadFactor;
static final class HashEntry<K,V>{
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
}
static class Node<K,V> implements Map.Entry<K,V>{
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
(3)JDK1.7和1.8都存在的特性:
(4)总结: