多线程面试题

线程的基本概念 

  • 一个程序中可以有多条执行线索同时执行,一个线程就是程序中的一条执行线索,每个程序至少都有一个线程,即main方法执行的那个线程。
  • 线程是进程的一个实体,是CPU调度和分派的基本单位。
  • 一个进程中的线程是可以相互通信的,但不同的进程之间的线程是不能相互通信的。

线程之间的通信:

线程之间的通信是为了避免多个线程对同一个共享变量的争夺

如何通信:wait()、notify()

就是在一个线程进行了规定操作后,就进入等待状态(wait), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify);

 

JDK 1.5新增了Lock接口及其实现类,提供了更为灵活的同步方式。如果是采用Lock对象进行同步,则需要依赖Condition实现线程通信,Condition对象是由Lock对象创建出来的,它依赖于Lock对象。Condition对象中定义的通信方法,与Object类中的通信方法类似,它包括await()、signal()、signalAll()。通过名字就能看出它们的含义了,当通过Condition调用await()时当前线程释放锁并等待,当通过Condition调用signal()时唤醒一个等待的线程,当通过Condition调用signalAll()时则唤醒所有等待的线程。

 

进程概念及与线程区别

  • 一个进程中可以启动多个线程。运行的一个应用程序至少有一个进程。
  • 进程作为系统分配资源的基本单位,而把线程作为cpu调度的基本单位。
  • 多个进程有不同的代码和数据空间,而多个线程则共享数据空间。
  • 操作系统,以多进程形式,允许多个任务同时运行;以多线程形式,允许单个任务分成不同的部分运行;

 

线程的优先级

java 中的线程优先级的范围是1~10,默认的优先级是5。“高优先级线程”会有更大概率先于“低优先级线程”执行(相邻的不明显差别越大发现优先级高的有更大的概率被调用)。

子线程的优先级会继承父线程的优先级,创建它的主线程是守护线程时子线程才会是守护线程

守护线程和用户线程

只要当前JVM实例中尚存任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束是,守护线程随着JVM一同结束工作,守护线程最典型的应用就是GC(垃圾回收器),

当系统只剩下守护进程的时候,java虚拟机会自动退出。

守护线程是用来辅助用户线程的

用户线程可以理解为是系统的工作线程,它会完成这个程序需要完成的业务操作。

 

将线程转换为守护线程可以通过调用 Thread 对象的 setDaemon(true) 方法来实现。

main线程

main线程是由java虚拟机在启动的时候创建的

main线程是一个用户线程(非守护线程)

main线程就是程序的主线程

main线程是在main()方法被调用的时候创建

我们创建的线程都是main线程的子线程

每个进程至少有一个主线程

可以在主线程程序中通过Thread.currentThread()来获取到主线程,主线程的线程名叫main

主线程和子线程

当Java程序启动时,一个线程立刻运行,该线程通常叫做程序的主线程(main thread),它是程序开始时就执行的

子线程都是从主线程的基础上产生的,我们创建的线程都是main线程的子线程

 

多任务概念

任务:一个任务一般对应一个进程,也可能包含好几个进程。就好比一个软件(应用程序)的运行。

 

任务相对进程就如同进程相对线程,例如我们可以同时聊QQ、写bolg、听音乐等等就是多任务。

多任务,多进程,多线程存在的意义

多线程:提高程序的运行效率,最大限度利用CPU资源。

多进程:提高程序的运行效率

多任务:实现同时运行多个软件

对线程同时执行的理解

一个cpu,如何同时执行多段程序呢?

这是从宏观上来看的,cpu一会执行a线索,一会执行b线索,切换时间很快,给人的感觉是a,b在同时执行。

线程的基本状态以及状态之间的关系

     状态:新建,就绪,运行,synchronize阻塞,wait和sleep挂起,结束。wait必须在synchronized内部调用。

     实例化线程,线程被start之前(未被启动)为新建状态,调用线程的start方法后线程进入就绪状态,线程调度系统将就绪状态的线程转为运行状态,遇到synchronized语句时,由运行状态转为阻塞,当synchronized获得锁后,由阻塞转为运行,在这种情况可以调用wait方法转为挂起状态,当线程关联的代码(run方法)执行完后,线程变为结束状态。

 

线程阻塞和线程挂起

 

挂起:一般是主动的,由系统或程序发出

Thread类的sleep方法和Object类的wait方法都能使线程挂起

 

阻塞:一般是被动的,在抢占资源中得不到资源会被阻塞,可以说阻塞是被动的挂起

释放cpu:线程阻塞和挂起都会释放cpu.

 

线程挂起详解:

挂起线程和恢复线程都需要用户态转入内核态去完成,这个状态之间的转换需要相对比较长的时间。

内核态:CPU可以访问内存所有数据, 包括外围设备, 例如硬盘, 网卡. CPU也可以将自己从一个程序切换到另一个程序

用户态:只能受限的访问内存, 且不允许访问外围设备. 占用CPU的能力被剥夺, CPU资源可以被其他程序获取

 

上下文切换

上下文切换是指CPU的控制权由由运行任务转移到另外一个就绪任务时所发生的事件;

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态,从任务保存到再加载的过程就是一次上下文切换。

往往多线程的数量也比cpu的核数多,所以多核cpu也存在线程上下文切换问题。

上下文切换要保存线程的执行状态,并切换线程执行,恢复原线程执行又要加载原线程数据等操作,所以会比较消耗性能

 

如何减少上下文切换:

使用最少线程:避免创建不需要的线程(减少了线程之间对CPU资源的竞争,避免线程阻塞)

无锁并发编程:多线程竞争时,会引起上下文切换,比如重量级锁线程获取不到锁就会线程挂起cpu让出进行上下文切换,Java的Atomic包使用CAS算法来更新数据,而不需要加锁。

jdk1.6及之后的synchronized充分利用了这一点

 

线程切换与程序计数器:

程序计数器会保存下一条要执行的指令,线程切换时也会保存现在线程的状态信息,当线程恢复执行时,程序计数器会恢复到之前保存的值,从而确保线程能够从上一次暂停的地方继续执行。

 

sleep方法说明

sleep方法说明:

Thread.sleep(1000);//Thread类中的静态方法,实现流程:挂起线程,并修改线程状态,用方法中的参数设置一个指定毫秒数的定时器,到达时间后定时器被触发,线程状态被标记为就绪状态,等待调度。

 

Thread.Sleep(0)的作用,就是“触发操作系统立刻重新进行一次CPU竞争”。竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。

 

sleep wait区别

sleep和wait都可以使线程挂起

sleep是Thread类的方法,wait是Object类的方法

sleep方法的调用不需要在synchronized标记的方法或代码块中调用,wait方法必须在synchronized标记的方法或代码块中调用

sleep方法调用后,在自定毫秒后自动恢复线程,wait方法调用后,其他线程notify或notifyAll可以唤醒它,进入就绪状态

sleep方法调用后会让出cpu,不会释放锁(如果是在同步方法/代码块中执行sleep的话);

wait方法调用后会让出cpu,并释放锁

wait、notify和notifyAll方法

特点:

都定义在Object类中,

都使用了final修饰方法,所以无法被子类重写

wait()

wait(long millis)

wait(long millis, int nanos)

后面两个传入了时间参数(nanos表示纳秒),表示如果指定时间过去还没有其他线程调用notify或者notifyAll方法来将其唤醒,那么该线程会自动退出等待状态之后尝试去获取锁。

notify()会随机唤醒一个线程,notifyAll会唤醒所有线程去竞争锁,先争到就先执行,后面的等待前面的释放锁之后进行执行。

 

应用场景:当一个线程需要等待某个条件成立时,它可以调用该对象的wait()方法,使当前线程等待直到其他线程调用同一对象的notify()或notifyAll()方法来唤醒它。例如,假设有一个生产者-消费者问题,其中生产者生产数据并将其放入缓冲区,而消费者从缓冲区中取出数据。当缓冲区为空时,消费者线程可能会调用wait()方法等待生产者放入数据;当生产者放入数据后,它会调用notify()或notifyAll()方法来唤醒等待的消费者线程。

notifyAll的应用场景:1、多个消费者都要进行消费消息,生产者可以调用notifyAll让所有的等待现场都唤醒去消费。

 

如果调用没有指定了超时时间的wait方法后,没有线程去唤醒线程,那这个线程会一直处于挂起状态,只能等待其他线程去唤起或中断这个线程。

notify方法说明

notify和wait方法一样都是Object类中定义的final方法,不能被子类重写,需要在

notify注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。

调用wait方法后终止的情况

  1. 其他线程调用notify()notifyAll():当其他线程持有相同的锁并调用notify()notifyAll()方法时,等待在wait()上的线程会被唤醒。被notify()唤醒的线程是等待集中随机选择的一个线程,而被notifyAll()唤醒的则是等待集中的所有线程。

  2. 线程被中断:如果等待的线程在等待期间被其他线程中断(通过调用该线程的interrupt()方法),那么wait()方法会抛出InterruptedException,从而结束等待状态。线程需要捕获这个异常并处理它。

  3. 发生了未捕获的异常:如果wait()方法在等待期间线程内部抛出了未捕获的异常,那么等待状态也会结束。

没有发生这些情况的话会一直等待。

被notify唤醒的线程会立即执行吗

被唤醒的线程并不会立即继续执行。它必须重新获取监视器锁,这通常意味着它必须等待当前持有锁的线程释放锁(同步方法或代码块执行完)。

 

wait和notify的使用代码举例

public class SharedBuffer {  
    private int[] buffer;  
    private int in = 0; // 下一个数据插入的位置  
    private int out = 0; // 下一个数据取出的位置  
    private int count = 0; // 当前缓冲区中的数据数量  
  
    public SharedBuffer(int size) {  
        buffer = new int[size];  
    }  
  
    // 生产数据  
    public synchronized void produce(int data) throws InterruptedException {  
        // 如果缓冲区已满,则等待  
        while (count == buffer.length) {  
            wait(); // 等待消费者消费数据  
        }  
        // 生产数据  
        buffer[in] = data;  
        System.out.println("Produced: " + data);  
        // 更新插入位置  
        in = (in + 1) % buffer.length;  
        // 数据数量增加  
        count++;  
        // 唤醒等待的消费者线程  
        notify(); // 通知一个等待的消费者线程  
    }  
  
    // 消费数据  
    public synchronized int consume() throws InterruptedException {  
        // 如果缓冲区为空,则等待  
        while (count == 0) {  
            wait(); // 等待生产者生产数据  
        }  
        // 消费数据  
        int data = buffer[out];  
        System.out.println("Consumed: " + data);  
        // 更新取出位置  
        out = (out + 1) % buffer.length;  
        // 数据数量减少  
        count--;  
        // 唤醒等待的生产者线程  
        notify(); // 通知一个等待的生产者线程  
        return data;  
    }  
}  
  
// 生产者线程  
class Producer extends Thread {  
    private SharedBuffer buffer;  
  
