JavaEE多线程进阶

多线程进阶

常见的锁策略

乐观锁和悲观锁

乐观锁 :认为多个线程访问同一个共享变量发生冲突概率小 ,并且并不是真正的加锁 ,而是直接访问数据,访问时候要识别当前的数据是否出现冲突
悲观锁 :认为多个线程访问同一共享变量发生冲突概率大 ,每次访问变量会真的加锁

重量级锁和轻量级锁

重量级锁 :加锁操作开销较大
轻量级锁 :加锁操作开销较小

并且重量级锁较依赖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++操作,并且是线程安全的

java 复制代码
//创建一个原子类对象,并且初始化为0
private static AtomicInteger count = new AtomicInteger(0);
java 复制代码
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接收不了

java 复制代码
//要重写call方法
Callable<Integer> callable = new Callable<Integer>() {
           @Override
           public Integer call() throws Exception {
               int sum = 0;
               for (int i = 0; i <= 100; i++) {
                   sum+=i;
               }
               return sum;
           }
       };
java 复制代码
public class demo30 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
       Callable<Integer> callable = new Callable<Integer>() {
           @Override
           public Integer call() throws Exception {
               int sum = 0;
               for (int i = 0; i <= 100; i++) {
                   sum+=i;
               }
               return sum;
           }
       };

       //Thread无法接收callable对象
        //使用FutureTask
        FutureTask<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

java 复制代码
 ReentrantLock locker = new ReentrantLock();
 locker.lock();//加锁
 locker.unlock();//解锁
java 复制代码
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区别

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

    2.并且这个支持公平锁,这里设置为true就是公平锁,此处就会按照时间来执行线程
    3.等待通知的不同
    synchronized搭配 wait 和notify,并且这里的notify只可以随机唤醒一个
    ReentrantLock搭配 Condition 类,可以指定唤醒

RenntrantLock是使用lock加锁,unlock解锁,要手动解锁,但是这样容易出现忘记解锁问题

Semaphore(信号量)

信号量本表示的是可使用资源个数,本质是一个计数器

java 复制代码
Semaphore semaphore = new Semaphore(3);//只有3个可用资源
semaphore.acquire();//获取资源 P操作
semaphore.release();//释放资源 V操作
java 复制代码
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个资源,因此第四次获取就会一直等资源释放

java 复制代码
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

java 复制代码
CountDownLatch latch = new CountDownLatch(8);//创建一个对象
java 复制代码
latch.countDown();//数量-1
java 复制代码
 latch.await();//等待结束所有,也就是当计数为0
java 复制代码
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维护新数组和老数组

相关推荐
SimonKing8 小时前
聊聊Spring里那个不打扰Controller就能统一改响应的“神器”
java·后端·程序员
鹓于8 小时前
Excel图片批量插入与文件瘦身
java·服务器·数据库
鬼火儿8 小时前
Redis Desktop Manager(Redis可视化工具)安装
java·后端
晨非辰8 小时前
《数据结构风云》:二叉树遍历的底层思维>递归与迭代的双重视角
数据结构·c++·人工智能·算法·链表·面试
凛_Lin~~8 小时前
安卓接入Twitter三方登录
android·java·twitter
ᐇ9598 小时前
Java核心概念深度解析:从包装类到泛型的全面指南
java·开发语言
cngm1108 小时前
若依分离版前端部署在tomcat刷新404的问题解决方法
java·前端·tomcat
华如锦8 小时前
使用SSE进行实时消息推送!替换WebSocket,轻量好用~
java·开发语言·网络·spring boot·后端·websocket·网络协议
Muroidea9 小时前
Kafka4.1.0 队列模式尝鲜
java·kafka