程序员最近都爱上了这个网站  程序员们快来瞅瞅吧!  it98k网:it98k.com

本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

并发编程-Java线程基础

发布于2021-05-29 19:49     阅读(532)     评论(0)     点赞(11)     收藏(1)


基础概念

程序、进程和线程

程序是指指令和数据的集合。

指令在CPU上执行。数据需要被加载到内存中。指令执行用到数据时,需要从内存中读取并拷贝对应数据到CPU的高速缓存中。

进程是程序的实例。

程序是静态的,进程是动态的。进程即为运行态的程序。进程运行时内部指令的执行需要CPU,所以会被分配CPU资源,指令执行时需要内存中的数据,所以需要被分配内存资源,内存中的数据可能从本地磁盘,硬盘上获取,也可能从网络中获取,所以需要IO资源。而资源的分配是操作系统完成的。进程只是资源的使用者,管理者。进程与进程之间是独立的,进程与进程之间的资源在没有得到进程的允许前是不共享的。

线程是进程的基本组成单位。

程序中包含大量的指令,这些指令一般不会只用来完成一个任务。而是完成多个任务。每个任务对应着由多条指令组成的指令流。进程运行中启动这些指令流,就是线程。即进程由多个线程组成。同一个进程下的多个线程共享这个进程下的资源。即线程执行内部指令时,需要获取其父进程分配到的CPU资源,线程中指令需要内存中数据时,需要获取进程其父进程获取到的内存资源,需要读取磁盘或网络数据时,需要获取其父进程分配到的IO资源。可以说进程时操作系统进行资源分配的基本单位,而线程是操作系统任务调度器的基本调度单位。

多进程和多线程

一个程序可以有多个进程实例,一个进程可以有多个线程。

举个例子:双击QQ.exe可以打开多个QQ客户端,每个客户端都可以登录不同的账号。其中一个QQ客户端可以打开两个聊天窗口,同时和好友A,好友B聊天。

其中QQ.exe就是程序,打开的多个QQ客户端就是多个进程实例,每个客户端登陆不同的账号而不会互相影响说明进程实例间的资源不会共享。某个QQ客户端打开两个聊天窗口,即为在一个进程中启动两个线程,这两个聊天窗口发出去的消息的发送人都是你,说明这两个线程共享了进程的内存中的数据。

并发和并行

并行(parallel)就是多个进程或线程 在同一时间点 同时执行。

并发(concurrent)在某个时间点只能运行一个进程或线程,但是很短的时间后(CPU时间片,windows好像是15ms),会剥夺当前执行进程或线程的执行权(CPU时间片),并给多个进程或线程抢占执行权,谁抢到,谁来执行,然后又过了一个很短的时间,继续这样的剥夺抢占模式。可以看出并发其实本质是串行执行的,但是从宏观来看,好像是并行执行的。所以叫并发。

我们知道不管是进程还是线程,其底层运行的是指令。而指令的执行需要CPU。所以要向实现进程或线程的并行必须要有多个CPU。即电脑常说的多核处理器。

对于单核处理器的电脑来说,只能实现并发执行。而多核处理器既可以并行,也可以并发。

同步和异步

同步:程序的继续运行需要等待某个结果

异步:程序的继续运行不依赖与某个结果

对于单线程程序而言,如果程序中间出现了一个耗时很长,但是不影响后面代码运行的操作时,单线程会阻塞在该操作处,即必须等待该耗时操作完成后,才能继续后面代码的执行。这个现象就是同步。这也是单线程效率低的原因。

