Java多线程

简介: Java多线程


文章目录

一、初始多线程

线程和进程

进程是程序向操作系统申请资源的基本单位,线程是进程中可独立执行的最小单位。通常一个进程可以包含多个线程,至少包含一个线程,同一个进程中所有线程共享该进程的资源。

  • 1、根本区别
      进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
  • 2、资源开销
      每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间(在Java中,线程之间是共享堆区和方法区的资源,但是它们拥有着独立的栈区),每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
  • 3、包含关系
      一个进程里可以包含多个线程。
  • 4、内存分配
      同一进程的线程共享本进程的地址空间和资源,而线程之间的地址空间和资源是相互独立的。
  • 5、影响关系
      一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
  • 6、执行过程
      每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。

线程并发、并行、串行

  • 并发
      多个任务在同一个CPU核上,按细分的时间片轮流执行,从逻辑上来看任务是同时执行。
  • 并行
      单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。
  • 串行
      有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。
      

二、创建线程

这里讲解创建线程的三种方式。

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

继承Thread类

使用方法:

  1. 创建一个继承Thread类的类。
  2. 重写Thread类中的run()方法。
  3. 用该类创建线程。
  4. 通过start()方法启动线程。

代码如下:

//创建线程的第一种方式:编写继承Thread的线程类。
package thread;
class myThread extends Thread{
    public void run(){
        for(int i = 0; i < 1000; i++){
            System.out.println("分支线程->" + i);
        }
    }
}
public class ThreadTest01 {
    public static void main(String[] args) {
        myThread t = new myThread();
        t.start();
    }
}

这种方式现在不是很推荐,好的Java程序应该将并行运行的任务与运行机制解耦合。

实现Runnable接口

使用方法:

  1. 创建一个实现(implements)了Runnable接口的类。
  2. 实现Runnable接口中的run()方法。
  3. 创建Thread类对象,将实现了Runnable接口的类对象通过参数的形式传进去。
  4. 通过strat()启动线程。

代码如下:

public class ThreadTest02 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunable());
        t.start();
        }
}
class MyRunable implements Runnable{
    public void run(){
        for(int i = 0; i < 1000; i++){
            System.out.println("分支线程->" + i);
        }
    }
}

该类较常用,因为Java程序要面向接口编程,实现了接口的类还可以继承其他类,而继承了类的类,不能在继承其他类。

实现Callable接口

使用步骤:

  1. 创建实现Callable接口的类;
  2. 以Callable接口的实现类为参数,创建FutureTask对象;
  3. 将FutureTask作为参数创建Thread对象;
  4. 调用线程对象的start()方法。
    代码如下:
