黑马程序员 —— Java多线程1 (第十一天)

来源:互联网 发布:森林优化 编辑:程序博客网 时间:2024/05/20 18:50

------- android培训、java培训、期待与您交流! ----------

一   概述


1.引子

要了解“线程”,就要先了解“进程”的概念,我们可以在Windows系统任务管理器中看到一个个“进程”。

这么多的进程,看上去是同时执行的,但其实只是CPU在做快速的切换。

而一个进程,包含至少一个或者包含多个线程。


2.进程与线程的定义

(1) 进程:一个执行中的程序,是系统进行资源分配和调度的独立单元。

           (每一个进程执行都有一个执行顺序,该顺序就是执行路径)

(2) 线程:进程中的一个独立的(执行顺序)控制单元。

(3) 关于Java.exe :Java JVM 启动的时候会有一个进程 Java.exe

该进程中至少有一个(当然就是多个)线程负责 Java 程序的执行。

而且这个线程运行的代码存在于main方法中。

该线程称为主线程。

例如:“迅雷”这个进程,它除了把下载分为多个下载的任务,还把任务分为多个部分,

同时进行发出请求,(将下载起点分成了N个地方),这就是多线程的下载了。


补充:

线程是进程中的内容,每个应用程序里面都有线程,它是程序的控制单元或者执行路径。

进程在内存中开辟了空间,而线程才是真正执行程序的单元。

其实更细节说明虚拟机JVM,JVM启动不止主线程一个线程,还有负责垃圾回收机制的线程。

javac 命令执行的之后可以在Windows的任务管理器看到编译进程javac.exe,java命令同理可以看到虚拟机执行的进程java.exe.

JVM启动的时候,会有一个进程java.exe,该进程中至少有一个线程在负责java程序的执行,

而且这个线程运行的代码存在于main方法中,该线程称之为主线程。


多线程存在的意义:可以让程序产生“同时执行”的效果,多段代码同时执行,提高效率。


二   了解如何创建线程


创建新执行线程有两种方法:

  • 继承Thread类,该子类应重写Thread 类的 run 方法。
  • 创建线程的另一种方法是声明实现 Runnable 接口的类。

1.继承Thread类

步骤:定义一个继承Thread类的类,重写Thread类中的run方法,调用线程的start方法。

start方法有两个作用:启动线程,调用run方法。

(注意!!!不调用start方法,这个线程是不会执行的,run里面的方法不会有效)

public class Test {    public static void main(String[] args) {        Demo demo = new Demo();        demo.start();        for (int x = 0; x <= 60; x++) {            System.out.println("demo --" + x);        }    }}class Demo extends Thread {    public void run() {        for (int x = 0; x <= 60; x++) {            System.out.println("run --" + x);        }    }}
上面的程序:输出是
run --59run --60demo --10demo --11……
两者交替输出,各自到达60。


观察上面的程序,可发现运行结果每一次都不同,

是因为多个线程都在获取CPU的执行权。CPU执行到谁,谁就运行。

明确一点,在某一个时刻,只能有一个程序在运行。(多核除外)

CPU在做着快速的切换,以达到看上去同时运行的效果。

我们可以形象地把多线程运行行为理解为在互相抢夺CPU的执行权。

这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长时间,CPU说了算。


题:解释为什么要重写run方法?

答:首先,Thread类用于描述线程。该类就定义了一个方法,用于存储线程要运行的代码,也就是run方法。

    Thread类中的run方法,是用于存储线程要运行的代码。也就是说,要让你的程序多线程执行,就需要重写run方法。

    所以就要重写run方法,将自定义代码存储在run方法中,让你想要执行的功能执行。


题:请说一下run()方法和start()方法的区别?

答:如果仅仅调用run()方法,则结果就是程序还是一个单线程程序;只有调用start()方法,程序才能启动多线程。

    run()方法就像个容器,仅仅是封装多线程要运行的代码,并不能启动多线程;

    Java程序不会创建线程,Java程序通过调用start()方法,

    然后start()方法调用OS的底层代码,去启动多线程。

 Demo d = new Dmo(); //创建好一个线程      //demo.start(); //开启线程并执行该线程的run方法      demo.run(); //这样会按顺序,执行完run -- 输出,再执行demo --输出。                  //仅仅是对象调用方法,而线程创建了,并没有运行。run仅仅是封装了线程所要执行的代码而已。

2.实现Runnable接口

步骤:

(1)定义类实现Runnable接口

(2)覆盖Runnable接口中的run方法。将线程要运行的代码存放在该run方法中。

