翟开顺

生活不止眼前的苟且,还有诗和远方的田野

Java多线程完整版基础知识

Java多线程完整版基础知识

(翟开顺由厚到薄系列)

1.前言

线程是现代操作系统中一个很重要的概念,多线程功能很强大,java语言对线程提供了很好的支持,我们可以使用java提供的thread类很容易的创建多个线程。线程很不难,我对之前学习过的基础,在这做了一个整理,本文主要参考的是Java研究组织出版的j2se进阶和张孝祥-java就业培训教材这两本书

2.概述

2.1线程是什么

主要是线程与进程的区别,这里不再阐述,自行网上搜索

为什么使用线程:操作系统切换多个线程要比调度进程在速度上快很多,进程间无法共享,通讯麻烦。线程之间由于共享数据,所以交换数据很方便

 

下面有个例子去解释多线程与单线程

 

A单线程例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package firstTread;
   
/**
 * @author zhaikaishun
 *
 */
public class TreadDemo1 {
    public static void main(String[] args) {
        new TestThread().run();  //会一直执行这段代码
        while(true){
            System.out.println("main thread is running");
        }
   
    }
   
}
class TestThread{  //这里没有继承Thread类
    public void run(){
        while (true){
        System.out.println("TestThread is here run");//会一直执行
        }
    }
}


运行后出现

TestThread is here run

TestThread is here run

TestThread is here run

TestThread is here run

TestThread is here run

...


分析:这里是单线程,会按照顺序,只会执行TestThread类的方法


B多线程例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package firstTread;
   
/**
 * @author zhaikaishun
 *
 */
public class TreadDemo1 {
    public static void main(String[] args) {
        new TestThread().start();  //开启一个线程
        while(true){
            System.out.println("main thread is running");
        }
   
    }
   
}
class TestThread extends Thread{  //这里继承了Thread类
    public void run(){
        while (true){
        System.out.println(Thread.currentThread().getName()+" is here run");//会一直执行
        }
    }
}


结果:



分析:这里使用了多线程,TreadDemo1中的run方法 和main中的run方法会抢cpu执行,所以有时候输出有两种情况。注意,使用java多线程需要继承Thread类,还需调用其start()方法。

 

2.2java对线程的支持

Java吸收了一些多线程操作系统的技术特性,经过优化处理,在语言层次上实现了对线程的支持,它提供了Thread,Runnable,Thread,Group等一系列封装和类的接口,让程序员可以高效的开发java多线程程序,java还提供synchronized关键字和Objectwait()notify()机制,用来实现进程的同步。

3.java中使用线程

3.1Thread类和Runable方法

 

a)继承Thread

JavaThread类对线程进行封装,一旦创建了这个Thread实例,jvm就会为我们创建一个线程,当我们调用Thread类的strat方法时,线程就开始运行起来。创建线程的方法如下

代码3.1,继承thread类创建线程的代码



 

我们也可以使用匿名类的办法创建线程,这样代码比较简洁但是可读性较差

代码3.2匿名类继承Thread创建线程


(b)实现Runble接口

Runblejava提供的一个线程相关的接口,接口定义了一个方法

public void run();

某一个类一旦实现了该接口,那么这个类的实例就可以被一个javathread对象调用

代码3.3自定义一个类,实现Runnable接口


 

代码3.匿名类实现Runnable接口



 

3.2两种线程实现方法的比较

不论是那种方式,最后都需要通过Thread类的实例调用start()方法来开始线程的执行,start()方法通过java虚拟机调用线程中定义的run方法来执行该线程。通过查看java源程序中的start()方法的定义可以看到,它是通过调用操作系统的start0方法来实现多线程的操作的。

但是一般在系统的开发中遇到多线程的情况的时候,以实现Runnable接口的方式为主要方式。这是因为实现接口的方式有很多的优点:

1、就是通过继承Thread类的方式时,线程类就无法继承其他的类来实现其他一些功能,实现接口的方式就没有这中限制;

