多线程并发3.0

应用

需要推送30万条数据给用户,但是推送的接口只支持单条推送

可以估计一下,30万条数据,一条数据按3s算,大概需要250个小时

因此这里考虑使用多线程来进行并发操作,降低数据推送的时间,提高数据推送的实时性

设计要点——防止重复

数据推送肯定是不能重复的,因此需要有一个机制来保证各个线程推送数据的隔离

思路一:

将所有数据放到一个集合里,然后对数据进行切割,每个线程推送不同段的数据

代码示例

 public void test(Map<String, String> phoneMap) {
        //拆分成10份,跑10个线程节省时间
        int threadNum = 10;
        List<Map<String, String>> subList = subMap(phoneMap, threadNum);
        List<Future<Map<String, String>>> futureList = new ArrayList<Future<Map<String, String>>>();
        for (int i = 0; i < threadNum; i++) {
            //获取子用户集
            Map<String, String> subPhoneMap = subList.get(i);
            Future<Map<String, String>> future = tagExecutorTask
                    .submit(new Callable<Map<String, String>>() {
                        @Override
                        public Map<String, String> call() throws Exception {
                            //遍历所有人
                            Iterator<Map.Entry<String, String>> it = subPhoneMap.entrySet().iterator();
                        }
                    }
        }
        public static <T > List < Map < String, T >> subMap(Map < String, T > map, int num){
            List<Map<String, T>> resultList = new ArrayList();
            int i;
            for (i = 0; i < num; ++i) {
                Map<String, T> subMap = new HashMap();
                resultList.add(subMap);
            }
            i = 0;
            Iterator var6 = map.keySet().iterator();
            while (var6.hasNext()) {
                String key = (String) var6.next();
                if ((i + 1) % num == 0) {
                    ((Map) resultList.get(i)).put(key, map.get(key));
                    i = 0;
                } else {
                    ((Map) resultList.get(i)).put(key, map.get(key));
                    ++i;
                }
            }
            return resultList;
        }
    }

思路2:数据库分页的方式

每个线程取[start,limit]I区间的数据推送,但需要保证start的一致性

这里采用第二种方式,因为考虑到数据量可以后续会持续增加,把所有图片都加载到内存,会占用很大内存

设计要点——线程池选择

这里线程池使用ThreadPoolExecutor提供线程池服务
在这里插入图片描述
为什么要使用线程池

-降低资源损耗,指的可以避免重复线程的创建和销毁

-提高响应速度,线程的创建时间为T1,执行时间T2,销毁时间T3,用线程池可以免去T1和T3的时间

-便于统一管理线程对象,可以对线程进行统一的分配、调优和监控

线程池的核心参数

来看一ThreadPoolExecutor的构造方法:

public ThreadPoolExecutor(int corePoolSize,
                         int maximumPoolSize,
                         long keepAliveTime,
                         TimeUnit unit,
                        BlockingQueue<Runnable> workQueue,
                        ThreadFactory threadFactory,
                        RejectedExecutionHandler handler) 

-corePoolSize:核心线程数,当运行线程数小于核心线程数时,会创建核心线程,核心线程默认会一直存活即使没有任务需要执行

当然可以设置成超时就销毁,需要配置,但这样违背线程池的初衷

–workQueue:等待队列,当运行线程数= corePoolSize时,新的任务会被添加到workQueue中

-maximumPoolSize:当等待队列满了,运行线程数< maximumPoolSize时候就会再次创建非核心线程,超过该值会执行饱和策略

-keepAliveTime:非核心线程闲置时间

-unit:线程池中非核心线程保持存活的时间

  • threadFactory:创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等

-RejectedExecutionHandler:饱和策略

线程池的工作流程

1、当一个任务通过execute或者submit方法提交到线程池的时候,如果当前池中线程数(包括闲置线程)小于coolPoolSize,则创建一个线程执行该任务

2、如果当前线程池中线程数已经达到coolPoolSize,则将任务放入等待队列

3、如果任务不能入队,说明等待队列已满,若当前池中线程数小于maximumPoolSize,则创建一个临时线程(非核心线程)执行该任务

4、如果当前池中线程数已经等于maximumPoolSize,此时无法执行该任务,根据拒绝策略处理

注意:当池中线程数大于coolPoolSize,超过keepAliveTime时间的闲置线程会被回收掉。回收的是非核心线程,核心线程一般是不会回收的。如果设置allowCoreThreadTimeOut(true),则核心线程在闲置keepAliveTime时间后也会被回收

核心线程数是怎么选的

线程在Java中属于稀缺资源,线程池不是越大越好也不是越小越好。任务分为计算密集型任务、IO密集型任务

-计算密集型任务:大部分时间用来做计算等cpu动作的程序叫做cpu密集型任务

计算密集型一般推荐线程池不要过大,一般是CPU数 + 1,+1是因为可能存在「页缺失」(就是可能存在有些数据在硬盘中需要多来一个线程将数据读入内存)。如果线程池数太大,可能会频繁的进行线程上下文切换跟任务调度

-IO密集型任务:指任务需要执行大量的IO操作,涉及到网络和磁盘,例如远程数据库读写、本地数据库读写

线程数适当大一点,机器的Cpu核心数*2

常见阻塞队列

-ArrayBlockingQueue :由数组结构组成的有界阻塞队列,按照先进先出的原则,初始化时,需要指定容量的大小,一旦创建,容量就不能改变。采用可重入锁进行并发控制,添加和删除操作采用的是同一个锁

-LinkedBlockingQueue :由单向链表实现组成的阻塞队列,是一个有界队列,按照先进先出的原则,默认容量为 Integer.MAX_VALUE。锁是分离的,添加和删除操作使用两个不同的锁,在高并发场景下,生产者和消费者可以并行的操作队列中的数据,所以提高了并发性能

-PriorityBlockingQueue :支持优先级排序的无界阻塞队列,不遵循先进先出,可以通过实现 compareTo 方法来指定元素比较规则,也可以使用 Comparator 比较器来指定比较规则

上面这三个都是blockingqueue,都是不允许为null值的,且都是线程安全的

-SynchronousQueue :它是一种不保存元素的阻塞队列,通过新建一个线程来处理新任务。是一种轻量级的ArrayBlockingQueue,在只有一个生产者和一个消费者的场景下性能较好

拒绝(饱和)策略

主要有4种拒绝策略:

AbortPolicy:直接丢弃任务,抛出异常,这是默认策略

DiscardPolicy:直接丢弃任务,也不抛出异常

CallerRunsPolicy:重复处理,重复调用execute()方法添加当前任务直至成功

DiscardOldestPolicy:丢弃等待队列中最旧的任务,即即将执行的任务,并执行当前任务

常见线程池

在上面我们直接用到了ThreadPoolExecutor的构造方法创建线程池,还有另一种方式,通过Executors 创建线程

比较典型常见的四种线程池包括: newSingleThreadPool 、newFixedThreadPool、newScheduledThreadPool、 newCachedThreadPool、

SingleThreadPool

只有一条核心线程,适用于一个资源只能有一个线程访问的场景,使用无界等待队列 LinkedBlockingQueue
在这里插入图片描述
FixedThreadPool

-定长的线程池,固定且稳定的并发线程,多用于服务器,有核心线程,没有非核心线程,使用无界等待队列LinkedBlockingQueue
在这里插入图片描述
ScheduledThreadPoolExecutor

适用于定时及周期性任务执行的场景,有核心线程,但也有非核心线程,非核心线程的大小也为无限大
在这里插入图片描述
CachedThreadPool

可缓存的线程池,适用于低延迟任务场景,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况
在这里插入图片描述
线程池设计

线程池:

每个线程能处理的条数:5000

核心线程:5

非核心线程:5

线程空闲时间:0

任务队列:LinkedBlockingQueue,长度100

拒绝策略:采用默认的AbortPolicy,直接丢弃任务,抛出异常

执行逻辑

1、将传来的数据存储到数据库里,然后状态设置成0

2、通过特定的条件查询查出未推送的所有数据,某个状态为0即未推送的数据数量

3、计算出需要进行多少轮,轮数等于所有未推送数据的数量/(每个线程能推送的数据量*起的线程数5)+1

4、一轮五个线程,循环开启5条线程去进行处理,注意每条线程拿的数据都是不一样的,每一轮都使用一个初始长度为32的arraylist来存储结果,即完成推送的数据数

offset=limit*计数器

同步块内容,锁住整个类对象,即表示同一个类只有一个实例能访问同步块内容

在同步块内容将会执行计数器的递增,这部分必须同步,即必须线程安全,防止数据的重复推送

每条线程数据都是从数据库里查,每个线程的offset不一样,但是limit一样

4、接下来推送数据,这里使用的是callable里的submit方法来进行调用,是由返回值的,推送成功的状态改成1,失败改成2,整个call方法返回值是修改的数据条数

5、将返回值直接存储到futurelist里,先不进行处理,出了代码块后再处理,防止线程阻塞

代码

@Service
public class PushProcessServiceImpl implements PushProcessService {
    @Autowired
    private PushUtil pushUtil;
    @Autowired
    private PushProcessMapper pushProcessMapper;
    private final static Logger logger = LoggerFactory.getLogger(PushProcessServiceImpl.class);
    //每个线程每次查询的条数
    private static final Integer LIMIT = 5000;
    //起的线程数
    private static final Integer THREAD_NUM = 5;
    //创建线程池
    ThreadPoolExecutor pool = new ThreadPoolExecutor(THREAD_NUM, THREAD_NUM * 2, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
    @Override
    public void pushData() throws ExecutionException, InterruptedException {
        //计数器,需要保证线程安全
        int count = 0;
        //未推送数据总数
        Integer total = pushProcessMapper.countPushRecordsByState(0);
        logger.info("未推送数据条数:{}", total);
        //计算需要多少轮
        int num = total / (LIMIT * THREAD_NUM) + 1;
        logger.info("要经过的轮数:{}", num);
        //统计总共推送成功的数据条数
        int totalSuccessCount = 0;
        for (int i = 0; i < num; i++) {
            //接收线程返回结果
            List<Future<Integer>> futureList = new ArrayList<>(32);
            //起THREAD_NUM个线程并行查询更新库,加锁
            for (int j = 0; j < THREAD_NUM; j++) {
                synchronized (PushProcessServiceImpl.class) {
                    int start = count * LIMIT;
                    count++;
                    //提交线程,用数据起始位置标识线程
                    Future<Integer> future = pool.submit(new PushDataTask(start, LIMIT, start));
                    //先不取值,防止阻塞,放进集合
                    futureList.add(future);
                }
            }
            //统计本轮推送成功数据
            for (Future f : futureList) {
                totalSuccessCount = totalSuccessCount + (int) f.get();
            }
        }
        //更新推送标志
        pushProcessMapper.updateAllState(1);
        logger.info("推送数据完成,需推送数据:{},推送成功:{}", total, totalSuccessCount);
    }
    /**
     * 推送数据线程类
     */
    class PushDataTask implements Callable<Integer> {
        int start;
        int limit;
        int threadNo;   //线程编号

        PushDataTask(int start, int limit, int threadNo) {
            this.start = start;
            this.limit = limit;
            this.threadNo = threadNo;
        }
        @Override
        public Integer call() throws Exception {
            int count = 0;
            //推送的数据
            List<PushProcess> pushProcessList = pushProcessMapper.findPushRecordsByStateLimit(0, start, limit);
            if (CollectionUtils.isEmpty(pushProcessList)) {
                return count;
            }
            logger.info("线程{}开始推送数据", threadNo);
            for (PushProcess process : pushProcessList) {
                boolean isSuccess = pushUtil.sendRecord(process);
                if (isSuccess) {   //推送成功
                    //更新推送标识
                    pushProcessMapper.updateFlagById(process.getId(), 1);
                    count++;
                } else {  //推送失败
                    pushProcessMapper.updateFlagById(process.getId(), 2);
                }
            }
            logger.info("线程{}推送成功{}条", threadNo, count);
            return count;
        }
    }
}

进程和线程

进程:进程是系统资源分配的最小单位,是指系统中正在执行的一个应用程序,进程拥有独立的数据空间

线程:线程是程序执行的最小单位,线程共享同一进程的数据空间,指的是堆和方法区,但是每个线程也有自己的数据空间,例如程序计数器、虚拟机栈和本地方法栈

创建线程的几种方式

-继承thread,thread类本质上实现runnable接口,启动线程的方式是通过thread类的start方法,然后等分配到cpu的执行权是再执行run方法

-实现runnable方法,如果一个类已经继承了另一个类,就无法直接继承thread,此时可以通过实现runnable接口来开启多线程

实现runnable的启动线程需要将一个实例赋予到新建的Thread对象里,让thread对象来代理调用run方法

-实现callable方法,重写call方法,可以返回一个future类型的返回值

线程阻塞

多个线程由于存在资源的竞争而同时被阻塞,他们中的一个或者多个都在等待某个资源的释放

例如:线程A持有资源2,线程B持有资源1,他们同时都想申请对方的资源,由由于资源同一时间内只能被一个线程所拥有,因此这两个线程就会互相等待而进入死锁状态
在这里插入图片描述
怎么避免死锁

只需要破坏掉死锁中四个必要条件其一即可

-破坏互斥条件:这个条件无法破坏,因为使用锁就是想让他们互斥

-破坏占用且申请条件:让线程一次性申请所有的资源

-破坏不可剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,主动释放它占有的资源,即是使用定时锁,但超过定时时间就释放锁

-破坏循环等待条件:按某一顺序来申请资源,释放资源则反序释放,破坏循环等待条件

synchronized

synchronized可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行,同时可以保证线程的有序性、原子性、可见性

有序性:在同步代码块/同步方法里程序执行的顺序是按照代码的顺序执行的

原子性:要么是加锁或解锁成功,不存在中间状态

可见性:当线程退出同步代码块/同步方法时,所有的共享变量都会被同步到主内存中去(解决可见性问题,还可以使用volatile)

synchronized可以修饰代码块或者方法

当修饰代码块时,当进入同步代码块时会先执行一个monitorenter指令,然后尝试去获取monitor锁,获取锁则锁的计数器+1。当退出同步代码块时,会执行monitorexit指令,然后释放锁,锁计数器-1

当修饰方法时,我们通过反编译字节码文件可以看到一个acc_synchronized的标志,该标志表明这个方法是同步方法,如果是普通同步方法,那么就去尝试获取调用该方法实例的monitor锁,如果是静态同步方法,那么就去获取该类的monitor锁,获得锁的才能进入同步方法且锁的计数器+1

更细地说,synchronized里面有三个队列contentionlist,entrylist,waitset

contentionlist是用来存放所有竞争锁的线程,而entrylist是存放所有有资格拿到锁的线程,而waitset是存放由于wait而阻塞的线程,在waitset里的线程需要用notify/notifyall来唤醒,而其他情况的阻塞都是存放到entrylist里
在这里插入图片描述
synchronized的优化

PS:首先知道synchronized里是通过进入、退出对象监视器来实现方法、代码块同步的,与对象有关

对象头里有个叫做mark word的里面存储了该对象的锁状态,该状态可以表明该对象的对象锁是否被线程获取,被谁获取

偏向锁

最乐观的锁,认为只有一个线程来竞争锁,只有一个线程多次获得

当线程访问同步代码块时,会先去对象头mark word里找线程信息

如果里面线程信息为空,那么会把自己的线程信息通过CAS操作更新到mark word里;

如果线程信息不为空且为当前线程,那么将直接进入同步代码块;

如果线程信息不为空且不是当前线程,那么将会在全局安全点进行偏向锁撤销或者升级,如果获得偏向锁的线程已经死亡或者同步代码块已经执行完毕,那么进行锁撤销,新的线程获取偏向锁;如果还在同步代码块,膨胀成轻量级锁,如果死亡进行锁撤销,如果还存货,那么升级成轻量级锁

PS:偏向锁是出现对象锁竞争才释放锁的机制,对象头里mark word存在一个记录锁撤销次数,如果超40次直接膨胀成轻量级锁
在这里插入图片描述
全局安全点位置

-循环的末尾,避免线程执行时间过长

-调用方法后

-方法返回前

-抛出异常

偏向锁的适用场景

始终只有一个线程在执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁或者升级偏向锁,撤销偏向锁或升级会导致stop the word操作

在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用;

轻量级锁

偏向锁是只允许一个线程多次获得锁,那么轻量级锁就是允许多个线程获得锁,但是必须是顺序获取锁,不允许出现竞争,每次进入同步块时都会进行CAS操作。可以避免线程的阻塞及唤醒

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录(Lock Record)的空间,并将对象头中的Mark Word拷贝到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向Lock Record的指针。

如果更新成功,则成功获取同步锁,执行完同步代码后将会解锁,用CAS将对象头里指向lock record的指针改成mark word,如果CAS成功则流程结束;

如果加锁时的CAS操作失败,那么将进行自旋尝试去获取锁,如果自旋失败,那么膨胀成重量级锁,执行成功则执行同步代码,执行完CAS去对象头里替换mark word

如果解锁时的CAS操作失败,则说明期间有线程尝试获取锁并自旋失败,膨胀成重量级锁

偏向锁、轻量级锁、重量级锁场景总结

只有一个线程进入加锁区,锁状态是偏向锁

多个线程交替进入加锁区,锁状态可能是轻量级锁

多线程同时进入加锁区,锁状态可能是重量级锁

自旋锁

由于大部分时候锁被占用的时间很短,因此没有必要让线程阻塞,频繁阻塞和唤醒对CPU负担很大

所谓“自旋”,就是让未获得锁的线程去执行一个无意义的循环,让它稍等一会,自旋默认次数是10次

jdk1.6对自旋锁进行了优化,自适应自旋,根据前一次在同一锁上的自旋时间及拥有者的状态来决定

此操作为了防止长时间的自旋,在自旋操作上加了一些限制条件

如果某个锁自旋很少成功获得,那么下一次就会减少自旋次数

如果自旋锁依然失败,只能膨胀成重量级锁了

上面无锁-偏向锁-轻量级锁-自旋锁-重量级锁就是整个膨胀的过程

锁消除,锁消除是在编译器级别的事情,指的是JVM检测到某一些同步的代码块完全不存在着竞争数据,那就不需要加锁,把锁消除掉

也许你会觉得既然不存在着数据竞争,那么代码直接不加锁不就好了

因为有些时候锁并不是程序员所写的,而是jdk本来就定义好的,比如stringbuffer和vector,它们中的很多方法都是有锁的,在一些不会有线程安全的情形下使用这些方法,编译器会将锁消除来提高性能

锁粗化,增大加锁范围

例如现在在一个方法里,逻辑是:同步代码块-普通代码-同步代码块,这个方法里是由两个同步方法+一些不需要同步的工作(但是很快执行完成),因此我们可以考虑把上面这部分的代码合成一次加锁操作、解锁操作

因为频繁的加锁、解锁是很消耗资源的
在这里插入图片描述
偏向锁、轻量级锁、重量级锁三者的对比
在这里插入图片描述
synchronized的执行过程

-检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁

-如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1

-如果失败,则说明发生竞争,撤销偏向锁或者升级成轻量级锁

-当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁

-如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁

-如果自旋成功则依然处于轻量级状态

-如果自旋失败,则升级为重量级锁

除了synchronized,还有什么方式可以加锁

可以使用juc包提供的锁,可以使用Lock接口

Lock中的主要方法:

-lock(),用来获取锁,如果锁被其他线程获取,进入等待状态

-unlock(),释放锁

-tryLock(),用来获取锁,如果获取成功返回true,否则返回false

-tryLock(long timeout, TimeUnit unit),若在等待时间内获取到锁则返回true,超时返回false

-lockInterruptibly(),通过这个方法去获取锁,如果线程获取不到锁而进入等待状态,那么这个线程等待状态可以被打断

通过调用threadB.interrupt()方法即可中断线程B的等待过程

Lock下的主要接口和类

-Reentrantlock(可重入锁):实现了Lock接口,是可重入锁,可以实现公平锁和非公平锁,可以完成synchronized的所有工作

可重入锁:某个线程已经获得某个锁,再次获取该对象的对象锁不会出现死锁,获取成功则计数器+1,释放锁时则-1,只有计数器等于0才会释放该对象的锁

例如:有两个同步方法,它们锁的都是同一个实例,这个实例对象可以同时调用这两个方法,这就是可重入锁,当加锁成功计数器+1,解锁则计数器-1,最后计数器=0

而如果不能同时调用那就是不可重入锁

-公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁

一个线程获取公平锁的过程:

首先会先判断AQS里同步状态state是否为0,如果为0表示当前没有其他线程获取锁,当前线程可以尝试获取锁;

看看当前AQS队列里是否有其他线程,如果有其他线程,则当前线程不会尝试获取锁,进行排队等待;

如果AQS队列里没有其他线程,那么就利用CAS将AQS的同步状态state修改为1,修改成功则成功获取锁,当前线程会独占锁;如果修改失败且state>0,那么说明此时锁已经被获取了,然后再判断是否是当前线程,如果是的话,state+1\

如果在第一步的获取锁失败,那么要将当前线程写入到队列里,写到队尾,写入失败则通过自旋CAS的方式,一定可以写入到队列里

-非公平锁:在尝试获取非公平锁时,不需要判断队列里是否还有其他线程,直接尝试获取锁,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁,因为阻塞线程、唤醒线程开销不少,因此效率比公平锁高,默认是非公平锁

释放锁时,由于都是可重入锁,因此需要将AQS里同步状态state减到0才认为完全释放锁,释放锁之后要唤醒被挂起的线程

-ReadWriteLock(读写锁),里面有一个读锁和写锁

读锁可以在没有写操作时被多个线程同时持有,而写锁是独占的,只能被一个线程所持有

当线程是执行写操作时,那么其他线程都会被阻塞;读操作时则多个线程可以一起访问,不会阻塞

-ReentrantReadWriteLock(可重入读写锁),是拥有可重入型的ReadWriteLock,也是可以实现公平锁,默认是非公平锁

ReetrantLock的原理

ReentrantLock是基于Lock实现的可重入锁,底层通过大量的CAS操作和AQS队列去维护state变量的状态去实现线程安全的功能

CAS

CAS是一种乐观锁,算法大概如下:包含3个参数:V需要更新变量的内存值,E表示需要更新变量的旧值,N表示需要更新变量的新值,当且仅当V=E时,才会将V设置成N,如果V!=E,则什么都不做。最后返回V值

原子类就是使用CAS操作来实现的

CAS会导致什么问题

1、ABA问题,当线程1从内存里拿了一个值A,这时候线程2也去内存里拿到这个值A,然后线程2把值改成B,然后再改成A,这时候线程1进行CAS操作是成功的,但是ABA问题大部分场景下不影响并发的最终结果

2、如果CAS一致不成功,那么将会一直自旋,这会给CPU带来很大的开销,可以尝试限制自旋的次数

3、CAS操作只能保证一个共享变量的原子操作,但是多个共享变量则不行,这时候需要用到锁

如何解决ABA

添加一个版本号,通过比较值跟版本号是否相同来决定是否能正常操作

AQS

AQS即是抽象的队列式同步器

AQS 使用⼀个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作

尝试加锁的时候通过CAS修改值,如果成功设置为1,并且把当前线程ID赋值,则代表加锁成功

如果CAS失败,那么该线程将会进入阻塞队列自旋,获得锁的线程释放锁的时候将会唤醒阻塞队列中的线程,释放锁的时候则会把state重新置为0,同时当前线程ID置为空

哪个线程竞争会获取到锁具体看是公平锁还是非公平锁
在这里插入图片描述
synchronized和lock的区别

-synchronized是基于JVM层面,可以自动加锁释放锁,而lock是基于api的,需要手动加锁和释放锁,一般都会在finally里释放锁,防止线程死锁

-lock比synchronized高级,可以实现公平锁、限时等待、等待可中断、选择性通知

1)

-公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁

-非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁,因为阻塞线程、唤醒线程开销不少

而synchronized只能实现非公平锁

2)synchronized不能限时等待或者等待可中断,而lock可以

