多线程常见详解

2021-12-17 22:57:07 浏览数 (1)

代码语言:txt复制
/**
代码语言:txt复制
 * @author Alan Chen
 * @description 线程按顺序执行-方式一
 * @date 2021/2/10
 */
public class ThreadOrder {
    public static void main(String[] args) {
        System.out.println("子线程调用前代码逻辑......");
代码语言:txt复制
        Thread t1 = new Thread(new Runnable() {
代码语言:txt复制
            @SneakyThrows
代码语言:txt复制
            @Override
代码语言:txt复制
            public void run() {
代码语言:txt复制
                Thread.sleep(40);
代码语言:txt复制
                System.out.println("t1 run:");
代码语言:txt复制
            }
代码语言:txt复制
        });
代码语言:txt复制
        Thread t2 = new Thread(new Runnable() {
代码语言:txt复制
            @SneakyThrows
代码语言:txt复制
            @Override
代码语言:txt复制
            public void run() {
代码语言:txt复制
                Thread.sleep(40);
代码语言:txt复制
                t1.join();//表明当前线程需要在t1线程上等待
代码语言:txt复制
                System.out.println("t2 run:");
代码语言:txt复制
            }
代码语言:txt复制
        });
代码语言:txt复制
        Thread t3 = new Thread(new Runnable() {
代码语言:txt复制
            @SneakyThrows
代码语言:txt复制
            @Override
代码语言:txt复制
            public void run() {
代码语言:txt复制
                Thread.sleep(40);
代码语言:txt复制
                t2.join();//表明当前线程需要在t2线程上等待
代码语言:txt复制
                System.out.println("t3 run:");
代码语言:txt复制
            }
代码语言:txt复制
        });
代码语言:txt复制
        t1.start();
代码语言:txt复制
        t2.start();
代码语言:txt复制
        t3.start();
代码语言:txt复制
        System.out.println("子线程调用后代码逻辑......");
代码语言:txt复制
    }
代码语言:txt复制
}

执行结果

代码语言:txt复制
子线程调用前代码逻辑......
代码语言:txt复制
子线程调用后代码逻辑......
代码语言:txt复制
t1 run:
代码语言:txt复制
t2 run:
代码语言:txt复制
t3 run:

线程按顺序执行-方式二

代码语言:txt复制
/**
代码语言:txt复制
 * @author Alan Chen
 * @description 线程按顺序执行-方式一
 * @date 2020-01-13DateTool
 */
public class TestClient {
代码语言:txt复制
    public static void main(String[] args)throws InterruptedException{
代码语言:txt复制
        System.out.println("子线程调用前代码逻辑......");
代码语言:txt复制
        Thread t1 = new Thread(new ThreadTest("t1"));
代码语言:txt复制
        Thread t2 = new Thread(new ThreadTest("t2"));
代码语言:txt复制
        Thread t3 = new Thread(new ThreadTest("t3"));
代码语言:txt复制
        t1.start();
代码语言:txt复制
        t1.join();
代码语言:txt复制
        t2.start();
代码语言:txt复制
        t2.join();
代码语言:txt复制
        t3.start();
代码语言:txt复制
        System.out.println("子线程调用后代码逻辑......");
代码语言:txt复制
    }
代码语言:txt复制
}
代码语言:txt复制
class ThreadTest extends Thread {
代码语言:txt复制
    private String threadName;
代码语言:txt复制
    public ThreadTest(String name) {
代码语言:txt复制
        threadName = name;
代码语言:txt复制
    }
代码语言:txt复制
    @SneakyThrows
代码语言:txt复制
    @Override
代码语言:txt复制
    public void run() {
代码语言:txt复制
        Thread.sleep(40);
代码语言:txt复制
        System.out.println(threadName " run:");
代码语言:txt复制
    }
代码语言:txt复制
}

执行结果

代码语言:txt复制
子线程调用前代码逻辑......
代码语言:txt复制
t1 run:
代码语言:txt复制
t2 run:
代码语言:txt复制
子线程调用后代码逻辑......
代码语言:txt复制
t3 run:

join方法是synchronized,所以需要获取Thread的对象锁才能进入,只有获得了锁才能调用wait放弃对锁的独占并等待再次获取锁。