多线程可以优化上面的程序,即将该程序中耗时长,但不影响后面执行的代码提取出来放到一个新线程中执行,其他代码继续保留在原线程中执行。此时我们可以发现原线程很快执行结束了,而新线程虽然执行很慢,但是没有影响程序整体的结果,所以给人的感官上,效率提升了。另外如下图中的耗时操作时IO操作,不使用CPU,所以单线程执行的话,在耗时操作处会浪费CPU,让CPU空闲,多线程执行的话,可以更好利用CPU。

  1. import java.io.*;
  2. public class Sync {
  3. public static void main(String[] args) throws InterruptedException {//JVM会为main方法创建一个main线程
  4. long start = System.currentTimeMillis();
  5. new Thread(()->{//在main线程中开启一个新线程,将耗时操作放到新线程中执行,即主线程不会阻塞在此处
  6. copy();//耗时操作
  7. long e = System.currentTimeMillis();
  8. System.out.println("异步耗时:"+(e-start)+"ms");
  9. }).start();
  10. doOtherThings();//不耗时操作继续保留在main线程中执行
  11. // long end = System.currentTimeMillis();
  12. // System.out.println("同步耗时:"+(end-start)+"ms");
  13. }
  14. private static void doOtherThings() {
  15. int sum = 0;
  16. for (int i = 0; i < 100000; i++) {
  17. sum+=i;
  18. }
  19. System.out.println(sum);
  20. }
  21. /**
  22. * copy方法用来实现文件的复制,文件复制操作不占用CPU,但是会阻塞当前线程
  23. */
  24. private static void copy() {
  25. try(FileInputStream fr = new FileInputStream("E:\\Desktop\\theme.mpg");
  26. FileOutputStream fo = new FileOutputStream("E:\\Desktop\\theme1.mpg")){
  27. byte[] bArr = new byte[32];
  28. int hasRead = 0;
  29. while ((hasRead=fr.read(bArr))>0){
  30. fo.write(bArr,0,hasRead);
  31. }
  32. } catch (IOException e) {
  33. e.printStackTrace();
  34. }
  35. }
  36. }

多线程的优势和缺点

多线程优势可以从两个对比来说:

  1. 1.线程对比进程:

进程是操作系统分配资源的基本单位,所以创建进程意味着配置资源,而线程的创建不需要配置资源,而是使用父进程的已分配的资源。所以线程的创建代价比进程小。

进程与进程之间不能共享内存。同一个进程下的线程可以共享内存。所以线程的通信比进程方便。

  1. 2.多线程对比单线程:

单线程运行效率低,对CPU的使用率低。多线程运行效率高,对CPU的使用率高。

多线程的缺点:

  1. 1并发执行的多线程需要频繁的上下文切换,造成操作系统资源的浪费。

我们知道并发本质是串行执行。但是每个线程在“串”上执行的时间很短,当线程的执行时间结束后,该线程中任务可能只执行了一点点,所以操作系统的内核需要记住该线程当前的状态,方便下次该线程再次获取到执行权时,可以在上一次的记录处继续执行。这样一个线程执行状态保存到线程再加载就被称为一次上下文切换。所以频繁的上下文切换需要耗费大量的系统内核资源去记录线程状态。而我们期望的是内核资源大部分用来计算,而不是保存状态。所以线程的数量应该不应该太多。上下文切换不能太频繁。

  1. 2并发执行的多线程可以共享内存中的数据,但是引发了共享数据的安全性。

比如A线程将获取到内存中共享数据0,并对其+1,还没来得及写回内存,A线程就被剥夺了CPU执行权,而此时B线程得到了CPU执行权,去内存中获取共享数据还是0,对其执行-1,并写回内存,此时B执行权被剥夺,A得到执行权,A执行未完成的步骤,将1写回内存。而理论上共享数据0,先经过A+1,后经过B-1,值应该还是0。而现在共享数据值确实1。

而多线程的缺点就是并发编程时需要思考解决的关键处。

线程的创建

继承Thread类

  1. public class MyThread extends Thread{// 1.创建java.lang.Thread类的子类
  2. int sum;
  3. @Override
  4. public void run() {// 2.重写父类Thread中的run方法
  5. for (int i = 1; i <= 100; i++) {// 3.run方法即为MyThread线程类的任务
  6. sum+=i;
  7. }
  8. System.out.println(sum);
  9. }
  10. }
  11. class Main{
  12. public static void main(String[] args) {
  13. MyThread thread = new MyThread();// 4.创建Thread子类对象
  14. thread.start();// 5.Thread子类对象调用start()方法启动线程,并运行run方法
  15. }
  16. }

继承Thread类创建线程的步骤,已经注释到如上代码中。需要注意的有两点:

1.Thread类的run()方法必须被子类重写:因为run方法是线程任务执行体,如果不重写run,那么创建的子类线程会执行Thread类的run方法,就没有意义了。

2.运行线程任务,必须使用start() ,而不能使用run():start()方法有两层含义:1.启动线程 2.执行run方法

如果直接thread.run(),则没有启动线程,还是在主线程上将thread当初普通对象,而不是线程,来调用普通方法run()。

而使用thread.start(),则先启动thread线程,当thread线程获得CPU时间片时,执行run方法中的任务执行体。