(3)通过Thread类建立线程对象。

(4)将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法。

(5)调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。


示例代码:

public class Test {    public static void main(String[] args) {        RunnableTest runnableTest = new RunnableTest();        Thread thread = new Thread(runnableTest);        thread.start();        /* 简化到一步到为,new Thread(new Runnable()).start(); */    }}class RunnableTest implements Runnable {    public void run() {        System.out.println("线程运行代码");    }}

不常用的简化模式:

public class Test {    public static void main(String[] args) {        new Thread(new Runnable() {            public void run() {                System.out.println("线程运行代码");            }        }).start();    }}

题:为什么要将Runnable接口的子类对象传递给Thread的构造方法?

答:因为自定义的run方法所属的对象是Runnable接口的子类对象。

由此看见,我们必须要让线程去执行指定对象的run方法,就必须明确该run方法所属的对象。


3.对比两种创建线程的区别

继承Thread类的方式:将线程运行代码放在Thread子类的run方法;

实现Runnable接口的方式:将线程代码存放在接口的子类的run方法中;

对比可知,实现接口的方式可避免单继承的局限性,更能体现面向对象的思想,

在创建线程时,也建议优先使用实现Runnable接口的方式。


三   线程运行的状态


线程的状态有:新建、运行、冻结、消亡、临时阻塞。

其实应该说是线程的“生命周期”,不同教程有不同的线程状态转换图,

在此以毕老师的视频教程图例为标准。它们之间的状态转换如下:


各种状态的简单解释:

新建:Thread t = new Thread();就创建了一个线程

运行:t.start();

冻结:t.sleep(500);   t.wait(); (sleep有时间限制,而wait没有)

消亡:t.stop();

临时状态(阻塞):线程被start后,有不运行,因为CPU还要先运行完其它进程,该线程正在等CPU的执行权。


四   获取线程对象及名称


每个线程都有自己默认的名称,一般是"Thread-编号",编号从0开始。

而主线程的名称,则是“main”。


Thread类 封装了获取线程名称的方法:

  • static Thread currentThread():获取当前线程对象。
  • getName():获取线程名称。
  • 设置线程名称:通过setName()方法或者构造方法设置。
  • 注意:主线程不能设置名称,它的线程名是默认的(main)。
