Java多线程:初级

参考学习:狂神说java(B站)、Java并发编程之美

1. 基础概念

进程:进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,当我们启动main函数时其实就启动一个JVM进程
线程:线程则是进程的一个执行路径,是进程中的一个实体,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。 main函数所在的线程就是进程中主线程。
注意点:

  1. 操作系统在分配资源时是把资源分配给进程的,但是 CPU 资源比较特殊,它是被分配到线程的,因为真正要占用 CPU 运行的是线程,所以也说线程是 CPU 分配的基本单位
  2. 一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。
  3. 程序计数器就是为了记录该线程让出 CPU 时的执行地址的,待再次分配到时间片时线程就可以从自己私有的计数器指定地址继续执行
  4. 栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程是访问不了的
  5. 堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用 new 操作创建的对象实例
  6. 方法区用来存放 JVM 加载的类、常量及静态变量等信息,也是线程共享的。

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

2.线程创建与运行

java提供三种创建线程的方法:

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口

1. Thread类
注意:Thread.run()和Thread.start()方法的区别,Thread.start方法会重新创建一个线程(就绪状态),java虚拟机调用这个线程的run方法;Thread.run方法则由java虚拟机直接调用的,如果我们没有启动线程(没有调用线程的start()方法)而是在应用代码中直接调用run()方法,那么这个线程的run()方法其实运行在当前线程(即run()方法的调用方所在的线程)之中,而不是运行在其自身的线程中。
在这里插入图片描述

public class ThreadDemo extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println(currentThread().getName()+i);
        }

    }

    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();
        threadDemo.start();
        for (int i = 0; i < 100; i++) {
            System.out.println(currentThread().getName()+i);
        }
    }
}

2. Runnable接口

实现Runnable接口具有多线程能力,启动线程:Thread.start(Runnable实现对象)。这样使用相对于继承Thread类避免OOP单继承局限性,灵活方便一个对象被多个线程使用。

public class RunnableDemo implements Runnable{


    @Override
    public void run() {
        System.out.println(currentThread().getName());
    }

    public static void main(String[] args) {
        RunnableDemo runnableDemo = new RunnableDemo();
        new Thread(runnableDemo).start();
        //通过lambda表达式实现Runnable接口
        new Thread(()-> System.out.println(currentThread().getName())).start();
    }
}

3.实现Callable接口

  1. 实现Callbale接口需要返回值类型;
  2. 重写call方法需要抛出异常。
  3. 创建目标对象
  4. 创建执行服务: ExecutorService executorService = Executors.newFixedThreadPool(1);
  5. 提交执行:Future submit = executorService.submit(callableDemo);
  6. .获取结果:Double result = submit.get();
  7. 关闭服务:executorService.shutdownNow();
public class CallableDemo implements Callable<Double> {
    @Override
    public Double call() throws Exception {
        System.out.println(currentThread().getName());
        return Math.random();
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CallableDemo callableDemo = new CallableDemo();
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        Future<Double> submit = executorService.submit(callableDemo);
        Double result = submit.get();
        System.out.println(result);
        executorService.shutdownNow();

    }
}

3. 停止、休眠、礼让和强制执行线程

1. 停止线程

不推荐使用stop()、destroy()方法,推荐使线程自己停止下来,使用一个标志位终止变量。

public class StopThreadDemo implements Runnable{

    //设置标志位
    private boolean flag=true;
    @Override
    public void run() {
        int i=0;
        while (flag){
            System.out.println("thread run"+i);
        }
    }
    //设置方法停止线程,转换标注位
    public void stop() {
        this.flag=false;
    }

    public static void main(String[] args) {
        StopThreadDemo stopThreadDemo = new StopThreadDemo();
        new Thread(stopThreadDemo).start();
        for (int i = 0; i < 1000; i++) {
            System.out.println(currentThread().getName());
            if (i==900){
                stopThreadDemo.stop();
                System.out.println("停止线程");
            }
        }
    }
}

2.线程休眠

Thread 类中有一个静态的 sleep 方法,当一个执行中的线程调用了 Thread 的 sleep 方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与 CPU 的调度,但是该线程所拥有的监视器资源。(抱着锁睡)等待睡眠时间到了后该函数会正常返回,线程处于就绪状态。

//休眠一秒
 Thread.sleep(1000);

JUC中也常用TimeUnit的枚举类进行线程休眠

TimeUnit.SECONDS.sleep(1);

3.线程礼让

yield 是Thread 类中的静态方法,当一个线程调用 yield 方法时,实际就是在暗示线程调度器当前线程请求让出自己的 CPU 使用,但是线程调度器可以无条件忽略这个暗示
当一个线程调用 yield 方法时,当前线程会让出 CPU 使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出 CPU 的那个线程来获取 CPU 执行权。

