多线程(一)

下文为JAVA并发的学习笔记,部分学习自《Java核心技术 卷I》并发章节

一、什么是线程

进程是系统分配资源的最小单位,线程是系统调度的最小单位。一个进程内的线程之间是可以共享资源的。每个进程至少有一个线程存在,即主线程。

多线程程序在更低一层 中扩展了多任务的概念:单个程序看起来在同时完成多个任务。每个任务在一个线程(Thread)中执行。线程是控制线程的简称。如果一个线程可以同时运行多个线程,就称这个程序是多线程的。

1.1创建线程

方法1-继承Thread类

可以通过继承Thread来创建一个线程类,该方法的好处是this代表的就是当前线程,不需要通过Thread.currentThread()来获取当前线程的引用。

class MyThread extends Thread{
	@Override
	public void run(){
		task code; // 任务代码
	}
}
Mythread t = new MyThread();
t.start(); // 线程进入可运行状态

方法2-实现Runnable接口

通过实现Runnable接口,并且调用Thread构造方法时将Runnable对象作为target参数传入来创建线程对象。该方法的好处是可以规避类的单继承的限制,但需要通过Thread.currentThread()来获取当前线程的引用。

class MyRunnable implements Runnable{
	@Override
	public void run(){
		task code; // 任务代码
	}
}
Thread t = new Thread(new Runnable);
t.start();

1.2Thread类及常见方法

Thread类是JVM管理线程的一个类,换句话说,每个线程都有一个唯一的Thread对象与之关联。

每个执行流,需要一个对象来描述,而Thread类的对象就是用来描述一个线程执行流的,JVM会将Thread对象组织起来,用于线程调度,线程管理。

1.2.1Thread的常见构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用Runnable对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target,String name)使用Runnable对象创建线程对象,并命名

1.2.2Thread的几个常见属性

属性获取方法
IDgetID()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  • ID是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况
  • 优先级高得线程理论上来说更容易被调度上。

1.2.3启动一个线程-start()

警告:不要调用Thread类或Runnable对象的run方法。直接调用run方法只会在同一个线程中执行这个任务——而没有启动线程。实际上应当调用Thread.start方法,这会创建一个执行run方法的新线程。

1.2.4中断一个线程

目前常见的有以下两种方式:

1.通过共享的标记来进行沟通

2.调用Interrupt()方法来通知

1.thread对象调用interrupt()方法通知该线程停止运行。

2.thread收到通知的方式有两种:

​ 1.如果线程调用了wait/join/sleep等方法而阻塞挂起,则以InterruptedException异常的形式通知,清除中断标志。

​ 2.否则只是内部的一个中断标志被设置,thread可以通过

​ (1)Thread.interrupted()判断当前线程的中断标志被设置,清除中断标志

​ (2)Thread.currentThread().isInterrupted()判断指定线程的中断标志被设置,不清除中断标识。

方法说明
public void interrupt()中断对象关联的线程,如果线程正在阻塞,则以异常的方式通知,负责设置标志位
public static boolean interrupted()判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isInterrupted()判断对象关联的线程的标志位是否设置,调用后不清除标志位

1.2.5等待一个线程-jion()

有时候,我们需要一个线程完成它的工作后,才能进行自己的下一步工作。

方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束,最多等millis毫秒
public void join(long millis,int nanos)同理,但可以更高精度

1.2.6休眠当前线程

因为线程的调度是不可控的,所以,这个方法只能保证休眠时间是大于等于休眠时间的。

方法说明
public static void sleep(long millis) throws InterruptedException休眠当前线程的millis毫秒
public static void sleep(long millis,int nanos) throws InterruptedException可以更高精度的休眠

二、线程状态

线程可以有如下六种状态:

  • New(新建)
  • Runnable(可运行)
  • Blocked(阻塞)
  • Waiting(等待)
  • Timed Waiting(计时等待)
  • Terminated(终止)

在这里插入图片描述

2.1新建线程

当用new操作符创建一个线程时候,这个线程还没有开始运行。这意味着它的状态是新建(new)。当一个线程处于新建状态时,程序还没有开始运行线程中的代码。在线程开始之前还有一些基础工作需要做。

2.2可运行状态

一旦调用了start方法,线程就处于可运行状态。一个可运行的线程可能正在运行也可能没有运行。要由操作系统为线程提供具体的运行时间。正是因为这样,这个状态叫做“可运行”而不是“运行”。

2.3阻塞和等待线程

