转载

JAVA线程的那些事?

1.线程是什么?

进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。 线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。
同一时刻运行多个程序的能力。每一个任务称为一个线程。可以同时运行一个以上线程的程序称为多线程程序。 Java编写程序都运行在在Java虚拟机(JVM)中,在JVM的内部,程序的多任务是通过线程来实现的。每用java命令启动一个java应用程序,就会启动一个JVM进程。在同一个JVM进程中,有且只有一个进程,就是它自己。在这个JVM环境中,所有程序代码的运行都是以线程来运行。 一般常见的Java应用程序都是单线程的。比如,用java命令运行一个最简单的HelloWorld的Java应用程序时,就启动了一个JVM进程,JVM找到程序程序的入口点main(),然后运行main()方法,这样就产生了一个线程,这个线程称之为主线程。当main方法结束后,主线程运行完成。JVM进程也随即退出 。 对于一个进程中的多个线程来说,多个线程共享进程的内存块,当有新的线程产生的时候,操作系统不分配新的内存,而是让新线程共享原有的进程块的内存。因此,线程间的通信很容易,速度也很快。不同的进程因为处于不同的内存块,因此进程之间的通信相对困难。
线程分为两类:用户线程和守候线程。当所有用户线程执行完毕后,JVM自动关闭。但是守候线程却不独立与JVM,守候线程一般是有操作系统或用户自己创建的。

2.线程的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。 下图显示了一个线程完整的生命周期。  
  • 新建状态:使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
  • 就绪状态:当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
  • 运行状态:如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
  • 阻塞状态:如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡状态:一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

3.如何创建一个线程

Java 提供了三种创建线程的方法:
  • 通过实现 Runnable 接口;
  • 通过继承 Thread 类本身;
  • 通过 Callable 和 Future 创建线程。

通过实现 Runnable 接口来创建线程

创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类。 为了实现 Runnable,一个类只需要执行一个方法调用 run(),声明如下:
 下面是一个创建线程并开始让它执行的实例:
package org.java.base.thread; public class RunnableDemo implements Runnable{ @Override public void run() { System.out.println("我是线程"); } }

通过继承Thread来创建线程

创建一个线程的第二种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。 该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。
package org.java.base.thread; public class ThreadDemo extends Thread{ @Override public void run() { System.out.println("我是线程"); } }

Thread 方法

下表列出了Thread类的一些重要方法:
序号 方法描述
1 public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
2 public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
3 public final void setName(String name) 改变线程名称,使之与参数 name 相同。
4 public final void setPriority(int priority) 更改线程的优先级。
5 public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。
6 public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。
7 public void interrupt() 中断线程。
8 public final boolean isAlive() 测试线程是否处于活动状态。
测试线程是否处于活动状态。 上述方法是被Thread对象调用的。下面的方法是Thread类的静态方法。
序号 方法描述
1 public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。
2 public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
3 public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
4 public static Thread currentThread() 返回对当前正在执行的线程对象的引用。
5 public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。

通过 Callable 和 Future 创建线程

  • 1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
  • 2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
  • 3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
  • 4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
 package org.java.base.thread; import java.util.concurrent.Callable; public class CallableThreadDemo implements Callable<Integer>{ @Override public Integer call() throws Exception { System.out.println("我是一个Callable实现"); return 1; } }
  • 创建线程的三种方式的对比

    • 1. 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。
    • 2. 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。
 
有效利用多线程的关键是理解程序是并发执行而不是串行执行的。例如:程序中有两个子系统需要并发执行,这时候就需要利用多线程编程。 通过对多线程的使用,可以编写出非常高效的程序。不过请注意,如果你创建太多的线程,程序执行的效率实际上是降低了,而不是提升了。 请记住,上下文的切换开销也很重要,如果你创建了太多的线程,CPU 花费在上下文的切换的时间将多于执行程序的时间!

4.线程通信

正常情况下,每个子线程完成各自的任务就可以结束了。不过有的时候,我们希望多个线程协同工作来完成某个任务,这时就涉及到了线程间通信了。 线程之间通信方式:
1.是通过共享变量,线程之间通过该变量进行协作通信; 例如:多个线程共享同一个变量,要考虑并发的问题 2.通过队列(本质上也是线程间共享同一块内存)来实现消费者和生产者的模式来进行通信; 例如:异步发送邮件或者短信
 

5.线程同步

java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),
 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,
 从而保证了该变量的唯一性和准确性。
即有synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,  内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
 代码如:
    public synchronized void save(){}
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

6.线程死锁

死锁就是两个或两个以上的线程被无限的阻塞线程之间相互等待所需的资源">死锁就是两个或两个以上的线程被无限的阻塞,线程之间相互等待所需的资源。这种情况可能发生在当两个线程尝试获取其他资源的锁,而每个线程又陷入无线等待其他资源锁的释放,除非一个用户的进程被终止。 线程死锁可能发生在以下的情况:
  1. 当两个线程相互调用Thread.join();
  2. 当两个线程使用嵌套的同步块时,一个线程占用了另一个线程的必需的锁,互相等待时被阻塞,就有可能出现死锁。
死锁一般都是由于对共享资源的竞争所引起的。但对共享资源的竞争又不一定就会发生死锁。 死锁的发生必需满足4个必要条件:
  1. 互斥
  2. 等待/持有
  3. 非抢占
  4. 形成等待环
package org.java.base.thread; /** * 一个简单的死锁类 * 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒 * 而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒 * td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定; * td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定; * td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。 */ public class DeadLock implements Runnable { public int flag = 1; //静态对象是类的所有对象共享的 private static Object o1 = new Object(), o2 = new Object(); @Override public void run() { System.out.println("flag=" + flag); if (flag == 1) { synchronized (o1) { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } synchronized (o2) { System.out.println("1"); } } } if (flag == 0) { synchronized (o2) { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } synchronized (o1) { System.out.println("0"); } } } } public static void main(String[] args) { DeadLock td1 = new DeadLock(); DeadLock td2 = new DeadLock(); td1.flag = 1; td2.flag = 0; //td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。 //td2的run()可能在td1的run()之前运行 new Thread(td1).start(); new Thread(td2).start(); } }
在有些情况下死锁是可以避免的。三种用于避免死锁的技术:
  1. 加锁顺序(线程按照一定的顺序加锁)
  2. 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  3. 死锁检测
本节具体代码详见:https://gitee.com/jxuasea/maxap/tree/master/java/src/main/java/org/java/base/thread
正文到此结束
Loading...