java多线程基础理解
参考博客:https://angela.blog.csdn.net/article/details/105625514
1、进程和线程
一个应用就是一个进程,一个进程可以包含多个线程,从操作系统层面看,同一个进程中的线程共享该进程的资源,例如内存空间和文件句柄。Linux 操作系统中线程是轻量级进程。
2、创建线程的两种方式
- 继承 Thread 类;
- 实现 Runnable 接口;
实例代码如下所示:
public static void main(String[] args) {
new Seller("笔").start();
new Thread(new Seller02("书")).start();
}
//第一种方式 继承 Thread
public static class Seller extends Thread{
String product;
public Seller(String product){
this.product = product;
}
@Override
public void run() {
System.out.println("继承 Thread类 卖 " + product);
}
}
//第二种方式 实现 Runnable
public static class Seller02 implements Runnable{
String product;
public Seller02(String product){
this.product = product;
}
@Override
public void run() {
System.out.println("实现 Runnable接口 卖 " + product);
}
}
2.1、start()和run()方法的区别
对象调用run()方法是本地线程操作;而调用start()方法,start()方法是java中的Thread类给我们操作当前操作系统中的线程的方法。
当执行到start()方法的时候,JVM会另起一个线程来执行在run()方法中的代码。
当我们在java代码中创建线程的时候,如下:
new Seller("笔")
操作系统不会真的帮我们来创建一个线程出来,而当我们调用了'.start()'方法之后,java通过与操作系统的接口,来映射操作系统的线程。
其实java中的线程就是操作系统中线程的映射,java操作在java代码中书写的线程,映射到操作系统中的线程。
例子说明:
public static void main(String[] args) {
new Seller("笔").run(); //没有另起一个线程
new Seller("笔").start(); //在新线程中执行 run 函数
}
//第一种方式 继承 Thread
public static class Seller extends Thread{
String product;
public Seller(String product){
this.product = product;
}
@Override
public void run() {
System.out.println(String.format("当前线程: %s 卖%s", Thread.currentThread().getName(), product));
}
}
查看控制台结果:
当前线程: main 卖笔
当前线程: Thread-1 卖笔
可以看到当前的线程名称是,第二个是新起来的线程对象。因为调用start()
方法后,JVM 会新建一个线程来执行run()
方法内容。
2.2、Thread类特点
Java 种新建的Thread 对象只是操作系统线程运行的载体,Thread类的作用主要有二点:
1、Thread 对象内的属性提供了创建新线程时所需要的线程描述信息,例如线程名、线程id、线程组、是否为守护线程;
2、Thread 对象内的方法提供了Java 程序可以跟操作系统线程打交道的手段,例如wait、sleep、join、interrupt等。
前面说到JVM new Thread对象时其实还没有真实创建线程,调用start() 方法时才开始正式创建。
关于线程的创建和销毁,需要从线程的生命周期来进行说明。
3、线程生命周期
在java线程中,提供了一个枚举类来说明线程的状态:
public static enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
下面用图来进行说明一下:
上面标注出来了线程的状态,并且还说明了线程之间如何通过方法从一个状态切换到另外一个状态的。
3.1、线程状态描述
-
New: 刚创建而未启动的线程就是这个状态。由于一个线程只能被启动一次,因此一个线程只可能有一次在这个状态。
-
Runnable:如上图,这个状态实际是个复合状态,包含两个子状态:Ready 和 Running。Ready是就绪状态,可以被JVM 线程调度器(Scheduler) 进行调度,如果是单核CPU,同一时刻只有一个线程处于Running 状态,可能有多个线程处于 Ready 状态,Running 表示当前线程正在被CPU 执行,在Java 中就是Thread 对象只 run() 方法正在被执行。当 yield() 方法被调用,或者线程时间片被用完,线程就会从 Running 状态转为 Ready 状态。
-
Blocked:一个线程发生一个阻塞式I/0 (文件读写I/O, 网络读写I/O)时,或者试图获取其他线程持有的锁时,线程会进入此状态,例如:获取别的线程已经持有的 synchronized 修饰的对象锁。在Blocked 状态的线程不会占用CPU 资源(这里看了好多资料,这里总算是搞清楚了),但是程序如果出现大量处于这个状态的线程,需要警惕了,可以考虑优化一下程序性能。
-
Waiting: 一个线程执行了Object.wait( )、 Thread.join( ) 、LockSupport.park( ) 后会进入这个状态,这个状态是处于无限等待状态,没有指定等待时间,可以和Timed_Waiting 对比,Timed_Waiting是有等待时间的。这个状态的线程如果要恢复到Runnable 状态需要通过别的线程调用Object.notify( )、Object.notifyAll( )、LockSupport.unpark( thread )。
-
Timed_Waiting: 带时间限制的Waiting。
-
Terminated: 已经执行结束的线程处于此状态。Thread 的 run( ) 方法执行结束,或者由于异常而提前终止都会让线程处于这个状态。
3.2、常见方法描述
wait( )、sleep( )、join( )、yield( ) 、notify()、notifyAll( ) 都是做什么的?什么区别?
上面这些方法都是线程控制方法,JAVA 通过这些方法跟它创建的操作系统线程进行交互,具体如下:
- wait():
线程等待,调用该方法会让线程进入 Waiting 状态,同时很重要的一点,线程会释放对象锁,所以wait 方法一般用在同步方法或同步代码块中;
- sleep(long time):
线程休眠,调用该方法会让线程进入Time_Waiting 状态,调sleep 方法需要传入一个参数标识线程需要休眠的时间;
- yield:
线程让步,yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争CPU 时间片,一般来说,优先级高的线程有更大的可能性成功竞争到CPU 时间片,但不是绝对的,有的系统对优先级不敏感。
- join:
在当前线程中调用另一个线程的join 方法,则当前线程转为阻塞状态,等到另一线程执行结束,当前线程才会从阻塞状态变为就绪状态,等待CPU 的调度。(之前只知道这么来进行使用,但是一直为能够了解清楚)
写个代码一看就明白:
public static void main(String[] args) {
System.out.println(String.format("主线程%s 开始运行...", Thread.currentThread().getName()));
Thread threadA = new Thread(new ThreadA());
threadA.start();
try {
// 主线程 wait(0) 释放 thread 对象锁,主线程进入 waiting 状态
threadA.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("主线程%s 运行结束...", Thread.currentThread().getName()));
}
private static class ThreadA implements Runnable{
@Override
public void run() {
System.out.println(String.format("子线程%s 开始运行...", Thread.currentThread().getName()));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("子线程%s 准备结束运行...", Thread.currentThread().getName()));
}
}
控制台输出结果:
主线程main 开始运行...
子线程Thread-0 开始运行...
子线程Thread-0 准备结束运行...
主线程main 运行结束...
主线程调用threadA.join()
导致主线程等Thread-0 线程执行结束才开始继续执行。
join() 函数的内部实现如下:
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
// 如果当前Thread对象关联的线程还是存活的,当前正在执行的线程进入 Waitting状态,如果当前Thread对象关联的线程执行结束,会 // 调用notifyAll() 唤醒进入 Waitting状态的线程。
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
//wait 属于 Object 对象方法
public class Object{
//线程进入 Time_Watting 或 Waiting 状态
public final native void wait(long timeout) throws InterruptedException;
}
上面的注释写的非常棒!之前没有留意到这一点。
为了便于大家理解,我画了图(一言不合就上图),大家对照着代码和图看,上面代码主要有二个线程,主线程和 ThreadA 线程,主线程创建ThreadA并启动ThreadA线程,然后调用threadA.join() 会导致主线程阻塞,直到ThreadA 线程执行结束 isActive 变为 false,主线程恢复继续执行。
- interrupt():
线程中断,调用interrupt 方法中断一个线程,是希望给这个线程一个通知信号,会改变线程内部的一个中断标识位,线程本身并不会因为中断而改变状态(如阻塞、终止等)。调用interrupt 方法有二种情况:
如果当前线程正处于 Running 状态,interrupt( ) 只会改变中断标识位,不会真的中断正在运行的线程;
如果线程当前处于 Timed_Waiting 状态,interrupt( ) 会让线程抛出 InterruptedException。
所以我们在编写多线程程序时,优雅关闭线程需要同时处理这二种情况,常规写法是:
public static class ThreadInterrupt implements Runnable{
@Override
public void run() {
//1. 非阻塞状态,通过检查中断标识位退出
while(!Thread.currentThread().isInterrupted()){
try{
//doSomething()
Thread.sleep(1000);
} catch (InterruptedException e) {
//2. 阻塞状态,捕获中断异常,break 退出
e.printStackTrace();
break;
}
}
}
}
上面这只是提供了一种优雅的让线程中断的方式
- notify():
notify方法和wait方法一样,也是Object 类中的方法,notify方法用于唤醒在此对象监视器(monitor)上等待的单个线程,如果有多个线程在此对象监视器上等待,选择其中一个进行唤醒。另外要注意一点的是,当前线程唤醒等待线程后不会立即释放锁,而是当前线程执行结束才会释放锁,因此被唤醒的线程不是说唤醒之后立即就可以开始执行,而是要等到唤醒的线程执行结束,获得对象锁之后开始执行。
看一下代码:
public static void main(String[] args) {
new Thread(new ThreadA()).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new ThreadB()).start();
}
private static final Object lock = new Object();
private static class ThreadA implements Runnable{
@Override
public void run() {
synchronized (lock){
System.out.println("Thread-A 进入状态 running...");
try {
System.out.println("Thread-A 进入状态 waiting...");
lock.wait();
System.out.println("Thread-A 进入状态 running...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread-A 执行完毕, 进入状态 terminated...");
}
}
private static class ThreadB implements Runnable{
@Override
public void run() {
synchronized (lock){
System.out.println("Thread-B 进入状态 running...");
try {
System.out.println("Thread-B 进入状态 time_waiting...");
Thread.sleep(3000);
System.out.println("Thread-B 进入状态 running...");
lock.notify();
System.out.println("Thread-B 进入状态 time_waiting...");
Thread.sleep(5000);
System.out.println("Thread-B 进入状态 running...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread-B 执行完毕, 进入状态 terminated...");
}
}
查看控制台输出信息:
Thread-A 进入状态 running...
Thread-A 进入状态 waiting...
Thread-B 进入状态 running...
Thread-B 进入状态 time_waiting...
Thread-B 进入状态 running...
Thread-B 进入状态 time_waiting...
Thread-B 进入状态 running...
Thread-B 执行完毕, 进入状态 terminated...
Thread-A 进入状态 running...
Thread-A 执行完毕, 进入状态 terminated...
可以看到B 线程调用 lock.notify() 之后A 线程没有立即开始执行,而是等到B 线程执行结束后才开始执行,所以lock.notify() 唤醒 A 线程只是让 A 线程进入预备执行的状态,而不是直接进 Running 状态,B 线程调 notify 没有立即释放对象锁。
4、面试篇
4.1、关闭线程的方式有哪几种?哪种方式最可取?(美团一面面试题)
1、使用退出标识位;
public class ThreadSafe extends Thread {
public volatile boolean exit = false;
public void run() {
while (!exit){
//do something
}
}
}
2、调用 interrupt 方法,这种是最可取的,但是要考虑到处理二种情况;
3、stop 方法,这种属于强行终止,非常危险。就像直接给线程断电,调用thread.stop() 方法时,会释放子线程持有的所有锁,这种突然的释放可能会导致数据不一致,因此不推荐使用这种方式终止线程。
4.2、wait 和sleep 的区别?(比心一面面试题)
主要有以下3点:
1、sleep 方法让线程进入 Timed_Waiting 状态,sleep 方法必须传入时间参数,会让当前线程挂起一段时间,过了这个时间会恢复到runnable 状态(取决于系统计时器和调度程序的精度和准确性)。而wait 方法会让当前线程进入Waiting 状态,会一直阻塞,直到别的线程调用 notify 或者 notifyAll 方法唤醒。
2、wait 是Object 类中的方法,sleep 是Thread 类中的方法,理解这点很重要,wait方法跟对象绑定的,调用wait方法会释放wait 关联的对象锁;
3、如果在同步代码块,当前线程持有锁,执行到wait 方法会释放对象锁,sleep 只是单纯休眠,不会释放锁;
我们看个代码巩固一下:
public static void main(String[] args) {
new Thread(new ThreadA()).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new ThreadB()).start();
}
private static final Object lock = new Object();
private static class ThreadA implements Runnable{
@Override
public void run() {
synchronized (lock){
System.out.println("Thread-A 进入状态 running...");
try {
System.out.println("Thread-A 进入状态 waiting...");
lock.wait();
System.out.println("Thread-A 进入状态 running...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread-A 执行完毕, 进入状态 terminated...");
}
}
private static class ThreadB implements Runnable{
@Override
public void run() {
synchronized (lock){
System.out.println("Thread-B 进入状态 running...");
try {
System.out.println("Thread-B 进入状态 time_waiting...");
Thread.sleep(3000);
System.out.println("Thread-B 进入状态 running...");
lock.notify();
System.out.println("Thread-B 进入状态 time_waiting...");
Thread.sleep(5000);
System.out.println("Thread-B 进入状态 running...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread-B 执行完毕, 进入状态 terminated...");
}
}
输出台控制如下所示:
Thread-A 进入状态 running...
Thread-A 进入状态 waiting...
Thread-B 进入状态 running...
Thread-B 进入状态 time_waiting...
Thread-B 进入状态 running...
Thread-B 进入状态 time_waiting...
Thread-B 进入状态 running...
Thread-B 执行完毕, 进入状态 terminated...
Thread-A 进入状态 running...
Thread-A 执行完毕, 进入状态 terminated...
4.3、手写一个死锁的例子(美团二面面试题)
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static class DeadLockSample implements Runnable{
Object[] locks;
public DeadLockSample(Object lock1, Object lock2){
locks = new Object[2];
locks[0] = lock1;
locks[1] = lock2;
}
@Override
public void run() {
synchronized (locks[0]) {
try {
Thread.sleep(3000);
synchronized (locks[1]) {
System.out.println(String.format("%s come in...", Thread.currentThread().getName()));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Thread a = new Thread(new DeadLockSample(lock1, lock2));
Thread b = new Thread(new DeadLockSample(lock2, lock1));
a.start();
b.start();
}
查看控制台,发现一直处于卡顿状态。
4.4、通过线程wait / notify通信的生产者消费者代码?(声网四面面试题)
static class MangoIce{
int counter;
public MangoIce(int counter) {
this.counter = counter;
}
}
static class Producer implements Runnable
{
private final List<MangoIce> barCounter;
private final int MAX_CAPACITY;
public Producer(List<MangoIce> sharedQueue, int size)
{
this.barCounter = sharedQueue;
this.MAX_CAPACITY = size;
}
@Override
public void run()
{
int counter = 1;
while (!Thread.currentThread().isInterrupted())
{
try
{
produce(counter++);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
break;
}
}
}
private void produce(int i) throws InterruptedException
{
synchronized (barCounter)
{
while (barCounter.size() == MAX_CAPACITY)
{
System.out.println("吧台满了,冰沙放不下 " + Thread.currentThread().getName() + " 线程等待,当前吧台冰沙数: " + barCounter.size());
barCounter.wait();
}
Thread.sleep(1000);
barCounter.add(new MangoIce(i));
System.out.println("生产第: " + i + "杯冰沙...");
barCounter.notifyAll();
}
}
}
static class Consumer implements Runnable
{
private final List<MangoIce> barCounter;
public Consumer(List<MangoIce> sharedQueue)
{
this.barCounter = sharedQueue;
}
@Override
public void run()
{
while (!Thread.currentThread().isInterrupted())
{
try
{
consume();
} catch (InterruptedException ex)
{
ex.printStackTrace();
break;
}
}
}
private void consume() throws InterruptedException
{
synchronized (barCounter)
{
while (barCounter.isEmpty())
{
System.out.println("吧台空的,没有冰沙 " + Thread.currentThread().getName() + " 消费者线程等待,当前吧台冰沙数: " + barCounter.size());
barCounter.wait();
}
Thread.sleep(1000);
MangoIce i = barCounter.remove(0);
System.out.println("消费第: " + i.counter + "杯冰沙...");
barCounter.notifyAll();
}
}
}
public static void main(String[] args)
{
List<MangoIce> taskQueue = new ArrayList<>();
int MAX_CAPACITY = 5;
Thread tProducer = new Thread(new Producer(taskQueue, MAX_CAPACITY), "生产者");
Thread tConsumer = new Thread(new Consumer(taskQueue), "消费者");
tProducer.start();
tConsumer.start();
}
也就是说多个线程之间可以协调来做事情。