Java 多线程学习笔记

Java进程与线程

进程是程序的一次动态执行过程,它需要经历从代码加载,代码执行到执行完毕的一个完整的过程,这个过程也是进程本身从产生,发展到最终消亡的过程。多进程操作系统能同时达运行多个进程(程序),由于CPU具备分时机制,所以每个进程都能循环获得自己的CPU时间片。由于CPU执行速度非常快,使得所有程序好像是在同时运行一样。

多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发的一个基本单位。线程是比进程更小的执行单位,线程是进程的基础之上进行进一步的划分。所谓多线程是指一个进程在执行过程中可以产生多个更小的程序单元,这些更小的单元称为线程,这些线程可以同时存在,同时运行,一个进程可能包含多个同时执行的线程。

Java多线程的实现

在Java中实现多线程有两种手段,一种是继承Thread类,另一种就是实现Runnable接口。

实现Runnable接口

public class RunnableInterface implements Runnable {

      public String name;

      public RunnableInterface(String name) {

      this.name = name;

}

@Override

public void run() {

      for (int i = 0; i <= 10; i++) {

           System.out.println(name + "运行" + i);

      }

}

public static class RunnableDome {

            public static void main(String[] args) {

                  RunnableInterface run1 = new RunnableInterface("线程A");

                  RunnableInterface run2 = new RunnableInterface("线程B");

                 Thread t1 = new Thread(run1);

                Thread t2 = new Thread(run2);

                t1.start();

               t2.start();

         }

}

}

继承Thread类

public class ExtendsThreadClass extends Thread {

private String name;

public ExtendsThreadClass(String name) {

this.name = name;

}

public void run() {

for (int i = 0; i <= 10; i++) {

System.out.println(name + "运行" + i);

}

}

public static class ExtendsThreadDome {

public static void main(String[] args) {

ExtendsThreadClass e1 = new ExtendsThreadClass("线程A");

ExtendsThreadClass e2 = new ExtendsThreadClass("线程B");

e1.start();

e2.start();

}

}

}

Thread类与Runnable接口

从程序可以看出,现在的两个线程对象是交错运行的,哪个线程对象抢到了CPU资源,哪个线程就可以运行,所以程序每次的运行结果肯定是不一样的,在线程启动虽然调用的是start()方法,但实际上调用的却是run()方法定义的主体。

通过Thread类和Runable接口都可以实现多线程,那么两者有哪些联系和区别呢?下面我们观察Thread类的定义。

public class Thread extends Object implements Runnable

从Thread类的定义可以清楚的发现,Thread类也是Runnable接口的子类,但在Thread类中并没有完全实现Runnable接口中的run()方法,下面是Thread类的部分定义。

Private Runnable target;

public Thread(Runnable target,String name){

init(null,target,name,0);

}

private void init(ThreadGroup g,Runnable target,String name,long stackSize){

...

this.target=target;

}

public void run(){

if(target!=null){

target.run();

}

}

从定义中可以发现,在Thread类中的run()方法调用的是Runnable接口中的run()方法,也就是说此方法是由Runnable子类完成的,所以如果要通过继承Thread类实现多线程,则必须覆写run()。

实际上Thread类和Runnable接口之间在使用上也是有区别的,如果一个类继承Thread类,则不适合于多个线程共享资源,而实现了Runnable接口,就可以方便的实现资源的共享。

​​​​​​​线程的状态

要想实现多线程,必须在主线程中创建新的线程对象。任何线程一般具有5种状态,即创建,就绪,运行,阻塞,终止。下面分别介绍一下这几种状态:

​​​​​​​创建状态 

在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时它已经有了相应的内存空间和其他资源,但还处于不可运行状态。新建一个线程对象可采用Thread类的构造方法来实现,例如Thread thread=new Thread()。

​​​​​​​就绪状态 

新建线程对象后,调用该线程的start()方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待CPU服务,这表明它已经具备了运行条件。

​​​​​​​运行状态

当就绪状态被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的run()方法。run()方法定义该线程的操作和功能。

​​​​​​​阻塞状态 

一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入/输出操作,会让CPU暂时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep(),suspend(),wait()等方法,线程都将进入阻塞状态,发生阻塞时线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。

​​​​​​​死亡状态 

线程调用stop()方法时或run()方法执行结束后,即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。​​​​​​​

Java程序每次运行至少启动几个线程?

至少启动两个线程,每当使用Java命令执行一个类时,实际上都会启动一个JVM,每一个JVM实际上就是在操作系统中启动一个线程,Java本身具备了垃圾的收集机制。所以在Java运行时至少会启动两个线程,一个是main线程,另外一个是垃圾收集线程。

