JUC
1、什么是JUC?
面试高配点
它是java.util的工具包
业务:普通的线程 Thread
一般来说callable的使用比Runnable更多,因为其拥有返回值,效率更高
2、线程和进程
进程:一个程序,其至少包含一个线程
线程:一个线程负责一项工作(执行路线)
java默认线程有:main主线程和gc线程(用于清理垃圾)
java真的可以开启线程吗?其实是开不了,他只能通过本地去调用,java无法调用本地硬件
并发和并行
并发(多线程操作同一个资源)
- CPU一核,模拟多条线程
并行(多个人一起行走)
- CPU多核,多个线程可以同时执行;线程池
并发编程的本质:充分利用CPU的资源
3、Lock锁(重点)
案例一、使用传统的synchronized来处理并发问题
package cn.linqin.test;
//使用传统的synchronized锁 来解决并发问题
public class SaleTicketDemo {
public static void main(String[] args) {
Ticket ticket=new Ticket();
new Thread(()->{
for(int i=0;i<30;i++){
ticket.sale();
}
},"小光").start();
new Thread(()->{
for(int i=0;i<30;i++){
ticket.sale();
}
},"linq").start();
}
}
//创建资源对象
class Ticket{
private int number=50;//50张票
//卖票方法 传统方式在这里加锁排队
public synchronized void sale(){
if(number>0){
System.out.println(Thread.currentThread().getName()+"卖出了"+number--+"张票,还余"+number+"张票");
}
}
}
案例二、使用Lock锁处理
流程为:创建锁–>加锁—>finaly 解锁
//创建资源对象
class Ticket{
private int number=50;//50张票
//卖票方法 传统方式在这里加锁排队 使用Lock锁
Lock lock=new ReentrantLock();
public void sale(){
try{
lock.lock();//加锁
if(number>0){
System.out.println(Thread.currentThread().getName()+"卖出了"+number--+"张票,还余"+number+"张票");
}
}finally {
lock.unlock();//释放锁
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rcnaquW9-1618297608440)(C:\Users\86155\AppData\Roaming\Typora\typora-user-images\1617937864101.png)]
Lock lock=new ReentrantLock();//点开其源码可以发现,默认使用非公平锁
公平锁:顾名思义十分公平,先来后到讲规矩
非公平锁(默认使用):十分不公平,蛮不讲理可以插队
synchronized和Lock锁的区别
- synchronized是java内置的关键字,Lock则是Java类
- synchronized无法判断获取锁的状态,而Lock可以判断是否获取到了锁
- synchronized会自动释放锁,而是用Lock则需要手动释放锁,不然会造成死锁现象
- synchronized的线程一(如果阻塞,第二个线程会一直等),而Lock锁不一定会等下去(因为其有 lock.tryLock()方法会尝试获取锁)
- synchronized默认是可重入锁,无法中断,Lock锁也是默认为可重入锁,但他可以自己设置
- synchronized适合锁少量的代码同步问题,Lock锁适合处理大量的代码同步问题
锁是什么?如何判断锁
4、生产者与消费者
面试的:单例模式、排序问题、生产者消费者、死锁(需要会手写)
传统的线程通信 synchronized
package cn.linqin.test;
//实现线程abcd 互相通知
//线程通知 我们实现A执行i-1之后B执行i+1
/**
* 线程通信,生产者与消费者问题: 等待唤醒,通知唤醒
* 线程交替进行 A B同时操作一变量 num = 0;
* A num+1
* B num-1
*/
public class Adding {
public static void main(String[] args) {
Data data=new Data();
//创建两个线程
new Thread(()->{
for (int i = 0; i <10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i <10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
//资源类
class Data {
private int number=0;
//+1方法
public synchronized void increment() throws InterruptedException {
if(number!=0){//0
//进入休眠状态,等待唤醒
this.wait();
}
number++;//添加完后唤醒
System.out.println(Thread.currentThread().getName()+"=>"+number);
this.notif yAll();//唤醒下一个线程
}
//-1方法
public synchronized void decrement() throws InterruptedException {
if(number==0){//0
//进入休眠状态,等待唤醒
this.wait();
}
number--;//添加完后唤醒
System.out.println(Thread.currentThread().getName()+"=>"+number);
this.notifyAll();//唤醒下一个线程
}
}
但这个代码会引起虚假唤醒的问题,如果是4个线程呢?
理解!:
因为上述代码使用if在条件成立时只会执行一次,而我们两个线程同时去操作就可以连续增加结果不再是1,可能是2,3甚至更高,这个时候我们只需要不断的判断将if改成while,只要条件成立则进入休眠状态
JUC版的生产者与消费者问题
//资源类
class Data {
private int number=0;
//使用Lock锁
Lock lock=new ReentrantLock();
//创建监听器,其中有等待和唤醒方法
//condition.await();//等待
//condition.signalAll();//唤醒
Condition condition = lock.newCondition();
//+1方法
public void increment() throws InterruptedException {
try{
//上锁
lock.lock();
while(number!=0){//0
//进入休眠状态,等待唤醒
condition.await();
}
number++;//添加完后唤醒
System.out.println(Thread.currentThread().getName()+"=>"+number);
condition.signalAll();//唤醒下一个线程
}finally {
lock.unlock();//解锁
}
}
//-1方法
public void decrement() throws InterruptedException {
try{
lock.lock();//上锁
while(number==0){//0
//进入休眠状态,等待唤醒
condition.await();
}
number--;//添加完后唤醒
System.out.println(Thread.currentThread().getName()+"=>"+number);
condition.signalAll();//唤醒下一个线程
}finally {
lock.unlock();//解锁
}
}
}
使用Lock锁
//使用Lock锁
Lock lock=new ReentrantLock();
//创建监听器,其中有等待和唤醒方法
//condition.await();//等待
//condition.signalAll();//唤醒
Condition condition = lock.newCondition();以上两种都可以实现,但还是有些问题
任何一个新技术的出现绝不仅仅只是覆盖了原有的技术,而是进行了优化和补充
Condition 精准的通知和唤醒线程
package cn.linqin.test;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//精确循序 A执行完B B执行完C C执行完A 重复十次
public class ConditionTest {
public static void main(String[] args) {
Test test=new Test();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
try {
test.printA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"霖磬").start();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
try {
test.printB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"欧阳").start();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
try {
test.printC();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"刘洋").start();
}
}
class Test{
private int num=1;//标识符,通过数字判断
private Lock lock=new ReentrantLock();//Lock锁
//创建三个监控器
Condition condition1=lock.newCondition();
Condition condition2=lock.newCondition();
Condition condition3=lock.newCondition();
//a执行方法
public void printA() throws InterruptedException {
try{
lock.lock();//上锁
if(num!=1){
condition1.await();//休眠
}
System.out.println(Thread.currentThread().getName()+"==>"+num);
num=2;
//唤醒B线程
condition2.signal();
}finally {
lock.unlock();//解锁
}
}
//B执行方法
public void printB() throws InterruptedException{
try{
lock.lock();//上锁
if(num!=2){
condition2.await();//休眠
}
System.out.println(Thread.currentThread().getName()+"==>"+num);
num=3;
//唤醒C线程
condition3.signal();
}finally {
lock.unlock();//解锁
}
}
//c执行方法
public void printC() throws InterruptedException{
try{
lock.lock();//上锁
if(num!=3){
condition3.await();//休眠
}
System.out.println(Thread.currentThread().getName()+"==>"+num);
num=1;
//唤醒B线程
condition1.signal();
}finally {
lock.unlock();//解锁
}
}
}
我们可以通过创建三个线程监控器和通过标识符来控制线程的状态,这样就可以做到按顺序输出了
5、8锁现象
8锁就是关于锁的八个问题
案例一、
对同一对象进行操作的执行顺序问题
package cn.linqin.test;
import java.util.concurrent.TimeUnit;
//8锁:就是关于锁的八个问题
public class CallTest {
public static void main(String[] args) {
Phone phone=new Phone();
//从这里看,是发短信在先打电话在后
//注意:这里不是先调用就先执行的原因,而是锁的原因
//每个对象都有一把锁,而两个线程同时操作一个对象,谁先获取到锁就先执行!
new Thread(()->{//发短信
try {
phone.call();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{//打电话
phone.sendSms();
}).start();
}
}
//电话类
class Phone{
//synchronized调用的锁就是对象本身
//两个方法谁先拿到锁就先执行
public synchronized void call() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public synchronized void sendSms(){
System.out.println("打电话");
}
}
案例二、
在上面的基础上再添加一个普通方法未上锁,那么最先打印出来的会是什么?
package cn.linqin.test.suo;
import java.util.concurrent.TimeUnit;
//那我们加一个普通方法来看,现在谁先执行
public class CallTest {
public static void main(String[] args) {
Phone phone=new Phone();
//从这里看,是发短信在先打电话在后
//注意:这里不是先调用就先执行的原因,而是锁的原因
//每个对象都有一把锁,而两个线程同时操作一个对象,谁先获取到锁就先执行!
new Thread(()->{//发短信
try {
phone.call();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{//打电话
phone.sendSms();
}).start();
//这个时候hello方法最先执行,因为他没有锁,它不受锁的影响!
new Thread(()->{//hello方法
phone.hello();
}).start();
}
}
//电话类
class Phone{
//synchronized调用的锁就是对象本身
//两个方法谁先拿到锁就先执行
public synchronized void call() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public synchronized void sendSms(){
System.out.println("打电话");
}
//普通方法
public void hello(){
System.out.println("hello");
}
}
普通方法调用不受锁的影响
案例三、
增加两个静态的同步方法,那么先打印的是发短信还是打电话?
package cn.linqin.test.suo;
import java.util.concurrent.TimeUnit;
//3、增加两个静态的同步方法,那么先打印的是发短信还是打电话?
public class CallTest3 {
public static void main(String[] args) {
/*
两个对象都被锁了,因为static锁的是Class 是类
*/
Phone2 phone=new Phone2();
Phone2 phone2=new Phone2();
//从这里看,是发短信在先打电话在后
//注意:这里不是先调用就先执行的原因,而是锁的原因
//每个对象都有一把锁,而两个线程同时操作一个对象,谁先获取到锁就先执行!
new Thread(()->{//发短信
try {
phone.call();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{//打电话
phone2.sendSms();
}).start();
}
}
//电话类
class Phone2{
//synchronized调用的锁就是对象本身
//两个方法谁先拿到锁就先执行
//static 是静态方法,类一加载就有了! 锁的是Class,类是全局唯一的,所以他们锁的是类
public static synchronized void call() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public synchronized void sendSms(){
System.out.println("打电话");
}
}
static 是静态方法,类一加载就有了! 锁的是Class,类是全局唯一的,所以他们锁的是类
案例四、
一个加了静态修饰,一个是普通的,先打电话还是先发短信?
package cn.linqin.test.suo;
import java.util.concurrent.TimeUnit;
//4、一个加了静态修饰,一个是普通的,先打电话还是先发短信?
public class CallTest3 {
public static void main(String[] args) {
Phone2 phone=new Phone2();
//从这里看,是发短信在先打电话在后
//注意:这里不是先调用就先执行的原因,而是锁的原因
//每个对象都有一把锁,而两个线程同时操作一个对象,谁先获取到锁就先执行!
new Thread(()->{//发短信
try {
phone.call();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{//打电话
phone.sendSms();
}).start();
}
}
//电话类
class Phone2{
//synchronized调用的锁就是对象本身
//两个方法谁先拿到锁就先执行
//static 是静态方法,类一加载就有了! 锁的是Class,类是全局唯一的,所以他们锁的是类
public static synchronized void call() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
//这次是打电话优先,因为其锁的是调用者,而static锁的直接就是Class对象
public synchronized void sendSms(){
System.out.println("打电话");
}
}
这次是打电话优先,因为其锁的是调用者,而static锁的直接就是Class对象
小结
new this 具体的一个手机
static Class 唯一的一个模板
6、集合类不安全
关于Arraylist引起的线程并发问题及解决方案
list不安全
package cn.linqin.test.suo;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
//并发异常问题 java.util.ConcurrentModificationException
public class ListTest {
public static void main(String[] args) {
//并发下的ArrayList是不安全的
//解决方案如下
/*
1、 List list=new Vector<String>(); Vector在jdk1.0时就出现了 其底层添加方法由synchronized修饰
2、 List list= Collections.synchronizedList(new ArrayList<>()); 通过转换将其转换为安全的
3、 List list= new CopyOnWriteArrayList(); 使用JUC包下的CopyOnWriteArrayList对象
*/
//CopyOnWrite 写入时复制 COW 算是计算机程序设计领域的一种优化策略
//因为多个线程调用List读取的时候固定的写入(覆盖),在写入的时候避免覆盖从而引起数据问题
//读写分离,CopyOnWriteArrayList在底层对之前的数据进行复制,在添加完新数据之后再插入回去
//同样是线程安全,为什么使用CopyOnWriteArrayList而不是Vector呢?因为Vector其由synchronized修饰,被synchronized修饰的方法效率会稍微慢点
List list= new CopyOnWriteArrayList();
//通过循环创建十个线程依次存数据到ArrayList
for (int i = 0; i <10 ; i++) {
new Thread(()->{
//存入UUID前五位
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
关于Set引起的线程并发问题及解决方案
set不安全
package cn.linqin.test.suo;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentSkipListSet;
//并发修改异常问题 java.util.ConcurrentModificationException
public class SetTest {
public static void main(String[] args) {
/*
同理可得:
解决方案:
1、使用转换类转换为安全的
Set set= Collections.synchronizedSet(new HashSet<>());
2、使用JUC解决
Set set= new ConcurrentSkipListSet();
*/
Set set= new ConcurrentSkipListSet();
for (int i = 0; i <30 ; i++) {
new Thread(()->{
//存入UUID前五位
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
HashSet的底层是什么?
public HashSet() {
map = new HashMap<>();
}
//add方法 set本质上就是map key是无法重复的!
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//不变的值
private static final Object PRESENT = new Object();
其底层实现的其实就是HashMap,HashMap由数组+链表+红黑树组成,线程不安全
Map是线程不安全的!
当然也有安全的,但是效率就大不如人意了,比如HashTable
但是其效率并不高,原因如下:
HashTable在竞争激烈的情况下,他的效率并不高,因为所有访问HashTable的线程都必须竞争同一把锁,一旦抢到则执行,抢不到则进入阻塞状态.
关于Map引起的线程并发问题及解决方案
package cn.linqin.test.suo;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
//Map类处理高并发问题
public class MapTest {
public static void main(String[] args) {
//map是这样用的吗? 工作中不这么使用
//默认等价为? new HashMap<>(16,0.75)
/*
同理可得:
解决方案:
1、使用转换类转换为安全的
Map<String,String> map= Collections.synchronizedMap(new HashMap<>());
2、使用JUC解决
Map<String,String> map=new ConcurrentHashMap<>();
*/
/*
HashTable 属于线程安全,但是他本身的效率并不高
HashTable在竞争激烈的情况下,他的效率并不高,因为所有访问HashTable的线程都必须竞争同一把锁,一旦抢到则执行,抢不到则进入阻塞状态.
*/
/*
为什么使用ConcurrentHashMap?
因为通过转换其他的方式效率低(如通过synchronizedMap(HashMap)或者HashTable),ConcurrentHashMap依赖的是java的内存模型
既然ConcurrentHashMap不同于前两种线程安全的方法,那么它一定是一种高效且安全的HashMap喽。那么为什么它会高效且安全呢?
ConcurrentHashMap的锁分段技术可以有效提高并发访问率,他提供了容器多把锁,每一把锁用来锁容器中的一部分数据
,那么在多线程的高并发下访问不同数据时可以同时竞争多把锁,从而有效提高并发访问率!
*/
Map<String,String> map=new ConcurrentHashMap<>();
//通过循环创建十个线程依次存数据到ArrayList
for (int i = 0; i <20 ; i++) {
new Thread(()->{
//存入UUID前五位
map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
ConcurrentHashMap 推荐使用,其效率更高!
1、为什么使用ConcurrentHashMap?
因为通过转换其他的方式效率低(如通过synchronizedMap(HashMap)或者HashTable),ConcurrentHashMap依赖的是java的内存模型
既然ConcurrentHashMap不同于前两种线程安全的方法,那么它一定是一种高效且安全的HashMap喽。
2、那么为什么它会高效且安全呢?
ConcurrentHashMap的锁分段技术可以有效提高并发访问率,他提供了容器多把锁,每一把锁用来锁容器中的一部分数据,那么在多线程的高并发下访问不同数据时可以同时竞争多把锁,从而有效提高并发访问率!
7、Callable
Callable的实现过程:
类继承Callable,重写其Call方法(需要返回值)
创建适配器再启动线程
**FutureTask是Runnable的实现类,而Callable和FutureTask有所关联 **
package cn.linqin.test.suo;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//CallableTest线程解析使用
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Linqin linq in=new Linqin();
// new Thread(new Runnable()).start();
// new Thread(new FutureTask<V>()).start();
//二者是存在关联的 FutureTask是Runnable的实现类
//FutureTask是和Callable有所关联的!
//创建适配器
FutureTask futureTask = new FutureTask<>(linqin);
new Thread(futureTask,"霖磬").start();
Integer i =(Integer)futureTask.get();//获取返回值,这个get方法可能会产生阻塞
System.out.println(i);
}
}
class Linqin implements Callable {
@Override
public Integer call() throws Exception {
System.out.println("测试");
return 123;
}
}
细节
1、获取返回值时可能会产生阻塞
2、有缓存!
8、常用辅助类(必会)
8.1、CountDownLatch
允许一个或多个线程等待知道其他线程中执行的一组操作完成同步辅助,可以用来计数
package cn.linqin.test.suo;
import java.util.concurrent.CountDownLatch;
//计数器
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
//倒计时,总数是6,主要要执行任务的时候在使用
CountDownLatch countDownLatch=new CountDownLatch(6);
for (int i = 0; i <6 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"-》gogo");
countDownLatch.countDown();//倒计时-1
},String.valueOf(i)).start();
}
countDownLatch.await();//等待计数器归零,归零之后再向下执行
System.out.println("Close Door");
}
}
//主要这两个方法,每次调用countDown()方法则会-1
//当await(),归零之后则会向下执行
countDownLatch.countDown();//倒计时-1
countDownLatch.await();//等待计数器归零,归零之后再向下执行
8.2、CyclicBarrier(加法计数器)
允许一组线程全部等待彼此到达共同屏障点的同步辅助.
加法计数器
package cn.linqin.test.suo;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
//加法计算器
public class CyclicBarrierDemo {
public static void main(String[] args) {
//集齐七颗龙珠召唤神龙
CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()->{
System.out.println("召唤神龙成功");
});
//创造召唤龙珠的线程 7个
for (int i = 1; i <= 7; i++) {
final int temp=i;
new Thread(()->{
//lambda不能操作i,因为隔着一个类,用个常量接受即可访问
System.out.println(Thread.currentThread().getName()+"获取龙珠"+temp);
try {
//当每次添加达到第七个时,调用后面的线程方法
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
8.3、Semaphore
信号量,它的作用是让有限的资源得到更多的利用
作业:共享资源互斥使用!并发限流,控制线程数,保证服务安全和高可用
package cn.linqin.test.suo;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
//停车位,互斥资源,共享使用
public class SemaphoreTest {
public static void main(String[] args) {
//创建车位共3个,每次只能由三个人使用
Semaphore semaphore=new Semaphore(3);
//这个时候有六个人想使用,但只能某人走了才能用(类似排队)
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
try {
semaphore.acquire();//抢到车位,先抢先用
System.out.println(Thread.currentThread().getName()+"抢到了车位");
//等待其他人离开
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"===>离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();//释放车位,离开了让出一个新车位
}
},String.valueOf(i)).start();
}
}
}
9、读写锁
ReadWriteLock案例、
理解!!!!
package cn.linqin.test.suo;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/*
独占锁(写锁):一次只能被一个线程占有
共享锁(读锁):可以被多个线程同时占有
ReadWriteLock 读写锁
读-读 可以共存
读-写 不可共存
写-写 不可共存
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCacher myCacher=new MyCacher();
//5个去写的对象
for (int i = 1; i <5 ; i++) {
final int temp=i;//调用不了i,加上fina给实现类用
new Thread(()->{
myCacher.setMap(temp+"",temp+"a");
}).start();
}
//5个去读的对象
for (int i = 1; i <5 ; i++) {
final int temp=i;//调用不了i,加上fina给实现类用
new Thread(()->{
myCacher.getMap(temp+"");
}).start();
}
}
}
//虚拟缓存
class MyCacher{
private Map<String,Object> map=new HashMap<>();
//创建读写锁
private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
//写方法
public void setMap(String key,String value) {
try{
//调用读写锁中的写锁
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName()+"正在写入");
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"->写入OK");
}finally {
//解锁
readWriteLock.writeLock().unlock();
}
}
//读方法
public void getMap(String key) {
try{
//开启写锁
readWriteLock.readLock().lock();
System.out.println(Thread.currentThread().getName()+"正在读取");
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"->读取OK");
}finally {
//解锁
readWriteLock.readLock().unlock();
}
}
}
10、阻塞队列
BlockingQueue(阻塞队列)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JFsTpzdp-1618297608442)(C:\Users\86155\AppData\Roaming\Typora\typora-user-images\1618058084727.png)]
我们可以看到阻塞队列下面有链表和数组的阻塞队列(和Set、List差不多)
根据FIFO(先进先出)原则
不得不阻塞
写入:如果队列满了,就必须阻塞等待
取:如果队列是空的,必须阻塞等待生产
什么时候使用阻塞队列?
1、多线程并发处理
2、线程池
四组API
方式 | 抛出异常 | 不抛出异常,有返回值 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add() | offer() | put() | offer(“a”,2, TimeUnit.SECONDS) |
移除 | remove() | poll() | take() | poll(2, TimeUnit.SECONDS); |
判断队列首部 | element() | peek() | - | - |
代码演示
package cn.linqin.test.suo;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
test4();
}
/*
* 抛出异常
* */
public static void test(){
//队列大小为3
BlockingQueue blockingQueue=new ArrayBlockingQueue(3);
//做操作会返回布尔值 要么false 要么true
System.out.println( blockingQueue.add("a"));
System.out.println( blockingQueue.add("b"));
System.out.println( blockingQueue.add("c"));
//判断队列首部
System.out.println("队列首部->"+blockingQueue.element());//a
//如果队列满了则会报错 抛出异常 IllegalStateException: Queue full
//System.out.println( blockingQueue.add("a"));
//根据FIFO先进先出原则,我们进行remove时也要遵守
System.out.println(blockingQueue.remove());//a
System.out.println(blockingQueue.remove());//b
System.out.println(blockingQueue.remove());//c
//如果队列为空再去删除则会抛出异常 NoSuchElementException
System.out.println(blockingQueue.remove());
}
/*
* 不抛出异常,有返回值
* */
public static void test2(){
//队列大小为3
BlockingQueue blockingQueue=new ArrayBlockingQueue(3);
//做操作会返回布尔值 要么false 要么true
System.out.println( blockingQueue.offer("a"));
System.out.println( blockingQueue.offer("b"));
System.out.println( blockingQueue.offer("c"));
//判断队列首个,不抛出异常
System.out.println(blockingQueue.peek());//a
//如果队列满了则会报错 返回false
//根据FIFO先进先出原则,我们进行remove时也要遵守
System.out.println("队列首部->"+blockingQueue.poll());//a
System.out.println(blockingQueue.poll());//b
System.out.println(blockingQueue.poll());//c
//如果队列为空再去删除则会告诉你为null
System.out.println(blockingQueue.poll());
}
/*
* 等待阻塞(一直阻塞)
* */
public static void test3() throws InterruptedException {
//队列大小为3
BlockingQueue blockingQueue=new ArrayBlockingQueue(3);
//存满队列
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
//如果队列满了,程序不会结束,而是会一直阻塞
//blockingQueue.put("d");
//刪除
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
//如果去删除后面的,他也会进入阻塞等待,而不会报错
//System.out.println(blockingQueue.take());
}
/*
* 超时等待
* */
public static void test4() throws InterruptedException {
//队列大小为3
BlockingQueue blockingQueue=new ArrayBlockingQueue(3);
//做操作会返回布尔值 要么false 要么true
System.out.println( blockingQueue.offer("a",2, TimeUnit.SECONDS));
blockingQueue.poll();
//如果删除,如果暂时删不了就等待2秒,如果2秒过后还是删不了则直接退出程序
blockingQueue.poll(2, TimeUnit.SECONDS);
}
}
SynchronousQueue(同步队列)
package cn.linqin.test.suo;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
//同步队列
//和其他BlockingQueue不同,SynchronousQueue不存储元素
//put进去一个值就必须take取出来,否则就不可以去put值
public class SynchronousQueueDemo {
public static void main(String[] args) {
//创建同步队列
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
//创建两个线程 一个去存 一个去拿
//存
new Thread(()->{
try {
blockingQueue.put("a");
System.out.println("存入put->a");
blockingQueue.put("b");
System.out.println("存入put->b");
blockingQueue.put("c");
System.out.println("存入put->c");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"霖磬").start();
//取
new Thread(()->{
try {
blockingQueue.take();
System.out.println("取出put->a");
blockingQueue.take();
System.out.println("取出put->b");
blockingQueue.take();
System.out.println("取出put->c");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"k").start();
}
}
11、线程池(重点!)
线程池:三大方法、7大参数、4种拒绝策略
池化技术
程序运行的本质:占用系统的资源! 优化资源的使用==》线程池
线程池、连接池、对象池…//他们的创建和关闭都是非常耗资源的
池化技术:实现准备好一些资源,你要用来我这拿,用完再还给我!
线程池的好处
- 降低资源的消耗
- 提高响应的速度
- 方便管理
总结来说就是线程复用、可以控制最大并发数、管理线程
三大方法七大参数
线程池:三大方法
注意->:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutors创建,因为有前者有弊端(允许请求队列长度为21亿?!)
package cn.linqin.test.pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//Executor 工具类、三大方法
public class PoolDemo {
public static void main(String[] args) {
//ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程池
//ExecutorService threadPool = Executors.newFixedThreadPool(5);//创建一个拥有固定大小的线程
ExecutorService threadPool = Executors.newCachedThreadPool();//可伸缩的,遇强则强遇弱则弱的线程池
//使用了线程池就需要使用线程池的方式创建线程
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "---->ok");
});
}
}catch(Exception e){
e.printStackTrace();
}finally {
//用完之后关闭线程池
threadPool.shutdownNow();
}
}
}
七大参数
我们点开源码来看一下三大发放的底层
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,//约等于21亿
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
//发现他们都是new了一个ThreadPoolExecutor对象,我们点开他来看一下
//其有七大参数
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
int maximumPoolSize,//最大的线程池大小
long keepAliveTime,//超时了,没有人调用就释放
TimeUnit unit,//释放时间单位
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,//线程工厂,用来创造线程
RejectedExecutionHandler handler) {//拒绝策略
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ldV9zS5q-1618297608443)(C:\Users\86155\AppData\Roaming\Typora\typora-user-images\1618063670324.png)]
银行开了两个柜台,有人了则其他人在等候区等候,如果来了很多人(候客区满了),其他柜台也打开,这个时候外面又来人了,那么就阻挡在门外!
手动创建线程池
package cn.linqin.test.pool;
import java.util.concurrent.*;
//Executor 工具类、三大方法
public class PoolDemo {
public static void main(String[] args) {
//参数顺序:核心线程数、最大线程数、超时时间、时间单位。阻塞队列、线程工厂、拒绝策略
/*
四大拒绝策略
一、new ThreadPoolExecutor.AbortPolicy(); 银行满了,还有人进来,不处理这个人,抛出异常
二、new ThreadPoolExecutor.CallerRunsPolicy(); 哪来的去哪里!银行不帮你办,让你回去让公司帮你办卡
三、new ThreadPoolExecutor.DiscardPolicy(); 队列满了,丢掉任务,不会抛出异常
四、new ThreadPoolExecutor.DiscardOldestPolicy(); 队列满了,但他会尝试去和最先执行的去竞争,也不会抛出异常
*/
ExecutorService threadPool = new ThreadPoolExecutor(2,5,
3, TimeUnit.SECONDS,new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardOldestPolicy());
//使用了线程池就需要使用线程池的方式创建线程
try {
for (int i = 1; i <= 9; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "---->ok");
});
}
}catch(Exception e){
e.printStackTrace();
}finally {
//用完之后关闭线程池
threadPool.shutdownNow();
}
}
}
四大拒绝策略
/*
四大拒绝策略
一、new ThreadPoolExecutor.AbortPolicy(); 银行满了,还有人进来,不处理这个人,抛出异常
二、new ThreadPoolExecutor.CallerRunsPolicy(); 哪来的去哪里!银行不帮你办,让你回去让公司帮你办卡
三、new ThreadPoolExecutor.DiscardPolicy(); 队列满了,丢掉任务,不会抛出异常
四、new ThreadPoolExecutor.DiscardOldestPolicy(); 队列满了,但他会尝试去和最先执行的去竞争,也不会抛出异常
*/
扩展
CPU密集型,IO密集型
最大线程到底怎么定义?
1、CPU密集型,几核就是几,可以保持CPU效率最高
2、IO密集型,判断程序中十分耗IO的线程只要>就可以了,一般可以设置为两倍
Runtime.getRuntime().availableProcessors();//获取CPU核数
12、四大函数式接口(必须掌握)
新时代的程序员:lambda表达式、链式编程、函数式接口、Stream流式计算
函数式接口:只有一个方法的接口
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
//有很多FunctionalInterface
//简化编程模型,在新版本中框架底层大量应用
//比如foreach(消费者类的函数式接口)
1、function 函数式接口 使用
package cn.linqin.test.pool;
import java.util.function.Function;
//四大函数型接口使用
//只要是函数是接口就可以用lambda表达式简化
public class FunctionDemo {
public static void main(String[] args) {
//Function函数型接口,有一个输入参数,输入什么返回什么值
// Function function=new Function<String,String>() {
// @Override
// public String apply(String o) {
// return o;
// }
// };
// System.out.println(function.apply("13"));//输出13
//使用lambda表达式简化
Function<String,String> function=(str)->{return str;};
System.out.println(function.apply("123"));//输出123
}
}
2、断定型接口
有一个输入参数,返回一个布尔值
package cn.linqin.test.pool;
import java.util.function.Function;
import java.util.function.Predicate;
//四大函数型接口使用
//只要是函数是接口就可以用lambda表达式简化
/*
有一个输入参数,返回一个布尔值
*/
public class PredicateDemo {
public static void main(String[] args) {
Predicate predicate=new Predicate<String>() {
@Override
public boolean test(String str) {
return str.isEmpty();//判断是否为空
}
};
System.out.println(predicate.test(123));
//lambda表达式简化
Predicate<String> predicate1=(str)->{return str.isEmpty();};
}
}
3、消费型接口
Consumer 消费型接口
只有输出没有返回值
package cn.linqin.test.pool;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
//四大函数型接口使用
//只要是函数是接口就可以用lambda表达式简化
//Consumer 只有输出没有返回值!
public class ConsumerDemo {
public static void main(String[] args) {
Consumer consumer =new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
consumer.accept("ASDAD");
//lambda表达式简化
Consumer<String> consumer1=(str)->{
System.out.println(str);
};
consumer1.accept("ASDAD");
}
}
4、供给型接口
Supplier
没有参数,只有返回值
package cn.linqin.test.pool;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
//四大函数型接口使用
//只要是函数是接口就可以用lambda表达式简化
//Supplier 没有参数只有返回值
public class SupplierDemo {
public static void main(String[] args) {
Supplier supplier=new Supplier<Integer>() {
@Override
public Integer get() {
System.out.println("get");
return 123;
}
};
//lambda表达式简化
Supplier<Integer> supplier1=()->{return 123;};
}
}
13、Stream流式计算
什么是流式计算?
大数据:存储+计算
集合、Mysql本质就是存储东西的
package cn.linqin.test.pool;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Arrays;
import java.util.List;
/*
需求:现在有五个用户,需要筛选条件,两行代码搞定!
1、ID必须为偶数
2、年龄必须大于23
3、用户名转换为小写
4、用户名倒着写
5、只输出一个用户
*/
public class Test {
public static void main(String[] args) {
//使用lambda表达式、链式编程、函数式接口、Stream流式计算
User u1=new User("A",15,1);
User u2=new User("B",25,2);
User u3=new User("C",35,3);
User u4=new User("D",25,4);
User u5=new User("ADA",15,5);
//讲五个对象转换为集合
List<User> users = Arrays.asList(u1, u2, u3, u4, u5);
//转换为Stream流进行计算操作
users.stream()
//里面是个Predicate函数型接口,第一个条件是Id必须为偶数,
//此函数型接口根据传入的值进行判断,返回布尔值,在Stream流中true通过false阻挡
.filter((user)->{return user.getId()%2==0;})
//条件年龄大于23岁
.filter((user)->{return user.getAge()>23;})
//map里面是个Function函数型接口,输入什么就返回什么!
//将用户名转换为小写打印,toLowerCase()转换为小写,toUpperCase()转换为大写
.map((user)->user.getName().toLowerCase())
//默认是正序,里面有Consumer函数型接口可以排序
//compareTo()和前一个比较,判断是正序还是倒序
.sorted((uu1,uu2)->{return uu1.compareTo(uu2);})
//只显示一条数据
.limit(1)
//forEach循环遍历出所有User
.forEach(System.out::println);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class User{
private String name;
private int age;
private int id;
}
14、FockJoin
什么是FockJoin?
FockJoin在JDK1.7中出现,他的作用:并发执行任务!提高效率,大量数据!
大数据中:MapReduce 核心思想->把大任务拆分为小任务!
FockJoin就是将任务拆分,下面去执行,最后将执行的结果合并!
FockJoin的特点:工作窃取
里面维护了一个双端队列
如何使用FockJoin?
1、通过ForkJoinPool来执行
2、计算任务 execute(ForkJoinTask task)
- 3、计算类要去继承ForkJoinTask;
package com.marchsoft.forkjoin;
import java.util.concurrent.RecursiveTask;
/**
* Description:
*
* @author 霖磬
* Date: 2020/8/13 8:33
**/
public class ForkJoinDemo extends RecursiveTask<Long> {
private long star;
private long end;
/** 临界值 */
private long temp = 1000000L;
public ForkJoinDemo(long star, long end) {
this.star = star;
this.end = end;
}
/**
* 计算方法
* @return
*/
@Override
protected Long compute() {
if ((end - star) < temp) {
Long sum = 0L;
for (Long i = star; i < end; i++) {
sum += i;
}
return sum;
}else {
// 使用ForkJoin 分而治之 计算
//1 . 计算平均值
long middle = (star + end) / 2;
ForkJoinDemo forkJoinDemo1 = new ForkJoinDemo(star, middle);
// 拆分任务,把线程压入线程队列
forkJoinDemo1.fork();
ForkJoinDemo forkJoinDemo2 = new ForkJoinDemo(middle, end);
forkJoinDemo2.fork();
long taskSum = forkJoinDemo1.join() + forkJoinDemo2.join();
return taskSum;
}
}
}
三种方法效率测试
package cn.linqin.test.pool;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;
//求和计算的任务!
public class FockJoinDemo {
private static final long SUM = 20_0000_0000;
public static void main(String[] args) throws ExecutionException, InterruptedException {
test1();
test2();
test3();
}
/**
* 使用普通方法
*/
public static void test1() {
System.out.println("-------普通方法----------");
long star = System.currentTimeMillis();
long sum = 0L;
for (long i = 1; i < SUM ; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println(sum);
System.out.println("时间:" + (end - star));
System.out.println("----------------------");
}
/**
* 使用ForkJoin 方法
*/
public static void test2() throws ExecutionException, InterruptedException {
System.out.println("-------ForkJoin方法----------");
long star = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinDemo(0L, SUM);
ForkJoinTask<Long> submit = forkJoinPool.submit(task);
Long along = submit.get();
System.out.println(along);
long end = System.currentTimeMillis();
System.out.println("时间:" + (end - star));
System.out.println("-----------");
}
/**
* 使用 Stream 流计算
*/
public static void test3() {
System.out.println("-------Stream流并行----------");
long star = System.currentTimeMillis();
long sum = LongStream.range(0L, 20_0000_0000L).parallel().reduce(0, Long::sum);
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println("时间:" + (end - star));
System.out.println("-----------");
}
}
Stream并行流>FockJoin>循环累加
有时候FockJoin比循环累加速度慢,这个需要看机器的效率以及数据大小
运行结果:
-------普通方法----------
1999999999000000000
时间:523
----------------------
-------ForkJoin方法----------
1999999999000000000
时间:6584
-----------
-------Stream流并行----------
1999999999000000000
时间:195
.parallel().reduce(0, Long::sum)使用一个并行流去计算整个计算,提高效率。**
15、异步回调
Future 设计的初衷:对将来的某个事件结果进行建模!
相当于前段发送ajax请求到后端请求数据
我们平常都用CompletableFuture
package cn.linqin.test.pool;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
//异步请求测试练习
public class CompletableFutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//发起一个有返回值的异步请求
CompletableFuture<String> completableFuture= CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName());//线程名
//int i=2/0;
return "ASD";
});
// System.out.println(completableFuture.get());//输出ASD 返回值
//我们可以根据异步请求的失败或者成功做出对应操作
completableFuture.whenComplete((t,u)->{
//正常的返回结果
System.out.println(t);
//错误则打印抛出异常的错误信息
System.out.println(u);
}).exceptionally((e)->{
//如果失败了,打印抛出异常的错误信息
e.printStackTrace();
System.out.println("请求失败");
return "404";
});
}
}
16. JMM
1、对Volatile 的理解
Volatile 是 Java 虚拟机提供 轻量级的同步机制
1、保证可见性
2、不保证原子性
3、禁止指令重排
2、JMM是什么?
JMM是java内存模型,不存在的东西,一个概念,一个约定!
关于JMM的一些约定
1、线程解锁前,必须将共享变量刷回(归还)
2、线程加锁前,必须读取内存中的最新值到工作内存
3、加锁和解锁是同一把锁
线程中分为:主内存、工作内存
他拥有8种操作
8种操作:
-
Read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
-
load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;
-
Use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
-
assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;
-
store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;
-
write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;
-
lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态;
-
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
当我们使用的时候需要走8种操作,线程想要使用主线程的变量,需要先读取再加载到线程的工作内存,再从工作内存中使用,并重新赋值返还给主存
JMM对这8种操作给了相应的规定:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作 - 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
17、Volatile
Volatile 是 Java 虚拟机提供 轻量级的同步机制
1、保证可见性
2、不保证原子性
3、禁止指令重排
1、保持可见性
public class JMMDemo01 {
// 如果不加volatile 程序会死循环
// 加了volatile是可以保证可见性的
private volatile static Integer number = 0;
public static void main(String[] args) {
//main线程
//子线程1
new Thread(()->{
while (number==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//子线程2
new Thread(()->{
while (number==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
number=1;
System.out.println(number);
}
}
2、不保证原子性
/**
* 不保证原子性
* number <=2w
*
*/
public class VDemo02 {
private static volatile int number = 0;
public static void add(){
number++;
//++ 不是一个原子性操作,是两个~3个操作
//
}
public static void main(String[] args) {
//理论上number === 20000
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){
//main gc 默认的线程
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+",num="+number);
}
}
如果不加lock和synchronized ,怎么样保证原子性?
JUC给我们提供了原子类,而这些类都是和操作形态挂钩的,是在内存中修改值!
public class VDemo02 {
private static volatile AtomicInteger number = new AtomicInteger();
public static void add(){
// number++;
number.incrementAndGet(); //底层是CAS保证的原子性
}
public static void main(String[] args) {
//理论上number === 20000
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){
//main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+",num="+number);
}
}
Unsafe类是一个很特殊的存在;
3、禁止指令重排
什么是指令重排?
我们写的程序,计算机并不是按照我们自己写的那样去执行的
源代码–>编译器优化重排–>指令并行也可能会重排–>内存系统也会重排–>执行
处理器在进行指令重排的时候,会考虑数据之间的依赖性!
int x=1; //1
int y=2; //2
x=x+5; //3
y=x*x; //4
//我们期望的执行顺序是 1_2_3_4 可能执行的顺序会变成2134 1324
//可不可能是 4123? 不可能的
1234567
volatile可以避免指令重排:
volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序。
内存屏障:CPU指令。作用:
1、保证特定的操作的执行顺序;
2、可以保证某些变量的内存可见性(利用这些特性,就可以保证volatile实现的可见性)
4、总结
volatile可以保证可见性;
不能保证原子性
由于内存屏障,可以保证避免指令重排的现象产生
面试官:那么你知道在哪里用这个内存屏障用得最多呢?单例模式
18、单例模式
单例模式就是一个类给外部提供一个唯一的实例
通常有 DCL懒汉式单例模式、饿汉式单例模式
饿汉式
/**
* 饿汉式单例
*/
public class Hungry {
/**
* 可能会浪费空间
*/
private byte[] data1=new byte[1024*1024];
private byte[] data2=new byte[1024*1024];
private byte[] data3=new byte[1024*1024];
private byte[] data4=new byte[1024*1024];
private Hungry(){
}
private final static Hungry hungry = new Hungry();
public static Hungry getInstance(){
return hungry;
}
}
它会在一开始就创建好,但你可能还没使用,所以会造成空间浪费的现象
懒汉式
package cn.linqin.test.pool;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
//懒汉式单例模式
public class LazyMan {
private static boolean key = false;
private LazyMan(){
synchronized (LazyMan.class){
if (key==false){
key=true;
}
else{
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
System.out.println(Thread.currentThread().getName()+" ok");
}
private volatile static LazyMan lazyMan;
//双重检测锁模式 简称DCL懒汉式
public static LazyMan getInstance(){
//需要加锁
if(lazyMan==null){
synchronized (LazyMan.class){
if(lazyMan==null){
lazyMan=new LazyMan();
/**
* 1、分配内存空间
* 2、执行构造方法,初始化对象
* 3、把这个对象指向这个空间
*
* 就有可能出现指令重排问题
* 比如执行的顺序是1 3 2 等
* 我们就可以添加volatile保证指令重排问题
*/
}
}
}
return lazyMan;
}
//单线程下 是ok的
//但是如果是并发的
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException, InvocationTargetException {
//Java中有反射
// LazyMan instance = LazyMan.getInstance();
Field key = LazyMan.class.getDeclaredField("key");
key.setAccessible(true);
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true); //无视了私有的构造器
LazyMan lazyMan1 = declaredConstructor.newInstance();
key.set(lazyMan1,false);
LazyMan instance = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(lazyMan1);
System.out.println(instance == lazyMan1);
}
}
19、深入理解CAS
public class casDemo {
//CAS : compareAndSet 比较并交换
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
//boolean compareAndSet(int expect, int update)
//期望值、更新值
//如果实际值 和 我的期望值相同,那么就更新
//如果实际值 和 我的期望值不同,那么就不更新
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
//因为期望值是2020 实际值却变成了2021 所以会修改失败
//CAS 是CPU的并发原语
atomicInteger.getAndIncrement(); //++操作
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
}
}
Unsafe 类
总结
CAS:比较当前工作内存中的值 和 主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环,使用的是自旋锁。
缺点:
- 循环会耗时;
- 一次性只能保证一个共享变量的原子性;
- 它会存在ABA问题
20、原子引用
解决ABA问题,使用乐观锁
带版本号的 原子操作!
Integer 使用了对象缓存机制,默认范围是-128~127,推荐使用静态工厂方法valueOf获取对象实例,而不是new,因为valueOf使用缓存,而new一定会创建新的对象分配新的内存空间。
package cn.linqin.test.pool;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* Description:
*
* @author 霖磬
* Date: 2020/8/12 22:07
**/
public class casDemo {
/**AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
* 正常在业务操作,这里面比较的都是一个个对象
*/
static AtomicStampedReference<Integer> atomicStampedReference = new
AtomicStampedReference<>(1, 1);
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("a1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改操作时,版本号更新 + 1
atomicStampedReference.compareAndSet(1, 2,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1);
System.out.println("a2=>" + atomicStampedReference.getStamp());
// 重新把值改回去, 版本号更新 + 1
System.out.println(atomicStampedReference.compareAndSet(2, 1,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1));
System.out.println("a3=>" + atomicStampedReference.getStamp());
}, "a").start();
// 乐观锁的原理相同!
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("b1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(1, 3,
stamp, stamp + 1));
System.out.println("b2=>" + atomicStampedReference.getStamp());
}, "b").start();
}
}
21、各种锁的理解
1)公平锁,非公平锁
-
公平锁:非常公平,不能插队,必须先来后到
/** * Creates an instance of {@code ReentrantLock}. * This is equivalent to using {@code ReentrantLock(false)}. */ public ReentrantLock() { sync = new NonfairSync(); }
-
非公平锁:非常不公平,允许插队,可以改变顺序
/** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
2) 可重入锁
1、synchronized锁(可重入锁)
public class Demo01 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sms();
},"A").start();
new Thread(()->{
phone.sms();
},"B").start();
}
}
class Phone{
public synchronized void sms(){
System.out.println(Thread.currentThread().getName()+"=> sms");
call();//这里也有一把锁
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName()+"=> call");
}
}
2、Lock锁
//lock
public class Demo02 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
new Thread(()->{
phone.sms();
},"A").start();
new Thread(()->{
phone.sms();
},"B").start();
}
}
class Phone2{
Lock lock=new ReentrantLock();
public void sms(){
lock.lock(); //细节:这个是两把锁,两个钥匙
//lock锁必须配对,否则就会死锁在里面
try {
System.out.println(Thread.currentThread().getName()+"=> sms");
call();//这里也有一把锁
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void call(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "=> call");
}catch (Exception e){
e.printStackTrace();
}
finally {
lock.unlock();
}
}
}
- lock锁必须配对,相当于lock和 unlock 必须数量相同;
- 在外面加的锁,也可以在里面解锁;在里面加的锁,在外面也可以解锁;
3)自旋锁
1、自我设计自旋锁
public class SpinlockDemo {
// 默认
// int 0
//thread null
AtomicReference<Thread> atomicReference=new AtomicReference<>();
//加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+"===> mylock");
//自旋锁
while (!atomicReference.compareAndSet(null,thread)){
System.out.println(Thread.currentThread().getName()+" ==> 自旋中~");
}
}
//解锁
public void myUnlock(){
Thread thread=Thread.currentThread();
System.out.println(thread.getName()+"===> myUnlock");
atomicReference.compareAndSet(thread,null);
}
}
使用:
public class TestSpinLock {
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
reentrantLock.unlock();
//使用CAS实现自旋锁
SpinlockDemo spinlockDemo=new SpinlockDemo();
new Thread(()->{
spinlockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinlockDemo.myUnlock();
}
},"t1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
spinlockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinlockDemo.myUnlock();
}
},"t2").start();
}
}
运行结果:
t2进程必须等待t1进程Unlock后,才能Unlock,在这之前进行自旋等待。。。。
4)死锁
package com.ogj.lock;
import java.util.concurrent.TimeUnit;
public class DeadLock {
public static void main(String[] args) {
String lockA= "lockA";
String lockB= "lockB";
new Thread(new MyThread(lockA,lockB),"t1").start();
new Thread(new MyThread(lockB,lockA),"t2").start();
}
}
class MyThread implements Runnable{
private String lockA;
private String lockB;
public MyThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+" lock"+lockA+"===>get"+lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+" lock"+lockB+"===>get"+lockA);
}
}
}
}
如何解开死锁
1、使用jps定位进程号,jdk的bin目录下: 有一个jps
命令:jps -l
2、使用jstack
进程进程号 找到死锁信息