一、多线程01
1.1 并发与并行
要想学习多线程,必须先理解什么是并发与并行
并行:指两个或多个事件在同一时刻发生(同时发生)。
并发:指两个或多个事件在同一个时间段内发生。
1.2 进程与线程
进程:进程是正在运行的程序的实例。
进程是线程的容器,即一个进程中可以开启多个线程。
比如打开一个浏览器、打开一个word等操作,都会创建进程。
线程:线程是进程内部的一个独立执行单元;
一个进程可以同时并发运行多个线程;
比如进程可以理解为医院,线程是挂号、就诊、缴费、拿药等业务活动
多线程:多个线程并发执行。
1.3 线程的创建
线程的创建有四种方式:
1.3.1 继承Thread类
/**
* 通过继承Thread实现多线程
* 重写run()方法,方法中一般写的都是主要的业务逻辑
* 优点:
* 1、能实现多线程
* 2、代码写起来比较简单
* 缺点:
* 1、只能单继承,不方便拓展
* 2、资源不能够不能够共享
*/
public class SalTicketThread extends Thread{
private String name;
private int ticketNum=5;
public SalTicket Thread(Stringname) {
this.name=name;
}
public SalTicketThread() {
}
@Override
public void run() {
while (true){
if (ticketNum>0){
//获取当前正在执行的线程的名字:Thread.currentThread().getName()
//默认名字的格式:Thread-0,1,2
//System.out.println(Thread.currentThread().getName()+"售出一张票,剩余的票数是"+ticketNum--);
System.out.println(name+"=售出一张票,剩余的票数是"+ticketNum--);
}
}
}
public static void main(String[] args) {
SalTicketThreadsalTicketThread1=newSalTicketThread("王五");
SalTicketThreadsalTicketThread2=newSalTicketThread("李四");
SalTicketThreadsalTicketThread3=newSalTicketThread("张三");
salTicketThread1.start();
salTicketThread2.start();
salTicketThread3.start();
}
}
这里运行的结果,是每一个售票员都能卖出去五张票,但实际上总共只有五张票,所以代表资源不能共享。
1.3.2 实现Runnable接口
/**
* 通过实现Runnable接口实现多线程
* 优点:
* 1、可以多实现,方便拓展
* 2、资源可以共享
* 缺点:
* 1、不能够抛出异常
* 2、没有返回值
*/
public class SalTicketRunnable implements Runnable{
private int ticketNum=5;
@Override
public void run() {
while (true){
if (ticketNum>0){
//获取当前正在执行的线程的名字:Thread.currentThread().getName()
//默认名字的格式:Thread-0,1,2
System.out.println(Thread.currentThread().getName()+"==售出一张票,剩余的票数是"+ticketNum--);
}
}
}
public static void main(String[] args) {
SalTicketRunnablesalTicketRunnable=newSalTicketRunnable();
Threadthread1=newThread(salTicketRunnable,"张三");
Threadthread2=newThread(salTicketRunnable,"李四");
Threadthread3=newThread(salTicketRunnable,"王五");
thread1.start();
thread2.start();
thread3.start();
}
}
1.3.3 实现Callable接口
利用实现Callable接口实现多线程时,需要Futuretask类的帮助,Futuretask类的结构如下:
![](https://i-blog.csdnimg.cn/blog_migrate/6785761af2aab4ae14973231bdb3b8ca.png)
/**
* 通过实现Callable接口实现多线程
* Callable接口后面跟的是返回值的类型
* isDone() 判断任务是否完成
* get() 获取任务执行结果
* cancel() 能够中断任务
* 优点:
* 1、可以多实现,方便拓展
* 2、资源可以共享
* 3、能抛出异常
* 4、可以有返回值
* 5、可以中断线程执行
* 缺点:
* 1、实现方法相对复杂
*
*/
public class SalTicketCallable implements Callable<String> {
private int ticketNum = 5;
@Override
public String call() throwsException {
for (inti=1;i<6;i++){
if(ticketNum>0){
System.out.println(Thread.currentThread().getName()+"===售出一张票,剩余的票数是"+ticketNum--);
}
}
return"卖票结束";
}
public static void main(String[] args) {
SalTicketCallablesalTicketCallable=newSalTicketCallable();
FutureTask<String>futureTask1=newFutureTask<>(salTicketCallable);
FutureTask<String>futureTask2=newFutureTask<>(salTicketCallable);
FutureTask<String>futureTask3=newFutureTask<>(salTicketCallable);
Threadthread1=newThread(futureTask1,"张三");
Threadthread2=newThread(futureTask2,"李四===");
Threadthread3=newThread(futureTask3,"王五======");
thread1.start();
thread2.start();
//这里使用了这个方法之后,该线程将不再参与线程分配,
//mayInterruptIfRunning - true如果执行该任务的线程应该被中断; 否则,正在进行的任务被允许完成
booleancancel=futureTask2.cancel(true);
thread3.start();
futureTask3.cancel(true);
try {
Stringvalue=futureTask1.get();
booleandone=futureTask1.isDone();
System.out.println("done = "+done);
System.out.println("返回值是:"+value);
} catch (InterruptedExceptione) {
e.printStackTrace();
} catch (ExecutionExceptione) {
e.printStackTrace();
}
}
}
1.3.4 线程池
具体的详细讲解参考下节《多线程02》
线程池类的关系图:
![](https://i-blog.csdnimg.cn/blog_migrate/187aaad77b36cefa8f6b507d01c52d5d.png)
Executor接口:
声明了execute(Runnable runnable)方法,执行任务代码
ExecutorService接口:
继承Executor接口,声明方法:submit、invokeAll、invokeAny以及shutDown等
AbstractExecutorService抽象类:
实现ExecutorService接口,基本实现ExecutorService中声明的所有方法
ScheduledExecutorService接口:
继承ExecutorService接口,声明定时执行任务方法
ThreadPoolExecutor类:
继承类AbstractExecutorService,实现execute、submit、shutdown、shutdownNow方法
ScheduledThreadPoolExecutor类:
继承ThreadPoolExecutor类,实现ScheduledExecutorService接口并实现其中的方法
Executors类:
提供快速创建线程池的方法
public class MyRunable implements Runnable {
privateStringname;
publicMyRunable(Stringname) {
this.name=name;
}
@Override
public void run() {
//每个线程执行五次
for (inti=0; i<5; i++){
System.out.println(name+"线程正在执行:"+newDate().getTime()+",对应的i值是"+i);
}
}
public static void main(String[] args) {
//1.使用Executors创建线程池(两个线程池)
ExecutorServiceexecutorService=Executors.newFixedThreadPool(2);
//2.通过线程池执行线程(三个线程)
for (inti=1;i<4;i++) {
executorService.execute(newMyRunable("test"+i));
}
}
}
1.3.5 创建线程的方式的比较
1)实现接口和继承Thread类比较
①接口更适合多个相同的程序代码的线程去共享同一个资源。
②接口可以避免java中的单继承的局限性。
③接口代码可以被多个线程共享,代码和线程独立。
④线程池只能放入实现Runable或Callable接口的线程,不能直接放入继承Thread的类。
2)Runnable和Callable接口比较
相同点:
①两者都是接口,可以实现多个接口;
②两者都可用来编写多线程程序;
③两者都需要调用Thread.start()启动线程;
④都可以进行资源共享
不同点:
①实现Callable接口的线程能返回执行结果;而实现Runnable接口的线程不能返回结果;
②Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的不允许抛异常;
③实现Callable接口的线程可以调用Future.cancel取消执行 ,而实现Runnable接口的线程不能
3)扩充:
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。
4)注意点:
Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!
1.4 线程声明周期
![](https://i-blog.csdnimg.cn/blog_migrate/968a85c7b7a2bdac02cb8052768a127a.png)
1 新建
new关键字创建了一个线程之后,该线程就处于新建状态
JVM为线程分配内存,初始化成员变量值
2 就绪
当线程对象调用了start()方法之后,该线程处于就绪状态
JVM为线程创建方法栈和程序计数器,等待线程调度器调度
3 运行
就绪状态的线程获得CPU资源,开始运行run()方法,该线程进入运行状态
4 阻塞
当发生如下情况时,线程将会进入阻塞状态
①线程调用sleep()方法主动放弃所占用的处理器资源
②线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
③线程试图获得一个同步锁(同步监视器),但该同步锁正被其他线程所持有。
④线程在等待某个通知(notify)
⑤ 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法
5 死亡
线程会以如下3种方式结束,结束后就处于死亡状态:
①run()或call()方法执行完成,线程正常结束。
②线程抛出一个未捕获的Exception或Error。
③调用该线程stop()方法来结束该线程,该方法容易导致死锁,不推荐使用。
1.5 线程安全问题
如果有多个线程同时运行同一个实现了Runnable接口的类,程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的;反之,则是线程不安全的。
1.5.1 问题发现
问题演示:
有五张票,多线程模拟多个窗口一起出售这五张票。
public class Ticket implements Runnable {
privateintticktNum=5;
@Override
publicvoidrun() {
while(true){
if(ticktNum>0){
//1.模拟出票时间
try {
Thread.sleep(10);
} catch (InterruptedExceptione) {
e.printStackTrace();
}
//2.打印进程号和票号,票数减1
Stringname=Thread.currentThread().getName();
System.out.println("线程"+name+"售票:"+ticktNum--);
}
}
}
public static void main(String[] args){
Ticketticket=newTicket();
Threadthread1=newThread(ticket, "窗口1");
Threadthread2=newThread(ticket, "窗口2");
Threadthread3=newThread(ticket, "窗口3");
thread1.start();
thread2.start();
thread3.start();
}
/**
* 运行结果
* 线程窗口2售票:5
* 线程窗口1售票:3
* 线程窗口3售票:4
* 线程窗口3售票:2
* 线程窗口2售票:1
* 线程窗口1售票:1
*/
}
程序出现了两个问题:
①相同的票数,比如5这张票被卖了两回。
②不存在的票,比如0票与-1票,是不存在的。
问题分析:
线程安全问题都是由全局变量及静态变量引起的。
若每个线程对全局变量、静态变量只读,不写,一般来说,这个变量是线程安全的;
若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
综上所述,线程安全问题根本原因:
多个线程在操作共享的数据;
操作共享数据的线程代码有多条;
多个线程对共享数据有写操作;
解决问题:
要解决以上线程问题,只要在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
为了保证每个线程都能正常执行共享资源操作,Java引入了7种线程同步机制。重点介绍前三种
1) 同步代码块(synchronized)
2) 同步方法(synchronized)
3) 同步锁(ReenreantLock)
4) 特殊域变量(volatile)
5) 局部变量(ThreadLocal)
6) 阻塞队列(LinkedBlockingQueue)
7) 原子变量(Atomic*)
1.5.2 同步代码块( synchronized)
synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
语法:
synchronized(同步锁){
需要同步操作的代码
}
/**
* synchronized
* 同步锁
* 1、对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
* 2、锁对象可以是任意类型。
* 3、多个线程要使用同一把锁。
* 4、注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
*/
public class SalTicketBlock implements Runnable{
privateintticketNum=5;
//创建锁对象
Objectobject=newObject();
@Override
public void run() {
while (true){
synchronized (object){
if (ticketNum>0) {
//模拟出票时间(线程休眠50ms)
try {
Thread.sleep(50);
} catch (InterruptedExceptione) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +"==售出一张票,剩余的票数是"+ticketNum--);
}
}
}
}
public static void main(String[] args) {
SalTicketBlocksalTicketRunnable=newSalTicketBlock();
Threadthread1=newThread(salTicketRunnable,"张三");
Threadthread2=newThread(salTicketRunnable,"李四");
Threadthread3=newThread(salTicketRunnable,"王五");
thread1.start();
thread2.start();
thread3.start();
}
}
1.5.3 同步方法(synchronized)
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
public class SalTicketMethod implements Runnable{
privateintticketNum=5;
@Override
public void run() {
while (true){
//调用封装过的同步方法
sclTicket();
}
}
//synchronized修饰的方法就是同步方法,同步锁系统会自动添加
//对于普通的方法,同步锁就是this synchronized(this){}
//对于static修饰的静态方法 同步锁就是当前类的class对象 synchronized(SalTicketMethod.class){}
public synchronized void sclTicket(){
if (ticketNum>0) {
//模拟出票时间(线程休眠50ms)
try {
Thread.sleep(50);
} catch (InterruptedExceptione) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +"==售出一张票,剩余的票数是"+ticketNum--);
}
}
publicstaticvoidmain(String[] args) {
SalTicketMethodsalTicketRunnable=newSalTicketMethod();
Threadthread1=newThread(salTicketRunnable,"张三");
Threadthread2=newThread(salTicketRunnable,"李四");
Threadthread3=newThread(salTicketRunnable,"王五");
thread1.start();
thread2.start();
thread3.start();
}
}
1.5.4 同步锁(ReenreantLock)
/**
* 同步锁
* 1、java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,
* 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
* 2、同步锁方法:
* public void lock() :加同步锁。
* public void unlock() :释放同步锁。
*/
public class SalTicketLock implements Runnable{
privateintticketNum=25;
//ReentrantLock类中有一个boolean参数,默认为false
//如果为false,就代表线程不公平的获取到锁,会被某个线程独占,反之true是公平
Locklock=newReentrantLock(true);
@Override
public void run() {
while (true){
//加锁
lock.lock();
//获取锁的状态
//if (lock.tryLock()){
//}
try {
if (ticketNum>0) {
//模拟出票时间(线程休眠50ms)
try {
Thread.sleep(50);
} catch (InterruptedExceptione) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +"==售出一张票,剩余的票数是"+ticketNum--);
}
}finally {
//释放锁
lock.unlock();
}
}
}
public static void main(String[] args) {
SalTicketLocksalTicketRunnable=newSalTicketLock();
Threadthread1=newThread(salTicketRunnable,"张三");
Threadthread2=newThread(salTicketRunnable,"李四");
Threadthread3=newThread(salTicketRunnable,"王五");
thread1.start();
thread2.start();
thread3.start();
}
}
1.5.5 Synchronized和Lock区别(面试题)
synchronized是java内置关键字,在jvm层面,Lock是个java类;
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
1.6 线程死锁(了解)
1.6.1 死锁的概念
多线程以及多进程改善了系统资源的利用率并提高了系统的处理能力。然而,并发执行也带来了新的问题--死锁。
死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
1.6.2 产生死锁的原因主要是:
(1) 因为系统资源不足。
(2) 进程运行推进的顺序不合适。
(3) 资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则
就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。
1.6.3 死锁产生的必要条件
以下这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
互斥条件
进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
不可剥夺条件
进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
请求与保持条件
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
循环等待条件
存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有,如图所示。
若干进程之间形成一种头尾相接的循环等待资源关系
public class DeadLockimplementsRunnable {
privatestaticObjectobj1=newObject();//定义成静态变量,使线程可以共享实例
privatestaticObjectobj2=newObject();//定义成静态变量,使线程可以共享实例
publicintflag=0;
publicvoidrun() {
if(flag==0){
System.out.println("flag:"+flag);
synchronized (obj1){
try {
Thread.sleep(500);
} catch (InterruptedExceptione) {
e.printStackTrace();
}
synchronized (obj2){
System.out.println("flag:"+flag);
}
}
}
if(flag==1){
System.out.println("flag:"+flag);
synchronized (obj2){
try {
Thread.sleep(500);
} catch (InterruptedExceptione) {
e.printStackTrace();
}
synchronized (obj1){
System.out.println("flag:"+flag);
}
}
}
}
publicstaticvoidmain(String[] args) {
DeadLockdeadLock1=newDeadLock();
DeadLockdeadLock2=newDeadLock();
deadLock2.flag=1;
Threadthread1=newThread(deadLock1);
Threadthread2=newThread(deadLock2);
thread1.start();
thread2.start();
}
/**运行结果
* flag:1
* flag:0
*/
}
1.6.4 死锁处理
①预防死锁:通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。
②避免死锁:在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生。
③检测死锁:允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除。
④解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。
1.6.4.1死锁预防
预防死锁是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现。
破坏“互斥”条件
“互斥”条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件。
破坏“占有并等待”条件
破坏“占有并等待”条件,就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源。即要想出一个办法,阻止进程在持有资源的同时申请其他资源。
方法一:一次性分配资源,即创建进程时,要求它申请所需的全部资源,系统或满足其所有要求,或什么也不给它。
方法二:要求每个进程提出新的资源申请前,释放它所占有的资源。这样,一个进程在需要资源S时,须先把它先前占有的资源R释放掉,然后才能提出对S的申请,即使它可能很快又要用到资源R。
破坏“不可抢占”条件
破坏“不可抢占”条件就是允许对资源实行抢夺。
方法一:如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
方法二:如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另一个进程,要求它释放资源。只有在任意两个进程的优先级都不相同的条件下,方法二才能预防死锁。
破坏“循环等待”条件
破坏“循环等待”条件的一种方法,是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。
1.6.4.2死锁避免
避免死锁不严格限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。
有序资源分配法
银行家算法
顺序加锁
限时加锁
1.7 线程通讯
1.7.1 为什么需要线程通讯
多个线程并发执行时,在默认情况下CPU是随机切换线程的,有时我们希望CPU按我们的规律执行线程,此时就需要线程之间协调通信。
1.7.2 线程通讯方式
1、休眠唤醒方式:
Object的wait、notify、notifyAll
Condition的await、signal、signalAll
2、CountDownLatch:用于某个线程A等待若干个其他线程执行完之后,它才执行
3、CyclicBarrier:一组线程等待至某个状态之后再全部同时执行
4、Semaphore:用于控制对某组资源的访问权限
注:
①Object类提供了线程间通信的方法:wait()、notify()、notifyAll(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。
②wait()/notify()/notifyAll() 必须配合 synchronized 使用,wait 方法释放锁,notify 方法不释放锁。
1.7.3 休眠唤醒方式
1)Object的wait、notify、notifyAll
public class OddOrEvenObject {
privateIntegeri=0;
//同步锁
Objectobject=newObject();
//打印小于10的奇数
publicvoidprintOdd(){
while (i<10){
synchronized (object) {
if (i%2==1) {
System.out.println("Object:奇数"+i);
//唤醒其他线程
object.notify();
i++;
}else {
try {
//当前线程等待
object.wait();
} catch (InterruptedExceptione) {
e.printStackTrace();
}
}
}
}
}
//打印小于10的偶数
publicvoidprintEven(){
while (i<10){
synchronized (object) {
if (i%2==0) {
System.out.println("Object:偶数"+i);
//唤醒其他线程
object.notify();
i++;
}else {
try {
//当前线程等待
object.wait();
} catch (InterruptedExceptione) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) {
OddOrEvenObjectoddOrEven=newOddOrEvenObject();
//这里使用匿名内部类创建一个线程
//线程1、打印奇数
ThreadthreadOdd=newThread(newRunnable() {
@Override
publicvoidrun() {
oddOrEven.printOdd();
}
});
//线程2、打印偶数
ThreadthreadEven=newThread(newRunnable() {
@Override
publicvoidrun() {
oddOrEven.printEven();
}
});
threadOdd.start();
threadEven.start();
}
}
2)Condition的await、signal、signalAll
Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition。
public class OddOrEvenCondition {
privateIntegeri=0;
//加锁
Locklock=newReentrantLock(true);
Conditioncondition=lock.newCondition();
//打印小于10的奇数
publicvoidprintOdd(){
while (i<10){
lock.lock();
try {
if (i%2==1) {
System.out.println("Condition:奇数"+i);
//唤醒其他线程
condition.signal();
i++;
}else {
try {
//当前线程等待
condition.await();
} catch (InterruptedExceptione) {
e.printStackTrace();
}
}
}finally {
lock.unlock();
}
}
}
//打印小于10的偶数
publicvoidprintEven(){
while (i<10){
lock.lock();
try {
if (i%2==0) {
System.out.println("Condition:偶数"+i);
//唤醒其他线程
condition.signal();
i++;
}else {
try {
//当前线程等待
condition.await();
} catch (InterruptedExceptione) {
e.printStackTrace();
}
}
}finally {
lock.unlock();
}
}
}
publicstaticvoidmain(String[] args) {
OddOrEvenConditionoddOrEven=newOddOrEvenCondition();
//这里使用匿名内部类创建一个线程
//线程1、打印奇数
ThreadthreadOdd=newThread(newRunnable() {
@Override
publicvoidrun() {
oddOrEven.printOdd();
}
});
//线程2、打印偶数
ThreadthreadEven=newThread(newRunnable() {
@Override
publicvoidrun() {
oddOrEven.printEven();
}
});
threadOdd.start();
threadEven.start();
}
}
Object和Condition休眠唤醒区别:
Object方式 | |
object wait()必须在synchronized(同步锁)下使用 | condition await() 必须和Lock(互斥锁/共享锁)配合使用 |
object wait()必须要通过Notify()方法进行唤醒 | condition await() 必须通过 signal() 方法进行唤醒 |
1.7.4 CountDownLatch方式
CountDownLatch是在java1.5被引入的,存在于java.util.concurrent包下。
CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。
方法及其描述 | |
void | await() 导致当前线程等到锁存器计数到零,除非线程是 interrupted 。 |
boolean | await(long timeout, TimeUnit unit) 使当前线程等待直到锁存器计数到零为止,除非线程为 interrupted或指定的等待时间过去。 |
void | countDown() 减少锁存器的计数,如果计数达到零,释放所有等待的线程。 |
long | getCount() 返回当前计数。 |
String | toString() 返回一个标识此锁存器的字符串及其状态。 |
/**
* CountDownLatch:
* CountDownLatch是在java1.5被引入的,存在于java.util.concurrent包下。
* CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。
* CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。
* 运行过程:
* 每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
*/
publicclassOddOrEvenCountDownLatch {
privateIntegeri=0;
CountDownLatchcountDownLatch=newCountDownLatch(1);
//打印小于10的奇数
publicvoidprintOdd(){
while (i<10){
if (i%2==1) {
System.out.println("countDownLatch:奇数"+i);
//当前任务数为1,减1后为0,代表当前线程结束,唤醒其他线程
countDownLatch.countDown();
i++;
}else {
try {
//当前线程等待
countDownLatch.await();
} catch (InterruptedExceptione) {
e.printStackTrace();
}
}
}
}
//打印小于10的偶数
publicvoidprintEven(){
while (i<10){
if (i%2==0) {
System.out.println("countDownLatch:偶数"+i);
//当前任务数为1,减1后为0,代表当前线程结束,唤醒其他线程
countDownLatch.countDown();
i++;
}else {
try {
//当前线程等待
countDownLatch.await();
} catch (InterruptedExceptione) {
e.printStackTrace();
}
}
}
}
publicstaticvoidmain(String[] args) {
OddOrEvenCountDownLatchoddOrEven=newOddOrEvenCountDownLatch();
//这里使用匿名内部类创建一个线程
//线程1、打印奇数
ThreadthreadOdd=newThread(newRunnable() {
@Override
publicvoidrun() {
oddOrEven.printOdd();
}
});
//线程2、打印偶数
ThreadthreadEven=newThread(newRunnable() {
@Override
publicvoidrun() {
oddOrEven.printEven();
}
});
threadOdd.start();
threadEven.start();
}
}
1.7.5 CyclicBarrier方式
/**
* 1、CyclicBarrier是在java1.5被引入的,存在于java.util.concurrent包下。
* 2、CyclicBarrier实现让一组线程等待至某个状态之后再全部同时执行。
* 3、CyclicBarrier底层是CyclicBarrier是直接借助ReentrantLock加上Condition 等待唤醒的功能 进而实现的。
* 4、在构建CyclicBarrier时,传入的值会赋值给CyclicBarrier内部维护count变量,也会赋值给parties变量,
* 每次调用await()方法时,会将count 减一 ,操作count值是直接使用ReentrantLock来保证线程安全性。
* 如果count不为0,则添加Condition队列中,如果count等于0时,则把节点从Condition队列添加至AQS的队列中
* 进行全部唤醒,并且将parties的值重新赋值为count的值。
*/
public class OddOrEvenCyclicBarrier {
private Integer i = 0;
//打印小于10的奇数
public void printOdd(){
while (i<10){
if (i % 2 == 1) {
System.out.println("cyclicBarrier:奇数" + i);
i++;
}
}
}
//打印小于10的偶数
public void printEven(){
while (i<10){
if (i % 2 == 0) {
System.out.println("cyclicBarrier:偶数" + i);
i++;
}
}
}
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
OddOrEvenCyclicBarrier oddOrEven = new OddOrEvenCyclicBarrier();
//这里使用匿名内部类创建一个线程
//线程1、打印奇数
Thread threadOdd = new Thread(new Runnable() {
@Override
public void run() {
oddOrEven.printOdd();
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
},"线程1");
//线程2、打印偶数
Thread threadEven = new Thread(new Runnable() {
@Override
public void run() {
oddOrEven.printEven();
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
},"线程2");
threadOdd.start();
threadEven.start();
}
}
1.7.6 Semaphore方式
/**
* 1、Semaphore是在java1.5被引入的,存在于java.util.concurrent包下。
* 2、Semaphore用于控制对某组资源的访问权限。
* 3、也就是信号量,用来控制同一时间,资源可被访问的线程数量,一般应用场景流量的控制。
* 4、Semaphore 底层实现是基于AQS实现的类似于CountDownLatch
*/
publicclassSemaphoreDemo {
staticclassMachineimplementsRunnable{
privateintnum;
privateSemaphoresemaphore;
publicMachine(intnum, Semaphoresemaphore) {
this.num=num;
this.semaphore=semaphore;
}
publicvoidrun() {
try {
semaphore.acquire();//请求机器
System.out.println("工人"+this.num+"请求机器,正在使用机器");
Thread.sleep(1000);
System.out.println("工人"+this.num+"使用完毕,已经释放机器");
semaphore.release();//释放机器
} catch (InterruptedExceptione) {
e.printStackTrace();
}
}
}
publicstaticvoidmain(String[] args){
intworker=8;//工人数
Semaphoresemaphore=newSemaphore(3);//机器数
for (inti=0; i<worker; i++){
newThread(newMachine(i, semaphore)).start();
}
}
}
1.7.7 AQS和CAS
AQS: AQS是一个抽象类的缩写简称,它原名为:AbstractQueuedSynchronizer.class 。看名字就知道,它就是一个抽象类。
CAS: CAS是一个单词的简称:compare and swap的缩写。我有时候也将AQS的 compareAndSet ;理解为它。CAS就是将两个值进行比较替换的意思。将旧的值原子性的进行更新为新的值。记住这是原子性操作。
回到AQS中:AQS故名思义,就是一个Abstract 抽象的 Queued队列,并且是Synchronizer同步的。所以他就是一个,抽象的同步队列框架。一般在java中的,抽象类和接口都是提供给人用的基类,并且封装好了部分共享的接口等东西。在此,AQS就是一个可供多线程访问共享资源的同步器框架。而它保证线程安全就是来源于CAS的。 CAS是一个Unsafe类的本地调用,通过调用JNI的代码实现的。
AQS可供多线程访问共享资源的同步器框架,所以在java中很多并发工具类都继承了它而实现的。比如:ReentrantLock,Semaphore,CountDownLatch......
AQS思路以及实现参考:https://zhuanlan.zhihu.com/p/472340606
1.7.8 区别与比较
1)wait和sleep的区别
wait | sleep | |
同步 | 只能在同步上下文中调用wait方法,否则会抛出异常 | 不需要在同步上下文中调用 |
作用对象 | 在Object中定义,作用于对象本身 | 在Thread类中定义,作用于当前的线程 |
释放锁 | 需要 | |
唤醒条件 | 其他现成调用notify或者notifyAll方法 | 超时或者调用interrupt() |
方法 | 是实例方法 | 静态方法 |
2) wait和notify区别
wait | notify |
是Object中的方法 | 是Object中的方法 |
执行线程前都必须获得对象锁 | 执行线程前都必须获得对象锁 |
使当前线程等待(休眠) | 通知(唤醒)其他等待当前线程的对象锁的线程 |