Java多线程 (详解)

目录

一、认识线程

1.1 进程

1.2 线程

二、线程的创建

三、Thread 类

3.1 run() 和 start()

3.2 构造方法

3.3 属性

3.4 后台线程

3.5 线程存活 

3.6 线程终止

3.7 线程等待 

四、线程状态

五、线程安全

5.1 认识线程安全

5.2 synchronized

5.3 死锁

5.4 volatile

5.5 wait() 和 notify()

总结


一、认识线程

1.1 进程

进程是操作系统中正在运行的程序,其组织为链表。

① 进程是系统进行资源分配的基本单位。

② 每个进程的内存空间彼此独立,互不干扰。

③ 进程间的切换会有较大的开销。

④ 一个进程至少包含一个线程。

多进程:操作系统中能够同时运行多个任务。

1.2 线程

线程,也称作"轻量级进程"。线程是进程中的单个顺序控制流,可以理解为一条执行路径。

① 线程是系统进行调度执行的基本单位。

② 同一进程中的线程共享内存空间。

③ 线程间切换的开销较小。

④ 进程中第一个创建的线程即为主线程。

多线程:一个进程中有多个顺序流在执行(并发执行)。

【线程与进程的其他区别】

1、在一些需要频繁的创建和销毁进程的场景下,若使用多进程编程,系统开销就会很大,此时就需要使用多线程编程,减少系统开销。

2、同一进程中的多个线程共享同一份系统资源,这样在创建线程时,就省去了申请资源开销;销毁线程时,也省去了释放资源的开销。

3、一个进程挂了一般不会影响到其他进程;但一个线程挂了,可能把同进程内的其他线程一起带走(整个进程崩溃)。


二、线程的创建

1、 继承 Thread 类

(1) 创建一个继承 Thread 类的线程类 MyThread。

(2) 创建 MyThread 实例。

(3) 调用 start() 方法启动线程。

//继承 Thread创建线程类
class MyThread extends Thread {
    //线程入口方法 run(),每个线程启动后都必须执行的方法。
    @Override
    public void run() {
        System.out.println("Thread run");
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        //创建实例
        Thread t = new MyThread();
        //启动线程
        //start()方法的作用:启动一个分支线程,在 JVM中开辟一个新的栈空间,
        //只要新的栈空间开出来后,start()方法就结束了,线程就启动成功。
        t.start();
        //启动成功的线程会自动调用 run()方法。
    }
}

2、实现 Runnable 接口

(1) 创建一个实现 Runnable 接口的线程类 MyThread。

(2) 创建 Thread 类实例,调用 Thread 的构造方法时将 Runnable 对象作为参数。

(3) 调用 start() 方法启动线程。

//实现 Runnable接口
//Runnable可以理解为"可执行的",
//通过该接口,可以抽象表示出一段可以被其他实体执行的代码
class MyThread implements Runnable {
    //线程入口
    @Override
    public void run() {
        System.out.println("Thread run");
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        //创建 Thread实例,Runnable对象作为参数
        //这样可以把 线程 和 要执行的任务 进行解耦合操作
        Thread t = new Thread(new MyThread());
        //启动线程
        t.start();
    }
}

3、匿名内部类、Thread 

(1) 使用匿名内部类创建 Thread 子类对象。(匿名内部类:没有名字,只用一次。)

(2) 调用 start() 方法启动线程。

public class ThreadDemo {
    public static void main(String[] args) {
        //使用匿名内部类创建 Thread子类对象
        Thread t = new Thread() {
            @Override
            public void run() {
                System.out.println("Thread run");
            }
        };
        //启动线程
        t.start();
    }
}

4、匿名内部类、 Runnable

(1) 使用匿名内部类创建 Runnable 子类对象,其作为 Thread 构造方法的参数。

(2) 调用 start() 方法启动线程。

public class ThreadDemo {
    public static void main(String[] args) {
        //使用匿名内部类创建 Runnable子类对象
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread run");
            }
        });
        //启动线程
        t.start();
    }
}

