当前位置:首页>编程知识库>后端开发知识>Java多线程
Java多线程
阅读 3
2022-11-28

1、Java线程的生命周期5种状态

1. 新建(NEW):新创建了一个线程。

2. 可运行(RUNNABLE):线程创建后,其他线程(比如main线程)调用了该线程的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。

3. 运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。

4. 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu时间片,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu时间片转到运行(running)状态。

阻塞的情况分三种:
4.1、 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
4.2、同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
4.3、其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

5. 死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

2、Thread.sleep 和 Object.wait区别

2.1、这两个方法来自不同的类,sleep来自Thread类,和wait来自Object类。sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。

2.2、 有没有释放锁,sleep方法没有释放锁,而wait方法释放了锁。sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源

2.3、wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用

2.4 sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。

3、线程池

3.1、为什么需要线程池?

重复利用线程资源,减少创建线程和销毁线程系统所花费的开销,也可以限制请求过多带来的系统压力。在一些场景也可以增加处理的速度。

3.2、线程池内部怎么保证线程安全?

线程池内部有两部分组成, 一部分是task任务列表, 一部分是线程数组,在处理任务是都要去上锁,等这个任务拿到后再释放锁。

3.3、如何创建线程池

阿里公司明确指出线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors返回的线程池对象的弊端如下:
1FixedThreadPoolSingleThreadPool:
  允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
2CachedThreadPool:
  允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM
阿里公司给出的创建线程池的demo
Positive example 1:
    //org.apache.commons.lang3.concurrent.BasicThreadFactory
    ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());
       
        
            
Positive example 2:
    ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();

    //Common Thread Pool
    ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    pool.execute(()-> System.out.println(Thread.currentThread().getName()));
    pool.shutdown();//gracefully shutdown


Positive example 3:
    <bean id="userThreadPool"
        class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <property name="corePoolSize" value="10" />
        <property name="maxPoolSize" value="100" />
        <property name="queueCapacity" value="2000" />

    <property name="threadFactory" value= threadFactory />
        <property name="rejectedExecutionHandler">
            <ref local="rejectedExecutionHandler" />
        </property>
    </bean>
    //in code
    userThreadPool.execute(thread);

3.4 、 ThreadPoolExecutor参数使用

ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 
 corePoolSize 核心线程数 会一直存在,除非allowCoreThreadTimeOut设置为true
maximumPoolSize 线程池最大线程数
keepAliveTime:除了核心线程数外的线程 如果没有任务多久释放。
unit:超时时间的单位
workQueue:工作队列,保存未执行的Runnable 任务
threadFactory:创建线程的工厂类
handler:当线程已满,工作队列也满了的时候,会被调用。被用来实现各种拒绝策略。

3.5 线程池的工作机制

如果没有空闲的线程执行该任务,并且线程数没有达到核心线程数,会创建一个新的线程。(如果创建的线程存在空闲的就直接用了)。
如果没有空闲的线程执行该任务,当前线程数已经达到核心线程数,会把任务放到任务队里中。
如果没有空闲的线程执行该任务,当前线程数已经达到核心线程数,如果队列满了会创建一个新的线程。
如果没有空闲的线程执行该任务,当前线程数已经到达最大线程数,会通过handler执行拒绝策略。

3.5 拒绝策略分四种

3.5.1.默认直接拒绝抛出ThreadPoolExecutor.AbortPolicy RejectedExecutionException

3.5.2.直接不处理ThreadPoolExecutor.DiscardPolicy()

3.5.3.把加入队列最早的任务删除。ThreadPoolExecutor.DiscardOldestPolicy()

3.5.4.让调用线程池的任务去处理。ThreadPoolExecutor.CallerRunsPolicy()

自定义拒绝策略 实现RejectedExecutionHandler接口,实现抽象方法rejectedExecution方法。 当引用自定义拒绝策略时会初始化自定义拒绝策略类的构造方法。 当线程堵塞触发拒绝策略时会执行rejectedExecution方法。 这几种拒绝策略都是静态内部类实现RejectedExecutionHandler接口。

4、线程池有哪些队列

线程池有五种常用的队列,ArrayBlockingQueue、LinkedBlockingQueue和SynchronousQueue。

