JavaEE多线程进阶
多线程进阶
- 常见的锁策略
- 乐观锁和悲观锁
- 重量级锁和轻量级锁
- 挂起等待锁和自旋锁(Spin Lock)
- 公平锁和非公平锁
- 可重入锁和不可以重入锁
- 读写锁和普通互斥锁
- synchronized
- 锁升级
- 锁消除
- 锁粗化
- CAS
- 原子类
- CAS自旋锁
- ABA问题
- JUC(java.util.concurrent)的常⻅类
- Callable
- ReentrantLock
- Semaphore(信号量)
- CountDownLatch
- 线程安全的集合类
- Hashtable
- ConcurrentHashMap
常见的锁策略
乐观锁和悲观锁
乐观锁:认为多个线程访问同一个共享变量发生冲突概率小,并且并不是真正的加锁,而是直接访问数据,访问时候要识别当前的数据是否出现冲突
悲观锁:认为多个线程访问同一共享变量发生冲突概率大,每次访问变量会真的加锁
重量级锁和轻量级锁
重量级锁:加锁操作开销较大
轻量级锁:加锁操作开销较小
并且重量级锁较依赖mutex,其会有大量内核态和用户态之间切换,并且容易发生线程调度
轻量级锁,尽量不使用mutex,尽量让代码在用户态完成,不太容易发生线程调度
挂起等待锁和自旋锁(Spin Lock)
挂起等待锁:遇到锁冲突,就把线程阻塞,等待未来某个时间唤醒
(会涉及系统内部线程调度,比较复杂,开销较大)
自旋锁:遇到锁冲突,先不把线程阻塞,重试几下
(用户态操作,不涉及内核态和线程调度,开销较小)
自旋锁会会消耗CPU资源,当锁释放了,起就会第一时间获取,但是可能会浪费CPU资源,反之挂起等待锁不会浪费CPU资源,但是会获取锁不及时
公平锁和非公平锁
公平锁:遵循"先来后到"
非公平锁:不遵循先来后到
可重入锁和不可以重入锁
可重入锁:允许一个线程多次获取同一把锁,不会出现死锁问题
不可冲入锁:如果一个线程多次获取同一把锁,会出现死锁问题
读写锁和普通互斥锁
普通互斥锁:就涉及到加锁和解锁
读写锁:加读锁、加写锁和解锁
读锁和读锁之间不互斥
写锁和写锁之间互斥
读锁和写锁之间互斥
遇到读就加读锁,遇到写就加写锁,读写锁适用于"读多写少"
synchronized
锁升级

无锁:还没有进入加锁代码块
偏向锁:没有真正加锁,只是通过一个标记,如果有竞争才进行加锁
自旋锁:真正加锁,轻量级锁
重量级锁:当竞争激烈时候,变成重量级锁
synchronized锁会根据竞争情况,自动升级,但是一旦升级无法回到过去
锁消除
在synchronized中一个编译器优化,有的地方并不需要加锁,但是我们进行了加锁,其编译器就会将这个锁给自动消除,毕竟加锁和解锁比较消耗时间和空间
锁粗化
锁的粒度
如果加锁和解锁中间有逻辑比较多,锁的粒度较粗
反之如果逻辑比较少,锁的粒度较细
CAS
cas的全程是 Compare and swap就是比较和交换的意思

上面这个CAS的伪代码,看着像一个函数,但是其是一条"指令",其是线程安全的
CPU提供了CAS的指令
操作系统对其封装,并且提供了API使用CAS的机制
JVM封装调用操作系统的API
java代码就可以使用JVM的API
但是这并不安全,因为只要涉及底层的都不是特别安全
原子类
java.util.concurrent.atomic,java的这个包中里面实现了这样AtomicInteger类,并且这个类中有方法可以进行i++操作,并且是线程安全的

//创建一个原子类对象,并且初始化为0
private static AtomicInteger count = new AtomicInteger(0);
public class demo29 {//使用原子类private static AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {count.getAndIncrement();//count++
// count.getAndDecrement();//count--
// count.incrementAndGet();//++count
// count.decrementAndGet();//--count}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {count.getAndIncrement();//count++}});t1.start();t2.start();t1.join();t2.join();//获取值System.out.println(count.get());}
}
此时使用这个类定义的成员,并且使用对应方法,这样其就是线程安全的

CAS自旋锁
synchronized中的自旋锁就是依赖CAS实现的

