Java基础知识每日总结(16)---Java多线程编程

1.线程基础
①线程与进程
一个应用程序(.exe)的启动代表了一个进程的启动。由于在操作系统上创建进程的开销非常大,因此提出了线程的概念,它相当于“轻量级”的进程。一个操作系统可能会包括多个进程,而1个进程可能会包括多个线程。
每一个进程都有它自己的内存空间和系统资源,而在同一进程中创建的线程共享这些资源。
注:在使用Java语言编写多线程程序时,运行结果与操作系统密切相关,即使在同一台机器上,多次运行同一个程序,结果也可能不同。
进程是内存分配的最小单位,多进程是为了提高CPU的使用率。
线程是代码执行的最小单位,多线程是为了提高应用程序的使用率,线程的执行具有随机性,但是一个进程中的线程多了以后,它抢到CPU资源的概率就会大很多。

并行是逻辑上的同时发生,指在某一个时间内同时运行多个程序。
并发是物理上的同时发生,指在某一个时间点同时运行多个程序。

JVM虚拟机的启动是多线程的,JVM的启动相当于一个进程,在程序运行时除了一个主线程调用main方法外,垃圾回收线程也要启动,否则在创建很多对象时很容易会出现内存溢出。

②创建无返回值线程
通常有两种方式用于创建无返回值线程:实现Runnable接口和继承Thread类。
实现Runnable接口:
Runnable接口位于java.lang包中,这个接口非常简单,仅定义了一个run()方法,通常将想要在新线程中运行的代码写在这个方法中:public abstract void run();
实现Runnable接口创建新线程的步骤:

  • 新建一个类,该类实现了Runnable接口并重写了run()方法。run()方法中包含要在新线程中运行的代码
  • 使用刚刚编写的类创建Runnable接口类型的对象
  • 使用Thread类的构造方法创建Thread类的对象
  • 调用Thread类的start()方法来运行新线程

注:不要使用Thread类的run()方法,它不会创建新的线程。
run()和start()的区别:
run仅仅是封装被线程执行的代码,直接调用就是普通方法;run方法的直接调用就相当于普通方法的调用,等价于单线程的效果。
start首先启动了线程,然后再由JVM去调用该线程的run方法

当一个线程被连续调用多次start方法时,会出现I(i)llegalThreadStateException(非法的线程状态异常)。原因是已经启动的线程再次被调用启动。

继承Thread类:
Thread类位于java.lang包,它包含了很多与线程密切相关的方法,该类还实现了Runnable接口。使用Thread类创建新线程时,也需要重写run()方法。

建议使用实现Runnable接口来创建新线程,因为:

  • Java仅支持单继承,一旦一个类继承了其他类,则不能继承Thread类
  • 创建大量的Thread类对象,开销较大
  • Java SE 5.0版中新增的很多简化线程开发的类是使用Runnable接口的

③常用方法
public final String getName()
当使用多线程时可以用此方法来获取线程的名字来区分是哪个线程在运行。
public final void setName(String name)
设置当前线程的名字
public static Thread currentThread()
返回当前正在执行的线程对象,静态方法,适合不是Thread子类的程序使用。

④线程调度
线程有两种调度模型:
分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
抢占式调度模型:优先级高的线程先使用CPU,如果优先级相同,随机选择一个,优先级高的获得CPU的时间较多一些,但仍具有随机性。Java使用抢占式调度模型
public final int getPriority()
public final void setPriority(int newPriority)

第一个方法用来获取当前线程的优先级,第二个方法用来设置当前线程的优先级。线程的优先级默认是5,范围是1-10。一旦设置的参数newPriority超过了此范围就会出现IllegalArgumentException非法参数异常。
Thread中有关优先级的常量:
MAX_PRIORITY:10
MIN_PRIORITY:0
NORM_PRIORITY:5

④线程生命周期
线程是具有生命周期的,生命周期是由不同的状态组成。对于不同的状态,能够执行的操作不同。在Thread类的内部,定义了一个枚举State,其中包含了线程的全部状态:
在这里插入图片描述在这里插入图片描述

