目录
多线程的创建
方式一:继承java.lang.thread,重写run方法,创建子类实例,调用子类start方法
认识多线程中的 start() 和 run()
方式二:实现Runnable接口,创建thread实例,将Runnable接口传入构造函数
方式三:实现Callable接口(JDK8新特性)
线程的生命周期
设置/获取线程名字
获取当前线程对象
sleep静态方法
sleep面试题
interruput终断休眠
stop终止线程
线程的调度
1.1常见的线程调度模型有哪些?
1.2java提供的有哪些方法去设置线程调度?
2.多线程并发环境下的数据安全问题
2.1什么情况下会存在多线程并发环境下的安全问题?
2.2怎么解决线程安全问题呢?
使用“线程同步机制”.
2.3下面看一个卖票窗口的例子
Java内存模型JMM
什么是JMM
内存划分
Java并发编程的三大特性:原子性,可见性,有序性
原子性
可见性
有序性
Voliate关键字
Synchronized 同步代码块
Lock锁接口
一:java.util.concurrent.locks包下常用的类与接口(lock是jdk 1.5后新增的)
二:synchronized的缺陷
tryLock() & tryLock(long time, TimeUnit unit)
死锁的定义
守护线程
线程通信
关于Object类中的wait和notify方法(生产者消费者模式)
生产者和消费者代码演示
奇偶数打印
线程池
1. 为什么要用线程池
2. ThreadPoolExecutor线程池类参数详解
3. 线程池任务执行
3.1. 添加执行任务
3.2. 线程池任务提交过程
3.3. 线程池关闭
4. 常用队列介绍
5. Executors线程工厂类
多线程的创建
方式一:继承java.lang.thread,重写run方法,创建子类实例,调用子类start方法
public class Thread {
public static void main(String[] args) {
//这里是main方法,这里的代码属于主线程,在主栈中运行
//新建一个分支线程对象
MyThread myThread = new MyThread();
//启动线程
myThread.start();
//start()方法:启动一个分支线程,在JVM中开辟一个新空间,只要新的栈空间开出来,start方法的
//任务就结束了
//启动成功的线程会自动调用run方法,并且run()方法在分支栈的栈底部(压栈)
//run方法在分支栈的底部,main方法在主栈的底部,run和main是平级的
for (int i = 0; i < 10000; i++)
System.out.println("主线程-->"+ i);
}
}
class MyThread extends java.lang.Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++)
System.out.println("分支线程-->"+ i);
}
}
认识多线程中的 start() 和 run()
start():
先来看看Java API中对于该方法的介绍:
使该线程开始执行;Java 虚拟机调用该线程的
run
方法。结果是两个线程并发地运行;当前线程(从调用返回给
start
方法)和另一个线程(执行其run
方法)。多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
Exception in thread "main" java.lang.IllegalThreadStateException
用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体中的代码执行完毕而直接继续执行后续的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里的run()方法 称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
run():
同样先看看Java API中对该方法的介绍:
如果该线程是使用独立的
Runnable
运行对象构造的,则调用该Runnable
对象的run
方法;否则,该方法不执行任何操作并返回。
Thread
的子类应该重写该方法。run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。
总结:
调用start方法方可启动线程,而run方法只是thread类中的一个普通方法调用,还是在主线程里执行。
方式二:实现Runnable接口,创建thread实例,将Runnable接口传入构造函数
public class Thread02 {
public static void main(String[] args) {
//创建一个可执行的对象
MyRunnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
for (int i = 0; i < 1000; i++)
System.out.println("主线程-->"+ i);
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000; i++)
System.out.println("分支线程-->"+ i);
}
}
方式三:实现Callable接口(JDK8新特性)
这种方式实现的线程可以获得线程的返回值
前两种方法是无法获取线程返回值的,因为run方法返回void
思考:
系统委派一个线程去执行任务,该线程执行完任务之后,可能会有一个执行结果,我们怎么能拿到这个执行结果呢?
使用第三种方式:实现Callable接口
package com.mlj;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//第一步创建一个“未来人物类”,传入Callable接口
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
System.out.println("call method begin");
Thread.sleep(1000 * 10);
return 100;
}
});
//创建线程对象
Thread t = new Thread(task);
//启动线程
t.start();
//获取返回值
System.out.println(task.get());
System.out.println("main end");
}
}
实现Callable接口有缺点:task.get()获取返回值会阻塞main线程,因为阻塞当前线程等待实现Callable的线程执行结束返回结果
效率比较低
线程的生命周期
线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。
- 新建:就是刚使用new方法,new出来的线程;
- 就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
- 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;
- 销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;
新建状态
我们来看下面一段代码:
1 |
|
这里的创建,仅仅是在JAVA的这种编程语言层面被创建,而在操作系统层面,真正的线程还没有被创建。只有当我们调用了 start() 方法之后,该线程才会被创建出来,进入Runnable状态。只有当我们调用了 start() 方法之后,该线程才会被创建出来
就绪状态
调用start()方法后,JVM 进程会去创建一个新的线程,而此线程不会马上被 CPU 调度运行,进入Running状态,这里会有一个中间状态,就是Runnable状态,你可以理解为等待被 CPU 调度的状态
1 |
|
用一张图表示如下:
那么处于Runnable状态的线程能发生哪些状态转变?
Runnable状态的线程无法直接进入Blocked状态和Terminated状态的。只有处在Running状态的线程,换句话说,只有获得CPU调度执行权的线程才有资格进入Blocked状态和Terminated状态,Runnable状态的线程要么能被转换成Running状态,要么被意外终止。
运行状态
当CPU调度发生,并从任务队列中选中了某个Runnable线程时,该线程会进入Running执行状态,并且开始调用run()方法中逻辑代码。
那么处于Running状态的线程能发生哪些状态转变?
- 被转换成Terminated状态,比如调用 stop() 方法;
- 被转换成Blocked状态,比如调用了sleep, wait 方法被加入 waitSet 中;
- 被转换成Blocked状态,如进行 IO 阻塞操作,如查询数据库进入阻塞状态;
- 被转换成Blocked状态,比如获取某个锁的释放,而被加入该锁的阻塞队列中;
- 该线程的时间片用完,CPU 再次调度,进入Runnable状态;
- 线程主动调用 yield 方法,让出 CPU 资源,进入Runnable状态
阻塞状态
Blocked状态的线程能够发生哪些状态改变?
- 被转换成Terminated状态,比如调用 stop() 方法,或者是 JVM 意外 Crash;
- 被转换成Runnable状态,阻塞时间结束,比如读取到了数据库的数据后;
- 完成了指定时间的休眠,进入到Runnable状态;
- 正在wait中的线程,被其他线程调用notify/notifyAll方法唤醒,进入到Runnable状态;
- 线程获取到了想要的锁资源,进入Runnable状态;
- 线程在阻塞状态下被打断,如其他线程调用了interrupt方法,进入到Runnable状态;
终止状态
一旦线程进入了Terminated状态,就意味着这个线程生命的终结,哪些情况下,线程会进入到Terminated状态呢?
- 线程正常运行结束,生命周期结束;
- 线程运行过程中出现意外错误;
- JVM 异常结束,所有的线程生命周期均被结束。
设置/获取线程名字
t.setName("threadName")
t.getName()
获取当前线程对象
//currentThread 当前线程对象 //这个代码出现在main方法中,获得就是main线程 //出现在哪里,就代表哪个线程对象,类似this Thread thread = Thread.currentThread(); System.out.println("分支线程-->"+thread.getName());
sleep静态方法
static void sleep( long millis )
1.静态方法:Thread.sleep( 1000 );
2.参数是毫秒
3.作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其他线程使用
线程A调用Thread.sleep()进入休眠状态
sleep面试题
t.sleep(10000)会让线程休眠吗?
答:不会,sleep是Thread的静态方法,t.sleep()==Thread.sleep(),在main中调用阻塞main进程
public class Thread03 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new MyRunnable());
t.start();
t.sleep(10000);
System.out.println("~~~~~~~~~~~~~");
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i=0; i<1000; i++)
System.out.println("分支线程"+i);
}
}
interruput终断休眠
t.interruput() //终端t线程的睡眠(这种终断方式依靠java的异常处理机制)
stop终止线程
t.stop() //已过时,不建议使用
缺点:容易丢失数据,因为这种方式是直接将线程杀死,线程没保存的数据会丢失
线程的调度
1.1常见的线程调度模型有哪些?
抢占式调度模型:
哪个线程的优先级比较高,抢到的cpu时间片的概率就高一些/多一些
java采用的就是抢占式调度模型
均分是调度模型:
平均分配时间片。每个线程占有的cpu时间片长度一样。
1.2java提供的有哪些方法去设置线程调度?
实例方法:
t.getPriority() 获取线程优先级
t.setPriority(int newPriority) 设置线程优先级
最高优先级10, 最低优先级1 ,默认5
t.join()合并线程
静态方法:
static void yieid() 让位方法
暂停当前正在执行的线程,并让位给其他线程
yieid()方法不是阻塞方法,会让当前线程从“运行状态”回到“就绪状态”
2.多线程并发环境下的数据安全问题
2.1什么情况下会存在多线程并发环境下的安全问题?
- 多线程并发
- 有共享数据
- 有共享数据的修改行为
2.2怎么解决线程安全问题呢?
当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就会引起线程安全问题,怎么解决这个问题呢?
线程排队执行。不能并发,这种机制被称为:线程同步机制。
怎么解决线程安全问题呀?
使用“线程同步机制”.
线程同步就是线程排队了,线程排队就会牺牲一部分效率,但是数据安全是第一位。
2.3下面看一个卖票窗口的例子
package com.mlj;
public class Ticket {
static int ticket = 100;
public static void main(String[] args) {
Runnable runnable = () -> {
//卖票
while (true) {
if (ticket > 0)
{
ticket--;
System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余:"+ticket);
}else {
return;
}
}
};
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
Thread t3 = new Thread(runnable);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
可以看到结果完全是错的。对于线程安全的问题,首先得明白JMM。
Java内存模型JMM
什么是JMM
JMM即为JAVA 内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。JMM从java 5开始的JSR-133发布后,已经成熟和完善起来。
内存划分
JMM规定了内存主要划分为主内存和工作内存两种。此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。
JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。因为JMM制定了一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程。
Java并发编程的三大特性:原子性,可见性,有序性
原子性
即一个或者多个操作作为一个整体,要么全部执行,要么都不执行,并且操作在执行过程中不会被线程调度机制打断;而且这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换(context switch)。
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。
有序性
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序在单线程环境下最终执行结果和代码顺序执行的结果是一致的。
Voliate关键字
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
重排序。
注意:voliate无法保证原子性,自增操作不是原子性(读取变量,修改,写入内存)三个子操作
少BB,看代码
下面对flag变量用voliate修饰,线程2修改flag的值,线程1是立马可见的,退出while循环,结束程序
不加voliate关键字修饰变量,线程1永远使用工作内存中的flag,无法退出循环。
这种方式可以控制线程的终止。
package com.mlj;
public class VoliateTest {
volatile static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread( () -> {
System.out.println("计数线程开始");
int num = 0;
while (flag) {
num ++;
}
System.out.println("num:"+ num);
} ).start();
Thread.sleep(1000);
new Thread( () -> {
flag = false;
System.out.println("修改flag:"+ flag);
}).start();
}
}
Synchronized 同步代码块
使用synchronized关键字主要是为了保护变量的数据安全
- 实例变量 :存在多线程并发安全
- 静态变量:存在多线程并发安全
- 局部变量:不存在多线程并发安全
例如:当使用StringBuilder(线程不安全)和StringBuffer(线程安全)作为局部变量时,不存在多线程并发安全问题,使用StringBuilder效率较高
synchronized的三种应用方式
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
Lock锁接口
一:java.util.concurrent.locks包下常用的类与接口(lock是jdk 1.5后新增的)
二:synchronized的缺陷
synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?
Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
synchronized 的局限性 与 Lock 的优点
如果一个代码块被synchronized关键字修饰,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待直至占有锁的线程释放锁。事实上,占有锁的线程释放锁一般会是以下三种情况之一:
1:占有锁的线程执行完了该代码块,然后释放对锁的占有;
2:占有锁线程执行发生异常,此时JVM会让线程自动释放锁;
3:占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。
试考虑以下三种情况:
Case 1 :
调用sleep方法不释放锁)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待,别无他法。这会极大影响程序执行效率。因此,就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间 (解决方案:tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案:lockInterruptibly())),这种情况可以通过 Lock 解决。
Case 2 :
我们知道,当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突。同样地,Lock也可以解决这种情况 (解决方案:ReentrantReadWriteLock) 。
Case 3 :
我们可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock) ,但这个是synchronized无法办到的。
上面提到的三种情形,我们都可以通过Lock来解决,但 synchronized 关键字却无能为力。事实上,Lock 是 java.util.concurrent.locks包 下的接口,Lock 实现提供了比 synchronized 关键字 更广泛的锁操作,它能以更优雅的方式处理线程同步问题。也就是说,Lock提供了比synchronized更多的功能。
// 获取锁
void lock()// 如果当前线程未被中断,则获取锁,可以响应中断
void lockInterruptibly()// 返回绑定到此 Lock 实例的新 Condition 实例
Condition newCondition()// 仅在调用时锁为空闲状态才获取该锁,可以响应中断
boolean tryLock()// 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁
boolean tryLock(long time, TimeUnit unit)// 释放锁
void unlock()
package com.mlj;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
final LockTest test = new LockTest();
new Thread() {
@Override
public void run() {
test.insert(Thread.currentThread());
}
}.start();
new Thread() {
@Override
public void run() {
test.insert(Thread.currentThread());
}
}.start();
}
public void insert(Thread thread) {
lock.lock();
try{
System.out.println(thread.getName()+"获得了锁");
}finally {
lock.unlock();
System.out.println(thread.getName()+"释放了锁");
}
}
}
tryLock() & tryLock(long time, TimeUnit unit)
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true;如果获取失败(即锁已被其他线程获取),则返回false,也就是说,这个方法无论如何都会立即返回(在拿不到锁时不会一直在那等待)。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
一般情况下,通过tryLock来获取锁时是这样使用的:
public void insert(Thread thread) {
if (lock.tryLock()){
try{
System.out.println(thread.getName()+"获得了锁");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(thread.getName()+"释放了锁");
}
}else {
System.out.println(thread.getName()+"放弃了锁,做其他的事");
}
}
死锁的定义
死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
package com.mlj;
public class ThreadTest {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
Thread t1 = new Thread01(o1, o2);
Thread t2 = new Thread02(o1, o2);
t1.start();
t2.start();
}
}
class Thread01 extends Thread {
private Object o1;
private Object o2;
public Thread01(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized( this.o1 ) {
System.out.println("t1拿到锁o1");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized ( this.o2 ) {
System.out.println("t1拿到锁o2");
}
}
}
}
class Thread02 extends Thread {
private Object o1;
private Object o2;
public Thread02(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized ( this.o2 ) {
System.out.println("t2拿到锁o2");
synchronized ( this.o1 ) {
System.out.println("t2拿到锁o1");
}
}
}
}
在有些情况下死锁是可以避免的。下面介绍三种用于避免死锁的技术:
- 加锁顺序(线程按照一定的顺序加锁)
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
- 死锁检测
守护线程
java语言中线程分为两大类:
- 用户线程
- 守护线程
守护线程的特点:一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束
垃圾回收线程
线程通信
关于Object类中的wait和notify方法(生产者消费者模式)
第一:wait和notify方法不是线程对象的方法,是java每一个对象都有的方法
因为这两个方法是Object类自带的
wait方法和notify方法不是通过线程对象调用
第二:wait()方法作用
Object o = new Object();
o.wait();
表示: 让正在o对象上活动的线程进入等待状态,无限期等待
wait:Object类的方法。作用是挂起当前线程,释放获取到的锁,直到别的线程调用了这个对象的notify或notifyAll方法。
notify:Object类的方法。作用是唤醒因调用wait挂起的线程,如果有过个线程,随机唤醒一个。
notifyAll:Object类的方法。作用是唤醒全部因调用wait挂起的线程。
对象有两个池:
锁池:请求锁的线程放在这里
等待池:被wait挂起的线程丢在这里,当线程被notify或者notifyAll唤醒后,进入锁池,继续抢锁
注意:wait和notify的使用必须是在线程同步机制的基础上(synchronized)
生产者和消费者代码演示
这里模拟了两个生产者进程和一个消费者进程,仓库容量为2
package com.mlj;
import java.util.ArrayList;
public class ProductAndCustom {
public static void main(String[] args) {
ArrayList list = new ArrayList();
Thread t1 = new Thread(new Product(list));
Thread t2 = new Thread(new Product(list));
Thread t3 = new Thread(new Custom(list));
t1.setName("生产者1");
t2.setName("生产者2");
t3.setName("消费者3");
t1.start();
t2.start();
t3.start();
}
}
class Product implements Runnable {
private ArrayList list;
public Product(ArrayList list){
this.list = list;
}
@Override
public void run() {
while(true) {
synchronized (list) {
if (list.size() > 1) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else if (list.size() < 2) {
Object o = new Object();
list.add(o);
System.out.println(Thread.currentThread().getName() + "--->" + o + "--->" +list.size());
list.notifyAll();
}
}
}
}
}
class Custom implements Runnable {
private ArrayList list;
public Custom(ArrayList list){
this.list = list;
}
@Override
public void run() {
while (true) {
synchronized (list) {
if (list.size() == 0) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (list.size() != 0){
Object o1 = list.remove(0);
System.out.println(Thread.currentThread().getName() + "--->" + o1+ "--->" +list.size());
list.notifyAll();
}
}
}
}
}
奇偶数打印
package com.mlj;
public class Test {
public volatile static int count = 0;
public static void main(String[] args) {
Test test = new Test();
Thread t1 = new Thread(() -> {
while (count <= 100) {
synchronized (test) {
if (count % 2 == 0) {
System.out.println(Thread.currentThread().getName() + "打印" + count++);
test.notify();
} else {
try {
test.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
test.notifyAll();
});
Thread t2 = new Thread(() -> {
while (count <= 100) {
synchronized (test) {
if (count % 2 == 1) {
System.out.println(Thread.currentThread().getName() + "打印" + count++);
test.notify();
} else {
try {
test.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
t1.setName("偶数");
t2.setName("奇数");
t1.start();
t2.start();
}
}
线程池
1. 为什么要用线程池
- 降低资源消耗。通过重复利用已创建的线程降低线程创建、销毁线程造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控
2. ThreadPoolExecutor线程池类参数详解
当线程池任务处理不过来的时候(什么时候认为处理不过来后面描述),可以通过handler指定的策略进行处理,ThreadPoolExecutor提供了四种策略:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常;也是默认的处理方式。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务(比如main主线程)
可以通过实现RejectedExecutionHandler接口自定义处理方式
3. 线程池任务执行
3.1. 添加执行任务
- submit() 该方法返回一个Future对象,可执行带返回值的线程;或者执行想随时可以取消的线程。Future对象的get()方法获取返回值。Future对象的cancel(true/false)取消任务,未开始或已完成返回false,参数表示是否中断执行中的线程
- execute() 没有返回值。
3.2. 线程池任务提交过程
一个线程提交到线程池的处理流程如下图
总结即:处理任务判断的优先级为 核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
注意:
- 当workQueue使用的是无界限队列时,maximumPoolSize参数就变的无意义了,比如new LinkedBlockingQueue(),或者new ArrayBlockingQueue(Integer.MAX_VALUE);
- 使用SynchronousQueue队列时由于该队列没有容量的特性,所以不会对任务进行排队,如果线程池中没有空闲线程,会立即创建一个新线程来接收这个任务。maximumPoolSize要设置大一点。
- 核心线程和最大线程数量相等时keepAliveTime无作用.
3.3. 线程池关闭
- shutdown() 不接收新任务,会处理已添加任务
- shutdownNow() 不接受新任务,不处理已添加任务,中断正在处理的任务
4. 常用队列介绍
- ArrayBlockingQueue: 这是一个由数组实现的容量固定的有界阻塞队列.
- SynchronousQueue: 没有容量,不能缓存数据;每个put必须等待一个take; offer()的时候如果没有另一个线程在poll()或者take()的话返回false。
- LinkedBlockingQueue: 这是一个由单链表实现的默认无界的阻塞队列。LinkedBlockingQueue提供了一个可选有界的构造函数,而在未指明容量时,容量默认为Integer.MAX_VALUE。
5. Executors线程工厂类
- Executors.newCachedThreadPool();
说明: 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程.
内部实现:new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue()); - Executors.newFixedThreadPool(int);
说明: 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
内部实现:new ThreadPoolExecutor(nThreads, nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue()); - Executors.newSingleThreadExecutor();
说明:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照顺序执行。
内部实现:new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue()) - Executors.newScheduledThreadPool(int);
说明:创建一个定长线程池,支持定时及周期性任务执行。
内部实现:new ScheduledThreadPoolExecutor(corePoolSize)