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

一、多线程概述:

     要了解多线程,就必须知道什么是线程。而要知道什么是线程就必须知道什么是进程。

1、进程:

    进程是一个正在执行中的程序(几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序)。

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

2、线程(Thread):

    线程是进程中的一个独立的控制单元,线程在控制着进程的执行。一个进程至少有一个线程。

3、多线程:

    在java虚拟机启动的时候会有一个java.exe的执行程序,也就是一个进程。该进程中至少有一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。JVM启动除了执行一个主线程,还有负责垃圾回收机制的线程。像种在一个进程中有多个线程执行的方式,就叫做多线程。

提示:归纳起来可以这样说:操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。

4、多线程的优势:

     在实际应用中,多线程是非常有用的,一个浏览器必须能同时下载多个图片;一个Web服务器必须能同时响应多个用户请求;Java虚拟机本身就在后台提供了一个超级线程来进行垃圾回收; 总之,多线程的出现能让程序产生同时运行效果。可以提高程序执行效率。

二、创建线程的方式:

    Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例(继承Thread类创建线程类)或是实现Runnable接口创建线程类。

1、继承Thread类创建线程类:

    通过继承Thread类来创建并启动多线程的步骤如下:

(1)、定义Thread的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。

(2)、创建Thread子类的实例,即创建了线程对象。

(3)、调用线程对象的start()方法来启动线程。注:如果对象直接调用run方法,等同于只有一个线程在执行,自定义的线程并没有启动。

下面程序示范了通过继承Thread类来创建并启动多线程。

// 通过继承Thread类来创建线程类

复制代码
 1 // 通过继承Thread类来创建线程类
 2 public class FirstThread extends Thread
 3 {
 4     private int i ;
 5     // 重写run方法,run方法的方法体就是线程执行体
 6     public void run()
 7     {
 8         for ( ; i < 100 ; i++ )
 9         {
10             // 当线程类继承Thread类时,直接使用this即可获取当前线程
11             // Thread对象的getName()返回当前该线程的名字
12             // 因此可以直接调用getName()方法返回当前线程的名
13             System.out.println(getName() +  " " + i);
14         }
15     }
16     public static void main(String[] args) 
17     {
18         for (int i = 0; i < 100;  i++)
19         {
20             // 调用Thread的currentThread方法获取当前线程
21             System.out.println(Thread.currentThread().getName()
22                 +  " " + i);
23             if (i == 20)
24             {
25                 // 创建、并启动第一条线程
26                 new FirstThread().start();
27                 // 创建、并启动第二条线程
28                 new FirstThread().start();
29             }
30         }
31     }
32 }
复制代码

2、实现Runnable接口创建线程类:

实现Runnable接口来创建并启动多线程的步骤如下:

(1)、定义类实现Runnable的接口。

(2)、覆盖Runnable接口中的run方法。目的也是为了将线程要运行的代码存放在该run方法中。

(3)、通过Thread类创建线程对象。

(4)、将Runnable接口的子类对象作为实参传递给Thread类的构造方法。

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

       因为,自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程去指定对象的run方法,就必须明确该run方法所属对象。

(5)、调用Thread类中start方法启动线程。start方法会自动调用Runnable接口子类的run方法。

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

程序示例:

 

复制代码
 1 package I10;
 2 /* 
 3 需求:简单的卖票程序。 
 4 多个窗口卖票。 
 5 */  
 6 
 7 public class TicketTest1 implements Runnable
 8 {
 9     private int count=100;
10     public void run()
11     {        
12        while(true)
13        {           
14           if(count>0)            
15               {
16                    //显示线程名及余票数  
17                 System.out.println(Thread.currentThread().getName()+"----"+count--);
18               }
19         }
20     }
21 }
22 class Ticked1
23 {
24     public static void main(String[] args) 
25     {
26 
27          //创建Runnable接口子类的实例对象  
28         TicketTest1 t1=new TicketTest1();
29         //有多个窗口在同时卖票,这里用三个线程表示 
30         Thread thread1=new Thread(t1);
31         Thread thread2=new Thread(t1);
32         Thread thread3=new Thread(t1);
33         thread1.start();//启动线程
34         thread2.start();
35         thread3.start();
36      }
37 }
复制代码

 