    public Producer(SharedBuffer buffer) {  
        this.buffer = buffer;  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 10; i++) {  
            try {  
                Thread.sleep(200); // 模拟生产数据的耗时  
                buffer.produce(i);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}  
  
// 消费者线程  
class Consumer extends Thread {  
    private SharedBuffer buffer;  
  
    public Consumer(SharedBuffer buffer) {  
        this.buffer = buffer;  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 10; i++) {  
            try {  
                int data = buffer.consume();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}  
  
public class ProducerConsumerExample {  
    public static void main(String[] args) {  
        SharedBuffer buffer = new SharedBuffer(5); // 创建一个大小为5的缓冲区  
  
        Producer producer = new Producer(buffer);  
        Consumer consumer = new Consumer(buffer);  
  
        producer.start(); // 启动生产者线程  
        consumer.start(); // 启动消费者线程  
    }  
}

wait的调用要放到whil中吗

wait()方法放在while循环中进行调用是一种常见的做法,但不是必须的。然而,推荐使用while循环来包裹wait()调用,这样可以确保线程在唤醒后仍然会检查等待条件是否满足,从而避免虚假唤醒的问题,虚假唤醒是指线程在没有被其他线程notify()notifyAll()唤醒的情况下自动退出wait()方法的情况。虽然Java规范并不保证这种情况会发生,但为了避免潜在的问题,使用while循环来检查条件是一个好的实践。

 

wait和notify为什么需要在synchronized里面

wait方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入等待队列, 而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。

而对于notify来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里?所以就必须要找到这个对象获取到这个对象的锁,然后到这个对象的等待队列中去唤醒一个线程。

 

如果没有在synchronized中调用,执行抛异常IllegalMonitorStateException

 

总结:

wait方法需要释放锁,并把线程放入锁的等待队列

notify方法要唤醒锁的等待队列中的随机一个线程

他们都依赖于锁机制,所以要放在synchronized中

 

 

wait和notify为什么要定义到Object类中

因为任何一个类对象都可以作为锁对象,锁对象关联了锁的等待队列,那个线程持有锁等相关信息,所以释放锁和唤醒锁相关的线程也都是对自身关联的信息进行操作。这些方法都是锁的功能,而锁又适用用所有对象,所以就定义到所有类的父类Object类中

 

 

java内存模型

(java内存区域:堆内存,方法区,虚拟机栈,本地方法栈,程序计数器)

 

Java内存模型(即Java Memory Model,简称JMM),定义了一种多线程访问Java内存的规范。

 

  • Java内存模型将内存分为了主内存(所有线程共享)和工作内存(每个线程创建时jvm给每个线程开辟的私有内存)。线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,线程间的通信(传值)必须通过主内存来完成。

 

  • 提供了原子性,可见性以及有序性问题的解决方案,

如原子性问题,除了JVM自身提供的对基本数据类型读写操作的原子性(concurrent.atomic包中)外,对于方法级别或者代码块级别的原子性操作,可以使用synchronized关键字或者重入锁(ReentrantLock)保证程序执行的原子性,

工作内存与主内存同步延迟现象导致的可见性问题,可以使用synchronized关键字或者volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。

对于指令重排导致的可见性问题和有序性问题,则可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化

除了靠sychronized和volatile关键字来保证原子性、可见性以及有序性外,JMM内部还定义一套happens-before 原则来保证多线程环境下两个操作间的原子性、可见性以及有序性。

原子性,可见性,有序性

多线程编程中的三个核心概念

原子性

原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。

可见性

可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。

有序性

代码的执行是按顺序依次执行

特性

操作

可见性

可以由final(不会修改)、volatile(强制更新+读取主内存)以及synchronized(在unlock时会刷新所有已修改数据到主内存,lock时会从主内存重新加载数据)实现

有序性

可以由volatile(禁止指令重排序)/synchronized(一个变量最多只能有一个线程对其lock)实现

原子性

通过锁来实现,synchronized可以实现原子性,保证synchronized的代码块串行执行,通过锁来实现资源的排它性,从而实际目标代码段同一时间只会被一个线程执行,进而保证了目标代码段的原子性。

 

指令重排:

重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段

 

happens-before:

即先行发生原则,定义了操作A必然先行发生于操作B的一些规则,比如在同一个线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁unlock的动作一定先行发生于后面对于同一个锁进行锁定lock的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的happens-before规则,则这段代码一定是线程非安全的

可以帮助我们理解和管理多线程之间的可见性和顺序性问题.

除了遵循Happens-Before原则外,还需要确保操作的原子性。

遵循Happens-Before原则编写代码是确保多线程环境下程序正确性的关键步骤,如何遵循:

  1. 理解并应用程序顺序规则
    • 在一个线程内部,确保按照代码顺序执行操作。避免在单个线程内引入可能导致操作顺序不确定性的因素,如复杂的条件分支或循环。
  2. 正确使用监视器锁
    • 当需要同步访问共享数据时,使用synchronized关键字来加锁和解锁。确保在加锁和解锁之间执行对共享状态的修改。
    • 避免在持有锁时执行长时间运行的操作或可能阻塞的操作,以防止其他线程长时间等待锁。
  3. 谨慎使用volatile变量
    • 当需要确保一个变量的写操作对其他线程立即可见时,将该变量声明为volatile
    • 注意,volatile只能保证单个变量的可见性和有序性,对于复合操作或涉及多个变量的操作,还需要其他同步机制。
  4. 正确启动和停止线程
    • 使用Thread对象的start()方法来启动线程,而不是直接调用线程的run()方法。
    • 在线程中执行完所有任务后,通过正常退出或设置某种标志来通知其他线程该线程已经终止。
  5. 利用Happens-Before的传递性
    • 如果操作A先行发生于操作B,操作B先行发生于操作C,那么可以利用传递性来推断操作A先行发生于操作C。这有助于在复杂的程序中推理和证明操作的顺序性。
  6. 避免数据竞争和不一致性
    • 确保对共享状态的访问都是同步的,避免数据竞争。
    • 对于复合操作(即涉及多个步骤的操作),要确保这些步骤作为一个整体是原子的,防止其他线程在中间插入操作。
  7. 使用并发工具类
    • Java并发包提供了许多工具类,如java.util.concurrent包中的类,它们内部实现了适当的同步机制,可以简化并发编程。
    • 优先使用这些工具类而不是手动编写复杂的同步逻辑。

 

主内存:

主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。

工作内存:

主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。

 

主内存与工作内存的数据存储类型以及操作方式

对于一个实例对象中的成员方法而言,如果方法中包含本地变量是基本数据类型(boolean,byte,short,char,int,long,float,double),将直接存储在工作内存的帧栈结构中,但倘若本地变量是引用类型,那么该变量的引用会存储在功能内存的帧栈中,而对象实例将存储在主内存(共享数据区域,堆)中。但对于实例对象的成员变量,不管它是基本数据类型或者包装类型(Integer、Double等)还是引用类型,都会被存储到堆区。至于static变量以及类本身相关信息将会存储在主内存中。需要注意的是,在主内存中的实例对象可以被多线程共享,倘若两个线程同时调用了同一个对象的同一个方法,那么两条线程会将要操作的数据拷贝一份到自己的工作内存中,执行完成操作后才刷新到主内存

 

synchronized和volatile

既然锁和synchronized即可保证原子性也可保证可见性,为何还需要volatile?

答:synchronized和锁需要通过操作系统来仲裁谁获得锁,开销比较高,而volatile开销小很多。因此在只需要保证可见性的条件下,使用volatile的性能要比使用锁和synchronized高得多。

synchronized会造成线程阻塞,volatile不会

 

volatile关键字

volatile是一个类型修饰符,用于修饰共享变量(类的成员变量、类的静态成员变量)

它具有两层语义:

1、保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2、禁止进行指令重排序。

volatile关键字禁止指令重排序有两层意思:

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

volatile并不能保证操作的原子性

 

指令优化:指编译器或解释器在执行程序时对代码进行优化,以提高程序的性能和效率

volatile原理

当线程操作对volatile变量进行写操作时,JMM会将该线程的本地内存中的副本变量刷新到主存中去;当线程对volatile变量进行读操作时,线程先将本地内存共享变量设置为无效,线程就会从主存中读取共享变量的值了。

volitile应用场景

volatile 主要用来解决多线程环境中内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,就无法解决线程安全问题。

配合原子型操作一块使用,如java.util.concurrent.atomic中提供的原子包装类型来保证原子性操作

 

状态变量:由于boolean的赋值是原子性的,所以volatile布尔变量作为多线程中停止标志还简单有效的

 

volatile使用

在变量前写上volatile关键字

 

在日常工作当中volatile大多被用在状态标志的场景当中(标志变量是一个类成员变量所有线程共享),典型的使用场景是用它修饰用于停止线程的状态标记。

volatile应用在双重检测中

 

 

 

CAS操作

CAS即compare and set的缩写,比较并替换,常见于java.util.concurrent中,是构成concurrent包的基础。

 

CAS有三个操作数,内存值M,旧的预期(expect)值E和更新(update)值U。在CAS操作中,只有当M==E时,才会更新U。否则什么都不做。这些操作是作为单个原子操作完成的。

(这就类是数据库的根据version去修改数据,所以CAS是乐观锁思想的一种实现方式)

 

Unsafe,是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。

 

非阻塞算法 :

一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

 

现代的CPU提供了特殊的指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。

利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。

 

CAS使用

java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,都是依靠CAS操作来保证这些操作是原子性操作。

CAS原理

CAS通过Unsafe类实例调用JNI的代码实现的。JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言。而CAS就是借助C来调用CPU底层指令实现的。

Java的CAS会使用cpu上提供的原子指令来实现原子操作。分析concurrent包,会发现一个通用化的实现模式:

 

  1. 首先,声明共享变量为volatile;
  2. 然后,使用CAS的原子条件更新来实现线程之间的同步;
  3. 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

Unsafe类

全限定名是sun.misc.Unsafe,该类是final的不允许继承;(由于JDK的src.zip里面没有sun包的源码,所以我们使用ide查看该类源码只能看到反编译的内容,可以直接下载openjdk。)

构造方法是private的,不允许通过构造方法获取实例;我们通过静态方法getUnsafe()来获取Unsafe实例

Unsafe类提供的功能:

allocateMemory、reallocateMemory、freeMemory分别用于分配内存,扩充内存和释放内存,与C语言中的3个方法对应。

CAS操作,具体实现是使用c++

 

CAS与synchronized

CAS操作只能保证原子性需要配合volatile关键字可以保证volatile修饰的变量的线程安全

 

concurrent包下使用CAS实现的锁是一种乐观锁,synchronized实现的锁是悲观锁

synchronized需要通过操作系统来仲裁谁获得锁,开销比较高,而concurrent包下CAS操作实现的锁是通过CPU级的原子操作来保证原子性,开销比较小。

JDK1.6对synchronized优化后synchronized的偏向锁和轻量级锁也使用到了CAS操作

 

创建线程有几种方式

4种

  1. 继承Thread,由子类复写run方法
  2. 实现Runnable接口创建线程,覆盖接口中的run方法
  3. 实现Callable接口实现call方法(可以有返回值)
  4. 使用执行器(Executor)创建线程池(thread pool),使用线程池创建线程

 

方法一:继承Thread类或或者用Thread的匿名子类。

继承Thread类:

public class MyThread extends Thread {
   public void run(){
     System.out.println("MyThread running");
   }
}
MyThread myThread = new MyThread();
myTread.start();//start()方法进行开启线程

 

匿名子类:

Thread thread = new Thread(){
   public void run(){
     System.out.println("Thread Running");
   }
};
thread.start();

 

方法二:实现接口Runnable,覆盖接口中的run方法,然后传给Thread类

public class MyRunnable implements Runnable {
   public void run(){
    System.out.println("MyRunnable running");
   }
}
Thread thread = new Thread(new MyRunnable());
thread.start();

 

同样也可以给用匿名类

new Thread(new Runnable(){public void run(){System.out.println(“执行了”)}}).start();

方法三:使用Callable和Future创建线程

FutureTask是Future的实现类

class CallableDemo implements Callable<Integer> {
    public Integer call() throws Exception {
        int sum = 0 ;
        return sum;
    }
}
CallableDemo callableDemo = new CallableDemo();
//执行Callable方式,需要FutureTask实现类的支持,用于接收运算结果
FutureTask<Integer> futureTask = new FutureTask<Integer>(callableDemo);
new Thread(futureTask).start();
 Integer sum = futureTask.get();
System.out.println("计算结果:"+sum);

 

方法四:使用Executor来构建线程池创建线程

ThreadPoolExecutor是Executor接口的一个实现类

public class ThreadPoolExecutorTest {
    public static void main(String[] args) {
        //设置核心池大小
        int corePoolSize = 5;
        //设置线程池最大能接受多少线程
        int maximumPoolSize=10;
        //当前线程数大于corePoolSize、小于maximumPoolSize时,超出corePoolSize的线程数的生命周期
        long keepActiveTime = 200;
        TimeUnit timeUnit = TimeUnit.SECONDS;//设置时间单位,秒
        //设置线程池缓存队列的排队策略为FIFO,并且指定缓存队列大小为5
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(5);
        //创建ThreadPoolExecutor线程池对象,并初始化该对象的各种参数
        ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepActiveTime, timeUnit,workQueue);
        //往线程池中循环提交线程
        for (int i = 0; i < 15; i++) {
            MyTask myTask = new MyTask();//创建线程类对象
            executor.execute(myTask);//开启线程
        //待线程池以及缓存队列中所有的线程任务完成后关闭线程池。
        executor.shutdown();
    }
}
 
class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("执行task " );
    }
}

 

Executors类提供了常用的创建线程池的方法

public class ThreadPoolCached {  
    public static void main(String[] args) {  
   ExecutorService cachedThreadPool = Executors.newCachedThreadPool();  
        for (int i = 0; i < 10; i++) {  
            cachedThreadPool.execute(new Runnable() {  
                @Override  
                public void run() {  
                    System.out.println("当前线程"+Thread.currentThread().getName());  
                }  
            });  
        }  
    }  
}

常用的静态方法有

①newSingleThreadExecutor只有一个线程的线程池

②newFixedThreadExecutor(n) 固定数量的线程池

③newCacheThreadExecutor可缓存线程池

④newScheduleThreadExecutor固定数量的线程池,支持定时及周期性任务执行

 

 

创建线程方法对比

实现Runnable相比继承thread类,可以避免单继承的局限性,这两个相比之下实现Runnable接口用的更多

使用线程池是比前两种相对少见的创建线程做法,如果我们的程序需要用到很多生命周期比较短的线程,那么应该使用线程池,线程池中包含了很多空闲线程,而且这些线程的生命周期不需要我们操心。另一个使用线程池的原因是:如果你的代码需要大量的线程,那么最好使用一个线程池来规定总线程数的上限,防止虚拟机崩溃。这样可以限制最大的并发数量。

对于自己不确定有多少线程的操作,一定要使用线程池,不然不但提高不了性能,反而会降低性能,甚至会导致jvm内存满了。

Callable与Runnable相似,但是Callable具有返回值,可以从线程中返回数据。

 

Future接口:用于查询任务执行状态,获取执行结果,或者取消未执行的任务。在ExecutorService框架中,由于使用线程池,所以Runnable与Callable实例都当做任务看待,而不会当做“线程”看待,所以Future才有取消任务执行等接口。

 

FutureTask类:集Runnable、Callable、Future于一身,它首先实现了Runnable与Future接口,然后在构造函数中还要注入Callable对象(或者变形的Callable对象:Runnable + Result),所以FutureTask类既可以使用new Thread(Runnable r)放到一个新线程中跑,也可以使用ExecutorService.submit(Runnable r)放到线程池中跑,而且两种方式都可以获取返回结果,但实质是一样的,即如果要有返回结果那么构造函数一定要注入一个Callable对象,或者注入一个Runnable对象加一个预先给定的结果(个人觉得这作用不大)。

 

Thread类

java.lang.Thread

其实:class Thread implements Runnable

只是Thread提供了一些额外的方法。如:获取线程名,线程ID,线程状态等

Thread类中的run()方法的默认实现是空实现,也就是什么也不做。

Runnable接口

java.lang.Runnable

该接口就定义了一个方法void run()

Callable<V>接口

java.util.concurrent.Callable<V>

只有一个方法V call()

一般情况下是配合ExecutorService来使用的,在ExecutorService接口中声明了若干个submit方法的重载版本:

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

一般情况下我们使用第一个submit方法和第三个submit方法,第二个submit方法很少使用。

Future<V>接口

Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

FutureTask<V>类

RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

如何停止一个线程

正常情况下线程执行完run方法就会结束

为什么要停止一个线程:

 

停止一个线程的方法:

  • 使用stop方法(Thread类的实例方法):不建议使用,强行终止线程,(1、已经过时的方法;2、stop方法会导致代码逻辑不完整:如一个现在还没有执行完run方法就执行了stop方法,run后面的代码就不执行了;3破坏原子性:如果当前线程正在执行synchronized标记的代码块或方法这些操作是原子性的,如果调用stop方法就会强行终止线程释放锁其他线程就会读到这个线程还未处理完的共享的变量);(stop是用Thread对象调用)

 

  • 使用interrupt方法(Thread类的实例方法):这个方法的作用是给目标线程发送一个通知,表示希望它退出。调用此方法会将目标线程的中断标志设置为true,即表示该线程已经被中断过。然而,目标线程如何处理这一中断信号完全取决于线程自身的逻辑。中断线程,建议不要用。线程需要定期检查自己的中断状态,并在适当的时候退出。如果线程处于阻塞状态(如sleep、wait、join等),调用interrupt会抛出InterruptedException,线程可以在捕获这个异常后退出。
  • 3使用退出标志:使用volatile修饰的成员boolean变量作为退出标志,建议让程序的run方法正常执行完,如
class TestRunnable implements Runnable{  
    volatile boolean isRunning = true;  
    int count = 0;  
    public void run() {  
        while(isRunning){  
            System.out.print("\n RunThread "+ count++);  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                // TODO Auto-generated catch block  
                e.printStackTrace();  
            }  
        }    
    }  
}  
  
  
public class Main {  
    public static void main(String args[]){  
        TestRunnable run = new TestRunnable();  
        Thread thread = new Thread(run,"runnable");  
        thread.start();  
        try {  
            Thread.sleep(5000);  
        } catch (InterruptedException e) {  
            // TODO Auto-generated catch block  
            e.printStackTrace();  
        }  
        System.out.print("\n parpare to stop the thread ");  
        run.isRunning = false;       
    }  
}

 

池化技术

就是保存部分资源,使用的时候从池中获取,用完再还到池中,避免重复的创建与销毁浪费性能

常见的池化技术有:对象池,线程池,连接池等

对象池

对象池通过复用对象减少创建对象,垃圾回收的开销;

 

线程池

线程池是用来管理线程,合理重复利用线程的技术,可以避免重复创建销毁线程

(线程池属于对象池,和对象池有一样的特征也就是为了最大限度复用对象,这里是线程对象。)

使用线程池的好处:

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池的原理