ArrayBlockingQueue 是有边界的堵塞队列。
LinkedBlockingQueue 是无边界的堵塞队列,可以设置边界值。比ArrayBlockingQueue吞吐量要高,原因是ArrayBlockingQueue添加任务和移除任务用的是同一把锁,而LinkedBlockingQueue分别会有一把锁。
SynchronousQueue是无边界的,队列的size始终为0,可用于与另一个线程交换单个元素。将元素插入队列的线程将被阻塞,直到另一个线程从队列中取出该元素。同样,如果一个线程试图获取一个元素而当前没有元素存在,则该线程将被阻塞,直到一个线程将一个元素插入队列。

当我们要向队列中添加一个元素时,我们需要调用put()方法。该方法将阻塞,直到其他某个线程调用take()方法,表明它已准备好获取一个元素。

SynchronousQueue并不是真正的队列,而是一种管理直接在线程之间移交信息的机制,但我们应该将其视为两个线程之间单个元素的交换点,其中一个线程正在传递一个元素,另一个线程正在获取该元素。

5、CPU密集型和IO密集型

5.1、CPU密集型

通常线线程数 = CPU核数 1
所以为了让它的优势完全发挥出来,避免过多的线程上下文切换。
对于JDK1.8来说,里面增加了一个并行计算,计算密集型的较理想 线程数 = CPU内核线程数*2

5.2 IO 密集型的应用

涉及到大量的网络传输,不仅如此,与数据库,与缓存间的交互也涉及到 IO ,一旦发生 IO ,线程就会处于等待状
线程数 = CPU核心数/(1-阻塞系数) 这个阻塞系数一般为 0.8~0.9 之间
对于双核 CPU 来说,它比较理想的线程数就是 20

6、同步(synchronized)

java中的同步是控制多个线程访问任何共享资源的能力。在多线程概念中,多个线程尝试一次访问共享资源以产生不一致的结果。同步对于线程之间的可靠通信是必需的。

6.1、synchronized的三种使用方式

6.1.1、修饰实例方法,为当前实例加锁,进入同步方法前要获得当前实例的锁。
6.1.2、修饰静态方法,为当前类对象加锁,进入同步方法前要获得当前类对象的锁。
6.1.3、修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。

6.2、synchronized的原理

synchronized是在需要同步代码块前后加入了monitorentermonitorexit这两个指令。
monitorenter指令会获取锁对象,如果获取到了锁对象,就将锁计数器加1,未获取到则会阻塞当前线程。monitorexit指令会释放锁对象,同时将锁计数器减1
所以两个项目使用同一个对象的同一个synchronized普通方法会争抢锁资源,使用两个对象的同一个方法不会争抢。使用两个对象的synchronized静态代码块也会争抢锁资源。
Synchronized锁的是对象头,堆内存中对象主要由三部分组成,1对象头、2实例变量、3填充数据。
对象头里除了有锁信息外还有存储对象的hashCode、分代年龄或GC标志等信息

6.3、JDK1.6后synchronized做了的改进

JDK1.6对对synchronized的优化主要体现在引入了“偏向锁”和“轻量级锁”的概念,同时synchronized的锁只可升级,不可降级

6.3.1 偏向锁

偏向锁的思想是如果一个线程获得了锁,那么就从无锁模式进入偏向模式
如果你的同步代码块很长时间都是同一个线程访问,偏向锁就会提高效率,因为他减少了重复获取锁和释放锁产生的性能消耗。
偏向锁优化了只有一个线程进入同步代码块的情况,当多个线程访问锁时偏向锁就升级为了轻量级锁

6.3.2 轻量级锁

轻量级锁是当多个线程进入同步代码块后,多个线程未发生竞争时一直保持轻量级锁,通过CAS来获取锁。如果发生竞争,首先会采用CAS自旋操作来获取锁,自旋在极短时间内发生,有固定的自旋次数,一旦自旋获取失败,则升级为重量级锁。

6.4 Synchronized唤醒机制

synchronized的等待唤醒是通过notify/notifyAllwait三个方法来实现的,这三个方法的执行都必须在同步代码块或同步方法中进行,否则将会报错。

7、Volatile

