java多线程常见锁策略CAS机制(2)

简介: java多线程常见锁策略CAS机制(2)

synchronized原理

我们总结上面的锁策略,就可以总结出synchronized的一些特性(JDK1.8版本)


自适应锁,根据锁竞争激烈程度,开始是乐观锁竞争加剧就变成悲观锁

开始是轻量级锁,如果锁冲突加剧,那就变成重量级锁

实现轻量级锁是采用自旋锁策略,重量级锁采用挂起等待锁策略

是普通的互斥锁

可重入锁

加锁过程

synchronized是如何做到自适应过程的呢?


JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。

image.png


我们用生活中的例子便于理解锁的变化!


好比一个男生喜欢上了一个女生(漂亮又对男生超好)

但是这个男生比较渣,不想和她纠缠,如果确认了关系,他就要要放弃一片森林了,但是他又想谈恋爱! 所以他选择和那个女生搞暧昧不确立关系(偏向锁)就是说避免了确立关系和分手的纠缠(避免了加锁解锁的开销),过了一段时间有一个其他的男生追这个女生,此时如果这个男生再不确立关系,就有可能失去女生,所以他马上和女生确立关系(自旋锁)…


synchronized锁的优化操作

锁膨胀/锁升级

体现了synchronized锁的自适应能力,根据锁的竞争激励程度自动升级锁


锁粗化/锁细化

这里的粗细指的是加锁的粒度,换句话说就是加锁代码的范围,范围越大,加锁的粒度越大,锁粗化!

编译器会根据你写的代码进行优化策略,在不改变代码逻辑的情况下,使代码效率更高!

Thread t1 = new Thread(()->{
           Object locker = new Object();
            int num = 0;
           synchronized (locker){ //针对一整个循环加锁,粒度大
               for (int i = 0; i < 10; i++) {
                   num++;
               }
           }
        });
        Thread t2 = new Thread(()->{
            Object locker = new Object();
            int num = 0;
                for (int i = 0; i < 10; i++) {
                    synchronized (locker) {//每次循环加锁,粒度小
                        num++;
                    }
            }
        });

image.png


锁消除

顾名思义,锁消除就是将锁给去掉!

有时候加锁操作并没有起到作用,编译器就会将该锁去掉,提供代码效率!

比如我们知道Vector和Stringbuffer类的关键方法都进行了加锁操作,如果在单线程代码使用这两个类,编译器就会对代码进行优化,进行锁消除!


java中的JUC

啥是JUC?

java.util.concurrent这个包简化为JUC

这个包下有很多java多线程并发编程的接口和类!

我们来了解一下其他的一些重要的类和接口!

image.png


image.png

Callable


Callable是一个接口,创建线程的一中方法

我们就疑惑了,不是已经有Runnable了嘛,Callable实现的对象可以返回结果,而Runnable取不方便!

例如我们要实现1到100的相加,Runnable就会比较麻烦,而我们通过Callable就比较方便!


//Runnable方式实现
public class Demo2 {
    static class Result {//辅助类保存结果
        public int sum = 0;
        public Object lock = new Object();
    }
    public static void main(String[] args) throws InterruptedException {
        Result result = new Result();
        Thread t = new Thread() {
            @Override
            public void run() {
                int sum = 0;
                for (int i = 1; i <= 1000; i++) {
                    sum += i;
                }
                synchronized (result.lock) {
                    result.sum = sum;
                    result.lock.notify();
                }
            }
        };
        t.start();
        synchronized (result.lock) {
            while (result.sum == 0) {
                result.lock.wait();
            }
            System.out.println(result.sum);
        }
    }
}

显然这个代码有点麻烦,还需要借助一个辅助类才能实现!


//Callable方式实现
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo3 {
    public static void main(String[] args) {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int result = 0;
                for (int i = 1; i <= 100; i++) {
                    result+=i;
                }
                return result;
            }
        }; //callable描述了这个任务!(你去点餐)
        //辅助的类将callable任务标记,便于执行线程!(给了小票,区分谁的食物)
        FutureTask<Integer> task = new FutureTask<>(callable);
        Thread t1 = new Thread(task);//执行线程任务!(给你做好了)
        try {
            int result = task.get(); //获取到结果(凭小票取餐)
            System.out.println("计算结果:"+result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

我们通过Callable接口实现时需要注意一些细节,我们要通过FutureTask对象将Callable传入标记,便于后面拿值(task.get())


ReentrantLock可重入锁


我们知道synchronized也是可重入锁!这个有什么过人之处呢?


public class Demo4 {
    public static void main(String[] args) {
        //一个参数的构造方法 true 为公平锁 false 非公平锁 默认非公平锁
        ReentrantLock lock = new ReentrantLock(true);
        //加锁
        lock.lock();
        //解锁
        lock.unlock();
    }
}

我们可以看到ReentrantLock将加锁和解锁分开操作!

其实分开的做法并不好,有时候可能会忘记解锁(lock,unlock())就会使线程造成阻塞!


和synchronized锁的区别

synchronized是一个关键字(背后逻辑是JVM,由C++实现),Callable是一个接口(背后逻辑由java代码编写)

synchronized不需要手动释放锁操作,出了代码块,锁自动释放,而ReentrantLock必须手动释放锁!

synchronized是一个非公平锁,ReentrantLock提供了公平锁和非公平两个版本,供选择!

synchronized锁竞争失败就会进行阻塞等待,而ReentrantLock除了阻塞等待外还提供了trylock失败直接返回

基于synchronized的等待机制是wait和notify功能相对有限,而ReentrantLock等待机制提供了Condition类功能强大

其实在日常开发synchronized功能就够用了!


semaphore 信号量


一个更广义的锁!

锁是信号量的一种为"二元信号量"


举个生活中的例子:

你去停车场停车:

当你到门口你可以看到有个牌子写了当前还剩多少车位!

进去一辆车 车位就减一

出来一辆车 车位就加一

如果当前车位为0 就阻塞等待!


这个标识多少车位牌子(描述可用资源的个数)就是信号量

每次申请一个资源 计数器就-1(称为p操作)

每次释放一个资源 计数器就+1(称为v操作)

资源为0阻塞等待!

锁是特殊的"二元信号量" 只有0或1标识资源个数!

信号量就是把锁推广到一般情况,可用资源更多的时候,如何处理


一般很少用到!


import java.util.concurrent.Semaphore;
public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        //创建一个可用资源个数为3的信号量
        Semaphore semaphore = new Semaphore(3);
        //p操作 申请信号量
        semaphore.acquire();
        System.out.println("申请成功");
        semaphore.acquire();
        System.out.println("申请成功");
        semaphore.acquire();
        System.out.println("申请成功");
        semaphore.acquire();
        System.out.println("申请成功");
        //v操作 释放信号量
        semaphore.release();
        System.out.println("释放成功");
    }
}