public class YieldTest implements Runnable{

    
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始执行");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"线程停止执行");
    }

    public static void main(String[] args) {
        YieldTest thread1=new YieldTest();
        YieldTest thread2=new YieldTest();
        YieldTest thread3=new YieldTest();
        new Thread(thread1,"a").start();
        new Thread(thread2,"b").start();
        new Thread(thread3,"c").start();
    }
}
b线程开始执行
b线程停止执行
c线程开始执行
a线程开始执行
a线程停止执行
c线程停止执行

4.线程强制执行

Join强制执行线程,待此线程执行完成后再执行其它线程,其它线程阻塞。

//线程强制执行相当于插队
public class TestJoin implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程VIP..."+i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        thread.start();
        //主线程
        for (int i = 0; i < 200; i++) {
            if (i==100){
                thread.join();
            }
            System.out.println("main "+i);
        }
    }
}

5.线程优先级

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
线程优先级用数字来表示,范围从1~10。优先级高低只是意味着获得调度的概率高低,都是看CPU的调度。

4. 守护线程与用户线程

Java 中的线程分为两类,分别为 daemon 线程(守护线程)和 user 线程(用户线程)。在 JVM 启动时会调用 main 函数, main 函数所在的线程就是一个用户线程,其实在 JVM内部同时还启动了好多守护线程,比如垃圾回收线程、后台操作记录日志、监控内存等等。
守护线程与用户线程的区别
当最后一个非守护线程结束时, JVM 会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响 JVM 的退出。也就是说JVM必须保证用户线程执行完毕

public class DemonTest {

    public static void main(String[] args) {
        God god = new God();
        You you = new You();
        Thread thread = new Thread(god);
        //默认是false表示用户线程,而正常的线程都是用户线程
        thread.setDaemon(true);
        thread.start();
        new Thread(you).start();
    }

}
class God implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("God bless you");
        }
    }
}

class You implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 36500; i++) {
            System.out.println("live...");
        }
        System.out.println("go die...");
    }
}

5. 线程同步

线程不安全问题:线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题,这个时候需要用到线程同步。

在这里插入图片描述
线程同步:线程同步是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,前面的使用完毕,下一个线程再使用,线程同步执行条件:队列 + 锁

synchronized 关键字

synchronized 块是 Java 提供的一种原子性内置锁, Java 中的每个对象都可以把它当作一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。线程的执行代码在进入 synchronized 代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的 wait 系列方法时释放该内置锁。内置锁是排它锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁

注意:由于 Java 中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而 synchronized 的使用就会导致上下文切换,并带来线程调度开销。并且一个线程持有锁会导致其他所有需要此锁的线程挂起。如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致性能倒置,引起性能问题

Demo:

public class SynchronizedDemo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            try {
                phone.sendMes();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            phone.call();
        }).start();

    }
}

class Phone{

    public synchronized void sendMes() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }

    public synchronized void call(){
        System.out.println("打电话");
    }
}

需要注意的是,synchronized锁的是方法的调用者,谁先拿到谁先执行。加入在方法中加入关键字static,则类一加载就有了,锁的是Class。

public static synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
} 
System.out.println("发短信");
} 

Lock(JUC)

  • 从JDK5.0开始,Java提供了通过显示定义同步锁对象来实现同步,同步锁用Lock对象充当
  • java.util.concurrent.lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该获得Lock对象
  • ReentrantLock类实现了Lock,它拥有与synchronized想通的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
    Lock三部曲
    1. new ReentrantLock();
    2. lock.lock(); 加锁
    3. finally=> lock.unlock();解锁

Synchronized 和 Lock 区别 :

  1. Synchronized 内置的Java关键字, Lock 是一个Java类
  2. Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
  3. Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁
  4. Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去;
  5. Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,非公平(可以自己设置);
  6. Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!

Demo:

public class LockDemo {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();
        new Thread(()->{
            buyTicket.buy();
        },"A").start();
        new Thread(()->{
            buyTicket.buy();
        },"B").start();
        new Thread(()->{
            buyTicket.buy();
        },"C").start();

    }

}

class BuyTicket{
    private int TicketNum=100;
    private final Lock lock=new ReentrantLock();

    public void buy(){
       while (true){
           try {
               lock.lock();
               if (TicketNum>0){
                   System.out.println(currentThread().getName()+TicketNum--);
               }else {
                   break;
               }

           }  finally {
               lock.unlock();
           }
       }

    }
}

6. 生产者消费者问题

1. 问题描述

  • 假设仓库只存放一件物品,生产者将生产出来的产品放入仓库,消费者将仓库中的产品取走消费
  • 如果仓库没有产品,则生产者将产品放入仓库,否则停止生产并且等待,直到仓库中的产品被消费者取走位置
  • 如果仓库中有产品,消费者则可以将产品取走消费,否则停止消费并等待,直到仓库再次放入产品为止