Java volatile关键字保证了线程间的可变可见性,这意味着读取和写入在线程间可见。volatile变量是 JVM 保证我们将始终检索到最新值的变量。每次读取 volatile 变量都会从计算机的主内存中读取,而不是从 CPU 缓存中读取,并且每次写入 volatile 变量都会写入主内存,而不仅仅是写入 CPU 缓存.

7.1 Volatile特点

7.1. 1. 不会造成线程的堵塞,也不会保证线程的安全性。
7.1. 2. 可以保证多个线程所看到的值是一致的。
7.1. 3. 禁止指定重排。
7.1. 4. volatile只能保证可见性,不能保证原子性。

7.2 Volatile怎么保证可见性的?

在对volatile修饰的变量进行写操作时,通过编译器生成反汇编指令后,会发现会多一条Lock前缀,就是由于这条Lock前缀所实现的可见性。Lock前缀在多核处理器中会引发下面这两件事情:
Lock指令会将当前处理器缓存行的数据写回到主内存。(ps:每个处理器都有自己的cache缓存,每次缓存中操作的变量都是主内存中变量的拷贝)
一个处理器写回主内存的操作会造成其他处理的缓存无效。

7.3 Volatile 怎么禁止指令重排的?

指令重排主要出现在多线程并发的情况,编译器不考虑多线程之间的语义,多线程在执行的时候可能会出现变量读写顺序问题。
通过volatile读写都加了内存屏障来实现禁止指令重排

7.4 Volatile为什么不能保证原子性

原子的意思代表着——“不可分”;
Java中只有对基本类型变量的赋值和读取是原子操作
在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。例如 i=1是原子性操作,但是ii =1就不是原子性操作。

7.5 为什么没有原子性呢

首先读取volatile变量值给另一变量;
增加变量的值;
把修改后的值写回,有可能在写回到原子的过程中,别的线程拿到的还是旧的值。

8、volatile和synchronized区别

8.1volatile是变量修饰符,而synchronized则作用于一段代码或方法。
8.2volatile只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值, 显然synchronized要比volatile消耗更多资源。
8.3volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
8.4volatile保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存中和公共内存中的数据做同步。
8.5volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化。

修改volatile变量时会强制将修改后的值刷新的主内存中。
修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。
通过这两个操作,就可以解决volatile变量的可见性问题。

9、CAS机制

CAS是英文单词Compare And Swap的缩写,也就是比较并交换。在JavaCAS 底层使用的就是自旋锁 UnSafe类。这是设计并发算法时使用的技术。方法是比较变量的实际值和变量的期望值,如果实际值与期望值匹配,则将变量的实际值交换为传入的新值。
当一个线程对一个变量进行操作时,会创建三个操作数保存该变量的信息,也就是内存地址V,变量原来的值A,要修改的新值B。当该线程对变量操作完毕要把新值保存到变量之前,会先比较A与内存地址V中当前的实际值是否相等,相等才能将变量的值更新为新值B,否则提交失败,并重新尝试。这个重新尝试的过程被称为自旋。

9.1 CAS机制的缺点

CPU开销较大,不适合用于写操作并发量高的场景。
只能保证一个变量的原子性操作,无法保证整个代码块或者说多个变量的原子性。

9.2 UnSafe类

Unsafe类都是直接调用操作系统底层资源执行任务,Unsafe可认为是Java中留下的后门,提供了一些低层次操作,如直接内存访问、线程调度等。

9.3 CAS机制与Synchronized的区别

CAS机制属于乐观锁,乐观的认为程序中的并发情况不那么严重,所以让线程不断进行尝试。
Synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以对于每一个操作都严防死守。它会让没有得到锁资源的线程进入阻塞态,当在争夺到锁资源后恢复为就绪态,这个过程涉及到操作系统用户模式和内核模式的状态,代价比较高。

10、ThreadLocal

Java 中的 ThreadLocal 是除了编写不可变类之外的另一种实现线程安全的方法。线程本地可以被认为是一个访问范围,如会话范围或请求范围。在线程局部,你可以设置任何对象,这个对象对于访问这个对象的特定线程来说是局部的和全局的。
Java ThreadLocal 类提供线程局部变量。它使您能够创建只能由同一线程读取和写入的变量。如果两个线程正在执行相同的代码并且该代码引用了一个 ThreadLocal 变量,那么这两个线程就看不到彼此的局部变量。

