读《Java核心技术卷1》最后一章了,并发,这一章讲的有点挺难懂的,看书真是看不下去,也许是我之前的基础不太行。所以,老样子,先读读别人的博客,自己再写一遍加深一下理解,冲!
基本概念
程序:是为了完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象。
进程:是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,有它自身的产生,存在和消亡的过程。
线程:进程可以进一步细化为线程,是一个程序内部的一条执行路径。
即:程序:静态的代码
进程:动态执行的程序
线程:进程中要干几件事时,每一件事的执行路径称为线程
并行:多个CPU同时执行多个任务,比如:多个人同时做不同的事
并发:一个CPU(采用时间片)同时执行多个任务,比如说秒杀平台,多个人做同一件事
线程相关API
Thread.currentThread().getName() 获取当前线程的名字
start():1.启动当前线程 2.调用线程中的run方法
run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
currentThread():静态方法,返回当前代码的线程
getName():获取当前线程的名字
setName():设置当前线程的名字
yield():主动释放当前线程的执行权
join():在线程中插入执行另一个线程,该线程被阻塞,知道插入执行的线程执行完毕以后,该线程才继续执行下去
stop():过时方法。当执行此方法,强制结束当前线程。
sleep(long millitime):线程休眠一段时间
isAlive():判断当前线程是否存活
线程的调度
调度策略:
时间片:线程的调度采用时间片轮转的方式
抢占式:高优先度的线程抢占CPU
Java的调度方法:
1.对于同优先级的线程组成先进先出(先到先服务),使用时间片策略
2.对高优先级,使用优先调度的抢占式策略
线程的优先级
等级:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5
方法:
getPriority():返回线程的优先级
setPriority():改变线程的优先级
注意!:高优先级的线程要抢占低优先级的线程的CPU的执行权。但是仅是从概率上来说的,高优先级的线程更有可能被执行。并不意味着只有高优先级的线程执行完之后,低优先级的线程才能执行。
多线程的创建方式
方式1:继承与Thread类
1、创建一个Thread的子类
2、重写Thread类的run() 方法
3、创建Thread子类的对象
4、通过此对象调用start() 方法
start 与 run 方法的区别:
run方法只是调用重写的run方法,start方法是启动当前线程并调用当前线程重写的run方法(有两条线程)。
我们不能通过run方法新开一个线程,只能调用线程中重写的run方法,start是开启线程,再调用方法。
如下面的卖票例子:
1 | public class MyThread extends Thread{ |
1 | public static void main(String[] args) { |
方式2:实现Runnable接口方式
1、创建一个实现了Runnable接口的类
2、类去实现Runnable中的抽象方法:run()
3、创建实现类的对象
4、将此对象作为参数传到Thread类的构造器中,创建Thread类的对象
5、通过Thread类的对象调用start()
1 | public class MyRunnable implements Runnable { |
1 | public static void main(String[] args) { |
方式3:实现Callable接口(JDK5.0新增)
与使用Runnable方式想比,Callable功能更强大些:
Runnable重写的run方法不如Callable的call方法强大,call方法可以有返回值
方法可以抛出异常
支持泛型的返回值
需要借助FutureTask类,比如获取返回结果
Callable实现新建线程的步骤:
1、创建一个实现Callable的实现类
2、实现call方法,将次线程需要执行的操作声明在call()中
3、创建Callable实现类的对象
4、将Callable接口实现类的对象作为传递到FutureTask的构造器中,创建FutureTask对象
5、将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start方法
1 | public class MyCallable implements Callable { |
1 | public static void main(String[] args) { |
方式4:使用线程池的方式
背景:经常创建和销毁,使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。类似生活中的公共交通。(数据库连接池)
好处:提高响应速度。(减少了创建新线程的时间)降低资源消耗。(重复利用池中线程,不需要每次都创建)。便于线程管理。
JDK5.0起提供了线程池相关API:ExecutorService 和 Executors
ExecutorService:真正的线程池接口,常见子类ThreadPoolExecutor
void execute(Runnable command):执行任务,没有返回值,一般用来执行Runnable
Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
void shutdown():关闭连接池
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
Executors | 工具类,线程池的工厂类,用于创建并返回不同类型的线程池 |
---|---|
Executors.newCachedThreadPool() | 创建一个可根据需要创建新线程的线程池 |
Executors.newFixedThreadPool(n) | 创建一个可重用固定线程数的线程池 |
Executors.newSingleThreadExecutor() | :创建一个只有一个线程的线程池 |
Executors.newScheduledThreadPool(n) | 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。 |
1、需要创建实现Runnable或者Callable接口方式的对象
2、创建ExecutorService线程池
3、将创建好的实现了Runnable接口类的对象放入ExecutorService对象的execute方法中执行
4、关闭线程池
MyRunnable如上面所写的,就不再写了。
1 | public static void main(String[] args) { |
JVM:Java虚拟机内存结构
程序(一段静态的代码)——》加载到内存中——》进程(加载到内存中的代码,动态的程序)
进程可细分为多个线程,一个线程代表一个程序内部的一条执行路径
每个线程都有其独立的程序计数器(PC,指导着程序向下执行)与本地栈(本地变量,本地方法等)
线程通信方法
wait() / notify() / notifyAll():此三个方法定义在Object类中的,因为这三个方法需要用到锁,而锁是任意对象都能充当的,所以这三个方法定义在Object类中。
wait(在进入锁住的区域以后阻塞等待,释放锁让别的线程先进来操作)——Object.wait进入Object这个锁住的区域的线程把锁交出来原地等待通知
notify(由于有很多锁住的区域,所以需要将区域用锁来标识,也涉及到锁)——Object.notify新线程进入Object这个区域进行操作并唤醒wait的线程
所以wait,notify需要使用在有锁的地方,也就是需要synchronize关键字来标识的区域,即使用在同步代码或者同步方法中,且为了保证wait和notify的区域是同一个锁住的区域,需要用锁来标识,也就是锁要相同的对象来充当。
线程的分类
Java中的线程分为两类:
1、守护线程(如垃圾回收线程,异常处理线程)
2、用户线程(如主线程)
若JVM中都是守护线程,当前JVM将退出。
线程状态转换
1、新建状态(New):新创建了一个线程的对象
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程可运行线程池中,变得可运行,等待CPU的使用权
3、运行态(Running):就绪状态的线程获取了CPU,执行程序代码
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU的使用权,暂时停止运行。知道线程进入就绪状态,才有机会转到运行状态。阻塞状态分为三种:
(一)、等待阻塞:运行的线程执行wait方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求,JVM会把该进程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep()不会释放持有的锁)
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程的安全问题
多个线程对同一个共享数据进行操作时,线程没来得及更新共享数据,从而导致另外线程没得到最新的数据,从而产生线程安全问题。
例如,上面卖票就会产生这样的问题,三个线程重复买,有的票未减少就卖没了。
问题出现的原因:当某个线程操作车票的过程中,尚未完成操作时,其他线程参与进来,也操作车票。(将此过程的代码看作一个区域,当有线程进去时,装锁,不让别的线程进去)
如何解决:当一个线程在操作ticket时,其他线程不能参与进来,直到此线程的生命周期结束。
在Java中,我们通过同步机制,来解决线程的安全问题。
方式一:同步代码块
使用同步监视器(锁)
synchronized(同步监视器){
//需要同步的代码
}
说明:
操作共享数据的代码(所有线程共享的数据操作的代码),即为需要共享的代码(同步代码块中,相当于是一个单线程,效率低)
共享数据:多个线程共同操作的数据,比如公共厕所就类比共享数据
同步监视器(俗称:锁):任何一个的对象都可以充当锁。(但是为了可读性一般设置英文成lock)当锁住以后只能有一个线程进去(要求:多个线程必须要共用同一把锁,比如火车上的厕所,同一标志表示有人)
Runnable天生共享锁,而Thread中需要static对象或者this关键字或者当前类来充当唯一锁。
方式二:同步方法
使用同步方法,对方法进行synchronized关键字修饰
将同步代码块提取出来成为一个方法,用synchronized关键字修饰此方法
对于实现Runnable接口的,只需要将同步方法用synchronized修饰
而对于继承自Thread方式,需要将同步方法用static和synchronized修饰,因为对象不唯一(锁不唯一)
总结:
- 同步方法任然涉及到同步监视器,只是不需要我们的声明
- 非静态的同步方法,同步监视器是this
- 静态的同步方法,同步监视器是当前类本身。继承自Thread.class
方式三:JDK5.0新增的lock锁方法
1 | public class MyRunnable implements Runnable { |
总结:Synchronized与lock的异同?
相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完相应的代码逻辑以后,自动的释放同步监视器;lock需要手动地启动同步,同时结束同步也需要手动的实现
优先使用顺序:
LOCK ——》 同步代码块 ——》 同步方法
解决单例模式懒汉式的线程安全问题
单例:只能通过一个静态方法获取一个实例,不能通过构造器来构造实例
1 | //懒汉式单例 |
假设有多个线程调用此案例,有可能会导致多个实例的产生,线程不安全。
我们通过代码测试一下:
1 | //懒汉式存在线程不安全的问题 |
我们同时并打100个线程,看看他们打印出来的hashCode是否不一样:
可以看出,这他妈还叫单例模式?建议两例模式呦!开个玩笑,我们可以看出这样会造成线程不安全的问题。
通过上面的学习,我们接下来给出解决方法:
方式一:同步代码块
1 | public class Singleton1 { |
方式二:同步代码块
1 | public class Singleton2 { |
方式三:更高效率的同步代码块方法
1 | public class Singleton3 { |
线程的死锁问题
死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限地阻塞,因此程序不可能正常终止。
死锁产生的四个必要条件:
- 互斥使用:即当资源被一个线程使用(占有)时,别的线程不能使用
- 不可抢占:即资源请求者不能强制从资源占有者手中夺取资源,资源只能由有者主动释放
- 请求和保持:即当资源请求者在请求其他资源的同时保持对原有资源的占有
- 循环等待:即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P3的资源。这样就形成了一个等待环路
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
举个例子:两个人用筷子去吃饭,开始时A占有了左筷子,B占有了右筷子,然后A等待右筷子,B等待左筷子,这样谁都吃不了饭,陷入了死锁状态。
1 | public class ChTest { |
1 | public class ChA implements Runnable { |
1 | public class ChB implements Runnable { |
执行结果如下:
信号量
上面那个筷子问题会产生死锁,那么怎么解决这个问题,使用信号量可以解决这一问题。
Semaphore的模型可以概括为一个计数器、一个等待队列、三个方法。 三个方法原子性分别是init()、down()、up()
;
init()
:设置计数器的初始值。
down()
:将计数器的值减一,如果减了一之后,计数器的值小于0,则当前的线程被阻塞,否则继续执行。
up()
:将计数器的值加一,如果加了一之后,计数器的值小于等于0,则唤醒等待队列中的一个线程,并且将它移除出等待队列。(注意是小于等于0,不应该理解为大于等于0,因为大于等于0表明此时没有等待的线程,所以不会有唤醒这个操作。)
简单的理解就是 Semaphore
就是通过这三个方法来改变计数器,通过计数器的值来判断此时的线程是应该加入到等待队列中等待还是成功执行。
信号量模型也被称为PV原语,也就是down
和up
操作最早称为P操作和V操作,有些人还称为semWait
和semSignal
。在JAVA中信号量模型是由 java.util.concurrent.Semaphore
的实现,并且down
和up
对应的实现方法是acquire和release
。
方法 | 描述 |
---|---|
Semaphore(int permits) | 创建一个 Semaphore与给定数量的许可证和非公平公平设置 |
Semaphore(int permits, boolean fair) | 创建一个 Semaphore与给定数量的许可证和给定的公平设置。 |
acquire() | 从该信号量获取许可证,阻止直到可用 |
acquire(int permits) | 从该信号量获取给定数量的许可证,阻止直到所有可用 |
tryAcquire() | 尝试从该信号量获得1个许可,如果获取不到则返回false |
tryAcquire(int permits) | 尝试从该信号量获得给定数量个许可,如果获取不到则返回false |
release() | 释放许可证 |
release(int permits) | 释放一定数量的许可证 |
后面可以写代码了,我总体先写写思路,本来筷子是两个字符串变量,然后同步方法进行保证同步。现在使用信号量,在测试类中增加两个数量都是1的信号量变量,然后用信号量的一些方法来保证同步,还有注意释放信号量的问题。具体代码如下:
1 | public class ChA implements Runnable { |
1 | public class ChB implements Runnable { |
1 | public class ChTest { |
输出结果:
线程的通信
常用线程通信方法:
通信方法 | 描述 |
---|---|
wait() | 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器 |
notify | 一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程,就唤醒优先级高的线程 |
notifyAll | 一旦执行此方法,就会唤醒所有被wait()的线程 |
使用前提:这三个方法均只能使用在同步代码块或者同步方法中
我们接下来写一个通信的例子,使用两个线程打印1-100,线程1、线程2交替打印。
我们先分析一波,若要交替打印,需要:
1、run方法设置为同步方法
2、第一次线程1获取到锁之后,打印数字,然后完成加一操作,然后就要wait以保证下一个线程2继续交替打印
3、线程2获取到锁之后,为了线程1能够获取下一把锁,要把线程1的阻塞状态取消,然后像线程1那样操作
OK,分析完毕,思路已经很清晰了,上代码:
1 | class Number implements Runnable{ |
1 | //线程通信的例子:使用两个线程打印1—100,线程1,线程2交替打印 |
输出结果:
sleep和wait的异同:
相同点:一旦方法执行以后,都会使得当前线程进入阻塞状态
不同点:
1、两个方法声明的位置不同:Thread类中声明sleep,Object类中声明wait
2、调用的要求不同:sleep可以在任何需要的场景下调用,wait必须在同步代码块或同步方法中调用
3、关于是否释放监视器,如果两个方法都使用在同步代码块或同步方法中,sleep不会释放,wait会释放
经典例题:生产者/消费者问题
生产者(Producer)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如20个),如果生产者视图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产:如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
先分析一波:
1、是否有线程问题?是的,生产者和消费者线程
2、多线程是否存在共享数据?存在——产品
3、多线程是否存在线程安全问题?存在,都对产品进行了操作
4、是否存在线程之间的通信?
对于生产者而言,数量小于20时,要去生产,并唤醒消费者线程,否则就等待;
对于消费者而言,数量大于0时,要去消费,并唤醒生产者线程,否则等待;
OK,经过上面的分析,我们就可以写代码,最关键的就是店员类,它的生产消费方法怎么写?这是问题关键,下面是店员(Clerk)类的代码:
1 | class Clerk{ |
然后写生产者、消费者类,这两个是比较简答的:
1 | //生产者 |
最后main方法调用:
1 | public static void main(String[] args) { |
join方法原理详解
synchronized中的对象锁是线程的实例
我们可以用同步语句块的方式对需要同步的代码进行包裹:
1 | Object o = new Object(); |
把它放在main方法中执行,此时主线程就会卡在这里等待,如下所示:
此时如果想要主线程继续执行,则需要其他线程通过notify、notifyAll唤醒或者中断。
但是如果obj是一个线程实例会怎么样呢?如下面例子:
1 | public class NotifyAllDemo { |
结果如下所示:
通过这个例子,我们可以看出,主线程是可以执行的。为什么?
因为在myThread线程执行完之后,会调用线程自身的 notifyAll() 方法,此时主线程会继续执行。
总结即:如果synchronized获得对象锁是线程的实例时,此时比较特殊,当该线程终止的时候,会调用线程自身的notifyAll()方法,会通知所有等待在该线程对象上的线程。
join的原理
Java中的join方法也可以控制线程的执行顺序,上面的代码的功能使用join方法也可以很方便的实现:
1 | public class JoinDemo { |
查找join的源码,如下:
1 | public final void join() throws InterruptedException { |
1 | public final synchronized void join(long millis) |
关键代码就是:
1 | while (isAlive()) { |
先不管 isAlive()
,wait()
源码其实就是 wait(0)
,就是让其等待。
其实join方法本质就是利用上面的线程实例作为对象锁的原理,当线程终止时,会调用线程自身的notifyAll() 方法,通知所有等待在该线程对象上的线程的特征。
while(isAlive)语句的作用
下面先看一个例子,让三个线程按顺序执行,先随便写一下(直接写在main方法中):
1 | Thread t1 = new Thread(){ |
运行之后,它们三个肯定不会按 t1 t2 t3 这样的顺序执行。其实因为是线程,后面的start执行顺序也是无所谓的。
OK,这样我们引入join,在t2 t3中引入,修改如下:
1 | Thread t2 = new Thread() { |
这样,t2 中添加一个 t1.join();
,t3 中添加一个 t2.join();
,再执行一下如下:
这样就可以了。
所以看到这里就明白了,join源码中while(isAlive()),其实相当于while(this.isAlive())就相当于判断这里的t1是不是已经是不是运行状态(有没有调用start方法)。这么设计是为了防止t1线程没有运行,此时t2线程如果直接执行wait(0)方法,那么将会一直等待下去,造成代码卡死。
这样我们稍微修改代码再测试一下:
1 | t2.start(); |
如果这样改,我们先猜想一下,t2 的run方法中执行 t1.join() 的时候,因为t1不是出于运行状态,所以肯定要先输出 t2 t3,最后输出 t1,就是这样,我们看输出结果:
yield方法原理详解
yield意味着放手、放弃、投降。Thread.yield的定义如下:
1 | /** |
让我们列举一下关于以上定义重要的几点:
- Yield是一个静态的原生(native)方法
- Yield告诉当前正在执行的线程把运行机会交给线程池中拥有相同优先级的线程。
- Yield不能保证使得当前正在运行的线程迅速转换到可运行的状态
- 它仅能使一个线程从运行状态转到可运行状态,而不是等待或阻塞状态
我们做一个测试:
1 | long beginTime = System.currentTimeMillis(); |
看看这样耗时多少毫秒,如下所示:
如果我们在for循环内增加一个 Thread.yield();
,看看会耗时多少,代码及结果如下:
1 | long beginTime = System.currentTimeMillis(); |
OK,不多BB,效果一目了然!
中断线程
一个线程在未结束之前,被强制终止是一件很危险的事情,所以Thread的stop和suspend等方法都已经过时不建议使用了。
那么不能直接把一个线程搞挂掉, 但有时候又有必要让一个线程死掉, 或者让它结束某种等待的状态 该怎么办呢?一个比较优雅而安全的做法是:使用等待/通知机制或者给那个线程一个中断信号, 让它自己决定该怎么办。
中断是通过调用Thread.interrupt()方法来做的. 这个方法通过修改了被调用线程的中断状态来告知那个线程, 说它被中断了. 对于非阻塞中的线程, 只是改变了中断状态, 即Thread.isInterrupted()将返回true; 对于可取消的阻塞状态中的线程, 比如等待在这些函数上的线程, Thread.sleep(), Object.wait(), Thread.join(), 这个线程收到中断信号后, 会抛出InterruptedException, 同时会把中断状态置回为true.但调用Thread.interrupted()会对中断状态进行复位。
对非阻塞中的线程中断的Demo:
1 | public class Demo1 extends Thread{ |
分析如上程序的结果:
在main线程sleep的过程中由于t线程中isInterrupted()为false所以不断的输出”Thread is going”。当调用t线程的interrupt()后t线程中isInterrupted()为true。此时会输出Someone interrupted me.而且线程并不会因为中断信号而停止运行。因为它只是被修改一个中断信号而已。
在Core Java中有这样一句话:”没有任何语言方面的需求要求一个被中断的程序应该终止。中断一个线程只是为了引起该线程的注意,被中断线程可以决定如何应对中断 “。好好体会这句话的含义,看看下面的代码:
1 | //Interrupted的经典使用代码 |
很显然,在上面的代码中,while循环中有一个决定因素就是需要不停地检查自己的中断状态,当中断状态为true时,就会终止循环,不再执行 do more work;
。
接下来让我们看下面例子:
1 | public class Demo2 extends Thread { |
我们先分析一波:
程序开始运行后,main线程会运行到 Thread.sleep(3000);
处,thread线程会运行到try语句的 Thread.sleep(2000);
处;
然后2s之后,thread会进入下一个循环,还会停在try语句的 Thread.sleep(2000);
处;
3s时,main线程会执行 thread.stop = true;
thread.interrupt();
这两句代码,然后停在 Thread.sleep(1000);
处,关键就在这里,这时,thread线程仍然处于阻塞状态,但是stop的值设置为true不会对其产生影响,它这时不会立即退出循环,但是中断为true就会让其捕获到异常,它将执行catch块的代码,然后进行下一次while循环,但是此时它才发现它的stop为true,所以thread线程执行完毕。
4s时,main线程也执行完毕。
综上我们分析,输出结果及对应时间对下:
线程在不同状态下对于中断所产生的反应
线程一共6种状态,分别是NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED(Thread类中有一个State枚举类型列举了线程的所有状态)。
- NEW和TERMINATED
线程的new状态表示还未调用start方法,还未真正启动。线程的terminated状态表示线程已经运行终止。这两个状态下调用中断方法来中断线程的时候,Java认为毫无意义,所以并不会设置线程的中断标识位,什么事也不会发生
- RUNNABLE
如果线程处于运行状态,那么该线程的状态就是RUNNABLE,但是不一定所有处于RUNNABLE状态的线程都能获得CPU运行,在某个时间段,只能由一个线程占用CPU,那么其余的线程虽然状态是RUNNABLE,但是都没有处于运行状态。而我们处于RUNNABLE状态的线程在遭遇中断操作的时候只会设置该线程的中断标志位,并不会让线程实际中断,想要发现本线程已经被要求中断了则需要用程序去判断。
- BLOCKED
当线程处于BLOCKED状态说明该线程由于竞争某个对象的锁失败而被挂在了该对象的阻塞队列上了。那么此时发起中断操作不会对该线程产生任何影响,依然只是设置中断标志位。
- WAITING/TIMED_WAITING
这两种状态本质上是同一种状态,只不过TIMED_WAITING在等待一段时间后会自动释放自己,而WAITING则是无限期等待,需要其他线程调用notify方法释放自己。但是他们都是线程在运行的过程中由于缺少某些条件(例如:调用wait())而被挂起在某个对象的等待队列上。当这些线程遇到中断操作的时候,会抛出一个InterruptedException异常,并清空中断标志位。