join方法用线程对象调用,如果在一个线程A中调用另一个线程B的join方法,线程A将会等待线程B执行完毕后再执行。

join 方法是一个阻塞方法,用来进行线程之间的交流。线程 A 调用 线程 B 的 join 方法,则线程 A 将阻塞,线程 B 执行结束后 线程 A

开始执行。

问:join方法的作用?

答:Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。

问:join方法传参和不传参的区别?

答:join方法中如果传入参数,则表示这样的意思:如果A线程中调用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并行执行。需要注意的是,jdk规定,join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。

问:join与start调用顺序问题

答:join方法必须在线程start方法调用之后调用才有意义。这个也很容易理解:如果一个线程都没有start,那它也就无法同步了。因为执行完start方法才会创建线程。

问:join方法实现原理

答:join方法是通过调用线程的wait方法来达到同步的目的的。例如A线程中调用了B线程的join方法,则相当于在A线程中调用了B线程的wait方法,当B线程执行完(或者到达等待时间),B线程会自动调用自身的notifyAll方法唤醒A线程,从而达到同步的目的。

二、在 Java 中 Lock 接口比 synchronized

块的优势是什么?你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?

lock 接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像ConcurrentHashMap

这样的高性能数据结构和有条件的阻塞。Java 线程面试的问题越来越会根据面试者的回答来提问。我强烈建议在你去参加多线程的面试之前认真读一下

Locks,因为当前其大量用于构建电子交易终统的客户端缓存和交易连接空间。

三、在 Java 中 wait 和 sleep 方法的不同?

通常会在电话面试中经常被问到的 Java 线程面试问题。最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。Wait

通常被用于线程间交互,sleep 通常被用于暂停执行。

四、用 Java 实现阻塞队列。

这是一个相对艰难的多线程面试问题,它能达到很多的目的。第一,它可以检测侯选者是否能实际的用 Java

线程写程序;第二,可以检测侯选者对并发场景的理解,并且你可以根据这个问很多问题。如果他用wait()和

notify()方法来实现阻塞队列,你可以要求他用最新的Java5 中的并发类来再写一次。

五、用 Java 写代码来解决生产者——消费者问题

与上面的问题很类似,但这个问题更经典,有些时候面试都会问下面的问题。在 Java

中怎么解决生产者——消费者问题,当然有很多解决方法,我已经分享了一种用阻塞队列实现的方法。有些时候他们甚至会问怎么实现哲学家进餐问题。

六、什么是原子操作,Java 中的原子操作是什么?
6.1 原子操作

原子操作是无法被别的线程打断的操作。要么不执行,要么就执行成功。

例如:x=3是原子操作。过程就是先把工作内存的x赋成3,再把主存的x赋成3。y=x不是原子操作,它涉及在工作内存先把x值读出来,再把这个值赋给y。x 或x=x 1也不是原子操作,它涉及取值,自加和赋值。

6.2 Java 中的原子操作

在Java中,我们可以通过同步锁或者CAS操作来实现原子操作。

CAS是Compare and

swap的简称,这个操作是硬件级别的操作,在硬件层面保证了操作的原子性。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。Java中的sun.misc.Unsafe类提供了compareAndSwapInt和compareAndSwapLong等几个方法实现CAS。

七、Java 中的 volatile 关键是什么作用?怎样使用它?在 Java 中它跟 synchronized 方法有什么不同?

自从 Java 5 和 Java 内存模型改变以后,基于 volatile 关键字的线程问题越来越流行。应该准备好回答关于 volatile

变量怎样在并发环境中确保可见性。

八、多线程有什么用?
8.1 发挥多核 CPU 的优势

随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4 核、8 核甚至 16 核的也都不少见,如果是单线程的程序,那么在双核 CPU

上就浪费了 50%,在 4 核 CPU 上就浪费了 75%。单核 CPU

上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核 CPU

上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核 CPU 的优势来,达到充分利用 CPU 的目的。

8.2 防止阻塞

从程序运行效率的角度来看,单核 CPU 不但不会发挥出多线程的优势,反而会因为在单核 CPU

上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核 CPU 我们还是要应用多线程,就是为了防止阻塞。试想,如果单核 CPU

使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

8.3 便于建模

这是另外一个没有这么明显的优点了。假设有一个大的任务 A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务