5、lambda 表达式(推荐)

(1) 用 lambda 表达式创建 Runnable 子类对象,其作为 Thread 构造方法的参数。

(2) 调用 start() 方法启动线程。

public class ThreadDemo {
    public static void main(String[] args) {
        //用 lambda 表达式创建 Runnable 子类对象
        Thread t = new Thread(() -> {
            System.out.println("Thread run");
        });
        //启动线程
        t.start();
    }
}

 【Thread 和 Runnable】

若一个类继承 Thread 类,则不适合资源共享;若实现 Runnable 接口,则很容易实现资源共享。

实现 Runnable 接口还可以避免 Java 中单继承的限制。


三、Thread 类

3.1 run() 和 start()

Thread 类可以使用 start() 方法来启动一个线程,启动线程后,系统会自动调用 run() 方法,而对于同一个 Thread 对象来说,start() 方法只能调用一次。所以如果要启动更多线程,就需要创建新的对象。

若直接调用 run() 是无法启动线程的,就不会分配新的分支栈,还会导致无法多线程并发。

上文中重写 run() 方法,类似于提供给线程一个指令清单,而调用 start() 方法,才开始执行指令,即开始执行 run() 方法。所以调用 start() 方法,才能真正在操作系统的内核中创建出一个线程。


3.2 构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名

3.3 属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被终止isInterrupted()

注: 获取当前线程引用可以使用 Thread.currentThread()。线程休眠可以使用 Thread.sleep(long millis)。

  • getId():ID 是 JVM 自动分配给线程的身份表示,保证唯一性。
  • getName():名称用于各种调试工具。
  • getState():状态表示线程当前所处情况,例如就绪状态、阻塞状态等。
  • getPriority():优先级高的线程理论上更容易被调度,但由于系统的随机调度,可能不准确。
  • isDaemon():JVM 会在一个进程的所有非后台线程结束后,才会结束运行。
  • isAlive():内核中线程是否还存在。
  • isInterrupted():线程 run() 方法是否被终止。

注:操作系统的"内核",即最核心的部分。内核中有一个"调度机"模块,实现方式类似于"随机调度",执行方式为"抢占式执行"。


3.4 后台线程

在 Java 中,我们可以利用 setDaemon() 方法设置线程为后台线程,与后台线程相对的还有前台线程。前台线程的运行会阻止进程结束;后台线程的运行不会阻止进程结束。而我们代码创建的线程默认就是前台线程,会阻止进程结束,只要前台线程没执行完,进程就不会结束,即使 main(主线程)已经执行完毕。

public class ThreadDemo {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println("Thread run");
                try {
                    //线程休眠 2 秒
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"MyThread");//给线程命名

        //设置线程为后台线程(不能在 start之后)
        t.setDaemon(true); //设置为 true为后台,不设置为 true为前台

        //启动线程
        t.start();
    }
}

3.5 线程存活 

在 Java 中,isAlive() 方法可以让我们知道内核中的线程是否还存在,当我们创建出线程对象(Thread)实例时,这个对象本身的生命周期和内核中的线程生命周期是完全不一样的。

1、创建出 Thread 对象时,尽管有了对象,但内核中的线程还未创建,故 isAlive 为 false。

2、当调用 start() 方法时,系统在内核中创建出线程,此时 isAlive 为 true。

3、当线程的 run() 方法执行结束,内核中的线程也就释放了,此时 isAlive 为 false。


3.6 线程终止

终止一个线程,即让线程的 run() 方法执行结束。故要想让线程提前终止,核心就是让 run() 方法提前结束。如果 main 线程想让 t 线程提前结束,而 t 线程里的代码没有配合,main 线程是无法让 t 线程提前结束的,所以说代码配合是让线程结束的前提

