黑马程序员——java基础之多线程

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

java基础之多线程

一. 多线程的概念

1. 进程、线程和多线程:

  • 进程:正在进行中的程序(直译)。进程是程序在某个数据集上的运行,它有自己的生命周期,因调度而运行。因等待资源或事件而被处于等待状态,因完成任务而被撤销。
  • 线程:进程中一个负责程序执行的控制单元(执行路径)。是一个程序中的不同执行路径。

    1. 一个进程中可以有多个执行路径,称之为多线程。
    2. 一个进程中至少要有一个线程。
    3. 开启多个线程是为了同时运行多部分代码,每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务。
    4. 多线程解决了多部分代码同时运行的问题。

知识扩展:其实更细节说明虚拟机,jvm不只一个线程 还有负责垃圾回收机制的线程

—————————-割——————————-割——————————–

2. 创建线程的两种方法

方法1: 继承Thread类
通过对api的查找 java已经提供了堆线程这类事物的描述 就是Thread类

  1. 定义类 继承Thread
  2. 复写Thread类中的run方法
    目的是将自定义的代码存储在run方法中,让线程运
  3. 调用线程的start方法
    两个作用:启动线程 调用run方法
    如下面的示例代码:
//定义类 继承Thread
class Demo extends Thread
{
    //复写run方法
    public void run()
    {
        for(int i = 0;i<60;i++)
        System.out.println("Demo run--"+i);
    }   
}
class ThreadDemo1
{
    public static void main(String[] args)
    {
        Demo d = new Demo();//创建好一个线程
        d.start();//启动线程并执行该线程的run方法

        for(int x = 0;x<60;x++)
            System.out.println("Hello run--"+x);
    }
}

这里写图片描述

从上面的实例代码运行结果可以得到几点:

  • 运行结果每一次都不同
  • 这是因为多个线程都获取cpu的执行权,cpu执行到谁,谁就运行
  • 明确一点,在某个时刻,只能有一个程序在运行
  • cpu在作者快速的切换,以达到看上去是同时运行的效果

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

小知识点:

  1. 为什么要覆盖run方法呢?
    Thread 类用于描述线程,该类定义了一个功能用于存储线程要运行的代码,该存储功能就是run方法
    也就是说Thread类中的run方法,用于存储要运行的代码
  2. 不能直接调用run()方法,因为这样不能开启你线程.仅仅是普通的调用.

—————————-割——————————-割——————————–

方法2: 实现runnable接口
使用继承方式有一个弊端,那就是如果该类本来就继承了其他父类,那么就无法通过Thread类来创建线程了。这样就有了第二种创建线程的方式:实现Runnable接口,并复写其中run方法的方式。

  1. 定义类实现Runnable接口.
  2. 覆盖接口中的run方法,将线程的任务代码封装到run方法中
  3. 通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。
  4. 调用线程对象的start方法开启线程。

为什么要将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递?

  • 因为线程的任务都封装在Runnable接口子类对象的run方法中。 所以要在线程对象创建时就必须明确要运行的任务

示例代码:

 //定义类,实现Runnable接口
 class Demo implements Runnable
 {
     public void run()//覆盖run方法
     {
         show();
     }
     public void show()
     {
         for(int x = 0; x < 20; x++)
         {
            System.out.println(Thread.currentThread().getName() + "..." + x);
         }
     }
 }
 class ThreadDemo2
 {
     public static void main(String[] args)
     {
         Demo d = new Demo();//创建子类对象
         //将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递
         Thread t1 = new Thread(d);
         Thread t2 = new Thread(d);
         t1.start();//启动线程并调用其中的run方法
         t2.start();
    }
 }

运行结果如下:

这里写图片描述

—————————-割——————————-割——————————–
实现Runnable接口的好处:

  1. 将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面向对象的思想将任务封装成对象。
  2. 避免了Java单继承的局限性。 所以,创建线程的第二种方式较为常用。

两种方式的区别

继承方式: 线程代码存放在Thread子类run方法中
实现方式: 线程代码存放在接口的子类的run方法中

—————————-割——————————-割——————————–

3. 线程的生命周期
线程的几种状态:

  1. 被创建:等待启动,调用start启动。
  2. 运行状态:具有执行资格和执行权。
  3. 临时状态(阻塞):有执行资格,但是没有执行权。
  4. 冻结状态:遇到sleep(time)方法和wait()方法时,失去执行资格和执行权,sleep方法时间到或者调用notify()方法时,获得执行格变为临时状态。
  5. 消忙状态:stop()方法,或者run方法结束。

如下图:这里写图片描述

—————————-割——————————-割——————————–

4. 线程的安全

/*
    需求:卖票程序
    多个卖票窗口
*/
class Ticket implements Runnable
{
    private int tick = 100;
    public void run()
    {
        while(true)
        {
            if(tick>0)
            {
                //显示余票
                System.out.println(Thread.currentThread().getName()+"---"+tick--);
            }
        }
    }
}