10.1 创建一个ThreadLocal

ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();

threadLocalValue.set(1);
Integer result = threadLocalValue.get();
当我们想从这个个线程中使用这个值时,我们只需要调用一个get()或set()方法。简单的说,我们可以想象ThreadLocal将数据存储在一个以线程为keymap里面。
因此,当我们在threadLocalValue上调用get()方法时,我们将获得请求线程的Integer值:

10.2 ThreadLocal实现原理

首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。
因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。

10.3 ThreadLocal 内存泄漏问题

实际上 ThreadLocalMap中使用的 keyThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocalkey 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 keynullvalue
ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 keynull 的记录。如果说会出现内存泄漏,那只有在出现了 keynull 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。

11、Lock

Lock接口从Java 1.5 开始就存在了。它是在java.util.concurrent.lock包中定义的,它提供了广泛的锁定操作。
java.util.concurrent.locks包中有很多Lock的实现类,常用的有ReentrantLockReadWriteLockSynchronized一样都是可重入锁。他需要手动加锁和释放锁。
当线程竞争锁时,他首先会去竞争锁资源,如果没竞争到会通过cas把当前线程放到队列的最尾部。这样极大的增加了吞吐效率,这也是一个不公平锁的由来。
这个队列是一个CLH队列,并不是一个真正的队列,是一个虚拟队列,是通过每个线程指针指向来构成的。当一个线程释放锁资源会唤醒他的下一个线程执行。

11.1 Lock和Synchronized的区别

Synchronized完全包含在方法中。我们可以在不同的方法中使用Lock API lock()和unlock()操作。
Synchronized同步块不支持公平性。一旦释放,任何线程都可以获得锁,并且不能指定优先级。我们可以通过指定公平性属性在Lock API 中实现公平性。它确保等待时间最长的线程可以访问锁。
如果线程无法访问同步块,它就会被阻塞。Lock API 提供了tryLock ()方法。线程仅在可用且未被任何其他线程持有时才获取锁。这减少了线程等待锁的阻塞时间。
处于“等待”状态以获取对同步块的访问权限的线程 不能被中断。Lock API 提供了一种方法lockInterruptibly(),可用于在等待锁时中断线程。
Lock会用CAS比较交换算法来加锁乐观锁
Synchronized 是悲观锁直接加锁。

synchronized当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1.获取锁的线程执行完了该代码块,然后线程释放对锁的占有
2.线程执行发生异常,此时JVM会让线程自动释放锁

Lock是一个接口,接口的实现类ReentrantLock, ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock

lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。

采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。

使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回,在拿不到锁时也不会一直在那等待。

11.2 lock和synchronized 性能

synchronized: 在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好。
ReentrantLock: 提供了多样化的同步,比如有时间限制的同步,可以被Interrupt中断(synchronized的同步是不能Interrupt的)。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。
Lock支持不阻塞的方式获取锁,以这种方式获取锁时会返回获取锁是否成功,当尝试获取锁不成功时,线程并不会阻塞。
synchronized只有一个等待队列,任何情况的阻塞都是放在一个队列里面的,Lock可以创建多个Condition队列,不同的Condition控制不同的条件,每个Condition有单独的一个队列。
Lock比synchronized 使用更灵活

12、execute和submit的区别

executesubmit都属于线程池的方法,execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务。
execute会直接抛出任务执行时的异常,submit会吃掉异常,可通过Futureget方法将任务执行时的异常重新抛出。
execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。

13、JUC包下的常用类

JUC包中的原子操作类可以分为4类。
1、基本类型: AtomicInteger, AtomicLong, AtomicBoolean ;
2、数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;
3、引用类型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;
4、对象的属性修改类型: AtomicIntegerFieldUpdater,AtomicLongFieldUpdater, AtomicReferenceFieldUpdater 。

14、AQS的实现原理

AQS全名:AbstractQueuedSynchronizer,是并发容器。J.U.Cjava.lang.concurrent)下locks包内的一个类。它实现了一个FIFO(FirstInFisrtOut先进先出)的队列。底层实现的数据结构是一个双向链表。
AQS核心是如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