​​​​​​​取得和设置线程的名称

Thread.currentThread().getName() //取得当前线程的名称

MyThread my=new MyThread(); //定义Runnable子类对象

new Thread(my).start; //系统自动设置线程名称

new Thread(my,"线程A").start(); //手工设置线程名称

​​​​​​​线程的强制运行

在线程操作中,可以使用join()方法让一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行。

​​​​​​​线程的休眠

在程序中允许一个线程进行暂时的休眠,直接使用Thread.sleep()即可实现休眠。​​​​​​​

中断线程

当一个线程运行时,另外一个线程可以直接通过interrupt()方法中断其运行状态。

​​​​​​​后台线程

在Java程序中,只要前台有一个线程在运行,则整个Java进程都不会消失,所以此时可以设置一个后台线程,这样即使Java线程结束了,此后台线程依然会继续执行,要想实现这样的操作,直接使用setDaemon()方法即可。

​​​​​​​线程的优先级

在Java的线程操作中,所有的线程在运行前都会保持在就绪状态,那么此时,哪个线程的优先级高,哪个线程就有可能会先被执行。

t1.setPriority(Thread.MIN_PRIORITY) ;   // 优先级最低

t2.setPriority(Thread.MAX_PRIORITY) ;   // 优先级最高

t3.setPriority(Thread.NORM_PRIORITY) ;  // 优先级最中等

​​​​​​​线程的礼让

在线程操作中,也可以使用yield()方法将一个线程的操作暂时让给其他线程执行。

​​​​​​​线程同步

一个多线程的程序如果是通过Runnable接口实现的,则意味着类中的属性被多个线程共享,那么这样就会造成一种问题,如果这多个线程要操作同一个资源时就有可能出现资源同步问题。

​​​​​​​解决方法:同步代码块

