Java多线程

Java多线程,第1张

Java多线程 1、多线程与多进程

首先我们来了解一下什么是进程,什么是线程?

进程: 进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是 *** 作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程一般由程序,数据集合和进程控制块三部分组成。程序用于描述进程要完成的功能,是控制进程执行的指令集;数据集合是程序在执行时所需要的数据和工作区;程序控制块包含进程的描述信息和控制信息是进程存在的唯一标志。

线程: 线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID,当前指令指针PC,寄存器和堆栈组成。而进程由内存空间(代码,数据,进程空间,打开的文件)和一个或多个线程组成。

进程与线程的区别
  • 线程是程序执行的最小单位,而进程是 *** 作系统分配资源的最小单位;

  • 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线

  • 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信

    号等),某进程内的线程在其他进程不可见;

  • 调度和切换:线程上下文切换比进程上下文切换要快得多

多线程: *** 作系统能够同时运行多个任务

多进程:在同一个程序中有多个顺序流在执行

多进程与多线程的区别
  • 本质区别:每个进程都有属于自己的一套变量,而线程则共享数据。
  • 共享数据使得线程之间的通信比进程之间的通信更有效、更容易。
  • 在有些 *** 作系统中,与进程相比,线程更加轻量级,创建、撤销一个线程n比启动新的进程开销要小得多。

在java中想要实现多线程,有三种常用方式:继承Thread类、实现Runnable接口、实现Callable接口,然后与Future,线程池结合使用。

2、创建线程的方式 2.1、继承Thread类
class Main extends Thread {
    private String name;