2.也是最重要的一点就是,通过实现Runnable接口的方式可以达到资源共享的效果。(2017年修改,其实使用继承Thread也可以)

这个不举一个例子可能不太清楚,下面我就举一个买票的程序的例子

首先我们先写一个继承Thread类的程序,看看效果

首先是一个线程类,继承了

程序清单: ThreadTest类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package firstTread;
  
/**
 * @author zhaikaishun
 *
 */
public class ThreadTest extends Thread {
     private int tickets = 100;
     public void run(){
         while (true){
             //模拟买票程序,每次调用这个方法,ticket就会减一张
             if(tickets>0)
                 System.out.println(Thread.currentThread().getName()+" is saling ticket "+tickets--);
         }
     }
}

程序清单:ThreadDemo4类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package firstTread;
  
/**
 * @author zhaikaishun
 *
 */
public class ThreadDemo4 {
  
    public static void main(String[] args) {
     ThreadTest t=new ThreadTest();
     t.start();
     t.start();
     t.start();
     t.start();
    }
}

假如我们想用上述代码去模拟买票程序,run方法中每一次循环总票都减1,模拟卖出一张票,我们创建了一个线程,并且启动4次,希望能通过此种方式产生4个线程,结果怎么样呢

结果:从运行结果来看,我们发现只有一个线程在运行,无论我们启动多少遍start()方法,结果只有一个线程




接着我们修改ThreadDemo4类,在main方法中创建四个threadTest对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package firstTread;
  
/**
 * @author zhaikaishun
 *
 */
public class ThreadDemo4 {
  
    public static void main(String[] args) {
     new ThreadTest().start();
     new ThreadTest().start();
     new ThreadTest().start();
     new ThreadTest().start();
    }
}

结果:确实是每个号被打了4遍,创建了4个线程,四个线程都在卖票,但是请注意,他们是各自在卖自己的100张票,并不能实现资源共享,不能去处理同一个资源




接着我们试着用实现Runable的方式,这才是正确的方式

程序清单:ThreadDemo5类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package firstTread;
  
/**
 * @author zhaikaishun
 
 */
public class ThreadDemo5 {
  