syncchronized(同步对象){

  需要同步的代码

​​​​​​​解决方法:同步方法

除了可以将需要的代码设置成同步代码块外,也可以使用synchronized 关键字将一个方法声明为同步方法。

synchronized 方法返回值 方法名称(参数列表){

线程死锁​​​​​​​

死锁的产生

同步可以保证资源共享操作的正确性,但是过多同步也会产生问题。例如,现在张三想要李四的画,李四想要张三的书,张三对李四说“把你的画给我,我就给你书”,李四也对张三说“把你的书给我,我就给你画”两个人互相等对方先行动,就这么干等没有结果,这实际上就是死锁的概念。

所谓死锁,就是两个线程都在等待对方先完成,造成程序的停滞,一般程序的死锁都是在程序运行时出现的。

​​​​​​​锁顺序死锁

线程A调用leftRight()方法,得到left锁

同时线程B调用rightLeft()方法,得到right锁

线程A和线程B都继续执行,此时线程A需要right锁才能继续往下执行。此时线程B需要left锁才能继续往下执行。

但是:线程A的left锁并没有释放,线程B的right锁也没有释放。

所以他们都只能等待,而这种等待是无期限的-->永久等待-->死锁

​​​​​​​动态锁顺序死锁

如果两个线程同时调用transferMoney()

线程A从X账户向Y账户转账

线程B从账户Y向账户X转账

那么就会发生死锁。

​​​​​​​协作对象之间发生死锁

两个线程都需要被锁的资源,并且在操作途中是没有释放锁的

​​​​​​​避免死锁

让程序每次至多只能获得一个锁。当然,在多线程环境下,这种情况通常并不现实

设计时考虑清楚锁的顺序,尽量减少嵌在的加锁交互数量

既然死锁的产生是两个线程无限等待对方持有的锁,那么只要等待时间有个上限不就好了。当然synchronized不具备这个功能,但是我们可以使用Lock类中的tryLock方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后变回返回一个失败信息

发生死锁的原因主要由于:

线程之间交错执行

解决:以固定的顺序加锁

执行某方法时就需要持有锁,且不释放

解决:缩减同步代码块范围,最好仅操作共享变量时才加锁

永久等待

解决:使用tryLock()定时锁,超过时限则返回错误信息

​​​​​​​死锁检测

虽然造成死锁的原因是因为我们设计得不够好,但是可能写代码的时候不知道哪里发生了死锁。

JDK提供了两种方式来给我们检测:

JconsoleJDK自带的图形化界面工具,使用JDK给我们的的工具JConsole

Jstack是JDK自带的命令行工具,主要用于线程Dump分析。

具体可参考:

https://www.cnblogs.com/flyingeagle/articles/6853167.html​​​​​​​

Java线程池

Executor框架是一种将线程的创建和执行分离的机制。它基于Executor和ExecutorService接口,及这两个接口的实现类ThreadPoolExecutor展开,Executor有一个内部线程池,并提供了将任务传递到池中线程以获得执行的方法,可传递的任务有如下两种:通过Runnable接口实现的任务和通过Callable接口实现的任务。在这两种情况下,只需要传递任务到执行器,执行器即可使用线程池中的线程或新创建的线程来执行任务。执行器也决定了任务执行时间。

​​​​​​​线程池优点

线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。

可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。

​​​​​​​线程池作用

线程池作用就是限制系统中执行线程的数量。
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排 队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池 中有等待的工作线程,就可以开始运行了;否则进入等待队列​​​​​​​

线程池工作流程

        (1):poolSize < corePoolSize,则直接创建新的线程(核心线程)来执行当前提交的任务;

        (2):poolSize = corePoolSize,并且此时阻塞队列没有满,那么会将当前任务添加到阻塞队列中,如果此时存在工作线程(非核心线程)的话,那么会由工作线程来处理该阻塞队列中的任务,如果此时工作线程数量为0的话,那么会创建一个工作线程(非核心线程)出来;

        (3):poolSize = corePoolSize,并且此时阻塞队列已经满了,那么会直接创建新的工作线程(非核心线程)来处理阻塞队列中的任务;

        (4):poolSize = maximumPoolSize,并且此时阻塞队列也满了的话,那么会触发拒绝机制,具体决绝策略采用的是什么就要看我们创建ThreadPoolExecutor的时候传入的RejectExecutionHandler参数了

​​​​​​​线程池状态

其中AtomicInteger变量ctl的功能非常强大:利用低29位表示线程池中线程数,通过高3位表示线程池的运行状态:
1、RUNNING:-1 << COUNT_BITS,即高3位为111,该状态的线程池会接收新任务,并处理阻塞队列中的任务;

可以接受新任务,同时也可以处理阻塞队列里面的任务;
2、SHUTDOWN: 0 << COUNT_BITS,即高3位为000,该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;

不可以接受新任务,但是可以处理阻塞队列里面的任务;
3、STOP : 1 << COUNT_BITS,即高3位为001,该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;

不可以接受新任务,也不处理阻塞队列里面的任务,同时还中断正在处理的任务;
4、TIDYING : 2 << COUNT_BITS,即高3位为010,该状态表示线程池对线程进行整理优化;

属于过渡阶段,在这个阶段表示所有的任务已经执行结束了,当前线程池中是不存在有效的线程的,并且将要调用terminated方法;
5、TERMINATED: 3 << COUNT_BITS,即高3位为011,该状态表示线程池停止工作;

终止状态,这个状态是在调用完terminated方法之后所处的状态;

线程池提交

Executor.execute(Runnable command);

1.首次通过workCountof()获知当前线程池中的线程数,

  如果小于corePoolSize, 就通过addWorker()创建线程并执行该任务;

 否则,将该任务放入阻塞队列;

2. 如果能成功将任务放入阻塞队列中,  

如果当前线程池是非RUNNING状态,则将该任务从阻塞队列中移除,然后执行reject()处理该任务;

如果当前线程池处于RUNNING状态,则需要再次检查线程池(因为可能在上次检查后,有线程资源被释放),是否有空闲的线程;如果有则执行该任务;

3、如果不能将任务放入阻塞队列中,说明阻塞队列已满;那么将通过addWoker()尝试创建一个新的线程去执行这个任务;如果addWoker()执行失败,说明线程池中线程数达到maxPoolSize,则执行reject()处理任务;

​​​​​​​ExecutorService.submit(Callable<T> task);

会将提交的Callable任务会被封装成了一个FutureTask对象

FutureTask类实现了Runnable接口,这样就可以通过Executor.execute()提交FutureTask到线程池中等待被执行,最终执行的是FutureTask的run方法; 

​​​​​​​比较

 两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。 

 

​​​​​​​线程池的关闭

ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:

shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务

shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

​​​​​​​线程池容量的动态调整

ThreadPoolExecutor提供了动态调整线程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),

​​​​​​​线程池参数详解

public ThreadPoolExecutor(int corePoolSize,

                              int maximumPoolSize,

                              long keepAliveTime,

                              TimeUnit unit,

                              BlockingQueue<Runnable> workQueue,

                              ThreadFactory threadFactory,

                              RejectedExecutionHandler handler)

corePoolSize: 规定线程池有几个线程(worker)在运行。

maximumPoolSize: 当workQueue满了,不能添加任务的时候,这个参数才会生效。规定线程池最多只能有多少个线程(worker)在执行。

keepAliveTime: 超出corePoolSize大小的那些线程的生存时间,这些线程如果长时间没有执行任务并且超过了keepAliveTime设定的时间,就会消亡。

unit: 生存时间对于的单位

workQueue: 存放任务的队列

threadFactory: 创建线程的工厂

handler: 当workQueue已经满了,并且线程池线程数已经达到maximumPoolSize,将执行拒绝策略

四种线程池

​​​​​​​可变尺寸的线程池

public class Test {

  public static void main(String[] args) {

ExecutorService pool = Executors.newCachedThreadPool();

    // 创建线程

    Thread t1 = new MyThread();

    Thread t2 = new MyThread();

    Thread t3 = new MyThread();

    Thread t4 = new MyThread();

    Thread t5 = new MyThread();

    // 将线程放入池中进行执行

    pool.execute(t1);

    pool.execute(t2);

    pool.execute(t3);

    pool.execute(t4);

    pool.execute(t5);

    // 关闭线程池

    pool.shutdown();

  }

}

class MyThread extends Thread {

  @Override

  public void run() {

    System.out.println(Thread.currentThread().getName() + "正在执行。。。");

  }

}

 

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,

那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

说明:初始化一个可以缓存线程的线程池,默认缓存60s,线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列;
特点:在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源;当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销;
因此,使用时要注意控制并发的任务数,防止因创建大量的线程导致而降低性能。

​​​​​​​固定大小的线程池

public class Test {

  public static void main(String[] args) {

    // 创建一个可重用固定线程数的线程池

    ExecutorService pool = Executors.newFixedThreadPool(2);

    // 创建线程

    Thread t1 = new MyThread();

    Thread t2 = new MyThread();

    Thread t3 = new MyThread();

    Thread t4 = new MyThread();

    Thread t5 = new MyThread();

    // 将线程放入池中进行执行

    pool.execute(t1);

    pool.execute(t2);

    pool.execute(t3);

    pool.execute(t4);

    pool.execute(t5);

    // 关闭线程池

    pool.shutdown();

  }

}

class MyThread extends Thread {

  @Override

  public void run() {

    System.out.println(Thread.currentThread().getName() + "正在执行。。。");

  }

}

 

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

说明:初始化一个指定线程数的线程池,其中corePoolSize == maxiPoolSize,使用LinkedBlockingQuene作为阻塞队列
特点:即使当线程池没有可执行任务时,也不会释放线程。

​​​​​​​延迟任务连接池

public class TestScheduledThreadPoolExecutor {

  public static void main(String[] args) {

    ScheduledThreadPoolExecutor exec = new ScheduledThreadPoolExecutor(1);

    exec.scheduleAtFixedRate(new Runnable() {//每隔一段时间就触发异常

           @Override

           publicvoid run() {

              //throw new RuntimeException();

              System.out.println("================");

           }

         }, 1000, 5000, TimeUnit.MILLISECONDS);

    exec.scheduleAtFixedRate(new Runnable() {//每隔一段时间打印系统时间,证明两者是互不影响的

           @Override

           publicvoid run() {

              System.out.println(System.nanoTime());

           }

         }, 1000, 2000, TimeUnit.MILLISECONDS);

  }

}

 

创建一个定长线程池,支持定时及周期性任务执行。

特定:初始化的线程池可以在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可以使用该线程池定期的同步数据。

​​​​​​​单任务线程池

public class Test {

  public static void main(String[] args) {

ExecutorService pool = Executors.newSingleThreadExecutor();

     // 创建线程

     Thread t1 = new MyThread();

     Thread t2 = new MyThread();

     Thread t3 = new MyThread();

     Thread t4 = new MyThread();

     Thread t5 = new MyThread();

    // 将线程放入池中进行执行

    pool.execute(t1);

    pool.execute(t2);

    pool.execute(t3);

    pool.execute(t4);

    pool.execute(t5);

    // 关闭线程池

    pool.shutdown();

  }

}

class MyThread extends Thread {

  @Override

  public void run() {

    System.out.println(Thread.currentThread().getName() + "正在执行。。。");

  }

}

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

说明:初始化只有一个线程的线程池,内部使用LinkedBlockingQueue作为阻塞队列。
特点:如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值