线程的生命周期

NEW 刚刚创建,还没有执行

RUNNABLE 调用start() 方法执行,处于RUNNABLE状态,表示线程所需资源都已经准备好,就绪状态?是否有正在执行状态?

BLOCKED 执行过程中遇到synchronize同步块,就会进入BLOCKED阻塞状态。线程会暂停执行,知道获得请求的锁。

WAITING 无时间限制的等待状态。线程在等待一些特殊事件,比如通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。一旦等到期望的事件,线程就会再次执行,进入RUNNABLE状态。

TIMED_WAITING 有时限的等待状态。

TERMINATED 线程执行完毕,进入TERMINATED状态,表示结束。

终止线程stop

正常情况下,线程执行完毕就会结束,无需手工关闭。

特殊情况下,比如常驻系统的后台线程,可能是一个无限循环,通常不会自动结束。

 

在线程A调用线程B.stop()方法关闭线程B,比较暴力,可能会引起数据不一致的问题。

更好的方法是在线程B内部增加退出的条件判断,满足条件后,正常结束线程B。

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TestThread implements Runnable {
    volatile static boolean bStop = false;
    
    public static void main(String[] args) throws InterruptedException {
        test3stopThread();
    }

    private static void test3stopThread() throws InterruptedException {
        log.info("main thread");
        Thread thread = new Thread(new TestStopTask());
        thread.start();
        Thread.sleep(1000);
        bStop = Boolean.TRUE;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            log.info(Thread.currentThread().getName() + ",子线程:" + i);
//            try {
//                Thread.sleep(10000);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }

        }
    }

    public static class TestStopTask implements Runnable {

        @Override
        public void run() {
            while (true) {
                System.out.println(bStop);
                if (bStop) {
                    System.out.println("exit by bStop");
                    break;
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
            }
            Thread.yield();
        }
    }
}


 

线程中断interrupt

重要的线程协作机制。

在线程A中调用目标线程B.interrupt(),给目标线程B发一个通知,通知目标线程B有人希望你退出。至于目标线程接到通知后如何处理,完全由目标线程决定。

Public void Thread.interrupt() // 中断线程,即发中断通知

Public Boolean Thread.isInterrupted() // 判断是否被中断,即是否收到中断通知

Public static Boolean Thread.interrupted() // 判断是否被中断,并清除当前中断状态

 

等待wait和通知notify

线程协作机制。

这两个方法属于Object类,意味着任何对象都可以调用这两个方法。

当线程A中,调用了object.wait()方法,线程A就会暂停执行,转为等待状态。线程A会一直等到其他线程调用了object.notify()方法为止。

通常object定义为final static Object object = new Object();

这样,object对象成为了多个线程之间通信的有效手段。

 

原理:当线程A调用object.wait()方法,该线程会进入object对象的等待队列。这个队列可能有多个线程,因为系统可能运行了多个线程同时等待一个对象。

当object.notify()被调用时,就会从等待队列随机选择一个线程并将其唤醒。另外object.notifyAll()被调用时,会唤醒等待队列中所有线程。

 

注意,object.wait()必须包含在Synchronized语句块中。无论wait()还是notify()都需要先获取目标对象的监视器。

 

相同的是,在线程A中调用object.wait()和Thread.sleep(),都可以让线程A等待。

区别一是object.wait()可以被(线程B)唤醒。

区别二是object.wait()会释放目标对象的锁,而Thread.sleep()不会释放任何资源。

 

挂起suspend和继续执行resume(废弃)

两个废弃方法。因为suspend在导致线程暂停的同时不会释放任何锁资源。此时,任何线程想要访问被其占用的锁时,都会被牵连,导致无法正常运行。

 

用法:在线程B中Thread.currentThread().suspend(); 挂起自己。在线程A中运行线程B,并调用线程B.resume()方法,但不一定生效。

 

等待线程结束join

如果在线程A中调用线程B.join()方法,表示线程A会无限等待线程B,并阻塞当前线程A,直到线程B结束。

同样的,在主线程A中调用异步任务CompletableFuture.join()方法,表示主线程阻塞,等待异步任务完成。


如果在线程A中调用线程A.join()方法,或者Thread.currentThread.join(),则阻塞自己,错误用法。

 

出让yield