A分解成几个小任务,任务 B、任务 C、任务 D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

九、创建线程的方式
9.1 继承Thread类

看jdk源码可以发现,Thread类其实是实现了Runnable接口的一个实例,继承Thread类后需要重写run方法并通过start方法启动线程。继承Thread类耦合性太强了,因为java只能单继承,所以不利于扩展。

9.2 实现Runnable接口

通过实现Runnable接口并重写run方法,并把Runnable实例传给Thread对象,Thread的start方法调用run方法再通过调用Runnable实例的run方法启动线程。所以如果一个类继承了另外一个父类,此时要实现多线程就不能通过继承Thread的类实现。

9.3 实现Callable接口

通过实现Callable接口并重写call方法,并把Callable实例传给FutureTask对象,再把FutureTask对象传给Thread对象。它与Thread、Runnable最大的不同是Callable能返回一个异步处理的结果Future对象并能抛出异常,而其他两种不能。

十、start()方法和 run()方法的区别

只有调用了 start()方法,才会表现出多线程的特性,不同线程的 run()方法里面的代码交替执行。如果只是调用

run()方法,那么代码还是同步执行的,必须等待一个线程的 run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其 run()方法里面的代码

十一、Runnable 接口和 Callable 接口的区别

有点深的问题了,也看出一个 Java 程序员学习知识的广度。Runnable 接口中的 run()方法的返回值是 void,它做的事情只是纯粹地去执行

run()方法中的代码而已;Callable 接口中的 call()方法是有返回值的,是一个泛型,和 Future、FutureTask

配合可以用来获取异步执行的结果。

这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能

做 的 只 是 等 待 这 条 多 线 程 的 任 务 执 行 完 毕 而 已 。 而Callable Future/FutureTask

却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。

十二、CyclicBarrier 和 CountDownLatch 的区别

两个看上去有点像的类,都在 java.util.concurrent 下,都可以用来表示代码运行到某个点上,二者的区别在于:

  • CyclicBarrier 的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch 则不是,某线程运行到某个点上之后,只是给某个数值-1 而已,该线程继续运行。
  • CyclicBarrier 只能唤起一个任务,CountDownLatch 可以唤起多个任务。
  • CyclicBarrier 可重用,CountDownLatch 不可重用,计数值为 0 该CountDownLatch 就不可再用了。
十三、volatile 关键字的作用

一个非常重要的问题,是每个学习、应用多线程的 Java 程序员都必须掌握的。volatile 关键字的作用主要有两个:

1、多线程主要围绕可见性和原子性两个特性而展开,使用 volatile 关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到 volatile

变量,一定是最新的数据。

2、代码底层执行不像我们看到的高级语言----Java 程序这么简单,它的执行是 Java 代 码 --> 字节码 --> 根据字节码执行对应的 C/C

代 码-->C/C 代码被编译成汇编语言-->和硬件电路交互,现实中,为了获取更好的性能 JVM

可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用 volatile 则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率。

从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性。

十四、什么是线程安全

如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。这个问题有值得一提的地方,就是线程安全也是有几个级别的:

14.1 不可变

像 String、Integer、Long 这些,都是 final

类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用。

14.2 绝对线程安全

不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java 中标注自己是线程安全的类,实际上绝大多数都 不 是 线

程 安 全 的 , 不 过 绝 对 线 程 安 全 的 类 , Java 中 也 有 , 比 方

说CopyOnWriteArrayList、CopyOnWriteArraySet

14.3 相对线程安全

相对线程安全也就是我们通常意义上所说的线程安全,像 Vector 这种,add、remove

方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个 Vector、有个线程同时在 add 这个

Vector,99%的情况下都会出现ConcurrentModificationException,也就是 fail-fast 机制。

14.4 线程非安全

这个就没什么好说的了,ArrayList、LinkedList、HashMap 等都是线程非安全的类

十五、Java 中如何获取到线程 dump 文件

死循环、死锁、阻塞、页面打开慢等问题,打线程 dump 是最好的解决问题的途径。所谓线程 dump 也就是线程堆栈,获取到线程堆栈有两步:

1、获取到线程的 pid,可以通过使用 jps 命令,在 Linux 环境下还可以使用ps -ef | grep java

2、打印线程堆栈,可以通过使用 jstack pid 命令,在 Linux 环境下还可以使用 kill -3 pid