其关系图:
在这里插入图片描述

注:当使用new操作符创建Thread对象时,线程处于NEW状态。

⑤线程各属性

  • 线程优先级属性:Java中所有线程都具有优先级属性,默认的优先级与创建该线程的线程优先级相同。
  • 线程名称属性:每个线程在创建时为其指定名称。如果未指定,则由系统进行指定。对于主线程,名称是main。对于其他线程,名称默认样式是“Thread-数字”。
    注:不同的线程可以有相同的名称,不能使用线程名称来区分线程。
  • 线程ID属性:每个线程在创建时会被分配一个ID属性,使用Thread类的getId()方法获得线程ID。Thread类并没有提供修改ID的方法。
    注:不同的线程不能有相同的ID,系统是使用ID值来区分线程。
    -守护线程:守护线程的作用是为其他线程提供服务,如果其他线程运行完毕,即虚拟机中只剩下守护线程时,虚拟机会退出。
    注:不要让守护线程去访问文件等资源,它们可能在任何时候终止而资源却没有释放。

2.线程控制
线程创建以后需要进行启动、休眠、停止等控制。
①线程的休眠
Thread类的sleep()方法可以让线程进入休眠状态。在使用该方法时,需要指明线程休眠的时间,在时间到达后,线程会自动唤醒,该方法有两种形式:

  • 精度到毫秒
    public static void sleep(long millis)throws InterruptedException
    参数说明:millis是以毫秒为单位的休眠时间
  • 精度到纳秒
    public static void sleep(long millis,int nanos)throws InterruptedException
    参数说明:nanos是以纳秒为单位的休眠时间

注:这两个方法都受系统计时器和调度程序精度和准确性的影响,因此时间可能并不十分精确。这两个方法都会抛出InterruptedException,需要对其进行捕获或抛出。
1秒 = 103毫秒 = 106微秒 = 109纳秒

②线程的插队
通常情况下,线程调度器是根据线程优先级来分配资源。对于高优先级的线程,其获得资源的概率也很高。如果想控制线程的执行顺序,例如让某个线程先运行完毕,再运行其他线程,可以使用Thread类提供的join()方法,该方法有3种形式:

  • 无时间参数
    public final void join()throws InterruptedException
  • 精度到毫秒
    public final void join(long millis)throws InterruptedException
    参数说明:millis是以毫秒为单位的休眠时间
  • 精度到纳秒
    public final void join(long millis,int nanos)throwsInterruptedException
    参数说明:nanos是以纳秒为单位的休眠时间

③线程的暂停
public static void yield()
此方法可以暂停正在执行的线程,让其他线程先执行。在一定程度上可以实现让两个线程交替执行。

④线程的停止
线程通常是在run()方法运行完毕后停止运行,此外还可以使用循环+布尔值来让线程提前停止。虽然Thread类也提供了stop()方法来停止线程,但是由于该方法固有的不安全性,已经不推荐使用(已过时)。用来替换stop方法的是:public void interrupt(),此方法用来中断线程,并抛出一个InterruptedException异常。

⑤线程同步
在使用线程时必须注意两点:CPU的一次操作必须是原子性的;线程的随机性和延迟。
在使用线程时有以下原因可能导致问题:

  • 是否是多线程环境
  • 是否有共享数据
  • 是否有多条语句操作共享数据

此时,如果不使用同步,则有可能发生错误。
Java中提供了很多种实现同步的方式,所谓同步方法就是使用synchronized关键字修饰的方法。
synchronized(对象){需要同步的代码;}
同步可以解决安全问题的原因在于对象,这个对象就像一把锁一样。这就要求多个线程共用同一把锁。使用同步的前提必须是多个线程。
如果某个对象调用了同步方法,其他对象在该方法运行完毕前是不能再进入这个方法。这样则可以避免因为同时修改数据而引发的问题。在使用synchronized关键字修饰run()方法后,在一个线程运行完毕前,其他的线程并没有进入该方法。
可以将synchronized关键字放在方法的声明上,此时整个方法就成了一个同步的方法,方法的锁对象是this。注意此时方法不能是静态方法,因为当静态方法在内存中加载时对象this还不存在,可以将类的.class文件当做锁对象,因为它比静态先进内存。
虽然同步方法很简单,但是同步的开销很大,因为每个线程都需要去判断那个对象锁。因此应该尽量减少同步的代码。此时可以考虑使用同步块来减负。
可以使用volatile关键字来修饰成员变量。它的用处在于告诉虚拟机该成员变量可能已经被修改,需要进行确认才能使用,这样则避免了同步的需求。