class ThreadTest extends Thread {    public void run() {        while (true) {            System.out.println(Thread.currentThread().getName() + "...run");        }    }}public class Test {    public static void main(String[] args) {        ThreadTest t1 = new ThreadTest();        ThreadTest t2 = new ThreadTest();        ThreadTest t3 = new ThreadTest();        t3.setName("ThreadTest");        t1.start();  //Thread-0...run        t2.start();  //Thread-1...run        t3.start();  //ThreadTest...run        while (true) {            System.out.println(Thread.currentThread().getName() + "...run");   //main...run        }    }}

上面的程序,就是 注释里面的语句一直交替输出。

五   售票的例子


需求:简单的卖票程序,多个窗口同时卖票。

// 多个窗口同时卖100张票public class Demo {    public static void main(String[] args) {        Ticket t = new Ticket();        new Thread(t, "窗口1").start();        new Thread(t, "窗口2").start();        new Thread(t, "窗口3").start();        new Thread(t, "窗口4").start();    }}class Ticket implements Runnable {    public static int tickets = 100;    public void run() {        while (tickets > 0) {            System.out.println(Thread.currentThread().getName() + " 卖出第 "                    + tickets-- + " 张票.");        }    }}

六   多线程存在的安全隐患问题


1.模拟案例


像上面那样多线程卖票,会不会出现问题呢?

下面通过Thread的sleep()方法刻意去模仿这种情况的出现

class Ticket extends Thread {    private static int tick = 100;    public void run() {        while (tick > 0) {            if(tick==20){   //注意这里                try {                    Thread.sleep(100);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }            System.out.println(Thread.currentThread().getName() + " ...sale:"                    + tick--);        }    }}public class TicketDemo {    public static void main(String[] args) {        new Ticket().start();        new Ticket().start();        new Ticket().start();        new Ticket().start();    }}
最后的输出是:
……Thread-1 ...sale:1Thread-0 ...sale:0Thread-2 ...sale:-1
想象一下这个极端情况,如果四个线程只卖1张票,

线程0在判断票数>0后,暂停了一下,

线程1进入判断后卖票了,票数减去1了,变成0张票,

当线程0暂停完毕,再卖票,得出的结果就会是-1。


所以,多线程就出现了安全问题。

不相关的知识点:接口的方法不能抛异常,只能try……catch。


2.问题原因(重点)


当多条语句在操作同一个线程共享数据时,

一个线程对多条语句只执行了一部分(,CPU就切换到其他线程去了),还没执行完,

另一个线程就参与进来执行,导致共享数据的错误。


3.解决方法

对多条操作共享数据的语句,只能让每一个线程都执行完。在执行过程中,其他线程不能参与。

Java对多线程的安全问题提供了专业的解决方式,

就是同步代码块:

synchronized(对象){需要被同步的代码}

操作共享数据的代码,就需要同步。


一个值得思考的问题:

用继承Thread类的方式,去写卖票程序,Ticket票数必须加上static。

而用实现接口Runnable的方式,Ticket票数类,则不需要加static。

否则,程序将不满足要求,为什么呢?

首先,用继承Thread类的方式,Ticket已经是Therad的子类了,

每次new Ticket()都会产生新的100张票,这显然不符合4个售票窗口卖100张票的要求。

再次,用实现Runnable接口的方式,它先要有一个实现Runnable接口的类Tick,

这个类定义好了线程所要执行的run()方法,再传入Thread类的构造方法中,

此时不会影响Tick类,它还是只有100张票,新建的4个线程就会共同操作Tick类的100张票,

因此,满足题目的要求,不需要加上static。


七   线程同步1 - 引入概念


1.简单示例代码

以上面卖票程序为例,加入同步代码块。

class Ticket implements Runnable {    private int tick = 100;    Object obj = new Object();    public void run() {        while (true) {            synchronized (obj) {   //注意这里                if (tick > 0) {                    try {                        Thread.sleep(100);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    System.out.println(Thread.currentThread().getName()                            + " sale: " + tick--);                }            }        }    }}class TicketDemo {    public static void main(String[] args) {        Ticket t = new Ticket();        Thread t1 = new Thread(t);        Thread t2 = new Thread(t);        Thread t3 = new Thread(t);        t1.start();        t2.start();        t3.start();    }}

可以看到,上面这个示例代码,用obj作为同步代码块的“锁”,使线程在同步中执行,保证了运行结果的正确。


2.“同步”的原理 - 加锁!


synchronized的原理,相当于加锁,

好像运行代码块里面的代码前,会先有道门(类似于标志位的机制,0是关,1是开),

当线程0运行 synchronized代码块里面的代码时,

先判断代码块里的代码有没有被上锁,如果没有那它自己就给标志位加锁,

然后就运行代码,这样就算它sleep了并且其他线程拿到了CPU执行权,

也只能等线程0运行完,释放锁后其他线程才能继续运行。

这样就避免了不同步导致的安全问题。

生活中的例子:火车里面每节车厢那唯一 一个厕所,要上厕所的人就是线程。


3.“同步”的前提

  • 必须要有两个或者两个以上的线程。
  • 必须是多个线程使用同一个锁。

必须保证同步中中只能有一个线程在运行。

这里注意第(2)点,加的“锁”可以是随便一个new Object(),

可以是 "随便".getClass() ,可以是 你这个文件名.class 都行,

只要你保证这么多个线程,它们用的锁,是同一个对象,就行了!


4.“同步”的利弊


好处:解决了多线程的安全问题。

弊端:多个线程都需要判断锁,较为耗费资源。

上锁后,明显感觉程序运行的时间慢了。越安全越耗资源,你加越多锁,越慢。


八   线程同步2 - 同步方法


1.银行金库问题

需求:银行有一个金库,有两个储户分别存300元,每次存100,存3次。
目的:该程序是否有安全问题,如果有,如何解决?

class Bank {private int sum;// private Object obj = new Object();public synchronized void add(int num)// 同步方法{// synchronized(obj)// {sum = sum + num;// -->try {Thread.sleep(10);} catch (InterruptedException e) {}System.out.println("sum=" + sum);// }}}class Cus implements Runnable {private Bank b = new Bank();public void run() {for (int x = 0; x < 3; x++) {b.add(100);}}}class BankDemo {public static void main(String[] args) {Cus c = new Cus();Thread t1 = new Thread(c);Thread t2 = new Thread(c);t1.start();t2.start();}}

2.解题思路

如何找到问题?

