黑马程序员——Java基础---多线程

——Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ——-

一.进程概述

进程就是正在执行的一个程序。可以说是静态的,当进程一旦执行,那么就会在内存中给其分配一定的内存空间,供其运行。
从系统上来理解:进程有单进程和多进程
单进程:就是只能运行一个程序,只有这个程序运行完了,才能运行其他的进程,例如:DOS系统,他的特点就是只要是一种病毒,就会死机,原因就是采用的是单进程
.多进程:就是可以同时运行多个程序,例如现在的Window系统,采用的就是多进程
Java虚拟机把其封装成了对象,供其使用。
进程和线程的之间的举例说明:打开qq程序,qq就是一个进程,那么线程就是其中的qq连天窗口。

二.线程的概述

1.概述:线程是动态的,是进程中的控制单元,一个进程中可以执行多个线程,线程控制着进程。

2.一个进程中至少存在一个线程在执行,
例如:java虚拟机启动后,不只是存在的一个线程,一个是主线程,一个是垃圾回收的线程。

3.线程的执行必须是在有进程的基础之上,当一个线程消失了,那么进程不一定会消失,但是进程消失了,那么线程肯定会消失。

4.主线程:在系统中就是进程一旦创建,就是创建一个线程,那么就是个主线程,那么再有其他的线程生成的话,那就是其子类,在java虚拟机中的主线程是在运行主函数的线程。

5.多线程:多线程会提高执行速度(这只是多线程其中的一个功能例如:下载)当是多线程的话,那么各个线程之间会有优先级。当线程抢到了CPU,谁就会执行。

三.线程的创建方法

1.继承Thread类。

(1)定义类继承Thread类。

(2)复写run()方法。

(3)启动线程,使用start()方法

2.实现接口Runnable接口。

(1)定义类(A)实现(implements)接口Runnable。

(2)重写run()方法。

(3)创建线程与类(A)创建联系,开启线程

总结:实现方式和继承方式的区别

1、实现方式避免了单继承的局限性,在定义线程时,建议使用实现方法。

2、继承方式:线程代码存放在Thread子类run方法中;

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

四.线程的创建—Thread类

1.代码举例:

public class MyThread extends Thread {  
   public void run(){  
      for(int i=0;i<10;i++)  
        System.out.println(Thread.currentThread().getName()+"--"+i);  
   }  
}  
结果:  
main--0  
Thread-0--0  
main--1  
Thread-0--1  
main--2  
Thread-0--2  
Thread-0--3  
main--3  
Thread-0--4  
main--4  
Thread-0--5  
main--5  
Thread-0--6  
main--6  
Thread-0--7  
main--7  
Thread-0--8  
main--8  
Thread-0--9  
main--9  

从结果上可以看出,每次的运行结果都不一样,原因就是自定义的那个线程和主线程抢夺CPU,那一个先抢到CPU,那个就先执行。

2.start()和run()的区别和联系

联系:线程调用start()方法实际上是调用的run()方法,run()方法中存储的线程运行的代码

区别:调用start()方法是启动线程,直接调用run()方法的话,那么就和调用普通的方法是一样的,没有启动线程。

public class MyThread extends Thread {  
   public void run(){  
     for(int i=0;i<10;i++)  
       System.out.println(Thread.currentThread().getName()+"--"+i);  
   }  
}  
public class Text {  
  public static void main(String[] agrs) {  
    MyThread tt = new MyThread();  
    tt.run();  
    for (int i = 0; i < 10; i++)  
      System.out.println(Thread.currentThread().getName() + "--" + i);  

  }  
}  

3.复写run()

run() 中存在的是线程运行的代码,对于主线程中运行的代码存储在main()方法中。

4.疑问解答

在主线程中,运行时候,也是按照顺序的。直到新的线程开启了,那么才会和存在线程抢夺CPU。

