java并发之多线程基础总结

1 篇文章 0 订阅
本文详细介绍了进程与线程的区别,包括它们的定义、内存模型以及创建成本。线程是CPU调度的基本单位,进程有独立的地址空间,而线程共享进程地址。文章还讨论了线程的七种状态转换、同步与异步的概念,以及CPU时间片调度。此外,探讨了线程安全问题,包括如何解决和通信,并提供了Java中线程的创建方式和停止策略。最后,提到了线程的优先级和原子性操作,以及单例模式在多线程环境下的实现和线程安全问题的避免方法。
摘要由CSDN通过智能技术生成

进程与线程的区别

  • 进程:是系统进行分配和管理资源的基本单位,比如一个pc上面启动QQ或者微信,都是一个单独的进程。

  • 线程:进程的一个执行单元,是进程内调度的实体、是CPU调度和分派的基本单位,是比进程更小的独立运行的基本单位。

  • 一个程序至少有一个进程,一个进程至少有一个线程。

  • 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。
    而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
    线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式进行。 如何处理好同步与互斥是编写多线程程序的难点。
    多进程程序更健壮,进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,
    而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,所以可能一个线程出现问题,进而导致整个程序出现问题。

欢迎关注个人公众号【好好学技术】交流学习

线程的7种状态及其相互转换

初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
可运行(RUNNABLE):处于可运行状态的线程正在JVM中执行,但它可能正在等待来自操作系统的其他资源,例如处理器。
运行(RUNNING): 处于Runnable状态的线程获取到 CPU 资源,执行程序代码。
阻塞(BLOCKED):线程阻塞于synchronized锁,等待获取synchronized锁的状态。
等待(WAITING):Object.wait()、join()、 LockSupport.park(),进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
超时等待(TIME_WAITING):Object.wait(long)、Thread.join()、LockSupport.parkNanos()、LockSupport.parkUntil,该状态不同于WAITING,它可以在指定的时间内自行返回。
终止(TERMINATED):表示该线程已经执行完毕自己结束,或者产生了异常而结束。

在这里插入图片描述

同步与异步的区别

  • 同步

代码自上向下执行,执行完一个,才能执行另一个。

  • 异步

多个任务同时执行,相互之间没有影响。

cpu调度时间片

  1. 单核cpu每次只能执行一次线程,此时开启了多线程,则会对每个线程轮流执行。
  2. cpu每次单个计算的时间,称为一个cpu时间片,因为时间很短,几乎无法感知,就会觉得在进行多线程一样。
  3. 对于线程,在等待cpu调度的时候,该线程状态为runnable就绪状态,如果被cpu调度了,则会变为运行状态。
  4. 当cpu转让执行其他线程的时候,该线程又变为就绪状态。

cpu密集型与IO密集型

  • cpu密集型

也叫计算密集型。指长时间占用cpu,例如计算一些复杂的运算,逻辑处理等情况

  • IO密集型

cpu使用率较低,程序中存在大量的IO操作占用时间。比如写大文件。

cpu调度算法

  1. 先来先服务
    类似于FIFO。缺点:如果最先来的是cpu密集型的,可能会导致其他线程一直无法执行。
  2. 最短作业法
    谁的计算时间短,最先执行谁。
  3. 优先级调度算法
    根据重要性将进程分为四个优先级。

多线程场景

  • 异步发送邮件
  • 异步记录日志
  • 分布式计算等

多线程一定快吗

不一定。多线程的创建,上下文切换也是需要开销的。
要考虑服务器的核数。

线程创建方式

  • 继承Thread,并重写父类的run方法
  • 实现Runable接口,并实现run方法
  • 使用匿名内部类
  • Lambda表达式
  • 线程池
  • Callable和Future创建线程

什么是线程安全性

当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类为线程安全的。----《并发编程实战》

什么是线程不安全?

当多个线程共享同一个全局变量,做写的操作,可能会受到其他线程的干扰,发生线程安全问题。
或者说:多线程并发访问时,得不到正确的结果。

怎么解决线程安全问题

核心思想就是加锁: jvm级别锁, 分布式锁 (后面单独开篇文章讲讲 synchronized锁 和 reentrantlock, 这里就不过多赘述了)

多线程如何通信

等待通知机制
  • notify()
    唤醒一个正在等待该对象的线程
  • notifyAll()
    唤醒所有等待该对象的线程
  • wait()
    使当前线程进入waiting状态,并释放当前对象的锁。只有被其他线程唤醒或者中断,才会返回。

注意: 这三个方法都是Object类提供的。 因为我们在使用synchronized锁时,可以对任意对象进行加锁。

public class ThreadTest extends Thread {

    @Override
    public void run() {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + "---当前线程阻塞并释放锁---");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "--- 被唤醒了 ---");
        }
    }

    public static void main(String[] args) {
        ThreadTest thread = new ThreadTest();
        thread.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (thread) {
            thread.notify();
        }
    }
    //打印结果
    //Thread-0---当前线程阻塞并释放锁---
    //Thread-0--- 被唤醒了 ---
}

