黑马程序员——线程

本文深入浅出地介绍了Java多线程的基础概念、线程的创建方式、线程的状态变化、线程间的通信机制,包括使用synchronized关键字解决线程安全问题,以及如何通过wait、notify和notifyAll方法实现线程间的同步与协调。文章还通过实例演示了如何避免常见的线程问题,如死锁和数据竞争,并提供了生产者消费者模式的实现方法。

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

1 线程的概述

进程:正在运行的程序,负责了这个程序的内存空间分配。

线程:就是在一个进程中负责一个执行路径。

多线程:就是在一个进程中多个执行路径同时执行。

 

多线程的好处:

1.         解决了一个进程里面可以同时运行多个任务(执行路径)。

2.         提供资源的利用率,而不是提供效率。

多线程的弊端:

1.         降低了一个进程里面的线程的执行频率。

2.         对线程进行管理要求额外的 CPU开销。线程的使用会给系统带来上下文切换的额外负担。

3.         公有变量的同时读或写。当多个线程需要对公有变量进行写操作时,后一个线程往往会修改掉前一个线程存放的数据,发生线程安全问题。

4.         线程的死锁。即较长时间的等待或资源竞争以及死锁等多线程症状。

2  创建线程的方式

2.1   创建线程的方式一

1.        继承Thread

 

getName()是获取线程的名字。

 

2.        需要复写run方法,把要执行的任务放在run方法中。

 

3.        调用start()方法启动线程

 

达到了我们预期的效果。

线程的使用细节:

1.         线程的启动使用父类的start()方法

2.         如果线程对象直接调用run(),那么JVN不会当作线程来运行,会认为是普通的方法调用。

3.         线程的启动只能由一次,否则抛出异常

4.         可以直接创建Thread类的对象并启动该线程,但是如果没有重写run(),什么也不执行。

5.         匿名内部类的线程实现方式

2.2   线程的状态

创建:新创建了一个线程对象。

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

运行:就绪状态的线程获取了CPU执行权,执行程序代码。

阻塞:阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

死亡:线程执行完它的任务时。

2.3   常见线程的方法

Thread(String name)    初始化线程的名字

 getName()            返回线程的名字

 setName(String name)   设置线程对象名

 getId()                返回线程的标识  同一个线程对象的id不同

 getPriority()            返回当前线程对象的优先级   默认线程的优先级是5

 setPriority(int newPriority)设置线程的优先级    虽然设置了线程的优先级,但是具体的实现取决于底层的操作系统的实现(最大的优先级是10,最小的1 ,默认是5)。

 currentThread()     返回CPU正在执行的线程的对象

class ThreadDemo1extends Thread

{

    public ThreadDemo1(){

     

    }

    public ThreadDemo1( String name ){

       super( name );

    }

   

    publicvoid run(){

       int i = 0;

       while(i < 30){

         i++;

          System.out.println(this.getName() +" "+ " : i = " + i);

         System.out.println( Thread.currentThread().getName() +" "+ " : i = " + i);

         System.out.println( Thread.currentThread() ==this );

         System.out.println("getId()" + " "+ " : id = " +super.getId() );

         System.out.println("getPriority()" +" "+ " : Priority = " +super.getPriority() );

       }

    }

}

classDemo3

{

    publicstaticvoid main(String[] args)

    {

        ThreadDemo1 th1 = new ThreadDemo1("线程1");

       ThreadDemo1 th2 = new ThreadDemo1("线程2");

        // 设置线程名

        th1.setName( "th1" );

       th2.setName( "th2" );

        // 设置线程优先级  1 ~ 10

       th1.setPriority( 10 );

       th2.setPriority( 7 );

       // 查看SUN定义的线程优先级范围

       System.out.println("max : " + Thread.MAX_PRIORITY );

       System.out.println("min : " + Thread.MIN_PRIORITY );

        System.out.println("nor : " + Thread.NORM_PRIORITY );

       th1.start();

       th2.start();

       System.out.println("Hello World!");

    }

}

 

2.4   创建线程的方式二

创建线程的第二种方式.使用Runnable接口.