 一个线程池包括以下四个基本组成部分:

   1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;

   2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;

   3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;

   4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

 

线程池在还没有任务到来之前,创建一定数量的线程,放入队列中。这些线程都是处于睡眠状态,即均未启动,不消耗CPU,而只是占用较小的内存空间。当请求到来之后,线程池给这次请求分配一个空闲线程,把请求传入此线程中运行,进行处理。当预先创建的线程都处于运行状态,即预制线程不够,线程池可以自由创建一定数量的新线程,用于处理更多的请求。当系统比较闲的时候,也可以通过移除一部分一直处于停用状态的线程。

 

线程池的饱和策略

当任务过来发现工作队列满了并且已经到了最大线程数,会触发饱和策略

线程池的作用

降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行。

提高线程的可管理性:使用线程池可以进行对线程数量控制、对线程的生命周期管理,统一的分配、调优和监控。

线程池的应用场景

如果我们的程序需要用到很多生命周期比较短的线程,那么应该使用线程池,线程池中包含了很多空闲线程,而且这些线程的生命周期不需要我们操心。

如果你的代码需要大量的线程,那么最好使用一个线程池来规定总线程数的上限,防止虚拟机崩溃。这样可以限制最大的并发数量。

对于自己不确定有多少线程的操作,一定要使用线程池,不然不但提高不了性能,反而会降低性能,甚至会导致jvm内存满了。

线程池的参数

//核心线程数量

 int corePoolSize = 5;

//最大线程数量

int maximumPoolSize=10;

//线程空闲时间

long keepActiveTime = 200;

//阻塞队列

BlockingQueue<Runnable> workQueue

//线程池创建线程使用的工厂

threadFactory threadFactory

//线程饱和策略

RejectedExecutionHandler handler

 

如何监控线程池

监控

高并发下的线程池,最好能够监控起来。可以使用日志、存储等方式保存下来,对后续的问题排查帮助很大。

 

通常,可以通过继承ThreadPoolExecutor,覆盖beforeExecute、afterExecute达到对线程行为的控制和监控。

beforeExecute(Thread t, Runnable r) 任务执行之前,记录任务开始时间

afterExecute(Runnable r, Throwable t) 任务执行之后,计算任务结束时间。统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、已完成任务数量、任务总数、队列里缓存的任务数量、池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否关闭、线程池是否终止信息

监控常用的方法:

getTaskCount():线程池已执行和未执行的任务总数

getCompletedTaskCount():已完成的任务数量

getPoolSize():线程池当前线程数量

getActiveCount():当前线程池中正在执行任务的线程数

getCorePoolSize();配置的核心线程数量

getMaximumPoolSize();配置的最大线程数量

getLargestPoolSize();线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过

getQueue().size():当前排队线程数

 

可以结合其他框架进行监控比如集成Cat

如何创建线程池

可以使用调用ThreadPoolExecutor类的构造函数创建线程池

可以调用Executors类的静态方法生成各种类型的ExecutorService线程池

ThreadPoolExecutor类创建线程

ThreadPoolExecutor常用方法

execute():提交Runnable类型任务给线程池运行,没有返回值

submit():提交Callable或者Runnable类型的任务,能够返回一个Future类型的对象

shutdown():线程池的状态修改为SHUTDOWN状态,已有的任务能执行完,新任务执行拒绝策略

shutdownNow():线程池的状态修改为STOP状态,试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,会返回那些未执行的任务。(调用Thread.interrupt()方法来停止线程,但这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的,所以可能会有很多正在执行的线程还是执行完了)

getTaskCount():线程池已执行和未执行的任务总数

getCompletedTaskCount():已完成的任务数量

getPoolSize():线程池当前线程数量

getActiveCount():当前线程池中正在执行任务的线程数

getCorePoolSize();配置的核心线程数量

getMaximumPoolSize();配置的最大线程数量

getLargestPoolSize();线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过

 

 

shutdown

使当前未执行的线程继续执行,新过来的任务触发拒绝策略

shutdownNow :    

中断正在执行的任务,使用list<Runnable>队列来存储未运行的任务,并返回

  1.  当在Runnable中使用 if(Thread.currentThread.isInterruptd() == true)来判断当前线程的中断状态,,中断所有的任务task,并且抛出InterruptedException异常,而未执行的线程不再执行,从任务队列中清除。  

 

