JAVA进阶3 多线程知识


JAVA进阶3 多线程知识_线程池


目录




一、一些概念

1. 线程与进程

一个程序至少有一个进程,一个进程至少有一个线程。
线程的划分尺度小于进程,使得多线程程序的并发性高。

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。

  • 进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。
  • 线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

2. 线程安全

经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果。

3. 线程状态

下图是线程的生命周期图:
JAVA进阶3 多线程知识_线程池_02

4. 线程Blocked状态说明

  1. 调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。
  2. 调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)
  3. 对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)。
    此外,在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。

5. monitor

Monitor其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象,主要特点是:

  • 对象的所有方法都被“互斥”的执行。好比一个Monitor只有一个运行“许可”,任一个线程进入任何一个方法都需要获得这个“许可”,离开时把许可归还。
  • 通常提供singal机制:允许正持有“许可”的线程暂时放弃“许可”,等待某个谓词成真(条件变量),而条件成立后,当前进程可以“通知”正在等待这个条件变量的线程,让他可以重新去获得运行许可。

Monitor对象可以被多线程安全地访问。
每个Java对象都有一个内部锁“Instrinsic lock”。有了这个锁的帮助,只要把类的所有对象方法都用synchronized关键字修饰,并且所有域都为私有(也就是只能通过方法访问对象状态),就是一个Monitor了。
示例:

public class Account {
private int balance;

public Account(int balance) {
this.balance = balance;
}

synchronized public boolean withdraw(int amount){
if(balance<amount)
return false;
balance -= amount;
return true;
}

synchronized public void deposit(int amount){
balance +=amount;
}

}

6. 线程组

ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。
相关方法:

  • ThreadGroup getThreadGroup() // 通过线程对象获取他所属于的组
  • getName() // 通过线程组对象获取线程组的名字
  • ThreadGroup(String name) // 创建指定名称的线程组对象
  • Thread(ThreadGroup group, Runnable target, String name) // 创建线程对象

示例:

ThreadGroup tg = new ThreadGroup("我是一个新的线程组");    //创建新的线程组
MyThread4 mt = new MyThread4(); //创建Runnable的子类对象
Thread m1 = new Thread(tg, mt,"张三"); //将线程m1放在组中
Thread m2 = new Thread(tg, mt,"李四"); //将线程m1放在组中
System.out.println(m1.getThreadGroup().getName()); //获取组名
System.out.println(m2.getThreadGroup().getName());

二、synchronized关键字

1. 修饰方法

表示进入该方法需要对Instrinsic lock加锁,离开时放锁。

2. 用在程序块中

对哪个对象的Instrinsic lock加锁。

示例:

synchronized public void deposit(int amount){
balance +=amount;
}
// 等价于
public void deposit(int amount){
synchronized(this){
balance +=amount;
}
}

三、基本线程类

基本线程类指的是Thread类,Runnable接口,Callable接口
Thread 类实现了Runnable接口,启动一个线程的方法:

MyThread my = new MyThread();
my.start();

1. Thread类的相关方法

//当前线程可转让cpu控制权,让别的就绪状态线程运行(切换)
public static Thread.yield()
//暂停一段时间
public static Thread.sleep()
//在一个线程中调用other.join(),将等待other执行完后才继续本线程。    
public join()
//后两个函数皆可以被打断
public interrupte()

关于中断:它并不像stop方法那样会中断一个正在运行的线程。线程会不时地检测中断标识位,以判断线程是否应该被中断(中断标识值是否为true)。终端只会影响到wait状态、sleep状态和join状态。被打断的线程会抛出InterruptedException。
​​​Thread.interrupted()​​​检查当前线程是否发生中断,返回boolean
synchronized在获锁的过程中是不能被中断的。

中断是一个状态,interrupt()方法只是将这个状态置为true而已。所以说正常运行的程序不去检测状态,就不会终止,而wait等阻塞方法会去检查并抛出异常。如果在正常运行的程序中添加​​while(!Thread.interrupted())​​ ,则同样可以在中断后离开代码体.

2. Thread类最佳实践

写的时候最好要设置线程名称 ​​Thread.name​​​,并设置线程组 ​​ThreadGroup​​,目的是方便管理。在出现问题的时候,打印线程栈 (jstack -pid) 一眼就可以看出是哪个线程出的问题,这个线程是干什么的。

示例:

public class Demo01_Thread {
public static void main(String[] args) {
MyThread mh = new MyThread();
mh.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println("线程执行到:"+i);
}
}
}

3. Runaable

示例:

