Java并发之线程
Posted 纸短情长,伏惟珍重.
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发之线程相关的知识,希望对你有一定的参考价值。
目标:
- 线程的状态
- 线程的几种实现方式
- 三个线程轮流打印ABC十次
- 判断线程是否销毁
- yield功能
- 给定三个线程t1,t2,t3,如何保证依次执行
1.基本概念
程序:是一个静态的概念;
进程:是一个动态的概念
a.进程是程序的一次动态执行过程,占用特定的地址空间;
b.每个进程都是独立的,包括三部分:CPU code data
c.缺点:内存的浪费,增加cpu的负担
线程:Thread,是进程中的一个\'单一的连续控制流程/执行路径\';
a.线程又被称为轻量级进程;
b.Thread run at the same time,independently of one Another (线程同时运行,彼此独立)
c.一个进程可以拥有多个并行的线程
d.一个进程中的线程共享相同的内存单元/可以变换的内存空间地址-->可以访问相同的变量和对象,而且他们从统一堆中分配对象|通信|数据交换|同步操作
e.由于线程间的通信是在同一内存地址空间中进行的,不需要额外的通信机制,这就使线程间的通信更简便而且信息传递的速度也更快
2.线程的启动
2.1 实现Runnable接口
a.自定义一个线程 实现Runnable的run()方法 run()方法就是要执行的内容,会在另一个分支进行.Thread类本身也实现了Runnable接口
b.主方法中new一个自定义线程对象,然后new一个Thread类对象,其构造方法的参数就是自定义线程对象
c.执行Thread类的start方法,线程开始执行 自此产生了分支,一个分支会执行run方法,在主方法中不会等待run方法调用完毕返回才继续执行,而是直接继续执行,是第二个分支。这两个分支并行运行
PS:这里运用了静态代理模式: Thread类和自定义线程类都实现了Runnable接口 Thread类是代理Proxy,自定义线程类是被代理类 通过调用Thread的start方法,实际上调用了自定义线程类的start方法(当然除此之外还有其他的代码)
2.2 继承Thread类
a.自定义一个类MyThread,继承Thread类,重写run方法
b.在main方法中new一个自定义类,然后直接调用start方法 两个方法比较而言第二个方法代码量较少 但是第一个方法比较灵活,自定义线程类还可以继承其他的类,而不限于Thread类
2.3 实现Callable接口
3. 线程的状态
初始态:NEW
创建一个Thread对象,但还未调用start()启动线程时,线程处于初始态。
就绪态 READY
在Java中,运行态包括就绪态 和 运行态。
该状态下的线程已经获得执行所需的所有资源,只要CPU分配执行权就能运行。 所有就绪态的线程存放在就绪队列中。
运行态 RUNNING
获得CPU执行权,正在执行的线程。 由于一个CPU同一时刻只能执行一条线程,因此每个CPU每个时刻只有一条运行态的线程。
阻塞态 BLOCKED
阻塞态专指请求排它锁失败时进入的状态。
等待态 WAITING
当前线程中调用wait、join、park函数时,当前线程就会进入等待态。 进入等待态的线程会释放CPU执行权,并释放资源(如:锁),它们要等待被其他线程显式地唤醒。
超时等待态 TIME_WAITING
当运行中的线程调用sleep(time)、wait、join、parkNanos、parkUntil时,就会进入该状态; 进入该状态后释放CPU执行权 和 占有的资源。 与等待态的区别:无需等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。
终止态
线程执行结束后的状态。
4. 线程的方法
getName
-
- new 一个子类对象的同时也new了其父类的对象,只是如果不显式调用父类的构造方法super(),那么会自动调用无参数的父类的构造方法。 可以在自定义类MyThread中(继承自Thread类)中写一个构造方法,显式调用父类的构造方法,其参数为一个字符串,表示创建一个以该字符串为名字的Thread对象。
- 效果是创建了一个MyThread对象,并且其父类Thread对象的名字是给定的字符串。
- 如果不显式调用父类的构造方法super(参数),那么默认父类Thread是没有名字的。
isAlive
isAlive活着的定义是就绪、运行、阻塞状态 线程是有优先级的,优先级高的获得Cpu执行时间长,并不代表优先级低的就得不到执行
sleep(当前线程.sleep)
sleep时持有的锁不会自动释放,sleep时可能会抛出InterruptedException。 Thread.sleep(long millis) 一定是当前线程调用此方法,当前线程进入TIME_WAIT状态,但不释放对象锁,millis后线程自动苏醒进入READY状态。作用:给其它线程执行机会的最佳方式。
join(其他线程.join)
t.join()/t.join(long millis) 当前线程里调用线程1的join方法,当前线程进入WAIT状态,但不释放对象锁,直到线程1执行完毕或者millis时间到,当前线程进入可运行状态。 join方法的作用是将分出来的线程合并回去,等待分出来的线程执行完毕后继续执行原有线程。类似于方法调用。(相当于调用thead.run())
yield(当前线程.yield)
Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的cpu时间片,由运行状态变会可运行状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。
interrupt(其他线程.interrupt)
-
- 调用Interrupt方法时,线程的中断状态将被置位。这是每一个线程都具有的boolean标志; 中断可以理解为线程的一个标志位属性,表示一个运行中的线程是否被其他线程进行了中断操作。这里提到了其他线程,所以可以认为中断是线程之间进行通信的一种方式,简单来说就是由其他线程通过执行interrupt方法对该线程打个招呼,让起中断标志位为true,从而实现中断线程执行的目的。
- 其他线程调用了interrupt方法后,该线程通过检查自身是否被中断进行响应,具体就是该线程需要调用Thread.currentThread().isInterrupted方法进行判断是否被中断或者调用Thread类的静态方法interrupted对当前线程的中断标志位进行复位(变为false)。需要注意的是,如果该线程已经处于终结状态,即使该线程被中断过,那么调用isInterrupted方法返回仍然是false,表示没有被中断。
- 那么是不是线程调用了interrupt方法对该线程进行中断,该线程就会被中断呢?答案是否定的。因为Java虚拟机对会抛出InterruptedException异常的方法进行了特别处理:Java虚拟机会将该线程的中断标志位清除,然后抛出InterruptedException,这个时候调用isInterrupted方法返回的也是false。
interrupt一个其他线程t时
-
- 1)如果线程t中调用了可以抛出InterruptedException的方法,那么会在t中抛出InterruptedException并清除中断标志位。
- 2)如果t没有调用此类方法,那么会正常地将设置中断标志位。
如何停止线程?
-
- 1)在catch InterruptedException异常时可以关闭当前线程;
- 2)循环调用isInterrupted方法检测是否被中断,如果被中断,要么调用interrupted方法清除中断标志位,要么就关闭当前线程。
- 3)无论1)还是2),都可以通过一个volatile的自定义标志位来控制循环是否继续执行
但是注意! 如果线程中有阻塞操作,在阻塞时是无法去检测中断标志位或自定义标志位的,只能使用1)的interrupt方法才能中断线程,并且在线程停止前关闭引起阻塞的资源(比如Socket)。
wait(对象.wait)
-
- 调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) 代码段内。
- obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout)timeout时间到自动唤醒。
- 调用wait()方法的线程,如果其他线程调用该线程的interrupt()方法,则会重新尝试获取对象锁。只有当获取到对象锁,才开始抛出相应的InterruptedException异常,从wait中返回。
notify(对象.notify)
obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。
wait¬ify 最佳实践
等待方(消费者)和通知方(生产者)
等待方:
synchronized(obj){
while(条件不满足){
obj.wait();
}
消费;
}
通知方:
synchonized(obj){
改变条件;
obj.notifyAll();
}
-
-
1)条件谓词:
-
将与条件队列相关联的条件谓词以及在这些条件谓词上等待的操作都写入文档。
-
在条件等待中存在一种重要的三元关系,包括加锁、wait方法和一个条件谓词。在条件谓词中包含多个状态变量,而状态变量由一个锁来保护,因此在测试条件谓词之前必须先持有这个锁。锁对象和条件队列对象(即调用wait和notify等方法所在的对象)必须是同一个对象。
-
当线程从wait方法中被唤醒时,它在重新请求锁时不具有任何特殊的优先性,而要去其他尝试进入同步代码块的线程一起正常地在锁上进行竞争。
-
每一次wait调用都会隐式地与特定的条件谓词关联起来。当调用某个特定条件谓词的wait时,调用者必须已经持有与条件队列相关的锁,并且这个锁必须保护着构成条件谓词的状态变量。
-
2)过早唤醒: 虽然在锁、条件谓词和条件队列之间的三元关系并不复杂,但wait方法的返回并不一定意味着线程正在等待的条件谓词已经变成真了。 当执行控制重新进入调用wait的代码时,它已经重新获取了与条件队列相关联的锁。现在条件谓词是不是已经变为真了?或许。在发出通知的线程调用notifyAll时,条件谓词可能已经变成真,但在重新获取锁时将再次变成假。在线程被唤醒到wait重新获取锁的这段时间里,可能有其他线程已经获取了这个锁,并修改了对象的状态。或者,条件谓词从调用wait起根本就没有变成真。你并不知道另一个线程为什么调用notify或notifyAll,也许是因为与同一条件队列相关的另一个条件谓词变成了真。一个条件队列与多个条件谓词相关是一种很常见的情况。 基于所有这些原因,每当线程从wait中唤醒时,都必须再次测试条件谓词。
-
3)notify与notifyAll: 由于多个线程可以基于不同的条件谓词在同一个条件队列上等待,因此如果使用notify而不是notifyAll,那么将是一种危险的操作,因为单一的通知很容易导致类似于信号地址(线程必须等待一个已经为真的条件,但在开始等待之前没有检查条件谓词)的问题。
-
只有同时满足以下两个条件时,才能用单一的notify而不是notifyAll:
-
- 1)所有等待线程的类型都相同。只有一个条件谓词与条件队列相关,并且每个线程在从wait返回后将执行相同的操作。
- 2)单进单出:在对象状态上的每次改变,最多只能唤醒一个线程来执行。
suspend resume stop destroy(废弃方法)
-
- 线程的暂停、恢复、停止对应的就是suspend、resume和stop/destroy。
- suspend会使当前线程进入阻塞状态并不释放占有的资源,容易引起死锁;
- stop在结束一个线程时不会去释放占用的资源。它会直接终止run方法的调用,并且会抛出一个ThreadDeath错误。
- destroy只是抛出一个NoSuchMethodError。
- suspend和resume已被wait、notify取代。
-
判断当前线程是否正在执行 注意优先级是概率而非先后顺序(优先级高可能会执行时间长,但也不一定)
线程优先级特性:
- 继承性 比如A线程启动B线程,则B线程的优先级与A是一样的。
- 规则性 高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。
- 随机性 优先级较高的线程不一定每一次都先执行完。 注意,在不同的JVM以及OS上,线程规划会存在差异,有些OS会忽略对线程优先级的设定。
守护线程
- 将线程转换为守护线程
- 守护线程的唯一用途是为其他线程提供服务。比如计时线程,它定时发送信号给其他线程;
- 当只剩下守护线程时,JVM就退出了。
- 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
- 注意!Java虚拟机退出时Daemon线程中的finally块并不一定会被执行。
未捕获异常处理器
在Runnable的run方法中不能抛出异常,如果某个异常没有被捕获,则会导致线程终止。
要求异常处理器实现Thread.UncaughtExceptionHandler接口。 可以使用setUncaughtExceptionHandler方法为任何一个线程安装一个处理器, 也可以使用Thread.setDefaultUncaughtExceptionHandler方法为所有线程安装一个默认的处理器;
如果不安装默认的处理器,那么默认的处理器为空。如果不为独立的线程安装处理器,此时的处理器就是该线程的ThreadGroup对象 ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,它的uncaughtException方法做如下操作:
- 1)如果该线程组有父线程组,那么父线程组的uncaughtException方法被调用。
- 2)否则,如果Thread.getDefaultExceptionHandler方法返回一个非空的处理器,则调用该处理器。
- 3)否则,如果Throwable是ThreadDeath的一个实例(ThreadDeath对象由stop方法产生,而该方法已过时),什么都不做。
- 4)否则,线程的名字以及Throwable的栈踪迹被输出到System.err上。
如果是由线程池ThreadPoolExecutor执行任务,只有通过execute提交的任务,才能将它抛出的异常交给UncaughtExceptionHandler,而通过submit提交的任务,无论是抛出的未检测异常还是已检查异常,都将被认为是任务返回状态的一部分。如果一个由submit提交的任务由于抛出了异常而结束,那么这个异常将被Future.get封装在ExecutionException中重新抛出。
以上是关于Java并发之线程的主要内容,如果未能解决你的问题,请参考以下文章