- 前言
- 01、线程简介
- 02、线程池
- 03、线程间通信
- 总结
前言
记录一下Java并发编程的知识点。有部分内容是借鉴《Java并发编程的艺术》这本书的。本次先介绍一下线程。
01、线程简介
进程和线程的区别
-
进程:当一个程序被运行,即把程序的代码从磁盘加载到内存,就是开启了一个进程。进程可以理解为程序的一个实例(例如打开网易云、浏览器)。
-
线程:一个进程里面可以有多个线程,每个线程执行不同的任务(比如360安全卫士可以一边清理垃圾,一边扫描病毒),线程是现代 *** 作系统调度的最小单位。
线程拥有各自的程序计数器,栈,并且可以共享堆里面的共享内存变量;
线程的状态
这里是从Java API层面上来讲的,先看一下Thread里面的枚举类
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
可以看出这里分成了六种状态:
- NEW:线程刚被创建,还没有调用start方法
- RUNNABLE:运行状态,涵盖了 *** 作系统层面的就绪状态、运行状态、阻塞状态
- BLOCKED:阻塞状态,表示线程阻塞于锁
- WAITING:等待状态,进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)
- TIMED_WAITING:超时等待状态,该状态不同于WAITING,它可以在指定的时间后自行返回
- TERMINATED:终止状态,表示当前线程已经执行完毕
下图来自《Java并发编程的艺术》
线程创建的方式
1、继承Thread类
Thread t=new Thread(){
@Override
public void run() {
System.out.println("创建了一个线程");
}
};
//启动线程
t.start();
2、实现Runnable接口
Runnable t2= new Runnable() {
@Override
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread(t2,"t2");
// 启动线程
t.start();
3、实现Callable接口(需要借助FutureTask来接收返回结果)
FutureTask<Integer> t3 = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 404;
}
});
new Thread(t3,"t").start();
//主线程阻塞,同步等待t3执行完毕返回结果
Integer result = t3.get();
4、使用线程池(第二部分详细介绍)
常见方法
方法名 | 功能说明 | 注意 |
---|---|---|
start() | 启动一个线程,在新的线程运行run方法中的代码 | 如果start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException |
run() | 新线程启动后会调用的方法 | |
join() | 等待目标线程运行结束 | |
getPriority() | 获取线程优先级 | |
setPriority() | 设置线程优先级 | java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率 |
getState() | 获取线程状态 | |
interrupt() | 打断线程 | 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记 ;如果打断的正在运行的线程,则会设置打断标记 ;park 的线程被打断,也会设置打断标记 |
isInterrupted() | 判断是否被打断 | 不会清除打断标记 |
interrupted() | 判断当前线程是否被打断 | 会清除打断标记 |
isAlive() | 线程是否存活 | |
sleep(long n) | 让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程 | |
yield() | 提示线程调度器让出当前线程对CPU的使用 |
sleep与yield的区别
sleep
- 调用 sleep 会让当前线程从 RUNNABLE 进入 TIMED_WAITING 状态
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,而且会把中断标识位清除
yield
- 调用 yield 会让当前线程让出CPU的使用权,注意当前线程会继续参与下一轮CPU使用权的争抢中,所以调用改方法后有可能还是当前线程继续运行
让线程“优雅”退出(线程中断)
中断可以理解为线程的一个标识位属性,它标识一个运行中的线程是否被其他线程进行了中断 *** 作。其他线程可以通过调用该线程的interrupt方法对其进行中断 *** 作。
线程通过检查自身是否被中断来进行响应,调用isInterrupted方法来进行判断是否被中断。
利用线程中断我们可以实现线程的“优雅”退出。代码如下
class TPTInterrupt {
private Thread thread;
public void start(){
thread = new Thread(() -> {
while(true) {
Thread current = Thread.currentThread();
//当我们调用stop对线程进行中断标记后这里就可以感知中断而结束
if(current.isInterrupted()) {
log.debug("收拾东西走人");
break;
}
try {
Thread.sleep(1000);
log.debug("摸鱼ing......");
} catch (InterruptedException e) {
//如果是在睡眠中被打断,会抛出异常,那么我们需要自己进行中断标记
current.interrupt();
}
}
},"小明");
thread.start();
}
public void stop() {
thread.interrupt();
}
}
在主方法调用
TPTInterrupt t = new TPTInterrupt();
t.start();
Thread.sleep(3500);
System.out.println("你被辞退了");
t.stop();
运行结果
12:56:11.123 [小明] DEBUG xyx.product.web.TPTInterrupt - 摸鱼ing......
12:56:12.142 [小明] DEBUG xyx.product.web.TPTInterrupt - 摸鱼ing......
12:56:13.147 [小明] DEBUG xyx.product.web.TPTInterrupt - 摸鱼ing......
12:56:13.522 [main] DEBUG xyx.product.web.Test - 你被辞退了
12:56:13.522 [小明] DEBUG xyx.product.web.TPTInterrupt - 收拾东西走人
我们用一个图来看看执行流程
核心参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
主要有7大核心参数:
- corePoolSize 核心线程数目 (最多保留的线程数)
- maximumPoolSize 最大线程数目
- keepAliveTime 生存时间
- unit 时间单位
- workQueue 阻塞队列
- threadFactory 线程工厂
- handler 拒绝策略
执行流程
核心线程数大小设置
- CPU 密集型任务(N+1):这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止某些原因导致的任务暂停(线程阻塞,如io *** 作,等待锁,线程sleep)而带来的影响。一旦某个线程被阻塞,释放了cpu资源,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- I/O 密集型任务(2N):系统会用大部分的时间来处理 I/O *** 作,而线程等待 I/O *** 作会被阻塞,释放 cpu资源,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法:最佳线程数 = CPU核心数 * (1/CPU利用率) = CPU核心数 * (1 + (I/O耗时/CPU耗时)),一般可设置为2N。
线程池的创建
使用ThreadPoolExecutor的构造方法创建线程池
ThreadPoolExecutor threadsPool = new ThreadPoolExecutor(9,
20, 60,
TimeUnit.SECONDS, new LinkedBlockingDeque<>(100),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
创建完线程池后有两种方式提交任务,分别是execute和submit方法
execute方法用于提交不需要返回值的任务
public void execute(Runnable command)
submit方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过future可以获取返回值,下面是它的三个接口
Future<?> submit(Runnable task);
<T> Future<T> submit(Runnable task, T result);
<T> Future<T> submit(Callable<T> task);
03、线程间通信
线程开始运行,拥有自己的栈空间,如果它们仅仅是孤立地运行,那么价值很少,如果多个线程之间可以相互配合完成工作,那么会带来更大的价值。
volatile和synchronized关键字
Java多线程访问共享变量时,每个线程都会有这个变量的拷贝副本,但是在执行过程中它可能看到的变量值不是最新的。
关键字volatile可以用来修饰共享变量,保证它对所有线程的“可见性”,即每个线程都可以读到最新值,详细原理我们下篇文章再介绍。
而关键字synchronized可以修饰方法或者同步代码块来确保同一个时刻,只能有一个线程处于方法或者同步块中,从而实现线程对共享变量访问的可见性和排他性。
等待/通知机制
这个可以有3种实现方式,我们分别介绍一下:
1、Object类的wait和notify方法
我们先看一下下面表格了解一下方法
再加上下面这个例子理解一下
final static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
}).start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
}).start();
// 主线程两秒后执行
sleep(2);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
obj.notify(); // 唤醒obj上一个线程
sleep(1000);
// obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
执行结果
09:36:09.796 [Thread-0] DEBUG xyx.product.web.Test - 执行....
09:36:09.799 [Thread-1] DEBUG xyx.product.web.Test - 执行....
09:36:10.797 [main] DEBUG xyx.product.web.Test - 唤醒 obj 上其它线程
09:36:11.801 [Thread-0] DEBUG xyx.product.web.Test - 其它代码....
注意事项
- 使用上面的方法时需要获取到对象的锁
- 使用wait方法后,线程状态由RUNNING变为WAITING,同时会释放对象锁
- 调用notify或notifyAll方法后,需要等待notify或notifyAll的线程释放锁以后,等待线程才有机会从wait返回。
- 从wait返回的前提是获取到了锁
- 还有就是wait和notify的顺序不可以颠倒
2、Condition接口的await和signal方法
当我们调用Condition定义的方法时,需要获取到Condition对象关联的锁。而Condition对象是由Lock对象创建的,换句话说,Condition是依赖Lock对象的。
我们看一个例子(使用上和上面的第一种方法其实差不多)
class ConditionUseCase {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
lock.lock();
try {
condition.wait();
} finally {
lock.unlock();
}
}
public void conditionSignal() {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
}
我们再来看看第二种方法对于第一种方法的提升,也可以理解为synchronized和ReentrantLock的一点区别:
- 第一种方式在等待状态中不响应中断,第二种方式支持
- 第一种方式不可以指定等待时间,第二种方式可以
- 第一种方式只能随机唤醒或唤醒全部,第二种方式可以利用多个Condition实现指定目标唤醒
3、LockSupport工具类的park和unpark方法
LockSupport里面的方法提供了最基本的线程阻塞和唤醒功能。它的最大优势就是不需要先获取锁就可以直接使用,而且park和unpark方法无需顺序执行。
我们直接看代码
Thread a = new Thread(() -> {
log.debug("执行....");
LockSupport.park();
log.debug("其它代码....");
});
a.start();
Thread b = new Thread(() -> {
log.debug("执行....");
//给指定线程放行
LockSupport.unpark(a);
log.debug("其它代码....");
});
b.start();
Thread.join() 方法
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止后才继续执行。我们还可以调用join(long millis)和join(long millis,int nacos)两个超时特性的方法。
我们可以看看下面的例子
Thread t1 = new Thread(() -> {
try {
sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
Thread t2 = new Thread(() -> {
try {
sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
r2 = 10;
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
运行结果
10:47:53.667 [main] DEBUG xyx.product.web.Test - r1: 10 r2: 10 cost: 3
总结
以上就是本篇文章的所有内容了,主要是对Java并发编程知识中的线程做一个介绍,后面再详细说一说底层的一些实现原理。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)