⑥守护线程
public final void setDaemon(boolean on)
此方法将线程标记为守护线程,当正在运行的线程都是守护线程时,Java虚拟机退出。此方法要在线程启动前调用。举一个例子,将王者荣耀中的水晶定为主线程,英雄们设置为守护线程,当水晶消亡时,只剩下守护线程,所以游戏结束。

⑦等待与唤醒
线程的等待与唤醒使用的是Object类的方法,因为等待与唤醒方法是通过锁对象调用的,而线程中锁对象是不确定的,所以将方法定义在Object类中。
public final void wait() throws InterruptedException
public final void notify()
public final void notifyAll()

3.Lock类
①简介
加锁解决了线程同步的问题,但是看不到在哪里加上了锁,在哪里释放了锁,为了解决这个问题,JDK5出现了锁对象Lock。这是java.util.concurrent.locks包下的接口。

②常用方法
void lock()
void unlock()

在需要加锁的代码前创建Lock接口实现类对象使用lock方法加锁,在代码后使用unlock方法释放锁,这两个方法的中间就是要加锁的内容。由于要加锁的内容可能会出现问题所以常常用try…finally语句将代码包装,释放锁的代码放在finally语句中。

4.死锁
①简介
在多线程使用时如果出现了同步嵌套就会容易产生死锁问题。死锁就是指两个或两个以上的线程在执行的过程中因争夺资源产生的一种互相等待的现象。这种现象有一个经典的案例:中国人和美国人吃饭的问题。

②死锁实例

public class ThreadTest extends Thread {
	private boolean flag;

	public ThreadTest(boolean flag) {
		super();
		this.flag = flag;
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		if (flag) {
			synchronized (Test.o1) {
				System.out.println("is t1");
				synchronized (Test.o2) {
					System.out.println("is t2");
				}
			}

		} else {
			synchronized (Test.o2) {
				System.out.println("is t2");
				synchronized (Test.o1) {
					System.out.println("is t1");
				}
			}

		}
	}

}

public class Test {
	public static final Object o1 = new Object();
	public static final Object o2 = new Object();

	public static void main(String[] args) {
		ThreadTest tt1 = new ThreadTest(true);
		ThreadTest tt2 = new ThreadTest(false);

		tt1.start();
		tt2.start();
	}
}

5.线程组
①简介
Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。ThreadGroup类是java.lang包下的类。可以通过Thread类的
public final ThreadGroup getThreadGroup() 方法来获取当前线程所属的线程组。

②构造方法
public ThreadGroup(String name)
创建一个叫name的新线程组,且这个新线程组的父亲是当前正在运行的线程所属的线程组。
Thread类中有一个构造方法可以在创建线程的同时为它设置线程组:
public Thread(ThreadGroup group, String name)
group为线程组的名字,name为新线程的名字。

③常用方法
public final String getName()
获取当前线程组的名字,一个线程默认属于main线程组。

6.线程池
一个程序启动一个新线程需要消耗大量的资源,因为这要与操作系统进行交互,特别是当程序中的很多线程都是仅使用一次时,此时就需要线程池来解决这个问题。线程池里的每一个线程在使用结束后并不会死亡,而是回到线程池成为空闲状态等待下一个对象来使用。
JDK5之前,线程池需要手动实现;从JDK5开始,Java内置支持线程池。

7.定时器
定时器是一个线程工具,它可以调度多个定时任务以后台线程的方式运行。在Java中,定时器是Timer类,定时器任务是TimerTask类,它们是java.util包下的类。

