Java并发编程的艺术2

线程优先级
现代操作系统调度运行的线程,会分出一个个时间片,线程会分配到若干时间片。当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或少分配一些处理器资源的线程属性。
线程构建的时候可通过setPriority(int)方法修改优先级,默认是5。在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。
线程的状态
NEW-----初始状态,线程被构建,但是还没调用start方法
RUNNING-----运行状态,就绪和运行统称为“运行中”
BLOCKED-----阻塞状态,表示线程阻塞于锁
WAITING-----等待状态,进入该状态表示线程需要等待其他线程做出一些特定动作(通知或中断)
TIME_WAITING-----超时等待状态,他可以在指定的时间自行返回
TERMINATED-----终止状态,线程已经执行完毕
在这里插入图片描述
构造线程
一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了父线程是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时分配一个唯一的ID来标识child线程。
对象、对象的监视器、同步队列和执行线程之间的关系
在这里插入图片描述
由图可知,任意线程对Object(Object由synchronized保护)的访问,首先要获得Obect的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED,当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使它重新尝试对监视器的获取。
等待、通知机制
notify()-----通知在一个对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁
notifyAll()-----通知所有等待在该对象上的线程
waiit()-----调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意的是,调用wait方法会释放对象的锁
wait(long)-----超时等待一段时间,这里参数是毫秒,等待n毫秒,如果没有通知就返回
wait(long,n)-----对于超时时间更细粒度的控制,可以达到纳秒

1.使用wait()/notify()/notifyAll()时需要先对调用对象加锁
2.调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列
3.notify()或notifyAll()调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifyAll()的线程释放锁之后,等待线程才有机会从wait()返回
4.notify()方法将等待队列中的一个等待线程从等待队列移到同步队列中,而notifyAll()方法则是将等待队列的全部线程都移到同步队列,被移动的线程状态由WAITING->BLOCKED
5.从wait()方法返回的前提是获得了调用对象的锁

Thread.join()
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。
当线程终止时,会调用线程自身的notifyAll()方法,会通知所有等待在该对象上的线程。join()方法的逻辑结构与等待、通知经典范式一致。
ThreadLocal的使用
链接
Lock接口
在Lock接口出现之前,java是靠Synchronized关键字来实现锁功能的,而Java SE5之后,并发包中新增了Lock接口,它提供了与synchronized类似的同步功能,只是在使用时需要显式地获取和释放锁。
Lock接口提供的而synchronized不具备的功能:
1.尝试非阻塞性地获取锁
2.能被中断地获取锁
3.超时获取锁
在这里插入图片描述
可重入锁
可重入性是指一条线程能够反复进入被他自己持有的锁的同步块的特性。《Java虚拟机规范》要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值加一,而在执行monitorexit指令时会将计数器的值减一,一旦计数器的值为零,锁随即就被释放。如果获取对象锁失败,那么当前线程就应该阻塞等待,直到请求锁定的对象被持有她的线程释放为止。
ReentrantLock和Synchronized都是可重入锁,区别如下

  • 等待可中断:是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。可中断特性对处理执行时间比较长的同步块很有帮助。
  • 公平锁:指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁,而非公平锁不保证这点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized和ReetrantLock默认都是非公平的,但ReentrantLock可以通过带布尔值的构造函数要求使用公平锁。使用了公平锁,会导致Reetranlock性能急剧下降,明显影响吞吐量。
  • 锁绑定多个条件:是指一个ReetrantLock对象可以同时绑定多个Condiion对象。在Synchronize中,锁对象的wait()方法跟他的notify()或者notifyAll()方法配合时可以实现一个隐含的条件,如果要和多于一个条件关联的时候,就不得不额外添加一个锁。而ReetranLock无需这样做,多次调用newCondition()方法即可。

读写锁
ReetrantLock是排他锁,在同一时刻只允许一个线程访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁一个写锁,通过锁的分离,使并发性能有很大提升,同时保证了写操作对读操作的可见性。
ReetranReadWriteLock
特性:
1.公平性选择:支持非公平(默认)和公平的锁获取方式,吞吐量非公平优于公平。
2.重进入:读线程获取了读锁之后,能够再次获取读锁;写线程获取了写锁之后能再次获取写锁,同时也能获取读锁。
3.锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级为写锁。