ABA问题
有一个共享数据num,初始值为A,t1线程在执行更新操作时候将其修改成Z,中间有t2线程插入将其数据修改成B,这时候执行t1线程发现不相同又将B修改成了A,在进行下面操作,这样就不符合我们的要求了
例如:
1.初始值100,线程1获取当前存款为100,期望更新为50,线程2获取当前存款值为100,期望更新为50
2.线程1执行扣款成功,存款变成50,此时线程2还在等待
3.但是在线程2执行之前,有人存入50,存款又变成了100
4.线程2执行,发现是100和之前一样,这样就再次执行扣款,这样存款变成了50
解决方案:使用一个version来表示版本号,版本号只可以加,通过版本号判断是否有插队执行的任务
JUC(java.util.concurrent)的常⻅类
Callable
1.创建一个匿名内部类,实现Callable接口,有泛型参数,并且有返回类型
2.需要重写call方法,使用FutureTask接收Callable对象,Thread接收不了
//要重写call方法
Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i <= 100; i++) {sum+=i;}return sum;}};
public class demo30 {public static void main(String[] args) throws ExecutionException, InterruptedException {Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i <= 100; i++) {sum+=i;}return sum;}};//Thread无法接收callable对象//使用FutureTaskFutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t = new Thread(futureTask);t.start();//get获取call的结果System.out.println(futureTask.get());}
}
Callable和Runnable都是描述一个任务,而Callable描述的任务有返回值,而Runnable没有返回值
Callable需要搭配FutureTask来接收Callable返回结果,并且要等待结果执行出来
ReentrantLock
ReentrantLock locker = new ReentrantLock();locker.lock();//加锁locker.unlock();//解锁
public class demo31 {private static int count = 0;public static void main(String[] args) throws InterruptedException {ReentrantLock locker = new ReentrantLock();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {locker.lock();count++;locker.unlock();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {locker.lock();count++;locker.unlock();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

ReentrantLock和synchronized区别
- ReentrantLock,支持tryLock,如果加锁不成,可以直接返回(放弃),并且也支持超时时间,但是lock加锁不成就进行阻塞等待

2.并且这个支持公平锁,这里设置为true就是公平锁,此处就会按照时间来执行线程
3.等待通知的不同
synchronized搭配 wait 和notify,并且这里的notify只可以随机唤醒一个
ReentrantLock搭配 Condition 类,可以指定唤醒
RenntrantLock是使用lock加锁,unlock解锁,要手动解锁,但是这样容易出现忘记解锁问题
Semaphore(信号量)
信号量本表示的是可使用资源个数,本质是一个计数器
Semaphore semaphore = new Semaphore(3);//只有3个可用资源
semaphore.acquire();//获取资源 P操作
semaphore.release();//释放资源 V操作
public class demo32 {public static void main(String[] args) throws InterruptedException {Semaphore semaphore = new Semaphore(3);//p操作semaphore.acquire();System.out.println("P操作");semaphore.acquire();System.out.println("P操作");semaphore.acquire();System.out.println("P操作");semaphore.acquire();System.out.println("P操作");//V操作semaphore.release();System.out.println("V操作");}
}
此时只有三个资源,但是我们要获取4个资源,因此第四次获取就会一直等资源释放

public class demo32 {public static void main(String[] args) throws InterruptedException {Semaphore semaphore = new Semaphore(3);//p操作semaphore.acquire();System.out.println("P操作");semaphore.acquire();System.out.println("P操作");semaphore.acquire();System.out.println("P操作");//V操作semaphore.release();System.out.println("V操作");semaphore.acquire();System.out.println("P操作");}
}
此时释放一个,就有资源释放了

CountDownLatch
CountDownLatch latch = new CountDownLatch(8);//创建一个对象
latch.countDown();//数量-1
latch.await();//等待结束所有,也就是当计数为0
public class demo33{public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(8);for (int i = 0; i < 8 ; i++) {final int id = i;Thread t = new Thread(() ->{System.out.println("运动员" + id + "出发");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("运动员" + id + "到达终点");latch.countDown();//结束就-1});t.start();}//通过awit进行等待,等待都执行完latch.await();System.out.println("比赛结束");}
}

线程安全的集合类
1.Vector,Stack,HashTable都是线程安全的,但是不建议使用,因为内部实现了锁,有时候是不需要锁,这样加了锁会增大开销
2.CopyOnWriteArrayList,当添加新元素时候,不直接向容器里面添加,而是先将起拷贝一份,将新元素添加到新容器中,添加完以后,将原容器的引用指向新对象,这样的确可以避免我们读到修改一半的数据
优点:读多写少的场景下,性能高
缺点:因为每次都要拷贝占用内存多,并且新添加的元素不能立即读取
3.阻塞队列
ArrayBlockingQueue基于数组实现
LinkedBlockingQueue//基于链表实现
PriorityBlockingQueue//基于堆实现
TransferQueue//最多包含一个元素的阻塞队列
Hashtable
HashMap是线程不安全的,在多线程下使用哈希表可以使用Hashtable和ConcurrentHashMap
Hashtable中队put和get进行了加锁


相当于给this对象加锁

存在问题
1.如果多线程访问同一个Hashtable就会直接造成锁阻塞
2.size属性同步比较满,因为sunchronized
3.当需要扩容时候,该线程完成整个扩容过程,会涉及大量拷贝,效率低下
ConcurrentHashMap

哈希表中每一个链表的头节点都有一个一把锁,将头节点作为锁对象
1.多线程竞争同一把锁,出现锁冲突,竞争不同的锁,不会出现锁冲突
并且链表的个数比较多,元素分布在不同链表上,这样出现锁冲突概率低
2.使用CAS的方式进行size更新,避免加锁
3.扩容,采用"化整为零",不是一次性将所有元素进行搬运,而是分成多次,这时候会一个ConcurrentHashMap维护新数组和老数组