  1. 如果没有if语句,则池中运行的线程直到执行完毕,而未执行的不再执行,从执行队列队列中删除。

 

execute方法

我们创建的任务通过execute方法来交给线程池进行处理:

判断线程池中的线程数量是否小于核心线程数量,是的话就添加核心线程执行任务

如果队列可以放使用offer方法放到尾部

 

线程池流程

使用 ThreadPoolExecutor类创建线程池处理任务流程(其实就是execute方法执行流程)

当提交一个新的任务到线程池(就是执行一次excute方法)

判断当前池中线程数是否比构造函数参数规定的核心线程数量少,如果少的话就新建一个核心线程执行任务(当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理),如果不少于且核心线程都在忙碌就判断工作队列(阻塞队列)是否已满(如果虽然达到了核心线程最大数量但有空闲的核心线程,空闲的会处理这个新任务),如果工作队列没有满,则将新提交的任务存储在这个工作队列里,如果工作队列满了就试着创建一个新的普通线程,如果线程池已达到最大线程数量了则交给饱和策略来处理这个任务。

对于非核心线程会检查其空闲时间,达到设定值会释放掉空闲的非核心线程,核心线程一般是不回收的,也可以通过参数配置可以回收

 

 

线程池的阻塞队列

整体分为有界队列和无界队列和同步移交

 

ArrayBlockingQueue :由数组结构组成的有界阻塞队列。(先进先出,有界,适合需要控制并发线程数量的场景)

LinkedBlockingQueue :由单向链表结构组成的阻塞队列(可以作为有界也可做无界,适用于那些任务提交频率较高,且不希望因为队列满而阻塞提交的场景。)。(先进先出,newCachedThreadPool中使用)

PriorityBlockingQueue :支持优先级排序的无界阻塞队列。(可以根据元素的优先级进行排序,适用于那些需要按照任务优先级执行的场景,例如某些关键任务需要优先处理)

DelayQueue:使用优先级队列实现的无界阻塞队列。

SynchronousQueue:同步移交队列,不存储元素的阻塞队列。(适合于传递性场景做交换工作,即生产者的线程和消费者的线程同步传递某些信息、事件或者任务)

LinkedTransferQueue:由链表结构组成的无界阻塞队列。

LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

 

常用的是:

ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue,SynchronousQueue

 

 

线程池为什么使用阻塞队列

阻塞队列主要是用于生产者-消费者模型的情况。

阻塞队列特点:

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。使得在线程不至于一直占用cpu资源。

 

如果不使用阻塞队列就需要在普通队列的基础上再去实现线程的挂起和唤醒

阻塞队列在线程池中的作用:

  • 通过阻塞的方式保证任务队列中没有任务时,获取任务的线程会进入等待状态,从而避免无效的轮询和资源的浪费;
  • 是当队列满时,插入任务的线程也会被阻塞避免被插入。

 

ArrayBlockingQueue和LinkedBlockingQueue的区别

数据结构不同:

数组实现和单向链表实现(只能头取元素,尾巴加元素)

队列大小初始化方式不同

  • ArrayBlockingQueue实现的队列中必须指定队列的大小;
  • LinkedBlockingQueue实现的队列中可以不指定队列的大小,但是默认是Integer.MAX_VALUE(相当于无界)

 

加锁(ReentrantLock)不同:

  • ArrayBlockingQueue实现的队列中的锁是没有分离的,即生产和消费用的是同一个锁;(在大多数情况下,插入和删除操作是交替进行的,而不是大量并发进行的。因此,使用单一锁可以避免锁分离带来的额外开销。)
  • LinkedBlockingQueue实现的队列中的锁是分离的,即生产用的是putLock,消费是takeLock

在生产或消费时操作不同

 

LinkedBlockingQueue在大多数并发的场景下吞吐量比ArrayBlockingQueue高,

  • 链表可以更快的进行插入和删除
  • 如果是无界的可以确保任务能够迅速地被添加到队列中,而不会因队列满而导致任务被拒绝或延迟。
  • 插入和删除是不一样的锁,可以提高并发性能,减少竞争

 

SynchronousQueue:

同步移交队列(需要一个线程调用put方法插入值,另一个线程调用take方法删除值),插入操作必须等待移除操作,反之亦然

同步队列没有任何内部容量

无缓冲无界等待队列,超出核心线程个数的任务时,创建新的线程执行任务,直到线程数达到最大线程数,触发拒绝策略,可缓存任务数:0。

直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。

 

PriorityBlockingQueue:

优先级队列,无限容量的阻塞队列,但是容量是随着放入的元素空间不够来逐步扩容的。

线程安全的,在入队出队时使用同一把锁,,在扩容时先解锁,再使用cas原子操作,再重新获取锁。

 

优先级规则:

元素需要实现Comparable接口实现compareTo方法,或者使用队列构造方法时传入Comparator比较器,根据提供的比较规则定义优先级

扩容:

 

并不是说元素一入队就会按照排序规则被排好序,而是只有通过调用take、poll方法出队或者drainTo转移出的队列顺序才是被优先级队列排过序的。所以通过调用 iterator() 以及可拆分迭代器 spliterator() 方法返回的迭代器迭代的元素顺序都没有被排序。

采用了平衡二叉堆的方式来存放,数组是一个一维数据结构,但是它的存储顺序则是按二叉树从上到下,从左到右的顺序依次存放到数组中的。该平衡二叉树有一个根节点即queue[0]也是最小的元素, 根节点下面有两个子节点,左子节点对应queue[1],右子节点对应queue[2],然后左右子节点又各自有自己的两个子节点,依次类推就将该二叉树与数组对应起来了。平衡二叉堆中的每一个节点都 小于等于 它的两个子节点。

 

ArrayBlockingQueue

构造函数必须指定长度,默认使用非公平,可以通过构造参数设置是否公平,如果设置为公平则任务的执行按照先加入的先执行的FIFO顺序执行

构造方法:

创建指定长度的Object数组,创建指定公平性的ReentrantLock

使用:

当核心线程数创建完,再来任务会通过offer方法添加进队列,添加过程使用ReentrantLock进行了lock(finally中unlock),

 

饱和策略

当线程池线程数量达到最大线程数量并且阻塞队列已经满了的情况下会使用饱和策略来处理接下来的任务

 

  • 丢任务抛出异常(默认)(RejectedExecutionException)(用于可以捕获异常处理的场景)
  • 丢任务不抛异常(任务丢失不敏感的场景)
  • 将最早进入队列的任务删(队头任务),之后重试执行该任务的提交(如果再次失败,则重复此过程)(用于任务队列比较重要的场景)
  • 由调用线程(提交任务的线程)处理该任务 (可以避免任务丢失,但可能会影响调用者的性能)

如果任务非常重要且不能丢失,那么由调用线程处理线程策略可能更合适;如果系统对性能要求较高,且可以容忍一定的任务丢失,那么丢任务不抛异常策略可能更合适。

你可以自定义自己的策略,比如将任务持久化到一些存储中。

如何自定义饱和策略

 

线程池优化

以下只是基本思想,如果真的需要精确的控制,还是需要上线以后观察线程池中线程数量跟队列的情况来定。

 

核心线程数选择

cpu密集型:cpu核数,避免上下文切换

IO密集型:2* CPU 核数

 

公式:线程数 = CPU可用核心数/(1 - 阻塞系数),其中阻塞系数的取值在0和1之间。

线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

如何查看cpu核数:Runtime.getRuntime().availableProcessors();

 

任务被阻塞的时间大于执行时间,即该任务是IO密集型;如数据库数据交互、文件上传下载、网络数据传输等等

小于就是cpu密集型。如:复杂的算法

 

 

最大线程数选择

IO密集型

IO 密集型任务应配置尽可能多的线程,因为 IO 操作不占用 CPU,不要让 CPU 闲下来,应加大线程数量,如配置2* CPU 核数 +1。

CPU密集型

可以考虑少些线程减少线程调度的消耗。如配置cpu核数+1个线程的线程池。

阻塞队列选择

有界队列:

建议使用有界队列,有界队列可以避免任务太多占用太多资源内存溢出等,有界队列的大小根据自己系统的访问量设置,

 

无界队列:

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

先进先出的任务可以选择ArrayBlockingQueue

 

拒绝策略的选择

  • 丢弃任务并抛异常:

特点:及时反馈程序运行状态;场景:如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时通过异常发现。

  • 丢弃任务不抛异常:

场景:一些无关紧要的业务采用此策略。

  • 丢弃队列队头的任务,尝试执行该任务:

场景:如果业务允许丢失老任务

  • 由调用线程处理该任务:

般在不允许失败的、对性能要求不高、并发量较小的场景下使用,由于是调用者线程自己执行的,当多次提交任务时,就会阻塞后续任务执行,性能和效率自然就慢了。

 

空闲时间的设置

如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。

 

线程工厂的设置

如果不需要特殊的操作使用默认的Executors.defaultThreadFactory()就可以,如果想指定线程的名称,优先级,是否为守护线程或者自定义一些其他内容可以自定义ThreadFactory

 

并发量大,执行时间短的任务:

可以设置的队列大一点,核心线程少一点,避免线程的切换

ThreadPoolExecutor应用实例

如:

ThreadPoolExecutor提供了多个构造方法来创建线程池

大致就是:调用构造方法,然后向execute方法中传入实现Runnable接口的实例来执行实例的run方法(也就是任务)

public class ThreadPoolExecutorTest {
    public static void main(String[] args) {
        //设置核心池大小
        int corePoolSize = 5;
        //设置线程池最大能接受多少线程
        int maximumPoolSize=10;
        //当前线程数大于corePoolSize、小于maximumPoolSize时,超出corePoolSize的线程数的生命周期
        long keepActiveTime = 200;
        //设置时间单位,秒
        TimeUnit timeUnit = TimeUnit.SECONDS;
        //设置线程池缓存队列的排队策略为FIFO,并且指定缓存队列大小为5
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(5);
        //创建ThreadPoolExecutor线程池对象,并初始化该对象的各种参数
        ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepActiveTime, timeUnit,workQueue);
        //往线程池中循环提交线程
        for (int i = 0; i < 15; i++) {
            //创建线程类对象
            MyTask myTask = new MyTask(i);
            //开启线程
            executor.execute(myTask);
            //获取线程池中线程的相应参数
            System.out.println("线程池中线程数目:" +executor.getPoolSize() + ",队列中等待执行的任务数目:"+executor.getQueue().size() + ",已执行完的任务数目:"+executor.getCompletedTaskCount());
        }
        //待线程池以及缓存队列中所有的线程任务完成后关闭线程池。
        executor.shutdown();
    }
}
/**
 *线程类
 */
class MyTask implements Runnable {
    private int num;
    public MyTask(int num) {
        this.num = num;
    }
    @Override
    public void run() {
        System.out.println("正在执行task " + num );
        try {
            Thread.currentThread().sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("task " + num + "执行完毕");
    }
    /**
     * 获取(未来时间戳-当前时间戳)的差值,
     * 也即是:(每个线程的睡醒时间戳-每个线程的入睡时间戳)
     * 作用:用于实现多线程高并发
     * @return
     * @throws ParseException
     */
    public long getDelta() throws ParseException {
        //获取当前时间戳
        long t1 = new Date().getTime();
        //获取未来某个时间戳(自定义,可写入配置文件)
        String str = "2016-11-11 15:15:15";
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        long t2 = simpleDateFormat.parse(str).getTime();
        return t2 - t1;
    }
}

 

Executors类创建线程池

Executors类里面提供了一些静态工厂,生成一些常用的线程池。以下是常用的静态方法

Executors类 调用这些方法来创建线程池

①newSingleThreadExecutor

单个线程的线程池,即线程池中每次只有一个线程工作(最大线程数量和核心线程数量都是1),单线程串行执行任务

这个线程处理完一个任务后接着处理下一个任务,若该线程出现异常,将会有一个新的线程来替代。

用于串行执行任务,顺序执行,不需要并发执行的场景。

该线程池使用的LinkedBlockingQueue链表阻塞队列,阻塞队列大小为Integer.MAX_VALUE

为什么使用LinkedBlockingQueue:

