最近被多线程的题目搅的有点混乱,写一篇整理一下思路,顺便与大家分享,纯属个人理解,要是有什么出入,欢迎大家指点。
参考文献:《操作系统》 第四版 西安电子科技大学出版
博客:https://www.cnblogs.com/snow-flower/p/6114765.html
百度学术尚智堂课件
不定期更新。
我们知道多线程大概有三种实现方式:继承thread类,实现runnable接口,实现callable接口。
然后再各自重写相应函数,如下:
package moreThread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class justtest extends Thread{
public static void main(String []args){
TryThread tryThread=new TryThread();
tryThread.start();
Runnable tRunnable=new Tryrunnable();
Thread thread=new Thread(tRunnable);
thread.start();
Callable callable=new TryCallable();
FutureTask task=new FutureTask(callable);
Thread thread2=new Thread(task);
thread2.start();
try {
System.out.println(task.get());
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
} catch (ExecutionException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
class Tryrunnable implements Runnable{
public void run(){
System.out.println("我是接口线程");
}
}
class TryThread extends Thread{
public void run(){
System.out.println("我是继承线程");
}
}
class TryCallable implements Callable{
public String call(){
System.out.println("我是回调线程");
return "我是回调线程的返回值";
}
}
使用的方式很简单,也很容易理解,将类实例化,然后调用start函数就好了。
//----------------------------------------------------------------------------------------------------------------
我们下面要探讨的是其他的。
从硬件上讲,我们知道,CPU是非常非常非常非常非常快的,以至于他不可能一直只为一件事情服务,所以就划分为多个时间片,为多个进程服务,当CPU处理完一轮乃至几十轮时间片的时候我们是还没有发觉的,所以,即使CPU是序列的处理这些时间片,我们认为这是“并发的”。
进程和线程有什么区别呢? 线程是对进程的细分,进程是一个比较大的说法,比方说,我们打开了QQ,就创建了个进程,打开了微信,又多了个进程,然后QQ打开了很多聊天窗口,就多了很多线程。
然后就是一个线程的生命周期:
一般来说,线程分为五个状态(更详细的理论及其调度可见操作系统的书):
1.新建状态:
我们new出一个线程对象的时候,这时候的线程对象就是新建状态。当然,处于新生状态的线程有自己的内存空间,我们使用start方法可以使其进入就绪状态。要是对已经启动的线程再次调用start方法,会报非法线程状态异常。
2.就绪状态:
处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU时间片,处于线程就绪队列(操作系统的书上将其成为队列,但是涉及到优先级的话,称为可运行池更为合适),等待CPU时间。等待并不是执行状态,当系统选定一个等待执行的Thread对象后(即CPU调度),它会从等待状态进入到执行状态。一旦获得CPU时间,线程就进入运行状态并自动调用自己的run方法。
提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。
3.运行状态:
处于运行状态的线程有多种转换的状态,十分复杂,可以转变为阻塞状态,就绪状态和死亡状态
。
处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出CPU资源,再次变为就绪状态。
那什么时候线程从运行状态变成阻塞状态呢?当发生以下几种情况的时候:
①线程调用sleep方法主动放弃占用的系统资源。
②线程调用一个阻塞式IO方法(IO方法速度远比CPU慢,不阻塞会极大影响效率),在该方法返回之前,该线程被阻塞。
③线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
④线程在等待某个通知(notify)。
⑤程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。
4.阻塞状态:
处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的IO设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。有三种方法可以暂停Thread的执行:sleep,yield,join。
①sleep:不会释放锁,sleep时别的线程也不可以访问锁定对象。
②yield:让出CPU的使用权,从运行态直接进入就绪状态。让CPU调度其他线程进入运行状态。
③join:当某一个线程等待另一个线程执行结束后,才继续执行时,使调用该方法的线程在此之前执行完毕,也就是等待调用该方法的线程执行完毕后再往下继续执行。
5.死亡状态:
当线程的run方法执行完,或者被强制性的终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。如果在一个死去的线程上调用start方法,会抛出非法线程状态异常。
//----------------------------------------------------------------------------------------------------------------
下面介绍在java中如何去使用管理一个线程。
1.线程睡眠(sleep)
如果我们需要让
当前正在执行的线程
暂停一段时间,并进入阻塞状态,则可以通过调用Thread的sleep方法。
①sleep是
静态方法
,最好不要用实例化对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。
②
Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。但是不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。
2.线程让步(yield)
yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。
实际上,当某个线程调用yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉cpu调度线程。
sleep()方法和yield()方法的区别:
①sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态;而yield方法调用后,直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。
②sleep方法声明抛出InterruptedException,所以调用sleep要捕获该异常,或者显示声明抛出该异常,而yield方法则没有声明抛出任务异常。
③sleep比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。
3.线程合并(join)
线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕后才能执行时(前驱后继),Thread类提供了join方法来完成这个功能,它不是
静态方法。
他有三个重载
void
join()
当前线程等该加入该线程后面,等待该线程终止。
void
join(
long
millis)
当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
void
join(
long
millis,
int
nanos)
等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
4.设置线程的优先级
每个线程执行时都有优先级的属性,顾名思义,优先级高的线程自然有较多的机会执行。与线程sleep类似,线程的优先级仍然无法保障线程的执行次序,只不过,优先级的线程获取CPU资源的概率较大,优先级低的并非没机会执行。
每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。
注:Thread类提供了 setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,设置的范围1~10间。也可以使用thread提供的三个静态常量:
MAX_PRIORITY=10
MIN_PRIORITY=1
NORM_PRIORITY=5
5.后台(守护)线程
守护线程使用的情况比较少,但并非无用,举例如,JVM的垃圾回收,内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数,超时时间,状态等等。调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。守护线程的用途为:
• 守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。
• Java的垃圾回收也是一个守护线程。守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。
6.正确结束线程
Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的!想要安全有效的结束一个线程,可以使用下面的方法:
=》正常执行完run方法,然后结束掉
=》控制循环条件和判断条件的标识符来结束掉线程
//----------------------------------------------------------------------------------------------------------------
接下来介绍一下
线程同步
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性的准确性(操作系统中wait和signal原语的作用)
1.同步方法
即有
synchronized关键字
修饰的方法。由于java的每个对象都有一个
内置锁
,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
注:synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
2.同步代码块
即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
public class Bank {
private int count =0;//账户余额
//存钱
public void addMoney(int money){
synchronized (this) {
count +=money;
}
System.out.println(System.currentTimeMillis()+"存进:"+money);
}
//取钱
public void subMoney(int money){
synchronized (this) {
if(count-money < 0){
System.out.println("余额不足");
return;
}
count -=money;
}
System.out.println(+System.currentTimeMillis()+"取出:"+money);
}
//查询
public void lookMoney(){
System.out.println("账户余额:"+count);
}
}
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
3.使用特殊域变量(volatile)实现线程同步
volatile关键字为语变量的访问提供了一种免锁机制;
使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新;
因此每次使用该域就要重新计算,而不是使用寄存器中的值;
volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
注:多线程中的非同步问题主要出现在对语的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。用final域,有锁保护的域和volatile域都可以避免非同步的问题。
4.使用重入锁(Lock)实现线程同步
在
Java
SE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
class Bank {
private int account = 100;
//需要声明这个锁
private Lock lock = new ReentrantLock();
public int getAccount() {
return account;
}
//这里不再需要synchronized
public void save(int money) {
lock.lock();
try{
account += money;
}finally{
lock.unlock();
}
}
}
写到这里我不得不思考一个问题,是否在写一个类的时候定义一个静态变量和相应的wait和signal函数来标识该类是否正在被调用(操作类似于操作系统中的同步处理方法),来实现线程间的同步?若是不能,是因为自己编写的函数不是原语还是因为其他原因?欢迎大家探讨。
//----------------------------------------------------------------------------------------------
线程间的通信
1.借助Object类的wait(),notify()和notifyAll()实现通信
线程执行wait()后,就放弃了运行资格,处于冻结状态。
线程运行时,内存中会建立一个线程池,冻结状态的线程都存在于线程池中,notify()执行时唤醒的也是线程池中的线程,线程池中有多个线程时唤醒第一个被冻结的线程。
notifyall(),唤醒线程池中所有线程。
注:①wait(),notify(),notifyAll()都用在同步里面,因为这3个函数是对持有锁的线程进行操作,而只有同步才有锁,所以要使用在同步中。
②wait(),notify(),notifyAll(),在使用时必须标识它们所操作的线程持有的锁,因为等待和唤醒必须是同一锁下的线程;而锁可以是任意对象,所以这3个方法都是Object类中的方法。
2.使用Condition控制线程通信
jdk1.5中,提供了多线程的升级解决方案为:
(1)将同步synchronized替换为显式的Lock操作;
(2)将Object类中的wait(), notify(),notifyAll()替换成了Condition对象,该对象可以通过Lock锁对象获取;
(3)一个Lock对象上可以绑定多个Condition对象,这样实现了本方线程只唤醒对方线程,而jdk1.5之前,一个同步只能有一个锁,不同的同步只能用锁来区分,且锁嵌套时容易死锁。
3.使用阻塞队列控制线程通信
BlockingQueue是一个接口,也是Queue的子接口。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞;但消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。
BlockingQueue提供如下两个支持阻塞的方法:
(1)put(E e):尝试把Eu元素放如BlockingQueue中,如果该队列的元素已满,则阻塞该线程。
(2)take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。
BlockingQueue继承了Queue接口,当然也可以使用Queue接口中的方法,这些方法归纳起来可以分为如下三组:
(1)在队列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法,当该队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。
(2)在队列头部删除并返回删除的元素。包括remove()、poll()、和take()方法,当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。
(3)在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。