sleep和wait 区别

sleep(long) 睡眠时并不释放对象锁,醒了后继续执行。
wait(long) 在等待过程中会释放锁,被唤醒后会去竞争cpu资源获取锁。

多个线程如何保证执行顺序

public static void main(String[] args) {
    Thread t1 = new Thread(()-> System.out.println(Thread.currentThread().getName() + "线程执行"), "t1");
    Thread t2 = new Thread(()-> System.out.println(Thread.currentThread().getName() + "线程执行"), "t2");
    Thread t3 = new Thread(()-> System.out.println(Thread.currentThread().getName() + "线程执行"), "t3");
    t1.start();
    t2.start();
    t3.start();
}

执行结果

t2线程执行
t3线程执行
t1线程执行

想要保证顺序可以用到join()方法

public static void main(String[] args) {
    Thread t1 = new Thread(()-> System.out.println(Thread.currentThread().getName() + "线程执行"), "t1");

    Thread t2 = new Thread(()-> {
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "线程执行");
    }, "t2");

    Thread t3 = new Thread(()-> {
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "线程执行");
    }, "t3");
    t1.start();
    t2.start();
    t3.start();
}

打印结果

t1线程执行
t2线程执行
t3线程执行

用户线程与守护线程区别

java中的线程分为两种:用户线程和守护线程。
通过Thread.setDaemon(true)来设置为守护线程,默认为false.

  1. 用户线程是独立存在的,不会因为其他线程退出而退出。
  2. 守护线程依赖于用户线程。用户线程退出了,守护线程也会退出。

如何安全停止线程

调用stop方法

stop: 中止线程,清除锁信息。但是可能导致线程安全问题,JDK不建议使用。

Interrupt

Interrupt 打断正在运行或者正在阻塞的线程。
1.如果目标线程在调用Object的wait()、wait(long)或wait(long, int)方法、join()、join(long, int)或sleep(long, int)方法时被阻塞,那么Interrupt会生效,该线程的中断状态将被清除,抛出InterruptedException异常。
2.如果目标线程是被I/O或者NIO中的Channel所阻塞,同样,I/O操作会被中断或者返回特殊异常值。达到终止线程的目的。
如果以上条件都不满足,则会设置此线程的中断状态。

打断正在阻塞的线程

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, "t1");
    t1.start();
    Thread.sleep(1000);
    System.out.println("打断子线程开始");
    t1.interrupt();
    System.out.println("获取打断标记"+ t1.isInterrupted());
}
打断子线程开始
获取打断标记false
java.lang.InterruptedException: sleep interrupted
   at java.lang.Thread.sleep(Native Method)
   at com.fandf.demo.thread.ThreadTest.lambda$main$0(ThreadTest.java:25)
   at java.lang.Thread.run(Thread.java:748)

打断正在运行的线程

public class ThreadTest extends Thread {

    public ThreadTest(String name) {
        super(name);
    }

    @Override
    public void run() {
        int i = 0;
        for (; ; ) {
            if (this.isInterrupted()) {
                break;
            }
            System.out.println(i++);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new ThreadTest("t1");
        t1.start();
        Thread.sleep(100);
        System.out.println("打断子线程开始");
        t1.interrupt();
        System.out.println("获取打断标记" + t1.isInterrupted());
    }
}
标记位

使用一个标记位来终止线程是最安全的。

public class ThreadTest extends Thread {

    private volatile boolean flag = true;

    @Override
    public void run() {
        while (flag) {

        }
        System.out.println("线程中止");
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadTest t1 = new ThreadTest();
        t1.start();
        t1.flag = false;
    }
}

yield方法

yield会使线程从运行状态变为就绪状态,主动让出cpu执行权,重新进行竞争。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。

/**
 * @author fandongfeng
 */
public class ThreadYield extends Thread{

    public ThreadYield(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 30; i++) {
            if(i == 0 && Thread.currentThread().getName().equals("t1")) {
                System.out.println(Thread.currentThread().getName() + "让出cpu执行权");
                Thread.yield();
            }
            System.out.println(Thread.currentThread().getName() + "拿到cpu执行权" + i);
        }
    }

    public static void main(String[] args) {
        new ThreadYield("t1").start();
        new ThreadYield("t2").start();
    }
}

执行结果,多次打印有可能出现如下结果