该类中的代码就是对线程要执行的任务的定义.

1:定义了实现Runnable接口

2:重写Runnable接口中的run方法,就是将线程运行的代码放入在run方法中

3:通过Thread类建立线程对象

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

5:调用Thread类的start方法开启线程,并调用Runable接口子类run方法

为什么要将Runnable接口的子类对象传递给Thread的构造函数,因为自定义的run方法所属对象是Runnable接口的子类对象,所以要让线程去执行指定对象的run方法

package cn.itcast.gz.runnable;

publicclass Demo1 {

    publicstaticvoid main(String[] args) {

       MyRun my = new MyRun();

       Thread t1 = new Thread(my);

       t1.start();

       for (int i = 0; i < 200; i++) {

           System.out.println("main:" + i);

       }

    }

}

class MyRunimplements Runnable {

    publicvoid run() {

       for (int i = 0; i < 200; i++) {

           System.err.println("MyRun:" + i);

       }

    }

}

 

 

理解Runnable:

Thread类可以理解为一个工人,而Runnable的实现类的对象就是这个工人的工作(通过构造方法传递).Runnable接口中只有一个方法run方法,该方法中定义的事会被新线程执行的代码.当我们把Runnable的子类对象传递给Thread的构造时,实际上就是让给Thread取得run方法,就是给了Thread一项任务.

 

买票例子使用Runnable接口实现

在上面的代码中故意照成线程执行完后,执行Thread.sleep(100),以让cpu让给别的线程,该方法会出现非运行时异常需要处理,这里必须进行try{}catch(){},因为子类不能比父类抛出更多的异常,接口定义中没有异常,实现类也不能抛出异常。

运行发现票号出现了负数,显示了同一张票被卖了4次的情况。

出现了同样的问题。如何解决?

class MyTicketimplements Runnable {

    inttickets = 100;

    publicvoid run() {

       while (true) {

           if (tickets > 0) {

              try {

                  Thread.sleep(100);

              } catch (InterruptedException e) {

                  e.printStackTrace();

              }

              System.out.println(Thread.currentThread().getName() +"窗口@销售:"

                     + tickets + "号票");

              tickets--;

             

           } else {

              System.out.println("票已卖完。。。");

              break;

           }

       }

    }

}

publicclass Demo6 {

    publicstaticvoidmain(String[] args) {

       MyTicket mt = new MyTicket();

       Thread t1 = new Thread(mt);

       Thread t2 = new Thread(mt);

       Thread t3 = new Thread(mt);

       Thread t4 = new Thread(mt);

       t1.start();

       t2.start();

       t3.start();

       t4.start();

    }

}

3 锁对象

publicclass DeadLock {

    publicstaticvoid main(String[] args) {

       new Thread(new Runnable() {// 创建线程,代表中国人

                  publicvoid run() {

                     synchronized ("刀叉") { // 中国人拿到了刀叉

                         System.out.println(Thread.currentThread().getName()

                                + ": 你不给我筷子, 我就不给你刀叉");

                         try {

                            Thread.sleep(10);

                         } catch (InterruptedException e) {

                            e.printStackTrace();

                         }

                         synchronized ("筷子") {

                            System.out.println(Thread.currentThread()

                                   .getName() +": 给你刀叉");

                         }

                     }

                  }

              }, "中国人").start();

       new Thread(new Runnable() {// 美国人

                  publicvoid run() {

                     synchronized ("筷子") { // 美国人拿到了筷子

                         System.out.println(Thread.currentThread().getName()

                                + ": 你先给我刀叉, 我再给你筷子");

                         try {

                            Thread.sleep(10);

                         } catch (InterruptedException e) {

                            e.printStackTrace();

                         }

                         synchronized ("刀叉") {

                            System.out.println(Thread.currentThread()

                                   .getName() +": 好吧,把筷子给你.");

                         }

                     }

                  }

              }, "美国人").start();

    }

}

4 线程的通讯

线程间通信其实就是多个线程在操作同一个资源,但操作动作不同

生产者消费者