class TicketDemo
{
    public static void main(String[] args)
    {
        //创建Runnable子类的实例对象
        Ticket t = new Ticket();
        //创建四个线程,模拟四个卖票窗口
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        Thread t4 = new Thread(t);
        //启动线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();

    }
}

这里写图片描述

线程安全问题产生的原因:
1. 多个线程在操作共享的数据。
2. 操作共享数据的线程代码有多条。
3. 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。

线程安全问题的解决方案—-同步
线程的同步: 就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。 必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。

在java中,用同步代码块就可以解决这个问题。
同步代码块的格式:

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

总结:
- 同步的好处:解决了线程的安全问题。
- 同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
- 同步的前提:必须有多个线程并使用同一个锁。

示例代码—-同步后的卖票程序

/*
    需求:卖票程序
    多个卖票窗口
*/
class Ticket implements Runnable
{
    private int tick = 100;
    Object obj = new Object();

    public void run()
    {
        while(true)
        {
            synchronized (obj)
            {
                if(tick>0)
                {
                    //显示余票
                    System.out.println(Thread.currentThread().getName()+"---"+tick--);
                }
            }
        }
    }
}

class TicketDemo
{
    public static void main(String[] args)
    {
        //创建Runnable子类的实例对象
        Ticket t = new Ticket();
        //创建四个线程,模拟四个卖票窗口
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        Thread t4 = new Thread(t);
        //启动线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();

    }
}

运行结果:
这里写图片描述

结果分析:
上图显示安全问题已被解决,原因在于Object对象相当于是一把锁,只有抢到锁的线程,才能进入同步代码块向下执行。

总结:

  • 要明确哪些代码是多线程运行代码。
  • 要明确共享数据
  • 要明确多线程运行代码中哪些语句是操作共享数据的。

—————————-割——————————-割——————————–

5. 线程的安全的单例设计模式–懒汉式

饿汉式不存在安全问题,因为不存在多个线程共同操作数据的情况。
懒汉式存在安全问题,可以使用同步函数解决,也可以使用代码块的方式来解决

示例代码

/*
    单例设计模式--懒汉式
    实例的延迟加载 
*/
class Single
{
    private static Single s = null;
     private Single(){}
     public static Single getInstance()
     {
         if(s ==null)
         {
             synchronized(Single.class)
             {
                 if(s == null)
                     s = new Single();
             }
         }
         return s ;
     }
 }

同步函数和同步代码块的区别:

  1. 同步函数的锁是固定的this。
  2. 同步代码块的锁是任意的对象。
    建议使用同步代码块。

—————————-割——————————-割——————————–

6. 线程的安全—-死锁

死锁常见情景之一:同步的嵌套。


    class Ticket implements Runnable {
    private static int num = 100;
    Object obj = new Object();
    boolean flag = true;

    public void run() {
        if (flag) {
            while (true) {
                synchronized (obj) {
                    show();
                }
            }
        } else
            while (true)
                show();
    }

    public synchronized void show() {
        synchronized (obj) {
            if (num > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                        + "...function..." + num--);
            }
        }
    }
}

    class DeadLockDemo {
    public static void main(String[] args) {
        Ticket t = new Ticket();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        t1.start();
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.flag = false;
        t2.start();
    }
}

这里写图片描述
由上图可以看到程序已经被锁死,无法向下执行。
由下面代码可以看到,run方法中的同步代码块需要获取obj对象锁,才能执行代码块中的show方法。而执行show方法则必须获取this对象锁,然后才能执行其中的同步代码块。

当线程t1获取到obj对象锁执行同步代码块,线程t2获取到this对象锁执行show方法。 同步代码块中的show方法因无法获取到this对象锁无法执行,show方法中的同步代码块因无法获取到obj对象锁无法执行,就会产生死锁。

另一个示例代码: 便于我们理解

//让一个类实现Runnable接口
class Test implements Runnable {
    private boolean flag;

    Test(boolean flag) {
        this.flag = flag;
    }
    //复写run方法
    public void run() {
        if (flag) {
            while (true)
                synchronized (MyLock.locka) {
                    System.out.println(Thread.currentThread().getName()
                            + "...if locka...");
                    synchronized (MyLock.lockb) {
                        System.out.println(Thread.currentThread().getName()
                                + "...if lockb...");
                    }
                }
        } else {
            while (true)
                synchronized (MyLock.lockb) {
                    System.out.println(Thread.currentThread().getName()
                            + "...else lockb...");
                    synchronized (MyLock.locka) {
                        System.out.println(Thread.currentThread().getName()
                                + "...else locka...");
                    }
                }
        }
    }
}
//定义两个锁
class MyLock {
    public static final Object locka = new Object();
    public static final Object lockb = new Object();
}

class DeadLockDemo_1 {
    public static void main(String[] args) {
        Test a = new Test(true);//创建Runnable子类的实例对象
        Test b = new Test(false);

        Thread t1 = new Thread(a);//创建线程
        Thread t2 = new Thread(b);

        t1.start();//启动线程
        t2.start();
    }
}

运行结果:
这里写图片描述
程序卡住了,每次的运行结果都不一样.

总结: 我们学习死锁是为了在写程序的时候避免这样的情况

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值