一、多线程基本概念
1、程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
说明:软件安装好了,但是还没跑起来,此时就是静态代码。比如qq,游戏,还没运行的时候。
2、进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。-生命周期,如:运行中的QQ,运行中的MP3播放器
程序是静态的,进程是动态的进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域(堆和方法区)说明:程序一旦运行起来,加载到内存中以后,就成了进程
3、线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程的。比如360安全卫士,同时进行木马查杀,电脑清理,和系统修复都是多线程的体现线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小一个进程中的多个线程共享相同的内存单元内存地址空间 它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资原可能就会带来安全的隐患。多线程程序的优点:(cpu是多核的)
(1),提高应用程序的响应。对图形化界面更有意义,可增强用户体验。//360可以同时清理,杀木马
(2)提高计算机系统CPU的利用率
(3)改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
4、JVM内存解析
虚拟机栈和程序计数器,每个线程各有一套。方法区和堆每个进程各一份(多个线程共享堆和方法区)。本地方法栈是c++写的底层,不用管
二、单核cpu与多核cpu
2.1 cpu的理解
1、单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务(就是一个人速度超快的做多个任务,每个任务都不一次做完,每个任务做几毫秒就换)。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果有某个人不想交钱,那么收费人员可以把他“挂起” (晾着他,等他想通了,准备好了钱,再去收费) 。但是因为CPU时间单元特别短,因此感觉不出来。
2、如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
3、一个Java应用程序java.exe,其实至少有三个线程: main()主线程, gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
说明:单核cpu也可以创建多线程,只是效率不行。比如把1个g的文件从c盘赋值到D盘,同时又把1个g的文件从F盘复制到G盘。单核cpu单独做,肯定比同时做快。因为线程切换也要花时间。所以,单核cpu就别玩多线程了。
2.2 并行与并发
1、并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
2、并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。(有点像原来的单核假cpu)
三、何时需要创建多线程
1、程序需要执行两个或多个任务
说明:比如java,一方面要执行main,另一方面又要回收垃圾(没有指针指向的对象),又不可能等main这个主线程执行完,再垃圾回收,所以需要两个线程
2、程序需要实现一些需要等待的任务时,比如用户输入、文件读写操作,网络操作、搜索等
说明:比如用饿了吗,要读取手机底部页面的时候,如果网络不好,图片还没加载出来,单线程可能就滑不动。但是专门用一个线程来加载图片,再设置一个个线程来加载页面,就不会因为网速不好加载不出图片,来影响你的下滑操作。
3、需要一些后台运行的程序
说明:比如java的异常处理线程,和垃圾回收线程
四、线程的创建和使用
说明:多线程是同时执行多段代码,不是一段代码顺序执行。
1、父类 Thread 类 查字典https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/lang/Thread.html
Class Thread implements Runnable{
Thread(); // 空参构造器,所有属性都用默认值
Thread(String name); //这个构造器可以给线程名属性赋值 name
void start(); //重点 启动线程,并执行对象的run()方法
void run(); //重点 线程在调度时执行的操作 需要在子类中重写该方法
String getName(); //获取当前线程的名字 线程默认名字是Thread-x
void setName(String name); //设置该线程的名称
static Thread currentThread();/*返回执行当前代码的线程。在Thread子类中,就是this,通常用于主线程和Runnable实现类
返回当前线程后,可以.类/对象的其他方法,对当前线程做点事。比如 Thread currentThread().setName("主线程"); 即可实现给main线程取名 */
static void yield();/*线程让步。暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程(若队列中没有同优先级的线程,则忽略此方法)*/
void join() throws InterruptedException : /*当某个程序执行流中调用其他线程的join()方法时,当前线程将被阻塞,直到join()方法加入过来的线程执行完为止。此时,当前线程才结束阻塞状态(后面就是看cpu啥时候给此线程再分配资源啦)。低优先级的线程也可以获得执行。总结:就是需要其他线程数据了,让其他线程join'进来,并且先执行*/
static void sleep(long mills) throws InterruptedException;/*时间为mm,让当前线程“睡眠”指定的毫秒数,指定时间内,当前线程是阻塞状态,使其他线程有机会被执行,时间到后重排队等cpu分配资源。注意此方法要抛异常,记得在调用这个sleep方法的方法中解决掉*/
boolean isAlive();//判断线程是否还存活,线程执行完成后,就会死去
}
一个重写了Thread 类run方法的子类对象,就是进程中的一个线程,每一个线程都有一个优先级。
2、创建多线程的方法:
2.1继承Thread方式来创建多线程
声明一个Thread类的子类,子类中要重写Thread类中的run方法,然后造一个子类对象,再调用Thread类里面的start方法,就可以执行多线程啦。
step1:创建一个继承于Thread类的子类
step2:重写Thread类的run() //将这个线程要做的事,写在run的方法体中
step3:创建Thread类的子类对象
step4:通过此对象调用Thread类的start() //此时该线程独立于主线程,自行执行
说明:
start方法两个作用,一是启动当前线程,二是调用当前线程的run方法
如果不调用start方法,直接调用run方法。那就没启动多线程,还是在主线程里面执行run方法,就还是会顺序执行代码。
注意:每个Thread子类(线程)只允许启动一个线程,即只允许调用一次start,否则报错。想要启动多个线程,请多new一个Thread子类对象,再让新对象去start
exp:遍历100以内所有的偶数
class Mythread extends Thread { //step1 创建一个继承于Thread类的子类Mythread
public void run(){ //step2 重写run idea中,写了run,就可以自动提示补全重写代码。
for(i=0;i<100;i++){ //将要做的事,写在run中 即"遍历100以内所有的偶数"
if(i%2==0){
try{
sleep(1000);} /*该线程阻塞1秒,此时cpu没法给该线程分配资源,1秒后,cpu可再给你分配资源。注意sleep方法会抛异常,但是Thread类里的run方法没有抛异常,现在重写run自然不准再向上抛异常,所以必须马上在调用sleep的run方法中处理掉异常,不能再往上抛了*/
catch(InterruptedException e) {
e.printStackTrace(); }
System.out.println(i);
}
}
}
main(){
Mythread t1 = new MyThread(); //step3 在main中(主线程中),创建子类对象 t1
t1.setName("线程一"); //给这个线程取名为 线程一
t1.setPriority(Thread.MAX_PRIORITY);//设置t1优先级为最大 即10
t1.start(); //step4 主线程main按下t1线程的开关,此时t1线程开始独立于主线程,自己执行。
Thread.currentThread.setName("主线程"); //给当前执行的线程(即main线程)取名为“主线程”
System.out.println(Thread.currentThread().getPriority());//查看main线程的优先级
//以下 主线程也来玩遍历,输出奇数
for(i=0;i<100;i++){
if(i%2!=0){ //输出奇数
System.out.println(i);
}
if(i==20){
try{
t1.join()
}; //当i循环到20次的时候,邀请t1线程join进来,并且优先执行j该线程,等t1线程执行完,再继续当前线程main线程。
catch(InterrupttedException e){ //join()方法会抛异常
e.printStackTrace();
}
}
}
System.out.println(t1.isAlive()); //看下t1线程是否存活,执行完了就死了
}
说明:一旦主线程调用Thread对象的start方法后,该线程就开始独立于主线程,和主线程一起并行运行,因此,就会一会打印t1线程的偶数,一会打印主线程的奇数。
2.2通过实现Runnable接口方式实现多线程
step1:创建一个实现了Runnable接口的类
step2:实现类去实现Runnable中的抽象方法:重写run()
step3:创建实现了的对象
step4:将此对象 作为参数传递到Thread类的构造器中,创建Thread类的对象
step5:通过Thread类的对象调用start()
exp:
class MThread implements Runnable{ //step1 创建一个实现Runnable接口的类
public void run() { //step2 实现接口中的run方法
for(i=0;i<100;i++){
if(i%2!=0){ //输出奇数
System.out.println(i);
}
}
}
}
main(){
MThread mThread = new MThread(); //step3 创建实现类对象
Thread t1 = new Thread(mThread)/*step4 将此对象 作为参数传递到Thread类的构造器中。注意多态性 构造器形参是Runnable target 即Runnable target= new MThread() 对象名为mThread
t1.start();//step5 通过Thread对象调用start方法。其实是Thread的run方法,因为接口run方法又内部调用了Threadrun方法
}
Thread t2 = new Thread(mThread);//再启动一个线程,但方法体还是原来的run
t2.start(); //启动t2线程,一起去执行run方法体的东西
扩展:哪种创建多线程的方式更好
开发当中,优先选择实现Runnable接口的方式。
说明:
1、JAVA是单继承,为了实现多线程,强行让类去继承Thread类,这个类就没法继承自己本身的一套体系了。但通过实现接口的方式,就没这个问题
2、实现类的方式更适合来处理多个线程有共享数据的情况。共享数据封装在实现类中,然后实现类再作为实参传给Thread构造器,就实现了数据共享。
3、Thread类本身也是一个Runnable接口的实现类。两种创建线程方法都需要重写run方法,并把要执行的函数体写在run方法中
2.3 通过实现Callable接口创建多线程 JDK5.0新方法
与使用Runnable接口相比,Callable拥有更强大的功能。具体如下:
(1)相比Runnable接口中重写run()方法,Callable接口需要重写call()方法,call()方法可以有返回值(返回值可以给其他线程用),可以抛出异常(run方法的异常内部就解决了,call方法可以把异常抛出来,在其他地方一起解决)
(2)支持泛型的返回值
(3)需要借助FutureTask类,比如获取返回结果
关于FutureTask类
Future接口
(1)可以对具体Runnable, Callable任务的执行结果进行取消、查询是否完成、获取结果等
(2) FutrueTask是Futrue接口的唯一的实现类
(3)FutureTask同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
语法: public Object call() 【throws Exception】 //返回值为祖先类Object(暗示啥对象都可以返回)
{多线程方法体;}
操作步骤:
(1)创建一个实现Callable接口的实现类
(2)在实现类中实现(重写)Callable接口的call方法
(3)创建Callable接口实现类的对象
(4)将此Callable接口实现类的对象作为参数传递到FutureTask构造器当中,创建一个FutureTask对象
(5)FutureTask对象作为参数传递到Thread构造器当中,创建线程。并start启动线程
exp:打印1-100的偶数,并返回和
class NumThread implements Callable{ //step1 创建一个实现Callable接口的实现类类NumThread,从而创建一个线程
public Object call() throws Exception{ /*step2 实现Callable接口的call方法,将此线程需要执行的操作写入方法体中。注意返回值类型 Object*/
int sum=0; //定义一个 sum 表示 和
for(int i=0;i<=100;i++){ //for循环求偶数和,每次+一个新偶数
if(i%2==0){ //如果判定是偶数,则打印偶数,并且累加
System.out.println(i);
sum+=i;
}
}
return sum; /*返回sum,sum是基本数据类型,返回值要求是Object对象,显然,sum被自动装箱成了Integer对象。如果不需要返回值,就return null即可*/
}
}
main(){
NumThread numThread = new NumThread();//step3 创建Callable接口实现类对象
FutureTask futureTask = new FutureTask(numThread);/*step4 将此Callable接口实现类的对象作为参数传递到FutureTask构造器当中,即用FutureTask构造器实例化一个对象futureTask*/
new Thread(futureTask).start();/*step5 将FutureTask作为参数传递到Thread构造器当中,这里用Thread构造器创建一个匿名对象来启动线程。futureTask也实现了Runnable接口,因此可以作为形参*/
Object sum = futureTask.get(); /*通过futureTask的get方法,可以获取到numThread对象call方法的返回值,如果不需要返回值,可忽略此步骤*/
}
2.4 使用线程池方法创建多线程,最常使用的方法 JDK5新增
1、背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。(造线程很浪费时间,且消耗资源)
2、思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
3、好处:
(1)提高响应速度(减少了创建新线程的时间)
(2)降低资源消耗(重复利用线程池中线程,不需要每次都创建)
(3)便于线程管理,比如API中自带以下属性:
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
4、线程池相关API ExecutorService(接口) 和 Executors(工具类)
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor常用方法:
(1)void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
(2)<T> Future<T> submit(CallablexT> task):执行任务,有返回值,一般用来执行Callable
(3)void shutdown() :关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池常用方法:
(1)Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
(2)Executors.newFixedThreadPool(n);创建一个可重用固定线程数的线程池
(3) Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
(4)Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
5、操作步骤
step1:提供指定数量的线程池
step2:执行指定的线程操作,需提供实现Runnable接口或Callable接口的实现类的对象
step3:关闭线程池
exp:遍历1-100偶数
class NumberThread implements Runnable{ //线程想做的事情,遍历1-100偶数
public void run(){
for(int i=0;i<=100;i++){
if(i%2==0){
System.out.println(i);
}
}
}
}
main(){
ExecutorService service = Executors.newFixedThreadPool(10); /*创建一个10个线程的线程池,注意ExecutorService是接口,不能实例化,实例化的是ExecutorService接口的实现类,所以这里是多态性体现,想知道实现类是什么,要用反射的getClass,找到实现类后,可以变量提升为实现类,就可以用线程池的属性啦。
service.execute(new NumberThread());/*通过接口多态,调用实现类的void execute()方法,执行多线程任务。形参是一个实现了Runnable接口的匿名NumberThread对象*/
service.shutdown();//关闭线程池
}
五、线程的调度
1、调度策略
(1)时间片:每个线程执行一段时间
(2)抢占式:高优先级的线程抢占CPU
2、Java的调度方法
(1)同优先级组成先进先出队列(先到先服务),使用时间片策略。
(2)对高优先级线程,使用优先调度的抢占式策略
(3)线程的优先等级 (Thread类定义的3个static常量 看都是大写)
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5 默认优先级
(4)涉及方法(Thread类的方法)
getPriority(); //获取线程的优先级
setPriority(int newPriority);//设置线程的优先级
说明:线程创建时继承父线程的优先级。高优先级线程抢占低优先级线程的低优先级线程cpu的执行权,但是只是概率上的,即高优先级线程优先执行的概率高,并不意味着低优先级线程一定是在高优先级线程之后才能被执行
六、线程的生命周期 Thread.State
Thread 类 的 内部类 Thread.State 定义了线程的几种状态。
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用了Thread类及其子类的对象来表示线程,在他的一个完整的生命周期中通常要经历如下的五种状态:
(1)新建(NEW):当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
(2)就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。或者运行时调用yield()函数,线程又回到就绪状态
(3)运行:当就绪的线程被CPU调度并获得CPU资源时,进入运行状态,run()方法定义了线程的操作和功能
(4)阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态(比如调用join、sleep、wait、等待同步锁的时候,此时线程就阻塞)。阻塞状态不能马上切换成运行状态,阻塞事件完成(比如sleep时间到了,join来的代价执行完了,获取同步锁了),要先回到就绪状态,cpu分配到资源后,再运行。
(5)死亡:线程完成它的全部工作或线程被提前强制的中止或出现异常导致结束。(执行完run()方法,出现Error,或出现Exception异常且没处理)
public enum State{ //Thread内部类 枚举类
NEW, //新创建一个多线程对象,还没执行
RUNNABLE, //线程跑起来了
BLOCKED, //线程阻塞,还没进入同步结构
WAITING, // 调用空参waiting方法
TIMED_WAITING, //指定时间调用waiting方法
TERMINATED; //线程死亡
}
七、线程的同步
1、问题的提出:
(1)多个线程执行的不确定性引起执行结果的不稳定
(2)多个线程对账本的共享,会造成操作的不完整性,会破坏数据。
说明:账户里面有3000,我要取1000,先系统判定存款多于我要取的钱,才能执行。但是判定过程中,还没执行取钱操作时,我媳妇的线程也来取2000钱,她的线程又刚好比较快,此时存款还是3000,也会判定成功。这样一共就取了4000,存款-1000。显然不合理。(一个线程操作某个共享数据,代码块还没执行完成(还没动那个共享数据)。另一个线程也进来一起操作数据。即多个线程没有顺序执行,而是并行执行,因此出现安全问题)
2、解决办法
当一个线程在操作某个共享数据时,禁止其他线程参与进来操作这个数据(即使当前线程出现了阻塞,也不能改变)。直到当前线程操作完这个共享数据后,其他线程才可以进来操作这个共享数据。
在Java中,我们通过同步机制,来解决线程安全问题。
方法一:同步代码块 关键字 synchronized
语法:synchronized(同步监视器){ //同步监视器,俗称“锁”,任何一个类的对象,都可以当作锁,
需要被同步的代码; // 指操作共享数据的代码
}
说明:
(1)同步监视器,俗称“锁”,任何一个类的对象,都可以当作锁,但是多个线程必须要共用同一把锁(很多异常都是由于,稍不注意(创建“锁”对象的位置不对)就给了不同线程不同的锁)。
(2)synchronized代码块中的代码,每次只允许一个线程进去操作,执行完成后,才允许第二个线程进来。相当于被“锁”住了。此时相当于一个单线程的过程,虽然解决了安全问题,但效率会变低。
注意:synchronized代码块中的代码不能包多了,也不能包少了。包多了会降低效率,更重要的是在循环语句中,可能让同一个线程一直不停重复执行代码块中的代码。包少了会导致还是没解决安全问题。
方法二:同步方法
如果操作共享数据的代码刚好完整的声明在一个方法中,就可直接将此方法声明成同步的。
语法: 权限修饰符 【static】 synchronized 返回值类型 方法名(){
方法体;}
说明:
(1)就是把方法一synchronized里面的代码块 弄出来, 单独搞成一个 synchronized方法就行了。
(2)继承类方式需要静态(确保锁一致,而不是3把锁),实现类不需要静态
注意:此方法不用显示的去显式声明同步监视器,此方法会默认锁的对象为this。 this对象在实现类方法实现多线程没问题(实现类的this都是同一个对象即同一把锁);但是在继承类方法中会不对,继承类中的this,是3个不通过对象,相当于有3把锁,就没法实现同步。因此继承类,需要将方法声明为静态,此时默认锁变为类(类也是对象 即 类名.class class为每个类的隐藏静态属性,代表当前类本身,参考反射)
总结:非静态的同步方法,同步监视器(锁)一般是 this;
静态的同步方法,同步监视器(锁)一般是 当前类本身 类名.class
4、方法三 LOCK锁 JKD5.0新增
4.1 Lock(锁)概述:
JDK5.0开始,Java提供新的线程同步机制。通过显式定义同步锁对象来实现同步。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock类实现了Lock ,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
用法:
step1:实例化ReentrantLock对象。
step2:调用 ReentrantLock对象.lock()方法开启同步,
step3:再到合适位置调用ReentrantLock对象.unlock()关闭同步。
建议将同步代码放入try中,以便用finally可以不管发生任何异常都能及时解锁。
exp:三个窗口卖100张票
class window implements Runnable{ //造一个window类来实现Runnable接口
private int ticket =100; //默认有100张票
private ReentrantLock lock = new ReentrantLock();//step1 创建一个实现了Lock接口的ReentrantLock对象lock
public void run(){ //实现Runnable接口中的run方法
while(true){ //只要有票,就卖
try{ //将需要同步的代码放入try中,不是因为有异常,是为了可以finally
lock.lock();//step2 通过lock对象,调用lock方法,开启同步
if(ticket>0){ //判定是否有票
System.out.println(Thread.currentThread().getName()+":售票,票号为"+ticket);
ticket--;//while循环一次,相当于卖了一张票,ticket-1
}else{
break; //如果没有票,退出循环
}
}finally{
lock.unlock();//step3 不管发生任何异常,一定结束同步
}
}
}
}
main(){
window w = new window(); //实例化一个window对象,取名w
Thread t1 = new Thread(w); //将window对象w作为形参传入Thread构造器,创建一个线程t1
Thread t2 = new Thread(w); //将window对象w作为形参传入Thread构造器,创建一个线程t2
Thread t3 = new Thread(w); //将window对象w作为形参传入Thread构造器,创建一个线程t3
t1.start();
t2.start();
t3.start();
}
总结:对比synchronized方法,synchronized方法是进入代码块开启同步,执行完代码块释放同步监视器。Lock需要手动启动同步(lock()),同时需要手动结束同步(unlock())。启动和结束的位置,都是自己看着办。
建议使用同步的顺序:Lock—>同步代码块—>同步方法
5、线程的死锁问题 主要针对synchronized同步方法
死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
说明:在同步块嵌套时,多个线程用了相同对象作为锁。已经进入某同步块(a锁)的A线程执行到另一同步代码块(b锁)时,执行此代码块的锁b正好正在B线程中被使用。而B线程又嵌套了需要a锁才能执行的代码块。A线程执行不完,没法交出a锁。B线程执行不完,没法交出b锁。此时A线程拿不到b锁,B线程拿不到a锁。就形成死锁。
解决方法:
(1)专门的算法、原则
(2)尽量减少同步资源的定义
(3)尽量避免嵌套同步
exp:
main{
StringBuffer s1=new StringBuffer();//造一个可变字符串s1
StringBuffer s2=new StringBuffer();//造一个可变字符串s2
new Thread(){ //创建一个Thread子类的匿名对象(实现run方法就可以实例化),直接start
public void run(){
synchronized (s1){ //同步块 用s1对象当锁,当拿到s1锁时,可执行下面代码
s1.append("a");//给字符串s1的末尾,添加一个字母a
s2.append("1");//给字符串s2的末尾,添加一个“1”
synchronized(s2){ //嵌套一个同步块, 用s2对象当锁,当拿到s2锁时,可执行下面代码(同时有s1和s2才能执行完)
s1.append("b");//给字符串s1的末尾,再添加一个b
s2.append("2");//给字符串s2的末尾,再添加一个“2”
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start(); //创建一个Thread子类的匿名对象(实现run方法就可以实例化),直接start
new Thread(new Runnable(){ //创建一个实现runnable接口的匿名类对象,作为Thread构造器的形参,实现一个线程,直接start
public void run(){ //重写匿名实现类的run方法
synchronized (s2){ //同步块 用s2对象当锁,当拿到s2锁时,可执行下面代码
s1.append("c");//给字符串s1的末尾,添加一个字母c
s2.append("3");//给字符串s2的末尾,添加一个“3”
synchronized(s1){ //嵌套一个同步块, 用s1对象当锁,当拿到s1锁时,可执行下面代码(同时有s2和s1才能执行完)
s1.append("d");//给字符串s1的末尾,再添加一个b
s2.append("4");//给字符串s2的末尾,再添加一个“2”
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start //创建一个实现runnable接口的匿名类对象,作为Thread构造器的形参,实现一个线程,直接start
}
说明:两个线程同时执行,第一个线程要s1 和 s2 才能执行完,第二个线程要s2,和s1才能执行完。如果第一个线程在执行s1监视器中的代码时,刚好第二个线程正在执行s2的代码。此时第一线程没法执行下面s2监视器的代码,要等第二线程执行完交出s2锁。刚好第二线程也没有s1锁,来继续执行下面的代码,也要等s1执行完。这样就都没法执行,永远卡住了。
八、线程的通信
wait(): Object类的方法,祖宗方法,谁都可以用。一旦执行此方法,当前线程进入阻塞,并释放锁(其他线程就可以进来了)
notify():Object类的方法。一旦执行此方法,会唤醒wait()的线程,如果多个线程wait(),就唤醒优先级高的线程
notifyAll():Object类的方法。一旦执行此方法,会唤醒所有被wait的线程。
说明:
(1)notify和notifyAll作为线程通信方法,因此只能用在同步代码块和同步方法中(synchronized同步,非lock同步)。
(2)这些方法必须由同步监视器(锁)调用,否则会报错。一般同步监视器都用当前对象(this),或类充当(类.class)
(3)wait方法要抛异常,需要try-catch
面试题:sleep() 和 wait() 的异同?
相同点:一旦执行方法,都可以使线程进入阻塞状态
不同点:
(1)sleep()是Thread类的方法,而wait()是Object的方法
(2)调用要求不同,sleep()可在任何需要的场景下调用。wait()是通过“同步监视器即锁”来调用的,因此必须在同步代码块中。
(3)如果两个方法用在同步代码块中。sleep()方法不会交出同步监视器;但是wait()方法,会释放同步监视器让另一线程进入
exp:交替打印1-100
class Number implements Runnable{ //定义一个Number实现类来实现Runnable接口
private int number =1; //起始为1
public void run(){ //重写run方法
while(true){ //每次打印一个数
synchronized(this){
notify();//唤醒wait的线程
if(number<=100){ //判定number值,小于100就继续打印
System.out.println(number); //打印number
number++; //打印后number+1
try{
wait(); //阻塞当前线程,并交出锁。注意wait要抛异常,顺便解决了。
}catch (InterruptedException e){
e.printStackTrace();
}
}else{ //大于100后,停止打印
break;
}
}
}
}
}
main(){
Number number = new Number();//实例化一个Number类对象,取名为number
Thread t1 = new Thread(number);//将number作为形参放入构造器,创建一个线程t1
Thread t1 = new Thread(number);//将number作为形参放入构造器,创建一个线程t2
t1.start();
t2.start();
}