如果有多个生产者和消费者,一定要使用while循环判断标记,然后在使用notifyAll唤醒,否者容易只用notify容易出现只唤醒本方线程情况,导致程序中的所有线程都在等待。

例如:有一个数据存储空间,划分为两个部分,一部分存储人的姓名,一部分存储性别,我们开启一个线程,不停地想其中存储姓名和性别(生产者),开启另一个线程从数据存储空间中取出数据(消费者)。

       由于是多线程的,就需要考虑,假如生产者刚向数据存储空间中添加了一个人名,还没有来得及添加性别,cpu就切换到了消费者的线程,消费者就会将这个人的姓名和上一个人的性别进行了输出。

还有一种情况是生产者生产了若干次数据,消费者才开始取数据,或者消费者取出数据后,没有等到消费者放入新的数据,消费者又重复的取出自己已经去过的数据。

publicclass Demo10 {

    publicstaticvoid main(String[] args) {

       Person p = new Person();

       Producer pro = new Producer(p);

       Consumer con = new Consumer(p);

       Thread t1 = new Thread(pro,"生产者");

       Thread t2 = new Thread(con,"消费者");

       t1.start();

       t2.start();

    }

}

 

// 使用Person作为数据存储空间

class Person {

    String name;

    String gender;

}

 

// 生产者

class Producerimplements Runnable {

    Person p;

 

    public Producer() {

 

    }

 

    public Producer(Person p) {

       this.p = p;

    }

 

    @Override

    publicvoid run() {

       int i = 0;

       while (true) {

           if (i % 2 == 0) {

              p.name ="jack";

              p.gender ="man";

           } else {

              p.name ="小丽";

              p.gender ="";

           }

           i++;

       }

 

    }

 

}

 

// 消费者

class Consumerimplements Runnable {

    Person p;

 

    public Consumer() {

 

    }

 

    public Consumer(Person p) {

       this.p = p;

    }

 

    @Override

    publicvoid run() {

 

       while (true) {

           System.out.println("name:" +p.name +"---gnder:" +p.gender);

       }

    }

 

}

 

在上述代码中,Producer和Consumer 类的内部都维护了一个Person类型的p成员变量,通过构造函数进行赋值,在man方法中创建了一个Person对象,将其同时传递给Producer和Consumer对象,所以Producer和Consumer访问的是同一个Person对象。并启动了两个线程。

显然屏幕输出了小丽 man这样的结果是出现了线程安全问题。所以需要使用synchronized来解决该问题。

 

升级:在Person类中添加两个方法,set和read方法并设置为synchronized的,让生产者和消费者调用这两个方法。

publicclass Demo10 {

    publicstaticvoidmain(String[] args) {

       Person p = new Person();

       Producer pro = new Producer(p);

       Consumer con = new Consumer(p);

       Thread t1 = new Thread(pro,"生产者");

       Thread t2 = new Thread(con,"消费者");

       t1.start();

       t2.start();

    }

}

 

// 使用Person作为数据存储空间

class Person {

    String name;

    String gender;

   

 

    publicsynchronizedvoid set(String name, String gender) {

       this.name = name;

       this.gender = gender;

    }

 

    publicsynchronizedvoid read() {

       System.out.println("name:" +this.name +"----gender:" +this.gender);

    }

 

}

 

// 生产者

class Producerimplements Runnable {

    Person p;

 

    public Producer() {

 

    }

 

    public Producer(Person p) {

       this.p = p;

    }

 

    @Override

    publicvoid run() {

       int i = 0;

       while (true) {

 

           if (i % 2 == 0) {

              p.set("jack","man");

           } else {

              p.set("小丽","");

           }

           i++;

 

       }

 

    }

 

}

 

// 消费者

class Consumerimplements Runnable {

    Person p;

 

    public Consumer() {

 

    }

 

    public Consumer(Person p) {

       this.p = p;

    }

 

    @Override

    publicvoid run() {

 

       while (true) {

           p.read();

 

       }

    }

 

}

 

 

需求:我们需要生产者生产一次,消费者就消费一次。然后这样有序的循环。

