多线程七
1.常见锁策略
1.1乐观锁&悲观锁
1.1.1乐观锁定义
乐观锁认为一般情况下不会出现冲突,所以只会在更新以及修改数据的时候才会对冲突进行检测,如果没有发生冲突直接进行修改,如果发生冲突了不做任何修改,然后把结果返回给用户,让用户自行决定处理。
1.1.2乐观锁实现-CAS
CAS(Compare And Swap)比较并替换,CAS比较并替换的流程是这样的,CAS中包含了三个操作单位:V(内存值),A(预期旧值),B(新值),比较V值和A值是否相等,如果相等的话则将V的值更换成B,否则就提示用户修改失败,从而实现了CAS机制。
1.CAS底层实-Unsafe-Atomic::cmpxchg
CAS 实现是借助 Unsafe 类,Unsafe 类调⽤操作系统的 Atomic::cmpxchg(原⼦性汇编指令)。
2.CAS应用-AtomicInteger
1.非线程安全代码:
public class CASDemo1 {
private static int number=0;
private final static int MAX_COUNT=100000;
public static void main(String[] args) throws InterruptedException {
//++
Thread t1=new Thread(()->{
for (int i = 0; i <MAX_COUNT ; i++) {
number++;
}
});
t1.start();
//--
Thread t2=new Thread(()->{
for (int i = 0; i <MAX_COUNT ; i++) {
number--;
}
});
t2.start();
t1.join();
t2.join();
System.out.println("最终结果:"+number);
}
}
2.AtomicInteger线程安全:
import java.util.concurrent.atomic.AtomicInteger;
/**
* CAS使用
*/
public class CASDemo1 {
private static int number=0;
private static AtomicInteger atomicInteger=new AtomicInteger(0);
private final static int MAX_COUNT=100000;
public static void main(String[] args) throws InterruptedException {
//++
Thread t1=new Thread(()->{
for (int i = 0; i <MAX_COUNT ; i++) {
atomicInteger.getAndIncrement();
}
});
t1.start();
//--
Thread t2=new Thread(()->{
for (int i = 0; i <MAX_COUNT ; i++) {
atomicInteger.getAndDecrement();
}
});
t2.start();
t1.join();
t2.join();
System.out.println("最终结果:"+atomicInteger.get());
}
}
3.CSA缺点ABA问题
1.张三进行转账操作(100):
第一次点击转账以后,系统卡顿点击了两次转账按钮。
1.第一次点击转账按钮:-50(V=100,A=100,B=50)
2.第二次点击转账按钮:-50(V=100,A=100,B=50)
3.发工资操作:+50 -> V=100
正确的结果应该是100元,而此时结果为50元,存在ABA问题。
2.ABA代码演示:
import java.util.concurrent.atomic.AtomicInteger;
public class ABADemo1 {
private static AtomicInteger money=new AtomicInteger(100);
public static void main(String[] args) throws InterruptedException {
//第一次点击转账按钮(-50)
Thread t1=new Thread(()->{
int oldmoney=money.get();
//执行花费2S
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
money.compareAndSet(oldmoney,oldmoney-50);
});
t1.start();
Thread t2=new Thread(()->{
int oldmoney=money.get();
money.compareAndSet(oldmoney,oldmoney-50);
});
t2.start();
//给账户+50
Thread t3=new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int oldmoney=money.get();
money.compareAndSet(oldmoney,oldmoney+50);
});
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("最终账号余额:"+money.get());
}
}
3.ABA解决⽅案AtomicStampedReference
引入版本号,每次操作之后让版本号+1,执行的时候判断版本号和值,就可以解决ABA问题。
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo2{
private static AtomicStampedReference<Integer> money=new AtomicStampedReference<>(100,0);
public static void main(String[] args) throws InterruptedException {
//第一次点击转账按钮(-50)
Thread t1=new Thread(()->{
int oldmoney=money.getReference();
int version=money.getStamp();
//执行花费2S
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
money.compareAndSet(oldmoney,oldmoney-50,version,version+1);
});
t1.start();
Thread t2=new Thread(()->{
int oldmoney=money.getReference();
int version=money.getStamp();
money.compareAndSet(oldmoney,oldmoney-50,version,version+1);
});
t2.start();
//给账户+50
Thread t3=new Thread(()->{
//执行花费1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int oldmoney=money.getReference();
int version=money.getStamp();
money.compareAndSet(oldmoney,oldmoney+50,version,version+1);
});
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("最终账号余额:"+money.getReference());
}
}
1.1.3悲观锁定义和应用
定义:总是假设最坏情况,每次去拿数据的时候都会认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
应⽤:synchronized、Lock 都是悲观锁。
1.2公平锁&非公平锁
1.非公平锁:
(1)抢占式执行,有一些先来的任务还在排队,刚好释放锁的时候新来了一个任务,此时并不会通知任务队列来执行任务,而是执行新来的任务。
(2)非公平锁:ReentrantLock lock = new ReentrantLock(false)
。如果构造函数不传递参数,则默认 是非公平锁。
2.公平锁:
(1)所有任务来了之后先排队,线程空闲之后去任务队列按顺序执行最早任务。
(2)公平锁:ReentrantLock lock= new ReentrantLock(true)
。
1.3读写锁
1.3.1读写锁
1.读写锁(Reader-Writer Lock)就是把一把锁分为两部分:读写和写锁,其中读锁允许多个线程同时获得,因为读操作本身是线程安全的,而写锁则是互斥锁,不允许多个线程同时获得(写锁),并且写操作和读操作也是互斥的。总之,读写锁的特点是:读读不互斥,读写互斥,写写互斥。
java标准库提供了ReentrantReadWriteLock类,实现了读写锁。
(1)ReentrantReadWriteLock.ReadLock
类表示一个读锁,这个对象提供了lock/unlock方法进行加锁解锁。
(2)ReentrantReadWriteLock.WriteLock
类表示一个写锁,这个对象也提供了lock/unlock方法进行加锁解锁。
(3)注意: 只要是涉及到 “互斥”, 就会产⽣线程的挂起等待. ⼀旦线程挂起, 再次被唤醒就不知道隔了多久了. 因此尽可能减少 “互斥” 的机会, 就是提⾼效率的重要途径.
读写锁特别适合于 “频繁读, 不频繁写” 的场景中.
2.代码演示读写锁使用:
import java.time.LocalDateTime;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 演示读写锁的使用
*/
class ReadWriteLockDemo1 {
public static void main(String[] args) {
//创建读写锁
final ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
//创建读锁
final ReentrantReadWriteLock.ReadLock readLock=readWriteLock.readLock();
//创建写锁
final ReentrantReadWriteLock.WriteLock writeLock=readWriteLock.writeLock();
//线程池
ThreadPoolExecutor executor=new ThreadPoolExecutor(5,5,0, TimeUnit.SECONDS,new LinkedBlockingDeque<>());
//启动线程执行任务(读)
executor.submit(()->{
//加锁操作
readLock.lock();
try{
//执行业务逻辑
System.out.println("执行读锁1:"+ LocalDateTime.now());
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
readLock.unlock();
}
});
//启动新线程执行任务(读操作2)
executor.submit(()->{
readLock.lock();
try{
//执行业务逻辑
System.out.println("执行读锁2:"+LocalDateTime.now());
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
});
//创建新线程执行(写操作)
executor.submit(()->{
//加锁
writeLock.lock();
try{
//业务逻辑
System.out.println("执行写锁1:"+LocalDateTime.now());
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
writeLock.unlock();
}
});
//创建新线程执行写操作
executor.submit(()->{
writeLock.lock();
try{
//业务逻辑
System.out.println("执行写锁2:"+LocalDateTime.now());
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
});
}
}
1.3.2读占锁
独占锁是指任何时候都只有一个线程能执行对资源操作。
synchronized,Lock.
1.3.3 共享锁
共享锁指定是可以同时被多个线程读取,但只能被一个线程修改
例如:ReentrantReadWriteLock就是共享锁的实现,它允许一个线程进行写操作,允许多个线程进行读操作。
1.4可重入锁&自旋锁
1.4.1可重入锁
可重入锁指的是该线程获取了锁之后,可以无限次的进入该锁锁住的代码。
1.4.2自旋锁
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文的切换的消耗,缺点是循坏会消耗CPU.
2.JUC
java.util.concurrent
下的类就叫JUC类,JUC下典型的类有:
1.ReentrantLock
2.Semaphore
3.CountDownLatch
4.CyclicBarrier
2.1 ReentrantLock:可重入锁
2.2 Semaphore:信号量
Semphore信号量可以实现限流:
(1)acquire()
:获取令牌。(2)
release
:释放令牌。
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* 信号量:实现限流
*/
public class SemaphoreDemo1 {
public static void main(String[] args) {
//创建线程池
ExecutorService service= Executors.newFixedThreadPool(5);
//创建信号量
Semaphore semaphore=new Semaphore(2);
//统一任务定义
Runnable runnable=new Runnable() {
@Override
public void run() {
Thread currThread=Thread.currentThread();//得到执行此任务的线程
System.out.println("进入线程:"+currThread.getName());
try {
semaphore.acquire();//如果没有可用令牌的话,那么线程会阻塞在当前位置
System.out.println(currThread.getName()+
":得到了令牌|Time"+ LocalDateTime.now());
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(currThread.getName()+
":释放了令牌|Time"+ LocalDateTime.now());
semaphore.release();
}
}
};
//定义新线程执行任务
service.submit(runnable);
//定义新线程执行任务
service.submit(runnable);
//定义新线程执行任务
service.submit(runnable);
//定义新线程执行任务
service.submit(runnable);
//定义新线程执行任务
service.submit(runnable);
}
}
2.3 CountDownLatch:计数器
1.等待所有线程执行完:
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// do something
}
});
t1.start();
// 创建线程2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// do something
}
});
t2.start();
// 等待线程 1和线程 2 执⾏完
t1.join();
t2.join();
如果是线程池,要等待所有的线程池都执行完,join就不行了,这个时候就可以使用CountDownLatch.
2.CountDownLatch计数器:
CountDownLatch
计数器:判断线程池的任务是否已经全部执行完
(1)countDown()
:计数器-1.
(2)awit()
:阻塞等待,所有的任务全部执行完(等待CountDownLatch=0,继续执行后面的代码)。
3.比赛的时候,需要等待所有选手都到达终点之后再公布成绩。
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 计数器的使用
*/
public class CountDownLatchDemo1 {
public static void main(String[] args) throws InterruptedException {
//创建计数器
CountDownLatch countDownLatch=new CountDownLatch(5);
ExecutorService service= Executors.newFixedThreadPool(5);
//创建线程执行任务
for (int i = 1; i <6 ; i++) {
service.submit(()->{
Thread currThread=Thread.currentThread();
System.out.println(currThread.getName()+"开始起跑");
//跑步所用时间
int runTime=(1+new Random().nextInt(5));
try {
TimeUnit.SECONDS.sleep(runTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(currThread.getName()+"到达终点,用时:"+runTime);
countDownLatch.countDown();
});
}
//阻塞等待,直到所有线程执行结束
countDownLatch.await();
System.out.println("比赛结果的宣布");
}
}
2.4 CyclicBarrier:循环屏障
CyclicBarrier循环屏障:线程分组的阻塞
awit()
:计数器-1,判断当前计数器是否为0,如果为0,冲破屏障之后的代码,否则阻塞等待,直到屏障被冲破。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 循环屏障
*/
public class CyclicBarrierDemo1 {
public static void main(String[] args) {
//循环屏障
CyclicBarrier cyclicBarrier=new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("计数器为0了");
}
});
ExecutorService service= Executors.newFixedThreadPool(10);
for (int i = 0; i <10 ; i++) {
int finalI = i;
service.submit(()->{
Thread currThread=Thread.currentThread();
System.out.println("执行线程:"+currThread.getName());
try {
Thread.sleep(500* finalI);
cyclicBarrier.await();//执行阻塞等待,直到循环屏障计数器为0
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("线程执行完成"+currThread.getName());
});
}
}
}
3.线程安全&非安全容器
3.1非线程安全容器
Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, ⼜没有任何加锁措施.
● ArrayList
● LinkedList
● HashMap
● TreeMap
● HashSet
● TreeSet
● StringBuilder
3.2线程安全容器
● Vector (不推荐使⽤)
● HashTable (不推荐使⽤)
● ConcurrentHashMap
● StringBuffer
3.3关于HashMap
关于HashMap是线程不安全的,其主要体现:
1.在jdk 1.7中,在多线程环境下,扩容时因为采用头插法,会造成环形链或数据覆盖。
2.在jdk 1.8中,在多线程环境下,会发生数据覆盖情况。
3.3.1HashMap JDK1.7死循环
hashMap jdk1.7扩容核心源码:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//通过 key 值的 hash 值和新数组的⼤⼩算出在当前数组中的存放位置
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
正常扩容之后:
多线程扩容:
线程2执行完,线程1继续执行:
3.3.2线程安全的字典
HashMap本身不是线程安全的,在多线程环境下使用哈希表可以使用:
●
Hashtable
●ConcurrentHashMap
(1)Hashtable
只是简单的把关键方法加上了synchronized关键字。
这相当于直接针对Hashtable对象本身加锁。
●如果多线程访问同一个Hashtable就会直接造成冲突。
●size属性也是通过synchronized来控制同步,也是比较慢的。
●一旦触发扩容,就由该线程完成整个扩容过程,这个过程会涉及到大量的元素拷贝,效率非常低。
(2)ConcurrentHashMap
ConcurrentHashMap是HashMap的多线程版本,HashMap在并发操作时会有各种问题,比如死循环问题,数据覆盖等问题。
ConcurrentHashMap在不同的JD版本中实现是不同的,在JDK1.7中它使用的是数组加链表的形式实现的,而数组又分为:大数组Segment和小数组HashEntry.大数组Segment可以理解为MYSQL中的数据库,而每个数据库(Segment)中又有很多张表HashEntry,每个HashEntry中又有多条数据,这些数据是用来链表连接的,如下图所示:
ConcurrentHashMap 的线程安全是建⽴在 Segment 加锁的基础上的,所以我们把它称之为分段锁或片 段锁,如下:
在JDK1.7中,ConcurrentHashMap虽然是线程安全的,但是因为它的底层实现是数组+链表的形式,所以在数据比较多的情况下访问是很慢的,因为要遍历整个链表,而JDK1.8则使用了数组+链表/红黑树的方式优化了ConcurrentHashMap的实现,具体实现结果如下:
链表升级为红黑树的规则:当链表长度大于8,并且数组的长度大于64时,链表就会升级为红黑树的结构。
ConcurrentHashMap 在 JDK 1.8 虽然保留了 Segment 的定义,但这仅仅是为了保证序列化时 的兼容性,不再有任何结构上的⽤处了。
在JDK1.8中我们可以简单的认为CouncurrentHashMap是在头结点加锁来保证线程安全的,锁的粒度相比Segment来说更小了,发生冲突和加锁的频率降低了,并发操作的性能就提高了。而且JDK1.8使用的是红黑树优化了之前的固定链表,那么当数据量比较大的时候,查询性能也得到了很大的提升,从之前的O(n)优化到O(longn)的时间复杂度,具体加锁示意图如下: