Java高级-多线程

进程基本概念

概念  

       进程就是程序的一次执行过程,是系统运行程序的基本单位。操作系统在启动每一个应用时,会为每一个应用划分一块独立内存空间称之为进程。进程是操作系统中运行的一个任务(一个应用程序运行在一个进程中),包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立存在,它必须是进程的一部分。

 

线程基本概念

线程概念

       线程与进行相似,称为轻量级进程,但是线程是一个比进程更小的执行单位。一个进程至少包含一个线程,同类的多个线程共享同一个进程空间。系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多。

线程的生命周期

       线程由5种基本状态,如下所示:

  • 新建状态(New)

         当线程对象创建后,即进入到新建状态。如Thread thread = new MyThread();

  • 就绪状态(Runnable也叫可运行状态)

          当调用线程对象的start()方法时(thread.start()),线程即进入到就绪状态。处于就绪状态的线程,只能说明此线程已经做好了准备,随时等待CPU调用指向,并不是说调用了start()方法后线程就会立即执行。

          注意:不能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常。

  • 运行状态(Running)

          当CPU开始调度处于就绪状态的线程时,此刻线程才是真正的开始执行,即进入到了运行状态。而且,线程要想进入运行状态执行,首先必须处于就绪状态中;

  • 阻塞状态(Blocked)

           处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此刻线程就进入到就绪状态。根据阻塞的原因可以分为三种:

1.等待阻塞运行状态中的线程执行wait()方法,使得该线程进入到等待阻塞状态。
2.同步阻塞线程在获取synchronized同步锁失败(因为锁被其他线程锁占用),它会进入同步阻塞状态。
3.其他阻塞通过调用线程的sleep()方法、join()方法或者发出来I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  • 死亡状态(Dead)

         线程执行完了或者因为异常退出了run()方法,该线程结束了生命周期进入了死亡状态。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

    线程状态之间的转换:

就绪状态转换为运行状态此线程得到处理器资源
运行状态转换为就绪状态此线程主动调用yieId()方法,或者在运行过程中失去处理器资源
运行状态转换为死亡状态当此线程线程执行体执行完毕或发生了异常。

多线程

      多线程是指一个进程(执行中的程序)同时运行多个线程(进程中负责程序执行的执行单元),多线程可以协作完成进程工作,其目的是更好的利用 CPU 资源。线程通常用于在一个程序中需要同时完成多个任务的情况,将每个任务定义为一个线程,使得它们可以一同工作。

      多线程是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。

为什么不提倡多进程

      线程就是轻量级进程,是程序执行的最小单位。使用多线程而不是用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。

线程优先级

       线程优先级总共分为10级,分别用1-10表示。1优先级最低,10优先级最高。优先级越高,则线程获取CPU获取时间片的机会越大,但并不代表优先级越高的线程一定会越先执行,只是概率大一些。

Thread.MIN_PRIORITY 代表优先级1
Thread.NORM_PRIORITY 代表优先级5
Thread.MAX_PRIORITY 代表优先级10

       线程默认的优先级为5。

线程分类

      线程分为用户线程(前台线程)守护线程(后台线程)。  

  • 用户线程

      默认创建的线程都是用户线程,可以通过Thread类提供的方法setDaemon来将用户线程转换成守护线程(必须在启动线程之前设置,否则无效)。

  • 守护线程 

       守护线程是为用户线程服务,只要用户线程存在,则守护线程将一直存在。 当进程中用户线程不存在了,只剩下守护线程时,所有守护线程都会被强制终止。如Java中的GC(垃圾回收器)就是一个守护线程,内存管理也是守护线程。

       守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。

进程与线程的区分

  • 进程由操作系统启动的,而线程由进程启动的。
  • 线程间的切换和调度的成本远远小于进程。
  • 进程包含一个或多个线程,且线程不能脱离进程而存在

线程池

      线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

      线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快;另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

创建

     线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。

  • Executors:线程池创建工厂类
  • public static ExecutorServicenewFixedThreadPool(int nThreads):返回线程池对象
  • ExecutorService:线程池类
  • Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
  • Future 接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用

这里介绍两种使用线程池创建线程的方法

1):使用Runnable接口创建线程池

使用线程池中线程对象的步骤:

  •  1、创建线程池对象
  •  2、创建 Runnable 接口子类对象
  •  3、提交 Runnable 接口子类对象
  •  4、关闭线程池