public class MyThread extends Thread {  
   public void run(){  
     for(int i=0;i<10;i++)  
       System.out.println(Thread.currentThread().getName()+"--"+i);  
   }  
}  
public class Text {  
  /* 
   * 主线程开始从上向下执行,直到执行到了tt线程,启动了tt线程, 
   * 但是此时主线程已经运行结束了,所以运行结果都是一样的。 
   * */  
  public static void main(String[] agrs) {  
    for (int i = 0; i < 10; i++)  
      System.out.println(Thread.currentThread().getName() + "--" + i);  
    MyThread tt = new MyThread();  
    tt.start();  
  }  
}  
public class Text1{  
  /* 
   * 主线程开始从上向下执行,开始就创建了线程,并且启动了tt线程, 
   * 然后tt线程会和主线程抢夺CPU,谁抢到就会执行谁,所以此时运行结果是不确定的 
   * */  
  public static void main(String[] agrs) {  
    MyThread tt = new MyThread();  
    tt.start();  
    for (int i = 0; i < 10; i++)  
      System.out.println(Thread.currentThread().getName() + "--" + i);  

  }  
}  

五.线程状态

线程状态如下图所示:

六.线程名称

1.默认的线程

默认线程的名称是Thread-0,Thread-2…….。主线程的名称是:main

public class Text {  
  public static void main(String[] agrs) {  
    MyThread tt = new MyThread();  
    System.out.println("默认的线程名称:"+tt.getName());  
    //tt.start();  
  System.out.println( "主线程的默认的名字:"+Thread.currentThread().getName());  

  }  
}  

2.自定义线程

自定义线程名称和获得线程名称名

设置线程名称:可以用setName()方法,可以用构造方法设置线程名称

获得线程名称:用getName()方法获得线程名称

Thread.currentThread()获得当前运行的线程对象

public class MyThread extends Thread {  
  public MyThread (String name){//通过构造方法设置线程名称  
    super(name);  
  }  
   public void run(){  

   }  
}  
public class Text {  
  public static void main(String[] agrs) {  
    MyThread tt = new MyThread("线程A");  
    System.out.println("线程名称:" + tt.getName());  
    // tt.start();  
    /* 
     * 也可以通过setName()来设置线程名称 
     */  
    tt.setName("setName");  
    System.out.println("线程名称:" + tt.getName());  
  }  
}  
结果:  
线程名称:线程A  
线程名称:setName  

七.售票小例子

public class Tick extends Thread {  
  private int tick = 10;  

  public void run() {  
    while (tick > 0) {  
      System.out.println(Thread.currentThread().getName() + "卖了第"  
           + (tick--) + "张票");  
    }  
  }  
}  
public class Text {  
  public static void main(String[] agrs) {  
    new Tick().start();  
    new Tick().start();//这样相当于开了3个线程,就是3个窗口  
    new Tick().start();//但是这样相当于各个窗口买各自的票,而不是共享票  

  }  
}  

这样的结果相当于买了40张票,但是本来就10张票,重复卖了
结果:
Thread-0卖了第10张票
Thread-2卖了第10张票
Thread-1卖了第10张票
Thread-1卖了第9张票
Thread-2卖了第9张票
Thread-0卖了第9张票

解决办法:

1. 把票(tick)定义成静态的,但是如果定义静态的话,但是静态变量声明周期长,不建议这样使用

public class Tick extends Thread {  
  private static int tick = 10;  

  public void run() {  
    while (tick > 0) {  
      System.out.println(Thread.currentThread().getName() + "卖了第"  
           + (tick--) + "张票");  
    }  
  }  
}  

2.可以把值开启一个线程,结果没有错,但是和日常生活多个窗口共同卖票不符合

public class Text {  
  public static void main(String[] agrs) {  
    new Tick().start();  
  }  
}  

3.使用实现Runnable接口线程处理此情况

步骤

(1).创建类(A)实现接口Runnable,复写run()方法
(2).创建Thread对象,然后把类A的对象作为参数传给Thread的构造函数,这是把其关联起来。
(3).通过创建的线程对象,然后开启线程。
解答:复写run()方法,里面存的是线程要运行的代码
因为Runnble接口中只有一个run()方法,所以没有办法启动线程,所以还得使用Thread类来启动线程,所以要把Runnable的子类的对象作为参数传给Thread类构造函数。

