Java中的多线程相关(二)

一、计算线程池核心线程数思路

        首先,针对线程中的任务可粗略分为:

                1、CPU密集型:需要使用到大量CPU计算的任务,加密、解密、压缩、计算等,相比IO密集型,耗时较短;

                2、IO密集型:对数据库、文件、接口等,存在大量IO读取等操作,且相比CPU密集型,耗时较长;

        其次,针对线程池中的线程:各个线程会不断争取CPU的使用权,按照不同策略进行CPU资源和任务的切换,即存在多线程之间的上下文切换,而上下文切换本身存在一定的系统资源开销。如果存在过多的上下文切换(过多的线程数,每个线程都会争取CPU的使用权),则非任务开销过多,则相关任务性能相对下降。

        通常设置线程池中的线程数的计算公式:

                核心线程数 =  CPU核心数 * (1 + (IO平均执行时间/CPU平均执行时间))

                个人以为等价于下面:

                核心线程数 =  CPU核心数 * (1 + (IO密集型任务数/CPU密集型任务数))

         一般而言,核心线程数的设定是CPU数量的整数倍。

        可以按以下思路进行理解:

        如果该线程池中主要任务是CPU密集型(此时,线程池中的任务存在一直占用CPU进行任务作业的情况,但如果设置的线程数过多,则存在多线程抢占CPU资源的情况,上下文切换的成本较高,反而影响性能),则设置的核心线程数不必过多;

        如果该线程池中主要任务是IO密集型(此时,线程池中的任务存在一直占用IO读写的情况,且较大概率存在CPU空闲的情况,可以适当增加线程数,以增加对CPU的使用效率),则设置的核心线程数可以增多。

二、锁的分类

        根据判断的维度不同,锁可以按照不同的定义分成很多类型的锁。

        1、偏向锁、轻量级锁、重量级锁

                这三种所,特指synchronized的三种不同的状态,在对象中的mark work标志位体现。

                偏向锁:对于JAVA中的对象初始化后,如果暂时不存在线程访问使用对象,则对象内部用于标志锁信息区域(mark work)的内容是空白,一旦存在线程访问该内存的对象信息,则在对象锁区域中会记录该线程信息,此时该线程为该对象的偏向锁拥有者。那么,一旦该线程下次在访问该对象,则直接放行,直接获得锁,进而减少性能开销。

                轻量级锁:一般认为,synchronized关键字覆盖的代码,在存在少量锁竞争时,此时mark work由偏向锁转变为轻量级锁,在轻量级锁状态下,线程不会陷入阻塞状态,但存在自旋方法获取锁的使用权。

                重量级锁:多线程存在较强的锁竞争时,轻量级锁转变为重量级锁,此时尝试获取锁的线程将会进入阻塞状态

        2、可重入锁、不可重入锁

                可重入锁:当前线程已经获取这把锁,在不释放该锁的情况下,再次获取这把锁。其中比较典型时Lock的实现,ReentrantLock(可重入锁)。

                不可重入锁:当前线程已经获取这把锁,但想要再次获取这把锁,则必须先释放锁,在去获取锁。

        3、共享锁、独占锁

                共享锁:一把锁同一时刻能被多个线程活得。比如读锁。

                独占锁:一把锁同一时刻只能被一个线程获得。比如写锁。

        4、公平锁、非公平锁

               公平的含义在于:线程获取不到锁,在竞争锁资源时,所采取的策列

                公平锁:线程在竞争锁资源时,按等待的时间长短进行优先级排序(先进先出策略),是一种按时间顺序的公平。

                非公平锁:采用其他排队策略,并非一定按时间有限顺序,允许插队情况发生。

        5、悲观锁、乐观锁

               依据能否锁住资源的角度,进行分类。

                悲观锁:整体上看,悲观锁认为资源是不可信,其他线程也不可信,只有给资源上锁,资源才可信。因此,线程在获取共享资源时,需要给资源上锁,已达到独享的作用。比如读写锁中的写锁。悲观锁上锁后,后续线程无法获取到锁,从而进行等待阻塞,根据线程池获取资源的不同,可能引发线程饥饿等情况的发生。

                悲观锁的主题是要确保数据资源的一致性,防止多线程修改导致资源数据的不可信。但悲观锁由于对资源长时间的锁定,会导致系统性能一定程度下降。常见的悲观锁:Lock(ReentrantLock)和Java中的syncronized

                悲观锁适合用于并发性写入多,临界区多,竞争激烈的场景,利用悲观锁避免大量无用的重复尝试。

                乐观锁:乐观锁和悲观锁是相反。在获取资源前,不获取资源锁,但会对对象资源进行状态记录(数据库中的乐观锁应用Version),完成任务后,判断资源在前后的状态是否一致进行决定下一步的方案。

                一般而言,乐观锁完成任务后,通过相关状态字段判断:无其他线程修改资源时,则将对资源进行修改;如判断出其他线程对资源进行了修改,则依据相关策略进行抛出异常或则重新执行等行为操作。

                乐观锁不要求在获取资源前,获取该锁,意味着其他线程同样可以访问该资源,不会陷入阻塞状态。但是会依据资源前后状态进行判断资源是否可信,进而采取下一步动作。      

                乐观锁适用于大部分读取,少部分修改,或虽然存在读写场景,但是并发并不激烈的清理,乐观锁不上锁的特性能减少不少开销。

        6、自旋锁、非自旋锁

                 依据线程在获取不到锁(资源)情况下的行动策略,进行分类。

                自旋锁:在线程获取不到资源锁时,线程通过循环不停的尝试获取资源锁,而不进入阻塞或者进入等待队列。

                非自旋锁:在线程获取不到资源锁时,线程进入阻塞或者等待队列。

        7、可中断锁、不可中断锁

                依据线程在等待锁过程中是否能响应中断信号,进行分类。

                可中断锁:线程在申请锁资源时,可以响应中断信号,例如ReentrantLock锁。

                不可中断锁:线程在申请锁资源后,无法响应中断信号,例如syhchronized

