Java线程总结

1. 创建线程的几种方法

1.1 继承Thread类

public class first extends Thread {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("你好");
    }
}

1.2 实现Runnable接口

public class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println("你好a");
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread);
        thread.start();
    }
}

1.3 实现使用Callable和Future创建线程

public class MyThread implements Callable<String> {

    @Override
    public String call() throws Exception {
        return "hello";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThread myThread = new MyThread();
        FutureTask futureTask = new FutureTask(myThread);
        new Thread(futureTask).start();
        System.out.println(futureTask.get());
    }
}

1.4 使用线程池

public class TestCallable extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "正在执行");
    }
}

class TestFixedThreadPool {
    public static void main(String[] args) {
        //创建一个可重用固定线程数的线程池
         ExecutorService pool = Executors.newFixedThreadPool(5);
        // 创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口
         Thread t1 = new TestCallable();
         Thread t2 = new TestCallable();
         Thread t3 = new TestCallable();
         Thread t4 = new TestCallable();
         Thread t5 = new TestCallable();
        // 将线程放入池中进行执行
         pool.execute(t1);
         pool.execute(t2);
         pool.execute(t3);
         pool.execute(t4);
         pool.execute(t5);
        // 关闭线程池
         pool.shutdown();
    }
}

result:
pool-1-thread-1正在执行
pool-1-thread-2正在执行
pool-1-thread-3正在执行
pool-1-thread-4正在执行
pool-1-thread-5正在执行

2. 什么是进程?是什么线程?

线程是处理器任务调度和执行的基本单位,进程是操作系统资源分配的基本单位。

进程是程序的一次执行过程,是系统运行的基本单位。线程是一个比进程更小的执行单位,一个进程可以包含多个线程。

从Java虚拟机的角度来理解:Java虚拟机的运行时数据区包含堆、方法区、虚拟机栈、本地方法栈、程序计数器。各个进程之间是相互独立的,每个进程会包含多个线程,每个进程所包含的多个线程并不是相互独立的,这个线程会共享进程的堆和方法区,但这些线程不会共享虚拟机栈、本地方法栈、程序计数器。即每个进程所包含的多个线程共享进程的堆和方法区,并且具备私有的虚拟机栈、本地方法栈、程序计数器,如图所示,假设某个进程包含三个线程。
在这里插入图片描述
由上面可知以下进程和线程在以下几个方面的区别:

1.内存分配: 进程之间的地址空间和资源是相互独立的,同一个进程之间的线程会共享线程的地址空间和资源(堆和方法区)。

2.资源开销: 每个进程具备各自的数据空间,进程之间的切换会有较大的开销。属于同一进程的线程会共享堆和方法区,同时具备私有的虚拟机栈、本地方法栈、程序计数器,线程之间的切换资源开销较小。

进程的特征:

  1. 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
  2. 动态性:进程和程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和不同的状态,这些概念在程序中都是不具备的。
  3. 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
    在这里插入图片描述

3. 并行和并发的区别?

并行:单位时间多个处理器同时处理多个任务。
并发:一个处理器处理多个任务,按时间片轮流处理多个任务。

4. 多线程的优缺点(为什么使用多线程、多线程会引发什么问题)

优点: 当一个线程进入等待状态或者阻塞时,CPU可以先去执行其他线程,提高CPU的利用率。

缺点:

  1. 上下文切换:频繁的上下文切换会影响多线程的执行速度。
  2. 死锁
  3. 资源限制:在进行并发编程时,程序的执行速度受限于计算机的硬件或软件资源。在并发编程中,程序执行变快的原因是将程序中串行执行的部分变成并发执行,如果因为资源限制,并发执行的部分仍在串行执行,程序执行将会变得更慢,因为程序并发需要上下文切换和资源调度。

5. 线程的上下文切换

即便是单核的处理器也会支持多线程,处理器会给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给每个线程的执行时间,一般来说时间片非常的短,所以处理器会不停地切换线程。

CPU会通过时间片分配算法来循环执行任务,当前任务执行完一个时间片后会切换到下一个任务,但切换前会保存上一个任务的状态,因为下次切换回这个任务时还要加载这个任务的状态继续执行,从任务保存到在加载的过程就是一次上下文切换。

6. Java中守护线程和用户线程的区别?

任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon() 设置,true则是将该线程设置为守护线程,false则是将该线程设置为用户线程。同时,Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。

用户线程:平时使用到的线程均为用户线程。

守护线程:用来服务用户线程的线程,例如垃圾回收线程。

守护线程和用户线程的区别主要在于Java虚拟机是后存活。

用户线程:当任何一个用户线程未结束,Java虚拟机是不会结束的。

守护线程:如何只剩守护线程未结束,Java虚拟机结束。

7. 线程死锁是如何产生的,如何避免

这块内容很重要,面试时也可能让手写死锁的代码示例。

死锁:由于两个或两个以上的线程相互竞争对方的资源,而同时不释放自己的资源,导致所有线程同时被阻塞。

死锁产生的条件:
1.互斥条件:一个资源在同一时刻只由一个线程占用。
2.请求与保持条件:一个线程在请求被占资源时发生阻塞,并对已获得的资源保持不放。
3.循环等待条件:发生死锁时,所有的线程会形成一个死循环,一直阻塞。
4.不剥夺条件:线程已获得的资源在未使用完不能被其他线程剥夺,只能由自己使用完释放资源。