售票例子(Runnable接口)
 public class Tick implements Runnable {
  private int tick = 10;

  public void run(){
    while (tick > 0) {
      System.out.println(Thread.currentThread().getName() + "卖了第"
           + (tick--) + "张票");
    }
  }
}

结果:
Thread-0卖了第10张票
Thread-1卖了第10张票
Thread-0卖了第9张票
Thread-1卖了第8张票
Thread-0卖了第7张票
Thread-1卖了第6张票
Thread-0卖了第5张票
Thread-0卖了第4张票
Thread-0卖了第3张票
Thread-0卖了第2张票
Thread-0卖了第1张票
Thread-1卖了第0张票

从结果上可以看出,这是我们想要的那种结果,多个窗口共同买共有的票。

总结:Runnable接口和Thread类的区别与联系

(1).Runnable接口可以避免单继承的限制,要是继承Thread类的话,那么就不能继承其他的类了,因为只能单继承,如果实现了接口Runnale后,还可以继承其他的类,或是是实现其他的接口
(2).Runnable实现了资源的共享,例如(售票程序的票)
(3).Runnable实增强了程序的健壮性,代码能够被多个程序共享,实现了数据 与代码是独立的。
(4).实现Runnable接口的线程的运行代码存在实现Runnable接口子类的run()方法中,继承Thread类的线程的代码存在Thread子类的run()方法中。
所以在以后的开发中,使用Runnble接口比较好,更多。

八.线程的安全问题

1.原因

当多条语句执行多个线程共享的资源,执行到一部分后,执行权被抢夺了,导致共享资源的不正正常修改,所以就产生了线程的安全问题。
例如:售票小例子

public class Ticket implements Runnable {
  private int tick = 10;

  public void run() {
    while (true) {
      try {
         if (tick > 0) {
           Thread.sleep(10);
           System.out.println(Thread.currentThread().getName()+ "卖了第"
               + (tick) + "张票");
           tick--;
         }
      } catch (InterruptedException e) {
         e.printStackTrace();
      }

    }
  }
}
public class Text {
  public static void main(String[] agrs) {
    Ticket tt=new Ticket();
    new Thread(tt).start();
    new Thread(tt).start();
  }
}
结果:
Thread-1卖了第10张票
Thread-0卖了第10张票
Thread-1卖了第8张票
Thread-0卖了第8张票
Thread-0卖了第6张票
Thread-1卖了第5张票
Thread-1卖了第4张票
Thread-0卖了第3张票
Thread-0卖了第2张票
Thread-1卖了第2张票
Thread-0卖了第0张票

结果出现了:产生了重复的票,并且出现了0票,那么这是不正常的现象的。

2.同步代码块—解决办法

(1)利用的是关键子(synchronized)来处理线程同步问题,保证线程安全,锁就好比门上的锁一下,执行的顺序是:首先是判断锁是否是开着的,若是开着的,那么就可以进去(执行同步代码)然后把锁锁上,执行完后,把锁释放(把锁开开),判断锁是锁着的,那么就在外面等着,直到里面的人把锁开开,出来。(这样保证的是同步代码在某一段时间只有一个线程在执行。)
(2).格式
synchronized(对象){执行共享资源的代码}
(3).同步规则
必须是多个线程执行(至少两个)才可以产生同步,必须是多个线程使用的是同一个锁,
优点:解决了多线程的安全问题
弊端:多线程需要判断,那么就会消耗时间,消耗资源

public class Ticket implements Runnable {
  private int tick = 10;

  public void run() {
    while (true) {
      synchronized (this) {
         try {
           if (tick > 0) {
             Thread.sleep(10);
             System.out.println(Thread.currentThread().getName()
                 + "卖了第" + (tick) + "张票");
             tick--;
           }
         } catch (InterruptedException e) {
           e.printStackTrace();
         }
      }

    }
  }
}
结果:
Thread-0卖了第10张票
Thread-0卖了第9张票
Thread-0卖了第8张票
Thread-1卖了第7张票
Thread-1卖了第6张票
Thread-1卖了第5张票
Thread-1卖了第4张票
Thread-1卖了第3张票
Thread-1卖了第2张票
Thread-1卖了第1张票