例如下方代码,如果想让 t 线程提前结束,此时就需要引入标志位,在 t 线程 run() 方法的 while 循环条件中放入 Thread 类自带的标志位 isInterrupted()(该标志位为blooean类型),后续在 main 线程中利用 interrupt() 改变标志位的值,令循环结束,从而实现终止 t 线程。

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            //标志位放入 while 循环的条件中
           while (!Thread.currentThread().isInterrupted()) {
               System.out.println("Thread run");
               //线程休眠 1 秒
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   break;
               }
           }
            System.out.println("线程释放");
        });

        //启动线程
        t.start();

        //休眠 3 秒
        Thread.sleep(3000);

        //利用 interrupt() 修改标志位的值
        System.out.println("让 t 线程结束");
        t.interrupt();
    }
}

3.7 线程等待 

在多线程中,由于系统的随机调度和抢占式执行,多个线程的执行顺序是无法确定的,这时就可以利用一些 API 来影响线程执行顺序。在 Thread 类中,可以使用 join() 方法实现等待线程(哪个线程调用 join() 哪个线程就阻塞等待)

例如,在 main 线程中调用 t.join(),此时若 t 线程正在运行,main 线程就会进入阻塞状态,等待 t 线程运行结束;若 t 线程运行结束,main 线程就会从阻塞中恢复,继续往下执行。这样就成功让 t 线程先执行,main 线程后执行,影响了线程的执行顺序。

/**
 * 让主线程创建一个新线程,
 * 由新线程完成一系列运算(例如,1+2+3+...+100),
 * 最后由主线程负责获取到最终结果。
 */

public class ThreadDemo {
    static int sum = 0;
    public static void main(String[] args) throws InterruptedException {

        Thread t = new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                sum += i;
            }
        });
        //启动线程
        t.start();

        //等待 t 线程运算结束,才能确保得到完整的运算结果
        t.join();

        //若不等待 t 线程运算结束,则结果可能并不完整
        System.out.println(sum);
    }
}

注:推荐使用 join(long millis) 方法实现线程等待,即最多等 millis 毫秒就结束等待,比 join() 方法的死等安全。 


四、线程状态

【Java 中的线程状态】

① NEW:Thread 对象创建好了,但还未调用 start() 创建线程。

② RUNNABLE:就绪状态,表示这个线程正在 CPU 上执行,或准备就绪随时可以去 CPU 上执行。

③ TERMINATED:Thread 对象仍然存在,但是系统内部的线程已经执行结束了。

④ WAITING:不带时间的阻塞 (死等),必须要满足一定条件,才会解除阻塞。使用 join() 或 wait() 都会进入此状态。

⑤ TIMED_WAITING:指定时间的阻塞,到达一定时间会自动解除阻塞。使用 sleep() 或  join(long millis) 都会进入此状态。

⑥ BLOCKED:锁竞争引起的阻塞。

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Thread run");
                //休眠 1 秒
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        System.out.println(t.getState());//NEW:线程启动之前
        t.start();//启动线程
        System.out.println(t.getState());//RUNNABLE:线程启动之后

        Thread.sleep(500);//线程休眠 0.5秒
        System.out.println(t.getState());//TIMED_WAITING:线程休眠

        t.join();//WAITING:等待线程

        System.out.println(t.getState());//TERMINATED:线程执行结束
    }
}

注:在线程运行过程中,可以前往 JDK 中 bin 目录下 jconsole.exe 中查看进程状态。


五、线程安全

5.1 认识线程安全

在 Java 中引入多线程的目的是为了能够实现"并发编程",若只使用多线程难免不会遇到安全问题。例如,我们创建两个线程 t1 和 t2,每个线程分别进行自增10000次的操作,最后由 main 线程获取到最终结果。

public class ThreadDemo {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        //线程 1
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        });
        //线程 2
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        });

        //线程启动
        t1.start();
        t2.start();

        //线程等待
        t1.join();
        t2.join();
        
        //预期结果为 20000
        System.out.println("count = " + count);
    }
}

