关于多线程的文章我已经写过一篇了,为什么还要再写一篇呢,因为上一篇《讲给女朋友听的java多线程》写的太生涩,也有一定小错误,女朋友并没有看懂。所以,博主又肝了一天,写了这篇通俗易懂,有可爱配图的多线程文章。 初入江湖的小李,在见识了“南慕容,北乔峰”的飒爽英姿之后,就励志成为一位武林高手。 通过阅读本文,您好将掌握如下知识点: 本小节介绍什么是程序,什么是进程,什么又是线程,这些概念是本章的基础,刚开始接触,可能会觉得这些概念晦涩难懂,没关系,可以结合后面的实例来理解这些概念。 注意: 由上面的三个图片,我们可以抽象出程序,进程,线程的概念: 注意: 现在,我们的电脑、手机都支持并发的执行程序,比如,我们在逛淘宝的时候,手机还放着音乐,我们在敲代码时,后台可能还运行着API文档,以备我们查看。 单线程的程序往往功能十分有限,例如:开发一个服务器程序,这个服务器程序需要向不同的客户端提供服务,不同的客户之间应该互不干扰,否则这个程序将不会被接收。 多线程程序的优点: 何时需要多线程: 在了解了线程的概念之后,我们在本节来创建线程。Java中使用Thread类表示线程,每一个线程都必须是Thread类的对象或其子类的对象。每个线程对象对应一定的任务,这些任务往往是需要被同时执行的。 Java中一共提供了四种创建线程的方式。分别是:继承Tread类,实现Runnable接口,实现Callable接口,使用线程池。其中,后两种是JDK5新增的创建线程的方式。 注意:使用者四种方式创建多线程时,自己多加思考,体会其中的异同。 多线程的创建:方法一:继承Thread类 步骤: 注意: 下面这个例子创建两个线程: 程序清单如下 Thread类的有关方法: ① 启动线程,并执行对象的run()方法 ② run()方法需要被重写,线程在被调度时执行的操作 ③ 返回线程的名称 ④ 设置该线程名称 ⑤ 返回当前线程。 ⑥ 线程让步,释放CPU使用权,有可能在释放之后有被分配到使用权。 ⑦ 线程阻塞。 ⑧ 让当前线程睡眠(指定时间:毫秒) ⑨ ⑩ 判断线程是否还活着,返回boolean 注意:这些方法暂时不要全部掌握,通过后面的学习,在实践中慢慢使用,就会融会贯通。 多线程的创建:方法二:实现Runnable接口 步骤: 和方法一基本类似,创建一个类去实现Runable接口,在这个类中实现run()方法,把需要执行的操作写在run()方法体中。 程序清单如下 两个创建线程的比较: 实现Runnable的方式更好一点。因为实现它的类可能还有其他的直接父类,导致不能继承Thread。因为java是单继承的,但是可以同时有继承和实现。实现的方式会默认共享数据。 所以,在开发中,优先使用实现Runnable的方式。 相同点:都需要重写run方法。继承的方式在内部也实现的Runable接口。 多线程的创建:方法三:实现Callable接口 注意:此方法为JDK5.0新增的创建线程的方式。 步骤: 特点: 如何理解实现Callable接口创建多线程比实现Runnable接口创建多线程的方式更强大? 在下面例子中,我们创建一个线程来获取100以内所有偶数的和。 程序清单如下 背景: 解决思路: 使用线程池的好处: 步骤: 例: 程序清单如下 本节,我们将理解线程的调度问题。计算机通常只有一个cpu,而在任意时刻只能执行一条指令,每个线程只有获得cpu的使用权才能执行指令。而我们有需要使用多线程,那么计算机是如何实现的呢? 所谓多线程的并发运行,其实是从宏观上看。在计算机内部,各个线程轮流获取cpu的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待cpu,java虚拟机的一项任务就是负责线程的调度。 调度策略: 线程的优先级: 方法: 返回线程优先值 改变线程的优先级。 例:设置为最大优先级 说明: 线程的分类 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。 人有生老病死,线程也和人一样,同样具有生命周期。线程的生命周期包含五种状态,分别是:新建,就绪,运行,阻塞,死亡。通过上面几节的学习,我们知道了如何创建线程,以及线程的调度问题。本节,我们将看到一个线程的生老病死。 线程的新建和就绪就类似我们人类的出生和大学毕业。 注意: 程序清单如下 通过上面的实例,我们可以看到,启动一个线程的正确方法是调用线程对象的start()方法,该方法会自动调用run()方法。如果直接调用线程对象的run()方法,系统就只会吧它当做一个普通对象,这时,多线程就失去的它存在的意义,当前程序就会变成一个单线程程序。 线程的运行状态,就恰好对应我们大学毕业找到了一份好工作,在勤勤恳恳的努力着。 线程的阻塞状态,对应到我们身上就是失业的时候,这时候,就需要重新进入就绪状态,比如我们学习了新知识,然后再去找工作,线程就比较懒了,它只是在等CPU的重新调度或其他线程的帮助。 当发生如下情况时,线程会进入阻塞状态。 线程在以下情况下会死亡: 使用isAlive()方法检测一个线程是否死亡,当线程处于新建和死亡两种状态时,该方法返回false,当线程处于就绪、运行、阻塞状态时,该方法返回true。 注意:不要对已经死亡的线程调用start()方法使其复活,这是不可能的。 在单线程中,每次只完成一个任务,当然没有什么安全问题。就像一个人专心致志做一件事情时,就很少发生做错或忘了某步骤。而在一心多用时,就有很大的几率发生做错或做的很不完美的的事情。同样,在多线程中,也存在这样类似的问题 — 数据共享。 我们通过一个例子来引如线程同步的问题,现在,我们设计一个程序,模拟车站卖票,为了贴近真实生活,我们创建三个窗口同时卖票,然而又恰逢春运期间,只有一百张票。 我们使用继承Thread类和实现Runnable接口的两种方式来创建窗口线程。 一、使用继承Thread类创建线程 程序清单如下 运行结果如下 这是程序运行结果的部分截图,我们可以看到,会出现重票的情况,这就情况肯定是不允许发生的。 二、实现Runnable接口创建多线程 程序清单如下 运行结果如下 从这个运行结果看,好像大部分的重票问题解决了,但是还是会有三张100的票,这显然也是不行的。 我们分析出现线程安全的原因: 解决思路: Java中通过线程同步来解决线程安全问题。 在上面模拟窗口卖票的程序中,由于run()中的卖票行为可以同时被多个线程执行,导致了线程安全问题。现在,我们限制对卖票行为的访问,同一时间只允许一个线程对其进行操作。我们可以使用同步代码块的方法,实现这个限制。 格式: 这段代码的含义是,在执行对共享数据的操作时,要首先获得同步监视器,简称:锁。 理解两个概念:共享数据和同步监视器 同步监视器的要求: 注意: 同步的优缺点: 同步的范围: 现在,我们使用两种方式来解决上面卖票程序中出现的线程安全问题。 方法一:实现Runnable接口创建多线程解决线程安全问题: 程序清单如下 运行结果如下 我们看到,现在,已经没有重票的问题出现了。 方法二:继承Thread类创建多线程使用同步代码块解决问题的代码: 程序清单如下 运行结果如下 我们看到。线程安全问题同样被解决了。 与同步代码块相对应的,是同步方法。使用synchronized关键字修饰操作共享数据的的方法。该方法就被称为同步方法。即:如果操作共享数据的代码完整的声明在一个方法中,我们可以将这个方法声明为同步的。 同步方法仍然有同步监视器,只是不需要我们显示的声明。 我们使用同步方法来解决懒汉式创建单例对象的线程安全问题。 程序清单如下 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当,每次只能有一个线程对Lock对象加锁,线程开始操作共享数据之前,要先获得Lock对象。 ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。 使用ReentrantLock对象来充当锁进行同步时,我们建议把释放锁的操作放在finally语句块中,确保一定会释放锁,防止死锁的出现。 我们同样以卖车票的例子来说明。 程序清单如下 synchronized 与 Lock 的对比: 优先使用顺序: 在同步代码块和同步方法中,何时会释放对同步监视器的锁定 以下情况不会释放同步监视器 总结: 我们来看一个典型例题,来加深对线程同步的理解 问题分析: 程序清单如下 运行结果如下 注意: 什么是线程死锁,举个很形象的例子。 下面的程序就演示了线程死锁。 程序清单如下 这个程序运行时,不会出错,也不会报异常,就是一直僵持着,除非我们手动停掉程序的运行。 我们的程序中不应该出现线程死锁的问题,我们可以通过下面几种常见的方法来避免线程死锁。 当执行多线程程序时,CPU会在多个线程之间进行调度,而调度具有一定的随机性,我们无法在程序中准确控制线程之间的轮换执行。 线程通信中的三个常用方法:wait(); 、 notify();、notifyAll(); 注意: 以下程序会出现非法监视器的错误 程序清单如下 在这个程序中,我们new了一个Object类的对象充当同步监视器,在同步代码块中调用notifyAll()非法,默认通过this调用,而这里的同步监视器是Object对象,出现了不一致,所以会报非法同步监视器异常。 sleep()方法和wait()方法的异同: 我们通过一个典型的例题来理解线程的通信 例: 这里可能出现两个问题: 问题分析: 程序清单如下 运行结果如下 至此,小李使用“多线程”心法,修炼多年,终成一代宗师。就让我们江湖见。文章目录
各位大佬,拿去给女朋友看吧。什么?没有女朋友?那就赶紧学java吧,new一个对象出来。一、前言
小李开始了日复一日的修炼,然而在修炼了一年之后,小李的进境缺很慢,小李百思不得其解,正在恼怒之际,一位仙风道骨的老人传授他一门心法,名叫“多线程”,这门功法的强大之处就在于可以分心多用,同时修炼多种功法,这样一来,小李的进境就很快。
然而,这门心法缺有一个很大的缺陷,“线程同步问题”,就是在运行心法的时,如果多门功法需要同时使用丹田,这时,如果不小心就会走火入魔。
二、线程概述
注意:线程的概念不必死记硬背,可以在完成本章的学习之后,再来回顾这些概念,你就回你有豁然开朗的感觉。2.1 概述
我们可以看到,小汽车是是静态的,即:程序时静态的。而运行中的发动机是动态的,即:进程是动态的。
2.2 并行与并发
这些程序好像是在同时运行,然而并不是,对一个CPU而言,每个时刻,只能有一个程序在运行,但是CPU会在多个程序之间进行切换,而CPU的切换速度又很快,我们就感觉好像是这些程序在同时执行。2.3 多线程优点
单线程程序只有一个顺序执行流,多线程则可以包括多个顺序执行流,多个线程之间互不干扰。
三、线程的创建
3.1 继承Thread类
① 需要通过对象来启动线程。
② start方法会自动调用当前线程的run方法。不能直接调用run方法。
③ 不能让已经start的线程再去执行,会报异常。需要再去创建一个线程对象,通过这个对象再start。
① 一个线程用来输出偶数
创建一个继承自Thread的类,在类中重写run()方法,run()方法中写在线程被调度是执行的操作,比如:这里我们在run()方法内部输出100以内的偶数。
② 一个线程用来输出奇数
我们在main方法中使用匿名内部类的方式创建一个线程,用来输出100以内的奇数,和上面的基本一致,在类中重写run()方法,在run()方法内部写需要执行的操作。
class Test extends Thread{ @Override public void run() { for(int i=0;i<100;i++) { //判断是偶数 if(i%2==0) { System.out.println("偶数:"+i); } } } } public class Demo1 { public static void main(String[] args) { //创建一个输出偶数的线程 Test test = new Test(); //启动该线程 test.start(); Thread.currentThread().setName("主线程");//给main程序命名 //创建Thread类的匿名子类 new Thread() { @Override public void run() { for(int i=0;i<100;i++) { //判断是奇数 if(i%2!=0) { System.out.println(Thread.currentThread().getName()+"匿名奇数:"+i); } } } }.start(); } }
void start();
void run();
String getName();
void setName(String name);
static Thread currentThread();
static void yield();
join();
比如:线程a的运行过程中需要一个参数,这个参数需要线程b提供,这时,就需要join()方法。
static void sleep(long millis);
强制线程生命期结束,不推荐使用。(已经过时了)
stop();
boolean isAlive();
3.2 实现Runnable接口
class Test3 implements Runnable{ @Override public void run() { for(int i=0;i<100;i++) { if(i%2==0) { System.out.println("偶数:"+i); } } } } public class Demo1 { public static void main(String[] args) { Test3 test3 = new Test3(); Thread t = new Thread(test3); t.start(); } }
3.3 实现Callable接口
① 与使用Runnable相比, Callable功能更强大些
② 相比run()方法,可以有返回值
③ 方法可以抛出异常
④ 支持泛型的返回值
⑤ 需要借助FutureTask类,比如获取返回结果 。
⑥ get方法的返回值即为FutureTask构造器参数Callable实现类对象重写的call方法的返回值。
① call()方法可以有返回值。
② call()方法可以抛出异常,被外面的操作捕获,然后处理异常
③ call()方法支持泛型
首先,我们创建一个类实现Callable接口,并实现call()方法,同样,把该线程需要执行的操作放在call()方法内部。Call()方法会在线程启动时,被自动调用。Call()方法可以理解为一个增强版的run()方法。 class NewThread implements Callable{ @Override public Object call() throws Exception { int sum=0; for(int i=1;i<=100;i++) { //判断是偶数 if(i%2==0) { sum=sum+i; } } //自动装箱 return sum; } } public class ThreadNew { public static void main(String[] args) { NewThread newThread = new NewThread(); FutureTask futureTask = new FutureTask(newThread); Thread t1 = new Thread(futureTask); t1.start(); try { //get方法只是为了返回call方法的返回值。 //get方法自动调用newThread类对象的call()方法 Object sum = futureTask.get(); System.out.println(sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
3.4 使用线程池
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
比如,我们开发一个类似美团外卖的APP,用户在浏览页面时,需要在加载店家信息的同时加载美食的图片,这时就需要使用多线程,一个线程用来加载文字信息,一个线程用来加载图片。
这时,饥肠辘辘的用户就有可能浏览页面很快,如果使用以上三种方式来创建线程,就会发生,图片信息和文字信息加载不同步的问题,因为线程的创建也需要一定的开销。这会大大影响我们软件的体验感。
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具,我们需要时就能直接使用,而不用等到它创建出来。
① 提高响应速度(减少了创建新线程的时间)
② 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
③ 便于线程管理
corePoolSize
:核心池的大小
maximumPoolSize
:最大线程数
keepAliveTime
:线程没有任务时最多保持多长时间后会终止
在下面例子中,我们使用实现Runnable接口的方式创建两个线程,并把这两个线程加入到线程池中,一个线程用来输出偶数,一个线程用来输出奇数。
先创建一个容量为10线程池,把我们创建的两个线程加入到线程池中,加入到线程池中的线程会被自动调用,线程执行结束之后,该线程并不会死亡,而是将该线程返还给线程池以备下次使用。
class NewThread5 implements Runnable{ @Override public void run() { for(int i=0;i<100;i++) { if(i%2==0) { System.out.println(Thread.currentThread().getName()+"偶数:"+i); } } } } class NewThread6 implements Runnable{ @Override public void run() { for(int i=0;i<100;i++) { //判断是奇数 if(i%2!=0) { System.out.println(Thread.currentThread().getName()+"奇数:"+i); } } } } public class NewThread4 { public static void main(String[] args) { //service是线程池 ExecutorService service = Executors.newFixedThreadPool(10); NewThread5 newThread5 = new NewThread5(); NewThread6 newThread6 = new NewThread6(); //输出偶数的线程 //适合于Runable service.execute(newThread5); //输出奇数的线程 service.execute(newThread6); //适合Callab // service.submit() //关闭线程池 service.shutdown(); } }
四、线程的调度
4.1 线程调度的理解
线程调度是指按照特定机制为多个线程分配CPU的使用。
① 时间片:同优先级线程组成先进先出队列(先到先服务),使用时间片策略
② 对高优先级,使用优先调度的抢占式策略
MAX_PRIORITY
:10
MIN _PRIORITY
:1
NORM_PRIORITY
:5
getPriority();
setPriority(int p);
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
高优先级的程序要抢占低优先级CPU的执行权,但只是从概率上讲,高有优先级的程序高概率被执行。并不意味着只有当高优先级执行完之后才执行低优先级。4.2 线程的分类
Java中的线程分为两类:
1. 一种是守护线程。
2. 一种是用户线程。
thread.setDaemon(true)
可以把一个用户线程变成一个守护线程。 Java垃圾回收就是一个典型的守护线程。五、线程的生命周期
5.1 新建和就绪
就绪状态是使用的start()方法,不是直接调用的run()方法,如果直接调用run()方法,那它仅仅是一个普通对象调用run()方法,并没有启动线程。 class Test3 implements Runnable{ @Override public void run() { for(int i=0;i<100;i++) { if(i%2==0) { System.out.println("偶数:"+i); } } } } public class Test { public static void main(String[] args) { Test3 test3 = new Test3(); Thread t = new Thread(test3); //没有启动线程,系统只会把它当做一个普通对象。 t.run(); } }
5.2 运行和阻塞状态
① 线程调用sleep()方法时,表示当前线程主动放弃CPU的执行权
② 当前的同步监视器被其他线程获得,该线程只能处于阻塞状态。
③ 当前线程在等待某个线程的notify().
对于以上情况,在阻塞结束时,该线程会重新进入就绪状态。
① sleep()指定时间已过
② 线程成功获得了同步监视器
③ 线程得到了其他线程的通知。5.3 线程死亡
① run()方法或call()方法执行完毕
② 线程执行过程中调用的stop()方法,强制让该线程死亡,但该方法容易发生死锁。
③ 线程捕获了一个异常。六、线程同步
6.1 为什么要线程同步
class Window extends Thread{ //票数 只有100张 private int ticket=100; @Override public void run() { while (true) { //当前票数大于0,就卖票 if(ticket>0) { System.out.println(Thread.currentThread().getName()+":"+"卖票,票号为:"+ticket); ticket--; } else { break; } } } } public class Test { public static void main(String[] args) { //new了三个Windowd对象,表示三个窗口 Window w1 = new Window(); Window w2 = new Window(); Window w3 = new Window(); //设置三个窗口线程的名字 w1.setName("窗口一"); w2.setName("窗口二"); w3.setName("窗口三"); //启动三个窗口线程,开始卖票 w1.start(); w2.start(); w3.start(); } }
为什么会出现这种情况,读者可能会发现,我们上面new了三个Window线程对象,是不是每个线程对象都持有100张票呢?我们继续看下面这个例子,它只new出一个Window对象。class Window2 implements Runnable{ private int ticket=100; @Override public void run(){ while (true) { //票数大于0,则卖票 if(ticket>0){ System.out.println(Thread.currentThread().getName()+":"+"卖票,票号为:"+ticket); ticket--; } else { break; } } } } public class Test { public static void main(String[] args) { //因为只new了一个Window对象,所有共用一个ticket Window2 w2 = new Window2(); //创建三个线程 Thread t1 = new Thread(w2); Thread t2 = new Thread(w2); Thread t3 = new Thread(w2); //设置线程名 t1.setName("窗口一"); t2.setName("窗口二"); t3.setName("窗口三"); //启动线程,开始卖票 t1.start(); t2.start(); t3.start(); } }
至此,我们可以得出,在使用多线程处理问题时,特别是有共享数据时。会发生线程安全问题。
当某个线程操作车票的过程中,尚未完成操作,其线程也参与进来操作车票,这就导致了重票问题的发生
当一个线程A在操作车票时,其他线程不能参与进来,当A操作完成后,其他线程再操作。即使线程A出现阻塞,也不能改变。
线程同步有三种方法,分别是:
① 同步代码块
② 同步方法
③ Lock锁6.2 同步代码块
synchronized(obj){ //需要被同步的代码,即:操作共享数据的代码 }
而且同一时间只能有一个线程获得同步监视器,其他没有获得同步监视器的线程则处于阻塞状态,直到该线程释放同步监视器,其他线程才可以获得同步监视器,进而对共享数据进行操作。
① 共享数据:多个线程共同操作的变量,例如:上面程序中的车票ticket。
② 同步监视器:俗称“锁”。一个监视器就相当于一扇门,里面锁着的是共享的资源,每次只能有一个人能进入,并且只能容纳一个人,也就是说只有一个线程能获得这个锁。
① 任何一个类的对象都可以充当一个锁。
② 多个线程共用同一个锁。
③ 推荐使用共享数据充当锁。
① 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
② 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),this的使用需谨慎,确保是同一个对象才可以。
③ 在实现Runnable接口创建多线程的方式中,我们可以使用this充当锁来代替手动new一个对象,因为后面我们只创建了一个线程的对象。
④ 在继承Thread类创建多线程的方式中,慎用this,考虑我们的this是不是唯一的。
① 明确哪些代码是多线程运行的代码。
② 明确多个线程是否有共享数据。
③ 明确多线程运行代码中是否有多条语句操作共享数据
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即:所有操作共享数据的这些语句都要放在同步范围中
① 范围太小:没锁住所有有安全问题的代码
② 范围太大:没发挥多线程的功能class Window2 implements Runnable{ private int ticket=100; @Override public void run() { while (true) { //同步代码块,因为我们创建了三个线程,所以不能使用this作同步监视器 synchronized(Window2.class) { //票数大于0,则卖票 if (ticket > 0) { try { //是当前线程sleep100毫秒,体现线程的等待 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":" + "卖票,票号为:" + ticket); ticket--; } else { break; } } } } } public class Test { public static void main(String[] args) { //创建一个窗口对象 Window2 w1 = new Window2(); //创建三个线程 Thread t1 = new Thread(w1); Thread t2 = new Thread(w1); Thread t3 = new Thread(w1); //设置线程名字 t1.setName("窗口一"); t2.setName("窗口二"); t3.setName("窗口三"); //启动线程 t1.start(); t2.start(); t3.start(); } }
class Window extends Thread{ //使用static修饰保证,保证上线程共用ticket private static int ticket=100; @Override public void run() { while (true) { synchronized (Window.class) { //票数大于0,则卖票 if (ticket > 0) { try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":" + "卖票,票号为:" + ticket); ticket--; } else { break; } } } } } public class Test { public static void main(String[] args) { //创建三个线程 Window w1 = new Window(); Window w2 = new Window(); Window w3 = new Window(); //设置线程名字 w1.setName("窗口一"); w2.setName("窗口二"); w3.setName("窗口三"); //启动线程 w1.start(); w2.start(); w3.start(); } }
6.3 同步方法
① 非静态同步方法,同步监视器:this
② 静态同步方法,同步监视器:当前类本身class Bank{ //无参构造器 private Bank() {} //缓存单例类的对象 private static Bank instance=null; //同步方法 public static synchronized Bank getInstance() { //表示还没有创建单例对象 if(instance==null) { instance = new Bank(); } return instance; } }
6.4 Lock锁
Lock锁也遵循“加锁–修改–释放锁”的逻辑。 class Window5 implements Runnable{ private int ticket = 100; //1. 实例化一个ReentrantLock对象 //默认参数是false,写为true之后,表示是公平的锁 private ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true) { //2. 调用锁定方法。lock方法。获得锁 lock.lock(); try { //票数大于0 则卖票 if(ticket>0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":" + "卖票,票号为:" + ticket); ticket--; } else { break; } }finally { //3. 解锁 lock.unlock(); } } } } public class Test { public static void main(String[] args) { //创建窗口对象 Window5 w5 = new Window5(); //创建三个线程 Thread t1 = new Thread(w5); Thread t2 = new Thread(w5); Thread t3 = new Thread(w5); //设置线程名字 t1.setName("窗口一:"); t2.setName("窗口二:"); t3.setName("窗口三:"); //启动三个线程 t1.start(); t2.start(); t3.start(); } }
6.5 对比三种方式
Lock –> 同步代码块(已经进入了方法体,分配了相应资源)–> 同步方法(在方法体之外)
解决线程安全问题,有几种方式?
① synchronized
同步代码块
同步方法
② Lock
例题:银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
① 是不是多线程问题?
肯定是,两个线程,分别是两个储户
② 是否有共享数据?
有,账户
③ 所以,存在线程安全问题。
使用同步机制解决 class Account{ //余额 private double balance; //使用继承的方式创建多线程,需要加static,保证是一把锁 private static ReentrantLock lock = new ReentrantLock(); public Account(double balance) { this.balance=balance; } //方法一:使用synchronized的同步方法 // public synchronized void deposit(double amt) //方法二:使用lock //存款 public void deposit(double amt) { //加锁 lock.lock(); try { //存款金额大于0,则存款 if(amt>0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } balance=balance+amt; System.out.println(Thread.currentThread().getName()+":"+"存钱成功,余额为:"+balance); } } finally { //释放锁 lock.unlock(); } } } /** * 为了演示这个程序可以使用this作为同步监视器,使用继承的方式创建多线程。 */ class Customer extends Thread{ //账户 private Account account; public Customer(Account account) { this.account=account; } @Override public void run() { for(int i=0;i<3;i++) { //存款1000 account.deposit(1000); } } } public class Test { public static void main(String[] args) { //创建一个账户 Account account = new Account(0); Customer customer1 = new Customer(account); Customer customer2 = new Customer(account); customer1.setName("甲"); customer2.setName("乙"); customer1.start(); customer2.start(); } }
推荐使用实现的方式创建多线程。本程序为了演示可以使用this作为同步监视器,使用继承的方式创建多线程。
采用继承的方式创建多线程,使用同步方法时,慎用this,这个题可以使用this,是因为this是唯一的,是同一个Account。6.6 线程死锁
你和你的好朋友去吃饭,餐桌上有一盘十分美味的佳肴,你和你的朋友都想吃,但是只有一双筷子,你和你的朋友一人抢到一只筷子,你们都不愿意放弃这顿美味佳肴,都在等对方放弃筷子,这时,你们便一直僵持着。这就是死锁。
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续类似与死循环。public class Lock { public static void main(String[] args) { StringBuffer s1 = new StringBuffer(); StringBuffer s2 = new StringBuffer(); //继承方式创建匿名多线程 new Thread() { @Override public void run() { synchronized (s1) { s1.append("A"); s2.append("1"); try { //加大死锁概率 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s2) { s1.append("B"); s2.append("2"); System.out.println(s1); System.out.println(s2); } } } }.start(); //实现方式创建匿名多线程 new Thread(new Runnable() { @Override public void run() { synchronized (s2) { s1.append("C"); s2.append("3"); synchronized (s1) { s1.append("D"); s2.append("4"); System.out.println(s1); System.out.println(s2); } } } }).start(); } }
七、线程通信
比如,我们开发时,需要多个模块之间的协调,A模块需要B模块给出一个参数,这时,在执行A模块时,就必须转去执行B模块,这就是线程之间的通信。
本小节通过例题来说明线程之间的通信。7.1 常用方法
wait();
是调用其的线程进入阻塞状态。并释放锁。notify();
唤醒被阻塞的线程。如果有多个线程,就唤醒优先级高的那个。notifyAll();
唤醒所有被阻塞的线程。
class Number implements Runnable{ private int number=1; private ReentrantLock lock = new ReentrantLock(); //创建一个对象充当同步监视器 Object object = new Object(); @Override public void run() { while (true) { synchronized (object) { //唤醒一个线程,默认是this调用。 notify(); if(number<101) { System.out.println(Thread.currentThread().getName()+"打印:"+number); number++; try { //使得调用如下wait方法的线程进入阻塞状态,wait会释放锁 wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { break; } } } } }
7.2 sleep()和wait()
生产者和消费者的问题:生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
① 是否是多线程?
是,生产者线程,消费者线
② 共享数据是什么?
店员(产品)
③ 如何解决线程安全问题?
同步机制,三种方法
线程的通信//店员 class Clerk{ private int amount=0; //生产产品 public synchronized void producePorduct()//同步方法 { //产品小于20时,生产者开始生产产品 if(amount<20){ amount++; System.out.println(Thread.currentThread().getName()+":开始生产第"+amount+"个产品"); notify(); } //产品大于20时,生产者停止生产,等待消费者消费 else{ //等待 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } //消费产品 public synchronized void consumeProduct() { //当产品数大于0时,即:当前有产品时,消费者开始消费产品 if(amount>0) { System.out.println(Thread.currentThread().getName()+":开始消费第"+amount+"个产品"); amount--; notify(); } //当前已经没有产品时,消费者等待生产者生产 else { //等待 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } //生产者线程 class Producer implements Runnable{ //店员对象 private Clerk clerk; public Producer(Clerk clerk) { this.clerk=clerk; } @Override public void run() { System.out.println(Thread.currentThread().getName()+"开始生产……"); while (true) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } clerk.producePorduct();//生产者生产产品 } } } //消费者线程 class Consumer implements Runnable{ private Clerk clerk; public Consumer(Clerk clerk) { this.clerk=clerk; } @Override public void run() { System.out.println(Thread.currentThread().getName()+"开始消费……"); while(true){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //消费者消费产品 clerk.consumeProduct(); } } } public class Test { public static void main(String[] args) { //创建一个店员对象 Clerk clerk = new Clerk(); //创建生产者对象 Producer p1 = new Producer(clerk); //创建消费者对象 Consumer c1 = new Consumer(clerk); //创建生产者线程 Thread t1 = new Thread(p1); t1.setName("生产者"); //创建消费者线程 Thread t2 = new Thread(c1); t2.setName("消费者"); //启动线程 t1.start(); t2.start(); } }
本网页所有视频内容由 imoviebox边看边下-网页视频下载, iurlBox网页地址收藏管理器 下载并得到。
ImovieBox网页视频下载器 下载地址: ImovieBox网页视频下载器-最新版本下载
本文章由: imapbox邮箱云存储,邮箱网盘,ImageBox 图片批量下载器,网页图片批量下载专家,网页图片批量下载器,获取到文章图片,imoviebox网页视频批量下载器,下载视频内容,为您提供.
阅读和此文章类似的: 全球云计算