  • 是先入先出的符合顺序执行的功能要求
  • 队列大小默认是Integer.MAX_VALUE可以满足一个线程也不会造成线程被拒绝
  • 读锁和写锁分离,可以提供并发处理能力

 

②newFixedThreadPool(n)

固定数量的线程池(核心线程数量和最大线程数量一样),每提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行

用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。

该线程池使用的LinkedBlockingQueue链表阻塞队列,阻塞队列大小为Integer.MAX_VALUE

 

③newCacheThreadPool(推荐使用)

可缓存线程池,核心线程数量为0,最大线程数量为Interger. MAX_VALUE

如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。

只要有请求到来,就必须要找到一条工作线程处理他,有空闲线程就使用空闲线程处理,如果当前没有空闲的线程,那么就会再创建一条新的线程。

在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。

它比较适合处理执行时间比较短的任务(可以在空闲时间范围内复用线程);

该线程池使用的SynchronousQueue同步移交队列,因为最大线程数量足够多不需要队列来存任务

CachedThreadPool被称为可缓存的线程池,主要是因为它能够根据任务需求动态地创建和销毁线程,实现线程资源的有效利用和缓存。

 

④newScheduleThreadPool(n)

固定数量的线程池,支持定时和周期性的执行线程

核心线程数量为设置的值,最大线程数量是Integer.MAX_VALUE

适用于需要按照预定时间或周期执行任务的场景。

该线程池使用的DelayedWorkQueue延迟队列,这个队列是一个无界队列

 

⑤WorkStealingPool(n)

使用多个工作队列来减少线程间的竞争,适用于大量并行计算任务的场景,对于有顺序要求或任务间有强依赖关系的不适用。
我们可以传入一个值作为核心线程数量(通常用cpu的核心数),最大线程数量和核心线程数量一致,使用的队列是ForkJoinPool.WorkQueue,它不是为了存储大量的任务而设计的,而是作为工作窃取算法的一部分来使用的。因此,队列的长度并不是由用户直接设置的。实际上,这个队列的长度是动态调整的,取决于当前线程池中的线程数和任务提交速率。

WorkStealingPool还能够根据可用的处理器核心数动态调整线程个数。如果不主动设置并发数,它会以当前机器的CPU处理器个数作为线程个数。

由于WorkStealingPool采用的是并行处理方式,它并不能保证任务执行的顺序。

适用于需要充分利用多核处理器优势、提高程序并发处理能力的场景。

 

不建议使用Executors来创建线程池:

FixedThreadPool 和 SingleThreadPool:

允许的请求队列(底层实现是LinkedBlockingQueue)长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM

CachedThreadPool 和 ScheduledThreadPool

允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

 

实例:

向ExecutorService类型实例的execute方法传实现Runnable接口的实现类

ublic class ThreadPoolCached {  
    public static void main(String[] args) {  
   ExecutorService cachedThreadPool = Executors.newCachedThreadPool();  
        for (int i = 0; i < 10; i++) {  
            cachedThreadPool.execute(new Runnable() {  
                @Override  
                public void run() {  
                    System.out.println("当前线程"+Thread.currentThread().getName());  
                }  
            });  
        }  
    }  
}  

 

Executors和ThreadPoolExecutor关系

Executors的newSingleThreadExecutor是调用的ThreadPoolExecutor,核心线程数和最大线程数设置的1,任务队列使用LinkedBlockingQueue长度为Integer.MAX_VALUE(2^31 - 1)

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

 

newFixedThreadPool核心线程数和最大线程数为指定的值,任务队列使用LinkedBlockingQueue长度为Integer.MAX_VALUE

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

newCachedThreadPool核心线程数为0,最大线程数为Integer.MAX_VALUE,空闲时间设置的60s,由于核心线程数为0,线程空闲后60s就会被释放

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

newScheduledThreadPool指定核心线程数量,可以延时执行任务(由执行时间排序任务在阻塞队列,定时任务获取最考前的比较时间实现)和周期性重复执行任务:

ScheduledThreadPoolExecutor继承了ThreadPoolExecutor

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS,
              new DelayedWorkQueue());
    }

 

ThreadPoolTaskExecutor与ThreadPoolExecutor

ThreadPoolTaskExecutor是spring core包中,ThreadPoolExecutor是JDK中的JUC包中的,ThreadPoolTaskExecutor是对ThreadPoolExecutor进行了封装处理。使我们更加简便的使用ThreadPoolExecutor

 

ThreadPoolTaskExecutor默认参数

corePoolSize核心池默认大小为1

maximumPoolSize最大线程数量默认Integer的最大数量2^31-1

keepAliveSeconds超过核心线程数量的线程空闲生存时间,默认60秒

阻塞队列默认使用LinkedBlockingQueue,长度为Integer.MAX_VALUE即2^31-1

Eexecutor框架

Executor:一个接口,其定义了一个接收Runnable对象的方法executor

ExecutorService:Executor子接口,定义了终止、提交,执行任务、跟踪任务返回结果等方法

AbstractExecutorService:ExecutorService执行方法的默认抽象类实现

ThreadPoolExecutor:实现了AbstractExecutorService的类,线程池,是线程池的真正实现,他通过构造方法的一系列参数,来构成不同配置的线程池。

Executors:Object的子类,提供了很多静态方法生成各种类型的ExecutorService线程池实例

我们项目中线程池的使用

使用的spring的ThreadPoolTaskExecutor,定义一个实现了Runnable接口的类,实现run方法,传给ThreadPoolTaskExecutor对象的execute方法

对象池

从java5.0开始,java虚拟机在启动的时候会实例化9个对象池,分别是:8中基本数据类型的包装类对象和String对象,主要还是为了效率问题。包装类池和String池原理一样。

对象池的存在就是为了避免频繁的创建和销毁对象而影响系统性能。

String对象池

  • String s = "abc";这时首先会去池里找,如果有,直接返回池中对象的引用,如果没有,会先在池中创建一个,然后返回引用,从池直接返回引用

所以String s1 = "abc"; String s2 = "abc"; s1==s2为true

  • String s = new String("abc"),不管池里有没有,都会在堆内存中(池子外面)创建一个String对象,此时负责检查并维护String s1 = new String("abc"); String s2 = new String("abc");

s1==s2为false,用equals比较为true.第一次创建对象发现池里面没有就放到里面一个,然后复制一份到堆内存,把堆内存中对象地址给变量(第一次创建两个对象一个是abc一个是new),第二次检查发现有了就去复制一份到堆内存(创建了一个对象)

String池,堆内存中创建的对象为池里那个对象的一个拷贝(副本)

 

 

数据库连接池

概念:

是用于控制数据库连接数量,对数据库连接进行分配,管理,释放等功能的池话技术。

可以对数据库连接进行重复利用

更为重要的是我们可以通过连接池的管理机制监视数据库的连接的数量﹑使用情况,为系统开发﹑测试及性能调整提供依据。释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。

 

常用的数据库连接池:C3P0、DBCP、HikariCP、Druid、Tomcat JDBC Pool

  • HikariCP:专为高并发场景而设计,性能优越,具有最快的初始化速度和最小的延迟,支持JDBC4 API。但是,由于需要更多的JVM资源,可能会造成资源消耗问题。
  • C3P0:一个开放源代码的JDBC连接池,它支持JDBC3规范和JDBC2的标准扩展,能够自动维护连接池,性能较好。但是,配置过于复杂,容易造成资源浪费。
  • DBCP(Database Connection Pool):一个依赖Jakarta commons-pool对象池机制的数据库连接池。
  • Tomcat JDBC Pool:由Apache Tomcat的开发人员创建,与Tomcat服务器集成良好,支持高度定制化配置。
  • Druid:支持JDBC和Oracle驱动程序,全面的性能监测,对等分布式,具有强大的扩展功能和高度定制化配置。

springBoot默认使用的HikariCP

 

常用到的概念:

最小连接--应用最小维持的连接数。跟线程池的核心线程数量一个作用。

最大连接数--应用能够使用的最多的连接数

 

c3p0与dbcp区别

  1. dbcp没有自动回收空闲连接的功能
  2. c3p0有自动回收空闲连接功能

C3P0提供最大空闲时间,DBCP提供最大连接数。

前者当连接超过最大空闲连接时间时,当前连接就会被断掉。DBCP当连接数超过最大连接数时,所有连接都会被断开

 

DBCP有着比C3P0更高的效率,但是实际应用中,DBCP可能出现丢失

连接的可能,而C3P0稳定性较高。因此在实际应用中,C3P0使用较为广泛。

c3p0可以自动回收连接,dbcp需要自己手动释放资源返回。

hibernate推荐使用c3p0 ,spring推荐dbcp

 

编写数据库连接池

编写连接池需实现java.sql.DataSource接口。

ThreadLocal类

所在包:java.lang

介绍:ThreadLocal类操作的是Thread的成员变量threadLocals,该变量是ThreadLocalMap类型的,用于存储线程范围的共享变量

ThreadLocal的三大主要用途:

  • 保存线程上下文信息,在任何地方都可以获取;
  • 实现线程安全,避免同步带来的性能损失;因为每个线程都拥有自己的数据副本,因此不会出现线程间的竞争和冲突
  • 以及实现线程之间数据隔离。

应用场景

数据库连接、Session:最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。确保每个线程都使用自己的数据库连接或 Session,从而避免了多线程之间的资源冲突。

线程内的数据传递:这些数据只在当前线程中有效,线程内的任何方法都可以方便地访问这些数据

线程上下文信息的存储:一些与其自身相关的上下文信息,例如用户ID、事务ID等。这些信息是线程特有的,不应该被其他线程访问或修改。使用 ThreadLocal 可以方便地存储和获取这些线程上下文信息。

 

数据库连接:以保证事务

先从threadlocal里面拿的,如果threadlocal里面有,则用,保证线程里的多个dao操作,用的是同一个connection,以保证事务。(如果每次都是从连接池中取就很有可能不是同一个数据库连接)

如果新线程,则将新的connection放在threadlocal里,返回新连接。

 

session管理:存放线程独有的全局数据

用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。

这样可以方便获取,避免参数传递,直接从ThreadLocal获取

 

spring中的应用

ThreadLocal在Spring中发挥着重要的作用,在管理request作用域的Bean、事务管理(事务的状态,数据库连接)、任务调度(保存上下文信息,避免异步任务的调用打乱后面的执行逻辑)、AOP(保存上下文信息,避免代理方法的执行打乱执行逻辑)等模块都出现了它们的身影;非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”

 

源码说明

set方法

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

get方法是获取当前线程t的threadLocals属性,该属性类型为ThreadLocalMap,所以说theadLocal存储的数据就是存储在了当前线程的一个ThreadLocalMap类型的成员变量中。

ThreadLocalMap内部也是维护了一个Entry数组来存储数据,默认数组长度是16,存储的key为当前ThreadLocal对象,value是我们要存的值

(数组位置计算方法:取hash值与数组长度-1做和运算

int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);)

 

ThreadLocalMap类是ThreadLocal的一个静态内部类

ThreadLocal原理

ThreadLocal类中有一个静态内部类ThreadLocalMap(其类似于Map),ThreadLocalMap中元素的key为当前ThreadLocal对象,而value是存入值对象。Thread类中成员变量threadLocals就是ThreadLocal.ThreadLocalMap类型;所以我们在当前线程中对ThreadLocal的操作都是对当前线程中一个属性指向的对象进行操作,是线程私有的。

ThreadLocalMap

ThreadLocalMap是ThreadLocal类的一个内部类,没有实现Map接口,但实现了部分Map的功能,用key,value进行存储内容,不过key是固定的为ThreadLocal对象

同一个线程不管创建了多少个ThreadLocal对象,都对应同一个ThreadLocalMap

ThreadLocalMap内部是一个Entry数组,而Entry继承自WeakReference,这意味着key是弱引用,而value不是。

hash冲突问题

如果一个线程中多次调用同一个ThreaLocal对象的set方法,数据会被覆盖。

如果一个线程中多次创建ThreadLocal对象,每个ThreadLocal共享一个ThreadLocalMap,存值时根据ThreadLocal对象的hash找到位置,不通的对象如果hash值一样就会有hash冲突的问题。

ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hash值计算元素在数组中的位置,如果发现这个位置上已经有其他key值的元素,则利用一定步长查看下个位置,依次判断,直至找到能够存放的位置。

ThreadLocal使用的步长为1,就是如果位置被占用就找下一个位置。

缺点时:效率比较低

一般我们都是一个线程使用一个ThreadLocal

 

内存泄漏问题

ThreadLocalMap的Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

 

如何避免内存泄漏

ThreadLocal自己提供了一定的处理方式:

每次操作set、get、remove操作时,会相应调用 ThreadLocalMap 的三个方法,ThreadLocalMap的三个方法在每次被调用时 都会直接或间接调用一个 expungeStaleEntry() 方法,这个方法会将key为null的 Entry 删除,从而避免内存泄漏。