15、CountDownLatch和CyclicBarrier的用法与区别

CountDownLatch是一个非常实用的多线程控制工具类,称之为“倒计时器”,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。CountDownLatch 允许一个或多个线程等待其他线程完成操作。
import java.util.concurrent.CountDownLatch;
public class ThreadRunnableDemo  implements Runnable{
    private CountDownLatch downLatch;

    public ThreadRunnableDemo(CountDownLatch downLatch) {
        this.downLatch = downLatch;
    }

    @Override
    public void run() {
        System.out.printf("Thread %s start\t",Thread.currentThread().getName());
        try {
            Thread.sleep(300);
            System.out.printf("Thread %s stop\n",Thread.currentThread().getName());
            downLatch.countDown();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }


    }
}
package com.jy.lejutaobao.testDemo;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestCountDownLatchDemo {
    public static void main(String[] args) {
        CountDownLatch downLatch=new CountDownLatch(3);
        ExecutorService executor = Executors.newFixedThreadPool(3);
        for(int i=0; i < 3; i  ) {
            executor.submit(new ThreadRunnableDemo(downLatch));
        }

        try {
            downLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executor.shutdown();

        System.out.println("都执行完了.");

    }
}
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
CountDownLatch: 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
CyclicBrrier: N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestCyclicBarrierExample1 {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(5);
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i  ) {
            final int threadNum = i;
            executor.execute(() -> {
                try {
                    System.out.print("线程 = "   threadNum " 开始 \t");
                    if((threadNum 1) % 5==0){
                        System.out.println("\n");
                    }
                    Thread.sleep(1000 threadNum);
                    System.out.print("线程 = "   threadNum " 已完成\t");
                    if((threadNum 1) % 5==0){
                        System.out.println("\n");
                    }
                    barrier.await();

                } catch (Exception e) {

                }
            });
        }

        executor.shutdown();

    }
}

16、信号量Semaphore

信号量通过使用计数器来控制对共享资源的访问。如果计数器大于零,则允许访问。如果它为零,则拒绝访问。计数器计数的是允许访问共享资源的许可。因此,要访问资源,线程必须获得信号量的许可。
线程池分批执行,一批一批执行.
五个一批
Semaphore semaphore = new Semaphore(5);
获取信号量
semaphore.acquire();
释放信号量
semaphore.release();
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class TestSemaphoreDemo {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Semaphore semaphore = new Semaphore(5);
        for(int i = 0;i<20;i  ){
            int finalI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        /*获取信号量*/
                        semaphore.acquire();
                        System.out.println("Thread = "   finalI " 获取acquire");
                        Thread.sleep(1000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    semaphore.release();
                    System.out.println("Thread = "   finalI " 释放release");
                }
            });

        }


    }
}

17、Condition接口及其实现原理

Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
        lock.lock();
        try {
            condition.await();
        } finally {
            lock.unlock();
        }
    }
    public void conditionSignal() throws InterruptedException {
        lock.lock();
        try {
            condition.signal();
        } finally {
            lock.unlock();
        }
}
一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。

17.1 、Condition原理分析

ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列,该队列是Condition对象实现等待/通知功能的关键。下面将分析Condition的实现,主要包括:等待队列、等待和通知
调用Conditionawait()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态
调用Conditionsignal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中

18、fork/join

fork/join作为一个并发框架在jdk7的时候就加入到了我们的java并发包java.util.concurrent中,并且在java 8lambda并行流中充当着底层框架的角色。
先把一个大任务分解(fork)成许多个独立的小任务,然后起多线程并行去处理这些小任务。处理完得到结果后再进行合并(join)就得到我们的最终结果。
当有线程把当前负责队列的任务处理完之后,它还可以从那些还没有处理完的队列的尾部窃取任务来处理,这连线程的空余时间也充分利用了!。

19、为什么线程Thread调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?

JVM执行start方法,会另起一条线程执行threadrun方法,这才起到多线程的效果。 如果直接调用Threadrun()方法,其方法还是运行在主线程中,没有起到多线程效果。
上一篇: Java集合知识
下一篇:设计模式
评论 (0)