public class ThreadTest09 {
    public static void main(String[] args) {
        FutureTask futureTask = new FutureTask(new myCallable());
        Thread thread = new Thread(futureTask);
        thread.start();
    }
}
class myCallable implements Callable<Integer>{
    public Integer call(){
        System.out.println("Call----->start");
        try {
            Thread.sleep(1000*3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Call----->end");
        return 1000;
    }
}

使用该方法创建线程时,核心方法是call(),与其他方式最大的不同是:该方法有返回值,其返回值类型就是Callable接口中泛型对应的类型。我们通过FutureTask对象调用get()方法,获得该线程的返回值。但是这个方法也有相应的缺点,调用get()方法后,在主线程获得该线程的返回值前,主线程会进入阻塞状态。

注意:调用get()方法会抛出两个异常:InterruptedExceptionExecutionException

三、线程的生命周期

线程的6种状态

Java中有如下6种状态

  • New(新创建)
  • Runnable(可运行)
  • Blocked(阻塞)
  • Waiting(等待)
  • Timed waiting(计时等待)
  • Terminated(被终止)
  1. 新建状态 :
    使用new关键字创建一个thread对象,刚刚创建出的这个线程就处于新建状态。在这个状态的线程没有与操作系真正的线程产生关联,仅仅是一个java对象。
  2. 可运行:
    正在进行运行的线程,只有处于可运行状态的线程才会得到cpu资源。
    可运行状态可以分为两类理解:就绪状态和运行状态:
    运行状态:拥有抢夺CPU时间片的能力,但还未抢夺成功。
    就绪状态:成功抢夺CPU时间片。
  3. 阻塞 :
    在可运行阶段争抢锁失败的线程就会从可运行—>阻塞
  4. 等待 :
    可运行状态争抢锁成功,但是资源不满足,主动放弃锁(调用wait()方法)。条件满足后再恢复可运行状态(调用notiy()方法)。
  5. 有时限等待:
    类似于等待,不过区别在于有一个等待的时间,到达等待时间后或者调用notiy(),都能恢复为可运行状态。
    有两种方式可以进入有时限等待:wait(Long)和sleep(Long)
  6. 终结 :代码全部执行完毕后,会进入到终结状态,释放所有的资源。

线程状态图

四、中断线程

中断线程作用

Java中可以调用interrupt()向线程发出中断请求,从而使线程中断,中断不会对处于运行状态的线程产生影响,但是可以打断线程的阻塞状态(wait(), sleep()),使其进入就绪状态,并抛出InterruptedException异常。

测试代码如下:

public class ThreadTest06 {
    public static void main(String[] args) {
        Thread t = new Thread(new myRunnable2());
        t.start();
        t.interrupt();
    }
}
class myRunnable2 implements Runnable{
    public void run(){
        System.out.println(Thread.currentThread().getName() + "------>start");
        try {
            Thread.sleep(1000*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "------>end");
    }
}

执行结果如下:

中断线程原理

每一个线程都有一个boolean类型的中断状态,该中断状态初始为false。当一个线程调用interrupted方法时,线程的中断状态被置为true。对于一个运行状态的线程来说,没有什么影响,但是对于阻塞状态的线程来说,遇到中断状态时(true),会抛出一个InterruptedException,同时将中断状态重新置为false

测试代码如下:

public class ThreadTest06 {
    public static void main(String[] args) {
        Thread t = new Thread(new myRunnable2());
        t.start();
    }
}
class myRunnable2 implements Runnable{
    public void run(){
        System.out.println(Thread.currentThread().getName() + "------>start");
        System.out.println("中断前:" + Thread.currentThread().isInterrupted());
        Thread.currentThread().interrupt();
        System.out.println("中断后(遇到堵塞之前):" + Thread.currentThread().isInterrupted());
        System.out.println();
        try {
            Thread.sleep(1000*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("中断后(遇到堵塞之后):" + Thread.currentThread().isInterrupted());
        System.out.println(Thread.currentThread().getName() + "------>end");
    }
}

运行结果如下:

关于中断的相关方法

  • public void interrupt()
    向线程发出中断请求,线程的中断状态被置为true。
  • public static boolean interrupted()
    测试当前线程(正在执行的线程)是否被中断,并将中断状态重置为false。
  • public boolean isInterrupted()
    测试线程的中断状态,但是并不会重置中断状态。

五、线程的属性

这里会对线程优先级和守护线程进行说明。

线程优先级

Java线程的优先级属性本质上只是一个给线程调度器的提示信息,以便于线程调度器决定优先调度哪些线程运行,也可以理解为,优先级高的线程更容易抢夺到CPU时间片。每个线程的优先级都在1到10之间,1的优先级为最低,10的优先级为最高,在默认情况下优先级都是Thread.NORM_PRIORITY(常数 5)。

  虽然开发者可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。


常用方法和属性

  • static int MIN_PRIORITY
    线程的最小优先级,最小优先级为1。
  • static int MAX_PRIORITY
    线程的最大优先级,最大优先级为10。
  • static int NORM_PRIORITY
    线程的默认优先级,默认优先级为5。
  • public final int getPriority()
    获得线程的优先级。
  • public static void yield()
    让当前线程处于让步状态,放弃CPU时间片,回到就绪状态,重新抢夺CPU时间片。
  • public final void setPriority(int newPriority)
    设置线程的优先级,但是必须在允许的范围内。

守护线程

Java 中的线程分为两种:守护线程和用户线程。任何线程都可以设置为守护线程和用户线程,通过方法setDaemon(true)可以把该线程设置为守护线程,反之则为用户线程。

  用户线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程。

  守护线程:运行在后台,为其他前台线程服务,比如垃圾回收线程,JIT(编译器)线程就可以理解为守护线程。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作。

  守护线程应该永远不去访问固有资源 ,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

注意事项:

  • setDaemon(true)必须在start()方法前执行,否则会抛出IllegalThreadStateException。
  • 在守护线程中产生的新线程也是守护线程。
  • 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑。
  • 守护线程中不能依靠finally块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作,所以守护线程中的finally语句块可能无法被执行。

相关方法

public final void setDaemon(boolean on)

将线程设置为守护线程。

六、线程同步

  • 同步
      多个线程访问共享资源时,只有当一个线程访问结束后其它线程才能继续方法。
  • 异步
     多个线程即使在访问共同资源时也互不影响,各自完成各自的任务。

线程竞争产生的风险

当两个线程存取相同对象,并且每一个线程都调用了修改线程状态的方法,将会发生什么呢?

下面用代码演示:

首先创建一个银行账户类,如下:

public class Account {
    //账号
    private String actno;
    //余额
    private int banlance;
    //构造器
    public Account() {
    }
    public Account(String actno, int banlance) {
        this.actno = actno;
        this.banlance = banlance;
    }
    //setter and getter
    public void setBanlance(int banlance) {
        this.banlance = banlance;
    }
    public int getBanlance() {
        return banlance;
    }
    //取钱操作
    public void takeOut(Account act, int takeOut){
        int newBanlance = act.getBanlance() - takeOut;
        //这里sleep()只是为了让效果更加明显
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        act.setBanlance(newBanlance);
        System.out.println( Thread.currentThread().getName() + "取出:" + takeOut + "   剩余:" + newBanlance);
    }
}

然后我们实现一个Runnable类,如下:

class myRunnable implements Runnable{
  //银行账户对象
    Account act;
    //要取出的金额
    int takeOut;
    //构造器
    public myRunnable(){
    }
    public myRunnable(Account act, int takeOut){
        this.act = act;
        this.takeOut = takeOut;
    }
    public void run(){
        act.takeOut(act,takeOut);
    }
}

然后我们用两个线程同时对一个账户进行取钱操作,如下:

class Test{
    public static void main(String[] args) {
        Account act = new Account("Joken",10000);
        //去五次钱,每次取五千
        Thread t1 = new Thread(new myRunnable(act,5000));
        Thread t2 = new Thread(new myRunnable(act,5000));
        t1.start();
        t2.start();
    }
}

程序运行结果如下:

取了两次钱,但是余额仍然还有5000。

对产生风险的解释

产生以上问题的原因在于:act.takeOut(act,takeOut)并不是一个原子操作

这条指令会被分解为:

  1. int newBanlance = act.getBanlance() - takeOut;
    计算出处取钱后剩余的余额。
  2. act.setBanlance(newBanlance);
    更新余额到对象账户种。
  3. 打印账户余额信息。

注意: 在这里我并未将每一个操作都分解为原子操作,因为这里的样例线程较少,操作也很少,为了方便讲解所以将风险忽略,但是在实际开发中并不能忽略。

现在,线程一执行完了第一步,然后他被剥夺了CPU执行权,线程二又开始执行第一步,虽然线程一取了5000元,但是还没来得及更新,所以线程二在执行时,账户对象的余额仍然是10000元,这样就产生了错误。

synchronized关键字实现线程同步

  • synchronized关键字是用来控制线程同步的。在多线程的环境下,synchronized控制的代码段不被多个线程同时执行,以达到保证并发安全的效果。
  • synchronized关键字实现的锁称为内部锁,内部锁是一种排他锁,能够保证原子性、可见性和有序性。之所以被称为内部锁,是因为线程对内部锁的申请与释放的动作都是由Java虚拟机负责实现的,开发者看不到这个锁的获取和释放过程。

将上部分代码通过synchronized改进,如下:

public void takeOut(Account act, int takeOut){
         synchronized (this){ 
            int newBanlance = act.getBanlance() - takeOut;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            act.setBanlance(newBanlance);
            System.out.println( Thread.currentThread().getName() + "取出:" + takeOut + "   剩余:" + newBanlance);
        }
 }

对以上代码的解释:

  1. synchronized (this){ … },意味着想要执行花括号中的代码,首先要在锁池中找到this的对象锁,找到锁后该线程才会进入可运行状态,没找到锁他就会进入阻塞状态,直到它在锁池中找到这个对象锁。(其它线程将这个锁释放)
  2. 两个线程同时对一个账户对象进行取钱操作,其中一个线程会先到账户对象锁,另一个线程找不到锁就会进入阻塞状态,当获得对象锁的线程将所有指令完成之后就会归还账户对象锁,处于阻塞状态的线程就会进入可运行状态。

**synchronized的三种用法 **

用法一:

synchronized(共享对象){
    //同步代码
}

用法二:

synchronized 加在实例方法上,锁的是this,同步整个方法体。

public synchronized void test(){......}

synchronized 加在静态方法上,找的是类锁,一个类无论new几个对象都只有一个类锁。

用法三:

public static synchronized void test(){......}

七、死锁

死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

死锁样例:

首先创建两个线程类,如下:

class myThread extends Thread{
    Object o1;
    Object o2;
    public myThread(Object o1, Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run(){
        synchronized (o1){
            System.out.println(Thread.currentThread().getName() + "拿到o1的锁,准备拿o2的锁");
            synchronized (o2){
                System.out.println(Thread.currentThread().getName() + "拿到o2的锁");
            }
        }
    }
}
class myThread2 extends Thread{
    Object o1;
    Object o2;
    public myThread2(Object o1, Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run(){
        synchronized (o2){
            System.out.println(Thread.currentThread().getName() + "拿到o2的锁,准备拿o1的锁");
            synchronized (o1){
                System.out.println(Thread.currentThread().getName() + "拿到o1的锁");
            }
        }
    } 

创建这两个线程类对象,并传入相同对象,如下:

public class deadLock {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();
        myThread t1 = new myThread(o1,o2);
        myThread2 t2 = new myThread2(o1,o2);
        t1.start();
        t2.start();
    }
}

运行结果如下:

八、简单实现生产消费者问题

生产消费者问题是多线程经典问题,其中生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据

相关方法

  • void notify()
    随机选择在该对象上调用wait方法的线程,解除其阻塞状态。该方法只能在一个同步方法或同步代码块中调用。如果当前线程不是该对象锁的持有者,该方法将会抛出一个IllegalMonitorStateException
  • void notifyAll()
    解除在该对象上调用wait方法的全部线程,解除其阻塞状态。该方法只能在一个同步方法或同步代码块中调用。如果当前线程不是该对象锁的持有者,该方法将会抛出一个IllegalMonitorStateException
  • void wait()
    使当前线程进入阻塞状态,并释放该对象的锁,直到它被通知(nitify),并且在调用时抛出InterruptedException,如果当前线程不是该对象锁的持有者,该方法将会抛出一个IllegalMonitorStateException

代码实现

public class ThreadApplication {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        ConsumerThread consumer = new ConsumerThread(list);
        ProcuderThread procuder = new ProcuderThread(list);
        consumer.setName("消费者");
        procuder.setName("生产者");
        consumer.start();
        procuder.start();
    }
}
//消费者线程
class ConsumerThread extends Thread{
    List<String> list = new ArrayList<>();
    public ConsumerThread(List<String> list){
        this.list = list;
    }
    public void run(){
        while(true){
            synchronized (list){
                if(list.isEmpty()){
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "消费了" + list.remove(0));
                list.notify();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
//生产者线程
class ProcuderThread extends Thread{
    int i = 1;
    List<String> list = new ArrayList<>();
    public ProcuderThread(List<String> list){
        this.list = list;
    }
    public void run(){
        while(true){
            synchronized (list){
                if(list.size() == 10){
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                list.add("产品" + i);
                System.out.println(Thread.currentThread().getName() + "生产了" + list.get(list.size() - 1));
                i++;
                list.notify();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

编译结果如下:


相关文章
|
6天前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
17天前
|
监控 Java 调度
【Java学习】多线程&JUC万字超详解
本文详细介绍了多线程的概念和三种实现方式,还有一些常见的成员方法,CPU的调动方式,多线程的生命周期,还有线程安全问题,锁和死锁的概念,以及等待唤醒机制,阻塞队列,多线程的六种状态,线程池等
79 6
【Java学习】多线程&JUC万字超详解
|
2天前
|
Java
深入理解Java中的多线程编程
本文将探讨Java多线程编程的核心概念和技术,包括线程的创建与管理、同步机制以及并发工具类的应用。我们将通过实例分析,帮助读者更好地理解和应用Java多线程编程,提高程序的性能和响应能力。
15 4
|
10天前
|
Java 调度 开发者
Java并发编程:深入理解线程池
在Java的世界中,线程池是提升应用性能、实现高效并发处理的关键工具。本文将深入浅出地介绍线程池的核心概念、工作原理以及如何在实际应用中有效利用线程池来优化资源管理和任务调度。通过本文的学习,读者能够掌握线程池的基本使用技巧,并理解其背后的设计哲学。
|
1天前
|
安全 Java 调度
Java 并发编程中的线程安全和性能优化
本文将深入探讨Java并发编程中的关键概念,包括线程安全、同步机制以及性能优化。我们将从基础入手,逐步解析高级技术,并通过实例展示如何在实际开发中应用这些知识。阅读完本文后,读者将对如何在多线程环境中编写高效且安全的Java代码有一个全面的了解。
|
10天前
|
缓存 监控 Java
Java中的并发编程:理解并应用线程池
在Java的并发编程中,线程池是提高应用程序性能的关键工具。本文将深入探讨如何有效利用线程池来管理资源、提升效率和简化代码结构。我们将从基础概念出发,逐步介绍线程池的配置、使用场景以及最佳实践,帮助开发者更好地掌握并发编程的核心技巧。
|
6天前
|
Java 调度 开发者
Java中的多线程基础及其应用
【9月更文挑战第13天】本文将深入探讨Java中的多线程概念,从基本理论到实际应用,带你一步步了解如何有效使用多线程来提升程序的性能。我们将通过实际代码示例,展示如何在Java中创建和管理线程,以及如何利用线程池优化资源管理。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的见解和技巧,帮助你更好地理解和应用多线程编程。
|
11天前
|
缓存 监控 Java
java中线程池的使用
java中线程池的使用
|
11天前
|
算法 Java 数据处理
Java并发编程:解锁多线程的力量
在Java的世界里,掌握并发编程是提升应用性能和响应能力的关键。本文将深入浅出地探讨如何利用Java的多线程特性来优化程序执行效率,从基础的线程创建到高级的并发工具类使用,带领读者一步步解锁Java并发编程的奥秘。你将学习到如何避免常见的并发陷阱,并实际应用这些知识来解决现实世界的问题。让我们一起开启高效编码的旅程吧!
|
16天前
|
存储 Java 程序员
优化Java多线程应用:是创建Thread对象直接调用start()方法?还是用个变量调用?
这篇文章探讨了Java中两种创建和启动线程的方法,并分析了它们的区别。作者建议直接调用 `Thread` 对象的 `start()` 方法,而非保持强引用,以避免内存泄漏、简化线程生命周期管理,并减少不必要的线程控制。文章详细解释了这种方法在使用 `ThreadLocal` 时的优势,并提供了代码示例。作者洛小豆,文章来源于稀土掘金。