上述代码预期结果为 20000,但运行多次就会发现,无法确保每次都得到 20000,这种情况就是线程存在安全问题。发生这种情况是因为系统在运行 count++ 这个操作时,实际上有三条指令:① load(读取)、② add(执行)、③ save(保存),而由于两个线程并发执行,有可能在 t1 load 后,还未来得及 add,t2 就开始进行 load 等操作,此时就有可能产生本应该自增 2 次甚至 3 次,结果只自增了 1 次的情况,故无法保证每次结果都达到预期。

此时就可以发现,导致线程不安全的根本原因是操作系统上的线程是"随机调度"和"抢占式执行"直接原因是进行自增操作的指令不具备"原子性"。所以我们要确保在 t1 完成 save 后,t2 才会进行 load 操作,这样才能保证线程安全。


5.2 synchronized

【用法】

1、修饰代码块:明确锁哪个对象

(1)锁任意对象

public class SynchronizedDemo {
    Object locker = new Object();
    
    public void method() {
        //可以理解为 "{"上锁,"}"解锁
        synchronized (locker) {
            
        }
    }
}

(2) 锁当前对象

public class SynchronizedDemo {
    public void method() {
        //可以理解为 "{"上锁,"}"解锁
        synchronized (this) {
            
        }
    }
}

2、 修饰普通方法:相当于给 this 加锁

public class SynchronizedDemo {
    public synchronized void method() {
        
    }
}

3、 修饰静态方法:相当于给类对象 (.class) 加锁

public class SynchronizedDemo {
    public synchronized static void method() {
        
    }
}

【两个特性】

① 互斥性:一个线程给一个对象上锁后,若其他线程也要给同一个对象上锁,此时其他线程就会阻塞等待 (BLOCKED),需等上锁的线程解锁后才能上锁。

② 可重入性:synchronized 对同一线程来说是可重入的,即同一线程可以对同一对象多次上锁,但一个锁对象只会有一把锁。

注:对于可重入锁来说,内部有两个信息:① 当前这个锁是哪个线程的;② 加锁次数的计数器。

【实例】 

鉴于上述的线程不安全问题,此时我们就可以使用 synchronized 关键字给自增操作中的指令上锁,将三个指令打包成一个原子的操作,以维护线程安全。

public class ThreadDemo {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {

        //实例化一个对象
        Object locker = new Object();

        //线程 1
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });
        //线程 2
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });

        //线程启动
        t1.start();
        t2.start();

        //线程等待
        t1.join();
        t2.join();

        System.out.println("count = " + count);
    }
}

上述代码首先实例化一个对象 locker,然后分别将两个线程都对 locker 上锁,此时就利用到了 synchronized 的互斥性,实现了锁竞争,成功维护线程安全。


5.3 死锁

【四个必要条件】

① 互斥使用:获取锁的过程是互斥的,一个线程拿到一把锁,另一个线程也想拿这把锁,此时就需要阻塞等待。

② 请求保持:一个线程拿到锁 A 后,在持有锁 A 的前提下,尝试获取锁 B。

③ 不可抢占:一个线程拿到一把锁后,只能主动解锁,不能让别的线程强行把锁抢走。

④ 循环等待:发生死锁时,必然存在一个线程—资源的循环链。

【三种典型场景】

1、一个线程一把锁

上文中说到,一个线程可以给一把可重入锁多次上锁,不会出现死锁的情况。故如果锁是不可重入锁,此时一个线程对这把锁加锁两次,就会出现死锁。

2、两个线程两把锁