2.分析

这是一个线程同步问题,生产者和消费者共享一个资源,并且生产者和消费者之间互为依赖,互为条件

  • 对于生产者,没有生产产品前,通知消费者等待,生产产品后,通知消费者消费
  • 对于消费者,消费之后要通知生产者已经结束消费,需要生产新的产品提供消费
  • 在生产者消费者问题中,仅有synchronized是不够的
    • synchronized可阻止并发更新同一个共享资源,实现了同步
    • synchronized不能用来实现线程之间的通讯

3.解决方法

  1. 并发协作模式“生产者/消费者模式”,俗称管理法
    生产者:负责生产数据的模块(可以是方法,对象,线程,进程)
    消费者:负责处理数据的模块(可以是方法,对象,线程,进程)
    缓冲区:消费者不能直接使用生产者的数据图,他们需要一个缓冲区
    生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
  2. 并发协作模型“生产者/消费者模式”,俗称信号灯法
    通过设置一个信号灯(flag),如果为True的时候通知消费者消费,如果False进行等待
Synchronized 版
public class ProducerConsumerTest {
    public static void main(String[] args) {
        Product product = new Product();

        new Thread(()->{
            try {
                product.producer();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();
        new Thread(()->{
            try {
                product.consumer();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"B").start();
    }
    
}
class Product{
    private int number=0;

    //生产
    public synchronized void producer() throws InterruptedException {
        if (number!=0){
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        this.notifyAll();
    }

    //消费
    public synchronized void consumer() throws InterruptedException {
        if (number==0){
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        this.notifyAll();
    }

}

代码存在问题,虚假唤醒:
在这里插入图片描述

所以,我们要把if改成while判断:

//生产
public synchronized void producer() throws InterruptedException {
    while (number!=0){
        this.wait();
    }
    number++;
    System.out.println(Thread.currentThread().getName()+"=>"+number);
    this.notifyAll();
}
JUC 版

在这里插入图片描述

public class ProducerConsumerJUCDemo {
    public static void main(String[] args) {
        ProductJUC productJUC = new ProductJUC();
        
        new Thread(()->{
            try {
                productJUC.producer();
            } catch (InterruptedException e) {
                
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {
                productJUC.consumer();
            } catch (InterruptedException e) {

                e.printStackTrace();
            }
        }).start();
    }
}
class ProductJUC{
    private int number=0;

    Lock lock=new ReentrantLock();
    Condition condition=lock.newCondition();

    //condition.await(); // 等待
    //condition.signalAll(); // 唤醒全部
    public void producer() throws InterruptedException {
        lock.lock();
        try {
            while (number!=0){
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName()+"=>"+number);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
    public void consumer() throws InterruptedException {
        lock.lock();
        try {
            while (number==0){
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName()+"=>"+number);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

}

JUC的lock版和synchronized版的区别和改进:可以使用Condition精准的通知和唤醒线程;而且synchronized上下文切换导致性能消耗;

精准唤醒:

//使用condition精准唤醒
class Print{
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    private int number = 1; // 1A 2B 3C

    public void printA(){
        lock.lock();
        try {
            // 业务,判断-> 执行-> 通知
            while (number!=1){
                // 等待
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName()+"=>AAAAAAA");
            // 唤醒,唤醒指定的人,B
            number = 2;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printB(){
        lock.lock();
        try {
            // 业务,判断-> 执行-> 通知
            while (number!=2){
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName()+"=>BBBBBBBBB");
            // 唤醒,唤醒指定的人,c
            number = 3;
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printC(){
        lock.lock();
        try {
            // 业务,判断-> 执行-> 通知
            // 业务,判断-> 执行-> 通知
            while (number!=3){
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName()+"=>BBBBBBBBB");
            // 唤醒,唤醒指定的人,c
            number = 1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

}

7. 线程池

  • 思路:提前创建好多个线程,放入线程池,使用时直接获取,使用完毕放回池中,可以避免重复频繁创建销毁,实现重复利用。
  • 好处
    • 提高响应速度(减少创建新线程时间)
    • 降低资源消耗(重复利用线程池中的线程,不需要每次创建)
    • 便于线程管理
      • corePoolSize:核心池大小
      • maxImumPoolSize:最大线程数
      • keepAliveTime:线程没有任务时最多保持多长时间后会终止

线程池相关API:ExecutorServiceExecutors

  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
    • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
    • Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
    • void shutdown():关闭线程池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
public class ThreadPoolDemo {
    public static void main(String[] args) {
        //创建线程池
        //newFixedThreadPool参数为线程池大小
        ExecutorService service = Executors.newFixedThreadPool(10);
        //启动
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        //关闭连接
        service.shutdown();

    }
}
class MyThread implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值