public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread tr = new Thread(mr);
tr.start();
}
}
class MyRunnable implements Runnable{

@Override
public void run() {
for (int i = 0; i < 10000; i++) { //2.重写Run方法
System.out.println(Thread.currentThread().getName()+"线程执行到:"+i);
}
}
}

4. Callable

future模式:并发模式的一种,可以有两种形式,即无阻塞和阻塞,分别是isDone和get。其中Future对象用来存放该线程的返回值以及状态

ExecutorService e = Executors.newFixedThreadPool(3);
//submit方法有多重参数版本,及支持callable也能够支持runnable接口类型.
Future future = e.submit(new myCallable());
future.isDone() //return true,false 无阻塞
future.get() // return 返回值,阻塞直到该线程运行结束

四、用户线程和守护线程

Java中线程分为用户线程、守护线程两种。

  • 主线程结束后用户线程还能继续运行
  • 守护线程不能单独存在,没用户线程了守护线程自动退出
  • 守护线程的优化级比较低,用于为系统中的其它对象和线程提供服务

可以通过setDaemon()方法设置线程为守护线程,未设置的默认为用户线程。
Java的垃圾回收就是典型的守护线程。

示例:

import java.io.IOException;
class TestMain extends Thread {
public void run() {
for(int i=0;;i++){
try {
Thread.sleep(1000);
} catch (InterruptedException ex) { }
System.out.println(i);
}
}

public static void main(String [] args){
TestMain test = new TestMain();
test.setDaemon(true); // 设置线程为守护线程
test.start();
System.out.println("isDaemon = " + test.isDaemon());
try {
System.in.read();
} catch (IOException ex) {}
}
}

五、处理Java线程池中的异常消失

由于线程池自身的保护机制,不会将异常打印到控制台,所以有时程序莫名其妙的结束。下面代码加异常捕获:

public  void start() {    
System.out.println("async TNonblockingServer start ....");

Runnable simple = new Runnable() {
public void run() {
TProcessor tprocessor = new MessageForwardsService.Processor<MessageForwardsService.Iface>(new MessageForwardsRpcInterface());

nonblocking(tprocessor);
}
};

Thread t = new Thread(simple);
t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {

@Override
public void uncaughtException(Thread t, Throwable e) {
//可以捕获到
System.out.println("uncaughtExceptionHandler catch a Exception---------");
System.out.println(e.getMessage());
}
});
t.start();

}

不能用try catch来获取线程中的异常。

六、高级多线程控制类

由jdk1.5+提供的java.util.concurrent包,包含了一些高级的多线程控制类。

1.ThreadLocal类

用来保存线程的独立变量。

2.原子类(AtomicInteger、AtomicBoolean……)

使用atomicInteger,或者自己保证原子操作,用来实现synchronized功能。

3.Lock类

包含三个实现:

  • ReentrantLock:提供lock和unlock方法来进行同步。
  • ReentrantReadWriteLock.ReadLock
  • ReentrantReadWriteLock.WriteLock
    类似synchronized功能。
  • 使用ReentrantLock类的newCondition()方法可以获取Condition对象
  • 需要等待的时候使用Condition的await()方法, 唤醒的时候用signal()方法
  • 不同的线程使用不同的Condition, 这样能区分唤醒哪个线程

4.容器类

  • BlockingQueue 阻塞队列
  • ConcurrentHashMap 高效线程安全的map

5.管理类

下面重点提到管理类的线程池。

6. 线程间通信

  • wait():线程等待
  • notify():随机唤醒一个线程
  • notifyAll():唤醒所有线程

七、线程池

程序启动新线程的资源占用比较大,线程池可以更好地管理线程,提高多线程的性能。
线程池有自动创建、手动创建两种方式。

1. 线程池类型

Java有四种线程池:

  • newSingleThreadExecutor:单线程化的线程池。
  • newFixedThreadPool:定长的线程池,可以控制最大并发数,超出的在队列中等待。
  • newScheduleThreadPool:创建一个可定期或延时执行任务的定长线程池,支持定时及周期性任务。
  • newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,无可回收的就创建新线程。

自动创建线程池的方法在Executors工具类,可以方便地创建上面4种线程池。
但直接使用这几个线程池可能因为疏忽造成线程数量溢出,在使用时需要非常小心。

2. 最佳实践

7.2.1 不要显式创建线程 请使用线程池

这是阿里的代码规范要求的,防止因不明工具类的内部机制造成线程池堆积,如:

  • Executors的FixedThreadPool和SingleThreadPool允许请求的队列长度为Integer最大值
  • Executors的CachedThreadPool和ScheduledThreadPool创建线程池允许创建线程数量为Integer最大值