锁的优化-自旋锁与自适应自旋
在JDK1.6中默认开启,自旋等待不能代替阻塞,自旋本身虽然避免了线程切换的开销,但它要占用处理器时间,如果锁被占用的时间很短,效果就会很好,否则自旋的线程只会白白消耗处理器资源而带来性能的浪费。因此自旋等待的时间必须有一定的限度,如果自旋超过了限定的次数,应该用传统的方式去挂起线程。自旋次数默认是十次,用户也可以使用参数-XX:PreBlockSpin来自行更改。
JDK6还引入了自适应自旋,如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很可能成功,进而允许自旋等待持续更长的时间,必须持续一百次忙循环。另一方面,如果对于某个锁,自旋很少成功获得过锁,那在以后要获取这个锁时有可能直接省略掉自旋过程,以避免浪费处理器资源。
等待多线程完成的CountDownLatch
CountDownLatch的构造函数接受一个int类型的参数作为计数器,如果想等待N个点完成,就传入N。
当我们调用CountDownLatch的countDown方法时,N就会减一,CountDownLatch的await方法会阻塞当前线程,直到N变成零。由于countDown方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个步骤。用在多个线程时,只需要把这个CountDownLatch的引用传递到线程里即可。
同步屏障CyclicBarrier
可循环使用的屏障,它的功能是,让一组线程到达一个屏障时被阻塞,知道最后一个线程到达屏障时,屏障才会开门,被屏障拦截的线程才会继续运行。
线程池相关:

  • 概念:线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
  • 工作机制:在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
  • 使用线程池的原因:多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过渡消耗系统资源,以及过渡切换线程的危险,从而可能导致系统资源的崩溃。
    在这里插入图片描述

线程池ThreadPoolExecutor构造函数的各个参数:
1.int corePoolSize => 该线程池中核心线程数最大值
线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程
核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。
2.int maximumPoolSize => 该线程池中线程总数最大值,线程总数 = 核心线程数 + 非核心线程数
3.long keepAliveTime => 该线程池中非核心线程闲置超时时长,一个非核心线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉
4.TimeUnit unit => keepAliveTime的单位,TimeUnit是一个枚举类型
5.BlockingQueue workQueue => 该线程池中的任务队列:维护着等待执行的Runnable对象,当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务
6.ThreadFactory threadFactory 用于创建线程的工厂
7.RejectedExecutionHandler handler(饱和策略)指定队列和线程池都满了的时候,采取的策略,JDK1.5提供了四种策略:AbortPolicy直接抛异常、CallerRunsPolicy只用调用者所在线程运行任务、DiscardOldestPolicy丢弃队列里最老的一个任务并执行当前任务、DiscardPolicy不处理(丢弃掉)

常见四种线程池

newFixedThreadPool

  public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

特点:
1.核心线程数和最大线程数大小一样
2.没有所谓的非空闲时间,即keepAliveTime为0
3.阻塞队列为无界队列LinkedBlockingQueue
流程:
在这里插入图片描述
面试题:使用无界队列的线程池会导致内存飙升吗?
会的,newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长(比如,上面demo设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终导致OOM。
使用场景
FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。
newCachedThreadPool

   public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }

特点
1.核心线程数为0
2.最大线程数为Integer.MAX_VALUE
3.阻塞队列是SynchronousQueue
4.非核心线程空闲存活时间为60秒
流程
在这里插入图片描述
使用场景
用于并发执行大量短期的小任务。
newSingleThreadExecutor

  public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

特点:
核心线程数为1,最大线程数也为1,阻塞队列是LinkedBlockingQueue,keepAliveTime为0
流程:
在这里插入图片描述
适用场景:
适用于串行执行任务的场景,一个任务一个任务地执行。
newScheduledThreadPool

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

特点
1.最大线程数为Integer.MAX_VALUE
2.阻塞队列是DelayedWorkQueue
3.keepAliveTime为0
4.scheduleAtFixedRate() :按某种速率周期执行
5.scheduleWithFixedDelay():在某个延迟后执行
流程
1.添加一个任务
2.线程池中的线程从 DelayQueue 中取任务
3.线程从 DelayQueue 中获取 time 大于等于当前时间的task
4.执行完后修改这个 task 的 time 为下次被执行的时间
5.这个 task 放回DelayQueue队列中
使用场景
周期性执行任务的场景,需要限制线程数量的场景
线程池都有哪几种工作队列?

  • ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。
  • LinkedBlockingQueue(可设置容量队列)基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列。
  • DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
  • PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列;
  • SynchronousQueue(同步队列)一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。

IO密集型和计算密集型的服务的区别是什么
计算密集型意味着要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,比较依赖CPU的运算能力。此时如果用多任务完成,则任务越多,切换的时间就越多,CPU的额外开销就大,效率就越低,所以可以设置任务数等于CPU的核心数。
IO密集型包括涉及到网络、磁盘IO的这些任务,特点是消耗CPU少,任务的大部分时间都在等待IO完成(因为IO速度远低于CPU和内存的速度),此时任务越多,CPU效率越高。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值