这样结果就和我们正常想要的是一样的,没有异常现象

3.同步方法—解决办法

(1)概述

因为方法和代码块都是用来封装代码的,那么代码块可以使用锁来解决同步问题,那么函数也可以使用锁,使方法也可以操作同步问题。

(2)格式

public synchronized 方法返回类型 方法名(参数){方法体}

(3)使用

怎眼确定锁的位置,寿面判断那些是线程运行的代码,那些是共享资源,那些同步代码是执行的是同步资源,那么就把那些操作同步资源的语句使用锁来进行括起来。

例如:客户向银行存钱,每次存三次,每次存100,分两个地方存户存
思路:共享资源:总价钱,银行,有两个线程(模拟两个地方存)

**
 * 银行类
 */
public class Bank {
  private int sum;// 表示的是当前账户的总额

  public synchronized void add(int mon) {// 存方法,加锁的话,那么里面的语句在一段时间内必须是一个线程在执行
    sum += mon;
    try {
      Thread.sleep(10);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("sum=" + sum);
  }
}
public class Cus implements Runnable {
    private Bank bank=new Bank();
    public void run(){//线程执行的代码(线程)
     for(int i=0;i<3;i++){
       bank.add(100);
    }
    }
}
public class Text {
  public static void main(String[] agrs) {
    Cus tt=new Cus();//客户
    new Thread(tt).start();//开始存
    new Thread(tt).start();//开始存

  }
}
结果:
sum=100
sum=200
sum=300
sum=400
sum=500
sum=600

(4)同步代码块和同步函数的选择

假如连续的代码是同步代码,并且都是执行的都是共享资源,那么就可以把其抽取成函数,并且定义为同步函数

假如一个函数中是全是同步代码,并且执行的全是共享资源,那么就把其定义为同步函数

(5)扩展

回顾:一共分为四种代码块
普通代码块:就是定义在方法中的代码块。
构造块:定义在类中的代码块,优先于构造方法,重复调用。
静态块:使用static关键字声明的,只执行一次,优先于构造块。
同步代码块:使用synchronized关键字声明的代码块,称为同步代码块。格式:synchronized(同步对象){}

4.锁对象的确定

(1)同步函数的锁是this

验证思路:还是使用的卖票程序,使用两个线程,他们执行的代码不同,一个是同步代码块(锁的对象是this synchronized(this){}),另一个线程执行的同步函数,如果结果没有异常,那么他们就完全符合线程同步的规则,
多个线程执行的是同步代码,并且他们的是锁是同一个锁,因为同步代码块的锁对象是this,那么此时同步函数锁的对象也是this

 /*售票机*/
public class Ticket implements Runnable {
  private int tick = 100;
  public boolean flag = true;

  public void run() {
    if (flag) {
      while (true) {
         synchronized (this) {
           try {
             if (tick > 0) {
               Thread.sleep(10);
               System.out.println(Thread.currentThread().getName()
                   + "卖了第" + (tick--) + "张票");
             }
           } catch (InterruptedException e) {
             e.printStackTrace();
           }
         }
      }

    } else
      while (true)
         show();
  }