线程 t1 获取到锁 A,线程 t2 获取到锁 B,然后线程 t1 尝试获取锁 B,线程 t2 尝试获取锁 A,此时就会出现死锁。

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Object A = new Object();
        Object B = new Object();

        Thread t1 = new Thread(() -> {
            //获取锁 A
            synchronized (A) {
                System.out.println("t1 获得锁 A");
                //休眠一秒,保证 t2 获得锁 B
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //尝试获取锁 B
                synchronized (B) {
                    System.out.println("t1 获得全部锁");
                }
            }
        });

        Thread t2 = new Thread(() -> {
           //获取锁 B
           synchronized (B) {
               System.out.println("t2 获得锁 B");
               //休眠一秒,保证 t1 获得锁 A
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
               //尝试获取锁 A
               synchronized (A) {
                   System.out.println("t2 获得全部锁");
               }
           }
        });

        //线程启动
        t1.start();
        t2.start();
    }
}

3、N 个线程 N 把锁

这种场景就是最经典的哲学家进餐问题,该问题是描述有五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替地进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐毕,放下筷子继续思考。

实际在这种场景下,大部分情况都是可以正常运行的,但难免存在一些极端情况,例如,在同一时刻,五个哲学家都饿了,都拿起了左边的筷子,此时他们尝试拿起右边的筷子,但是右边的筷子已经没有了,没人进餐,也没人放下筷子,就出现了死锁。

【解决死锁】

解决死锁的关键就在于破坏死锁的四个必要条件,其中循环等待最好破坏,只需要制定一些规则即可有效避免循环等待。例如,引入加锁顺序的规则,我们给每根筷子进行编号,规定到每位哲学家必须先拿编号小的,才能再拿编号大的,若编号小的被拿了,直接进入阻塞等待,这样就可以成功避免死锁。上文中场景二的死锁也可以用同样的办法解决,规定 t1 和 t2 都是先拿到锁 A,才能拿锁 B,就可以避免死锁。

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Object A = new Object();
        Object B = new Object();

        Thread t1 = new Thread(() -> {
            //获取锁 A
            synchronized (A) {
                System.out.println("t1 获得锁 A");
                //尝试获取锁 B
                synchronized (B) {
                    System.out.println("t1 获得全部锁");
                }
            }
        });

        Thread t2 = new Thread(() -> {
           //获取锁 A
           synchronized (A) {
               System.out.println("t2 获得锁 A");
               //尝试获取锁 B
               synchronized (B) {
                   System.out.println("t2 获得全部锁");
               }
           }
        });

        //线程启动
        t1.start();
        t2.start();
    }
}

5.4 volatile

volatile 关键字一个功能是强制读取内存,保证内存可见性;另一个功能是禁止指令重排序。例如,创建一个标志位 flag,再创建两个线程,t1 将 flag 是否等于 0 作为循环条件放入 while,t2 负责输入 flag 的值,预期结果是 t2 中输入除 0 以外的数字,t1 线程都可以结束。

import java.util.Scanner;

public class ThreadDemo {

    private static int flag = 0;
    public static void main(String[] args) {
        //线程 1
        Thread t1 = new Thread(() -> {
            while (flag == 0) {
                //循环体没有内容
            }
            System.out.println("t1 线程结束");
        });
        //线程 2
        Thread t2 = new Thread(() -> {
            System.out.println("请输入 flag 的值:");
            Scanner sc = new Scanner(System.in);
            flag = sc.nextInt();
        });

        //线程启动
        t1.start();
        t2.start();
    }
}

运行后我们会发现,输入除 0 以外的数字,t1 线程并不会结束,这相当于 t2 修改了内存,但是 t1 没有看见内存的变化,这就称为 "内存可见性" 引起的线程安全问题。具体原因是在 t1 线程的 while 循环判定条件时,核心指令有两条:① load(读取 flag 的值到内存中);② 拿着寄存器的值和 0 进行比较。此时由于系统的循环执行速度非常快,在不停的执行两条指令,可每次执行 load 指令的结果都是一样的,由于 load 指令的开销大,此时 JVM 就会觉得这条 load 指令没有存在的必要,就会进行代码优化,将 "主内存" 中 flag 的值复制到 "工作内存" 中,后续都是读取寄存器中的值,以减少开销,这就导致无论后续输入什么值,load 读取 flag 的值都为 0。

