黑马程序员_八 【线程认识】【线程单例】【线程创建使用】

 

--------------------- android培训java培训、java学习型技术博客、期待与您交流! -------------------

 

 1  线程认识

 

线程和进程:

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

       每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。(Cpu其实只执行一个程序,只不过切换快感觉想在执行多个程序)

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

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

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

Java VM  启动的时候会有一个进程java.exe. 该进程中至少一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。

该线程称之为主线程。

一个进程里面有多个线程执行,就是多线程。

扩展:其实更细节说明jvmjvm启动不止一个线程,还有负责垃圾回收机制的线程。

多线程的意义:

 一个采用了多线程技术的应用程序可以更好地利用系统资源。其主要优势在于充分利用了CPU的空闲时间片,可以用尽可能少的时间来对用户的要求做出响应, 使得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。更为重要的是,由于同一进程的所有线程是共享同一内存,所以不需要特殊的数据传送机制,不需要建立共享存储区或共享文件,从而使得不同任务之间的协调操作与 运行、数据的交互、资源的分配等问题更加易于解决。

如虚拟机,程序运行的同时,没有垃圾回收机制的线程,有可能会造成系统卡死。如果多线程,会优化程序运行更细致的说jvm不止启动一个线程,还有负责垃圾回收机制的线程。

 2  线程中的单例模式 

单例模式:

单例模式结合多线程,指的是懒汉式单例模式,在懒汉式单例模式中,存在着安全隐患。

普通懒汉式单例:

class Single

{

//私有化构造方法

private Single(){};

//定义静态引用

private static Single s=null;

//获取实例对象方法

public static Single getSingle()

{

if(s==null)

s=new Single();

else

return s;

}

}

这样的操作存在着安全隐患,当多个程序同时调用该方法时,一个程序运行到判断s是否为空,结果为空,恰巧进序放弃了执行权,又有一个进程来判断,接扩也为空,都进入创建对象的地方,就有可能创建了多个对象,不能实现单例的效果。

解决方法:使用同步进行,将程序同步后,可以防止一个进程进入后另一个再次进入。

正确示例:

class Single

{

private Single(){};

private static Single s=null;

public static Single getSingle()

{

if(s==null)

{

Synchronized(Single.class)

{

if(s==null)

s=new Single();

}

}

return s;

}

}

通过两次判断和一个同步代码块,将线程冲突问题解决。在一般情况下都会选择饿汉式单例模式,如下:

class Single

{

private Single(){};

private static Single s=new Single();

public static Single getSingle()

{

return s;

}

}

饿汉式单例模式,更加安全,唯一的不同就是先创建对象。

 3  线程的状态 

线程有多中状态,可分为多种情况,这里分为了六种状态,分别为被创建,准备,运行,冻结,阻塞,消亡 六种状态,如下图所示:

上图可能不是那么准确,都是为了理解线程的存在过程,每个线程的存在都需要被创建,然后再去获取执行资格,有了执行资格就可以获取cpu的执行权,从而启动线程运行。线程运行完毕后就进入了消亡状态,java虚拟机将内存垃圾清除。

冻结状态:
一个线程如果被强制停止,就会进入冻结状态。如使用sleep方法和wait方法,使线程处于睡眠或者等待状态。

阻塞态:

通过使用方法sleepwait停止掉的线程,可以使用notify方法唤醒,唤醒后有可能因为没有执行资格而处于阻塞挂起状态。当线程拥有了执行资格可以解除阻塞状态而运行线程程序。

 4  线程的创建方法 

创建新执行线程有两种方法。一种方法是将类声明为 Thread 的子类。该子类应重写 
    Thread 类的 run 方法。接下来可以分配并启动该子类的实例。
    创建线程的步骤:
      1)定义类继承Thread
      2)覆写Thread类中的run方法。
      3)调用线程的start方法。该方法两个作用:启动线程,调用run 方法


为什么要将Runnable接口的子类对象传递给Thread的构造函数
因为自定义的run 方法所属的对象是Runnable接口的子类对象
所以要让线程去指定对象的run 方法。就必须明确该run方法
所属的对象

实现方式和继承方式有什么区别?
实现方式好处:避免了单继承的局限性
在定义线程时,建立使用实现方式,
两种方式区别:
     |--继承Thread:线程代码存放在Thread子类run方法中。
     |--实现Runnable,线程代码存放在接口的子类的run 方法