Test.java 代码如下:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {
    public static void main(String[] args) {
        //创建线程池对象  参数5,代表有5个线程的线程池
        ExecutorService service = Executors.newFixedThreadPool(5);
        //创建Runnable线程任务对象
        TaskRunnable task = new TaskRunnable();
        //从线程池中获取线程对象
        service.submit(task);
        System.out.println("----------------------");
        //再获取一个线程对象
        service.submit(task);
        //关闭线程池
        service.shutdown();
    }
}

TaskRunnable.java 接口文件如下:

public class TaskRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("自定义线程任务在执行"+i);
        }
    }
}

2)使用Callable接口创建线程池

Callable接口:与Runnable接口功能相似,用来指定线程的任务。其中的call()方法,用来返回线程任务执行完毕后的结果,call方法可抛出异常。

ExecutorService:线程池类

<T> Future<T> submit(Callable<T> task):获取线程池中的某一个线程对象,并执行线程中的 call() 方法

Future 接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用

使用线程池中线程对象的步骤:

  •  1、创建线程池对象
  •  2、创建 Callable 接口子类对象
  •  3、提交 Callable 接口子类对象
  •  4、关闭线程池

Test.java 代码如下:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test{
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(3);
        TaskCallable c = new TaskCallable();
        //线程池中获取线程对象,调用run方法
        service.submit(c);
        //再获取一个
        service.submit(c);
        //关闭线程池
        service.shutdown();
    }
}

TaskCallable.java 接口文件如下:

import java.util.concurrent.Callable;

public class TaskCallable implements Callable<Object>{
    @Override
    public Object call() throws Exception {
        for (int i = 0; i < 1000; i++) {
            System.out.println("自定义线程任务在执行"+i);
        }
        return null;
    }
}

 

创建线程方式

方式一:使用Thread类

  • 创建类继承Thread类形式
public class ThreadDemo1{
    public static void main(String[] args) {
        MyThread1 myThread1 = new MyThread1();
        //启动线程
        myThread1.start();
}
class MyThread1 extends Thread{
    @Override
    public void run() {
        //TODO
    }
}
  • 使用匿名内部类形式
public class ThreadDemo1{
    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                //TODO
            }
        }.start();
    }
}

方式二:使用Runnable接口

  • 创建类实现Runnable接口形式

        需要在Thread的构造方法里面传入实现Runnable接口子类的对象。

public class ThreadDemo2 {
    public static void main(String[] args) {
        MyRunnable1 myRunnable1 = new MyRunnable1();
        Thread thread1 = new Thread(myRunnable1);
        thread1.start();
    }
}
class MyRunnable1 implements Runnable{
    @Override
    public void run() {
        //TODO	
    }
}
  • 使用匿名内部类形式
public class ThreadDemo2 {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //TODO			    	
            }
        }).start();	
    }
}

方式三:使用线程池

       使用线程池的方式也是最推荐的一种方式。

方式四:使用Callable接口

 

线程启动注意事项

       线程在new之后,启动调用其start()方法时,就会执行run方法,如果再次调用run方法,则会执行两次run方法。其线程只能启动一次,否则出错

 注意

       因为类的单一继承原则,因此推荐使用Runnable接口。

Thread

源码

       Thread类implements自Runnable接口。

public class Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }
    private volatile String name;
    private int            priority;
    //......
}

构造方法

Thread()分配新的Thread对象
Thread(Runnable target)

分配新的Thread对象

参数:

target - 其 run 方法被调用的对象。

Thread(Runnable target, String name)

分配新的Thread对象

参数:

target - 其 run 方法被调用的对象。

name - 新线程的名称。

Thread(String name)

分配新的Thread对象

参数:

name - 新线程的名称。

Thread(ThreadGroup group, String name)

分配新的Thread对象

参数:

group - 线程组。

name - 新线程的名称。

Thread(ThreadGroup group, Runnable traget)分配新的Thread对象
............

常用方法

public void start()启动线程,使得线程从初始态进入到可运行状态。
public static Thread currentThread()返回对当前正在执行的线程对象的引用。
public long getId()

返回该线程的标识符ID。

线程 ID 是一个正的 long 数,在创建该线程时生成。线程 ID 是唯一的,并终生不变。线程终止时,该线程 ID 可以被重新使用。

public final String getName()返回该线程的名称
public final void setName(String name)

改变线程名称。 

首先调用线程的 checkAccess 方法,且不带任何参数。 

抛出:

SecurityException - 如果当前线程不能修改该线程。

public final int getPriority()返回线程的优先级
public final void setPriority(int newPriority)

更改线程的优先级。

抛出:

IllegalArgumentException - 如果优先级不在 MIN_PRIORITY 到 MAX_PRIORITY 范围内。 

SecurityException - 如果当前线程无法修改该线程。