如果一个线程运行周期较长,而且将一个大对象放入LocalThreadMap后便不再调用set、get、remove方法仍然有可能key的弱引用被回收后,引用没有被回收,此时该仍然可能会导致内存泄漏。

 

手动remove:

这个问题确实存在,没办法通过ThreadLocal解决,而是需要程序员在完成ThreadLocal的使用后要养成手动调用remove的习惯,从而避免内存泄漏。

总结:使用完theadLocal后手动调用remove方法;

 

为什么使用弱引用

弱引用也是为了防止内存泄漏,如果ThreadLocal对象已经不用了,ThreadLocalMap还持有它,如果是强引用就无法进行回收他们;如果是弱引用,就可以判断ThreadLocalMap的entry的key是否为null来设置value为null促进垃圾回收。

常用的方法

(1) void set(Object value)设置当前线程的线程局部变量的值。

(2) public Object get()该方法返回当前线程所对应的线程局部变量。

remove()将当前线程局部变量的值删除,目的是为了减少内存的占用。当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

 

线程同步和ThreadLocal对比

前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

 

使用示例

import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
  
public class ThreadLocalExample {  
  
    // 创建一个ThreadLocal对象  
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();  
  
    public static void main(String[] args) {  
        // 创建一个固定大小的线程池  
        ExecutorService executorService = Executors.newFixedThreadPool(5);  
  
        // 提交多个任务到线程池  
        for (int i = 0; i < 10; i++) {  
            final int taskId = i;  
            executorService.submit(() -> {  
                // 设置线程特有的值  
                threadLocal.set(taskId);  
  
                try {  
                    // 模拟工作耗时  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
  
                // 获取并打印线程特有的值  
                System.out.println(Thread.currentThread().getName() + " 的 ThreadLocal 值为: " + threadLocal.get());  
  
                // 清理ThreadLocal,避免内存泄漏  
                threadLocal.remove();  
            });  
        }  
  
        // 关闭线程池  
        executorService.shutdown();  
    }  
}

多线程问题

多线程的概念

多线程程序是指一个程序中包含多个执行流,每个执行流都称为一个线程,多线程是实现并发机制的一种有效手段。

 

并行与并发:

并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。

并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时,是来回切换线程。

 

多线程编程的优缺点

优点:

发挥多核CPU的优势(单核cup是通过来回切换线程实现的假的多线程,多核cpu可以让多个线程同时执行提升执行效率)

充分利用cup资源(当一个线程在等待的时候cpu可以去处理另一个线程)

合理使用多线程可以提升执行效率:对于耗时可分开不影响数据的操作利用多线程可以提升执行效率

 

缺点:

要考虑多线程下的线程安全问题

消耗性能:线程的创建销毁,以及线程之间的互相切换都是会消耗时间的。(所以要性能可以的情况下使用)

 

多线程会提高程序的运行效率吗

不一定,如果不是适合的场景还会由于线程的创建销毁,以及线程之间的互相切换都是会消耗时间,效率更低

什么场景需要用多线程编程

  • 图形界面应用程序:像Java应用是部署在Servlet容器(如Tomcat、Jetty等)中进行运行的,容器本身会负责为每个请求分配一个线程。这意味着,当有多个用户同时发送请求时,Servlet容器会创建多个线程来并行处理这些请求。用户通过按钮或查看页面请求到后台就可以避免卡顿提升用户体验。
  • 异步请求处理:你可以将耗时的操作(如数据库查询、远程调用等)放在后台线程中执行,而主线程则可以继续处理其他请求或返回响应给客户端。
  • 数据处理和计算密集型任务:当需要处理大量数据或进行复杂的计算时,可以使用多线程来加速处理过程。
  • 定时任务中一般也用到多线程

任务执行器:

 

 

多线程与异步

异步指的是让CPU暂时搁置当前请求的响应,处理下一个请求,当通过轮询或其他方式得到回调通知后,开始运行。

多线程应该是一种实现异步的手段

 

轮询和回调

轮询:在启动类里启动了线程后,启动类无限循环地去询问线程是否已经执行完。

回调:回调比轮询比较简单有效,不用再无限循环地询问线程是否执行完了。线程执行完后主动将消息告知启动类,那么启动类在线程执行过程中就可以休息,主要线程来通知它消息就可以了。

异步和同步

同步:所有的操作都做完,才返回给用户。(提交一个请求,没有处理完我们就是等待状态)

异步:指发送一个请求,不需要等待返回,随时可以再发送下一个请求(前一个请求返回数据前,我们还可以提交其他请求)

Junit多线程执行问题

使用junit测试多线程,会出现run方法还没执行完就结束了的问题

其实junit是将test作为参数传递给了TestRunner的main函数。并通过main函数进行执行。

test函数在main中执行。如果test执行结束,那么main将会调用System.exit(0);即使还有其他的线程在运行,main也会调用System.exit(0);

System.exit()是系统调用,通知系统立即结束jvm的运行,即使jvm中有线程在运行,jvm也会停止的。所以会出现之前的那种情况。其中System.exit(0);的参数如果是0,表示系统正常退出,如果是非0,表示系统异常退出。

junit.textui.TestRunner
 public static void main (String[] args) {
      TestRunner aTestRunner = new TestRunner();
      try {
        TestResult r = aTestRunner.start(args);
       if (!(r.wasSuccessful()))
          System.exit (1);
       System.exit(0);
      } catch (Exception e) {
       System.err.println(e.getMessage());
        System.exit(2);
      }
   }

所以不要使用junit测试多线程,可以使用main方法来测试

线程安全

线程安全的概念

多个线程访问线程共享的资源,如果不进行管理就会出现访问和修改之间被其他线程修改的情况,就是线程安全问题

单线程没有线程安全问题,没有共享资源没有线程安全问题,只读没有线程安全问题

线程安全问题处理方案

解决多线程要面对的原子性,可见性,有序性问题来保证线程安全

 

java中如何面对线程安全:

使用synchonized实现同步方法或同步代码块

使用支持线程安全的类创建对象:比如ConcurrentHashMap(java.util.concurrent 包下),Vector(java.util包下),StringBuffer等

利用java.util.concurrent.atomic包下提供的可以进行原子操作的类实现成员变量或静态变量的线程安全(atomic包下使用了硬件支持的cas操作也使用volatile保证下一个读取操作会在前一个写操作之后发生)

 

java.utils.concurrent包下lock类也可以作为对象锁来使用

ConcurrentHashMap线程安全原理:

主要依赖于CAS操作和synchronized关键字

 

StringBuffer线程安全原理:

方法中使用synchonized关键字来保证线程安全

单例和多例的线程安全问题

单例模式下如果没有可修改的成员变量或静态变量,就不会有线程安全问题。如果有多线程情况下如果要修改这些变量就会有线程安全问题。

多例模式下,如果可修改的静态变量,如果多个线程去修改这个变量,也会有线程安全问题。

对于无状态的(没有可修改的成员变量或静态变量)单例或多利实例,都是线程安全的。

 

synchronized 和Lock的异同

主要相同点:Lock能完成Synchronized所实现的所有功能。

主要不同点:Lock有比synchronied更精确的线程语义和更好的性能。Synchronized会自动释放所,而Lock一定要程序员手工释放,一般都在finally子句中释放。

 

synchronized的原理

早期synchronized使用重量级锁通过监视器锁(monitor)实现

Java6之后通过偏向锁、轻量级锁、、重量级锁这样锁升级方式实现。重量级锁还是用monitor机制实现

synchronized能够保证线程安全是由java的monitor机制实现的,Java的monitor机制JVM 内部基于 C++ 实现的一套机制

Java中每个对象都存在着一个monitor(监视器锁)与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。其他线程被阻塞。

synchronized 关键字在使用的时候,往往需要指定一个对象与之关联,这个对象就是 monitor object,任何一个 Java 对象都可以作为 monitor 机制的 monitor object。

monitor机制

monitor直译过来是监视器的意思,专业一点叫管程。monitor是属于编程语言级别的,

操作系统本身并不支持 monitor 机制,monitor 是属于编程语言的范畴,当你想要使用 monitor 时,先了解一下语言本身是否支持 monitor 原语,例如 C 语言它就不支持 monitor,Java 语言支持 monitor。

monitor机制是基于操作系统的mutex互斥量原语

monitor的作用就是限制同一时刻,只有一个线程能进入monitor框定的临界区,达到线程互斥,保护临界区中临界资源的安全

 

monitor基本元素

临界区

monitor对象和锁

条件变量以及定义在monitor对象上的wait,notify操作

 

对象锁升级为重量级锁,那么其中在对象头中存储了指向基于monitor锁的指针ptr_to_heavyweight_monitor。

java的monitor的定义和初始化是有c++语言编写的。

objectMonitor就是对象头中指向的monitor重量级锁,objectWaiter是对等待线程的封装,可以用双向链表保存起来。

 

objectMonitor的一些属性:

_count:抢占该锁的线程数 约等于 WaitSet.size + EntryList.size

_recursions:锁重入次数

_WaitSet:处于wait状态的线程,被加入到这个linkedList(双向链表);

_owner :指向获得ObjectMonitor对象的线程或基础锁

_WaitSetLock:保护WaitSet的一个自旋锁(monitor大锁里面的一个小锁,这个小锁用来保护_WaitSet更改)

_EntryList:管程的入口线程队列(双向链表);

OwnerIsThread:当前owner是thread还是BasicLock

 

 

大概流程

  1. 线程访问同步代码,需要获取monitor锁
  2. 线程被jvm托管
  3. jvm获取充当临界区锁的java对象
  4. 根据java对象对象头中的重量级锁 ptr_to_heavyweight_monitor指针找到objectMonitor
  5. 将当前线程包装成一个ObjectWaitor对象
  6. 将ObjectWaiter放在_cxq(ContentionList)队列头部
  7. _count++
  8. 如果owner是其他线程说明当前monitor被占据,则当前线程阻塞。如果没有被其他线程占据,则将owner设置为当前线程,将线程从等待队列中删除,count--。
  9. 当前线程获取monitor锁,如果条件变量不满足,则将线程放入WaitSet中。当条件满足之后被唤醒,把线程从WaitSet转移到EntrySet中。
  10. 当前线程临界区执行完毕
  11. Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交个OnDeck,OnDeck需要重新竞争锁

 

被 synchronized 关键字修饰的方法、代码块,就是 monitor 机制的临界区。

synchronzied 需要关联一个对象来加锁,而这个对象就是 monitor object。

monitor 的机制中,monitor object 充当着维护 mutex以及定义 wait/signal API 来管理线程的阻塞和唤醒的角色。

Java 对象存储在内存中,分别分为三个部分,即对象头、实例数据和对齐填充,而在其对象头中,保存了锁标识;同时,java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于一个叫 ObjectMonitor 模式的实现,这是 JVM 内部基于 C++ 实现的一套机制

 

 

对象头

(对象头的内容:分代年龄、锁状态、指向类元数据指针、数组长度等)

 

在Hotspot虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充;Java对象头是实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里。

JVM中对象头的方式有以下两种(以32位JVM为例):

  • 普通对象

Object Header (64 bits)

Mark Word (32 bits)

Klass Word (32 bits)

 

 