避免死锁的方法主要是破坏死锁产生的条件:
1.破坏互斥条件:这个条件无法进行破坏,锁的作用就是使他们互斥。
2.破坏请求与保持条件:一次性申请所有的资源。
3.破坏循环等待条件:按顺序来申请资源。
4. 破坏不剥夺条件:线程在申请不到所需资源时,主动放弃所持有的资源。

8. 用Java实现死锁,并给出避免死锁的解决方案

class DeadLockDemo {
    private static Object resource1 = new Object();
    private static Object resource2 = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);   //线程休眠,保证线程2先获得资源2
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000); //线程休眠,保证线程1先获得资源1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]waiting get resource1
Thread[线程 1,5,main]waiting get resource2

上面代码产生死锁的原因主要是线程1获取到了资源1,线程2获取到了资源2,线程1继续获取资源2而产生阻塞,线程2继续获取资源1而产生阻塞。解决该问题最简单的方式就是两个线程按顺序获取资源,线程1和线程2都先获取资源1再获取资源2,无论哪个线程先获取到资源1,另一个线程都会因无法获取线程1产生阻塞,等到先获取到资源1的线程释放资源1,另一个线程获取资源1,这样两个线程可以轮流获取资源1和资源2。代码如下:

    private static Object resource1 = new Object();
    private static Object resource2 = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000); 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 2").start();
    }
}

9.Java中的死锁、活锁、饥饿有什么区别?

1.活锁
任务或者执行者没有被阻塞,由于某些条件没有被满足,导致线程一直重复尝试、失败、尝试、失败。例如,线程1和线程2都需要获取一个资源,但他们同时让其他线程先获取该资源,两个线程一直谦让,最后都无法获取

2.活锁和死锁的区别:

  1. 活锁是在不断地尝试、死锁是在一直等待。
  2. 活锁有可能自行解开、死锁无法自行解开。

饥饿: 一个或者多个线程因为种种原因无法获得所需要的资源, 导致一直无法执行的状态。以打印机打印文件为例,当有多个线程需要打印文件,系统按照短文件优先的策略进行打印,但当短文件的打印任务一直不间断地出现,那长文件的打印任务会被一直推迟,导致饥饿。活锁就是在忙式等待条件下发生的饥饿,忙式等待就是不进入等待状态的等待。

产生饥饿的原因:

  1. 高优先级的线程占用了低优先级线程的CPU时间
  2. 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
  3. 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait()方法),因为其他线程总是被持续地获得唤醒。

死锁、饥饿的区别:饥饿可自行解开,死锁不行。

10.线程的生命周期和状态

  当线程被创建并启动以后,它既不是已启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blokced)、和死亡(Dead)五种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。

10.1 新建和就绪状态

  当程序使用new关键字创建一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对下你给没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

  当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。

在这里插入图片描述

  调用线程的start()方法之后,该线程立即进入就绪状态–就绪状态相当于“等待执行”,该线程并未真正进入运行状态。

10.2 运行和阻塞状态

  如果处于就绪状态的线程获取到了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态,当然,在一个多处理器的机器上,将会有多个线程并行(注意是平行 :parallel)执行;当线程数大于处理器数时,仍然会存在多个线程在同一个CPU上轮换的现象。

  当一个线程开始运行后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了),线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所使用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务;当该时间段用完之后,系统就会剥夺该线程所占用的资源,让其他线程获取执行的机会。在选择下一个线程时,系统会考虑线程的优先级。

  所有现代的桌面和服务器操作系统都采用抢占式调度的策略,但一些小型设备如手机可能采用协作式调度策略,在这样的系统中,只有当一个线程调用了它的sleep()或yield()方法后才会放弃所占用的资源–也就是必须由线程主动放弃所占用的资源。

在这里插入图片描述
  当前正在执行的线程被阻塞之后,其他线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态而不是运行状态。也就是说,被阻塞线程的阻塞解除后,必须重新等待线程调度器再次调度它。

在这里插入图片描述

线程状态转换图
在这里插入图片描述

10.3 线程死亡

线程会以如下三种方式结束,结束后就处于死亡状态。
1. run()或 call()方法执行完成,线程正常结束。
2. 线程抛出一个未捕获的Exception或者Error。
3. 直接调用该线程的stop()方法来结束该线程–该方法容易导致死锁,通常不推荐使用。

在这里插入图片描述
  为了测试某个线程是否死亡,可以调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞三种状态时,该方法返回true;当线程处于新建、死亡两种状态时该方法返回false

11.控制线程

11.1 join线程

  Thread提供了让一个线程等待另一个线程完成的方法–join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞
// TODO

CAS Demo

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo {

    //CAS compareAndSet:比较并交换!
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        //期望、更新
        //public final boolean compareAndSet(int expect, int update)
        //如果我期望的值达到了,那么就更新,否则就不更新
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
        atomicInteger.getAndIncrement();

        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());

    }
}

结果:true
	2021
	false
	2022

// TODO

附录

1.并发编程
2.3y的多线程知识点
请添加图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

boy快快长大

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值