Java 多线程基础
前言
Java中多线程并发技术可以说是十分重要的,并且在面试Java开发岗位时,多线程并发技术已经是必问的知识点。这里我将讲解一些多线程的知识,如有遗漏或错误,请大家多包涵。
1.基本概念
-
进程和线程
- 进程比较官方的解释,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。简单来说,在Windows系统中,打开的.exe结尾的文件就是一个进程,打开任务管理器可以看到当前系统中的进程。
- 线程是轻量级的进程,是程序执行的最小单位。一个进程可以容纳若干个线程。在并发程序开发中,使用多线程而不使用多进程,是因为线程间的切换和调度的成本远远小于进程。
- 进程比较官方的解释,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。简单来说,在Windows系统中,打开的.exe结尾的文件就是一个进程,打开任务管理器可以看到当前系统中的进程。
-
并发和并行
- 并行的多个任务是真的同时执行。只有当系统有一个以上CPU时,才有可能并行任务。
- 并发的多个任务是交替执行的。它是将CPU运行时间划分成若干个时间段,再将时间段分配给各个线程。由于并发的最终效果和并行的差不多,并不需要特别注意。
2.多线程的优点
举个例子,当有两个任务执行分别需要10秒、15秒,在单线程串行执行时,总共就需要25秒,而在多线程并发执行时,可能就只需要15秒时间。
3.使用多线程
JDK中已经包含了对多线程技术的支持,可以非常方便的进行多线程开发。
使用多线程编程主要有两种方式
- 继承Thread类
- 实现Runnable接口
在上面的Thread类图中,可以看到Thread类也是实现了Runnable接口。在Runnable接口中有且只有一个抽象方法run()方法,run()方法便是实际执行任务的方法。
1.创建一个线程
方式一:继承Thread类,创建线程
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new MyThread();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程中实际执行的任务逻辑");
}
}
方式二:实现Runnable接口,创建线程
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程中实际执行的任务逻辑");
}
}
注意:由于Thread类是实现Runnable接口的,所以使用构造方法创建线程时,可以将Thread的实例对象当做参数。
方式三:使用匿名内部类,创建线程,这只是简化方式二而已
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new Thread(new Runnable(){
@Override
public void run() {
System.out.println("线程中实际执行的任务逻辑");
}
});
}
}
方式四:由于Runnable接口中有且只有一个抽象方法run()方法,在Java8之后的版本,Runnable接口还使用了@FunctionalInterface注解,所以可以使用lambda表达式来简化创建线程
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("线程中实际执行的任务逻辑");
});
}
}
2.启动线程
创建完线程之后,我们就需要启动线程。
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("线程中实际执行的任务逻辑");
});
t.start();
}
}
注意:调用start()方法就会新建一个线程并让这个线程执行run()方法,而使用run()方法只会在当前线程中调用run()方法,只是作为一个普通方法调用。
4.线程状态
在Thread类的内部有一个枚举类,线程的所有状态都在这个枚举类中定义
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- NEW:表示刚刚创建的线程,还没开始执行。
- RUNNABLE:表示线程在执行中
- BLOCKED: 阻塞状态,这时线程就会暂停执行
- WAITING: 表示等待状态
- TIMED_WAITING: 和WAITING一样是表示等待状态,不一样的是TIMED_WAITING是有时限的,而WAITING则无时限
- TERMINATED:表示线程结束
可以调用getState()方法获取线程的状态
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new Thread();
Thread.State state = t.getState();
System.out.println(state.name()); // 输出结果为 NEW
}
}
5.线程的基本操作
线程的新建和线程的启动在上面已经介绍过了,接下来尝试一下线程的其他操作。
1.线程的一些基本信息
在上面的案例中,我们通过线程对象获取了线程的状态,但是在线程的run()方法中,没线程对象又该怎么获取线程信息呢。Thread类提供了一个静态方法currentThread(),通过该方法可以获取当前线程对象。通过线程对象,我们就可以获取到线程id、线程名字等信息。
public class ThreadDemo {
public static void main(String[] args) {
System.out.println("main方法线程名字: " + Thread.currentThread().getName()); // 输出的结果: 主线程名字: main
for(int i=0; i<10; i++){
new Thread(new Runnable() {
@Override
public void run() {
// 获取线程对象
Thread thread = Thread.currentThread();
System.out.println("线程id: " + thread.getId());
System.out.println("线程名字: " + thread.getName());
}
}).start();
}
}
}
观察控制台打印的结果,可以看到子线程名字由 Thread-0到 Thread-9,而main方法线程名字为mian。这其实是在构造线程对象时,若没有传入线程名字,则会生成默认的线程名字。
// Thread类的源码
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
可以在构造线程对象时,定义线程名字。
public class ThreadDemo {
public static void main(String[] args) {
Runnable runnable = new Runnable(){
@Override
public void run() {
System.out.println("线程名字: " + Thread.currentThread().getName());
}
};
new Thread(runnable, "我的线程名字").start();
}
}
2.线程睡眠
sleep()方法可以让当前线程等待若干时间。sleep()是个静态方法,可以直接使用Thread类调用。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始执行");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "线程执行结束");
}
}).start();
Thread.sleep(1000, 500);
System.out.println("mian线程执行结束");
}
}
sleep()中的参数为睡眠时间,单位为毫秒,其有个重载方法sleep(long millis, int nanos),参数一个为毫秒,一个为微秒。
注意:sleep()方法会抛出InterruptedException异常,由于run()没有抛出异常,所以需要在run()方法中抓取异常。
3.终止线程
一般来说,线程执行完任务就会自动结束,但有时候我们也需要手动结束线程。例如线程中执行了一个非常耗时的任务,并且由于一些原因,已经不需要再执行该任务了。
Thread类提供了一个stop()方法,可以立即将一个线程终止,但该方法已经被标记为废除,原因是stop()方法过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致问题,不推荐使用。这里还是给大家做个案例。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true) {
System.out.println(Thread.currentThread().getName() + "执行中。。。");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(1000);
t.stop();
System.out.println("主线程执行完毕");
}
}
4.线程中断
前面讲了stop()方法已经被标记为废除,但若想要实现线程终止该怎么办。我们可以在Runnable接口的实现类中定义一个为boolean类型的实例变量,每当任务执行到一定阶段时,就判断一下该实例变量以控制任务是否继续执行。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
Thread.sleep(1000);
t.isRun = false;
System.out.println("主线程执行完毕");
}
}
class MyThread extends Thread{
// 为了方便外面调用,这边使用public,在实际开发中最好使用private
public boolean isRun = true;
@Override
public void run() {
while (isRun) {
System.out.println(Thread.currentThread().getName() + "执行中。。。");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
上面的案例是简单的一个实现逻辑,还有很多的不足,例如当线程使用sleep()方法睡眠时,想要退出线程只能等到线程睡眠结束之后。事实上JDK给我们提供了更强大的支持,就是线程中断。
- public void interrupt() 中断线程
- public boolean isInterrupted() 判断是否被中断
- public static boolean interrupted() 判断是否被中断,并清除当前中断状态
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
Thread thread = Thread.currentThread();
while (!thread.isInterrupted()) {
System.out.println(Thread.currentThread().getName() + "执行中。。。");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
}
System.out.println(Thread.currentThread().getName() + "执行中断");
});
t.start();
Thread.sleep(1000);
t.interrupt();
System.out.println("主线程执行完毕");
}
}
注意:
- interrupt() 方法只是给线程增加一个中断状态,并不是直接中断线程,想中断线程需要在线程的 run() 方法中判断线程是否被中断和退出线程
- 当线程等待或睡眠(调用 wait() 方法或 sleep() 方法)时,调用 interrupt() 会使线程抛出 InterruptedException 异常,可在捕获异常后,退出线程
5.等待和通知
为了支持多线程之间的协作,JDK提供了两个重要的方法:等待wait()和通知notify()。这两个方法并不是定义在Thread类中,而是定义在Object类中,所以任意对象都能调用这两个方法。
public final void wait() throws InterruptedException
public final native void notify()
当在一个对象上调用wait()方法后,当前线程就会在这个对象上等待,直到其他线程调用了这个对象的notify()方法为止。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程开始执行。。。");
synchronized (obj){
System.out.println(Thread.currentThread().getName() + "线程调用了obj的wait()方法。开始等待");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "线程被唤醒了,继续执行。。。");
}).start();
Thread.sleep(1000);
synchronized (obj){
System.out.println(Thread.currentThread().getName() + "线程调用了obj的notify()方法。唤醒其他线程");
obj.notify();
// obj.notifyAll();
}
}
}
注意:
- 调用wait()和notify()的对象必须是持有锁,否则将抛出异常。
- 调用wait()会使线程释放锁,等线程被唤醒之后,会去竞争锁。而调用sleep()则不会释放锁。
- notify()方法必须在wait()方法之后被调用,不然线程不会被唤醒。
- 当有多个线程在同一个对象上等待时,notify()方法只会随机唤醒其中一个线程,若想要唤醒所有线程可以使用notifyAll()方法。
6.等待线程结束(join)
在多线程协作中,经常需要某个线程等待其他线程结束后再执行,这时就可以使用join方法。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程开始执行。。。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "线程执行结束");
});
thread.start();
thread.join();
System.out.println(Thread.currentThread().getName() + "线程执行结束");
}
}
7.谦让(yeild)
谦让yield() 方法是一个静态方法,一旦执行,它会使当前线程让出CPU,在让出CPU后,该线程还会进行CPU资源的争夺
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
for(int i=0; i<10; i++){
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程开始执行。。。");
Thread.yield();
System.out.println(Thread.currentThread().getName() + "线程执行结束");
}).start();
}
}
}
6.volatile
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
new MyThread().start();
Thread.sleep(500);
System.out.println("修改b为false");
MyThread.b = false;
System.out.println(Thread.currentThread().getName() + "线程执行结束");
}
}
class MyThread extends Thread {
public static /*volatile*/ boolean b=true;
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始执行。。。");
while (b){
}
System.out.println(Thread.currentThread().getName() + "线程执行结束");
}
}
在这个例子中,有volatile和没有会产生两者不同的结果,当有volatile关键字时,程序能够按照我们想要的正常结束,但当注释掉volatile后,子线程会一直执行while循环,不会结束。
就这涉及到一些Java编译器优化知识,简单来说就是子线程在第一次使用变量b时会保存一份数据在自己线程内部,之后运行时再次用到时会在线程内部获取,当这个变量被其他线程修改后,该线程并不会知道变量已经被修改,还是会使用原来的值。当volatile关键字修饰变量b之后,子线程每次使用变量b都会去内存中获取该变量的值,所以此时程序能够正常执行。
注意:
- volatile 用来修饰变量,保证数据的可见性和有序性
- 不能代替锁,无法保证一些复合操作的原子性
7.线程组
当线程数量很多时,并行功能分配比较明确,就可以将相同功能的线程放置在同一个线程组里,来统一管理线程。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
ThreadGroup tg = new ThreadGroup("线程组名字");
new Thread(tg, new MyRunnable(), "t1").start();
new Thread(tg, new MyRunnable(), "t2").start();
System.out.println("存活线程个数: " + tg.activeCount());
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
// ...
}
}
8.守护线程
守护线程是一种特殊的线程,在后台默默的完成一些系统性的服务,比如Java的垃圾回收线程。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyRunnable());
thread.setDaemon(true);
thread.start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
}
}
注意:
- thread.setDaemon(true) 可以将线程设置为守护线程,但是必须在线程 start() 之前设置。
- 用户线程可以认为是系统的工作线程,它会完成这个程序应该要完成的业务操作,如果用户线程全部结束,那么整个应用程序就应该结束,即一个程序只有守护线程时,程序就会结束。
9.线程优先级
Java中的线程可以有自己的优先级,优先级高的线程在竞争资源时会更有优势。
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.setPriority(5);
thread.start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
}
}
注意:
- Java中使用 1 到 10 表示线程优先级,数字越大,表示优先级越高,默认为5
- 高优先级的线程只是在竞争资源时会更有优势,更可能抢到资源,但不能保证一定能抢到资源,并且在不同平台上,线程优先级表现不一,无法精准控制
10.线程安全
在讲线程同步之前,我们先来了解一下同步和异步两个概念。
- 同步: 同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
- 异步: 异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。
1. 多线程中数据安全问题
到这不难得出,多线程就是异步操作,但为何还需要线程同步呢。来看下面这个例子。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable(){
public Integer count = 0;
@Override
public void run() {
for(int i=0; i<10; i++){
count++;
System.out.println(count);
}
}
};
for(int i=0; i<100; i++){
new Thread(runnable).start();
}
}
}
在这段代码中共创建了100个线程,每个线程都执行了10次count++,按以前的程序串行执行任务,可以毫无疑问的得出最后一个count打印应该是1000,但真正执行这段程序时,往往每次运行结果都不一样,而且结果也基本是小于1000的。其原因就是count++并非是原子性操作,count++在计算机运行时,实际是分为两步:先计算出count+1的值,再将值赋值给count,所以在程序运行时可能发生数据安全问题:假设count此时值为90,线程a计算完count+1,这时线程a的时间片结束了,切换到线程b执行count+1,并将值赋值为count,这时count为91,线程b时间片结束切换到线程a,此时线程a已经计算完count+1为91,它将值赋值为count,count还是为91。
2. 线程同步synchronized
关键字synchronized的作用就是实现线程间的同步,它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable(){
public Integer count = 0;
@Override
public void run() {
for(int i=0; i<10; i++){
synchronized (this){
count++;
}
System.out.println(count);
}
}
};
for(int i=0; i<100; i++){
new Thread(runnable).start();
}
}
}
这里,我们仅仅只对前面的例子稍作改造了一下,程序已经能正确的得到我们想要的结果了。关键字synchronized 内置锁是一种对象锁(锁的是对象而非引用变量),作用粒度是对象 ,可以用来实现对 临界资源的同步互斥访问 ,每次当线程进入被关键字synchronized包裹的代码块时,就会要求请求synchronized修饰对象的锁,如果当前有其他线程正持有这把锁,那么新到的线程就必须等待,这样保证每次只有一个线程进入该同步代码块。
关键字synchronized的作用可以分为三块:
- 原子性:确保线程互斥的访问同步代码
- 可见性:保证共享变量的修改能够及时可见
- 有序性:有效解决重排序问题
关键字synchronized可以有多种用法:
- 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
- 直接作用于实例方法:相当于对当前实例对象加锁,进入同步代码前要获得当前实例对象的锁。
- 直接作用于静态方法:相当于对当前类对象(即Class对象)加锁,进入同步代码前要获得当前类对象的锁。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable(){
public Integer count = 0;
@Override
public void run() {
addCount();
}
public synchronized void addCount(){
for(int i=0; i<10; i++){
count++;
System.out.println(count);
}
}
};
for(int i=0; i<100; i++){
new Thread(runnable).start();
}
}
}
注意:synchronized虽然保证了线程间的数据安全,但其会降低程序的运行效率。在实际开发中使用synchronized关键字时,尽量减小synchronized同步代码块。
3.synchronized锁可重入
synchronized锁可重入指的就是当一个线程持有了某一个对象锁,这时该线程就可以直接进入该对象锁的其他同步代码块,可重入最大的作用是避免死锁。死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable(){
@Override
public void run() {
synchronized (this){
System.out.println(Thread.currentThread().getName() + "线程抢到了锁");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
a();
}
}
public synchronized void a(){
System.out.println(Thread.currentThread().getName() + "线程执行a方法");
}
};
for(int i=0; i<100; i++){
new Thread(runnable).start();
}
}
}
输出结果
Thread-3线程抢到了锁
Thread-3线程执行a方法
Thread-2线程抢到了锁
Thread-2线程执行a方法
Thread-1线程抢到了锁
Thread-1线程执行a方法
4.synchronized锁误区
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable(){
private Integer num = 0;
@Override
public void run() {
num++;
synchronized (num){
System.out.println(Thread.currentThread().getName() + "线程抢到了锁");
System.out.println(Thread.currentThread().getName() + "线程释放了锁");
}
}
};
for(int i=0; i<100; i++){
new Thread(runnable).start();
}
}
}
输出结果
Thread-0线程抢到了锁
Thread-1线程抢到了锁
Thread-1线程释放了锁
Thread-0线程释放了锁
这样的输出结果好像违反上面所讲的同步代码块同一时间只能有一个线程进入,其实不然,实例变量num是Integer包装类对象,每次num++之后,本质已经与之前的num不是同一个对象了,不同线程进入同步代码块时申请的也就不是同一个对象锁了。
11.Java容器中线程不安全问题
这里主要讲解ArrayList和HashMap两个容器的线程不安全问题。
1.多线程并发下ArrayList不安全问题
public class ThreadDemo {
static List<Integer> list = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyRunnable());
Thread t2 = new Thread(new MyRunnable());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(list.size());
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=0; i<10000; i++){
list.add(i);
}
}
}
}
运行这段代码,可能得到3个结果
- 程序正常结束,ArrayList的最终大小为20000,这说明即使并行下数据不安全,也不会每次都表现出来。
- 程序抛出异常
这时因为ArrayList在扩容过程中,内部一致性被破坏,但由于没有锁的保护,另一个线程访问到了不一致的内部状态,导致出现越界问题。Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 15 at java.util.ArrayList.add(ArrayList.java:463) at com.jingsuanxx.erp.ThreadDemo$MyRunnable.run(ThreadDemo.java:26) at java.lang.Thread.run(Thread.java:748)
- 程序正常结束,但ArrayList的大小小于20000,这时因为由于多线程访问冲突,使得保存容器大小的变量被多线程不正常访问,同时两个线程对ArrayList中的同一个位置进行赋值。
改进方法很简单,可以使用线程安全的容器代替ArrayList,比如Vector和CopyOnWriteArrayList,当然也可以使用synchronized关键字对list.add(i);这段代码加锁。
2.多线程并发下HashMap不安全问题
public class ThreadDemo {
static Map<String, String> map = new HashMap<>();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyRunnable());
Thread t2 = new Thread(new MyRunnable());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(map.size());
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=0; i<10000; i++){
map.put(Thread.currentThread().getName() + i, i + "");
}
}
}
}
运行这段代码,也可能得到3个结果
- 程序正常结束,HashMap的最终大小为20000。
- 程序正常结束,但HashMap的大小小于20000。这两点跟ArrayList问题类似。
- 程序抛出异常
这个异常是在jdk8中会出现的,原因是HashMap的数据结构是数组和链表组成的,当链表在达到阈值时,链表结构(Node类型节点)会转换成红黑树结构(TreeNode类型节点)。于是当一个线程将Node类型节点转换成TreeNode类型节点并平衡红黑树时,另一个线程执行put方法并正好将数据放到同一个数组格子中时,先会插入Node类型的数据,这将导致前一个线程平衡红黑树时在转换数据类型时发生异常。Exception in thread "Thread-0" Exception in thread "Thread-1" java.lang.ClassCastException: java.util.HashMap$Node cannot be cast to java.util.HashMap$TreeNode at java.util.HashMap$TreeNode.moveRootToFront(HashMap.java:1835) at java.util.HashMap$TreeNode.treeify(HashMap.java:1951) at java.util.HashMap.treeifyBin(HashMap.java:772) at java.util.HashMap.putVal(HashMap.java:644) at java.util.HashMap.put(HashMap.java:612) at com.jingsuanxx.erp.ThreadDemo$MyRunnable.run(ThreadDemo.java:26) at java.lang.Thread.run(Thread.java:748) java.lang.ClassCastException: java.util.HashMap$Node cannot be cast to java.util.HashMap$TreeNode at java.util.HashMap$TreeNode.moveRootToFront(HashMap.java:1835) at java.util.HashMap$TreeNode.treeify(HashMap.java:1951) at java.util.HashMap.treeifyBin(HashMap.java:772) at java.util.HashMap.putVal(HashMap.java:644) at java.util.HashMap.put(HashMap.java:612) at com.jingsuanxx.erp.ThreadDemo$MyRunnable.run(ThreadDemo.java:26) at java.lang.Thread.run(Thread.java:748)
- 程序永远无法结束,该现象笔者并未碰到过,只是在一些书中看到,所以简单提一下,其原因是两个线程在put数据时,会遍历HashMap内部数据,由于多线程冲突,导致这个链表结构遭到破坏,链表结构变成环形,于是遍历便成了死循环。