另外提一点,Thread 类提供了一个

getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈。

十六、如何在两个线程之间共享数据

通 过 在 线 程 之 间 共 享 对 象 就 可 以 了 , 然 后 通 过 wait/notify/notifyAll

、await/signal/signalAll 进行唤起和等待,比方说阻塞队列 BlockingQueue就是为线程之间共享数据而设计的。

十七、sleep 方法和 wait 方法有什么区别

这个问题常问,sleep 方法和 wait 方法都可以用来放弃 CPU 一定的时间,不同点在于如果线程持有某个对象的监视器,sleep

方法不会放弃这个对象的监视器,wait 方法会放弃这个对象的监视器。

十八、生产者消费者模型的作用是什么

1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用。

2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约。

十九、ThreadLocal 有什么用

简单说 ThreadLocal 就是一种以空间换时间的做法,在每个 Thread 里面维护了一个以开地址法实现的

ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

二十、为什么 wait()方法和 notify()/notifyAll()方法要在同步块中被调用

这是 JDK 强制的,wait()方法和 notify()/notifyAll()方法在调用前都必须先获得对象的锁。

二十一、wait()方法和 notify()/notifyAll()方法在放弃对象监视器时有什么区别

wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。

二十二、为什么要使用线程池

避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。

二十三、怎么检测一个线程是否持有对象监视器

Thread 类提供了一个 holdsLock(Object obj)方法,当且仅当对象 obj 的监视器被某条线程持有的时候才会返回

true,注意这是一个static 方法,这意味着"某条线程"指的是当前线程。

二十四、synchronized 和 ReentrantLock 的区别

synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock是类,这是二者的本质区别。既然

ReentrantLock 是类,那么它就提供了比synchronized

更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock 比 synchronized 的扩展性体现在几点上:

1、ReentrantLock 可以对获取锁的等待时间进行设置,这样就避免了死锁

2、ReentrantLock 可以获取各种锁的信息

3、ReentrantLock 可以灵活地实现多路通知

另外,二者的锁机制其实也是不一样的。 ReentrantLock 底层调用的是Unsafe 的 park 方法加锁,synchronized

操作的应该是对象头中 mark word。

二十五、ConcurrentHashMap 的并发度是什么

ConcurrentHashMap 的并发度就是 segment 的大小,默认为 16,这意味着 最 多 同 时 可 以 有 16 条线程 操 作

ConcurrentHashMap ,这也是ConcurrentHashMap 对 Hashtable 的最大优势。

二十六、ReadWriteLock 是什么

如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程

D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。因为这个,才诞生了读写锁

ReadWriteLock。ReadWriteLock 是一个读写锁接口,ReentrantReadWriteLock 是 ReadWriteLock

接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

二十七、FutureTask 是什么

FutureTask 表示一个异步运算的任务。FutureTask里面可以传入一个 Callable

的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于 FutureTask也是 Runnable

接口的实现类,所以 FutureTask 也可以放入线程池中。

二十八、Linux 环境下如何查找哪个线程使用 CPU 最长

1、获取项目的 pid,jps 或者 ps -ef | grep java

2、top -H -p pid,顺序不能改变这样就可以打印出当前的项目,每条线程占用 CPU 时间的百分比。注意这里

打出的是 LWP,也就是操作系统原生线程的线程号。

使用"top -H -p pid" "jps pid"可以很容易地找到某条占用 CPU 高的线程的线程堆栈,从而定位占用 CPU

高的原因,一般是因为不当的代码操作导致了死循环。最后提一点,"top -H -p pid"打出来的 LWP 是十进制的,"jps

pid"打出来的本地线程号是十六进制的,转换一下,就能定位到占用 CPU 高的线程的当前线程堆栈了。

二十九、什么是多线程的上下文切换

多线程的上下文切换是指 CPU 控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取 CPU 执行权的线程的过程。

三十、如果你提交任务时,线程池队列已满,这时会发生什么

1、如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为

LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务

2、如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中 ,

ArrayBlockingQueue 满 了 , 会 根 据maximumPoolSize

的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继 续

满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy

三十一、Java 中用到的线程调度算法是什么

抢占式。一个线程用完 CPU 之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

三十二、Thread.sleep(0)的作用是什么