当线程处于阻塞和等待状态时,它暂时是不活动的。它不运行任何代码,而且消耗最少的资源。要由线程调度器重新激活这个线程。具体细节取决于他是怎么到达非活动状态的。

  • 当一个线程试图获取一个内部的对象锁,而这个线程目前被其它线程占有,该线程就会被阻塞。当所有其他线程都释放了这个锁,并且线程调度器允许该线程持有这个锁时,它将变成非阻塞状态。
  • 当线程等待另一个线程通知调度器出现一个条件时,这个线程会进入等待状态。实际上,阻塞状态与等待状态并没有太大区别。
  • 有几个方法有超时参数,调用这些方法会让线程进入计时等待状态。

2.4终止线程

线程会由于以下两个原因之一而终止:

  • run方法正常退出,线程自然终止。
  • 因为一个没有捕获的异常终止了run方法,使线程意外终止。

三、线程属性

3.1中断线程

当线程的run方法执行方法体最后一句后再执行return语句返回时,或者出现了方法中没有捕获的异常时,线程将终止。

除了已经废除的stop方法,没有办法可以强制线程终止。不过,interrupt方法可以用来请求终止一个线程。

当对一个线程调用interrupt方法时,就会设置线程的中断状态。这是每个线程都有的boolean标志。每个线程都应该不时地检查这个表示,以判断线程是否被中断。

要想得出是否设置了中断状态,首先调用静态地Thread.currentThread方法获得当前线程,然后调用isInterrupted方法。

但是,如果线程被阻塞,就无法检查中断状态。这里就要引入InterruptedException异常。当在一个被sleep或wait调用interrupt方法时,那个阻塞调用将被一个InterrupedException中断。

没有任何语言要求被中断的线程应当终止。中断一个线程只是要引起它的注意。被中断的线程可以决定如何响应中断。某些线程非常重要,所以应当处理这个异常。然后再继续执行。但是更普遍的情况是,线程只希望中断解释为一个终止请求。

注意区分:interrupt,interupted,isInteerrupted

3.2守护线程

守护线程的唯一用途是为其他线程提供服务。可以通过

t.setDaemon(true);

将一个线程转换为守护线程。

当只剩下守护线程时候,虚拟机就会退出。因为如果只剩下守护线程。就没有必要继续运行程序了。

3.3未捕获异常的处理器

四、同步

在大多数实际的多线程应用中,两个或两个以上的线程需要共享对同一数据的存取。如果两个线程存取同一个对象,并且每个线程分别调用一个修改该对象状态的方法,会发生什么?可以相见,这两个线程会相互覆盖。取决于线程访问数据的次序,可能会导致对象被破坏。这种情况称为竞态条件(race condition)。经常被用来做例子的就是抢票和银行转账了,有兴趣自己查下。

在银行转账的例子中,当两个线程试图同时更新一个账户时,就会出现这个问题。假设两个线程同时执行如下指令。

accounts[to] += amount;

问题在于这不是原子操作。这个指令可能如下处理:

1.将accounts[to]加载到寄存器上

2.增加account。

3.将结果写回accounts[to]

由于线程可能在任何一条指令上被中断,由此引出多线程带来的风险-线程安全。

如果多线程环境下运行的结果是符合我们预期的,即在单线程环境应该的结果,就说这个线程是安全的。

线程不安全的因素可以归结为三个:

4.1多线程不安全的原因

4.1.1原子性

前面已经解释过什么叫原子性。

一条java语句不一定是原子的,也不一定只是一条指令

我们看到的n++,其实是由三步操作组成的:

1.从内存把数据读到CPU

2.进行数据更新

3.将数据写会CPU

不保证原子性会给多线程带来什么问题:

如果一个线程正在对一个变量操作,中途其他线程插入进来,如果这个操作被打断了,结果就可能是错误的。

4.1.2可见性

在这里插入图片描述
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。

4.1.3代码顺序性(指令重排序)

Java语言规范JVM线程内部维持顺序化语义,即只要程序的最终结果与它的顺序化情况结果相等,那么指令的执行顺序可以与逻辑顺序不一致.这个过程就叫做指令重排序.

指令重排序的意义:使指令更加符合CPU的执行特性,最大限度发挥机器的性能,提高程序的执行效率.

源代码到最终执行的指令序列示意图:

在这里插入图片描述

指令重排序主要分三种:

1.编译器重排序:JVM中进行完成的

2.指令级并行重排序

3.处理器重排序

指令重排序会给多线程带来什么问题

还是刚才转账的那个例子中,单线程是没问题的,优化是正确的,但在多线程场景下就有问题了,什么问题呢?

如果将123的顺序改变132就会产生错误,这是显而易见的.

4.2锁对象

有两种机制可以防止并发访问代码块.Java语言提供了一个Synchronized关键字来达到这一目的,另外java5引入了ReetrantLock类.Synchronized关键字提供了一个锁以及相关的"条件",对于大多数需要显式锁的情况,这种机制功能很强大.