Thread.yield()方法会使当前线程让出CPU。让出后还会进行CPU资源的争夺。

因此,调用Thread.yield()表示当前线程已经完成了最重要的工作,可以休息一会儿,给其他线程一些工作机会。

 

Volatile(易变的,不稳定的)和JMM

Java内存模型围绕着原子性,有序性和可见性展开。Java使用一些特殊关键字或者操作来告诉虚拟机,在这个地方需要特别注意,不要随意变动优化目标指令

 

Volatile对于保证操作的原子性有非常大的帮助。但是不能代替锁它也无法保证一些复合操作的原子性。

 

Volatile可以保证数据的可见性和有序性。确保一个线程修改了数据后,其他线程能够看到这个改动。

 

注意:Volatile并不能保证线程安全。当两个线程同时修改某一个数据时,依然会产生冲突。

 

用法:public volatile static int = 0;

 

线程安全和synchronized

关键字synchronized的作用是实现线程间的同步。通过对同步区的代码加锁,使得每次只有一个线程进入同步块,从而保证线程间的安全性。

同时,还可以保证线程间的可见性和有序性。

换言之,被关键字synchronized限制的多个线程是串行执行的。


问题:对性能消耗大。

用法

指定加锁对象:给指定对象加锁,进入同步区代码前获得给定对象的锁。

public class AccountingSync implements Runnable {
    static AccountingSync instance = new AccountingSync();
    static int i = 0;

    @Override
    public void run() {
        for (int j = 0; j< 100; j++) {
            synchronized (instance) {
                i++;
            }
        }

    }
}


直接作用于实例方法:相当于对当前实例加锁,进入同步区代码前获得给定当前实例的锁。

@Slf4j
public class AccountingSync implements Runnable {
    static AccountingSync instance = new AccountingSync();
    static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(instance);
        Thread thread2 = new Thread(instance);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        log.info("" + i);
    }

    public synchronized void increase() {
        i++;
    }

    @Override
    public void run() {
        for (int j = 0; j < 100; j++) {
            increase();
        }

    }
}

 错误的做法:

Thread thread1 = new Thread(new AccountingSync());
Thread thread2 = new Thread(new AccountingSync());

两个线程指向了两个不同的Runnable实例对象,因此thread1在进入同步区前会加锁自己的Runnable实例对象,而thread2也加锁自己的Runnable实例对象。

两个线程使用的两把不同的锁,因此线程安全无法保证。


直接作用于静态方法:相当于对当前类加锁,进入同步区代码前获得当前类的锁。

@Slf4j
public class AccountingSync implements Runnable {
    static AccountingSync instance = new AccountingSync();
    static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new AccountingSync());
        Thread thread2 = new Thread(new AccountingSync());

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        log.info("" + i);
    }

    public static synchronized void increase() {
        i++;
    }

    @Override
    public void run() {
        for (int j = 0; j < 100; j++) {
            increase();
        }

    }
}

如上,即使两个线程指向不同的Runnable实例对象,但是由于方法块需要请求的是当前类的锁,而非当前实例。

因此两个线程使用的是相同的锁,线程间还是可以正确同步,线程安全。 

常见的并发错误

在线程中操作线程不安全的数据类型,比如ArrayList和HashMap等

Static Map<String, String> map = new HashMap< String, String >();

解决方案,改用ConcurrentHashMap

 

使用Integer类型对象加锁

@Slf4j
public class TestSynchronized {
    static Integer i = 0;
    static WrongLockOnInteger instance = new WrongLockOnInteger();

    public static class WrongLockOnInteger implements Runnable {

