黑马程序员-多线程技术

来源:互联网 发布:什么软件赚钱最快 编辑:程序博客网 时间:2024/05/04 11:34
------- android培训、java培训、期待与您交流! ----------

线程简介

   进程:是一个正在执行中的程序

          每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元

   线程:就是进程中一个独立的控制单元

          线程在控制着进程的执行。

   一个进程中至少要有一个线程

  开启多个线程是为了同时运行多部分代码

  每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务

  多线程的好处:解决了多部分同时运行的问题

  多线程的弊端:线程太多回到的效率降低

  其实应用程序的执行都是cpu在做着快速的切换完成的 ,这个切换是随机的

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

这就是多线程的随机性。

 jvm启动时就启动了多个线程,至少有两个线程可以分析出来

 1,执行main函数的线程

    该线程的任务代码都定义在main函数中

 2,负责垃圾回收的线程

 

如何创建一个线程

   方式一 继承thread类

     步骤:

      1,定义类继承Thread.

      2,复写Thread类中的run方法。

          目的:将自定义代码存储在run方法。让线程运行。

      3,调用线程的start方法。该方法有两个作用。

                 启动线程,调用run方法。

  为什么要覆盖run方法呢?

      Thread类用于描述线程。

          该类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法。

         run方法是用于存储线程要运行的代码。

public class Demo { public static void main(String[] args) {  /*   * 创建线程的目的就是为了开启一条执行路径,去运行的代码和其他代码实现同时运行   *    * 而运行的代码就是这个执行路径的任务   *    * jvm创建的主线程的任务都定义在了主函数中。   *    * 而自定义的线程它的任务在哪呢? Thread类用于描述线程,线程是需要任务的,所以Thread类也对任务的描述   * 这个任务就是通过Thread类的run方法来体现的, 也就是说,run方法封装自定义线程运行任务的函数   *    * run方法中定义就是线程要运行的任务代码   *    * 开启线程就是运行指定代码,所以只有继承Thread类,并复写run方法。 将运行的代码定义在run方法中即可   */  Demo4 d4 = new Demo4("周珂珂");  Demo4 d5 = new Demo4("张三");  d4.start();// 开启线程调用run方法  d5.start();  System.out.println("结束了这个线程。。" + Thread.currentThread().getName()); }}/** * 继承方式 * @author Administrator * */class Demo4 extends Thread { private String name;

