多线程与并发编程
多线程介绍
什么是程序?
程序(Program)是一个静态的概念,一般对应于操作系统中的一个可执行文件。
什么是进程?
执行中的程序叫做进程(Process),是一个动态的概念。其实进程就是一个在内存中独立运行的程序空间 。
现代操作系统比如Mac OS X,Linux,Windows等,都是支持“多任务”的操作系统,叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用逛淘宝,一边在听音乐,一边在用微信聊天,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。
什么是线程?
线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
有些进程还不止同时干一件事,比如微信,它可以同时进行打字聊天,视频聊天,朋友圈等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
什么是并发
并发是指在一段时间内同时做多个事情。当有多个线程在运行时,如果只有一个CPU,这种情况下计算机操作系统会采用并发技术实现并发运行,具体做法是采用“ 时间片轮询算法”,在一个时间段的线程代码运行时,其它线程处于就绪状。这种方式我们称之为并发。(Concurrent)。
- 串行(serial):一个CPU上,按顺序完成多个任务
- 并行(parallelism):指的是任务数小于等于cpu核数,即任务真的是一起执行的
- 并发(concurrency):一个CPU采用时间片管理方式,交替的处理多个任务。一般是是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
什么是主线程以及子线程
主线程
当Java程序启动时,一个线程会立刻运行,该线程通常叫做程序的主线程(main thread),即main方法对应的线程,它是程序开始时就执行的。
Java应用程序会有一个main方法,是作为某个类的方法出现的。当程序启动时,该方法就会第一个自动的得到执行,并成为程序的主线程。也就是说,main方法是一个应用的入口,也代表了这个应用的主线程。JVM在执行main方法时,main方法会进入到栈内存,JVM会通过操作系统开辟一条main方法通向cpu的执行路径,cpu就可以通过这个路径来执行main方法,而这个路径有一个名字,叫main(主)线程
主线程的特点
它是产生其他子线程的线程。
它不一定是最后完成执行的线程,子线程可能在它结束之后还在运行。
子线程
在主线程中创建并启动的线程,一般称之为子线程。
线程的创建
通过继承Thread类实现多线程
继承Thread类实现多线程的步骤:
-
在Java中负责实现线程功能的类是java.lang.Thread 类。
此种方式的缺点:如果我们的类已经继承了一个类(如小程序必须继承自 Applet 类),则无法再继承 Thread 类。
-
可以通过创建 Thread的实例来创建新的线程。
-
每个线程都是通过某个特定的Thread对象所对应的方法run( )来完成其操作的,方法run( )称为线程体。
-
通过调用Thread类的start()方法来启动一个线程。run()方法不能直接调用,否则会造成线程串行处理现象,并行是调用start()方法后自动调用的。
多线程原理:相当于玩游戏机,只有一个游戏机(cpu),可是有很多人要玩,于是,start是排队!等CPU选中你就是轮到你,你就run(),当CPU的运行的时间片执行完,这个线程就继续排队,等待下一次的run()。
调用start()后,线程会被放到等待队列,等待CPU调度,并不一定要马上开始执行,只是将这个线程置于可动行状态。然后通过JVM,线程Thread会调用run()方法,执行本线程的线程体。先调用start后调用run,这么麻烦,为了不直接调用run?就是为了实现多线程的优点,没这个start不行。
通过Runnable接口实现多线程
在开发中,我们应用更多的是通过Runnable接口实现多线程。这种方式克服了继承Thread类的缺点,即在实现Runnable接口的同时还可以继承某个类。
从源码角度看,Thread类也是实现了Runnable接口。Runnable接口的源码如下:
public interface Runnable {
void run();
}
线程状态和生命周期
一个线程对象在它的生命周期内,需要经历5个状态。
-
新生状态(New)
用new关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。
-
就绪状态(Runnable)
处于就绪状态的线程已经具备了运行条件,但是还没有被分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。有4种原因会导致线程进入就绪状态:
- 新建线程:调用start()方法,进入就绪状态;
- 阻塞线程:阻塞解除,进入就绪状态;
- 运行线程:调用yield()方法,直接进入就绪状态;
- 运行线程:JVM将CPU资源从本线程切换到其他线程。
-
运行状态(Running)
在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。
-
阻塞状态(Blocked)
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。
有4种原因会导致阻塞:
- 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。
- 执行wait()方法,使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后,它进入就绪状态。
- 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。
- join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。
-
死亡状态(Terminated)
死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它run()方法内的全部工作; 另一个是线程被强制终止,如通过执行stop()或destroy()方法来终止一个线程(注:stop()/destroy()方法已经被JDK废弃,不推荐使用)。
当一个线程进入死亡状态以后,就不能再回到其它状态了。
线程的使用
终止线程的典型方式
终止线程我们一般不使用JDK提供的stop()/destroy()方法(它们本身也被JDK废弃了)。通常的做法是提供一个boolean型的终止变量,当这个变量置为false,则终止线程的运行。
线程休眠
sleep()方法:可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。sleep方法的参数为休眠的毫秒数。
-
线程让步
yield()让当前正在运行的线程回到就绪状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。
使用yield方法时要注意的几点:
- yield是一个静态的方法。
- 调用yield后,yield告诉当前线程把运行机会交给具有相同优先级的线程。
- yield不能保证,当前线程迅速从运行状态切换到就绪状态。
- yield只能是将当前线程从运行状态转换到就绪状态,而不能是等待或者阻塞状态。
线程联合
当前线程邀请调用方法的线程优先执行,在调用方法的线程执行结束之前,当前线程不能再次执行。线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。
join方法的使用
join()方法就是指调用该方法的线程在执行完run()方法后,再执行join方法后面的代码,即将两个线程合并,用于实现同步控制。
Thread类中的其他常用方法
获取当前线程名称
方式一
this.getName()获取线程名称,该方法适用于继承Thread实现多线程方式。
class GetName1 extends Thread{
@Override
public void run() {
System.out.println(this.getName());
}
}
方式二
Thread.currentThread().getName()获取线程名称,该方法适用于实现Runnable接口实现多线程方式。他是Thread类的静态方法,可以直接调用。
class GetName2 implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
判断线程是否存活
isAlive()方法: 判断当前的线程是否处于活动状态。
活动状态是指线程已经启动且尚未终止,线程处于正在运行或准备开始运行的状态,就认为线程是存活的。
线程的优先级
什么是线程的优先级
每一个线程都是有优先级的,我们可以为每个线程定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程的优先级用数字表示,范围从1到10,一个线程的缺省优先级是5。
Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。
注意线程的优先级,不是说哪个线程优先执行,如果设置某个线程的优先级高。那就是有可能被执行的概率高。并不是优先执行。
守护线程
什么是守护线程
在Java中有两类线程:
- User Thread(用户线程):就是应用程序里的自定义线程。
- Daemon Thread(守护线程):比如垃圾回收线程,就是最典型的守护线程。
守护线程(即Daemon Thread),是一个服务线程,准确地来说就是服务其他的线程,这是它的作用,而其他的线程只有一种,那就是用户线程。
守护线程特点:
守护线程会随着用户线程死亡而死亡。
守护线程与用户线程的区别:
用户线程,不随着主线程的死亡而死亡。用户线程只有两种情况会死掉,1在run中异常终止。2正常把run执行完毕,线程死亡。
守护线程,随着用户线程的死亡而死亡,当用户线程死亡守护线程也会随之死亡。
方法:setDaemon(true/false),true表示守护线程,false表示用户线程。
线程同步
什么是线程同步
线程冲突现象
同步问题的提出
现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题。 比如:教室里,只有一台电脑,多个人都想使用。天然的解决办法就是,在电脑旁边,大家排队。前一人使用完后,后一人再使用。
线程同步的概念
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。
实现线程同步
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问造成的这种问题。这套机制就是synchronized关键字。
synchronized语法结构:
synchronized(锁对象){ 同步代码 }
synchronized关键字使用时需要考虑的问题:
- 需要对那部分的代码在执行时具有线程互斥的能力(线程互斥:并行变串行)。
- 需要对哪些线程中的代码具有互斥能力(通过synchronized锁对象来决定)。
它包括两种用法:(this,注意和class锁区别,class锁方法要用static)
synchronized 方法和 synchronized 块。
-
synchronized 方法
通过在方法声明中加入 synchronized关键字来声明,语法如下:
1public synchronized void accessVal(int newVal);
synchronized 在方法声明时使用:放在访问控制符(public)之前或之后。这时同一个对象下synchronized方法在多线程中执行时,该方法是同步的,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。
-
synchronized块
synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。
Java 为我们提供了更好的解决办法,那就是 synchronized 块。 块可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率。
使用字符串作为线程对象锁
字符串锁的原理就是String对象在常量池中不可变,每一个对象都拥有这个字符串,这样字符串是不可变的,那么线程在进入synchronize方法时,就会被锁对象限制,因为在相同字符串的情况下,并发操作才是不被允许的
语法结构:
synchronized(“字符串”){
//同步代码
}
使用Class作为线程对象锁
语法结构:
synchronized(XX.class){
//同步代码
}
或
synchronized public static void accessVal()
死锁及解决方案
死锁的概念
“死锁”指的是:
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。
某一个同步块需要同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。比如,“化妆线程”需要同时拥有“镜子对象”、“口红对象”才能运行同步块。那么,实际运行时,“小丫的化妆线程”拥有了“镜子对象”,“大丫的化妆线程”拥有了“口红对象”,都在互相等待对方释放资源,才能化妆。这样,两个线程就形成了互相等待,无法继续运行的“死锁状态”。
线程并发协作(生产者/消费者模式)
多线程环境下,我们经常需要多个线程的并发和协作。这个时候,就需要了解一个重要的多线程并发协作模型“生产者/消费者模式”。
角色介绍
-
什么是生产者?
生产者指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。
-
什么是消费者?
消费者指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。
-
什么是缓冲区?
消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。
缓冲区是实现并发的核心,缓冲区的设置有两个好处:
-
实现线程的并发协作
有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离,解除了生产者与消费者之间的耦合。
-
解决忙闲不均,提高效率
生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据 。