实现Runnable接口

  1. public class MyTask implements Runnable{// 1.创建Runnable接口的实现类
  2. int sum;
  3. @Override
  4. public void run() {// 2.在实现类对象中重写Runnable接口中定义的run方法
  5. for (int i = 1; i <= 100; i++) {// 3.在run方法中编写线程任务
  6. sum+=i;
  7. }
  8. System.out.println(sum);
  9. }
  10. }
  11. class Main{
  12. public static void main(String[] args) {
  13. MyTask task = new MyTask();// 4.创建Runnable接口实现类对象
  14. Thread thread = new Thread(task);// 5.将Runnable实现类对象作为new Thread(Runnbale target)构造器的参数
  15. thread.start();// 6.启动线程,并执行run方法
  16. }
  17. }

 通过继承Thread类创建线程,我们知道创建线程的有一个重要的步骤就是 定义线程任务执行体。

继承Thread类的方式将“定义任务执行体”的代码和线程创建的代码杂糅在一起了,这种方式其实是不符合面向对象编程的。

所以Thread类提供了一个构造器

  1. public Thread(Runnable target) {//带Runnable实现了对象参数的构造器
  2. init(null, target, "Thread-" + nextThreadNum(), 0);
  3. }
  1. private void init(ThreadGroup g, Runnable target, String name,
  2. long stackSize) {
  3. init(g, target, name, stackSize, null, true);
  4. }
  1. private void init(ThreadGroup g, Runnable target, String name,
  2. long stackSize, AccessControlContext acc,
  3. boolean inheritThreadLocals) {
  4. .......
  5. this.target = target;//最终构造器中的Runnable实现类对象会被赋值给Thread类的成员变量target
  6. .......
  7. }
  8. private Runnable target;//Thread类的成员变量target
  1. @Override
  2. public void run() {//当Thread类对象调用start方法,start方法启动线程并执行run方法时,会调用target中定义的run方法,即Runnable实现类对象中的run方法
  3. if (target != null) {
  4. target.run();
  5. }
  6. }

