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();
        }
    }
}
  • 生产者和消费者会共享产品数量
  • 我们可以在售货员类中定义方法,当此时的生产数量未达到标准时,就会进行生产,然后会唤醒消费的进程,否则就会进入阻塞状态
  • 而当产品数量不足时,消费者就会唤醒生产进程,此时,自己进入阻塞状态


目录
相关文章
|
4天前
|
安全 Java
深入理解 Java 多线程和并发工具类
【4月更文挑战第19天】本文探讨了Java多线程和并发工具类在实现高性能应用程序中的关键作用。通过继承`Thread`或实现`Runnable`创建线程,利用`Executors`管理线程池,以及使用`Semaphore`、`CountDownLatch`和`CyclicBarrier`进行线程同步。保证线程安全、实现线程协作和性能调优(如设置线程池大小、避免不必要同步)是重要环节。理解并恰当运用这些工具能提升程序效率和可靠性。
|
5天前
|
安全 Java
java多线程(一)(火车售票)
java多线程(一)(火车售票)
|
5天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
5天前
|
安全 Java 程序员
Java中的多线程并发编程实践
【4月更文挑战第18天】在现代软件开发中,为了提高程序性能和响应速度,经常需要利用多线程技术来实现并发执行。本文将深入探讨Java语言中的多线程机制,包括线程的创建、启动、同步以及线程池的使用等关键技术点。我们将通过具体代码实例,分析多线程编程的优势与挑战,并提出一系列优化策略来确保多线程环境下的程序稳定性和性能。
|
6天前
|
缓存 分布式计算 监控
Java并发编程:深入理解线程池
【4月更文挑战第17天】在Java并发编程中,线程池是一种非常重要的技术,它可以有效地管理和控制线程的执行,提高系统的性能和稳定性。本文将深入探讨Java线程池的工作原理,使用方法以及在实际开发中的应用场景,帮助读者更好地理解和使用Java线程池。
|
6天前
|
存储 安全 Java
Java中的容器,线程安全和线程不安全
Java中的容器,线程安全和线程不安全
15 1
|
6天前
|
Java 开发者
Java中多线程并发控制的实现与优化
【4月更文挑战第17天】 在现代软件开发中,多线程编程已成为提升应用性能和响应能力的关键手段。特别是在Java语言中,由于其平台无关性和强大的运行时环境,多线程技术的应用尤为广泛。本文将深入探讨Java多线程的并发控制机制,包括基本的同步方法、死锁问题以及高级并发工具如java.util.concurrent包的使用。通过分析多线程环境下的竞态条件、资源争夺和线程协调问题,我们提出了一系列实现和优化策略,旨在帮助开发者构建更加健壮、高效的多线程应用。
7 0
|
7天前
|
存储 缓存 安全
Java并发基础之互斥同步、非阻塞同步、指令重排与volatile
在Java中,多线程编程常常涉及到共享数据的访问,这时候就需要考虑线程安全问题。Java提供了多种机制来实现线程安全,其中包括互斥同步(Mutex Synchronization)、非阻塞同步(Non-blocking Synchronization)、以及volatile关键字等。 互斥同步(Mutex Synchronization) 互斥同步是一种基本的同步手段,它要求在任何时刻,只有一个线程可以执行某个方法或某个代码块,其他线程必须等待。Java中的synchronized关键字就是实现互斥同步的常用手段。当一个线程进入一个synchronized方法或代码块时,它需要先获得锁,如果
23 0
|
7天前
|
缓存 监控 Java
Java并发编程:线程池与任务调度
【4月更文挑战第16天】Java并发编程中,线程池和任务调度是核心概念,能提升系统性能和响应速度。线程池通过重用线程减少创建销毁开销,如`ThreadPoolExecutor`和`ScheduledThreadPoolExecutor`。任务调度允许立即或延迟执行任务,具有灵活性。最佳实践包括合理配置线程池大小、避免过度使用线程、及时关闭线程池和处理异常。掌握这些能有效管理并发任务,避免性能瓶颈。
|
8天前
|
设计模式 运维 安全
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第15天】在Java开发中,多线程编程是提升应用程序性能和响应能力的关键手段。然而,它伴随着诸多挑战,尤其是在保证线程安全的同时如何避免性能瓶颈。本文将探讨Java并发编程的核心概念,包括同步机制、锁优化、线程池使用以及并发集合等,旨在为开发者提供实用的线程安全策略和性能优化技巧。通过实例分析和最佳实践的分享,我们的目标是帮助读者构建既高效又可靠的多线程应用。