t1让出cpu执行权
t2拿到cpu执行权0
t2拿到cpu执行权1
t2拿到cpu执行权2
t2拿到cpu执行权3
t2拿到cpu执行权4
t2拿到cpu执行权5
t1拿到cpu执行权0
t2拿到cpu执行权6
t2拿到cpu执行权7
t2拿到cpu执行权8
t2拿到cpu执行权9
t2拿到cpu执行权10
t2拿到cpu执行权11
t2拿到cpu执行权12
t2拿到cpu执行权13
t2拿到cpu执行权14
t2拿到cpu执行权15
t2拿到cpu执行权16
t2拿到cpu执行权17
t2拿到cpu执行权18
t2拿到cpu执行权19
t2拿到cpu执行权20
t2拿到cpu执行权21
t2拿到cpu执行权22
t2拿到cpu执行权23
t2拿到cpu执行权24
t2拿到cpu执行权25
t2拿到cpu执行权26
t2拿到cpu执行权27
t2拿到cpu执行权28
t2拿到cpu执行权29
t1拿到cpu执行权1
t1拿到cpu执行权2
t1拿到cpu执行权3
t1拿到cpu执行权4
t1拿到cpu执行权5
t1拿到cpu执行权6
t1拿到cpu执行权7
t1拿到cpu执行权8
t1拿到cpu执行权9
t1拿到cpu执行权10
t1拿到cpu执行权11
t1拿到cpu执行权12
t1拿到cpu执行权13
t1拿到cpu执行权14
t1拿到cpu执行权15
t1拿到cpu执行权16
t1拿到cpu执行权17
t1拿到cpu执行权18
t1拿到cpu执行权19
t1拿到cpu执行权20
t1拿到cpu执行权21
t1拿到cpu执行权22
t1拿到cpu执行权23
t1拿到cpu执行权24
t1拿到cpu执行权25
t1拿到cpu执行权26
t1拿到cpu执行权27
t1拿到cpu执行权28
t1拿到cpu执行权29

Process finished with exit code 0

多线程优先级

每个线程都有一个优先级,范围从1到10,其中1是最低优先级,10是最高优先级。线程默认的优先级是5。
可以使用setPriority(int priority)方法设置线程的优先级。注意,优先级只是给出一个提示,操作系统不保证按照优先级执行线程

通常情况下,我们不需要设置线程的优先级,而是使用Java中提供的线程调度器进行线程调度。线程调度器根据线程的状态、优先级、以及等待时间等因素来决定哪个线程首先执行。

public class ThreadTest extends Thread {

    public ThreadTest(String name) {
        super(name);
    }

    @Override
    public void run() {
        int i = 0;
        for (int j = 0; j < 100; j++) {
            i++;
            System.out.println(Thread.currentThread().getName() + "," + i);
        }
        System.out.println(Thread.currentThread().getName() + "执行完毕");
    }

    public static void main(String[] args) {
        ThreadTest t1 = new ThreadTest("t1");
        ThreadTest t2 = new ThreadTest("t2");
        t1.setPriority(MIN_PRIORITY);
        t2.setPriority(MAX_PRIORITY);
        t1.start();
        t2.start();
    }
}

什么是原子性操作

一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 volatile关键字仅仅保证可见性,并不保证原子性。 synchronize关键字,使得操作具有原子性。

volatile关键字及其使用场景

指令重排:
CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化。
在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。

volatile用法

  • 能且仅能修饰变量
  • 保证该变量的可见性,volatile关键字仅仅保证可见性,并不保证原子性
  • 禁止指令重排序
  • A、B两个线程同时读取volatile关键字修饰的对象,A读取之后,修改了变量的值,修改后的值,对B线程来说,是可见
  • 使用场景 1:作为线程开关 2:单例,修饰对象实例,禁止指令重排序

单例与线程安全

  • 饿汉式–本身线程安全 在类加载的时候,就已经进行实例化,无论之后用不用到。如果该类比较占内存,之后又没用到,就白白浪费了资源。
public class HungerSingleton {

    private static HungerSingleton ourInstance = new HungerSingleton();

    public static HungerSingleton getInstance() {
        return ourInstance;
    }

    private HungerSingleton() {
    }

}
  • 懒汉式 – 最简单的写法是非线程安全的 在需要的时候再实例化
public class LazySingleton {

    private static volatile LazySingleton ourInstance = null;

    public static LazySingleton getInstance() {
        if(null == ourInstance){
            synchronized (ourInstance){
                if(ourInstance == null){
                    ourInstance = new LazySingleton();
                }
            }
        }
        return ourInstance;
    }

    private LazySingleton() {
    }
}

如何保证单例? private私有的空参构造器 static的对象和方法getInstance

如何避免线程安全性问题

  • 线程安全性问题成因
     多线程环境
     多个线程操作同一共享资源
     对该共享资源进行了非原子性操作
    
  • 如何避免

    打破成因中三点任意一点
    1:多线程环境–将多线程改单线程(必要的代码,加锁访问)
    2:多个线程操作同一共享资源–不共享资源(ThreadLocal、不共享、操作无状态化、不可变)
    3:对该共享资源进行了非原子性操作–将非原子性操作改成原子性操作(加锁、使用JDK自带的原子性操作的类、JUC提供的相应的并发工具类)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值