使用FutureTask类

  1. import java.util.concurrent.Callable;
  2. import java.util.concurrent.ExecutionException;
  3. import java.util.concurrent.FutureTask;
  4. public class MyTask implements Callable<Integer>{// 1.创建Callable实现类对象
  5. int sum;
  6. @Override
  7. /**
  8. * 注意call方法可以抛出异常,且可以顶返回值
  9. */
  10. public Integer call() throws Exception {//2.重写call方法
  11. for (int i = 1; i <= 100; i++) {//3.call方法体即为线程任务执行体
  12. sum += i;
  13. }
  14. return sum;
  15. }
  16. }
  17. class Main{
  18. public static void main(String[] args) {
  19. MyTask myTask = new MyTask();//4.创建Callable实现类对象
  20. FutureTask<Integer> task = new FutureTask<>(myTask);//5.创建FutureTask类对象,FutureTask类对象需要绑定Callable实现类对象
  21. Thread thread = new Thread(task);//5.使用new Thread(Runnable target)创建thread线程类对象
  22. thread.start();//6.使用线程类对象调用start方法,启动线程并运行run方法
  23. try {
  24. System.out.println(task.get());//7.FutureTask类对象的get方法可以获得Callable实现类对象的call方法的返回值
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. } catch (ExecutionException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. }

实现Runnable接口的方式创建线程似乎已经很完美了,但是还是有一些美中不足。那就是Runnable接口的run方法两个缺点:

1.方法无法抛出异常

2.方法不能定义返回值 

这两个方法导致了run方法不具备普适性。即无法适配线程任务需要返回值,或者需要抛出异常给外界处理的场景。

所以Java提供了FutureTask类,该类间接实现了Runnable接口,所以可以作为Runnable实现类使用,即作为new Thread(Runnable target)的参数。

                                                   该类内部组合了private Callable<V> callable;所以可以接受一个Callable实现类对象作为构造器参数。

                                                   该类重写run方法时,在run方法内部调用了callable.call(),当call()方法执行成功,则将返回值给一个中间变量,后面可以通过get方法获得。

                                                                                                                                                当call()方法执行异常,则将异常信息给中间变量,后面可以通过get方法获得。

需要注意的是:

1.FutureTask类对象的get方法只有在对应线程执行run(call)方法结束后,才能返回值,否则,get方法会一直等待run(call)结束。

2.如果不希望FutureTask类对象get方法一直等待,则

2.1 使用重载方法get(long timeout,TimeUnit unit),即指定对应时间等待,如果时间到了,call还没有结束,则返回TimeOutException。

2.2 使用FutureTask对象的cancel(boolean mayInterruptRunning)方法:尝试取消call方法执行。

2.3 通过isCanceled(),判断call()是否在正常完成前被取消

2.4 通过isDone(),判断call()方法是否执行完成

3.Callable实现类重写call方法抛出的异常,需要通过对应的FutureTask类对象的get方法获得。

线程使用方法

获取当前线程对象

public static native Thread currentThread();

静态方法,类名直接调用。返回值是调用该方法所在任务体的线程对象。

获取,设置线程名字

public final synchronized void setName(String name)
public final String getName()

实例方法。

线程睡眠

public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos) throws InterruptedException

静态方法。在哪个线程任务执行体中调用,就让哪个线程睡眠。

该方法让 线程 从运行态(RUNNABLE) 进入 阻塞态(TIMED_WAITING)。

睡眠时间结束后,线程从  阻塞态(TIMED_WAITING)进入 就绪态(RUNNABLE),而不是直接运行。

剥夺线程CPU执行权,重新在平级或高优先级线程中调度

public static native void yield();

静态方法,在哪个线程任务执行体中调用,就让哪个线程让出CPU。

该方法让线程 从运行态(RUNNABLE) 进入 就绪态(RUNNABLE)。

让出CPU后该线程还可以继续抢占CPU,很有可能出现“假让”现象。

另外yield方法让出的CPU执行权只会分配给优先级和让出线程相同或者比让出线程高的线程。

获取,设置线程优先级

public final int getPriority()
public final void setPriority(int newPriority)

Java优先级从1到10,1最小,10最大

Java在Thread类中定义了三个常量表示三个常用优先级

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

线程插队

public final void join() throws InterruptedException

实例方法。调用该方法的线程叫插队线程,调用该方法所在任务执行体的线程叫被插队线程。

该方法可以让 插队线程 从 就绪态(RUNNABLE)变为 运行态(RUNNABLE)

不带时间参数的join方法,可以让 被插队线程 从 运行态(RUNNABLE) 变为 阻塞态(WAITING)

public final synchronized void join(long millis)
public final synchronized void join(long millis, int nanos)

带时间参数的join方法,可以让可以让 被插队线程 从 运行态(RUNNABLE) 变为 阻塞态(TIMED_WAITING)

线程打断

public void interrupt()

实例方法。该方法只是给线程设置打断标志,并不是真的中止线程。

如果打断的是正在sleep的线程,则会抛出异常,并清除线程的打断状态。

如果打断的是正常运行的线程,则不会清除打断状态。

如果打断的时park的线程,则不会清除打断标志。到下次继续park带true打断标志的线程时,park失效,即无法暂停线程。

获取线程打断标志

  1. public boolean isInterrupted() {
  2. return isInterrupted(false);
  3. }

无参isInterrupted方法用于获取线程的打断标志,且获取后不清除线程的打断标志。

private native boolean isInterrupted(boolean ClearInterrupted);

带boolean参数的isInterrupted方法用于获取线程的打断标志,当参数为true时,表示获取后清除打断标志,当参数为false,表示获取后不清除打断标志。

  1. public static boolean interrupted() {
  2. return currentThread().isInterrupted(true);
  3. }

interrupted方法表示获取后,清除打断标志。

设置守护线程

public final void setDaemon(boolean on)

线程调用setDaemon(true)即变为守护线程。

JVM中当所有非守护线程都结束后,守护线程就算没有运行完成,也会被迫结束。

垃圾回收器线程就是守护线程。

过期.线程挂起,唤醒

public void suspend() 

public void resume() 

过期.线程终止

public void stop()

终止线程使用stop方法是不安全的。因为stop是强制杀死线程,且杀死线程后不会释放同步锁。可能导致死锁。

代替方案:两阶段终止模式

  1. public class TOperation{
  2. private static Thread thread;
  3. public static void start(){
  4. thread = new Thread(()->{
  5. while (true){
  6. if (thread.isInterrupted()){
  7. System.out.println("程序被打断了");
  8. break;
  9. } else {
  10. try {
  11. Thread.sleep(1000);
  12. } catch (InterruptedException e) {
  13. thread.interrupt();
  14. }
  15. for (int i = 0; i < 5; i++) {
  16. System.out.println(i);
  17. }
  18. }
  19. }
  20. });
  21. thread.start();
  22. }
  23. public static void stop(){
  24. thread.interrupt();
  25. }
  26. public static void main(String[] args) throws InterruptedException {
  27. TOperation.start();
  28. Thread.sleep(15000);
  29. TOperation.stop();
  30. }
  31. }

 

判断线程是否死亡

public final native boolean isAlive();

死亡了返回true

存活着返回false

线程生命周期

线程的生命周期说法不一,原因是线程是操作系统创建的,所以操作系统对线程生命周期有一套定义。而Java封装了操作系统层面线程的操作,所以Java自己也有一套线程生命周期。但是Java的线程生命周期需要能和操作系统对应。

操作系统版本的线程生命周期

Java版本的线程生命周期

Java自己定义了一套线程状态,即Thread.State

BLOCKED,NEW,RUNNABLE,TERMINATED,TIMED_WAITING,WAITING

对比可以发现

JAVA将【操作系统层面的就绪和运行,以及部分阻塞状态】合并成了RUNNABLE状态

         将操作系统的阻塞状态分解成了【BLOCKED,WAITING,TIMED_WIATING】状态

JAVA的线程状态和操作系统状态不是完全一致的,比如

操作系统层面,线程中有执行阻塞API时,线程会放弃CPU,进入阻塞状态。但是JAVA却将该线程状态认为是RUNNABLE。

还有线程执行suspend操作后被挂起,操作系统层面理解为线程阻塞,但是JAVA却认为该线程是RUNNABLE。

所以按照正常人思维理解,建议按照操作系统层面线程状态理解。否则会搞糊涂。

线程同步

什么是线程同步?

之前关于同步异步的解释是:

同步:程序的继续执行需要等待某个结果的返回,否则会一直阻塞着等待结果

异步:程序的继续执行不需要等待某个结果,直接跳过执行

对于多线程而言,一段程序可以多个线程同时进入并执行,各个线程之间互相不等待,这就是线程间的异步执行

如果一段程序只允许一个线程进入执行,若进入线程没有执行完程序,则其他线程必须阻塞等待,只有进入线程执行完成,其他线程才能继续抢占进入程序,这就是线程同步。

总结概括:某段程序只允许单线程执行,那么该段程序就是线程同步的。

为什么要引入线程同步?

如果不引入线程同步,可能就会出现安全性问题。所以线程同步,有时也叫线程安全。我们说集合类时,通常会说ArrayList是线程不安全的,Vector是线程安全的。其实就是说ArrayList中内部代码是线程异步的,Vector中代码是线程同步的。

那么线程异步会引入什么安全问题呢?请看如下代码

  1. public class TestSecurity {
  2. public static void main(String[] args) throws InterruptedException {
  3. TestTask task = new TestTask();
  4. Thread t1 = new Thread(task);
  5. Thread t2 = new Thread(task);
  6. Thread t3 = new Thread(task);
  7. t1.start();
  8. t2.start();
  9. t3.start();
  10. t1.join();
  11. t2.join();
  12. t3.join();
  13. System.out.println(task.getA());
  14. }
  15. }
  16. class TestTask implements Runnable{
  17. private int a = 0;
  18. @Override
  19. public void run() {
  20. for (int i = 0; i < 10; i++) {
  21. //多次尝试无问题的话,可以增加如下代码增强线程安全问题概率
  22. // try {
  23. // Thread.sleep(1);
  24. // } catch (InterruptedException e) {
  25. // e.printStackTrace();
  26. // }
  27. a++;
  28. }
  29. }
  30. public int getA(){
  31. return a;
  32. }
  33. }

t1,t2,t3线程的任务都是task,即这三个线程的任务是同一个,任务内容是给同一个变量a自增十次。

理想情况是a的最后数据是30

但是当多线程异步执行run方法时,就有概率出现安全问题。安全问题的理解需要你知道几件事:

1.a++的执行是单步的还是多步的?

2.线程执行a++时,有没有可能失去CPU执行权

对于1,我们需要知道在Java层面来看,a++就是单步,但是Java语言是高级语言,它并不能直接被计算机底层硬件识别,计算机硬件能识别的是二进制机器码语言,另外不同操作系统的机器码都有一些不同,所以Java代码需要编译生成与平台无关的字节码代码,它可以由不同的JVM转化成不同平台的机器码。所以我们知道看Java编译后的class文件的代码,查看a++对应的步骤是单步还是多步的。

 

 

线程的通信

 



所属网站分类: 技术文章 > 博客

作者:我爱编程

链接:http://www.javaheidong.com/blog/article/207126/1085af67cb4cd1d62c4b/

来源:java黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

11 0
收藏该文
已收藏

评论内容:(最多支持255个字符)