线程、进程、并发、并行
1、线程与进程
- 线程也被称作轻量级进程,线程是进程的执行单元;
- 线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程;
2、并发与并行
- 并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果;
- 并行:指在同一时刻,有多条指令在多个处理器上同时执行;
创建线程的两种方式
1、继承Thread类创建线程类
通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法。把run()方法称为线程执行体;
- 创建Thread子类的实例,即创建线程对象;
- 调用线程对象的start()方法来启动该线程;
public class Test7 extends Thread{
@Override
public void run() {
//当线程类继承Thread类时,可以直接调用getName()方法来返回当前线程的名。
System.out.println(getName());
}
public static void main(String[] args) {
//调用Thread的currentThread方法获取当前线程
System.out.println(Thread.currentThread().getName());
new Thread(new Test7()).start();
}
}
//控制台打印
main
Thread-0
复制代码
2、实现Runnable接口创建线程类
通过实现Runnable接口来创建并启动多线程的步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法;
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象;
- 调用线程对象的start()方法来启动线程;
public class Test7 implements Runnable
{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
//调用Thread的currentThread方法获取当前线程
System.out.println(Thread.currentThread().getName());
new Thread(new Test7()).start();
//设定线程名字
//new Thread(new Test7(),"新线程").start();
}
}
//控制台打印
main
Thread-0
复制代码
3、创建线程的两种方式对比
采用实现Runnable接口的方式:
- 还可以继承其他类;
- 这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况;
- 如需访问当前线程,则必须使用Thread.currentThread()方法;
采用继承Thread类的方式:
- 已经继承了Thread类,所以不能再继承别的类;
- 如需访问当前线程,直接使用this即可获得当前线程;
推荐使用接口的形式;
4、对同一线程多次调用start()方法会怎样?
public class Test implements Runnable {
@Override
public void run() {
System.out.println("Test");
}
public static void main(String[] args) {
Thread thread = new Thread(new Test());
thread.start();
thread.start();
}
}
复制代码
运行控制台会报错: 这是因为start()方法会在调用开始前检查当前线程的状态。线程创建时默认状态为0,当调用start()方法后,线程状态被修改,所以再次调用start()方法会报错;
/* Java thread status for tools,
* initialized to indicate thread 'not yet started'
*/
private volatile int threadStatus = 0;
if (threadStatus != 0)
throw new IllegalThreadStateException();
复制代码
5、容易混淆的创建线程的几种方式? 网上对于创建线程的几种方式各有不同,有说两种、四种、三种的?那么到底是几种呢?
/* What will be run. */
private Runnable target;
@Override
public void run() {
if (target != null) {
target.run();
}
}
复制代码
我们查看Thread源代码可知,其实本质上就两种,一种是继承Thread,重写run方法的话,那么Thread原本的run方法则不再存在,我们调用的就是我们重写后的方法。
还有一种就是实现Runnable并重写run方法,此种方式的话,我们传入了Runnable对象,如上代码进行判断,target不为空,则执行了target的run方法;
查看网上其它的创建方式,其实本质上都是调用的我们最基本的两种创建方式,只是对他们进行了封装!
6、如果同时使用两种创建方式运行多线程会出现什么结果?
public class Test{
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我来自Runnable");
}
}){
@Override
public void run() {
System.out.println("我来自Thread");
}
}.start();
}
}
复制代码
控制台打印:我来自Thread;
这是为什么呢?这是因为我们重写了Thread的run()方法,所以即使我们传入了Runnable对象,但是下面的代码已经不存在了。最终是直接执行我们所重写的run()方法
@Override
public void run() {
if (target != null) {
target.run();
}
}
复制代码
详细分析 Java 中实现多线程的方法有几种?(从本质上出发)
线程的生命周期
java.lang.Thread定义了一个内部枚举State,如下:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
复制代码
可以知道Java线程有六个状态:NEW(新建)、RUNNABLE(可运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(计时等待)、TERMINATED(终止);
线程状态之间的转换关系如下:
当程序使用new关键字创建了一个线程之后,该线程就处于NEW(新建)状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体;
当线程对象调用了start()方法后,该线程处于RUNNABLE(可运行)状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度; 调用线程对象的start()方法之后,该线程立即进入RUNNABLE状态-----RUNNABLE状态相当于等待执行,但该线程并未真正进入运行状态;
1、NEW、RUNNABLE、TERMINATED状态演示
public class Test implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Test());
System.out.println(thread.getState());
//调用start()方法转为RUNNABLE状态,此状态并不会立马执行,至于该线程何时开始运行,取决于JVM里线程调度器的调度
thread.start();
System.out.println(thread.getState());
//休眠10毫秒让程序正常走完,线程执行完为TERMINATED状态
Thread.sleep(10);
System.out.println(thread.getState());
}
}
//控制台打印
NEW
RUNNABLE
0
1
...
99
TERMINATED
复制代码
2、WAITING、TIMED_WAITING、BLOCKED状态演示
public class Test implements Runnable {
@Override
public void run() {
sync();
}
private synchronized void sync(){
try {
Thread.sleep(1000);
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Test runnable = new Test();
Thread thread1 = new Thread(runnable);
thread1.start();
Thread thread2 = new Thread(runnable);
thread2.start();
//避免主线程执行太快,休眠50毫秒再打印状态
Thread.sleep(50);
System.out.println(thread1.getState());
System.out.println(thread2.getState());
//同步方法里子线程休眠1000毫秒,那么这里主线程需要休眠超过1000毫秒,等待子线程代码走到wait()再打印状态
Thread.sleep(1500);
System.out.println(thread1.getState());
}
}
//控制台打印
TIMED_WAITING
BLOCKED
WAITING
复制代码
一般习惯而言,我们通常会把WAITING、TIMED_WAITING、BLOCKED统称为阻塞状态!
interrupt()、interrupted()、isInterrupted()
1、interrupt()
public void interrupt()
复制代码
中断线程,将会设置该线程的中断状态位,即设置为true。如果当前线程没有中断它自己(这在任何情况下都是允许的),则该线程的 checkAccess 方法就会被调用,这可能抛出 SecurityException。
如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException。
2、interrupted()
public static boolean interrupted()
复制代码
测试当前线程是否已经中断。线程的中断状态由该方法清除。
换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。
3、isInterrupted()
public boolean isInterrupted()
复制代码
测试线程是否已经中断。线程的 中断状态 不受该方法的影响。
wait()、notify()、notifyAll()
1、使用notify唤醒等待线程
public class Test{
private static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (object){
System.out.println(Thread.currentThread().getName()+"获取到了锁");
try {
//释放对象锁,释放cpu资源并进入等待
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"被唤醒");
}
}).start();
//主线程休眠50毫秒,保证上面的线程先执行
Thread.sleep(50);
new Thread(() -> {
synchronized (object){
System.out.println(Thread.currentThread().getName()+"获取到了锁");
object.notify();
}
}).start();
}
}
//控制台打印
Thread-0获取到了锁
Thread-1获取到了锁
Thread-0被唤醒
复制代码
2、notify与notifyAll的区别
public class Test2 implements Runnable{
private static Object object = new Object();
@Override
public void run() {
synchronized (object){
System.out.println(Thread.currentThread().getName()+"获取到锁");
try {
//释放对象锁,释放cpu资源并进入等待
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"被唤醒");
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new Test2()).start();
new Thread(new Test2()).start();
//主线程休眠50毫秒,保证上面的线程先执行
Thread.sleep(50);
new Thread(() -> {
synchronized (object){
//object.notify();
object.notifyAll();
}
}).start();
}
}
//控制台打印
Thread-0获取到锁
Thread-1获取到锁
Thread-1被唤醒
Thread-0被唤醒
复制代码
使用notify()的话,多次运行会发现,唤醒的线程是随机的!
3、wait只释放当前锁
public class Test3{
private static volatile Object objectA = new Object();
private static volatile Object objectB = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (objectA){
System.out.println(Thread.currentThread().getName()+"获取到objectA锁");
synchronized (objectB){
System.out.println(Thread.currentThread().getName()+"获取到objectB锁");
try {
objectA.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
Thread.sleep(1000);
new Thread(() -> {
synchronized (objectA){
System.out.println(Thread.currentThread().getName()+"获取到objectB锁");
synchronized (objectB){
System.out.println(Thread.currentThread().getName()+"获取到objectA锁");
}
}
}).start();
}
}
//控制台打印
Thread-0获取到objectA锁
Thread-0获取到objectB锁
Thread-1获取到objectB锁
复制代码
4、为什么wait()、notify()、notifyAll()必须在同步方法/代码块中调用?
对于对象的同步方法/代码块来说,在任意时刻有且仅有一个拥有该对象独占锁的线程能够调用它们;
wait()方法强制当前线程释放对象锁,这意味着在调用某对象的wait()方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法;
当某线程调用某对象的notify()或notifyAll()方法时,任意一个(对于notify())或者所有(对于notifyAll())在该对象的等待队列中的线程,将被转移到该对象的入口队列。接着这些队列(可能只有一个)将竞争该对象的锁,最终获得锁的线程继续执行。如果没有线程在该对象的等待队列中等待获得锁,那么notify()和notifyAll()将不起任何作用。在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法。
join()、sleep()、yield()
1、join()
Thread提供了让一个线程等待另一个线程完成的方法---join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join()线程执行完成为止。
public class JoinThread extends Thread
{
@Override
public void run()
{
for (int i = 0; i < 100 ; i++ )
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) throws Exception
{
for (int i = 0; i < 100 ; i++ )
{
if (i == 20)
{
JoinThread jt = new JoinThread();
jt.start();
//main线程调用了jt线程的join方法,main线程
//必须等jt执行结束才会向下执行
jt.join();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
复制代码
控制台打印: 主线程等待子线程的终止。也就是说主线程的代码块中,如果碰到了t.join()方法,此时主线程需要等待,等待子线程结束了,才能继续执行t.join()之后的代码块;
2、sleep()
如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现:
- static void sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态;
- static void sleep(long millis,int nanos):让当前正在执行的线程暂停millis毫秒加nanos毫微秒;
当当前线程调用sleep()方法进入阻塞状态后,在其睡眠时间段内,该线程不会释放锁,也不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序执行;
public class TestSleep
{
public static void main(String[] args) throws Exception
{
for (int i = 0; i < 10 ; i++ )
{
System.out.println("当前时间: " + new Date());
//调用sleep方法让当前线程暂停1s。
Thread.sleep(1000);
}
}
}
复制代码
sleep(long mills):让出CPU资源,但是不会释放锁资源(包括synchronized和lock)! wait():让出CPU资源和锁资源!
2.1、sleep不释放synchronized锁
public class Test4 implements Runnable{
@Override
public void run() {
sync();
}
public synchronized void sync(){
System.out.println(Thread.currentThread().getName()+"获取锁");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"释放锁");
}
public static void main(String[] args) {
Test4 runnable = new Test4();
new Thread(runnable).start();
new Thread(runnable).start();
}
}
//控制台打印
Thread-1获取锁
Thread-1释放锁
Thread-0获取锁
Thread-0释放锁
复制代码
2.2、sleep不释放lock锁
public class Test5 implements Runnable{
private static final Lock LOCK = new ReentrantLock();
@Override
public void run() {
try {
LOCK.lock();
System.out.println(Thread.currentThread().getName()+"获取锁");
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}finally {
LOCK.unlock();
System.out.println(Thread.currentThread().getName()+"释放锁");
}
}
public static void main(String[] args) {
Test5 runnable = new Test5();
new Thread(runnable).start();
new Thread(runnable).start();
}
}
//控制台打印
Thread-0获取锁
Thread-0释放锁
Thread-1获取锁
Thread-1释放锁
复制代码
总结:sleep方法可以让线程进入Waiting状态,并且不占用CPU资源,但是不释放锁,直到规定时间后再运行,休眠期间如果被中断,会抛出异常并清除中断状态
3、yield()
yield()方法是一个和sleep()方法有点相似的方法,它也是Thread类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。yield()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行;
实际上,当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会;
public class TestYield extends Thread
{
public TestYield()
{
}
public TestYield(String name)
{
super(name);
}
//定义run方法作为线程执行体
public void run()
{
for (int i = 0; i < 50 ; i++ )
{
System.out.println(getName() + " " + i);
//当i等于20时,使用yield方法让当前线程让步
if (i == 20)
{
Thread.yield();
}
}
}
public static void main(String[] args) throws Exception
{
//启动两条并发线程
TestYield ty1 = new TestYield("高级");
//将ty1线程设置成最高优先级
ty1.setPriority(Thread.MAX_PRIORITY);
ty1.start();
TestYield ty2 = new TestYield("低级");
//将ty1线程设置成最低优先级
ty2.setPriority(Thread.MIN_PRIORITY);
ty2.start();
}
}
复制代码
resume()、suspend()、stop()
这三个方法分别是恢复线程、暂停线程、终止线程,这三个方法已经弃用;
suspend()可能出现死锁,stop()是线程不安全的;
线程属性:线程ID、线程名字、守护线程、优先级
1、线程ID
每个线程都有自己的ID,用于标识不同的线程;
public class Test7 implements Runnable
{
@Override
public void run() {
System.out.println("子线程ID为:"+Thread.currentThread().getId());
}
public static void main(String[] args) {
new Thread(new Test7()).start();
System.out.println("主线程ID为:"+Thread.currentThread().getId());
}
}
//控制台打印
主线程ID为:1
子线程ID为:12
复制代码
查看源码可知,线程ID每次都是自增1,并且是从1开始的!
tid = nextThreadID();
private static long threadSeqNumber;
private static synchronized long nextThreadID() {
return ++threadSeqNumber;
}
public long getId() {
return tid;
}
复制代码
那么为什么上述程序打印主线程ID为1,而子线程ID为12了呢?
这是因为JVM启动的时候还有很多后台进程同步启动: 2、线程名字
让用户或者程序员在开发、调试或运行过程中,更容易区分每个不同的线程、定位问题等;
public class Test7 implements Runnable
{
@Override
public void run() {
System.out.println("子线程名称为:"+Thread.currentThread().getName());
}
public static void main(String[] args) {
new Thread(new Test7(),"新建子线程").start();
new Thread(new Test7()).start();
System.out.println("主线程名称为:"+Thread.currentThread().getName());
}
}
//控制台打印:
主线程名称为:main
子线程名称为:新建子线程
子线程名称为:Thread-0
复制代码
查看源码可知,当我们没有传入名称时,会自动生成一个默认名字,因为++写在后面,所以是从0开始的:
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
复制代码
3、守护线程
有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为“后台线程”,又称为“守护线程”。JVM的垃圾回收线程就是典型的后台线程;
后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡;调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程:
public class DaemonThread extends Thread
{
//定义后台线程的线程执行体与普通线程没有任何区别
@Override
public void run()
{
System.out.println("守护线程");
}
public static void main(String[] args) throws InterruptedException {
DaemonThread t = new DaemonThread();
//前台线程创建的子线程默认也是前台线程
System.out.println(t.isDaemon());
//将此线程设置成后台线程
t.setDaemon(true);
//启动后台线程
t.start();
//因为前台线程结束的话,后台线程也随之结束,所以在这休眠50毫秒以让后台线程可以执行完毕
Thread.sleep(50);
//isDaemon()方法用于判断指定线程是否为后台线程
System.out.println(t.isDaemon());
System.out.println(Thread.currentThread().getName());
//------程序执行到此处,前台线程(main线程)结束------
//后台线程也应该随之结束
}
}
//控制台打印
false
守护线程
true
main
复制代码
从以上程序可以看出,主线程默认是前台线程,t线程默认也是前台线程。并不是所有的线程默认都是前台线程,有些线程默认就是后台线程-----前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。 4、线程优先级
每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会;
每个线程默认的优先级都与创建它的父线程的优先级相同,在默认的情况下,main线程具有普通优先级,由main线程创建的子进程也具有普通优先级;
Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级。其中setPriority()方法的参数可以是一个整数,范围是1~10之间,也可以是Thread类的如下三个静态常量:
- MAX_PRIORITY:其值是10;
- MIN_PRIORITY:其值是1;
- NORM_PRIORITY:其值是5;
线程异常
public class Test7 implements Runnable
{
@Override
public void run() {
throw new RuntimeException();
}
public static void main(String[] args) throws InterruptedException {
new Thread(new Test7()).start();
Thread.sleep(1000);
System.out.println("执行结束");
}
}
//控制台打印
Exception in thread "Thread-0" java.lang.RuntimeException
at com.example.demo.Test7.run(Test7.java:11)
at java.lang.Thread.run(Thread.java:748)
执行结束
复制代码
由上代码运行结果可知:子线程的异常不会影响到主线程的执行,即使子线程有异常抛出,并且打印了异常信息,主线程依然能够正常运行!
使用try/catch包住线程语句:
try{
new Thread(new Test7()).start();
}catch (Exception e){
System.out.println("捕获异常");
}
复制代码
运行结果与上述仍然一样,可知:子线程异常无法用传统方法进行捕获!
使用try/catch包住线程执行体内代码:
@Override
public void run() {
try{
throw new RuntimeException();
}catch (RuntimeException e){
System.out.println("捕获异常");
}
}
复制代码
运行程序控制台打印捕获异常,可知:try/catch只能捕获对应线程内的异常;
但是,如果我们手动在每个run方法里面都使用try/catch进行捕获的话,那工作量不仅大、重复性工作还麻烦,代码也很冗余。
所以我们可以使用UncaughtExceptionHandler,这个接口可以检测出由于未捕获异常而终止的情况,并且对此进行处理;
@FunctionalInterface
public interface UncaughtExceptionHandler {
void uncaughtException(Thread t, Throwable e);
}
复制代码
编写我们自己的全局异常处理器:
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("线程名称为:"+t.getName()+" 出现异常,异常为:"+e.getMessage());
}
}
复制代码
设置子线程的全局异常处理器:
public class Test7 implements Runnable
{
@Override
public void run() {
throw new RuntimeException();
}
public static void main(String[] args){
Thread thread = new Thread(new Test7());
thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
thread.start();
}
}
复制代码
运行程序,控制台打印:线程名称为:Thread-0 出现异常,异常为:null