可以通过lockInterruptibly()来进行加锁,然后使用interrupt()来停止等待

可以通过tryLock(long timeout, TimeUnit unit)来限时等待

-synchronized不能获取锁的状态,而lock可以通过trylock

3)synchronized通过wait()和notify()/notifyAll()来实现等待/通知机制,ReentrantLock类通过Condition接口里的await()和signal()/signalAll()来实现等待/通知机制

wait和notify、notifyAll

在synchronized里通过wait和notify/notifyAll来实现线程之间的通信,这三个方法的执行都必须在同步方法/同步代码块里执行

为什么叫做线程之间的通信

当一个线程调用wait方法时,那么这个线程将会释放锁并进入waitset队列里等待,然后需要其他线程来通知唤醒

wait方法还有wait(long),wait(long,int):这两个方法是设置等待超时的,这两个方法是表明如果线程在被通知之前超过了指定的时间,那么这个线程将会进入到entrylist里重新竞争锁

notify():这个方法调用的前提需要明确,这个方法会随机通知在waitset队列里等待该对象的对象锁的一个线程

如果执行完notify之后还没退出同步块,那么被唤醒的线程将会进入阻塞状态,执行notify的线程会继续执行直到退出同步块

当释放锁之后,被唤醒的线程才会进入entrylist里进行竞争,即进入就绪状态,但不一定能成功转成运行状态,进入运行状态会继续之前的操作