例如,计算大于某一规定值的质数的线程可以写成
              class PrimeThread extends Thread {
                    long minPrime;
                    PrimeThread(long minPrime) {
                         this.minPrime = minPrime;
                   }
                   public void run() {
                   PrimeThread p = new PrimeThread(143);
                  p.start();
               }
            }

          public void run()如果该线程是使用独立的 Runnable 运行对象构造的,则调用该

            Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回 

          public void start()使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 

              结果是两个线程并发地运行;当前线程(从调用返回给 start 方法)和另一个线程(执行其 run 方法)。 

     多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。

问题:为什么要覆盖run方法?

Thread类用于与描述线程。该类定义了一个功能,用于存储线程要运行的代码。

该存储功能就是run 方法。也就是说Thread类中的run 方法,用于存储线程要运行的代码

为什么要用"线程对象.start()"而不是"线程对象.run()"方法来开启线呢?  

线程对象.start();作用是开启线程并执行线程的run 方法。

线程对象.run();仅仅是对象调用方法,run();依然是主线程执行的,而线程创建了并没有运行。

创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。

创建线程的第二种方式:实现Runnable接口

          步骤:

             1)定义类实现Runnable接口

             2)覆盖Runnable接口中的run方法 

                    将线程要运行的代码存放在该run方法中

             3)通过Thread类建立对象

             4)将Runnable接口的子对象作为实际参数传给Thread类的构造函数

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

采用这种风格的同一个例子如下所示:

  class PrimeRun implements Runnable {
         long minPrime;
         PrimeRun(long minPrime) {
             this.minPrime = minPrime;
         }
         public void run() {
              PrimeRun p = new PrimeRun(143);
              new Thread(p).start();
         }
     }

  Thread(Runnable target) 

        分配新的 Thread 对象  Runnable 接口应该由那些打算通过

                 某一线程执行其实例的类来实现。

        类必须定义一个称为 run 的无参数方法。

多线程的特点:
         随机性,某一时刻只有一个程序在运行(多核除外),至于执行多长CPU说了算。
多线程运行出现了安全问题。
问题原因:当多条语句操作同一个线程共享数据时,一个线程对多条语句执行了
 一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。
         解决办法:对多条操作共享数据的语句,只能让一个线程执行完,在执行过程中,其 他线程不可以参与执行。
         Java对于多线程的安全问题提供了专业的解决方式。就是同步代码块;

Synchronized(对象)

{
    需要被同步的代码
         
   对象如同锁,持有锁的线程可以再同步中执行。没有持锁的线程即使获CPU的执行权,也进不去,因为没有获取锁。

同步前提:
        1)必须有两个以上的线程使用同一个锁。
        2)必须是多个线程使用同一个锁。
        3)必须保证同步中只能有一个线程在运行。
       好处:解决了多线程的安全问题。
       弊端:多个线程需要判断锁,较为消耗资源
   例:
      需求:银行有一个金库,有两个储户分别存300元,每次存100,存3次。