image.png

我们只有3个资源,而却申请了4次,那么第4次就会进行阻塞等待!


CountDownLatch


重点线,这里怎么解释呢!

就好比一场跑步比赛,裁判要等到所有人越过终点线,比赛才算结束!


这样的场景在开放中也很常见!


例如多线程下载!

比如迅雷等 都是将一个文件分给多个线程同时下载,当所有线程下载完毕,才算下载完成!


import java.util.concurrent.CountDownLatch;
public class Demo6 {
    public static void main(String[] args) throws InterruptedException {
        //5个线程!
        CountDownLatch latch = new CountDownLatch(5);
        for (int i = 0; i <5; i++) {
            Thread t1 = new Thread(()->{
                try {
                    Thread.sleep(300);
                    //获取该线程名
                    System.out.println(Thread.currentThread().getName());
                    latch.countDown();//该任务执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t1.start();
        }
        latch.await(); //等待所有线程执行结束
        System.out.println("比赛结束");
    }
}

image.png

CopyOnWriteArrayList写时拷贝


当我们在多线程环境下使用ArrayList时

读操作并不会导致线程不安全!

但是写操作就可能出现线程不安全问题!


当多个线程进行多写操作时,显然就线程不安全,会有脏读问题!

我们可以自己加锁操作!

我们java提供了多线程环境下使用的ArrayList

CopyOnWriteArrayList


当多线程去写操作时,我们的ArrayList会创建一个副本进行写操作!当写操作完成后再更新数据! 就不会出现数据修改一般的情况!


当ArrayList扩容时,也会慢慢搬运,不会一致性将ArrayList直接拷贝,导致操作卡顿!


多线程下使用hash表(常考)

HashMap线程不安全!

HashTable[不推荐]

因为HashTable就是将关键方法进行synchronized加锁!

也就相当于直接给HashTable加锁,效率很低!

无论进行什么操作都会导致锁竞争!


ConcurrentHashMap[推荐]


HashTable对象加锁

就好比一个公司里的员工需要请假,都要向老板请假才有用!

而老板就相当于锁,如果有很多员工请假就会导致锁竞争激烈,线程阻塞!

image.png


解决方案:

老板权利下放!

让每个部门的人向部门管理人员请假!

image.png

我们知道哈希表的结构是由数组,数组元素是链表!

并且链表长度相对短! 所以锁冲突就很小!!!


ConcurrentHashMap优点


针对读操作不进行加锁,只对写操作加锁

减少锁冲突,在每个表头加锁

广泛使用CAS操作,进一步提高效率(比如维护size操作)

进行扩容巧妙的化整为零,进行了优化

目录
相关文章
|
25天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
10天前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
27天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
27天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
27天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
150 2
|
1月前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
1月前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
63 3
|
1月前
|
Java 程序员
深入理解Java异常处理机制
Java的异常处理是编程中的一块基石,它不仅保障了代码的健壮性,还提升了程序的可读性和可维护性。本文将深入浅出地探讨Java异常处理的核心概念、分类、处理策略以及最佳实践,旨在帮助读者建立正确的异常处理观念,提升编程效率和质量。
142 1
|
1月前
|
Java 开发者 UED
深入探索Java中的异常处理机制##
本文将带你深入了解Java语言中的异常处理机制,包括异常的分类、异常的捕获与处理、自定义异常的创建以及最佳实践。通过具体实例和代码演示,帮助你更好地理解和运用Java中的异常处理,提高程序的健壮性和可维护性。 ##
61 2
|
1月前
|
Java API 开发者
深入理解Java中的异常处理机制
本文探讨了Java编程语言中异常处理的核心概念,包括异常类型、异常捕获与抛出、以及最佳实践。通过分析常见的异常场景和处理策略,旨在帮助开发者更好地理解和运用异常处理机制,提高代码的健壮性和可维护性。文章不仅涵盖了基本的try-catch结构,还深入讨论了自定义异常的创建与使用,以及finally块的重要性和应用。此外,还将介绍一些高级技巧,如多异常捕获和嵌套异常处理,为读者提供全面的技术指导。
102 0

热门文章

最新文章