由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

三十三、什么是自旋

很多 synchronized

里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然

synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized

的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

三十四、什么是 Java 内存模型

Java 内存模型定义了一种多线程访问 Java 内存的规范。Java 内存模型的几部分内容:

1、Java 内存模型将内存分为了主内存和工作内存。类的状态,也就是类之间共享的变量,是存储在主内存中的,每次 Java

线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,会将最新的值更新到主内存中去。

2、定义了几个原子操作,用于操作主内存和工作内存中的变量

3、定义了 volatile 变量的使用规则

4、happens-before,即先行发生原则,定义了操作 A 必然先行发生于操作B

的一些规则,比如在同一个线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁 unlock 的动作一定先行发生于后面对于同一个锁进行锁定

lock 的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的 happens-before 规则,则这段代码一定是线程非安全的。

三十五、什么是 CAS

CAS,全称为 Compare and Swap,即比较-替换。假设有三个操作数:内存值 V、旧的预期值 A、要修改的值 B,当且仅当预期值 A 和内存值

V 相同时,才会将内存值修改为 B 并返回 true,否则什么都不做并返回 false。当然 CAS 一定要 volatile

变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值 A 对某条线程来说,永远是一个不会变的值 A,只要某次 CAS

操作失败,永远都不可能成功。

三十六、什么是乐观锁和悲观锁
36.1 乐观锁

就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-

替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

36.2 悲观锁

还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像

synchronized,不管三七二十一,直接上了锁就操作资源了。

三十七、什么是 AQS

简单说一下 AQS,AQS 全称为 AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。如果说

java.util.concurrent 的基础是 CAS 的话,那么 AQS 就是整个

Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore 等等都用到了它。AQS

实际上以双向队列的形式连接所有的 Entry,比方说ReentrantLock,所有等待的线程都被放在一个 Entry 中并连成双向队列,前面一个线程使用

ReentrantLock 好了,则双向队列实际上的第一个 Entry开始运行。AQS 定义了对双向队列所有的操作,而只开放了 tryLock 和

tryRelease 方法给开发者使用,开发者可以根据自己的实现重写 tryLock 和 tryRelease 方法,以实现自己的并发功能。

三十八、Hashtable 的 size()方法中明明只有一条语句"return count",为什么还要做同步?

1、同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程 A 在执行Hashtable

的 put 方法添加数据,线程 B 则可以正常调用 size()方法读取Hashtable 中当前元素的个数,那读取到的值可能不是最新的,可能线程

A添加了完了数据,但是没有对 size ,线程 B 就已经读取 size 了,那么对于线程 B 来说读取到的 size 一定是不准确的。而给

size()方法加了同步之后,意味着线程 B 调用 size()方法只有在线程 A 调用 put 方法完毕之后才可以调用,这样就保证了线程安全性

2、CPU 执行代码,执行的不是 Java 代码,这点很关键。Java代码最终是被翻译成机器码执行的,机器码才是真正可以和硬件电路交互的代码。即使你看到

Java 代码只有一行,甚至你看到 Java 代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个。一句"return

count"假设被翻译成了三句汇编语句执行,一句汇编语句和其机器码做对应,完全可能执行完第一句,线程就切换了。

三十九、同步方法和同步块,哪个是更好的选择

同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。

虽说同步的范围越少越好,但是在 Java 虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说

StringBuffer,它是一个线程安全的类,自然最常用的append()方法是一个同步方法,我们写代码的时候会反复 append

字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着 Java 虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此 Java

虚拟机会将多次 append 方法调用的代码进行一个锁粗化的操作,将多次的 append的操作扩展到 append

方法的头尾,变成一个大的同步块,这样就减少了加锁-->解锁的次数,有效地提升了代码执行的效率。

四十、高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

1、高并发、任务执行时间短的业务,线程池线程数可以设置为 CPU 核数 1,减少线程上下文的切换

2、并发不高、任务执行时间长的业务要区分开看:

a)假如是业务时间长集中在 IO 操作上,也就是 IO 密集型的任务,因为 IO操作并不占用 CPU,所以不要让所有的 CPU

闲下来,可以加大线程池中的线程数目,让 CPU 处理更多的业务。

b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换

3、并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考其他有关线程池的文章。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

0 人点赞