        @Override
        public void run() {
            for(int j = 0; j < 200000; j++) {
                synchronized (i) {
                    i++;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(instance);
        Thread thread2 = new Thread(instance);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        log.info("" + i);
    }
}

分析:因为i++相当于i=Integer.valueOf(i.intValue() + 1);

而Integer.valueOf()实际上会每次返回new Integer();

所以每次加锁可能都加在不同的对象实例上。

 synchronized (i),实际上i对象一直在变。

解决:改为synchronized (instance)

线程组ThreadGroup

 

驻守后台:守护线程Daemon

比如垃圾回收线程,JIT线程等等。

用法:

t.setDaemon(true);

t.start();

 

线程的几种场景

有几个任务同时进行,只有所有的任务都完成,线程才完成。

有几个任务同时进行,只要其中一个任务完成,线程就完成。

一个线程的输入依赖另一个线程的输出

两个线程如何共享数据

 

Threadlocal

ThreadLocal 详解_似寒若暖的博客-CSDN博客_threadlocal

Thread的局部变量,线程局部变量ThreadlocalValue。不同线程之间互不干扰。在线程的生命周期内起作用。

在并发场景中,成员变量是线程不安全的,因为多个线程在同时操作同一个变量。

ThreadLocal使得每个线程都拥有自己独立的变量,竞态条件被彻底消除了,在并发模式下是绝对安全的变量。

解决多线程间共享变量线程安全问题的大杀器——ThreadLocal_threadlocal怎么保证线程安全_YHJ的博客-CSDN博客

在Thread类中定义了类型为ThreadLocalMap的变量ThreadLocals,是一个定制化的HashMap,初始值为null。

在ThreadLocal类中定义了内部静态类ThreadLocalMap,定义了get(), set(T), getMap()对其操作。

getMap(Thread t)

以当前线程作为参数和key,返回当前线程的变量ThreadLocals。

set()

获取当前线程,以当前线程

父子线程

深入解析父子线程_blpluto的博客-CSDN博客_父子线程

第一种是父线程是进程的主线程,子线程由主线程创建;

只需要主线程的进入点函数返回,就能够终止整个进程的运行。请注意,进程中运行的任何其他线程都随着进程而一起终止运行。

还有一种特殊的情况,如果子进程内发生死锁,那么这个子进程就无法退出,也会导致整个进程都无法退出。这种就是为什么有时候我们的程序已经退出(至少界面已经关闭),但任务管理器中却还有这个应用程序的进程存在的原因。

 

第二种情况是父线程为进程主线程创建的一个子线程,而这个子线程又创建了一个孙线程,这种情况大多被称为子孙线程。

假设主线程为A,创建了线程B,然后B又创建了线程C,现在,如果线程B终止了,那么线程C会不会也终止呢?

如果我们创建的C线程没有使用B线程的任何资源,也就是说,B线程创建的C线程,在创建之后,这两者就是相互独立的。所以这种情况下,如果B线程终止,那么C线程在线程函数没有返回的时候,是不会结束的。

 

线程之间共享数据

实现了BlockingQueue接口的ArrayBlockingQueue类和LinkedBlockingQueue。

前者适合做有界队列,后者适合做无界队列。

服务线程需要从队列取数据或者存数据,当队列为空或者满时,BlockingQueue会让服务线程等待,当有新消息进入队列(或者有消息从满队列移出后),自动将服务线程唤醒。

 

线程池

CompletableFuture 和@Async 配置自定义线程池_wangfenglei123456的博客-CSDN博客

 

单例和多线程

单例不是线程安全的。

在并发场景,多个线程对单例对象的非静态成员变量的写操作会存在线程安全问题。

有两种常⻅的解决⽅案:

  1. 在bean对象中尽量避免定义可变的成员变量(不太现实)。
  2. 在类中定义⼀个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal中(推荐的⼀种⽅式)。

面试官:Controller是单例还是多例?_androidstarjack的博客-CSDN博客

Spring中的Controller ,Service,Dao是不是线程安全的? (qq.com)

 

Spring默认是单例多线程模式

服务启动的时候,会启动一个主线程,main方法是入口。

当请求进来时,会为每个请求新增一个子线程。在并发请求场景,就可能会有线程安全问题。

Spring单例模式下的多线程访问_多个请求同一个接口是多线程吗_小白写程序的博客-CSDN博客

当请求进来时,怎么线程安全地获取请求头?

SpringBoot--Controller获取HttpServletRequest_controller httpservletrequest_IT利刃出鞘的博客-CSDN博客

 


线程的等待和通知

CountdownLatch

等待线程A

被等待线程B(B1,B2.。。)

所有的被等待线程B执行完毕后,A开始执行。

CountdownLatch设置了一个计数器,被等待线程执行完成一个,计数器减一。

在计数器减至0之前,A阻塞(在闭锁上等待)。

计数器减至0后,A开始执行。