  • 数组对象

Object Header (96 bits)

Mark Word(32bits)

Klass Word(32bits)

array length(32bits)

 

对象头的组成

Mark Word

这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄、锁状态等。mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。

为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:

normal无锁状态 Biased为偏向锁状态 light为轻量级锁 heavyweight为重量级锁

 

其中各部分的含义如下:

lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。

biased_lock lock 状态

0 01 无锁

1 01 偏向锁

0 00 轻量级锁

0 10 重量级锁

0 11 GC标记

 

biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。

 

age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

 

identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。

 

thread:持有偏向锁的线程ID。

epoch:偏向时间戳。

ptr_to_lock_record:指向栈中锁记录的指针。

ptr_to_heavyweight_monitor:指向管程Monitor的指针。

64位下的标记字与32位的相似

 

 

class pointer

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。

如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

 

1每个Class的属性指针(即静态变量)

2每个对象的属性指针(即对象变量)

3普通对象数组的每个元素指针

 

一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

 

array length

如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

 

早期synchronized效率低的原因

早期,Synchronized属于重量级锁,通过对象内部的monitor实现,monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。所以开销比较大

 

内核态:CPU可以访问内存所有数据, 包括外围设备, 例如硬盘, 网卡. CPU也可以将自己从一个程序切换到另一个程序

用户态:只能受限的访问内存, 且不允许访问外围设备. 占用CPU的能力被剥夺, CPU资源可以被其他程序获取

所有用户程序都是运行在用户态的, 但是有时候程序确实需要做一些内核态的事情, 例如从硬盘读取数据

 

现在synchronized效率高的原因

 

在Java 6之后Java官方对从JVM层面对synchronized较大优化

Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁、轻量级锁和自旋锁等概念,他们都属于乐观锁。

每个Java对象的头部都有关于锁的标志位,这里存放了锁的有关信息。为了提高效率,锁有一个粗化过程,从轻到重依次是:无锁状态 ——> 偏向锁 ——> 轻量级锁 ——>重量级锁。锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级

线程同步

为什么要使用线程同步

java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突。利用线程同步可以解决这个问题。

 

锁的概念

锁是保证线程安全的一种机制,保障同一时间只有一个线程操作资源

 

对象锁和类锁

对象锁:

使用类的实例对象的对象头中的锁标志位判断是否获取到锁

一个时间内针对该对象的操作只能有一个线程得到执行

 

类锁:

使用该类的class对象的对象头中的锁标志位判断是否获取到锁

一个时间内针对该类只能有一个线程得到执行

 

类的对象实例可以有很多个,但是每个类只有一个class对象

 

同步方法和同步代码块

定义同步的前提:

1,必须要有两个或者两个以上的线程,才需要同步。

2,多个线程必须保证使用的是同一个锁。

 

同步的方式:

  • 同步代码块

任意对象都可以做为同步的锁,

如:

synchronized(对象){
//需要同步的代码;
}

三种用法:

synchronized (this)、synchronized (非this对象)、synchronized (类.class)

this代表使用当前对象的对象锁

非this对象代表使用指定对象的对象锁

类.class代表使用指定的类的类锁

 

  • 同步函数

用synchronized关键字修饰方法,如:public synchronized void save(){}

如果synchronized用在类声明中,则表示该类中的所有方法都是synchronized的。

 

同步函数用的是当前调用的对象作为锁,static修饰的同步函数是该类的Class对象作为锁;

同步代码块可以把任意对象作为同步锁

 

在一个类中只有一个同步,可以使用同步函数。如果有多同步,必须使用同步代码块,来确定不同的锁。所以同步代码块相对灵活一些。

线程同步的弊端

当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

锁的释放

获取锁的线程释放锁只会有两种情况:

1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

2)线程执行发生异常,此时JVM会让线程自动释放锁。

3)在同步方法或代码块中调用wait方法会释放锁

 

java中锁的分类

根据锁的性质分为:乐观锁、悲观锁

根据锁的级别分为(专门针对synchronized的):无锁,偏向锁,轻量级锁,重量级锁

根据锁是否公平分为:公平锁和非公平锁

乐观锁:

乐观锁是对其他线程持有乐观态度,乐观的认为其他线程不会修改自己要操作的资源,在需要修改资源的时候再去判断资源是否被修改过,所以先执行业务逻辑,到需要修改资源的时候去获取锁,如果修改失败重新来执行逻辑直到更新成功

java中的乐观锁:版本号机制 和 CAS实现 。

在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

 

悲观锁:

悲观锁是对其他线程持有悲观态度,悲观的认为其他线程会修改自己要操作的资源,所以在执行业务逻辑前,先获取锁,再执行业务逻辑及修改数据,其他线程想要执行如果锁一样就会被阻塞,等到悲观锁把资源释放为止

Java 中的 Synchronized 和 ReentrantLock 等独占锁(排他锁)是一种悲观锁思想的实现

 

乐观锁适用于读比较多的场景,悲观锁适用于写比较多的场景

 

自旋锁:

如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁(称为自旋等待)。

所以,自旋等待的时间或者次数是有一个限度的,如果自旋超过了定义的时间仍然没有获取到锁,则该线程应该被挂起。时间和次数由jvm控制

单核单线程的CPU不适合自旋锁

 

为什么需要自旋锁:

如果锁住的逻辑执行时间很短,当没有获取到锁就进行线程的挂起,锁释放回复线程,这样会浪费很多时间,如果使用自旋锁不进行线程挂起就可以很高效了

所以它是通过占用处理器避免线程切换带来的开销

 

自旋锁的应用场景:

自旋锁比较适用于锁使用者保持锁时间比较短的情况

concurrent包下很多类使用了自旋锁,syncronized在锁级别为轻量级锁也使用了自旋锁

 

自旋锁的实现:

一般都是一个while循环,循环判断是否获取到了锁

 

自适应自旋锁:

在jdk1.6时推出的自适应自旋锁,线程的自旋时间是根据上一个持有该锁的线程的自旋时间以及状态来确定的

无锁

 

偏向锁:

当一个线程首次访问一个synchronized块或方法时,它会把该锁标记为偏向锁,并将自己的线程ID记录在锁对象的头部。此后,该线程再次访问该锁时,只需要检查锁对象的线程ID是否与自己的线程ID一致,如果一致则可以直接进入同步块,无需再进行任何锁的竞争。这种优化可以减少不必要的锁竞争,提高程序的性能。

偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作(获取锁的过程),这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

偏向锁会在一个线程的时候出现,通过CAS操作获取锁

获取锁:通过cas操作替换对象头中markword中的threadId为当前线程,成功的话锁对象就标记为偏向模式

竞争锁:cas操作修改markword中的threadId失败代表有线程竞争,当到达全局安全点(这个时间点是没有正在执行的代码),会挂起偏向锁线程升级为轻量级锁,然后恢复被挂起的线程

释放锁:修改threadId为空,偏向锁在发生竞争的时候才会释放

 

是否使用偏向锁是可以设置的,默认偏向锁的使用是开启的,可以通过jvm运行参数来开启和关闭

什么时候关闭:如果自己代码中偏向锁使用的很少,就可以关闭,避免偏向锁转化为轻量级锁的开销。

轻量级锁:

引入轻量级锁的主要目的:是在只有少量线程竞争的前提下,减少传统的重量级锁产生的性能消耗。

当线程尝试获取偏向锁失败时就会尝试去获取轻量级锁,获取不到就自旋,自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定,自旋还是没有获取到锁,就会转为重量级锁。

获取锁:jvm为当前线程建立用于存储锁记录的空间(Lock Record),cas操作尝试将对象头的Mark Word 更新为该线程Lock Record空间的地址,如果更新成功就成功获取到了锁

如果更新失败,代表对象是有锁状态,检查对象头中的Mark Word 是否指向当前线程的栈帧如果就是一次锁重入,可以执行代码;如果不是指向该线程当前线程便尝试使用自旋来获取锁。如果自旋获取锁失败就升级为重量级锁

 

释放锁:如果是重入,重入几次就需要释放几次,重入锁的释放是修改的当前线程Lock Record中的信息,最后一次释放锁是通过cas操作修改对象头中的锁信息为无锁状态。

 

重量级锁

其利用操作系统底层的同步机制去实现Java中的线程同步。

获取不到锁的线程会被阻塞挂起,挂起和恢复线程需要的时间比较长,消耗大量的系统资源

线程竞争不使用自旋

公平锁

多个线程竞争锁获取锁顺序按照先来后到,等待时间最长的线程最先获得锁

在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,锁释放后在队列头的线程先获取到锁

 

优点:

等待锁的线程不会饿死,总会有执行的机会的

 

缺点:

由于需要被唤醒的线程比非公平锁多,所以性能开销比较大

非公平锁

多个线程竞争锁获取锁顺序是随机的。

非公平锁就是可以插队,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式加入到等待队列,锁释放后如果刚好有插队的就把锁给他了,没有就给队列头的线程

synchronized是非公平锁

 

非公平锁优点:

非公平锁性能高于公平锁性能,因为它可以减少唤起线程的开销

缺点是处于等待队列中的线程可能会饿死(一直获取不到锁无法执行),或者等很久才会获得锁。

 

公平锁和非公平锁选择:

大部分情况下我们使用非公平锁,因为其性能比公平锁好很多。但是公平锁能够避免线程饥饿,某些情况下也很有用。

 

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。不会因为之前已经获取过还没释放而阻塞。

Java中ReentrantLock和synchronized都是可重入锁,可重入锁可以避免已经获取到锁的线程再次获取锁产生死锁。

 

重入的场景

public class Demo1 {
    public synchronized void functionA(){
        System.out.println("iAmFunctionA");
        functionB();
    }
    public synchronized void functionB(){
        System.out.println("iAmFunctionB");
    }
}

带有相同锁的两个方法的调用,带有synchronized的方法递归调用

 

重入锁原理,线程获取到锁时会记录该线程信息和一个计数器,重入一次计数器+1,释放一次计数器-1,当为0时就是完全释放锁,删除线程信息

流程:

加锁时,需要判断锁是否已经被获取。如果已经被获取,则判断获取锁的线程是否是当前线程。如果是当前线程,则给获取次数加1。

java.util.concurrent.locks

Lock接口

常用方法

void lock() – 如果锁可用就获得锁,如果锁不可用就阻塞直到锁释放

void lockInterruptibly() – 可以响应中断的获取锁, 过程中会检测是否中断(interrupt),若是会抛出InterruptedException异常

boolean tryLock() –尝试获取锁,如果成功返回true,失败返回false

boolean tryLock(long timeout, TimeUnit timeUnit) – 指定等待获取锁的时间,超过时间未获取到锁返回false,时间内获取到锁返回true。

void unlock() – 释放锁

 

实现类

 

线程中断

持有锁的线程长期不释放锁,正在等待的线程可以选择放弃等待,去做其他事情

持有锁的线程调用thread对象的中断方法thread.interrupt();抛出中断异常,其他线程如果是通过lockInterruptibly方法获取锁,就可以继续获取锁执行

 

ReentrantLock

实现了Lock接口,所在包:java.util.concurrent.locks

特性:

  • 可重入锁
  • 可响应中断
  • 可尝试加锁
  • 限时等待尝试加锁
  • 公平锁、非公平锁
  • 与Condition信号量结合使用

 

构造函数ReentrantLock(),使用非公平锁

构造函数ReentrantLock(boolean fair),参数为true为公平锁,false为非公平锁

lockInterruptibly()可以响应中断的获取锁

支持两种获取锁的方式,一种是公平模型,一种是非公平模型。

ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:

非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;

公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。

原理:

ReentrantLock类有个变量sync,是一个继承AbstractQueuedSynchronizer (以下称为AQS) 的Sync抽象类,分别由FairSync、NonfairSync类实现,代表着公平和非公平策略。

ReentrantLock的实现基本依靠AQS实现的

lock方法:

非公平锁:

先CAS尝试更新队列Node的state字段,在ReentrantLock中state字段表示当前线程重入锁的次数,当state为0时候,表示锁是空闲的。之后设置当前线程为锁的占有者;

final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());//设置当前线程为锁的占有者
            else
                acquire(1);
        }
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();//Thread.currentThread().interrupt();
    }

非公平锁的acquire方法会先判断state是否是0,是0会去cas加锁,不是0就判断该线程是否持有这个锁,如果持有进行重入,state+1,如果未获取到锁,就创建一个新的Node,通过CAS操作放到队列尾部;加入队列以后,线程阻塞挂起,被唤醒后再次获取资源,获取失败再次阻塞;如果加入队列线程挂起过程中失败,线程中断

 

 