故 "内存可见性" 问题实际上是个高度依赖编译器优化的问题,什么时候会出现这个问题是不确定的。此时为了能够确保不出现这种问题。Java 就引入了 volatile 关键字,它可以确保每次 while 循环都会重新从内存中读取数据,即给成员变量 flag 加上 volatile 修饰,就可以解决问题。


 【线程不安全总结】

① 操作系统的线程是 "抢占式执行"、"随机调度" 的;

② 多线程中的操作不具备 "原子性";

③ 内存可见性问题;

④ 指令重排序问题;

⑤ 代码结构。例如:多个线程同时修改一个变量。


5.5 wait() 和 notify()

wait():让当前持有 Object 对象锁的线程进入阻塞等待状态,并释放持有的 Object 对象锁。

notify():唤醒正在 Object 对象上等待的线程,若有多个正在等待的线程,就随机唤醒一个。

wait() 和 notify() 需要一个统一的 Object 对象进行加锁,而且两个方法都必须建立在 synchronized 线程同步的基础上。

例如,首先创建一个统一的对象来调用 wait() 和 notify() 方法,然后创建两个线程,此时 t1 获取到 locker 锁,然后调用 wait() 进入阻塞等待,此时由于调用了 wait(),所以 locker 锁已被释放,t2 获得 locker 锁后,调用 notify() 将 t1 唤醒,可由于 t2 尚未释放锁,所以 t1 被唤醒后,还会有一小段锁竞争引起的阻塞等待,等 t2 执行完,释放锁后,t1 方可继续执行。

public class ThreadDemo {
    public static void main(String[] args) {
        //wait()和 notify()需要一个统一的对象进行加锁
        Object locker = new Object();

        //线程 1
        Thread t1 = new Thread(() -> {
            //获取 locker 锁
            synchronized (locker) {

                System.out.println("t1 wait 之前");
                //t1 wait
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 wait 之后");
            }

            System.out.println("t1 执行结束");
        });

        //线程 2
        Thread t2 = new Thread(() -> {
            //休眠 3 秒,保证 t1 获取到 locker 锁
            try {
                Thread.sleep(3000);

                //获取 locker 锁
                synchronized (locker) {
                    System.out.println("t2 notify 之前");
                    //t2 notify
                    locker.notify();
                    System.out.println("t2 notify 之后");

                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println("t2 执行结束");
        });

        //线程启动
        t1.start();
        t2.start();
    }
}

【其他方法】

wait(long timeoutMillis):表示进入 wait 后,最多等待 timeoutMillis ms,若这个时间内没有收到 notify,就自行唤醒。

notifyAll():可以唤醒所有在 Object 对象上等待的线程。 


【wait() 和 sleep()】

1、wait() 需要搭配 synchronized 使用,而 sleep() 不需要。

2、wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法。


总结

1、进程是系统分配资源的基本单位,线程是系统调度执行的基本单位。

2、进程是包含线程的,每个进程至少有一个线程存在,即主线程。

3、进程和进程之间不共享内存空间,同一个进程的线程之间共享同一个内存空间。

4、若直接调用 run() 方法无法启动线程,就不会分配新的分支栈,会导致无法多线程并发。

5、对于同一个 Thread 对象来说,start() 方法只能调用一次。

6、前台线程的运行会阻止进程结束;后台线程的运行不会阻止进程结束。

7、在线程运行过程中,可以前往 JDK 中 bin 目录下 jconsole.exe 中查看进程状态。

8、在 Java 中,任何一个对象都可以作为锁对象。

9、解决死锁的关键就在于破坏死锁的四个必要条件。

10、volatile 关键字一个功能是强制读取内存,保证内存可见性;另一个功能是禁止指令重排序。

11、wait() 和 notify() 需要一个统一的 Object 对象进行加锁,而且两个方法都必须建立在 synchronized 线程同步的基础上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值