三、两种方式的区别和线程的几种状态

1、两种创建方式的区别

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

        实现Runnable:线程代码存放在接口子类run方法中,实现方式好处:避免了单继承的局限性。在定义线程时,建议使用实现方式。。     

2、几种状态

       被创建:等待启动,调用start启动---该线程处于就绪状态即等待执行。

        运行状态:具有执行资格和执行权。

        临时状态(阻塞):有执行资格,但是没有执行权。

        冻结状态:遇到sleep(time)方法和wait()方法时,失去执行资格和执行权,sleep方法时间到或者调用notify()方法时,获得执行资格,变为临时状态。

        消忙状态:stop()方法,或者run方法结束。

注:当已经从创建状态到了运行状态,再次调用start()方法时,就失去意义了,java运行时会提示线程状态异常。

 

四、线程安全问题

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

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

简单的说就两点:

        (1)、多个线程访问出现延迟。

        (2)、线程随机性

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

2、解决办法——同步

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

        在java中对于多线程的安全问题提供了专业的解决方式——synchronized(同步)

        这里也有两种解决方式,一种是同步代码块,还有就是同步函数。都是利用关键字synchronized来实现。

         (1)、同步代码块

        用法:

                 synchronized(对象)

                  {需要被同步的代码}

        同步可以解决安全问题的根本原因就在那个对象上。其中对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。

实例:

 

复制代码
 1 package I10;
 2 /* 
 3 需求:简单的卖票程序。 
 4 多个窗口卖票。 
 5 */  
 6 
 7 public class TicketTest1 implements Runnable
 8 {
 9     private int count=100;
10     Object obj=new Object();
11     public void run()
12     {        
13        while(true)
14        {        
15          //给程序加同步,即锁  
16            synchronized (obj)
17            {
18              if(count>0)
19                {
20                  try {                   
21                      //使用线程中的sleep方法,模拟线程出现的安全问题  因为sleep方法有异常声明,所以这里要对其进行处理  
22                        Thread.sleep(1000);
23                       } 
24                  catch (InterruptedException e) 
25                     {                 
26                        e.printStackTrace();
27                     }
28                }
29            }
30        }
31     }
32 }
33 class Ticked1
34 {
35     public static void main(String[] args) 
36     {
37 
38          //创建Runnable接口子类的实例对象  
39         TicketTest1 t1=new TicketTest1();
40         //有多个窗口在同时卖票,这里用三个线程表示 
41         Thread thread1=new Thread(t1);
42         Thread thread2=new Thread(t1);
43         Thread thread3=new Thread(t1);
44         thread1.start();//启动线程
45         thread2.start();
46         thread3.start();
47      }
48 }
复制代码

 

  (2),同步函数

       格式:

                在函数上加上synchronized修饰符即可。

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

        函数需要被对象调用。那么函数都有一个所属对象引用。就是this。所以同步函数使用的锁是this。

拿同步代码块的示例:

 

复制代码
 1 package I10;
 2 
 3 
 4 
 5 public class TicketTest implements Runnable {
 6     private int count=100;
 7     public void run()
 8     {
 9         while(true)
10         {
11             show();
12         }
13     }
14             public synchronized void show()
15             {
16                 if(count>0)
17                   {
18                         try {
19                             Thread.sleep(100);
20                         } catch (InterruptedException e) {
21                             // TODO Auto-generated catch block
22                             e.printStackTrace();
23                         }
24                         System.out.println(Thread.currentThread().getName()+"----"+count--);
25                   }
26             }
27         }
28 class Ticked{
29 public static void main(String[] args) {
30     TicketTest t1=new TicketTest();
31     Thread thread1=new Thread(t1);
32     Thread thread2=new Thread(t1);
33     Thread thread3=new Thread(t1);
34     thread1.start();
35     thread2.start();
36     thread3.start();
37 }
38 }
复制代码

 

3、同步的前提

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

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

4、同步的利弊

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

        弊端:多个线程需要判断锁,较为消耗资源。