建议使用创建线程池,可以自主控制线程池的大小等参数:
定义:

public ThreadPoolExecutor(int corePoolSize,     // 线程池中核心线程数的最大值
int maximumPoolSize, // 线程池中能拥有最多线程数
long keepAliveTime, // 表示空闲线程的存活时间
TimeUnit unit, // 表示keepAliveTime的单位
BlockingQueue<Runnable> workQueue, // 用于缓存任务的阻塞队列
ThreadFactory threadFactory, // 指定创建线程的工厂
RejectedExecutionHandler handler // 当workQueue已满,且池中的线程数达到maximumPoolSize时,线程池拒绝添加新任务时采取的策略
)

其中 ​​handler​​ 的值取:

  • ThreadPoolExecutor.AbortPolicy() 抛出RejectedExecutionException异常
  • ThreadPoolExecutor.CallerRunsPolicy() 由向线程池提交任务的线程来执行该任务
  • ThreadPoolExecutor.DiscardOldestPolicy() 抛弃最旧的任务(最先提交而没有得到执行的任务)
  • ThreadPoolExecutor.DiscardPolicy() 抛弃当前的任务

示例:

ThreadPoolExecutor executor= new ThreadPoolExecutor(2, 10, 1, TimeUnit.SECONDS ,
new LinkedBlockingQueue<Runnable>(50) , Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
executor.execute(()->{

});
//

7.2.2 使用ScheduledExecutorService代替Timer

Timer的内部只有一个线程,如果多个任务的话会按顺序执行。而ScheduledExecutorService是线程池,阿里的编程规范建议使用它代替Timer。
Timer的代码示例:

Timer timer=new Timer();
timer.schedule(()->{

},1000,1000);
//停止
timer.purge();
timer.cancel();

如果使用newSingleThreadScheduledExecutor代替Timer的代码:

ScheduledExecutorService executorService=null;
executorService = Executors.newSingleThreadScheduledExecutor();
executorService.scheduleAtFixedRate(() -> {
//定时的代码
}, 1000, 1000, TimeUnit.SECONDS);
}

但这个代码也不符合阿里编程规范,原因是线程池允许创建线程的数量没有约束,为Integer.MAX_VALUE。
阿里的编程规范要求:
JAVA进阶3 多线程知识_线程池_03
可引用guava的ThreadFactoryBuilder来创建线程池:

//延迟执行时间(秒)
long delay = 0;
//执行的时间间隔(秒)
long period = 5;
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("thread-runner-%d").build();
ScheduledExecutorService scheduExec = new ScheduledThreadPoolExecutor(1,namedThreadFactory);
service.scheduleAtFixedRate(()->{
//定时代码
}, delay, period, TimeUnit.SECONDS);

3. 在Springboot中使用线程池

@Configuration
public class ThreadPoolConfig {
@Bean(value = "threadPoolInstance")
public ExecutorService createThreadPoolInstance() {
// 创建线程池
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-pool-%d").build();
ExecutorService threadPool = new ThreadPoolExecutor(10, 16, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), threadFactory, new ThreadPoolExecutor.AbortPolicy());
return threadPool;
}
}
//通过线程池名称引用线程池实例
@Resource(name = "threadPoolInstance")
private ExecutorService executorService;

public void myfunction() {
executorService.execute(()->{

});
}

4. 关闭线程池相关的方法

  • shutdownNow():立即关闭线程池,正在执行中的及队列中的任务会被中断,同时该方法会返回被中断的队列中的任务列表
  • shutdown():平滑关闭线程池,正在执行中的及队列中的任务能执行完成,后续进来的任务会被拒绝执行
  • isTerminated():当正在执行的任务及对列中的任务全部都执行完返回true

5. 阻塞任务直到获取运行结果

7.5.1 Future示例:

ExecutorService executorService = Executors.newFixedThreadPool(10);
Future<Integer> future = executorService.submit(new Task());
Integer integer = future.get();
System.out.println(integer);
executorService.shutdown();

7.5.2 FutureTask示例:

ExecutorService executorService = Executors.newFixedThreadPool(10);
FutureTask<Integer> futureTask = new FutureTask<>(new Task());
executorService.submit(futureTask);
Integer integer = futureTask.get();
System.out.println(integer);
executorService.shutdown();

相关方法:

  • get():等待任务完成获取结果
  • get(long timeout, TimeUnit unit) : 等待执行完成,但有超时时间,超时时抛出TimeoutException异常
  • cancel(boolean mayInterruptIfRunning) : 取消任务
  • isDone(): 任务是否执行完毕
  • isCancelled():任务是否被取消