公平锁:

final void lock() {
            acquire(1);
        }

 

 

 

Sync抽象类是ReentrantLock中的静态内部类

abstract static class Sync extends AbstractQueuedSynchronizer

NonfairSync和FairSync都是ReentrantLock中的静态内部类

static final class NonfairSync extends Sync

static final class FairSync extends Sync

AQS(AbstractQueuedSynchronizer)

AQS 基本数据结构是一个FIFO(先进先出)的双向队列,每个结点Node存储线程和其他信息。队列的头部结点表示:该结点对应的线程已经处于执行状态,占用了资源;剩下的队列里的线程则被挂起等待唤醒。

Node节点的一些属性:

  • prev 前继结点
  • next 后继结点
  • thread 对应的线程
  • nextWaiter 下一个等待condition的结点
  • state 状态;是一个int值,表示当前线程占用资源的数量;0表示空闲,没有线程占用;ReentrantLock的state表示线程重入锁的次数

 

 

AQS 有两种模式,一种是独占模式 EXCLUSIVE(只有一个线程能执行) ,另外一种是共享模式 SHARED(可以多个线程同时执行);而ReentrantLock是独占模式。

 

ReentrantLock与synchronized对比

ReentrantLock需要手动释放锁,synchronized是自动释放锁(执行完或抛异常都会释放)

synchronized使用的锁是非公平锁,ReentrantLock可以控制使用公平锁还是非公平锁

 

使用synchronized如果获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能等待,这样非常影响程序执行效率。

使用ReentrantLock可以设置只等待一定时间,或者能够响应中断

 

synchronized对于线程通信是调用Object类中定义的wait,notify,notifyAll方法,

ReentrantLock可以调用newCondition()方法获取到Condition实现类,来调用await(),signal(),signalAll()方法来等待和唤醒,而且可以指定多个条件

ReadWriteLock 接口

该接口只定义了两个方法

Lock readLock();返回读线程的锁

Lock writeLock();返回写线程的锁

ReentrantReadWriteLock

该类实现了ReadWriteLock接口,可以通过构造函数参数是false还是true指定是非公平锁还是公平锁,默认无参的构造函数是非公平锁

 

Read Lock – 没有线程获得写锁且没有获取写锁的请求,多个线程可以获得读锁

Write Lock – 如果没有线程读或者写,只有一个线程可以获取写锁

StampedLock类

Java 8中引入(不是ReadWriteLock 的实现类),同样支持读锁和写锁,不同的是获取锁的方法返回一个用于释放锁或检查锁是否有效的标记

Map<String,String> map = new HashMap<>();
    private StampedLock lock = new StampedLock();
    public void put(String key, String value){
        long stamp = lock.writeLock();
        try {
            map.put(key, value);
        } finally {
            lock.unlockWrite(stamp);
        }
    }
    public String get(String key) throws InterruptedException {
        long stamp = lock.readLock();
        try {
            return map.get(key);
        } finally {
            lock.unlockRead(stamp);
        }
    }

 

StampedLock 的另一个特点是采用了乐观锁策略,大部分的时间里读操作不需要等待写操作的完成,因此不需要一个完善的读锁,相反可以升级到读锁

Condition接口

Condition 类提供了在临界区线程可以等待某些条件发生时再去执行。 可以指定多个条件

使用synchronized结合Object上的wait和notify方法可以实现线程间的等待通知机制。ReentrantLock结合Condition接口同样可以实现这个功能。而且相比前者使用起来更清晰也更简单。

Condition接口在使用前必须先调用ReentrantLock的lock()方法获得锁。之后调用Condition接口的await()将释放锁,并且在该Condition上等待,直到有其他线程调用Condition的signal()或signalAll()方法唤醒线程。

Stack<String> stack = new Stack<>();
    ReentrantLock lock = new ReentrantLock();
    Condition stackEmptyCondition = lock.newCondition();
    Condition stackFullCondition = lock.newCondition();
    public void pushToStack(String item){
        try {
            lock.lock();
            while(stack.size() == 5){
                stackFullCondition.await();
            }
            stack.push(item);
            stackEmptyCondition.signalAll();
        } finally {
            lock.unlock();
        }
    }
    public String popFromStack() {
        try {
            lock.lock();
            while(stack.size() == 0){
                stackEmptyCondition.await();
            }
            return stack.pop();
        } finally {
            stackFullCondition.signalAll();
            lock.unlock();
        }
    }

 

死锁

线程彼此需要对方释放需要的锁才能继续执行就会造成死锁问题。

死锁的危害

  • 死锁会使进程得不到正确的结果。因为处于死锁状态的进程得不到所需的资源,不能向前推进,故得不到结果。
  • 死锁会使资源的利用率降低。因为处于死锁状态的进程不释放已占有的资源,以至于这些资源不能被其他进程利用,故系统资源利用率降低。
  • 死锁还会导致产生新的死锁。其它进程因请求不到死锁进程已占用的资源而无法向前推进,所以也会发生死锁。

 

产生死锁的条件

互斥条件:资源同一时间只能被一个线程持有,其他线程只能等待。

不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。

请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。

如何避免死锁

加锁顺序,加锁时限锁超时,死锁检测,避免在已经获取锁的状态下获取其他锁

规定加锁顺序

确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。

比如线程1会用到锁ABC,线程2用到AC,规定获取锁顺序是ABC,先获取A锁才能获取到其他锁

但是,这种方式需要你事先知道所有可能会用到的锁,并对这些锁做适当的排序,但总有些时候是无法预知的。

加锁时限

尝试获取锁的时候加一个超时时间,超过了这个时限该线程则放弃对该锁请求。并释放所有已经获得的锁,然后等待一段随机的时间再重试。

可能会导致线程重复地尝试但却始终得不到锁。

锁超时

获取锁时,指定锁的超时时间,到达时间后自动释放锁,如果担心到时间了还没执行完,可以有个线程来进行续期

 

死锁检测

每当一个线程获得了锁,记录线程信息和锁信息到一个数据结构中如map,线程请求锁也记录到这个数据结构中,当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生(互相需求又互相持有对方的锁)。

检测到死锁后:

一种方案是释放所有锁,回退,并且等待一段随机的时间后重试。

给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

总结:

我们最好避免锁的交叉,加锁按照一定的顺序进行

 

检测线程是否拥有锁

在java.lang.Thread中有一个方法叫holdsLock,它返回true如果当且仅当当前线程拥有某个具体对象的锁。

boolean holdsLock(Object obj)

public static void main(String[] args){
synchronized (ThreadTest.class) {
        System.out.println(Thread.holdsLock(ThreadTest.class));
    }
System.out.println(Thread.holdsLock(ThreadTest.class));
}

运行返回true,false

     

同步和异步有何异同,在什么情况下分别使用他们?举例说明。 

     如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。 

     当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。 

启动一个线程是用run()还是start()?

     启动一个线程是调用start()方法,使线程就绪状态,以后可以被调度为运行状态,一个线程必须关联一些具体的执行代码,run()方法是该线程所关联的执行代码。 

 

当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?

分几种情况:

     1.其他方法前是否加了synchronized关键字,如果没加,则能。

     2.如果这个方法内部调用了wait,则可以进入其他synchronized方法。

     3.如果其他个方法都加了synchronized关键字,并且内部没有调用wait,则不能。

4.如果其他方法是static,它用的同步锁是当前类的字节码,与非静态的方法不能同步,因为非静态的方法用的是this。

简述synchronizedjava.util.concurrent.locks.Lock的异同  

主要相同点:Lock能完成synchronized所实现的所有功能

主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally语句中释放。Lock还有更强大的功能,例如,它的tryLock方法可以非阻塞方式去拿锁。

举例说明(对下面的题用lock进行了改写):

package com.huawei.interview;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadTest {
     private int j;
     private Lock lock = new ReentrantLock();
     public static void main(String[] args) {
          // TODO Auto-generated method stub
          ThreadTest tt = new ThreadTest();
          for(int i=0;i<2;i++)
          {
              new Thread(tt.new Adder()).start();
              new Thread(tt.new Subtractor()).start();
          }
     }
     private class Subtractor implements Runnable
     {
          @Override
          public void run() {
              // TODO Auto-generated method stub
              while(true)
              {
                   /*synchronized (ThreadTest.this) {             
                        System.out.println("j--=" + j--);
                        //这里抛异常了,锁能释放吗?
                   }*/
                   lock.lock();
                   try
                   {
                        System.out.println("j--=" + j--);
                   }finally
                   {
                        lock.unlock();
                   }
              }
          }
     }
    
     private class Adder implements Runnable
     {
          @Override
          public void run() {
              // TODO Auto-generated method stub
              while(true)
              {
                   /*synchronized (ThreadTest.this) {
                   System.out.println("j++=" + j++);
                   }*/
                   lock.lock();
                   try
                   {
                        System.out.println("j++=" + j++);
                   }finally
                   {
                        lock.unlock();
                   }                 
              }            
          }
     }
}

设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序。 

以下程序使用内部类实现线程,对j增减的时候没有考虑顺序问题。

public class ThreadTest1
{
private int j;
public static void main(String args[]){
   ThreadTest1 tt=new ThreadTest1();
   Inc inc=tt.new Inc();
   Dec dec=tt.new Dec();
   for(int i=0;i<2;i++){
       Thread t=new Thread(inc);
       t.start();
      t=new Thread(dec);
       t.start();
       }
   }
private synchronized void inc(){
   j++;
   System.out.println(Thread.currentThread().getName()+"-inc:"+j);
   }
private synchronized void dec(){
   j--;
   System.out.println(Thread.currentThread().getName()+"-dec:"+j);
   }
class Inc implements Runnable{
   public void run(){
       for(int i=0;i<100;i++){
       inc();
       }
   }
}
class Dec implements Runnable{
   public void run(){
       for(int i=0;i<100;i++){
       dec();
       }
   }
}
}

 

多线程编程实例

三个售票窗口同时出售20张票;

程序分析:

1.票数要使用同一个静态值

2.为保证不会出现卖出同一个票数,要java多线程同步锁。

设计思路:

  1. 创建一个站台类Station,继承Thread,重写run方法,在run方法里面执行售票操作!售票要使用同步锁:即有一个站台卖这张票时,其他站台要等这张票卖完!
  2. 创建主方法调用类
  • 创建一个站台类,继承Thread
package com.xykj.threadStation;
public class Station extends Thread {
    // 通过构造方法给线程名字赋值
    public Station(String name) {
       super(name);// 给线程名字赋值
    }
    // 为了保持票数的一致,票数要静态
    static int tick = 20;
    // 创建一个静态钥匙
    static Object ob = "aa";//值是任意的
    // 重写run方法,实现买票操作
    @Override
    public void run() {
      while (tick > 0) {
        synchronized (ob) {// 这个很重要,必须使用一个锁,
          // 进去的人会把钥匙拿在手上,出来后才把钥匙拿让出来
          if (tick > 0) {
            System.out.println(getName() + "卖出了第" + tick + "张票");
            tick--;
          } else {
            System.out.println("票卖完了");
          }
        }
        try {
           sleep(1000);//休息一秒
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
  }
}

 

创建主方法调用类

package com.xykj.threadStation;
public class MainClass {
  /**
   * java多线程同步锁的使用
   * 示例:三个售票窗口同时出售10张票
   * */
  public static void main(String[] args) {
    //实例化站台对象,并为每一个站台取名字
     Station station1=new Station("窗口1");
     Station station2=new Station("窗口2");
     Station station3=new Station("窗口3");
    // 让每一个站台对象各自开始工作
     station1.start();
     station2.start();
     station3.start();
  }
}

 

posted @ 2023-02-02 10:10  星光闪闪  阅读(120)  评论(0)    收藏  举报