5、如何寻找多线程中的安全问题

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

        b,明确共享数据。

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

 

五、静态函数的同步方式

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

     通过验证,发现不在是this。因为静态方法中也不可以定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。如:

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

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

单例模式:懒汉式

 

复制代码
 1 package I10;
 2 /**
 3  * 加同步的单例设计模式----懒汉式
 4  * 
 5  */
 6 public class Single 
 7 {
 8     private static Single s=null;
 9     private Single(){}
10     public static Single getInstance()
11     {
12     if(s==null)
13       {
14         synchronized(Single.class)
15           {
16              if(s==null)
17              s=new Single();
18            }
19        }
20     return s;
21     }
22 }
复制代码

六、线程间通信

        其实就是多个线程在操作同一个资源,但是操作的动作不同。

1、使用同步操作同一资源的示例:

 

复制代码
package I10;
/* 
    有一个资源 
一个线程往里存东西,如果里边没有的话 
一个线程往里取东西,如果里面有得话 
*/  
//资源
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 block
                e.printStackTrace();
            }
            this.name=name;
            this.sex=sex;
            flag=true;//表示有资源
            this.notify();//唤醒等待
        }
        public synchronized void out(){
        //如果没有资源,等待存入资源  
            if(!flag)
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                //这里用打印表示取出  
            System.out.println(name+"--"+sex);
            flag=false;//资源已取出
            this.notify();//等待唤醒
        }
    }
//存资源
class Input implements Runnable{
    private Res r;
     Input(Res r) {
        this.r=r;
    }
     //重写run()方法
    @Override
    public void run() {
        // TODO Auto-generated method stub
        int x=0;
        while(true)
        {                
                if(x==0){
                    r.set("小四", "男");
                }
                else {
                    r.set("女侠", "女");
                }
                x=(x+1)%2;//控制交替打印
            }    
        }
    }
//取资源
public class Output implements Runnable{
    private Res r;
    Output(Res r){
        this.r=r;
    }
    @Override
    public void run() {
        while(true){
            r.out();
                }
            }
        }
class OutInt2{
    public static void main(String[] args) {
        Res r=new Res();//表示操作的是同一个资源  
        new Thread(new Input(r)).start();//开启存线程  
        new Thread(new Output(r)).start();//开启取线程  
    }
}
复制代码

几个小问题:

        1)wait(),notify(),notifyAll(),用来操作线程为什么定义在了Object类中?

                a,这些方法存在与同步中。

                b,使用这些方法时必须要标识所属的同步的锁。同一个锁上wait的线程,只可以被同一个锁上的notify唤醒。

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

        2)wait(),sleep()有什么区别?

              wait():释放cpu执行权,释放锁。

              sleep():释放cpu执行权,不释放锁。

        3)为甚么要定义notifyAll?

        因为在需要唤醒对方线程时。如果只用notify,容易出现只唤醒本方线程的情况。导致程序中的所以线程都等待。

2、JDK1.5中提供了多线程升级解决方案。

        将同步synchronized替换成显示的Lock操作。将Object中wait,notify,notifyAll,替换成了Condition对象。该Condition对象可以通过Lock锁进行获取,并支持多个相关的Condition对象。

升级解决方案的示例:

 