 Demo4(String name) {  super(name);//给定义的线程命名 }    //重写run方法 public void run() {  //循环测试  for (int x = 0; x < 10; x++) {   System.out.println(name + ".." + x + "..."     + Thread.currentThread().getName());  } }}

线程运行状态

  新建状态(New):新创建了一个线程对象。 

  就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。 

  没有执行资格的情况下是冻结状态。sleep(), 时间到  wait(),notify()
  有执行资格的状态叫做临时状态。

   既有资格又有执行权运行状态。

   死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程对象以及名称

   原来线程都有自己默认的名称。

   Thread-编号  该编号从0开始。

   Thread 对象的setName() getName();方法

   线程初始化名称:构造方法 super(name);

     Thread.currentThread();返回对当前正在执行的线程对象的引用。

 创建线程方法之二  实现Runnable接口

     

/* * 创建线程的第二种方式:实现Runnable接口 * 1,定义类实现Runnable接口 * 2,覆盖接口中的run方法,将线程的任务代码封装到run方法中 * 3,通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递 *  为什么呢?因为线程的任务都封装在Runnable接口子类对象的run方法中。 *  所以要线程对象的start方法开启线程 *  4,调用线程对象的start方法开启线程 *  *  实现Runnable接口的好处: *  1,将线程的任务从线程的子类中分离出来,进行单独的封装 *      按照面向对象的思想将任务封装成对象 *  2,避免了java单继承的局限性 *    所以创建线程的第二种方式较为常用 */public class Test9 {public static void main(String[] args) {TextThread t = new TextThread();Thread t1 = new Thread(t);//线程1Thread t2 = new Thread(t);//线程2t1.start();t2.start();}}/** * 继承方式,实现Runnable接口 * @author Administrator * */class TextThread implements Runnable { //重写run方法@Overridepublic void run() {// TODO Auto-generated method stubshow();}    //测试方法public void show() {for (int i = 0; i < 20; i++) {System.out.println(Thread.currentThread().getName() + "..." + i);}}}

 

实现方式好处:避免了单继承的局限性。

    在定义线程时,建议使用实现方式。

两种方式的区别:

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

实现Runnnable,线程代码存在接口子类的run方法。

线程的安全问题 

   通过分析,发现,打印出0,-1,-2等错票

public class Test9 { public static void main(String[] args) {  Ticket1 t = new Ticket1();  //开启四个线程  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(); }}

class Ticket1 implements Runnable { private int ticket = 100;//共享数据

 public void run() {

  while (true) {   if (ticket > 0) {    try {     Thread.sleep(10);    } catch (InterruptedException e) {     // TODO Auto-generated catch block     e.printStackTrace();    }    System.out.println(Thread.currentThread().getName() + "sale"      + ticket--);//有负数的存在,安全隐患   }  } }}

以上多线程的运行出现了安全问题。

    问题原因:

         当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。

          导致共享数据的错我。

   解决办法:

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

   java对于多线程的安全问题提供了专业的解决方式。

    就是同步代码块。 哪些代码需要同步,就看哪些语句在操作共享数据。

    synchronized(对象){

           需要被同步的代码

    }

如下面的解决方案。
 

public class Test9 {public static void main(String[] args) {//开启4个线程Ticket1 t = new Ticket1();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();}}class Ticket1 implements Runnable {private int ticket = 1000;// Object obj=new Object();public void run() {while (true) {//同步代码块,加锁     对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。、synchronized (this) {if (ticket > 0) {try {Thread.sleep(10);//让线程休眠} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}System.out.println(Thread.currentThread().getName()+ "sale" + ticket--);//没有出现负数票}}}}}

 对象如同锁,持有锁的线程可以在同步中执行。

没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。

火车上的卫生间

同步的前提:

1,必须要有两个或者两个以上的线程。

2,必须是多个线程使用同一个锁。
必须保证同步中只能有一个线程在运行。

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

弊端:多个线程需要判断锁,较为消耗资源。允许消耗范围内的。

同步有两种表现形式,第一个是同步代码块,第二是同步函数。把synchronized作为修饰符放在函数上。

public class Test9 { public static void main(String[] args) {  //开启4个线程  Ticket1 t = new Ticket1();  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(); }}

class Ticket1 implements Runnable { private int ticket = 1000;

 // Object obj=new Object(); public void run() {

  while (true) {  //同步代码块,加锁     对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。、   synchronized (this) {    if (ticket > 0) {     try {      Thread.sleep(10);//让线程休眠     } catch (InterruptedException e) {      // TODO Auto-generated catch block      e.printStackTrace();     }     System.out.println(Thread.currentThread().getName()       + "sale" + ticket--);//没有出现负数票    }   }  } }}

public class Test10 {       public static void main(String[] args) {     Tickets t=new Tickets();     //创建4个线程卖票,并开启        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();}}class Tickets implements Runnable{     private int tick=1000;//票数     //复写run方法调用show@Overridepublic void run() {// TODO Auto-generated method stubwhile(true){this.show();}}//同步函数所持有的锁是thispublic synchronized void show(){if(tick>0){try {Thread.sleep(10);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}

同步函数用的是哪一个锁呢?
函数需要被对象调用,那么函数都有一个所属的对象,就是this.

同步函数的使用的锁是this  

验证:使用两个线程来买票。

一个线程在同步代码块中。

一个线程在同步函数中。

都在执行买票动作。

public class Test10 {public static void main(String[] args) {Tickets t = new Tickets();// 创建4个线程卖票,并开启Thread t1 = new Thread(t);Thread t2 = new Thread(t);/* * Thread t3=new Thread(t); Thread t4=new Thread(t); */t1.start();// t1一开启跑到同步代码块中。,开启这个线程不一定立即执行。处于临时状态,有可能执行下面一句t.flag = false;// 在t2开启之前,把标识变为false;try {Thread.sleep(10);// 主线程停止10毫秒,只能是t1在运行。过了时间段,可能执行下面的语句} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}t2.start();// t2一开启跑到同步函数中。/* * t3.start(); t4.start(); */}}class Tickets implements Runnable {private int tick = 1000;// 票数// 复写run方法调用showObject obj = new Object();boolean flag = true;@Overridepublic void run() {// TODO Auto-generated method stubif (flag) {while (true) {// 同步代码块// synchronized(obj),存在安全问题synchronized (this) {if (tick > 0) {try {Thread.sleep(10);System.out.println("同步代码块");} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}} else {while (true) {show();}}}// 同步函数public synchronized void show() {if (tick > 0) {try {Thread.sleep(10);System.out.println("同步函数");} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}


 

同步代码块使用的是任意的对象

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

不再是this了。因为静态方法也不可以定义this.

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

类名.class 该对象的类型是Class.

静态同步函数的锁:

   静态的同步函数使用的锁是该函数所属字节码文件对象

  可以使用getClass方法获取,也可以用当前类名.class表示

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

   验证方法如下:

public class Test10 {public static void main(String[] args) {Tickets t = new Tickets();// 创建4个线程卖票,并开启Thread t1 = new Thread(t);Thread t2 = new Thread(t);/* * Thread t3=new Thread(t); Thread t4=new Thread(t); */t1.start();// t1一开启跑到同步代码块中。,开启这个线程不一定立即执行。处于临时状态,有可能执行下面一句t.flag = false;// 在t2开启之前,把标识变为false;try {Thread.sleep(10);// 主线程停止10毫秒,只能是t1在运行。过了时间段,可能执行下面的语句} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}t2.start();// t2一开启跑到同步函数中。/* * t3.start(); t4.start(); */}}class Tickets implements Runnable {private static int tick = 1000;// 票数// 复写run方法调用showObject obj = new Object();boolean flag = true;@Overridepublic void run() {// TODO Auto-generated method stubif (flag) {while (true) {// 同步代码块// synchronized(obj),存在安全问题synchronized (Test10.class) {if (tick > 0) {try {Thread.sleep(10);System.out.println("同步代码块");} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}} else {while (true) {show();}}}// 同步函数public synchronized void show() {if (tick > 0) {try {Thread.sleep(10);System.out.println("同步函数");} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}

 

   懒汉式单例模式  

     加入同步为了解决线程安全问题

    加入双重判断是为了解决效率问题

 

public class Test10 {public static void main(String[] args) {System.out.println("hello");}}class SingleDemo {private static SingleDemo s = null;// 共享数据,多个线程并发访问getInstance(),有可能存在安全问题,多条语句操作private SingleDemo() {// 私有构造函数}public static SingleDemo getInstance() {if (s == null) {synchronized (SingleDemo.class) {// 锁是字节码文件对象if (s == null) {s = new SingleDemo();// 对象延迟加载}}}return s;}}



以上饿汉式不说,单说懒汉式。 

多个线程并发访问getInstance(), 多条语句操作共享数据s,存在安全隐患。

懒汉式:实例延迟加载,多线程访问存在安全隐患。加同步来解决,加同步函数,和同步代码块都行。但是稍微有些低效,

用双重判断可以解决效率问题。加同步使用的锁是哪个,该类所属的字节码对象

线程死锁的案例

 两个对象互相依赖,所以死锁!示例代码如下:
 

public class Test10 implements Runnable {public int flag = 1;static Object o1 = new Object(), o2 = new Object();//两个锁public void run() {System.out.println("flag=" + flag);//两个锁相持不下if (flag == 1) {synchronized (o1) {try {Thread.sleep(500);} catch (Exception e) {e.printStackTrace();}synchronized (o2) {System.out.println("1");}}}if (flag == 0) {synchronized (o2) {try {Thread.sleep(500);} catch (Exception e) {e.printStackTrace();}synchronized (o1) {System.out.println("0");}}}}public static void main(String[] args) {Test10 td1 = new Test10();Test10 td2 = new Test10();//定义标识td1.flag = 1;td2.flag = 0;//两个线程开启Thread t1 = new Thread(td1);Thread t2 = new Thread(td2);t1.start();t2.start();}}


线程间的通讯:

其实就是多个线程在操作同一个资源,

但是操作的动作不同。
 

//资源class Res { String name; String sex;}//添加的方法class Input implements Runnable { private Res r;

 public Input(Res r) {  this.r = r; }

 public void run() {  // TODO Auto-generated method stub  int x = 0;//标识变量如果是0添加女,如果是1,添加男  while (true) {   synchronized (r) {//同步代码块添加,不加同步执行权可能被输出抢走。锁可以是资源对象。    if (x == 0) {     r.name = "丽丽";     r.sex = "女";    } else {     r.name = "mike";     r.sex = "man";    }    x = (x + 1) % 2;   }  } }}//输出类class Output implements Runnable { private Res r;//资源对象

 public Output(Res r) {  this.r = r; } public void run() {  // TODO Auto-generated method stub  while (true) {   synchronized (r) {//这里也必须要加线程,操作共享数据,同一个锁,可以是资源对象    System.out.println(r.name + "..." + r.sex);//输出是统一资源   }  } }}

public class Test11 {

 public static void main(String[] args) {  //形象的比喻  有一堆煤,有两个大卡车,一个进的,一个出的,把煤放到大卡车上,把卡车放到高速公路上。  Res r = new Res();  Input in = new Input(r);  Output out = new Output(r);  //开启两个线程  Thread t1 = new Thread(in);  Thread t2 = new Thread(out);  t1.start();  t2.start(); }}

以上的代码还是有问题的,按理说应该是存一个打印一个这样是比较靠谱的。上面的情况是一大片一大片的男,或者女。为什么出现这种情况?

    输入的线程如果获得了cpu执行权,它存了一个值后,其他线程进不来,这个时候出了同步,output,input都有可能抢到cpu执行权。所以输入有可能还会抢到,前面的值就回被覆盖掉了。当某一时刻,执行权被抢走了,输出被抢到了,他也可能把一个值打印多遍,所以造成了上面的情况。cpu切换造成的。现在需求是这样的,添加一个,取出一个,这样才是最靠谱的。为了满足条件需求,我们要做的是,在资源中加入一个标记,默认false;输入线程在往里面添加数据时,判断标记,false则存入,存完后,输入线程可能还持有执行权,将标记改为真,代表里面有数据了。为true时,不能在存入了,这个时候,让输入线程等着不动,wait()放弃了执行资格;当取走了之后,才能醒,notify()。

当output具备执行权的时候,开始输出,之前也要进行判断,如果true,取出,打印,变为false还持有执行权,回来之后,为false,wati(),叫醒 input,input等的时候,再把output叫醒。等待唤醒机制。

wait();

notity();

notityAll();

都是用在同步中。因为要对持有监视器(锁)的线程操作。

所以要使用同步中,因为只有同步才具有锁。

为什么这些操作线程的方法要定义在Object中呢?

因为这些方法在操作同步线程时,都必须要标识它们所操作线程只有的锁。

只有同一个锁上的被等待线程,可以被同一个锁上notity唤醒。

也就是说,等待和唤醒必须是同一个锁。

而锁可以是任意对象,所以可以被任意对象调用的方法定义在Object类中。

class Res {String name;String sex;boolean flag = false;// 标记是否有 资源}//添加类,实现Runnable接口class Input implements Runnable {private Res r;//资源对象public Input(Res r) {// 关联资源对象this.r = r;}   //重写run方法public void run() {// TODO Auto-generated method stubint x = 0;// 这里也必须要加线程,操作共享数据,同一个锁,可以是资源对象while (true) {synchronized (r) {if (r.flag) {try {r.wait();// 等待} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}if (x == 0) {r.name = "丽丽";r.sex = "女";} else {r.name = "mike";r.sex = "man";// 线程结束后,有可能还能抢到cpu执行权}x = (x + 1) % 2;r.flag = true;r.notify();// 唤醒线程池中的最早wait的线程。}}}}//输出类实现Runnable接口 class Output implements Runnable {private Res r;public Output(Res r) {//关联资源对象this.r = r;}    //重写run方法public void run() {// TODO Auto-generated method stubwhile (true) {synchronized (r) {if (!r.flag) {try {r.wait();// 等待,取消了执行资格} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}System.out.println(r.name + "..." + r.sex);r.flag = false;r.notify();// 叫醒线程池中的最早线程}}}}public class Test {public static void main(String[] args) {Res r = new Res();// 形象的比喻 有一堆煤,有两个大卡车,一个进的,一个出的,把煤放到大卡车上,把卡车放到高速公路上。Input in = new Input(r);Output out = new Output(r);// 开启两个线程Thread t1 = new Thread(in);Thread t2 = new Thread(out);t1.start();t2.start();}}

优化后的代码:

class Res {private String name;private String sex;boolean flag = false;    //设置添加方法public synchronized void set(String name, String sex) {if (flag) {try {this.wait();//线程等待,没有执行资格} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}this.name = name;this.sex = sex;flag = true;//有了数据,标识变为true;this.notify();//唤醒线程池中的最早wait的线程}     //输出方法public synchronized void out() {if (!flag) {try {this.wait();//线程等待,没有执行资格} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}System.out.println(name + ".." + sex);//打印flag = false;//取走了数据,变为false;this.notify();//唤醒线程池中的最早wait的线程}}//添加类,inputclass Input implements Runnable {private Res r;public Input(Res r) {//关联资源this.r = r;}    //重写run方法public void run() {// TODO Auto-generated method stubint x = 0;while (true) {if (x == 0) {r.set("mike", "man");} else {r.set("丽丽", "女");}x = (x + 1) % 2;}}}//输出类class Output implements Runnable {private Res r;public Output(Res r) {//关联资源this.r = r;}public void run() {// TODO Auto-generated method stubwhile (true) {r.out();}}}public class Test {public static void main(String[] args) {Res r = new Res();//资源对象//创建两个线程,并开启new Thread(new Input(r)).start();new Thread(new Output(r)).start();}}


生产者与消费者的例子。出现更多线程运行程序。以上的例子会出现问题。

public class Test { public static void main(String[] args) {  Resource r = new Resource();//创建资源对象  Producer pro = new Producer(r);//生成者  Cousumer con = new Cousumer(r);//消费者  //创建4个对象,并开启  Thread t1 = new Thread(pro);  Thread t2 = new Thread(pro);  Thread t3 = new Thread(con);  Thread t4 = new Thread(con);  t1.start();  t2.start();  t3.start();  t4.start(); }}//资源类class Resource { private String name; private int count = 1;// 编号 private boolean flag = false;//判断标记   //赋值方法给  添加资源时调用 public synchronized void set(String name) {  while (flag) {   try {    this.wait();//线程等待,不具执行资格了。   } catch (InterruptedException e) {    // TODO Auto-generated catch block    e.printStackTrace();   }  }  this.name = name + "--" + count++;  System.out.println(Thread.currentThread().getName() + "............生产者"    + this.name);  flag = true;//输出了之后,没有资源了,标记改为true;  this.notifyAll();//唤醒线程池全部线程 }

 public synchronized void out() {  while (!flag) {   try {    this.wait();////线程等待,不具执行资格了。   } catch (InterruptedException e) {    // TODO Auto-generated catch block    e.printStackTrace();   }  }  System.out.println(Thread.currentThread().getName() + "..消费者"    + this.name);  flag = false;//输出了之后,没有资源了,标记改为false;  this.notifyAll();//线程等待,不具执行资格了。 }}//生产者类,添加商品class Producer implements Runnable { private Resource res;  public Producer(Resource res) {//生成者关联资源对象  this.res = res; }    //重写run方法。 public void run() {  // TODO Auto-generated method stub  while (true) {   res.set("+商品+");//赋值,添加产品  } }}//消费者类,取走商品class Cousumer implements Runnable {// 消费者 private Resource res;

 public Cousumer(Resource res) {  this.res = res; }

 public void run() {  // TODO Auto-generated method stub  while(true){    res.out();//输出  } }

}

 

当出现多个生产者消费的时候,必须要有while循环,要用notifyAll,用原来的就不行,这个是比较通用的。

对于多个生产者和消费者。

为什么要定义while判断标记。

原因:让被唤醒的线程再一次判断标记。

为什么定义notifyALL.

因为需要唤醒对方线程。

因为只用notify,容易出现只唤醒本方线程的情况,导致程序中的所有线程都等待。

Lock接口

 

解决线程安全问题使用同步的形式,(同步代码块,要么同步函数)其实最终使用的都是锁机制。


到了后期版本,直接将锁封装成了对象。线程进入同步就是具备了锁,执行完,离开同步,就是释放了锁。

在后期对锁的分析过程中,发现,获取锁,或者释放锁的动作应该是锁这个事物更清楚。所以将这些动作定义在了锁当中,并把锁定义成对象。


所以同步是隐示的锁操作,而Lock对象是显示的锁操作,它的出现就替代了同步。


在之前的版本中使用Object类中wait、notify、notifyAll的方式来完成的。那是因为同步中的锁是任意对象,所以操作锁的等待唤醒的方法都定义在Object类中。


而现在锁是指定对象Lock。所以查找等待唤醒机制方式需要通过Lock接口来完成。而Lock接口中并没有直接操作等待唤醒的方法,而是将这些方式又单独封装到了一个对象中。这个对象就是

Condition,将Object中的三个方法进行单独的封装。并提供了功能一致的方法await()、signal()、signalAll()体现新版本对象的好处。

< java.util.concurrent.locks > Condition接口:await()、signal()、signalAll();

  成功的 lock 操作与成功的 Lock 操作具有同样的内存同步效应。

 成功的 unlock 操作与成功的 Unlock 操作具有同样的内存同步效应

原创粉丝点击