多线程编程细节
Thread类
Thread类实现了Runnable接口,所以Thread对象也是可运行Runnable对象,同时Thread类也是线程类
构造器
Thread()//一般用于在Thread类中覆盖定义run方法,可以使用匿名内部类进行定义
Thread(Runnable)//使用最多的情况,run方式是由Runnable参数对象提供
Thread(String name) //自定义线程名称
Thread(Runnable,String name)
… …
//常见简化写法
Thread t = new Thread(()->{
System.out.println(Thread.currentThread());
});
t.start();
常见方法:
方法 | 说明 |
---|---|
void start() | 使该线程开始执行,注意不是立即执行,不是一般方法调用;Java 虚拟机调用该 线程的 run 方法 |
void run() | 线程的执行体 |
void setName(String) | 改变线程名称 |
void setPriority(int) | 更改线程的优先级,Java中线程的优先级可以分为1-10,默认为5 |
void setDaemon(boolean) | 设置守护线程,守护线程是一种用于提供服务的线程,一般线程体中使用的是死循 环,会在所有非守护线程退出后自动关闭 |
void join()/(long millisec) | 等待该线程终止的时间最长为 millis 毫秒 |
void interrupt() | 中断线程,不是中断线程的执行,而是修改中断参数 |
boolean isAlive() | 测试线程是否处于活动状态,活动状态就是线程已经启动且尚未终止。线程处于 正在运行或准备开始运行的状态,就认为线程是“存活”的 |
static void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
static void sleep(long millisec) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计 时器和调度程序精度和准确性的影响 |
static Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
Runnable接口
Runnable接口只定义了一个方法public void run(),这个方法要求实现Runnable接口的类实现,Runnable对象称 为可运行对象,一个线程的运行就是执行该对象的run()方法
run()方法没有返回值void,而且不能抛出异常
class MyRunnable implements Runnable{
@Override
public void run()throws Exception {//语法报错,这里不允许抛出异常,如果其中有异常则需要使用 try.catch处理
//没有返回值,如果需要记录处理结果,需要自己编程处理
}
}
//简化写法
new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("左手画一条龙...");
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Callable接口
继承Thread或实现Runnable接口这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。
如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。
call()方法有返回值,这个返回值可以通过泛型进行约束,允许抛出异常。
class MyRunnable implements Callable<Number> {
// <>中用于指定返回值类型,必须使用引用类型,不能使用简单类型
public Number call() throws Exception {// 允许抛出异常
return null;
}
}
// 简化写法
new Thread(new FutureTask<>(()->{
for(int i = 0;i<10;i++){
System.out.println("右手画彩虹");
Thread.sleep(30);// 因为call方法允许抛出异常
}
return null;
})).start();
Future接口
Future表示一个任务的生命周期,并提供了方法来判断是否已经完成或取消以及获取任务的结果和取消任务等
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通 过get方法获取执行结果,该方法会阻塞直到任务返回结果。
- cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数 mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消 正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返 回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为 true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论 mayInterruptIfRunning为true还是false,肯定返回true
- isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true
- isDone方法表示任务是否已经完成,若任务完成,则返回true
- get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回
- get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就抛出 TimeoutException超时异常。
Future f = new FutureTask(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread() + "...start..." + i);
Thread.sleep(10);
System.out.println(Thread.currentThread() + "...end..." + i); }
return null;
});
if(f instanceof Runnable)
new Thread((Runnable)f).start();
int counter = 0;while(true){
Thread.sleep(20);
System.out.println("任务是否被取消:" + f.isCancelled() + "--" + counter);
System.out.println("任务是否执行完毕:" + f.isDone());
counter++;
if (counter > 10)
f.cancel(true);//取消任务的执行
if (counter > 12)
break;
}
Future f = new FutureTask(() -> {
int res=0;
for (int i = 0; i < 1000; i++) {
Thread.sleep(10);
res+=i;
}
return res;
});
if(f instanceof Runnable){
new Thread((Runnable) f).start();
int counter = 0;
long start = System.currentTimeMillis();
// Object obj=f.get();
Object obj = f.get(5, TimeUnit.SECONDS);// 参数1为超时时长,参数2为时长的单位,是一个枚举类型 数据,超时TimeoutException
long end = System.currentTimeMillis();
System.out.println("get...执行时间为:" + (end - start) + "ms");
System.out.println("线程执行结果为:" + obj);
}
FutureTask
public class FutureTask<V> implements RunnableFuture<V>
具体使用
FutureTask<Integer> future = new FutureTask<Integer>(callable);
new Thread(future).start();
FutureTask实现了两个接口,Runnable和Future,所以它既可以作为Runnable被线程执行,又可以作为Future得 到Callable的返回值
FutureTask是一个可取消的异步计算,FutureTask 实现了Future的基本方法,提供start cancel 操作,可以查询 计算是否已经完成,并且可以获取计算的结果。结果只可以在计算完成之后获取,get方法会阻塞当计算没有完成 的时候,一旦计算已经完成, 那么计算就不能再次启动或是取消。
一个FutureTask 可以用来包装一个 Callable 或是一个Runnable对象。因为FurtureTask实现了Runnable方法,所 以一个 FutureTask可以提交(submit)给一个Excutor执行(excution). 它同时实现了Callable, 所以也可以作为Future 得到Callable的返回值。
ThreadPoolExecutor
先有一个大概了解,在集合框架后深入认识
ThreadPoolExecutor是线程池框架的一个核心类,线程池通过线程复用机制,并对线程进行统一管理
- 降低系统资源消耗。通过复用已存在的线程,降低线程创建和销毁造成的消耗;
- 提高响应速度。当有任务到达时,无需等待新线程的创建便能立即执行;
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗大量系统资源,还会降低系统的稳定性,使用线程池可以进行对线程进行统一的分配、调优和监控。
线程池的运行状态总共有5种,其值和含义分别如下:
- RUNNING: 高3位为111,接受新任务并处理阻塞队列中的任务
- SHUTDOWN: 高3位为000,不接受新任务但会处理阻塞队列中的任务
- STOP:高3位为001,不会接受新任务,也不会处理阻塞队列中的任务,并且中断正在运行的任务
- TIDYING: 高3位为010,所有任务都已终止,工作线程数量为0,线程池将转化到TIDYING状态,即将要执行 terminated()结束钩子方法
- TERMINATED: 高3位为011,terminated()方法已经执行结束
构造器中各个参数的含义:
1.corePoolSize
线程池中的核心线程数。当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于 corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行。
2.maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线 程数小于maximumPoolSize。
3.keepAliveTime
线程空闲时的存活时间。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作 用,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是 如果调用了allowCoreThreadTimeOut(boolean)方法,keepAliveTime参数也会起作用,直到线程池中的线程数为 0。
4.unit
keepAliveTime参数的时间单位。
5.workQueue
任务缓存队列,用来存放等待执行的任务。如果当前线程数为corePoolSize,继续提交的任务就会被保存到任务缓 存队列中,等待被执行。
一般来说,这里的BlockingQueue有以下三种选择:
- SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则 插入操作一直处于阻塞状态。因此,如果线程池中始终没有空闲线程(任务提交的平均速度快于被处理的速 度),可能出现无限制的线程增长。
- LinkedBlockingQueue:基于链表结构的阻塞队列,如果不设置初始化容量,其容量Integer.MAX_VALUE, 即为无界队列。因此,如果线程池中线程数达到了corePoolSize,且始终没有空闲线程(任务提交的平均速度 快于被处理的速度),任务缓存队列可能出现无限制的增长。
- ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务。
6.threadFactory
线程工厂,创建新线程时使用的线程工厂。
7.handler
任务拒绝策略,当阻塞队列满了,且线程池中的线程数达到maximumPoolSize,如果继续提交任务,就会采取任 务拒绝策略处理该任务,线程池提供了4种任务拒绝策略:
- AbortPolicy:丢弃任务并抛出RejectedExecutionException异常,默认策略;
- CallerRunsPolicy:由调用execute方法的线程执行该任务;
- DiscardPolicy:丢弃任务,但是不抛出异常;
- DiscardOldestPolicy:丢弃阻塞队列最前面的任务,然后重新尝试执行任务(重复此过程)。
当然也可以根据应用场景实现RejectedExecutionHandler接口自定义饱和策略,如记录日志或持久化存储不能处理的任务。
Executors创建线程池
- newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收重用时则新建线程
- 用来创建一个可以无限扩大的线程池,适用于服务器负载较轻,执行很多短期异步任务
- newFixedThreadPool 创建一个固定大小的定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
- 因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于可以预测线程数量的业务中,或者 服务器负载较重,对当前线程数量进行限制
- newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行
- 可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景
public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService
-
newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任 务按照指定顺序(FIFO, LIFO, 优先级)执行
适用于需要保证顺序执行各个任务,并且在任意时间点,不会有多个线程是活动的场景
-
newWorkStealingPool:创建一个拥有多个任务队列的线程池,可以减少连接数
创建当前可用cpu数量的线程来并行执行,适用于大耗时的操作,可以并行来执行
提交任务的方式
线程池框架提供了两种方式提交任务,submit()和execute(),通过submit()方法提交的任务可以返回任务执行的结 果,通过execute()方法提交的任务不能获取任务执行的结果。
关闭线程池
shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回 还没开始的任务列表
shutdown:当调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务
ExecutorService es = Executors.newFixedThreadPool(2);
es.submit(()->{
for(int i=0;i<10;i++){
System.out.println("Hello "+i);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println("main......");
ExecutorService es = Executors.newFixedThreadPool(2);
Future f = es.submit(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("Hello " + i);
Thread.sleep(200); } return 100;
});
Object obj=f.get(); //阻塞当前main线程
System.out.println("main......");
Java内存模型
Java内存模型定义了一种多线程访问Java内存的规范。
- Java内存模型将内存分为了主内存和工作内存。类的状态也就是类之间共享的变量,是存储在主内存中的,每 次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有 一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执 行完毕之后,会将最新的值更新到主内存中去
- 定义了几个原子操作,用于操作主内存和工作内存中的变量
- 定义了volatile变量的使用规则
- happens-before即先行发生原则,定义了操作A必然先行发生于操作B的一些规则,比如在同一个线程内控制 流前面的代码一定先行发生于控制流后面的代码、一个释放锁unlock的动作一定先行发生于后面对于同一个 锁进行锁定lock的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的 happens-before规则,则这段代码一定是线程非安全的
硬件模型
- Java内存模型JMM规定了JVM有主内存,主内存是多个线程共享的
- 当new一个对象时,也是被分配在主内存中,每个线程都有自己的工作内存,工作内存存储了主存的某些对象 的副本,当然线程的工作内存大小是有限制的
public class T1 {
private int num;
public static void main(String[] args) {
T1 t = new T1();
t.pp();
}
public void pp() {
// 通过对num的操作实现了t1和t2之间的通信,这里目前不保证输出的正确性.可以通过同步锁 synchronized保证数据的正确性
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++)
add();
});
new Thread(() -> {
for (int i = 0; i < 100; i++)
sub();
}).start();
t1.start();
}
public void add() {
num++;
System.out.println(Thread.currentThread() + "加法:" + num);
}
public void sub() {
num--;
System.out.println(Thread.currentThread() + "减法:" + num);
}
}
volatile关键字
volatile是java提供的一种同步手段,只不过它是轻量级的同步,为什么这么说,因为volatile只能保证多线程的内 存可见性,不能保证多线程的执行原子性。而最彻底的同步要保证有序性、可见性和原子性的synchronized
任何被volatile修饰的变量,都不拷贝副本到工作内存,任何修改都及时写在主存。因此对于volatile修饰的变量的 修改,所有线程马上就能看到,但是volatile不能保证对变量的修改是原子的
public class VolatileTest {
public volatile int a;
public void add(int count) {
a = a + count;
}
}
volatile存在的意义是,任何线程对a的修改,都会马上被其他线程读取到,因为直接操作主存,没有线程对工作内 存和主存的同步。所以,volatile的使用场景是有限的,在有限的一些情形下可以使用 volatile 变量替代锁
要使 volatile 变量提供理想的线程安全,必须同时满足两个条件
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
volatile特性
- 保证可见性:当写一个 volatile 变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存,使其 他线程立即可见
- 保证有序性:当变量被修饰为 volatile 时,JMM 会禁止读写该变量前后语句的大部分重排序优化,以保证变 量赋值操作的顺序与程序中的执行顺序一致
- 部分原子性:对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++ 这种复合操作不具有原子性
volatile的认识
public class Test1 {
private static boolean flag = false;
private static int i = 0;
public static void main(String[] args) {
new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(100);
flag = true;
System.out.println("flag changed...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
while (!flag) {
i++;
}
System.out.println("progress end...");
}
}
程序不能执行结束,会进入死循环状态。
解决方案:flag上添加关键字volatile
总结
Java内存模型规定和指引Java程序在不同的内存架构、CPU和操作系统间有确定性地行为。它在多线程的情况下尤 其重要。Java内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行发生关系。这个关系 定义了一些规则让程序员在并发编程时思路更清晰。
1、线程内的代码能够按先后顺序执行,这被称为程序次序规则。
2、对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。
3、前一个对volatile的写操作在后一个volatile的读操作之前,也叫volatile变量规则。
4、一个线程内的任何操作必需在这个线程的start()调用之后,也叫作线程启动规则。
5、一个线程的所有操作都会在线程终止之前,线程终止规则。
6、一个对象的终结操作必需在这个对象构造完成之后,也叫对象终结规则。
7、可传递性。如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论