Java 并发编程(Ⅰ)
Posted 364.99°
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 并发编程(Ⅰ)相关的知识,希望对你有一定的参考价值。
目录
1. 概念
1. 基本概念
进程和线程:
- 图示:
- 文字:
- 进程:程序的一个实例,一个程序可以有多个实例,如下图
现在QQ这一个程序就拥有了两个进程 - 线程:线程就是进程的执行单元,一个进程中有多个线程
- 进程:程序的一个实例,一个程序可以有多个实例,如下图
- 概念:
- 进程:进程是 分配和管理资源 的基本单位,不同进程会竞争计算机系统资源
- 线程:线程是 最小调度单位(处理器调度的基本单位)
- 区别:
- 独立 | 共享
- 线程之间共享本进程的地址空间、资源(内存、CPU、IO等)
- 每个进程都有独立的地址空间,资源独立
- 健壮 ?
- 一个线程崩溃之后,整个进程都要崩溃
- 一个进程崩溃之后,在保护模式下不会对其他进程产生影响
- 开销?
- 线程依附于进程运行,显然进程开销大(包括启动开销、切换开销)
- 通信
- 同一台计算机的进程通信称为 IPC(Inter-process communication),不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
- 线程因为共享进程资源,所以通信很简单,比如说共享一个静态资源
- 独立 | 共享
并发与并行:
单核CPU同一时间只能执行一个线程,即 串行执行 。操作系统中的 任务调度器 会将 CPU 的时间片(时间很短)分给不同的线程使用。因为 CPU 在线程间切换非常快,会给人一种多线程同时运行的错觉。
- 并发(concurrent): 同一时间应对(dealing with)多件事的能力
- 并行(parallel): 同一时间做(doing)多件事的能力
同步与异步:
-
同步: 即串行执行,需要等前面的代码返回之后,才能继续执行后续的代码。
@Slf4j public class ShowSync public static void count(int i) log.info("第" + i + "次执行count方法"); public static void main(String[] args) for (int i = 0; i < 3; i++) count(i); log.info("线程:" + Thread.currentThread().getName());
-
异步: 不需要等待前面的代码返回,就能执行下面的代码。
@Slf4j public class ShowAsync public static void main(String[] args) for (int i = 0; i < 3; i++) new Thread(() -> log.info("线程:" + Thread.currentThread().getName()) ).start(); log.info("线程:" + Thread.currentThread().getName());
2. 线程的状态
Thread 类中提供了一个枚举类 State,如下:
public enum State
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
可见,一个Java线程有 六大状态 。可实际上,我们会把 RUNNABLE 分为 Ready 和 RUNNABLE 两大状态,如下图所示,所以我们一般会说:一个Java线程有 七大状态 。
- NEW
- Thread t1 = new Thread() 进入此状态
- READY
- t.start() 进入此状态
- 此时线程已经可以运行,但还未获取到 CPU 时间片,处于就绪状态
- RUNNING
- 就绪的线程获取到了 CPU 时间片,进入运行状态
- BLOCKED
- 线程进入 synchronized 修饰的方法或代码块
- TERMINATED
- 线程的 run() 跑完或者 main() 跑完
- WAITING
- 进入此状态的线程不会被分配 CPU时间片,需要被其他线程唤醒,不然会一直等待下去
- TIMED_WAITING
- 进入此状态的线程不会被分配 CPU时间片,不需要被其他线程唤醒,到达一定时间之后会自动苏醒
至于线程中的状态转换及相关方法,我将在后文陆续介绍。
2. 线程的初始化
线程的初始化,也就是 new Thread(),我们有三种方式来实现初始化,分别是:
- 直接 new Thread() 然后重写 run()
- new Thread(new Runnable()) ,将 Runnable 的 run() 组装到 Thread 中
- new Thread(new FutureTask(new Callable())) ,将 Callable 中的 call() 先组装到 FutureTask 再组装到 Thread
1. new Thread()
直接 new Thread() 初始化线程,是将线程(Thread)和任务(run())绑在一起,实际开发中不建议使用,代码如下:
@Slf4j
public class JustThread
public static void main(String[] args)
Thread thread1 = new Thread("线程1")
@Override
public void run()
log.info("线程:" + Thread.currentThread().getName() + " 运行中...");
;
thread1.start();
log.info("线程:" + Thread.currentThread().getName() + " 运行中...");
/*
lambda表达式
*/
Thread thread2 = new Thread(() ->
log.info("线程:" + Thread.currentThread().getName() + " 运行中...");
, "线程2");
thread2.start();
2. new Thread(new Runnable())
将线程(Thread)和任务(Runnable)分开初始化,然后组合在一起。
@Slf4j
public class RunnableAndThread
public static void main(String[] args)
Runnable runnable = new Runnable()
@Override
public void run()
log.info("线程:" + Thread.currentThread().getName() + " 运行中...");
;
Thread thread = new Thread(runnable, "线程3");
thread.start();
/*
lambda表达式
*/
Runnable runnable1 = () -> log.info("线程:" + Thread.currentThread().getName() + " 运行中...");
Thread thread1 = new Thread(runnable1, "线程4");
thread1.start();
3. Thread 和 Runnable 的关系
先看一下 Runnable 的源码,如下:
@FunctionalInterface
public interface Runnable
public abstract void run();
一个函数式接口,只有一个 run() 。
然后看一下 Thread 的部分源码:
@Override
public void run()
if (target != null)
target.run();
public Thread(Runnable target)
init(null, target, "Thread-" + nextThreadNum(), 0);
private void init(ThreadGroup g, Runnable target, String name,
long stackSize)
init(g, target, name, stackSize, null, true);
好的,就看这几段方法,就不在这里做深入分析了。从上面的代码,我们可以很清晰地弄懂 Thread 和 Runnable 之间的关系:
- 在 new Thread 的时候,如果没有传入 Runnable,就会重写 Thread 的 run()
- 在 new Thread 的时候,如果传入了 Runnable,就会使用 Runnable 中的 run()
所以,Runnable 接口就是为了给 Thread 提供一个方法体 run() 。
在实际开发中,我们通常会自定义一个类来 implements Runnable,然后将此类传入 Thread 中。案例如下:
- 定义一个每间隔十秒打印一下当前时间的线程
public class TimePrinter implements Runnable
private SimpleDateFormat dateFormat= new SimpleDateFormat("hh:mm:ss");
@Override
public void run()
while (true)
try
System.out.println("当前时间为: " + dateFormat.format(new Date()));
TimeUnit.SECONDS.sleep(10);
catch (InterruptedException e)
throw new RuntimeException(e);
public class TaskDemo
public static void main(String[] args)
Thread thread = new Thread(new TimePrinter(), "测试线程");
thread.start();
4. new Thread(new FutureTask(new Callable()))
@Slf4j
public class FutureTaskAndThread
public static void main(String[] args) throws ExecutionException, InterruptedException
FutureTask<Long> futureTask = new FutureTask<>(new Callable<Long>()
@Override
public Long call() throws Exception
long start = System.currentTimeMillis();
log.info(Thread.currentThread().getName() + " 运行中...");
TimeUnit.MILLISECONDS.sleep(123);
long end = System.currentTimeMillis();
return end - start;
);
Thread thread = new Thread(futureTask, "线程5");
thread.start();
log.info(thread.getName() + "花费了 " + futureTask.get() + " 毫秒");
Runnable与Callable的关系如下图:
可见,相比于 Runnable 的 run(),Callable 的 call() 多了一个返回值。
@FunctionalInterface
public interface Callable<V>
V call() throws Exception;
3. 常用方法
线程状态的切换:
TIMED_WAITING 和 WAITING 的区别:
- TIMED_WAITING 即使没有外部信号,在等待时间超时后,线程也会恢复
- WAITING 需要等待外部信号来唤醒
1. start
先来看一下源码:
// 线程组
private ThreadGroup group;
// NEW 状态的线程的 threadStatus = 0
private volatile int threadStatus = 0;
public synchronized void start()
if (threadStatus != 0)
throw new IllegalThreadStateException();
// 将要启动(start)的线程装入线程组中
group.add(this);
boolean started = false;
try
start0();
started = true;
finally
try
if (!started)
// 告诉组,这个线程启动(start)失败
group.threadStartFailed(this);
catch (Throwable ignore)
// native方法,调用本地操作系统开启一个新的线程
private native void start0();
看完源码之后,可以得出以下结论:
- start 启动一个线程需要经历以下步骤:
- 将调用 start 的线程装入线程组 ThreadGroup
- 调用native方法,启动线程
1. 线程组
此处参考链接:https://zhuanlan.zhihu.com/p/61331974
线程组(Thread Group): 一个线程集合,可以很方便地管理一个组中的线程。
线程组树:
-
System线程组: 处理JVM的系统任务的线程组,例如对象的销毁等
-
main线程组:包含至少一个线程——main(用来执行main方法)
-
面线程组的子线程组: 应用程序创建的线程组
public static void main(String[] args) // 输出当前线程组——main线程组 System.out.println(Thread.currentThread().getThreadGroup().getName()); // 输出当前线程组的父线程组——System System.out.println(Thread.currentThread().getThreadGroup().getParent().getName());
给线程指定线程组:
-
在初始化线程的时候如果不指定数组,就会默认指定为 main线程组,代码如下:
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) /* 其余代码省略 */ Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); if (g == null) if (security != null) g = security.getThreadGroup(); if (g == null) g = parent.getThreadGroup(); /* 其余代码省略 */
-
可以利用以下任意一个构造器来给线程指定一个线程组:
public Thread(ThreadGroup group, Runnable target) public Thread(ThreadGroup group, Runnable target) public Thread(ThreadGroup group, Runnable target, String name) public Thread(ThreadGroup group, Runnable target, String name, long stackSize)
演示一下:
public static void main(String[] args) ThreadGroup group = new ThreadGroup("我的线程组"); Thread thread = new Thread(group, "我的线程"); System.out.println(thread.getName() + " 的线程组是 " + group.getName());
线程组方法解析: https://blog.csdn.net/a1064072510/article/details/87455525
小结一下: 将线程装入线程组就是为了方便批量操作线程,比如说我要通知好几个线程要终止了,就可以事先把这几个线程装入一个线程组,然后直接通过线程组批量通知:
// 通知main线程组中的所有线程要终止了
Thread.currentThread().getThreadGroup().interrupt();
2. start 和 run
问:为什么 start 能启动一个线程,而 run 就不行呢?
答:因为 Java 并不能直接作用于操作系统,所以需要调用native方法(比如C、C++等编写的方法)来告诉操作系统开启新的线程。而在 start 方法中调用了native方法 start0 来启动线程。至于 run, 这就是线程中的一个方法,当线程跑起来的时候,就会去执行 run,当 run 方法执行完毕线程就会结束。
2. sleep
先来看一下 Thread.sleep 的源码:
/**
*让当前线程睡眠指定毫秒数,线程不会丢失任何monitors的所有权
*/
public static native void sleep(long millis) throws InterruptedException;
好的,现在来使用一下:
@Slf4j
public class Sleep_md
public static void main(String[] args) throws InterruptedException
Thread t1 = new Thread(()->
long start = System.currentTimeMillis();
log.info("t1睡眠5s");
try
Thread.sleep(5 * 1000);
catch (InterruptedException e)
long end = System.currentTimeMillis();
log.info("已过去 毫秒", (end - start));
);
t1.start();
通过上述代码的运行,可发现 Thread.sleep 可以让当前线程进入 TIMED_WAITING 睡眠一段时间,然后进入 RUNNABLE 。在此期间会让出CPU,给其他线程机会。
1. TimeUtil
除了 Thread.sleep ,TimeUtil 也有 sleep,可以让线程进入睡眠,两者效果相同。
public void sleep(long timeout) throws InterruptedException
if (timeout > 0)
long ms = toMillis(timeout);
int ns = excessNanosJava 多线程知识的简单总结