    public static void main(String[] args) {
        ThreadTest t = new ThreadTest();  //这个类的实例就可以被一个java的thread对象调用。
        new Thread(t).start();//thread对象调用。
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package firstTread;
  
/**
 * @author zhaikaishun
 *
 */
public class ThreadTest implements Runnable { //现在是实现Runnable接口
     private int tickets = 100;
     public void run(){
         while (true){
             //模拟买票程序,每次调用这个方法,ticket就会减一张
             if(tickets>0)
                 System.out.println(Thread.currentThread().getName()+" is saling ticket "+tickets--);
         }
     }
}


结果:
如我们所想要的,四个线程同时处理一个资源





3.有关这两种方法的性能差异,现在的pc速度如此的快,我们认为在上面的前提下比较性能差异没有多大意义

我的建议:建议使用第二种方式,也就是实现Runable接口的方式

 

3.3线程的状态和属性



 

1. 新建状态(New):新创建了一个线程对象。
2. 
就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3. 
运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4. 
阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。


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

 

注:Thread有个isAlive()方法,用来判断



4.线程同步

4.1线程安全问题:

在上面的卖票得例子中,有可能出现一种我们不想要的情况,那就是有可能同一张票被打印两次多多次,打印的票号码为0甚至是负数等

原因在这一段代码中

 if(tickets>0)
    System.out.println(Thread.currentThread().getName()+" is saling ticket "+tickets--);


假如当tickets=1的时候,

线程1刚刚判断完if(tickets>0),正要处理下面的语句的时候,cpu被线程2给抢走,线程2开始执行,当线程2执行完一个run方法,这里的tickets会减少1,这时候tickets为0,然后跳转到线程1的中断的地方继续执行,因为之前线程1判断过if(tickets>0),所以这里不再需要判断,直接执行System.out.println(Thread.currentThread().getName()+" is saling ticket "+tickets--);,将会打印出为0的票,也就意味着最后一张票卖了2次

4.2同步代码块

  使用synchronized方法:保证代码块的内容是“原子的”(物理中原子的也是可以分割的,所以我一般不说原子),保证里面的内容只能被一个线程在执行,必须等执行的线程离开后才能让其他线程执行,也就和独木桥差不多。
      
1
2
3
synchronized (object) { 
            //这里写代码块
        }

独木桥会让我们的过桥效率降低,同样,同步代码块也会降低代码的执行速度,所以,如果确定代码是安全的,就不要使用同步代码块了。

同步代码块实现同步的原理:任何类型的对象都有一个标志位,该标志位具有0,1两种状态,其开始为1,当执行到synchrozied方法之后,object对象标识位变为0,另外一个线程执行到synchrozed方法之后,将会先判断这个状态,如果发现是0,就暂时阻塞。可以把这个标志位理解成一个箱子的锁,该箱子只能放一个人的东西。

上述卖票程序的ThreadTest类可以如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package firstTread;
  
/**
 * @author zhaikaishun
 
 */
public class ThreadTest implements Runnable { // 现在是实现Runnable接口
    private int tickets = 100;
    String str = new String("");  //这里设置一个对象,任意一个对象都可以
    public void run() {
        while (true) {
            // 模拟买票程序,每次调用这个方法,ticket就会减一张
            synchronized (str) {
                // 这里写代码块
                if (tickets > 0) {
                    try {
                        Thread.sleep(10);
                    catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
  
                    System.out.println(Thread.currentThread().getName()
                            " is saling ticket " + tickets--);
                }
            }
        }
    }
}


结果:



注意: String str = new String("");  这个标志对象,相当于监听对象,必须放在run方法的外面,如果放在run方法里面,四个线程每次调用run方法,就会产生4个监听对象,这四个同步监视器是4个不同的对象,会导致彼此之间不能同步。


4.3.同步函数

    上述是对代码块进行的同步,同样,我们也能对某一个方法进行同步,只需要在同步的函数前加上关键字synchronized即可

例如上述代码可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package firstTread;
/**
 * @author zhaikaishun
 
 */
public class ThreadTest implements Runnable { // 现在是实现Runnable接口
    private int tickets = 100;
    String str = new String("");  //这里设置一个对象,任意一个对象都可以
    public void run() {
        while (true) {
            // 模拟买票程序,每次调用这个方法,ticket就会减一张
            sale();
        }
    }
    public  synchronized void sale(){  //同步方法
        if(tickets>0){
            try{
                Thread.sleep(10);
            }catch(Exception e){
                System.out.println(e.getMessage());
            }
            System.out.println(Thread.currentThread().getName()
                    " is saling ticket " + tickets--);
        }
    }
}


当有一个线程进入了synchronized方法(获得监视器),其他线程就不能进入通一个对象所有使用了synchronized修饰的方法,直到第一个对象执行完他所在的synchronized方法(离开监视器)。

思考:既然synchronized方法需要有一个标志位,那么同步方法的标志位是什么呢,这里我先给出答案,同步方法的标志对象就是所在的对象,即this。

4.4.代码块与函数间的同步

请看下面方法,通过一个str的取值,来判断是代码块还是函数间的同步


代码清单 ThreadDemo6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package firstTread;
  
/**
 * @author zhaikaishun
 
 */
public class ThreadDemo6 {
  
    public static void main(String[] args) {
          
        ThreadTest t = new ThreadTest(); 
        new Thread(t).start();//thread对象调用。
        //让线程暂停一会儿才直观
        try{Thread.sleep(1);}catch(Exception e){};
          
        t.str=new String("method");//如果str是method,调用同步函数
        new Thread(t).start();
          
    }
}



代码清单 ThreadTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package firstTread;
  
/**
 * @author zhaikaishun
 
 */
public class ThreadTest implements Runnable { // 现在是实现Runnable接口
    private int tickets = 100;
    String str = new String(""); // 这里设置一个对象,任意一个对象都可以
  
    public void run() {
        if ("method".equals(str)) {
            while (true) {
                sale();
            }
        else {
            synchronized (str) {
                while (true) {
                    if (tickets > 0) {
                        try {
                            Thread.sleep(10);
                        catch (Exception e) {
                            System.out.println(e.getMessage());
                        }
                        System.out.println(Thread.currentThread().getName()
                                " is saling ticket " + tickets--);
                    }
                }
            }
        }
    }
  
    public synchronized void sale() { // 同步方法
        if (tickets > 0) {
            try {
                Thread.sleep(10);
            catch (Exception e) {
                System.out.println(e.getMessage());
            }
            System.out.print("函数方法在执行:");
            System.out.println(Thread.currentThread().getName()
                    " is saling ticket " + tickets--);
        }
    }
}

运行结果:由于代码块和函数使用的监听器不一样,所以他们没有同步





如果想让他们同步,只需要设置相同的监听对象,将synchronized (str)改为synchronized (this)即可




5.死锁:

死锁比较少见,而且难于调试:

所谓死锁: 是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

其实很久之前学习数字电路,经常会遇到一些锁,这也是自动化的一些常见的问题,在计算机中,也有类似的东西,请看下图






R1 和R2,都只能被一个进程使用

T1在使用R1,同时没有使用完R1的情况下,想使用R2

T2在使用R2,同时在没有使用完R2的情况下,想使用R1

这时,T1等待T2放弃使用R2,同时T2等待T1放弃使用R1,他们都不会放弃自己所使用的,于是产生了等待,将会一直僵持下去。


下面这个例子就是


线程1进去对象obj1的监视器,而线程2进入了obj2的监视器,这时候进入了obj1的监视器的线程还试图进入使用obj2作为监视器的方法中,这显然会被阻塞隔离,是进不去的;同时,进入了obj2的监视器的线程也试图进入使用obj1作为监视器的方法中,这也显然会被阻塞隔离,是进不去的。 然后双方一致僵持着,程序停滞不前,这就是我们所谓的死锁,代码清单如下

代码清单:死锁例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package deadlock;
 
public class RunnableTest implements Runnable {  
    private int flag = 1;  
    private static Object obj1 = new Object(), obj2 = new Object();  
   
    public void run() {  
        System.out.println("flag=" + flag);  
        if (flag == 1) {  
            synchronized (obj1) {  
                System.out.println("我已经锁定obj1,休息0.5秒后锁定obj2去,但是估计进不去obj2,因为obj2也正在一个同步方法中");  
                try {  
                    Thread.sleep(500);  
                catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                synchronized (obj2) {  
                    System.out.println("进入了obj2");  
                }  
            }  
        }  
        if (flag == 0) {  
            synchronized (obj2) {  
                System.out.println("我已经锁定obj2,休息0.5秒后锁定obj1去,但是估计进不了obj1,因为这obj1也在一个同步方法中");  
                try {  
                    Thread.sleep(500);  
                catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                synchronized (obj1) {  
                    System.out.println("进入了obj1");  
                }  
            }  
        }  
    }  
   
    public static void main(String[] args) {  
        RunnableTest run01 = new RunnableTest();  
        RunnableTest run02 = new RunnableTest();  
        run01.flag = 1;  
        run02.flag = 0;  
        Thread thread01 = new Thread(run01);  
        Thread thread02 = new Thread(run02);  
        System.out.println("线程开始喽!");  
        thread01.start();  
        thread02.start();  
    }  
}  


结果:一直处于僵持状态





6.线程间的通信

我们先从下面一个例子引出线程中的通信

下面例子讲的是一个生产和消费的关系,生产一样东西,取走这样东西。这个程序是每生产出一个PDD(人名),并且给这个人赋值为男

然后再取出来,然后生产一个“娇妹”(人名),并且赋值为女,然后再取出来。代码清单如下。


一个类Q,用来存储数据  Q:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package communication;
 
public class Q {
  private String name="PDD";
  private String sex="男";
  public synchronized void put(String name,String sex) {
      this.name=name;
      try{Thread.sleep(1);}catch(Exception e){System.out.println(e.getMessage());}
      this.sex=sex; 
  }  
  public synchronized void get(){
       
      System.out.println(name+"----"+sex);
  }
}

生产者类Producer:生产数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package communication;
 
public class Producer implements Runnable {
  Q q=null;
  public Producer(Q q){
      this.q=q;
  }
  int i=0;
  public void run(){
      while(true){
          if(i==0)
              q.put("PDD""男");
          else
              q.put("娇妹","女");
          i=(i+1)%2;
      }
  }
}

消费者类 Customer: 获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package communication;
 
public class Customer implements Runnable {
    Q q=null;
    public Customer(Q q){
        this.q=q;
    }
    public void run(){
        while(true){
            q.get();
        }
    }
     
}


主方法:


1
2
3
4
5
6
7
8
9
10
11
12
package communication;
 
public class ThreadCommunication {
 
    public static void main(String[] args) {
        Q q = new Q();
        new Thread(new Producer(q)).start();
        try{Thread.sleep(1);}catch(Exception e){System.out.println(e.getMessage());}
        new Thread(new Customer(q)).start();
 
    }
}


运行结果:

.....

PDD----男
PDD----男
PDD----男
PDD----男
娇妹----女
娇妹----女
娇妹----女
娇妹----女
娇妹----女
娇妹----女
娇妹----女
.......

分析:这并不是我们想要的,我们想要的是下面这种类型的,producer每存放一次数据,customer取一次数据,反之,producer必须等customer取完数据之后才能开始存数据。这就是要将的线程间的通信问题,Java通过Object的wait,notify,notifyAll这几个方法实现线程间的通信。

PDD----男

娇妹----女

PDD----男

娇妹----女

PDD----男

娇妹----女


    wait:告诉当前线程放弃监视器并且进入线程休眠状态,直到其他线程进入相同的监视器并且调用notify为止。

     notify:唤醒同一对象监视器中调用wait的第一个线程。

     notifyAll:唤醒同一对象监视器中调用wait的所有线程,具有优先级高的线程将会被先唤醒。

如果想让上面的程序满足我们的要求,我们可以在 类Q中定义一个新的成员变量bFull来标示数据存储空间的状态,当Customer取走数据后,bFull为false;当Producer存入数据后,bFull为true。只有bFull为true时,Customer才能取走数据,只有当bFull为False时Producer才能放入数据 Q的清单如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package communication;
 
public class Q {
  private String name="PDD";
  private String sex="男";
  boolean bFull=false;
  public synchronized void put(String name,String sex) {
      if(bFull)
        try {
            wait();
        catch (InterruptedException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
      this.name=name;
      try{Thread.sleep(1);}catch(Exception e){System.out.println(e.getMessage());}
      this.sex=sex; 
      bFull=true;
      notify();
  }  
  public synchronized void get(){
      if(!bFull)
            try {
                wait();
            catch (InterruptedException e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
            }
      System.out.println(name+"----"+sex);
      bFull=false;
      notify();
  }
}

结果:运行流程,自己看代码思考

娇妹----女
PDD----男
娇妹----女
PDD----男
娇妹----女
PDD----男
娇妹----女



参考文献:

【1】J2SE进阶(java研究组织 精品图书)

【2】张孝祥-Java就业教程

【3】java编程思想

【4】java核心技术卷1






阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载 https://blog.csdn.net/T1DMzks/article/details/51893818
个人分类: java
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