三、Synchronized和Lock

        1、现在来简单学习一下synchronized的相关内容。synchronized背后的现实方式是使用对象的内置锁(monitor锁)。

        被synchronized修饰的方法或则代码块,只能在同一时间被一个线程所访问,其余线程如果需要访问则需要等待该线程释放掉对应锁。

        内置锁,也可以成为管程锁或者监视器锁(monitor锁)。内置锁(monitoer)是一种互斥锁,其中内置锁嘴简单和常见的使用就是synchronized (this)的使用。内置锁的是每一个Java对象都具有的。

        内置锁可以通过synchronized关键字来获取和释放。当一个线程获得了一个对象的内置锁后,它可以执行由synchronized关键字保护的代码块,其他线程必须等待这个线程释放锁后才能获得锁并执行相应的代码。

         使用内置锁可以提供线程安全性,即当多个线程同时访问一个共享资源时,只有一个线程可以执行对该资源的操作,避免了竞态条件和数据不一致的问题。

        内置锁的最常见用法是将关键代码块包装在synchronized语句块中,如下所示:

synchronized (obj) {
    // 关键代码块
}

        其中obj是一个对象引用,这个对象就是用来获取和释放锁的对象。当一个线程进入synchronized语句块,它会尝试获取obj对象的锁,如果锁已被其他线程持有,则该线程将被阻塞,直到锁被释放。     

        在使用synchronized关键字时,关于内置锁的锁定与锁释放无需显示声明,但存在的问题是,synchronized无法中断进行响应。一旦内置锁被其他线程所使用,该线程进入阻塞状态,可能造成资源的浪费。 

       

       2、现在记录Lock类的相关内容,lock的基本路径在java.util.concurrent.locks类中记录。

        在Java中,Lock是一种比内置锁更灵活和功能更强大的同步机制。它是Java.util.concurrent.locks包下的一个接口,定义了使用锁的基本操作。

        与内置锁不同,Lock提供了更多的功能和灵活性,包括:

  1. 可以实现公平性:Lock可以通过构造函数或方法来指定是否实现公平性,公平锁会按照线程请求锁的顺序来分配锁,而非公平锁则允许插队。
  2. 可以实现可重入性:线程可以多次获得同一个锁,而不会被自己所持有的锁所阻塞。
  3. 支持尝试非阻塞地获取锁:Lock提供了tryLock()方法,可以尝试获取锁而不被阻塞,如果锁已被其他线程持有,则返回false。
  4. 提供超时机制:Lock提供了tryLock(long timeout, TimeUnit unit)方法,可以在指定的时间内尝试获取锁,如果超时仍未获取到锁,则返回false。
  5. 支持中断:Lock提供了lockInterruptibly()方法,可以被中断地获取锁,如果线程在等待锁的过程中被中断,将抛出InterruptedException异常。

        Java中的Lock接口有多个实现类,其中最常用的是ReentrantLock。它是一种可重入的锁,与内置锁的功能相似,但提供了更多的灵活性。下面是一个使用ReentrantLock的示例:

ReentrantLock lock = new ReentrantLock();

try {
    lock.lock(); // 获取锁
    // 关键代码块
} finally {
    lock.unlock(); // 释放锁
}

        在上面的示例中,使用lock()方法获取锁,然后在try-finally块中执行关键代码块,最后使用unlock()方法释放锁。

        总的来说,使用Lock可以提供更高级别的同步控制,可重入性、公平性、超时等功能更加强大。但需要注意的是,在使用Lock时需要手动确保锁的获取和释放,否则可能会导致死锁等问题。

       

        Syhchronized和Lock存在较多的相似性,以下是两者的相似的地方:       

        1、syhchronized和lock都用于保证线程的安全性;

        2、synchronized和lock都具有可见性;

        3、synchronized和lock都可重入;

        Synchronized和Lock之间的差异性,以下是两者差异的地方:

        1、语法不同。synchronized可以使用在方法,或者代码块上,lock则需使用在代码中。

        2、提示方式不同。synchronized隐式上锁和解锁,lock则是显示上锁和解锁。

        3、加解锁顺序不同。synchronized先加锁的,要后解锁,lock则没有这些要求。

        4、synchronized不够灵活,synchronized使用java内置锁,导致等待线程会被阻塞,直到获取到到申请的锁,而lock则相对灵活,存在tryLock、lockInterruptibly可以响应中断或者短暂获取尝试获取锁信息。同时,lock锁可以实现读锁,因此lock锁可以被多线程同时访问,但是synchronized不行,属于线程独占。

        5、公平锁,syhchronized不可以设置公平锁,但是lock的实现类ReentrantLock可以。

        

        

  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值