  public synchronized void show() {// 同步函数
    try {
      if (tick > 0) {
         Thread.sleep(10);
        System.out.println(Thread.currentThread().getName()+ "卖了第"
             + (tick) + "张票");
         tick--;
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}
public class Text {
  public static void main(String[] agrs) {
    Ticket tt=new Ticket();
    new Thread(tt).start();
    /*这里是主线程睡眠一下,让线程1先运行,防止直接改变标志位*/
    try {
      Thread.sleep(50);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    tt.flag=false;//更改标志位
    new Thread(tt).start();//开始存

  }
}

部分结果:
   Thread-0卖了第100张票
Thread-0卖了第99张票
Thread-0卖了第98张票
Thread-0卖了第97张票
Thread-0卖了第96张票
Thread-0卖了第95张票
Thread-1卖了第94张票
Thread-1卖了第93张票
Thread-1卖了第92张票

从结果上额可以看出,结果是正常的,所以结论是正确的,同步函数的锁对象是this

(2)静态同步函数锁对象

静态同步函数的锁是:该方法所在类的字节码文件对象:类名.class
验证方法:和上面的一样,把同步代码块的锁对象改为:Ticket.class

/*售票机*/
public class Ticket implements Runnable {
  private static int tick = 100;
  public boolean flag = true;

  public void run() {
    if (flag) {
      while (true) {
         synchronized (Ticket.class) {
           try {
             if (tick > 0) {
               Thread.sleep(10);
               System.out.println(Thread.currentThread().getName()
                   + "卖了第" + (tick--) + "张票");
             }
           } catch (InterruptedException e) {
             e.printStackTrace();
           }
         }
      }

    } else
      while (true)
         show();
  }

  public static synchronized void show() {// 同步函数
    try {
      if (tick > 0) {
         Thread.sleep(10);
        System.out.println(Thread.currentThread().getName()+ "卖了第"
             + (tick) + "张票");
         tick--;
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

结果:和上面的一样,正常。
我们可以这样思考,静态方法是随着类的加载而加载,此时没有类的对象,但是还是要找个对象,那么就是类的字节码文件的对象。

九.死锁

1.死锁产生的原因

死锁产生的原因是:一个所中还有另外一个锁,但是这两个锁对象是不相同的,其中一个锁(A)需要另外一个锁(B),而锁(B)也需要锁(A),这样有时候她们都不会妥协,那么就会产生死锁。

2.举例死锁

class MyLock {
  static Object lockA = new Object();
  static Object lockB = new Object();
}

public class DeadLock implements Runnable {
  public boolean flag = true;

  public DeadLock(boolean f) {
    this.flag = f;
  }

  public void run() {
    if (flag) {
      // while (true) {
      synchronized (MyLock.lockA) {
         System.out.println("ifLockA");
         synchronized (MyLock.lockB) {
           System.out.println("ifLockB");
         }
         // }
      }
    } else {
      // while (true) {
      synchronized (MyLock.lockB) {
         System.out.println("elseLockB");
         synchronized (MyLock.lockA) {
           System.out.println("elseLockA");
         }
         // }
      }
    }
  }
}
public class Text {
  public static void main(String[] agrs) {
    DeadLock l1 = new DeadLock(true);// 第一个线程
    DeadLock l2 = new DeadLock(false);// 第二个线程
    new Thread(l1).start();// 开启线程
    new Thread(l2).start();// 开启线程
  }
}

结果:可能会死锁
  例如:
if LockA
else LockB

这就产生了死锁

(1). 资源共享的时候需要进行同步操作
(2). 程序中过多了同步操作会产生死锁
死锁就是程序中互相的等待。

明白了死锁的原理以后在编写程序中就要尽量避免死锁。

十、停止线程及线程的其它方法

如何停止线程?

1、 定义循环结束标记,只有让run方法结束,开启多线程运行,运行代码通常是循环结构。只要控制住循环就可以让run方法结束,也就是线程结束。

特殊情况:

当线程处于冻结状态,就不会读到标记,线程也就不会结束。当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结进行清除。强制让线程恢复到运行状态中来,这样就可以操作标记让线程结束。Thread类中提供来interrupt方法来强制清除冻结状态。

2、 使用interrupt(中断)方法该方法是结束线程的冻结状态,是线程回到运行状态中来。

线程的其它方法

1、守护线程

setDaemon(): 当剩下的线程都为守护线程是时,java虚拟机退出;代码运行结束。

该方法在线程启动前调用。

2、Join方法

当A线程执行到B线程的.join()方法时,A就等待,等B线程都执行完,A才会执行。Join可以临时加入线程执行。Join方法会抛出异常。

结语:单单掌握这些线程的基本知识是不够的,实际用到的线程知识却很灵活,本以为自己掌握的特别好,今天编写关于线程的程序时却感觉力不从心,无从下手,所以一定要多理解,勤动脑思考。

——Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ——-

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值