    public Main(String name) {
        super(name);
        this.name = name;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
        for (int i = 0; i < 5; i++) {
            System.out.println("线程" + name + "运行 : " + i);
            try {
                sleep((int) (Math.random() * 10));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
    }

    public static void main(String[] args){
        Main mTh1=new Main("A");
        Main mTh2=new Main("B");
        mTh1.start();
        mTh2.start();
    }

}

这种方式就是继承Thread类,并实现run方法。调用start()方法执行当前线程。

观察上面的结果我们发现两次执行的结果不一样?==这是因为我们调用start()方法后线程没有立即执行,而是进入了就绪态,具体什么时侯执行需要等待 *** 作系统的调度。==具体细节我们放在线程状态讨论。

2.2、实现Runnable接口
class Main implements Runnable {
    private String name;

    Main(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
        for (int i = 0; i < 5; i++) {
            System.out.println("线程" + name + "运行 : " + i);
            try {
                Thread.sleep((int) (Math.random() * 10));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(new Main("A"));
        Thread thread2 = new Thread(new Main("B"));
        thread1.start();
        thread2.start();
    }
}

Main类通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个约定。所有的多线程代码都在run()方法里面。Thread类实际上也是实现了Runnable接口的类。

通过该方法实现run()方法的类,在创建线程的时候需要先用该类构建Thread类然后在调用start()方法。那么就会有人有疑问了,继承Thread方法是不是比实现Runnable接口方便一些,为什么要提供两种创建线程的方式?

首先所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口或者Callable来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

3、线程状态

线程状态细分为七种:(java核心技术中说明是6种状态,把就绪跟运行中统一称为可运行,我们这里分开说明)

  • 新建(New):新创建了一个线程对象,但还没有开始执行,即还没有调用start()方法。
  • 就绪(Runnable):线程对象调用了start()方法,但 *** 作系统还没有给该线程提供具体的运行时间,即还没有被线程调度算法调度。
  • 运行中(Running):线程被调度,获取CPU的使用权,开始运行。
  • 阻塞(Blocked):线程阻塞于锁。
  • 等待(Waiting):线程等待其他线程进入一个特定的状态或者特定的动作。
  • 计时等待(Timed waiting):在等待的基础上线程在到达指定时间后自行返回。
  • 终止(Terminated):线程执行结束,或者发生意外使线程终止。

3.1、新建状态

new了一个Thread线程类,此时该线程就进入了新建状态。此时线程还没有开始执行,程序还没有开始执行run方法中的代码。

3.2、就绪状态
  • 线程对象一旦调用start()方法,且调度程序没有挑选到该线程,此时线程就处于就绪状态。
  • 当前线程执行sleep()方法结束,进入就绪状态。
  • 当前线程时间片用完,调用线程的yield()方法进入就绪状态。
  • 线程拿到对象锁后进入就绪状态。
  • 脱离就绪状态只能等待调度程序挑选到该线程!
3.3、运行中状态

线程调度程序从处于就绪状态的线程中挑选一个进程,此时被挑选到线程转变为运行中状态,这是线程进入此状态的唯一方式。

3.4、阻塞状态

当一个线程试图获取一个内部的对象锁,而这个锁正在被其他线程占有,该线程就会被阻塞。

当其他线程都释放了这个锁且线程调度程序运行改线程持有这个锁时,它变为非阻塞状态(就绪状态),等待调度程序调度。

3.5、等待状态

当一个线程等待另一个线程通知调度器出现一个条件(也就是另一个线程执行某个特殊动作),这个线程会进入等待状态。如调用Object.wait(), Thread.join()或者等待Lock或Condition时,会进入等待状态。

这种状态需要等待被显示的唤醒,否则会无限期等待。

3.6、计时等待

有些方法在等待的时候会传入参数,调用这些方法会进入计时等待方法,到达一定的时间会自行唤醒。

带有超时参数的方法有:Thread.sleep()、Object.wait()、Thread.join()、Lock.tryLock()以及Condition.await()。

3.7、终止状态
  • run()方法正常退出,线程自然终止。
  • 以为出现了一个未处理的异常而终止了run()方法,线程意外终止。
4、线程属性 4.1、中断线程

当线程中的run方法执行到方法体最后一条语句后在返回return语句返回时,或者出现了未捕获的异常时线程将终止。在java的早期版本中有一个stop()方法可以强制终止线程,但是这个方法现在被弃用了。

除了stop()方法外没有办法终止一个线程。==但是我们可以通过interrupt方法可以用来请求终止一个线程。==当我们对一个线程调用interrupt方法时,就会设置线程的中断状态。这是每个线程都有的boolean标志。每个线程都应该不时地检查这个标志来判断线程是否被中断。我们通过以下方法来判断线程是否处于中断状态:

Thread.currentThread().isInterrupted() //true or false

如果线程被阻塞则无法通过上述方式判定中断状态。这时候是通过InterruptedException异常来判定,如果调用上述方法抛出了InterruptedException异常,说明线程在阻塞的时候中断了。==普遍情况下发生中断我们需要终止线程。==所以大多时候我们采用如下形式编写run方法:

Runnable r = () -> {
    try {
        while (!Thread.currentThread().isInterrupted() && ...) {
            //...
        } catch (InterruptedException e) {
            //进程在sleep或者wait时发生中断。
        } finally {
            //清除资源
        }
        //退出run方法
    }
}

如果我们在每次工作迭代后都调用sleep()等其他中断方法,那么isInterrupted()就没有必要了。在设置中断状态之后调用sleep并不会休眠,而是清除中断状态并抛出InterruptedException异常。所以如果循环调用了sleep等终端方法,直接捕获InterruptedException异常即可:

Runnable r = () -> {
    try {
        while (...) {
            //...
            Thread.sleep(...);
        } catch (InterruptedException e) {
            //进程在sleep时发生中断。
        } finally {
            //清除资源
        }
        //退出run方法
    }
}

下面是对中断线程相关方法的说明:

void interrupt();//向线程发送中断请求。线程的中断状态设置为true。如果当前线程被一个sleep调用阻塞,则抛出InterruptedException异常。
static boolean interrupted();//测试当前线程是否被中断,注意是一个静态方法。调用该方法会将当前线程的中断状态改为false。
boolean isInterrupted();//测试线程是否中断,不会改变中断状态
static Thread currentThread();//获取当前正在执行的Thread对象。
4.2、守护线程

在线程调用start方法之前调用setDaemon(boolean)方法来标识该线程为守护线程。

所谓守护线程就是为其他线程提供服务,例如我们的计时器线程,以及清空过时缓存线程。当虚拟机中只剩下守护线程是就没必要进行下去,虚拟机会退出。

class Main {
    public static void main(String[] args) {
        Thread time = new Thread(() -> {
            try {
                for (int i = 0; i >=0 ; i++) {
                    System.out.println("程序已经执行了" + i + "秒");
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        time.setDaemon(true);

        Thread thread = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
            for (int i = 0; i < 5; i++) {
                System.out.println("线程运行 : " + i);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
        });
        time.start();
        thread.start();
    }
}

time就是我们的守护线程,来计时。当thread线程执行结束的时候程序就结束了。

4.3、线程名

我们线程有默认的名字如:Thread-1,当然我们可以调用setName方法给线程设置名字。

4.4、线程优先级

在java中,每一个线程都有一个优先级。默认情况下已个线程会继承构造该线程的那个线程的优先级。

我们也可以使用setPriority方法提高或降低任何一个线程的优先级。

这是Thread里面有的三个优先级:

public final static int MIN_PRIORITY = 1;


public final static int NORM_PRIORITY = 5;


public final static int MAX_PRIORITY = 10;

我们赋值可以给定1~10的任意值而不是只有这三个。

在没有 *** 作系统线程的早期java版本中,设置优先级可能很有用,不过现在不要使用线程优先级了。

5、同步

在我们大多数开发多线程应用的时候,两个或者两个以上的线程需要共享对同一个数据的存取。如果两个线程同时存取同一个对象,并且每个线程分别调用一个修改该对象状态的方法,会发生什么呢?可以想见这两个线程对对象的修改会相互覆盖,覆盖的顺序取决于访问对象的顺序,这样可能导致对象被破坏。这种情况我们通常称为竟态条件。

我们先列出一些方法方便我们后续的例子:

  • sleep(): 强迫一个线程睡眠N毫秒。
  • isAlive(): 判断一个线程是否存活。
  • join(): 等待线程终止。
  • activeCount(): 程序中活跃的线程数。
  • enumerate(): 枚举程序中的线程。
  • currentThread(): 得到当前线程。
  • isDaemon(): 一个线程是否为守护线程。
  • setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
  • setName(): 为线程设置一个名称。
  • wait(): 强迫一个线程等待。
  • notify(): 通知一个线程继续运行。
  • setPriority(): 设置一个线程的优先级
5.1、竞态条件的例子
package example;

import pojo.Balance;

import java.util.ArrayList;
import java.util.List;


public class BalanceExample {
    private List accounts;

    BalanceExample() {
        accounts = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            accounts.add(new Balance("账户" + i, 1000.0));
        }
    }

    private double getTotal() {
        double result = 0.0;
        for (Balance account : accounts) {
            result += account.getMoney();
        }
        return result;
    }

    public void transfer(int from, int to, double money) {
        Balance balanceFrom = accounts.get(from);
        Balance balanceTo = accounts.get(to);
        if (!Balance.transfer(balanceFrom, balanceTo, money)) return;
        System.out.print(Thread.currentThread().getName());
        System.out.printf("%s转出%10.2f元到%s,总金额%10.2fn" , balanceFrom.getName(), money, balanceTo.getName(), getTotal());
    }

    public static void main(String[] args) throws InterruptedException {
        BalanceExample balanceExample = new BalanceExample();
        for (int i = 0; i < 10; i++) {
            int from = i;
            new Thread(() -> {
                try {
                    while (true) {
                        int to = (int) (10 * Math.random());
                        double money = 1000 * Math.random();
                        balanceExample.transfer(from, to, money);
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {

                }
            }).start();
        }
    }
}
package pojo;


public class Balance {
    private String name;
    private double money;

    public Balance(String name, double money) {
        this.name = name;
        this.money = money;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }

    public static boolean transfer(Balance from, Balance to, double money) {
        if (from.getMoney() < money) return false;
        from.setMoney(from.getMoney() - money);
        to.setMoney(to.getMoney() + money);
        return true;
    }
}

这个程序是模拟同时进行用户之间的转账,总金额为一万元。在程序执行一段时间后总金额变为了9297.59,这就是我们之前所说的竞态条件。多个线程访问同一个对象造成对象破坏。

如果现在有两个线程A、B同时给账户1进行转账 *** 作。也就是有两个线程同时执行了指令

to.setMoney(to.getMoney() + money);

这条指令分为三部:

  • 将to.getMoney()的值加载到寄存器…1
  • 增加money…2
  • 将结果调用to.setMoney()赋给to…3

如果线程A在执行1、2步后运行权被抢占。此时线程B被唤醒,更新了to对象的状态。然后线程A被唤醒完成第三步。此时线程A会抹去线程B对对象to的 *** 作。

这样一来我们就出问题了,总金额变少了。

5.2、锁对象

JVM把锁分为内部锁和显示锁两种。内部所通过synchronized关键字实现;显示锁通过java.concurrent.locks.lock接口实现类实现的。

**锁的作用:**锁可以实现对共享数据的安全访问,保障线程的原子性,可见性,与有序性。

  • 锁是通过互斥保障原子性,一个锁只能被一个线程持有,这就保证临界区的代码一次只能被一个线程执行,使得 *** 作不可分割,保证原子性。
  • 可见性的保障是通过写线程冲刷处理器的缓存和读线程刷新处理器缓存这两个动作实现的。在java中,锁的获得隐含着刷新处理器缓存的动作,锁的释放隐含着冲刷处理器缓存的动作。保证写线程对数据的修改,第一时间推送到处理器的高速缓存中,保证读线程第一时间可见。
  • 锁能够保证有序性,写线程在临界区所执行的在读线程所执行的临界区看来像是完全按照源码顺序执行的。
5.2.1、Lock/Condition
Lock

使用ReentrantLock这种方式保护代码块的基本结构如下:

myLock.lock();//一个ReentrantLock对象
try {
    //...
} finally {
    myLock.unlock();//释放锁
}

这个结构保证无论何时只有一个线程进入临界区。一旦有一个线程获取到了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock语句的时候,会停在这里等待一个线程释放锁对象。

如果我们对上面的例子的transfer方法加上锁:

public void transfer(int from, int to, double money) {
    lock.lock();
    try {
        Balance balanceFrom = accounts.get(from);
        Balance balanceTo = accounts.get(to);
        if (!Balance.transfer(balanceFrom, balanceTo, money)) return;
        System.out.print(Thread.currentThread().getName());
        System.out.printf("%s转出%10.2f元到%s,总金额%10.2fn" , balanceFrom.getName(), money, balanceTo.getName(), getTotal());
    } finally {
        lock.unlock();
    }
}

观察结果:

我们发现总金额一直正确。

再加上锁之后,5.1中发生的情况会变为:线程A执行结束前被抢占,线程B被唤醒但是因为锁还在线程A的手中,所以线程B因为获取不到锁而等待线程A释放锁。这样5.1的情况就不会发生了。

Condition

通常线程进入临界区后发现只有满足了某个条件之后才能执行。我们可以使用条件对象来管理已经获取了锁却因为条件不满足而不能有用工作的线程。

还拿我们转账的例子,只有在资金足够的情况下才能转账:

public void transfer(int from, int to, double money) {
    lock.lock();
    try {
        Balance balanceFrom = accounts.get(from);
        Balance balanceTo = accounts.get(to);
        while (balanceFrom.getMoney() < money)
            condition.await();
        if (!Balance.transfer(balanceFrom, balanceTo, money)) return;
        System.out.print(Thread.currentThread().getName());
        System.out.printf("%s转出%10.2f元到%s,总金额%10.2fn" , balanceFrom.getName(), money, balanceTo.getName(), getTotal());
        condition.signalAll();
    } catch (InterruptedException e) {
    } finally {
        lock.unlock();
    }
}

当transfer方法发现资金不足时会调用condition.await()方法放弃锁。这样使其他线程执行。当另一个线程完成转账之后调用condition.signalAll()方法激活这个条件的所有线程。

5.2.2、synchronized关键字

Lock允许程序员充分控制锁定。不过大多数情况下,你并不需要那样控制。从1.0版本开始,Java的每一个对象都拥有一个内部锁。如果一个方法声明时有synchronized关键字,那么对象的锁将保护整个方法。也就是说线程想要调用这个方法,必须获得内部对象锁。相比于Lock我们只需将方法声明为synchronized即可。

public synchronized void transfer(int from, int to, double money) {
    Balance balanceFrom = accounts.get(from);
    Balance balanceTo = accounts.get(to);
    while (balanceFrom.getMoney() < money)
        wait();
    if (!Balance.transfer(balanceFrom, balanceTo, money)) return;
    System.out.print(Thread.currentThread().getName());
    System.out.printf("%s转出%10.2f元到%s,总金额%10.2fn" , balanceFrom.getName(), money, balanceTo.getName(), getTotal());
	notifyAll();
}

相比于ReentrantLock,我们代码简介了许多。

我们也可以将静态方法声明为synchronized。

内部锁和条件存在一些限制:

  • 不能中断一个正在尝试获取锁的线程。
  • 不能指定尝试获取锁时的超时时间。
  • 每个锁仅有一个条件可能不够。
5.2.3、我们在真实开发中使用哪一种做法
  • 最好既不是使用Lock/Condition,也不适用synchronized关键字。在许多情况下我们使用concurrent包中的某种机制,他会为你处理所有的锁定。
  • 如果synchronized关键字适合你的程序,那么尽量使用此种做法,即可以减小代码量也可以减少出错的概率。
  • 如果特别需要Lock/Condition结构提供的额外能力,则使用Lock/Condition。
5.2.4、同步块
public void transfer(int from, int to, double money) {
    Balance balanceFrom = accounts.get(from);
    Balance balanceTo = accounts.get(to);
    synchronized (this) {
        if (!Balance.transfer(balanceFrom, balanceTo, money)) return;
        System.out.print(Thread.currentThread().getName());
        System.out.printf("%s转出%10.2f元到%s,总金额%10.2fn" , balanceFrom.getName(), money, balanceTo.getName(), getTotal());
    }
}

我们刚才说每一个对象都有一个内部锁,线程可以通过调用同步方法来获取锁。我们还有另一种机制就是同步块,如上所示。

5.3、volatile

volatile关键字为实例字段的同步访问提供了一种免锁机制。如果声明一个字段为volatile,那么编译器和虚拟机就知道该字段可能被线程并发更新。也就是说当一个字段被volatile修饰,那么编译器会插入适当的代码,以确保如果一个线程对该字段进行了修改,这个修改对读取这个变量的所有线程都可见。

package example;

import java.text.SimpleDateFormat;
import java.util.Date;


public class Main {

    public static void main(String[] args) {
        PrintString  printString = new PrintString();
        //打印字符串的方法
        new Thread(printString::printStringMethod).start();

        //main线程睡眠1000毫秒
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("在main线程中修改打印标志");
        printString.setContinuePrint(false);
        //在main修改玩打印标志后,子线程是否结束打印。

    }
    static class PrintString{
        private volatile boolean continuePrint = true;
        public void printStringMethod(){
            while (continuePrint){
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        public void setContinuePrint(boolean continuePrint) {
            this.continuePrint = continuePrint;
        }
    }

}

而如果flag变量没有用volatile修饰,在main中修改的标志就不会更新到主内存。

volatile 与 synchronized比较
  • volatile 关键字是线程同步的轻量级实现,所以volatile性能比synchronized更好,volatile只能修饰变量,而synchronized可以修饰方法,代码块。随着JDK新版本的发布,synchronized的执行效率也有了很大的提升。在开发中我们使用synchronized的比例较大。

  • 多线程访问volatile变量不会发生阻塞,而synchronized可能会阻塞。

  • volatile能保证数据的可见性,不能保证原子性。synchronized都可以保证,会数据同步。

  • volatile解决的是变量在多个线程之间的可见性;synchronized解决多个线程之间访问公共资源的同步性。

5.4、死锁

假如我们现在只有两个账户,且有下述情况:

  • 账户1:200元
  • 账户2:300元
  • 线程1:从账户1向账户2转账300元
  • 线程2:从账户2向账户1转账400元

这时候就会出现由于两个账户自己不足而一直处于阻塞状态,两个线程无限等待下去。

为什么我们之前的例子没有出现死锁的状态,那是因为我们将转账的金额设置成为了1000元之内的金额,如果去掉限制,那么很快就会发生死锁:

在程序里面我增加了一个守护线程,每隔一秒记一次时,可以看出,8秒之后转账线程开始没有任何消息传出,说明发生了死锁。

在Java中没有任何东西可以避免或打破这种死锁。我们必须仔细设计程序,保证不会出现死锁。

5.5、总结
  • 线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。
  • 线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法
  • 对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
  • 对于同步,要时刻清醒在哪个对象上同步,这是关键。
  • 编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子” *** 作做出分析,并保证原子 *** 作期间别的线程无法访问竞争资源。
  • 当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
  • 死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使,呵呵。但是,一旦程序发生死锁,程序将死掉。
6、线程通信方式

线程间通信机制就是wait/notify机制,实际上我们在之前的例子中已经用过这种机制。

具体我们可以参考这个博客:https://blog.csdn.net/justloveyou_/article/details/54929949

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5564102.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-14
下一篇 2022-12-14

发表评论

登录后才能评论

评论列表(0条)

保存