notifyAll():这个方法会唤醒全部在waitset里的线程,这些线程会进入阻塞状态,当对象锁被释放,那么这些线程会去竞争,拿到锁的线程会继续之前的位置执行

ReentrantLock的选择性通知

reentrantLock里通过condition接口来实现等待/通知,接口里await方法类似于wait方法,signal/signalAll类似于notify/notifyAll方法,当然,await和signal/signalAll方法都需要有锁,即lock.lock(),也要记得在finally释放锁

signal/signalAll()方法只会唤醒注册在该Condition实例中的一个/所有等待线程,跟notify/notifyAll区别比较大

volatile原理

相比synchronized的加锁方式来解决共享变量的内存可见性问题,volatile就是更轻量的选择,使用volatile声明的变量,可以确保值被更新的时候对其他线程立即可见

当一个线程修改一个被volatile修饰的变量时,那么这个变量的值会重新刷回主内存

当另一个线程需要使用到这个共享变量时,它会去主内存里刷新,如果不这么做就会造成数据的不一致,即可见性问题

volatile使用内存屏障来保证不会发生指令重排,解决了内存可见性的问题

在每个volatile读操作后插入LoadLoad屏障,在读操作后插入LoadStore屏障
在这里插入图片描述
在每个volatile写操作的前面插入一个StoreStore屏障,后面插入一个SotreLoad屏障
在这里插入图片描述
JMM的理解

java内存模型(JMM)可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现程序在各种平台下运行的结果相同

JMM规定所有得变量都存储在主内存里,包括实例变量、静态变量,但是不包括局部变量和方法参数

每个线程都有自己的工作内存,线程的工作内存保存了该方法的局部变量和方法参数,以及主内存里变量的拷贝副本

特别说明

1)线程对变量的操作都必须在工作内存中进行,不能直接读写主内存中的变量,操作完成后刷回主内存

2)线程无法访问其他线程工作内存中的变量,线程之间变量值的传递必须通过主内存来完成

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值