public final ThreadGroup getThreadGroup()返回该线程所属的线程组。如果该线程已经终止(停止运行),该方法则返回 null。
public void interrupt()

中断线程。

抛出:

SecurityException - 如果当前线程无法修改该线程

public void run()如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。Thread 的子类要重写该方法。
public final boolean isAlive()测试线程是否处于活动状态。
public final boolean isDaemon()测试该线程是否为守护线程。
public boolean isInterrupted()

测试线程是否已经中断,但不清楚状态标志。

返回:

如果当前线程已经中断,则返回 true;否则返回 false

public static boolean interrupted()

测试当前线程是否已经中断,执行后具有将状态标志清除为false的功能。

返回:

如果当前线程已经中断,则返回 true;否则返回 false

public static void sleep(long millis)

在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。当阻塞指定毫秒数后,当前线程会重新进入Runnable状态,等待分配时间片。

抛出:

InterruptedException - 如果任何线程中断了当前线程。当抛出该异常时,当前线程的 中断状态 被清除。

public final void join()

等待调用该方法的线程终止。 

join方法会使当前线程从运行状态进入阻塞状态,当被加入的调用join的线程执行完毕后,当前线程又会从阻塞状态到可运行状态,等待线程调度为其分配时间片,重新运行。join方法会使得异步操作转变为同步操作。

抛出:

InterruptedException - 如果任何线程中断了当前线程。当抛出该异常时,当前线程的中断状态被清除

public static void yield()

暂停当前正在执行的线程对象,让出cpu资源给其他的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。

 

Runnable

源码

public interface Runnable {
    public abstract void run();
}

 

start和run方法的区别

    调用start方法会创建一个子线程并启动,run方法只是线程的一个普通方法。

同步和异步

同步

      同步的操作有先后顺序的操作,相当于你干完我再干。

异步

      多线程并发的操作,相当于各自做各自的事情,互不干涉。

并发(Concurrency)和并行(Parallelism)

并发

      多个线程”同时运行“只是感官上的一种表现,事实上线程是并发运行的。操作系统会将时间片划分成很多的时间片,然后尽可能均匀的将时间片分配给不同的线程,获得时间片的线程会被CPU执行,而其他线程全部等待,所以微观上是走走停停,宏观上都是在同时运行,这种现象叫做并发。

      并发偏重于多个任务交替执行。

并行

      并行是真正意义上的“同时执行”。多线程在单核CPU的话是顺序执行,也就是交替运行(并发)。多核CPU的话,因为每个CPU有自己的运算器,所以在多个CPU中可以同时运行(并行)。

高并发

      高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。

      高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。
 

临界区

       临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。在并行程序中,临界区资源是保护的对象。

临界资源

      常见的临界资源:1)多线程共享同一个实例变量;2)多线程共享静态公共变量;3)同一个变量等等。

线程同步

     多个线程并发读取和修改同一个临界资源时会发生”线程并发安全问题“。若想解决线程安全问题,需要将异步的操作变为同步操作。

synchronized关键字

     synchronized关键字是Java中的同步锁。用于将出现并发安全性的代码加锁。

  • 同步方法
public synchronized void save(){
    //TODO
}
  • 同步代码块
public class Bank {  
    private int count =0;//账户余额  
     
    //存钱  
    public void addMoney(int money){  
        synchronized (this) {  
            count +=money;  
        }  
        System.out.println(System.currentTimeMillis()+"存进:"+money);  
    }  
     
    //取钱  
    public void subMoney(int money){  
        synchronized (this) {  
            if(count-money < 0){  
                System.out.println("余额不足");  
                return;  
            }  
            count -=money;  
        }  
        System.out.println(+System.currentTimeMillis()+"取出:"+money);  
    }  
     
    //查询  
    public void lookMoney(){  
        System.out.println("账户余额:"+count);  
    } 
}

        同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

        使用synchronize同步代码块时注意事项:

1.必须保证是同一把锁对象(锁对象类型没有要求),否则同步代码块无效。
2.在非静态方法上声明synchronize关键字表示将整个方法加锁,且使用的锁对象为this当前对象。
3.在静态方法上声明synchronize关键字,表示将整个静态方法加锁并且使用的锁对象为当前类的类对象。

 

 

线程的通信

 

线程死锁

 

线程控制:挂起、停止和恢复

 

 

 

 

参考资料

https://blog.csdn.net/houbin0912/article/details/77969563

https://www.runoob.com/java/java-multithreading.html

https://blog.csdn.net/qq_34337272/article/details/79640870

https://www.cnblogs.com/snow-flower/p/6114765.html

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

luckyliuqs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值