8.线程应用
使用多线程编程的原因之一是提高程序的响应性能,在Swing中尤为重要。对于用户而言,更希望程序能够时刻响应自己的各种操作。
Swing不是线程安全的。如果希望在多个线程中操作同一控件,很可能让程序崩溃。
①EventQueue与线程分配
EventQueue是一个与平台无关的类,它是Swing的一个重要组成部分,负责AWTEvent类及其子类的分发。首先将所有事件放置在一个队列中,然后使用dispatchEvent()方法来依次执行各个事件。
Swing中的事件如果耗时过长,就会让程序暂时死掉。就是因为使用dispatchEvent()方法来依次执行各个事件。为了将Swing程序在事件分配线程中运行,需要使用invokeLater()方法:方法声明:public static void invokeLater(Runnable runnable) 参数说明:runnable是Runnable对象,其run方法应该在EventQueue上同步执行
②SwingWorker类的使用
对于Swing中的耗时任务,可以使用Java SE 6.0版中提供的SwingWorker类来实现。它是Swing中专门用于执行耗时任务的抽象类。

Thread 和 Runnable 的关系

从Thread的源码看:

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

Thread初始化时传入Runnable对象后,会调用init方法。
在这里插入图片描述
当此线程对象start后,会调用run方法。

public void run() {
    if (target != null) {
        target.run();
    }
}

这个是典型的策略模式。

Callable 、Future 和 FutureTask

对于 Thread 和 Runable,其 run() 都是无返回值的,并且无法抛出异常,所以当需要返回多线程的数据,就需要借助 Callable 和 Future。

Callable

在这里插入图片描述

FutureTask

FutureTask 继承了 RunnableFuture,RunnableFuture 继承了 Runnable 和 Future。

在这里插入图片描述

所以,FutureTask 也是个 Runnable !

既然 FutureTask 是个 Runnable,肯定就需要实现 FutureTask.run() 方法,那么 FutureTask 也可以作为 Thread 的初始化入参。

new Thread(FutureTask对象).start();

Callable 和 FutureTask 的关系

FutureTask 初始化时,Callable 必须作为 FutureTask 的初始化入参。

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

当执行 FutureTask.run() 时,执行的是 Callable.call()。
在这里插入图片描述
这里又是一个典型的策略模式 !

Thread 、Runnable、FutureTask 和 Callable 的关系

  • Thread.run() 执行的是 Runnable.run()
  • FutureTask 继承了 Runnable,并实现了 FutureTask.run()
  • FutureTask.run() 执行的是 Callable.run()
  • 依次传递,最后 Thread.run(),其实是执行的 Callable.run()

所以整个设计方法,其实就是 2 个策略模式,Thread 和 Runnable 是一个策略模式,FutureTask 和 Callable 又是一个策略模式,最后通过 Runnable 和 FutureTask 的继承关系,将这 2 个策略模式组合在一起。

Future

通过 FutureTask,借助 Thread 执行线程后,结果数据可以通过Future获取。

FutureTask实现了RunnableFuture接口,RunnableFuture接口继承了Future接口。

public interface Future<V> {
    // 取消任务,如果任务正在运行的,mayInterruptIfRunning为true时,表明这个任务会被打断的,并返回true;
    // 为false时,会等待这个任务执行完,返回true;若任务还没执行,取消任务后返回true,如任务执行完,返回false
    boolean cancel(boolean mayInterruptIfRunning);
    // 判断任务是否被取消了,正常执行完不算被取消
    boolean isCancelled();
    // 判断任务是否已经执行完成,任务取消或发生异常也算是完成,返回true
    boolean isDone();
    // 获取任务返回结果,如果任务没有执行完成则等待完成将结果返回,如果获取的过程中发生异常就抛出异常,
    // 比如中断就会抛出InterruptedException异常等异常
    V get() throws InterruptedException, ExecutionException;
    // 在规定的时间如果没有返回结果就会抛出TimeoutException异常
    V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

对于 FutureTask,Callable 就是他的任务,而 FutureTask 内部维护了一个任务状态,所有的状态都是围绕这个任务来进行的,随着任务的进行,状态也在不断的更新。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值