4.3条件对象

4.5synchronized关键字

在开始之前,先对锁和条件来做一个总结:

  • 锁是用来保护代码片段,一次只能有一个线程执行被保护的线程
  • 锁可以管理试图进入被保护代码段的程序
  • 一个锁可以有一个或多个相关联的条件对象
  • 每个条件对象管理哪些已经进入被保护代码段但还不能运行的线程.

从1.0版本开始,Java中的每个对象都有一个内部锁.如果一个方法声明时有synchronized关键字,那么对象的锁将保护整个方法.也就是说,要调用这个方法,线程必须获得内部对象锁.

内部锁和条件存在一些限制.包括:

  • 不能中断一个正在尝试获得锁的线程.
  • 不能指定尝试获得锁时的超时时间.
  • 每个锁仅有一个锁是不够的

在代码中应该使用哪一种做法?Lock和Condition对象还是同步方法?下面是一些建议:

  • 最好既不使用Lock/Condition也不使用synchronized关键字.在许多情况下.可以使用Java.util.concurrent包中的某种机制 .它会为你处理所有锁定
  • 如果synchronized关键字适合你的程序,那么尽量使用这种方法,这样可以减少编写的代码量,还能减少出错的概率.
  • 如果特别需要Lock/Condition结构提供的额为能力,则使用Lock/Condition

总结下synchronized关键字的作用:

  • 当线程释放锁时,JMM会把该线程对应的工作内存种的共享变量刷星到主内存种
  • 当线程获取锁时,JMM会把线程对应的本地内存置为无效.从而使得被监视保护的临界区代码必须从主内存种读取共享变量.

4.6同步块

Java对象都有一个锁.线程可以通过调用同步方法获得锁.还有另一种机制获得锁:即进入一个同步块.

在这里,创建lock对象只是为了使用每个Java对象拥有的锁.

有时程序员使用一个对象的锁来实现额为的原子操作,这种做法称为客户端锁定.客户端锁定是非常脆弱的,通常不推荐使用.

4.7监视器概念

4.8volatile字段

有时,如果只是为了读写一两个实例字段而使用同步,所带来的开销好像有些划不来.毕竟,什么地方能出错呢?遗憾的是,使用现代的处理器与编译器,出错的可能性很大.

如果你使用锁来保护可能被多个线程访问的代码,那么不存在这个问题.编译器被要求在必要的时候刷新本地缓存来支持锁,而且不能不相应的重新相应的重新排列指令顺序.

volatile关键字为实例字段的同步访问提供了一种免锁机制.如果声明一个字段为volatile,那么编译器和虚拟机就知道该字段可能被另一个线程并发更新.保证了可见性,但volatile变量不能提供原子性.

4.9final变量

还有一种情况可以安全的访问一个共享字段,即这个字段声明为final.考虑以下声明:

final varaccounts = new HashMap<String,Double>();

其他线程会在构造器完成构造之后才看到这个accounts的变量.

五、线程安全的集合

六、任务和线程池

构造一个新的线程开销有些大,因为这涉及与操作系统的交互.如果你的程序中创建了大量的生命期很短的线程,那么不应该把每个任务映射到一个单独的线程,而应该使用线程池(Thread pool).线程池中包含了许多准备运行的线程.为线程池提供一个Runnable,就会有一个线程调用run方法.当run方法退出时,这个线程不会死亡,而是留在池中准备为下一个请求提供服务.\

6.1Callable与Future

6.2执行器

执行器类(Executors)有许多静态工厂,用来构造线程池,下表对这些方法进行了汇总:

方法描述
newCachedThreadPool必要时创建新线程;空闲线程会保留60秒
newFixedThreadPool池中包含固定数目的线程;空闲线程会一直保留
newWorkStelingPool一种适合"fork-join"任务的线程池,其中复杂的任务会分解为更简单的任务,空闲线程池会"密取"较简单的任务
newSingleThreadExecutor只有一个线程的池,会顺序执行所提交的任务
newScheduledThreadPool用于调度执行的固定线程池
newSingleThreadScheduledExector用于调度执行的单线程"池"

6.3控制任务组

6.4fork-join框架

一些应用可能对每个处理器内核分别使用一个线程,以完成计算密集型任务,如图像或视频处理.

在后台,fork-join框架使用了一种有效的智能方法来平衡可用线程的工作负载,这种方法称为工作密取(work stealing).每个工作线程都有一个双端队列来完成任务.一个工作线程将子任务压入其双端队列的队头.一个工作线程空闲时,它会从另一个双端队列的队尾"密取"一个任务.由于大的任务都在队尾,这种密取很少出现.

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值