这就需要使用线程间的通信了。Java通过Object类的wait,notify,notifyAll这几个方法实现线程间的通信。

1.1.1.  等待唤醒机制

wait:告诉当前线程放弃执行权,并放弃监视器(锁)并进入阻塞状态,直到其他线程持有获得执行权,并持有了相同的监视器(锁)并调用notify为止。

notify:唤醒持有同一个监视器(锁)中调用wait的第一个线程,例如,餐馆有空位置后,等候就餐最久的顾客最先入座。注意:被唤醒的线程是进入了可运行状态。等待cpu执行权。

notifyAll:唤醒持有同一监视器中调用wait的所有的线程。

 

如何解决生产者和消费者的问题?

可以通过设置一个标记,表示数据的(存储空间的状态)例如,当消费者读取了(消费了一次)一次数据之后可以将标记改为false,当生产者生产了一个数据,将标记改为true。

,也就是只有标记为true的时候,消费者才能取走数据,标记为false时候生产者才生产数据。

代码实现:

package cn.itcast.gz.runnable;

 

publicclass Demo10 {

    publicstaticvoidmain(String[] args) {

       Person p = new Person();

       Producer pro = new Producer(p);

       Consumer con = new Consumer(p);

       Thread t1 = new Thread(pro,"生产者");

       Thread t2 = new Thread(con,"消费者");

       t1.start();

       t2.start();

    }

}

 

// 使用Person作为数据存储空间

class Person {

    String name;

    String gender;

    booleanflag =false;

 

    publicsynchronizedvoid set(String name, String gender) {

       if (flag) {

           try {

               wait();

           } catch (InterruptedException e) {

 

              e.printStackTrace();

           }

       }

       this.name = name;

       this.gender = gender;

       flag =true;

       notify();

    }

 

    publicsynchronizedvoid read() {

       if (!flag) {

           try {

              wait();

           } catch (InterruptedException e) {

 

              e.printStackTrace();

           }

       }

       System.out.println("name:" +this.name +"----gender:" +this.gender);

       flag =false;

       notify();

    }

 

}

 

// 生产者

class Producerimplements Runnable {

    Person p;

 

    public Producer() {

 

    }

 

    public Producer(Person p) {

       this.p = p;

    }

 

    @Override

    publicvoid run() {

       int i = 0;

       while (true) {

 

           if (i % 2 == 0) {

              p.set("jack","man");

           } else {

              p.set("小丽","");

           }

           i++;

 

       }

 

    }

 

}

 

// 消费者

class Consumerimplements Runnable {

    Person p;

 

    public Consumer() {

 

    }

 

    public Consumer(Person p) {

       this.p = p;

    }

 

    @Override

    publicvoid run() {

 

       while (true) {

           p.read();

 

       }

    }

 

}

 

 

线程间通信其实就是多个线程在操作同一个资源,但操作动作不同,wait,notify(),notifyAll()都使用在同步中,因为要对持有监视器(锁)的线程操作,所以要使用在同步中,因为只有同步才具有锁。

为什么这些方法定义在Object类中

因为这些方法在操作线程时,都必须要标识他们所操作线程持有的锁,只有同一个锁上的被等待线程,可以被统一锁上notify唤醒,不可以对不同锁中的线程进行唤醒,就是等待和唤醒必须是同一个锁。而锁由于可以使任意对象,所以可以被任意对象调用的方法定义在Object类中

wait() 和 sleep()有什么区别?

wait():释放资源,释放锁。是Object的方法

sleep():释放资源,不释放锁。是Thread的方法

定义了notify为什么还要定义notifyAll,因为只用notify容易出现只唤醒本方线程情况,导致程序中的所有线程都在等待。

 

2.     停止线程

任何事物都是生命周期,线程也是,

1. 正常终止 当线程的run()执行完毕,线程死亡。

2. 使用标记停止线程

注意:Stop方法已过时,就不能再使用这个方法。

如何使用标记停止线程停止线程。

开启多线程运行,运行代码通常是循环结构,只要控制住循环,就可以让run方法结束,线程就结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值