JAVA进阶3 多线程知识
目录
- 一、一些概念
- 七、线程池
一、一些概念
1. 线程与进程
一个程序至少有一个进程,一个进程至少有一个线程。
线程的划分尺度小于进程,使得多线程程序的并发性高。
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。
- 进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。
- 线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
2. 线程安全
经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果。
3. 线程状态
下图是线程的生命周期图:
4. 线程Blocked状态说明
- 调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。
- 调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)
- 对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{
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{
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() {
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。
阿里的编程规范要求:
可引用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中使用线程池
public class ThreadPoolConfig {
(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;
}
}
//通过线程池名称引用线程池实例
(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():任务是否被取消