Java多线程与并发框(完结篇)——再看不懂我找不到女朋友

简介: Java多线程与并发框(完结篇)——再看不懂我找不到女朋友

多线程

关于多线程有关的概念:

  • 进程:进程指正在运行的程序,并且具有一定独立功能
  • 线程:线程是进程中的一个执行单位,负责当前进程程序的执行,一个进程中至少会有一个线程,如果一个进程中包含多个线程,那么可称为多线程程序
  • 单线程:当要执行多个任务时,cpu只会依次执行,当一个任务执行完后,再去执行另外一个任务
  • 多线程:多个任务可以同时进行

在Java中,不同线程会有不同的优先级抢占cpu,如果线程优先级相同,就会随机先去一个线程去执行

Java程序运行时会默认执行3个进程:

  • main主线程
  • gc垃圾回收机制
  • 异常处理机制

我们如何能够判断程序是否是多线程?

如果我们能够将程序的执行用一条直线画出来,就说明是单线程

关于线程的常用API方法

  1. run():该方法需要被重写,重写的内容就是需要执行的操作
  2. start():调用该方法就会启动相应的线程,并调用当前线程的run方法
  3. sleep(long millitime):将当前线程进入阻塞状态(不会释放锁,即同步监视器)
  4. join():当a线程调用b线程的join方法时,a线程会进入阻塞状态,直到b线程的任务执行完毕
  5. isAlive():判断当前线程是否存活
  6. yield():调用该方法后回释放当前线程的cpu执行权,当时并不代表不会再次执行,有可能释放后,又是该线程抢占到了cpu的执行权
  7. currentThread():Thread类中的静态方法,会返回执行当前程序的线程
  8. getName():返回当前线程的名字
  9. setName():设置线程的名字
  10. getPriority():设置线程的优先级(

MAX_PRIORITY=10

MIN_PRIORITY=1

NORM_PRIORITY=5 默认优先级

  1. wait():将线程进入阻塞状态(会释放掉锁),只能在同步代码块或同步方法中使用
  2. notify():将另外一个线程唤醒
  3. notifyAll():唤醒所有被阻塞的线程

线程创建的4种方式

一:继承Thread类

  1. 首先创建一个类去继承Thread
  2. 重写Thread中的run方法
  3. main()中创建该对象
  4. 调用该对象的start方法,启动线程
public class test {
    public static void main(String[] args) {
        MyThread1 myThread1=new MyThread1();
        myThread1.start();
    }
}
class MyThread1 extends Thread{
    @Override
    public void run() {
        System.out.println("继承了Thread");
    }
}

二:实现Runnable接口

  1. 创建一个类实现Runnable接口
  2. 实现接口的run方法
  3. 在main()中创建实现Runnable的对象
  4. 创建Thread对象,并把刚创建好的类传参
  5. 调用Thread对象的start方法,启动线程
public class test {
    public static void main(String[] args) {
        MyThread1 myThread1=new MyThread1();
        Thread t=new Thread(myThread1);
        t.start();
    }
}
class MyThread1 implements Runnable{
    @Override
    public void run() {
        System.out.println("实现了Runnable");
    }
}

三:实现Callable接口

  1. 创建一个类实现Callable接口
  2. 实现接口的call()方法
  3. 在main中创建实现Callable的对象
  4. 创建FutureTask对象并把上面创建的对象传参
  5. 创建Thread对象,传入FutureTask对象
  6. 调用Thread的start方法,启动线程
public class test {
    public static void main(String[] args) {
        MyThread2 myThread2=new MyThread2();
        FutureTask futureTask=new FutureTask(myThread2);
        Thread t=new Thread(futureTask);
        t.start();
    }
}
class MyThread2 implements Callable{
    @Override
    public Object call() throws Exception {
        System.out.println("实现了Callable");
        return null;
    }
}

此方式如果需要得到返回值需要调用futureTask.get();

但是会抛异常,用try,catch方法捕捉一下就好了

四:线程池

  1. 创建ExecutorService对象
  2. 传入相应的线程对象
  3. 结束线程池
public class test {
    public static void main(String[] args) {
        //创建线程池,设置线程池线程的数量为10
        ExecutorService service = Executors.newFixedThreadPool(10);
        //execute适用于实现了Runnable的对象
        service.execute(new MyThread1());
        //submit适用于实现了Callable的对象
        service.submit(new MyThread2());
        //结束线程池
        service.shutdown();
    }
}
class MyThread1 implements Runnable{
    @Override
    public void run() {
        System.out.println("实现了Runnable");
    }
}
class MyThread2 implements Callable{
    @Override
    public Object call() throws Exception {
        System.out.println("实现了Callable");
        return null;
    }
}

Runnable和Callable的对比:

  • Callable可以有返回值,执行完相应操作后可以返回需要的结果
  • 可以抛异常,可以将call方法中的异常抛出
  • 支持泛型,可以指定返回值类型

线程安全

什么是线程安全问题呢?

当多个线程对同一个共享数据操作时,线程执行还没来得及更新处理共享的数据,从而使得其他操作的线程并未得到最新的数据,从而产生问题

举个例子:

  1. 当甲乙两人向同一账户存钱,让甲乙两个线程同时存钱,如果甲向账户存了1000元,并打印此时余额,应为1000元,但是如果此时乙也存了1000元,就会导致,显示余额为2000元,并不是甲当时的余额
  2. 还有就是火车售票问题,如果多个窗口同时售票,如果1号窗口正在卖001号票时,此时还未处理完成,这是2号窗口也卖了001号票,这就导致产生了两个001号票

那么如何解决呢?

有三种方式:

方法一:同步代码块

synchronized(Object obj)
{
  //操作内容                
}
  • synchronized():传入的可以是任意类的对象,但必须是多个线程共用的,一般可以利用this,即当前对象(Runnable),Thread不太行,因为继承多个Thread类会导致this对象不一致
  • 被包住的代码执行为单线程,当一个线程执行完后,另外一个线程才有可能会分配到执行权去执行
  • 多个线程必须共用同一把锁,这样才能够判断一个线程是够执行
  • Runnable一般很实用,因为多个线程都调用同一个类的方法,但是Thread就需要自己定义静态变量或者当前的唯一类即(windows.class)
public class test {
    public static void main(String[] args) {
        Window t1 = new Window("窗口1");
        Window t2 = new Window("窗口2");
        Window t3 = new Window("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Window extends Thread{
    private static int ticket=100;
    //继承方式,要用静态对象,因为继承实现多线程有多个对象,不是共用一个对象
    private static Object obj=new Object();
    public Window(String name){
        super(name);
    }
    @Override
    public void run() {
        while(true){
            //不可以用this,同理,因为有很多对象,不唯一
            synchronized(obj){
                if(ticket>0){
                    System.out.println(getName()+":卖票,票号为:"+ticket);
                    ticket--;
                }else{
                    break;
                }
            }
        }
    }
}

方法二:同步方法

和同步代码块类似

就是将共享数据的操作封装成方法

将这个方法用锁锁住

public class test {
    public static void main(String[] args) {
        Window t1 = new Window("窗口1");
        Window t2 = new Window("窗口2");
        Window t3 = new Window("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Window extends Thread{
    private static int ticket=100;
    public Window(String name){
        super(name);
    }
    @Override
    public void run() {
        while(true){
            show();
        }
    }
    public static synchronized void show(){
        if(ticket>0){
            System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket);
            ticket--;
        }
    }
}

注意:

  • show方法要定义成静态,因为如果是非静态,它的锁默认是当前对象,继承方式就会有多个锁,如果是Runnable就可以,所以需要编程静态,这样默认锁就是当前类对象

方法三:lock锁

  • 首先创建一个ReentrantLock对象
  • 在执行共享数据之前将锁打开,调用lock方法
  • 在结束时将锁解开,调用unlock方法
public class test {
    public static void main(String[] args) {
        Windows w = new Windows();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Windows implements Runnable{
    private int ticket=100;
    private ReentrantLock lock=new ReentrantLock(true);
    @Override
    public void run() {
        while(true){
            try {
                lock.lock();
                if(ticket>0){
                    System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket);
                    ticket--;
                }
                else{
                    break;
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

问题:该方式和synchrnized有什么不同呢?

synchronized在执行完相应代码后会自动上锁解锁,而lock需要手动上锁和解锁,较为灵活

线程的死锁

描述:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

举个例子:两个人迎面相遇,甲希望乙会给他让路,而乙希望甲给他让他让路,就这样两个人僵持在这里,最终谁也不给谁让路,导致死锁问题

public class test {
    public static void main(String[] args) {
        StringBuffer s1=new StringBuffer();
        StringBuffer s2=new StringBuffer();
        new Thread(){
            @Override
            public void run() {
                synchronized (s1){
                    s1.append("a");
                    s2.append("1");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){
                    s1.append("c");
                    s2.append("3");
                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

本例中,线程1首先拿到了s1锁,然后阻塞了一段时间,这段时间线程2拿到了s2锁,这是线程1就绪需要s2锁,而线程2需要s1锁,两者谁都拿不到,就会僵持住

解决死锁的相应办法:

  • 减少共享变量的使用
  • 设计相应的算法去规避死锁问题
  • 尽量减少锁的嵌套使用

线程的通信

  • wait():将线程进入阻塞状态(会释放掉锁),只能在同步代码块或同步方法中使用
  • notify():将另外一个优先级高的线程唤醒
  • notifyAll():唤醒所有被阻塞的线程

注意:这三个方法只能够在同步代码块或者同步方法使用,都定义在了Object类中

例题要求:

让两个线程交替打印1-100之间的数字

public class 线程通信 {
    public static void main(String[] args) {
        Number number=new Number();
        Thread t1=new Thread(number);
        Thread t2=new Thread(number);
        t1.setName("线程一");
        t2.setName("线程二");
        t1.start();
        t2.start();
    }
}
class Number implements Runnable{
    private int number=1;
    @Override
    public void run() {
        while(true){
            synchronized (this) {
                notify();
                //唤醒全部
                // notifyAll();
                if(number<=100){
                    System.out.println(Thread.currentThread().getName()+":"+number);
                    number++;
                    try {
                        //使得调用如下方法进程阻塞,执行wait后,锁就被释放
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }
    }
}
  • 当线程1首次进入,会打印出1,然后调用了wait方法 ,进入阻塞状态
  • 此时线程2进入,首先会唤醒线程1,然后打印2,然后自己进入阻塞状态
  • 两者交替阻塞唤醒,直到打印完为止

那么问题是sleep和wait方法有什么异同?

两个方法声明的位置不同,sleep是Thread中声明的,而wait是Object中声明的

sleep可以在任何情景调用,而wait只能够在同步代码块或同步方法中使用

sleep执行后不会释放当前的锁,而wait会释放掉当前的锁

生产者和消费者问题:

生产者(Priductor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如20个),如果生产者视图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产:如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

package 生产者与消费者问题;
public class 生产者 {
    public static void main(String[] args) {
        Clerk clerk=new Clerk();
        Producer p1 = new Producer(clerk);
        p1.setName("生产者");
        Consumer c1 = new Consumer(clerk);
        c1.setName("消费者");
        p1.start();
        c1.start();
    }
}
class Clerk{
    private int productCount=0;
    public synchronized void consumeProduct() {
        if(productCount>0){
            System.out.println(Thread.currentThread().getName()+":开始消费第"+productCount+"个产品");
            productCount--;
            notify();
        }else{
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public synchronized void produceProduct() {
        if(productCount<20){
            productCount++;
            System.out.println(Thread.currentThread().getName()+":开始生产第"+productCount+"个产品");
            notify();
        }else{
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class Producer extends Thread{
    private Clerk clerk;
    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        System.out.println(getName()+":开始生产产品......");
        while(true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.produceProduct();
        }
    }
}
class Consumer extends Thread{
    private Clerk clerk;
    public Consumer(Clerk clerk){
        this.clerk=clerk;
    }
    @Override
    public void run() {
        System.out.println(getName()+":开始消费产品......");
        while(true){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consumeProduct();
        }
    }
}
  • 生产者和消费者会共享产品数量
  • 我们可以在售货员类中定义方法,当此时的生产数量未达到标准时,就会进行生产,然后会唤醒消费的进程,否则就会进入阻塞状态
  • 而当产品数量不足时,消费者就会唤醒生产进程,此时,自己进入阻塞状态


目录
相关文章
|
2天前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
33 14
|
5天前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
34 13
|
6天前
|
安全 Java 开发者
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
|
1月前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
109 17
|
2月前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
1月前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
2月前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
2月前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
2月前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
73 3
|
2月前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
221 2

热门文章

最新文章