复制代码
 1 package I10;
 2 
 3 import java.util.concurrent.locks.Condition;
 4 import java.util.concurrent.locks.Lock;
 5 import java.util.concurrent.locks.ReentrantLock;
 6 
 7 class Resource2{
 8     private String name;
 9     private int count=1;
10     boolean flag=false;//标记无资源
11     private Lock lock=new ReentrantLock();
12     private Condition con=lock.newCondition();
13     private Condition pro=lock.newCondition();
14     public void set(String name){
15         lock.lock();//16             try {
17                 while(flag)//重复判断标识,确认是否生产
18                 con.await();//本方等待  
19                 this.name=name+"....."+count++;
20                 System.out.println(Thread.currentThread().getName()+"生产者。。。。"+this.name);
21                 flag=true;
22                 pro.signal();
23                 
24             } catch (InterruptedException e) {
25                 e.printStackTrace();
26             } 
27               finally{
28                   lock.unlock();//释放锁的机制一定要执行
29             }
30         }
31         public void out(){
32             lock.lock();
33             try {
34                 while(!flag)
35                     pro.await();
36                     System.out.println(Thread.currentThread().getName()+"消费者。。。。"+this.name);
37                 flag=false;
38                 con.signal();
39             } catch (Exception e) {
40                 // TODO: handle exception
41             }finally{
42                  lock.unlock();
43             }
44         }
45     }
46 //生产者
47 public class Producker2 implements Runnable{
48     private Resource2 res;
49     Producker2(Resource2 res) {
50         this.res=res;
51     }
52     @Override
53     public void run() {
54         // TODO Auto-generated method stub
55         while(true)
56         {
57             res.set("商品");
58             }    
59         }
60     }
61 //消费者
62  class customer2 implements Runnable{
63     private Resource2 res;
64     customer2(Resource2 res){
65         this.res=res;
66     }
67     @Override
68     public void run() {
69         while(true){
70             res.out();
71                 }
72             }
73         }
74 class OutInt3{
75 
76     public static void main(String[] args) {
77         Resource2 r=new Resource2();
78 //        new Thread(new customer(r)).start();
79 //        new Thread(new Producker(r)).start();
80         customer2 n1=new customer2(r);
81         Producker2 o1=new Producker2(r);
82         Thread t1=new Thread(n1);
83         Thread t2=new Thread(o1);
84         Thread t3=new Thread(n1);
85         Thread t4=new Thread(o1);
86         t1.start();
87         t2.start();
88         t3.start();
89         t4.start();
90     }
91 }
复制代码

 

七、停止线程

        JDK 1.5版本之前,有stop停止线程的方法,但升级之后,此方法已经过时。

那么现在我们该如果停止线程呢?

        只有一种办法,那就是让run方法结束。

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

      如:run方法中有如下代码,设置一个flag标记。 

复制代码
1 public  void run()  
2 {  
3        while(flag)  
4        {     
5            System.out.println(Thread.currentThread().getName()+"....run");  
6          }  
7 }  
复制代码

那么只要在主函数或者其他线程中,在该线程执行一段时间后,将标记flag赋值false,该run方法就会结束,线程也就停止了。

2、上面的1方法可以解决一般情况,但是有一种特殊情况:就是当线程处于冻结状态。就不会读取到标记。那么线程就不会结束。

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

如:

 

复制代码
 1 package I10;
 2 
 3 class StopThreadDemo implements Runnable  
 4 {  
 5     private boolean flag =true;  
 6     public  void run()  
 7     {  
 8         while(flag)  
 9         {  
10             System.out.println(Thread.currentThread().getName()+"....run");  
11         }  
12     }  
13     public void changeFlag()  
14     {  
15         flag = false;  
16     }  
17 }  
18   
19 class StopThread 
20 {  
21     public static void main(String[] args)   
22     {  
23         StopThreadDemo st = new StopThreadDemo();  
24         Thread t1 = new Thread(st);  
25         Thread t2 = new Thread(st);   
26         t1.start();  
27         t2.start();   
28   
29         int num = 0;  
30         while(true)  
31         {  
32             if(num++ == 60)  
33             {  
34                 t1.interrupt();//清除冻结状态  
35                 t2.interrupt();  
36                 st.changeFlag();//改变循环标记  
37                 break;  
38             }  
39             System.out.println(Thread.currentThread().getName()+"......."+num);  
40         }  
41         System.out.println("over");  
42     }  
43 }  
复制代码

 

1、join方法

       当A线程执行到了b线程的.join()方法时,A线程就会等待,等B线程都执行完,A线程才会执行。(此时B和其他线程交替运行。)join可以用来临时加入线程执行。

2、setPriority()方法用来设置优先级

        MAX_PRIORITY 最高优先级10

        MIN_PRIORITY   最低优先级1

        NORM_PRIORITY 分配给线程的默认优先级

3、yield()方法可以暂停当前线程,让其他线程执行。

原文地址:https://www.cnblogs.com/Hanxia/p/4476813.html