  • 明确哪些代码是多线程运行代码。   →    run方法和add方法
  • 明确共享数据。                   →     b和sum是共享数据
  • 明确多线程运行代码哪些语句是操作共享数据的。    →   sum=sum+n

所以,经过分析,sum=sum+n;那三句应该被同步。


3.同步方法


题:同步代码块是用来封装代码的,方法也是用来封装代码的,那它两有什么不一样呢?
答:同步代码块封装代码具备了同步性。

题:那如果让方法具备同步性呢?
答:可以把synchronized作为关键字修饰方法,让方法具备同步性。


因此引入了同步方法的概念。

public synchronized void add(int n) {  sum = sum + n;  System.out.println("sum= " + sum);}


九   线程同步3 - 同步方法的锁

1.同步方法的锁

同步方法用的锁是this!!!

验证:使用两个线程来卖票。一个在同步代码块,一个在同步方法,

      都在执行买票动作。如果没同步,则会出现错误的票。


线程t1一开启就运行同步代码块,线程t2一开启就运行同步函数。

class Ticket implements Runnable {private static int Ticket = 500;public static boolean flag = true;public void run() {if (flag) {while (true) {synchronized (this) {if (Ticket > 0) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+ " code--- " + Ticket--);}}}} else {while (true)show();}}public synchronized void show() {if (Ticket > 0) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " show--- "+ Ticket--);}}}public class Prove {public static void main(String[] args) throws InterruptedException {Ticket Ticket = new Ticket();Thread t1 = new Thread(Ticket, "买家1");Thread t2 = new Thread(Ticket, "买家2");t1.start();Thread.sleep(20);Ticket.flag = false;t2.start();}}
方法需要被对象调用,那么函数都有一个所属对象引用,就是this。

所以,同步方法使用的锁是 this!

2.静态同步方法的锁


如果同步函数被静态static修饰后,使用的锁是什么呢?

通过验证,将上面程序的同步函数加静态后(注意票数也要加static修饰了),

执行,发现不再是this。

因为静态方法也不可以定义this。


内存中有一个对象,字节码文件对象,Ticket.class 。

静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。

类名.class,该对象类型是Class类型。


静态的同步方法,使用的锁是该方法所在类的字节码文件对象。

类名.class,该对象在内存中是唯一的。

十   死锁

1.死锁的概念


死锁:你有一个锁,我有一个锁,你要锁定我的锁才能修改,我也要锁定你的锁才能修改。

通常死锁出现的情况,就是 同步中嵌套同步。

我们应该尽量避免死锁。

百度所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象:死锁。

2.死锁的示例代码

class MyTest extends Thread {    private boolean flag;    public MyTest(boolean flag) {        super();        this.flag = flag;    }    public void run() {        if (flag) {            synchronized (Lock.LockA) {                System.out.println("if locka");                synchronized (Lock.LockB) {                    System.out.println("if lockb");                }            }        } else {            synchronized (Lock.LockB) {                System.out.println("else lockb");                synchronized (Lock.LockA) {                    System.out.println("else locka");                }            }        }    }}class Lock {    public static Object LockA = new Object();    public static Object LockB = new Object();}public class DeadLockDemo {    public static void main(String[] args) {        new MyTest(true).start();        new MyTest(false).start();    }}
关键是要理解,“锁”作为线程在同步情况下所先要取得的资源,

只能在一个时刻被某一个线程锁定,一锁上就要等代码块执行完,

释放锁后,才能继续执行。


十一   单例设计模式


单例设计模式有两种方式,分别是饿汉式懒汉式

饿汉式:一上来就初始化了。

懒汉式:用到的时候才初始化。

1.饿汉式

//饿汉式class Single {    private final static Single s = new Single();    private Single() {    }    public static Single getInstance() {        return s;    }}

2.懒汉式

// 懒汉式class AnotherSingle {    private static AnotherSingle as = null;    private AnotherSingle() {    }    public static AnotherSingle getInstance() {        if (as == null) {            synchronized (AnotherSingle.class) {                if (as == null)                    as = new AnotherSingle();            }        }        return as;    }}public class Single {    public static void main(String[] args) {        AnotherSingle.getInstance();    }}
懒汉式多线程访问下的安全隐患:

A线程判断完类实例为null后挂起了,这时候B线程获取到CPU资源判断进来,也挂起了,

然后A线程醒了new实例后B线程又醒了new实例,就不符合单例设计模式的要求了。

怎么改?加同步,并且用双重判断提高效率。


3.两种方式的对比

饿汉式和懒汉式的区别就在于 延时加载。

懒汉式有什么弊端?懒汉式在多线程运行下,可能会出现线程安全隐患。

该如何解决?加同步,并双重否定提高效率。


------- android培训、java培训、期待与您交流! ----------

0 0
原创粉丝点击