如何找问题:
       1)明确哪些代码是多线程运行代码。
       2)明确共享数据。
       3)明确多线程运行代码中哪些语句操作共享数据

            class Bank{
                    private int sum;
                    Object obj= new Object();
                    public void add(int n)

{
      synchronized(obj)

{
    try

{                                                  Thread.sleep(100);
    }

catch(Exception e){}
                                                         sum=sum+n;                                                   System.out.println(Thread.currentThread().getName()+"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 BankTest

{
      public static void main(String[] args)

{
                                                   Cus c=new Cus();
                                                  Thread t1=new Thread(c)
                                                  Thread t2=new Thread(c);
                                                   t1.start();
                                                   t2.start();

 }

}

             将上面的函数需要同步的代码 改成
                public synchronized void add(int n){……}
             就成为了同步函数。此时就可以去除obj对象锁。

同步函数用的是那个锁呢?
      函数需要被对象调用,那么函数都有一个所属对象引用,就是this
      所以同步函数使用的锁是this
问题:如果同步函数用被静态修饰后使用的锁是什么呢?
      因为静态方法中不可以定义this,所以不可能是this。静态进内存中没有本类对象,但是一定有该类对应的字节码文件对象,即类名.class。静态的同步方法,使用的锁是该类方法所在类的字节码文件对象

 5  线程的使用 

线程的使用,主要体现在多用户访问上,如卖票,多个窗口同时卖票,但是卖出的票都是唯一的,不会重复,这就是多线程。

经典案例:卖票

需求:卖票程序,多个窗口同时买票。

演示代码:

Class Ticketextends Thread

{

      Private int tick = 100;

      Public void run();

      While (true)

{

           if(tick>0)

           {

System.out.println(Thread.currentThread().getName()+"....sale: "+ tick--);

           }

}

}

class  TicketDemo

{

      public static void main(String[] args)

      {

           Ticket t1 = new Ticket();

           Ticket t2 = new Ticket();

           Ticket t3 = new Ticket();

           Ticket t4 = new Ticket();

           t1.start();

           t2.start();

           t3.start();

           t4.start();

// t1.start();如果只用t1.start();连续运行4次,提示错误。因为运行一次后,线程由创建状态到运行状态,如果再运行,提示该线程再从创建状态到运行状态,会出错。已经运行的程序是不允许再次开启的

           //t1.start();

           //t1.start();

           //t1.start();

 

      }

}

这样输出的话,会造成每一个Ticket各自卖100张票,总共卖了400张票,因为tick的值不共享,每个线程单独调用100。假如用static修饰tick的话不出现错误,但是静态的生命太长,即使程序运行玩了tick的值还是100,不严谨,所以不用。

1,定义类实现Runnable接口

2,覆盖Runnable接口中的run方法。

      将线程要运行的代码存放在该run方法中。

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

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

      为什么要将Runnable接口的子类对象传递给Thread的构造函数。

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

      所以要让线程去指定指定对象的run方法。就必须明确该run方法所属对象。

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

演示代码:

class Ticketimplements Runnable//1,定义类实现Runnable接口

{

      private int tick = 100;

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

      {

           while(true)

           {

                 if(tick>0)

                 {

                      System.out.println(Thread.currentThread().getName()+"....sale: "+ tick--);

                 }

           }

      }

}

class  TicketDemo

{

      public static void main(String[] args)

      {

           Ticket t = new Ticket();//创建一个ticket的对象t通过Thread类建立线程对象。

           Thread t1 = new Thread(t);//创建了一个线程;

           Thread t2 = new Thread(t);//创建了一个线程;Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。

           Thread t3 = new Thread(t);//创建了一个线程;

           Thread t4 = new Thread(t);//创建四个线程;因为threadobject中,所以可以直接使用这个类创建线程。又查看API里面thread里面有构造函数Thread(Runnable target),所以thread构造函数可以接受以Runnable接口的对象,所以t因为可以实现Runnable,作为一个对象可以调用thread的构造函数。创建一个线程。其实在创建线程的时候,没有对象是无法创建的。每个线程都得有一个对象。

           t1.start();

           t2.start();

           t3.start();

           t4.start();

      }

}

 5  线程进一步解析:

问题:实现方式和继承方式有什么区别呢?

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

(如果一个类继承了别的父类,没有继承thread,那么就用接口的方式来实现多线程。)

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

两种方式区别:线程代码的存放位置

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

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

多线程安全问题:

导致安全问题出现的原因:

1、  多线程访问出现随机性

2、  线程的随机性

线程的安全性在理想状态下,不容易出现,但一旦出现对软件影响是非常大的。

例子:

class Ticketimplements Runnable

{

      private static int tick = 100;

      public void run()

      {

           while(true)//这里为什么不能抛出?试着解决

           {

                 if(tick>0)//当四个线程的数字假如是0,1,2,3的时候,0先进入,下面的就代码就不允许了,tick0.但是线程因为是同时进行,所以1也有可能运行,23也有可能运行,然后输出的的结果可能会有0,-1,-2.

                 {

                      try{Thread.sleep(10);}catch(Exceptione){}//演示代码,让这个代码在停止运行前暂时进入sleep状态(10毫秒),这样可以演示出代码输出0,-1,-2.在实际使用过程中也有会有类似情况,譬如运行当钱程序时,切出去运转别的程序,当前程序也会进入sleep状态。

                      System.out.println(Thread.currentThread().getName()+"....sale: "+ tick--);

                 }

           }

      }

}

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

多线程的运行出现了安全问题。多线程安全问题比较重要,有可能在测试时发现不了。所以编写多线程的时候,一定要注意多线程的安全问题

问题的原因:

当多条语句在操作同一个线程共享数据时(如火车票),一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行(4个窗口卖同一类的火车票)。导致共享数据的错误。

解决办法:

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

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

同步代码块:

synchronized(对象)

{

       需要被同步的代码

}

例子:

class Ticketimplements Runnable

{

      private static int tick = 100;

      Object obj = new Object();

      public void run()

      {

           while(true)

           {

                 synchronized(obj)// 同步代码块,注意格式,同步代码块后面括号内部必须有对象

                 {//为什么是这部分为同步代码块?因为这部分都有需要被同步的值tick

                 if(tick>0)

                      {

                            try{Thread.sleep(10);}catch(Exceptione){}

                            System.out.println(Thread.currentThread().getName()+"....sale: "+ tick--);

                      }

                 }

           }

      }

}

同步代码块原理:

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

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

(自己语言总结:当0线程进入同步代码块synchronized时,就把可以执行的权利给锁住了,别的线程无法同步进行,当0线程执行完以后,才会把锁解开,然后别的线程才会进来。所以解决了线程的安全性问题,但是需要多个线程运行之前要判断锁是否打开,比较容易消耗系统资源)

火车上的卫生间---经典。

同步的前提:

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

2必须是多个线程使用同一个锁。

这样才能保证同步中只能有一个线程在运行。

好处:解决了多线程的安全问题。弊端:多个线程需要判断锁,较为消耗资源,

同步函数:

同步函数的格式:在函数修饰符上面加上synchronized即可

同步的两种表现形式:同步代码块和同步函数

需求:

银行有一个金库。

有两个储户分别存300员,每次存100,存3次。

目的:该程序是否有安全问题,如果有,如何解决?

如何找问题:

1,明确哪些代码是多线程运行代码。

      Add()这部分的是运行代码块。其实class Cus中的for函数也是,但是for函数中执行的仍然是add()这块的函数。所以核心多线程代码还是add

2,明确共享数据。

      BSum,多线程一起运行互相抢占的都是sum的值。一般成员都是共享数据。

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

      可以通过sleep函数,和try语句验证。明确    sum= sum + n;语句是操作共享数据的。建立多线程同步代码块

例子:

class Bank

{

      private int sum;

      Object obj = new Object();//建立obj的对象,指向object,作为synchronized的对象

      public synchronized void add(int n)//加上synchronized修饰这个add函数就行了

      {

           //synchronized(obj)

           //{

                 sum = sum + n;

                 try{Thread.sleep(10);}catch(Exceptione){}//通过sleep函数延迟10毫秒,验证此语句是否为操作共享数据的语句。输出的时候发现200 200, 400400,证明是这条语句。然后建立同步代码块。

                 System.out.println("sum="+sum);

           //}

      }

}

class Cusimplements Runnable

{

      private Bank b = new Bank();

      public void run()

      {         

           for(int x=0; x<3; x++)//这条语句就不用建立同步代码块了,因为add()语句已经有锁了,这个for函数调用add,所以这个锁就起到了同步的作用,锁个线程必须使用同一把锁.

           {

                 b.add(100);

           }

      }

}

class  BankDemo

{

      public static void main(String[] args)

      {

           Cus c = new Cus();

           Thread t1 = new Thread(c);//通过c对象来建立一个多线程t1

           Thread t2 = new Thread(c);// 通过c对象来建立一个多线程t2

           t1.start();//建立同步代码块的前提,至少2个线程,只有一个线程运行。符合条件

           t2.start();

      }

}

例子:

class Ticketimplements Runnable

{

      private int tick = 1000;

      public synchronized void run()

      {

           while(true)

           {

                      if(tick>0)

                      {

                            //try{Thread.sleep(10);}catch(Exceptione){}

                            System.out.println(Thread.currentThread().getName()+"....sale: "+ tick--);

                      }

           }

      }

}

上面这段代码如果用多线程运行时,无法实现多线程的。因为:run函数用synchronized修饰,成为了同步函数,当0线程进入以后,自己进行的循环,再循环没有结束之前,是不会出来的。也就是说进了厕所一直撒尿不出来,别人也无法继续使用厕所撒尿。他还关着门。所以无法实现多线程。

解决方法

将循环封装到一个函数里面,在run里面调用这个函数,对这个函数使用synchronized函数修饰,这样就可以是实现多线程了。

public synchronizedvoid show()

      {

           if(tick>0)

           {

  System.out.println(Thread.currentThread().getName()+"....show....: "+ tick--);

           }

      }

 6  同步锁 

同步函数用的是哪一个锁呢?

函数需要被对象调用。那么函数都有一个所属对象引用。就是this

所以同步函数使用的锁是this

通过该程序进行验证。

使用两个线程来买票。

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

一个线程在同步函数中。

都在执行买票动作。

class Ticketimplements Runnable

{

      private int tick = 100;

      Object obj = new Object();

      boolean flag = true;

      public void run()

      {

           if(flag)

           {

                 while(true)

                 {

                      synchronized(this)//因为show方法锁住的是this,所以这里的对象只有是this,才能将线程放在一个锁上面

                      {

                            if(tick>0)

                            {

                                  try{Thread.sleep(10);}catch(Exceptione){}

                                  System.out.println(Thread.currentThread().getName()+"....code: "+ tick--);

                            }

                      }

                 }

           }

           else

                 while(true)

                      show();

      }

      public synchronized void show()//this

      {

           if(tick>0)

           {

                 try{Thread.sleep(10);}catch(Exceptione){}

                 System.out.println(Thread.currentThread().getName()+"....show....: "+ tick--);

           }

      }

}

class  ThisLockDemo

{

      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(Exceptione){}

           t.flag = false;

           t2.start();

      }

}

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

被静态修饰的同步函数

通过验证,发现不在是this。因为静态方法中也不可以定义this

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

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

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

懒汉式单例设计

class Single

{

      private static Single s = null;

      private Single(){}

      public static  Single getInstance()//可以在这里放synchronized,但是会造成程序效率太低。因为每次都会排队进入判断。

      {

           if(s==null)

           {

                 synchronized(Single.class)//可以在这里放synchronized,因为函数是静态,所以锁住类名,这样更高效,而且安全。

                 {//当第一个对象建立以后,如果后面又有几个跟进来,可以再次判断,保证安全。而对象建立后,外面的if判断完毕,直接返回s,就不会用进入锁中再次判断,减少判断次数

                      if(s==null)

                            s = new Single();

                 }

           }

           return s;

      }

}

死锁的原因:互相抢执行权

lass Testimplements Runnable

{

      private boolean flag;

      Test(boolean flag)

      {

           this.flag = flag;

      }

      public void run()

      {

           if(flag)

           {

                 while(true)

                 {

                      synchronized(MyLock.locka)//a

                      {

                            System.out.println(Thread.currentThread().getName()+"...iflocka ");

                            synchronized(MyLock.lockb)//a锁进b

                            {

                                  System.out.println(Thread.currentThread().getName()+"..iflockb");                          

                            }

                      }

                 }

           }

           else

           {

                 while(true)

                 {

                      synchronized(MyLock.lockb)//b

                      {

                            System.out.println(Thread.currentThread().getName()+"..elselockb");

                            synchronized(MyLock.locka)//b锁进a锁,如果当线程进入了b锁,而这时b锁住了,就无法执行这条语句,因为这个是被a锁锁住的。造成死锁了

                            {

                                  System.out.println(Thread.currentThread().getName()+".....elselocka");

                            }

                      }

                 }

           }

      }

}

class MyLock

{

      static Object locka = new Object();

      static Object lockb = new Object();

}

class  DeadLockTest

{

      public static void main(String[] args)

      {

           Thread t1 = new Thread(newTest(true));

           Thread t2 = new Thread(newTest(false));

           t1.start();

           t2.start();

      }

}

 

总结:

实现多线程的方法:
 实现多线程可以通过继承Thread类和实现Runnable接口。
 (1)继承Thread
     定义一个类继承Thread类 复写Thread类中的public
     void run()方法,将线程的务代码封装到run方法中
     直接创建Thread的子类对象,创建线程调用start()方法,
       开启线程(调用线程的任务run方法)
     //另外可以通过ThreadgetName()获取线程的名称。
 (2)实现Runnable接口;
       定义一个类,实现Runnable接口;
       覆盖接口的public void run()的方法,将线程的任务代码封装
       到run方法中;
       创建Runnable接口的子类对象
       将Runnabl接口的子类对象作为参数传递给Thread类的构造
       函数,创建Thread类对象(原因:线程的任务都封装在
       Runnable接口子类对象的run方法中。所以要在线程对象创
       建时就必须明确要运行的任务)。调用start()方法,启动线程。
 两种方法区别:
      (1)实现Runnable接口避免了单继承的局限性
      (2)继承Thread类线程代码存放在Thread子类的run方法中
        实现Runnable接口线程代码存放在接口的子类的run方法中;
       在定义线程时,建议使用实现Runnable接口,因为几乎所有
     多线程都可以使用这种方式实现

 

本篇博文结束!




                                                                                                   @感谢老师的辛苦批阅

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值